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
- Variables
- func EchoResult(text string) map[string]any
- func Main()
- func MainIfFake()
- func NewServer(protocol string) *mcp.Server
- func RefusingURL() string
- func StartGETStream(t testing.TB, chunk string) string
- func StartHanging(t testing.TB) string
- func StartSSEOnly(t testing.TB) string
- type HTTPUpstream
- type OAuthUpstream
- func (u *OAuthUpstream) AcceptClientSecret(secret string)
- func (u *OAuthUpstream) AdvertiseIss()
- func (u *OAuthUpstream) AdvertiseOfflineAccess()
- func (u *OAuthUpstream) AppType() string
- func (u *OAuthUpstream) ExpireIn(seconds int)
- func (u *OAuthUpstream) FailCodeExchange(how string)
- func (u *OAuthUpstream) FailRefresh(how string)
- func (u *OAuthUpstream) FailRegistration(how string)
- func (u *OAuthUpstream) GrantTypes() []string
- func (u *OAuthUpstream) Issue() map[string]any
- func (u *OAuthUpstream) IssueNoRefreshToken()
- func (u *OAuthUpstream) MCPAuth() []string
- func (u *OAuthUpstream) NameIssuer(issuer string)
- func (u *OAuthUpstream) OfferScopes(challenge string, supported []string)
- func (u *OAuthUpstream) OnRefresh(f func())
- func (u *OAuthUpstream) RedirectMCP(link string)
- func (u *OAuthUpstream) RedirectWith(params url.Values)
- func (u *OAuthUpstream) Refreshes() []url.Values
- func (u *OAuthUpstream) Registrations() int
- func (u *OAuthUpstream) Rejected() int
- func (u *OAuthUpstream) Release()
- func (u *OAuthUpstream) Released() <-chan struct{}
- func (u *OAuthUpstream) RequestedScope() (string, bool)
- func (u *OAuthUpstream) RequireScope(scope string)
- func (u *OAuthUpstream) RevokeAccessTokens()
- type Rendezvous
- type Startup
- type Stdio
Constants ¶
const ( Legacy = "2025-06-18" Modern = "2026-07-28" )
Protocol versions of the two eras: legacy before 2026-07-28, modern from it.
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.
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 ¶
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.
var Secrets = []string{ClientSecret, Code, AccessToken, RefreshToken}
Secrets are every secret of the fake Authorization Server.
Functions ¶
func EchoResult ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
type Rendezvous ¶
Rendezvous is where and how many fake Upstreams meet; see Stdio.
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.