xmcp

package
v0.0.0-...-2c84295 Latest Latest
Warning

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

Go to latest
Published: May 9, 2026 License: AGPL-3.0 Imports: 37 Imported by: 0

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

View Source
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

func (s *Service) HandleWellKnownOAuthServerMetadata(w http.ResponseWriter, r *http.Request) error

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

func (s *Service) ServeMCP(w http.ResponseWriter, r *http.Request) error

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

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

Name implements proxy.ToolsCallResponseInterceptor.

Jump to

Keyboard shortcuts

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