diff --git a/app/__test__/app/mcp/mcp.media.test.ts b/app/__test__/app/mcp/mcp.media.test.ts index c2d8669..4300ac8 100644 --- a/app/__test__/app/mcp/mcp.media.test.ts +++ b/app/__test__/app/mcp/mcp.media.test.ts @@ -1,19 +1,23 @@ -import { describe, it, expect, beforeAll } from "bun:test"; -import { type App, createApp } from "core/test/utils"; +import { describe, test, expect, beforeAll, beforeEach, afterAll } from "bun:test"; +import { type App, createApp, createMcpToolCaller } from "core/test/utils"; import { getSystemMcp } from "modules/mcp/system-mcp"; import { registries } from "index"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; +import { disableConsoleLog, enableConsoleLog } from "core/utils"; + +beforeAll(disableConsoleLog); +afterAll(enableConsoleLog); /** - * - [ ] config_media_get - * - [ ] config_media_update - * - [ ] config_media_adapter_get - * - [ ] config_media_adapter_update + * - [x] config_media_get + * - [x] config_media_update + * - [x] config_media_adapter_get + * - [x] config_media_adapter_update */ describe("mcp media", async () => { let app: App; let server: ReturnType; - beforeAll(async () => { + beforeEach(async () => { registries.media.register("local", StorageLocalAdapter); app = createApp({ initialConfig: { @@ -35,5 +39,79 @@ describe("mcp media", async () => { }); await app.build(); server = getSystemMcp(app); + server.setLogLevel("error"); + server.onNotification((message) => { + console.dir(message, { depth: null }); + }); + }); + + const tool = createMcpToolCaller(); + + test("config_media_{get,update}", async () => { + const result = await tool(server, "config_media_get", {}); + expect(result).toEqual({ + path: "", + secrets: false, + partial: false, + value: app.toJSON().media, + }); + + // partial + expect((await tool(server, "config_media_get", { path: "adapter" })).value).toEqual({ + type: "local", + config: { + path: "./", + }, + }); + + // update + await tool(server, "config_media_update", { + value: { + storage: { + body_max_size: 1024 * 1024 * 10, + }, + }, + return_config: true, + }); + expect(app.toJSON().media.storage.body_max_size).toBe(1024 * 1024 * 10); + }); + + test("config_media_adapter_{get,update}", async () => { + const result = await tool(server, "config_media_adapter_get", {}); + expect(result).toEqual({ + secrets: false, + value: app.toJSON().media.adapter, + }); + + // update + await tool(server, "config_media_adapter_update", { + value: { + type: "local", + config: { + path: "./subdir", + }, + }, + }); + const adapter = app.toJSON().media.adapter as any; + expect(adapter.config.path).toBe("./subdir"); + expect(adapter.type).toBe("local"); + + // set to s3 + { + await tool(server, "config_media_adapter_update", { + value: { + type: "s3", + config: { + access_key: "123", + secret_access_key: "456", + url: "https://example.com/what", + }, + }, + }); + + const adapter = app.toJSON(true).media.adapter as any; + expect(adapter.type).toBe("s3"); + expect(adapter.config.url).toBe("https://example.com/what"); + } }); }); diff --git a/app/src/core/object/SchemaObject.ts b/app/src/core/object/SchemaObject.ts index c22b811..23470a3 100644 --- a/app/src/core/object/SchemaObject.ts +++ b/app/src/core/object/SchemaObject.ts @@ -177,7 +177,6 @@ export class SchemaObject { this.throwIfRestricted(partial); - // overwrite arrays and primitives, only deep merge objects // @ts-ignore const config = set(current, path, value); diff --git a/app/src/media/media-schema.ts b/app/src/media/media-schema.ts index c506ae2..4e71d83 100644 --- a/app/src/media/media-schema.ts +++ b/app/src/media/media-schema.ts @@ -39,6 +39,7 @@ export function buildMediaSchema() { }, { default: {} }, ), + // @todo: currently cannot be updated partially using mcp adapter: $schema( "config_media_adapter", s.anyOf(Object.values(adapterSchemaObject)), diff --git a/app/src/modules/mcp/$schema.ts b/app/src/modules/mcp/$schema.ts index bc707ab..9c86d4a 100644 --- a/app/src/modules/mcp/$schema.ts +++ b/app/src/modules/mcp/$schema.ts @@ -50,12 +50,28 @@ export const $schema = < { ...mcp.getToolOptions("update"), inputSchema: s.strictObject({ - full: s.boolean({ default: false }).optional(), value: schema as any, + return_config: s.boolean({ default: false }).optional(), + secrets: s.boolean({ default: false }).optional(), }), }, async (params, ctx: AppToolHandlerCtx) => { - return ctx.json(params); + const { value, return_config, secrets } = params; + const [module_name, ...rest] = node.instancePath; + + await ctx.context.app.mutateConfig(module_name as any).overwrite(rest, value); + + let config: any = undefined; + if (return_config) { + const configs = ctx.context.app.toJSON(secrets); + config = getPath(configs, node.instancePath); + } + + return ctx.json({ + success: true, + module: module_name, + config, + }); }, ); }; diff --git a/docs/mcp.json b/docs/mcp.json index b97d05c..064ebd4 100644 --- a/docs/mcp.json +++ b/docs/mcp.json @@ -2910,10 +2910,6 @@ "type": "object", "additionalProperties": false, "properties": { - "full": { - "type": "boolean", - "default": false - }, "value": { "anyOf": [ { @@ -3028,6 +3024,14 @@ ] } ] + }, + "return_config": { + "type": "boolean", + "default": false + }, + "secrets": { + "type": "boolean", + "default": false } } },