224 lines
5.7 KiB
Go
224 lines
5.7 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
const protocolVersion = "2025-06-18"
|
|
|
|
type Tool struct {
|
|
Name string
|
|
Description string
|
|
InputSchema json.RawMessage
|
|
Handler func(ctx context.Context, args json.RawMessage) (string, error)
|
|
Rich func(ctx context.Context, args json.RawMessage) Result
|
|
}
|
|
|
|
type Content struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text"`
|
|
Data string `json:"data"`
|
|
MimeType string `json:"mimeType"`
|
|
}
|
|
|
|
func (c Content) MarshalJSON() ([]byte, error) {
|
|
if c.Type == "image" {
|
|
return json.Marshal(map[string]string{"type": c.Type, "data": c.Data, "mimeType": c.MimeType})
|
|
}
|
|
return json.Marshal(map[string]string{"type": c.Type, "text": c.Text})
|
|
}
|
|
|
|
type Result struct {
|
|
Content []Content `json:"content"`
|
|
IsError bool `json:"isError,omitempty"`
|
|
}
|
|
|
|
func TextResult(text string, isError bool) Result {
|
|
return Result{Content: []Content{{Type: "text", Text: text}}, IsError: isError}
|
|
}
|
|
|
|
const (
|
|
MaxTimeout = 10 * time.Minute
|
|
Grace = 5 * time.Second
|
|
)
|
|
|
|
type timeoutKey struct{}
|
|
|
|
func Timeout(ctx context.Context) time.Duration {
|
|
return ctx.Value(timeoutKey{}).(time.Duration)
|
|
}
|
|
|
|
func (t Tool) Schema() json.RawMessage {
|
|
s := map[string]any{}
|
|
err := json.Unmarshal(t.InputSchema, &s)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("tool %s: input schema: %s", t.Name, err))
|
|
}
|
|
props, _ := s["properties"].(map[string]any)
|
|
if props == nil {
|
|
props = map[string]any{}
|
|
s["properties"] = props
|
|
}
|
|
if props["timeout_ms"] == nil {
|
|
props["timeout_ms"] = map[string]any{"type": "integer", "description": fmt.Sprintf("Give up on the call after this long (at most %d).", MaxTimeout.Milliseconds())}
|
|
}
|
|
required, _ := s["required"].([]any)
|
|
s["required"] = append(required, "timeout_ms")
|
|
out, err := json.Marshal(s)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("tool %s: input schema: %s", t.Name, err))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (t Tool) Call(ctx context.Context, args json.RawMessage) Result {
|
|
p := struct {
|
|
TimeoutMs *int `json:"timeout_ms"`
|
|
}{}
|
|
err := json.Unmarshal(args, &p)
|
|
if err != nil {
|
|
return TextResult(fmt.Sprintf("malformed %s call: %s", t.Name, err), true)
|
|
}
|
|
if p.TimeoutMs == nil || *p.TimeoutMs <= 0 || *p.TimeoutMs > int(MaxTimeout.Milliseconds()) {
|
|
return TextResult(fmt.Sprintf("timeout_ms is required, between 1 and %d", MaxTimeout.Milliseconds()), true)
|
|
}
|
|
timeout := time.Duration(*p.TimeoutMs) * time.Millisecond
|
|
|
|
cctx, cancel := context.WithTimeout(context.WithValue(ctx, timeoutKey{}, timeout), timeout)
|
|
defer cancel()
|
|
done := make(chan Result, 1)
|
|
go func() {
|
|
done <- t.run(cctx, args)
|
|
}()
|
|
late := time.NewTimer(timeout + Grace)
|
|
defer late.Stop()
|
|
select {
|
|
case res := <-done:
|
|
return res
|
|
case <-ctx.Done():
|
|
return TextResult(ctx.Err().Error(), true)
|
|
case <-late.C:
|
|
return TextResult(fmt.Sprintf("%s gave no result within %s", t.Name, timeout), true)
|
|
}
|
|
}
|
|
|
|
func (t Tool) run(ctx context.Context, args json.RawMessage) Result {
|
|
if t.Rich != nil {
|
|
return t.Rich(ctx, args)
|
|
}
|
|
text, err := t.Handler(ctx, args)
|
|
if err != nil {
|
|
return TextResult(err.Error(), true)
|
|
}
|
|
return TextResult(text, false)
|
|
}
|
|
|
|
type Server struct {
|
|
Name string
|
|
Version string
|
|
Tools []Tool
|
|
}
|
|
|
|
type Stdio struct {
|
|
Name string
|
|
Command string
|
|
Args []string
|
|
}
|
|
|
|
type request struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID json.RawMessage `json:"id,omitempty"`
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params,omitempty"`
|
|
}
|
|
|
|
type rpcError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type response struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID json.RawMessage `json:"id"`
|
|
Result any `json:"result,omitempty"`
|
|
Error *rpcError `json:"error,omitempty"`
|
|
}
|
|
|
|
type toolDesc struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
InputSchema json.RawMessage `json:"inputSchema"`
|
|
}
|
|
|
|
func (s *Server) Tool(name string) (Tool, bool) {
|
|
for _, t := range s.Tools {
|
|
if t.Name == name {
|
|
return t, true
|
|
}
|
|
}
|
|
return Tool{}, false
|
|
}
|
|
|
|
func (s *Server) Handle(ctx context.Context, raw json.RawMessage) (json.RawMessage, error) {
|
|
req := request{}
|
|
err := json.Unmarshal(raw, &req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse jsonrpc: %w", err)
|
|
}
|
|
|
|
if req.ID == nil {
|
|
return json.Marshal(response{JSONRPC: "2.0", Result: map[string]any{}})
|
|
}
|
|
|
|
result, rerr := s.dispatch(ctx, req)
|
|
resp := response{JSONRPC: "2.0", ID: req.ID, Result: result, Error: rerr}
|
|
return json.Marshal(resp)
|
|
}
|
|
|
|
func (s *Server) dispatch(ctx context.Context, req request) (any, *rpcError) {
|
|
switch req.Method {
|
|
case "initialize":
|
|
return map[string]any{
|
|
"protocolVersion": protocolVersion,
|
|
"capabilities": map[string]any{"tools": map[string]any{}},
|
|
"serverInfo": map[string]any{"name": s.Name, "version": s.Version},
|
|
}, nil
|
|
|
|
case "ping":
|
|
return map[string]any{}, nil
|
|
|
|
case "tools/list":
|
|
tools := []toolDesc{}
|
|
for _, t := range s.Tools {
|
|
tools = append(tools, toolDesc{Name: t.Name, Description: t.Description, InputSchema: t.Schema()})
|
|
}
|
|
return map[string]any{"tools": tools}, nil
|
|
|
|
case "tools/call":
|
|
return s.call(ctx, req.Params)
|
|
}
|
|
|
|
return nil, &rpcError{Code: -32601, Message: fmt.Sprintf("method not found: %s", req.Method)}
|
|
}
|
|
|
|
func (s *Server) call(ctx context.Context, params json.RawMessage) (any, *rpcError) {
|
|
p := struct {
|
|
Name string `json:"name"`
|
|
Arguments json.RawMessage `json:"arguments"`
|
|
}{}
|
|
err := json.Unmarshal(params, &p)
|
|
if err != nil {
|
|
return nil, &rpcError{Code: -32602, Message: err.Error()}
|
|
}
|
|
|
|
t, ok := s.Tool(p.Name)
|
|
if !ok {
|
|
return nil, &rpcError{Code: -32602, Message: fmt.Sprintf("unknown tool: %s", p.Name)}
|
|
}
|
|
|
|
return t.Call(ctx, p.Arguments), nil
|
|
}
|