From 2e145bbf95e573a3ea1e3a464c83e732cd6d692f Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 12 Aug 2025 08:51:32 +0200 Subject: [PATCH] init mcp tools test --- app/__test__/app/mcp.spec.ts | 42 ---------------- app/__test__/app/mcp/mcp.auth.test.ts | 41 ++++++++++++++++ app/__test__/app/mcp/mcp.base.test.ts | 32 +++++++++++++ app/__test__/app/mcp/mcp.data.test.ts | 48 +++++++++++++++++++ app/__test__/app/mcp/mcp.media.test.ts | 39 +++++++++++++++ app/__test__/app/mcp/mcp.system.test.ts | 38 +++++++++++++++ app/bunfig.toml | 3 +- app/package.json | 2 +- app/src/cli/commands/mcp/mcp.ts | 2 +- app/src/core/test/utils.ts | 15 ++++++ app/src/modules/{server => mcp}/system-mcp.ts | 0 app/src/modules/server/SystemController.ts | 10 +++- bun.lock | 12 +++-- 13 files changed, 233 insertions(+), 51 deletions(-) delete mode 100644 app/__test__/app/mcp.spec.ts create mode 100644 app/__test__/app/mcp/mcp.auth.test.ts create mode 100644 app/__test__/app/mcp/mcp.base.test.ts create mode 100644 app/__test__/app/mcp/mcp.data.test.ts create mode 100644 app/__test__/app/mcp/mcp.media.test.ts create mode 100644 app/__test__/app/mcp/mcp.system.test.ts rename app/src/modules/{server => mcp}/system-mcp.ts (100%) diff --git a/app/__test__/app/mcp.spec.ts b/app/__test__/app/mcp.spec.ts deleted file mode 100644 index 15c19ea..0000000 --- a/app/__test__/app/mcp.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, it, expect } from "bun:test"; -import { makeAppFromEnv } from "cli/commands/run"; -import { createApp } from "core/test/utils"; -import { ObjectToolSchema } from "modules/mcp"; -import { s } from "bknd/utils"; - -describe("mcp", () => { - it("...", async () => { - const app = createApp({ - initialConfig: { - auth: { - enabled: true, - }, - }, - }); - await app.build(); - - const appConfig = app.modules.configs(); - const { version, ...appSchema } = app.getSchema(); - - const schema = s.strictObject(appSchema); - - const nodes = [...schema.walk({ data: appConfig })] - .map((n) => { - const path = n.instancePath.join("."); - if (path.startsWith("auth")) { - console.log("schema", n.instancePath, n.schema.constructor.name); - if (path === "auth.jwt") { - //console.log("jwt", n.schema.IS_MCP); - } - } - return n; - }) - .filter((n) => n.schema instanceof ObjectToolSchema) as s.Node[]; - const tools = nodes.flatMap((n) => n.schema.getTools(n)); - - console.log( - "tools", - tools.map((t) => t.name), - ); - }); -}); diff --git a/app/__test__/app/mcp/mcp.auth.test.ts b/app/__test__/app/mcp/mcp.auth.test.ts new file mode 100644 index 0000000..7f3643b --- /dev/null +++ b/app/__test__/app/mcp/mcp.auth.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, beforeAll } from "bun:test"; +import { type App, createApp } from "core/test/utils"; +import { getSystemMcp } from "modules/mcp/system-mcp"; + +/** + * - [ ] auth_me + * - [ ] auth_strategies + * - [ ] auth_user_create + * - [ ] auth_user_token + * - [ ] auth_user_password_change + * - [ ] auth_user_password_test + * - [ ] config_auth_update + * - [ ] config_auth_strategies_get + * - [ ] config_auth_strategies_add + * - [ ] config_auth_strategies_update + * - [ ] config_auth_strategies_remove + * - [ ] config_auth_roles_get + * - [ ] config_auth_roles_add + * - [ ] config_auth_roles_update + * - [ ] config_auth_roles_remove + */ +describe("mcp auth", async () => { + let app: App; + let server: ReturnType; + beforeAll(async () => { + app = createApp({ + initialConfig: { + auth: { + enabled: true, + }, + server: { + mcp: { + enabled: true, + }, + }, + }, + }); + await app.build(); + server = getSystemMcp(app); + }); +}); diff --git a/app/__test__/app/mcp/mcp.base.test.ts b/app/__test__/app/mcp/mcp.base.test.ts new file mode 100644 index 0000000..34816e9 --- /dev/null +++ b/app/__test__/app/mcp/mcp.base.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from "bun:test"; +import { createApp } from "core/test/utils"; +import { getSystemMcp } from "modules/mcp/system-mcp"; +import { registries } from "index"; +import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; + +describe("mcp", () => { + it("should have tools", async () => { + registries.media.register("local", StorageLocalAdapter); + + const app = createApp({ + initialConfig: { + auth: { + enabled: true, + }, + media: { + enabled: true, + adapter: { + type: "local", + config: { + path: "./", + }, + }, + }, + }, + }); + await app.build(); + + const server = getSystemMcp(app); + expect(server.tools.length).toBeGreaterThan(0); + }); +}); diff --git a/app/__test__/app/mcp/mcp.data.test.ts b/app/__test__/app/mcp/mcp.data.test.ts new file mode 100644 index 0000000..23bf496 --- /dev/null +++ b/app/__test__/app/mcp/mcp.data.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, beforeAll } from "bun:test"; +import { type App, createApp } from "core/test/utils"; +import { getSystemMcp } from "modules/mcp/system-mcp"; + +/** + * - [ ] data_sync + * - [ ] data_entity_fn_count + * - [ ] data_entity_fn_exists + * - [ ] data_entity_read_one + * - [ ] data_entity_read_many + * - [ ] data_entity_insert + * - [ ] data_entity_update_many + * - [ ] data_entity_update_one + * - [ ] data_entity_delete_one + * - [ ] data_entity_delete_many + * - [ ] data_entity_info + * - [ ] config_data_get + * - [ ] config_data_update + * - [ ] config_data_entities_get + * - [ ] config_data_entities_add + * - [ ] config_data_entities_update + * - [ ] config_data_entities_remove + * - [ ] config_data_relations_get + * - [ ] config_data_relations_add + * - [ ] config_data_relations_update + * - [ ] config_data_relations_remove + * - [ ] config_data_indices_get + * - [ ] config_data_indices_add + * - [ ] config_data_indices_update + * - [ ] config_data_indices_remove + */ +describe("mcp data", async () => { + let app: App; + let server: ReturnType; + beforeAll(async () => { + app = createApp({ + initialConfig: { + server: { + mcp: { + enabled: true, + }, + }, + }, + }); + await app.build(); + server = getSystemMcp(app); + }); +}); diff --git a/app/__test__/app/mcp/mcp.media.test.ts b/app/__test__/app/mcp/mcp.media.test.ts new file mode 100644 index 0000000..c2d8669 --- /dev/null +++ b/app/__test__/app/mcp/mcp.media.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect, beforeAll } from "bun:test"; +import { type App, createApp } from "core/test/utils"; +import { getSystemMcp } from "modules/mcp/system-mcp"; +import { registries } from "index"; +import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; + +/** + * - [ ] config_media_get + * - [ ] config_media_update + * - [ ] config_media_adapter_get + * - [ ] config_media_adapter_update + */ +describe("mcp media", async () => { + let app: App; + let server: ReturnType; + beforeAll(async () => { + registries.media.register("local", StorageLocalAdapter); + app = createApp({ + initialConfig: { + media: { + enabled: true, + adapter: { + type: "local", + config: { + path: "./", + }, + }, + }, + server: { + mcp: { + enabled: true, + }, + }, + }, + }); + await app.build(); + server = getSystemMcp(app); + }); +}); diff --git a/app/__test__/app/mcp/mcp.system.test.ts b/app/__test__/app/mcp/mcp.system.test.ts new file mode 100644 index 0000000..9808a51 --- /dev/null +++ b/app/__test__/app/mcp/mcp.system.test.ts @@ -0,0 +1,38 @@ +import { describe, test, expect, beforeAll } from "bun:test"; +import { type App, createApp, createMcpToolCaller } from "core/test/utils"; +import { getSystemMcp } from "modules/mcp/system-mcp"; +import { inspect } from "node:util"; +inspect.defaultOptions.depth = 10; + +/** + * - [ ] system_config + * - [ ] system_build + * - [ ] system_ping + * - [ ] system_info + * - [ ] config_server_get + * - [ ] config_server_update + */ +describe("mcp system", async () => { + let app: App; + let server: ReturnType; + beforeAll(async () => { + app = createApp({ + initialConfig: { + server: { + mcp: { + enabled: true, + }, + }, + }, + }); + await app.build(); + server = getSystemMcp(app); + }); + + const tool = createMcpToolCaller(); + + test("system_ping", async () => { + const result = await tool(server, "system_ping", {}); + expect(result).toEqual({ pong: true }); + }); +}); diff --git a/app/bunfig.toml b/app/bunfig.toml index 6f4fe9a..c39b588 100644 --- a/app/bunfig.toml +++ b/app/bunfig.toml @@ -2,4 +2,5 @@ #registry = "http://localhost:4873" [test] -coverageSkipTestFiles = true \ No newline at end of file +coverageSkipTestFiles = true +console.depth = 10 \ No newline at end of file diff --git a/app/package.json b/app/package.json index 72936b9..34c8bbd 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.7.1", + "jsonv-ts": "^0.7.2", "kysely": "0.27.6", "lodash-es": "^4.17.21", "oauth4webapi": "^2.11.1", diff --git a/app/src/cli/commands/mcp/mcp.ts b/app/src/cli/commands/mcp/mcp.ts index 35d86a8..2030413 100644 --- a/app/src/cli/commands/mcp/mcp.ts +++ b/app/src/cli/commands/mcp/mcp.ts @@ -1,6 +1,6 @@ import type { CliCommand } from "cli/types"; import { makeAppFromEnv } from "../run"; -import { getSystemMcp } from "modules/server/system-mcp"; +import { getSystemMcp } from "modules/mcp/system-mcp"; import { $console } from "bknd/utils"; import { stdioTransport } from "jsonv-ts/mcp"; diff --git a/app/src/core/test/utils.ts b/app/src/core/test/utils.ts index d4cefa9..30e0642 100644 --- a/app/src/core/test/utils.ts +++ b/app/src/core/test/utils.ts @@ -1,6 +1,7 @@ import { createApp as createAppInternal, type CreateAppConfig } from "App"; import { bunSqlite } from "adapter/bun/connection/BunSqliteConnection"; import { Connection } from "data/connection/Connection"; +import type { getSystemMcp } from "modules/mcp/system-mcp"; export { App } from "App"; @@ -10,3 +11,17 @@ export function createApp({ connection, ...config }: CreateAppConfig = {}) { connection: Connection.isConnection(connection) ? connection : bunSqlite(connection as any), }); } + +export function createMcpToolCaller() { + return async (server: ReturnType, name: string, args: any) => { + const res = await server.handle({ + jsonrpc: "2.0", + method: "tools/call", + params: { + name, + arguments: args, + }, + }); + return JSON.parse((res.result as any)?.content?.[0]?.text ?? "null"); + }; +} diff --git a/app/src/modules/server/system-mcp.ts b/app/src/modules/mcp/system-mcp.ts similarity index 100% rename from app/src/modules/server/system-mcp.ts rename to app/src/modules/mcp/system-mcp.ts diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts index 3ec2b76..f5b024c 100644 --- a/app/src/modules/server/SystemController.ts +++ b/app/src/modules/server/SystemController.ts @@ -15,6 +15,7 @@ import { openAPISpecs, mcpTool, mcp as mcpMiddleware, + isNode, } from "bknd/utils"; import type { Context, Hono } from "hono"; import { Controller } from "modules/Controller"; @@ -28,7 +29,7 @@ import { import * as SystemPermissions from "modules/permissions"; import { getVersion } from "core/env"; import type { Module } from "modules/Module"; -import { getSystemMcp } from "./system-mcp"; +import { getSystemMcp } from "modules/mcp/system-mcp"; export type ConfigUpdate = { success: true; @@ -94,6 +95,8 @@ export class SystemController extends Controller { }, endpoint: { path: "/mcp", + // @ts-ignore + _init: isNode() ? { duplex: "half" } : {}, }, }), ); @@ -365,7 +368,10 @@ export class SystemController extends Controller { }), (c) => c.json({ - version: c.get("app")?.version(), + version: { + config: c.get("app")?.version(), + bknd: getVersion(), + }, runtime: getRuntimeKey(), connection: { name: this.app.em.connection.name, diff --git a/bun.lock b/bun.lock index d8bf271..d139a29 100644 --- a/bun.lock +++ b/bun.lock @@ -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.7.1", + "jsonv-ts": "^0.7.2", "kysely": "0.27.6", "lodash-es": "^4.17.21", "oauth4webapi": "^2.11.1", @@ -1227,7 +1227,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], - "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], + "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], @@ -2511,7 +2511,7 @@ "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], - "jsonv-ts": ["jsonv-ts@0.7.1", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-UMqzVRE93NKO/aPROYIbE7yZJxzZ+ab7QaR7Lkxqltkh9ss9c6n8beDxlen71bpsTceLbSxMCbO05r87UMf4JA=="], + "jsonv-ts": ["jsonv-ts@0.7.2", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-HxtHbMQhReJpxDIWHcM+kLekRLgJIo+drQnxiXep9thbh5jA44pd3DxwApEV1/oTufH2xAfDV6uu6O0Fd4s9lA=="], "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=="], @@ -3827,6 +3827,8 @@ "@bknd/plasmic/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "@bknd/postgres/@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], + "@bknd/postgres/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "@bknd/sqlocal/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], @@ -4071,7 +4073,7 @@ "@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], - "@types/bun/bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], + "@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], "@typescript-eslint/experimental-utils/eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "^1.1.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="], @@ -4677,6 +4679,8 @@ "@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="], + "@bknd/postgres/@types/bun/bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], + "@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], "@cloudflare/vitest-pool-workers/miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],