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.1or[::1]and allow an ephemeral port. Alocalhosthostname 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 ashttps://<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,8010by 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:
pnpm install --frozen-lockfile
cp .env.example .env # then fill it in
pnpm db:migrate
pnpm dev # http://127.0.0.1:8010/mcpFor 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.

