Skip to content

The TypeScript framework

Everything in the preceding pages — the contract, forwarding the user’s auth, the bind endpoint — is plain Model Context Protocol plus a small set of HTTP conventions. You can implement it in any language with no dependency on DioscHub, and nothing here changes that.

If you build in TypeScript, there is an optional package that implements that contract for you: @dioschub/mcp-server. You write tools; it owns the auth plumbing — the BYOA token broker, the opaque handle, the store, the tool-call auth gate, and the result wrapping. A server becomes a list of tools and one createMcpServer call.

The framework is the reference implementation of the auth-forwarding flow. It holds two properties by construction:

  • Credential-blind, enforced. Your app binds its native artifacts to the framework once, server-to-server. The framework mints a handle-JWT — a random jti, an audience, and an expiry, and nothing else — and hands only that to the Hub. The Hub replays the handle on every tool call; your real credentials never leave the MCP server. The model and the Hub see the handle, never the secret. A devGuard even scans tool results for leaked artifact values and screams if one appears.
  • Store miss → 401, never 500. A missing, expired, or evicted session returns a clean 401 with WWW-Authenticate: Bearer. That is exactly the signal that trips the Hub’s mid-turn re-auth, so the widget re-binds and the turn continues. You get automatic recovery instead of a dropped call.
  • The admin-key trap, caught loudly. The bind route rejects a non-admin key with 403 up front, not with a confusing downstream failure — so wiring an embed key where an admin key belongs fails at the door with a clear message.
Terminal window
npm install @dioschub/mcp-server

A whole server is a createMcpServer call, one or more tool registrations, and listen:

import { createMcpServer } from '@dioschub/mcp-server';
import { z } from 'zod';
// Your app's artifact shape. The framework is blind to it; you own what's inside.
interface OrdersAuth {
sessionCookie: string;
}
const server = createMcpServer<OrdersAuth>({
name: 'orders-mcp', // also the JWT audience
adminKey: process.env.ADMIN_BIND_KEY!, // authenticates your app's /bind call
jwtSecrets: process.env.MCP_JWT_SECRET!, // signs the opaque handle
hub: {
url: process.env.HUB_URL!,
apiKey: process.env.HUB_API_KEY!, // a diosc_ak_… admin key, scope auth:bind
},
});
server.tool({
name: 'list_orders',
description: 'List the signed-in customer’s orders',
input: z.object({ status: z.enum(['open', 'shipped', 'all']).default('all') }),
handler: async ({ status }, ctx) => {
// ctx.auth is exactly what your app sent at /bind — you know its shape.
const res = await fetch(`${process.env.API}/orders?status=${status}`, {
headers: { cookie: ctx.auth.sessionCookie },
});
return res.json(); // plain data → wrapped into an MCP result for you
},
});
server.listen(8080);

That is the entire server. The framework owns POST /bind and POST /mcp; you never touch the transport, the JWT, or the auth header.

Business arguments come first, validated against the tool’s z.object({...}). Everything else arrives as the second argument, ctx:

FieldWhat it is
ctx.authYour native artifacts, replayed verbatim — the framework never parses them.
ctx.connectionIdThe Hub connection this call is bound to, for correlation.
ctx.loggerA redaction-aware logger scoped to this tool and connection. It never logs ctx.auth.
ctx.signalAborts when the caller disconnects — honor it at the next boundary (drain, don’t abort).

Return plain data and the framework wraps it into an MCP result. Return an object with a content array to take full control of the response.

FieldRequiredNotes
nameyesServer name; also the JWT audience (aud).
adminKeyyesAuthenticates your app’s /bind call. Must be an admin key — an embed key here is a 403.
jwtSecretsyesHS256 secret, or an array. The first signs; all verify — add a new one at the front to rotate with zero downtime.
hubyes*{ url, apiKey, bindPath?, apiKeyHeader? }. apiKey is a diosc_ak_… admin key scoped auth:bind. Defaults: path /api/auth/bind, header x-api-key.
hubClientyes**Provide hub or a custom hubClient (your own HubBinder — custom retry, transport, or a test fake).
storenoMemoryArtifactStore (default) or RedisArtifactStore.
ttlSecondsnoSession and JWT lifetime. Default 28800 (8 hours).
basePathnoMount prefix for /bind and /mcp.
devGuardnoScan tool results for leaked artifacts. Default: on unless NODE_ENV=production.

Mount your own routes — a health check, readiness — on server.app, the underlying Express app.

Registering the server as a tool source is the same for any MCP server, framework or not: register it as a conduit instance (authConfig: {}, so the Hub forwards per-user auth), attach it to an Assistant, and restart the Hub so it scans the new tools into its graph. That flow — and the boot-scan restart it depends on — is covered once in MCP servers & Toolsets.

MemoryArtifactStore (the default) keeps bound sessions in process, so it is single-instance only — after a restart every user re-binds, correctly but visibly, through the 401 path. For more than one replica, a handle minted on replica A has to resolve on replica B, so back the store with Redis:

import { createMcpServer, RedisArtifactStore } from '@dioschub/mcp-server';
const server = createMcpServer<OrdersAuth>({
// …name, adminKey, jwtSecrets, hub…
store: new RedisArtifactStore({ url: process.env.REDIS_URL }),
});

The store keys on the JWT jti, never the raw credential.