From ae3c579dd7de8591454c5b799b4ec6c4d53b5b9e Mon Sep 17 00:00:00 2001 From: mrsibe Date: Fri, 25 Sep 2026 21:08:30 +0800 Subject: [PATCH 1/2] docs(adr): decide the local MCP server, pinned to spec revision 2026-07-28 Closes #79. Writes ADR 0001: KnowNote is the MCP *server* and MCP is its export surface, not an agent platform (`#38` is the demand signal and prior art only - its fork is not merged or used as a template). The protocol revision is the part that had to be checked against the live spec rather than a summary, and it changes the shape of the work. Verified on 2026-09-25: `2026-07-28` is the newest non-prerelease revision in the specification repository. That revision **removed the `initialize` handshake** - "There is no negotiation handshake" - and moved version, capabilities and identity into per-request `_meta`. So the review comment in #38 that the revision "changed session/handshake behaviour" is right, and anything built from an older tutorial implements an era the protocol no longer has. The ADR therefore commits to modern (`2026-07-28`) only, with `server/discover` (the spec's MUST-implement discovery RPC, usable as a backward-compatibility probe on stdio) and a named revisit trigger instead of speculative dual-era support. It also records the revision's concrete obligations on #80: required `resultType` on every result, `ttlMs`/`cacheScope` on list results, deterministic `tools/list` order, the removed `ping`/`logging/setLevel`, and that Roots/Sampling/Logging are deprecated and must not be added. Decisions recorded: stdio-only transport (no network listener, so no remote attack surface); a small read-only tool surface with `read_document` bounded by page; explicit opt-in visible in the UI; no path exposure (the reader's bytes-by-id rule); and the reuse requirement - the same `Retriever`/`RetrievedEvidence` the chat path and eval harness use, never a second RAG path. One correction to its own inputs: #79 and #80 both say the server should sit on a `CitationService`, which does not exist. #60 recorded that it was deliberately never created - citation parsing and source-anchor mapping are stateless and shared between main and renderer, so they are pure functions in `src/shared/utils/`. The ADR names the real modules, because otherwise #80 sends someone hunting for a class that was never written. Verification: the spec revision, the removed handshake, `server/discover` and the deprecations are quoted from the fetched specification pages, and the revision is confirmed against the specification repository's release list. Documentation only - no code, no behaviour change. --- docs/adr/0001-knowledge-mcp-server.md | 205 ++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 docs/adr/0001-knowledge-mcp-server.md diff --git a/docs/adr/0001-knowledge-mcp-server.md b/docs/adr/0001-knowledge-mcp-server.md new file mode 100644 index 0000000..e037a02 --- /dev/null +++ b/docs/adr/0001-knowledge-mcp-server.md @@ -0,0 +1,205 @@ +# ADR 0001 — KnowNote as a local MCP server + +- **Status:** Accepted — implementation deferred to #80 (v1.5) +- **Date:** 2026-09-25 +- **Issues:** #38 (prior art), #79 (this decision), #80 (implementation), #94 (scoped search) +- **Related:** `docs/architecture.md` (the Retrieval seam, the reader's byte-serving rule) + +## Context + +#38 is a user request: they reached KnowNote from an external agent and published the result as a +fork. It is real demand, and the fork is not something to merge — it was written by someone who says +they have no programming background, against a protocol revision that has since changed. + +The wrong response is to become an agent platform. That is the AnythingLLM / SurfSense feature race, +and it would make KnowNote a worse knowledge base. The intended shape is the opposite: + +```text +Claude Code / Codex / Cursor / Claude Desktop / other agents + │ + ▼ + KnowNote MCP ← KnowNote is the SERVER + │ + list / search / read / citations + │ + ▼ + the user's local knowledge base +``` + +KnowNote stays a desktop knowledge layer; MCP is its **export surface**. + +Most of what an MCP server needs already exists, and one piece of it was written with MCP in mind. +`src/main/services/retrieval/types.ts` says so in its own header comment: + +> 检索是一等能力,不是 Chat 的附属逻辑。Chat、MCP、Search 面板都通过同一个 `Retriever` 拿到同一份 +> `RetrievedEvidence`,因此不必各自重写一遍 RAG,也让 #75 的 eval harness 有一个稳定的入口。 + +So this ADR is mostly about **not forking a query path**, and about pinning the one thing that has +already changed underneath the prior art. + +## Decision + +### 1. Target protocol revision: `2026-07-28` + +**Verified against the live specification on 2026-09-25**, not a summary: + +- The specification is published at `modelcontextprotocol.io/specification/2026-07-28/`. +- The specification repository's release list shows `2026-07-28` as the newest **non-prerelease** + revision (`published 2026-07-28`), preceded by `2025-11-25`, with `2026-07-28-RC` as its + prerelease. Nothing newer exists. + +This matters because the revision #38 was written against is **not** a small delta. From +`/specification/2026-07-28/changelog`: + +> **Make MCP stateless: remove the `initialize`/`notifications/initialized` handshake.** Every request +> now carries its protocol version and client capabilities in `_meta` +> (`io.modelcontextprotocol/protocolVersion`, `io.modelcontextprotocol/clientCapabilities`). + +and from `/specification/2026-07-28/basic/versioning`: + +> **There is no negotiation handshake.** Every request carries its protocol version, and the server +> accepts or rejects each request independently + +The specification names the two eras explicitly: + +| Term | Meaning | +| ------------ | ------------------------------------------------------------------------------------------ | +| **Modern** | `2026-07-28` and later — version, identity and capabilities travel as per-request metadata | +| **Legacy** | `2025-11-25` and earlier — a session is established with an `initialize` handshake | +| **Dual-era** | supports both | + +**Decision: implement modern (`2026-07-28`) only.** There is no session to establish, so a server that +follows old tutorials or the #38 fork would implement a handshake the protocol no longer has. The +spec also requires a discovery RPC, which is what makes a modern-only server discoverable rather than +just incompatible: + +> Add `server/discover`: servers **MUST** implement this RPC to advertise their supported protocol +> versions, capabilities, and identity. Clients **MAY** call it before any other request for up-front +> version selection, or use it as a backward-compatibility probe on STDIO. + +**Revisit trigger:** if a client the user actually wants to use cannot speak `2026-07-28`, add the +legacy handshake as a dual-era shim. Not before — dual-era support is real complexity, and +`server/discover` is the designed answer to "what do you speak?". + +Concrete requirements this pins on #80, all from the same revision: + +- `server/discover` **MUST** be implemented; it is not optional. +- Every result carries a required `resultType` (`"complete"`, or `"input_required"` for multi + round-trip); clients must treat a missing field from older servers as `"complete"`. +- `tools/list` results carry `ttlMs` and `cacheScope` (the `CacheableResult` interface). +- `tools/list` **SHOULD** be returned in a deterministic order — which also helps prompt caching. +- Version mismatch returns `UnsupportedProtocolVersionError` (error code `-32022`). +- `ping`, `logging/setLevel` and `notifications/roots/list_changed` were **removed**; there are no + protocol-level sessions to keep alive. +- **Roots, Sampling and Logging are deprecated** in this revision, and new implementations should not + add support. KnowNote's server will not implement them. + +### 2. Transport: local stdio only + +`stdio`: newline-delimited JSON-RPC over the standard streams of a **client-launched subprocess**. + +No network listener. No remote bind. No HTTP transport in v1. The protocol semantics are identical on +every binding, so choosing stdio costs no capability an agent needs, and it means the server is not +reachable by anything that did not start it. Cancellation arrives as `notifications/cancelled` on this +binding. + +### 3. Tool surface: small, read-only + +| Tool | Arguments | Returns | +| ----------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------- | +| `server/discover` | — | supported revisions, capabilities, identity (**required by the spec**) | +| `list_notebooks` | — | notebook id + title (+ source/note counts) | +| `search_notebook` | `notebook_id`, `query`, `top_k` (default 5, **max 50**) | evidence **with provenance**, not bare text | +| `get_source` | `document_id` | source metadata + structure outline (pages / blocks) | +| `read_document` | `document_id`, `page?` | canonical text, page-scoped when `page` is given, otherwise a bounded window | +| `search_notes` | `notebook_id`, `query` | notes | + +Deliberately absent in v1: **every write path**, every tool that spends money or calls a model, and +anything that reports a filesystem path. Read-only is what makes an MCP surface safe to grant to an +agent that is itself acting on untrusted document text. + +Two naming points: + +- A "source" in this document is a `documents` row. `docs/architecture.md` §Source provenance already + establishes that "a document is an identity", so `get_source(document_id)` returns the source + identity and its structure. The tool name follows the app's domain language, the argument is the + app's existing id. +- `search_notebook` is **notebook-scoped today**, because `Retriever.search(notebookId, query, opts)` + takes a notebook and nothing else. Per-document scoping arrives with #94 (v1.5); v1 covers "this + specific source" through `get_source` / `read_document` rather than by inventing a filter on the + retriever here. + +`read_document` is bounded on purpose: a whole document can exceed any sensible context budget, so a +page (or a capped window) is the unit, and the tool says which one it returned. + +### 4. Reuse: one query path, one provenance model + +The server must sit on the app's own retrieval and provenance, not fork them. + +- **Retrieval:** `Retriever` / `RetrievedEvidence` (`src/main/services/retrieval/`), the same seam + Chat and the eval harness use. No second RAG path, no direct vector-store access from the server. +- **Provenance:** `RetrievedEvidence.locator` (`pageStart`, `pageEnd`, `blocks[]`) plus `source` + (`title`, `type`). This is what lets an agent surface `[1] p.5` instead of an opaque chunk. +- **Citations:** the `Citation` type (`src/shared/types/citation.ts`) is deliberately a **snapshot** — + quote, page, block id and character offsets are written when the answer is produced, and survive a + reindex or a deleted source. An agent's citation must have the same property for the same reason. + +**There is no `CitationService`.** #79 and #80 both name one, but `docs/architecture.md` recorded +after #60 that it was deliberately never created: citation parsing, resolution and source-anchor +mapping are stateless and shared between the main and renderer processes, so they are pure functions +in `src/shared/utils/` (`citations.ts`, `citationResolution.ts`, `sourceAnchor.ts`). This ADR uses the +real names. Anyone implementing #80 should read "the app's citation logic" as those modules. + +Reusing them is what keeps a future "click this citation" path possible: an agent's citation carries +`documentId` + page + block, which is exactly a `SourceAnchor`, which is what #72's +`openSourceAnchor()` consumes. + +### 5. Security and consent + +- **Explicit opt-in.** The server runs only when the user turns it on. Off by default, and enabling it + is a visible state: while it is active the UI says so, because a running MCP server means an + external process can read the knowledge base. +- **No filesystem path exposure.** Tools return ids, pages and text — never `localFilePath` or any + path. This is the same rule the reader already follows (`knownote-doc://` serves bytes **by id**, + never by path), and it is the rule that keeps a path-traversal class of bug structurally impossible + here. +- **Read-only in v1.** No tool mutates state, so a prompt-injected document cannot turn an agent into + a writer. An agent can be _wrong_ about what it read; it cannot _change_ the knowledge base. +- **The blast radius is "everything in the notebook".** Search is notebook-scoped, so consenting to + the server means consenting to that. #94 (v1.5) narrows this; until then the ADR is explicit about + it rather than implying per-document isolation. +- **Provenance is also a security property.** Because every retrieved item carries its source, page + and block, a claim an agent makes can be checked against the document rather than trusted. + +### 6. Prior art + +**#38** — acknowledged as the demand signal, and as prior art only. Its fork +(`agx7993/KnowNote-MCP-RC`) is **not merged and not used as a template**, for two reasons: it targets +a protocol revision that has since removed the handshake it implements, and #79's whole point is that +the server should be thin over the seams the app already has rather than a parallel query path bolted +onto it. The issue itself stays closed; this ADR is what it becomes. + +## Consequences + +- #80 is a thin adapter: protocol plumbing plus a mapping from `RetrievedEvidence` to tool output. If + it starts to look like a second retrieval stack, the decision above has been violated. +- The server is **coupled to the protocol revision**, so an implementation note must record the + revision it was built against (as this ADR does) — the next revision will move things again. +- Modern-only means an old client cannot talk to it. That is deliberate and reversible via the + dual-era trigger above. +- Being stdio-only means no remote/multi-device access. If that is ever wanted it is a **new ADR**, + not a flag on this one: a network listener changes the security model completely. + +## Alternatives considered and rejected + +- **Become an agent platform** (host agents, tool registry, workflow engine) — the AnythingLLM / + SurfSense race. Rejected: it makes KnowNote a worse knowledge base and is not the demand in #38. +- **HTTP / Streamable HTTP transport** — rejected for v1: it introduces a network listener, and with + it a remote attack surface, for a capability no requested client needs. +- **Merge or port #38's implementation** — rejected above; its protocol era is obsolete and it forked + the query path. +- **Implement `2025-11-25` (legacy handshake) first** — rejected: it is the previous revision, and + building the deprecated shape first means rewriting the transport immediately after. +- **A writable v1 (create notes / add sources from an agent)** — deferred, not rejected on principle. + It needs its own consent and confirmation design, and it belongs in a follow-up once the read path + is proven. From 9f7920451e92b04eb40293b8b618cb528acfae1c Mon Sep 17 00:00:00 2001 From: mrsibe Date: Fri, 25 Sep 2026 21:32:19 +0800 Subject: [PATCH 2/2] docs(adr): serve both protocol eras, because the SDK makes it the default Corrects the core decision in ADR 0001. The first version concluded that serving 2025-era clients would mean maintaining two server implementations, and therefore chose modern-only. That premise was wrong, and I asserted it without checking the SDK. With the official TypeScript SDK v2 it is the reverse: `serveStdio(factory)` "is the stdio entry point for serving the 2026-07-28 protocol revision ... with 2025-era serving as the default", and its `legacy?: 'reject' | 'serve'` option defaults to `'serve'`. The entry owns the era decision and constructs ONE server instance from a single era-agnostic factory. Dual-era therefore costs one function choice, not a second implementation - and the same file documents the trap this ADR exists to avoid: hand-constructed servers connected straight to a `StdioServerTransport` "keep serving the 2025-era protocol they were written for", so the obvious first thing to write serves the wrong era. The compatibility cost is equally real and the ADR now records it: Codex keeps the legacy lifecycle for local stdio by default and requires `CODEX_MCP_PROTOCOL_VERSION=2026-07-28` to speak the new revision (openai/codex#35724). Modern-only would have broken a client the ADR itself names, for every user who had not set an environment variable. So: target the 2026-07-28 model, serve both eras via the SDK, keep one factory / one Retriever / one provenance model, hand-write no legacy stack, and revisit dropping legacy (`legacy: 'reject'`) with a support matrix in a small follow-up ADR. Also in this commit, from the same review: - `server/discover` is a protocol RPC, not a tool, and the surface table no longer lists it beside `list_notebooks`. The spec requires it of the server; registering it in `tools/list` would be wrong. - The stdio claim was too strong. It no longer leans on "protocol semantics are identical on every binding" (HTTP and stdio cancellation genuinely differ) but on the narrower, checkable statement that the surface defined here needs no HTTP-only capability. - `resultType` is a `string` discriminator, not a closed set of two: the base protocol only requires the field. Recorded as `complete` for this surface, `input_required` for MRTR-capable operations, Tasks out of scope. - A new constraint on #80, from the SDK source: the factory may be constructed twice per connection (optimistic modern probe, then legacy instance on fallback), so it must be cheap and side-effect-free. #80's body is patched in the same pass: it still said "the same `Retriever` and `CitationService`" (a class #60 recorded as deliberately never created) and pointed at a "citation service" in its dependencies. It now names the real modules and carries the dual-era constraints above. --- docs/adr/0001-knowledge-mcp-server.md | 138 +++++++++++++++++++++----- 1 file changed, 112 insertions(+), 26 deletions(-) diff --git a/docs/adr/0001-knowledge-mcp-server.md b/docs/adr/0001-knowledge-mcp-server.md index e037a02..724a9e3 100644 --- a/docs/adr/0001-knowledge-mcp-server.md +++ b/docs/adr/0001-knowledge-mcp-server.md @@ -39,7 +39,7 @@ already changed underneath the prior art. ## Decision -### 1. Target protocol revision: `2026-07-28` +### 1. Target the `2026-07-28` protocol model, and serve both eras **Verified against the live specification on 2026-09-25**, not a summary: @@ -68,24 +68,83 @@ The specification names the two eras explicitly: | **Legacy** | `2025-11-25` and earlier — a session is established with an `initialize` handshake | | **Dual-era** | supports both | -**Decision: implement modern (`2026-07-28`) only.** There is no session to establish, so a server that -follows old tutorials or the #38 fork would implement a handshake the protocol no longer has. The -spec also requires a discovery RPC, which is what makes a modern-only server discoverable rather than -just incompatible: +**Decision: target the 2026-07-28 protocol model, and let the official SDK serve both eras over stdio.** -> Add `server/discover`: servers **MUST** implement this RPC to advertise their supported protocol -> versions, capabilities, and identity. Clients **MAY** call it before any other request for up-front -> version selection, or use it as a backward-compatibility probe on STDIO. +There is no session to establish, so a server that hand-writes an `initialize` handshake from an old +tutorial — or copies #38's fork — implements the previous era by hand. That much stands. -**Revisit trigger:** if a client the user actually wants to use cannot speak `2026-07-28`, add the -legacy handshake as a dual-era shim. Not before — dual-era support is real complexity, and -`server/discover` is the designed answer to "what do you speak?". +What does **not** stand is the conclusion I first drew from it, which was that serving 2025-era +clients would mean maintaining two server implementations. With the official TypeScript SDK v2 it is +the opposite: the SDK's stdio entry point **is** the compatibility boundary, and dual-era is its +default. + +From the SDK source (`packages/server/src/server/serveStdio.ts`): + +> `serveStdio` — the stdio entry point for serving the 2026-07-28 protocol revision on a long-lived +> connection, **with 2025-era serving as the default** for clients that open with the `initialize` +> handshake. +> +> The entry owns the stdio transport and **the era decision** for the connection… constructs **ONE** +> server instance from the consumer's factory for the era the client opened with, pins that instance +> for the lifetime of the connection, and passes every later message straight through to it. + +The same file documents the option and the trap: + +- `legacy?: 'reject' | 'serve'` — `'serve'` is **the default**; `'reject'` answers an `initialize` + with the unsupported-protocol-version error naming the supported modern revisions. +- A `server/discover` probe is answered by an optimistically built modern instance but **does not pin + the connection** — the spec's stdio backward-compatibility flow, where a client may probe first and + then either continue modern or fall back to `initialize`. +- "Hand-constructed servers connected directly to a `StdioServerTransport` are unaffected by this + entry: **they keep serving the 2025-era protocol they were written for.**" So + `Server.connect(new StdioServerTransport())` — the obvious first thing to write — is exactly the + modern-communication gap this ADR exists to avoid. + +And from the SDK's dual-era example, which the SDK documents as the recommended first read for this +migration: + +> One server factory, both protocol eras (2025 `initialize` and 2026-07-28 per-request envelope), both +> transports… the entry (`serveStdio` / `createMcpHandler`) owns the era decision, **the factory is +> era-agnostic**. + +So the decision is: + +| | | +| ------------------------- | --------------------------------------------------------------------------------------------- | +| Architectural target | the `2026-07-28` model (per-request envelope, `server/discover`, no session) | +| Compatibility boundary | the SDK's `serveStdio(factory)` — dual-era, `legacy: 'serve'` (its default) | +| Server code | **one** factory, one set of tool handlers, one `Retriever`, one provenance model | +| Hand-written legacy stack | **none.** We serve the 2025 era by _delegating_ to the SDK, never by implementing a handshake | +| Network transport | none (see §2) | + +This is not a compromise against the product goal; it is what makes the product goal reachable. This +ADR names Codex as a target client, and Codex today is **legacy-by-default for local stdio**: + +> Add an opt-in `mcp_2026_07_28` protocol mode **while preserving the legacy lifecycle by default**… +> **Require stdio servers to opt in with `CODEX_MCP_PROTOCOL_VERSION=2026-07-28`** +> — openai/codex#35724, _Add MCP 2026-07-28 discovery support_ + +A modern-only server would therefore fail to connect to one of the clients this ADR was written for, +until every user sets an environment variable. Dual-era costs one function choice and removes that +entire class of "it does not work on my client" report. + +**Revisit trigger:** when the clients we care about all speak `2026-07-28` comfortably, dropping the +legacy era is a **one-line change** (`legacy: 'reject'`) and should be its own small ADR with the +support matrix recorded. Until then, dual-era is the default and the burden is on dropping it, not on +keeping it. + +One implementation note this pins on #80: the factory **may be called twice** for one connection +(optimistic modern probe instance, then a legacy instance when the probe falls back), so it must be +cheap and side-effect-free to construct. The SDK says so explicitly, and a factory that opens the +database or starts an index on construction would break under that flow. Concrete requirements this pins on #80, all from the same revision: -- `server/discover` **MUST** be implemented; it is not optional. -- Every result carries a required `resultType` (`"complete"`, or `"input_required"` for multi - round-trip); clients must treat a missing field from older servers as `"complete"`. +- `server/discover` **MUST** be implemented. It is a protocol RPC, not a tool — see §3. +- Every result carries a required `resultType`. The base protocol types it as a `string` + discriminator: _"The `result` **MUST** include a `resultType` field to indicate the type of the + result."_ For the surface defined here, results are `"complete"`; MRTR-capable operations may return + `"input_required"`; the Tasks extension is **out of scope** for v1 and would use its own form. - `tools/list` results carry `ttlMs` and `cacheScope` (the `CacheableResult` interface). - `tools/list` **SHOULD** be returned in a deterministic order — which also helps prompt caching. - Version mismatch returns `UnsupportedProtocolVersionError` (error code `-32022`). @@ -98,22 +157,37 @@ Concrete requirements this pins on #80, all from the same revision: `stdio`: newline-delimited JSON-RPC over the standard streams of a **client-launched subprocess**. -No network listener. No remote bind. No HTTP transport in v1. The protocol semantics are identical on -every binding, so choosing stdio costs no capability an agent needs, and it means the server is not -reachable by anything that did not start it. Cancellation arrives as `notifications/cancelled` on this -binding. +No network listener. No remote bind. No HTTP transport in v1. Protocol semantics are the same on +every binding, but _transport_ mechanics are not — HTTP cancellation closes the request's response +stream while stdio uses `notifications/cancelled` — so the justification is not "bindings are +interchangeable" but the narrower and checkable claim: **the tool surface defined in §3 requires no +HTTP-only capability.** It is request/response reads, so stdio carries all of it, and the server is +not reachable by anything that did not start it. -### 3. Tool surface: small, read-only +### 3. Tool surface: one required protocol RPC, then read-only application tools + +`server/discover` is a **protocol RPC**, not a tool. The specification requires it of the server ("servers +**MUST** implement this RPC to advertise their supported protocol versions, capabilities, and +identity") and it is what a modern client calls first. It must **not** appear in `tools/list`, and #80 +must not register it as a tool named `server/discover`. + +**Protocol-required RPC — the SDK entry owns this:** `server/discover`. The spec's stdio +backward-compatibility flow (probe, then continue modern or fall back to `initialize`) is implemented +by `serveStdio`; our factory supplies a server instance and does not answer discovery itself. + +**KnowNote application tools:** | Tool | Arguments | Returns | | ----------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------- | -| `server/discover` | — | supported revisions, capabilities, identity (**required by the spec**) | | `list_notebooks` | — | notebook id + title (+ source/note counts) | | `search_notebook` | `notebook_id`, `query`, `top_k` (default 5, **max 50**) | evidence **with provenance**, not bare text | | `get_source` | `document_id` | source metadata + structure outline (pages / blocks) | | `read_document` | `document_id`, `page?` | canonical text, page-scoped when `page` is given, otherwise a bounded window | | `search_notes` | `notebook_id`, `query` | notes | +These are the tools the factory registers, and they are the same for both eras — the SDK owns the era +decision, so there is no modern list and legacy list to keep in step. + Deliberately absent in v1: **every write path**, every tool that spends money or calls a model, and anything that reports a filesystem path. Read-only is what makes an MCP surface safe to grant to an agent that is itself acting on untrusted document text. @@ -181,12 +255,18 @@ onto it. The issue itself stays closed; this ADR is what it becomes. ## Consequences -- #80 is a thin adapter: protocol plumbing plus a mapping from `RetrievedEvidence` to tool output. If - it starts to look like a second retrieval stack, the decision above has been violated. +- #80 is a thin adapter: **one era-agnostic factory**, protocol plumbing supplied by `serveStdio`, + and a mapping from `RetrievedEvidence` to tool output. If it starts to look like a second retrieval + stack — or like two sets of handlers, one per era — the decision above has been violated. - The server is **coupled to the protocol revision**, so an implementation note must record the revision it was built against (as this ADR does) — the next revision will move things again. -- Modern-only means an old client cannot talk to it. That is deliberate and reversible via the - dual-era trigger above. +- Serving both eras is the **default** because dropping legacy is the change that needs justifying, + not the other way round. When the support matrix allows it, that is `legacy: 'reject'` and a small + follow-up ADR. +- The SDK's entry point may construct the factory's server **twice** for one connection (an + optimistic modern probe instance, then a legacy instance when the probe falls back), so the factory + must be cheap and side-effect-free. #80 must not open the database, start an index, or attach + process-level state in the factory body. - Being stdio-only means no remote/multi-device access. If that is ever wanted it is a **new ADR**, not a flag on this one: a network listener changes the security model completely. @@ -198,8 +278,14 @@ onto it. The issue itself stays closed; this ADR is what it becomes. it a remote attack surface, for a capability no requested client needs. - **Merge or port #38's implementation** — rejected above; its protocol era is obsolete and it forked the query path. -- **Implement `2025-11-25` (legacy handshake) first** — rejected: it is the previous revision, and - building the deprecated shape first means rewriting the transport immediately after. +- **Hand-implement the legacy `initialize`/session path** — rejected: `serveStdio` already serves + 2025-era clients from the same era-agnostic factory, so writing that layer by hand would duplicate + the protocol for no capability. Delegating to the SDK is the whole point of the decision above. +- **Modern-only, `legacy: 'reject'`** — rejected for v1, and this was the first draft of this ADR. It + looks like the smaller surface, but it trades one function argument for real client breakage: Codex + keeps the legacy lifecycle for local stdio by default and needs + `CODEX_MCP_PROTOCOL_VERSION=2026-07-28` to speak 2026-07-28 (openai/codex#35724). A modern-only + server would fail to connect to a client this ADR names, and dual-era costs less than that. - **A writable v1 (create notes / add sources from an agent)** — deferred, not rejected on principle. It needs its own consent and confirmation design, and it belongs in a follow-up once the read path is proven.