Skip to content

feat(mcp): serve the library over read-only stdio MCP - #171

Merged
mrsibe merged 1 commit into
mainfrom
feat/80-knowledge-mcp
Sep 28, 2026
Merged

mrsibe merged 1 commit into
mainfrom
feat/80-knowledge-mcp

Conversation

@mrsibe

@mrsibe mrsibe commented Sep 28, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

Implements the Knowledge MCP server (#80) exactly as ADR 0001 specifies: KnowNote as a read-only MCP server over local stdio, using the TypeScript SDK v2 serveStdio(factory) entry.

Why?

#38 is a real user request — someone reached KnowNote from an external agent and published a fork. The right response is not to become an agent platform; it is to make MCP KnowNote's export surface over the retrieval path the app already has.

Related issue

Fixes #80
Related to #79 (ADR), #38 (prior art), #154

SDK note (the ADR is unchanged)

The implementation targets @modelcontextprotocol/server@2.1.0 — the v2 split package, not the v1 @modelcontextprotocol/sdk. serveStdio is imported from @modelcontextprotocol/server/stdio.

import { serveStdio } from '@modelcontextprotocol/server/stdio'

Dual-era is left at its default (legacy: 'serve'), so one factory serves both a 2025-era initialize and a 2026-07-28 per-request envelope. There is no hand-written handshake and no Server.connect(new StdioServerTransport()). No ADR amendment is needed.

Dependencies: @modelcontextprotocol/server (runtime) and @modelcontextprotocol/client (dev, for protocol tests).

What changed

  • src/main/mcp/runtime.ts — McpRuntime: the query seam the tools talk to, never KnowledgeService/Electron/the DB directly. Read-only by construction.
  • src/main/mcp/server.ts — buildMcpServer(runtime), registering exactly the five application tools. server/discover is not registered as a tool (it is a protocol RPC the SDK entry owns).
  • src/main/mcp/entry.ts — runMcpServer(): redirects every console method to stderr before the DB logs on init (stdio is the protocol), builds the runtime once, then serveStdio(() => buildMcpServer(runtime)).
  • src/main/index.ts — a --mcp headless entry alongside --smoke-test / --eval-harness.
  • src/main/db/index.ts — KNOWNOTE_DATA_DIR lets the server use a profile other than Electron's app.getPath('userData'), and creates it if missing.
  • README / README_CN — how to point a client at it, and a credit for @agx7993, who asked for this (我用kimi k3给这个应用添加了外部应用访问它的mcp功能,作者考不考虑把它加到正式版里 #38) and built the first prototype.

Tool surface

Tool Arguments Returns
list_notebooks — notebook id + title + source/note counts
search_notebook notebook_id, query, top_k (≤50, default 5) passages with provenance (document id, page, char offsets)
get_source document_id metadata + block outline
read_document document_id, page? canonical text, page-scoped or a bounded window
search_notes notebook_id, query notes

No write path, no model call, and no result reports a filesystem path.

How was this tested?

  • npm run typecheck — passes.
  • npm test — 404 pass, including 6 new protocol tests that drive the real server over the SDK's in-memory transport: the tool list is exactly the five read-only tools (server/discover absent, every tool readOnlyHint), provenance is returned, page passes through, a missing source returns null rather than an error, and top_k > 50 is rejected at the schema boundary.
  • npm run check:design — no violations.
  • npm run build — passes.
  • Eval baseline unchanged (docs/eval/baseline-v1.5.json).
  • electron . --smoke-test — PASS.
  • End-to-end over real stdio: ran electron . --mcp and drove it with the v2 client (StdioClientTransport):
    • stdout contained only JSON-RPC frames — initialize answered with serverInfo: knownote, no log lines leaked into the stream;
    • tools/list returned exactly ["list_notebooks","search_notebook","get_source","read_document","search_notes"];
    • list_notebooks → [], get_source for a missing id → null.

Not verified

  • No real MCP client (Claude Code / Codex / Cursor) was used end to end — the protocol was driven with the official v2 client over stdio. The dual-era path was exercised on the legacy (initialize) era; the modern 2026-07-28 path is served by the SDK entry but was not driven by a modern client here.

Checklist

  • I have reviewed my own changes.
  • npm run typecheck passes.
  • npm run build passes.
  • I have tested the affected user workflow.
  • I have not included unrelated changes.
  • I have updated documentation when necessary.

Desktop / build changes

  • Adds runtime dependencies (@modelcontextprotocol/server); electron-vite externalizes dependencies automatically and electron-builder packs them.

@mrsibe mrsibe added enhancement New feature or request area:foundation Module boundaries, architecture, tech debt labels Sep 28, 2026
Implements ADR 0001 / #80 with the v2 SDK: serveStdio from
@modelcontextprotocol/server/stdio, dual-era by default (legacy stays 'serve'), so a
2025-era initialize and a 2026-07-28 per-request envelope are served by one factory.
No hand-written handshake, and no Server.connect(new StdioServerTransport()).

- Five read-only tools: list_notebooks, search_notebook, get_source, read_document,
  search_notes. search_notebook returns evidence WITH provenance (document id, page,
  character offsets), not bare text. No write tool, no model call, and no result
  carries a filesystem path — that is what makes the surface safe to grant to an
  agent reading untrusted documents. server/discover is not registered as a tool.
- stdio IS the protocol, so the entry redirects every console method to stderr
  before the database logs on init. Verified end to end: the only bytes on stdout
  are JSON-RPC frames.
- The factory closes over an already-built runtime and opens nothing. serveStdio may
  call it twice for one connection (optimistic modern probe, then legacy fallback),
  so anything expensive inside it would run twice.
- McpRuntime is the seam: the tools talk to it, never to KnowledgeService, Electron
  or the database, so the protocol surface is testable with a stub.
- KNOWNOTE_DATA_DIR lets the server use a profile other than Electron's default, and
  the directory is created if it does not exist.

Verified: npm run typecheck; npm test (404 pass, 6 new protocol tests driven over the
SDK's in-memory transport); npm run check:design; npm run build; eval baseline
unchanged; electron . --smoke-test PASS; and an end-to-end run of `electron . --mcp`
driven by the v2 client over real stdio (tools/list + tools/call).

Co-authored-by: agx7993 <105156832+agx7993@users.noreply.github.com>
@mrsibe
mrsibe force-pushed the feat/80-knowledge-mcp branch from 5766a10 to b12c994 Compare September 28, 2026 10:53
@mrsibe
mrsibe merged commit ab418f4 into main Sep 28, 2026
3 checks passed
@mrsibe
mrsibe deleted the feat/80-knowledge-mcp branch September 28, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:foundation Module boundaries, architecture, tech debt enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] Implement the Knowledge MCP server

1 participant