Repository navigation
[RFC] Support legacy and 2026-07-28 MCP servers through dual-era negotiation #1650
Description
Summary
Maka currently speaks the legacy MCP initialization era through
@modelcontextprotocol/sdk v1 (^1.24.3, resolved to 1.29.0 in the current
lockfile).
MCP 2026-07-28 removes initialize and the optional protocol-session
mechanism. Modern HTTP requests are self-describing, while a long-lived stdio
connection still pins one negotiated era. Modern servers must implement
server/discover; SDK v2 uses that method as the connect-time probe for
auto and exact-pin modes.
I propose:
- migrating the internal MCP implementation to the v2 TypeScript SDK packages;
- retaining
McpClientManageras the seam that hides legacy/modern wire
behavior; - adding one optional per-server protocol preference and one live negotiated
protocol status; - preserving omitted config as
legacy, while new remote entries created in
Extensions > MCP (editor or catalog) write an explicitauto; and - preserving the existing
ToolRuntimeexecution path for model-proposed MCP
tool calls, including pre-implementation call recording, abort propagation,
telemetry, and result normalization; when a Runtime commit sink is
configured, the existing T1/T2 durable boundary also remains in force.
This proposal is based on
ae43cb29.
I have not found a reported Maka user outage caused by a modern-only endpoint,
so this is a standards-readiness proposal rather than an urgency claim. As a
runtime check, a stdio fixture using @modelcontextprotocol/server@2.0.0 with
legacy: 'reject' rejects Maka's currently pinned v1.29 client with -32022 Unsupported protocol version: 2025-11-25; the same fixture connects with
@modelcontextprotocol/client@2.0.0 in auto mode and selects
modern / 2026-07-28. This contract belongs in the proposed test matrix.
Current behavior and problem
Desktop currently owns one McpClientManager. It reconciles config, owns
clients/transports, lists tools, publishes connection status, and executes
model-proposed remote calls through the Runtime's normal tool execution path:
mcp.json / Extensions > MCP
|
v
McpClientManager
|
+-- status/tools() -----------------------> Extensions > MCP
|
+-- boundTools() -> buildMcpTools() ------> Desktop backend
|
+-- callTool(binding) <- MakaTool <--------- ToolRuntime execution path
(T1/T2 when configured)
This existing seam already keeps SDK types out of Desktop and Runtime. I propose
retaining it: control/status callers keep sync, status, and display
tools; the Runtime adapter uses the bound-tool/call seam below. Neither
branches on protocol era.
This is already a user-facing product surface: Extensions > MCP says users may
add any stdio, Streamable HTTP, or SSE server. Supporting modern-only peers is
therefore part of keeping that transport promise current, even though no outage
has been reported yet.
The current implementation has four compatibility gaps:
- SDK v1 only performs the legacy
initializesequence; - installing SDK v2 alone does not enable modern MCP because its default remains
legacy; - Maka only registers the legacy unsolicited tool-list notification handler,
while modern delivery requires an honoredsubscriptions/listen; and - remote
transport: 'auto'currently attempts SSE after every Streamable HTTP
connection error, which can turn an auth or infrastructure failure into a
misleading transport fallback.
The proposal preserves these authority boundaries:
- MCP config authorizes Desktop/
McpClientManagerto establish a connection,
including starting a configured stdio process; McpClientManagerowns live connection/protocol truth;ToolRuntimerecords the model tool call before invoking the MCP
implementation; whenruntimeCommitSinkis configured, it additionally
commits T1 beforeMcpClientManager.callTool()and settles the result through
T2;McpClientManagerand the selected SDK transport own the actual MCP
network/stdio side effect;RuntimeEventrecords committed runtime facts; and- server identity, capabilities, and annotations remain untrusted remote claims,
not permission authority.
Since #1581, Maka no longer has a generic per-tool PermissionEngine.
categoryHint: 'network_send' still keeps MCP tools out of Plan mode and is
included in tool-start trace metadata when tracing is enabled, but it does not
create an ordinary per-call approval prompt. The current MCP tool implementation
also does not consult the session ExecutionBoundary. Adding per-MCP approval
or mapping main-process MCP calls to a session network boundary is a separate
security design, not part of this protocol migration.
Proposed interface
In the final state, both server shapes accept one repository-owned preference:
export type McpProtocolPreference = "legacy" | "auto" | "2026-07-28";
export interface McpStdioServerConfig {
enabled?: boolean;
command: string;
args?: string[];
env?: Record<string, string>;
cwd?: string;
protocol?: McpProtocolPreference;
}
export interface McpRemoteServerConfig {
enabled?: boolean;
url: string;
transport?: "streamable-http" | "sse" | "auto";
headers?: Record<string, string>;
protocol?: McpProtocolPreference;
}Status exposes only the current connected result, not SDK discovery evidence:
export interface McpNegotiatedProtocol {
era: "legacy" | "modern";
revision: string;
}
export interface McpServerStatus {
// existing fields...
negotiatedProtocol?: McpNegotiatedProtocol;
}The Runtime adapter uses a separate main-process-only binding contract:
declare const mcpToolBindingBrand: unique symbol;
export type McpToolBinding = string & {
readonly [mcpToolBindingBrand]: true;
};
export interface McpBoundTool {
descriptor: McpToolDescriptor;
binding: McpToolBinding; // opaque repository-owned token
}
export interface McpToolProvider {
boundTools(): readonly McpBoundTool[];
callTool(
binding: McpToolBinding,
args: Record<string, unknown>,
options?: { signal?: AbortSignal; timeoutMs?: number },
): Promise<McpCallResult>;
}McpServerStatus.tools remains display-only descriptors. The binding never
enters renderer IPC, model-visible schemas, or persisted/audit tool arguments.
Desktop main fingerprints the main-only bound view, including binding tokens,
when deciding whether to refreshIdleBackends(). Therefore an
output-schema-only change or replacement connection refreshes future idle
backends even if every public descriptor is byte-identical; an already-active
backend retains its old binding and follows the stale-call rule below.
negotiatedProtocol is absent before a connection completes. It is cleared
when the connection state leaves connected (including state: 'error'),
disconnects, or is replaced; a diagnostic in optional status.error alone does
not clear it. It is never inferred from config or a prior connection.
No protocol-era method and no SDK-shaped public port are proposed. boundTools
is an internal execution view over the manager-owned snapshot, not another
registry or a renderer-facing tool list.
Terminology
- legacy era means the pre-
2026-07-28initialization protocol supported by
SDK v2; - modern era means
2026-07-28in this RFC; - protocol fallback selects the legacy era without changing the configured
transport kind; and - transport fallback changes from Streamable HTTP to legacy HTTP+SSE.
A legacy Streamable HTTP server may itself be sessionful or stateless. Only a
server that issues Mcp-Session-Id requires subsequent requests to preserve
that protocol session; modern MCP removes that protocol-level option.
Default policy
| Source/config | Effective preference | Reason |
|---|---|---|
existing/imported config without protocol |
legacy |
No silent wire change for current users |
| new remote entry created in Extensions > MCP | explicit auto |
Modern remote endpoints work without expert setup |
stdio without protocol |
legacy |
Preserve current one-child startup behavior |
| legacy SSE | legacy |
SSE is not a modern transport |
The explicit values mean:
legacy: skip modern probing and use legacy negotiation;auto: use SDK v2's transport-aware probe classifier. Definitive modern
evidence selects modern; otherwise it may fall back while legacy remains
allowed. HTTP auth, 5xx, timeout, and outage outcomes remain errors;2026-07-28: require that exact revision; never downgrade to legacy.
For a remote transport: 'auto' entry with explicit protocol auto, Maka first
uses Streamable HTTP dual-era and only considers legacy SSE under the narrow
transport fallback described below.
transport: 'sse' combined with protocol: 'auto' or '2026-07-28' is rejected
during config normalization. An exact modern pin never falls back to SSE.
PR 2 bumps the MCP config wrapper to version 2. The new store accepts a v1
file as a one-way migration (missing protocol retains the legacy default) and
writes v2 on the next successful mutation/import. PR 3 bumps it to version 3
when stdio gains the field; the final store accepts v1/v2 as one-way
migrations. Each older build rejects the higher wrapper instead of normalizing
away a field it does not understand. This is necessary because erasing an exact
modern pin and then interpreting absence as legacy would be a fail-open
downgrade. Version-absent input is treated as v1 only when it contains no
protocol field; a protocol field under an absent/older wrapper is rejected.
No down-migration is proposed.
Manager behavior and invariants
Negotiation and status
McpClientManager maps the preference to SDK v2 versionNegotiation:
| Maka preference | SDK mode |
|---|---|
legacy |
legacy |
auto |
auto |
2026-07-28 |
{ pin: "2026-07-28" } |
After connection, the manager reads the actual era/revision and publishes it in
status. One live client keeps one immutable era/revision; config changes and
reconnects create a new client and negotiate again.
Stdio process behavior
Maka's proposed stdio auto/pin path uses the SDK's exact base
StdioClientTransport, not a subclass or a custom stdio-shaped transport. For
that SDK implementation:
- starts a disposable sibling using the same launch configuration (
command,
args,cwd, andenv); - sends the
server/discoverprobe; - reaps that sibling; and
- starts the actual caller transport child with the selected era.
A probe outcome classified as legacy is followed by the actual caller child. If
connect is aborted or negotiation terminates with an error, no caller child may
remain. The disposable sibling must always be reaped, and its discarded stderr
must not enter Maka's actual-child stderr tail.
The sibling is an SDK base-transport implementation choice, not an MCP stdio
protocol requirement. Subclasses and custom stdio-shaped transports may probe
in place.
Because startup may be expensive or side-effectful, omitted stdio config stays
legacy. Auto/pin is an explicit choice in the MCP editor's Advanced section,
with copy explaining the short-lived extra process.
The initial design does not persist SDK prior discovery evidence. That would
introduce cache ownership, TTL, authorization-context isolation, invalidation,
corruption recovery, and downgrade behavior before measurements show a need.
Streamable HTTP and legacy SSE
Protocol negotiation happens inside the selected Streamable HTTP attempt.
Changing transport to SSE is a separate Maka policy.
For the initial rollout, transport fallback is one exact classifier. Maka may
attempt SSE only when all of these are true:
- the configured transport is
auto; - the protocol preference allows legacy (never an exact modern pin);
- the Streamable HTTP connect failed before producing valid MCP identity or
handshake evidence; and - the error is an SDK
SdkHttpErrorwith
code === SdkErrorCode.ClientHttpNotImplementedand status404or405.
Those two statuses preserve the current legacy SSE endpoint shape: POST to a
GET-only or absent SSE route fails, then a valid SSE GET confirms the transport.
The classifier uses typed fields, never error-message matching. A failed SSE
attempt returns one normalized connection error retaining both the Streamable
HTTP and SSE causes.
Everything else is a negative fixture and does not attempt SSE: HTTP 400
(including a JSON-RPC/protocol error), 401/403, 408, 409, 415, 429, 5xx;
successful HTTP with unexpected content; timeout, abort, DNS/network failure;
era/pin mismatch; or any failure after valid discover/initialize evidence.
Positive fixtures cover 404 and 405 followed by a valid SSE stream. Paired
fixtures cover those same statuses followed by an invalid SSE response, plus
every negative class above. Expanding the allowlist requires another
evidence-backed change.
Tool discovery and list changes
SDK v2's no-cursor listTools() aggregates pages and writes the result to an SDK
response cache. Maka must not adopt that path as its discovery authority:
- the current manager stops while paging as soon as the page/tool limit is
exceeded, instead of buffering the complete aggregate; and - an invalid refresh could overwrite the SDK cache while Maka correctly keeps
its previous public snapshot. A latercallTool()would then derive output
validation andx-mcp-header/Mcp-Param-*mirroring from an unvalidated
tool definition.
The SDK's public high-level API does not expose the exact seam Maka needs:
no-cursor listTools() aggregates and caches, while its per-page branch
requires an already-present explicit cursor. The manager therefore uses
client.request({ method: 'tools/list', ... }) for every page and owns a narrow
SEP-2243 validator for modern Streamable HTTP. The normative stable rules allow
x-mcp-header only on statically reachable string, integer, or boolean
properties with valid, case-insensitively unique RFC 9110 header tokens.
number definitions are excluded even though the pinned SDK 2.0.0 accepts one
published conformance case; the test suite records that upstream divergence
explicitly rather than calling it parity. An invalid definition excludes that
Tool, not the whole list. Each refresh emits at most one aggregate warning
containing the total exclusion count and a repository-capped sample of
truncated Tool names and stable reason codes. The rendered warning has a fixed
length cap; valid Tools still publish. If the SDK later exports a page/filter
seam, the repository-owned copy can be removed.
The safe-integer rule is call-time, not discovery-time. A Tool with
type: 'integer' remains valid without proving minimum/maximum; a safe
integer argument emits its header, while an unsafe integer fails with a stable
local McpToolCallError before any HTTP request.
The paginator treats nextCursor !== undefined as continuation, so an empty
string remains a valid opaque cursor. It rejects any repeated cursor, enforces
the existing page/tool limits before accepting more data, filters invalid
SEP-2243 tool definitions before publication when modern Streamable HTTP rules
apply, emits at most one length-capped aggregate exclusion diagnostic per
refresh, and never writes the SDK response cache.
The manager owns one private validated snapshot with two atomic views: the
existing public descriptors and a name -> { binding, SDK Tool } map. It swaps
both views only after the complete result validates; an invalid refresh changes
neither view. Every call passes the selected raw Tool through
callTool(..., { toolDefinition }); the SDK response cache is never the
authority for a Maka call.
The current validated snapshot is also execution authority.
buildMcpTools() captures each tool's opaque binding token. On execution, the
manager compares it with the current connection's validated entry. An
unchanged full Tool definition on the same connection retains its token across
a successful refresh; the token derives from the manager-owned connection
generation plus a canonical fingerprint of the full validated definition. A
changed/removed tool or replacement connection invalidates it and fails with a
stable McpToolCallError before any network send. This keeps an active turn
from advertising one input schema while executing with another tool's output
schema or header mapping. Manager change events make Desktop recompute the
main-only bound fingerprint; a changed token refreshes idle backends even when
display descriptors did not change.
Capability omission is also explicit rather than inherited from an SDK default.
For a legacy peer, omission of capabilities.tools does not suppress the
request: the low-level client is configured with
enforceStrictCapabilities: false, and Maka preserves current v1 behavior by
attempting the bounded tools/list rather than entering v2's high-level
empty-list shortcut. For a modern peer, the manager treats omission as a
connected empty tool snapshot without sending tools/list. Both paths have
contract fixtures.
Initial discovery, explicit refreshTools(), and SDK list-change callbacks pass
through the same bounded validate-and-publish function. List-change handling
uses autoRefresh: false, so the callback is a signal rather than a second
unbounded/cache-owning discovery path.
Refresh is a per-connection single-flight, coalescing loop. A signal received
during a fetch marks one follow-up fetch pending; the earlier result is not
published when a newer signal is pending. Each fetch captures its connection
entry, client identity, and connection generation. Only the final non-stale
attempt may atomically replace both views or write the refresh diagnostic; a
disconnect/reconnect invalidates the in-flight attempt. All coalesced callers
settle from that final attempt, so list-change signals are not lost and an old
client cannot overwrite its replacement.
When ClientOptions.listChanged.tools is configured and the server advertises
tools.listChanged, SDK v2:
- invokes the same callback for legacy unsolicited notifications; and
- opens a modern
subscriptions/listenstream for the honored filter.
A refresh error keeps the previous callable tool snapshot, matching current
behavior. SDK Tool/output-schema/header details remain private to
@maka/mcp.
For a modern connection whose server advertised tools.listChanged,
connect-time subscription setup is not assumed successful. After connect, the
manager requires an autoOpenedSubscription whose
honoredFilter.toolsListChanged === true. A rejected/timed-out acknowledgement,
missing handle, or unhonored filter keeps the usable connection and last
validated tools but records a degraded diagnostic in status.error; an
unhonored handle is closed locally. Opening errors reported through
client.onerror are buffered on the candidate connection and published only
if that candidate becomes current. Later errors use the same entry-and-client
identity fence.
Once a handle exists, the manager observes its closed promise. Every callback
is fenced by both the current connection entry and client identity, so a
replaced client cannot degrade its successor. A local close during manager
teardown is ignored. If the reason is graceful or remote while that client
is still current, the manager keeps the last validated tool snapshot, usable
client, and negotiated protocol, records a degraded diagnostic in
status.error, and relies on explicit reconnect to restore delivery. Automatic
re-listen/backoff is not part of this RFC.
The manager keeps refresh and subscription diagnostics in separate private
slots and projects all active diagnostics into the existing status.error in a
stable order. A successful tool refresh clears only the refresh slot; it cannot
hide a subscription degradation. The subscription slot remains until a
replacement connection successfully opens and honors the subscription, or the
server is disconnected. This does not require another public status field.
Results and unsupported interaction
Maka already models structuredContent as unknown and checks presence with
!== undefined, so the public type does not need to change. The wire contract
still differs by era: the legacy 2025-11-25 codec accepts an object, while the
modern 2026-07-28 codec accepts any JSON value. PR 1 therefore preserves the
legacy object case; PR 2 covers object, array, scalar, and null modern results.
Modern resultType is a wire discriminator consumed by SDK v2. A malformed
modern result without it is a protocol error; legacy results continue through
the legacy codec. Maka does not add resultType to McpCallResult.
The client sets inputRequired: { autoFulfill: false } and does not pass
per-call allowInputRequired. If a server returns input_required,
client.callTool() rejects with a typed SDK error, which the manager normalizes
to McpToolCallError rather than leaking the SDK class into Runtime.
Elicitation, sampling, roots callbacks, and durable pause/resume require a
separate authority/interaction design.
Invariants
- Callers never branch on protocol era.
- Pinning
2026-07-28never silently downgrades. - Connected status reports the current live client, not intent or stale
evidence. - Public descriptors and raw Tool definitions publish atomically; refresh
failure keeps both previous views. - An active backend either executes the same validated tool binding it
advertised or fails stale before network send. - Protocol negotiation cannot bypass
ToolRuntime's pre-implementation call
recording, abort propagation, telemetry, result normalization, or, when a
Runtime commit sink is configured, its T1/T2 durable boundary. input_requiredcannot trigger an implicit callback or hidden side effect.- Closing/cancelling a connection reaps every child and subscription it owns.
Proposed rollout
PR 1: SDK v2 migration, legacy protocol era only
- Replace the monolithic v1 dependency with v2 packages.
- Configure production clients explicitly for legacy behavior.
- Keep config/status unchanged and port transports, cancellation/timeouts,
stderr, bounded pagination, opaque tool bindings, private Tool snapshots, and
result normalization. - Add the main/runtime-only
McpBoundToolprovider seam; keep renderer status
and persisted/audit arguments descriptor-only, and include bindings in
Desktop's idle-backend refresh fingerprint. - Set
enforceStrictCapabilities: falseexplicitly for the low-level legacy
request path, preserving current capability-omission behavior without
entering the v2 high-level empty-list shortcut. - Use low-level
tools/listrequests plus the repository-owned SEP-2243
validator; preserve early page/tool limits, empty/repeated cursor semantics,
per-Tool exclusion with one bounded aggregate diagnostic per refresh,
normative primitive-type behavior, and no-cache publication. - Preserve Maka's complete-result contract: a legacy deferred/task result
withoutcontentbecomes a stableMcpToolCallError, without the current
“unsupported by V1” wording. - Use
@modelcontextprotocol/serverand@modelcontextprotocol/nodefor modern
or Streamable HTTP fixtures, and
@modelcontextprotocol/server-legacy/ssetest-only for the legacy SSE fixture. - Keep at least one real v1.29 peer fixture rather than testing only a v2 server
in legacy mode. - Keep the legacy stdio/HTTP/SSE matrix green and update the lockfile and
third-party notices.
PR 2: remote Streamable HTTP dual-era
- Add
protocoltoMcpRemoteServerConfigonly and validate it in storage/UI. - Bump the config wrapper to v2, read v1 as a one-way migration, and ensure an
older build rejects rather than rewrites v2. - Add live
negotiatedProtocolstatus. - Preserve omitted remote config as legacy; make newly created remote
Extensions > MCP entries write explicitauto. - Add modern/legacy HTTP wire fixtures and narrow SSE fallback.
- Adopt the shared list-change path and fail closed on
input_required. - Cover renderer draft/serialization, JSON/store round trips, MCP editor/catalog
defaults, and status UI so editing a server cannot eraseprotocol.
Stdio has no protocol field in this intermediate state, so every merged commit
remains usable.
PR 3: explicit stdio dual-era
- Extend
McpStdioServerConfigwithprotocol. - Bump the config wrapper to v3 and migrate v1/v2 forward; v1/v2 builds reject
rather than rewrite a v3 stdio pin. - Add the choice to the MCP editor's Advanced section and explain the extra
process. - For the exact base SDK
StdioClientTransport, test sibling/actual-child
ordering, matching launch configuration, abort, terminal negotiation errors,
stderr isolation, and process reaping. Custom transport behavior is outside
this assertion. - Keep omitted stdio behavior legacy/one-child.
Acceptance criteria
- Existing legacy stdio, Streamable HTTP, and SSE fixtures remain usable.
- A v1 config migrates to v2 with absent-as-legacy semantics; protocol pins
survive every v2 round trip, and a v1 build rejects rather than erases v2. - A v1/v2 config migrates to final v3; a v2 build rejects rather than erases a
v3 stdio pin. - A
protocolfield under a missing or older wrapper version is rejected
rather than guessed. - Contract fixtures cover legacy and modern missing-tools-capability behavior,
early stop on oversized pagination, empty and repeated cursors, SEP-2243
normative cases, the pinned SDKnumberdivergence, a legacy
content-less/deferred result, and SDK-error normalization. - Invalid
x-mcp-headerdefinitions exclude only those Tools while valid Tools
still publish. A fixture with more invalid Tools than the diagnostic sample
cap, including overlong names and multiple reason codes, emits exactly one
aggregate warning with the total exclusion count and a fixed rendered-length
cap. - A
type: 'integer'definition remains discoverable; a safe integer argument
emits the mirrored header, while an unsafe integer fails locally before the
HTTP wire. - After an invalid refresh, calling a previously valid tool still uses its old
validated schema/header definition. - After a successful refresh, an already-bound tool whose definition was
removed/changed (or whose connection was replaced) fails stale before the
wire; an unchanged same-connection binding remains callable. - A raw-definition-only change or same-descriptor replacement connection
changes the main-only bound fingerprint and refreshes idle backends, while an
already-active backend stale-fails. - A refresh signal received during a slow refresh causes one coalesced follow-up;
the first result is not published, and an in-flight old-client refresh cannot
write after reconnect. - Remote auto connects to both legacy and modern Streamable HTTP fixtures.
- Exact modern pin fails rather than downgrades against a legacy server.
- Omitted stdio protocol starts exactly one child.
- A modern-only stdio fixture rejects the legacy client and succeeds under
explicit SDK-v2 auto/pin withmodern / 2026-07-28status. - With the exact base SDK
StdioClientTransport, explicit stdio auto/pin always
reaps the same-launch-configuration probe sibling; abort/terminal error leaves
no actual caller child. - Legacy notifications and modern honored subscriptions refresh through the
same validation path. - Refresh failure retains the previous callable snapshot.
- A bounded explicit refresh reaches the wire regardless of SDK cache
freshness; a non-local subscription close is identity-fenced, retains the
snapshot, and exposes a degraded diagnostic. - A rejected/timed-out subscription acknowledgement, missing auto-opened
handle, or unhonored tools filter exposes a degraded diagnostic without
replacing the usable connection. - A successful manual refresh clears its own error but does not clear an active
subscription degradation; a successful reconnect clears both. - Disconnect, replacement, and every transition to
state: 'error'clear
negotiatedProtocol; connected degraded diagnostics retain it. - Legacy structured-object output survives normalization; modern object, array,
scalar, andnulloutput survive once PR 2 enables the2026-07-28codec. input_requiredrejects without invoking an implicit handler.- Typed 404/405 pre-identity failures followed by a valid SSE stream are the only
auto-transport fallback successes. The paired invalid-SSE cases and every
listed 400/auth/rate-limit/5xx/content/timeout/abort/network/pin case do not
silently switch transports. - Runtime integration tests prove that a modern MCP tool remains
network_sendand is omitted in Plan mode. Without a commit sink, the manager
is reached only afterToolRuntime's normal pre-implementation call
recording. With a commit sink, they additionally prove that T1 commits before
McpClientManager.callTool()and T2 settles after its result.
Non-goals
- MCP Apps or Tasks;
- OAuth browser flow or credential storage;
- elicitation/sampling/roots and multi-round-trip UI;
- resources/prompts UI or resource subscriptions;
- persistent era/discovery caching;
- serving Maka as an MCP server or adding a loopback proxy;
- Runtime Host ownership migration or CLI parity;
- per-MCP user approval or mapping main-process MCP calls to a session network
boundary; - finer-grained MCP permission categories;
- complete MCP
2026-07-28authentication/extension conformance; - automatic crash recovery/backoff;
- removing legacy SSE; or
- changing Maka Session/WAL semantics.
Alternatives considered
- Expose the full SDK policy: probe retry/timeout,
prior, and cache controls
would make the public config mirror SDK mechanics. - Persist an era verdict immediately: this saves probes but introduces
storage/freshness/authorization-context semantics; defer until measured. - Wait for an exported SDK page/filter seam: this would avoid the narrow
SEP-2243 normative validator, but blocks bounded migration on an API that does
not exist in v2.0. Keep the validator isolated and remove it if the SDK
exports that seam. - Interpret omitted remote config as
auto: this is simpler for new JSON
imports, but silently adds a probe to every existing remote entry. The
proposal instead preserves absent-as-legacy and writesautofor new
Extensions > MCP entries.
References
- MCP
2026-07-28stable release - MCP
2026-07-28changelog - Protocol eras and negotiation
- Supporting
2026-07-28with SDK v2 - Stable Streamable HTTP schema extension
- Stable stdio transport
- SEP-2243 HTTP header standardization
- SDK v1 to v2 migration
- Legacy Streamable HTTP session management
- Maka PR #1581: remove generic permission engine
- Current
McpClientManager - Current MCP Runtime adapter
- Current Plan-mode filter
- Current
ToolRuntime
Activity
me2seeks commentedon Jul 30, 2026
I updated the RFC after a second pass over current main and the final 2026-07-28 spec/SDK v2 docs.
The revision now matches the Runtime contract after #1581: MCP calls still go through ToolRuntime's normal recording, abort, telemetry, and result-normalization path, with T1/T2 additionally enforced when a Runtime commit sink is configured. It also makes the SEP-2243 policy normative-first (
numberdefinitions are rejected;integerremains valid and is range-checked at call time), bounds invalid-Tool diagnostics per refresh, and keeps per-MCP approval/network-boundary work outside this protocol migration.I rechecked main at
eee37b81; the commits since the RFC's pinned source snapshot do not change the relevant MCP or Runtime paths. I'm starting with the first rollout slice now: SDK v2 in explicit legacy mode, preserving the current config, status, and user-visible transport behavior.me2seeks commentedon Jul 30, 2026
One implementation probe exposed an era-specific detail worth correcting in the RFC: the SDK v2 legacy codec still accepts structured output as an object, while the modern
2026-07-28codec accepts any JSON value. I updated the rollout/acceptance wording so PR 1 preserves the legacy object contract and PR 2 covers object, array, scalar, andnullmodern results. Maka's publicstructuredContent?: unknowntype remains unchanged.fix(mcp): own discovery by connection generation (#2663)
docs: link design work to issue trackers (#4333)
docs: link design work to issue trackers (apache#4333)