auto generated tools docs, added stdio transport, added additional mcp config and permissions

This commit is contained in:
dswbx
2025-08-09 14:14:51 +02:00
parent 170ea2c45b
commit cb873381f1
25 changed files with 3770 additions and 87 deletions

View File

@@ -116,12 +116,14 @@ export class ModuleHelper {
async throwUnlessGranted(
permission: Permission | string,
c: { context: ModuleBuildContextMcpContext; request: Request },
c: { context: ModuleBuildContextMcpContext; raw?: unknown },
) {
invariant(c.context.app, "app is not available in mcp context");
invariant(c.request instanceof Request, "request is not available in mcp context");
invariant(c.raw instanceof Request, "request is not available in mcp context");
const user = await c.context.app.module.auth.authenticator.resolveAuthFromRequest(c.request);
const user = await c.context.app.module.auth.authenticator.resolveAuthFromRequest(
c.raw as Request,
);
if (!this.ctx.guard.granted(permission, user)) {
throw new Exception(

View File

@@ -6,6 +6,7 @@ import {
type McpSchema,
type SchemaWithMcpOptions,
} from "./McpSchemaHelper";
import type { Module } from "modules/Module";
export interface ObjectToolSchemaOptions extends s.IObjectOptions, SchemaWithMcpOptions {}
@@ -80,13 +81,36 @@ export class ObjectToolSchema<
...this.mcp.getToolOptions("update"),
inputSchema: s.strictObject({
full: s.boolean({ default: false }).optional(),
value: s
.strictObject(schema.properties as any)
.partial() as unknown as s.ObjectSchema<P, O>,
return_config: s
.boolean({
default: false,
description: "If the new configuration should be returned",
})
.optional(),
value: s.strictObject(schema.properties as {}).partial(),
}),
},
async (params, ctx: AppToolHandlerCtx) => {
return ctx.json(params);
const { full, value, return_config } = params;
const [module_name] = node.instancePath;
if (full) {
await ctx.context.app.mutateConfig(module_name as any).set(value);
} else {
await ctx.context.app.mutateConfig(module_name as any).patch("", value);
}
let config: any = undefined;
if (return_config) {
const configs = ctx.context.app.toJSON();
config = getPath(configs, node.instancePath);
}
return ctx.json({
success: true,
module: module_name,
config,
});
},
);
}

View File

@@ -7,3 +7,4 @@ export const configReadSecrets = new Permission("system.config.read.secrets");
export const configWrite = new Permission("system.config.write");
export const schemaRead = new Permission("system.schema.read");
export const build = new Permission("system.build");
export const mcp = new Permission("system.mcp");

View File

@@ -22,6 +22,9 @@ export const serverConfigSchema = $object(
}),
allow_credentials: s.boolean({ default: true }),
}),
mcp: s.strictObject({
enabled: s.boolean({ default: false }),
}),
},
{
description: "Server configuration",

View File

@@ -14,6 +14,7 @@ import {
InvalidSchemaError,
openAPISpecs,
mcpTool,
mcp as mcpMiddleware,
} from "bknd/utils";
import type { Context, Hono } from "hono";
import { Controller } from "modules/Controller";
@@ -27,6 +28,7 @@ import {
import * as SystemPermissions from "modules/permissions";
import { getVersion } from "core/env";
import type { Module } from "modules/Module";
import { getSystemMcp } from "./system-mcp";
export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = {
success: true;
@@ -52,6 +54,32 @@ export class SystemController extends Controller {
return this.app.modules.ctx();
}
register(app: App) {
app.server.route("/api/system", this.getController());
if (!this.app.modules.get("server").config.mcp.enabled) {
return;
}
this.registerMcp();
const mcpServer = getSystemMcp(app);
app.server.use(
mcpMiddleware({
server: mcpServer,
sessionsEnabled: true,
debug: {
logLevel: "debug",
explainEndpoint: true,
},
endpoint: {
path: "/mcp",
},
}),
);
}
private registerConfigController(client: Hono<any>): void {
const { permission } = this.middlewares;
// don't add auth again, it's already added in getController

View File

@@ -0,0 +1,36 @@
import type { App } from "App";
import { mcpSchemaSymbol, type McpSchema } from "modules/mcp";
import { getMcpServer, isObject, s, McpServer } from "bknd/utils";
import { getVersion } from "core/env";
export function getSystemMcp(app: App) {
const middlewareServer = getMcpServer(app.server);
const appConfig = app.modules.configs();
const { version, ...appSchema } = app.getSchema();
const schema = s.strictObject(appSchema);
const nodes = [...schema.walk({ data: appConfig })].filter(
(n) => isObject(n.schema) && mcpSchemaSymbol in n.schema,
) as s.Node<McpSchema>[];
const tools = [
// tools from hono routes
...middlewareServer.tools,
// tools added from ctx
...app.modules.ctx().mcp.tools,
// tools from app schema
...nodes.flatMap((n) => n.schema.getTools(n)),
];
const resources = [...middlewareServer.resources, ...app.modules.ctx().mcp.resources];
return new McpServer(
{
name: "bknd",
version: getVersion(),
},
{ app, ctx: () => app.modules.ctx() },
tools,
resources,
);
}