From 49aee371997a63505c50d88241c189b4957d24a1 Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 23 Sep 2025 13:46:39 +0200 Subject: [PATCH] feat: lazy load mcp server --- app/__test__/app/AppServer.spec.ts | 2 + app/__test__/app/mcp/mcp.auth.test.ts | 1 + app/__test__/app/mcp/mcp.base.test.ts | 5 ++ app/__test__/app/mcp/mcp.data.test.ts | 1 + app/__test__/app/mcp/mcp.media.test.ts | 1 + app/__test__/app/mcp/mcp.server.test.ts | 1 + app/__test__/app/mcp/mcp.system.test.ts | 1 + app/package.json | 2 +- app/src/App.ts | 6 +-- app/src/modules/mcp/system-mcp.ts | 2 +- app/src/modules/server/AppServer.ts | 6 ++- app/src/modules/server/SystemController.ts | 54 +++++++++++++--------- bun.lock | 6 +-- 13 files changed, 56 insertions(+), 32 deletions(-) diff --git a/app/__test__/app/AppServer.spec.ts b/app/__test__/app/AppServer.spec.ts index 46ad76e..1933787 100644 --- a/app/__test__/app/AppServer.spec.ts +++ b/app/__test__/app/AppServer.spec.ts @@ -16,6 +16,7 @@ describe("AppServer", () => { mcp: { enabled: false, path: "/api/system/mcp", + logLevel: "warning", }, }); } @@ -38,6 +39,7 @@ describe("AppServer", () => { mcp: { enabled: false, path: "/api/system/mcp", + logLevel: "warning", }, }); } diff --git a/app/__test__/app/mcp/mcp.auth.test.ts b/app/__test__/app/mcp/mcp.auth.test.ts index 4d20d40..e7658b4 100644 --- a/app/__test__/app/mcp/mcp.auth.test.ts +++ b/app/__test__/app/mcp/mcp.auth.test.ts @@ -44,6 +44,7 @@ describe("mcp auth", async () => { }, }); await app.build(); + await app.getMcpClient().ping(); server = app.mcp!; server.setLogLevel("error"); server.onNotification((message) => { diff --git a/app/__test__/app/mcp/mcp.base.test.ts b/app/__test__/app/mcp/mcp.base.test.ts index 40b77c4..48acd2e 100644 --- a/app/__test__/app/mcp/mcp.base.test.ts +++ b/app/__test__/app/mcp/mcp.base.test.ts @@ -34,6 +34,11 @@ describe("mcp", () => { }); await app.build(); + // expect mcp to not be loaded yet + expect(app.mcp).toBeNull(); + + // after first request, mcp should be loaded + await app.getMcpClient().listTools(); expect(app.mcp?.tools.length).toBeGreaterThan(0); }); }); diff --git a/app/__test__/app/mcp/mcp.data.test.ts b/app/__test__/app/mcp/mcp.data.test.ts index 4a962b3..43aecf5 100644 --- a/app/__test__/app/mcp/mcp.data.test.ts +++ b/app/__test__/app/mcp/mcp.data.test.ts @@ -50,6 +50,7 @@ describe("mcp data", async () => { }, }); await app.build(); + await app.getMcpClient().ping(); server = app.mcp!; server.setLogLevel("error"); server.onNotification((message) => { diff --git a/app/__test__/app/mcp/mcp.media.test.ts b/app/__test__/app/mcp/mcp.media.test.ts index ff47ddd..ad5383e 100644 --- a/app/__test__/app/mcp/mcp.media.test.ts +++ b/app/__test__/app/mcp/mcp.media.test.ts @@ -39,6 +39,7 @@ describe("mcp media", async () => { }, }); await app.build(); + await app.getMcpClient().ping(); server = app.mcp!; server.setLogLevel("error"); server.onNotification((message) => { diff --git a/app/__test__/app/mcp/mcp.server.test.ts b/app/__test__/app/mcp/mcp.server.test.ts index 1b64edb..ce072b6 100644 --- a/app/__test__/app/mcp/mcp.server.test.ts +++ b/app/__test__/app/mcp/mcp.server.test.ts @@ -24,6 +24,7 @@ describe("mcp system", async () => { }, }); await app.build(); + await app.getMcpClient().ping(); server = app.mcp!; }); diff --git a/app/__test__/app/mcp/mcp.system.test.ts b/app/__test__/app/mcp/mcp.system.test.ts index d20765f..de52198 100644 --- a/app/__test__/app/mcp/mcp.system.test.ts +++ b/app/__test__/app/mcp/mcp.system.test.ts @@ -23,6 +23,7 @@ describe("mcp system", async () => { }, }); await app.build(); + await app.getMcpClient().ping(); server = app.mcp!; }); diff --git a/app/package.json b/app/package.json index f087b57..fc36811 100644 --- a/app/package.json +++ b/app/package.json @@ -65,7 +65,7 @@ "hono": "4.8.3", "json-schema-library": "10.0.0-rc7", "json-schema-to-ts": "^3.1.1", - "jsonv-ts": "0.8.2", + "jsonv-ts": "0.8.4", "kysely": "0.27.6", "lodash-es": "^4.17.21", "oauth4webapi": "^2.11.1", diff --git a/app/src/App.ts b/app/src/App.ts index caf1e6a..020cebb 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -302,13 +302,13 @@ export class App< } getMcpClient() { - if (!this.mcp) { + const config = this.modules.get("server").config.mcp; + if (!config.enabled) { throw new Error("MCP is not enabled"); } - const mcpPath = this.modules.get("server").config.mcp.path; return new McpClient({ - url: "http://localhost" + mcpPath, + url: "http://localhost" + config.path, fetch: this.server.request, }); } diff --git a/app/src/modules/mcp/system-mcp.ts b/app/src/modules/mcp/system-mcp.ts index b1e7fa8..deabb4d 100644 --- a/app/src/modules/mcp/system-mcp.ts +++ b/app/src/modules/mcp/system-mcp.ts @@ -6,7 +6,7 @@ import { getVersion } from "core/env"; export function getSystemMcp(app: App) { const middlewareServer = getMcpServer(app.server); - const appConfig = app.modules.configs(); + //const appConfig = app.modules.configs(); const { version, ...appSchema } = app.getSchema(); const schema = s.strictObject(appSchema); const result = [...schema.walk({ maxDepth: 3 })]; diff --git a/app/src/modules/server/AppServer.ts b/app/src/modules/server/AppServer.ts index eeaab9f..b9fb531 100644 --- a/app/src/modules/server/AppServer.ts +++ b/app/src/modules/server/AppServer.ts @@ -1,6 +1,6 @@ import { Exception } from "core/errors"; import { isDebug } from "core/env"; -import { $console, s } from "bknd/utils"; +import { $console, mcpLogLevels, s } from "bknd/utils"; import { $object } from "modules/mcp"; import { cors } from "hono/cors"; import { Module } from "modules/Module"; @@ -25,6 +25,10 @@ export const serverConfigSchema = $object( mcp: s.strictObject({ enabled: s.boolean({ default: false }), path: s.string({ default: "/api/system/mcp" }), + logLevel: s.string({ + enum: mcpLogLevels, + default: "warning", + }), }), }, { diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts index 8db32e4..93533a2 100644 --- a/app/src/modules/server/SystemController.ts +++ b/app/src/modules/server/SystemController.ts @@ -70,33 +70,41 @@ export class SystemController extends Controller { this.registerMcp(); - this._mcpServer = getSystemMcp(app); - this._mcpServer.onNotification((message) => { - if (message.method === "notification/message") { - const consoleMap = { - emergency: "error", - alert: "error", - critical: "error", - error: "error", - warning: "warn", - notice: "log", - info: "info", - debug: "debug", - }; - - const level = consoleMap[message.params.level]; - if (!level) return; - - $console[level]("MCP notification", message.params.message ?? message.params); - } - }); - app.server.use( mcpMiddleware({ - server: this._mcpServer, + setup: async () => { + if (!this._mcpServer) { + this._mcpServer = getSystemMcp(app); + this._mcpServer.onNotification((message) => { + if (message.method === "notification/message") { + const consoleMap = { + emergency: "error", + alert: "error", + critical: "error", + error: "error", + warning: "warn", + notice: "log", + info: "info", + debug: "debug", + }; + + const level = consoleMap[message.params.level]; + if (!level) return; + + $console[level]( + "MCP notification", + message.params.message ?? message.params, + ); + } + }); + } + return { + server: this._mcpServer, + }; + }, sessionsEnabled: true, debug: { - logLevel: "debug", + logLevel: config.mcp.logLevel as any, explainEndpoint: true, }, endpoint: { diff --git a/bun.lock b/bun.lock index 96739b7..8b5995e 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,7 @@ }, "app": { "name": "bknd", - "version": "0.18.0-rc.4", + "version": "0.18.0-rc.6", "bin": "./dist/cli/index.js", "dependencies": { "@cfworker/json-schema": "^4.1.1", @@ -35,7 +35,7 @@ "hono": "4.8.3", "json-schema-library": "10.0.0-rc7", "json-schema-to-ts": "^3.1.1", - "jsonv-ts": "0.8.2", + "jsonv-ts": "0.8.4", "kysely": "0.27.6", "lodash-es": "^4.17.21", "oauth4webapi": "^2.11.1", @@ -2529,7 +2529,7 @@ "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], - "jsonv-ts": ["jsonv-ts@0.8.2", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-1Z7+maCfoGGqBPu5vN8rU9gIsW7OatYmn+STBTPkybbtNqeMzAoJDDrXHjsZ89x5dPH9W+OgMpNLtN0ouwiMYg=="], + "jsonv-ts": ["jsonv-ts@0.8.4", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-TZOyAVGBZxHuzk09NgJCx2dbeh0XqVWVKHU1PtIuvjT9XO7zhvAD02RcVisJoUdt2rJNt3zlyeNQ2b8MMPc+ug=="], "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],