Skip to content

[RFC] Support legacy and 2026-07-28 MCP servers through dual-era negotiation #1650

Closed

Description

@me2seeks

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:

  1. migrating the internal MCP implementation to the v2 TypeScript SDK packages;
  2. retaining McpClientManager as the seam that hides legacy/modern wire
    behavior;
  3. adding one optional per-server protocol preference and one live negotiated
    protocol status;
  4. preserving omitted config as legacy, while new remote entries created in
    Extensions > MCP (editor or catalog) write an explicit auto; and
  5. preserving the existing ToolRuntime execution 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 initialize sequence;
  • 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 honored subscriptions/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/McpClientManager to establish a connection,
    including starting a configured stdio process;
  • McpClientManager owns live connection/protocol truth;
  • ToolRuntime records the model tool call before invoking the MCP
    implementation; when runtimeCommitSink is configured, it additionally
    commits T1 before McpClientManager.callTool() and settles the result through
    T2;
  • McpClientManager and the selected SDK transport own the actual MCP
    network/stdio side effect;
  • RuntimeEvent records 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-28 initialization protocol supported by
    SDK v2;
  • modern era means 2026-07-28 in 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:

  1. starts a disposable sibling using the same launch configuration (command,
    args, cwd, and env);
  2. sends the server/discover probe;
  3. reaps that sibling; and
  4. 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 SdkHttpError with
    code === SdkErrorCode.ClientHttpNotImplemented and status 404 or 405.

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 later callTool() would then derive output
    validation and x-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/listen stream 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

  1. Callers never branch on protocol era.
  2. Pinning 2026-07-28 never silently downgrades.
  3. Connected status reports the current live client, not intent or stale
    evidence.
  4. Public descriptors and raw Tool definitions publish atomically; refresh
    failure keeps both previous views.
  5. An active backend either executes the same validated tool binding it
    advertised or fails stale before network send.
  6. 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.
  7. input_required cannot trigger an implicit callback or hidden side effect.
  8. 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 McpBoundTool provider seam; keep renderer status
    and persisted/audit arguments descriptor-only, and include bindings in
    Desktop's idle-backend refresh fingerprint.
  • Set enforceStrictCapabilities: false explicitly 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/list requests 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
    without content becomes a stable McpToolCallError, without the current
    “unsupported by V1” wording.
  • Use @modelcontextprotocol/server and @modelcontextprotocol/node for modern
    or Streamable HTTP fixtures, and
    @modelcontextprotocol/server-legacy/sse test-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 protocol to McpRemoteServerConfig only 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 negotiatedProtocol status.
  • Preserve omitted remote config as legacy; make newly created remote
    Extensions > MCP entries write explicit auto.
  • 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 erase protocol.

Stdio has no protocol field in this intermediate state, so every merged commit
remains usable.

PR 3: explicit stdio dual-era

  • Extend McpStdioServerConfig with protocol.
  • 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 protocol field 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 SDK number divergence, a legacy
    content-less/deferred result, and SDK-error normalization.
  • Invalid x-mcp-header definitions 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 with modern / 2026-07-28 status.
  • 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, and null output survive once PR 2 enables the 2026-07-28 codec.
  • input_required rejects 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_send and is omitted in Plan mode. Without a commit sink, the manager
    is reached only after ToolRuntime'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-28 authentication/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 writes auto for new
    Extensions > MCP entries.

References

Activity

  1. me2seeks commented on Jul 30, 2026

    @me2seeks
    ContributorAuthor

    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 (number definitions are rejected; integer remains 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.

  2. me2seeks commented on Jul 30, 2026

    @me2seeks
    ContributorAuthor

    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-28 codec 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, and null modern results. Maka's public structuredContent?: unknown type remains unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions