Skip to content
Docs menu

Docs The MCP surface

The MCP surface

The three tools a code-mode profile exposes, what the model's code can and cannot do inside the sandbox, how approvals travel over MCP, and the default limits.

A code-mode profile exposes exactly three tools. Nothing else is always loaded, so the fixed cost of the surface, tool definitions and server instructions together, has a target of 1,500 tokens or fewer. The server targets MCP 2026-07-28 (stateless requests, multi-round-trip input) and falls back for older clients.

Annotations: read-only, idempotent.

Find the user's tools and skills by what you want to do, for example "create a GitHub issue" or "recent Slack messages". Returns tool addresses with one-line TypeScript signatures, best match first. Read full types with describe before writing code. Pass namespace to search or list one source.

Field Type Notes
query string, 1–200 Empty lists a namespace
namespace string Optional
limit integer, 1–25 Default 8

It returns plain text, best match first. A tool marked [asks] needs approval under your current policy, and tools your policy denies are hidden, with a count.

github  (connections: personal*, work)
  github.createIssue(input: { owner: string; repo: string; title: string; body?: string; labels?: string[] }): Promise<Issue>   [asks]
    Create an issue in a repository.
  github.searchIssues(input: { q: string; perPage?: number; page?: number }): Promise<IssueSearchResult>
    Search issues and pull requests.
3 more matches. Narrow the query or pass namespace.

Ranking is BM25F over tool names, descriptions and parameter names, and never over connection names.

describe

Annotations: read-only, idempotent.

Get exact TypeScript declarations for tools: an address (github.createIssue), a whole namespace (github, budget permitting), or a type named in an earlier declaration (github.Issue). Write execute code against these types.

It takes targets, a list of 1 to 10 strings, and returns one declare const <namespace> block per namespace, with required fields first and each field's description as JSDoc. Output stays within a token budget, 2,500 by default; deeper types become named interfaces you can describe on their own.

declare const github: {
	/** Search issues and pull requests. */
	searchIssues(input: {
		/** Search query, GitHub syntax. */
		q: string;
		perPage?: number;
		page?: number;
	}, options?: CallOptions): Promise<github.IssueSearchResult>;
};

execute

Annotations: not read-only, open world. It has an output schema.

Run JavaScript in a sandbox where each of the user's sources is a global object of async functions: await github.createIssue({ owner, repo, title }). TypeScript annotations are allowed and stripped. Write the body of an async function and return what you need; run independent calls in parallel with Promise.all. There is no network or filesystem besides these functions. If a call needs the user's approval or a connection, the result says so and a page opens on the user's screen; tell the user, then continue with { "resume": "<runId>" } after they decide. Never ask the user for passwords or tokens in chat.

Send exactly one of code (up to 64 KB) or resume (a run id). The result is an envelope, as structured content plus one compact line of text. Abridged, without the M2 fields:

interface ExecuteEnvelope {
	runId: string;
	status: "completed" | "failed" | "awaiting_approval" | "awaiting_connection" | "cancelled";
	result?: unknown;
	error?: { code: string; message: string; hint?: string };
	logs?: string[];
	calls: { address: string; ok: boolean; ms: number }[];
	pending?: { id: string; kind: "approval" | "connection"; address?: string; summary: string; message?: string }[];
	truncated?: { result?: boolean; logs?: boolean };
	notices?: string[];
}

The hub forgives common code shapes. It takes the body of a fenced block, unwraps export default, calls a lone function, and returns a final expression, and each rewrite adds one line to notices so the model learns what changed.

const bugs = await github.searchIssues({ q: "repo:acme/app is:open label:bug" });
const top = bugs.items.slice(0, 3).map((i) => `• ${i.title} (#${i.number})`);
await slack.postMessage({ channel: "#eng", text: top.join("\n") });
return top;

Inside the sandbox

Both sandbox drivers load the same guest prelude, so this list holds everywhere.

  • Your sources as frozen global objects, one per namespace. Calling an unknown tool raises a TypeError that names the closest tools.
  • hub.search(query) and hub.describe(target), the same lookups from inside code.
  • ToolError for expected tool failures, with code, address, status, hint and retryable.
  • console, captured within a log budget.
  • Determinism: Date runs on the run's logical clock, and Math.random, crypto.randomUUID and crypto.getRandomValues draw from a per-run seed. That is what lets a resumed run replay exactly.
  • setTimeout up to 30 seconds, plus structuredClone, TextEncoder, TextDecoder, atob and btoa.

Not available: fetch, import and require, process, Buffer, eval and the Function constructor. URL and URLSearchParams are not in M1. A second connection is picked per call with { connection: "work" }.

Approvals over MCP

When a call needs a person, the run records a pending input and stops. What the client receives depends on what it declared:

  1. A 2026-07-28 client with form elicitation gets an input request with the call's address, connection, a redacted preview of the arguments and the reason, and four choices. The answer is accepted only with the state the server signed for that exact pending call, and only once.
  2. Every other client gets status: "awaiting_approval". A local hub opens its approval page in your browser and tells the model only that a page opened, with capabilities hub approve <runId> as the fallback. It never hands the model a URL or a token.
  3. Always: capabilities hub approve <runId> from a terminal.

The model then calls execute({ resume: runId }). Until a person decides, resume returns the same pending envelope and runs nothing.

Default limits

These are targets that prototypes and perf tests check. Hosts may lower them but not raise them.

Limit Default Meaning
codeBytes 64 KB Size of the code in one execute
computeMs 10,000 Guest execution time, paused while tool calls are in flight
wallMs 90,000 Ceiling for one attempt, tool calls included
memoryBytes 64 MB Guest heap
maxCalls 500 Tool calls per run, all attempts together
maxConcurrent 8 Tool calls in flight at once
inlineResultBytes 24 KB Result and logs returned inline, across every channel
upstreamTimeoutMs 30,000 One upstream call
approvalTtlMs 24 h How long a pending approval waits

Arriving in M2

M1 serves MCP sources, static credentials, the default code-mode profile and the search and describe helpers. M2 adds skills over MCP (the Skills extension, plus describe("skill:<name>")), large results as handles read with hub.read, OpenAPI sources, OAuth connections, named profiles and direct mode, where each tool is exposed as its own MCP tool for clients and models that do better that way.

Updated 2026-11-06