Example: an MCP resource server and clients that describe themselves¶
Goal¶
Let editors and assistants you did not deploy connect to an MCP server, each with a token minted for that server alone.
What you need¶
-
A hostname that serves the MCP endpoint and its RFC 9728 protected-resource metadata.
-
A host that serves client documents, here
mcp-clients.example. - Permission to change the policy.
The policy snippet¶
client_documents:
origins: [mcp-clients.example] # hosts that may serve a Client ID Metadata Document; empty = off
requires: [all:observability:user] # mandatory once origins is set
ttl_cap: 10m
resources:
https://observability-mcp.example/mcp:
requires: [all:observability:user]
ttl_cap: 5m
display_name: Observability MCP server
A resource id is an absolute URI without fragment. Scheme and host match case-insensitively, the rest case-sensitively. Declare it without a trailing slash, as the server publishes it. For a read-only server add read_only: true and absolute_cap: 168h, at most seven days.
The exchange / command¶
The client presents an HTTPS URL as its client_id and sends the resource on the authorization and token requests:
resource=https://observability-mcp.example/mcp. The token's aud is that URI. A Go server uses identity/resource. It verifies the token, answers 401 with the challenge and serves the RFC 9728 metadata at the path-inserted well-known URL. A stock server runs behind resource-proxy.
res, err := resource.New(resource.Config{
IssuerURL: "https://access.example.com",
ResourceURL: "https://observability-mcp.example/mcp",
})
mux.Handle(res.Path(), res.Metadata()) // /.well-known/oauth-protected-resource/mcp
mux.Handle("/", res.Protect(mcpHandler))
The client reads that metadata, then the issuer's /.well-known/oauth-authorization-server. When a tool calls a backend,
the server uses its own workload identity, never the caller's bearer.
Verify¶
A person in the group signs in through a client whose document is on an allow-listed host. The token carries the server's URI as aud. A request naming an undeclared resource, or two, is refused with invalid_target. A document from another host is refused before anything is fetched.
Undo¶
Empty client_documents.origins and remove the resource row.
Recipe and limits: Connect an MCP server.
Snippet source: docs/guides/sluis/connect/mcp.md; policy/clientdoc.go validates client_documents, and the snippet is accepted
by sluisctl policy render.