- Fix schema-derived flags not being collected into tool call params - Add registerHTTPServer for plugin streamable-http tool discovery - Include dev plugins in ListInstalled output - Isolate settings path for test environments - Add stdio and HTTP MCP end-to-end integration tests
127 lines
3.4 KiB
Go
127 lines
3.4 KiB
Go
// Minimal MCP stdio server for integration tests.
|
|
// Implements initialize, tools/list, tools/call over newline-delimited JSON-RPC.
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
type request struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID json.RawMessage `json:"id"`
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params,omitempty"`
|
|
}
|
|
|
|
type response struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID json.RawMessage `json:"id"`
|
|
Result any `json:"result,omitempty"`
|
|
Error *rpcError `json:"error,omitempty"`
|
|
}
|
|
|
|
type rpcError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func main() {
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Bytes()
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
var req request
|
|
if err := json.Unmarshal(line, &req); err != nil {
|
|
writeResp(response{JSONRPC: "2.0", Error: &rpcError{Code: -32700, Message: "parse error"}})
|
|
continue
|
|
}
|
|
|
|
switch req.Method {
|
|
case "initialize":
|
|
writeResp(response{JSONRPC: "2.0", ID: req.ID, Result: map[string]any{
|
|
"protocolVersion": "2025-03-26",
|
|
"capabilities": map[string]any{"tools": map[string]any{}},
|
|
"serverInfo": map[string]any{"name": "test-server", "version": "0.0.1"},
|
|
}})
|
|
|
|
case "tools/list":
|
|
writeResp(response{JSONRPC: "2.0", ID: req.ID, Result: map[string]any{
|
|
"tools": []map[string]any{
|
|
{
|
|
"name": "test_echo",
|
|
"description": "Echo the input message",
|
|
"inputSchema": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"message": map[string]any{"type": "string", "description": "Message to echo"},
|
|
},
|
|
"required": []string{"message"},
|
|
},
|
|
},
|
|
{
|
|
"name": "test_add",
|
|
"description": "Add two numbers",
|
|
"inputSchema": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"a": map[string]any{"type": "integer"},
|
|
"b": map[string]any{"type": "integer"},
|
|
},
|
|
"required": []string{"a", "b"},
|
|
},
|
|
},
|
|
},
|
|
}})
|
|
|
|
case "tools/call":
|
|
handleCall(req.ID, req.Params)
|
|
|
|
case "notifications/initialized":
|
|
// no response
|
|
continue
|
|
|
|
default:
|
|
writeResp(response{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32601, Message: "method not found"}})
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleCall(id, params json.RawMessage) {
|
|
var p struct {
|
|
Name string `json:"name"`
|
|
Arguments map[string]any `json:"arguments"`
|
|
}
|
|
if err := json.Unmarshal(params, &p); err != nil {
|
|
writeResp(response{JSONRPC: "2.0", ID: id, Error: &rpcError{Code: -32602, Message: "invalid params"}})
|
|
return
|
|
}
|
|
|
|
switch p.Name {
|
|
case "test_echo":
|
|
msg, _ := p.Arguments["message"].(string)
|
|
writeResp(response{JSONRPC: "2.0", ID: id, Result: map[string]any{
|
|
"content": []map[string]any{{"type": "text", "text": fmt.Sprintf("Echo: %s", msg)}},
|
|
}})
|
|
case "test_add":
|
|
a, _ := p.Arguments["a"].(float64)
|
|
b, _ := p.Arguments["b"].(float64)
|
|
writeResp(response{JSONRPC: "2.0", ID: id, Result: map[string]any{
|
|
"content": []map[string]any{{"type": "text", "text": fmt.Sprintf("%.0f", a+b)}},
|
|
}})
|
|
default:
|
|
writeResp(response{JSONRPC: "2.0", ID: id, Error: &rpcError{Code: -32601, Message: "unknown tool: " + p.Name}})
|
|
}
|
|
}
|
|
|
|
func writeResp(resp response) {
|
|
data, _ := json.Marshal(resp)
|
|
fmt.Fprintf(os.Stdout, "%s\n", data)
|
|
}
|