Skip to content

Commit 2b39455

Browse files
author
molty3000
committed
gomcp: major API improvements for odek reuse
- Tool.InputSchema changed from InputSchema struct to any (flexible schemas) - Added SetProtocolVersion() override - Added ping handler - Added initialized notification handler - Added init-tracking: tools/list and tools/call rejected before initialize - Tool errors return isError: true in result (MCP convention), not JSON-RPC error - Exported RunWithIO for testing with custom readers/writers - Updated protocol version to 2025-03-26 - All 28 tests passing
1 parent 1f1765f commit 2b39455

3 files changed

Lines changed: 149 additions & 79 deletions

File tree

gomcp/server.go

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,46 @@ import (
1414
"fmt"
1515
"io"
1616
"os"
17+
"sync"
1718
)
1819

20+
// DefaultProtocolVersion is the MCP protocol version this server speaks.
21+
const DefaultProtocolVersion = "2025-03-26"
22+
1923
// Server is an MCP server that communicates over stdio using JSON-RPC 2.0.
2024
// It handles the MCP handshake and dispatches tools, resources, and prompts
2125
// to registered handlers.
2226
type Server struct {
23-
name string
24-
version string
25-
tools map[string]Tool
26-
resources map[string]Resource
27-
prompts map[string]Prompt
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
2835
}
2936

3037
// NewServer creates a new MCP server with the given name and version.
3138
// These are reported to the client during the initialize handshake.
3239
func NewServer(name, version string) *Server {
3340
return &Server{
34-
name: name,
35-
version: version,
36-
tools: make(map[string]Tool),
37-
resources: make(map[string]Resource),
38-
prompts: make(map[string]Prompt),
41+
name: name,
42+
version: version,
43+
protocolVer: DefaultProtocolVersion,
44+
tools: make(map[string]Tool),
45+
resources: make(map[string]Resource),
46+
prompts: make(map[string]Prompt),
3947
}
4048
}
4149

50+
// SetProtocolVersion overrides the default MCP protocol version.
51+
func (s *Server) SetProtocolVersion(v string) {
52+
s.mu.Lock()
53+
defer s.mu.Unlock()
54+
s.protocolVer = v
55+
}
56+
4257
// AddTool registers a tool with the server. Tools are callable functions
4358
// that the AI client can invoke with arguments.
4459
func (s *Server) AddTool(tool Tool) {
@@ -60,12 +75,12 @@ func (s *Server) AddPrompt(prompt Prompt) {
6075
// Run starts the MCP server using os.Stdin and os.Stdout. It blocks until
6176
// stdin closes. Errors are returned if reading or writing fails.
6277
func (s *Server) Run() error {
63-
return s.runWithIO(os.Stdin, os.Stdout)
78+
return s.RunWithIO(os.Stdin, os.Stdout)
6479
}
6580

66-
// runWithIO is the internal entry point accepting arbitrary io.Reader/Writer
67-
// for testing with pipes.
68-
func (s *Server) runWithIO(r io.Reader, w io.Writer) error {
81+
// RunWithIO starts the MCP server with custom I/O readers and writers,
82+
// useful for testing with pipes or buffers.
83+
func (s *Server) RunWithIO(r io.Reader, w io.Writer) error {
6984
decoder := json.NewDecoder(r)
7085
encoder := json.NewEncoder(w)
7186

@@ -87,6 +102,17 @@ func (s *Server) runWithIO(r io.Reader, w io.Writer) error {
87102
switch req.Method {
88103
case "initialize":
89104
respErr = s.handleInitialize(req, encoder)
105+
case "initialized":
106+
// Notification — silently mark as initialized
107+
s.mu.Lock()
108+
s.initialized = true
109+
s.mu.Unlock()
110+
case "ping":
111+
respErr = encoder.Encode(JSONRPCResponse{
112+
JSONRPC: "2.0",
113+
ID: req.ID,
114+
Result: map[string]any{},
115+
})
90116
case "tools/list":
91117
respErr = s.handleToolsList(req, encoder)
92118
case "tools/call":
@@ -114,11 +140,16 @@ func (s *Server) runWithIO(r io.Reader, w io.Writer) error {
114140

115141
// handleInitialize responds to the MCP initialize handshake.
116142
func (s *Server) handleInitialize(req JSONRPCRequest, encoder *json.Encoder) error {
143+
s.mu.Lock()
144+
s.initialized = true
145+
ver := s.protocolVer
146+
s.mu.Unlock()
147+
117148
resp := JSONRPCResponse{
118149
JSONRPC: "2.0",
119150
ID: req.ID,
120151
Result: map[string]any{
121-
"protocolVersion": "2024-11-05",
152+
"protocolVersion": ver,
122153
"serverInfo": map[string]any{
123154
"name": s.name,
124155
"version": s.version,
@@ -135,6 +166,13 @@ func (s *Server) handleInitialize(req JSONRPCRequest, encoder *json.Encoder) err
135166

136167
// handleToolsList returns metadata for all registered tools.
137168
func (s *Server) handleToolsList(req JSONRPCRequest, encoder *json.Encoder) error {
169+
s.mu.Lock()
170+
init := s.initialized
171+
s.mu.Unlock()
172+
if !init {
173+
return encoder.Encode(NewJSONRPCError(req.ID, -32600, "Not initialized"))
174+
}
175+
138176
toolList := make([]Tool, 0, len(s.tools))
139177
for _, tool := range s.tools {
140178
toolList = append(toolList, tool)
@@ -157,6 +195,13 @@ type toolsCallParams struct {
157195

158196
// handleToolsCall dispatches a tool call to the registered handler.
159197
func (s *Server) handleToolsCall(req JSONRPCRequest, encoder *json.Encoder) error {
198+
s.mu.Lock()
199+
init := s.initialized
200+
s.mu.Unlock()
201+
if !init {
202+
return encoder.Encode(NewJSONRPCError(req.ID, -32600, "Not initialized"))
203+
}
204+
160205
var params toolsCallParams
161206
if err := json.Unmarshal(req.Params, &params); err != nil {
162207
errResp := NewJSONRPCError(req.ID, -32602, "Invalid params")
@@ -165,15 +210,35 @@ func (s *Server) handleToolsCall(req JSONRPCRequest, encoder *json.Encoder) erro
165210

166211
tool, ok := s.tools[params.Name]
167212
if !ok {
168-
errResp := NewJSONRPCError(req.ID, -32602, fmt.Sprintf("Unknown tool: %s", params.Name))
169-
return encoder.Encode(errResp)
213+
// Return in-band error per MCP convention
214+
resp := JSONRPCResponse{
215+
JSONRPC: "2.0",
216+
ID: req.ID,
217+
Result: map[string]any{
218+
"content": []map[string]any{
219+
{"type": "text", "text": fmt.Sprintf("Unknown tool: %s", params.Name)},
220+
},
221+
"isError": true,
222+
},
223+
}
224+
return encoder.Encode(resp)
170225
}
171226

172227
ctx := context.Background()
173228
result, err := tool.Handler(ctx, params.Arguments)
174229
if err != nil {
175-
errResp := NewJSONRPCError(req.ID, -32000, err.Error())
176-
return encoder.Encode(errResp)
230+
// Return in-band error per MCP convention
231+
resp := JSONRPCResponse{
232+
JSONRPC: "2.0",
233+
ID: req.ID,
234+
Result: map[string]any{
235+
"content": []map[string]any{
236+
{"type": "text", "text": fmt.Sprintf("Error: %v", err)},
237+
},
238+
"isError": true,
239+
},
240+
}
241+
return encoder.Encode(resp)
177242
}
178243

179244
resp := JSONRPCResponse{

0 commit comments

Comments
 (0)