MCP

MCP integration for organizations

Streamable HTTP transport, JSON-RPC methods, OAuth 2.0 authorization code + PKCE, and Bearer authentication to /api/mcp.

What is MCP

MCP is a JSON-RPC protocol for tool discovery (tools/list), invocation (tools/call), and structured results. Stratalize exposes governed intelligence tools over HTTP using @modelcontextprotocol/sdk streamable transports on /api/mcp (org-authenticated) and /api/mcp-public (public reference surface).

Authentication

Org MCP uses OAuth 2.0 authorization code flow with PKCE (RFC 7636). Method S256 is required for code_challenge_method (app/api/mcp/oauth/authorize/route.ts).

Resource and authorization-server metadata follow RFC 9728:

  • Authorization endpoint https://www.stratalize.com/api/mcp/oauth/authorize
  • Token endpoint https://www.stratalize.com/api/mcp/oauth/token (POST, JSON or form body; returns access_token typed as Bearer opaque sk_mcp_* material)
  • Dynamic client registration https://www.stratalize.com/api/mcp/oauth/register
  • Protected resource metadata https://www.stratalize.com/.well-known/oauth-protected-resource
  • Authorization server metadata https://www.stratalize.com/.well-known/oauth-authorization-server

The token response does not include expires_in; keys remain valid until revoked. There is no refresh_token grant in the current token handler — mint a new key by repeating authorization or use dashboard key rotation via DELETE /api/mcp/keys and re-issue.

OAuth consent stores the authorized scope string on the resulting MCP API key. The authorize handler only accepts scopes from the discovery allowlist (see DISCOVERY_SCOPES in app/api/mcp/oauth/authorize/route.ts, mirrored in /.well-known/oauth-*). Tools that require scopes outside that allowlist need an org-issued key from POST /api/mcp/keys (session-authenticated user) with scopes from MCP_ALLOWLIST_SCOPES in lib/mcp/scopes.ts.

PKCE flow (TypeScript, Node 20+)

End-to-end: dynamic registration → verifier/challenge → browser authorization → code exchange. Replace REDIRECT_URI with a URI allowed for your MCP host (loopback is fine for local tooling). After step 4, open authorizeUrl in a browser, sign in to Stratalize, approve consent, then paste the code from the redirect query.

import crypto from "node:crypto";
import * as readline from "node:readline/promises";

const ORIGIN = "https://www.stratalize.com";
const REDIRECT_URI = "http://127.0.0.1:43123/mcp-callback";
const LISTEN_PORT = 43123;

function base64url(buf: Buffer): string {
  return buf.toString("base64url");
}

function randomVerifier(): string {
  return base64url(crypto.randomBytes(32));
}

function challengeS256(verifier: string): string {
  return base64url(crypto.createHash("sha256").update(verifier, "utf8").digest());
}

async function registerClient(): Promise<string> {
  const res = await fetch(`${ORIGIN}/api/mcp/oauth/register`, {
    method: "POST",
    headers: { "content-type": "application/json", accept: "application/json" },
    body: JSON.stringify({
      client_name: "Stratalize MCP doc example",
      redirect_uris: [REDIRECT_URI],
    }),
  });
  if (!res.ok) throw new Error(`register failed ${res.status}: ${await res.text()}`);
  const j = (await res.json()) as { client_id: string };
  return j.client_id;
}

async function waitForRedirectCode(): Promise<string> {
  const http = await import("node:http");
  return new Promise((resolve, reject) => {
    const server = http.createServer((req, res) => {
      if (!req.url) return res.end();
      const url = new URL(req.url, REDIRECT_URI);
      const code = url.searchParams.get("code");
      const err = url.searchParams.get("error");
      res.setHeader("content-type", "text/plain");
      if (code) {
        res.statusCode = 200;
        res.end("You can close this tab and return to the terminal.");
        server.close();
        resolve(code);
      } else {
        res.statusCode = 400;
        res.end(err ?? "missing code");
        server.close();
        reject(new Error(err ?? "missing code"));
      }
    });
    server.listen(LISTEN_PORT, "127.0.0.1", () => {
      console.log(`Listening on ${REDIRECT_URI} for OAuth redirect...`);
    });
  });
}

async function exchangeToken(params: {
  code: string;
  clientId: string;
  verifier: string;
}): Promise<string> {
  const res = await fetch(`${ORIGIN}/api/mcp/oauth/token`, {
    method: "POST",
    headers: { "content-type": "application/json", accept: "application/json" },
    body: JSON.stringify({
      grant_type: "authorization_code",
      code: params.code,
      redirect_uri: REDIRECT_URI,
      client_id: params.clientId,
      code_verifier: params.verifier,
    }),
  });
  if (!res.ok) throw new Error(`token failed ${res.status}: ${await res.text()}`);
  const j = (await res.json()) as { access_token: string };
  return j.access_token;
}

async function main() {
  const clientId = await registerClient();
  const codeVerifier = randomVerifier();
  const codeChallenge = challengeS256(codeVerifier);
  const state = crypto.randomUUID();

  const authorizeUrl = new URL(`${ORIGIN}/api/mcp/oauth/authorize`);
  authorizeUrl.searchParams.set("response_type", "code");
  authorizeUrl.searchParams.set("client_id", clientId);
  authorizeUrl.searchParams.set("redirect_uri", REDIRECT_URI);
  authorizeUrl.searchParams.set("scope", "read:benchmarks read:market");
  authorizeUrl.searchParams.set("code_challenge", codeChallenge);
  authorizeUrl.searchParams.set("code_challenge_method", "S256");
  authorizeUrl.searchParams.set("state", state);

  console.log("Open this URL, complete login + consent:\n", authorizeUrl.toString());
  const code = await waitForRedirectCode();
  const accessToken = await exchangeToken({ code, clientId, verifier: codeVerifier });
  console.log("access_token prefix:", accessToken.slice(0, 16));

  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  const answer = await rl.question(
    "Call tools/list against /api/mcp? [y/N] ",
  );
  rl.close();
  if (answer.trim().toLowerCase() !== "y") return;

  const mcpRes = await fetch(`${ORIGIN}/api/mcp`, {
    method: "POST",
    headers: {
      accept: "application/json, text/event-stream",
      "content-type": "application/json",
      authorization: `Bearer ${accessToken}`,
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
      method: "tools/list",
      params: {},
    }),
  });
  console.log("tools/list status:", mcpRes.status);
  console.log((await mcpRes.text()).slice(0, 2000));
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

Tool discovery

Call tools/list with a valid JSON-RPC envelope. Tool definitions include inputSchema (JSON Schema) suitable for client-side form generation. Execution is still subject to Bearer scopes, org permission-set surface checks, and role/resource gates implemented in app/api/mcp/route.ts and lib/mcp/server.ts; a tool appearing in tools/list does not imply the current key may call it successfully.

POST /api/mcp HTTP/1.1
Host: www.stratalize.com
Authorization: Bearer sk_mcp_…
Accept: application/json, text/event-stream
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "method": "tools/list",
  "params": {}
}

Invalid arguments on methods that validate params surface as JSON-RPC -32602 Invalid params where the MCP handler applies schema validation; transport-level parse failures return -32700 per JSON-RPC 2.0.

Tool invocation

Use tools/call with params.name and params.arguments. Successful structured JSON payloads from governed tools are signed with the Stratalize Ed25519 attestation envelope (_stratalize) where the server applies signMcpResponse. Error payloads may omit attestation; treat errors as unsigned unless the response explicitly includes _stratalize.

{
  "jsonrpc": "2.0",
  "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "method": "tools/call",
  "params": {
    "name": "get_spend_summary",
    "arguments": { "period": "30d" }
  }
}

Example result shape (illustrative)

{
  "jsonrpc": "2.0",
  "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{ \"data\": { … }, \"_stratalize\": { \"signed_at\": \"…\", \"signature\": \"…\", \"public_key_url\": \"https://www.stratalize.com/api/trust/signing-key\", \"canonicalization\": \"JCS-RFC8785\", \"version\": \"1\" } }"
      }
    ]
  }
}

Scopes and permissions

Least privilege: each tool declares a minimum scope in MCP_TOOL_REQUIRED_SCOPE. A small set of org-authenticated agent-definition tools bypass per-key scope checks while remaining tenant-bound (lib/mcp/org-mcp-tool-gate.ts). Keys carrying admin satisfy any tool scope check. The table below maps the product scope taxonomy to the scope strings enforced in code today (lib/mcp/scopes.ts).

Taxonomy (docs)Enforced scope stringsCovers
read:public— (no Bearer on /api/mcp-public)Free reference tools and x402-gated tools per public MCP policy; not an OAuth scope string.
read:org`read:spend`, `read:vendors`, `read:benchmarks`, `read:integrations`, `read:business_memory`, `read:health`, …Org-context economic and vendor intelligence reads (exact tool → scope map in code).
read:roles`read:roles`Executive role intelligence briefs (`get_*_brief` tools) and related role-scoped reads.
read:agents`read:intelligence`Agent inventory, execution reads, governance analytics, lineage (`get_data_lineage`), pending approvals listing.
read:governance`read:intelligence`Compliance coverage, policy/adoption signals, regulatory report generation entrypoints guarded as intelligence reads.
read:lineage`read:intelligence`Synthesis lineage and anomaly surfaces that are classified as intelligence reads in `MCP_TOOL_REQUIRED_SCOPE`.
write:agents`write:agents`Agent and permission-set mutators (`create_agent`, `update_agent`, `create_permission_set`, …) per code map.
write:permissions`write:agents`Permission-set create/update and org invites are enforced under `write:agents` today.
write:approvals`write:approvals`Approver decisions on pending governed executions (`approve_agent_execution`, `reject_agent_execution`).

Additional allowlisted scopes

Keys may also include, where issued: read:market, read:actions, read:alerts, read:healthcare, write:strategy, ask:stratalize, read:trader, admin — see McpScope and MCP_ALLOWLIST_SCOPES in lib/mcp/scopes.ts. get_morning_briefing requires read:roles, read:actions, and read:intelligence simultaneously (app/api/mcp/route.ts).

Public vs org-authenticated

Stratalize splits governed tools across two HTTP surfaces. Counts follow the same platform inventory as /docs: 175 unique MCP tools total.

  • Public surface (108 tools) — No OAuth Bearer. Includes 11 free reference tools and 97 tools reachable via x402 micropayments. HTTP entrypoints: /api/mcp-public (MCP JSON-RPC) and /api/x402/… (paid). Paid tools called from the public MCP host without payment return x402_required with routing hints (see /docs/x402).
  • Org-authenticated surface (+67 tools) — Requires Authorization: Bearer sk_mcp_… on POST /api/mcp (same Streamable HTTP + JSON-RPC conventions). Unauthenticated requests receive 401 with an explicit JSON error message.

Quickstart (tools/list)

After OAuth exchange (or when using a dashboard-issued key), call /api/mcp with JSON-RPC. Use the same Accept header as production clients.

const res = await fetch("https://www.stratalize.com/api/mcp", {
  method: "POST",
  headers: {
    authorization: `Bearer ${process.env.STRATALIZE_MCP_KEY}`,
    accept: "application/json, text/event-stream",
    "content-type": "application/json",
  },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: "550e8400-e29b-41d4-a716-446655440000",
    method: "tools/list",
    params: {},
  }),
});
console.log(res.status, await res.text());

Failure modes

ClassMeaning
-32700 Parse errorMalformed JSON-RPC payload or invalid JSON body (HTTP 400 on bad JSON).
-32600 Invalid RequestJSON-RPC object missing required members per MCP handler rules.
-32601 Method not foundUnknown `method` string for this server build.
-32602 Invalid paramsTool arguments failed schema validation for `tools/call`.
-32603 Internal errorUnhandled tool or transport failure inside the MCP server stack.
401 UnauthorizedMissing/invalid Bearer material for `/api/mcp`.
403 insufficient_scope / tool_not_scopedKey lacks `MCP_TOOL_REQUIRED_SCOPE` entry or required scope; or tool missing from scope map.
403 Access deniedScope passed but org role/resource policy denied the call (role briefs, cross-role insights, etc.).
403 SENSITIVITY_TIER_VIOLATIONOrg subscription tier insufficient for tool sensitivity class.
429 rate_limit_exceededOrg or public per-key/IP limits; includes `Retry-After` when returned by public surface.

Registry

Stratalize publishes MCP registry manifests under the com.stratalize/* namespace, including com.stratalize/governance, com.stratalize/finance, com.stratalize/healthcare, com.stratalize/realestate, com.stratalize/crypto, and com.stratalize/intelligence (see repository mcp/server-*.json). Browse listings at registry.modelcontextprotocol.io. Curated registry JSON for Stratalize is also served at GET /api/mcp/registry.

Runtime attestation for signed JSON uses the public key at /api/trust/signing-key (JWK). Registry submission signing is described alongside trust operations in /docs/attestation.

Related: x402, Tool catalog, /.well-known/mcp.json.