mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
auto generated tools docs, added stdio transport, added additional mcp config and permissions
This commit is contained in:
35
app/internal/docs.build-assets.ts
Normal file
35
app/internal/docs.build-assets.ts
Normal 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();
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ export {
|
|||||||
BaseIntrospector,
|
BaseIntrospector,
|
||||||
Connection,
|
Connection,
|
||||||
customIntrospector,
|
customIntrospector,
|
||||||
|
DummyConnection,
|
||||||
type FieldSpec,
|
type FieldSpec,
|
||||||
type IndexSpec,
|
type IndexSpec,
|
||||||
type DbFunctions,
|
type DbFunctions,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
36
app/src/modules/server/system-mcp.ts
Normal file
36
app/src/modules/server/system-mcp.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
55
docs/components/McpTool.tsx
Normal file
55
docs/components/McpTool.tsx
Normal 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;
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
361
docs/content/docs/(documentation)/modules/server/mcp.mdx
Normal file
361
docs/content/docs/(documentation)/modules/server/mcp.mdx
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"pages": ["overview", "mcp"]
|
||||||
|
}
|
||||||
3047
docs/mcp.json
Normal file
3047
docs/mcp.json
Normal file
File diff suppressed because it is too large
Load Diff
26
docs/package-lock.json
generated
26
docs/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
65
docs/scripts/generate-mcp.ts
Normal file
65
docs/scripts/generate-mcp.ts
Normal 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();
|
||||||
Reference in New Issue
Block a user