Mount the server in an existing HTTP service¶
A service that already runs an HTTP server under go/controls adds MCP as one
more handler on that server. The module owns operation-level validation,
authorisation and bounded results; the service keeps its listener, TLS,
authentication middleware and lifecycle. New to this shape? The
tutorial builds it from an empty directory.
Build the server¶
Register your operations explicitly against application functions (never discovered from routes or reflection), with a policy that reads the request's identity from the context:
registry, err := mcp.New(operations, mcp.WithPolicy(policy)) // see "Authorise operations"
if err != nil {
return err
}
host, err := server.New(registry,
server.WithIdentity("studio", version),
server.WithLogger(logger),
server.WithTrustedOrigins("https://studio.example.com"),
)
if err != nil {
return err
}
server.New starts nothing. host.Handler() is a stateless Streamable HTTP
handler; the identity your middleware puts in the request context reaches the
registry and resource policies on every request.
Mount it under your middleware¶
Put the handler on the service's mux at a path you choose, and register the mux with the transport behind the chain you already run:
chain := transithttp.NewChain(
transithttp.LoggingMiddleware(logger),
authenticate, // transporthttp.AuthMiddleware(...) or your own
)
mux := http.NewServeMux()
mux.Handle("/api/", api)
mux.Handle("/mcp", host.Handler())
if _, err := transporthttp.Register(ctx, "http", controller, logger, mux, settings,
transporthttp.WithMiddleware(chain),
transporthttp.WithWriteTimeout(0),
); err != nil {
return err
}
Authentication is the service's job: the module does not verify bearer tokens
or API keys. Whatever authenticate establishes must be what Policy.ScopeKey
and Policy.Authorize read; with the transport's AuthMiddleware that is
transporthttp.IdentityFromContext. A request the chain refuses never reaches
the protocol.
The handler applies MCP's Origin rule itself: a request with no Origin
header passes, a browser origin passes only when it is the server's own or
named in WithTrustedOrigins, and anything else is refused with 403 before the
protocol sees it.
The transport's request-body limit (1 MiB unless you set
WithMaxRequestBodyBytes) applies to MCP requests like any other; one over it
is refused with 413, and nothing executes. Set the write timeout to zero for
this server, or SSE responses are cut off mid-call (the transport's default is
ten seconds).
A call is bound to its request. When the client disconnects, or the server cannot write to it any more, the operation's context is cancelled and the server stays healthy for the next call. This holds on every protocol version the adapter serves, not only where the SDK propagates it.
Register the lifecycle¶
The MCP server has its own shutdown and health, so register them with the controller after the transport:
The controller stops services in reverse registration order, so the MCP server cancels its in-flight calls first and the listener then drains promptly; registered the other way round, the listener waits on calls that nothing is cancelling until the shutdown budget runs out.
Shutdown(ctx) stops admission, cancels active calls and waits within ctx;
Status() stays non-nil while any work is unfinished, so an unresolved call
remains visible in health after the controller's own stop has returned. Do not
create a second controller or a signal handler for it: the service's is the
owner (spec 0001 D7). The mounted server owns no background work of its own.
What the module proves¶
The tests under internal/servicefixture run exactly this shape: a service
with a route of its own, API-key authentication in a transit chain, the
handler registered through the transport under one controller. They show
identity reaching policy for discovery and execution, an unauthenticated
call refused before the protocol with no side effect, a foreign Origin
refused with 403 through the chain, the transport's body limit answering 413,
a stream that fails to write leaving the server healthy, a client disconnect
cancelling the operation on the current and the legacy protocol, and the
controller's shutdown cancelling an in-flight call, draining, closing the
listener and reporting a clean stop, with no second listener, controller or
signal owner. The server reference states the
contracts.
Add resources or an Apps shell¶
server.WithResources(resources) publishes a mcp.Resources container with
its own policy; server.WithApps(...) mounts a static UI shell on the compact
dispatcher. Both are covered in the server reference.
What a CLI does instead¶
A command-line tool has no service to mount into. cli.Command's stream
subcommand builds this shape for it: the transport module's server under a
controller tied to the command context, with the MCP server and the Cobra
binding registered beside it. Read the command tree reference.