mirror of
https://github.com/therootcompany/golib.git
synced 2026-11-04 06:55:00 +00:00
345 lines
11 KiB
Go
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
|