Skip to content

Commit 09ecc66

Browse files
refactor: separate ServerTool into own file with HandlerFunc pattern
- Extract ServerTool struct into pkg/toolsets/server_tool.go - Add ToolDependencies struct for passing common dependencies to handlers - HandlerFunc allows lazy handler generation from Tool definitions - NewServerTool for new dependency-based tools - NewServerToolLegacy for backward compatibility with existing handlers - Update toolsets.go to store and pass dependencies - Update all call sites to use NewServerToolLegacy Co-authored-by: Adam Holt <4619+omgitsads@users.noreply.github.com>
1 parent adaa6a1 commit 09ecc66

File tree

5 files changed

+158
-40
lines changed

5 files changed

+158
-40
lines changed

internal/ghmcp/server.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/github/github-mcp-server/pkg/lockdown"
1919
mcplog "github.com/github/github-mcp-server/pkg/log"
2020
"github.com/github/github-mcp-server/pkg/raw"
21+
"github.com/github/github-mcp-server/pkg/toolsets"
2122
"github.com/github/github-mcp-server/pkg/translations"
2223
gogithub "github.com/google/go-github/v79/github"
2324
"github.com/modelcontextprotocol/go-sdk/mcp"
@@ -181,7 +182,7 @@ func NewMCPServer(cfg MCPServerConfig) (*mcp.Server, error) {
181182
enabledTools, _ = tsg.ResolveToolAliases(enabledTools)
182183

183184
// Register the specified tools (additive to any toolsets already enabled)
184-
err = tsg.RegisterSpecificTools(ghServer, enabledTools, cfg.ReadOnly)
185+
err = tsg.RegisterSpecificTools(ghServer, enabledTools, cfg.ReadOnly, toolsets.ToolDependencies{})
185186
if err != nil {
186187
return nil, fmt.Errorf("failed to register tools: %w", err)
187188
}

pkg/github/dynamic_tools.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,7 @@ func EnableToolset(s *mcp.Server, toolsetGroup *toolsets.ToolsetGroup, t transla
6161
//
6262
// Send notification to all initialized sessions
6363
// s.sendNotificationToAllClients("notifications/tools/list_changed", nil)
64-
for _, serverTool := range toolset.GetActiveTools() {
65-
serverTool.RegisterFunc(s)
66-
}
64+
toolset.RegisterTools(s)
6765

6866
return utils.NewToolResultText(fmt.Sprintf("Toolset %s enabled", toolsetName)), nil, nil
6967
})

pkg/toolsets/server_tool.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package toolsets
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
7+
"github.com/modelcontextprotocol/go-sdk/mcp"
8+
)
9+
10+
// HandlerFunc is a function that takes dependencies and returns an MCP tool handler.
11+
// This allows tools to be defined statically while their handlers are generated
12+
// on-demand with the appropriate dependencies.
13+
type HandlerFunc func(deps ToolDependencies) mcp.ToolHandler
14+
15+
// ToolDependencies contains all dependencies that tool handlers might need.
16+
// Fields are pointers/interfaces so they can be nil when not needed by a specific tool.
17+
type ToolDependencies struct {
18+
// GetClient returns a GitHub REST API client
19+
GetClient any // func(context.Context) (*github.Client, error)
20+
21+
// GetGQLClient returns a GitHub GraphQL client
22+
GetGQLClient any // func(context.Context) (*githubv4.Client, error)
23+
24+
// GetRawClient returns a raw HTTP client for GitHub
25+
GetRawClient any // raw.GetRawClientFn
26+
27+
// RepoAccessCache is the lockdown mode repo access cache
28+
RepoAccessCache any // *lockdown.RepoAccessCache
29+
30+
// T is the translation helper function
31+
T any // translations.TranslationHelperFunc
32+
33+
// Flags are feature flags
34+
Flags any // FeatureFlags
35+
36+
// ContentWindowSize is the size of the content window for log truncation
37+
ContentWindowSize int
38+
}
39+
40+
// ServerTool represents an MCP tool with a handler generator function.
41+
// The tool definition is static, while the handler is generated on-demand
42+
// when the tool is registered with a server.
43+
type ServerTool struct {
44+
// Tool is the MCP tool definition containing name, description, schema, etc.
45+
Tool mcp.Tool
46+
47+
// HandlerFunc generates the handler when given dependencies.
48+
// This allows tools to be passed around without handlers being set up,
49+
// and handlers are only created when needed.
50+
HandlerFunc HandlerFunc
51+
}
52+
53+
// Handler returns a tool handler by calling HandlerFunc with the given dependencies.
54+
func (st *ServerTool) Handler(deps ToolDependencies) mcp.ToolHandler {
55+
if st.HandlerFunc == nil {
56+
return nil
57+
}
58+
return st.HandlerFunc(deps)
59+
}
60+
61+
// RegisterFunc registers the tool with the server using the provided dependencies.
62+
func (st *ServerTool) RegisterFunc(s *mcp.Server, deps ToolDependencies) {
63+
handler := st.Handler(deps)
64+
s.AddTool(&st.Tool, handler)
65+
}
66+
67+
// NewServerTool creates a ServerTool from a tool definition and a typed handler function.
68+
// The handler function takes dependencies and returns a typed handler.
69+
func NewServerTool[In any, Out any](tool mcp.Tool, handlerFn func(deps ToolDependencies) mcp.ToolHandlerFor[In, Out]) ServerTool {
70+
return ServerTool{
71+
Tool: tool,
72+
HandlerFunc: func(deps ToolDependencies) mcp.ToolHandler {
73+
typedHandler := handlerFn(deps)
74+
return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
75+
var arguments In
76+
if err := json.Unmarshal(req.Params.Arguments, &arguments); err != nil {
77+
return nil, err
78+
}
79+
resp, _, err := typedHandler(ctx, req, arguments)
80+
return resp, err
81+
}
82+
},
83+
}
84+
}
85+
86+
// NewServerToolFromHandler creates a ServerTool from a tool definition and a raw handler function.
87+
// Use this when you have a handler that already conforms to mcp.ToolHandler.
88+
func NewServerToolFromHandler(tool mcp.Tool, handlerFn func(deps ToolDependencies) mcp.ToolHandler) ServerTool {
89+
return ServerTool{Tool: tool, HandlerFunc: handlerFn}
90+
}
91+
92+
// NewServerToolLegacy creates a ServerTool from a tool definition and an already-bound typed handler.
93+
// This is for backward compatibility during the refactor - the handler doesn't use ToolDependencies.
94+
// Deprecated: Use NewServerTool instead for new code.
95+
func NewServerToolLegacy[In any, Out any](tool mcp.Tool, handler mcp.ToolHandlerFor[In, Out]) ServerTool {
96+
return ServerTool{
97+
Tool: tool,
98+
HandlerFunc: func(_ ToolDependencies) mcp.ToolHandler {
99+
return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
100+
var arguments In
101+
if err := json.Unmarshal(req.Params.Arguments, &arguments); err != nil {
102+
return nil, err
103+
}
104+
resp, _, err := handler(ctx, req, arguments)
105+
return resp, err
106+
}
107+
},
108+
}
109+
}
110+
111+
// NewServerToolFromHandlerLegacy creates a ServerTool from a tool definition and an already-bound raw handler.
112+
// This is for backward compatibility during the refactor - the handler doesn't use ToolDependencies.
113+
// Deprecated: Use NewServerToolFromHandler instead for new code.
114+
func NewServerToolFromHandlerLegacy(tool mcp.Tool, handler mcp.ToolHandler) ServerTool {
115+
return ServerTool{
116+
Tool: tool,
117+
HandlerFunc: func(_ ToolDependencies) mcp.ToolHandler {
118+
return handler
119+
},
120+
}
121+
}

pkg/toolsets/toolsets.go

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package toolsets
22

33
import (
4-
"context"
5-
"encoding/json"
64
"fmt"
75
"os"
86
"strings"
@@ -32,27 +30,7 @@ func NewToolsetDoesNotExistError(name string) *ToolsetDoesNotExistError {
3230
return &ToolsetDoesNotExistError{Name: name}
3331
}
3432

35-
type ServerTool struct {
36-
Tool mcp.Tool
37-
RegisterFunc func(s *mcp.Server)
38-
}
39-
40-
func NewServerTool[In any, Out any](tool mcp.Tool, handler mcp.ToolHandlerFor[In, Out]) ServerTool {
41-
return ServerTool{Tool: tool, RegisterFunc: func(s *mcp.Server) {
42-
th := func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
43-
var arguments In
44-
if err := json.Unmarshal(req.Params.Arguments, &arguments); err != nil {
45-
return nil, err
46-
}
47-
48-
resp, _, err := handler(ctx, req, arguments)
49-
50-
return resp, err
51-
}
52-
53-
s.AddTool(&tool, th)
54-
}}
55-
}
33+
// ServerTool is defined in server_tool.go
5634

5735
type ServerResourceTemplate struct {
5836
Template mcp.ResourceTemplate
@@ -86,6 +64,8 @@ type Toolset struct {
8664
readOnly bool
8765
writeTools []ServerTool
8866
readTools []ServerTool
67+
// deps holds the dependencies for tool handlers
68+
deps ToolDependencies
8969
// resources are not tools, but the community seems to be moving towards namespaces as a broader concept
9070
// and in order to have multiple servers running concurrently, we want to avoid overlapping resources too.
9171
resourceTemplates []ServerResourceTemplate
@@ -114,16 +94,22 @@ func (t *Toolset) RegisterTools(s *mcp.Server) {
11494
if !t.Enabled {
11595
return
11696
}
117-
for _, tool := range t.readTools {
118-
tool.RegisterFunc(s)
97+
for i := range t.readTools {
98+
t.readTools[i].RegisterFunc(s, t.deps)
11999
}
120100
if !t.readOnly {
121-
for _, tool := range t.writeTools {
122-
tool.RegisterFunc(s)
101+
for i := range t.writeTools {
102+
t.writeTools[i].RegisterFunc(s, t.deps)
123103
}
124104
}
125105
}
126106

107+
// SetDependencies sets the dependencies for this toolset's tool handlers.
108+
func (t *Toolset) SetDependencies(deps ToolDependencies) *Toolset {
109+
t.deps = deps
110+
return t
111+
}
112+
127113
func (t *Toolset) AddResourceTemplates(templates ...ServerResourceTemplate) *Toolset {
128114
t.resourceTemplates = append(t.resourceTemplates, templates...)
129115
return t
@@ -358,7 +344,7 @@ func (tg *ToolsetGroup) FindToolByName(toolName string) (*ServerTool, string, er
358344
// RegisterSpecificTools registers only the specified tools.
359345
// Respects read-only mode (skips write tools if readOnly=true).
360346
// Returns error if any tool is not found.
361-
func (tg *ToolsetGroup) RegisterSpecificTools(s *mcp.Server, toolNames []string, readOnly bool) error {
347+
func (tg *ToolsetGroup) RegisterSpecificTools(s *mcp.Server, toolNames []string, readOnly bool, deps ToolDependencies) error {
362348
var skippedTools []string
363349
for _, toolName := range toolNames {
364350
tool, _, err := tg.FindToolByName(toolName)
@@ -373,7 +359,7 @@ func (tg *ToolsetGroup) RegisterSpecificTools(s *mcp.Server, toolNames []string,
373359
}
374360

375361
// Register the tool
376-
tool.RegisterFunc(s)
362+
tool.RegisterFunc(s, deps)
377363
}
378364

379365
// Log skipped write tools if any

pkg/toolsets/toolsets_test.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package toolsets
22

33
import (
4+
"context"
5+
"encoding/json"
46
"errors"
57
"testing"
68

@@ -9,15 +11,20 @@ import (
911

1012
// mockTool creates a minimal ServerTool for testing
1113
func mockTool(name string, readOnly bool) ServerTool {
12-
return ServerTool{
13-
Tool: mcp.Tool{
14+
return NewServerToolFromHandler(
15+
mcp.Tool{
1416
Name: name,
1517
Annotations: &mcp.ToolAnnotations{
1618
ReadOnlyHint: readOnly,
1719
},
20+
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
1821
},
19-
RegisterFunc: func(_ *mcp.Server) {},
20-
}
22+
func(_ ToolDependencies) mcp.ToolHandler {
23+
return func(_ context.Context, _ *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
24+
return nil, nil
25+
}
26+
},
27+
)
2128
}
2229

2330
func TestNewToolsetGroupIsEmptyWithoutEverythingOn(t *testing.T) {
@@ -375,20 +382,25 @@ func TestRegisterSpecificTools(t *testing.T) {
375382
toolset.writeTools = append(toolset.writeTools, mockTool("issue_write", false))
376383
tsg.AddToolset(toolset)
377384

385+
deps := ToolDependencies{}
386+
387+
// Create a real server for testing
388+
server := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "1.0.0"}, nil)
389+
378390
// Test registering with canonical names
379-
err := tsg.RegisterSpecificTools(nil, []string{"issue_read"}, false)
391+
err := tsg.RegisterSpecificTools(server, []string{"issue_read"}, false, deps)
380392
if err != nil {
381393
t.Errorf("expected no error registering tool, got %v", err)
382394
}
383395

384396
// Test registering write tool in read-only mode (should skip but not error)
385-
err = tsg.RegisterSpecificTools(nil, []string{"issue_write"}, true)
397+
err = tsg.RegisterSpecificTools(server, []string{"issue_write"}, true, deps)
386398
if err != nil {
387399
t.Errorf("expected no error when skipping write tool in read-only mode, got %v", err)
388400
}
389401

390402
// Test registering non-existent tool (should error)
391-
err = tsg.RegisterSpecificTools(nil, []string{"nonexistent"}, false)
403+
err = tsg.RegisterSpecificTools(server, []string{"nonexistent"}, false, deps)
392404
if err == nil {
393405
t.Error("expected error for non-existent tool")
394406
}

0 commit comments

Comments
 (0)