httpserver

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Aug 10, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package httpserver serves the Melange MCP server over the MCP Streamable HTTP transport for remote agent clients.

The design is stateless by construction: every request builds a fresh *mcp.Server whose API client is bound to that request's verified bearer token, so N replicas behind a load balancer share nothing and no request can ever observe another request's credential. The bearer token itself never appears in logs, error text, or tool results.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AuthMiddleware

func AuthMiddleware(verifier auth.TokenVerifier, resourceMetadataURL string, next http.Handler) http.Handler

AuthMiddleware wraps the MCP handler with bearer authentication: the SDK's RequireBearerToken rejects requests without a well-formed bearer (401) and runs verifier on the token, then bearerToContext captures the raw bearer in the request context for the per-request ClientProvider. Every 401 leaves with a WWW-Authenticate challenge (see challengeWriter).

resourceMetadataURL is the absolute URL of this server's RFC 9728 protected-resource metadata document, or "" when no resource identity is configured (the document is then not served at all — see metadata.go).

func CanonicalResource

func CanonicalResource(raw string) (string, error)

CanonicalResource validates and normalizes a canonical resource URL — the identity this server asserts as an OAuth 2.1 protected resource. The same value is the audience MeVerifier enforces and the `resource` field of the RFC 9728 protected-resource metadata document, whose grammar this enforces: an absolute https URL with a host and no query or fragment. Plain http is allowed only for loopback hosts (localhost/127.0.0.1/[::1]) so a local dev loop does not need TLS; anything else non-https would advertise an identity tokens should never be bound to.

Normalization: scheme and host are lowercased, the path is cleaned (path.Clean: duplicate slashes collapse, dot segments resolve) and the trailing "/" is trimmed, so equivalent spellings configure the same identity. Without the clean, `https://host//` canonicalized to `https://host/` — an identity no minted aud could ever match (the authorization server's allowlist entries have no trailing slash and resourceMatches only forgives ONE), and one whose derived RFC 9728 well-known path ended in "/", a ServeMux SUBTREE pattern that served the metadata document at every subpath. Errors are operator-facing startup text (the flag value never contains credentials); callers map them to a usage error (exit 2).

func PassthroughVerifier

func PassthroughVerifier(_ context.Context, token string, _ *http.Request) (*auth.TokenInfo, error)

PassthroughVerifier accepts any non-empty bearer token without validating it upstream. This is the default relay posture: the token's real check happens when the per-request API client presents it to the Melange API, and a bad token fails there as a tool error carrying the HTTP reconnect hints.

Any token shape is accepted deliberately — ztp_ personal access tokens today, zoa_ OAuth access tokens in CLI-PR4, whatever the API mints next. The relay must never gatekeep token formats; only the API knows what a valid credential looks like.

Types

type Config

type Config struct {
	// Listen is the TCP listen address, e.g. ":8321" or "127.0.0.1:0".
	Listen string
	// APIHost is the Melange API base URL for per-request clients.
	APIHost string
	// UserAgent is sent on outgoing API requests.
	UserAgent string
	// APITimeout bounds one outgoing API request; 0 means
	// api.DefaultRequestTimeout.
	APITimeout time.Duration
	// Version is the MCP server version advertised during initialization and
	// reported by /healthz.
	Version string
	// Logger receives server diagnostics; nil means discard. It must never
	// receive credentials.
	Logger *slog.Logger
	// AllowedOrigins lists the browser Origins allowed to reach the MCP
	// endpoint. Empty rejects every request that carries an Origin header;
	// see originMiddleware for the rationale.
	AllowedOrigins []string
	// ValidateTokens verifies bearers against GET /v1/me (MeVerifier) before
	// any tool runs; false relays any non-empty bearer to the API unchecked
	// (PassthroughVerifier) unless Resource is set, which forces validation.
	ValidateTokens bool
	// Resource is this server's canonical resource URL as an OAuth 2.1
	// protected resource (RFC 8707/9728), e.g. "https://mcp.zetic.ai".
	// Setting it turns on MeVerifier regardless of ValidateTokens — audience
	// enforcement cannot happen without validating — and rejects OAuth
	// bearers bound to a different resource. Empty disables audience
	// enforcement (the PAT-only/dev posture). Validated and normalized by
	// New via CanonicalResource.
	Resource string
	// contains filtered or unexported fields
}

Config configures a Server. Listen and APIHost are required.

type MeVerifier

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

MeVerifier validates bearer tokens against GET /v1/me on the configured API host (the same host the tools call). Enabled by --validate-tokens or by configuring --resource: it rejects bad credentials at the door with a 401 instead of letting each tool call fail downstream.

/v1/me is the authorization server's substitute for token introspection (there is no introspection endpoint by design): it accepts both credential kinds the API mints — ztp_ personal access tokens and zoa_ OAuth 2.1 access tokens — and 401s everything else, including zor_ refresh tokens. Both response shapes share the token block this verifier maps into auth.TokenInfo (scopes, expires_at); OAuth responses additionally carry the grant's RFC 8707 audience, which validate enforces against resource.

Positive results are cached for meCacheTTL keyed by SHA-256 of the token — raw token bytes are never held as map keys, so no cache dump or debugger snapshot can yield a credential. Negative results are never cached: a just-created token must work on the next request even if a stale attempt preceded it, and an attacker gains nothing from re-verification (each miss is one upstream 401). Audience enforcement happens before the cache store, so every cached entry has already passed it for this verifier's resource (the resource is fixed at construction, never per request).

func NewMeVerifier

func NewMeVerifier(apiOptions func(bearer string) api.Options, resource string, logger *slog.Logger) *MeVerifier

NewMeVerifier builds a MeVerifier; its Verify method is the auth.TokenVerifier. resource is the canonical resource URL audience-bound tokens must name (already in CanonicalResource form), or "" to skip audience enforcement. logger receives the failure detail that is withheld from callers; nil discards it.

func (*MeVerifier) Verify

func (v *MeVerifier) Verify(ctx context.Context, token string, _ *http.Request) (*auth.TokenInfo, error)

Verify implements auth.TokenVerifier: cached success, else one GET /v1/me with the presented bearer.

type Server

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

Server is the Streamable HTTP front end for the Melange MCP server.

func New

func New(cfg Config) (*Server, error)

New validates cfg and assembles the server. The handler chain for the MCP endpoint is (outermost first): IP rate limit -> Origin policy -> bearer auth -> bearer capture -> token rate limit -> streamable handler; /healthz and (with a configured Resource) the RFC 9728 metadata document bypass all of it.

func (*Server) Addr

func (s *Server) Addr() net.Addr

Addr reports the bound listen address, or nil before ListenAndServe binds it. With a ":0" listen address this is the only way to learn the port.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(ctx context.Context) error

ListenAndServe binds cfg.Listen and serves until ctx is canceled, then drains: in-flight requests get drainTimeout to complete, after which the server returns nil (exit 0). A drain overrun force-closes remaining connections and returns an error (exit 1). A second stop signal (SIGINT or SIGTERM) during the drain closes connections immediately and returns an error wrapping context.Canceled (exit 130): the operator asked twice, so waiting out the remaining drain window would turn a deliberate "stop now" into an apparent hang.

Jump to

Keyboard shortcuts

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