Skip to main content
ACP v2 is a consolidation release. It redesigns the prompt lifecycle and allows for more flexible session state patterns, unifies streaming and non-streaming updates, makes the schema forward-compatible by default, and removes protocol surface that the ecosystem had already moved away from. Migrating does not mean dropping v1. v1-only Agents and Clients will remain common for some time, so implementers should support both versions side by side: negotiate the version per connection, keep your v1 support working, and add v2 behind feature flags until it stabilizes. See Supporting v1 and v2 side by side for how to structure this. If you only remember five things, remember these:
  1. The session/prompt response no longer ends the turn. It acknowledges acceptance. Foreground progress and completion arrive as state_update notifications, and the stop reason moved there too, including failures after acceptance.
  2. Updates are upserts. Messages, tool calls, and plans are patched by ID with uniform semantics: omitted field = unchanged, null = cleared, value = replaced, chunks append.
  3. The Client file system, terminal execution, and session modes APIs are gone. Agent-owned terminal output is a separate display-only v2 surface. Use client-provided MCP servers when the Agent needs Client-side tools.
  4. Capabilities were reorganized. One capabilities + required info field on both sides, session-scoped groups nested under session, object support markers instead of booleans, and a required baseline of session methods.
  5. Everything is extensible now. Enums and tagged unions accept unknown values. _-prefixed values are yours, the rest are reserved for future ACP versions.

Scope: stable v2 and draft surfaces

This guide describes the stable v2 baseline (schema/v2/schema.json). The v2 protocol surface as a whole is still labeled draft, so gate v2 support behind explicit version negotiation and feature flags until it stabilizes. The unstable schema (schema/v2/schema.unstable.json) layers experimental features on top of that baseline. Negotiating protocolVersion: 2 does not imply support for them. Follow each feature’s documented negotiation requirements: optional operations requiring peer participation use explicit capabilities or another documented mechanism, while informational additions with safe forward-compatible handling may be sent without a capability when the feature permits it. Local feature flags control rollout, not peer support. See Experimental Features and Negotiation.

Version negotiation

The Client sends the latest protocol version it supports in initialize. An Agent implementing v2 selects the highest supported version at or below the requested version, or rejects initialization with -32022 (Unsupported protocol version) and structured requested and supported data if none exists. To use v2, send "protocolVersion": 2. A legacy v1-only Agent can return "protocolVersion": 1 to a Client requesting v2. The Client continues only if it supports v1, otherwise it disconnects. Conversely, a v1 Client talking to a v2-only Agent receives an ordinary initialization error. It does not need to understand the new error code to report that initialization failed. Treat v2 support as additive. Keep serving protocolVersion: 1 peers when you add v2: an Agent that drops v1 cuts itself off from existing Clients, and a Client that drops v1 loses access to existing Agents. Each side selects its v1 or v2 surface per connection based on the negotiated version. Nothing about v2 changes the underlying JSON-RPC framing, so a single connection always speaks exactly one negotiated version after initialize.

At a glance

Method changes

session/update variant changes

Initialization

Role-agnostic info and capabilities

v1 used role-specific field names: the Client sent clientCapabilities and optional clientInfo, and the Agent returned agentCapabilities and optional agentInfo. v2 uses the same two field names in both directions (capabilities and info), and info is now required on both sides.
Both responses above have an empty authMethods array. In v2, omitting authMethods has the same availability meaning: the Agent does not advertise the authentication surface, and Clients MUST NOT call auth/login or auth/logout.

Support markers are objects, not booleans

In v1, capability support was a mix of booleans ("image": true) and objects ("list": {}). In v2, every support marker is an object: supplying {} (or an object with fields) means supported, and omitting the key or supplying null means unsupported. This means checks like promptCapabilities.image === true become presence checks: capabilities.session.prompt.image != null. The object shape leaves room for each capability to grow fields or extend with _meta without another breaking change.

Capability reorganization

  • All session-scoped capability groups now live under capabilities.session. What was agentCapabilities.promptCapabilities is now capabilities.session.prompt, and agentCapabilities.mcpCapabilities is now capabilities.session.mcp.
  • capabilities.session itself is optional, so agents that don’t do session-based prompting (for example, agents that only serve specialized extension surfaces) can omit it entirely. Currently there isn’t protocol support for this, but several experiments are in flight and we are making sure non-session components can be expressed via capabilities.
  • loadSession is gone along with session/load (see Session setup).
  • The individual list, resume, and close markers are gone: advertising capabilities.session at all now requires supporting the baseline methods session/new, session/list, session/resume, session/close, session/prompt, session/cancel, and session/update. Optional extras such as session.delete, session.additionalDirectories, session.prompt, and session.mcp keep their own markers.
  • The v1 Client capabilities fs and terminal are removed entirely (see Client file system and terminal execution). Stable v2 defines Client auth and elicitation capability groups. Agent-owned terminal display is baseline behavior, not a Client execution capability.

Complete v1 → v2 capability mapping

The tables below cover the capability fields in the stable and draft schemas. Paths in the first column are relative to v1 agentCapabilities or clientCapabilities. Paths in the second are relative to v2 capabilities. A Draft only row does not become available merely by negotiating v2. Convert a boolean support marker from true to {} and from false to an omitted key or null only where the table defines a corresponding v2 marker. Object support markers remain objects. Do not copy removed v1 fields into v2 capabilities or move them into _meta as though they were standard v2 capabilities. A removed marker may mean the feature is now baseline, not that the feature was removed.

Agent capability fields

Client capability fields

The draft per-child-session capabilities.cancel marker in subagent_update is unchanged. It is not an initialize capability. For surviving capability objects, move _meta with its owning group (for example, Agent promptCapabilities._meta → capabilities.session.prompt._meta). Root capability metadata moves to capabilities._meta. For removed groups or markers, review extension semantics and choose a supported root or nested _meta location explicitly. See Advertising Custom Capabilities. Receivers MUST ignore unknown fields, including nested capability fields, but this does not make v1 field names valid v2 advertisements. See Unknown Object Fields.

Authentication

The auth methods are grouped under an auth/ prefix:
  • authenticate → auth/login. The params are unchanged (methodId selecting an advertised method whose type defines a protocol-driven login flow). The descriptor’s identifier field is renamed from id to methodId to match, and its type discriminator is now required.
  • logout → auth/logout. In v1, logout support was opt-in via a capability marker. In v2, returning one or more valid entries in authMethods advertises the authentication surface, and the Agent MUST implement both auth/login and auth/logout. If authMethods is omitted or empty, Clients MUST NOT call either method. There is no logout support marker. capabilities.auth remains orthogonal and advertises only authentication-related extensions.

Auth methods

Each entry in authMethods now uses methodId instead of id, and carries a required type discriminator so future auth flows can be introduced without breaking existing implementations:
Because type: "agent" defines a protocol-driven login flow, including this entry in an initialize response requires the Agent to support both auth/login and auth/logout. Stable v2 defines two types: agent and terminal. Custom types MUST begin with _. Unknown values without a leading _ are reserved for future ACP versions.

Terminal auth methods

terminal methods work as in v1: the Client launches the configured Agent program interactively with the method’s args and env, and never sends auth/login for them. Besides the methodId rename, two things changed shape:
  • env is an array of { "name", "value" } entries with unique names, instead of an object that maps names to values.
  • The Client advertises terminal auth support with the support marker capabilities.auth.terminal: {} instead of clientCapabilities.auth.terminal: true (see Support markers).

The new prompt lifecycle

This is the most significant semantic change in v2. Read this section even if you skim the rest. In short, foreground progress and completion moved from the pending session/prompt request into session/update notifications, while the response now acknowledges user-message insertion:

v1: the response is the turn

In v1, session/prompt stayed pending for the entire turn. The Agent streamed session/update notifications while working, and the eventual response carried the stopReason that ended the turn:
This entangled prompt acceptance with turn completion, which made replay, multi-client sessions, background work, and queued messages awkward to express.

v2: the response acknowledges insertion

In v2, acceptance means insertion of the user message into the ACP conversation. The Agent MUST respond to session/prompt once the message has been inserted, without waiting for foreground processing to finish. Merely receiving, queueing, or assigning an ID to the input does not justify a successful response:
messageId is a required, non-null string identifying the inserted user message. The request is unchanged. Clients do not generate this ID. Insertion adds a logical user message with an ID and a position relative to other messages. See Prompt Accepted for adapter examples. Everything else happens through session/update notifications:
  1. User message update. The Agent MUST report the inserted message’s content and placement, either as a user_message update with a full content array or as streamed user_message_chunk updates. These use the same agent-owned messageId as the response. The response and updates describe the same insertion, but updates may arrive before or after the response:
  2. Active state. When foreground work starts, the Agent MUST report running, or requires_action if it starts blocked on user action. After reporting requires_action, it MUST report running when foreground progress resumes. Active state reporting may precede insertion, for example while a foreground hook runs. It is not acknowledgment of insertion.
  3. Output. Messages, thoughts, plans, and tool calls stream as before (with the changes described in the following sections).
  4. Completion. When foreground work ends, the Agent MUST report idle and SHOULD include the stop reason:
Clients reconcile their submitted input with the response’s (sessionId, messageId), including when the message update arrived first. They must not match by content or assume that a successful response means the session is idle. SDKs must keep the session’s processing and notification handling alive after the prompt RPC completes. An Agent may know it is idle without knowing why the work stopped. Clients MUST recognize idle even when the stop reason is omitted, null, malformed, or unknown. They cannot infer success or failure without a recognized stop reason. User messages, including locally handled commands, must be reported live but need not be retained. If an Agent retains and replays an inserted user message, it must reuse the response’s ID. A lost prompt response remains ambiguous. Neither replay omission nor an identical message proves which submission was inserted, and retrying is not automatically safe. Pre-insertion rejection receives a JSON-RPC error response with no user-message updates for that submission. State updates still describe actual foreground work, independently of the request outcome. See Prompts that are never inserted.

Session states

state_update is new in v2 and reports foreground work, whether or not a prompt started it, using one of three stable states (extensible like all v2 enums):
  • running: Foreground work is in progress.
  • idle: No foreground work is active. Background activity may continue. Agents SHOULD include a stopReason when the transition ends foreground work.
  • requires_action: Foreground work as a whole is blocked on user action. Agents SHOULD send this when blocked, including on elicitations. Once reported, they MUST send running when foreground progress resumes, or idle if the work ends without resuming.
Foreground work is the Agent’s active response or workflow in the session, including model processing, tool execution, and waiting for user action needed to continue. Agent-initiated work, such as pursuing an ongoing goal or following up on a completed background task, MUST use the same foreground lifecycle as prompt-driven work, with the same guidance for stop reasons. It does not require a prompt request or a synthetic user message. Background activity can continue and emit other session/update notifications while the Agent reports idle. A detached task’s progress, completion, or permission request does not by itself change the foreground state. If the Agent starts a response or workflow to act on a background result, it MUST report an active state and report idle when that work ends, and SHOULD include a stop reason on the idle update. See Foreground and Background Work for contrasting examples. States are aggregate snapshots, not per-request events. Clients MUST apply repeats idempotently, without counting additional work, completions, or usage. With concurrent user-action requests, resolving one does not emit running unless foreground progress actually resumes. If other foreground work can progress, the state remains running while requests are pending. The Agent MUST report idle when no foreground activity remains, even between immediately consecutive turns. Completing an individual prompt or command while other foreground activity continues does not require a boundary. Accepted local slash commands MUST also report an active state and completion when handled from idle, even without model work. A command with no user-action wait uses running → idle. New sessions default to idle until a state update reports otherwise. Updates may arrive before the new-session response, which does not overwrite them. Every successful session/resume MUST be preceded by a current-state snapshot, after any requested replay. Resume does not imply idle, and Agents MUST NOT replay historical state updates as current activity. See Current-State Synchronization and the transition table and lifecycle examples. The v1 stop reasons (end_turn, max_tokens, max_turn_requests, refusal, cancelled) are unchanged. They just moved from the session/prompt response to the idle state_update and describe why foreground work stopped. v2 adds the error stop reason. v1 reported a failure during the turn as an error response to the pending session/prompt. In v2, once the user message is inserted, a failure ends foreground work with an idle state_update whose stopReason is error and whose optional error field carries the same JSON-RPC error object. An error response to session/prompt now always means the message was not inserted. See Failures.

Cancellation

$/cancel_request and internal request cancellation can no longer answer an inserted session/prompt with -32800: once the user message is inserted, the Agent responds with its messageId, so an error response always means the message was not inserted. Stop the work it started with session/cancel. session/cancel is unchanged as a notification, but confirmation moved with the rest of the lifecycle: when active foreground work is cancelled, the Agent MUST finish sending any pending updates and then send an idle state_update with the cancelled stop reason. Clients respond to pending session/request_permission requests in that session with the cancelled outcome. Agents SHOULD cancel individual pending elicitations that the affected work no longer needs rather than cancel every elicitation with that session ID. Clients SHOULD accept tool-call updates that arrive after sending session/cancel. After cancellation completes, Agents SHOULD ignore late answers and external completions rather than use them to resume the cancelled work. Responses can race with cancellation before completion. A repeated or resume snapshot carrying an old cancelled stop reason does not cancel a current interaction. If no foreground work was active, no additional completion update is required.

Why this matters beyond bookkeeping

Because foreground progress is now expressed entirely in notifications, the same message flow works for history replay on session/resume and agent-initiated work. It also supports future queued-work flows.

Messages and message IDs

Message IDs are required

Every message chunk and message update in v2 MUST carry a messageId. In v1, messageId was optional on chunks. Message IDs are opaque strings generated by the Agent: the Agent owns session history, so it is the single source of message identity. All chunks of one message share one messageId. A changed messageId starts a new message. Message IDs are unique per message type within a session, not across types: a user message, an Agent message, and an Agent thought may share the same messageId and remain distinct messages. The same applies to other entity IDs such as toolCallId and planId. Clients key entities by type and ID. See Entity IDs.

Whole-message upserts

Alongside the chunk updates, v2 adds user_message, agent_message, and agent_thought updates that carry a full content array (chunks carry a single content block). These are upserts keyed by messageId, with three-state patch semantics:
  • Omitted content leaves existing content unchanged (useful for updating _meta or other fields without resending content).
  • content: null or content: [] clears the message content.
  • A concrete array replaces all content currently stored for that message, including content accumulated from earlier chunks.
Chunks always append to whatever content is current. For example: agent_message with content: [A], then agent_message_chunk with B renders as [A, B]. A later agent_message with content: [C] resets the message to [C], and subsequent chunks append to that. Use chunks for streaming. Use whole-message updates for replay, correction, or output that is already complete.

Tool calls

One upsert notification

v1 had tool_call (create) and tool_call_update (modify). In practice the split bought nothing, so v2 keeps only tool_call_update. The first update with an unseen toolCallId creates the tool call. Subsequent updates patch it. Only toolCallId is required, but the Agent SHOULD include title on the first report:
The patch semantics are the same three-state model as messages: omitted fields stay unchanged, null explicitly clears a field, and concrete values replace. Array fields such as content and locations are replaced wholesale when present. The optional name metadata is available in both versions. Concrete names map directly between versions, but v2’s name: null clear cannot be represented by a v1 update because v1 treats null like omission. A strict adapter must reject that conversion, while an intentionally lossy adapter may omit the clear. The remaining fields (title, kind, status, content, locations, rawInput, rawOutput) and kind values are unchanged from v1. The status enum adds cancelled, and both enums are now extensible. Top-level _meta is now stored tool-call state, too. v1 gave no rule for keeping a tool_call_update’s _meta, so Agents could use it for data about one report. In v2, a concrete _meta patches the tool call’s stored metadata by top-level key: keys it omits keep their stored values, a key set to null is removed, and any other value replaces that key whole (see Metadata Patches). A key sent on an earlier report therefore stays until the Agent sets it to null or clears _meta. Put data about a single report in the session/update notification’s params._meta instead.

Streaming tool-call content

v2 adds tool_call_content_chunk, which appends a single ToolCallContent item to a tool call instead of resending the whole array:
Chunks and updates compose exactly like message chunks and updates: a tool_call_update that includes content replaces the accumulated content. Later chunks append to it.

Agent-owned terminal display

v2 retains a terminal ToolCallContent shape, but changes its ownership and meaning. It is now only a reference to a display terminal owned by the Agent:
The reference contains no mutable state. The Agent reports that separately with a terminal_update upsert keyed by terminalId:
Only terminalId is required. command, cwd, output, exitStatus, and _meta are patch fields: omission leaves the stored value unchanged, null clears it, and a concrete value replaces it, except that a _meta object patches stored metadata by top-level key. On a first-seen terminal, omitted fields start unknown or empty. Every supplied cwd must be absolute. output is an authoritative replacement snapshot for replay, correction, or resynchronization. Its required data field is RFC 4648 base64-encoded bytes and is the complete byte sequence the Agent wants the Client to retain for the terminal at that point. A concrete snapshot replaces all previously stored bytes. Clients must not merge or splice bytes from the previous snapshot into it. Optional nullable output._meta is snapshot-scoped. Omission and null both mean no snapshot metadata was provided. A concrete exitStatus marks the terminal as exited. Its optional nullable exitCode and signal fields use omission or null when unavailable. Optional nullable exitStatus._meta is scoped to that exit information and likewise treats omission and null as equivalent. Conventional POSIX signal names include SIGTERM, SIGKILL, and SIGINT. Other platforms may use platform-specific names. For example: "exitStatus": { "signal": "SIGTERM" }. For efficient live output, the Agent sends append updates:
Each chunk’s data is independently base64-encoded. Decode each chunk separately, then append the resulting bytes in received order for that terminal. Do not concatenate encoded strings before decoding. Chunk boundaries may split UTF-8 code points and ANSI escape sequences, so text and terminal parsers retain state between chunks. A later terminal_update.output snapshot replaces all bytes accumulated so far, and subsequent chunks append to that replacement. A Client may use an isolated terminal emulator, but it only needs to provide a safe sanitized transcript fallback. The surface is display-only: it has no input, resize, interrupt, kill, wait, release, or execution semantics.

Diff content

The v1 diff shape (a single path with oldText/newText) could not distinguish a deleted file from an emptied one, had no way to express renames, copies, or binary changes, and forced clients to compute renderable diffs themselves. v2 replaces it with structured changes plus optional renderable patch text:
  • changes (required) is the authoritative list of file-level operations. add, delete, and modify carry a path. move and copy carry oldPath and path. Each change may carry an optional fileType (text, binary, directory, symlink) and mimeType. Clients can build file trees and summaries from changes without parsing any patch text.
  • patch (optional) carries renderable text for some or all changes and MUST be consistent with changes. Stable v2 defines git_patch as one or more diff --git sections in Git’s --patch (-p) text format. Unlike Git’s repository-relative paths, every path MUST be absolute, including in rename from, rename to, copy from, and copy to lines, with no prefix such as Git’s default a/ and b/. Otherwise paths follow Git, including /dev/null for the missing side of an added or deleted file and Git’s quoting of unusual paths. See Diffs for producing this with git diff. Surrounding commit metadata and email envelopes MUST NOT be included. Agents SHOULD provide patch whenever feasible. Clients MUST handle diffs where patch is omitted or null (binary changes, symlink retargets, and similar cases have no useful text rendering).
A deleted file is now simply:
There is no mechanical mapping from v2 diffs back to oldText/newText. When migrating a v1 client, render patch.text according to patch.format, using changes for headers, file trees, and non-text cases.

Edits known only as a snippet

Many v1 Agents filled oldText and newText with just the replaced snippet, such as an edit tool’s old_string and new_string, without reading the file. v2 has no form for a snippet: a git_patch hunk needs line numbers, and each entry in changes needs an operation, which a snippet does not always tell. An edit with an empty old_string, for example, is an add or a modify depending on whether the file exists. A tool that writes a whole file has the same gap: its input has the new contents, but not the old ones or the operation. Such an Agent has two options:
  • Read the file before the tool runs, and compute changes and patch from its contents.
  • Send no diff until the change is done. Until then, locations can still point the Client at the file.
Read the file somewhere the tool waits on, so it still holds its contents from before the change. Reading once the tool input is complete races an auto-approved edit, which may already have run, and the patch would then be computed against the edited file. A permission check is safe, but auto-approved edits skip it, so they need a hook that runs before the tool, such as Claude Code’s PreToolUse hook.

Permission requests

session/request_permission no longer takes a bare toolCall. The prompt copy and the subject of the request are now separate concerns:
  • title is required: human-readable text for the permission prompt itself. It does not update any tool call’s displayed title.
  • description is optional supporting copy.
  • subject is an optional tagged union describing what needs permission. Stable v2 defines type: "tool_call", whose toolCall payload is the same ToolCallUpdate upsert shape used in session updates, and type: "command", which describes an Agent-executed command. Requests may omit subject entirely for approvals without structured context, and unknown subject types should be preserved when proxying and either rendered generically or declined by policy.
In v1, agents routinely stuffed permission prompt text into the tool call’s title, mutating displayed tool state as a side effect. In v2, put prompt copy in title/description and let subject.toolCall carry only genuine tool-call state. A command permission subject is self-contained rather than a patch:
command and cwd are required, and cwd must be absolute. toolCallId, terminalId, and _meta are optional and nullable. Omission and null are equivalent and mean the value was not provided. The IDs only associate the permission with displayed state and may be absent when permission is requested before execution. Approval authorizes the Agent to execute the command. It never asks the Client to execute it. options and the response shape are unchanged: options carry optionId, name, and kind (allow_once, allow_always, reject_once, reject_always), and the response outcome is { "outcome": "selected", "optionId": ... } or { "outcome": "cancelled" }. Both unions are extensible in v2, with one safety rule: an Agent that receives an outcome it does not understand MUST NOT treat it as approval. While foreground work as a whole is blocked on a permission response, the Agent SHOULD report state_update with requires_action. Once reported, it MUST report running when foreground progress resumes, or idle if the work ends without resuming. A permission request for independent background activity does not by itself change the foreground state, and resolving a permission request does not itself imply running. Apply the aggregate transition rules, including with concurrent requests and late answers.

Plans

The v1 plan update was a flat entries list with no identity and no room to grow. v2 replaces it with plan_update, whose payload is a tagged union keyed by a required planId:
Stable v2 defines the items plan type. The entry shape (content, priority, status, all required) is unchanged from v1. Entry priorities and statuses are now extensible enums, and status adds cancelled. Each plan_update for a given planId replaces that plan’s entries, and the planId allows multiple plans per session and future plan content types (such as markdown or file-backed plans, which remain unstable) without another breaking change.

Session setup and lifecycle

session/load is gone. session/resume does both

v1 had two ways to reattach to a session: session/load (replays full history, gated by loadSession) and session/resume (no replay, gated by sessionCapabilities.resume). v2 keeps only session/resume with an optional replayFrom cursor:
  • Omit replayFrom (or send null) for v1 resume behavior: reattach without replay.
  • "replayFrom": { "type": "start" } for v1 load behavior: the Agent replays the entire conversation as session/update notifications (using the same message, tool-call, and plan updates as live traffic) before responding.
The cursor is a tagged union so future versions can add replay-from-a-point variants without new methods. The schema accepts custom and future cursor types for preservation by intermediaries, but Agents MUST reject unsupported types or malformed cursors with -32602 (Invalid params), not silently fall back to another replay mode. Retained user messages MUST preserve the ID returned by session/prompt. Agents SHOULD preserve agent and thought message IDs when possible, but Clients cannot assume those IDs remain stable across reconnects or restarts. For full replay, Clients SHOULD rebuild agent and thought message history rather than merge it into existing history solely by messageId. An empty-content primer resets only the ID used in that replay; it cannot clear a message previously delivered under another ID. See Resuming Sessions.

Consistent lifecycle requests

session/new and session/resume take the same shape of environment parameters: a required cwd (absolute path), plus optional additionalDirectories and mcpServers. Note that mcpServers was required (even if empty) on session/new in v1 and is now optional. Omitting it and sending [] are equivalent. Both responses MAY include the session’s configOptions and initial availableCommands. Neither carries modes anymore. additionalDirectories still requires the session.additionalDirectories capability and absolute paths, and each session/resume must send the full intended additional-root list. Omitting the field or sending [] activates no additional roots rather than restoring previous ones.

Baseline methods

As covered under Initialization, advertising capabilities.session commits the Agent to session/new, session/list, session/resume, session/close, session/prompt, session/cancel, and session/update. Clients can rely on listing, resuming, and closing sessions without probing individual capability markers. session/delete remains optional behind session.delete.

Session modes become config options

The dedicated modes API is removed: the modes field on session responses, session/set_mode, the current_mode_update notification, and all SessionMode* types. Mode-like state (and model selection, thinking level, and similar knobs) is expressed through session config options, which already existed in v1:
  • The option’s identifier field is renamed from id to configId, matching the parameter name used by session/set_config_option.
  • The stable category values are mode, model, model_config, and thought_level. Clients can use them for UX affordances (placement, shortcuts) and must tolerate unknown categories.
  • Clients change values with session/set_config_option (sessionId, configId, type, value). The required type discriminator is "id" with a string option ID or "boolean" with a boolean value. The response returns the full updated configOptions array, since one change can affect other options. Agent-initiated changes arrive as config_option_update session updates, which replace the v1 current_mode_update.

Client file system and terminal execution removed

v2 removes the entire v1 Client-provided execution surface:
  • clientCapabilities.fs and the fs/read_text_file / fs/write_text_file methods
  • clientCapabilities.terminal and the terminal/create, terminal/output, terminal/release, terminal/wait_for_exit, terminal/kill methods
In practice this surface was inconsistently implemented outside of a few IDEs, and agents already needed their own file and execution handling for clients that didn’t offer it. Clients that want to expose file access, unsaved editor state, or command execution to agents should do so by providing an MCP server to the session (via mcpServers on session/new / session/resume), which puts those tools on the same footing as every other tool the Agent uses. Agent-side, drop the capability checks and the dual execution paths. Report file changes through diff content. When byte-faithful command display matters, report output through the new Agent-owned terminal stream. The v2 terminal content reference does not restore this Client execution surface. It only anchors display state supplied by the Agent.

MCP server configuration

MCP transport configuration is aligned with the current MCP transport model:
  • Every server config MUST carry a type discriminator. In v1, stdio configs had no type field. In v2 they are "type": "stdio". Unknown transport types can then be carried as extension variants instead of failing to parse.
  • The deprecated HTTP+SSE transport ("type": "sse") is removed.
  • Support is advertised per transport under capabilities.session.mcp: stdio is now an explicit capability (session.mcp.stdio), so agents that cannot launch subprocesses can opt out, and session.mcp.http covers remote servers.
  • Empty arrays are no longer required: args and env on stdio configs and headers on HTTP configs are optional in v2 (v1 required args and env even when empty).

Content blocks

The five content block types are unchanged: text, image, audio, resource_link, and resource. v2 realigns their fields with the latest MCP specification. Notably, resource_link gains an optional icons array (each icon has a required src URI plus optional mimeType, sizes, and theme of light/dark), and the type discriminator is extensible like every other v2 enum, so unknown block types no longer break deserialization for receivers that have a fallback. If you maintain a validator or generated models, v2 also tightens schema-level details that v1 left loose: base64 payloads (image/audio and terminal data, resource blob) are marked with contentEncoding: "base64", URI fields are marked format: "uri", and annotations.priority is bounded to the inclusive range 0–1.

Slash commands

Agents MAY now return initial availableCommands alongside configOptions in session/new and session/resume responses. The draft session/fork response also supports commands for the newly created session when forking is supported. This avoids requiring command notifications before the session/new response. See the setup response example. In setup responses, availableCommands is an optional array: omission or [] means no initial commands are advertised, and empty lists are omitted when serializing. null is not a valid outbound value. Tolerant v2 receivers treat null or a malformed non-array value like omission and skip invalid command items within an array. available_commands_update notifications remain supported for advertising commands later or changing them. Each notification replaces the complete list previously advertised in a response or notification. [] clears it. A command still carries name and description, but its optional input specification is now a tagged union. The v1 untagged unstructured input gains an explicit type: "text" discriminator so richer input types can be added later:
As with other extensible unions, unknown input types should be preserved when proxying. A client that can’t render one can fall back to plain text input or hide the command’s input hint.

Consistent ID naming

v2 applies one naming rule across the schema: a field that identifies a protocol entity or references one is named after its domain (sessionId, messageId, toolCallId, planId, optionId), never a generic id. The concrete renames from v1: New entities introduced in v2 follow the same rule (messageId on messages, planId on plans). A few schema definition names also changed without any wire-format change, which matters only to schema-codegen consumers: the session/update and session/cancel payload types are now UpdateSessionNotification and CancelSessionNotification, and the auth request/response types follow the new method names (LoginAuthRequest/LoginAuthResponse, LogoutAuthRequest/LogoutAuthResponse).

Extensibility and forward compatibility

v2 makes the whole schema forward-compatible by convention:
  • Open enums and tagged unions. Every enum-like string accepts unknown values. Values beginning with _ are reserved for implementation-specific extensions (e.g. _mycompany_widget). Unknown values without a leading _ are reserved for future ACP versions. Receivers SHOULD preserve unknown values when storing, replaying, proxying, or forwarding, and either render them generically or fall back safely. Do not mint non-underscore custom values.
  • _meta everywhere. As in v1, _meta objects carry implementation metadata. In v2’s upsert-style updates, top-level _meta is a patch: omitted means unchanged, null clears it, and an object patches stored metadata by top-level key, where a key set to null is removed and any other value replaces that key whole (see Metadata Patches). Metadata nested in a snapshot, exit-status object, content item, command permission subject, or chunk is scoped to that object. Omission and null are equivalent.
  • Custom methods still use the _ prefix, unchanged from v1.

Transports

stdio remains the primary transport and is unchanged except that v2 explicitly follows JSON-RPC 2.0 batch behavior: a newline-delimited message may be a single request, notification, response, or a batch array. Receivers may process batch entries concurrently, must not reply to notifications, and return per-entry -32600 errors for invalid entries. Don’t batch lifecycle-sensitive messages (initialize, auth/login, session/new, session/resume, session/prompt). They change which later messages are valid. A standard remote transport (streamable HTTP with SSE streams, plus WebSocket) is being specified in its own RFD and is not part of the core v2 protocol surface. See the transport RFD for its current state.

Migration checklist: Agents

  1. Initialization. Read params.capabilities / params.info. Return capabilities and a required info. Restructure your capability object: nest prompt and MCP capabilities under session, convert boolean markers to {} objects, drop loadSession and the list/resume/close markers.
  2. Implement the baseline. If you advertise session, implement session/new, session/list, session/resume, session/close, session/prompt, session/cancel, and session/update.
  3. Auth. Rename authenticate → auth/login for method types that define protocol-driven login. If you return one or more valid authMethods, implement both auth/login and auth/logout. If you omit the field or return an empty array, Clients MUST NOT call either method. Do not add a logout support marker: capabilities.auth is only for authentication-related extensions. Add type and methodId to your auth method descriptors, and send terminal method env as { "name", "value" } entries.
  4. Prompt lifecycle. Respond to an accepted session/prompt with { "messageId": "…" }, carrying the agent-generated ID of the inserted user message. Emit a user_message acknowledgment with the same messageId, a running state update when foreground work starts, and an idle state_update carrying the stop reason when it ends, including cancelled after session/cancel. Report a failure after insertion as an idle state_update with stopReason: "error" and the JSON-RPC error in error, never as an error response to the prompt. Background updates may continue while idle.
  5. Message IDs. Generate and attach a messageId to every message chunk and update.
  6. Tool calls. Stop sending tool_call. Send tool_call_update for creation and patches. Use tool_call_content_chunk to stream content. Respect omit/null/value patch semantics, including key-level _meta patches.
  7. Terminal output. Add terminal content references for Agent-owned display terminals. Send terminal_update for command, absolute cwd, snapshots, and exit state. Send independently base64-encoded terminal_output_chunk values for live bytes. Do not expect Client control methods or capability negotiation.
  8. Diffs. Replace oldText/newText with changes (+ fileType/mimeType where known) and provide patch in git_patch format whenever feasible. If a tool’s input describes an edit only as a snippet, read the file somewhere the tool waits on, before it can run, or send no diff until the change is done (see Edits known only as a snippet).
  9. Permissions. Put prompt copy in required title (+ optional description). Use the tool_call subject for tool-call state and the command subject for a command, required absolute cwd, and optional tool-call/terminal associations.
  10. Plans. Send plan_update with { "type": "items", "planId": ..., "entries": [...] }.
  11. Modes. Drop session/set_mode, modes, and current_mode_update. Expose the same state as config options with category: "mode" and emit config_option_update.
  12. Replay. Handle replayFrom on session/resume. Replay history as ordinary session/update notifications when asked, including terminal replacement snapshots rather than requiring every historical output chunk. When the Client may already hold an entity’s _meta, remove stale keys explicitly, since an _meta object no longer replaces them. Remove session/load.
  13. Client surface. Remove all fs/* and terminal/* calls. The new terminal content is Agent-owned display state, not a Client resource.
  14. MCP config. Require and emit type discriminators. Drop SSE. Advertise session.mcp.stdio / session.mcp.http as appropriate.
  15. Slash commands. Add the type: "text" discriminator to command input specifications. Optionally advertise initial commands in setup responses via availableCommands, and use complete-replacement available_commands_update notifications for later changes.
  16. Parsing. Make enum handling open: preserve unknown variants where a safe fallback exists, and accept batch arrays on stdio.

Migration checklist: Clients

  1. Initialization. Send protocolVersion: 2, required info, and capabilities (drop fs/terminal, and advertise terminal auth as auth.terminal: {}). Read the Agent’s capabilities from result.capabilities with presence checks instead of boolean checks.
  2. Rely on the baseline. When capabilities.session is present, use list/resume/close/prompt/cancel without probing markers. Keep checking session.delete and other optional extras.
  3. Prompt lifecycle. Don’t treat the session/prompt response as turn completion. Drive UI from state_update (running / idle / requires_action), take the stop reason from the idle update, and render the user’s message from the user_message acknowledgment (which carries its canonical messageId). Treat stopReason: "error" as a failed outcome and read its details from the idle update’s error. An error response to session/prompt means the message was not inserted. Keep accepting background updates while idle.
  4. Messages. Track messages by required messageId. Implement upsert semantics: whole-message updates replace content (or clear it with null/[]), chunks append.
  5. Tool calls. Create tool calls on first-seen toolCallId in tool_call_update. Apply omit/null/value patches, and patch top-level _meta by key. Append tool_call_content_chunk items. Replace accumulated content when an update carries content.
  6. Terminal output. Maintain Agent-owned terminal state by terminalId. Apply update patch and snapshot-replacement semantics. Independently decode and append chunk bytes. Render them in an isolated terminal emulator or sanitized transcript, with no input or process controls.
  7. Diffs. Render patch.text when present. Drive file trees and summaries from changes. Handle patch-less diffs (binary, symlink, directory).
  8. Permissions. Render title/description. Apply tool_call subjects like ordinary tool-call updates, and render command subjects from their command, required absolute cwd, and optional associations. Show a generic prompt for absent or unknown subjects.
  9. Plans. Handle plan_update keyed by planId. Each update replaces that plan’s entries.
  10. Modes. Drop session/set_mode and current_mode_update handling. Drive mode/model UI from configOptions (using category) via session/set_config_option and config_option_update.
  11. Resume. Replace session/load with session/resume + replayFrom: { "type": "start" }, and plain reconnects with session/resume without replayFrom.
  12. Client surface. Remove fs/* and terminal/* implementations for v2 connections. Expose Client-side tools through MCP servers passed in mcpServers. Do not treat display terminals as Client resources.
  13. Cancellation. After session/cancel, keep accepting updates and wait for the idle state_update with stopReason: "cancelled" as confirmation.
  14. Parsing. Preserve unknown enum variants, tolerate unknown sessionUpdate types, read command input as a tagged union, and accept batch arrays on stdio. Read initial availableCommands from setup responses and replace the list on each available_commands_update, including [] to clear it.

Migration checklist: SDKs

  1. Separate the versions. Keep v1 and v2 schemas, generated models, and test fixtures fully separate. Exposing unstable-v2 types does not advertise support; enforce each feature’s documented negotiation requirements independently of protocolVersion: 2.
  2. Three-state fields. Model omitted vs null vs concrete values distinctly wherever v2 defines patch semantics. A plain nullable/optional type erases a distinction the protocol depends on. Apply the top-level _meta of upserts as a key-level patch rather than a replacement.
  3. Strict where it counts. Reject malformed payloads for known discriminator values instead of demoting them to the unknown-variant fallback. The fallback exists only for genuinely unknown values.
  4. Preserve what you don’t understand. Round-trip _-prefixed extension values, unknown future variants, and _meta when storing, replaying, proxying, or forwarding.
  5. Fixtures. Cover the full prompt lifecycle, resume replay, cancellation, permission flow, message replacement and clearing, tool-call content chunks, terminal snapshot replacement, split and invalid UTF-8 bytes, exit state, command subjects, structured diffs, and JSON-RPC batches.
  6. Stop reasons. Model the idle update’s stop reason as a union tagged by stopReason and flattened into the update, so a stop reason can carry fields of its own, such as the error stop reason’s error. Keep unknown stop reasons with their fields, and treat a missing, null, or non-string stopReason as no stop reason rather than rejecting the update.

Supporting v1 and v2 side by side

Supporting both versions is the recommended path, not an edge case: v1 peers will remain in the wild well after v2 stabilizes, and dropping v1 support means losing them. Version negotiation gives you one protocol version per connection, so the cleanest approach is to keep two thin protocol surfaces behind shared application logic and select one after initialize.

Negotiate once, then use the selected surface

The v2 version negotiation rules apply to Agent endpoints implementing v2, including dual-stack endpoints. They do not impose new requirements on legacy v1-only Agents:
  • The Client MUST request the latest version it supports.
  • An Agent implementing v2 MUST select the highest supported version at or below the requested version. If none exists, it MUST return -32022 with requested and supported data instead of a higher-version success response.
  • Both peers MUST use the selected version’s specification for subsequent traffic. A Client that cannot support the returned version SHOULD close the connection and inform the user.
For an Agent supporting both v1 and v2, a v1 request selects v1, a v2 request selects v2, and a request for a newer unsupported version selects v2. An Agent supporting only v1 returns v1 to a Client requesting v2. The returned version is a selection, not a promise that the Agent will translate subsequent v2 messages into v1. Keep initialization separate from session setup. Prepare handlers for both versions before connecting, but dispatch subsequent traffic through only the selected surface. Inspect the selected version before sending session/new or interpreting capabilities. The request uses the Client’s requested-version initialization shape, while the response uses the Agent’s selected-version shape. For example, a v2 request carries info and capabilities, while a v1 response carries agentInfo and agentCapabilities. A version-specific SDK Client API is not necessarily a dual-stack Client: it may reject a response selecting the other version. Use the SDK’s negotiation support where available. If falling back requires a new SDK connection, close the first connection and initialize the replacement with the selected version’s API. Do not send a second initialize on an already initialized connection.

Agent routing in the SDKs

The TypeScript SDK exposes the router through its experimental v2 entry point:
The runnable dual-version TypeScript agent shows both implementations on one stdio endpoint, including their different prompt lifecycles. From a checkout of the TypeScript SDK with dependencies installed, run npm run build, then configure your Client to launch node dist/examples/dual-version-agent.js from that checkout. The Rust SDK provides the corresponding builder with the unstable_protocol_v2 crate feature:
See the Rust SDK’s protocol v2 guide for agent routing and Client-side fallback. These routers select a version-specific implementation and normalize initialization parameters for it. They do not translate later requests, notifications, or responses. A standalone v2 Agent API can negotiate v2 with a Client requesting a newer version, but it does not provide a v1 implementation. Register both implementations to serve v1 Clients.

Version mismatch is an initialization failure

A v2-only endpoint rejects a v1 request with -32022 and data: { "requested": 1, "supported": [2] }. It does not need to invoke the v2 initialization handler or require v2’s Client identity just to report this mismatch. The connection is not initialized, and the Client should close it. A v2-aware Client can display the supported versions, while an existing v1 Client can report the initialization error generically. A request for a newer version can still select v2. A v1-only endpoint can select v1 for a v2 request. In either case, the Client must check whether it supports the selected version before proceeding. The latest-version integer does not advertise a complete set of supported versions. Do not confuse version mismatch with malformed initialization parameters. A missing, non-integer, or out-of-range protocolVersion, or malformed parameters for a supported version, remains -32602 (Invalid params). Use the SDK routers to negotiate before dispatching to a version-specific implementation.

Share application logic, not wire shapes

Keep business logic, tool execution, and any shared persistence behind version-specific adapters. Each adapter owns its protocol’s capabilities, validation, and lifecycle:
  • Prompt completion: v1 keeps session/prompt pending until the turn ends. v2 acknowledges insertion and reports foreground completion through an idle state_update.
  • Conversation updates: v1 streams chunks, while v2 also supports identified entities, replacements, clearing, and replay. A v1 projection must not assume it can represent every v2 update.
  • Client tools: v1 may use Client file system and terminal methods. v2 uses MCP servers for Client-side tools. Agent-owned display terminals are not a replacement execution API.
Advertise only what each adapter implements. Negotiating v2 does not enable unstable features, and sharing a session store does not automatically make every session resumable through both versions. Define cross-version session access explicitly if your application offers it. General-purpose v1↔v2 struct conversions cannot implement these lifecycle differences or represent every state. Prefer adapters over blanket conversions. Any shared conversion helper should have a narrow contract and report unsupported or lossy mappings explicitly rather than silently dropping behavior.

Validate the dual-stack endpoint

Exercise the same published endpoint with both v1 and v2 Clients:
  1. Check that each Client reaches the matching implementation and receives that version’s capabilities.
  2. Check fallback from a v2 Client to a v1-only Agent, including whether the Client continues with v1 or disconnects.
  3. Send a higher requested version with compatible initialization parameters and check that the Agent selects its latest supported version.
  4. Run prompt completion and cancellation through both surfaces, and assert each version’s completion signal, not just successful initialization.
  5. Check that disabling draft v2 support leaves the v1 endpoint working.
  6. Send a v1 request without Client identity to a v2-only endpoint. Check for -32022 with requested: 1 and supported: [2], and check that no v2 session work starts.