harness

package
v1.2.5 Latest Latest
Warning

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

Go to latest
Published: May 12, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package harness provides a unified TransportHarness interface that lets one test body exercise stdio, legacy HTTP, streamable HTTP, and gRPC in parallel. Each factory binds an OS-assigned port (or bufconn for gRPC) so tests can run with t.Parallel() without colliding on fixed ports.

The harness is test-only; it is NOT imported by production code.

Index

Constants

This section is empty.

Variables

View Source
var ErrGRPCUnavailable = errors.New("harness: gRPC unavailable (build with -tags=grpc)")

ErrGRPCUnavailable signals that the harness was compiled without the grpc build tag; callers should skip gRPC-specific tests rather than fail.

Functions

func BlockingTool

func BlockingTool(name string, started chan<- struct{}) mcp.ToolDescriptor

BlockingTool returns a ToolDescriptor whose handler signals start on the given channel, then blocks until its context is cancelled. The handler returns the context's error, so a successful cancellation surfaces as an RPC error with code=-32603 (internal error carrying context.Canceled) or an isError=true result. Used by cancellation parity tests to prove that Cancel() / ctx cancellation actually aborts in-flight work rather than sitting there until the per-tool 45s timeout.

func ContainsTool

func ContainsTool(tools []map[string]any, name string) bool

ContainsTool returns true iff the decoded tools list includes one whose name equals the given value.

func IsNotFound

func IsNotFound(err *RPCError) bool

IsNotFound returns true when the error message matches the common JSON-RPC "method not found" / "tool not found" shapes the transports emit. Used by parity tests that check invalid-params behaviour without hardcoding error-code numbers.

func MockTool

func MockTool(name string, handler mcp.ToolHandler) mcp.ToolDescriptor

MockTool is a convenience constructor for tests that need a minimal ToolDescriptor with a fixed handler.

func ProtocolVersion

func ProtocolVersion(resp Response) (string, error)

ProtocolVersion unmarshals an initialize Response.Result and extracts the protocolVersion string.

func ToolName

func ToolName(m map[string]any) string

ToolName extracts the "name" field from a tools/list result.tools[] entry decoded into a generic map. Returns "" if the shape is unexpected.

func ToolsFromListResult

func ToolsFromListResult(resp Response) ([]map[string]any, error)

ToolsFromListResult decodes a tools/list Response.Result into a flat list of tool descriptors (each a generic map) for assertions.

func WaitForHTTP200

func WaitForHTTP200(ctx context.Context, url string) error

WaitForHTTP200 polls the given URL every 20ms until it returns HTTP 200 or until ctx expires (or a 5-second deadline if ctx has none). Used by adapters to avoid racing Initialize against the server goroutine.

Types

type Factory

type Factory func(ctx context.Context, opts Options) (Transport, error)

Factory constructs a Transport for a given Options; used by the parity matrix so one test can iterate factories without knowing their types.

type Options

type Options struct {
	// MaxMessageSize, if >0, configures the transport's size cap and the
	// server's MaxMessageSize. Zero leaves both at their default (4 MiB).
	MaxMessageSize int64

	// BearerToken, if non-empty, enables static-bearer auth on transports
	// that require it (legacy HTTP, streamable HTTP, gRPC). stdio ignores
	// this field.
	BearerToken string

	// Tools lists the ToolDescriptors the mock server should expose. Nil
	// means the harness registers only the default mock_tool.
	Tools []mcp.ToolDescriptor

	// Enforcement, if non-nil, is installed on the mock server. Most harness
	// tests leave this nil; parity tests that exercise protocol-level
	// validation can opt in without rebuilding each transport fixture.
	Enforcement mcp.Enforcement
}

Options controls per-harness construction. Defaults are chosen to match the production defaults (4 MiB size cap, stdio-style single-user tools).

type RPCError

type RPCError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
	Data    any    `json:"data,omitempty"`
}

RPCError is the JSON-RPC 2.0 error envelope.

type Response

type Response struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      int             `json:"id,omitempty"`
	Result  json.RawMessage `json:"result,omitempty"`
	Error   *RPCError       `json:"error,omitempty"`
	Method  string          `json:"method,omitempty"`
	Params  json.RawMessage `json:"params,omitempty"`
}

Response is the decoded JSON-RPC envelope returned by harness operations. Result is kept as raw JSON so each test can unmarshal into whatever struct it cares about; tests that only want a sanity check can compare against Error == nil.

type ServerSharer

type ServerSharer interface {
	SharedServer() (*mcp.Server, bool)
}

ServerSharer is an optional interface implemented by transports whose underlying mcp.Server is reachable from the test body — used for firing server-initiated notifications in parity tests. stdio and streamable HTTP implement it directly; legacy HTTP and gRPC return (nil, false) so tests can skip.

type Transport

type Transport interface {
	Name() string
	Initialize(ctx context.Context) (Response, error)
	ListTools(ctx context.Context) (Response, error)
	CallTool(ctx context.Context, name string, args map[string]any) (Response, error)
	CallToolAsync(ctx context.Context, name string, args map[string]any) (requestID int, done <-chan Response, err error)
	Cancel(ctx context.Context, requestID int) error
	// Notifications returns a channel of server→client frames (for
	// transports that support them: stdio, streamable HTTP, gRPC).
	// Legacy HTTP returns nil — callers must handle the nil case
	// explicitly rather than blocking forever on <-nil.
	Notifications() <-chan Response
	// SendRaw delivers arbitrary bytes through the transport's framing
	// layer without wrapping them in a JSON-RPC envelope and returns
	// the decoded server reply. Intended for malformed-JSON / parse-
	// error boundary tests; not for normal call flow.
	//
	// Line-delimited transports (stdio, gRPC) send the bytes verbatim
	// and read the next anonymous error frame. HTTP transports POST
	// the bytes as the request body and decode the HTTP response. Only
	// one SendRaw at a time per harness instance — serialisation is
	// enforced internally.
	SendRaw(ctx context.Context, frame []byte) (Response, error)
	MaxSupportedSize() int64
	Close() error
}

Transport is the contract every transport adapter satisfies. Callers issue Initialize → ListTools → CallTool and optionally Cancel; tests that need to assert server-initiated notifications read from Notifications(). MaxSupportedSize is what the transport was configured to enforce — tests use it to compute at-limit / over-limit boundaries without hard-coding numbers.

func NewGRPC

func NewGRPC(_ context.Context, _ Options) (Transport, error)

NewGRPC is a stub for non-grpc builds. The real implementation lives in grpc.go behind -tags=grpc. Parity tests skip gRPC when this factory returns ErrGRPCUnavailable.

func NewLegacyHTTP

func NewLegacyHTTP(ctx context.Context, opts Options) (Transport, error)

NewLegacyHTTP builds a harness over the legacy POST-only /mcp transport (mcp.Server.ServeHTTPListener). Each call is a single HTTP POST; there is no server→client notification channel, so Notifications() returns nil.

func NewStdio

func NewStdio(ctx context.Context, opts Options) (Transport, error)

NewStdio constructs a TransportHarness wrapping an mcp.Server.Run loop communicating over an in-memory io.Pipe pair. The harness owns both the server goroutine and the response reader; Close terminates both.

Request IDs are monotonic atomic counters; tests that need to pair a response with its request use the ID from CallToolAsync.

func NewStreamable

func NewStreamable(ctx context.Context, opts Options) (Transport, error)

NewStreamable builds a harness over the MCP Streamable HTTP 2025-03-26 transport. POST /mcp delivers requests; GET /mcp opens the long-lived SSE stream on which the server emits responses and notifications.

This adapter:

  • opens an ephemeral TCP listener (127.0.0.1:0)
  • creates a memory control-plane store
  • establishes an initialize round-trip to capture the session ID
  • opens the SSE subscription
  • parses SSE frames into Response values and routes them by request ID (or onto the notifications channel for server-initiated frames)

Jump to

Keyboard shortcuts

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