mcp

package
v1.21.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 35 Imported by: 0

Documentation

Overview

Package mcp exposes a REST API described by an OpenAPI 3.x document as a Model Context Protocol server with a fixed set of tools:

  • whoami: who the caller is.
  • search_functions: what exists, by module or free text, one dense line per function.
  • describe_function: how to call one or more functions (typed args, return type).
  • explain_term: the glossary (schemas and concepts, with the functions that use them).
  • call_function: invoke any function through the REST handler in-process.
  • run_script: optional, run a short JavaScript program that chains calls.

Config.Tools promotes chosen catalog functions to tools of their own, for clients that should call them without searching and describing first. Server.Call invokes a function in-process without the MCP transport, and Function.InputSchema is the JSON Schema a promoted tool advertises, so a host that embeds its own agent can build a tool list from the same catalog. A promoted tool is named by Function.ToolName, units.list as units-list. Server.Tools lists the registered tools themselves, with the descriptions, schemas and hints a client sees, and Server.CallTool runs one in-process with the REST headers given and returns the text a client would get, so such a host can offer its model exactly the MCP tools.

The catalog is derived from the document alone. Modules are the operation tags, functions are the operations, terms are the named component schemas. Nothing in this package knows about a particular application; projects add composites and concepts through Config.

Mounting on an Echo app that already serves its OpenAPI document:

server, err := mcp.New(mcp.Config{
	Spec:         openapiJSON,
	Handler:      e,
	Instructions: "Acme manages widgets and the people who order them.",
	WhoAmI:       "accounts.me",
	Concepts:     map[string]mcp.Concept{"Order": {Text: "A purchase placed by a customer.", Aliases: []string{"pedido"}}},
	Script:       mcp.ScriptConfig{Enabled: true},
})
if err != nil {
	return ez.Wrap(err)
}
e.Any("/mcp", echo.WrapHandler(server.Handler()))

Clients connect over streamable HTTP with "Authorization: Bearer <token>"; the default Auth forwards that header to the REST API unchanged, and BearerToHeaders maps a "key:secret" token to two headers instead. The server is stateless, so it runs behind any load balancer. Hosted clients that cannot hold an API key, such as claude.ai and ChatGPT connectors, log in through the OAuth server of the oauth subpackage, which supplies Auth and guards the endpoint.

Server.Snapshot returns the catalog as JSON; commit it and compare it in a test to catch unintended changes. Server.Reference renders a page that shows every tool, function and term exactly as the agent sees them.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Arg

type Arg struct {
	// Name is the argument name as the agent passes it.
	Name string
	// In is where the REST request carries it: path, query, header or body.
	In string
	// Required mirrors the parameter flag or the body required list.
	Required bool
	// Description is the parameter or property description.
	Description string
	// Schema is the schema of the argument with its ref resolved, except a
	// nullable reference to a shared component, kept as {allOf: [{$ref}],
	// nullable: true} so the component stays named. Function.InputSchema
	// gives the resolved JSON Schema of every argument.
	Schema *reference.Schema
	// contains filtered or unexported fields
}

Arg is one argument of a function.

type AuthFunc

type AuthFunc func(ctx context.Context, token string) (http.Header, error)

AuthFunc turns the bearer token of an MCP request into REST headers. The token is "" when the request has no well-formed "Authorization: Bearer <token>" header. An error means the token is not accepted; an empty token yields no headers.

func BearerPassthrough added in v1.18.0

func BearerPassthrough() AuthFunc

BearerPassthrough returns an AuthFunc that forwards the bearer token of the MCP request to the REST API unchanged, as "Authorization: Bearer <token>". An empty token yields no headers. It is the default Config.Auth.

func BearerToHeaders

func BearerToHeaders(keyHeader, secretHeader string) AuthFunc

BearerToHeaders maps a "key:secret" bearer token to the two headers the REST API authenticates with. The token is split at the first ":". An empty token yields no headers.

type Caller

type Caller interface {
	// Call invokes the function by catalog name with a flat argument object
	// and returns the response body. Errors carry the ez code of the REST
	// error.
	Call(ctx context.Context, name string, args map[string]any) (json.RawMessage, error)
}

Caller invokes catalog functions with the credentials of the current MCP request. Composites and scripts use it so every call goes through the REST handler.

type Catalog

type Catalog struct {
	Modules []*Module
	Terms   []*Term
	// contains filtered or unexported fields
}

Catalog is the agent-facing view of the document: modules holding functions, and terms. Order is deterministic so tool output and snapshots are stable.

func (*Catalog) Function

func (c *Catalog) Function(name string) (*Function, bool)

Function returns the function with the given full name.

func (*Catalog) Functions

func (c *Catalog) Functions() []*Function

Functions lists every function in module order, then name order.

func (*Catalog) Module

func (c *Catalog) Module(name string) (*Module, bool)

Module returns the module with the given name.

func (*Catalog) Term

func (c *Catalog) Term(name string) (*Term, bool)

Term returns the term by exact name, else by name or alias case-insensitively.

type Composite

type Composite struct {
	// Module is the module name the function is listed under.
	Module string
	// Name is the function name inside the module, snake_case.
	Name string
	// Summary is the one-line description shown by search_functions.
	Summary string
	// Description is the longer text shown by describe_function.
	Description string
	// ReadOnly marks the composite as safe for read-only scripts.
	ReadOnly bool
	// Input and Output are zero values of the argument and result types.
	// Their JSON Schemas are derived by reflection, which follows the Go
	// types: a field whose JSON form differs from its Go type, such as
	// json:",string" or json.RawMessage, is advertised as the Go type.
	Input  any
	Output any
	// Run executes the composite with the decoded arguments.
	Run func(ctx context.Context, call Caller, args json.RawMessage) (any, error)
}

Composite is a hand-written function that lives in a module next to the functions derived from the document. Build one with NewComposite.

func NewComposite

func NewComposite[In, Out any](module, name string, run func(context.Context, Caller, In) (Out, error)) Composite

NewComposite builds a Composite from a typed run function. In and Out are structs with json tags; their schemas are derived by reflection.

type Concept

type Concept struct {
	// Text is the paragraph explaining the term.
	Text string
	// Aliases are other names the agent may use for the term, in any language.
	Aliases []string
}

Concept is a glossary entry supplied by the project.

type Config

type Config struct {
	// Name identifies the server to clients. Defaults to info.title.
	Name string
	// Version is reported to clients. Defaults to info.version.
	Version string
	// Spec is the OpenAPI 3.x document, JSON or YAML.
	Spec []byte
	// Handler serves the REST API. call_function builds an HTTP request and
	// dispatches it to Handler in-process, so auth, validation and errors
	// are exactly those of the REST API.
	Handler http.Handler
	// Instructions is the application paragraph placed before the generated
	// module list in the server instructions.
	Instructions string
	// Auth maps the bearer token of an MCP request to the headers the REST
	// API authenticates with. Defaults to BearerPassthrough().
	Auth AuthFunc
	// WhoAmI names the catalog function that whoami calls with no arguments,
	// for example "users.me"; New refuses a function with a required
	// argument. Empty makes whoami report the server identity only.
	WhoAmI string
	// Composites are hand-written functions added to the catalog.
	Composites []Composite
	// Concepts adds glossary entries for terms that are not schemas, or sets
	// the description and aliases of a schema term. Keys are term names.
	Concepts map[string]Concept
	// Script enables and bounds run_script.
	Script ScriptConfig
	// Tools names catalog functions, operations or composites, that the server
	// also exposes as first-class MCP tools next to the fixed set, for clients
	// that should call them without search_functions and describe_function
	// first. Each name is a full catalog name, "module.function", and becomes a
	// tool named by Function.ToolName, units.list as units-list. New refuses a
	// name the catalog does not have, one whose tool name does not match
	// ^[a-zA-Z0-9_-]{1,64}$, one with an argument name that does not match
	// ^[a-zA-Z0-9_.-]{1,64}$ (the Claude API's rule for property names), one
	// whose input schema has more than 1000 nodes, or a name given twice.
	Tools []string
	// MaxOutputBytes caps the text of one tool result. Longer results are cut
	// with a hint to narrow the request. Defaults to 24000 (about 6k tokens).
	MaxOutputBytes int
	// OnError, when set, receives failures the agent sees only as generic
	// text: a panic in a tool handler or in a function a script calls, with
	// its stack, and internal failures of the script runtime. Optional.
	OnError func(ctx context.Context, err error)
}

Config configures a server. Spec and Handler are required.

type Function

type Function struct {
	// Name is the full name, "module.function".
	Name string
	// Module is the module name.
	Module string
	// Summary is the one-line description.
	Summary string
	// Description is the longer text, may be empty.
	Description string
	// Method and Path are the HTTP operation; empty for composites.
	Method string
	Path   string
	// Args is the flat argument list: path, query and header parameters
	// followed by the body properties. When a body property name collides
	// with a parameter the body is exposed as one "body" argument instead.
	Args []*Arg
	// Returns is the term name of the success response, or empty.
	Returns string
	// ReturnSchema is the resolved success response schema, or nil.
	ReturnSchema *reference.Schema
	// Deprecated mirrors the operation flag.
	Deprecated bool
	// Composite is true for hand-written functions.
	Composite bool
	// ReadOnly is true for GET and HEAD operations and read-only composites.
	ReadOnly bool
	// Destructive is true for DELETE operations.
	Destructive bool
	// contains filtered or unexported fields
}

Function is one operation or composite as the agent sees it.

func (*Function) InputSchema added in v1.20.0

func (f *Function) InputSchema() (*jsonschema.Schema, error)

InputSchema is the JSON schema of the function's flat arguments, the one a promoted tool advertises.

For an operation it has one property per argument, with the full JSON Schema of each argument, refs resolved, the required arguments in required, path arguments always among them, and no other properties allowed. An argument name used twice, such as a path and a query id, is one property, required when either is. A schema of more than 1000 nodes is ez.EINVALID. A composite has the schema reflected from its Input type, or any object when Input is not a struct, since Run takes anything.

func (*Function) ToolName added in v1.20.0

func (f *Function) ToolName() string

ToolName is the name of the tool the function becomes when promoted: its catalog name with the dot replaced by a hyphen, units.list as units-list, since claude.ai and the LLM APIs only allow [a-zA-Z0-9_-]{1,64}. Module and function names have no hyphen, so the catalog name maps back. ToolName only swaps the dot: New checks promoted names against ^[a-zA-Z0-9_-]{1,64}$, and a host listing other functions should check them too.

type Module

type Module struct {
	// Name is the snake_case tag name.
	Name string
	// Description is the tag description, when the document has one.
	Description string
	// Functions are sorted by name.
	Functions []*Function
}

Module groups the functions of one tag.

type ScriptConfig

type ScriptConfig struct {
	// Enabled registers the run_script tool.
	Enabled bool
	// Allow, when set, decides per caller whether run_script may run: it
	// receives the JSON the WhoAmI function returned for the caller's
	// credentials. Use it to limit scripts to operators, for example by
	// role. Nil allows every caller WhoAmI accepts.
	Allow func(ctx context.Context, whoami json.RawMessage) bool
	// AllowWrites lets scripts call functions that are not read-only.
	AllowWrites bool
	// Timeout stops the script, function calls included. Defaults to 5s.
	Timeout time.Duration
	// MaxCalls bounds the function calls one script may make. Defaults to 200.
	MaxCalls int
	// MaxConcurrent bounds the scripts running at once across all callers;
	// defaults to 4. Each caller runs one script at a time.
	MaxConcurrent int
}

ScriptConfig bounds run_script. The zero value disables it.

Runtime: each script runs in a fresh QuickJS runtime compiled to WebAssembly and executed by wazero inside this process. The module is compiled once, when New enables scripts, and instantiated anew for every run, so nothing survives from one script to the next. The guest sees only its own linear memory; it cannot read or write Go memory or the Go stack.

Memory: the embedded quickjs.wasm declares a 64 MiB maximum for its linear memory and the glue's allocator refuses past 48 MiB, so a script that allocates without end fails with a JavaScript out-of-memory error instead of growing the process. A script that catches that error and goes on allocating without freeing gets a few more such errors, then null, since QuickJS cannot build an error with no memory at all; an uncaught one is always reported as out of memory. On unix each run's memory is an anonymous mapping outside the Go heap, committed as the script touches it and released when the run ends, and a run whose memory cannot be mapped is refused with ERESOURCEEXHAUSTED and reported to OnError, since a limit on the process makes every run fail that way; elsewhere wazero grows it on the Go heap, copying as it grows, which can allocate a few hundred MiB for a run that fills its memory. The binary is this package's own build of quickjs-ng; quickjs/README.md and script.go record the recipe and the checksum.

Stack: a few thousand nested calls fit; a deeper recursion throws a RangeError the script can catch. The runtime stack sits at the bottom of linear memory, so native code that overflows it traps and the script fails with "the script exhausted the runtime stack or memory".

Timeout: the deadline is checked inside the compiled code, so native builtins such as JSON.stringify, regular expressions and array scans stop at the timeout too, and the runtime is then discarded. The timeout covers the whole run, function calls included. Go host code cannot be interrupted, so a Caller that ignores its context can hold a script, and its slot, past the timeout.

Reach: the guest has no network, no environment, no files, no timers and no modules: QuickJS's standard library is not linked, so there is no std, os or bjson module, and there is no module loader, so every import() fails. Its stdout and stderr are discarded. Credentials never enter the guest; its only way out is two host functions, one that calls a catalog function and one that records a console.log line.

Go enforces every rule on that way out: the function must exist in the catalog and be read-only unless AllowWrites is set, MaxCalls bounds the calls, the arguments must be a JSON object under a size cap, a response over its cap becomes an Error the script can catch instead of entering the guest, a result over its cap fails the script, and console.log output is capped in lines and bytes. The source of a script is capped as well. A panic in a function call reaches the script as a generic error and Config.OnError with its stack. The tool result puts the script's result, or its error, first and the console.log lines after it, so the output cap of the tool cuts logs, not the result.

Concurrency: MaxConcurrent bounds the scripts running at once across all callers, and each caller runs one script at a time. Both refusals are ERESOURCEEXHAUSTED: a caller whose script is still running is refused at once, and a script that finds every slot busy waits briefly first. Callers are told apart by a hash of their bearer token, or of the JSON their WhoAmI call returns when the request carries none. Every request claims its caller before it takes a slot: by its bearer token before the WhoAmI call, or by the WhoAmI response after it, so it is refused at once while its own script is still running. Every request makes the WhoAmI call and the Allow check before it takes a slot, so a caller without credentials the API accepts never takes one. A script takes only the memory it uses, up to the linear memory maximum, so the scripts of one Server hold at most MaxConcurrent times that maximum plus overhead; several Servers in one process each allow that many. The ceiling does not bound the REST handler or the rest of the process, and decoding a call's arguments in Go can briefly cost tens of MiB of heap per MiB of JSON.

Who may run scripts: New requires Config.WhoAmI when run_script is enabled, and every run first calls that function with the caller's credentials and refuses the script when the call fails. The default Auth, BearerPassthrough, forwards any token without checking it, and BearerToHeaders accepts any key:secret pair, so the WhoAmI call is what keeps anonymous callers out; name a function that fails without valid credentials. Set Allow to limit scripts to callers the operator trusts, for example by role in the WhoAmI response, and keep run_script disabled otherwise.

Limit: this is an in-process sandbox, not a process boundary. It keeps a script from crashing the process through recursion, from running a native builtin past the timeout and from exhausting the Go heap, but a bug in wazero's compiler would be a way out, and a script can keep one CPU core busy for the whole timeout. The WhoAmI call still keeps anonymous callers out, and Allow is still recommended for callers the operator does not fully trust.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is an MCP server over one REST API. Build it with New and mount Handler on an HTTP route.

func New

func New(cfg Config) (*Server, error)

New parses the spec, builds the catalog and registers the tools.

func (*Server) Call added in v1.20.0

func (s *Server) Call(ctx context.Context, header http.Header, name string, args map[string]any) (json.RawMessage, error)

Call invokes a catalog function with the REST headers given, as call_function would for an MCP request carrying them. The headers are the credentials the REST API authenticates with (for example Authorization), set after the arguments, so an argument never overrides them. There is no MCP request to copy X-Forwarded-For or X-Request-Id from; set them in header if the REST API should see them. Unlike a tool call, Call does not recover a panic in a composite nor cut the result to MaxOutputBytes; use CallTool for that.

func (*Server) CallTool added in v1.21.0

func (s *Server) CallTool(ctx context.Context, header http.Header, name string, args map[string]any) (string, bool, error)

CallTool runs one tool of Tools with the REST headers given, as the transport runs it for an MCP request, and returns the same text and the same failed flag a client gets: tool errors such as invalid arguments, an unknown function or a REST error come back as text with failed set, the text of a tool that runs is cut to MaxOutputBytes, and a panic becomes an internal error that also goes to Config.OnError. The arguments of a fixed tool are validated against its input schema and decoded into its input before it runs, as the MCP SDK does for the transport, and a failure of either is the uncut text of a failed call, as the SDK answers it.

The headers are the credentials the REST API authenticates with, used as given: Config.Auth does not run, and there is no bearer token, so whoami without Config.WhoAmI reports none and run_script keys its one-at-a-time limit by the WhoAmI response. There is no MCP request to copy X-Forwarded-For or X-Request-Id from; set them in header if the REST API should see them.

The error is ez.ENOTFOUND for a name that is not a registered tool, and ez.EINVALID when args cannot be encoded as JSON.

func (*Server) Catalog

func (s *Server) Catalog() *Catalog

Catalog returns the catalog the tools serve.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler serves the MCP streamable HTTP transport. It is stateless and answers with plain JSON, so it runs behind any load balancer.

func (*Server) Instructions

func (s *Server) Instructions() string

Instructions returns the server instructions sent to clients on initialize.

func (*Server) Reference

func (s *Server) Reference() ([]byte, error)

Reference renders the human-readable reference page of this server: the instructions, the tools, and every function and term exactly as the agent sees them. Render it once at startup and serve the bytes as text/html.

func (*Server) Snapshot

func (s *Server) Snapshot() ([]byte, error)

Snapshot returns the agent-facing surface of the server as indented JSON in a stable order: identity, instructions, tools, modules with their functions, and terms. Projects commit it and compare it in a test to catch drift of the API as the agent sees it.

func (*Server) Tools added in v1.21.0

func (s *Server) Tools() []Tool

Tools returns the tools the server registers, in registration order: the fixed set, run_script when scripts are enabled, then the functions of Config.Tools. A host that embeds its own agent offers these to its model and runs the calls through CallTool, so it never copies a description or schema by hand. Each call returns new copies.

type Term

type Term struct {
	// Name is the display name, unique within the catalog.
	Name string
	// Description is the schema description or the concept text.
	Description string
	// Aliases are other names that resolve to the term.
	Aliases []string
	// Schema is the component schema, or nil for concept-only terms.
	Schema *reference.Schema
	// AcceptedBy lists the functions whose arguments reference the schema.
	AcceptedBy []string
	// ReturnedBy lists the functions whose success response is the schema.
	ReturnedBy []string
}

Term is a glossary entry: a named schema, a project concept, or both.

type Tool added in v1.21.0

type Tool struct {
	Name        string
	Description string
	// InputSchema is the JSON Schema of the tool's arguments.
	InputSchema json.RawMessage
	// The hints are the tool's annotations as an MCP client reads them. A
	// destructiveHint or openWorldHint the server leaves unset is true, the
	// MCP default, and Destructive is false for a read-only tool, where MCP
	// gives the hint no meaning. A write promoted from Config.Tools that is
	// not Function.Destructive is therefore Destructive: MCP reads a false
	// destructiveHint as additive updates only, which PUT and PATCH are not.
	// Idempotent is idempotentHint as set, also on a read-only tool, where
	// MCP gives it no meaning either, so a read-only run_script reports false
	// and the other read-only tools true.
	ReadOnly    bool
	Idempotent  bool
	Destructive bool
	OpenWorld   bool
}

Tool describes one tool the server registers, as a client sees it in tools/list.

Directories

Path Synopsis
Package oauth lets hosted MCP clients such as claude.ai and ChatGPT connect to a compose MCP server.
Package oauth lets hosted MCP clients such as claude.ai and ChatGPT connect to a compose MCP server.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL