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>
This commit is contained in:
2026-08-26 17:07:27 +02:00
parent 196de21731
commit db7f1fac7d
34 changed files with 927 additions and 404 deletions

View File

@@ -151,6 +151,7 @@
"@js-joda/timezone": "2.5.0",
"@logseq/react-tweet-embed": "1.3.1-1",
"@logseq/simple-wave-record": "^0.0.3",
"@modelcontextprotocol/sdk": "^1.27.1",
"@sentry/react": "^10.51.0",
"@sqlite.org/sqlite-wasm": "3.51.2-build9",
"@tabler/icons-react": "3.44.0",
@@ -198,7 +199,8 @@
"uniqolor": "1.1.1",
"url": "^0.11.4",
"util": "^0.12.5",
"yup": "^1.7.1"
"yup": "^1.7.1",
"zod": "^4.3.6"
},
"optionalDependencies": {
"@zvec/bindings-darwin-arm64": "0.5.0",

505
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,231 +0,0 @@
(ns electron.mcp-server
"MCP server routes for the desktop API server."
(:require ["@modelcontextprotocol/sdk/server/mcp.js" :refer [McpServer]]
["@modelcontextprotocol/sdk/server/streamableHttp.js" :refer [StreamableHTTPServerTransport]]
["@modelcontextprotocol/sdk/types.js" :refer [isInitializeRequest]]
["zod/v3" :as z] ;; zod 4 doesn't work w/ mcp - https://github.com/modelcontextprotocol/typescript-sdk/issues/925
[promesa.core :as p]))
;; Server util fns
;; ===============
;; "Stores transports by session ID"
(defonce ^:private transports
(atom {}))
(declare create-mcp-api-server)
;; See https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http
;; for how to respond to different MCP requests
(defn handle-post-request [api-fn {:keys [port host]} req res]
(let [session-id (aget (.-headers req) "mcp-session-id")]
(js/console.log "POST /mcp request" session-id (pr-str (.-body req)))
(cond
(and session-id (@transports session-id))
(let [^js transport (@transports session-id)]
(.handleRequest transport (.-raw req) (.-raw res) (.-body req)))
(and (not session-id)
(isInitializeRequest (.-body req)))
(let [transport (StreamableHTTPServerTransport.
#js {:sessionIdGenerator (comp str random-uuid)
:enableDnsRebindingProtection true
:allowedHosts #js [(str host ":" port)]})
mcp-server (create-mcp-api-server api-fn)]
(set! (.-onclose transport)
(fn []
(js/console.log "Transport closed" (.-sessionId transport))
(swap! transports dissoc (.-sessionId transport))))
(.connect mcp-server transport)
(.handleRequest transport (.-raw req) (.-raw res) (.-body req))
(js/console.log "Initialize sessionId" (.-sessionId transport))
(if (.-sessionId transport)
(swap! transports assoc (.-sessionId transport) transport)
(js/console.error "No sessionId to initialize!"))
res)
:else
(do
(.code res 400)
(.send res #js {:jsonrpc "2.0"
:error #js {:code -32000
:message "Bad Request: No valid session ID provided"}
:id nil})))))
(defn handle-get-request
[req res]
(let [session-id (aget (.-headers req) "mcp-session-id")]
(js/console.log "GET /mcp" session-id)
(if-let [transport (and session-id (@transports session-id))]
(.handleRequest ^js transport (.-raw req) (.-raw res))
(-> res (.code 400) (.send "Invalid or missing session ID")))))
(defn handle-delete-request
[req res]
(let [session-id (aget (.-headers req) "mcp-session-id")]
(js/console.log "DELETE /mcp" session-id)
(if-let [transport (and session-id (@transports session-id))]
(do
(.close transport)
(-> res (.code 200) (.send #js {:ok true})))
(-> res (.code 400) (.send "Invalid or missing session ID")))))
(defn mcp-error-response [msg]
#js {:content
#js [#js {:type "text"
:text msg}]})
(defn mcp-success-response [data]
(clj->js {:content
[{:type "text"
:text (js/JSON.stringify (clj->js data))}]}))
;; API tool fns
;; ============
(defn- unexpected-api-error [error]
#js {:content
#js [#js {:type "text"
:text (str "Unexpected API error: " (.-message error))}]})
(defn- api-tool
"Calls API method w/ args and returns a MCP response"
[api-fn api-method method-args]
(-> (p/let [body (api-fn api-method method-args)]
(if-let [error (and body (aget body "error"))]
(mcp-error-response (str "API Error: " error))
(mcp-success-response body)))
(p/catch unexpected-api-error)))
(defn- api-get-page
[call-api-fn args]
(call-api-fn "logseq.cli.getPageData" [(aget args "pageName")]))
(defn- api-list-pages
[call-api-fn args]
(call-api-fn "logseq.cli.listPages" [#js {:expand (aget args "expand")}]))
(defn- api-list-tags
[call-api-fn args]
(call-api-fn "logseq.cli.listTags" [#js {:expand (aget args "expand")}]))
(defn- api-list-properties
[call-api-fn args]
(call-api-fn "logseq.cli.listProperties" [#js {:expand (aget args "expand")}]))
(defn- api-search-blocks
[call-api-fn args]
(call-api-fn "logseq.app.search" [(aget args "searchTerm") #js {:enable-snippet? false}]))
(defn- api-upsert-nodes
[call-api-fn args]
(call-api-fn "logseq.cli.upsertNodes" [(aget args "operations") #js {:dry-run (aget args "dry-run")}]))
(def ^:large-vars/data-var api-tools
"MCP Tools when calling API server"
{:listPages
{:fn api-list-pages
:config #js {:title "List Pages"
:description "List all pages in a graph"
:inputSchema
#js {:expand (-> (z/boolean) .optional (.describe "Provide additional detail on each page"))}}}
:getPage
{:fn api-get-page
:config #js {:title "Get Page"
:description "Get a page's content including its blocks. A property and a tag are pages."
:inputSchema #js {:pageName (-> (z/string) (.describe "The page's name or uuid"))}}}
:upsertNodes
{:fn api-upsert-nodes
:config
#js {:title "Upsert Nodes"
:description
"This tool must be called at most once per user request. Never re-call it unless explicitly asked.
It takes an object with field :operations, which is an array of operation objects.
Each operation creates or edits a page, block, tag or property. Each operation is a object
that must have :operation, :entityType and :data fields. More about fields in an operation object:
* :operation - Either :add or :edit
* :entityType - What type of node, e.g. :block, :page, :tag or :property
* :id - For :edit, this _must_ be a string uuid. For :add, use a temporary unique string if the new page is referenced by later operations e.g. add blocks
* :data - A map of fields to set or update. This map can have the following keys:
* :title - A page/tag/property's name or a block's content
* :page-id - A page string uuid of a block. Required when adding a block.
* :tags - A list of tags as string uuids
* :property-type - A property's type
* :property-cardinality - A property's cardinality. Must be :one or :many
* :property-classes - A property's list of allowed tags, each being a uuid string or a tag's name
* :class-extends - List of parent tags, each being a uuid string or a tag's name
* :class-properties - A tag's list of properties, each eing a uuid string or a property's name
Example inputs with their prompt, description and data as clojure EDN:
Description: This input adds a new block to page with id '119268a6-704f-4e9e-8c34-36dfc6133729' and update the title of a page with uuid '119268a6-704f-4e9e-8c34-36dfc6133729':
{:operations
[{:operation :add
:entityType :block
:id nil
:data {:page-id \"119268a6-704f-4e9e-8c34-36dfc6133729\"
:title \"New block text\"}}
{:operation :edit
:entity :page
:id \"119268a6-704f-4e9e-8c34-36dfc6133729\"
:data {:title \"Revised page title\"}}]}
Prompt: Add task 't1' to new page 'Inbox'
Description: This input creates a page 'Inbox' and adds a 't1' block with tag \"00000002-1282-1814-5700-000000000000\" (task) to it:
{:operations
[{:operation :add
:entityType :page
:id \"temp-Inbox\"
:data {:title \"Inbox\"}}
{:operation :add
:entityType :block
:data {:page-id \"temp-Inbox\"
:title \"t1\"
:tags [\"00000002-1282-1814-5700-000000000000\"]}}]}
Additional advice for building operations:
* Before creating any page, tag or property, check that it exists with getPage"
:inputSchema
#js {:operations
(z/array
(z/object
#js {:operation (z/enum #js ["add" "edit"])
:entityType (z/enum #js ["block" "page" "tag" "property"])
:id (.optional (z/union #js [(z/string) (z/number) (z/null)]))
:data (-> (z/object #js {}) (.passthrough))}))
:dry-run (-> (z/boolean) .optional (.describe "Pretend to do batch update. Does everything except actually commit change to db e.g. validation."))}}}
:searchBlocks
{:fn api-search-blocks
:config #js {:title "Search Blocks"
:description "Search graph for blocks containing search term"
:inputSchema #js {:searchTerm (z/string)}}}
:listTags
{:fn api-list-tags
:config #js {:title "List Tags"
:description "List all tags in a graph"
:inputSchema
#js {:expand (-> (z/boolean) .optional (.describe "Provide additional detail on each tag e.g. their parents (extends) and tag properties"))}}}
:listProperties
{:fn api-list-properties
:config #js {:title "List Properties"
:description "List all properties in a graph"
:inputSchema
#js {:expand (-> (z/boolean) .optional (.describe "Provide additional detail on each property e.g. property type, cardinality"))}}}})
(defn call-api-tool [tool-fn api-fn args]
(tool-fn (partial api-tool api-fn) args))
;; Server fns
;; ==========
(defn create-mcp-server []
(McpServer. #js {:name "Logseq MCP Server"
:version "0.1.0"}))
(defn create-mcp-api-server [api-fn]
(let [mcp-server (create-mcp-server)]
(doseq [[k v] api-tools]
(.registerTool mcp-server
(name k)
(:config v)
(partial call-api-tool (:fn v) api-fn)))
mcp-server))

View File

@@ -9,7 +9,6 @@
[clojure.string :as string]
[electron.configs :as cfgs]
[electron.logger :as logger]
[electron.mcp-server :as desktop-mcp-server]
[electron.utils :as utils]
[electron.window :as window]
[promesa.core :as p]))
@@ -36,8 +35,7 @@
:host (get-host)
:port (get-port)
:tokens (normalize-tokens (cfgs/get-item :server/tokens))
:autostart (cfgs/get-item :server/autostart)
:mcp-enabled? (cfgs/get-item :server/mcp-enabled?)}))
:autostart (cfgs/get-item :server/autostart)}))
(defn- set-status!
([status] (set-status! status nil))
@@ -141,18 +139,6 @@
(p/catch (fn [^js e]
(set-status! :running e))))))
(defn- initialize-mcp-routes [^js server]
(let [api-fn (fn api-fn [meth args]
(if-let [meth' (resolve-real-api-method meth)]
(invoke-logseq-api! meth' args)
#js {:error (str "No method found for " (pr-str meth))}))]
(logger/debug "[server] MCP routes initialized")
(.post server "/mcp"
#(desktop-mcp-server/handle-post-request api-fn {:port (get-port)
:host (get-host)} %1 %2))
(.get server "/mcp" desktop-mcp-server/handle-get-request)
(.delete server "/mcp" desktop-mcp-server/handle-delete-request)))
(defn start!
[]
(-> (p/let [_ (close!)
@@ -175,8 +161,6 @@
(string/replace-first "${PORT}" PORT))]
(doto rep (.type "text/html")
(.send html))))))
_ (when (:mcp-enabled? @*state)
(initialize-mcp-routes s))
;; listen port
_ (.listen s (bean/->js (select-keys @*state [:host :port])))]
(reset! *server s)

View File

@@ -144,7 +144,7 @@
#(js/clearTimeout t))))
[])
(let [{:keys [status error mcp-enabled?]} server-state
(let [{:keys [status error]} server-state
status (keyword (util/safe-lower-case status))
running? (= :running status)
href (and running? (str "http://" (:host server-state) ":" (:port server-state)))]
@@ -192,14 +192,7 @@
(server-status-label status)
[:span.flex.flex-col.gap-1.text-xs.font-mono
[:a.hover:underline.flex.items-center {:href href}
href (shui/tabler-icon "external-link" {:size 12 :class "inline-block ml-1 pt-[1px]"})]
(when mcp-enabled?
[:a.hover:underline.flex.items-center
{:on-click (fn []
(util/copy-to-clipboard! (str href "/mcp"))
(notification/show! (t :server/mcp-url-copied) :success))}
(str href "/mcp")
(shui/tabler-icon "copy" {:size 12 :class "inline-block ml-1 mt-[1px]"})])])]]
href (shui/tabler-icon "external-link" {:size 12 :class "inline-block ml-1 pt-[1px]"})]])]]
(for [{:keys [hr? title options icon]} items]
(cond
hr?

View File

@@ -1222,42 +1222,6 @@
[:<>])
(hsx/defc mcp-server-row
[t]
(let [server-state (rfx/use-sub [:electron/server])
[checked set-checked!] (hooks/use-state false)]
(hooks/use-effect!
(fn []
(let [initial (:mcp-enabled? server-state)]
(set-checked! initial)))
[server-state])
(let [on-toggle (fn []
(let [new-val (not checked)]
(set-checked! new-val)
;; Enable HTTP server to simplify starting MCP
(when (and new-val (not (storage/get ::storage-spec/http-server-enabled)))
(storage/set ::storage-spec/http-server-enabled true))
(-> (ipc/ipc :server/set-config {:mcp-enabled? new-val})
;; Don't start server if it's not running
(p/then #(when (= "running" (state/get-state [:electron/server :status]))
(p/let [_ (p/delay 1000)]
(ipc/ipc :server/do :restart))))
(p/catch #(notification/show! (str %) :error)))))]
(toggle "mcp-server"
(t :settings.ai/enable-mcp-server)
checked
on-toggle
[:span.text-sm.opacity-50
(t :settings.ai/enable-mcp-server-desc)]))))
(hsx/defc settings-ai
[]
[:div.panel-wrap
(when (util/electron?)
(mcp-server-row t))])
(hsx/defc ^:large-vars/cleanup-todo settings
[_active-tab]
(let [current-repo (rfx/use-sub [:git/current-repo])
@@ -1270,11 +1234,8 @@
(fn []
(state/load-app-user-cfgs)
(state/load-electron-app-base-info)
(let [active-tab (if (and (= _active-tab :ai) (not (util/electron?)))
:advanced
_active-tab)]
(when (keyword? active-tab)
(reset! *active [active-tab nil])))
(when (keyword? _active-tab)
(reset! *active [_active-tab nil]))
#(state/close-settings!))
[])
@@ -1291,9 +1252,6 @@
[:general (t :settings/general) (ui/icon "adjustments")]
[:editor (t :settings/editor) (ui/icon "writing")]
[:keymap (t :settings/keymap) (ui/icon "keyboard")]
(when (util/electron?)
[:ai (t :settings/ai) (ui/icon "wand")])
[:advanced (t :settings/advanced) (ui/icon "bulb")]
[:features (t :settings/features) (ui/icon "app-feature")]
@@ -1348,9 +1306,4 @@
:features
(settings-features)
:ai
(if (util/electron?)
(settings-ai)
(settings-advanced))
nil)]]]))

View File

@@ -8,6 +8,7 @@
[frontend.worker.db-worker-node-auth :as auth]
[frontend.worker.db-worker-node-lock :as db-lock]
[frontend.worker.db-worker-node-ws :as db-ws]
[frontend.worker.mcp-server :as mcp-server]
[frontend.worker.platform.node :as platform-node]
[frontend.worker.semantic-routes :as semantic-routes]
[frontend.worker.state :as worker-state]
@@ -676,6 +677,15 @@
(string/starts-with? request-path "/v1/semantic/")
(semantic-request-handler req res bound-repo proxy parsed-url request-path method)
(and (= request-path "/mcp") (= method "POST"))
(mcp-server/handle-post-request {:host host :port (effective-port port)} req res)
(and (= request-path "/mcp") (= method "GET"))
(mcp-server/handle-get-request req res)
(and (= request-path "/mcp") (= method "DELETE"))
(mcp-server/handle-delete-request req res)
(= request-path "/v1/invoke-batch")
(if (= method "POST")
(-> (p/let [body (<read-body req)

View File

@@ -0,0 +1,280 @@
(ns frontend.worker.mcp-server
"MCP server for the daemon (Fase 9), exposing the semantic API
(semantic-routes/operations) as MCP tools. The CF-Worker's equivalent
(deps/db-sync/worker/entry.mjs) auto-generates its tools from the OpenAPI
document via @cloudflare/codemode's openApiMcpServer, a package that only
exists inside Cloudflare Workers -- not portable to Node -- plus a pile of
ChatGPT-App-specific tooling (asset image resources, upload_asset,
Cognito protected-resource metadata) this daemon has no use for. So this
registers one tool per operation by hand instead, 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 docs instead of writing 34
one-off registrations, so the MCP tool surface can't drift from the HTTP
routes and the OpenAPI document.
Each tool's handler makes a loopback HTTP call to this same daemon's own
/v1/semantic/* route (see db_worker_node.cljs), forwarding the
Authorization header the /mcp request itself carried -- reusing the
already-tested HTTP-layer logic (repo-error validation, lock-owner
assertion, response shaping) rather than re-implementing a second
request path directly against the thread-api."
(:require ["@modelcontextprotocol/sdk/server/mcp.js" :refer [McpServer]]
["@modelcontextprotocol/sdk/server/streamableHttp.js" :refer [StreamableHTTPServerTransport]]
["@modelcontextprotocol/sdk/types.js" :refer [isInitializeRequest]]
["zod/v3" :as z]
[clojure.string :as string]
[frontend.worker.semantic-routes :as semantic-routes]
[lambdaisland.glogi :as log]
[promesa.core :as p]))
;; Shared Zod field fragments
;; ==========================
(def ^:private pagination-fields
{:limit (-> (z/number) .int .optional (.describe "Max items per page (default 50, max 200)"))
:cursor (-> (z/string) .optional (.describe "Opaque pagination cursor from a previous response's next-cursor"))})
(def ^:private time-filter-fields
{:created-after (-> (z/number) .int .optional (.describe "Unix epoch ms; only entities created strictly after this"))
:updated-after (-> (z/number) .int .optional (.describe "Unix epoch ms; only entities updated strictly after this"))})
(def ^:private listing-fields (merge pagination-fields time-filter-fields))
(def ^:private block-tree-schema
"Self-recursive, so it needs z/lazy -- the thunk closes over this same var,
resolved once def completes, the standard cljs recursive-def idiom."
(z/lazy (fn []
(z/object #js {:title (z/string)
:children (-> (z/array block-tree-schema) .optional)}))))
(def ^:private property-value-schema
(-> (z/union #js [(z/string) (z/number) (z/boolean)
(z/array (z/union #js [(z/string) (z/number)]))])
(.describe (str "For ref properties, an existing entity's UUID, ident, or title. Status/Priority also "
"accept built-in aliases like TODO or HIGH. Class properties like Tags accept one "
"selector or an array of them. Scalar properties take their JSON value directly."))))
(def ^:private task-status-field
(-> (z/string) .optional
(.describe "todo|doing|in-review|done|canceled|backlog, or an existing Status choice's UUID/ident/title")))
(def ^:private task-priority-field
(-> (z/string) .optional
(.describe "low|medium|high|urgent, or an existing Priority choice's UUID/ident/title")))
(def ^:private property-type-field
(-> (z/enum #js ["default" "number" "date" "datetime" "checkbox" "url" "node" "asset" "map" "json" "string"])
.optional))
(def ^:private property-cardinality-field
(-> (z/enum #js ["db.cardinality/one" "db.cardinality/many"]) .optional))
;; Per-operation extra fields (beyond the path params every operation gets
;; automatically -- see path-arg-fields below)
;; ============================================================================
(def ^:private extra-fields
{"listPages" listing-fields
"createPage" {:title (z/string)}
"listPageBlocks" listing-fields
"listPageReferences" listing-fields
"updatePage" {:title (z/string)}
"updateBlock" {:title (z/string)}
"moveBlocks" {:block-ids (z/array (z/string))
:target-id (z/string)
:position (z/enum #js ["before" "after" "first-child" "last-child"])}
"insertBlockChildren" {:position (z/enum #js ["append" "prepend"])
:blocks (z/array block-tree-schema)}
"insertBlockTree" {:target-id (z/string)
:position (-> (z/enum #js ["append" "prepend"]) .optional)
:blocks (z/array block-tree-schema)}
"setBlockProperty" {:value property-value-schema}
"batchSetBlockProperty" {:isResetExistingValues (-> (z/boolean) .optional)
:entries (z/array (z/object #js {:block-id (z/string)
:property-id (z/string)
:value property-value-schema}))}
"batchDeleteBlockProperty" {:entries (z/array (z/object #js {:block-id (z/string) :property-id (z/string)}))}
"captureToToday" {:blocks (z/array block-tree-schema)}
"listTasks" (merge listing-fields
{:status task-status-field
:priority task-priority-field
:content (-> (z/string) .optional (.describe "Case-insensitive task title filter"))})
"createTask" {:title (z/string)
:page-id (-> (z/string) .optional (.describe "Destination page UUID; omit for today's journal"))
:status task-status-field
:priority task-priority-field
:scheduled (-> (z/number) .int .optional (.describe "Scheduled date as YYYYMMDD"))
:deadline (-> (z/number) .int .optional (.describe "Deadline date as YYYYMMDD"))}
"listTags" listing-fields
"createTag" {:title (z/string)}
"listTagObjects" listing-fields
"updateTag" {:title (z/string)}
"listProperties" listing-fields
"createProperty" {:title (z/string) :type property-type-field :cardinality property-cardinality-field}
"updateProperty" {:title (-> (z/string) .optional) :type property-type-field :cardinality property-cardinality-field}
"listAssets" listing-fields
"createAsset" {:file-name (z/string)
:size (-> (z/number) .int (.describe "Exact decoded byte size of the file, max 100MB"))
:checksum (-> (z/string) (.regex (js/RegExp. "^[0-9a-fA-F]{64}$"))
(.describe "SHA-256 hex digest of the decoded file"))
:title (-> (z/string) .optional)
:page-id (-> (z/string) .optional (.describe "Destination page UUID; omit for today's journal"))
:content (-> (z/string) (.describe "Base64-encoded file contents"))}
"searchGraph" (merge listing-fields
{:q (z/string)
:types (-> (z/string) .optional
(.describe "Comma-separated subset of blocks,tags,properties,assets"))})})
;; Request construction
;; ====================
(defn- path-param-names [path]
(mapv second (re-seq #":([^/]+)" path)))
(defn- path-arg-fields [path]
(into {}
(map (fn [param]
[(keyword param) (-> (z/string) (.describe (str "The " param)))]))
(path-param-names path)))
(defn- build-path [path args]
(reduce (fn [p param]
(string/replace p (str ":" param) (js/encodeURIComponent (str (get args (keyword param))))))
path
(path-param-names path)))
(defn- query-string [args]
(let [pairs (->> args
(remove (fn [[_ v]] (nil? v)))
(map (fn [[k v]] (str (js/encodeURIComponent (name k)) "=" (js/encodeURIComponent (str v))))))]
(if (seq pairs) (str "?" (string/join "&" pairs)) "")))
(defn- request-for
[{:keys [operation-id method path]} args]
(let [path-names (set (map keyword (path-param-names path)))
rest-args (apply dissoc args path-names)
url-path (build-path path args)]
(cond
(= operation-id "createAsset")
{:method method
:path (str url-path (query-string (assoc (dissoc rest-args :content) :encoding "base64")))
:body (:content rest-args)
:content-type "text/plain"}
(contains? #{"GET" "DELETE"} method)
{:method method :path (str url-path (query-string rest-args)) :body nil :content-type nil}
:else
{:method method :path url-path :body (js/JSON.stringify (clj->js rest-args)) :content-type "application/json"})))
(defn- call-semantic-operation!
[{:keys [host port authorization]} operation args]
(let [{:keys [method path body content-type]} (request-for operation args)
url (str "http://" host ":" port path)]
(-> (js/fetch url (clj->js (cond-> {:method method
:headers (cond-> {}
authorization (assoc "Authorization" authorization)
content-type (assoc "Content-Type" content-type))}
body (assoc :body body))))
(p/then (fn [^js res]
(p/let [text (.text res)]
{:status (.-status res) :ok (.-ok res) :text text}))))))
(defn- mcp-result [{:keys [ok text]}]
#js {:content #js [#js {:type "text" :text (if (string/blank? text) "{}" text)}]
:isError (not ok)})
(defn- mcp-error-result [error]
#js {:content #js [#js {:type "text" :text (str "Unexpected MCP tool error: " (or (.-message error) error))}]
:isError true})
;; Server construction
;; ===================
(defn- register-operation-tool!
[^js mcp-server ctx operation]
(let [{:keys [operation-id]} operation
[title description] (get semantic-routes/operation-docs operation-id)
input-shape (clj->js (merge (path-arg-fields (:path operation)) (get extra-fields operation-id {})))]
(.registerTool mcp-server operation-id
#js {:title title
:description description
:inputSchema input-shape}
(fn [args]
(-> (call-semantic-operation! ctx operation (js->clj args :keywordize-keys true))
(p/then mcp-result)
(p/catch mcp-error-result))))))
(defn create-mcp-api-server
[ctx]
(let [mcp-server (McpServer. #js {:name "Logseq Daemon Semantic API" :version "1.0.0"})]
(doseq [operation semantic-routes/operations]
(register-operation-tool! mcp-server ctx operation))
mcp-server))
;; HTTP glue -- adapted from electron/mcp_server.cljs's Fastify-based version:
;; that one gets req/res already wrapped (`.raw` unwraps back to the
;; underlying Node primitives, `.body` is pre-parsed by Fastify's body
;; parser); the daemon calls this straight from its own raw
;; http.createServer callback, so req/res ARE already the Node primitives
;; StreamableHTTPServerTransport wants, and the body needs parsing here.
;; Auth is the daemon's existing bearer-token check on every route (see
;; db_worker_node.cljs's request-authorized?) -- already done by the time
;; this is reached, so unlike the CF-Worker/Electron versions there's no
;; separate auth check in here.
(defonce ^:private *transports (atom {}))
(defn- <read-body [^js req]
(p/create
(fn [resolve reject]
(let [chunks (array)]
(.on req "data" (fn [chunk] (.push chunks chunk)))
(.on req "end" (fn [] (resolve (.toString (js/Buffer.concat chunks) "utf8"))))
(.on req "error" reject)))))
(defn handle-post-request
[{:keys [host port]} ^js req ^js res]
(let [session-id (aget (.-headers req) "mcp-session-id")
authorization (.. req -headers -authorization)]
(p/let [body-text (<read-body req)
body (when-not (string/blank? body-text) (js/JSON.parse body-text))]
(cond
(and session-id (@*transports session-id))
(let [^js transport (@*transports session-id)]
(.handleRequest transport req res body))
(and (not session-id) (isInitializeRequest body))
(let [transport (StreamableHTTPServerTransport.
#js {:sessionIdGenerator (comp str random-uuid)
:enableDnsRebindingProtection true
:allowedHosts #js [(str host ":" port)]})
mcp-server (create-mcp-api-server {:host host :port port :authorization authorization})]
(set! (.-onclose transport)
(fn [] (swap! *transports dissoc (.-sessionId transport))))
(p/do!
(.connect mcp-server transport)
(.handleRequest transport req res body)
(if (.-sessionId transport)
(swap! *transports assoc (.-sessionId transport) transport)
(log/error :mcp-server-no-session-id-after-initialize nil))))
:else
(do
(.writeHead res 400 #js {"Content-Type" "application/json"})
(.end res (js/JSON.stringify
#js {:jsonrpc "2.0"
:error #js {:code -32000 :message "Bad Request: No valid session ID provided"}
:id nil})))))))
(defn handle-get-request
[^js req ^js res]
(let [session-id (aget (.-headers req) "mcp-session-id")]
(if-let [^js transport (and session-id (@*transports session-id))]
(.handleRequest transport req res)
(do (.writeHead res 400) (.end res "Invalid or missing session ID")))))
(defn handle-delete-request
[^js req ^js res]
(let [session-id (aget (.-headers req) "mcp-session-id")]
(if-let [^js transport (and session-id (@*transports session-id))]
(do (.close transport)
(.writeHead res 200 #js {"Content-Type" "application/json"})
(.end res (js/JSON.stringify #js {:ok true})))
(do (.writeHead res 400) (.end res "Invalid or missing session ID")))))

View File

@@ -83,7 +83,10 @@
{:method "GET" :path "/v1/semantic/search"
:handler :semantic/search :operation-id "searchGraph" :success-status 200}])
(def ^:private operation-docs
(def operation-docs
"Public so worker.mcp-server can source MCP tool titles/descriptions from
the same place the OpenAPI document does, instead of a second copy that
could drift."
{"listPages" ["List pages" "Returns a cursor-paginated list of page blocks in the graph."]
"createPage" ["Create a page" "Creates a page block with the supplied title."]
"listPageBlocks" ["List a page's blocks" "Returns a cursor-paginated list of the page's top-level blocks, including each selected block's descendant tree."]

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Nuwe opsie: {1}"
:server/error-notification "[Server] {1}"
:server/mcp-url-copied "MCP-URL gekopieerd"
:server/start "Begin"
:server/stop "Stop"
:server/title "HTTP API-server"
@@ -1543,7 +1542,6 @@
:settings/account "Rekening"
:settings/advanced "Gevorderd"
:settings/ai "KI"
:settings/collaboration "Samewerking"
:settings/editor "Redigeerder"
:settings/encryption "Enkripsie"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Deaktiveer ongelukrapportering"
:settings.advanced/network-proxy "Netwerkvolmag"
:settings.ai/enable-mcp-server "Aktiveer MCP-bediener"
:settings.ai/enable-mcp-server-desc "MCP-server aktiveer vir AI-integratie"
:settings.editor/auto-expand-block-refs "Blokreferenties outomaties uitklappen"
:settings.editor/auto-expand-block-refs-tip "Klik om blokreferenties outomaties uit te klappen"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ خيار جديد: {1}"
:server/error-notification "[الخادم] {1}"
:server/mcp-url-copied "تم نسخ رابط MCP"
:server/start "بدء"
:server/stop "إيقاف"
:server/title "خادم HTTP API"
@@ -1543,7 +1542,6 @@
:settings/account "الحساب"
:settings/advanced "متقدم"
:settings/ai "الذكاء الاصطناعي"
:settings/collaboration "التعاون"
:settings/editor "المحرر"
:settings/encryption "التشفير"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "تعطيل تقارير الأعطال"
:settings.advanced/network-proxy "وكيل الشبكة"
:settings.ai/enable-mcp-server "تفعيل خادم MCP"
:settings.ai/enable-mcp-server-desc "تفعيل خادم MCP لتكامل الذكاء الاصطناعي"
:settings.editor/auto-expand-block-refs "توسيع مراجع الكتل تلقائياً"
:settings.editor/auto-expand-block-refs-tip "انقر لتوسيع مراجع الكتل تلقائياً"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Opció nova: {1}"
:server/error-notification "[Servidor] {1}"
:server/mcp-url-copied "URL MCP copiada"
:server/start "Iniciar"
:server/stop "Aturar"
:server/title "Servidor d'API HTTP"
@@ -1543,7 +1542,6 @@
:settings/account "Compte"
:settings/advanced "Avançat"
:settings/ai "IA"
:settings/collaboration "Col·laboració"
:settings/editor "Editor"
:settings/encryption "Encriptació"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc " Logseq mai recol·lectarà la seva base de dades de grafs local o vendrà les seves dades."
:settings.advanced/network-proxy "Proxy de xarxa"
:settings.ai/enable-mcp-server "Activar servidor MCP"
:settings.ai/enable-mcp-server-desc "Activar servidor MCP per integració amb IA"
:settings.editor/auto-expand-block-refs "Expandir referències de blocs automàticament"
:settings.editor/auto-expand-block-refs-tip "Expandir referències de blocs automàticament en fer clic"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Nová možnost: {1}"
:server/error-notification "[Server] {1}"
:server/mcp-url-copied "URL MCP zkopírováno"
:server/start "Spustit"
:server/stop "Zastavit"
:server/title "HTTP API server"
@@ -1543,7 +1542,6 @@
:settings/account "Účet"
:settings/advanced "Pokročilé"
:settings/ai "AI"
:settings/collaboration "Spolupráce"
:settings/editor "Editor"
:settings/encryption "Šifrování"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Společnost Logseq nikdy nebude shromažďovat vaši místní databázi grafů ani prodávat vaše data."
:settings.advanced/network-proxy "Síťový proxy server"
:settings.ai/enable-mcp-server "Zapnout server MCP"
:settings.ai/enable-mcp-server-desc "Zapnout server MCP pro integraci s AI"
:settings.editor/auto-expand-block-refs "Automaticky rozbalit odkazy na bloky"
:settings.editor/auto-expand-block-refs-tip "Automaticky rozbalit odkazy na bloky při kliknutí"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Neue Option: {1}"
:server/error-notification "[Server] {1}"
:server/mcp-url-copied "MCP-URL kopiert"
:server/start "Starten"
:server/stop "Stoppen"
:server/title "HTTP-API-Server"
@@ -1543,7 +1542,6 @@
:settings/account "Konto"
:settings/advanced "Erweitert"
:settings/ai "KI"
:settings/collaboration "Zusammenarbeit"
:settings/editor "Editor"
:settings/encryption "Verschlüsselung"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Logseq wird niemals Ihre lokale Graphdatenbank sammeln oder Ihre Daten verkaufen."
:settings.advanced/network-proxy "Netzwerk-Proxy"
:settings.ai/enable-mcp-server "MCP-Server aktivieren"
:settings.ai/enable-mcp-server-desc "MCP-Server für KI-Integration aktivieren"
:settings.editor/auto-expand-block-refs "Automatisch beim Heranzoomen Blockreferenzen erweitern..."
:settings.editor/auto-expand-block-refs-tip "Diese Option steuert, ob Blockreferenzen beim Heranzoomen automatisch ausgeklappt werden."

View File

@@ -1658,7 +1658,6 @@
:select/new-option "+ New option: {1}"
:server/error-notification "[Server] {1}"
:server/mcp-url-copied "MCP URL copied to clipboard!"
:server/start "Start server"
:server/stop "Stop server"
:server/title "HTTP API server"
@@ -1685,7 +1684,6 @@
:settings/account "Account"
:settings/advanced "Advanced"
:settings/ai "AI"
:settings/collaboration "Collaboration"
:settings/editor "Editor"
:settings/encryption "Encryption"
@@ -1706,8 +1704,6 @@
:settings.advanced/disable-sentry-desc "Logseq will never collect your local graph database or sell your data."
:settings.advanced/network-proxy "Network proxy"
:settings.ai/enable-mcp-server "MCP Server"
:settings.ai/enable-mcp-server-desc "Enable MCP Server to allow commmunication with local AI applications. MCP Server depends on HTTP API server."
:settings.editor/auto-expand-block-refs "Expand block references automatically when zoom-in"
:settings.editor/auto-expand-block-refs-tip "This option controls whether to expand the block references automatically when zoom-in."

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Nueva opción: {1}"
:server/error-notification "[Servidor] {1}"
:server/mcp-url-copied "URL MCP copiada"
:server/start "Iniciar"
:server/stop "Detener"
:server/title "Servidor API HTTP"
@@ -1543,7 +1542,6 @@
:settings/account "Cuenta"
:settings/advanced "Avanzado"
:settings/ai "IA"
:settings/collaboration "Colaboración"
:settings/editor "Editor"
:settings/encryption "Cifrado"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc " Logseq nunca va a recolectar tu base de datos de grafos local o vender tus datos."
:settings.advanced/network-proxy "Proxy de red"
:settings.ai/enable-mcp-server "Habilitar servidor MCP"
:settings.ai/enable-mcp-server-desc "Habilitar servidor MCP para integración con IA"
:settings.editor/auto-expand-block-refs "Expandir referencias de bloque automáticamente al hacer un acercamiento"
:settings.editor/auto-expand-block-refs-tip "Esta opción controla si expandir el bloque de referencias automáticamente al hacer un acercamiento."

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ گزینه جدید: {1}"
:server/error-notification "[سرور] {1}"
:server/mcp-url-copied "آدرس MCP کپی شد"
:server/start "شروع"
:server/stop "توقف"
:server/title "سرور HTTP API"
@@ -1543,7 +1542,6 @@
:settings/account "حساب"
:settings/advanced "پیشرفته"
:settings/ai "هوش مصنوعی"
:settings/collaboration "همکاری"
:settings/editor "ویرایشگر"
:settings/encryption "رمزگذاری"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "غیرفعال‌سازی گزارش خرابی"
:settings.advanced/network-proxy "پروکسی شبکه"
:settings.ai/enable-mcp-server "فعال‌سازی سرور MCP"
:settings.ai/enable-mcp-server-desc "فعال‌سازی سرور MCP برای ادغام هوش مصنوعی"
:settings.editor/auto-expand-block-refs "باز کردن خودکار مراجع بلوک"
:settings.editor/auto-expand-block-refs-tip "برای باز کردن خودکار مراجع بلوک کلیک کنید"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Nouvelle option : {1}"
:server/error-notification "[Serveur] {1}"
:server/mcp-url-copied "URL MCP copiée"
:server/start "Démarrer"
:server/stop "Arrêter"
:server/title "Serveur API HTTP"
@@ -1543,7 +1542,6 @@
:settings/account "Compte"
:settings/advanced "Avancé"
:settings/ai "IA"
:settings/collaboration "Collaboration"
:settings/editor "Éditeur"
:settings/encryption "Chiffrement"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Logseq ne collectera jamais votre base de données de graphes locale ni ne vendra vos données."
:settings.advanced/network-proxy "Proxy réseau"
:settings.ai/enable-mcp-server "Activer le serveur MCP"
:settings.ai/enable-mcp-server-desc "Activer le serveur MCP pour l'intégration IA"
:settings.editor/auto-expand-block-refs "Étendre automatiquement les références de bloc lors du zoom"
:settings.editor/auto-expand-block-refs-tip "Cette option contrôle si les références de bloc sont développées automatiquement lors du zoom."

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Opsi baru: {1}"
:server/error-notification "[Server] {1}"
:server/mcp-url-copied "URL MCP disalin"
:server/start "Mulai"
:server/stop "Berhenti"
:server/title "Server HTTP API"
@@ -1543,7 +1542,6 @@
:settings/account "Akun"
:settings/advanced "Lanjutan"
:settings/ai "AI"
:settings/collaboration "Kolaborasi"
:settings/editor "Editor"
:settings/encryption "Enkripsi"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Logseq tidak akan pernah mengumpulkan database grafik lokal Anda atau menjual data Anda."
:settings.advanced/network-proxy "Proxy jaringan"
:settings.ai/enable-mcp-server "Aktifkan server MCP"
:settings.ai/enable-mcp-server-desc "Aktifkan server MCP untuk integrasi AI"
:settings.editor/auto-expand-block-refs "Perluas referensi blok secara otomatis saat zoom-in"
:settings.editor/auto-expand-block-refs-tip "Opsi ini mengontrol apakah referensi blok akan diperluas secara otomatis saat zoom-in."

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Nuova opzione: {1}"
:server/error-notification "[Server] {1}"
:server/mcp-url-copied "URL MCP copiato"
:server/start "Avvia"
:server/stop "Ferma"
:server/title "Server API HTTP"
@@ -1543,7 +1542,6 @@
:settings/account "Account"
:settings/advanced "Avanzate"
:settings/ai "IA"
:settings/collaboration "Collaborazione"
:settings/editor "Editor"
:settings/encryption "Crittografia"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Logseq non raccoglierà mai il database del grafo locale e non venderà mai i tuoi dati."
:settings.advanced/network-proxy "Proxy di rete"
:settings.ai/enable-mcp-server "Abilita server MCP"
:settings.ai/enable-mcp-server-desc "Abilita il server MCP per l'integrazione AI"
:settings.editor/auto-expand-block-refs "Espandi riferimenti ai blocchi automaticamente quando si ingrandisce"
:settings.editor/auto-expand-block-refs-tip "Questa opzione controlla se espandere i riferimenti ai blocchi automaticamente quando si ingrandisce."

View File

@@ -1529,7 +1529,6 @@
:select/new-option "+ 新しいオプション: {1}"
:server/error-notification "[サーバー] {1}"
:server/mcp-url-copied "MCP URLをコピーしました"
:server/start "開始"
:server/stop "停止"
:server/title "HTTP APIサーバー"
@@ -1556,7 +1555,6 @@
:settings/account "アカウント"
:settings/advanced "高度な設定"
:settings/ai "AI"
:settings/collaboration "コラボレーション"
:settings/editor "エディタ"
:settings/encryption "暗号化"
@@ -1577,8 +1575,6 @@
:settings.advanced/disable-sentry-desc "Logseqはあなたのローカルなグラフを決して取得しませんし、あなたのデータを売ることも決してありません。 "
:settings.advanced/network-proxy "ネットワークプロキシ"
:settings.ai/enable-mcp-server "MCPサーバーを有効化"
:settings.ai/enable-mcp-server-desc "MCPサーバーを有効にしてAI連携を可能にします"
:settings.editor/auto-expand-block-refs "ブロック参照を自動展開"
:settings.editor/auto-expand-block-refs-tip "このオプションは、ズームインしたときにブロック参照を展開するかどうかを指定します。"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ 새 옵션: {1}"
:server/error-notification "[서버] {1}"
:server/mcp-url-copied "MCP URL이 복사되었습니다"
:server/start "시작"
:server/stop "중지"
:server/title "HTTP API 서버"
@@ -1543,7 +1542,6 @@
:settings/account "계정"
:settings/advanced "고급"
:settings/ai "AI"
:settings/collaboration "협업"
:settings/editor "편집기"
:settings/encryption "암호화"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Logseq는 사용자의 로컬 그래프 데이터베이스 정보를 수집하지 않습니다."
:settings.advanced/network-proxy "네트워크 프록시"
:settings.ai/enable-mcp-server "MCP 서버 활성화"
:settings.ai/enable-mcp-server-desc "MCP 서버를 활성화하여 AI 연동을 가능하게 합니다"
:settings.editor/auto-expand-block-refs "블록 참조 자동 확장"
:settings.editor/auto-expand-block-refs-tip "이 설정은 블록을 확대할 때 블록 참조를 자동으로 펼칠지를 결정합니다."

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Nytt alternativ: {1}"
:server/error-notification "[Server] {1}"
:server/mcp-url-copied "MCP-URL kopiert"
:server/start "Start"
:server/stop "Stopp"
:server/title "HTTP API-server"
@@ -1543,7 +1542,6 @@
:settings/account "Konto"
:settings/advanced "Avansert"
:settings/ai "AI"
:settings/collaboration "Samarbeid"
:settings/editor "Redigering"
:settings/encryption "Kryptering"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Deaktiver krasjrapportering"
:settings.advanced/network-proxy "Nettverksproxy"
:settings.ai/enable-mcp-server "Aktiver MCP-server"
:settings.ai/enable-mcp-server-desc "Aktiver MCP-server for AI-integrasjon"
:settings.editor/auto-expand-block-refs "Utvid blokkreferanser automatisk"
:settings.editor/auto-expand-block-refs-tip "Klikk for å utvide blokkreferanser automatisk"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Nieuwe optie: {1}"
:server/error-notification "[Server] {1}"
:server/mcp-url-copied "MCP-URL gekopieerd"
:server/start "Starten"
:server/stop "Stoppen"
:server/title "HTTP API-server"
@@ -1543,7 +1542,6 @@
:settings/account "Account"
:settings/advanced "Geavanceerd"
:settings/ai "AI"
:settings/collaboration "Samenwerking"
:settings/editor "Editor"
:settings/encryption "Versleuteling"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Crashrapportage uitschakelen"
:settings.advanced/network-proxy "Netwerk proxy"
:settings.ai/enable-mcp-server "MCP-server inschakelen"
:settings.ai/enable-mcp-server-desc "MCP-server inschakelen voor AI-integratie"
:settings.editor/auto-expand-block-refs "Blokreferenties automatisch uitklappen"
:settings.editor/auto-expand-block-refs-tip "Klik om blokreferenties automatisch uit te klappen"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Nowa opcja: {1}"
:server/error-notification "[Serwer] {1}"
:server/mcp-url-copied "URL MCP skopiowany"
:server/start "Uruchom"
:server/stop "Zatrzymaj"
:server/title "Serwer HTTP API"
@@ -1543,7 +1542,6 @@
:settings/account "Konto"
:settings/advanced "Zaawansowane"
:settings/ai "AI"
:settings/collaboration "Współpraca"
:settings/editor "Edytor"
:settings/encryption "Szyfrowanie"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Logseq nigdy nie zbiera danych Twoich lokalnych grafów i nie sprzedaje twoich danych."
:settings.advanced/network-proxy "Ustawienia proxy"
:settings.ai/enable-mcp-server "Włącz serwer MCP"
:settings.ai/enable-mcp-server-desc "Włącz serwer MCP do integracji z AI"
:settings.editor/auto-expand-block-refs "Automatycznie rozwijaj odniesienia do bloków"
:settings.editor/auto-expand-block-refs-tip "Automatycznie rozwijaj odniesienia do bloków po kliknięciu"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Nova opção: {1}"
:server/error-notification "[Servidor] {1}"
:server/mcp-url-copied "URL MCP copiada"
:server/start "Iniciar"
:server/stop "Parar"
:server/title "Servidor de API HTTP"
@@ -1543,7 +1542,6 @@
:settings/account "Conta"
:settings/advanced "Avançado"
:settings/ai "IA"
:settings/collaboration "Colaboração"
:settings/editor "Editor"
:settings/encryption "Criptografia"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "O Logseq nunca coletará o banco de dados do seu grafo local nem venderá seus dados."
:settings.advanced/network-proxy "Proxy de rede"
:settings.ai/enable-mcp-server "Habilitar servidor MCP"
:settings.ai/enable-mcp-server-desc "Habilitar servidor MCP para integração com IA"
:settings.editor/auto-expand-block-refs "Expandir automaticamente as referências de bloco ao dar zoom"
:settings.editor/auto-expand-block-refs-tip "Esta opção controla se as referências de bloco são expandidas automaticamente ao dar zoom."

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Nova opção: {1}"
:server/error-notification "[Servidor] {1}"
:server/mcp-url-copied "URL MCP copiada"
:server/start "Iniciar"
:server/stop "Parar"
:server/title "Servidor de API HTTP"
@@ -1543,7 +1542,6 @@
:settings/account "Conta"
:settings/advanced "Avançado"
:settings/ai "IA"
:settings/collaboration "Colaboração"
:settings/editor "Editor"
:settings/encryption "Encriptação"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "A Logseq nunca irá recolher informações sobre a sua base de dados de grafos local ou vender os seus dados."
:settings.advanced/network-proxy "Proxy de rede"
:settings.ai/enable-mcp-server "Ativar servidor MCP"
:settings.ai/enable-mcp-server-desc "Ativar servidor MCP para integração com IA"
:settings.editor/auto-expand-block-refs "Expandir referências de blocos automaticamente"
:settings.editor/auto-expand-block-refs-tip "Expandir referências de blocos automaticamente ao clicar"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Новая опция: {1}"
:server/error-notification "[Сервер] {1}"
:server/mcp-url-copied "URL MCP скопирован"
:server/start "Запустить"
:server/stop "Остановить"
:server/title "HTTP API сервер"
@@ -1543,7 +1542,6 @@
:settings/account "Аккаунт"
:settings/advanced "Продвинутые"
:settings/ai "ИИ"
:settings/collaboration "Совместная работа"
:settings/editor "Редактор"
:settings/encryption "Шифрование"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Logseq никогда не будет собирать вашу локальную базу данных графов или продавать ваши данные"
:settings.advanced/network-proxy "Прокси-сервер"
:settings.ai/enable-mcp-server "Включить сервер MCP"
:settings.ai/enable-mcp-server-desc "Включить сервер MCP для интеграции с ИИ"
:settings.editor/auto-expand-block-refs "Автораскрытие ссылок на блоки"
:settings.editor/auto-expand-block-refs-tip "Автоматически раскрывать ссылки на блоки при нажатии"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Nová možnosť: {1}"
:server/error-notification "[Server] {1}"
:server/mcp-url-copied "URL MCP skopírované"
:server/start "Spustiť"
:server/stop "Zastaviť"
:server/title "HTTP API server"
@@ -1543,7 +1542,6 @@
:settings/account "Účet"
:settings/advanced "Pokročilé"
:settings/ai "AI"
:settings/collaboration "Spolupráca"
:settings/editor "Editor"
:settings/encryption "Šifrovanie"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Logseq nebude nikdy zhromažďovať vašu lokálnu databázu grafov ani predávať vaše údaje."
:settings.advanced/network-proxy "Sieťová proxy"
:settings.ai/enable-mcp-server "Zapnúť server MCP"
:settings.ai/enable-mcp-server-desc "Zapnout server MCP pro integraci s AI"
:settings.editor/auto-expand-block-refs "Automaticky rozbalit odkazy na bloky"
:settings.editor/auto-expand-block-refs-tip "Automaticky rozbalit odkazy na bloky při kliknutí"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Yeni seçenek: {1}"
:server/error-notification "[Sunucu] {1}"
:server/mcp-url-copied "MCP URL'si kopyalandı"
:server/start "Başlat"
:server/stop "Durdur"
:server/title "HTTP API sunucusu"
@@ -1543,7 +1542,6 @@
:settings/account "Hesap"
:settings/advanced "Gelişmiş"
:settings/ai "AI"
:settings/collaboration "İş birliği"
:settings/editor "Editör"
:settings/encryption "Şifreleme"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Logseq asla yerel graf veritabanınızı toplamayacak veya verilerinizi satmayacaktır."
:settings.advanced/network-proxy "Ağ ara sunucusu"
:settings.ai/enable-mcp-server "MCP sunucusunu etkinleştir"
:settings.ai/enable-mcp-server-desc "AI entegrasyonu için MCP sunucusunu etkinleştir"
:settings.editor/auto-expand-block-refs "Blok referanslarını otomatik genişlet"
:settings.editor/auto-expand-block-refs-tip "Tıklandığında blok referanslarını otomatik genişlet"

View File

@@ -1516,7 +1516,6 @@
:select/new-option "+ Нова опція: {1}"
:server/error-notification "[Сервер] {1}"
:server/mcp-url-copied "URL MCP скопирован"
:server/start "Запустити"
:server/stop "Зупинити"
:server/title "HTTP API сервер"
@@ -1543,7 +1542,6 @@
:settings/account "Обліковий запис"
:settings/advanced "Розширення"
:settings/ai "ШІ"
:settings/collaboration "Спільна робота"
:settings/editor "Редактор"
:settings/encryption "Шифрування"
@@ -1564,8 +1562,6 @@
:settings.advanced/disable-sentry-desc "Logseq ніколи не збиратиме вашу локальну базу даних графів і не відправлятиме ваші дані."
:settings.advanced/network-proxy "Мережевий проксі"
:settings.ai/enable-mcp-server "Увімкнути сервер MCP"
:settings.ai/enable-mcp-server-desc "Увімкнути сервер MCP для интеграции с ИИ"
:settings.editor/auto-expand-block-refs "Автораскрытие ссылок на блоки"
:settings.editor/auto-expand-block-refs-tip "Автоматически раскрывать ссылки на блоки при нажатии"

View File

@@ -1658,7 +1658,6 @@
:select/new-option "+ 新建选项:{1}"
:server/error-notification "[服务器] {1}"
:server/mcp-url-copied "MCP URL 已复制到剪贴板!"
:server/start "启动服务器"
:server/stop "停止服务器"
:server/title "HTTP API 服务器"
@@ -1685,7 +1684,6 @@
:settings/account "账户"
:settings/advanced "高级"
:settings/ai "AI"
:settings/collaboration "协作"
:settings/editor "编辑器"
:settings/encryption "加密"
@@ -1706,8 +1704,6 @@
:settings.advanced/disable-sentry-desc "Logseq 从来不会收集或出售你的图谱数据。"
:settings.advanced/network-proxy "网络代理"
:settings.ai/enable-mcp-server "MCP 服务器"
:settings.ai/enable-mcp-server-desc "启用 MCP 服务器,允许与本地 AI 应用通信"
:settings.editor/auto-expand-block-refs "自动展开聚焦块的引用"
:settings.editor/auto-expand-block-refs-tip "此选项控制是否在页面加载时展开块引用"

View File

@@ -1529,7 +1529,6 @@
:select/new-option "+ 新選項: {1}"
:server/error-notification "[伺服器] {1}"
:server/mcp-url-copied "MCP URL 已複製到剪貼簿!"
:server/start "啟動伺服器"
:server/stop "停止伺服器"
:server/title "HTTP API 伺服器"
@@ -1556,7 +1555,6 @@
:settings/account "帳戶"
:settings/advanced "進階功能"
:settings/ai "AI"
:settings/collaboration "協作"
:settings/editor "編輯器"
:settings/encryption "加密"
@@ -1577,8 +1575,6 @@
:settings.advanced/disable-sentry-desc "Logseq 永遠不會收集你的圖譜資料或出售你的數據。"
:settings.advanced/network-proxy "網路代理"
:settings.ai/enable-mcp-server "MCP 伺服器"
:settings.ai/enable-mcp-server-desc "啟用 MCP 伺服器,允許與本地 AI 應用程式通訊"
:settings.editor/auto-expand-block-refs "自動展開聚焦 block 的引用"
:settings.editor/auto-expand-block-refs-tip "此選項控制是否在頁面載入時展開區塊引用"

View File

@@ -0,0 +1,120 @@
(ns frontend.worker.mcp-server-test
"Exercises the daemon's /mcp endpoint (Fase 9) with a real MCP client SDK
connection over HTTP against a live daemon -- the same live-daemon
pattern db_worker_node_test.cljs and semantic_test.cljs use."
(:require ["@modelcontextprotocol/sdk/client/index.js" :refer [Client]]
["@modelcontextprotocol/sdk/client/streamableHttp.js" :refer [StreamableHTTPClientTransport]]
[cljs.test :refer [async deftest is]]
[frontend.test.node-helper :as node-helper]
[frontend.worker.db-worker-node :as db-worker-node]
[frontend.worker.semantic-routes :as semantic-routes]
[promesa.core :as p]))
;; Same literal credential db_worker_node_test.cljs and semantic_test.cljs
;; bootstrap with -- see semantic_test.cljs for why this must not be a
;; separate value (all three namespaces share one test.js process).
(def ^:private test-daemon-username "db-worker-node-test-user")
(def ^:private test-daemon-password "db-worker-node-test-password")
(aset (.-env js/process) "LOGSEQ_DAEMON_USERNAME" test-daemon-username)
(aset (.-env js/process) "LOGSEQ_DAEMON_PASSWORD" test-daemon-password)
(defn- start-daemon!
[opts]
(db-worker-node/start-daemon! (update opts :log-level #(or % "error"))))
(defn- <token!
[host port]
(p/let [^js res (js/fetch (str "http://" host ":" port "/v1/auth/login")
#js {:method "POST"
:headers #js {"Content-Type" "application/json"}
:body (js/JSON.stringify
#js {:username test-daemon-username
:password test-daemon-password
:deviceLabel "mcp-test"})})
^js body (.json res)]
(.-token body)))
(defn- <connected-client!
^js
[host port token]
(p/let [transport (StreamableHTTPClientTransport.
(js/URL. (str "http://" host ":" port "/mcp"))
#js {:requestInit #js {:headers #js {"Authorization" (str "Bearer " token)}}})
^js client (Client. #js {:name "semantic-mcp-test" :version "1.0.0"})
_ (.connect client transport)]
client))
(defn- tool-text
[^js call-result]
(.-text (aget (.-content call-result) 0)))
(defn- with-mcp-client!
"Starts a fresh daemon over a fresh repo, connects an MCP client to /mcp,
runs (f client), and stops the daemon afterwards regardless of outcome."
[f]
(let [daemon (atom nil)
data-dir (node-helper/create-tmp-dir "mcp-server")
repo (str "logseq_db_mcp_" (subs (str (random-uuid)) 0 8))]
(-> (p/let [{:keys [host port stop!]} (start-daemon! {:root-dir data-dir :repo repo})
_ (reset! daemon {:stop! stop!})
token (<token! host port)
client (<connected-client! host port token)]
(f client))
(p/catch (fn [e]
(println "[mcp-server-test] error:" e)
(is false (str e))))
(p/finally (fn []
(when-let [stop! (:stop! @daemon)]
(stop!)))))))
(deftest mcp-lists-all-semantic-tools
(async done
(-> (with-mcp-client!
(fn [^js client]
(p/let [^js result (.listTools client)]
(let [tools (js->clj (.-tools result) :keywordize-keys true)]
(is (= (count semantic-routes/operations) (count tools)))
(is (some #(= "createPage" (:name %)) tools))
(is (some #(= "searchGraph" (:name %)) tools))
(is (not (some #(= "listGraphs" (:name %)) tools)))))))
(p/finally done))))
(deftest mcp-create-and-get-page-round-trip
(async done
(-> (with-mcp-client!
(fn [^js client]
(p/let [^js create-result (.callTool client #js {:name "createPage"
:arguments #js {:title "MCP Test Page"}})
page (js->clj (js/JSON.parse (tool-text create-result)) :keywordize-keys true)
^js get-result (.callTool client #js {:name "getPage"
:arguments #js {:page-id (:uuid page)}})
got (js->clj (js/JSON.parse (tool-text get-result)) :keywordize-keys true)]
(is (not (.-isError create-result)))
(is (= "MCP Test Page" (:title page)))
(is (= "MCP Test Page" (:title got))))))
(p/finally done))))
(deftest mcp-capture-block-tree-round-trip
(async done
(-> (with-mcp-client!
(fn [^js client]
(p/let [^js result (.callTool
client #js {:name "captureToToday"
:arguments #js {:blocks #js [#js {:title "MCP root"
:children #js [#js {:title "MCP child"}]}]}})
body (js->clj (js/JSON.parse (tool-text result)) :keywordize-keys true)
root (first (:blocks body))]
(is (not (.-isError result)))
(is (= "MCP root" (:title root)))
(is (= "MCP child" (:title (first (:children root))))))))
(p/finally done))))
(deftest mcp-unknown-page-surfaces-as-error-content
(async done
(-> (with-mcp-client!
(fn [^js client]
(p/let [^js result (.callTool client #js {:name "getPage"
:arguments #js {:page-id (str (random-uuid))}})]
(is (true? (.-isError result))))))
(p/finally done))))