Documentation
¶
Overview ¶
Package xmcp implements the experimental MCP runtime endpoint at /x/mcp/{slug}. It is a temporary path used to prove out the MCP Servers / MCP Endpoints fronting model — slug + optional custom domain → mcp_endpoint → mcp_server → backend dispatch (Remote MCP proxy vs. existing toolset-backed serving). Once the model is exercised here, runtime handling will move under /mcp/... per AGE-1902.
This package owns the HTTP lifecycle (routing, slug resolution, auth, DB loads) for the experimental endpoint and delegates the actual serving work to either github.com/speakeasy-api/gram/server/internal/remotemcp/proxy (Remote MCP backend) or github.com/speakeasy-api/gram/server/internal/mcp.Service.ServeToolsetResolved (toolset backend).
Index ¶
Constants ¶
const RuntimePath = "/x/mcp/{slug}"
RuntimePath is the experimental runtime path served by this package.
Variables ¶
This section is empty.
Functions ¶
func Attach ¶
func Attach(mux goahttp.Muxer, service *Service, metadataService *mcpmetadata.Service)
Attach registers the experimental MCP runtime handler for all supported HTTP methods. DELETE, GET, and POST are required by the MCP Streamable HTTP transport (see spec § Session Management for DELETE and § Listening for Messages from the Server for GET).
Attach also registers /x/mcp aliases for the install page and OAuth .well-known metadata routes. The install page delegates to mcpmetadata for parity with /mcp; the .well-known routes are owned by xmcp directly so they can dispatch per-backend (see Service.HandleWellKnownOAuthServerMetadata).
Types ¶
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service owns dependencies for the experimental MCP runtime endpoint.
func NewService ¶
func NewService( logger *slog.Logger, tracerProvider trace.TracerProvider, meterProvider metric.MeterProvider, db *pgxpool.Pool, enc *encryption.Client, authzEngine *authz.Engine, guardianPolicy *guardian.Policy, billingRepo billing.Repository, billingTracker billing.Tracker, mcpService *mcp.Service, serverURL *url.URL, ) *Service
NewService constructs a Service with its full dependency graph wired up.
func (*Service) HandleWellKnownOAuthProtectedResourceMetadata ¶
func (s *Service) HandleWellKnownOAuthProtectedResourceMetadata(w http.ResponseWriter, r *http.Request) error
HandleWellKnownOAuthProtectedResourceMetadata serves /.well-known/oauth-protected-resource/x/mcp/{mcpSlug}.
The resource URL embedded in the response is the runtime URL the caller is actually addressing — `<baseURL>/x/mcp/<mcp_endpoint.slug>`. See Service.HandleWellKnownOAuthServerMetadata for the dispatch rationale.
func (*Service) HandleWellKnownOAuthServerMetadata ¶
HandleWellKnownOAuthServerMetadata serves /.well-known/oauth-authorization-server/x/mcp/{mcpSlug}.
Resolution mirrors the runtime path: slug → mcp_endpoint → mcp_server. Dispatch is per-backend so each backend can source OAuth state from the model that fits it best:
- Toolset-backed: load the linked toolset by ID and reuse the existing toolset-keyed wellknown resolver. The OAuth flow itself is still keyed by toolsets.mcp_slug today; the production model assumes mcp_endpoints.slug == toolsets.mcp_slug for these servers, and a separate upcoming OAuth migration (tracked independently of AGE-1902) will re-key the OAuth machinery onto mcp_servers.id, at which point the dependency on toolsets.mcp_slug drops entirely.
- Remote-backed: returns 404 today. Remote MCP servers publish their own .well-known and Gram does not yet act as an authorization server for them. Once that upcoming OAuth migration generalises the machinery off toolset_id, this branch will source from mcp_servers / oauth_proxy_servers.
func (*Service) ServeMCP ¶
ServeMCP handles DELETE, GET, and POST on /x/mcp/{slug}. It resolves the slug (and optional custom domain context) to an mcp_endpoint, loads the associated mcp_server, and dispatches to the backend implementation: Remote MCP proxy when remote_mcp_server_id is set, or the existing toolset-backed serving body when toolset_id is set.
type ToolUsageLimitsInterceptor ¶
type ToolUsageLimitsInterceptor struct {
// contains filtered or unexported fields
}
ToolUsageLimitsInterceptor enforces the free-tier hard cap on tools/call invocations by consulting the billing repository's cached period usage. It is a proxy.ToolsCallRequestInterceptor: it runs after the generic user-request chain and before the request is forwarded upstream. Non-free tiers and orgs with an active subscription skip the check.
The interceptor intentionally fails open when cached usage is unavailable (the billing cache should always be warm, but a transient miss must not take down tool invocation). Failures are logged with the originating org ID so operators can spot them in dashboards.
func NewToolUsageLimitsInterceptor ¶
func NewToolUsageLimitsInterceptor(billingRepo billing.Repository, logger *slog.Logger) *ToolUsageLimitsInterceptor
NewToolUsageLimitsInterceptor constructs an interceptor bound to the given billing repository. The same instance can be reused across requests.
func (*ToolUsageLimitsInterceptor) InterceptToolsCallRequest ¶
func (i *ToolUsageLimitsInterceptor) InterceptToolsCallRequest(ctx context.Context, _ *proxy.ToolsCallRequest) error
InterceptToolsCallRequest implements proxy.ToolsCallRequestInterceptor. It reads the organization and account type from the request's auth context, consults cached billing usage, and returns a forbidden error when the org has exceeded its hard cap.
func (*ToolUsageLimitsInterceptor) Name ¶
func (i *ToolUsageLimitsInterceptor) Name() string
Name implements proxy.ToolsCallRequestInterceptor.
type ToolUsageTrackingInterceptor ¶
type ToolUsageTrackingInterceptor struct {
// contains filtered or unexported fields
}
ToolUsageTrackingInterceptor emits a billing.ToolCallUsageEvent for each tools/call response so Remote MCP Server invocations feed the same Polar meter that gates free-tier usage on the existing /mcp endpoint. It is a proxy.ToolsCallResponseInterceptor: it runs after the generic proxy.RemoteMessageInterceptor chain has accepted the response and before the payload is relayed to the user.
Tracking is fire-and-forget: events are emitted in a goroutine bound to a context derived via context.WithoutCancel so the call completes even if the inbound request context cancels mid-relay. Missing auth context is treated as a no-op and logged so operators can spot misconfiguration without taking down tool invocation.
func NewToolUsageTrackingInterceptor ¶
func NewToolUsageTrackingInterceptor(tracker billing.Tracker, logger *slog.Logger) *ToolUsageTrackingInterceptor
NewToolUsageTrackingInterceptor constructs an interceptor bound to the given billing tracker. The same instance can be reused across requests.
func (*ToolUsageTrackingInterceptor) InterceptToolsCallResponse ¶
func (i *ToolUsageTrackingInterceptor) InterceptToolsCallResponse(ctx context.Context, call *proxy.ToolsCallResponse) error
InterceptToolsCallResponse implements proxy.ToolsCallResponseInterceptor. It emits a billing event for every observed tools/call response — paid tiers included — so Polar metering matches the existing /mcp surface. Always returns nil: tracking is best-effort and must not block the response from reaching the user.
func (*ToolUsageTrackingInterceptor) Name ¶
func (i *ToolUsageTrackingInterceptor) Name() string
Name implements proxy.ToolsCallResponseInterceptor.