Bläddra i källkod

enhance: mcp server works with local graph

when given -g option
Gabriel Horner 1 månad sedan
förälder
incheckning
f2f799b49b

+ 1 - 0
.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

+ 1 - 0
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"

+ 76 - 27
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)))))

+ 2 - 0
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"}})