113 lines
3.7 KiB
Go
113 lines
3.7 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Tool is one MCP tool: its advertised schema plus the Invoke func that parses
|
|
// arguments, does the work, and returns a JSON-encodable payload. Invoke's error
|
|
// is surfaced to the client as a tool error (isError: true), not a protocol
|
|
// error. The context carries request scope (deadline, cancellation, trace span);
|
|
// it flows from the HTTP request through to Invoke.
|
|
type Tool struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
InputSchema json.RawMessage `json:"inputSchema"`
|
|
// Annotations are optional hints about the tool's behaviour; nil means
|
|
// unannotated (clients should then assume the safest interpretation).
|
|
Annotations *ToolAnnotations `json:"annotations,omitempty"`
|
|
Invoke func(ctx context.Context, args json.RawMessage) (any, error) `json:"-"`
|
|
}
|
|
|
|
// Text wraps a payload as a successful tools/call result: one JSON text block.
|
|
func Text(v any) any {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return ErrorResult(err)
|
|
}
|
|
return map[string]any{
|
|
"content": []map[string]any{{"type": "text", "text": string(b)}},
|
|
}
|
|
}
|
|
|
|
// ErrorResult wraps an operational error as an isError tools/call result (a tool
|
|
// failure the model can read, not a JSON-RPC protocol error).
|
|
func ErrorResult(err error) any {
|
|
return map[string]any{
|
|
"isError": true,
|
|
"content": []map[string]any{{"type": "text", "text": err.Error()}},
|
|
}
|
|
}
|
|
|
|
// ToolAnnotations is the MCP tool-annotations subset that matters here:
|
|
// hints clients (and our own agent loop) use to treat calls differently —
|
|
// e.g. auto-run reads, confirm destructive writes. Both fields are always
|
|
// serialized so absence never falls back to the spec's dangerous defaults.
|
|
type ToolAnnotations struct {
|
|
ReadOnlyHint bool `json:"readOnlyHint"`
|
|
DestructiveHint bool `json:"destructiveHint"`
|
|
}
|
|
|
|
// Shared annotation presets.
|
|
var (
|
|
// ReadOnly: pure queries — safe to run without confirmation.
|
|
ReadOnly = &ToolAnnotations{ReadOnlyHint: true}
|
|
// Mutable: writes state but is not destructive (create/update/register).
|
|
Mutable = &ToolAnnotations{}
|
|
// Destructive: deletes, reversals, bulk overwrites.
|
|
Destructive = &ToolAnnotations{DestructiveHint: true}
|
|
)
|
|
|
|
// AnnotateByPrefix fills in Annotations for tools that don't carry one yet,
|
|
// using the repo-wide tool naming convention:
|
|
//
|
|
// get_*/list_*/search_*/find_*/suggest_*/describe_* → ReadOnly
|
|
// delete_*/remove_*/reverse_*/import_*/purge_* → Destructive
|
|
// export_*/calculate_*/verify_*/preview_* → ReadOnly (pure computation)
|
|
//
|
|
// Tools that set Annotations explicitly are left untouched, so a package can
|
|
// override individual cases after calling this.
|
|
func AnnotateByPrefix(tools []Tool) {
|
|
for i := range tools {
|
|
if tools[i].Annotations != nil {
|
|
continue
|
|
}
|
|
name := strings.ToLower(tools[i].Name)
|
|
switch {
|
|
case hasAny(name,
|
|
"get_", "list_", "search_", "find_", "suggest_",
|
|
"export_", "calculate_", "verify_", "preview_"):
|
|
tools[i].Annotations = ReadOnly
|
|
case hasAny(name,
|
|
"delete_", "remove_", "reverse_", "import_", "purge_"):
|
|
tools[i].Annotations = Destructive
|
|
default:
|
|
tools[i].Annotations = Mutable
|
|
}
|
|
}
|
|
}
|
|
|
|
func hasAny(name string, prefixes ...string) bool {
|
|
for _, p := range prefixes {
|
|
if strings.HasPrefix(name, p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Decode unmarshals tool arguments into v, treating empty/null args as a no-op
|
|
// (zero value). Returns a wrapped error suitable for returning from Invoke.
|
|
func Decode(args json.RawMessage, v any) error {
|
|
if len(args) == 0 || string(args) == "null" {
|
|
return nil
|
|
}
|
|
if err := json.Unmarshal(args, v); err != nil {
|
|
return fmt.Errorf("invalid arguments: %w", err)
|
|
}
|
|
return nil
|
|
}
|