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

surface

package
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package surface builds the protocol-facing MCP server every entry point of a server shares: the stdio server operators run, the in-process capture used for the offline pre-flight, and the loopback HTTP host the official conformance suite connects to.

It exists so those three cannot drift. Conformance evidence gathered against the HTTP host only transfers to the shipped stdio binary if both serve the same manifest, the same capabilities and the same instructions, through the same middleware — and the cheapest way to guarantee that is for one function to build all of it.

Index

Constants

View Source
const (
	MethodDiscover   = "server/discover"
	MethodInitialize = "initialize"
)

Protocol method names used for wire-frame lookup.

The 2026-07-28 handshake is server/discover; initialize is retained because a pre-2026-07-28 client still negotiates with it, and the capture has to find whichever one the session actually used.

Variables

This section is empty.

Functions

func CacheHintsMiddleware

func CacheHintsMiddleware() mcp.Middleware

CacheHintsMiddleware overwrites the SDK's default caching hints with ones that describe this server.

It is receiving middleware, so it runs after the handler has produced the result and can amend the envelope on the way out. Only the envelope is touched: the rug-pull fingerprints deliberately ignore these fields (see TestHashToolsIgnoresResultEnvelope), so amending them here cannot read as drift.

func CaptureRawResults added in v0.2.2

func CaptureRawResults(ctx context.Context, server *mcp.Server, probe ProbeFunc) (map[string][]json.RawMessage, error)

CaptureRawResults runs safe probes against a server whose handlers the caller registers, returning the replies after the SDK has applied wire decoration. It is useful for OS-backed result constructors that cannot invoke a desktop action on CI but still need to prove their emitted protocol shape.

func CombineInstructions

func CombineInstructions(parts ...string) string

CombineInstructions joins the persona guidance and the toolset-derived instructions, omitting empty parts.

func CredentialsDeclareUnmaskedTargets

func CredentialsDeclareUnmaskedTargets(path string, checkPerms func(string) error) bool

CredentialsDeclareUnmaskedTargets reports whether any entry in the credentials document opts out of the masked-destination check.

It decodes only that flag. The secret fields are deliberately not part of the struct, so this runs before startup admission without materialising a plaintext — the file's permissions are still checked (checkPerms), and the real load (which does decode secrets, into wipeable bytes) happens later and only if admitted.

A malformed document is not diagnosed here; it reports false and lets the real credentials loader produce the error, so there is one place that decides whether a credentials file is valid.

func MCPPrompts

func MCPPrompts(ctx context.Context, inv *inventory.Inventory) []*mcp.Prompt

MCPPrompts returns the registered prompts as []*mcp.Prompt for fingerprinting.

func MCPResources

func MCPResources(ctx context.Context, inv *inventory.Inventory) []*mcp.Resource

MCPResources returns the registered fixed-URI resources as []*mcp.Resource for fingerprinting.

func MCPTools

func MCPTools(ctx context.Context, inv *inventory.Inventory) []*mcp.Tool

MCPTools returns the registered tool definitions as []*mcp.Tool for fingerprinting.

func PinnedCapabilities

func PinnedCapabilities() *mcp.ServerCapabilities

PinnedCapabilities declares every server capability explicitly.

This must stay explicit. The SDK *infers* capabilities it finds unset: registering a prompt or resource makes Server.capabilities() fill in Prompts/Resources with ListChanged: true. That would re-open exactly the silent re-advertisement channel the tools capability was pinned to close — a mutated manifest could be pushed to the client without the client re-listing, which is what rug-pull detection exists to catch. Any non-nil field we set wins over inference, so pinning each one with ListChanged false keeps the manifest static and drift detectable.

Two fields are deliberately left unset. Extensions (added in 2026-07-28) stays absent because the server implements no protocol extension — declaring one it does not honour would be a false advertisement, and the rug-pull discover baseline trips if a future SDK starts populating it. Logging stays absent because SEP-2577 deprecated the feature and the servers log to stderr or a file, never as MCP notifications.

func RequireProbeMethods added in v0.2.2

func RequireProbeMethods(got Captured, methods ...string) error

RequireProbeMethods fails when an advertised operation was never exercised. This prevents a green schema run from meaning only that declarations parsed.

func ToolsOutsidePersona

func ToolsOutsidePersona(tools []string, enabled []inventory.ToolsetMetadata, membership map[string]string) []string

ToolsOutsidePersona returns the --tools entries whose toolset is not among the enabled toolsets — the tools served only by the additional-tools bypass. A persona is a documented guarantee about the served surface, so widening it with --tools is refused; without a persona the operator is composing the surface by hand and the bypass is theirs to use. Unknown names (and the always-served GuardrailStatus/Kill, which belong to no toolset) are ignored.

membership maps tool name to toolset ID for the server's full manifest.

func ToolsetIDs

func ToolsetIDs(tss []inventory.ToolsetMetadata) []string

ToolsetIDs renders toolset metadata as its string IDs.

func ValidateCaptured added in v0.2.2

func ValidateCaptured(spec *mcpspec.Spec, got Captured) error

ValidateCaptured checks the protocol surface actually returned to a client against the selected published MCP revision. It checks every advertised item, not a fixed list of names from the conformance suite's example server.

func ValidateMethodResult added in v0.2.2

func ValidateMethodResult(spec *mcpspec.Spec, method string, raw json.RawMessage) error

ValidateMethodResult checks a response shape for a supported server method. It also covers result constructors whose OS-backed handler cannot safely run on a hosted CI runner, such as screenshots and desktop-state resources.

Types

type Captured

type Captured struct {
	// ToolsListResult is the tools/list result as served.
	ToolsListResult json.RawMessage
	// These lists are captured from the server's raw replies, before the client
	// SDK can normalize or discard fields.
	PromptsListResult           json.RawMessage
	ResourcesListResult         json.RawMessage
	ResourceTemplatesListResult json.RawMessage
	// Results contains every successful raw reply, grouped by request method.
	// Optional probes can add tools/call, prompts/get, resources/read and
	// completion/complete replies without changing production registration.
	Results map[string][]json.RawMessage
	// HandshakeResult is the server/discover result (or, before 2026-07-28, the
	// initialize result) as served.
	HandshakeResult json.RawMessage
	// Capabilities is the serverCapabilities object as served.
	Capabilities json.RawMessage
	// NegotiatedVersion is the protocol revision the session settled on.
	NegotiatedVersion string
}

Captured is what a client actually receives from a server, as raw wire JSON.

Everything here is bytes off a real session rather than a re-marshalling of Go types, because the divergence worth catching lives exactly there: between a project's hand-written tool schemas, the SDK's serialization, and the published spec.

This capture is the input to the product spec gate. It validates the actual registered surface and safe method results against the published schema. It also provides the reference side of the optional HTTP host equivalence test.

func Capture

func Capture(ctx context.Context, s *Surface, inv *inventory.Inventory, deps any, clientVersion string) (Captured, error)

Capture runs a real in-process MCP session against a built surface over an in-memory transport and returns the wire objects a client actually receives.

The surface must already have had InstallReceiving called (with no guardrail layers — the capture exists to record the advertised surface offline). The two always-served guardrail tools are registered here with inert handlers so they are part of the scored manifest, exactly as a served session has them.

Capture without probes only lists definitions, so its dependency set may carry a nil engine. CaptureWithProbes may call handlers chosen by the caller.

func CaptureWithProbes added in v0.2.2

func CaptureWithProbes(ctx context.Context, s *Surface, inv *inventory.Inventory, deps any, clientVersion string, probe ProbeFunc) (Captured, error)

CaptureWithProbes captures the advertised surface and optional method replies from the real registered handlers over an in-memory MCP session.

type CompletionHandler

type CompletionHandler = func(context.Context, *mcp.CompleteRequest) (*mcp.CompleteResult, error)

CompletionHandler is the completion/complete handler a surface serves.

func CompletionHandlerFor

func CompletionHandlerFor(inv *inventory.Inventory, src CompletionSources) CompletionHandler

CompletionHandlerFor serves completion/complete for prompt arguments.

The SDK validates that Ref and Argument.Name are present before dispatch, so this handler can assume both.

type CompletionSources

type CompletionSources struct {
	// PersonaIDs lists the built-in persona selectors.
	PersonaIDs []string
	// CommonApps are frequently automated applications, offered as launch
	// suggestions. Deliberately a short curated list rather than an enumeration
	// of installed software, which would be slow and leak the machine's inventory.
	CommonApps []string
}

CompletionSources is what a server knows that a completion may offer.

Values come only from data the server already holds — persona IDs, toolset IDs, tool names, common application names, and configured credential *names*. It never sources a credential secret: the Credentials tool's never-read invariant applies here too, and a completion response goes straight into the model's context.

type Config

type Config struct {
	// Name is the implementation name, e.g. "macos-mcp-server".
	Name string
	// Title is the human-readable name, e.g. "macOS MCP Server".
	Title string
	// Version is the build version stamped into the implementation info.
	Version string
}

Config names the server as it appears in server/discover.

type CredentialExposure

type CredentialExposure struct {
	Risky      []string
	Perception []string
}

CredentialExposure names the toolsets that put installed credentials at risk.

Risky toolsets can read an installed credential back out of the calling user's credential store, defeating the never-read guarantee: a shell (the platform's credential CLI) and a filesystem (a credential-store backup is a file). Perception toolsets can read a secret back off the *screen* once it has been typed somewhere unmasked: a screenshot returns it as pixels, a text read lifts it out of an unmasked control, and the clipboard returns a copied selection. They are not risky on their own — injection normally requires a destination that reports itself as masked — but a credential declaring allow_unmasked_target gives that up, and only then does a perception toolset become a credential-disclosure surface.

func (CredentialExposure) Split

func (e CredentialExposure) Split(
	enabled []inventory.ToolsetMetadata,
	ack policy.StringSet,
	anyUnmaskedTarget bool,
) (unacknowledged, acknowledged []string)

Split partitions the risky toolsets that are actually served into those the policy acknowledges and those it does not, preserving the canonical order. A non-empty unacknowledged list is what forces a startup refusal; the acknowledged list is what gets recorded when startup proceeds anyway. The order of the two returns matches Risky so the audit record and the error message are stable.

func (CredentialExposure) Toolsets

func (e CredentialExposure) Toolsets(anyUnmaskedTarget bool) []string

Toolsets returns the toolsets whose presence puts installed credentials at risk, given whether any credential opts out of the masked-destination check.

type FrameLog

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

FrameLog collects frames from both directions. Safe for concurrent use: the SDK reads and writes from different goroutines.

func NewFrameLog

func NewFrameLog() *FrameLog

func (*FrameLog) Methods

func (f *FrameLog) Methods() []string

Methods returns every request method observed on the wire.

func (*FrameLog) ResultFor

func (f *FrameLog) ResultFor(method string) (json.RawMessage, bool)

ResultFor returns the raw result bytes of the reply to the given method, and whether one was seen.

func (*FrameLog) ResultsByMethod added in v0.2.2

func (f *FrameLog) ResultsByMethod() map[string][]json.RawMessage

ResultsByMethod returns successful raw results in request order. It is used by protocol probes so the schema gate validates server bytes, not values reconstructed by the client SDK.

type ProbeFunc added in v0.2.2

type ProbeFunc func(context.Context, *mcp.ClientSession) error

ProbeFunc makes safe, product-specific MCP calls over the same session used for capture. Callers choose arguments that cannot actuate the desktop in CI.

type RecordingTransport

type RecordingTransport struct {
	Inner  mcp.Transport
	Frames *FrameLog
}

RecordingTransport wraps an mcp.Transport and records every JSON-RPC frame that crosses it, so the product spec gate can validate what a client actually receives rather than a re-marshalling of our Go types.

This exists because the SDK's client-side accessors normalize results: on protocol 2026-07-28 the handshake is server/discover, but ClientSession only exposes InitializeResult() — a synthesized legacy view that omits the fields DiscoverResult requires. Validating that view reported the server as non-conformant when it was not.

Preferred over the SDK's LoggingTransport for two reasons: it yields typed jsonrpc.Message values instead of a text log format that would have to be re-parsed, and LoggingTransport does not forward the optional interfaces the SDK probes for (notably ProtocolVersionSupporter, consulted by filterSupportedVersions), which can change the advertised version list and so perturb the very thing being measured.

func (*RecordingTransport) Connect

Connect implements mcp.Transport.

type Surface

type Surface struct {
	Server *mcp.Server
	// Capabilities is the pinned capability set handed to the server, kept so the
	// rug-pull detector can baseline exactly what was advertised.
	Capabilities *mcp.ServerCapabilities
	// Instructions is the combined persona and toolset guidance, kept for the same
	// reason.
	Instructions string
	// contains filtered or unexported fields
}

Surface is the protocol-facing construction.

func New

func New(
	cfg Config,
	inv *inventory.Inventory,
	personaInstructions string,
	deps any,
	completer CompletionHandler,
) *Surface

New builds the server. It installs no middleware: every entry point must call InstallReceiving exactly once, with the layers that belong inside the two unconditional ones.

deps is the server's dependency set, injected into every request's context. completer serves completion/complete; see CompletionHandlerFor.

func (*Surface) InstallReceiving

func (s *Surface) InstallReceiving(inner ...mcp.Middleware)

InstallReceiving installs the whole receiving chain, outermost first: inject deps, cache hints, then inner.

It must be ONE call, and that is the entire reason this method exists. AddReceivingMiddleware composes the middleware handed to a single call outermost-first, but each separate call wraps the chain built so far — so across several calls the last middleware added ends up outermost, the reverse of the order intended. Installing across calls put the policy engine outside the audit layer, and a refused call therefore produced no tool.call entry at all: the refusals are precisely the events the chain exists to hold.

Dependency injection has to be outermost for every other layer to read deps from the context, and the cache hints have to sit outside the guardrails so nothing inside can undo the envelope they set.

Jump to

Keyboard shortcuts

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