init mcp data tests, added crud for $record

This commit is contained in:
dswbx
2025-08-12 12:55:14 +02:00
parent 1e8c373dd4
commit f40ea0ec5b
6 changed files with 117 additions and 21 deletions

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, beforeAll } from "bun:test";
import { type App, createApp } from "core/test/utils";
import { describe, test, expect, beforeAll } from "bun:test";
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
import { getSystemMcp } from "modules/mcp/system-mcp";
/**
@@ -16,10 +16,10 @@ import { getSystemMcp } from "modules/mcp/system-mcp";
* - [ ] 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
* - [x] config_data_entities_get
* - [x] config_data_entities_add
* - [x] config_data_entities_update
* - [x] config_data_entities_remove
* - [ ] config_data_relations_get
* - [ ] config_data_relations_add
* - [ ] config_data_relations_update
@@ -44,5 +44,60 @@ describe("mcp data", async () => {
});
await app.build();
server = getSystemMcp(app);
server.setLogLevel("error");
server.onNotification((message) => {
console.dir(message, { depth: null });
});
});
const tool = createMcpToolCaller();
test("config_data_entities_{add,get,update,remove}", async () => {
const result = await tool(server, "config_data_entities_add", {
key: "test",
value: {},
});
expect(result.success).toBe(true);
expect(result.module).toBe("data");
expect(result.config.entities.test.type).toEqual("regular");
const entities = Object.keys(app.toJSON().data.entities ?? {});
expect(entities).toContain("test");
{
// get
const result = await tool(server, "config_data_entities_get", {
key: "test",
});
expect(result.module).toBe("data");
expect(result.key).toBe("test");
expect(result.value.type).toEqual("regular");
}
{
// update
const result = await tool(server, "config_data_entities_update", {
key: "test",
value: {
config: {
name: "Test",
},
},
});
expect(result.success).toBe(true);
expect(result.module).toBe("data");
expect(result.config.entities.test.config?.name).toEqual("Test");
expect(app.toJSON().data.entities?.test?.config?.name).toEqual("Test");
}
{
// remove
const result = await tool(server, "config_data_entities_remove", {
key: "test",
});
expect(result.success).toBe(true);
expect(result.module).toBe("data");
expect(app.toJSON().data.entities?.test).toBeUndefined();
}
});
});

View File

@@ -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.2",
"jsonv-ts": "^0.7.3",
"kysely": "0.27.6",
"lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1",

View File

@@ -24,6 +24,7 @@ export function createMcpToolCaller() {
});
if ((res.result as any)?.isError) {
console.dir(res.result, { depth: null });
throw new Error((res.result as any)?.content?.[0]?.text ?? "Unknown error");
}

View File

@@ -3,7 +3,7 @@ import { MediaField, mediaFieldConfigSchema } from "../media/MediaField";
import { FieldClassMap } from "data/fields";
import { RelationClassMap, RelationFieldClassMap } from "data/relations";
import { entityConfigSchema, entityTypes } from "data/entities";
import { primaryFieldTypes } from "./fields";
import { primaryFieldTypes, baseFieldConfigSchema } from "./fields";
import { s } from "bknd/utils";
import { $object, $record } from "modules/mcp";
@@ -12,6 +12,7 @@ export const FIELDS = {
...RelationFieldClassMap,
media: { schema: mediaFieldConfigSchema, field: MediaField },
};
export const FIELD_TYPES = Object.keys(FIELDS);
export type FieldType = keyof typeof FIELDS;
export const RELATIONS = RelationClassMap;
@@ -40,6 +41,19 @@ export const entitiesSchema = s.strictObject({
fields: entityFields.optional(),
});
export type TAppDataEntity = s.Static<typeof entitiesSchema>;
export const simpleEntitiesSchema = s.strictObject({
type: s.string({ enum: entityTypes, default: "regular" }).optional(),
config: entityConfigSchema.optional(),
fields: s
.record(
s.object({
type: s.anyOf([s.string({ enum: FIELD_TYPES }), s.string()]),
config: baseFieldConfigSchema.optional(),
}),
{ default: {} },
)
.optional(),
});
export const relationsSchema = Object.entries(RelationClassMap).map(([name, relationClass]) => {
return s.strictObject(
@@ -70,6 +84,6 @@ export const dataConfigSchema = $object("config_data", {
default: {},
}).optional(),
indices: $record("config_data_indices", indicesSchema, { default: {} }).optional(),
});
}).strict();
export type AppDataConfig = s.Static<typeof dataConfigSchema>;

View File

@@ -32,6 +32,10 @@ export class RecordToolSchema<
return this[mcpSchemaSymbol];
}
private getNewSchema(fallback: s.Schema = this.additionalProperties) {
return this[opts].new_schema ?? fallback;
}
private toolGet(node: s.Node<RecordToolSchema<AP, O>>) {
return new Tool(
[this.mcp.name, "get"].join("_"),
@@ -60,8 +64,10 @@ export class RecordToolSchema<
async (params, ctx: AppToolHandlerCtx) => {
const configs = ctx.context.app.toJSON(params.secrets);
const config = getPath(configs, node.instancePath);
const [module_name] = node.instancePath;
// @todo: add schema to response
const schema = params.schema ? this.getNewSchema().toJSON() : undefined;
if (params.key) {
if (!(params.key in config)) {
@@ -70,15 +76,19 @@ export class RecordToolSchema<
const value = getPath(config, params.key);
return ctx.json({
secrets: params.secrets ?? false,
module: module_name,
key: params.key,
value: value ?? null,
schema,
});
}
return ctx.json({
secrets: params.secrets ?? false,
module: module_name,
key: null,
value: config ?? null,
schema,
});
},
);
@@ -93,20 +103,26 @@ export class RecordToolSchema<
key: s.string({
description: "key to add",
}),
value: this[opts].new_schema ?? this.additionalProperties,
value: this.getNewSchema(),
}),
},
async (params, ctx: AppToolHandlerCtx) => {
const configs = ctx.context.app.toJSON();
const config = getPath(configs, node.instancePath);
const [module_name, ...rest] = node.instancePath;
if (params.key in config) {
throw new Error(`Key "${params.key}" already exists in config`);
}
await ctx.context.app
.mutateConfig(module_name as any)
.patch([...rest, params.key], params.value);
return ctx.json({
key: params.key,
value: params.value ?? null,
success: true,
module: module_name,
config: ctx.context.app.module[module_name as any].config,
});
},
);
@@ -121,22 +137,26 @@ export class RecordToolSchema<
key: s.string({
description: "key to update",
}),
value: this[opts].new_schema ?? s.object({}),
value: this.getNewSchema(s.object({})),
}),
},
async (params, ctx: AppToolHandlerCtx) => {
const configs = ctx.context.app.toJSON(params.secrets);
const config = getPath(configs, node.instancePath);
const [module_name, ...rest] = node.instancePath;
if (!(params.key in config)) {
throw new Error(`Key "${params.key}" not found in config`);
}
const value = getPath(config, params.key);
await ctx.context.app
.mutateConfig(module_name as any)
.patch([...rest, params.key], params.value);
return ctx.json({
updated: false,
key: params.key,
value: value ?? null,
success: true,
module: module_name,
config: ctx.context.app.module[module_name as any].config,
});
},
);
@@ -156,14 +176,20 @@ export class RecordToolSchema<
async (params, ctx: AppToolHandlerCtx) => {
const configs = ctx.context.app.toJSON();
const config = getPath(configs, node.instancePath);
const [module_name, ...rest] = node.instancePath;
if (!(params.key in config)) {
throw new Error(`Key "${params.key}" not found in config`);
}
await ctx.context.app
.mutateConfig(module_name as any)
.remove([...rest, params.key].join("."));
return ctx.json({
removed: false,
key: params.key,
success: true,
module: module_name,
config: ctx.context.app.module[module_name as any].config,
});
},
);