345 lines
11 KiB
Go

// Package mcptypes defines the types for the Model Context Protocol (JSON-RPC 2.0).
//
// This is a minimal, self-contained implementation covering the subset of the
// MCP spec we use: tools, resources, and the MCP Apps extension. Originally
// derived from mcp-go (github.com/mark3labs/mcp-go) then simplified to struct
// literals, typed schemas, and no builder patterns.
package mcptypes
import (
"bytes"
"encoding/json"
"fmt"
"io"
"maps"
)
// ---------------------------------------------------------------------------
// JSON-RPC 2.0
// ---------------------------------------------------------------------------
// JSONRPCVersion is the JSON-RPC protocol version.
const JSONRPCVersion = "2.0"
// IDish is a JSON-RPC request ID. Its zero value is null.
//
// JSON-RPC permits string, number, and null IDs. json.Number preserves the
// original numeric spelling and avoids losing precision through float64.
type IDish struct {
value any // string | json.Number | nil
}
// NewInt creates an integer ID.
func NewInt(value int64) IDish {
return IDish{value: json.Number(fmt.Sprintf("%d", value))}
}
// NewFloat creates a floating-point ID.
func NewFloat(value float64) IDish {
return IDish{value: json.Number(fmt.Sprintf("%g", value))}
}
// NewString creates a string ID.
func NewString(value string) IDish {
return IDish{value: value}
}
// NewNull creates a null ID. The zero value is equivalent.
func NewNull() IDish { return IDish{} }
func (id IDish) MarshalJSON() ([]byte, error) {
if id.value == nil {
return []byte("null"), nil
}
return json.Marshal(id.value)
}
func (id *IDish) UnmarshalJSON(data []byte) error {
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
var value any
if err := decoder.Decode(&value); err != nil {
return fmt.Errorf("invalid JSON-RPC id: %s", data)
}
var extra any
if err := decoder.Decode(&extra); err != io.EOF {
return fmt.Errorf("invalid JSON-RPC id: %s", data)
}
switch value := value.(type) {
case nil, string, json.Number:
id.value = value
return nil
default:
return fmt.Errorf("JSON-RPC id must be a string, number, or null: %s", data)
}
}
func (id IDish) String() string {
switch value := id.value.(type) {
case string:
return value
case json.Number:
return value.String()
default:
return "null"
}
}
// MarshalText encodes the ID as text. Null is encoded as "null".
func (id IDish) MarshalText() ([]byte, error) {
return []byte(id.String()), nil
}
// MarshalCSV encodes null as an empty field and other IDs as their text form.
func (id IDish) MarshalCSV() (string, error) {
text, err := id.MarshalText()
if id.value == nil {
return "", err
}
return string(text), err
}
// IsNull reports whether the ID explicitly contains JSON null.
func (id IDish) IsNull() bool {
return id.value == nil
}
// RequestID is retained as an alias for callers using the original name.
type RequestID = IDish
// JSONRPCRequest is an incoming JSON-RPC 2.0 request.
type JSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
ID *RequestID `json:"id,omitempty"` // nil for notifications
IDPresent bool `json:"-"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
Meta map[string]any `json:"_meta,omitempty"`
}
// UnmarshalJSON records whether the request contained an ID. JSON null and an
// absent ID both decode to nil pointers, but only the latter is a notification.
func (r *JSONRPCRequest) UnmarshalJSON(data []byte) error {
type request JSONRPCRequest
var decoded request
if err := json.Unmarshal(data, &decoded); err != nil {
return err
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
return err
}
*r = JSONRPCRequest(decoded)
_, r.IDPresent = fields["id"]
return nil
}
// IsNotification returns true if this is a JSON-RPC notification (no id).
func (r *JSONRPCRequest) IsNotification() bool {
return !r.IDPresent
}
// JSONRPCResponse is a successful JSON-RPC 2.0 response.
type JSONRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID *RequestID `json:"id"`
Result any `json:"result"`
}
// JSONRPCError is a JSON-RPC 2.0 error response.
type JSONRPCError struct {
JSONRPC string `json:"jsonrpc"`
ID *RequestID `json:"id"`
Error JSONRPCErrorDetails `json:"error"`
}
// JSONRPCErrorDetails contains the error code, message, and optional data.
type JSONRPCErrorDetails struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
// Standard JSON-RPC error codes.
const (
ErrCodeParse = -32700
ErrCodeInvalidRequest = -32600
ErrCodeMethodNotFound = -32601
ErrCodeInvalidParams = -32602
ErrCodeInternal = -32603
ErrCodeResourceNotFound = -32002
)
// PaginatedRequest contains the optional opaque cursor used by list methods.
type PaginatedRequest struct {
Cursor string `json:"cursor,omitempty"`
}
// JSONRPCNotification is an outbound notification (no id).
type JSONRPCNotification struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params any `json:"params,omitempty"`
}
// ---------------------------------------------------------------------------
// Initialize response
// ---------------------------------------------------------------------------
// InitializeResult is the typed response for the "initialize" method.
type InitializeResult struct {
ProtocolVersion string `json:"protocolVersion"`
Capabilities ServerCapabilities `json:"capabilities"`
ServerInfo ServerInfo `json:"serverInfo"`
Instructions string `json:"instructions,omitempty"`
}
// InitializeParams contains the client's version, capabilities, and identity.
type InitializeParams struct {
ProtocolVersion string `json:"protocolVersion"`
Capabilities ClientCapabilities `json:"capabilities"`
ClientInfo ClientInfo `json:"clientInfo"`
}
// ClientCapabilities describes optional features offered by a client.
type ClientCapabilities struct {
Experimental map[string]map[string]any `json:"experimental,omitempty"`
Roots *RootsCapability `json:"roots,omitempty"`
Sampling map[string]any `json:"sampling,omitempty"`
Elicitation map[string]any `json:"elicitation,omitempty"`
}
type RootsCapability struct {
ListChanged bool `json:"listChanged,omitempty"`
}
// ServerCapabilities describes optional features offered by a server.
type ServerCapabilities struct {
Experimental map[string]map[string]any `json:"experimental,omitempty"`
Logging map[string]any `json:"logging,omitempty"`
Completions map[string]any `json:"completions,omitempty"`
Prompts *PromptsCapability `json:"prompts,omitempty"`
Resources *ResourcesCapability `json:"resources,omitempty"`
Tools *ToolsCapability `json:"tools,omitempty"`
}
type PromptsCapability struct {
ListChanged bool `json:"listChanged,omitempty"`
}
type ResourcesCapability struct {
Subscribe bool `json:"subscribe,omitempty"`
ListChanged bool `json:"listChanged,omitempty"`
}
type ToolsCapability struct {
ListChanged bool `json:"listChanged,omitempty"`
}
// ClientInfo identifies the client implementation.
type ClientInfo struct {
Name string `json:"name"`
Version string `json:"version"`
}
// ServerInfo identifies the server implementation.
type ServerInfo struct {
Name string `json:"name"`
Title string `json:"title,omitempty"`
Version string `json:"version"`
}
// ---------------------------------------------------------------------------
// Meta
// ---------------------------------------------------------------------------
// AppUI is the _meta.ui object shared by tools and resources in the MCP Apps spec.
// Tools set ResourceURI and Visibility; resources set CSP and Permissions.
// All fields are optional; omitempty keeps the wire format clean.
// See https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx
type AppUI struct {
// Tool fields
ResourceURI string `json:"resourceUri,omitempty"`
Visibility []string `json:"visibility,omitempty"`
// Resource fields
CSP *AppCSP `json:"csp,omitempty"`
Permissions *AppPermissions `json:"permissions,omitempty"`
}
// AppCSP controls which external origins the sandboxed app can load from.
type AppCSP struct {
ConnectDomains []string `json:"connectDomains,omitempty"`
ResourceDomains []string `json:"resourceDomains,omitempty"`
FrameDomains []string `json:"frameDomains,omitempty"`
BaseURIDomains []string `json:"baseUriDomains,omitempty"`
}
// AppPermissions declares iframe sandbox permissions as empty-object markers.
type AppPermissions struct {
ClipboardWrite *struct{} `json:"clipboardWrite,omitempty"`
Camera *struct{} `json:"camera,omitempty"`
Microphone *struct{} `json:"microphone,omitempty"`
Geolocation *struct{} `json:"geolocation,omitempty"`
}
// Meta carries additional metadata on tools, resources, and results.
// Values holds non-MCP extension fields; UI is the typed MCP Apps field.
type Meta struct {
Values map[string]any `json:"-"`
UI *AppUI `json:"-"`
}
// MarshalJSON preserves arbitrary extension metadata while encoding the typed
// MCP Apps field under its reserved "ui" key.
func (m Meta) MarshalJSON() ([]byte, error) {
values := make(map[string]any, len(m.Values)+1)
maps.Copy(values, m.Values)
if m.UI != nil {
values["ui"] = m.UI
}
return json.Marshal(values)
}
// UnmarshalJSON decodes arbitrary metadata and the typed MCP Apps field.
func (m *Meta) UnmarshalJSON(data []byte) error {
var values map[string]any
if err := json.Unmarshal(data, &values); err != nil {
return err
}
delete(values, "ui")
m.Values = values
var raw struct {
UI *AppUI `json:"ui"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
m.UI = raw.UI
return nil
}
// IsEmpty returns true when Meta carries no data. Used by containers that
// need to decide whether to omit the _meta key entirely.
func (m *Meta) IsEmpty() bool {
return m == nil || (m.UI == nil && len(m.Values) == 0)
}
// ---------------------------------------------------------------------------
// Content types
// ---------------------------------------------------------------------------
// Content is a union type for tool result content.
// mcp-go uses separate TextContent, ImageContent, AudioContent structs each with
// an Annotated embed and Meta field. We use a single flat struct since we only
// produce text content; add typed variants when image/audio support is needed.
type Content struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
Data string `json:"data,omitempty"`
MIMEType string `json:"mimeType,omitempty"`
}
// TextContent represents text content in a tool result.
type TextContent = Content