protocol

package
v0.7.1 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package protocol is the module-internal boundary between transports and the client. It is the only package (besides pkg/transport/*) allowed to import the MCP go-sdk.

Its job is to convert SDK values into the module-neutral types declared here, so that no SDK type ever reaches a pkg/... exported API. Everything crossing this boundary comes from an untrusted server: conversions bound it (see Bounds) before retaining it, copy every slice and map they keep, and never panic on a malformed or nil input.

This package must not import pkg/client: the client maps its own Limits into the narrow Bounds view below and passes it down.

Index

Constants

View Source
const (
	// WhatElicitMessageBytes is reported when a server's elicitation prompt
	// exceeds Bounds.MaxElicitMessageBytes.
	WhatElicitMessageBytes = "elicit_message_bytes"
	// WhatElicitSchemaBytes is reported when a server's requested schema
	// exceeds Bounds.MaxElicitSchemaBytes.
	WhatElicitSchemaBytes = "elicit_schema_bytes"
)

The limits.OverLimitError.What values this file reports.

View Source
const (
	KindText         = "text"
	KindImage        = "image"
	KindAudio        = "audio"
	KindResource     = "resource"
	KindResourceLink = "resource_link"
	KindToolUse      = "tool_use"
	KindToolResult   = "tool_result"
	KindUnknown      = "unknown"
)

Values for UnsupportedContent.Kind. They mirror the MCP wire "type" discriminator, with KindUnknown for a content type this SDK version returns but this module does not know.

View Source
const MaxCursorBytes = 4 << 10

MaxCursorBytes bounds an opaque pagination cursor.

A cursor is server-supplied, retained for the whole fetch (every cursor seen is kept, to detect a cycle), and handed straight back on the next request. It is the one value in a page that this module stores per round trip rather than per item, so an unbounded one is a way to make a bounded number of pages cost an unbounded amount of memory. 4 KiB is far beyond any real cursor, which is typically an offset or an opaque ID.

View Source
const MaxDefectBytes = 256

MaxDefectBytes bounds a recorded defect reason. The text comes from an error this package produced, but it can quote a server's own bytes (a JSON decoder naming the offending token), so it is bounded like anything else that came from a peer.

View Source
const MaxPromptArgs = 64

MaxPromptArgs caps how many arguments one prompt may declare.

A prompt's argument list is server-chosen and retained in a catalog for the life of a connection, so it needs a ceiling for the same reason the catalog itself does. The number is set where a prompt stops being one a person could fill in: past this, the server is not describing a prompt.

View Source
const MaxRoots = 64

MaxRoots caps how many roots one client will advertise, so a misbehaving provider cannot hand an unbounded set to the SDK. A root without a URI has no identity and is dropped rather than sent; the cap bounds what remains.

View Source
const MaxWarnings = 8

MaxWarnings caps the Warnings a single conversion may report, so a hostile server cannot turn tolerated defects into unbounded memory.

Per-item messages beyond the cap are discarded, but the drops they described are not hidden: convertItems spends the last slot on a summary carrying the true total. The cap bounds how much is said, never whether it is said.

View Source
const WhatSampleTextBytes = "sample_text_bytes"

WhatSampleTextBytes is the limits.OverLimitError.What this file reports when a sampling conversation's text exceeds Bounds.MaxTextBytes.

View Source
const WhatSchemaBytes = "schema_bytes"

WhatSchemaBytes is the limits.OverLimitError.What reported when a schema document exceeds Bounds.MaxSchemaBytes.

View Source
const WhatStructuredBytes = "structured_bytes"

WhatStructuredBytes is the limits.OverLimitError.What reported when a structured tool result exceeds Bounds.MaxStructuredBytes.

Variables

This section is empty.

Functions

func ToSDKCreateMessageResult

func ToSDKCreateMessageResult(r SampleResult, b Bounds) (*mcp.CreateMessageResult, error)

ToSDKCreateMessageResult converts the host's completion into the SDK's result.

The output is bounded too, and against the same budget as the input. This is the host's own text rather than a server's, so it is not untrusted — but a bound that only ever runs one way is not a bound on the conversation, and the server on the other end of this reply is entitled to the same protection from an unbounded payload that this module gives itself.

func ToSDKElicitResult

func ToSDKElicitResult(r ElicitResult) (*mcp.ElicitResult, error)

ToSDKElicitResult converts an answer into the SDK's result.

The Content conversion to map[string]any is this module's only elicitation `any`: it is the SDK's wire type for the JSON object the schema describes, and this is the serialization boundary where such a narrowing belongs. Handing the SDK the object (rather than the raw bytes) is also what lets it validate the answer against the requested schema before it reaches the server — a check this module wants and does not have to write.

Types

type AudioContent

type AudioContent struct {
	Data     []byte
	MIMEType string
}

AudioContent is an audio payload within Bounds.MaxBinaryItemBytes.

type Bounds

type Bounds struct {
	// MaxSchemaBytes caps one marshaled tool schema document.
	MaxSchemaBytes int
	// MaxSchemaDepth caps schema nesting.
	MaxSchemaDepth int
	// MaxTextBytes caps one text (or embedded-resource text) payload before
	// it is truncated.
	MaxTextBytes int
	// MaxStructuredBytes caps one structured-content document, enforced by
	// FromSDKCallToolResult.
	MaxStructuredBytes int
	// MaxBinaryItemBytes caps one binary (image/audio/blob) item.
	MaxBinaryItemBytes int
	// MaxBinaryItems caps how many binary items one content list may carry.
	MaxBinaryItems int
	// MaxLogBytes caps one server log message's text.
	MaxLogBytes int
	// MaxElicitMessageBytes caps one elicitation prompt: its message, and in
	// url mode the URL and correlation id shown with it. It is separate from
	// MaxTextBytes because it bounds what a *person* is shown, not what a model
	// consumes — a tool result may reasonably be a megabyte; a prompt may not.
	MaxElicitMessageBytes int
	// MaxElicitSchemaBytes caps one elicitation's requested schema. It is
	// separate from MaxSchemaBytes because a form a human fills in is a
	// different thing from a tool's interface; sizing them together would mean
	// raising one to raise the other.
	MaxElicitSchemaBytes int
}

Bounds is the narrow limit view this package needs — the subset of the client's Limits that applies to converting SDK values. The client passes a normalized (all-positive) value; a non-positive field is not "unbounded" but fails closed, rejecting or truncating everything it governs.

type CallOptions

type CallOptions struct {
	// Progress, when non-nil, receives the call's progress notifications. It is
	// invoked on the connection's notification goroutine and blocks it, so it
	// must not do work.
	//
	// Registering it is what puts a progress token on the request; a server is
	// only allowed to send progress for a request that carries one, so a nil
	// Progress means the notifications are never generated in the first place.
	Progress func(ProgressUpdate)
}

CallOptions are the per-call knobs a Conn honors. Deadlines are deliberately absent: a deadline is a ctx, and a second way to spell one is a second thing to get wrong.

type ClientCapabilities

type ClientCapabilities struct {
	Elicitation bool
	Sampling    bool
	Roots       bool
}

ClientCapabilities are the optional client-side capabilities to advertise on a connection. The client only sets a field when the application both asked for the capability and installed a handler able to serve it, so a transport may advertise these verbatim.

type ClientIdentity

type ClientIdentity struct {
	Name    string
	Version string
	Title   string
}

ClientIdentity is what this client tells a server it is. It is cosmetic and carries no authority; it must never carry a credential.

type Conn

type Conn interface {
	// Initialize performs the MCP handshake and reports what the server said
	// about itself. Everything in the result is server-supplied and untrusted;
	// implementations bound it against the ConnectConfig Bounds before
	// returning it.
	Initialize(ctx context.Context) (InitializeResult, error)

	// ListTools fetches one page of tools. cursor is empty for the first page,
	// and otherwise the preceding page's NextCursor. Paginating — and bounding
	// the pagination — is the caller's job; see internal/catalog.
	ListTools(ctx context.Context, cursor string) (ToolPage, error)
	// ListPrompts fetches one page of prompts.
	ListPrompts(ctx context.Context, cursor string) (PromptPage, error)
	// ListResources fetches one page of concrete resources.
	ListResources(ctx context.Context, cursor string) (ResourcePage, error)
	// ListResourceTemplates fetches one page of resource templates.
	ListResourceTemplates(ctx context.Context, cursor string) (ResourceTemplatePage, error)

	// CallTool invokes a tool by its raw server name. args is passed through
	// verbatim; validating it against the tool's schema is the caller's job.
	// Cancelling ctx cancels the call at the protocol level.
	CallTool(ctx context.Context, rawName string, args json.RawMessage, opts CallOptions) (ToolResult, error)
	// GetPrompt fetches a prompt's messages.
	GetPrompt(ctx context.Context, name string, args map[string]string) (PromptResult, error)
	// ReadResource reads a resource by its opaque URI.
	ReadResource(ctx context.Context, uri string) (ResourceResult, error)
	// Subscribe asks the server to report changes to a resource.
	Subscribe(ctx context.Context, uri string) error
	// Unsubscribe asks the server to stop reporting changes to a resource.
	Unsubscribe(ctx context.Context, uri string) error
	// SetLogLevel asks the server to send logs at or above level. A server
	// sends none until this is called.
	SetLogLevel(ctx context.Context, level string) error

	// Close releases the connection's resources.
	Close(ctx context.Context) error
}

Conn is an established connection to an MCP server. A Conn is transport- agnostic: everything it returns has already been converted to the neutral, bounded types in this package, so no caller above it ever names an SDK type.

A Conn is single-use for Initialize: the MCP handshake happens once per connection. Close is idempotent from the caller's point of view — the client guarantees it calls it at most once — and must never panic on a connection that was never initialized. A Conn's request methods must only be called between a successful Initialize and Close; they report an error otherwise rather than panicking. None of them checks whether the server advertised the capability behind the method — that is the caller's decision (see the design's compatibility rule) and this interface would have to guess at a policy to enforce it.

type ConnectConfig

type ConnectConfig struct {
	// Client identifies this client to the server.
	Client ClientIdentity
	// Capabilities are the client capabilities to advertise.
	Capabilities ClientCapabilities
	// Bounds caps everything the connection converts from server data. The
	// client passes a normalized (all-positive) value.
	Bounds Bounds
	// Wire caps what a transport buffers off the network, before any of it is
	// parsed. The client passes a normalized (all-positive) value.
	Wire WireLimits
	// OnLog receives the server's log messages, already bounded. Nil drops
	// them.
	//
	// It is a callback on the config rather than a method on Conn because a log
	// is unsolicited: it belongs to no request, so there is nothing to return
	// it from. It is invoked on the connection's notification goroutine and
	// blocks it, so an implementation must not do work.
	OnLog func(LogRecord)

	// OnListChanged receives the server's list-change notifications. Nil drops
	// them.
	//
	// Like OnLog it is a callback rather than a method for want of a request to
	// return it from, and it is invoked on the connection's notification
	// goroutine and blocks it: an implementation must record the change and
	// return, never fetch.
	OnListChanged func(ListChange)

	// OnResourceUpdated receives the server's resource-update notifications: a
	// server telling a subscriber that a resource it subscribed to has changed.
	// Nil drops them.
	//
	// Like OnListChanged it is a callback for want of a request to return it
	// from, and it is invoked on the connection's notification goroutine and
	// blocks it: an implementation must record the update and return, never
	// re-read the resource here. The URI it carries is server-supplied and
	// bounded before it arrives.
	OnResourceUpdated func(ResourceUpdate)

	// OnElicit serves the server's requests for human input, already bounded
	// and validated. Nil means no elicitation is served — and, because a
	// capability with nothing behind it must never reach the wire, nil also
	// means the elicitation capability is not advertised however Capabilities
	// is set (see Session.Initialize).
	//
	// Unlike OnLog and OnListChanged this one answers: it is the only callback
	// here whose return value goes to the server, and the only one that may
	// block. It is invoked on the connection's request-dispatch goroutine with
	// the caller's elicitation deadline already on ctx; returning an error
	// refuses the request, which is a complete and honest answer.
	//
	// It is a callback rather than a Conn method for the same reason as the
	// others: it belongs to no request this module made, so there is nothing to
	// return it from.
	OnElicit func(context.Context, ElicitRequest) (ElicitResult, error)

	// OnSample serves the server's requests for an LLM completion, already
	// bounded and validated. Nil means no sampling is served — and, because a
	// capability with nothing behind it must never reach the wire, nil also
	// means the sampling capability is not advertised however Capabilities is
	// set (see Session.Initialize).
	//
	// Like OnElicit it answers, and its return value goes to the server. It is
	// invoked on the connection's request-dispatch goroutine with the caller's
	// deadline already on ctx; returning an error refuses the request, which a
	// host is always entitled to do — sampling spends the host's money, and
	// "no" is a complete answer.
	OnSample func(context.Context, SampleRequest) (SampleResult, error)

	// OnRoots supplies the filesystem roots this client exposes to the server.
	// Nil means no roots are served — and, because a capability with nothing
	// behind it must never reach the wire, nil also means the roots capability
	// is not advertised however Capabilities is set (see Session.Initialize).
	//
	// Unlike the other callbacks it is not a server request handler: the SDK
	// answers roots/list from a set the client supplies, so this is consulted
	// once, at Initialize, to populate that set. The roots it returns are the
	// only ones a server ever learns — this module never invents host
	// filesystem roots of its own. Returning an error fails the handshake:
	// establishing a binding that advertises roots it cannot determine would be
	// advertise-without-honor, the very thing the gating exists to prevent.
	OnRoots func(context.Context) ([]Root, error)
}

ConnectConfig carries the client-side connection parameters a transport needs. It is secret-free by construction: credentials reach a transport through its own configuration, never through this struct.

type Content

type Content interface {
	// contains filtered or unexported methods
}

Content is a sealed union of the neutral content kinds. Only this package can add a member; callers exhaust it with a type switch and must handle UnsupportedContent, which is the default for anything else.

func FromSDKContent

func FromSDKContent(c mcp.Content, b Bounds) (Content, error)

FromSDKContent converts one content item.

A content kind this module does not model — resource links and the sampling-only tool_use/tool_result kinds today, anything new the SDK grows tomorrow — converts to UnsupportedContent rather than an error: one exotic item must not fail a whole result, and it must not vanish silently either. Errors are reserved for content that is structurally broken (nil, or an embedded resource with no contents).

Binary items over Bounds.MaxBinaryItemBytes also become UnsupportedContent, carrying the size that was refused. The oversized bytes are never retained.

func FromSDKContents

func FromSDKContents(cs []mcp.Content, b Bounds) ([]Content, error)

FromSDKContents converts a content list, additionally enforcing Bounds.MaxBinaryItems across the binary items in it. Items past the budget become UnsupportedContent, so the list keeps its length and position: a caller can always tell which item it lost.

Only items actually retained spend the budget. An item already refused on size does not, or a single oversized image would evict a later good one.

type ElicitAction

type ElicitAction uint8

ElicitAction is how a human answered. The zero value is not a valid action: a caller must say what happened, because "nothing" is not an answer this module may put on the wire.

const (
	// ElicitAccept means the human supplied the requested content.
	ElicitAccept ElicitAction = iota + 1
	// ElicitDecline means the human refused.
	ElicitDecline
	// ElicitCancel means the human dismissed the request without deciding.
	ElicitCancel
)

The elicitation outcomes, per the MCP elicitation result actions.

func (ElicitAction) String

func (a ElicitAction) String() string

String returns the action's wire identifier, or "unknown".

type ElicitMode

type ElicitMode uint8

ElicitMode is which kind of elicitation a server sent. The zero value is not a mode: a request this module cannot classify is refused, never guessed.

const (
	// ElicitModeForm is a bounded schema of typed fields to fill in.
	ElicitModeForm ElicitMode = iota + 1
	// ElicitModeURL is an out-of-band action to perform at a URL.
	ElicitModeURL
)

The elicitation modes MCP defines.

func (ElicitMode) String

func (m ElicitMode) String() string

String returns the mode's wire identifier, or "unknown" for any value outside the declared range.

type ElicitRequest

type ElicitRequest struct {
	// Mode is the kind of elicitation. It is always a declared mode: an
	// unrecognized one is refused at conversion.
	Mode ElicitMode
	// Message is the prompt to show the human, within
	// Bounds.MaxElicitMessageBytes.
	Message string
	// Schema is the JSON Schema the answer must satisfy, in form mode only; nil
	// otherwise. It is raw JSON because a schema is a serialization-boundary
	// document, not domain data.
	Schema json.RawMessage
	// URL is the out-of-band action's URL, in url mode only; empty otherwise.
	URL string
	// ElicitationID is the server's correlation id, in url mode only. It may be
	// empty even there: MCP does not require one.
	ElicitationID string
}

ElicitRequest is a server's request for human input, bounded and detached from SDK memory. Every field is server-supplied and untrusted: it is content to show a person, never an instruction to the host.

func FromSDKElicitParams

func FromSDKElicitParams(params *mcp.ElicitParams, b Bounds) (ElicitRequest, error)

FromSDKElicitParams converts an elicitation/create request, bounding it.

It is exported separately from onElicit so a fuzzer can drive it with anything a server could send.

An over-bound message or schema is rejected outright rather than truncated, which is the opposite of what this package does to a server's log line or instructions — and deliberately. Those are read; this is *acted on*. A truncated prompt is a prompt whose meaning this module silently altered, and a human who accepts it has consented to something they were not shown. A truncated schema is not a schema at all.

type ElicitResult

type ElicitResult struct {
	Action ElicitAction
	// Content is the accepted answer as a JSON object, or nil. It is raw JSON
	// for the same reason Schema is: its shape is the server's schema, which is
	// data, so there is no Go type to narrow it to.
	Content json.RawMessage
}

ElicitResult is the human's answer, as the layer above produced it. Content is meaningful only when Action is ElicitAccept.

type EmbeddedResourceContent

type EmbeddedResourceContent struct {
	URI       string
	MIMEType  string
	Text      string
	Data      []byte
	Truncated bool
}

EmbeddedResourceContent is resource content inlined in a result. Exactly one of Text or Data is meaningful, per what the server sent.

type ImageContent

type ImageContent struct {
	Data     []byte
	MIMEType string
}

ImageContent is an image payload within Bounds.MaxBinaryItemBytes.

type InitializeResult

type InitializeResult struct {
	// Server is what the server claims to be.
	Server ServerIdentity
	// ProtocolVersion is the version the server wants to speak.
	ProtocolVersion ProtocolVersion
	// Instructions is the server's usage hint, truncated to
	// Bounds.MaxTextBytes.
	Instructions string
	// Capabilities is what the server advertised.
	Capabilities ServerCapabilities
}

InitializeResult is the outcome of the MCP handshake, bounded and detached from SDK memory. Every field is server-supplied: it describes a peer, it never authorizes one.

func FromSDKInitializeResult

func FromSDKInitializeResult(r *mcp.InitializeResult, b Bounds) (InitializeResult, error)

FromSDKInitializeResult converts the SDK's handshake result.

A missing protocol version is fatal: the whole point of the handshake is to learn which protocol the peer speaks, and a client that guesses one is a client that mis-parses everything afterwards. Everything else is tolerated — an anonymous server (no serverInfo) and one advertising no capabilities are both legal — because neither prevents the connection from working, and both are already visible to the caller in the converted result.

Every string the server supplies — its identity, the protocol version and the instructions — is truncated to b.MaxTextBytes rather than rejected. All three are retained for the life of the connection and rendered freely afterwards, so none may be unbounded; but a server padding one must not be able to fail the connection, only to have its padding dropped.

type ListChange

type ListChange struct {
	// Family names the list the server says changed.
	Family ListFamily
}

ListChange is a server's announcement that a catalog family changed.

It carries no content: MCP's list-change notifications say only that a list changed, never how. That is a property worth keeping rather than papering over — a client that acted on a server's account of its own delta would be trusting an untrusted peer's diff. The only safe response is to refetch the family and compare, which is what internal/catalog and the client do.

type ListFamily

type ListFamily uint8

ListFamily names the catalog family a list-change notification refers to. It is this package's own enum rather than internal/catalog's Family because catalog imports protocol, not the other way round; the client maps between them.

const (
	// ListFamilyTools is notifications/tools/list_changed.
	ListFamilyTools ListFamily = iota + 1
	// ListFamilyPrompts is notifications/prompts/list_changed.
	ListFamilyPrompts
	// ListFamilyResources is notifications/resources/list_changed.
	ListFamilyResources
)

The families a server can announce a change to. MCP defines one notification per family; there is none for resource templates, which travel with resources.

func (ListFamily) String

func (f ListFamily) String() string

String returns the family's stable identifier, or "unknown" for any value outside the declared range.

type LogRecord

type LogRecord struct {
	// Level is the severity the server claimed, verbatim.
	Level string
	// Logger is the server-side logger name, if it sent one.
	Logger string
	// Text is the message, truncated to Bounds.MaxLogBytes.
	Text string
}

LogRecord is one server log message, bounded. It is the neutral form of an MCP logging notification.

func FromSDKLogParams

func FromSDKLogParams(params *mcp.LoggingMessageParams, b Bounds) LogRecord

FromSDKLogParams converts a logging notification, bounding its text.

The MCP log payload is `any`: a server may log a string or an arbitrary JSON value. A string is used as-is and anything else is rendered as JSON, so that a structured log is readable rather than dropped. Either way the result is truncated to Bounds.MaxLogBytes — a log line is a diagnostic from an untrusted peer, not a data channel.

Nil params yield the zero LogRecord. The function reports no error — a log line has no failure a caller could act on — so nil cannot be an error return, and it must not be a panic: this converter is exported precisely so a fuzzer can drive it directly with anything a server could produce, and "the caller must nil-check first" is a contract every sibling converter here declines to rely on. It matches the treatment of a nil Data payload, which is likewise empty rather than fatal.

type ProgressUpdate

type ProgressUpdate struct {
	// Progress is how far the server claims to have got.
	Progress float64
	// Total is the server's claimed total, or 0 when it did not say.
	Total float64
	// Message is the server's bounded description of what it is doing.
	Message string
}

ProgressUpdate is one progress notification for an in-flight call. Every field is server-supplied: it is a hint to render, never a fact to act on.

type PromptArgSpec

type PromptArgSpec struct {
	Name        string
	Title       string
	Description string
	Required    bool
}

PromptArgSpec is one argument a prompt accepts.

type PromptMessage

type PromptMessage struct {
	Role    string
	Content Content
}

PromptMessage is one message of a prompt. Role is the server's string verbatim ("user" or "assistant" in practice); it is not narrowed to an enum because a prompt message is content to render, not a decision to route on, and an unknown role must not fail the fetch.

type PromptPage

type PromptPage struct {
	Prompts    []PromptSpec
	NextCursor string
	Warnings   []string
}

PromptPage is one page of prompts/list.

func FromSDKPromptPage

func FromSDKPromptPage(res *mcp.ListPromptsResult, b Bounds) (PromptPage, error)

FromSDKPromptPage converts a prompts/list result.

type PromptResult

type PromptResult struct {
	Description string
	Messages    []PromptMessage
}

PromptResult is the outcome of a prompts/get.

func FromSDKGetPromptResult

func FromSDKGetPromptResult(res *mcp.GetPromptResult, b Bounds) (PromptResult, error)

FromSDKGetPromptResult converts a prompts/get result.

type PromptSpec

type PromptSpec struct {
	RawName     string
	Title       string
	Description string
	Arguments   []PromptArgSpec
}

PromptSpec is a prompt as advertised by a server. RawName carries the same caveat as ToolSpec.RawName.

func FromSDKPrompt

func FromSDKPrompt(p *mcp.Prompt, b Bounds) (PromptSpec, error)

FromSDKPrompt converts an SDK prompt, copying the argument list.

Identifiers are bounded by REJECTION and prose by TRUNCATION, and the split is deliberate. A prompt's name and its arguments' names are used to address the thing later; a truncated identifier is a different identifier that still looks like one, so an over-long name drops the prompt (with a warning, via convertItems) rather than silently becoming a name the server never chose. Title and Description are cosmetic — they reach logs and UIs and nothing addresses them — so they truncate, for the same reason FromSDKServerIdentity's do: padding is not a reason to refuse an otherwise-working prompt.

The argument COUNT is bounded too. Bounding each string while accepting a million of them would be a bound in name only.

type ProtocolVersion

type ProtocolVersion string

ProtocolVersion is an MCP protocol version string as negotiated during initialize (e.g. "2025-06-18"). It is server-supplied and untrusted.

func (ProtocolVersion) Stateless

func (v ProtocolVersion) Stateless() bool

Stateless reports whether this revision speaks the stateless wire model. Version strings are ISO dates, so lexical comparison is date comparison. A value that is not date-shaped is treated as legacy: the string is server-supplied and untrusted, and the legacy path is the conservative one.

type ResourceContent

type ResourceContent struct {
	URI       string
	MIMEType  string
	Text      string
	Data      []byte
	Truncated bool
}

ResourceContent is one item of a resource's contents. Exactly one of Text or Data is meaningful, per what the server sent.

type ResourcePage

type ResourcePage struct {
	Resources  []ResourceSpec
	NextCursor string
	Warnings   []string
}

ResourcePage is one page of resources/list.

func FromSDKResourcePage

func FromSDKResourcePage(res *mcp.ListResourcesResult, b Bounds) (ResourcePage, error)

FromSDKResourcePage converts a resources/list result.

type ResourceResult

type ResourceResult struct {
	Contents []ResourceContent
}

ResourceResult is the outcome of a resources/read.

func FromSDKReadResourceResult

func FromSDKReadResourceResult(res *mcp.ReadResourceResult, b Bounds) (ResourceResult, error)

FromSDKReadResourceResult converts a resources/read result.

type ResourceSpec

type ResourceSpec struct {
	URI         string
	Name        string
	Title       string
	Description string
	MIMEType    string
}

ResourceSpec is a concrete resource a server exposes.

func FromSDKResource

func FromSDKResource(r *mcp.Resource, b Bounds) (ResourceSpec, error)

FromSDKResource converts an SDK resource. A resource without a URI cannot be read and is rejected.

Bounds are applied the same way as FromSDKPrompt's: the URI is the address a later read is issued to, so an over-long one is rejected rather than truncated into a URI naming something else. Name and MIMEType are rejected on the same grounds — both are matched against, not merely shown. Title and Description are prose and truncate.

type ResourceTemplatePage

type ResourceTemplatePage struct {
	Templates  []ResourceTemplateSpec
	NextCursor string
	Warnings   []string
}

ResourceTemplatePage is one page of resources/templates/list.

func FromSDKResourceTemplatePage

func FromSDKResourceTemplatePage(res *mcp.ListResourceTemplatesResult, b Bounds) (ResourceTemplatePage, error)

FromSDKResourceTemplatePage converts a resources/templates/list result.

type ResourceTemplateSpec

type ResourceTemplateSpec struct {
	URITemplate string
	Name        string
	Title       string
	Description string
	MIMEType    string
}

ResourceTemplateSpec is an RFC 6570 template for a family of resources.

func FromSDKResourceTemplate

func FromSDKResourceTemplate(rt *mcp.ResourceTemplate, b Bounds) (ResourceTemplateSpec, error)

FromSDKResourceTemplate converts an SDK resource template. The URI template is carried verbatim; expanding it safely is the caller's problem.

Bounded exactly as FromSDKResource is, and the template is an identifier for the same reason a URI is — it is expanded into one.

type ResourceUpdate

type ResourceUpdate struct {
	// URI names the resource the server says changed. It may be a sub-resource
	// of the one that was subscribed to.
	URI string
}

ResourceUpdate is a server's announcement that a subscribed resource changed.

It carries only the URI of the resource the server says changed, bounded before it reaches this type. Like a list-change notification it is a claim, not content: the resource's new value is not here, and a subscriber that wants it re-reads the resource, which is the only way to learn what changed from an untrusted peer rather than trusting its account of the delta.

type Root

type Root struct {
	URI  string
	Name string
}

Root is a filesystem root a client exposes to a server, neutral and bounded. Exposing one grants a server knowledge of a path, never access to it. URI is the root's canonical identity (a file:// URI); Name is an optional display name.

type SampleMessage

type SampleMessage struct {
	// Role is always a declared role: an unrecognized one is refused at
	// conversion.
	Role SampleRole
	// Text is the message's text content.
	Text string
}

SampleMessage is one turn of a sampling conversation, bounded and detached from SDK memory.

type SampleRequest

type SampleRequest struct {
	// SystemPrompt is the server's requested system prompt. The host may modify
	// or ignore it.
	SystemPrompt string
	// Messages is the conversation to complete. It is never empty: a request
	// with nothing to complete is refused at conversion.
	Messages []SampleMessage
	// MaxTokens is the server's requested completion budget, always positive.
	// It is a request, not a grant: the layer above caps it against its own
	// limit, and a server can only ever lower the ceiling, never raise it.
	MaxTokens int
}

SampleRequest is a server's request for an LLM completion, bounded and detached from SDK memory. Every field is server-supplied: it describes what a server would like completed, and authorizes nothing.

func FromSDKCreateMessageParams

func FromSDKCreateMessageParams(params *mcp.CreateMessageParams, b Bounds) (SampleRequest, error)

FromSDKCreateMessageParams converts a sampling/createMessage request, bounding it.

It is exported separately from onSample so a fuzzer can drive it with anything a server could send.

Over-bound text is rejected outright rather than truncated, for the reason FromSDKElicitParams gives about a prompt: this text is not read, it is *acted on*. A truncated conversation is a conversation whose meaning this module silently altered, and the completion of it would be the answer to a question nobody asked.

type SampleResult

type SampleResult struct {
	// Model names the model the host actually used, which need not be one the
	// server asked for.
	Model string
	// Text is the completion.
	Text string
	// StopReason is why generation stopped.
	StopReason string
}

SampleResult is the completion the host produced.

type SampleRole

type SampleRole uint8

SampleRole is who authored a sampling message. The zero value is not a role: a message this module cannot attribute is refused, never guessed.

const (
	// SampleRoleUser is a message from the user's side of the conversation.
	SampleRoleUser SampleRole = iota + 1
	// SampleRoleAssistant is a message from the model's side.
	SampleRoleAssistant
)

The sampling message roles MCP defines.

func (SampleRole) String

func (r SampleRole) String() string

String returns the role's wire identifier, or "unknown" for any value outside the declared range.

type ServerCapabilities

type ServerCapabilities struct {
	// Tools reports whether the server exposes tools.
	Tools bool
	// Prompts reports whether the server exposes prompts.
	Prompts bool
	// Resources reports whether the server exposes resources.
	Resources bool
	// ResourcesSubscribe reports whether the server supports subscribing to
	// resource updates. It is only meaningful when Resources is set.
	ResourcesSubscribe bool
	// Logging reports whether the server sends log messages.
	Logging bool
	// Completions reports whether the server supports argument autocompletion.
	Completions bool
}

ServerCapabilities is what a server advertised at initialize, reduced to the presence flags this module acts on. The SDK models each capability as a nillable struct whose only members are listChanged-style notification flags; those are not modelled here until a task consumes them.

type ServerIdentity

type ServerIdentity struct {
	Name    string
	Version string
	Title   string
}

ServerIdentity is what a server claims to be. Every field is server-supplied and cosmetic: it names a peer, it never authorizes one.

func FromSDKServerIdentity

func FromSDKServerIdentity(impl *mcp.Implementation, b Bounds) ServerIdentity

FromSDKServerIdentity converts the server's self-description. A nil Implementation yields the zero identity rather than an error: identity here is cosmetic, and an anonymous server is not a protocol violation.

Every field is truncated to b.MaxTextBytes. An identity is the one piece of server data this module keeps for the whole life of a connection and renders freely — it reaches logs, telemetry and UIs through client.Status — so a server must not be able to make its own name a memory or log-volume problem. Truncation rather than rejection, for the same reason as Instructions: a padded name is not a reason to refuse an otherwise-working server.

type Session

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

Session is a Conn backed by the MCP Go SDK. It is created unconnected: the handshake — which the SDK performs as part of its own Connect — happens in Initialize, so that a transport can establish its byte stream (and fail on its own terms) before any protocol traffic is attempted.

A Session is safe for concurrent use.

func NewSession

func NewSession(t mcp.Transport, cfg ConnectConfig) *Session

NewSession returns an uninitialized Session that will speak MCP over t. t must be a transport the SDK has not connected yet: the SDK connects each transport exactly once, from Initialize.

func (*Session) CallTool

func (s *Session) CallTool(ctx context.Context, rawName string, args json.RawMessage, opts CallOptions) (ToolResult, error)

CallTool invokes a tool by its raw server name.

args is raw JSON because tool arguments are a serialization-boundary document: their shape is the server's input schema, which is data, so there is no Go type to narrow them to. They are passed through untouched — validating them against the schema is the caller's job, and doing it here would mean this layer had an opinion about a schema it did not fetch.

Cancelling ctx cancels the call at the protocol level: the SDK sends notifications/cancelled and stops waiting. The server is told, rather than merely abandoned.

func (*Session) Close

func (s *Session) Close(ctx context.Context) error

Close ends the MCP conversation. The SDK's session close is graceful — it stops accepting new requests and waits for in-flight ones to return before tearing the connection down — which is the whole reason a transport must route its shutdown through here before it touches the underlying stream. A stream yanked from under a pending request loses the reply and makes the peer exit on a read error rather than a clean stop.

ctx bounds the wait, so a peer that will not drain cannot block shutdown forever; the transport's own teardown (terminate, reap) is what makes that case terminal. Closing a Session that was never initialized is a no-op.

func (*Session) GetPrompt

func (s *Session) GetPrompt(ctx context.Context, name string, args map[string]string) (PromptResult, error)

GetPrompt fetches a prompt's messages, substituting args.

args is map[string]string rather than a typed struct because that is exactly what MCP defines a prompt's arguments to be: a flat string map, whose keys are named by the prompt's own declared arguments. There is no domain type here to narrow to.

func (*Session) Initialize

func (s *Session) Initialize(ctx context.Context) (InitializeResult, error)

Initialize performs the MCP handshake and converts the result.

Errors here are deliberately untyped: this package has no error taxonomy of its own and must not import pkg/client. The transport that owns the Session classifies the failure, because only it can tell "the server spoke badly" from "the process died" — a distinction the SDK reports identically, as a closed connection.

func (*Session) ListPrompts

func (s *Session) ListPrompts(ctx context.Context, cursor string) (PromptPage, error)

ListPrompts fetches one page of the server's prompts.

func (*Session) ListResourceTemplates

func (s *Session) ListResourceTemplates(ctx context.Context, cursor string) (ResourceTemplatePage, error)

ListResourceTemplates fetches one page of the server's resource templates.

func (*Session) ListResources

func (s *Session) ListResources(ctx context.Context, cursor string) (ResourcePage, error)

ListResources fetches one page of the server's concrete resources.

func (*Session) ListTools

func (s *Session) ListTools(ctx context.Context, cursor string) (ToolPage, error)

ListTools fetches one page of the server's tools. cursor is empty for the first page, and otherwise the NextCursor of the preceding one.

func (*Session) ReadResource

func (s *Session) ReadResource(ctx context.Context, uri string) (ResourceResult, error)

ReadResource reads a resource by URI. The URI is opaque: it is a protocol identifier the server issued, not a host path, and nothing here resolves it.

func (*Session) SetLogLevel

func (s *Session) SetLogLevel(ctx context.Context, level string) error

SetLogLevel asks the server to send log messages at or above level.

It is required, not optional: an MCP server sends nothing until the client sets a level, so a client that installs a log handler and never calls this receives silence and cannot tell it from a quiet server.

The level is also remembered on the session (see withLogLevel): on a peer negotiating protocol >= 2026-07-28, the legacy logging/setLevel RPC this method sends is not enough on its own to keep the promise above — see withLogLevel's comment for why.

func (*Session) Subscribe

func (s *Session) Subscribe(ctx context.Context, uri string) error

Subscribe asks the server to notify this connection when a resource changes.

Whether the server supports subscribing at all is the caller's check: this method issues the request, it does not gate it.

func (*Session) Unsubscribe

func (s *Session) Unsubscribe(ctx context.Context, uri string) error

Unsubscribe asks the server to stop reporting changes to a resource. It is the counterpart to Subscribe: after it returns, the server sends no further resource-update notifications for uri.

type TextContent

type TextContent struct {
	Text      string
	Truncated bool
}

TextContent is a text payload, truncated to Bounds.MaxTextBytes. Truncation is normal, not an error: Truncated says whether it happened.

type ToolAnnotations

type ToolAnnotations struct {
	Title           string
	ReadOnlyHint    bool
	IdempotentHint  bool
	DestructiveHint *bool
	OpenWorldHint   *bool
}

ToolAnnotations mirrors the server's behavioural hints. The tri-state hints are pointers because "unspecified" differs from "false": absent means the server said nothing, and the caller applies its own default.

type ToolPage

type ToolPage struct {
	Tools []ToolSpec
	// NextCursor is the cursor for the following page, empty when this is the
	// last one. It is opaque: nothing in this module interprets it.
	NextCursor string
	// Warnings records items dropped during conversion, bounded by MaxWarnings.
	Warnings []string
}

ToolPage is one page of tools/list.

func FromSDKToolPage

func FromSDKToolPage(res *mcp.ListToolsResult, b Bounds) (ToolPage, error)

FromSDKToolPage converts a tools/list result.

It is separate from ListTools — which only has to fetch — because this is the half that touches server data, and it is the half worth driving directly: a fuzzer can hand it any page a server could produce, where reaching the same code through a live session would require a hostile server to exist first.

type ToolResult

type ToolResult struct {
	// IsError reports that the tool itself failed. Content carries the tool's
	// explanation.
	IsError bool
	// Content is the unstructured result, already bounded.
	Content []Content
	// Structured is the optional structured result, within
	// Bounds.MaxStructuredBytes. Nil when the server sent none, or when an
	// over-bound one was dropped (see Warnings).
	Structured json.RawMessage
	// Warnings records defects tolerated during conversion.
	Warnings []string
}

ToolResult is the outcome of a tools/call, bounded and detached from SDK memory.

IsError carries the MCP protocol-level tool error, which is emphatically not a transport failure: the call succeeded, and the tool reported that whatever it was asked to do did not work. The design requires that distinction survive all the way to the model — an expected remote failure becomes a structured result, not a control-plane error — so it is a field here rather than an error return.

func FromSDKCallToolResult

func FromSDKCallToolResult(res *mcp.CallToolResult, b Bounds) (ToolResult, error)

FromSDKCallToolResult converts a tools/call result.

It is exported separately from CallTool so a fuzzer can drive it with any result a server could return.

type ToolSpec

type ToolSpec struct {
	RawName      string
	Title        string
	Description  string
	InputSchema  json.RawMessage
	OutputSchema json.RawMessage
	// Annotations are server-declared behavioural hints. They are untrusted
	// policy *input* and never authority: a server claiming ReadOnlyHint does
	// not make a tool read-only. Nil when the server sent none.
	Annotations *ToolAnnotations
	// Warnings records defects tolerated during conversion (e.g. a dropped
	// output schema). Bounded by MaxWarnings.
	Warnings []string
	// OutputSchemaDefect is the bounded reason this tool's optional output
	// schema was dropped, or empty when there was nothing wrong with it (which
	// includes the ordinary case of a server sending none at all).
	//
	// It exists so that dropping the schema is a *decision* the layer above can
	// make rather than one this conversion makes for it. The drop is the safe
	// tolerance, but whether it is applied is compatibility policy — a strict
	// profile rejects the tool instead — and policy cannot be expressed by
	// pattern-matching the text of a warning.
	OutputSchemaDefect string
}

ToolSpec is a tool as advertised by a server, bounded and detached from SDK memory. RawName is exactly what the server sent — unvalidated, unnamespaced, and not safe to use as an identifier until the client has qualified it.

func FromSDKTool

func FromSDKTool(t *mcp.Tool, b Bounds) (ToolSpec, error)

FromSDKTool converts an SDK tool, enforcing the schema bounds before retaining anything.

The two schemas are deliberately asymmetric, because only one of them is load-bearing. An input schema constrains the arguments a model may send, so a missing, malformed, non-object, or over-bound one is an error: the tool is rejected rather than exposed with unconstrained arguments (widening a schema is never a safe fallback).

An output schema is optional and only describes what comes back, so any defect in one — malformed, non-object, or over a bound — is tolerated identically: the schema is dropped, OutputSchema is left nil, and the reason is reported in Warnings. Failing the tool instead would let a server make an otherwise-usable tool unavailable by padding an optional field, while protecting nothing that dropping does not already protect.

type UnsupportedContent

type UnsupportedContent struct {
	// Kind is one of the Kind* constants.
	Kind string
	// Bytes is the size of the payload that was refused. It is exact for an
	// item dropped on a size bound (image, audio, embedded blob), and a
	// diagnostic lower bound for a kind dropped as unsupported, whose size is
	// summed from its known fields rather than by marshalling data we are
	// refusing. Treat it as a diagnostic, never as an accounting figure.
	Bytes int
}

UnsupportedContent stands in for content this boundary will not retain: a kind the module does not model, or a payload over a bound. It records only bounded metadata, so an unusable item is always visible to the caller rather than silently disappearing.

type WireLimits

type WireLimits struct {
	// MaxBodyBytes caps one whole non-streaming response body.
	//
	// It deliberately does not cap a long-lived stream: an SSE stream carries
	// an entire session's worth of frames and is bounded per frame, by
	// MaxFrameBytes, because a total on it is just a slow session's expiry
	// date.
	MaxBodyBytes int
	// MaxFrameBytes caps one wire frame — one JSON-RPC message, or one SSE
	// event — however long the stream carrying it lives.
	MaxFrameBytes int
}

WireLimits bounds untrusted bytes at the point they arrive, which is a different job from Bounds and is why it is a different type. Bounds governs conversion — how much of a *decoded* value this module will retain — and can only be applied to something already in memory. WireLimits governs what may reach memory at all, so it is the only thing standing between a hostile server and an unbounded allocation.

It is separate from Bounds rather than folded into it because only a byte-oriented transport can enforce it: there is nothing for a converter to do with MaxBodyBytes.

Jump to

Keyboard shortcuts

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