Skip to content
Udibo
On this page
View Markdown

Protect an MCP server

Availability: this walkthrough describes a service template available through repository access arranged with Udibo. It is not a public standalone starter. You need an accessible Udibo Identity tenant. For runnable public package examples, see starter projects.

The template runs a stateless MCP server, built with the MCP TypeScript SDK on Hono and run by Node.js 26, keeping each organization's notes in Postgres. Its team_notes tool reads the notes of the organization the access token names, a page at a time, and its add_team_note tool adds one, recording the person and the client that wrote it. The server accepts people whose token names an organization and carries notes:read, notes:write, or both, and offers each person only the tools their permissions allow.

Register the client and grant access

Choose the server's canonical external URL, ending in /mcp, such as https://notes.example.com/mcp. In your tenant, define notes:read and notes:write, create roles carrying them, and grant those roles to the intended people inside each organization. Follow permissions for role assignment.

Manually register the MCP client's application with these settings:

  • Type: third-party. Because the client is public, hosted authorization shows the consent screen on every sign-in, including a repeat sign-in for scopes the person approved before.

  • Client authentication: public (confidential: false), with no secret.

  • Grant: authorization code.

  • Access token format: JWT.

  • Audience: the exact external MCP URL, including /mcp.

  • Redirect URI: the client's IP-loopback callback, including the path and any fixed query parameters. Production registrations permit HTTP callbacks on 127.0.0.1 or [::1] and allow an ephemeral port. A localhost hostname is not the production loopback exception.

  • Access token lifetime: as short as the clients you support tolerate. The server checks tokens locally, so this lifetime is how long a person keeps access after you remove them.

See application registration for the dashboard and API workflow. Dynamic client registration is not part of this starter.

Configure your MCP client with its registered client ID and openid profile scopes. Permission names are separate from OAuth scopes. The client must use PKCE S256, generate and verify OAuth state, and request the intended organization with the hosted authorization endpoint's organization slug parameter. The TypeScript SDK's OAuth provider supports state() but makes it optional; supply it for Udibo. Sign in and approve the displayed consent each time the client authorizes.

Configure the server

Set three environment values for the server, and optionally a fourth:

  • UDIBO_ISSUER: your tenant origin, such as https://<tenant-id>.udibo.com.

  • MCP_RESOURCE_URL: the canonical external server URL ending in /mcp.

  • DATABASE_URL: the Postgres database the notes live in.

  • PORT: optional; where the server listens, 8010 by default.

A missing or invalid value stops the server at startup with one line naming each problem. For local development, a loopback resource URL such as http://127.0.0.1:8010/mcp works. In your copy of the template, install, migrate the database and start it:

Shell
pnpm install --frozen-lockfile
cp .env.example .env    # then fill it in
pnpm db:migrate
pnpm dev                # http://127.0.0.1:8010/mcp

For your deployment, set PORT if the default does not suit, serve HTTPS through your proxy, and preserve the canonical Host header. The server rejects other Host values and foreign Origin headers, except that its metadata document answers any Origin; this template is intended for native MCP clients, not cross-origin browser clients. /healthz and /readyz answer any Host, for your platform's probes.

Checkpoint: call /mcp without a bearer token. It should return 401 with a WWW-Authenticate header containing resource_metadata. Fetch that URL: its resource must be this MCP server's URL, and its authorization_servers must contain your tenant origin. The client uses those documents to discover the hosted authorization and token endpoints.

Verify a tool call and a refusal

Connect the MCP client using Streamable HTTP. After authorization, initialize the connection, list tools and call team_notes. A successful call returns the token's organization ID and a page of that organization's notes — none until someone writes one. With notes:write, call add_team_note and read the note back.

Repeat with a member who holds neither permission, and with a token that names no organization. Both must receive 403. A person in several organizations gets a token that names none unless the authorization request's organization parameter names one or they picked one in their sign-in session. A person holding only notes:read sees only team_notes in the tool list. Signing in to another organization serves that organization's notes, never the first one's: every query is scoped by the verified organization ID. A token from another tenant or for another audience must receive 401. Machine credentials cannot supply the required openid scope.

The server creates a fresh transport per POST and returns JSON without a server-side session ID. GET streams and session DELETE return 405. Each request must carry its own bearer token in the Authorization header.

Keep the boundaries when extending it

Replace the notes with your application's data and keep every query scoped to the token's organization. This starter demonstrates organization-scoped read and write tools, not tenant administration or resource-level sharing.

Udibo currently mints the application's fixed registered audience. MCP clients send resource on authorization and token requests, but that parameter does not select or narrow token issuance. Register a separate application for another resource; this is not general RFC 8707 resource selection. Never pass the MCP token onward to UserInfo, /api/check, or another API.

JWT validation uses the tenant's public keys and checks signature, access-token type, issuer, audience and expiry. It does not perform an immediate revocation lookup, and introspection is not available to it: a tenant answers introspection only for a confidential application asking about its own tokens. Choose an access-token lifetime appropriate for your data and keep host clocks synchronized: the starter allows no clock leeway. See application registration and the production checklist before deploying.

Last verified 2026-10-13.