Help us shape the future of Go! Take the 2026 Go Developer Survey

mcp

package
v0.0.26 Latest Latest
Warning

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

Go to latest
Published: Oct 5, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package mcp serves the Model Context Protocol, as a server, over stdio.

A coding agent or assistant launches this shell as a subprocess and speaks JSON-RPC to it a line at a time. What it gets is the five terminal verbs — start a command, read what it has written, wait for it, kill it, let it go — each one an interp.Action through the shell's gate, into the shell's audit trail.

This shell is an MCP server and is still not an MCP host

docs/design/acp.md:136 says `session/new` carries `mcpServers`, that ours is accepted and ignored, and that **a shell is not an MCP host**. That line is about *consuming* MCP servers and it is unchanged. Being one is the other direction, and it is #1338.

The revision implemented against

**`2026-07-28`**, which is the current released specification. That revision is "modern" in its own words: there is no `initialize` handshake, every request declares its version in `_meta`, and `server/discover` is the probe.

A **legacy** handshake at `2025-11-25` is served beside it, and that is a measurement rather than a hedge. Measured 2026-09-21 against Claude Code 2.1.267, which is the client population this work is for: it opens with `initialize` carrying `protocolVersion: "2025-11-25"`, sends no `_meta` at all, never probes with `server/discover`, and accepts a server that answers with a different revision than it asked for. A modern-only server would be correct against the specification and unreachable by the client the issue names. The specification provides for exactly this: see its "Backward Compatibility with Initialization-Based Versions", which names a server that serves both a **dual-era** server and says how one selects its behavior — from how the client opens, per request, which is what eraOf does.

What is served, and what is not

Tools, and nothing else. No resources, no prompts, no completions, no logging, no subscriptions, and no extensions: each of those is a capability this shell would have to have something to put in, and it does not. What is absent is absent because it is empty, not because it is unfinished.

Server-initiated requests are absent for a stronger reason. On stdio the 2026-07-28 revision says the server **MUST NOT** write JSON-RPC requests to standard output; a server that needs something from a person answers a tool call with an `InputRequiredResult` and is retried. So there is no elicitation-driven permission prompt here, and a policy is what refuses — which is the shape #1334 asked for anyway. docs/design/mcp.md argues it.

Index

Constants

View Source
const (
	ToolCreate  = "terminal_create"
	ToolOutput  = "terminal_output"
	ToolWait    = "terminal_wait_for_exit"
	ToolKill    = "terminal_kill"
	ToolRelease = "terminal_release"
)

The five verbs, and why they are five.

A shell command is four things at once — a stream, an exit status, two output channels, and possibly standard input — and an MCP tool call is one request and one result. #1338 records the three honest mappings and the maintainer's decision between them, on 2026-09-21: **mirror ACP's terminal model**. A long-running command is a handle the client polls or is notified about, and "return the whole output as a string" is rejected, because it is the answer that looks fine until somebody runs `tail -f` — it either blocks forever or truncates, and truncating a command's output while returning a success status is the silent-wrong-answer class this repository treats as its worst failure.

So these are `terminal/create`, `terminal/output`, `terminal/wait_for_exit`, `terminal/kill` and `terminal/release` under names MCP's own rules allow, over one implementation with the ACP client — internal/termhost — rather than a second copy of the verbs.

Underscores rather than the dots the naming rules also permit. Dots are legal in this revision and were not always, and a client that rejects a tool name it cannot parse rejects it silently, leaving a server that connects and serves nothing. Underscores have never not been allowed.

View Source
const (
	MethodDiscover    = "server/discover"
	MethodInitialize  = "initialize"
	MethodPing        = "ping"
	MethodToolsList   = "tools/list"
	MethodToolsCall   = "tools/call"
	MethodInitialized = "notifications/initialized"
	// The protocol spells this with two Ls; it is a wire value and not prose,
	// so it stays as the schema has it.
	MethodCancelled = "notifications/cancelled" //nolint:misspell // the protocol's spelling
	MethodProgress  = "notifications/progress"
)

The methods this server answers. The notifications are taken and dropped; everything else is refused by name.

View Source
const (
	MetaProtocolVersion = "io.modelcontextprotocol/protocolVersion"
	MetaServerInfo      = "io.modelcontextprotocol/serverInfo"
)

The `_meta` keys the revision reserves that this server reads or writes. They are namespaced and the namespace is part of the key, so they are written out once here rather than composed at each use.

`io.modelcontextprotocol/clientInfo` is the third of the trio and is not here: a modern request carries it, and this server does nothing with what a client calls itself. The revision says as much about the mirror-image field — serverInfo is self-reported, for display and logging, and a client should not change its behavior on it.

View Source
const CodeUnsupportedProtocolVersion = -32022

CodeUnsupportedProtocolVersion is what a modern request naming a revision this server does not speak is answered with. It is the revision's own code, in the application range JSON-RPC reserves, and the client is expected to retry with something from the `supported` list rather than to give up.

View Source
const Instructions = "This is a shell. " +
	"Starting a command returns a terminal id rather than the command's output: " +
	"call " + ToolWait + " to wait for it, " + ToolOutput + " to read what it wrote, " +
	ToolKill + " to stop it and " + ToolRelease + " when you are finished with it. " +
	"Pass a progress token on " + ToolWait + " to be sent output as it is produced. " +
	"Commands run under whatever policy this shell was started with, and one may be refused."

Instructions is what a client tells a model about this server.

It says the shape rather than the verbs, because the verbs are in the tool descriptions and a model that reads both should not be told two versions of one thing. What it has to say is the part no single tool description can: a command is a handle, and reading its output is a second call.

View Source
const LegacyVersion = "2025-11-25"

LegacyVersion is the revision answered to a client that opens with the `initialize` handshake instead. See the package comment for the measurement that put it here, and for why it is not a hedge.

View Source
const ResultComplete = "complete"

ResultComplete is the discriminator every modern result carries. A legacy result carries none, which is why the field is omitempty and set per era rather than written into each constructor.

View Source
const Version = "2026-07-28"

Version is the protocol revision this server implements. Every shape in this file was taken from that revision's published specification.

Variables

This section is empty.

Functions

This section is empty.

Types

type CallToolRequest

type CallToolRequest struct {
	Name      string          `json:"name"`
	Arguments json.RawMessage `json:"arguments,omitempty"`
}

CallToolRequest is one tool invocation.

type CallToolResult

type CallToolResult struct {
	ResultType        string    `json:"resultType,omitempty"`
	Content           []Content `json:"content"`
	StructuredContent any       `json:"structuredContent,omitempty"`
	IsError           bool      `json:"isError,omitempty"`
}

CallToolResult is what a tool produced.

Content is what a model reads; StructuredContent is the same answer typed, against the tool's output schema. Both, because the revision says a tool that returns structured content should also return the serialized JSON in a text block, and because a client that validates the one still shows the other.

IsError is the *tool* failing rather than the protocol: a command refused by policy, a terminal id that is not there. Those are things a model can correct and retry, which is exactly the line the revision draws between a tool execution error and a JSON-RPC one.

type Capabilities

type Capabilities struct {
	Tools *ToolsCapability `json:"tools,omitempty"`
}

Capabilities is what this server claims. Tools and nothing else; see the package comment for why each absence is an absence rather than a gap.

type Content

type Content struct {
	Type string `json:"type"`
	Text string `json:"text,omitempty"`
}

Content is one piece of a tool's unstructured answer. Text is the only variant a shell produces: an image, an audio clip and an embedded resource are all things this server has nothing to put in.

func TextContent

func TextContent(text string) []Content

TextContent wraps text as tool content.

type CreateArgs

type CreateArgs struct {
	Command         string            `json:"command"`
	Args            []string          `json:"args,omitempty"`
	Cwd             string            `json:"cwd,omitempty"`
	Env             map[string]string `json:"env,omitempty"`
	OutputByteLimit *int              `json:"outputByteLimit,omitempty"`
}

CreateArgs is what `terminal_create` takes.

Command is a command *line* by default, as the shell would run one, and Args is there for a peer that means an argv. Which is which is the same rule the shared machinery states: no args means a line. See termhost.

type CreateResult

type CreateResult struct {
	TerminalID string `json:"terminalId"`
}

CreateResult names the terminal that was made.

type DiscoverResult

type DiscoverResult struct {
	ResultType        string         `json:"resultType"`
	SupportedVersions []string       `json:"supportedVersions"`
	Capabilities      Capabilities   `json:"capabilities"`
	Instructions      string         `json:"instructions,omitempty"`
	Meta              map[string]any `json:"_meta,omitempty"`
}

DiscoverResult answers `server/discover`: what this server speaks, what it can do, and what it is.

The revision requires every server to implement this, and it is also the stdio backward-compatibility probe — a dual-era client sends it first and falls back to `initialize` on any error that is not a modern one. So a server that answered it with "method not found" would be telling every modern client that it is a legacy server.

type Implementation

type Implementation struct {
	Name    string `json:"name"`
	Title   string `json:"title,omitempty"`
	Version string `json:"version"`
}

Implementation names a side of the connection.

type InitializeRequest

type InitializeRequest struct {
	ProtocolVersion string          `json:"protocolVersion"`
	Capabilities    json.RawMessage `json:"capabilities,omitempty"`
	ClientInfo      *Implementation `json:"clientInfo,omitempty"`
}

InitializeRequest is how a legacy client opens.

Capabilities is kept as raw JSON for the reason ACP keeps `mcpServers` that way: decoding a shape this server will not act on would be pretending to support it, and refusing a request that carries one would break every client that always sends it.

type InitializeResult

type InitializeResult struct {
	ProtocolVersion string         `json:"protocolVersion"`
	Capabilities    Capabilities   `json:"capabilities"`
	ServerInfo      Implementation `json:"serverInfo"`
	Instructions    string         `json:"instructions,omitempty"`
}

InitializeResult is the legacy handshake's answer. No resultType: that field belongs to the modern era and a legacy client validating against its own schema has no place to put it.

type ListToolsResult

type ListToolsResult struct {
	ResultType string `json:"resultType,omitempty"`
	Tools      []Tool `json:"tools"`
}

ListToolsResult answers `tools/list`.

type ProgressNotification

type ProgressNotification struct {
	ProgressToken json.RawMessage `json:"progressToken"`
	Progress      float64         `json:"progress"`
	Total         *float64        `json:"total,omitempty"`
	Message       string          `json:"message,omitempty"`
}

ProgressNotification is what a long-running call reports while it runs.

This is the streaming answer, and it is the whole of it: a command is a handle, and what it writes reaches the client here rather than in the result of the call that started it. The revision requires Progress to increase with every notification, which is why it counts bytes the command has *ever* written rather than what is currently retained — a buffer with a limit on it shrinks, and a progress value may not.

type Property

type Property struct {
	Type        any       `json:"type"`
	Description string    `json:"description,omitempty"`
	Items       *Property `json:"items,omitempty"`
}

Property is one field of a schema.

Type is `any` because a nullable field is spelled as a list of two type names, and an exit status that is genuinely null half the time is the whole reason this server has an output schema at all.

type Schema

type Schema struct {
	Type                 string              `json:"type"`
	Properties           map[string]Property `json:"properties,omitempty"`
	Required             []string            `json:"required,omitempty"`
	AdditionalProperties *bool               `json:"additionalProperties,omitempty"`
}

Schema is the part of JSON Schema a tool of ours needs. The revision defaults an unmarked schema to draft 2020-12, which is what these are.

type Server

type Server struct {
	// Info is what this server calls itself to a client.
	Info Implementation
	// contains filtered or unexported fields
}

Server answers MCP on one connection.

func NewServer

func NewServer(sh driver.Shell, info Implementation) *Server

NewServer builds one on a shell template.

The Shell is the invocation's own — its dialect, its gate, its event sink, its startup files — so a `--policy` on the same command line governs every command a client asks for, exactly as it governs a script. That is the whole of why an MCP server closes the gap #1334 records: an MCP server is configured with a *user-controlled command line*, where `$SHELL -c` has nowhere to put a flag.

func (*Server) Close

func (s *Server) Close()

Close ends every terminal this server is still holding.

func (*Server) Handle

func (s *Server) Handle(ctx context.Context, method string, params json.RawMessage) (any, error)

Handle answers a request.

There is no "initialize first" check, and its absence is the revision rather than an omission: a modern request is self-contained and carries its own version, so refusing one for arriving before a handshake would be enforcing a handshake that no longer exists. A legacy client sends `initialize` first because its own revision tells it to.

func (*Server) Notify

func (s *Server) Notify(_ context.Context, method string, _ json.RawMessage)

Notify takes a notification, which has no answer and cannot fail.

Both of the two that arrive are taken and dropped, and that is the honest state rather than a stub. `notifications/initialized` is a legacy client saying the handshake is over, and there is nothing this server was waiting to do. The cancellation notification names a request id, and this transport hands a handler the *connection's* context rather than one per request — so there is nothing here that could be canceled, and a client that wants a command stopped has a tool for exactly that. See docs/design/mcp.md.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context, r io.Reader, w io.Writer) error

Serve reads MCP on r, writes it on w, and returns when the input ends.

r and w are the process's standard input and output when this is a binary, and they are not the shell's: the revision says a server must write nothing to standard output that is not a protocol message, and a shell's whole job is writing to standard output. What a command writes is held in its terminal and reaches the client as a tool result or as a progress notification.

type TerminalArgs

type TerminalArgs struct {
	TerminalID string `json:"terminalId"`
}

TerminalArgs is what every tool after create takes: which terminal.

type Tool

type Tool struct {
	Name         string  `json:"name"`
	Title        string  `json:"title,omitempty"`
	Description  string  `json:"description"`
	InputSchema  Schema  `json:"inputSchema"`
	OutputSchema *Schema `json:"outputSchema,omitempty"`
}

Tool is one verb, as the client is told about it.

func Tools

func Tools() []Tool

Tools is what this server advertises, in a fixed order.

Deterministic, which the revision asks for so that a client can cache the list: the order is the order a command is driven in, which is also the order somebody reading it would want.

type ToolsCapability

type ToolsCapability struct {
	ListChanged bool `json:"listChanged,omitempty"`
}

ToolsCapability says whether the tool list changes. Ours does not — the five verbs are the five verbs — so listChanged is false and no subscription is served.

Jump to

Keyboard shortcuts

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