enhance: mcp server works with local graph

when given -g option
This commit is contained in:
Gabriel Horner
2025-09-10 15:03:24 -04:00
parent 96704e9998
commit f2f799b49b
4 changed files with 80 additions and 27 deletions

View File

@@ -219,6 +219,7 @@
logseq.outliner.op outliner-op logseq.outliner.op outliner-op
logseq.outliner.page outliner-page logseq.outliner.page outliner-page
logseq.outliner.pipeline outliner-pipeline logseq.outliner.pipeline outliner-pipeline
logseq.outliner.tree otree
logseq.outliner.validate outliner-validate logseq.outliner.validate outliner-validate
logseq.shui.popup.core shui-popup logseq.shui.popup.core shui-popup
logseq.shui.ui shui logseq.shui.ui shui

View File

@@ -100,6 +100,7 @@
:args->opts [:args] :require [:args] :coerce {:args []} :args->opts [:args] :require [:args] :coerce {:args []}
:spec cli-spec/append} :spec cli-spec/append}
{:cmds ["mcp-server"] :desc "Run a MCP server" {: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) :fn (lazy-load-fn 'logseq.cli.commands.mcp-server/start)
:spec cli-spec/mcp-server} :spec cli-spec/mcp-server}
{:cmds ["help"] :fn help-command :desc "Print a command's help" {:cmds ["help"] :fn help-command :desc "Print a command's help"

View File

@@ -2,11 +2,28 @@
"Command to run a MCP server" "Command to run a MCP server"
(:require ["@modelcontextprotocol/sdk/server/mcp.js" :refer [McpServer]] (:require ["@modelcontextprotocol/sdk/server/mcp.js" :refer [McpServer]]
["@modelcontextprotocol/sdk/server/stdio.js" :refer [StdioServerTransport]] ["@modelcontextprotocol/sdk/server/stdio.js" :refer [StdioServerTransport]]
["fs" :as fs]
["zod/v3" :as z] ["zod/v3" :as z]
[datascript.core :as d]
[logseq.cli.util :as cli-util] [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] [nbb.core :as nbb]
[promesa.core :as p])) [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] (defn- unexpected-api-error [error]
#js {:content #js {:content
#js [#js {:type "text" #js [#js {:type "text"
@@ -17,17 +34,13 @@
(-> (p/let [resp (cli-util/api-fetch api-server-token "logseq.editor.getAllPages" [])] (-> (p/let [resp (cli-util/api-fetch api-server-token "logseq.editor.getAllPages" [])]
(if (= 200 (.-status resp)) (if (= 200 (.-status resp))
(p/let [body (.json resp) (p/let [body (.json resp)
pages (clj->js (map #(hash-map :title (.-title %) pages (map #(hash-map :title (.-title %)
:id (.-uuid %)) :createdAt (.-createdAt %)
body))] :updatedAt (.-updatedAt %)
(clj->js {:content :id (.-uuid %))
[{:type "text" body)]
:text (js/JSON.stringify pages)}]})) (mcp-success-response pages))
(cli-util/api-handle-error-response resp (cli-util/api-handle-error-response resp mcp-error-response)))
(fn [msg]
#js {:content
#js [#js {:type "text"
:text msg}]}))))
(p/catch unexpected-api-error))) (p/catch unexpected-api-error)))
(defn- api-get-page (defn- api-get-page
@@ -35,17 +48,12 @@
(-> (p/let [resp (cli-util/api-fetch api-server-token "logseq.Editor.getPageBlocksTree" [(aget args "pageName")])] (-> (p/let [resp (cli-util/api-fetch api-server-token "logseq.Editor.getPageBlocksTree" [(aget args "pageName")])]
(if (= 200 (.-status resp)) (if (= 200 (.-status resp))
(p/let [body (.json resp)] (p/let [body (.json resp)]
(clj->js {:content (mcp-success-response body))
[{:type "text" (cli-util/api-handle-error-response resp mcp-error-response)))
:text (js/JSON.stringify body)}]}))
(cli-util/api-handle-error-response resp
(fn [msg]
#js {:content
#js [#js {:type "text"
:text msg}]}))))
(p/catch unexpected-api-error))) (p/catch unexpected-api-error)))
(def ^:private api-tools (def ^:private api-tools
"MCP Tools when running with API server"
{:getAllPages {:getAllPages
{:fn api-get-all-pages {:fn api-get-all-pages
:config #js {:title "List Pages"}} :config #js {:title "List Pages"}}
@@ -55,15 +63,56 @@
:description "Get a page's content" :description "Get a page's content"
:inputSchema #js {:pageName (z/string)}}}}) :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 debug-tool
(if-let [tool-m (get api-tools debug-tool)] (if graph
(p/let [resp ((:fn tool-m) m (clj->js (dissoc opts :debug-tool)))] (if-let [tool-m (get local-tools debug-tool)]
(js/console.log resp)) (let [conn (apply sqlite-cli/open-db! (cli-util/->open-db-args graph))]
(cli-util/error "Tool" (pr-str debug-tool) "not found")) (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" (let [server (McpServer. #js {:name "Logseq MCP Server"
:version "0.1.0"}) :version "0.1.0"})
transport (StdioServerTransport.)] transport (StdioServerTransport.)
(doseq [[k v] api-tools] conn (when graph (apply sqlite-cli/open-db! (cli-util/->open-db-args graph)))]
(.registerTool server (name k) (:config v) (partial (:fn v) m))) (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))))) (nbb/await (.connect server transport)))))

View File

@@ -53,6 +53,8 @@
(def mcp-server (def mcp-server
{:api-server-token {:alias :a {:api-server-token {:alias :a
:desc "API server token to connect to current graph"} :desc "API server token to connect to current graph"}
:graph {:alias :g
:desc "Local graph to use with MCP server"}
:debug-tool {:alias :t :debug-tool {:alias :t
:coerce :keyword :coerce :keyword
:desc "Debug mcp tool with direct invocation"}}) :desc "Debug mcp tool with direct invocation"}})