Skip to content

Commit d71a60b

Browse files
authored
Merge pull request #1 from BackendStack21/claude/performance-optimizations-review-77TN3
2 parents 2b39455 + 3e3f801 commit d71a60b

5 files changed

Lines changed: 251 additions & 52 deletions

File tree

.github/workflows/ci.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
test:
14+
name: Test (Go ${{ matrix.go-version }})
15+
runs-on: ubuntu-latest
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
# Test against the module's minimum supported version and the latest
20+
# stable release to catch version-specific regressions.
21+
go-version: ['1.24.3', 'stable']
22+
steps:
23+
- name: Checkout
24+
uses: actions/checkout@v4
25+
26+
- name: Set up Go
27+
uses: actions/setup-go@v5
28+
with:
29+
go-version: ${{ matrix.go-version }}
30+
31+
- name: Verify gofmt
32+
run: |
33+
unformatted=$(gofmt -l .)
34+
if [ -n "$unformatted" ]; then
35+
echo "The following files are not gofmt-formatted:"
36+
echo "$unformatted"
37+
exit 1
38+
fi
39+
40+
- name: Vet
41+
run: go vet ./...
42+
43+
- name: Test (race + coverage)
44+
run: go test ./gomcp/ -race -count=1 -coverprofile=coverage.out -covermode=atomic
45+
46+
- name: Coverage summary
47+
run: go tool cover -func=coverage.out
48+
49+
- name: Build
50+
run: go build ./...
51+
52+
- name: Build examples
53+
# Mirrors the Makefile `examples` target. db-explorer is intentionally
54+
# omitted: it is gated behind a build constraint and is not part of the
55+
# default build.
56+
run: |
57+
go build -o /dev/null ./examples/greet/
58+
go build -o /dev/null ./examples/sys-monitor/
59+
go build -o /dev/null ./examples/fs-navigator/
60+
61+
- name: Benchmarks (compile + smoke run)
62+
run: go test ./gomcp/ -run '^$' -bench . -benchmem -benchtime 10x

examples/db-explorer/main.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
//
1010
// Requires: github.com/lib/pq driver
1111
// Build: go build -tags ignore -o db-explorer .
12-
// or remove the build tag and: go get github.com/lib/pq
12+
//
13+
// or remove the build tag and: go get github.com/lib/pq
14+
//
1315
// Test: echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | ./db-explorer
1416
package main
1517

@@ -21,8 +23,8 @@ import (
2123
"os"
2224
"strings"
2325

24-
_ "github.com/lib/pq"
2526
"github.com/BackendStack21/go-mcp/gomcp"
27+
_ "github.com/lib/pq"
2628
)
2729

2830
func main() {

gomcp/bench_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package gomcp
2+
3+
import (
4+
"context"
5+
"io"
6+
"strings"
7+
"testing"
8+
)
9+
10+
// newInitializedServer returns a server marked as initialized so benchmarks can
11+
// exercise post-handshake request handling directly.
12+
func newInitializedServer() *Server {
13+
srv := NewServer("bench-server", "1.0.0")
14+
srv.initialized = true
15+
return srv
16+
}
17+
18+
// runOnce feeds a single request followed by EOF through the server, discarding
19+
// the encoded output. It models the per-request decode → dispatch → encode path.
20+
func runOnce(b *testing.B, srv *Server, req string) {
21+
b.Helper()
22+
if err := srv.RunWithIO(strings.NewReader(req), io.Discard); err != nil {
23+
b.Fatalf("RunWithIO: %v", err)
24+
}
25+
}
26+
27+
func BenchmarkInitialize(b *testing.B) {
28+
srv := NewServer("bench-server", "1.0.0")
29+
req := `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}` + "\n"
30+
b.ReportAllocs()
31+
b.ResetTimer()
32+
for i := 0; i < b.N; i++ {
33+
runOnce(b, srv, req)
34+
}
35+
}
36+
37+
func BenchmarkToolsList(b *testing.B) {
38+
srv := newInitializedServer()
39+
for i := 0; i < 16; i++ {
40+
name := "tool" + string(rune('a'+i))
41+
srv.AddTool(Tool{
42+
Name: name,
43+
Description: "a benchmark tool",
44+
InputSchema: InputSchema{
45+
Type: "object",
46+
Properties: map[string]Property{
47+
"value": {Type: "string", Description: "the value"},
48+
},
49+
Required: []string{"value"},
50+
},
51+
})
52+
}
53+
req := `{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}` + "\n"
54+
b.ReportAllocs()
55+
b.ResetTimer()
56+
for i := 0; i < b.N; i++ {
57+
runOnce(b, srv, req)
58+
}
59+
}
60+
61+
func BenchmarkToolsCall(b *testing.B) {
62+
srv := newInitializedServer()
63+
srv.AddTool(Tool{
64+
Name: "echo",
65+
Handler: func(ctx context.Context, args map[string]any) (string, error) {
66+
return "hello world", nil
67+
},
68+
})
69+
req := `{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}` + "\n"
70+
b.ReportAllocs()
71+
b.ResetTimer()
72+
for i := 0; i < b.N; i++ {
73+
runOnce(b, srv, req)
74+
}
75+
}

gomcp/server.go

Lines changed: 34 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ const DefaultProtocolVersion = "2025-03-26"
2424
// It handles the MCP handshake and dispatches tools, resources, and prompts
2525
// to registered handlers.
2626
type Server struct {
27-
name string
28-
version string
29-
protocolVer string
30-
tools map[string]Tool
31-
resources map[string]Resource
32-
prompts map[string]Prompt
33-
initialized bool
34-
mu sync.Mutex
27+
name string
28+
version string
29+
protocolVer string
30+
tools map[string]Tool
31+
resources map[string]Resource
32+
prompts map[string]Prompt
33+
initialized bool
34+
mu sync.Mutex
3535
}
3636

3737
// NewServer creates a new MCP server with the given name and version.
@@ -111,7 +111,7 @@ func (s *Server) RunWithIO(r io.Reader, w io.Writer) error {
111111
respErr = encoder.Encode(JSONRPCResponse{
112112
JSONRPC: "2.0",
113113
ID: req.ID,
114-
Result: map[string]any{},
114+
Result: emptyObject{},
115115
})
116116
case "tools/list":
117117
respErr = s.handleToolsList(req, encoder)
@@ -148,16 +148,11 @@ func (s *Server) handleInitialize(req JSONRPCRequest, encoder *json.Encoder) err
148148
resp := JSONRPCResponse{
149149
JSONRPC: "2.0",
150150
ID: req.ID,
151-
Result: map[string]any{
152-
"protocolVersion": ver,
153-
"serverInfo": map[string]any{
154-
"name": s.name,
155-
"version": s.version,
156-
},
157-
"capabilities": map[string]any{
158-
"tools": map[string]any{},
159-
"resources": map[string]any{},
160-
"prompts": map[string]any{},
151+
Result: initializeResult{
152+
ProtocolVersion: ver,
153+
ServerInfo: serverInfo{
154+
Name: s.name,
155+
Version: s.version,
161156
},
162157
},
163158
}
@@ -180,9 +175,7 @@ func (s *Server) handleToolsList(req JSONRPCRequest, encoder *json.Encoder) erro
180175
resp := JSONRPCResponse{
181176
JSONRPC: "2.0",
182177
ID: req.ID,
183-
Result: map[string]any{
184-
"tools": toolList,
185-
},
178+
Result: toolsListResult{Tools: toolList},
186179
}
187180
return encoder.Encode(resp)
188181
}
@@ -214,11 +207,11 @@ func (s *Server) handleToolsCall(req JSONRPCRequest, encoder *json.Encoder) erro
214207
resp := JSONRPCResponse{
215208
JSONRPC: "2.0",
216209
ID: req.ID,
217-
Result: map[string]any{
218-
"content": []map[string]any{
219-
{"type": "text", "text": fmt.Sprintf("Unknown tool: %s", params.Name)},
210+
Result: toolCallResult{
211+
Content: []textContent{
212+
{Type: "text", Text: fmt.Sprintf("Unknown tool: %s", params.Name)},
220213
},
221-
"isError": true,
214+
IsError: true,
222215
},
223216
}
224217
return encoder.Encode(resp)
@@ -231,11 +224,11 @@ func (s *Server) handleToolsCall(req JSONRPCRequest, encoder *json.Encoder) erro
231224
resp := JSONRPCResponse{
232225
JSONRPC: "2.0",
233226
ID: req.ID,
234-
Result: map[string]any{
235-
"content": []map[string]any{
236-
{"type": "text", "text": fmt.Sprintf("Error: %v", err)},
227+
Result: toolCallResult{
228+
Content: []textContent{
229+
{Type: "text", Text: fmt.Sprintf("Error: %v", err)},
237230
},
238-
"isError": true,
231+
IsError: true,
239232
},
240233
}
241234
return encoder.Encode(resp)
@@ -244,12 +237,9 @@ func (s *Server) handleToolsCall(req JSONRPCRequest, encoder *json.Encoder) erro
244237
resp := JSONRPCResponse{
245238
JSONRPC: "2.0",
246239
ID: req.ID,
247-
Result: map[string]any{
248-
"content": []map[string]any{
249-
{
250-
"type": "text",
251-
"text": result,
252-
},
240+
Result: toolCallResult{
241+
Content: []textContent{
242+
{Type: "text", Text: result},
253243
},
254244
},
255245
}
@@ -265,9 +255,7 @@ func (s *Server) handleResourcesList(req JSONRPCRequest, encoder *json.Encoder)
265255
resp := JSONRPCResponse{
266256
JSONRPC: "2.0",
267257
ID: req.ID,
268-
Result: map[string]any{
269-
"resources": resourceList,
270-
},
258+
Result: resourcesListResult{Resources: resourceList},
271259
}
272260
return encoder.Encode(resp)
273261
}
@@ -298,12 +286,12 @@ func (s *Server) handleResourcesRead(req JSONRPCRequest, encoder *json.Encoder)
298286
resp := JSONRPCResponse{
299287
JSONRPC: "2.0",
300288
ID: req.ID,
301-
Result: map[string]any{
302-
"contents": []map[string]any{
289+
Result: resourcesReadResult{
290+
Contents: []resourceContent{
303291
{
304-
"uri": res.URI,
305-
"mimeType": res.MimeType,
306-
"text": content,
292+
URI: res.URI,
293+
MimeType: res.MimeType,
294+
Text: content,
307295
},
308296
},
309297
},
@@ -320,9 +308,7 @@ func (s *Server) handlePromptsList(req JSONRPCRequest, encoder *json.Encoder) er
320308
resp := JSONRPCResponse{
321309
JSONRPC: "2.0",
322310
ID: req.ID,
323-
Result: map[string]any{
324-
"prompts": promptList,
325-
},
311+
Result: promptsListResult{Prompts: promptList},
326312
}
327313
return encoder.Encode(resp)
328314
}
@@ -354,9 +340,7 @@ func (s *Server) handlePromptsGet(req JSONRPCRequest, encoder *json.Encoder) err
354340
resp := JSONRPCResponse{
355341
JSONRPC: "2.0",
356342
ID: req.ID,
357-
Result: map[string]any{
358-
"messages": messages,
359-
},
343+
Result: promptsGetResult{Messages: messages},
360344
}
361345
return encoder.Encode(resp)
362346
}

0 commit comments

Comments
 (0)