| 1 | package mcp |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "math" |
| 9 | "net/http" |
| 10 | "regexp" |
| 11 | "sort" |
| 12 | "strconv" |
| 13 | "strings" |
| 14 | "unicode/utf8" |
| 15 | ) |
| 16 | |
| 17 | // Tool is one entry in a server's tool table. |
| 18 | type Tool struct { |
| 19 | Name string `json:"name"` |
| 20 | Title string `json:"title,omitempty"` |
| 21 | Description string `json:"description"` |
| 22 | InputSchema map[string]any `json:"inputSchema"` |
| 23 | OutputSchema map[string]any `json:"outputSchema,omitempty"` |
| 24 | Annotations map[string]any `json:"annotations,omitempty"` |
| 25 | Handler Handler `json:"-"` |
| 26 | } |
| 27 | |
| 28 | // Handler executes a tool. A returned error is reported to the client as a |
| 29 | // tool execution error the model can act on; protocol errors are raised by |
| 30 | // the transport before the handler runs. |
| 31 | type Handler func(ctx context.Context, call Call) (Result, error) |
| 32 | |
| 33 | // Call carries the validated arguments of a tools/call request together with |
| 34 | // the authenticated actor. |
| 35 | type Call struct { |
| 36 | Actor string |
| 37 | Name string |
| 38 | Arguments map[string]any |
| 39 | Request *http.Request |
| 40 | } |
| 41 | |
| 42 | // String returns a string argument or "". |
| 43 | func (c Call) String(key string) string { |
| 44 | value, _ := c.Arguments[key].(string) |
| 45 | return value |
| 46 | } |
| 47 | |
| 48 | // Int returns an integer argument or 0. |
| 49 | func (c Call) Int(key string) int { |
| 50 | value, _ := c.Arguments[key].(float64) |
| 51 | return int(value) |
| 52 | } |
| 53 | |
| 54 | // Content is one unstructured content block of a tool result. |
| 55 | type Content struct { |
| 56 | Type string `json:"type"` |
| 57 | Text string `json:"text,omitempty"` |
| 58 | Data string `json:"data,omitempty"` |
| 59 | MimeType string `json:"mimeType,omitempty"` |
| 60 | } |
| 61 | |
| 62 | // Result is what a tool returns. StructuredContent is mirrored into a text |
| 63 | // block when Content is empty so every client sees the same data. |
| 64 | type Result struct { |
| 65 | Content []Content `json:"content"` |
| 66 | StructuredContent any `json:"structuredContent,omitempty"` |
| 67 | IsError bool `json:"isError,omitempty"` |
| 68 | } |
| 69 | |
| 70 | // Value builds a successful result from a JSON value. |
| 71 | func Value(value any) Result { |
| 72 | return Result{StructuredContent: value} |
| 73 | } |
| 74 | |
| 75 | // Failure builds a tool execution error. Structured data, when given, helps |
| 76 | // the client correct the call. |
| 77 | func Failure(message string, structured any) Result { |
| 78 | return Result{Content: []Content{{Type: "text" , Text: message}}, StructuredContent: structured, IsError: true} |
| 79 | } |
| 80 | |
| 81 | func (r Result) filled() Result { |
| 82 | if len(r.Content) == 0 { |
| 83 | text, err := json.Marshal(r.StructuredContent) |
| 84 | if err != nil { |
| 85 | text = []byte("null" ) |
| 86 | } |
| 87 | r.Content = []Content{{Type: "text" , Text: string(text)}} |
| 88 | } |
| 89 | return r |
| 90 | } |
| 91 | |
| 92 | // Registry is an ordered tool table. Tools list in the order they were |
| 93 | // added so clients can cache the list. |
| 94 | type Registry struct { |
| 95 | tools []Tool |
| 96 | index map[string]int |
| 97 | } |
| 98 | |
| 99 | var toolName = regexp.MustCompile(`^[A-Za-z0-9._-]{1,128}$` ) |
| 100 | |
| 101 | // Add registers a tool. Names must be unique and header safe. |
| 102 | func (r *Registry) Add(tool Tool) error { |
| 103 | if !toolName.MatchString(tool.Name) { |
| 104 | return fmt.Errorf("mcp: invalid tool name %q" , tool.Name) |
| 105 | } |
| 106 | if tool.Handler == nil { |
| 107 | return fmt.Errorf("mcp: tool %q has no handler" , tool.Name) |
| 108 | } |
| 109 | if tool.InputSchema == nil { |
| 110 | tool.InputSchema = Object(nil) |
| 111 | } |
| 112 | if r.index == nil { |
| 113 | r.index = map[string]int{} |
| 114 | } |
| 115 | if _, exists := r.index[tool.Name]; exists { |
| 116 | return fmt.Errorf("mcp: duplicate tool %q" , tool.Name) |
| 117 | } |
| 118 | r.index[tool.Name] = len(r.tools) |
| 119 | r.tools = append(r.tools, tool) |
| 120 | return nil |
| 121 | } |
| 122 | |
| 123 | // Tools returns the table in registration order. |
| 124 | func (r *Registry) Tools() []Tool { |
| 125 | if r == nil { |
| 126 | return nil |
| 127 | } |
| 128 | return append([]Tool(nil), r.tools...) |
| 129 | } |
| 130 | |
| 131 | // Lookup finds a tool by name. |
| 132 | func (r *Registry) Lookup(name string) (Tool, bool) { |
| 133 | if r == nil || r.index == nil { |
| 134 | return Tool{}, false |
| 135 | } |
| 136 | i, ok := r.index[name] |
| 137 | if !ok { |
| 138 | return Tool{}, false |
| 139 | } |
| 140 | return r.tools[i], true |
| 141 | } |
| 142 | |
| 143 | // Object builds a closed object schema. Properties are sorted when listed so |
| 144 | // the schema serializes deterministically. |
| 145 | func Object(properties map[string]any, required ...string) map[string]any { |
| 146 | if properties == nil { |
| 147 | properties = map[string]any{} |
| 148 | } |
| 149 | schema := map[string]any{"type" : "object" , "properties" : properties, "additionalProperties" : false} |
| 150 | if len(required) > 0 { |
| 151 | schema["required" ] = required |
| 152 | } |
| 153 | return schema |
| 154 | } |
| 155 | |
| 156 | // Validate checks a value against the subset of JSON Schema 2020-12 the tool |
| 157 | // tables use: type, properties, required, additionalProperties, enum, not, |
| 158 | // pattern, minimum, maximum, minLength, maxLength, minProperties and items. |
| 159 | func Validate(schema map[string]any, value any) error { |
| 160 | return validate(schema, value, "" ) |
| 161 | } |
| 162 | |
| 163 | func validate(schema map[string]any, value any, path string) error { |
| 164 | where := func() string { |
| 165 | if path == "" { |
| 166 | return "argument" |
| 167 | } |
| 168 | return path |
| 169 | } |
| 170 | if typ, ok := schema["type" ].(string); ok { |
| 171 | if err := checkType(typ, value); err != nil { |
| 172 | return fmt.Errorf("%s %s" , where(), err) |
| 173 | } |
| 174 | } |
| 175 | if options := enumOptions(schema["enum" ]); len(options) > 0 && !contains(options, scalar(value)) { |
| 176 | return fmt.Errorf("%s must be one of %s" , where(), strings.Join(options, ", " )) |
| 177 | } |
| 178 | if excluded, ok := schema["not" ].(map[string]any); ok { |
| 179 | if validate(excluded, value, path) == nil { |
| 180 | return fmt.Errorf("%s matches an excluded value" , where()) |
| 181 | } |
| 182 | } |
| 183 | if pattern, ok := schema["pattern" ].(string); ok { |
| 184 | text, _ := value.(string) |
| 185 | if re, err := regexp.Compile(pattern); err == nil && !re.MatchString(text) { |
| 186 | return fmt.Errorf("%s does not match %s" , where(), pattern) |
| 187 | } |
| 188 | } |
| 189 | if text, ok := value.(string); ok { |
| 190 | if min, ok := number(schema["minLength" ]); ok && float64(utf8.RuneCountInString(text)) < min { |
| 191 | return fmt.Errorf("%s must not be empty" , where()) |
| 192 | } |
| 193 | if max, ok := number(schema["maxLength" ]); ok && float64(utf8.RuneCountInString(text)) > max { |
| 194 | return fmt.Errorf("%s is longer than %d characters" , where(), int(max)) |
| 195 | } |
| 196 | } |
| 197 | if n, ok := value.(float64); ok { |
| 198 | if min, ok := number(schema["minimum" ]); ok && n < min { |
| 199 | return fmt.Errorf("%s must be at least %v" , where(), min) |
| 200 | } |
| 201 | if max, ok := number(schema["maximum" ]); ok && n > max { |
| 202 | return fmt.Errorf("%s must be at most %v" , where(), max) |
| 203 | } |
| 204 | } |
| 205 | if object, ok := value.(map[string]any); ok { |
| 206 | properties, _ := schema["properties" ].(map[string]any) |
| 207 | for _, key := range required(schema) { |
| 208 | if _, present := object[key]; !present { |
| 209 | return fmt.Errorf("%s is required" , join(path, key)) |
| 210 | } |
| 211 | } |
| 212 | if min, ok := number(schema["minProperties" ]); ok && float64(len(object)) < min { |
| 213 | return fmt.Errorf("%s must not be empty" , where()) |
| 214 | } |
| 215 | keys := make([]string, 0, len(object)) |
| 216 | for key := range object { |
| 217 | keys = append(keys, key) |
| 218 | } |
| 219 | sort.Strings(keys) |
| 220 | for _, key := range keys { |
| 221 | property, known := properties[key].(map[string]any) |
| 222 | if !known { |
| 223 | if closed, _ := schema["additionalProperties" ].(bool); schema["additionalProperties" ] != nil && !closed { |
| 224 | return fmt.Errorf("%s is not a known argument" , join(path, key)) |
| 225 | } |
| 226 | continue |
| 227 | } |
| 228 | if object[key] == nil { |
| 229 | continue |
| 230 | } |
| 231 | if err := validate(property, object[key], join(path, key)); err != nil { |
| 232 | return err |
| 233 | } |
| 234 | } |
| 235 | } |
| 236 | if list, ok := value.([]any); ok { |
| 237 | if items, ok := schema["items" ].(map[string]any); ok { |
| 238 | for i, item := range list { |
| 239 | if err := validate(items, item, fmt.Sprintf("%s[%d]" , where(), i)); err != nil { |
| 240 | return err |
| 241 | } |
| 242 | } |
| 243 | } |
| 244 | } |
| 245 | return nil |
| 246 | } |
| 247 | |
| 248 | func checkType(typ string, value any) error { |
| 249 | switch typ { |
| 250 | case "string" : |
| 251 | if _, ok := value.(string); !ok { |
| 252 | return errors.New("must be a string" ) |
| 253 | } |
| 254 | case "integer" : |
| 255 | n, ok := value.(float64) |
| 256 | if !ok || n != math.Trunc(n) { |
| 257 | return errors.New("must be an integer" ) |
| 258 | } |
| 259 | case "number" : |
| 260 | if _, ok := value.(float64); !ok { |
| 261 | return errors.New("must be a number" ) |
| 262 | } |
| 263 | case "boolean" : |
| 264 | if _, ok := value.(bool); !ok { |
| 265 | return errors.New("must be true or false" ) |
| 266 | } |
| 267 | case "object" : |
| 268 | if _, ok := value.(map[string]any); !ok { |
| 269 | return errors.New("must be an object" ) |
| 270 | } |
| 271 | case "array" : |
| 272 | if _, ok := value.([]any); !ok { |
| 273 | return errors.New("must be an array" ) |
| 274 | } |
| 275 | } |
| 276 | return nil |
| 277 | } |
| 278 | |
| 279 | func required(schema map[string]any) []string { |
| 280 | switch list := schema["required" ].(type) { |
| 281 | case []string: |
| 282 | return list |
| 283 | case []any: |
| 284 | out := make([]string, 0, len(list)) |
| 285 | for _, item := range list { |
| 286 | if text, ok := item.(string); ok { |
| 287 | out = append(out, text) |
| 288 | } |
| 289 | } |
| 290 | return out |
| 291 | } |
| 292 | return nil |
| 293 | } |
| 294 | |
| 295 | // enumOptions renders enum members as strings so integer and string enums |
| 296 | // compare the same way. |
| 297 | func enumOptions(value any) []string { |
| 298 | switch list := value.(type) { |
| 299 | case []string: |
| 300 | return list |
| 301 | case []int: |
| 302 | out := make([]string, 0, len(list)) |
| 303 | for _, item := range list { |
| 304 | out = append(out, strconv.Itoa(item)) |
| 305 | } |
| 306 | return out |
| 307 | case []any: |
| 308 | out := make([]string, 0, len(list)) |
| 309 | for _, item := range list { |
| 310 | out = append(out, scalar(item)) |
| 311 | } |
| 312 | return out |
| 313 | } |
| 314 | return nil |
| 315 | } |
| 316 | |
| 317 | func scalar(value any) string { |
| 318 | switch v := value.(type) { |
| 319 | case string: |
| 320 | return v |
| 321 | case float64: |
| 322 | return strconv.FormatFloat(v, 'f' , -1, 64) |
| 323 | case int: |
| 324 | return strconv.Itoa(v) |
| 325 | case bool: |
| 326 | return strconv.FormatBool(v) |
| 327 | } |
| 328 | return "" |
| 329 | } |
| 330 | |
| 331 | func number(value any) (float64, bool) { |
| 332 | switch n := value.(type) { |
| 333 | case int: |
| 334 | return float64(n), true |
| 335 | case int64: |
| 336 | return float64(n), true |
| 337 | case float64: |
| 338 | return n, true |
| 339 | } |
| 340 | return 0, false |
| 341 | } |
| 342 | |
| 343 | func join(path, key string) string { |
| 344 | if path == "" { |
| 345 | return key |
| 346 | } |
| 347 | return path + "." + key |
| 348 | } |
| 349 | |
| 350 | func contains(values []string, want string) bool { |
| 351 | for _, value := range values { |
| 352 | if value == want { |
| 353 | return true |
| 354 | } |
| 355 | } |
| 356 | return false |
| 357 | } |