Skip to content

docs(adr): decide the local MCP server, pinned to spec revision 2026-07-28 - #128

Merged
mrsibe merged 2 commits into
mainfrom
docs/mcp-adr
Sep 25, 2026
Merged

mrsibe merged 2 commits into
mainfrom
docs/mcp-adr

Conversation

@mrsibe

@mrsibe mrsibe commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Closes #79.

Writes docs/adr/0001-knowledge-mcp-server.md: 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 and not used as a template.

The verified part

Verified against the live specification on 2026-09-25: 2026-07-28 is the newest non-prerelease revision (predecessor 2025-11-25; nothing newer exists). That revision removed the initialize handshake — "There is no negotiation handshake. Every request carries its protocol version, and the server accepts or rejects each request independently" — and moved version/capabilities/identity into per-request _meta. The spec's own terminology calls 2026-07-28+ modern and 2025-11-25- legacy.

So the review comment in #38 was right, and a hand-written initialize/session server built from an old tutorial is building the previous era by hand.

Corrected in review: serve both eras, because the SDK makes it the default

My first version concluded from the above that serving 2025-era clients meant maintaining two implementations, and chose modern-only. That premise was wrong — I asserted it without checking the SDK — and it would have broken a client this ADR names.

From the official TypeScript SDK v2 (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…

  • legacy?: 'reject' | 'serve' — 'serve' is the default.
  • 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 (Server.connect(new StdioServerTransport())) serves the wrong era.
  • The SDK's dual-era example: "the entry (serveStdio / createMcpHandler) owns the era decision, the factory is era-agnostic."

The compatibility cost is equally real. Codex keeps the legacy lifecycle for local stdio by default and needs CODEX_MCP_PROTOCOL_VERSION=2026-07-28 to speak the new revision (openai/codex#35724, Add MCP 2026-07-28 discovery support). Modern-only would have failed against a client the ADR itself names.

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 tool handler set, one Retriever, one provenance model
Hand-written legacy stack none — we serve the 2025 era by delegating to the SDK
Dropping legacy later legacy: 'reject', its own small ADR with a support matrix

Other review corrections

  • server/discover is a protocol RPC, not a tool. The spec requires "servers MUST implement this RPC"; it is what a modern client calls first. The surface table now separates the protocol-required RPC from the KnowNote application tools, so [Feat] Implement the Knowledge MCP server #80 does not register a tool named server/discover.
  • The stdio justification 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 claim that the surface defined here needs no HTTP-only capability.
  • resultType is not a closed set of two. The base protocol only requires the field, typed string. Recorded as complete for this surface, input_required for MRTR-capable operations, and Tasks out of scope for v1.
  • A new constraint on [Feat] Implement the Knowledge MCP server #80, straight from the SDK source: the factory may be constructed twice per connection (optimistic modern probe, then a legacy instance on fallback), so it must be cheap and side-effect-free — no database open, no index start, no process-level state.

One thing I did not change

The review asked me to fix "#80 (v1.5)" to v1.6 in the ADR header. #80 is in v1.5, so I left it. Evidence: gh api repos/mrsibe/KnowNote/issues/80 reports milestone: v1.5; the v1.5 milestone contains [77, 78, 80, 94, 95, 96, 97, 98, 100] while v1.6 contains only [99]; and #80's own body records "Moved up from v1.6 to v1.5 at the maintainer's request". Changing it would have introduced drift rather than removed it.

#80 patched in the same pass

#80 still said "backed by the same Retriever and CitationService" and listed "the citation service (#69, #70)" as a dependency. There is no CitationService — #60 recorded that it was deliberately never created, because citation parsing, resolution and source-anchor mapping are stateless and shared by main and renderer. #80 now names the real modules (src/shared/utils/), carries the dual-era constraints, and no longer implies a class that does not exist.

Acceptance

Criterion Status
ADR committed under docs/adr/ Done
Final tool list, transport decision, security model and verified spec revision recorded Done, all four — with the dual-era correction
#38 linked and marked as prior art Done, with the two reasons it is not used as a template
#80 (implementation) referenced Done, and its body corrected

Verification

Documentation only — no code, no behaviour change. Prettier clean.

The spec facts are quoted from fetched specification pages (/specification/2026-07-28/changelog, /basic/versioning, /basic/transports, /basic/index); the revision is confirmed against the specification repository's release list; the SDK behaviour is quoted from packages/server/src/server/serveStdio.ts and the dual-era example; the Codex behaviour is from openai/codex#35724. Nothing here rests on a chat summary.

…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.
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 25, 2026
…ault

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.
@mrsibe
mrsibe merged commit c444464 into main Sep 25, 2026
4 checks passed
@mrsibe
mrsibe deleted the docs/mcp-adr branch September 25, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ADR] KnowNote as a local MCP server: capability surface, transport, security

1 participant