# 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](https://www.udibo.com/docs/identity/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](https://www.udibo.com/docs/identity/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](https://www.udibo.com/docs/identity/applications) 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://.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: ```bash 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](https://www.udibo.com/docs/identity/applications) and the [production checklist](https://www.udibo.com/docs/identity/production) before deploying. _Last verified 2026-10-13._