Put OAuth 2.1 on the user's MCP server. Then stop.
Guardrails
- MUST use Streamable HTTP. stdio cannot do OAuth.
- MUST keep
/.well-known/oauth-protected-resourcepublic. - MUST return 401 with
WWW-Authenticateandresource_metadataon a missing or invalid token. A bare 401 is a host silent-fail. - MUST call
validateTokenwith the dashboard Server URL asaudience. - MUST register well-known, then Bearer middleware, then the MCP POST. A POST registered first never sees auth. MUST write the Node files and stop. MUST NOT run
npm installor start the server.
Gotchas
- Default path is Node + Express + Streamable HTTP. FastAPI → open references/fastapi.md. FastMCP Scalekit provider → open references/fastmcp.md. Stay on that file.
- Read
SCALEKIT_ENVIRONMENT_URL,SCALEKIT_CLIENT_ID,SCALEKIT_CLIENT_SECRET. NeverSCALEKIT_ENV_URL. Do not prependhttps://— the env URL already has a scheme. - Audience must match the dashboard Server URL exactly, including a trailing slash when the dashboard has one. If Server URL is empty, use the generated resource id.
- Paste the dashboard Metadata JSON. Do not build
authorization_serversby adding a scheme ontoSCALEKIT_ENVIRONMENT_URL. - Reuse the existing Express
appif the file has one. Do not create a secondexpress()app. - Write
@scalekit-sdk/node, Express, and MCP imports only when the repo is missing them. Do not runnpm install.
Step 1 — Confirm Streamable HTTP
stdio cannot do OAuth. The default path is Express + StreamableHTTPServerTransport from @modelcontextprotocol/sdk/server/streamableHttp.js. FastAPI or FastMCP → open the matching file under references/ and stay there.
Keep the user's tools. Do not invent a new MCP server if one already exists.
Done when: this skill is the right path, and the transport is Streamable HTTP (or Step 5 will add it).
Step 2 — Init
Write the imports. Do not run npm install. Ask the human to write missing SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET into .env from app.scalekit.com → Developers → Settings → API Credentials.
Put imports at the top of the file.
import express from 'express';
import { ScalekitClient } from '@scalekit-sdk/node';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENVIRONMENT_URL,
process.env.SCALEKIT_CLIENT_ID,
process.env.SCALEKIT_CLIENT_SECRET
);
Reuse the existing Express app if the file has one. If there is no app yet:
const app = express();
app.use(express.json());
Do not register the MCP POST here. That route goes on after the Bearer middleware in Step 5.
Done when: the imports are in the file, and the client initializes from those env vars.
Step 3 — Register the MCP server (user action)
Print this checklist. Ask the human to write missing SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET into .env. Then write the routes. Do not invent those env values or Metadata JSON.
- Open app.scalekit.com → MCP servers → Add MCP server.
- Enter a name.
- Enable dynamic client registration.
- Enable Client ID Metadata Document (CIMD).
- Set Server URL to the public MCP base URL (local default
http://localhost:3002/, keep the trailing slash). - Save.
- Copy Metadata JSON from MCP servers → your server → Metadata JSON.
- Restart the MCP process after DCR or CIMD toggles.
Audience is that Server URL. If Server URL is empty, use the generated resource id.
Done when: the checklist is printed, and the routes use a Metadata JSON placeholder plus the Server URL (local default http://localhost:3002/).
Step 4 — Public discovery endpoint
Serve the copied Metadata JSON at /.well-known/oauth-protected-resource. Keep this route public. Register it before the Bearer middleware.
const metadata = /* paste dashboard Metadata JSON */;
app.get('/.well-known/oauth-protected-resource', (req, res) => {
res.json(metadata);
});
If you must edit authorization_servers, join SCALEKIT_ENVIRONMENT_URL with /resources/<RESOURCE_ID>. Do not prepend https://.
Done when: that path returns the dashboard JSON with no auth.
Step 5 — Bearer middleware, then MCP POST
Register order: well-known → this auth → MCP POST. Express runs handlers in registration order.
const audience = 'http://localhost:3002/'; // dashboard Server URL
const metadataUrl = `${audience.replace(/\/$/, '')}/.well-known/oauth-protected-resource`;
const wwwAuthenticate = `Bearer realm="OAuth", resource_metadata="${metadataUrl}"`;
async function auth(req, res, next) {
if (req.path.includes('.well-known')) return next();
const header = req.headers.authorization || '';
const token = header.startsWith('Bearer ') ? header.slice(7).trim() : '';
if (!token) return res.status(401).set('WWW-Authenticate', wwwAuthenticate).end();
try {
await scalekit.validateToken(token, { audience: [audience] });
next();
} catch {
return res.status(401).set('WWW-Authenticate', wwwAuthenticate).end();
}
}
app.use(auth);
If POST / (or the existing MCP path) is already on the stack, move it below app.use(auth), or attach auth on that route: app.post('/', auth, handler).
If the repo has no Streamable HTTP route yet, add it now. Keep an existing path if the server already has one. Default is POST /.
const server = new McpServer({ name: 'mcp-server', version: '1.0.0' });
app.post('/', async (req, res) => {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
If the file has no listener, add app.listen(Number(process.env.PORT) || 3002). Reuse an existing listener.
Done when: auth sits above the MCP POST, a request without a token returns 401 with WWW-Authenticate and resource_metadata, and a listener is on 3002 (or PORT).
Step 6 — Optional scopes
If the user asked for tool-level scopes, re-validate with requiredScopes. Extra Express notes: references/express.md.
await scalekit.validateToken(token, {
audience: [audience],
requiredScopes: ['todo:write'],
});
Insufficient scope → 403 insufficient_scope.
Done when: scopes are skipped, or a tool checks requiredScopes.
Step 7 — Verify
curl -i -X POST http://localhost:3002/
curl -i http://localhost:3002/.well-known/oauth-protected-resource
Use the real MCP path if it is not /. Expect 401 + WWW-Authenticate with resource_metadata on the MCP POST, and JSON with resource, authorization_servers, and scopes_supported on well-known. Do not start the server. Do not run the curls.
Done when: the human has both curl commands.
Step 8 — Stop
Write the Node files and stop. Do not run npm install. Do not start the server. Do not write API keys. Do not expose AgentKit tools over MCP.
Done when: well-known is public, Bearer middleware validates audience, and this skill has stopped.
Reach for
setup-saaskitif env is missingadd-api-authfor API keys or client credentialsexpose-agentkit-mcpto expose AgentKit tools over MCP- references/express.md for Express extras
- references/fastapi.md for FastAPI
- references/fastmcp.md for the FastMCP Scalekit provider
Live lookups
- Docs index: https://docs.scalekit.com/llms.txt
- MCP OAuth: https://docs.scalekit.com/authenticate/mcp/quickstart/
- MCP: https://mcp.scalekit.com