-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Expand file tree
/
Copy pathserver_tool.go
More file actions
193 lines (171 loc) · 8.27 KB
/
server_tool.go
File metadata and controls
193 lines (171 loc) · 8.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package inventory
import (
"context"
"encoding/json"
"github.com/github/github-mcp-server/pkg/octicons"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// HandlerFunc is a function that takes dependencies and returns an MCP tool handler.
// This allows tools to be defined statically while their handlers are generated
// on-demand with the appropriate dependencies.
// The deps parameter is typed as `any` to avoid circular dependencies - callers
// should define their own typed dependencies struct and type-assert as needed.
type HandlerFunc func(deps any) mcp.ToolHandler
// ToolsetID is a unique identifier for a toolset.
// Using a distinct type provides compile-time type safety.
type ToolsetID string
// ToolsetMetadata contains metadata about the toolset a tool belongs to.
type ToolsetMetadata struct {
// ID is the unique identifier for the toolset (e.g., "repos", "issues")
ID ToolsetID
// Description provides a human-readable description of the toolset
Description string
// Default indicates this toolset should be enabled by default
Default bool
// Icon is the name of the Octicon to use for tools in this toolset.
// Use the base name without size suffix, e.g., "repo" not "repo-16".
// See https://primer.style/foundations/icons for available icons.
Icon string
// InstructionsFunc optionally returns instructions for this toolset.
// It receives the inventory so it can check what other toolsets are enabled.
InstructionsFunc func(inv *Inventory) string
}
// Icons returns MCP Icon objects for this toolset, or nil if no icon is set.
// Icons are provided in both 16x16 and 24x24 sizes.
func (tm ToolsetMetadata) Icons() []mcp.Icon {
return octicons.Icons(tm.Icon)
}
// ServerTool represents an MCP tool with metadata and a handler generator function.
// The tool definition is static, while the handler is generated on-demand
// when the tool is registered with a server.
// Tools are now self-describing with their toolset membership and read-only status
// derived from the Tool.Annotations.ReadOnlyHint field.
type ServerTool struct {
// Tool is the MCP tool definition containing name, description, schema, etc.
Tool mcp.Tool
// Toolset contains metadata about which toolset this tool belongs to.
Toolset ToolsetMetadata
// HandlerFunc generates the handler when given dependencies.
// This allows tools to be passed around without handlers being set up,
// and handlers are only created when needed.
HandlerFunc HandlerFunc
// FeatureFlagEnable specifies a feature flag that must be enabled for this tool
// to be available. If set and the flag is not enabled, the tool is omitted.
FeatureFlagEnable string
// FeatureFlagDisable specifies a feature flag that, when enabled, causes this tool
// to be omitted. Used to disable tools when a feature flag is on.
FeatureFlagDisable string
// Enabled is an optional function called at build/filter time to determine
// if this tool should be available. If nil, the tool is considered enabled
// (subject to FeatureFlagEnable/FeatureFlagDisable checks).
// The context carries request-scoped information for the consumer to use.
// Returns (enabled, error). On error, the tool should be treated as disabled.
Enabled func(ctx context.Context) (bool, error)
// RequiredScopes specifies the minimum OAuth scopes required for this tool.
// These are the scopes that must be present for the tool to function.
RequiredScopes []string
// AcceptedScopes specifies all OAuth scopes that can be used with this tool.
// This includes the required scopes plus any higher-level scopes that provide
// the necessary permissions due to scope hierarchy.
AcceptedScopes []string
}
// IsReadOnly returns true if this tool is marked as read-only via annotations.
func (st *ServerTool) IsReadOnly() bool {
return st.Tool.Annotations != nil && st.Tool.Annotations.ReadOnlyHint
}
// HasHandler returns true if this tool has a handler function.
func (st *ServerTool) HasHandler() bool {
return st.HandlerFunc != nil
}
// Handler returns a tool handler by calling HandlerFunc with the given dependencies.
// Panics if HandlerFunc is nil - all tools should have handlers.
func (st *ServerTool) Handler(deps any) mcp.ToolHandler {
if st.HandlerFunc == nil {
panic("HandlerFunc is nil for tool: " + st.Tool.Name)
}
return st.HandlerFunc(deps)
}
// RegisterFunc registers the tool with the server using the provided dependencies.
// Icons are automatically applied from the toolset metadata if not already set.
// A shallow copy of the tool is made to avoid mutating the original ServerTool.
// Panics if the tool has no handler - all tools should have handlers.
func (st *ServerTool) RegisterFunc(s *mcp.Server, deps any) {
handler := st.Handler(deps) // This will panic if HandlerFunc is nil
// Make a shallow copy of the tool to avoid mutating the original
toolCopy := st.Tool
// Apply icons from toolset metadata if tool doesn't have icons set
if len(toolCopy.Icons) == 0 {
toolCopy.Icons = st.Toolset.Icons()
}
s.AddTool(&toolCopy, handler)
}
// NewServerTool creates a ServerTool from a tool definition, toolset metadata, and a typed handler function.
// The handler function takes dependencies (as any) and returns a typed handler.
// Callers should type-assert deps to their typed dependencies struct.
//
// Deprecated: This creates closures at registration time. For better performance in
// per-request server scenarios, use NewServerToolWithContextHandler instead.
func NewServerTool[In any, Out any](tool mcp.Tool, toolset ToolsetMetadata, handlerFn func(deps any) mcp.ToolHandlerFor[In, Out]) ServerTool {
return ServerTool{
Tool: tool,
Toolset: toolset,
HandlerFunc: func(deps any) mcp.ToolHandler {
typedHandler := handlerFn(deps)
return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
var arguments In
if err := json.Unmarshal(req.Params.Arguments, &arguments); err != nil {
return nil, err
}
resp, _, err := typedHandler(ctx, req, arguments)
return resp, err
}
},
}
}
// NewServerToolWithContextHandler creates a ServerTool with a handler that receives deps via context.
// This is the preferred approach for tools because it doesn't create closures at registration time,
// which is critical for performance in servers that create a new instance per request.
//
// The handler function is stored directly without wrapping in a deps closure.
// Dependencies should be injected into context before calling tool handlers.
func NewServerToolWithContextHandler[In any, Out any](tool mcp.Tool, toolset ToolsetMetadata, handler mcp.ToolHandlerFor[In, Out]) ServerTool {
return ServerTool{
Tool: tool,
Toolset: toolset,
// HandlerFunc ignores deps - deps are retrieved from context at call time
HandlerFunc: func(_ any) mcp.ToolHandler {
return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
var arguments In
if err := json.Unmarshal(req.Params.Arguments, &arguments); err != nil {
return nil, err
}
resp, _, err := handler(ctx, req, arguments)
return resp, err
}
},
}
}
// NewServerToolFromHandler creates a ServerTool from a tool definition, toolset metadata, and a raw handler function.
// Use this when you have a handler that already conforms to mcp.ToolHandler.
//
// Deprecated: This creates closures at registration time. For better performance in
// per-request server scenarios, use NewServerToolWithRawContextHandler instead.
func NewServerToolFromHandler(tool mcp.Tool, toolset ToolsetMetadata, handlerFn func(deps any) mcp.ToolHandler) ServerTool {
return ServerTool{Tool: tool, Toolset: toolset, HandlerFunc: handlerFn}
}
// NewServerToolWithRawContextHandler creates a ServerTool with a raw handler that receives deps via context.
// This is the preferred approach for tools that use mcp.ToolHandler directly because it doesn't
// create closures at registration time.
//
// The handler function is stored directly without wrapping in a deps closure.
// Dependencies should be injected into context before calling tool handlers.
func NewServerToolWithRawContextHandler(tool mcp.Tool, toolset ToolsetMetadata, handler mcp.ToolHandler) ServerTool {
return ServerTool{
Tool: tool,
Toolset: toolset,
// HandlerFunc ignores deps - deps are retrieved from context at call time
HandlerFunc: func(_ any) mcp.ToolHandler {
return handler
},
}
}