mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +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:ui": "playwright test --ui",
|
||||
"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",
|
||||
"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;
|
||||
await this.modules.build({ fetch: options?.fetch });
|
||||
|
||||
const { guard, server } = this.modules.ctx();
|
||||
const { guard } = this.modules.ctx();
|
||||
|
||||
// load system controller
|
||||
guard.registerPermissions(Object.values(SystemPermissions));
|
||||
const systemController = new SystemController(this);
|
||||
systemController.registerMcp();
|
||||
server.route("/api/system", systemController.getController());
|
||||
systemController.register(this);
|
||||
|
||||
// emit built event
|
||||
$console.log("App built");
|
||||
|
||||
@@ -1,104 +1,85 @@
|
||||
import type { CliCommand } from "cli/types";
|
||||
import { makeAppFromEnv } from "../run";
|
||||
import { s, mcp as mcpMiddleware, McpServer, isObject, getMcpServer } from "bknd/utils";
|
||||
import type { McpSchema } from "modules/mcp";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { Hono } from "hono";
|
||||
import { mcpSchemaSymbol } from "modules/mcp/McpSchemaHelper";
|
||||
import { getVersion } from "cli/utils/sys";
|
||||
import { getSystemMcp } from "modules/server/system-mcp";
|
||||
import { $console } from "bknd/utils";
|
||||
import { stdioTransport } from "jsonv-ts/mcp";
|
||||
|
||||
export const mcp: CliCommand = (program) =>
|
||||
program
|
||||
.command("mcp")
|
||||
.description("mcp server")
|
||||
.option("--verbose", "verbose output")
|
||||
.description("mcp server stdio transport")
|
||||
.option("--config <config>", "config file")
|
||||
.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(
|
||||
"--token <token>",
|
||||
"token to authenticate requests, if not provided, uses BEARER_TOKEN environment variable",
|
||||
)
|
||||
.option("--verbose", "verbose output")
|
||||
.option("--log-level <level>", "log level")
|
||||
.option("--force", "force enable mcp")
|
||||
.action(action);
|
||||
|
||||
async function action(options: {
|
||||
verbose?: boolean;
|
||||
config?: string;
|
||||
dbUrl?: string;
|
||||
port?: string;
|
||||
path?: string;
|
||||
token?: 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({
|
||||
config: options.config,
|
||||
dbUrl: options.dbUrl,
|
||||
server: "node",
|
||||
});
|
||||
|
||||
const token = options.token || process.env.BEARER_TOKEN;
|
||||
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 = [
|
||||
...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,
|
||||
});
|
||||
if (!app.modules.get("server").config.mcp.enabled && !options.force) {
|
||||
$console.enable();
|
||||
Object.assign(console, __oldConsole);
|
||||
console.error("MCP is not enabled in the config, use --force to enable it");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const hono = new Hono().use(
|
||||
mcpMiddleware({
|
||||
server,
|
||||
sessionsEnabled: true,
|
||||
debug: {
|
||||
logLevel: options.logLevel as any,
|
||||
explainEndpoint: true,
|
||||
},
|
||||
endpoint: {
|
||||
path: String(options.path) as any,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const token = options.token || process.env.BEARER_TOKEN;
|
||||
const server = getSystemMcp(app);
|
||||
|
||||
serve({
|
||||
fetch: hono.fetch,
|
||||
port: Number(options.port) || 3000,
|
||||
});
|
||||
|
||||
if (options.verbose) {
|
||||
console.info(`Server is running on http://localhost:${options.port}${options.path}`);
|
||||
if (verbose) {
|
||||
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(
|
||||
`📚 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";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to resolve version");
|
||||
//console.error("Failed to resolve version");
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
|
||||
@@ -76,6 +76,7 @@ declare global {
|
||||
| {
|
||||
level: TConsoleSeverity;
|
||||
id?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
@@ -86,6 +87,7 @@ const defaultLevel = env("cli_log_level", "log") as TConsoleSeverity;
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
|
||||
const config = (globalThis.__consoleConfig ??= {
|
||||
level: defaultLevel,
|
||||
enabled: true,
|
||||
//id: crypto.randomUUID(), // for debugging
|
||||
});
|
||||
|
||||
@@ -95,6 +97,14 @@ export const $console = new Proxy(config as any, {
|
||||
switch (prop) {
|
||||
case "original":
|
||||
return console;
|
||||
case "disable":
|
||||
return () => {
|
||||
config.enabled = false;
|
||||
};
|
||||
case "enable":
|
||||
return () => {
|
||||
config.enabled = true;
|
||||
};
|
||||
case "setLevel":
|
||||
return (l: TConsoleSeverity) => {
|
||||
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 requested = keys.indexOf(prop as string);
|
||||
|
||||
@@ -118,6 +132,8 @@ export const $console = new Proxy(config as any, {
|
||||
} & {
|
||||
setLevel: (l: TConsoleSeverity) => void;
|
||||
resetLevel: () => void;
|
||||
disable: () => void;
|
||||
enable: () => void;
|
||||
};
|
||||
|
||||
export function colorizeConsole(con: typeof console) {
|
||||
|
||||
@@ -9,6 +9,7 @@ export {
|
||||
type ConnQueryResults,
|
||||
customIntrospector,
|
||||
} from "./Connection";
|
||||
export { DummyConnection } from "./DummyConnection";
|
||||
|
||||
// sqlite
|
||||
export { SqliteConnection } from "./sqlite/SqliteConnection";
|
||||
|
||||
@@ -130,6 +130,7 @@ export {
|
||||
BaseIntrospector,
|
||||
Connection,
|
||||
customIntrospector,
|
||||
DummyConnection,
|
||||
type FieldSpec,
|
||||
type IndexSpec,
|
||||
type DbFunctions,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
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/*"],
|
||||
"bknd": ["./src/index.ts"],
|
||||
"bknd/utils": ["./src/core/utils/index.ts"],
|
||||
"bknd/core": ["./src/core/index.ts"],
|
||||
"bknd/adapter": ["./src/adapter/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"]
|
||||
"bknd/client": ["./src/ui/client/index.ts"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -13,10 +13,12 @@ export default async function Page(props: {
|
||||
if (!page) notFound();
|
||||
|
||||
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 (
|
||||
<DocsPage
|
||||
toc={page.data.toc}
|
||||
toc={toc}
|
||||
full={page.data.full}
|
||||
tableOfContent={{
|
||||
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)/",
|
||||
"---Modules---",
|
||||
"./modules/overview",
|
||||
"./modules/server",
|
||||
"./modules/server/",
|
||||
"./modules/data",
|
||||
"./modules/auth",
|
||||
"./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": {
|
||||
"": {
|
||||
"name": "bknd-docs",
|
||||
"version": "0.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@iconify/react": "^6.0.0",
|
||||
@@ -36,6 +35,7 @@
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.3.5",
|
||||
"fumadocs-docgen": "^2.1.0",
|
||||
"jsonv-ts": "^0.7.0",
|
||||
"postcss": "^8.5.6",
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
@@ -6816,6 +6816,17 @@
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
||||
@@ -7525,6 +7536,19 @@
|
||||
"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": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev:turbo": "next dev --turbo",
|
||||
"build": "bun generate:openapi && next build",
|
||||
"build": "bun generate:openapi && bun generate:mcp && next build",
|
||||
"start": "next start",
|
||||
"generate:openapi": "bun scripts/generate-openapi.mjs",
|
||||
"generate:mcp": "bun scripts/generate-mcp.ts",
|
||||
"postinstall": "fumadocs-mdx",
|
||||
"preview": "npm run build && wrangler dev",
|
||||
"cf:preview": "wrangler dev",
|
||||
@@ -42,6 +43,7 @@
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.3.5",
|
||||
"fumadocs-docgen": "^2.1.0",
|
||||
"jsonv-ts": "^0.7.0",
|
||||
"postcss": "^8.5.6",
|
||||
"rimraf": "^6.0.1",
|
||||
"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