From f2f799b49bb30d4e37c0d5d5563f48588ff5b01d Mon Sep 17 00:00:00 2001 From: Gabriel Horner Date: Wed, 10 Sep 2025 15:03:24 -0400 Subject: [PATCH] enhance: mcp server works with local graph when given -g option --- .clj-kondo/config.edn | 1 + deps/cli/src/logseq/cli.cljs | 1 + .../src/logseq/cli/commands/mcp_server.cljs | 103 +++++++++++++----- deps/cli/src/logseq/cli/spec.cljs | 2 + 4 files changed, 80 insertions(+), 27 deletions(-) diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index a5210af9b0..3c419b7a49 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -219,6 +219,7 @@ logseq.outliner.op outliner-op logseq.outliner.page outliner-page logseq.outliner.pipeline outliner-pipeline + logseq.outliner.tree otree logseq.outliner.validate outliner-validate logseq.shui.popup.core shui-popup logseq.shui.ui shui diff --git a/deps/cli/src/logseq/cli.cljs b/deps/cli/src/logseq/cli.cljs index 79b99eb7dc..22a04c37af 100644 --- a/deps/cli/src/logseq/cli.cljs +++ b/deps/cli/src/logseq/cli.cljs @@ -100,6 +100,7 @@ :args->opts [:args] :require [:args] :coerce {:args []} :spec cli-spec/append} {:cmds ["mcp-server"] :desc "Run a MCP server" + :description "Run a MCP server against a local graph if --graph is given or against the current in-app graph." :fn (lazy-load-fn 'logseq.cli.commands.mcp-server/start) :spec cli-spec/mcp-server} {:cmds ["help"] :fn help-command :desc "Print a command's help" diff --git a/deps/cli/src/logseq/cli/commands/mcp_server.cljs b/deps/cli/src/logseq/cli/commands/mcp_server.cljs index e127c05682..b2df98fdf9 100644 --- a/deps/cli/src/logseq/cli/commands/mcp_server.cljs +++ b/deps/cli/src/logseq/cli/commands/mcp_server.cljs @@ -2,11 +2,28 @@ "Command to run a MCP server" (:require ["@modelcontextprotocol/sdk/server/mcp.js" :refer [McpServer]] ["@modelcontextprotocol/sdk/server/stdio.js" :refer [StdioServerTransport]] + ["fs" :as fs] ["zod/v3" :as z] + [datascript.core :as d] [logseq.cli.util :as cli-util] + [logseq.db :as ldb] + [logseq.db.common.initial-data :as common-initial-data] + [logseq.db.common.sqlite-cli :as sqlite-cli] + [logseq.db.frontend.entity-util :as entity-util] + [logseq.outliner.tree :as otree] [nbb.core :as nbb] [promesa.core :as p])) +(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))}]})) + (defn- unexpected-api-error [error] #js {:content #js [#js {:type "text" @@ -17,17 +34,13 @@ (-> (p/let [resp (cli-util/api-fetch api-server-token "logseq.editor.getAllPages" [])] (if (= 200 (.-status resp)) (p/let [body (.json resp) - pages (clj->js (map #(hash-map :title (.-title %) - :id (.-uuid %)) - body))] - (clj->js {:content - [{:type "text" - :text (js/JSON.stringify pages)}]})) - (cli-util/api-handle-error-response resp - (fn [msg] - #js {:content - #js [#js {:type "text" - :text msg}]})))) + pages (map #(hash-map :title (.-title %) + :createdAt (.-createdAt %) + :updatedAt (.-updatedAt %) + :id (.-uuid %)) + body)] + (mcp-success-response pages)) + (cli-util/api-handle-error-response resp mcp-error-response))) (p/catch unexpected-api-error))) (defn- api-get-page @@ -35,17 +48,12 @@ (-> (p/let [resp (cli-util/api-fetch api-server-token "logseq.Editor.getPageBlocksTree" [(aget args "pageName")])] (if (= 200 (.-status resp)) (p/let [body (.json resp)] - (clj->js {:content - [{:type "text" - :text (js/JSON.stringify body)}]})) - (cli-util/api-handle-error-response resp - (fn [msg] - #js {:content - #js [#js {:type "text" - :text msg}]})))) + (mcp-success-response body)) + (cli-util/api-handle-error-response resp mcp-error-response))) (p/catch unexpected-api-error))) (def ^:private api-tools + "MCP Tools when running with API server" {:getAllPages {:fn api-get-all-pages :config #js {:title "List Pages"}} @@ -55,15 +63,56 @@ :description "Get a page's content" :inputSchema #js {:pageName (z/string)}}}}) -(defn start [{{:keys [debug-tool] :as opts} :opts :as m}] +(defn- local-get-page [conn args] + (let [db @conn + page-id (common-initial-data/get-first-page-by-title db (aget args "pageName")) + blocks (when page-id (ldb/get-page-blocks db page-id))] + (if page-id + (->> (otree/blocks->vec-tree "logseq_db_repo_stub" db blocks page-id) + (map #(update % :block/uuid str)) + mcp-success-response) + (mcp-error-response (str "Error: Page " (pr-str (aget args "pageName")) " not found"))))) + +(defn- local-get-all-pages [conn _args] + (->> (d/datoms @conn :avet :block/name) + (map #(d/entity @conn (:e %))) + (remove entity-util/hidden?) + (mapv (fn [e] + {:id (str (:block/uuid e)) + :title (:block/title e) + :createdAt (:block/created-at e) + :updatedAt (:block/updated-at e)})) + mcp-success-response)) + +(def ^:private local-tools + "MCP Tools when running with a local graph" + (merge-with + merge + api-tools + {:getPage {:fn local-get-page} + :getAllPages {:fn local-get-all-pages}})) + +(defn start [{{:keys [debug-tool graph] :as opts} :opts :as m}] + (when (and graph (not (fs/existsSync (cli-util/get-graph-dir graph)))) + (cli-util/error "Graph" (pr-str graph) "does not exist")) (if debug-tool - (if-let [tool-m (get api-tools debug-tool)] - (p/let [resp ((:fn tool-m) m (clj->js (dissoc opts :debug-tool)))] - (js/console.log resp)) - (cli-util/error "Tool" (pr-str debug-tool) "not found")) + (if graph + (if-let [tool-m (get local-tools debug-tool)] + (let [conn (apply sqlite-cli/open-db! (cli-util/->open-db-args graph))] + (p/let [resp ((:fn tool-m) conn (clj->js (dissoc opts :debug-tool)))] + (js/console.log (clj->js resp)))) + (cli-util/error "Tool" (pr-str debug-tool) "not found")) + (if-let [tool-m (get api-tools debug-tool)] + (p/let [resp ((:fn tool-m) m (clj->js (dissoc opts :debug-tool)))] + (js/console.log resp)) + (cli-util/error "Tool" (pr-str debug-tool) "not found"))) (let [server (McpServer. #js {:name "Logseq MCP Server" :version "0.1.0"}) - transport (StdioServerTransport.)] - (doseq [[k v] api-tools] - (.registerTool server (name k) (:config v) (partial (:fn v) m))) + transport (StdioServerTransport.) + conn (when graph (apply sqlite-cli/open-db! (cli-util/->open-db-args graph)))] + (if graph + (doseq [[k v] local-tools] + (.registerTool server (name k) (:config v) (partial (:fn v) conn))) + (doseq [[k v] api-tools] + (.registerTool server (name k) (:config v) (partial (:fn v) m)))) (nbb/await (.connect server transport))))) \ No newline at end of file diff --git a/deps/cli/src/logseq/cli/spec.cljs b/deps/cli/src/logseq/cli/spec.cljs index f5c9b9b225..5eb0d6c09a 100644 --- a/deps/cli/src/logseq/cli/spec.cljs +++ b/deps/cli/src/logseq/cli/spec.cljs @@ -53,6 +53,8 @@ (def mcp-server {:api-server-token {:alias :a :desc "API server token to connect to current graph"} + :graph {:alias :g + :desc "Local graph to use with MCP server"} :debug-tool {:alias :t :coerce :keyword :desc "Debug mcp tool with direct invocation"}}) \ No newline at end of file