270 lines
7.8 KiB
Go

package mcptypes
import (
"encoding/json"
"fmt"
)
// ---------------------------------------------------------------------------
// Tool definition
// ---------------------------------------------------------------------------
// Tool describes an MCP tool with its name, description, input schema, and hints.
//
// Differences from mcp-go:
// - Annotations is a pointer (omitted when nil); mcp-go uses a value (always serialized).
// - Omits: RawInputSchema, OutputSchema, RawOutputSchema, DeferLoading, Icons, Execution.
// These are unused; add them when needed.
//
// We intentionally reject complex runtime schema validation here. The schema
// is typed instead, and handlers validate and decode their arguments using Go
// types and the accessors on CallToolRequest.
type Tool struct {
Meta *Meta `json:"_meta,omitempty"`
Name string `json:"name"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
InputSchema ToolInputSchema `json:"inputSchema"`
Annotations *ToolAnnotation `json:"annotations,omitempty"`
}
// Property describes a single property in a tool's input schema.
type Property struct {
Type string `json:"type"`
Description string `json:"description,omitempty"`
Enum []string `json:"enum,omitempty"`
Default string `json:"default,omitempty"`
Items *ToolInputSchema `json:"items,omitempty"`
}
// ToolInputSchema is the JSON Schema advertised for tool input. It covers the
// simple typed schemas supported by this package; handlers enforce semantics.
type ToolInputSchema struct {
Type string `json:"type"`
Properties map[string]Property `json:"properties,omitempty"`
Required []string `json:"required,omitempty"`
}
// ToolAnnotation contains behavioral hints for clients.
type ToolAnnotation struct {
Title string `json:"title,omitempty"`
ReadOnlyHint *bool `json:"readOnlyHint,omitempty"`
DestructiveHint *bool `json:"destructiveHint,omitempty"`
IdempotentHint *bool `json:"idempotentHint,omitempty"`
OpenWorldHint *bool `json:"openWorldHint,omitempty"`
}
// ---------------------------------------------------------------------------
// Tool call request
// ---------------------------------------------------------------------------
// CallToolRequest is the request object passed to tool handlers.
type CallToolRequest struct {
Method string
Params CallToolParams
}
// CallToolParams contains the tool call parameters.
type CallToolParams struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
}
// GetArguments returns the raw arguments map.
func (r CallToolRequest) GetArguments() map[string]any {
return r.Params.Arguments
}
// GetString returns a string argument or the default value.
func (r CallToolRequest) GetString(key, defaultValue string) string {
v, ok := r.Params.Arguments[key]
if !ok {
return defaultValue
}
s, ok := v.(string)
if !ok {
return defaultValue
}
return s
}
// GetInt returns an integer argument or the default value.
func (r CallToolRequest) GetInt(key string, defaultValue int) int {
v, ok := r.Params.Arguments[key]
if !ok {
return defaultValue
}
switch n := v.(type) {
case float64:
return int(n)
case json.Number:
i, err := n.Int64()
if err != nil {
return defaultValue
}
return int(i)
case int:
return n
case int64:
return int(n)
default:
return defaultValue
}
}
// GetFloat returns a float argument or the default value.
func (r CallToolRequest) GetFloat(key string, defaultValue float64) float64 {
v, ok := r.Params.Arguments[key]
if !ok {
return defaultValue
}
switch n := v.(type) {
case float64:
return n
case json.Number:
f, err := n.Float64()
if err != nil {
return defaultValue
}
return f
default:
return defaultValue
}
}
// GetBool returns a boolean argument or the default value.
func (r CallToolRequest) GetBool(key string, defaultValue bool) bool {
v, ok := r.Params.Arguments[key]
if !ok {
return defaultValue
}
b, ok := v.(bool)
if !ok {
return defaultValue
}
return b
}
// RequireString returns the string argument or an error if missing.
func (r CallToolRequest) RequireString(key string) (string, error) {
v, ok := r.Params.Arguments[key]
if !ok {
return "", fmt.Errorf("missing required argument: %s", key)
}
s, ok := v.(string)
if !ok {
return "", fmt.Errorf("argument %s must be a string", key)
}
return s, nil
}
// RequireInt returns the integer argument or an error if missing.
func (r CallToolRequest) RequireInt(key string) (int, error) {
v, ok := r.Params.Arguments[key]
if !ok {
return 0, fmt.Errorf("missing required argument: %s", key)
}
switch n := v.(type) {
case float64:
return int(n), nil
case json.Number:
i, err := n.Int64()
if err != nil {
return 0, fmt.Errorf("argument %s must be an integer", key)
}
return int(i), nil
default:
return 0, fmt.Errorf("argument %s must be a number", key)
}
}
// BindArguments unmarshals arguments into the target struct.
func (r CallToolRequest) BindArguments(target any) error {
data, err := json.Marshal(r.Params.Arguments)
if err != nil {
return err
}
return json.Unmarshal(data, target)
}
// ---------------------------------------------------------------------------
// Tool call result
// ---------------------------------------------------------------------------
// CallToolResult is the response from a tool handler.
type CallToolResult struct {
Meta *Meta `json:"-"` // marshaled manually; see MarshalJSON
Content []Content `json:"-"`
StructuredContent any `json:"-"`
IsError bool `json:"-"`
}
// MarshalJSON writes only non-empty fields and omits _meta when it carries no
// data — matching the wire format produced by mcp-go.
func (r CallToolResult) MarshalJSON() ([]byte, error) {
m := make(map[string]any, 4)
if r.Meta != nil && !r.Meta.IsEmpty() {
m["_meta"] = r.Meta
}
m["content"] = r.Content
if r.StructuredContent != nil {
m["structuredContent"] = r.StructuredContent
}
if r.IsError {
m["isError"] = r.IsError
}
return json.Marshal(m)
}
// NewToolResultText creates a result with a single text content.
func NewToolResultText(text string) *CallToolResult {
return &CallToolResult{
Content: []Content{TextContent{Type: "text", Text: text}},
}
}
// NewToolResultError creates an error result with text content.
func NewToolResultError(message string) *CallToolResult {
return &CallToolResult{
Content: []Content{TextContent{Type: "text", Text: message}},
IsError: true,
}
}
// NewToolResultJSON creates a result with JSON-serialized structured content and text.
func NewToolResultJSON(data any) (*CallToolResult, error) {
textBytes, err := json.Marshal(data)
if err != nil {
return nil, err
}
return &CallToolResult{
Content: []Content{TextContent{Type: "text", Text: string(textBytes)}},
StructuredContent: data,
}, nil
}
// NewToolResultStructured creates a result with both structured and text content.
func NewToolResultStructured(structured any, text string) *CallToolResult {
return &CallToolResult{
Content: []Content{TextContent{Type: "text", Text: text}},
StructuredContent: structured,
}
}
// NewToolResultStructuredOnly creates a result with structured content and a
// JSON text fallback for backward compatibility with clients that don't
// support structuredContent.
func NewToolResultStructuredOnly(structured any) *CallToolResult {
var fallbackText string
jsonBytes, err := json.Marshal(structured)
if err != nil {
fallbackText = fmt.Sprintf("Error serializing structured content: %v", err)
} else {
fallbackText = string(jsonBytes)
}
return &CallToolResult{
Content: []Content{TextContent{Type: "text", Text: fallbackText}},
StructuredContent: structured,
}
}