MCP TypeScript Client¶
The official @modelcontextprotocol/client package, pinned to revision 2026-07-28, lists and calls Mission Control's governed tools with a key or with browser sign-in; the gateway's conformance suite runs exactly this client.
Prerequisites and installation¶
Use Node.js 24 and @modelcontextprotocol/client@2.0.0, the package pinned by the gateway's conformance suite. Complete Get a personal key and use that scratch directory and shell environment. The caller needs tool.invoke and Team allowance for the selected tool.
In the mission-control-client directory:
List schemas and call a selected tool¶
Save this complete program as mcp-client.mts. Initially leave MISSION_CONTROL_TOOL unset: the program only discovers and lists schemas. Choose a read-only tool and prepare a JSON object matching its inputSchema before enabling invocation.
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
const origin = process.env.MISSION_CONTROL_URL;
const key = process.env.MISSION_CONTROL_API_KEY;
if (!origin || !key) throw new Error("Set MISSION_CONTROL_URL and MISSION_CONTROL_API_KEY.");
const transport = new StreamableHTTPClientTransport(new URL(`${origin.replace(/\/$/, "")}/mcp`), {
requestInit: { headers: { authorization: `Bearer ${key}` } },
fetch: async (input, init) => {
const response = await fetch(input, init);
console.log("Trace ID:", response.headers.get("x-mission-control-trace-id"));
console.log("Correlation ID:", response.headers.get("x-mission-control-correlation-id"));
return response;
},
});
const client = new Client({ name: "my-app", version: "1.0.0" }, {
capabilities: {},
supportedProtocolVersions: ["2026-07-28"],
versionNegotiation: { mode: { pin: "2026-07-28" } },
});
try {
await client.connect(transport);
const { tools } = await client.listTools();
for (const tool of tools) console.log(tool.name, JSON.stringify(tool.inputSchema));
const name = process.env.MISSION_CONTROL_TOOL;
if (name) {
if (!tools.some((tool) => tool.name === name)) throw new Error("Selected tool is not visible.");
const args: unknown = JSON.parse(process.env.MISSION_CONTROL_TOOL_ARGUMENTS ?? "{}");
if (!args || typeof args !== "object" || Array.isArray(args)) throw new Error("Arguments must be a JSON object.");
const result = await client.callTool({ name, arguments: args as Record<string, unknown> });
console.log(JSON.stringify(result, null, 2));
if (result.isError) {
const meta = (result._meta ?? {}) as Record<string, unknown>;
console.error("Tool did not succeed:", meta["mission-control/code"] ?? "tool error");
// Display the returned text and any approval/connect link; do not automatically retry.
process.exitCode = 1;
}
}
} finally {
await client.close();
}
Run discovery:
After inspecting the schema, set the exact tool and arguments. These values require the explicit synthetic fixture described in Your first MCP tool:
export MISSION_CONTROL_TOOL=crm__lookup-customer
export MISSION_CONTROL_TOOL_ARGUMENTS='{"query":"customer-42"}'
node mcp-client.mts
Expected result: visible tool names and schemas, followed by the selected tool's content and metadata when a name is set. A JSON-RPC error rejects the SDK call; isError: true is an in-band tool outcome that your program must handle separately. No restart, new session, or configuration restart is required for an already enabled tool.
Pinning matters: Mission Control serves only 2026-07-28 and does not fall back to initialize. A client that negotiates downward fails with -32022; a client pinned to the revision connects with one server/discover.
Read the Trace headers¶
The complete program logs traceparent's Trace ID through X-Mission-Control-Trace-Id and also logs X-Mission-Control-Correlation-Id on every response. Keep the call response's ID, rather than discovery's ID. With observability.read, inspect it under Observe → Traces; audit.read additionally grants Audit → Decisions access.
To continue a Trace you own, add a valid traceparent to requestInit.headers; when grouping this with a model request, use the same Trace ID and a fresh parent Span ID. See Tracing.
Sign in instead of using a key¶
The runnable recipe above uses a key. Browser sign-in requires a real implementation of the SDK's OAuthClientProvider: registration and token persistence, PKCE verifier storage, authorization redirect handling, callback validation, and resuming transport authorization. Supplying an undefined provider object is not a runnable sign-in flow.
For a client with an implemented OAuth flow, follow Connect an AI client. The gateway exposes protected-resource metadata and the Console authorization server; compatible clients use dynamic registration and PKCE. Consent authorizes the person inside one Team for this MCP endpoint. It does not configure /v1 model access. Use a user or service virtual key for model recipes.
Your client must protect stored tokens and rotate refresh tokens correctly. Access tokens live one hour and refresh tokens rotate on use; people can revoke the connection under Virtual keys. See Authentication and authorization for the contract.
The following recovery snippets illustrate branches to add inside the complete program's try block in place of its selected-tool call. Names and arguments require corresponding discovered fixtures; these are not additional tools installed by the SDK.
Connect your own account¶
Some Connectors act as the calling person. Until you connect your own account under My connections, a call to one of their tools does not fail: it succeeds with a result marked as an error whose text names the Connector and the Console page to connect at, and whose _meta["mission-control/code"] is connector_user_auth_required. Nothing reached the upstream. Show the text, or follow the link, then call again.
const result = await client.callTool({
name: "records__lookup-record",
arguments: { query: "customer-42" },
});
const meta = (result._meta ?? {}) as Record<string, unknown>;
if (result.isError && meta["mission-control/code"] === "connector_user_auth_required") {
// The person connects once at meta["mission-control/connectUrl"]; the same call then succeeds.
console.log(String(meta["mission-control/connectUrl"]));
}
The link opens My connections with the Connector's card highlighted. A key or connection of another person never borrows the grant: each caller connects their own account. See Use your own account with a Connector.
Wait for an approval¶
A tool declared action pauses for a person unless an administrator declared it approval: none. The call does not fail: it returns a result marked as an error whose text links to the approval page and whose _meta["mission-control/code"] is approval_required, with mission-control/approvalId and mission-control/approvalUrl beside it. Nothing reached the upstream. Show the link; once a person has approved, repeat the identical call (same tool, same arguments) and it runs exactly once.
const call = {
name: "crm__create-ticket",
arguments: { title: "Refund for order 42" },
};
const result = await client.callTool(call);
const meta = (result._meta ?? {}) as Record<string, unknown>;
switch (result.isError ? meta["mission-control/code"] : undefined) {
case "approval_required":
// Ask a person to open meta["mission-control/approvalUrl"], then retry the identical call.
break;
case "approval_denied":
// The text carries the decider's note; the same call stays denied for 24 hours.
break;
case "approval_consumed":
// A concurrent identical call ran under this approval; inspect the recorded result before deciding whether your workflow is complete.
break;
default:
// The tool's ordinary result, or another in-band answer such as connector_user_auth_required.
}
Send exactly the same arguments on the retry: the approval is keyed by the caller, the tool, its declared schema and the canonical arguments, so a changed argument is a new call with its own approval. An approval expires 24 hours after the request, and Approvals → My requests in the Console shows its state and the note. See Approvals.
Handle denials and limits¶
Failures before dispatch use the JSON-RPC error codes below; the client rejects the call with an error that carries the code.
try {
await client.callTool({ name: "crm__create-ticket", arguments: {} });
} catch (error) {
const code = (error as { code?: number }).code;
if (code === -32003) {
// Not enabled in the Team, not permitted by your key, or hidden: nothing reached the tool.
} else if (code === -31029) {
// A Mission Control limit; honor the safe retry data in error.data.
} else if (
code === -32603 &&
(error as { data?: { code?: string } }).data?.code === "rule_blocked"
) {
// An administrator's rule blocked the arguments or the result; the decision names the rule.
} else {
throw error;
}
}
| Code | HTTP | Meaning |
|---|---|---|
-32003 |
200 (in-band) or 403 | Denied: outside Team/key allowance, ineligible tool, or an Origin or Team state problem. Hidden tools are refused in-band so that their existence is not disclosed. |
-31029 |
429 | A spend, request-rate, token-rate or concurrency limit; Retry-After and error.data carry safe retry details. |
-32020 |
400 | A required MCP header is missing or disagrees with the body. |
-32022 |
400 | Unsupported protocol revision; pin to 2026-07-28. |
-32001 |
401 | Authentication failed; the response carries the WWW-Authenticate challenge that starts sign-in. |
-32603 |
200 | The tool failed upstream (the fixed Tool invocation failed), or a rule blocked it (Tool invocation blocked by policy with data.code of rule_blocked). |