mcp: added auth tests, updated data tests

This commit is contained in:
dswbx
2025-08-12 22:13:09 +02:00
parent a6ed74d904
commit 70f0240da5
12 changed files with 1422 additions and 250 deletions

View File

@@ -72,6 +72,7 @@ export const authConfigSchema = $object(
},
s.strictObject({
type: s.string(),
enabled: s.boolean({ default: true }).optional(),
config: s.object({}),
}),
),

View File

@@ -385,7 +385,11 @@ export class Authenticator<
headers = c.headers;
} else {
is_context = true;
headers = c.req.raw.headers;
try {
headers = c.req.raw.headers;
} catch (e) {
throw new Exception("Request/Headers/Context is required to resolve auth", 400);
}
}
let token: string | undefined;

View File

@@ -13,15 +13,18 @@ export function createApp({ connection, ...config }: CreateAppConfig = {}) {
}
export function createMcpToolCaller() {
return async (server: ReturnType<typeof getSystemMcp>, name: string, args: any) => {
const res = await server.handle({
jsonrpc: "2.0",
method: "tools/call",
params: {
name,
arguments: args,
return async (server: ReturnType<typeof getSystemMcp>, name: string, args: any, raw?: any) => {
const res = await server.handle(
{
jsonrpc: "2.0",
method: "tools/call",
params: {
name,
arguments: args,
},
},
});
raw,
);
if ((res.result as any)?.isError) {
console.dir(res.result, { depth: null });

View File

@@ -80,9 +80,19 @@ export const dataConfigSchema = $object("config_data", {
basepath: s.string({ default: "/api/data" }).optional(),
default_primary_format: s.string({ enum: primaryFieldTypes, default: "integer" }).optional(),
entities: $record("config_data_entities", entitiesSchema, { default: {} }).optional(),
relations: $record("config_data_relations", s.anyOf(relationsSchema), {
default: {},
}).optional(),
relations: $record(
"config_data_relations",
s.anyOf(relationsSchema),
{
default: {},
},
s.strictObject({
type: s.string({ enum: Object.keys(RelationClassMap) }),
source: s.string(),
target: s.string(),
config: s.object({}).optional(),
}),
).optional(),
indices: $record("config_data_indices", indicesSchema, {
default: {},
mcp: { update: false },

View File

@@ -7,7 +7,7 @@ import type { ModuleBuildContext, ModuleBuildContextMcpContext } from "./Module"
import type { EntityRelation } from "data/relations";
import type { Permission } from "core/security/Permission";
import { Exception } from "core/errors";
import { invariant } from "bknd/utils";
import { invariant, isPlainObject } from "bknd/utils";
export class ModuleHelper {
constructor(protected ctx: Omit<ModuleBuildContext, "helper">) {}
@@ -119,12 +119,14 @@ export class ModuleHelper {
c: { context: ModuleBuildContextMcpContext; raw?: unknown },
) {
invariant(c.context.app, "app is not available in mcp context");
invariant(c.raw instanceof Request, "request is not available in mcp context");
const auth = c.context.app.module.auth;
if (!auth.enabled) return;
const user = await auth.authenticator?.resolveAuthFromRequest(c.raw as Request);
if (c.raw === undefined || c.raw === null) {
throw new Exception("Request/Headers/Context is not available in mcp context", 400);
}
const user = await auth.authenticator?.resolveAuthFromRequest(c.raw as any);
if (!this.ctx.guard.granted(permission, user)) {
throw new Exception(

View File

@@ -42,7 +42,7 @@ export class RecordToolSchema<
}
private getNewSchema(fallback: s.Schema = this.additionalProperties) {
return this[opts].new_schema ?? fallback;
return this[opts].new_schema ?? this.additionalProperties ?? fallback;
}
private toolGet(node: s.Node<RecordToolSchema<AP, O>>) {
@@ -122,7 +122,7 @@ export class RecordToolSchema<
}),
},
async (params, ctx: AppToolHandlerCtx) => {
const configs = ctx.context.app.toJSON();
const configs = ctx.context.app.toJSON(true);
const config = getPath(configs, node.instancePath);
const [module_name, ...rest] = node.instancePath;
@@ -134,12 +134,16 @@ export class RecordToolSchema<
.mutateConfig(module_name as any)
.patch([...rest, params.key], params.value);
const newConfig = getPath(ctx.context.app.toJSON(), node.instancePath);
return ctx.json({
success: true,
module: module_name,
config: params.return_config
? ctx.context.app.module[module_name as any].config
: undefined,
action: {
type: "add",
key: params.key,
},
config: params.return_config ? newConfig : undefined,
});
},
);
@@ -154,7 +158,7 @@ export class RecordToolSchema<
key: s.string({
description: "key to update",
}),
value: this.getNewSchema(s.object({})),
value: this.mcp.getCleanSchema(this.getNewSchema(s.object({}))),
return_config: s
.boolean({
default: false,
@@ -164,7 +168,7 @@ export class RecordToolSchema<
}),
},
async (params, ctx: AppToolHandlerCtx) => {
const configs = ctx.context.app.toJSON(params.secrets);
const configs = ctx.context.app.toJSON(true);
const config = getPath(configs, node.instancePath);
const [module_name, ...rest] = node.instancePath;
@@ -176,12 +180,16 @@ export class RecordToolSchema<
.mutateConfig(module_name as any)
.patch([...rest, params.key], params.value);
const newConfig = getPath(ctx.context.app.toJSON(), node.instancePath);
return ctx.json({
success: true,
module: module_name,
config: params.return_config
? ctx.context.app.module[module_name as any].config
: undefined,
action: {
type: "update",
key: params.key,
},
config: params.return_config ? newConfig : undefined,
});
},
);
@@ -205,7 +213,7 @@ export class RecordToolSchema<
}),
},
async (params, ctx: AppToolHandlerCtx) => {
const configs = ctx.context.app.toJSON();
const configs = ctx.context.app.toJSON(true);
const config = getPath(configs, node.instancePath);
const [module_name, ...rest] = node.instancePath;
@@ -217,12 +225,16 @@ export class RecordToolSchema<
.mutateConfig(module_name as any)
.remove([...rest, params.key].join("."));
const newConfig = getPath(ctx.context.app.toJSON(), node.instancePath);
return ctx.json({
success: true,
module: module_name,
config: params.return_config
? ctx.context.app.module[module_name as any].config
: undefined,
action: {
type: "remove",
key: params.key,
},
config: params.return_config ? newConfig : undefined,
});
},
);

View File

@@ -43,16 +43,18 @@ export class McpSchemaHelper<AdditionalOptions = {}> {
public name: string,
public options: McpToolOptions & AdditionalOptions,
) {
this.cleanSchema = this.getCleanSchema();
this.cleanSchema = this.getCleanSchema(this.schema as s.ObjectSchema);
}
private getCleanSchema() {
getCleanSchema(schema: s.ObjectSchema) {
if (schema.type !== "object") return schema;
const props = excludePropertyTypes(
this.schema as any,
schema as any,
(i) => isPlainObject(i) && mcpSchemaSymbol in i,
);
const schema = s.strictObject(props);
return rescursiveClean(schema, {
const _schema = s.strictObject(props);
return rescursiveClean(_schema, {
removeRequired: true,
removeDefault: false,
}) as s.ObjectSchema<any, any>;