Commit Graph

2 Commits

Author SHA1 Message Date
db7f1fac7d Fase 9 (2/N): daemon-hosted MCP server, retire electron/mcp_server.cljs
Completes Fase 9's "consolidar MCP" half: the daemon now serves its own
MCP endpoint at /mcp, exposing every semantic-routes operation as an MCP
tool against the daemon's authoritative graph, and the old Electron-local
MCP server is retired since it's now redundant.

New: src/main/frontend/worker/mcp_server.cljs
The CF-Worker's own MCP endpoint (deps/db-sync/worker/entry.mjs) generates
its tools from the OpenAPI document via @cloudflare/codemode's
openApiMcpServer, a package that only exists inside Cloudflare Workers,
plus a pile of ChatGPT-App-specific tooling (asset image resources,
upload_asset, Cognito protected-resource metadata) this daemon has no use
for -- none of that ports to Node. So this registers one tool per
operation by hand instead, using @modelcontextprotocol/sdk directly, the
same pattern electron/mcp_server.cljs already used for its own 6 tools
(registerTool with an explicit title/description/inputSchema/handler) --
just generated from semantic-routes' operation table and operation-docs
(now public) instead of writing 35 one-off registrations by hand, so the
MCP tool surface can't drift from the HTTP routes or the OpenAPI document.
Each tool's Zod input shape is assembled from path params (auto-derived
from the route's :param segments) plus a per-operation-id map of the
extra query/body fields; a handful of shared fragments (pagination,
time filters, the recursive block-tree shape, property-value/task-status
selectors) cover the repetition across list/create/update operations.

Each tool's handler makes a loopback HTTP call to this same daemon's own
/v1/semantic/* route, forwarding the /mcp request's own Authorization
header -- reusing the already-tested HTTP-layer logic (repo-error
validation, lock-owner assertion, response shaping) instead of a second
request path directly against the thread-api. Session/transport plumbing
(handle-post/get/delete-request, the transports-by-session-id map) is
adapted from electron/mcp_server.cljs's Fastify-based version: that one
gets req/res already unwrapped-to-Node-primitives via `.raw` with the
body pre-parsed by Fastify; this one is called straight from the daemon's
own raw http.createServer callback, so req/res already are what
StreamableHTTPServerTransport wants, and the body needs parsing here
instead. No separate auth check either -- the daemon's existing
bearer-token check on every route (request-authorized? in
db_worker_node.cljs) already gates /mcp before this is reached.

Wires POST/GET/DELETE /mcp into db_worker_node.cljs's make-server cond
chain, alongside the existing /v1/semantic/* branch.

Dependencies: adds @modelcontextprotocol/sdk and zod (zod/v3 -- the SDK
doesn't work with zod 4's default export, same constraint
electron/mcp_server.cljs already worked around) to the root package.json;
previously only resources/package.json (Electron's own bundle) had them.

Retired: electron/mcp_server.cljs and its wiring (electron/server.cljs's
initialize-mcp-routes, its 3 POST/GET/DELETE /mcp route registrations,
the :mcp-enabled? config field; components/server.cljs's "copy MCP URL"
popup link; components/settings.cljs's entire AI settings tab, which had
nothing in it besides the MCP-server toggle). Electron already talks to
the daemon (Fase 3) rather than driving itself locally, so a second,
separately-configured MCP surface bolted onto Electron's own local
automation API server (which otherwise stays -- /api is a distinct,
still-used concern) was redundant with the daemon's new one. Removed the
4 associated i18n keys (server/mcp-url-copied, settings/ai,
settings.ai/enable-mcp-server[-desc]) from all ~26 locale dicts.

New: src/test/frontend/worker/mcp_server_test.cljs
Connects a real @modelcontextprotocol/sdk client over HTTP to a live
daemon's /mcp endpoint: lists all registered tools, round-trips
createPage/getPage and a recursive captureToToday block tree, and checks
a tool call against a nonexistent page surfaces as MCP error content
rather than throwing. 4 tests, 11 assertions.

Verification: all 5 shadow-cljs targets compile with 0 warnings. Full
`bb dev:test` run: the only failures are the same 7 pre-existing,
sandboxed-network-related persist_db_test.cljs failures already
documented in the Fase 8 and Fase 9 (1/N) commits -- identical test names,
identical ECONNREFUSED/Invalid URL errors. Every other shard, including
all three daemon-HTTP worker test files sharing one test.js process
(db_worker_node_test.cljs, semantic_test.cljs, mcp_server_test.cljs),
passes clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 17:07:27 +02:00
196de21731 Fase 9 (1/N): re-host the semantic API onto the daemon
Ports the 38-operation semantic API from deps/db-sync's Cloudflare Worker
handler (handler/semantic.cljs + routes/semantic.cljs) onto the daemon's
own authoritative conn, per the plan's Fase 9. deps/db-sync itself is
untouched -- this only adds the daemon-side equivalent.

New:
- worker/semantic_routes.cljs: route table + OpenAPI 3.1.0 document
  generator, adapted from the CF-Worker's routes/semantic.cljs. Drops the
  :graph-id URL segment and listGraphs operation entirely -- the daemon is
  bound to a single repo per process (see db_worker_node.cljs's
  bound-repo), unlike the CF-Worker's per-graph Durable Objects, so every
  request implicitly targets the daemon's own bound repo. Security scheme
  is a bearer token (the daemon's actual session auth) instead of OAuth2.
- worker/handler/semantic.cljs: the 38 operations themselves, ported from
  the CF-Worker's handler/semantic.cljs. Adaptations from the original:
  * ensure-conn! (lazy SQLite conn open) -> worker.state/get-datascript-conn
    (the daemon already holds the authoritative conn for its bound repo).
  * broadcast-change! calls dropped entirely -- any transact! against the
    conn is already picked up by the listener worker.db-listener installs,
    which broadcasts to every connected client, so nothing here needs to
    push manually.
  * HTTP body reads (p/let + body-clj) dropped -- bodies arrive already
    JSON-parsed from the HTTP layer, and every operation is synchronous
    (DataScript itself is synchronous), so none of this needs promises.
  * http/bad-request, http/not-found, js/Response error helpers -> ex-info
    with :status/:code, matching every other thread-api in this codebase.
  * R2 asset storage -> platform/asset-write-bytes! (local filesystem),
    using the <uuid>.<type> naming convention components/block/asset.cljs
    already establishes. R2 signed URLs -> server-relative
    /v1/assets/:repo/:name paths (already served, with ETag caching, by
    the existing Fase 5 asset routes) since bearer-token auth already
    gates those with the same ?token= fallback semantic GET routes need.
  Registered as a single :thread-api/semantic-request thread-api
  dispatched by handler keyword, mirroring how the CF-Worker original
  dispatched via one big `case` in `handle`.
- db_worker_node.cljs: wires a /v1/semantic/* branch (JSON body parsed
  for most operations; raw bytes read directly for assets-create,
  mirroring the existing /v1/assets/:repo/:name PUT route) plus
  /v1/semantic/openapi.json. Unlike /v1/invoke -- an internal RPC
  transport whose transit-encoded results Logseq's own clients already
  know how to decode -- /v1/semantic/* is the public HTTP API documented
  by the OpenAPI doc and meant for arbitrary HTTP/MCP clients, so results
  are decoded server-side into plain JSON, the same way the existing
  /v1/assets/:repo/:name GET route already does for the other route with
  external HTTP consumers.
- src/test/frontend/worker/semantic_test.cljs: exercises pages,
  blocks/properties, capture, tags/tasks, search, the OpenAPI document,
  401-without-auth, and 404-on-unknown-route against a live daemon over
  real HTTP. 7 tests, 31 assertions.

Verification: all 5 shadow-cljs targets compile with 0 warnings. Full
`bb dev:test` run has zero regressions -- the only failures are the same
7 pre-existing, sandboxed-network-related persist_db_test.cljs failures
(ECONNREFUSED / Invalid URL) already documented as environmental in the
Fase 8 commit.

Found and fixed in-session: semantic_test.cljs originally set its own
LOGSEQ_DAEMON_USERNAME/PASSWORD env vars at namespace load time, colliding
with db_worker_node_test.cljs doing the same (both get bundled into the
same test.js process) -- whichever namespace loaded last won the race and
locked the other file's daemons out of their own bootstrapped credential,
cascading into ~150 unrelated failures. Fixed by reusing the same literal
credential constants instead of introducing a second one.

Known follow-up, not yet done: the plan's "consolidar MCP" half of Fase 9
-- an MCP JSON-RPC endpoint on the daemon (the CF-Worker's equivalent,
deps/db-sync/worker/entry.mjs, leans on @cloudflare/codemode's
openApiMcpServer plus ChatGPT-app-specific tooling that doesn't port to
Node) and the decision on electron/mcp_server.cljs's fate are still open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 16:34:14 +02:00