- Status: Living roadmap — direction accepted; individual designs require their phase gates
- Target: ExMCP
2.0.0, after stable1.0.0and the supported 1.x line - Last updated: 2026-08-12
- Related release work:
RELEASE_1_0_0_RC_7.md,RELEASE_1_0_0_RC_6.md,MCP_2026_07_28_MIGRATION_PLAN.md
1. Purpose
ExMCP 1.0 establishes a dual-era MCP implementation with a broad, compatible public API. ExMCP 2.0 should make that implementation easier to operate, extend, and reason about without turning the major release into an unrelated rewrite.
This roadmap records:
- the changes intentionally reserved for a major release;
- which ideas from Anubis MCP and the external Grok review are being adopted;
- which ideas are useful only with constraints, and which are rejected;
- the order in which the runtime, dispatch, storage, and public API should evolve; and
- the rules for safely backporting selected work to 1.x.
It is the canonical ExMCP 2.0 planning document. The similarly named
PRE_2_0_TECH_DEBT_PLAN.md is completed rc.5
release history, not the 2.0 roadmap.
2. Inputs are evidence, not specifications
The roadmap uses three inputs:
- ExMCP's current architecture, tests, deprecations, and 1.0 release gates.
- Anubis MCP, especially its smaller public surface, component boundaries, supervision-oriented design, and developer ergonomics.
- The Grok design review, which proposed improvements around dispatch, handler scheduling, result and schema helpers, persistence adapters, protocol boundaries, telemetry, and client ergonomics.
Neither external project defines ExMCP's compatibility contract. Ideas are accepted only when they solve an observed ExMCP problem and fit OTP, MCP wire compatibility, and the maintenance cost of a multi-transport library. ExMCP will not copy another library's public API merely to look familiar.
3. Guiding constraints
3.1 Preserve protocol capability
The 2.0 package version and MCP protocol revisions are separate compatibility boundaries. A public Elixir API redesign does not justify losing modern MCP conformance or silently dropping legacy protocol support.
Any removal of a legacy MCP revision or deprecated protocol feature requires a separate decision with usage evidence, notice, and migration guidance. It is not an automatic consequence of releasing ExMCP 2.0.
3.2 Prefer one runtime owner
A started MCP server should own its registries, handler execution, session and subscription state, replay store, and telemetry identity through one supervision subtree. Process-global state makes multiple servers in one VM hard to isolate and obscures restart behavior.
3.3 Keep concurrency bounded and explicit
Running every handler in an unlinked task would improve throughput while weakening cancellation and state guarantees. ExMCP should use supervised, bounded work and define state-commit semantics before enabling concurrent callbacks.
Stateful handlers remain serialized by default. Concurrent execution is for handlers that explicitly select a stateless, re-entrant, or otherwise isolated state model.
3.4 Make side effects replaceable, not mandatory
The core should define small store contracts for state that must outlive a connection. ETS remains a useful default. Durable or clustered implementations should be replaceable adapters rather than database dependencies in the core package.
3.5 Spend the breaking-change budget deliberately
The major release should remove deprecated APIs, clarify ownership, and regularize callback/result contracts. Cosmetic renames, duplicate aliases, and speculative abstraction consume migration effort without producing the same value.
4. Current 1.0 baseline
The 1.0 line already provides:
- dual-era MCP support through explicit protocol modes;
- a canonical method registry and shared server dispatch primitives;
ExMCP.Server.HandlerandExMCP.Server.DSLas the preferred server APIs;- modern request context, result envelopes, MRTR, subscriptions, and Tasks;
- stdio, Streamable HTTP, legacy HTTP+SSE, BEAM-local, and test transports;
- ACP client, agent, and adapter support; and
- conformance, characterization, property, security, and interoperability coverage.
The 1.0.0-rc.7 candidate packages legacy SSE persistence end to end:
events are stored before delivery, connection gaps retain their session, and
Last-Event-ID resumes from the retained cursor. Together with the
2026-08-12 security harden and ACP fixes, this is 1.0 release work, not a
reason to wait for 2.0. It requires a fresh soak before stable 1.0.
The remaining architectural pressure is concentrated in runtime ownership, callback execution, storage contracts, duplicated public concepts, and the size of the compatibility surface.
5. Decision register
| Idea | Source | Decision | Target | Rationale |
|---|---|---|---|---|
| Per-server runtime and scoped registries | Anubis comparison; ExMCP review | Adopt | 2.0 | Makes ownership, multi-server isolation, restart behavior, and testing explicit. |
| Supervised handler scheduler | Grok review | Adopt with constraints | 2.0 | Use bounded Task.Supervisor work, cancellation propagation, and explicit state semantics; never fire-and-forget tasks. |
| One dispatch/context/result pipeline | Both reviews; existing ExMCP drift | Adopt | 2.0 foundation | All transports should observe the same authorization, timeout, telemetry, normalization, and error behavior. |
| Event and session store behaviours | Grok review; SSE implementation | Adopt | Design in 2.0; adapter may later backport | ETS remains the default; append/replay/TTL/delete semantics must be specified before adapters are public. |
| Small result-construction API | Anubis comparison; Grok review | Adopt | 2.0 | Consolidate result helpers and normalizers instead of adding another parallel facade in 1.x. |
| Richer DSL parameter constraints | Both reviews | Adopt selectively | 2.0 | Add high-value JSON Schema constraints without trying to reproduce the entire schema vocabulary as macros. |
Client with_connection lifecycle helper | Grok review | Adopt | 2.0 | Useful ergonomics, but it should be designed with ownership, links, shutdown, and reconnect behavior together. |
| Component grouping and composition | Anubis comparison | Adopt with constraints | 2.0 | Prefer compile-time composition and scoped runtime registration; avoid a second DSL or unconstrained global component registry. |
| Central additive telemetry | Grok review | Adopt | 1.x-compatible subset, complete in 2.0 | Instrument the shared dispatch boundary; payload capture stays opt-in and bounded. |
| Public middleware/pipeline API | Grok review | Defer | Reconsider after internal pipeline lands | First prove stable phases and use cases internally; a premature public pipeline becomes another compatibility surface. |
| General protocol-dialect framework | Grok review | Defer | Only with two concrete consumers | Keep existing era/version modules unless a second protocol family demonstrates that a general dialect abstraction removes real duplication. |
| Built-in distributed database/event sourcing | External review extrapolation | Reject for core | External adapters | ExMCP should define contracts, not require a database or event-source all runtime state. |
| Copy Anubis APIs or rewrite ExMCP around them | Comparison exercise | Reject | — | ExMCP has broader protocol, transport, authorization, ACP, and compatibility requirements. |
| Treat every additive API as a safe 1.x backport | Backport discussion | Reject | — | Additions create support obligations and can still change lifecycle, defaults, ordering, or wire behavior. |
| Remove every legacy MCP feature in 2.0 | Version-number assumption | Reject as a default | Separate decision | Package-major and protocol-version policies are independent. |
6. Target runtime shape
The target is one server-owned supervision subtree with a common dispatch boundary. Transport-specific framing stays at the edges.
flowchart LR
A["Transport adapters"] --> B["Server runtime"]
B --> C["Request context and policy"]
C --> D["Bounded request scheduler"]
D --> E["Handler / DSL callback"]
E --> F["Result and error normalizer"]
F --> A
B --> G["Scoped registries"]
B --> H["Session / event / subscription stores"]
C --> I["Telemetry boundary"]
D --> I
F --> IThe runtime reference, not a global registered name, should identify a server instance. Convenience startup may still provide a default name, but core code must accept an explicit runtime or server reference.
Runtime-owned children
A server runtime should own, as applicable:
- the handler state owner;
- the bounded request task supervisor and scheduler;
- session and SSE-handler registration;
- event replay storage;
- subscription indexes and optional cluster fanout;
- request-state/replay caches;
- transport listeners; and
- a stable telemetry identity.
Failures must have a documented blast radius. Restarting a transport listener must not silently discard an independently configured durable event store, and a failed request task must not terminate unrelated requests or another server instance.
7. Delivery phases
Phases are dependency ordered. A later phase may be prototyped early, but it does not merge to the 2.0 release branch until its prerequisites pass.
Phase 0 — Finish and freeze the 1.0 baseline
Goal: establish the exact behavior from which 2.0 migrates.
- Publish
1.0.0-rc.7with the legacy SSE persistence fix, ACP fixes, and 2026-08-12 security harden. - Repeat the full conformance, interop, security, API, and load gates.
- Restart the minimum soak after rc.7 is published.
- Complete the mixed-version rollback drill.
- Tag stable 1.0 only from a release candidate with identical wire and public behavior.
Exit: stable 1.0.0 is published, and its public/wire characterization
fixtures are committed as the 2.0 comparison baseline.
Phase 1 — Specify the 2.0 contract before moving code
Goal: make breaking changes reviewable rather than emergent.
- Inventory documented modules, exports, callbacks, types, structs, options, process names, telemetry, and wire-visible defaults.
- Publish the proposed removal and replacement table.
- Write focused decision records for runtime ownership, handler state and concurrency, store contracts, and legacy-protocol support.
- Add characterization tests for callback process identity, links, timeout and cancellation behavior, state ordering, and per-server isolation.
- Define the migration path for every removal before deleting it.
Exit: maintainers can answer what breaks, why it breaks, and how users migrate without referring to implementation diffs.
Phase 2 — Introduce ExMCP.Server.Runtime
Goal: replace process-global ownership with one explicit server boundary.
- Add a runtime child specification and stable runtime reference.
- Move registries, sessions, subscriptions, and request caches under the server's supervisor.
- Allow multiple independent server runtimes in one VM without key collisions or shared cleanup.
- Define restart strategies and ownership of external adapter processes.
- Keep transports thin: they resolve a runtime and submit framed requests.
Exit: isolation tests can start, crash, restart, and stop two servers independently; no request/session/subscription state crosses the boundary.
Phase 3 — Unify dispatch and add bounded scheduling
Goal: make every transport use the same request semantics.
- Route stdio, HTTP, BEAM-local, and test requests through one dispatch entry.
- Centralize request context, authorization outcome, method lookup, deadlines, telemetry, result normalization, and error mapping.
- Execute eligible callbacks through a runtime-owned
Task.Supervisorwith configurable bounds and queue pressure policy. - Propagate disconnects, cancellation, deadline expiry, and supervisor shutdown to request work.
- Keep stateful callbacks serialized by default. Require an explicit execution mode before callbacks may run concurrently.
- Document ordering guarantees for replies, notifications, and state commits.
Exit: cross-transport golden tests produce equivalent results and errors; stress tests prove bounded processes, mailboxes, queues, and cancellation time.
Phase 4 — Define state and replay adapters
Goal: make connection-surviving state replaceable without leaking backend details into transports.
Define narrow behaviours for the state that benefits from replacement. The event-store contract must cover at least:
- atomic append returning a store-owned opaque event ID;
- ordered replay after an exact cursor;
- bounded retention and cursor-eviction behavior;
- session TTL and explicit deletion;
- idempotent overwrite/deduplication expectations;
- adapter ownership and restart behavior; and
- telemetry that excludes event payloads by default.
Ship supervised ETS implementations as the defaults. A filesystem, Mnesia, PostgreSQL, or third-party clustered adapter can be supplied separately once it passes the same contract suite.
Exit: the SSE reconnect suite passes unchanged against every supported adapter, including disconnect-after-append and publish-during-gap races.
Phase 5 — Consolidate the public server API
Goal: make the common path smaller while retaining low-level control.
- Consolidate result constructors and normalization behind one
ExMCP.Server.Resultcontract; avoid parallelDSL.Result, response helper, and transport-specific result vocabularies. - Add selected DSL constraints such as numeric/string bounds, patterns, defaults, enums, and nested array/object schemas where they produce valid, inspectable JSON Schema.
- Support compile-time component grouping without adding a second tool DSL.
- Add a
with_connectionclient helper or an equivalent bracketed lifecycle helper with explicit ownership and shutdown behavior. - Keep one-file client/server examples as acceptance tests for the public API.
Exit: the quick-start server, advanced server, and client lifecycle require fewer concepts than 1.x, while low-level Handler users retain a complete path.
Phase 6 — Remove deprecated and duplicated surface
Goal: spend the major-version compatibility budget on known debt.
Planned removals include:
ExMCP.Server.Tools,Tools.Simplified, and their companion modules after the DSL migration path is verified;- deprecated image transformation stubs that are outside MCP/ACP scope;
- retained aliases and compatibility functions that have a documented 2.0 replacement; and
- global runtime entry points superseded by explicit server references.
Legacy HTTP+SSE, Roots, Sampling, Logging, legacy subscriptions, and older MCP revisions are not on this list by default. Their disposition requires the separate protocol-support decision from Phase 1.
Exit: no removal lacks a migration example, deprecation history, and API diff entry.
Phase 7 — Migration and release qualification
Goal: prove that 2.0 is a controlled migration, not only a green unit suite.
- Generate a 1.x-to-2.0 API diff for modules, exports, callbacks, types, structs, options, and documented process names.
- Publish a migration guide with before/after examples for every removal.
- Run all supported MCP conformance and official-SDK interoperability lanes.
- Run cross-transport equivalence, multi-runtime isolation, cancellation, pressure, persistence-adapter, security, and upgrade tests.
- Establish performance budgets against the final 1.x release on the same runner.
- Ship at least one 2.0 RC and require a soak appropriate to the runtime and persistence changes.
Exit: every release gate has an owner and durable evidence; stable 2.0 is behavior-identical to its final RC except for release metadata.
8. The 1.x backport lane
The roadmap deliberately permits some 2.0-derived work in 1.x. “No removed function” is not enough to qualify a backport.
8.1 Required backport tests
A 1.x backport must satisfy all applicable conditions:
- It fixes documented/spec behavior, or is additive and opt-in/default-off.
- Existing function signatures, callbacks, return shapes, structs, and types remain compatible.
- Existing wire output, ordering, defaults, and negotiated behavior remain compatible except for an explicitly documented bug or security fix.
- Process ownership, callback process identity, links, cancellation, restart behavior, and state ordering do not change unexpectedly.
- Resource usage remains bounded and existing telemetry contracts do not change.
- Characterization and end-to-end regression tests land with the change.
A failed condition sends the work to 2.0 unless maintainers explicitly approve another 1.x RC or document a patch-level correctness/security exception.
8.2 Current classifications
| Change | 1.x decision | Notes |
|---|---|---|
| Legacy SSE persist-before-delivery, gap replay, and session retention | Packaged in 1.0.0-rc.7 | Behavior correction requires a fresh RC soak. |
| Correct bounded replay-buffer retention | Backport with SSE fix | Correctness fix with regression coverage. |
| Documentation, examples, diagnostics, and characterization tests | Backport | No runtime compatibility cost. |
| Additive dispatch telemetry | Eligible for a 1.x minor | Preserve existing events; bounded metadata only; payload capture opt-in. |
| Store adapter seam | Possible later 1.x minor | Only after the 2.0 contract is designed; ETS must remain the default and current behavior unchanged. |
| Internal dispatch deduplication | Case by case | Backport only when golden tests prove identical wire, errors, ordering, and lifecycle. |
| Richer DSL constraints | Hold for 2.0 | Additive, but expands the stable public language before its design is settled. |
| Result facade and client lifecycle helpers | Hold for 2.0 | Technically additive, but would create parallel APIs and long-term support obligations. |
| Per-server runtime/scoped registries | 2.0 only | Changes ownership, naming, restart, and cleanup behavior. |
| Concurrent handler scheduler | 2.0 only | Changes callback process identity, state timing, cancellation, and failure behavior. |
| Callback/context/result contract cleanup | 2.0 only | Public semantic change even if compatibility shims could preserve arities. |
| Deprecated API removals | 2.0 only | Breaking by definition. |
8.3 SemVer interpretation
For ExMCP, observable compatibility includes more than exported functions:
- JSON-RPC and SSE wire shapes, event ordering, and opaque cursor behavior;
- callback invocation and returned state ordering;
- which process executes user code and what it is linked to;
- timeout, cancellation, retry, and disconnect behavior;
- supervision names, registry scope, and restart effects;
- defaults and configuration precedence;
- telemetry names and established metadata; and
- documented side effects such as session cleanup.
Opaque SSE event IDs may change representation because clients must not parse them. Losing events, replaying duplicates, terminating a resumable session on disconnect, or changing callback execution processes is observable behavior.
9. Explicit non-goals
ExMCP 2.0 is not intended to:
- replace OTP supervision with an application-level framework;
- require Phoenix, Ecto, a database, or a distributed registry;
- expose transport internals through the common server API;
- add an abstraction for every possible protocol or JSON Schema keyword;
- make stateful handlers concurrently mutable by default;
- remove legacy MCP support solely because the package major changed; or
- preserve deprecated APIs under new names indefinitely.
10. Open decisions
These need focused design records during Phase 1:
- Handler state model: whether 2.0 supports serialized stateful and concurrent stateless modes only, or also defines isolated/partitioned state.
- Runtime reference: PID, registered name, supervisor reference, or an opaque struct, including how it crosses Plug configuration boundaries.
- Store split: one session/replay behaviour or separate session, event, subscription, and request-state behaviours.
- Legacy protocol policy: which deprecated protocol features remain for the complete 2.x line and what evidence could justify removal.
- Component composition: compile-time-only composition versus constrained runtime registration and how capability-change notifications are emitted.
- Public pipeline: whether concrete authorization/telemetry middleware use cases justify exposing the internal dispatch phases.
An open decision is not permission to let an implementation choose the public contract accidentally.
11. Roadmap maintenance
- Update the decision register when a design is accepted, rejected, or moved between release lines.
- Mark completed phase items in their implementation PRs; do not use this file as an issue tracker for individual code changes.
- Link substantial design records from the relevant phase.
- Record every 1.x backport in the classification table and
CHANGELOG.md. - Revisit external comparison inputs for ideas, but evaluate them against the current ExMCP baseline rather than treating parity as a goal.