Skip to content

Your First MCP Tool

Purpose

Discover governed tools, inspect their input schema, call one read-only tool, and verify its Trace. The MCP gateway defines the wire contract.

Prerequisites

Complete Get a personal key and keep its shell environment in the mission-control-client scratch directory. You need tool.invoke at the tool's scope and Team allowance; a model-only key policy is insufficient.

These examples require an administrator-provided Data fixture: server crm, tool lookup-customer, input schema with required string query, and a synthetic customer-42 record. This fixture is illustrative and is not installed automatically. Before running the combined snippets, inspect discovery with the command below. If your deployment has another read-only tool, replace every crm__lookup-customer and its arguments in the chosen snippet to match its returned inputSchema. Never guess arguments or use an Action tool just to test connectivity.

No restart or configuration change is needed for calls to an already configured and enabled tool. A missing fixture requires administrator configuration and Team allowance first.

1. Inspect discovery and choose a tool

The complete TypeScript SDK recipe lists schemas without invoking anything when no tool name is set. Run it first, or copy only the tools/list part of a raw recipe below. Check the exact server__tool name and inputSchema, then select the safe fixture described above.

2. List and call the selected fixture

Run the shell block directly. For Python, install requests with python3 -m pip install requests in your scratch virtual environment, save the block as first_tool.py, and run python3 first_tool.py. For TypeScript, save as first-tool.mts and run node first-tool.mts with Node.js 24; this raw recipe needs no packages. These snippets show both discovery and invocation; only run the full block after selecting the fixture.

Mission Control speaks MCP revision 2026-07-28 only: stateless POST requests, a per-request _meta envelope, and the MCP-Protocol-Version, Mcp-Method and (for calls) Mcp-Name headers. The MCP TypeScript client provides a complete SDK recipe; the raw shape is shown here so you can see what is on the wire.

ENVELOPE='{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"quickstart","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}'

curl --fail-with-body --include \
  --request POST \
  --header "Authorization: Bearer $MISSION_CONTROL_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Accept: application/json, text/event-stream" \
  --header "MCP-Protocol-Version: 2026-07-28" \
  --header "Mcp-Method: tools/list" \
  --data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{\"_meta\":$ENVELOPE}}" \
  "$MISSION_CONTROL_URL/mcp"

curl --fail-with-body --include \
  --request POST \
  --header "Authorization: Bearer $MISSION_CONTROL_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Accept: application/json, text/event-stream" \
  --header "MCP-Protocol-Version: 2026-07-28" \
  --header "Mcp-Method: tools/call" \
  --header "Mcp-Name: crm__lookup-customer" \
  --data "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"crm__lookup-customer\",\"arguments\":{\"query\":\"customer-42\"},\"_meta\":$ENVELOPE}}" \
  "$MISSION_CONTROL_URL/mcp"
import os
import requests

GATEWAY = os.environ["MISSION_CONTROL_URL"].rstrip("/")
KEY = os.environ["MISSION_CONTROL_API_KEY"]

ENVELOPE = {
    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
    "io.modelcontextprotocol/clientInfo": {"name": "quickstart", "version": "1.0.0"},
    "io.modelcontextprotocol/clientCapabilities": {},
}


def mcp(method: str, params: dict, name: str | None = None) -> dict:
    headers = {
        "Authorization": f"Bearer {KEY}",
        "Accept": "application/json, text/event-stream",
        "MCP-Protocol-Version": "2026-07-28",
        "Mcp-Method": method,
    }
    if name:
        headers["Mcp-Name"] = name
    body = {"jsonrpc": "2.0", "id": 1, "method": method, "params": {**params, "_meta": ENVELOPE}}
    response = requests.post(f"{GATEWAY}/mcp", json=body, headers=headers, timeout=30)
    response.raise_for_status()
    payload = response.json()
    print("Trace ID:", response.headers.get("x-mission-control-trace-id"))
    if "error" in payload:
        raise RuntimeError(payload["error"])
    return payload


tools = mcp("tools/list", {})["result"]["tools"]
print([tool["name"] for tool in tools])
result = mcp(
    "tools/call",
    {"name": "crm__lookup-customer", "arguments": {"query": "customer-42"}},
    name="crm__lookup-customer",
)
print(result["result"]["content"])
const GATEWAY = process.env.MISSION_CONTROL_URL?.replace(/\/$/, "");
const KEY = process.env.MISSION_CONTROL_API_KEY;
if (!GATEWAY || !KEY) throw new Error("Set MISSION_CONTROL_URL and MISSION_CONTROL_API_KEY.");
const ENVELOPE = {
  "io.modelcontextprotocol/protocolVersion": "2026-07-28",
  "io.modelcontextprotocol/clientInfo": { name: "quickstart", version: "1.0.0" },
  "io.modelcontextprotocol/clientCapabilities": {},
};

async function mcp(method: string, params: object, name?: string) {
  const response = await fetch(`${GATEWAY}/mcp`, {
    method: "POST",
    headers: {
      authorization: `Bearer ${KEY}`,
      "content-type": "application/json",
      accept: "application/json, text/event-stream",
      "mcp-protocol-version": "2026-07-28",
      "mcp-method": method,
      ...(name ? { "mcp-name": name } : {}),
    },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params: { ...params, _meta: ENVELOPE } }),
  });
  if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
  console.log("Trace ID:", response.headers.get("x-mission-control-trace-id"));
  const payload = await response.json();
  if (payload.error) throw new Error(JSON.stringify(payload.error));
  return payload;
}

const { result } = await mcp("tools/list", {});
console.log(result.tools.map((tool: { name: string }) => tool.name));
const called = await mcp(
  "tools/call",
  { name: "crm__lookup-customer", arguments: { query: "customer-42" } },
  "crm__lookup-customer",
);
console.log(called.result.content);

Expected tools/list shape:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "tools": [
      {
        "name": "crm__lookup-customer",
        "description": "Look up a customer record",
        "inputSchema": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] }
      }
    ],
    "ttlMs": 0,
    "cacheScope": "private",
    "_meta": {
      "io.modelcontextprotocol/serverInfo": { "name": "mission-control-mcp-gateway", "version": "1.0.0" }
    }
  }
}

Tool names are server__tool. A tool you cannot see is not described, and calling it is refused in-band with -32003 on HTTP 200 so that hidden tools stay hidden.

3. Verify the result

A successful call returns HTTP 200 with a JSON-RPC result containing content. Check for both a top-level error and result.isError: HTTP 200 alone does not prove the tool ran. Keep X-Mission-Control-Trace-Id from the call response. Locate it under Observe → Traces with observability.read; a reader with audit.read can find the tool:crm/lookup-customer decision under Audit → Decisions.

Bounded tool charges

For tools under finite spend caps, configure the actual tool's costPerCallMicros in reviewed YAML. An explicit 0 declares known-zero cost; absent pricing remains unknown and can be refused before dispatch. Every physical business call is counted once, with no model-token charge; model rounds are metered separately. Restart gateway/Console after price changes. See tool pricing.

Failure recovery

  • -32022: the client uses an unsupported revision; pin 2026-07-28.
  • -32020: match the protocol, method, and call-name headers to the JSON body.
  • -32003: the tool is hidden or denied; check Team allowance and the actual key policy.
  • result.isError with _meta["mission-control/code"] equal to connector_user_auth_required: open the supplied My connections link, connect your own account, and retry.
  • approval_required: nothing ran; show the approval URL and retry the identical call only after approval. See Approvals.
  • -31029 / HTTP 429: honor Retry-After and safe retry metadata.