Ver código fonte

enhance: Add optional mcp server to API server

Also switch CLI MCP server to fastify to reuse frontend code.
Also fix DELETE /mcp which was wrong
Gabriel Horner 1 mês atrás
pai
commit
a16c89b986

+ 1 - 0
deps/cli/package.json

@@ -13,6 +13,7 @@
     "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v28",
     "@modelcontextprotocol/sdk": "^1.17.5",
     "better-sqlite3": "~11.10.0",
+    "fastify": "5.3.2",
     "fs-extra": "^11.3.0",
     "jszip": "3.8.0",
     "mldoc": "^1.5.9",

+ 44 - 137
deps/cli/src/logseq/cli/commands/mcp_server.cljs

@@ -1,165 +1,77 @@
 (ns logseq.cli.commands.mcp-server
   "Command to run a MCP server"
-  (:require ["@modelcontextprotocol/sdk/server/mcp.js" :refer [McpServer]]
-            ["@modelcontextprotocol/sdk/server/stdio.js" :refer [StdioServerTransport]]
-            ["@modelcontextprotocol/sdk/server/streamableHttp.js" :refer [StreamableHTTPServerTransport]]
-            ["@modelcontextprotocol/sdk/types.js" :refer [isInitializeRequest]]
-            ["express$default" :as express]
+  (:require ["@modelcontextprotocol/sdk/server/stdio.js" :refer [StdioServerTransport]]
+            ["fastify$default" :as Fastify]
             ["fs" :as fs]
-            ["zod/v3" :as z]
+            [logseq.cli.common.mcp.server :as cli-common-mcp-server]
             [logseq.cli.common.mcp.tools :as cli-common-mcp-tools]
             [logseq.cli.util :as cli-util]
             [logseq.db.common.sqlite-cli :as sqlite-cli]
             [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"
-                 :text (str "Unexpected API error: " (.-message error))}]})
-
-(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"
-  {: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 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]
   (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"))))
+    (cli-common-mcp-server/mcp-success-response blocks)
+    (cli-common-mcp-server/mcp-error-response (str "Error: Page " (pr-str (aget args "pageName")) " not found"))))
 
 (defn- local-list-pages [conn _args]
-  (mcp-success-response (cli-common-mcp-tools/list-pages @conn)))
+  (cli-common-mcp-server/mcp-success-response (cli-common-mcp-tools/list-pages @conn)))
 
 (defn- local-list-properties [conn _args]
-  (mcp-success-response (cli-common-mcp-tools/list-properties @conn)))
+  (cli-common-mcp-server/mcp-success-response (cli-common-mcp-tools/list-properties @conn)))
 
 (defn- local-list-tags [conn _args]
-  (mcp-success-response (cli-common-mcp-tools/list-tags @conn)))
+  (cli-common-mcp-server/mcp-success-response (cli-common-mcp-tools/list-tags @conn)))
 
 (def ^:private local-tools
   "MCP Tools when running with a local graph"
   (merge-with
    merge
-   api-tools
+   cli-common-mcp-server/api-tools
    {:getPage {:fn local-get-page}
     :listPages {:fn local-list-pages}
     :listProperties {:fn local-list-properties}
     :listTags {:fn local-list-tags}}))
 
-(def ^:private transports
-  "Stores transports by session ID"
-  (atom {}))
-
-(defn- handle-post-session-request [mcp-server {:keys [port]} req res]
-  (let [session-id (aget (.-headers req) "mcp-session-id")]
-    (js/console.log "POST /mcp request" session-id)
-    (cond
-      (and session-id (@transports session-id))
-      (.handleRequest (@transports session-id) req res (.-body req))
-
-      (and (not session-id)
-           (isInitializeRequest (.-body req)))
-      (let [transport (StreamableHTTPServerTransport.
-                       #js {:sessionIdGenerator (comp str random-uuid)
-                            :enableDnsRebindingProtection true
-                            :allowedHosts #js [(str "localhost:" port)]})]
-        (set! (.-onclose transport)
-              (fn []
-                (js/console.log "Transport closed")
-                (when (.-sessionId transport)
-                  (swap! transports dissoc (.-sessionId transport)))))
-        (.connect mcp-server transport)
-        (.handleRequest transport req 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!")))
-
-      :else
-      (do
-        (.status res 400)
-        (.json res #js {:jsonrpc "2.0"
-                        :error #js {:code -32000
-                                    :message "Bad Request: No valid session ID provided"}
-                        :id nil})))))
-
-(defn- handle-session-request
-  "Reusable handler for GET and DELETE"
-  [req res]
-  (let [session-id (aget (.-headers req) "mcp-session-id")]
-    (js/console.log (.-method req) "/mcp" session-id)
-    (if-let [transport (and session-id (@transports session-id))]
-      (.handleRequest transport req res)
-      (-> res (.status 400) (.send res "Invalid or missing session ID")))))
-
-(defn- create-http-server [mcp-server opts]
-  (let [app (express)]
-    (.use app (.json express))
-    (.post app "/mcp" #(handle-post-session-request mcp-server opts %1 %2))
-    (.get app "/mcp" handle-session-request)
-    (.delete app "/mcp" handle-session-request)
+(defn- create-http-server
+  [mcp-server opts]
+  (let [app (Fastify. #js {:requestTimeout (* 1000 30)})]
+    (.post app "/mcp" #(cli-common-mcp-server/handle-post-request mcp-server opts %1 %2))
+    (.get app "/mcp" cli-common-mcp-server/handle-get-request)
+    (.delete app "/mcp" cli-common-mcp-server/handle-delete-request)
     app))
 
-(defn- start-http-server [mcp-server {:keys [port] :as opts}]
+(defn- start-http-server [mcp-server {:keys [port host] :as opts}]
   (let [app (create-http-server mcp-server opts)]
-    (.listen app port
+    (.listen app (clj->js (select-keys opts [:port :host]))
              (fn [error]
                (if error
                  (do (js/console.error "Failed to start server:" error)
                      (js/process.exit 1))
                  (js/console.log
-                  (str "MCP Streamable HTTP Server started on port " port)))))))
+                  (str "MCP Streamable HTTP Server started on " host ":" port)))))))
 
-(defn start [{{:keys [debug-tool graph stdio] :as opts} :opts :as m}]
+(defn- call-api
+  "Calls API from CLI for use w/ cli-common-mcp-server/api-tool"
+  [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))
+      (.json resp)
+      (p/let [body (.text resp)]
+        #js {:error (str "Server status " (.-status resp)
+                         "\nAPI Response: " (pr-str body))}))))
+
+(defn- create-mcp-server [{{:keys [api-server-token]} :opts} graph]
+  (if graph
+    (let [mcp-server (cli-common-mcp-server/create-mcp-server)
+          conn (apply sqlite-cli/open-db! (cli-util/->open-db-args graph))]
+      (doseq [[k v] local-tools]
+        (.registerTool mcp-server (name k) (:config v) (partial (:fn v) conn)))
+      mcp-server)
+    (cli-common-mcp-server/create-mcp-api-server (partial call-api api-server-token))))
+
+(defn start [{{:keys [debug-tool graph stdio api-server-token] :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
@@ -169,18 +81,13 @@
           (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)))]
+      (if-let [tool-m (get cli-common-mcp-server/api-tools debug-tool)]
+        (p/let [resp (cli-common-mcp-server/call-api-tool (:fn tool-m)
+                                                          (partial call-api api-server-token)
+                                                          (clj->js (dissoc opts :debug-tool)))]
           (js/console.log resp))
         (cli-util/error "Tool" (pr-str debug-tool) "not found")))
-    (let [mcp-server (McpServer. #js {:name "Logseq MCP Server"
-                                      :version "0.1.0"})]
-      (if graph
-        (let [conn (apply sqlite-cli/open-db! (cli-util/->open-db-args graph))]
-          (doseq [[k v] local-tools]
-            (.registerTool mcp-server (name k) (:config v) (partial (:fn v) conn))))
-        (doseq [[k v] api-tools]
-          (.registerTool mcp-server (name k) (:config v) (partial (:fn v) m))))
+    (let [mcp-server (create-mcp-server m graph)]
       (if stdio
         (nbb/await (.connect mcp-server (StdioServerTransport.)))
-        (start-http-server mcp-server (select-keys opts [:port]))))))
+        (start-http-server mcp-server (select-keys opts [:port :host]))))))

+ 144 - 0
deps/cli/src/logseq/cli/common/mcp/server.cljs

@@ -0,0 +1,144 @@
+(ns logseq.cli.common.mcp.server
+  "MCP server related fns shared between CLI and frontend"
+  (: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
+;; ===============
+(def ^:private transports
+  "Stores transports by session ID"
+  (atom {}))
+
+;; See https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http
+;; for how to respond to different MCP requests
+(defn handle-post-request [mcp-server {:keys [port host]} req res]
+  (let [session-id (aget (.-headers req) "mcp-session-id")]
+    (js/console.log "POST /mcp request" session-id (.-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)]})]
+        (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 (.status 400) (.send res "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 (.status 400) (.send res "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 (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" []))
+
+(defn- api-list-tags
+  [call-api-fn _args]
+  (call-api-fn "logseq.cli.listTags" []))
+
+(defn- api-list-properties
+  [call-api-fn _args]
+  (call-api-fn "logseq.cli.listProperties" []))
+
+(def api-tools
+  "MCP Tools when calling API server"
+  {: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 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 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))

+ 1 - 1
deps/cli/src/logseq/cli/common/mcp/tools.cljs

@@ -42,7 +42,7 @@
                 (: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)
+                (assoc :logseq.property.view/type (:db/ident (:logseq.property.view/type e)))
                 (:logseq.property/description e)
                 (update :logseq.property/description db-property/property-value-content))))))
 

+ 2 - 0
deps/cli/src/logseq/cli/spec.cljs

@@ -61,6 +61,8 @@
           :default 3000
           :coerce :long
           :desc "Port for streamable HTTP server"}
+   :host {:default "localhost"
+          :desc "Host for streamable HHP server"}
    :debug-tool {:alias :t
                 :coerce :keyword
                 :desc "Debug mcp tool with direct invocation"}})

+ 305 - 2
deps/cli/yarn.lock

@@ -2,6 +2,47 @@
 # yarn lockfile v1
 
 
+"@fastify/ajv-compiler@^4.0.0":
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz#da05938cf852901bfb953738764f553b5449b80b"
+  integrity sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==
+  dependencies:
+    ajv "^8.12.0"
+    ajv-formats "^3.0.1"
+    fast-uri "^3.0.0"
+
+"@fastify/error@^4.0.0":
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/@fastify/error/-/error-4.2.0.tgz#d40f46ba75f541fdcc4dc276b7308bbc8e8e6d7a"
+  integrity sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==
+
+"@fastify/fast-json-stringify-compiler@^5.0.0":
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz#fae495bf30dbbd029139839ec5c2ea111bde7d3f"
+  integrity sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==
+  dependencies:
+    fast-json-stringify "^6.0.0"
+
+"@fastify/forwarded@^3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@fastify/forwarded/-/forwarded-3.0.0.tgz#0fc96cdbbb5a38ad453d2d5533a34f09b4949b37"
+  integrity sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==
+
+"@fastify/merge-json-schemas@^0.2.0":
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz#3aa30d2f0c81a8ac5995b6d94ed4eaa2c3055824"
+  integrity sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==
+  dependencies:
+    dequal "^2.0.3"
+
+"@fastify/proxy-addr@^5.0.0":
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz#e9d1c7a49b8380d9f92a879fdc623ac47ee27de3"
+  integrity sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==
+  dependencies:
+    "@fastify/forwarded" "^3.0.0"
+    ipaddr.js "^2.1.0"
+
 "@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v28":
   version "1.2.173-feat-db-v28"
   resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/c0410ff81a8b0d510705581cccd39788d862dc91"
@@ -26,6 +67,11 @@
     zod "^3.23.8"
     zod-to-json-schema "^3.24.1"
 
+abstract-logging@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839"
+  integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==
+
 accepts@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895"
@@ -34,6 +80,13 @@ accepts@^2.0.0:
     mime-types "^3.0.0"
     negotiator "^1.0.0"
 
+ajv-formats@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578"
+  integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==
+  dependencies:
+    ajv "^8.0.0"
+
 ajv@^6.12.6:
   version "6.12.6"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@@ -44,6 +97,16 @@ ajv@^6.12.6:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
+ajv@^8.0.0, ajv@^8.12.0:
+  version "8.17.1"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
+  integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
+  dependencies:
+    fast-deep-equal "^3.1.3"
+    fast-uri "^3.0.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+
 ansi-regex@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@@ -54,6 +117,19 @@ ansi-regex@^3.0.0:
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1"
   integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==
 
+atomic-sleep@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
+  integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
+
+avvio@^9.0.0:
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/avvio/-/avvio-9.1.0.tgz#0ff80ed211682441d8aa39ff21a4b9d022109c44"
+  integrity sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==
+  dependencies:
+    "@fastify/error" "^4.0.0"
+    fastq "^1.17.1"
+
 base64-js@^1.3.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -173,6 +249,11 @@ cookie@^0.7.1:
   resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
   integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
 
+cookie@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610"
+  integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==
+
 core-util-is@~1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
@@ -235,6 +316,11 @@ [email protected], depd@^2.0.0:
   resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
   integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
 
+dequal@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
+  integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
+
 detect-libc@^2.0.0:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8"
@@ -361,7 +447,12 @@ express@^5.0.1:
     type-is "^2.0.1"
     vary "^1.1.2"
 
-fast-deep-equal@^3.1.1:
+fast-decode-uri-component@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543"
+  integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==
+
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
@@ -371,6 +462,63 @@ fast-json-stable-stringify@^2.0.0:
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
+fast-json-stringify@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-6.0.1.tgz#82f1cb45fa96d0ca24b601f1738066976d6e2430"
+  integrity sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==
+  dependencies:
+    "@fastify/merge-json-schemas" "^0.2.0"
+    ajv "^8.12.0"
+    ajv-formats "^3.0.1"
+    fast-uri "^3.0.0"
+    json-schema-ref-resolver "^2.0.0"
+    rfdc "^1.2.0"
+
+fast-querystring@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/fast-querystring/-/fast-querystring-1.1.2.tgz#a6d24937b4fc6f791b4ee31dcb6f53aeafb89f53"
+  integrity sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==
+  dependencies:
+    fast-decode-uri-component "^1.0.1"
+
+fast-redact@^3.1.1:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4"
+  integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==
+
+fast-uri@^3.0.0, fast-uri@^3.0.1:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa"
+  integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
+
[email protected]:
+  version "5.3.2"
+  resolved "https://registry.yarnpkg.com/fastify/-/fastify-5.3.2.tgz#88c895a30c0f67166979077ac8649fe8b205a1b3"
+  integrity sha512-AIPqBgtqBAwkOkrnwesEE+dOyU30dQ4kh7udxeGVR05CRGwubZx+p2H8P0C4cRnQT0+EPK4VGea2DTL2RtWttg==
+  dependencies:
+    "@fastify/ajv-compiler" "^4.0.0"
+    "@fastify/error" "^4.0.0"
+    "@fastify/fast-json-stringify-compiler" "^5.0.0"
+    "@fastify/proxy-addr" "^5.0.0"
+    abstract-logging "^2.0.1"
+    avvio "^9.0.0"
+    fast-json-stringify "^6.0.0"
+    find-my-way "^9.0.0"
+    light-my-request "^6.0.0"
+    pino "^9.0.0"
+    process-warning "^5.0.0"
+    rfdc "^1.3.1"
+    secure-json-parse "^4.0.0"
+    semver "^7.6.0"
+    toad-cache "^3.7.0"
+
+fastq@^1.17.1:
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5"
+  integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
+  dependencies:
+    reusify "^1.0.4"
+
 [email protected]:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@@ -388,6 +536,15 @@ finalhandler@^2.1.0:
     parseurl "^1.3.3"
     statuses "^2.0.1"
 
+find-my-way@^9.0.0:
+  version "9.3.0"
+  resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-9.3.0.tgz#9f57786b5d772cc45142bf39dd5349f9cc883f91"
+  integrity sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==
+  dependencies:
+    fast-deep-equal "^3.1.3"
+    fast-querystring "^1.0.0"
+    safe-regex2 "^5.0.0"
+
 find-up@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
@@ -547,6 +704,11 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
   integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
 
+ipaddr.js@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8"
+  integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==
+
 is-fullwidth-code-point@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
@@ -579,11 +741,23 @@ isexe@^2.0.0:
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
   integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
 
+json-schema-ref-resolver@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz#c92f16b452df069daac53e1984159e0f9af0598d"
+  integrity sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==
+  dependencies:
+    dequal "^2.0.3"
+
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
   integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
 
+json-schema-traverse@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+  integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
 jsonfile@^6.0.1:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@@ -617,6 +791,15 @@ lie@~3.3.0:
   dependencies:
     immediate "~3.0.5"
 
+light-my-request@^6.0.0:
+  version "6.6.0"
+  resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-6.6.0.tgz#c9448772323f65f33720fb5979c7841f14060add"
+  integrity sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==
+  dependencies:
+    cookie "^1.0.1"
+    process-warning "^4.0.0"
+    set-cookie-parser "^2.6.0"
+
 locate-path@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
@@ -744,6 +927,11 @@ object-inspect@^1.13.3:
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
   integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
 
+on-exit-leak-free@^2.1.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8"
+  integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==
+
 on-finished@^2.4.1:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
@@ -831,6 +1019,35 @@ path-to-regexp@^8.0.0:
   resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f"
   integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==
 
+pino-abstract-transport@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60"
+  integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==
+  dependencies:
+    split2 "^4.0.0"
+
+pino-std-serializers@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b"
+  integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==
+
+pino@^9.0.0:
+  version "9.9.5"
+  resolved "https://registry.yarnpkg.com/pino/-/pino-9.9.5.tgz#f06a5a0b4c715e34606290070dbb938c27eddd8b"
+  integrity sha512-d1s98p8/4TfYhsJ09r/Azt30aYELRi6NNnZtEbqFw6BoGsdPVf5lKNK3kUwH8BmJJfpTLNuicjUQjaMbd93dVg==
+  dependencies:
+    atomic-sleep "^1.0.0"
+    fast-redact "^3.1.1"
+    on-exit-leak-free "^2.1.0"
+    pino-abstract-transport "^2.0.0"
+    pino-std-serializers "^7.0.0"
+    process-warning "^5.0.0"
+    quick-format-unescaped "^4.0.3"
+    real-require "^0.2.0"
+    safe-stable-stringify "^2.3.1"
+    sonic-boom "^4.0.1"
+    thread-stream "^3.0.0"
+
 pkce-challenge@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.0.tgz#c3a405cb49e272094a38e890a2b51da0228c4d97"
@@ -859,6 +1076,16 @@ process-nextick-args@~2.0.0:
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
   integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
 
+process-warning@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.1.tgz#5c1db66007c67c756e4e09eb170cdece15da32fb"
+  integrity sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==
+
+process-warning@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7"
+  integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==
+
 proxy-addr@^2.0.7:
   version "2.0.7"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@@ -887,6 +1114,11 @@ qs@^6.14.0:
   dependencies:
     side-channel "^1.1.0"
 
+quick-format-unescaped@^4.0.3:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
+  integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
+
 range-parser@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@@ -934,16 +1166,41 @@ readable-stream@~2.3.6:
     string_decoder "~1.1.1"
     util-deprecate "~1.0.1"
 
+real-require@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
+  integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
+
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
   integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
 
+require-from-string@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+  integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
 require-main-filename@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
   integrity sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==
 
+ret@~0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/ret/-/ret-0.5.0.tgz#30a4d38a7e704bd96dc5ffcbe7ce2a9274c41c95"
+  integrity sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==
+
+reusify@^1.0.4:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"
+  integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
+
+rfdc@^1.2.0, rfdc@^1.3.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
+  integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
+
 router@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef"
@@ -965,17 +1222,34 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
   integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
 
+safe-regex2@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-5.0.0.tgz#762e4a4c328603427281d2b99662f2d04e4ae811"
+  integrity sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==
+  dependencies:
+    ret "~0.5.0"
+
+safe-stable-stringify@^2.3.1:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd"
+  integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==
+
 "safer-buffer@>= 2.1.2 < 3.0.0":
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
+secure-json-parse@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-4.0.0.tgz#2ee1b7581be38ab348bab5a3e49280ba80a89c85"
+  integrity sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==
+
 semver@^5.5.0:
   version "5.7.2"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
   integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
 
-semver@^7.3.5:
+semver@^7.3.5, semver@^7.6.0:
   version "7.7.2"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
   integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
@@ -1012,6 +1286,11 @@ set-blocking@^2.0.0:
   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
   integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
 
+set-cookie-parser@^2.6.0:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943"
+  integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==
+
 set-immediate-shim@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
@@ -1105,6 +1384,18 @@ simple-get@^4.0.0:
     once "^1.3.1"
     simple-concat "^1.0.0"
 
+sonic-boom@^4.0.1:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d"
+  integrity sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==
+  dependencies:
+    atomic-sleep "^1.0.0"
+
+split2@^4.0.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
+  integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
+
 [email protected]:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
@@ -1191,6 +1482,18 @@ tar-stream@^2.1.4:
     inherits "^2.0.3"
     readable-stream "^3.1.1"
 
+thread-stream@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1"
+  integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==
+  dependencies:
+    real-require "^0.2.0"
+
+toad-cache@^3.7.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/toad-cache/-/toad-cache-3.7.0.tgz#b9b63304ea7c45ec34d91f1d2fa513517025c441"
+  integrity sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==
+
 [email protected]:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"

+ 6 - 4
resources/package.json

@@ -24,6 +24,7 @@
   "dependencies": {
     "@fastify/cors": "11.0.1",
     "@logseq/rsapi": "0.0.92",
+    "@modelcontextprotocol/sdk": "^1.17.5",
     "@sentry/electron": "2.5.1",
     "abort-controller": "3.0.0",
     "chokidar": "^3.5.1",
@@ -44,20 +45,21 @@
     "posthog-js": "1.10.2",
     "semver": "7.5.2",
     "socks-proxy-agent": "8.0.2",
-    "update-electron-app": "2.0.1"
+    "update-electron-app": "2.0.1",
+    "zod": "^4.1.5"
   },
   "devDependencies": {
     "@electron-forge/cli": "^7.8.3",
     "@electron-forge/maker-deb": "^7.8.3",
     "@electron-forge/maker-dmg": "^7.8.3",
-    "@electron-forge/maker-wix": "^7.8.3",
     "@electron-forge/maker-rpm": "^7.8.3",
     "@electron-forge/maker-squirrel": "^7.8.3",
+    "@electron-forge/maker-wix": "^7.8.3",
     "@electron-forge/maker-zip": "^7.8.3",
     "@electron/rebuild": "4.0.1",
     "electron": "37.2.6",
     "electron-builder": "26.0.12",
-    "electron-forge-maker-appimage": "https://github.com/logseq/electron-forge-maker-appimage.git",
-    "electron-devtools-installer": "4.0.0"
+    "electron-devtools-installer": "4.0.0",
+    "electron-forge-maker-appimage": "https://github.com/logseq/electron-forge-maker-appimage.git"
   }
 }

+ 19 - 2
src/electron/electron/server.cljs

@@ -11,6 +11,7 @@
             [electron.logger :as logger]
             [electron.utils :as utils]
             [electron.window :as window]
+            [logseq.cli.common.mcp.server :as cli-common-mcp-server]
             [promesa.core :as p]))
 
 (defonce ^:private *win (atom nil))
@@ -29,7 +30,8 @@
                   :host      (get-host)
                   :port      (get-port)
                   :tokens    (cfgs/get-item :server/tokens)
-                  :autostart (cfgs/get-item :server/autostart)}))
+                  :autostart (cfgs/get-item :server/autostart)
+                  :mcp-enabled? true}))
 
 (defn- set-status!
   ([status] (set-status! status nil))
@@ -130,6 +132,19 @@
         (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))}))
+        mcp-server (cli-common-mcp-server/create-mcp-api-server api-fn)]
+    (logger/debug "[server] MCP routes initialized")
+    (.post server "/mcp"
+           #(cli-common-mcp-server/handle-post-request mcp-server {:port (get-port)
+                                                                           :host (get-host)} %1 %2))
+    (.get server "/mcp" cli-common-mcp-server/handle-get-request)
+    (.delete server "/mcp" cli-common-mcp-server/handle-get-request)))
+
 (defn start!
   []
   (-> (p/let [_     (close!)
@@ -151,7 +166,9 @@
                                                  (string/replace-first "${HOST}" HOST)
                                                  (string/replace-first "${PORT}" PORT))]
                                     (doto rep (.type "text/html")
-                                              (.send 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)