Skip to main content

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.

When to use this

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.

What you get

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

  1. Register the application in the AuthSec admin UI. Copy the env block.
  2. Install the SDK (above).
  3. Publish the tool manifest — the list of tools your application exposes, with each one's required scopes. mountMCP does it for you on startup. Tools you pass appear on the Tools tab of the Application detail page.
  4. Review tool access in the admin UI — map each tool to a scope, define which roles get that scope.
  5. 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 varConfig fieldWhat it is
AUTHSEC_RESOURCE_SERVER_IDresourceServerIdApplication UUID
AUTHSEC_RESOURCE_URIresourceUriThe Application's Resource URI
AUTHSEC_RESOURCE_NAMEresourceNameHuman-readable name for metadata and logs
AUTHSEC_ISSUERissuerAuthSec's issuer URL
AUTHSEC_AUTHORIZATION_SERVERauthorizationServerAuthSec API origin for policy and manifest calls
AUTHSEC_JWKS_URLjwksUrlPublic key set for local JWT validation
AUTHSEC_INTROSPECTION_URLintrospectionUrlRFC 7662 introspection URL
AUTHSEC_INTROSPECTION_CLIENT_IDintrospectionClientIdBasic-auth username, usually the Application ID
AUTHSEC_INTROSPECTION_CLIENT_SECRETintrospectionClientSecretBasic-auth password, shown once at registration or rotation
AUTHSEC_VALIDATION_MODEvalidationModejwt_and_introspect / jwt_only / introspection_only / jwt_or_introspect
AUTHSEC_POLICY_MODEpolicyModeremote_required / remote_with_local_fallback / local_only / open
AUTHSEC_PUBLISH_MANIFESTpublishManifesttrue 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

SymptomMost likely causeFix
401 invalid_audienceAUTHSEC_RESOURCE_URI mismatch between SDK and admin UICopy the env block from the Application detail page; don't hand-write
403 insufficient_scopeToken doesn't have the scope the tool requiresOpen the user's consent in the admin UI; re-consent for the missing scope
503 policy_unavailableSDK can't reach AuthSec's /sdk-policy endpointCheck network egress and the introspection credentials; the SDK denies all when policy is unavailable in remote_required mode
npm install complains about joseOld Node versionSDK 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.