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

mcptest

package
v0.3.3 Latest Latest
Warning

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

Go to latest
Published: Oct 13, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package mcptest provides fake Upstreams for tests: a stdio Upstream that is the test binary re-executing itself, and HTTP Upstreams on httptest servers. Each offers the tools echo and fail, plus whatever its spec adds.

Index

Constants

View Source
const (
	Legacy = "2025-06-18"
	Modern = "2026-07-28"
)

Protocol versions of the two eras: legacy before 2026-07-28, modern from it.

View Source
const (
	ClientID     = "fake-client"
	ClientSecret = "fake-client-secret-c3f0"
	Code         = "fake-auth-code-55e2"
	AccessToken  = "fake-access-token-7d1e"
	RefreshToken = "fake-refresh-token-91ab"
	Scope        = "mcp:tools"
)

The secrets the fake Authorization Server hands out, distinctive enough that finding one in sprut's output can only mean a leak.

View Source
const Lifetime = time.Minute

Lifetime bounds how long a fake Upstream or its child that only waits to be stopped stays alive: longer than any test, so a test that stops it proves something, but short enough that one the Gateway failed to stop does not linger on the machine.

Variables

View Source
var (
	// ObjectSchema is the input schema of a tool that takes no arguments.
	ObjectSchema    = map[string]any{"type": "object"}
	EchoInputSchema = map[string]any{
		"type": "object",
		"properties": map[string]any{
			"text": map[string]any{"type": "string", "description": "text to echo"},
		},
		"required": []any{"text"},
	}
	EchoOutputSchema = map[string]any{
		"type": "object",
		"properties": map[string]any{
			"echoed": map[string]any{"type": "string"},
		},
	}
)

The fake Upstreams' tools, as raw JSON so that tests can assert that the Gateway passes them through unchanged.

Secrets are every secret of the fake Authorization Server.

Functions

func EchoResult

func EchoResult(text string) map[string]any

EchoResult is a fake Upstream's answer to echo with text, as JSON.

func Main

func Main()

Main runs the fake stdio Upstream that SPRUT_TEST_FAKE_UPSTREAM describes as a Stdio in JSON, or a well-behaved one if it is unset, and exits. It is the command that a testscript Config launches.

func MainIfFake

func MainIfFake()

MainIfFake runs the fake stdio Upstream and exits, if Entry launched this process; otherwise it returns. Call it first thing in TestMain.

func NewServer

func NewServer(protocol string) *mcp.Server

NewServer is the MCP server of a fake Upstream, stdio or HTTP, offering echo and fail. A non-empty protocol is the one protocol version it supports, which sets its era.

func RefusingURL

func RefusingURL() string

RefusingURL returns an HTTP URL at which nothing listens any more, so connecting to it is refused.

func StartGETStream

func StartGETStream(t testing.TB, chunk string) string

StartGETStream starts a fake Streamable HTTP Upstream that fails every POST but, as the transport allows of a server that needs no session id, answers a GET with an event stream, and returns its URL. The stream repeats chunk until the request ends; it stays silent if chunk is empty.

func StartHanging

func StartHanging(t testing.TB) string

StartHanging starts a fake HTTP Upstream that leaves its first request unanswered until the test ends, and returns its URL. Later requests, such as the client cancelling the first, are accepted at once, so that stopping the Upstream does not wait out the SDK's timeout.

func StartSSEOnly

func StartSSEOnly(t testing.TB) string

StartSSEOnly starts a fake HTTP Upstream that speaks only the deprecated HTTP+SSE transport, and returns its URL.

Types

type HTTPUpstream

type HTTPUpstream struct {
	URL   string
	Close func() // stops the server, which then refuses connections
	// contains filtered or unexported fields
}

HTTPUpstream is a fake HTTP Upstream: go-sdk's Streamable HTTP handler serving NewServer from an httptest server, which records the headers of every request it gets.

func StartHTTP

func StartHTTP(t testing.TB, protocol string) *HTTPUpstream

StartHTTP starts a fake HTTP Upstream supporting only protocol, or the SDK's versions if it is empty. It stops when the test ends.

func (*HTTPUpstream) Headers

func (u *HTTPUpstream) Headers() []http.Header

Headers returns the headers of each request the Upstream got so far.

type OAuthUpstream

type OAuthUpstream struct {
	Base string // the server's origin, which is also the issuer
	URL  string // the MCP endpoint
	// contains filtered or unexported fields
}

OAuthUpstream is a fake OAuth Upstream and its Authorization Server on one httptest server: Protected Resource Metadata, Authorization Server metadata, Dynamic Client Registration, an authorization endpoint that redirects straight to the callback, a token endpoint that also refreshes, rotating the refresh token, and an MCP endpoint at /mcp that answers 401 to any request without a live access token.

It honours only the refresh token it issued last, so one fake serves one set of Credentials that is renewed; renewable Credentials of another Upstream need a fake of their own.

func StartOAuth

func StartOAuth(t testing.TB) *OAuthUpstream

StartOAuth starts a fake OAuth Upstream, which stops when the test ends.

func StartOAuthOn

func StartOAuthOn(t testing.TB, host string) *OAuthUpstream

StartOAuthOn is StartOAuth with host in its URLs in place of 127.0.0.1, if set: on localhost, it has subdomains for a proxy to stand in for. It is always plain HTTP: seam 1 can't make sprut trust a test certificate, so no fake Upstream is on HTTPS.

func (*OAuthUpstream) AcceptClientSecret

func (u *OAuthUpstream) AcceptClientSecret(secret string)

AcceptClientSecret makes the token endpoint accept secret, and only it, as the client's secret from now on.

func (*OAuthUpstream) AdvertiseIss

func (u *OAuthUpstream) AdvertiseIss()

AdvertiseIss makes the metadata say that the callback carries iss (RFC 9207).

func (*OAuthUpstream) AdvertiseOfflineAccess

func (u *OAuthUpstream) AdvertiseOfflineAccess()

AdvertiseOfflineAccess makes the Authorization Server list offline_access in its scopes_supported.

func (*OAuthUpstream) AppType

func (u *OAuthUpstream) AppType() string

AppType returns the application_type of the last client registration.

func (*OAuthUpstream) ExpireIn

func (u *OAuthUpstream) ExpireIn(seconds int)

ExpireIn makes every access token issued from now on live for seconds.

func (*OAuthUpstream) FailCodeExchange

func (u *OAuthUpstream) FailCodeExchange(how string)

FailCodeExchange makes every exchange of an authorization code from now on fail as writeEcho does with how.

func (*OAuthUpstream) FailRefresh

func (u *OAuthUpstream) FailRefresh(how string)

FailRefresh makes every refresh request from now on fail: "invalid_grant", "5xx", "text" (an error body that is not JSON; see writeEcho), "drop" (the connection closes without an answer) or "hang" (until Release, then 503). "" makes them work again.

func (*OAuthUpstream) FailRegistration

func (u *OAuthUpstream) FailRegistration(how string)

FailRegistration makes every client registration from now on fail as writeEcho does with how.

func (*OAuthUpstream) GrantTypes

func (u *OAuthUpstream) GrantTypes() []string

GrantTypes returns the grant types of the last client registration.

func (*OAuthUpstream) Issue

func (u *OAuthUpstream) Issue() map[string]any

Issue issues a new access token and refresh token, and returns the token response. The first are AccessToken and RefreshToken; later ones add a counter to them.

func (*OAuthUpstream) IssueNoRefreshToken

func (u *OAuthUpstream) IssueNoRefreshToken()

IssueNoRefreshToken makes every token response from now on leave out the refresh token.

func (*OAuthUpstream) MCPAuth

func (u *OAuthUpstream) MCPAuth() []string

MCPAuth returns the Authorization header of each request to the MCP endpoint so far.

func (*OAuthUpstream) NameIssuer

func (u *OAuthUpstream) NameIssuer(issuer string)

NameIssuer makes the Protected Resource Metadata name issuer as the Upstream's Authorization Server.

func (*OAuthUpstream) OfferScopes

func (u *OAuthUpstream) OfferScopes(challenge string, supported []string)

OfferScopes makes a 401 name challenge as its scope ("" for none), and the Protected Resource Metadata list supported as scopes_supported (nil to leave it out).

func (*OAuthUpstream) OnRefresh

func (u *OAuthUpstream) OnRefresh(f func())

OnRefresh makes f run before each refresh request from now on is answered, and after it is recorded.

func (*OAuthUpstream) RedirectMCP

func (u *OAuthUpstream) RedirectMCP(link string)

RedirectMCP makes /mcp redirect every request with a live access token to link (307, so that a POST stays one), from now on.

func (*OAuthUpstream) RedirectWith

func (u *OAuthUpstream) RedirectWith(params url.Values)

RedirectWith makes the authorization endpoint redirect to the callback with params and the state, in place of the code.

func (*OAuthUpstream) Refreshes

func (u *OAuthUpstream) Refreshes() []url.Values

Refreshes returns the form of each refresh request so far.

func (*OAuthUpstream) Registrations

func (u *OAuthUpstream) Registrations() int

Registrations returns how many clients were registered so far.

func (*OAuthUpstream) Rejected

func (u *OAuthUpstream) Rejected() int

Rejected returns how many requests to the MCP endpoint were answered 401.

func (*OAuthUpstream) Release

func (u *OAuthUpstream) Release()

Release ends every hanging refresh request.

func (*OAuthUpstream) Released

func (u *OAuthUpstream) Released() <-chan struct{}

Released returns a channel that Release closes.

func (*OAuthUpstream) RequestedScope

func (u *OAuthUpstream) RequestedScope() (string, bool)

RequestedScope returns the scope parameter of the last authorization request, and whether it had one.

func (*OAuthUpstream) RequireScope

func (u *OAuthUpstream) RequireScope(scope string)

RequireScope makes /mcp answer 403 insufficient_scope, naming scope, to an access token that was not granted it.

func (*OAuthUpstream) RevokeAccessTokens

func (u *OAuthUpstream) RevokeAccessTokens()

RevokeAccessTokens makes every access token issued so far fail with 401.

func (*OAuthUpstream) SlashIssuer added in v0.3.1

func (u *OAuthUpstream) SlashIssuer()

SlashIssuer makes the issuer Base with a trailing slash, in the metadata of both the Protected Resource and the Authorization Server.

type Rendezvous

type Rendezvous struct {
	N   int
	Dir string
}

Rendezvous is where and how many fake Upstreams meet; see Stdio.

type Startup

type Startup string

Startup is how a fake stdio Upstream misbehaves at startup.

const (
	// Fail exits with an error before speaking MCP.
	Fail Startup = "fail"
	// Hang never answers and ignores its stdin closing, so only a signal
	// stops it.
	Hang Startup = "hang"
)

type Stdio

type Stdio struct {
	// Protocol, if set, is the one protocol version it supports, which sets
	// its era.
	Protocol string  `json:",omitempty"`
	Startup  Startup `json:",omitempty"`

	// PIDFile gets its PID.
	PIDFile string `json:",omitempty"`
	// EnvFile gets its environment, one VAR=value per line.
	EnvFile string `json:",omitempty"`
	// Stderr is written to its stderr at startup.
	Stderr string `json:",omitempty"`

	// Rendezvous makes it mark its arrival in Rendezvous.Dir and not answer
	// until Rendezvous.N fakes have arrived there.
	Rendezvous *Rendezvous `json:",omitempty"`

	// IgnoreSIGTERM makes it ignore SIGTERM and its stdin closing, so only
	// SIGKILL stops it.
	IgnoreSIGTERM bool `json:",omitempty"`
	// GrandchildPIDFile makes it start a child that shares its stderr and
	// sleeps until killed, and gets that child's PID.
	GrandchildPIDFile string `json:",omitempty"`
	// LeaveGroupPIDFile makes it leave its process group for that of a
	// child that sleeps until killed, and gets the child's PID, which is
	// also the new group's. Signals to its old group no longer reach it,
	// and it ignores its stdin closing, so it never exits on its own.
	LeaveGroupPIDFile string `json:",omitempty"`
	// HangUpAfterList makes it exit 1 as soon as it has written its answer
	// to tools/list.
	HangUpAfterList bool `json:",omitempty"`

	// ExtraTools are names of further tools, each taking no arguments and
	// answering with ID, so a test can tell which Upstream answered.
	ExtraTools []string `json:",omitempty"`
	ID         string   `json:",omitempty"`
	// BadSchemaTool is the name of one more tool, whose input schema is not
	// an object schema.
	BadSchemaTool string `json:",omitempty"`
	// CrashTool adds a tool "crash" that makes it exit 1 mid-call, without
	// answering. It hangs up its stdout first and exits only once the
	// Gateway reacts with SIGTERM, so the Gateway always sees the exit after
	// closing the connection itself.
	CrashTool bool `json:",omitempty"`
	// OrphanToolPIDFile adds a tool "orphan" that starts a child sharing
	// its stdout and stderr, then exits 1 mid-call, without answering. The
	// child sleeps until killed, holding the Upstream's stdout and stderr
	// open; the file gets its PID.
	OrphanToolPIDFile string `json:",omitempty"`
	// BlockToolFile adds a tool "block" that writes "started" to the file,
	// blocks until the call is cancelled, then writes "cancelled" to it.
	BlockToolFile string `json:",omitempty"`
	// GatherTool, if positive, adds a tool "gather" that answers only once
	// GatherTool calls to it are in flight together.
	GatherTool int `json:",omitempty"`
	// AskTool adds a tool "ask" that asks for interactive input mid-call
	// (multi-round-trip), and answers once it has some.
	AskTool bool `json:",omitempty"`
}

Stdio is the behaviour of a fake stdio Upstream. The zero value offers echo and fail and behaves well. Fields named ...File are paths the fake writes to, for the test to read back.

func (Stdio) Entry

func (s Stdio) Entry(t testing.TB, env ...string) map[string]any

Entry returns a Config entry that launches the fake Upstream that s describes, with env (VAR=value pairs) added to the entry's env. The test binary's TestMain must call MainIfFake.

Jump to

Keyboard shortcuts

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