Identify the caller, isolate their state, cap their request rate. Make PKB-TS safe to expose.
MCP Servers Module 5
Any client that can reach /sse opens a session. One process, one notes store, no caller identity.
Bearer auth, per-tenant note store, per-tenant rate limit, per-session transport map.
"Three independent guarantees: who, what data, how often."
Each is independent. Getting one wrong does not save you on the others.
export function bearerAuth(req, res, next) {
const m = (req.header("authorization") ?? "").match(/^Bearer\s+(\S+)$/i);
if (!m) return res.status(401).json({ error: "missing bearer token" });
const tenant = TOKENS.get(m[1]);
if (!tenant) return res.status(401).json({ error: "invalid token" });
req.tenant = tenant;
next();
}
Token → tenant lookup at the edge. No tenant means 401 before the SDK ever sees the request.
Danger: Tokens are the simplest auth that works — not the strongest.
For the L14 lab, PKB_TOKENS="alice:tok_alice,bob:tok_bob" is enough. For real deployments you graduate to OAuth (L09 covered the flow) or a managed identity provider. The middleware shape — Authorization: Bearer X → tenant — does not change; only the lookup behind it does.
const sessions = new Map<string, { transport: SSEServerTransport; tenant: string }>();
app.get("/sse", bearerAuth, async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
sessions.set(transport.sessionId, { transport, tenant: req.tenant });
res.on("close", () => sessions.delete(transport.sessionId));
await server.connect(transport);
});
The SDK's SSEServerTransport exposes a sessionId you key the map on. The POST /messages route looks the session up by ?sessionId= query param.
const STORES = new Map<string, Map<string, string>>();
export function storeFor(tenant: string) {
let s = STORES.get(tenant);
if (!s) { s = new Map(); STORES.set(tenant, s); }
return s;
}
storeFor(tenant) is the only way tool code reaches notes. The tenant id comes from the authenticated session, never from request args.
Danger: Never let tool args carry the tenant id.
If a pkb.fetch arg looks like { "tenant": "...", "id": "..." }, a caller can pass any tenant id. Source the tenant from the authenticated session and override any arg field with the same name. Trust the session, not the wire.
// Refill REFILL_PER_MIN tokens/min, cap CAPACITY.
function check(tenant) {
const b = BUCKETS.get(tenant) ?? { tokens: CAPACITY, lastRefill: Date.now() };
// ... refill, decrement, return { allowed, remaining, retryAfterMs } ...
}
Per-tenant bucket. CAPACITY tokens of burst; REFILL_PER_MIN sustained rate. Rejected calls return { isError: true, content: ... "retry after X ms" }.
Inside the CallTool handler, after tenant resolution. Per-IP rate limit at the edge (Caddy) is a separate, complementary layer — it catches anonymous floods before they hit the SDK. Per-tenant rate limit catches abusive authenticated clients.
Use both. Edge stops the unknown; in-process stops the known-but-misbehaving.
phase=denied reason=... (no token / unknown token / expired token).These three patterns cover ~90% of real abuse traffic.
auth.ts, store.ts, rateLimit.ts.server.ts with the session map.pkb.search / pkb.fetch / pkb.write.