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; returnsaccess_tokentyped asBeareropaquesk_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 strings | Covers |
|---|---|---|
| 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 returnx402_requiredwith routing hints (see /docs/x402). - Org-authenticated surface (+67 tools) — Requires
Authorization: Bearer sk_mcp_…onPOST /api/mcp(same Streamable HTTP + JSON-RPC conventions). Unauthenticated requests receive401with 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
| Class | Meaning |
|---|---|
| -32700 Parse error | Malformed JSON-RPC payload or invalid JSON body (HTTP 400 on bad JSON). |
| -32600 Invalid Request | JSON-RPC object missing required members per MCP handler rules. |
| -32601 Method not found | Unknown `method` string for this server build. |
| -32602 Invalid params | Tool arguments failed schema validation for `tools/call`. |
| -32603 Internal error | Unhandled tool or transport failure inside the MCP server stack. |
| 401 Unauthorized | Missing/invalid Bearer material for `/api/mcp`. |
| 403 insufficient_scope / tool_not_scoped | Key lacks `MCP_TOOL_REQUIRED_SCOPE` entry or required scope; or tool missing from scope map. |
| 403 Access denied | Scope passed but org role/resource policy denied the call (role briefs, cross-role insights, etc.). |
| 403 SENSITIVITY_TIER_VIOLATION | Org subscription tier insufficient for tool sensitivity class. |
| 429 rate_limit_exceeded | Org 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.