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

@@ -0,0 +1,35 @@
import { createApp } from "bknd/adapter/bun";
async function generate() {
console.info("Generating MCP documentation...");
const app = await createApp({
initialConfig: {
server: {
mcp: {
enabled: true,
},
},
auth: {
enabled: true,
},
media: {
enabled: true,
adapter: {
type: "local",
config: {
path: "./",
},
},
},
},
});
await app.build();
const res = await app.server.request("/mcp?explain=1");
const { tools, resources } = await res.json();
await Bun.write("../docs/mcp.json", JSON.stringify({ tools, resources }, null, 2));
console.info("MCP documentation generated.");
}
void generate();

View File

@@ -43,7 +43,8 @@
"test:e2e:adapters": "bun run e2e/adapters.ts", "test:e2e:adapters": "bun run e2e/adapters.ts",
"test:e2e:ui": "playwright test --ui", "test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug", "test:e2e:debug": "playwright test --debug",
"test:e2e:report": "playwright show-report" "test:e2e:report": "playwright show-report",
"docs:build-assets": "bun internal/docs.build-assets.ts"
}, },
"license": "FSL-1.1-MIT", "license": "FSL-1.1-MIT",
"dependencies": { "dependencies": {

View File

@@ -168,13 +168,12 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
if (options?.sync) this.modules.ctx().flags.sync_required = true; if (options?.sync) this.modules.ctx().flags.sync_required = true;
await this.modules.build({ fetch: options?.fetch }); await this.modules.build({ fetch: options?.fetch });
const { guard, server } = this.modules.ctx(); const { guard } = this.modules.ctx();
// load system controller // load system controller
guard.registerPermissions(Object.values(SystemPermissions)); guard.registerPermissions(Object.values(SystemPermissions));
const systemController = new SystemController(this); const systemController = new SystemController(this);
systemController.registerMcp(); systemController.register(this);
server.route("/api/system", systemController.getController());
// emit built event // emit built event
$console.log("App built"); $console.log("App built");

View File

@@ -1,104 +1,85 @@
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, isObject, getMcpServer } from "bknd/utils"; import { getSystemMcp } from "modules/server/system-mcp";
import type { McpSchema } from "modules/mcp"; import { $console } from "bknd/utils";
import { serve } from "@hono/node-server"; import { stdioTransport } from "jsonv-ts/mcp";
import { Hono } from "hono";
import { mcpSchemaSymbol } from "modules/mcp/McpSchemaHelper";
import { getVersion } from "cli/utils/sys";
export const mcp: CliCommand = (program) => export const mcp: CliCommand = (program) =>
program program
.command("mcp") .command("mcp")
.description("mcp server") .description("mcp server stdio transport")
.option("--verbose", "verbose output")
.option("--config <config>", "config file") .option("--config <config>", "config file")
.option("--db-url <db>", "database url, can be any valid sqlite url") .option("--db-url <db>", "database url, can be any valid sqlite url")
.option("--port <port>", "port to listen on", "3000")
.option("--path <path>", "path to listen on", "/mcp")
.option( .option(
"--token <token>", "--token <token>",
"token to authenticate requests, if not provided, uses BEARER_TOKEN environment variable", "token to authenticate requests, if not provided, uses BEARER_TOKEN environment variable",
) )
.option("--verbose", "verbose output")
.option("--log-level <level>", "log level") .option("--log-level <level>", "log level")
.option("--force", "force enable mcp")
.action(action); .action(action);
async function action(options: { async function action(options: {
verbose?: boolean; verbose?: boolean;
config?: string; config?: string;
dbUrl?: string; dbUrl?: string;
port?: string;
path?: string;
token?: string; token?: string;
logLevel?: string; logLevel?: string;
force?: boolean;
}) { }) {
const verbose = !!options.verbose;
const __oldConsole = { ...console };
// disable console
if (!verbose) {
$console.disable();
Object.entries(console).forEach(([key]) => {
console[key] = () => null;
});
}
const app = await makeAppFromEnv({ const app = await makeAppFromEnv({
config: options.config, config: options.config,
dbUrl: options.dbUrl, dbUrl: options.dbUrl,
server: "node", server: "node",
}); });
const token = options.token || process.env.BEARER_TOKEN; if (!app.modules.get("server").config.mcp.enabled && !options.force) {
const middlewareServer = getMcpServer(app.server); $console.enable();
Object.assign(console, __oldConsole);
const appConfig = app.modules.configs(); console.error("MCP is not enabled in the config, use --force to enable it");
const { version, ...appSchema } = app.getSchema(); process.exit(1);
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 = [
...middlewareServer.tools,
...app.modules.ctx().mcp.tools,
...nodes.flatMap((n) => n.schema.getTools(n)),
];
const resources = [...middlewareServer.resources, ...app.modules.ctx().mcp.resources];
const server = new McpServer(
{
name: "bknd",
version: await getVersion(),
},
{ app, ctx: () => app.modules.ctx() },
tools,
resources,
);
if (token) {
server.setAuthentication({
type: "bearer",
token,
});
} }
const hono = new Hono().use( const token = options.token || process.env.BEARER_TOKEN;
mcpMiddleware({ const server = getSystemMcp(app);
server,
sessionsEnabled: true,
debug: {
logLevel: options.logLevel as any,
explainEndpoint: true,
},
endpoint: {
path: String(options.path) as any,
},
}),
);
serve({ if (verbose) {
fetch: hono.fetch,
port: Number(options.port) || 3000,
});
if (options.verbose) {
console.info(`Server is running on http://localhost:${options.port}${options.path}`);
console.info( console.info(
`⚙️ Tools (${server.tools.length}):\n${server.tools.map((t) => `- ${t.name}`).join("\n")}\n`, `\n⚙️ Tools (${server.tools.length}):\n${server.tools.map((t) => `- ${t.name}`).join("\n")}\n`,
); );
console.info( console.info(
`📚 Resources (${server.resources.length}):\n${server.resources.map((r) => `- ${r.name}`).join("\n")}`, `📚 Resources (${server.resources.length}):\n${server.resources.map((r) => `- ${r.name}`).join("\n")}`,
); );
console.info("\nMCP server is running on STDIO transport");
}
if (options.logLevel) {
server.setLogLevel(options.logLevel as any);
}
const stdout = process.stdout;
const stdin = process.stdin;
const stderr = process.stderr;
{
using transport = stdioTransport(server, {
stdin,
stdout,
stderr,
raw: new Request("https://localhost", {
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
}),
});
} }
} }

View File

@@ -26,7 +26,7 @@ export async function getVersion(_path: string = "") {
return JSON.parse(pkg).version ?? "preview"; return JSON.parse(pkg).version ?? "preview";
} }
} catch (e) { } catch (e) {
console.error("Failed to resolve version"); //console.error("Failed to resolve version");
} }
return "unknown"; return "unknown";

View File

@@ -76,6 +76,7 @@ declare global {
| { | {
level: TConsoleSeverity; level: TConsoleSeverity;
id?: string; id?: string;
enabled?: boolean;
} }
| undefined; | undefined;
} }
@@ -86,6 +87,7 @@ const defaultLevel = env("cli_log_level", "log") as TConsoleSeverity;
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation> // biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
const config = (globalThis.__consoleConfig ??= { const config = (globalThis.__consoleConfig ??= {
level: defaultLevel, level: defaultLevel,
enabled: true,
//id: crypto.randomUUID(), // for debugging //id: crypto.randomUUID(), // for debugging
}); });
@@ -95,6 +97,14 @@ export const $console = new Proxy(config as any, {
switch (prop) { switch (prop) {
case "original": case "original":
return console; return console;
case "disable":
return () => {
config.enabled = false;
};
case "enable":
return () => {
config.enabled = true;
};
case "setLevel": case "setLevel":
return (l: TConsoleSeverity) => { return (l: TConsoleSeverity) => {
config.level = l; config.level = l;
@@ -105,6 +115,10 @@ export const $console = new Proxy(config as any, {
}; };
} }
if (!config.enabled) {
return () => null;
}
const current = keys.indexOf(config.level); const current = keys.indexOf(config.level);
const requested = keys.indexOf(prop as string); const requested = keys.indexOf(prop as string);
@@ -118,6 +132,8 @@ export const $console = new Proxy(config as any, {
} & { } & {
setLevel: (l: TConsoleSeverity) => void; setLevel: (l: TConsoleSeverity) => void;
resetLevel: () => void; resetLevel: () => void;
disable: () => void;
enable: () => void;
}; };
export function colorizeConsole(con: typeof console) { export function colorizeConsole(con: typeof console) {

View File

@@ -9,6 +9,7 @@ export {
type ConnQueryResults, type ConnQueryResults,
customIntrospector, customIntrospector,
} from "./Connection"; } from "./Connection";
export { DummyConnection } from "./DummyConnection";
// sqlite // sqlite
export { SqliteConnection } from "./sqlite/SqliteConnection"; export { SqliteConnection } from "./sqlite/SqliteConnection";

View File

@@ -130,6 +130,7 @@ export {
BaseIntrospector, BaseIntrospector,
Connection, Connection,
customIntrospector, customIntrospector,
DummyConnection,
type FieldSpec, type FieldSpec,
type IndexSpec, type IndexSpec,
type DbFunctions, type DbFunctions,

View File

@@ -116,12 +116,14 @@ export class ModuleHelper {
async throwUnlessGranted( async throwUnlessGranted(
permission: Permission | string, 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.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)) { if (!this.ctx.guard.granted(permission, user)) {
throw new Exception( throw new Exception(

View File

@@ -6,6 +6,7 @@ import {
type McpSchema, type McpSchema,
type SchemaWithMcpOptions, type SchemaWithMcpOptions,
} from "./McpSchemaHelper"; } from "./McpSchemaHelper";
import type { Module } from "modules/Module";
export interface ObjectToolSchemaOptions extends s.IObjectOptions, SchemaWithMcpOptions {} export interface ObjectToolSchemaOptions extends s.IObjectOptions, SchemaWithMcpOptions {}
@@ -80,13 +81,36 @@ export class ObjectToolSchema<
...this.mcp.getToolOptions("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 return_config: s
.strictObject(schema.properties as any) .boolean({
.partial() as unknown as s.ObjectSchema<P, O>, default: false,
description: "If the new configuration should be returned",
})
.optional(),
value: s.strictObject(schema.properties as {}).partial(),
}), }),
}, },
async (params, ctx: AppToolHandlerCtx) => { 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 configWrite = new Permission("system.config.write");
export const schemaRead = new Permission("system.schema.read"); export const schemaRead = new Permission("system.schema.read");
export const build = new Permission("system.build"); 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 }), allow_credentials: s.boolean({ default: true }),
}), }),
mcp: s.strictObject({
enabled: s.boolean({ default: false }),
}),
}, },
{ {
description: "Server configuration", description: "Server configuration",

View File

@@ -14,6 +14,7 @@ import {
InvalidSchemaError, InvalidSchemaError,
openAPISpecs, openAPISpecs,
mcpTool, mcpTool,
mcp as mcpMiddleware,
} from "bknd/utils"; } from "bknd/utils";
import type { Context, Hono } from "hono"; import type { Context, Hono } from "hono";
import { Controller } from "modules/Controller"; import { Controller } from "modules/Controller";
@@ -27,6 +28,7 @@ import {
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"; import type { Module } from "modules/Module";
import { getSystemMcp } from "./system-mcp";
export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = { export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = {
success: true; success: true;
@@ -52,6 +54,32 @@ export class SystemController extends Controller {
return this.app.modules.ctx(); 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 { private registerConfigController(client: Hono<any>): void {
const { permission } = this.middlewares; const { permission } = this.middlewares;
// don't add auth again, it's already added in getController // 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,
);
}

View File

@@ -32,12 +32,8 @@
"*": ["./src/*"], "*": ["./src/*"],
"bknd": ["./src/index.ts"], "bknd": ["./src/index.ts"],
"bknd/utils": ["./src/core/utils/index.ts"], "bknd/utils": ["./src/core/utils/index.ts"],
"bknd/core": ["./src/core/index.ts"],
"bknd/adapter": ["./src/adapter/index.ts"], "bknd/adapter": ["./src/adapter/index.ts"],
"bknd/client": ["./src/ui/client/index.ts"], "bknd/client": ["./src/ui/client/index.ts"]
"bknd/data": ["./src/data/index.ts"],
"bknd/media": ["./src/media/index.ts"],
"bknd/auth": ["./src/auth/index.ts"]
} }
}, },
"include": [ "include": [

View File

@@ -13,10 +13,12 @@ export default async function Page(props: {
if (!page) notFound(); if (!page) notFound();
const MDXContent = page.data.body; const MDXContent = page.data.body;
// in case a page exports a custom toc
const toc = (page.data as any).custom_toc ?? page.data.toc;
return ( return (
<DocsPage <DocsPage
toc={page.data.toc} toc={toc}
full={page.data.full} full={page.data.full}
tableOfContent={{ tableOfContent={{
style: "clerk", style: "clerk",

View File

@@ -0,0 +1,55 @@
import type { Tool } from "jsonv-ts/mcp";
import components from "fumadocs-ui/mdx";
import { TypeTable } from "fumadocs-ui/components/type-table";
import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
import type { JSONSchemaDefinition } from "jsonv-ts";
export const slugify = (s: string) => s.toLowerCase().replace(/ /g, "-");
export const indent = (s: string, indent = 2) => s.replace(/^/gm, " ".repeat(indent));
export function McpTool({ tool }: { tool: ReturnType<Tool["toJSON"]> }) {
return (
<div>
<components.h3 id={slugify(tool.name)}>
<code>{tool.name}</code>
</components.h3>
<p>{tool.description}</p>
<JsonSchemaTypeTable schema={tool.inputSchema} />
</div>
);
}
export function JsonSchemaTypeTable({ schema }: { schema: JSONSchemaDefinition }) {
const properties = schema.properties ?? {};
const required = schema.required ?? [];
const getTypeDescription = (value: any) =>
JSON.stringify(
{
...value,
$target: undefined,
},
null,
2,
);
return Object.keys(properties).length > 0 ? (
<TypeTable
type={Object.fromEntries(
Object.entries(properties).map(([key, value]: [string, JSONSchemaDefinition]) => [
key,
{
description: value.description,
typeDescription: (
<DynamicCodeBlock lang="json" code={indent(getTypeDescription(value), 1)} />
),
type: value.type,
default: value.default ? JSON.stringify(value.default) : undefined,
required: required.includes(key),
},
]),
)}
/>
) : null;
}

View File

@@ -24,7 +24,7 @@
"./integration/(runtimes)/", "./integration/(runtimes)/",
"---Modules---", "---Modules---",
"./modules/overview", "./modules/overview",
"./modules/server", "./modules/server/",
"./modules/data", "./modules/data",
"./modules/auth", "./modules/auth",
"./modules/media", "./modules/media",

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
{
"pages": ["overview", "mcp"]
}

3047
docs/mcp.json Normal file

File diff suppressed because it is too large Load Diff

26
docs/package-lock.json generated
View File

@@ -6,7 +6,6 @@
"packages": { "packages": {
"": { "": {
"name": "bknd-docs", "name": "bknd-docs",
"version": "0.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@iconify/react": "^6.0.0", "@iconify/react": "^6.0.0",
@@ -36,6 +35,7 @@
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "15.3.5", "eslint-config-next": "15.3.5",
"fumadocs-docgen": "^2.1.0", "fumadocs-docgen": "^2.1.0",
"jsonv-ts": "^0.7.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
@@ -6816,6 +6816,17 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/hono": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.9.0.tgz",
"integrity": "sha512-JAUc4Sqi3lhby2imRL/67LMcJFKiCu7ZKghM7iwvltVZzxEC5bVJCsAa4NTnSfmWGb+N2eOVtFE586R+K3fejA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/html-void-elements": { "node_modules/html-void-elements": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
@@ -7525,6 +7536,19 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/jsonv-ts": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/jsonv-ts/-/jsonv-ts-0.7.0.tgz",
"integrity": "sha512-zN5/KMs1WOs+0IbYiZF7mVku4dum8LKP9xv8VqgVm+PBz5VZuU1V8iLQhI991ogUbhGHHlOCwqxnxQUuvCPbQA==",
"dev": true,
"license": "MIT",
"optionalDependencies": {
"hono": "*"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
},
"node_modules/jsx-ast-utils": { "node_modules/jsx-ast-utils": {
"version": "3.3.5", "version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",

View File

@@ -4,9 +4,10 @@
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"dev:turbo": "next dev --turbo", "dev:turbo": "next dev --turbo",
"build": "bun generate:openapi && next build", "build": "bun generate:openapi && bun generate:mcp && next build",
"start": "next start", "start": "next start",
"generate:openapi": "bun scripts/generate-openapi.mjs", "generate:openapi": "bun scripts/generate-openapi.mjs",
"generate:mcp": "bun scripts/generate-mcp.ts",
"postinstall": "fumadocs-mdx", "postinstall": "fumadocs-mdx",
"preview": "npm run build && wrangler dev", "preview": "npm run build && wrangler dev",
"cf:preview": "wrangler dev", "cf:preview": "wrangler dev",
@@ -42,6 +43,7 @@
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "15.3.5", "eslint-config-next": "15.3.5",
"fumadocs-docgen": "^2.1.0", "fumadocs-docgen": "^2.1.0",
"jsonv-ts": "^0.7.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",

View File

@@ -0,0 +1,65 @@
import type { Tool, Resource } from "jsonv-ts/mcp";
import { rimraf } from "rimraf";
const config = {
mcpConfig: "./mcp.json",
outFile: "./content/docs/(documentation)/modules/server/mcp.mdx",
};
async function generate() {
console.info("Generating MCP documentation...");
await cleanup();
const mcpConfig = await Bun.file(config.mcpConfig).json();
const document = await generateDocument(mcpConfig);
await Bun.write(config.outFile, document);
console.info("MCP documentation generated.");
}
async function generateDocument({
tools,
resources,
}: {
tools: ReturnType<Tool["toJSON"]>[];
resources: ReturnType<Resource["toJSON"]>[];
}) {
return `---
title: "MCP"
description: "Built-in full featured MCP server."
tags: ["documentation"]
---
import { JsonSchemaTypeTable } from '@/components/McpTool';
## Tools
${tools
.map(
(t) => `
### ${t.name}
${t.description ?? ""}
<JsonSchemaTypeTable schema={${JSON.stringify(t.inputSchema)}} key={"${String(t.name)}"} />`,
)
.join("\n")}
## Resources
${resources
.map(
(r) => `
### ${r.name}
${r.description ?? ""}
`,
)
.join("\n")}
`;
}
async function cleanup() {
await rimraf(config.outFile);
}
void generate();