Model Context Protocol

Model Context Protocol server hardening — input validation, OAuth integration per RFC 9728, scope enforcement, audit emission.

The guard is the secure-by-default front door for an HTTP endpoint that speaks MCP. Every default refuses; operators opt into capabilities (dynamic client registration, specific tools, specific resources) deliberately. The 2025-2026 CVE class — auth-bypass on unauthenticated tool / resource invocations (CVE-2026-33032 class) plus OAuth redirect_uri abuse (CVE-2025-6514 class) plus the confused-deputy pattern when static client IDs combine with dynamic registration — is what the guard's defaults exist to close.

Wire format is JSON-RPC 2.0; parseRequest is the envelope validator (jsonrpc version, method shape, id type, params type) and refuse is the matching error responder so handlers stay in the same shape the guard rejects with. OAuth redirect_uris are exact-match against an allowlist and required to be HTTPS (or localhost) per RFC 9700 §4.1.1.

b.mcp.toolRegistry.create(opts) #

stable0.8.85
{
  tools:         array of { name, inputSchema, outputSchema?, description? }
  signingKey:    PEM string (required for signCall + register)
  verifyingKey:  PEM string (required for verifyCall on inbound calls)
  alg:           algorithm name (default "ml-dsa-87")
  ttlMs:         default call envelope TTL (default 5 minutes)
}

Build an MCP tool registry. Operator passes tools (array of tool descriptors) and signingKey (PEM). Each tool gets a signed descriptor blob { tool, alg, signature } that the operator can ship to the LLM-side runtime as an attestation. signCall / verifyCall produce + verify the per-call envelope.

Returned object is frozen at construction; tool changes go through register(tool) which re-signs the descriptor.

var registry = b.mcp.toolRegistry.create({
  tools: [
    { name: "search", inputSchema: { type: "object", properties: {
      query: { type: "string" } }, required: ["query"] } },
  ],
  signingKey: pair.privateKey,
  verifyingKey: pair.publicKey,
});

// Outbound call
var envelope = registry.signCall({
  toolName: "search",
  args:     { query: "blamejs" },
});
// → { envelope: { tool, argsHash, nonce, iat, exp }, signature: "..." }

// Inbound verify (operator supplies a seen() callback for replay defense)
var ok = registry.verifyCall(envelope, {
  seen: function (nonce) { return nonceStore.has(nonce); },
});

b.mcp.toolRegistry.create.signCall(opts) #

0.8.85
{
  toolName: string,   // required — must match a registered tool
  args:     object,   // tool input arguments
  nonce:    string,   // optional — caller-supplied; default 128-bit random hex
  ttlMs:    number,   // optional — overrides registry default
}

Build + sign an outbound tool-call envelope. Returns { envelope, signature, alg }. The envelope shape is { tool, argsHash, nonce, iat, exp } where argsHash is the SHA3-256 hex digest of canonical-JSON(args) so the server can verify the args bytes match without including them in the signature (smaller envelope, no double-encoding).

var env = registry.signCall({
  toolName: "search",
  args:     { query: "blamejs" },
});
// env.envelope.tool      === "search"
// env.envelope.argsHash  === 
// env.envelope.nonce     === 
// env.envelope.iat       === ISO timestamp
// env.envelope.exp       === ISO timestamp (iat + ttlMs)

b.mcp.toolRegistry.create.verifyCall(signedCall, opts?) #

0.8.85
{
  args:    object,                            // optional — when present, argsHash is checked
  seen:    function (nonce) → boolean,        // optional — replay-defense callback
  nowMs:   number,                            // optional — override Date.now() (testing only)
}

Verify an inbound tool-call envelope. Required signedCall shape is { envelope, signature, alg }. Returns true on success; throws mcp/call-verify-failed (or a more specific code) on any failure:

Replay defense: operator supplies opts.seen(nonce) → boolean. Common shape: Map.has(nonce) against an in-memory cache with TTL matching the envelope's exp - iat. Without a seen() callback, replay defense is skipped (caller's choice).

try {
  var ok = registry.verifyCall(signedFromClient, {
    args: actualArgs,
    seen: function (nonce) { return nonceCache.has(nonce); },
  });
} catch (e) {
  if (e.code === "mcp/call-replay")   return refuseReplay();
  if (e.code === "mcp/call-expired")  return refuseExpired();
  throw e;
}

b.mcp.parseRequest(body, opts) #

0.7.68
{
  errorClass: Function,   // default McpError; inject for custom error classes
}

Validate a JSON-RPC 2.0 envelope. Accepts a raw string (parsed via b.safeJson.parse with a 1 MiB cap) or an already-parsed object. Throws an McpError with a code matching the violation (BAD_JSON / BAD_ENVELOPE / BAD_VERSION / BAD_METHOD / BAD_ID / BAD_PARAMS). Returns the parsed envelope on success.

var envelope = b.mcp.parseRequest('{"jsonrpc":"2.0","method":"tools/list","id":1}', {});
envelope.method;
// → "tools/list"

b.mcp.refuse(res, code, message, id) #

0.7.68

Write a JSON-RPC 2.0 error reply to res. The code is the negative JSON-RPC error code (-32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error, -32001 auth required); HTTP status is mapped from it (parse / invalid-request -> 400, method-not-found -> 404, internal -> 500, default -> 400). id defaults to null when undefined per the spec for unidentifiable requests.

var http = require("http");
var srv  = http.createServer(function (req, res) {
  b.mcp.refuse(res, -32601, "method not found", 7);
});
srv.listen(0);
// → writes { jsonrpc: "2.0", error: { code: -32601, message: "method not found" }, id: 7 }
srv.close();

b.mcp.serverGuard(opts) #

0.7.68
{
  requireBearer:           boolean,                                 // default true
  verifyBearer:            function,                                // (token, req) -> Promise
  redirectUriAllowlist:    Array,                           // exact-match URIs
  allowDynamicRegister:    boolean,                                 // default false
  registerClientAllowlist: function,                                // (body) -> bool — required when allowDynamicRegister
  toolAllowlist:           Array,                           // null = allow any shape-valid tool
  resourceAllowlist:       Array,                           // null = allow any shape-valid resource
  maxBodyBytes:            number,                                  // default 1 MiB
  errorClass:              Function,                                // default McpError
  audit:                   boolean,                                 // default true
}

Build the MCP request-lifecycle middleware. Bearer-required by default (operator supplies verifyBearer to validate the token); dynamic-client-registration refused by default; redirect_uris exact-match an HTTPS-or-localhost allowlist; tool / resource names are shape-validated and optionally allowlist-gated; the body is read through a bounded chunk collector. Every refusal emits an audit event (mcp.auth.missing-bearer / mcp.tool.refused / etc.) unless audit:false. Returns a (req, res, next) middleware function that attaches req.mcpRequest + req.mcpClaims on success.

var guard = b.mcp.serverGuard({
  requireBearer: true,
  verifyBearer:  function (token, _req) {
    return token === "operator-issued-bearer-token-32-chars-min" ? { sub: "ops" } : null;
  },
  toolAllowlist:     ["search.docs", "search.tickets"],
  resourceAllowlist: ["mcp://docs/handbook"],
});
typeof guard;
// → "function"

b.mcp.toolResult.sanitize(result, opts?) #

0.8.70
{
  {
    posture?:        "refuse" | "sanitize" | "audit-only",  // default "refuse"
    maxTextBytes?:   number,    // default 64 KiB per content block
    allowedHosts?:   string[],  // for image/audio/resource_link refs
  }
}

OWASP LLM02 — model/tool-output sanitization. MCP tool calls frequently return content the host model interprets as further instructions; an attacker-controlled tool surface can return { type: "text", text: "Ignore prior instructions and ..." }, , OR markdown image links pointing at exfiltration endpoints. The framework's defense:

- Strip / refuse executable HTML (