added initial $record

This commit is contained in:
dswbx
2025-08-05 13:20:00 +02:00
parent 5e5f0ef70f
commit 3e2938f77d
13 changed files with 430 additions and 148 deletions

View File

@@ -100,7 +100,7 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"jotai": "^2.12.2", "jotai": "^2.12.2",
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"jsonv-ts": "^0.5.1", "jsonv-ts": "link:jsonv-ts",
"kysely-d1": "^0.3.0", "kysely-d1": "^0.3.0",
"kysely-generic-sqlite": "^1.2.1", "kysely-generic-sqlite": "^1.2.1",
"libsql-stateless-easy": "^1.8.0", "libsql-stateless-easy": "^1.8.0",

View File

@@ -172,7 +172,9 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
// load system controller // load system controller
guard.registerPermissions(Object.values(SystemPermissions)); guard.registerPermissions(Object.values(SystemPermissions));
server.route("/api/system", new SystemController(this).getController()); const systemController = new SystemController(this);
systemController.registerMcp();
server.route("/api/system", systemController.getController());
// emit built event // emit built event
$console.log("App built"); $console.log("App built");

View File

@@ -1,7 +1,7 @@
import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator"; import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator";
import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies"; import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies";
import { objectTransform, s } from "bknd/utils"; import { objectTransform, s } from "bknd/utils";
import { $object } from "modules/mcp"; import { $object, $record } from "modules/mcp";
export const Strategies = { export const Strategies = {
password: { password: {
@@ -37,7 +37,7 @@ export type AppAuthStrategies = s.Static<typeof strategiesSchema>;
export type AppAuthOAuthStrategy = s.Static<typeof STRATEGIES.oauth.schema>; export type AppAuthOAuthStrategy = s.Static<typeof STRATEGIES.oauth.schema>;
export type AppAuthCustomOAuthStrategy = s.Static<typeof STRATEGIES.custom_oauth.schema>; export type AppAuthCustomOAuthStrategy = s.Static<typeof STRATEGIES.custom_oauth.schema>;
const guardConfigSchema = $object("config_auth_guard", { const guardConfigSchema = s.object({
enabled: s.boolean({ default: false }).optional(), enabled: s.boolean({ default: false }).optional(),
}); });
export const guardRoleSchema = s.strictObject({ export const guardRoleSchema = s.strictObject({
@@ -55,7 +55,10 @@ export const authConfigSchema = $object(
allow_register: s.boolean({ default: true }).optional(), allow_register: s.boolean({ default: true }).optional(),
jwt: jwtConfig, jwt: jwtConfig,
cookie: cookieConfig, cookie: cookieConfig,
strategies: s.record(strategiesSchema, { strategies: $record(
"config_auth_strategies",
strategiesSchema,
{
title: "Strategies", title: "Strategies",
default: { default: {
password: { password: {
@@ -66,9 +69,14 @@ export const authConfigSchema = $object(
}, },
}, },
}, },
},
s.strictObject({
type: s.string(),
config: s.object({}),
}), }),
),
guard: guardConfigSchema.optional(), guard: guardConfigSchema.optional(),
roles: s.record(guardRoleSchema, { default: {} }).optional(), roles: $record("config_auth_roles", guardRoleSchema, { default: {} }).optional(),
}, },
{ title: "Authentication" }, { title: "Authentication" },
); );

View File

@@ -1,10 +1,10 @@
import type { CliCommand } from "cli/types"; import type { CliCommand } from "cli/types";
import { makeAppFromEnv } from "../run"; import { makeAppFromEnv } from "../run";
import { s, mcp as mcpMiddleware, McpServer } from "bknd/utils"; import { s, mcp as mcpMiddleware, McpServer, isObject } from "bknd/utils";
import { ObjectToolSchema } from "modules/mcp"; import type { McpSchema } from "modules/mcp";
import { serve } from "@hono/node-server"; import { serve } from "@hono/node-server";
import { Hono } from "hono"; import { Hono } from "hono";
import type { Module } from "modules/Module"; import { mcpSchemaSymbol } from "modules/mcp/McpSchemaHelper";
export const mcp: CliCommand = (program) => export const mcp: CliCommand = (program) =>
program program
@@ -25,8 +25,8 @@ async function action(options: { port: string; path: string }) {
const schema = s.strictObject(appSchema); const schema = s.strictObject(appSchema);
const nodes = [...schema.walk({ data: appConfig })].filter( const nodes = [...schema.walk({ data: appConfig })].filter(
(n) => n.schema instanceof ObjectToolSchema, (n) => isObject(n.schema) && mcpSchemaSymbol in n.schema,
) as s.Node<ObjectToolSchema>[]; ) as s.Node<McpSchema>[];
const tools = [...nodes.flatMap((n) => n.schema.getTools(n)), ...app.modules.ctx().mcp.tools]; const tools = [...nodes.flatMap((n) => n.schema.getTools(n)), ...app.modules.ctx().mcp.tools];
const resources = [...app.modules.ctx().mcp.resources]; const resources = [...app.modules.ctx().mcp.resources];
@@ -39,43 +39,6 @@ async function action(options: { port: string; path: string }) {
tools, tools,
resources, resources,
); );
server
.resource("system_config", "bknd://system/config", (c) =>
c.json(c.context.app.toJSON(), {
title: "System Config",
}),
)
.resource(
"system_config_module",
"bknd://system/config/{module}",
(c, { module }) => {
const m = c.context.app.modules.get(module as any) as Module;
return c.json(m.toJSON(), {
title: `Config for ${module}`,
});
},
{
list: Object.keys(appConfig),
},
)
.resource("system_schema", "bknd://system/schema", (c) =>
c.json(c.context.app.getSchema(), {
title: "System Schema",
}),
)
.resource(
"system_schema_module",
"bknd://system/schema/{module}",
(c, { module }) => {
const m = c.context.app.modules.get(module as any);
return c.json(m.getSchema().toJSON(), {
title: `Schema for ${module}`,
});
},
{
list: Object.keys(appSchema),
},
);
const hono = new Hono().use( const hono = new Hono().use(
mcpMiddleware({ mcpMiddleware({
@@ -91,6 +54,10 @@ async function action(options: { port: string; path: string }) {
port: Number(options.port) || 3000, port: Number(options.port) || 3000,
}); });
console.info(`Server is running on http://localhost:${options.port}${options.path}`); console.info(`Server is running on http://localhost:${options.port}${options.path}`);
console.info(`⚙️ Tools:\n${server.tools.map((t) => `- ${t.name}`).join("\n")}\n`); console.info(
console.info(`📚 Resources:\n${server.resources.map((r) => `- ${r.name}`).join("\n")}`); `⚙️ Tools (${server.tools.length}):\n${server.tools.map((t) => `- ${t.name}`).join("\n")}\n`,
);
console.info(
`📚 Resources (${server.resources.length}):\n${server.resources.map((r) => `- ${r.name}`).join("\n")}`,
);
} }

View File

@@ -189,6 +189,30 @@ export function objectDepth(object: object): number {
return level; return level;
} }
export function limitObjectDepth<T>(obj: T, maxDepth: number): T {
function _limit(current: any, depth: number): any {
if (isPlainObject(current)) {
if (depth > maxDepth) {
return undefined;
}
const result: any = {};
for (const key in current) {
if (Object.prototype.hasOwnProperty.call(current, key)) {
result[key] = _limit(current[key], depth + 1);
}
}
return result;
}
if (Array.isArray(current)) {
// Arrays themselves are not limited, but their object elements are
return current.map((item) => _limit(item, depth));
}
// Primitives are always returned, regardless of depth
return current;
}
return _limit(obj, 1);
}
export function objectCleanEmpty<Obj extends { [key: string]: any }>(obj: Obj): Obj { export function objectCleanEmpty<Obj extends { [key: string]: any }>(obj: Obj): Obj {
if (!obj) return obj; if (!obj) return obj;
return Object.entries(obj).reduce((acc, [key, value]) => { return Object.entries(obj).reduce((acc, [key, value]) => {

View File

@@ -5,6 +5,7 @@ import { RelationClassMap, RelationFieldClassMap } from "data/relations";
import { entityConfigSchema, entityTypes } from "data/entities"; import { entityConfigSchema, entityTypes } from "data/entities";
import { primaryFieldTypes } from "./fields"; import { primaryFieldTypes } from "./fields";
import { s } from "bknd/utils"; import { s } from "bknd/utils";
import { $object, $record } from "modules/mcp";
export const FIELDS = { export const FIELDS = {
...FieldClassMap, ...FieldClassMap,
@@ -61,12 +62,14 @@ export const indicesSchema = s.strictObject({
unique: s.boolean({ default: false }).optional(), unique: s.boolean({ default: false }).optional(),
}); });
export const dataConfigSchema = s.strictObject({ export const dataConfigSchema = $object("config_data", {
basepath: s.string({ default: "/api/data" }).optional(), basepath: s.string({ default: "/api/data" }).optional(),
default_primary_format: s.string({ enum: primaryFieldTypes, default: "integer" }).optional(), default_primary_format: s.string({ enum: primaryFieldTypes, default: "integer" }).optional(),
entities: s.record(entitiesSchema, { default: {} }).optional(), entities: $record("config_data_entities", entitiesSchema, { default: {} }).optional(),
relations: s.record(s.anyOf(relationsSchema), { default: {} }).optional(), relations: $record("config_data_relations", s.anyOf(relationsSchema), {
indices: s.record(indicesSchema, { default: {} }).optional(), default: {},
}).optional(),
indices: $record("config_data_indices", indicesSchema, { default: {} }).optional(),
}); });
export type AppDataConfig = s.Static<typeof dataConfigSchema>; export type AppDataConfig = s.Static<typeof dataConfigSchema>;

View File

@@ -1,7 +1,7 @@
import { MediaAdapters } from "media/media-registry"; import { MediaAdapters } from "media/media-registry";
import { registries } from "modules/registries"; import { registries } from "modules/registries";
import { s, objectTransform } from "bknd/utils"; import { s, objectTransform } from "bknd/utils";
import { $object } from "modules/mcp"; import { $object, $record } from "modules/mcp";
export const ADAPTERS = { export const ADAPTERS = {
...MediaAdapters, ...MediaAdapters,
@@ -39,7 +39,9 @@ export function buildMediaSchema() {
}, },
{ default: {} }, { default: {} },
), ),
adapter: s.anyOf(Object.values(adapterSchemaObject)).optional(), adapter: $record("config_media_adapter", s.anyOf(Object.values(adapterSchemaObject)), {
maxProperties: 1,
}).optional(),
}, },
{ {
default: {}, default: {},

View File

@@ -1,99 +1,71 @@
import { excludePropertyTypes, rescursiveClean } from "./utils"; import { Tool, getPath, limitObjectDepth, s } from "bknd/utils";
import { import {
type Resource, McpSchemaHelper,
Tool, mcpSchemaSymbol,
type ToolAnnotation, type AppToolHandlerCtx,
type ToolHandlerCtx, type McpSchema,
autoFormatString, type SchemaWithMcpOptions,
getPath, } from "./McpSchemaHelper";
s,
} from "bknd/utils";
import type { App } from "App";
import type { ModuleBuildContext } from "modules";
export interface McpToolOptions { export interface ObjectToolSchemaOptions extends s.IObjectOptions, SchemaWithMcpOptions {}
title?: string;
description?: string;
annotations?: ToolAnnotation;
tools?: Tool<any, any, any>[];
resources?: Resource<any, any, any, any>[];
}
export interface ObjectToolSchemaOptions extends s.IObjectOptions {
mcp?: McpToolOptions;
}
type AppToolContext = {
app: App;
ctx: () => ModuleBuildContext;
};
type AppToolHandlerCtx = ToolHandlerCtx<AppToolContext>;
export class ObjectToolSchema< export class ObjectToolSchema<
const P extends s.TProperties = s.TProperties, const P extends s.TProperties = s.TProperties,
const O extends s.IObjectOptions = s.IObjectOptions, const O extends ObjectToolSchemaOptions = ObjectToolSchemaOptions,
> extends s.ObjectSchema<P, O> { >
public readonly mcp: McpToolOptions; extends s.ObjectSchema<P, O>
private cleanSchema: s.ObjectSchema<P, O>; implements McpSchema
{
constructor( constructor(name: string, properties: P, options?: ObjectToolSchemaOptions) {
public name: string,
properties: P,
options?: ObjectToolSchemaOptions,
) {
const { mcp, ...rest } = options || {}; const { mcp, ...rest } = options || {};
super(properties, rest as any); super(properties, rest as any);
this.name = name; this[mcpSchemaSymbol] = new McpSchemaHelper(this, name, mcp || {});
this.mcp = mcp || {};
this.cleanSchema = this.getCleanSchema();
} }
private getMcpOptions(action: "get" | "update") { get mcp(): McpSchemaHelper {
const { tools, resources, ...rest } = this.mcp; return this[mcpSchemaSymbol];
const label = (text?: string) =>
text && [autoFormatString(action), text].filter(Boolean).join(" ");
return {
title: label(this.title ?? this.mcp.title),
description: label(this.description ?? this.mcp.description),
annotations: {
destructiveHint: true,
idempotentHint: true,
...rest.annotations,
},
};
}
private getCleanSchema() {
const props = excludePropertyTypes(this as any, [ObjectToolSchema]);
const schema = s.strictObject(props);
return rescursiveClean(schema, {
removeRequired: true,
removeDefault: false,
}) as s.ObjectSchema<P, O>;
} }
private toolGet(node: s.Node<ObjectToolSchema>) { private toolGet(node: s.Node<ObjectToolSchema>) {
return new Tool( return new Tool(
[this.name, "get"].join("_"), [this.mcp.name, "get"].join("_"),
{ {
...this.getMcpOptions("get"), ...this.mcp.getToolOptions("get"),
inputSchema: s.strictObject({ inputSchema: s.strictObject({
path: s path: s
.string({ .string({
pattern: /^[a-zA-Z0-9_.]{0,}$/, pattern: /^[a-zA-Z0-9_.]{0,}$/,
description: "(optional) path to the property to get, e.g. `key.subkey`", title: "Path",
description: "Path to the property to get, e.g. `key.subkey`",
})
.optional(),
depth: s
.number({
description: "Limit the depth of the response",
})
.optional(),
secrets: s
.boolean({
default: false,
description: "Include secrets in the response config",
}) })
.optional(), .optional(),
include_secrets: s.boolean({ default: false }).optional(),
}), }),
}, },
async (params, ctx: AppToolHandlerCtx) => { async (params, ctx: AppToolHandlerCtx) => {
const configs = ctx.context.app.toJSON(params.include_secrets); const configs = ctx.context.app.toJSON(params.secrets);
const config = getPath(configs, node.instancePath); const config = getPath(configs, node.instancePath);
const value = getPath(config, params.path ?? []); let value = getPath(config, params.path ?? []);
if (params.depth) {
value = limitObjectDepth(value, params.depth);
}
return ctx.json({ return ctx.json({
path: params.path ?? "", path: params.path ?? "",
secrets: params.secrets ?? false,
partial: !!params.depth,
value: value ?? null, value: value ?? null,
}); });
}, },
@@ -101,11 +73,11 @@ export class ObjectToolSchema<
} }
private toolUpdate(node: s.Node<ObjectToolSchema>) { private toolUpdate(node: s.Node<ObjectToolSchema>) {
const schema = this.cleanSchema; const schema = this.mcp.cleanSchema;
return new Tool( return new Tool(
[this.name, "update"].join("_"), [this.mcp.name, "update"].join("_"),
{ {
...this.getMcpOptions("update"), ...this.mcp.getToolOptions("update"),
inputSchema: s.strictObject({ inputSchema: s.strictObject({
full: s.boolean({ default: false }).optional(), full: s.boolean({ default: false }).optional(),
value: s value: s
@@ -120,14 +92,9 @@ export class ObjectToolSchema<
} }
getTools(node: s.Node<ObjectToolSchema>): Tool<any, any, any>[] { getTools(node: s.Node<ObjectToolSchema>): Tool<any, any, any>[] {
const { tools = [] } = this.mcp; const { tools = [] } = this.mcp.options;
return [this.toolGet(node), this.toolUpdate(node), ...tools]; return [this.toolGet(node), this.toolUpdate(node), ...tools];
} }
override toJSON(): s.JSONSchemaDefinition {
const { toJSON, "~standard": _, mcp, cleanSchema, name, ...rest } = this;
return JSON.parse(JSON.stringify(rest));
}
} }
export const $object = < export const $object = <

View File

@@ -0,0 +1,190 @@
import { getPath, s, Tool } from "bknd/utils";
import {
McpSchemaHelper,
mcpSchemaSymbol,
type AppToolHandlerCtx,
type McpSchema,
type SchemaWithMcpOptions,
} from "./McpSchemaHelper";
export interface RecordToolSchemaOptions extends s.IRecordOptions, SchemaWithMcpOptions {}
const opts = Symbol.for("bknd-mcp-record-opts");
export class RecordToolSchema<
AP extends s.Schema,
O extends RecordToolSchemaOptions = RecordToolSchemaOptions,
>
extends s.RecordSchema<AP, O>
implements McpSchema
{
constructor(name: string, ap: AP, options?: RecordToolSchemaOptions, new_schema?: s.Schema) {
const { mcp, ...rest } = options || {};
super(ap, rest as any);
this[mcpSchemaSymbol] = new McpSchemaHelper(this, name, mcp || {});
this[opts] = {
new_schema,
};
}
get mcp(): McpSchemaHelper {
return this[mcpSchemaSymbol];
}
private toolGet(node: s.Node<RecordToolSchema<AP, O>>) {
return new Tool(
[this.mcp.name, "get"].join("_"),
{
...this.mcp.getToolOptions("get"),
inputSchema: s.strictObject({
key: s
.string({
description: "key to get",
})
.optional(),
secrets: s
.boolean({
default: false,
description: "(optional) include secrets in the response config",
})
.optional(),
schema: s
.boolean({
default: false,
description: "(optional) include the schema in the response",
})
.optional(),
}),
},
async (params, ctx: AppToolHandlerCtx) => {
const configs = ctx.context.app.toJSON(params.secrets);
const config = getPath(configs, node.instancePath);
// @todo: add schema to response
if (params.key) {
if (!(params.key in config)) {
throw new Error(`Key "${params.key}" not found in config`);
}
const value = getPath(config, params.key);
return ctx.json({
secrets: params.secrets ?? false,
key: params.key,
value: value ?? null,
});
}
return ctx.json({
secrets: params.secrets ?? false,
key: null,
value: config ?? null,
});
},
);
}
private toolAdd(node: s.Node<RecordToolSchema<AP, O>>) {
return new Tool(
[this.mcp.name, "add"].join("_"),
{
...this.mcp.getToolOptions("add"),
inputSchema: s.strictObject({
key: s.string({
description: "key to add",
}),
value: this[opts].new_schema ?? this.additionalProperties,
}),
},
async (params, ctx: AppToolHandlerCtx) => {
const configs = ctx.context.app.toJSON();
const config = getPath(configs, node.instancePath);
if (params.key in config) {
throw new Error(`Key "${params.key}" already exists in config`);
}
return ctx.json({
key: params.key,
value: params.value ?? null,
});
},
);
}
private toolUpdate(node: s.Node<RecordToolSchema<AP, O>>) {
return new Tool(
[this.mcp.name, "update"].join("_"),
{
...this.mcp.getToolOptions("update"),
inputSchema: s.strictObject({
key: s.string({
description: "key to update",
}),
value: this[opts].new_schema ?? s.object({}),
}),
},
async (params, ctx: AppToolHandlerCtx) => {
const configs = ctx.context.app.toJSON(params.secrets);
const config = getPath(configs, node.instancePath);
if (!(params.key in config)) {
throw new Error(`Key "${params.key}" not found in config`);
}
const value = getPath(config, params.key);
return ctx.json({
updated: false,
key: params.key,
value: value ?? null,
});
},
);
}
private toolRemove(node: s.Node<RecordToolSchema<AP, O>>) {
return new Tool(
[this.mcp.name, "remove"].join("_"),
{
...this.mcp.getToolOptions("get"),
inputSchema: s.strictObject({
key: s.string({
description: "key to remove",
}),
}),
},
async (params, ctx: AppToolHandlerCtx) => {
const configs = ctx.context.app.toJSON();
const config = getPath(configs, node.instancePath);
if (!(params.key in config)) {
throw new Error(`Key "${params.key}" not found in config`);
}
return ctx.json({
removed: false,
key: params.key,
});
},
);
}
getTools(node: s.Node<RecordToolSchema<AP, O>>): Tool<any, any, any>[] {
const { tools = [] } = this.mcp.options;
return [
this.toolGet(node),
this.toolAdd(node),
this.toolUpdate(node),
this.toolRemove(node),
...tools,
].filter(Boolean) as Tool<any, any, any>[];
}
}
export const $record = <const AP extends s.Schema, const O extends RecordToolSchemaOptions>(
name: string,
ap: AP,
options?: s.StrictOptions<RecordToolSchemaOptions, O>,
new_schema?: s.Schema,
): RecordToolSchema<AP, O> => new RecordToolSchema(name, ap, options, new_schema) as any;

View File

@@ -0,0 +1,75 @@
import type { App } from "bknd";
import {
type Tool,
type ToolAnnotation,
type Resource,
type ToolHandlerCtx,
s,
isPlainObject,
autoFormatString,
} from "bknd/utils";
import type { ModuleBuildContext } from "modules";
import { excludePropertyTypes, rescursiveClean } from "./utils";
export const mcpSchemaSymbol = Symbol.for("bknd-mcp-schema");
export interface McpToolOptions {
title?: string;
description?: string;
annotations?: ToolAnnotation;
tools?: Tool<any, any, any>[];
resources?: Resource<any, any, any, any>[];
}
export interface SchemaWithMcpOptions {
mcp?: McpToolOptions;
}
export type AppToolContext = {
app: App;
ctx: () => ModuleBuildContext;
};
export type AppToolHandlerCtx = ToolHandlerCtx<AppToolContext>;
export interface McpSchema extends s.Schema {
getTools(node: s.Node<any>): Tool<any, any, any>[];
}
export class McpSchemaHelper {
cleanSchema: s.ObjectSchema<any, any>;
constructor(
public schema: s.Schema,
public name: string,
public options: McpToolOptions,
) {
this.cleanSchema = this.getCleanSchema();
}
private getCleanSchema() {
const props = excludePropertyTypes(
this.schema as any,
(i) => isPlainObject(i) && mcpSchemaSymbol in i,
);
const schema = s.strictObject(props);
return rescursiveClean(schema, {
removeRequired: true,
removeDefault: false,
}) as s.ObjectSchema<any, any>;
}
getToolOptions(suffix?: string) {
const { tools, resources, ...rest } = this.options;
const label = (text?: string) =>
text && [suffix && autoFormatString(suffix), text].filter(Boolean).join(" ");
return {
title: label(this.options.title ?? this.schema.title),
description: label(this.options.description ?? this.schema.description),
annotations: {
destructiveHint: true,
idempotentHint: true,
...rest.annotations,
},
};
}
}

View File

@@ -1 +1,3 @@
export * from "./$object"; export * from "./$object";
export * from "./$record";
export * from "./McpSchemaHelper";

View File

@@ -35,16 +35,15 @@ export function rescursiveClean(
export function excludePropertyTypes( export function excludePropertyTypes(
input: s.ObjectSchema<any, any>, input: s.ObjectSchema<any, any>,
props: (new (...args: any[]) => s.Schema)[], props: (instance: s.Schema | unknown) => boolean,
): s.TProperties { ): s.TProperties {
const properties = { ...input.properties }; const properties = { ...input.properties };
return transformObject(properties, (value, key) => { return transformObject(properties, (value, key) => {
for (const prop of props) { if (props(value)) {
if (value instanceof prop) {
return undefined; return undefined;
} }
}
return value; return value;
}); });
} }

View File

@@ -26,6 +26,7 @@ import {
} from "modules/ModuleManager"; } from "modules/ModuleManager";
import * as SystemPermissions from "modules/permissions"; import * as SystemPermissions from "modules/permissions";
import { getVersion } from "core/env"; import { getVersion } from "core/env";
import type { Module } from "modules/Module";
export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = { export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = {
success: true; success: true;
@@ -357,4 +358,46 @@ export class SystemController extends Controller {
return hono; return hono;
} }
override registerMcp() {
const { mcp } = this.app.modules.ctx();
const { version, ...appConfig } = this.app.toJSON();
mcp.resource("system_config", "bknd://system/config", (c) =>
c.json(this.app.toJSON(), {
title: "System Config",
}),
)
.resource(
"system_config_module",
"bknd://system/config/{module}",
(c, { module }) => {
const m = this.app.modules.get(module as any) as Module;
return c.json(m.toJSON(), {
title: `Config for ${module}`,
});
},
{
list: Object.keys(appConfig),
},
)
.resource("system_schema", "bknd://system/schema", (c) =>
c.json(this.app.getSchema(), {
title: "System Schema",
}),
)
.resource(
"system_schema_module",
"bknd://system/schema/{module}",
(c, { module }) => {
const m = this.app.modules.get(module as any);
return c.json(m.getSchema().toJSON(), {
title: `Schema for ${module}`,
});
},
{
list: Object.keys(this.app.getSchema()),
},
);
}
} }