TypeScript SDK
Protect your Node.js MCP server in one line:
import express from "express";
import { loadConfigFromEnv, mountMCP } from "@authsec/sdk";
const app = express();
app.use(express.json());
await mountMCP(app, {
config: loadConfigFromEnv(),
path: "/mcp",
tools: [
{ name: "read_note", description: "Read a note", suggested_scopes: ["notes:read"] },
{ name: "write_note", description: "Write a note", suggested_scopes: ["notes:write"] },
],
});
app.post("/mcp", (req, res) => {
const principal = (req as any).locals?.principal;
res.json({ jsonrpc: "2.0", id: req.body.id, result: { actor: principal?.sub } });
});
app.listen(8080);
That's the whole integration. Bearer validation, scope enforcement, RFC 9728 protected-resource metadata, manifest publishing, and structured denials all happen inside mountMCP.
You're writing an MCP server, API, or AI agent in TypeScript / Node.js using Express. Use @authsec/sdk 4.3.0 or newer for the runtime-first mountMCP path.
A protected /mcp endpoint that returns 401 / 403 with structured denial bodies, automatic protected-resource metadata at /.well-known/oauth-protected-resource/mcp, and tool manifest pushed to AuthSec on startup.
Install
npm install @authsec/sdk express
The five product steps
- Register the application in the AuthSec admin UI. Copy the env block.
- Install the SDK (above).
- Publish the tool manifest — the list of tools your application exposes, with each one's required scopes.
mountMCPdoes it for you on startup. Tools you pass appear on the Tools tab of the Application detail page. - Review tool access in the admin UI — map each tool to a scope, define which roles get that scope.
- Launch — flip the Application to Live; the SDK starts enforcing.
Configuration
Config matches the Python and Go SDKs field-for-field. Use loadConfigFromEnv() to read from environment variables:
| Env var | Config field | What it is |
|---|---|---|
AUTHSEC_RESOURCE_SERVER_ID | resourceServerId | Application UUID |
AUTHSEC_RESOURCE_URI | resourceUri | The Application's Resource URI |
AUTHSEC_RESOURCE_NAME | resourceName | Human-readable name for metadata and logs |
AUTHSEC_ISSUER | issuer | AuthSec's issuer URL |
AUTHSEC_AUTHORIZATION_SERVER | authorizationServer | AuthSec API origin for policy and manifest calls |
AUTHSEC_JWKS_URL | jwksUrl | Public key set for local JWT validation |
AUTHSEC_INTROSPECTION_URL | introspectionUrl | RFC 7662 introspection URL |
AUTHSEC_INTROSPECTION_CLIENT_ID | introspectionClientId | Basic-auth username, usually the Application ID |
AUTHSEC_INTROSPECTION_CLIENT_SECRET | introspectionClientSecret | Basic-auth password, shown once at registration or rotation |
AUTHSEC_VALIDATION_MODE | validationMode | jwt_and_introspect / jwt_only / introspection_only / jwt_or_introspect |
AUTHSEC_POLICY_MODE | policyMode | remote_required / remote_with_local_fallback / local_only / open |
AUTHSEC_PUBLISH_MANIFEST | publishManifest | true to push tool inventory on startup |
The admin UI's Show Config panel emits these as a paste-ready block — don't hand-write.
Compatibility aliases from older docs (AUTHSEC_RESOURCE, AUTHSEC_JWKS_URI, AUTHSEC_INTROSPECTION_ENDPOINT, AUTHSEC_INTROSPECTION_ID, AUTHSEC_INTROSPECTION_SECRET) are still accepted by the SDK, but new deployments should use the canonical names above.
Common scenarios
Observe-only (audit without blocking)
Set AUTHSEC_POLICY_MODE=observe. The SDK logs every decision but lets every request through. Use this when migrating an existing service to AuthSec without breakage.
Reading the principal in a tool handler
app.post("/mcp", (req, res) => {
const principal = (req as any).locals?.principal;
// principal.subject, principal.scopes, principal.claims.workspace_id
if (req.body.method === "tools/call" && req.body.params?.name === "read_note") {
return readNoteFor(principal.subject, req.body.params.arguments, res);
}
});
req.locals.principal is populated on every authorized request.
Manually constructing the Runtime (no Express)
If you're on Fastify, Koa, or a custom HTTP server, use Runtime directly:
import { Runtime, loadConfigFromEnv } from "@authsec/sdk";
const runtime = await Runtime.create(loadConfigFromEnv());
// In your request handler:
const result = await runtime.authorize(bearerToken, toolId);
if (!result.allowed) {
return reply.code(result.denial.status)
.header("WWW-Authenticate", result.denial.wwwAuthenticate)
.send({ error: result.denial.code, error_description: result.denial.description });
}
// result.principal is the validated principal
Both APIs in the same package
The package keeps the legacy decorator API (protectedByAuthSec, mcpTool, runMcpServerWithOAuth) alongside the new runtime. You can use either; they coexist.
// Legacy decorator API — still supported
import { protectedByAuthSec, runMcpServerWithOAuth } from "@authsec/sdk";
// New runtime API (Go/Python parity) — recommended for new projects
import { mountMCP, Runtime, loadConfigFromEnv } from "@authsec/sdk";
Troubleshooting
| Symptom | Most likely cause | Fix |
|---|---|---|
401 invalid_audience | AUTHSEC_RESOURCE_URI mismatch between SDK and admin UI | Copy the env block from the Application detail page; don't hand-write |
403 insufficient_scope | Token doesn't have the scope the tool requires | Open the user's consent in the admin UI; re-consent for the missing scope |
503 policy_unavailable | SDK can't reach AuthSec's /sdk-policy endpoint | Check network egress and the introspection credentials; the SDK denies all when policy is unavailable in remote_required mode |
npm install complains about jose | Old Node version | SDK requires Node 18+ |
Example app
A runnable example lives at examples/mcp-express/ in the SDK repo. About 50 lines, runs against any AuthSec workspace — see the example's README.
Related
- How AuthSec protects your MCP server — the request flow
- What is MCP and a resource server? — background
- Register an application — register before installing
- Python SDK — same API, in Python
- Go SDK — same API, in Go
- SDK FAQ