enhance: add listTags and listProperties to API mcp server

by introducing logseq.cli.* for internal API usage.
Also refactor existing api tools to share same implementation as
local tools. This fixes a couple bugs w/ the api tools as get-page
was returning :block.temp/* keys and list-pages were returning uuids under
the id key unlike all other responses
This commit is contained in:
Gabriel Horner
2025-09-12 11:47:52 -04:00
parent 179f08cf90
commit 70a6f617aa
7 changed files with 137 additions and 85 deletions

View File

@@ -165,10 +165,11 @@
frontend.worker.util worker-util
lambdaisland.glogi log
logseq.cli.common.graph cli-common-graph
logseq.cli.text-util cli-text-util
logseq.cli.common.export.common cli-export-common
logseq.cli.common.export.text cli-export-text
logseq.cli.common.file common-file
logseq.cli.common.mcp.tools cli-common-mcp-tools
logseq.cli.text-util cli-text-util
logseq.common.config common-config
logseq.common.date-time-util date-time-util
logseq.common.graph common-graph

View File

@@ -6,3 +6,4 @@ logseq.cli.commands.query/query
logseq.cli.commands.search/search
logseq.cli.commands.export/export
logseq.cli.commands.append/append
logseq.cli.commands.mcp-server/start

View File

@@ -12,6 +12,7 @@
clojure.string string
datascript.core d
logseq.cli.commands.graph cli-graph
logseq.cli.common.mcp.tools cli-common-mcp-tools
logseq.cli.common.graph cli-common-graph
logseq.cli.common.export.text cli-export-text
logseq.cli.common.export.common cli-export-common

View File

@@ -4,14 +4,9 @@
["@modelcontextprotocol/sdk/server/stdio.js" :refer [StdioServerTransport]]
["fs" :as fs]
["zod/v3" :as z]
[datascript.core :as d]
[logseq.cli.common.mcp.tools :as cli-common-mcp-tools]
[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.db.frontend.property :as db-property]
[logseq.outliner.tree :as otree]
[nbb.core :as nbb]
[promesa.core :as p]))
@@ -30,97 +25,62 @@
#js [#js {:type "text"
:text (str "Unexpected API error: " (.-message error))}]})
(defn- api-get-all-pages
[{{:keys [api-server-token]} :opts} _args]
(-> (p/let [resp (cli-util/api-fetch api-server-token "logseq.editor.getAllPages" [])]
(if (= 200 (.-status resp))
(p/let [body (.json resp)
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
[{{:keys [api-server-token]} :opts} args]
(-> (p/let [resp (cli-util/api-fetch api-server-token "logseq.Editor.getPageBlocksTree" [(aget args "pageName")])]
(defn- api-tool
"Calls API method w/ args and returns a MCP response"
[api-server-token api-method method-args]
(-> (p/let [resp (cli-util/api-fetch api-server-token api-method method-args)]
(if (= 200 (.-status resp))
(p/let [body (.json resp)]
(mcp-success-response body))
(cli-util/api-handle-error-response resp mcp-error-response)))
(p/catch unexpected-api-error)))
(defn- api-get-page
[{{:keys [api-server-token]} :opts} args]
(api-tool api-server-token "logseq.cli.getPageData" [(aget args "pageName")]))
(defn- api-list-pages
[{{:keys [api-server-token]} :opts} _args]
(api-tool api-server-token "logseq.cli.listPages" []))
(defn- api-list-tags
[{{:keys [api-server-token]} :opts} _args]
(api-tool api-server-token "logseq.cli.listTags" []))
(defn- api-list-properties
[{{:keys [api-server-token]} :opts} _args]
(api-tool api-server-token "logseq.cli.listProperties" []))
(def ^:private api-tools
"MCP Tools when running with API server"
{:getAllPages
{:fn api-get-all-pages
{:listPages
{:fn api-list-pages
:config #js {:title "List Pages"}}
:getPage
{:fn api-get-page
:config #js {:title "Get Page"
:description "Get a page's content"
:inputSchema #js {:pageName (z/string)}}}})
:description "Get a page's content including its blocks"
:inputSchema #js {:pageName (z/string)}}}
:listTags
{:fn api-list-tags
:config #js {:title "List Tags"}}
:listProperties
{:fn api-list-properties
:config #js {:title "List Properties"}}})
(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")))))
(if-let [blocks (cli-common-mcp-tools/get-page-blocks @conn (aget args "pageName"))]
(mcp-success-response blocks)
(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))
(defn- local-list-pages [conn _args]
(mcp-success-response (cli-common-mcp-tools/list-pages @conn)))
(defn- local-list-properties [conn _args]
(->> (d/datoms @conn :avet :block/tags :logseq.class/Property)
(map #(d/entity @conn (:e %)))
#_((fn [x] (prn :prop-keys (distinct (mapcat keys x))) x))
(map (fn [e]
(cond-> (into {} e)
true
(dissoc e :block/tags :block/order :block/refs :block/name :db/index
:logseq.property.embedding/hnsw-label-updated-at :logseq.property/default-value)
true
(update :block/uuid str)
(:logseq.property/classes e)
(update :logseq.property/classes #(mapv :db/ident %))
(:logseq.property/description e)
(update :logseq.property/description db-property/property-value-content))))
mcp-success-response))
(mcp-success-response (cli-common-mcp-tools/list-properties @conn)))
(defn- local-list-tags [conn _args]
(->> (d/datoms @conn :avet :block/tags :logseq.class/Tag)
(map #(d/entity @conn (:e %)))
(map (fn [e]
(cond-> (into {} e)
true
(dissoc e :block/tags :block/order :block/refs :block/name
:logseq.property.embedding/hnsw-label-updated-at)
true
(update :block/uuid str)
(:logseq.property.class/extends e)
(update :logseq.property.class/extends #(mapv :db/ident %))
(:logseq.property.class/properties e)
(update :logseq.property.class/properties #(mapv :db/ident %))
(:logseq.property.view/type e)
(update :logseq.property.view/type :db/ident)
(:logseq.property/description e)
(update :logseq.property/description db-property/property-value-content))))
mcp-success-response))
(mcp-success-response (cli-common-mcp-tools/list-tags @conn)))
(def ^:private local-tools
"MCP Tools when running with a local graph"
@@ -128,11 +88,9 @@
merge
api-tools
{:getPage {:fn local-get-page}
:getAllPages {:fn local-get-all-pages}
:listProperties {:fn local-list-properties
:config #js {:title "List Properties"}}
:listTags {:fn local-list-tags
:config #js {:title "List Tags"}}}))
:listPages {:fn local-list-pages}
:listProperties {:fn local-list-properties}
:listTags {:fn local-list-tags}}))
(defn start [{{:keys [debug-tool graph] :as opts} :opts :as m}]
(when (and graph (not (fs/existsSync (cli-util/get-graph-dir graph))))

View File

@@ -0,0 +1,68 @@
(ns logseq.cli.common.mcp.tools
"MCP tool related fns shared between CLI and frontend"
(:require [datascript.core :as d]
[logseq.db :as ldb]
[logseq.db.frontend.entity-util :as entity-util]
[logseq.db.common.initial-data :as common-initial-data]
[logseq.db.frontend.property :as db-property]
[logseq.outliner.tree :as otree]))
(defn list-properties
"Main fn for ListProperties tool"
[db]
(->> (d/datoms db :avet :block/tags :logseq.class/Property)
(map #(d/entity db (:e %)))
#_((fn [x] (prn :prop-keys (distinct (mapcat keys x))) x))
(map (fn [e]
(cond-> (into {} e)
true
(dissoc e :block/tags :block/order :block/refs :block/name :db/index
:logseq.property.embedding/hnsw-label-updated-at :logseq.property/default-value)
true
(update :block/uuid str)
(:logseq.property/classes e)
(update :logseq.property/classes #(mapv :db/ident %))
(:logseq.property/description e)
(update :logseq.property/description db-property/property-value-content))))))
(defn list-tags
"Main fn for ListTags tool"
[db]
(->> (d/datoms db :avet :block/tags :logseq.class/Tag)
(map #(d/entity db (:e %)))
(map (fn [e]
(cond-> (into {} e)
true
(dissoc e :block/tags :block/order :block/refs :block/name
:logseq.property.embedding/hnsw-label-updated-at)
true
(update :block/uuid str)
(:logseq.property.class/extends e)
(update :logseq.property.class/extends #(mapv :db/ident %))
(:logseq.property.class/properties e)
(update :logseq.property.class/properties #(mapv :db/ident %))
(:logseq.property.view/type e)
(update :logseq.property.view/type :db/ident)
(:logseq.property/description e)
(update :logseq.property/description db-property/property-value-content))))))
(defn get-page-blocks
"Get page blocks for GetPage tool"
[db page-title]
(when-let [page-id (common-initial-data/get-first-page-by-title db page-title)]
(let [blocks (ldb/get-page-blocks db page-id)]
;; Use repo stub since this is a DB only tool
(->> (otree/blocks->vec-tree "logseq_db_repo_stub" db blocks page-id)
(map #(update % :block/uuid str))))))
(defn list-pages
"Main fn for ListPages tool"
[db]
(->> (d/datoms db :avet :block/name)
(map #(d/entity db (:e %)))
(remove entity-util/hidden?)
(map #(-> %
;; Until there are options to limit pages, return minimal info to avoid
;; exceeding max payload size
(select-keys [:block/uuid :block/title :block/created-at :block/updated-at])
(update :block/uuid str)))))

View File

@@ -130,7 +130,7 @@
method' (last ns-method)
args (.-args data)
ret-fn! #(ipc/invoke (str :electron.server/sync! sync-id) %)
app? (contains? #{"app" "editor" "db"} ns')
app? (contains? #{"app" "editor" "db" "cli"} ns')
^js sdk1 (aget js/window.logseq "api")
^js sdk2 (aget js/window.logseq "sdk")]

View File

@@ -48,6 +48,7 @@
[goog.object :as gobj]
[lambdaisland.glogi :as log]
[logseq.api.block :as api-block]
[logseq.cli.common.mcp.tools :as cli-common-mcp-tools]
[logseq.common.util :as common-util]
[logseq.common.util.date-time :as date-time-util]
[logseq.db :as ldb]
@@ -1156,6 +1157,28 @@
(when-let [args (and args (seq (bean/->clj args)))]
(shell/run-git-command! args)))
;; Internal CLI API
;; TODO: Use transit for internal APIs
(defn ^:export list_tags
[]
(clj->js (cli-common-mcp-tools/list-tags (db/get-db))))
(defn ^:export list_properties
[]
(clj->js (cli-common-mcp-tools/list-properties (db/get-db))))
(defn ^:export get_page_data
"Like get_page_blocks_tree but for MCP tools"
[page-title]
(when-let [tools (cli-common-mcp-tools/get-page-blocks (db/get-db) page-title)]
(->> tools
(map #(dissoc % :block.temp/has-children? :block.temp/load-status))
clj->js)))
(defn ^:export list_pages
[]
(clj->js (cli-common-mcp-tools/list-pages (db/get-db))))
;; ui
(def ^:export show_msg sdk-ui/-show_msg)
(def ^:export query_element_rect sdk-ui/query_element_rect)