Compare commits

...

8 Commits

35 changed files with 2189 additions and 1388 deletions
+22 -1
View File
@@ -23,13 +23,34 @@ import type { IEmailDriver, ICacheDriver } from "core/drivers";
import { Api, type ApiOptions } from "Api"; import { Api, type ApiOptions } from "Api";
export type AppPluginConfig = { export type AppPluginConfig = {
/**
* The name of the plugin.
*/
name: string; name: string;
/**
* The schema of the plugin.
*/
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>; schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
/**
* Called before the app is built.
*/
beforeBuild?: () => MaybePromise<void>; beforeBuild?: () => MaybePromise<void>;
/**
* Called after the app is built.
*/
onBuilt?: () => MaybePromise<void>; onBuilt?: () => MaybePromise<void>;
/**
* Called when the server is initialized.
*/
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>; onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
onFirstBoot?: () => MaybePromise<void>; /**
* Called when the app is booted.
*/
onBoot?: () => MaybePromise<void>; onBoot?: () => MaybePromise<void>;
/**
* Called when the app is first booted.
*/
onFirstBoot?: () => MaybePromise<void>;
}; };
export type AppPlugin = (app: App) => AppPluginConfig; export type AppPlugin = (app: App) => AppPluginConfig;
+6 -2
View File
@@ -11,7 +11,7 @@ type BunEnv = Bun.Env;
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">; export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
export async function createApp<Env = BunEnv>( export async function createApp<Env = BunEnv>(
{ distPath, ...config }: BunBkndConfig<Env> = {}, { distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
args: Env = {} as Env, args: Env = {} as Env,
opts?: RuntimeOptions, opts?: RuntimeOptions,
) { ) {
@@ -20,7 +20,11 @@ export async function createApp<Env = BunEnv>(
return await createRuntimeApp( return await createRuntimeApp(
{ {
serveStatic: serveStatic({ root }), serveStatic:
_serveStatic ??
serveStatic({
root,
}),
...config, ...config,
}, },
args ?? (process.env as Env), args ?? (process.env as Env),
+1 -1
View File
@@ -6,7 +6,7 @@ import { resolve } from "node:path";
* Vite plugin that provides Node.js filesystem access during development * Vite plugin that provides Node.js filesystem access during development
* by injecting a polyfill into the SSR environment * by injecting a polyfill into the SSR environment
*/ */
export function devFsPlugin({ export function devFsVitePlugin({
verbose = false, verbose = false,
configFile = "bknd.config.ts", configFile = "bknd.config.ts",
}: { }: {
+1
View File
@@ -14,6 +14,7 @@ import { usersFields } from "./auth-entities";
import { Authenticator } from "./authenticate/Authenticator"; import { Authenticator } from "./authenticate/Authenticator";
import { Role } from "./authorize/Role"; import { Role } from "./authorize/Role";
export type UsersFields = typeof AppAuth.usersFields;
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>; export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
declare module "bknd" { declare module "bknd" {
interface Users extends AppEntity, UserFieldSchema {} interface Users extends AppEntity, UserFieldSchema {}
+15
View File
@@ -39,6 +39,9 @@ import {
type PolymorphicRelationConfig, type PolymorphicRelationConfig,
} from "data/relations"; } from "data/relations";
import type { MediaFields } from "media/AppMedia";
import type { UsersFields } from "auth/AppAuth";
type Options<Config = any> = { type Options<Config = any> = {
entity: { name: string; fields: Record<string, Field<any, any, any>> }; entity: { name: string; fields: Record<string, Field<any, any, any>> };
field_name: string; field_name: string;
@@ -199,6 +202,18 @@ export function entity<
return new Entity(name, _fields, config, type); return new Entity(name, _fields, config, type);
} }
type SystemEntities = {
users: UsersFields;
media: MediaFields;
};
export function systemEntity<
E extends keyof SystemEntities,
Fields extends Record<string, Field<any, any, any>>,
>(name: E, fields: Fields) {
return entity<E, SystemEntities[E] & Fields>(name, fields as any);
}
export function relation<Local extends Entity>(local: Local) { export function relation<Local extends Entity>(local: Local) {
return { return {
manyToOne: <Foreign extends Entity>(foreign: Foreign, config?: ManyToOneRelationConfig) => { manyToOne: <Foreign extends Entity>(foreign: Foreign, config?: ManyToOneRelationConfig) => {
+1
View File
@@ -157,6 +157,7 @@ export {
medium, medium,
make, make,
entity, entity,
systemEntity,
relation, relation,
index, index,
em, em,
+1
View File
@@ -9,6 +9,7 @@ import { buildMediaSchema, registry, type TAppMediaConfig } from "./media-schema
import { mediaFields } from "./media-entities"; import { mediaFields } from "./media-entities";
import * as MediaPermissions from "media/media-permissions"; import * as MediaPermissions from "media/media-permissions";
export type MediaFields = typeof AppMedia.mediaFields;
export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>; export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
declare module "bknd" { declare module "bknd" {
interface Media extends AppEntity, MediaFieldSchema {} interface Media extends AppEntity, MediaFieldSchema {}
@@ -19,22 +19,46 @@ const schema = s.partialObject({
type ImageOptimizationSchema = s.Static<typeof schema>; type ImageOptimizationSchema = s.Static<typeof schema>;
export type CloudflareImageOptimizationOptions = { export type CloudflareImageOptimizationOptions = {
/**
* The url to access the image optimization plugin
* @default /api/plugin/image/optimize
*/
accessUrl?: string; accessUrl?: string;
/**
* The path to resolve the image from
* @default /api/media/file
*/
resolvePath?: string; resolvePath?: string;
/**
* Whether to explain the image optimization schema
* @default false
*/
explain?: boolean; explain?: boolean;
/**
* The default options to use
* @default {}
*/
defaultOptions?: ImageOptimizationSchema; defaultOptions?: ImageOptimizationSchema;
/**
* The fixed options to use
* @default {}
*/
fixedOptions?: ImageOptimizationSchema; fixedOptions?: ImageOptimizationSchema;
/**
* The cache control to use
* @default public, max-age=31536000, immutable
*/
cacheControl?: string; cacheControl?: string;
}; };
export function cloudflareImageOptimization({ export function cloudflareImageOptimization({
accessUrl = "/_plugin/image/optimize", accessUrl = "/api/plugin/image/optimize",
resolvePath = "/api/media/file", resolvePath = "/api/media/file",
explain = false, explain = false,
defaultOptions = {}, defaultOptions = {},
fixedOptions = {}, fixedOptions = {},
}: CloudflareImageOptimizationOptions = {}): AppPlugin { }: CloudflareImageOptimizationOptions = {}): AppPlugin {
const disallowedAccessUrls = ["/api", "/admin", "/_optimize"]; const disallowedAccessUrls = ["/api", "/admin", "/api/plugin"];
if (disallowedAccessUrls.includes(accessUrl) || accessUrl.length < 2) { if (disallowedAccessUrls.includes(accessUrl) || accessUrl.length < 2) {
throw new Error(`Disallowed accessUrl: ${accessUrl}`); throw new Error(`Disallowed accessUrl: ${accessUrl}`);
} }
+1 -1
View File
@@ -42,7 +42,7 @@ export const JsonViewer = ({
copyIconProps = {}, copyIconProps = {},
className, className,
}: JsonViewerProps) => { }: JsonViewerProps) => {
const size = showSize ? (json === null ? 0 : JSON.stringify(json).length) : undefined; const size = showSize ? (!json ? 0 : JSON.stringify(json).length) : undefined;
const formattedSize = formatNumber.fileSize(size ?? 0); const formattedSize = formatNumber.fileSize(size ?? 0);
const showContext = size || title || showCopy; const showContext = size || title || showCopy;
+50 -1
View File
@@ -46,24 +46,73 @@ export type DropzoneRenderProps = {
}; };
export type DropzoneProps = { export type DropzoneProps = {
/**
* Get the upload info for a file
*/
getUploadInfo: (file: { path: string }) => { url: string; headers?: Headers; method?: string }; getUploadInfo: (file: { path: string }) => { url: string; headers?: Headers; method?: string };
/**
* Handle the deletion of a file
*/
handleDelete: (file: { path: string }) => Promise<boolean>; handleDelete: (file: { path: string }) => Promise<boolean>;
/**
* The initial items to display
*/
initialItems?: FileState[]; initialItems?: FileState[];
flow?: "start" | "end"; /**
* Maximum number of media items that can be uploaded
*/
maxItems?: number; maxItems?: number;
/**
* The allowed mime types
*/
allowedMimeTypes?: string[]; allowedMimeTypes?: string[];
/**
* If true, the media item will be overwritten on entity media uploads if limit was reached
*/
overwrite?: boolean; overwrite?: boolean;
/**
* If true, the media items will be uploaded automatically
*/
autoUpload?: boolean; autoUpload?: boolean;
/**
* Whether to add new items to the start or end of the list
* @default "start"
*/
flow?: "start" | "end";
/**
* The on rejected callback
*/
onRejected?: (files: FileWithPath[]) => void; onRejected?: (files: FileWithPath[]) => void;
/**
* The on deleted callback
*/
onDeleted?: (file: { path: string }) => void; onDeleted?: (file: { path: string }) => void;
/**
* The on uploaded all callback
*/
onUploadedAll?: (files: FileStateWithData[]) => void; onUploadedAll?: (files: FileStateWithData[]) => void;
/**
* The on uploaded callback
*/
onUploaded?: (file: FileStateWithData) => void; onUploaded?: (file: FileStateWithData) => void;
/**
* The on clicked callback
*/
onClick?: (file: FileState) => void; onClick?: (file: FileState) => void;
/**
* The placeholder to use
*/
placeholder?: { placeholder?: {
show?: boolean; show?: boolean;
text?: string; text?: string;
}; };
/**
* The footer to render
*/
footer?: ReactNode; footer?: ReactNode;
/**
* The children to render
*/
children?: ReactNode | ((props: DropzoneRenderProps) => ReactNode); children?: ReactNode | ((props: DropzoneRenderProps) => ReactNode);
}; };
@@ -10,15 +10,38 @@ import { mediaItemsToFileStates } from "./helper";
import { useInViewport } from "@mantine/hooks"; import { useInViewport } from "@mantine/hooks";
export type DropzoneContainerProps = { export type DropzoneContainerProps = {
/**
* The initial items to display
* @default []
*/
initialItems?: MediaFieldSchema[] | false; initialItems?: MediaFieldSchema[] | false;
/**
* Whether to use infinite scrolling
* @default false
*/
infinite?: boolean; infinite?: boolean;
/**
* If given, the initial media items fetched will be from this entity
* @default undefined
*/
entity?: { entity?: {
name: string; name: string;
id: PrimaryFieldType; id: PrimaryFieldType;
field: string; field: string;
}; };
/**
* The media config
* @default undefined
*/
media?: Pick<TAppMediaConfig, "entity_name" | "storage">; media?: Pick<TAppMediaConfig, "entity_name" | "storage">;
/**
* Query to filter the media items
*/
query?: RepoQueryIn; query?: RepoQueryIn;
/**
* Whether to use a random filename
* @default false
*/
randomFilename?: boolean; randomFilename?: boolean;
} & Omit<Partial<DropzoneProps>, "initialItems">; } & Omit<Partial<DropzoneProps>, "initialItems">;
+1
View File
@@ -218,6 +218,7 @@ const SidebarResize = ({
return ( return (
<div <div
data-shell="sidebar-resize"
data-active={isResizing ? 1 : undefined} data-active={isResizing ? 1 : undefined}
className="w-px h-full hidden md:flex bg-muted after:transition-colors relative after:absolute after:inset-0 after:-left-px after:w-[2px] select-none data-[active]:after:bg-sky-400 data-[active]:cursor-col-resize hover:after:bg-sky-400 hover:cursor-col-resize after:z-2" className="w-px h-full hidden md:flex bg-muted after:transition-colors relative after:absolute after:inset-0 after:-left-px after:w-[2px] select-none data-[active]:after:bg-sky-400 data-[active]:cursor-col-resize hover:after:bg-sky-400 hover:cursor-col-resize after:z-2"
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
+3
View File
@@ -7,8 +7,11 @@ import { useBknd } from "ui/client/bknd";
import { Empty } from "ui/components/display/Empty"; import { Empty } from "ui/components/display/Empty";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { appShellStore } from "ui/store"; import { appShellStore } from "ui/store";
import { useBrowserTitle } from "ui/hooks/use-browser-title";
export default function ToolsMcp() { export default function ToolsMcp() {
useBrowserTitle(["MCP UI"]);
const { config, options } = useBknd(); const { config, options } = useBknd();
const feature = useMcpStore((state) => state.feature); const feature = useMcpStore((state) => state.feature);
const setFeature = useMcpStore((state) => state.setFeature); const setFeature = useMcpStore((state) => state.setFeature);
+1 -1
View File
@@ -14,7 +14,7 @@ export const useMcpStore = create(
content: null as ToolJson | null, content: null as ToolJson | null,
history: [] as { type: "request" | "response"; data: any }[], history: [] as { type: "request" | "response"; data: any }[],
historyLimit: 50, historyLimit: 50,
historyVisible: true, historyVisible: false,
}, },
(set) => ({ (set) => ({
setTools: (tools: ToolJson[]) => set({ tools }), setTools: (tools: ToolJson[]) => set({ tools }),
+74 -75
View File
@@ -128,83 +128,82 @@ export function Content() {
} catch (e) {} } catch (e) {}
return ( return (
<div className="flex flex-grow flex-col max-w-screen"> <Form
<Form className="flex flex-grow flex-col max-w-screen"
key={content.name} key={content.name}
schema={{ schema={{
title: "InputSchema", title: "InputSchema",
...content?.inputSchema, ...content?.inputSchema,
}} }}
validateOn="submit" validateOn="submit"
initialValues={payload} initialValues={payload}
hiddenSubmit={false} hiddenSubmit={false}
onChange={(value) => { onChange={(value) => {
setPayload(value); setPayload(value);
}} }}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<AppShell.SectionHeader <AppShell.SectionHeader
className="max-w-full min-w-0" className="max-w-full min-w-0"
right={ right={
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
<IconButton <IconButton
Icon={historyVisible ? TbHistory : TbHistoryOff} Icon={historyVisible ? TbHistory : TbHistoryOff}
onClick={() => setHistoryVisible(!historyVisible)} onClick={() => setHistoryVisible(!historyVisible)}
/> />
<Button <Button
type="submit" type="submit"
disabled={!content?.name || isPending} disabled={!content?.name || isPending}
variant="primary" variant="primary"
className="whitespace-nowrap" className="whitespace-nowrap"
> >
Call Tool Call Tool
</Button> </Button>
</div>
}
>
<AppShell.SectionHeaderTitle className="leading-tight">
<span className="opacity-50">
Tools <span className="opacity-70">/</span>
</span>{" "}
<span className="truncate">{content?.name}</span>
</AppShell.SectionHeaderTitle>
</AppShell.SectionHeader>
<div className="flex flex-grow flex-row w-vw">
<div className="flex flex-grow flex-col w-full">
<AppShell.Scrollable>
<div key={JSON.stringify(content)} className="flex flex-col py-4 px-5 gap-4">
<p className="text-primary/80">{content?.description}</p>
{hasInputSchema && <Field name="" />}
<JsonViewerTabs
ref={jsonViewerTabsRef}
expand={9}
showCopy
showSize
tabs={{
Arguments: {
json: payload,
title: "Payload",
enabled: hasInputSchema,
},
Result: { json: readableResult, title: "Result" },
Configuration: {
json: content ?? null,
title: "Configuration",
},
}}
/>
</div>
</AppShell.Scrollable>
</div> </div>
{historyVisible && ( }
<AppShell.Sidebar name="right" handle="left" maxWidth={window.innerWidth * 0.4}> >
<History /> <AppShell.SectionHeaderTitle className="leading-tight">
</AppShell.Sidebar> <span className="opacity-50">
)} Tools <span className="opacity-70">/</span>
</span>{" "}
<span className="truncate">{content?.name}</span>
</AppShell.SectionHeaderTitle>
</AppShell.SectionHeader>
<div className="flex flex-grow flex-row w-vw">
<div className="flex flex-grow flex-col w-full">
<AppShell.Scrollable>
<div key={JSON.stringify(content)} className="flex flex-col py-4 px-5 gap-4">
<p className="text-primary/80">{content?.description}</p>
{hasInputSchema && <Field name="" />}
<JsonViewerTabs
ref={jsonViewerTabsRef}
expand={9}
showCopy
showSize
tabs={{
Arguments: {
json: payload,
title: "Payload",
enabled: hasInputSchema,
},
Result: { json: readableResult, title: "Result" },
Configuration: {
json: content ?? null,
title: "Configuration",
},
}}
/>
</div>
</AppShell.Scrollable>
</div> </div>
</Form> {historyVisible && (
</div> <AppShell.Sidebar name="right" handle="left" maxWidth={window.innerWidth * 0.4}>
<History />
</AppShell.Sidebar>
)}
</div>
</Form>
); );
} }
+25 -2
View File
@@ -21,6 +21,29 @@ export function McpTool({ tool }: { tool: ReturnType<Tool["toJSON"]> }) {
); );
} }
const getType = (value: JSONSchemaDefinition) => {
if (value === undefined || value === null) {
return "any";
}
if (value.type) {
if (Array.isArray(value.type)) {
return value.type.join(" | ");
}
return value.type;
}
if ("anyOf" in value) {
return value.anyOf.map(getType).join(" | ");
}
if ("oneOf" in value) {
return value.oneOf.map(getType).join(" | ");
}
return "any";
};
export function JsonSchemaTypeTable({ schema }: { schema: JSONSchemaDefinition }) { export function JsonSchemaTypeTable({ schema }: { schema: JSONSchemaDefinition }) {
const properties = schema.properties ?? {}; const properties = schema.properties ?? {};
const required = schema.required ?? []; const required = schema.required ?? [];
@@ -44,8 +67,8 @@ export function JsonSchemaTypeTable({ schema }: { schema: JSONSchemaDefinition }
typeDescription: ( typeDescription: (
<DynamicCodeBlock lang="json" code={indent(getTypeDescription(value), 1)} /> <DynamicCodeBlock lang="json" code={indent(getTypeDescription(value), 1)} />
), ),
type: value.type, type: getType(value),
default: value.default ? JSON.stringify(value.default) : undefined, default: value.default !== undefined ? JSON.stringify(value.default) : undefined,
required: required.includes(key), required: required.includes(key),
}, },
]), ]),
+16
View File
@@ -0,0 +1,16 @@
import { cn } from "@/lib/cn";
import type { HTMLAttributes } from "react";
export function Wrapper(props: HTMLAttributes<HTMLDivElement>) {
return (
<div
{...props}
className={cn(
"rounded-lg bg-radial-[at_bottom] from-fd-primary/10 p-4 border bg-origin-border border-fd-accent-primary/10 prose-no-margin dark:bg-black/20",
props.className,
)}
>
{props.children}
</div>
);
}
@@ -0,0 +1,211 @@
---
title: Plugins
tags: ["documentation"]
---
import { TypeTable } from 'fumadocs-ui/components/type-table';
bknd allows you to extend its functionality by creating plugins. These allows to hook into the app lifecycle and to provide a data structure that is guaranteed to be merged. A plugin is a function that takes in an instance of `App` and returns the following structure:
<AutoTypeTable path="../app/src/App.ts" name="AppPluginConfig" />
## Creating a simple plugin
To create a simple plugin which guarantees an entity `pages` to be available and an additioanl endpoint to render a html list of pages, you can create it as follows:
```tsx title="myPagesPlugin.tsx"
/** @jsxImportSource hono/jsx */
import { type App, type AppPlugin, em, entity, text } from "bknd";
export const myPagesPlugin: AppPlugin = (app) => ({
name: "my-pages-plugin",
// define the schema of the plugin
// this will always be merged into the app's schema
schema: () => em({
pages: entity("pages", {
title: text(),
content: text(),
}),
}),
// execute code after the app is built
onBuilt: () => {
// register a new endpoint, make sure that you choose an endpoint that is reachable for bknd
app.server.get("/my-pages", async (c) => {
const { data: pages } = await app.em.repo("pages").findMany({});
return c.html(
<body>
<h1>Pages: {pages.length}</h1>
<ul>
{pages.map((page: any) => (
<li key={page.id}>{page.title}</li>
))}
</ul>
</body>,
);
});
},
});
```
And then register it in your `bknd.config.ts` file:
```typescript
import type { BkndConfig } from "bknd/adapter";
import { myPagesPlugin } from "./myPagesPlugin";
export default {
options: {
plugins: [myPagesPlugin],
}
} satisfies BkndConfig;
```
The schema returned from the plugin will be merged into the schema of the app.
## Built-in plugins
bknd comes with a few built-in plugins that you can use.
### `syncTypes`
A simple plugin that writes down the TypeScript types of the data schema on boot and each build. The output is equivalent to running `npx bknd types`.
```typescript title="bknd.config.ts"
import { syncTypes } from "bknd/plugins";
import { writeFile } from "node:fs/promises";
export default {
options: {
plugins: [
syncTypes({
// whether to enable the plugin, make sure to disable in production
enabled: true,
// your writing function (required)
write: async (et) => {
await writeFile("bknd-types.d.ts", et.toString(), "utf-8");
}
}),
]
},
} satisfies BkndConfig;
```
### `syncConfig`
A simple plugin that writes down the app configuration on boot and each build.
```typescript title="bknd.config.ts"
import { syncConfig } from "bknd/plugins";
import { writeFile } from "node:fs/promises";
export default {
options: {
plugins: [
syncConfig({
// whether to enable the plugin, make sure to disable in production
enabled: true,
// your writing function (required)
write: async (config) => {
await writeFile("config.json", JSON.stringify(config, null, 2), "utf-8");
},
}),
]
},
} satisfies BkndConfig;
```
### `showRoutes`
A simple plugin that logs the routes of your app in the console.
```typescript title="bknd.config.ts"
import { showRoutes } from "bknd/plugins";
export default {
options: {
plugins: [
showRoutes({
// whether to show the routes only once (on first build)
once: true
})
],
},
} satisfies BkndConfig;
```
### `cloudflareImageOptimization`
A plugin that add Cloudflare Image Optimization to your app's media storage.
```typescript title="bknd.config.ts"
import { cloudflareImageOptimization } from "bknd/plugins";
export default {
options: {
plugins: [
cloudflareImageOptimization({
// the url to access the image optimization plugin
accessUrl: "/api/plugin/image/optimize",
// the path to resolve the image from, defaults to `/api/media/file`
resolvePath: "/api/media/file",
// for example, you may want to have default option to limit to a width of 1000px
defaultOptions: {
width: 1000,
}
})
],
},
} satisfies BkndConfig;
```
Here is a break down of all configuration options:
<AutoTypeTable path="../app/src/plugins/cloudflare/image-optimization.plugin.ts" name="CloudflareImageOptimizationOptions" />
When enabled, you can now access your images at your configured `accessUrl`. For example, if you have a media file at `/api/media/file/image.jpg`, you can access the optimized image at `/api/plugin/image/optimize/image.jpg` for optimization.
Now you can add query parameters for the transformations, e.g. `?width=1000&height=1000`.
<TypeTable
type={{
dpr: {
description:
'The device pixel ratio to use',
type: 'number',
default: 1,
},
fit: {
description: 'The fit mode to use',
type: '"scale-down" | "contain" | "cover" | "crop" | "pad"',
},
format: {
description: 'The format to use',
type: '"auto" | "avif" | "webp" | "jpeg" | "baseline-jpeg" | "json"'
},
height: {
description: 'The height to use',
type: 'number',
},
width: {
description: 'The width to use',
type: 'number',
},
metadata: {
description: 'The metadata to use',
type: '"copyright" | "keep" | "none"',
default: 'copyright'
},
quality: {
description: 'The quality to use',
type: 'number',
default: 85
},
}}
/>
@@ -123,9 +123,8 @@ import { serve } from "bknd/adapter/cloudflare";
export default serve<Env>({ export default serve<Env>({
// ... // ...
onBuilt: async (app) => { onBuilt: async (app) => {
// [!code highlight]
app.server.get("/hello", (c) => c.json({ hello: "world" })); // [!code highlight] app.server.get("/hello", (c) => c.json({ hello: "world" })); // [!code highlight]
}, // [!code highlight] },
}); });
``` ```
@@ -141,7 +140,6 @@ With the Cloudflare Workers adapter, you're being offered to 4 modes to choose f
| `fresh` | On every request, the configuration gets refetched, app built and then served. | Ideal if you don't want to deal with eviction, KV or Durable Objects. | | `fresh` | On every request, the configuration gets refetched, app built and then served. | Ideal if you don't want to deal with eviction, KV or Durable Objects. |
| `warm` | It tries to keep the built app in memory for as long as possible, and rebuilds if evicted. | Better response times, should be the default choice. | | `warm` | It tries to keep the built app in memory for as long as possible, and rebuilds if evicted. | Better response times, should be the default choice. |
| `cache` | The configuration is fetched from KV to reduce the initial roundtrip to the database. | Generally faster response times with irregular access patterns. | | `cache` | The configuration is fetched from KV to reduce the initial roundtrip to the database. | Generally faster response times with irregular access patterns. |
| `durable` | The bknd app is ran inside a Durable Object and can be configured to stay alive. | Slowest boot time, but fastest responses. Can be kept alive for as long as you want, giving similar response times as server instances. |
### Modes: `fresh` and `warm` ### Modes: `fresh` and `warm`
@@ -172,76 +170,6 @@ export default serve<Env>({
}); });
``` ```
### Mode: `durable` (advanced)
To use the `durable` mode, you have to specify the Durable Object to extract from your
environment, and additionally export the `DurableBkndApp` class:
```ts
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
export { DurableBkndApp };
export default serve<Env>({
// ...
mode: "durable",
bindings: ({ env }) => ({ dobj: env.DOBJ }),
keepAliveSeconds: 60, // optional
});
```
Next, you need to define the Durable Object in your `wrangler.toml` file (refer to the [Durable
Objects](https://developers.cloudflare.com/durable-objects/) documentation):
```toml
[[durable_objects.bindings]]
name = "DOBJ"
class_name = "DurableBkndApp"
[[migrations]]
tag = "v1"
new_classes = ["DurableBkndApp"]
```
Since the communication between the Worker and Durable Object is serialized, the `onBuilt`
property won't work. To use it (e.g. to specify special routes), you need to extend from the
`DurableBkndApp`:
```ts
import type { App } from "bknd";
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
export default serve({
// ...
mode: "durable",
bindings: ({ env }) => ({ dobj: env.DOBJ }),
keepAliveSeconds: 60, // optional
});
export class CustomDurableBkndApp extends DurableBkndApp {
async onBuilt(app: App) {
app.modules.server.get("/custom/endpoint", (c) => c.text("Custom"));
}
}
```
In case you've already deployed your Worker, the deploy command may complain about a new class
being used. To fix this issue, you need to add a "rename migration":
```toml
[[durable_objects.bindings]]
name = "DOBJ"
class_name = "CustomDurableBkndApp"
[[migrations]]
tag = "v1"
new_classes = ["DurableBkndApp"]
[[migrations]]
tag = "v2"
renamed_classes = [{from = "DurableBkndApp", to = "CustomDurableBkndApp"}]
deleted_classes = ["DurableBkndApp"]
```
## D1 Sessions (experimental) ## D1 Sessions (experimental)
D1 now supports to enable [global read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/). This allows to reduce latency by reading from the closest region. In order for this to work, D1 has to be started from a bookmark. You can enable this behavior on bknd by setting the `d1.session` property: D1 now supports to enable [global read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/). This allows to reduce latency by reading from the closest region. In order for this to work, D1 has to be started from a bookmark. You can enable this behavior on bknd by setting the `d1.session` property:
@@ -272,3 +200,83 @@ If bknd is used in a stateful user context (like in a browser), it'll automatica
```bash ```bash
curl -H "x-cf-d1-session: <bookmark>" ... curl -H "x-cf-d1-session: <bookmark>" ...
``` ```
## Filesystem access with Vite Plugin
The [Cloudflare Vite Plugin](https://developers.cloudflare.com/workers/vite-plugin/) allows to use Vite with Miniflare to emulate the Cloudflare Workers runtime. This is great, however, `unenv` disables any Node.js APIs that aren't supported, including the `fs` module. If you want to use plugins such as [`syncTypes`](/extending/plugins#synctypes), this will cause issues.
To fix this, bknd exports a Vite plugin that provides filesystem access during development. You can use it by adding the following to your `vite.config.ts` file:
```ts
import { devFsVitePlugin } from "bknd/adapter/cloudflare/vite";
export default defineConfig({
plugins: [devFsVitePlugin()], // [!code highlight]
});
```
Now to use this polyfill, you can use the `devFsWrite` function to write files to the filesystem.
```ts
import { devFsWrite } from "bknd/adapter/cloudflare/vite"; // [!code highlight]
import { syncTypes } from "bknd/plugins";
export default {
options: {
plugins: [
syncTypes({
write: async (et) => {
await devFsWrite("bknd-types.d.ts", et.toString()); // [!code highlight]
}
}),
]
},
} satisfies BkndConfig;
```
## Cloudflare Bindings in CLI
The bknd CLI does not automatically have access to the Cloudflare bindings. We need to manually proxy them to the CLI by using the `withPlatformProxy` helper function:
```typescript title="bknd.config.ts"
import { d1 } from "bknd/adapter/cloudflare";
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
export default withPlatformProxy({
app: ({ env }) => ({
connection: d1({ binding: env.DB }),
}),
});
```
Now you can use the CLI with your Cloudflare resources.
<Callout type="warning">
Make sure to not import from this file in your app, as this would include `wrangler` as a dependency.
</Callout>
Instead, it's recommended to split this configuration into separate files, e.g. `bknd.config.ts` and `config.ts`:
```typescript title="config.ts"
import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare";
export default {
app: ({ env }) => ({
connection: d1({ binding: env.DB }),
}),
} satisfies CloudflareBkndConfig;
```
`config.ts` now holds the configuration, and can safely be imported in your app. Since the CLI looks for a `bknd.config.ts` file by default, we change it to wrap the configuration from `config.ts` in the `withPlatformProxy` helper function.
```typescript title="bknd.config.ts"
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
import config from "./config";
export default withPlatformProxy(config);
```
As an additional safe guard, you have to set a `PROXY` environment variable to `1` to enable the proxy.
```bash
PROXY=1 npx bknd types
```
@@ -15,9 +15,11 @@
"./usage/sdk", "./usage/sdk",
"./usage/react", "./usage/react",
"./usage/elements", "./usage/elements",
"./usage/mcp/",
"---Extending---", "---Extending---",
"./extending/config", "./extending/config",
"./extending/events", "./extending/events",
"./extending/plugins",
"---Integration---", "---Integration---",
"./integration/introduction", "./integration/introduction",
"./integration/(frameworks)/", "./integration/(frameworks)/",
@@ -1,3 +0,0 @@
{
"pages": ["overview", "mcp"]
}
+12 -10
View File
@@ -16,22 +16,24 @@ Here is the output:
$ npx bknd $ npx bknd
Usage: bknd [options] [command] Usage: bknd [options] [command]
⚡ bknd cli v0.16.0 ⚡ bknd cli v0.17.0
Options: Options:
-V, --version output the version number -V, --version output the version number
-h, --help display help for command -h, --help display help for command
Commands: Commands:
user <action> create/update users, or generate a token (auth) config [options] get app config
types [options] generate types copy-assets [options] copy static assets
schema [options] get schema create [options] create a new project
run [options] run an instance debug <subject> debug bknd
debug <subject> debug bknd mcp [options] mcp server stdio transport
create [options] create a new project run [options] run an instance
copy-assets [options] copy static assets schema [options] get schema
config [options] get default config sync [options] sync database
help [command] display help for command types [options] generate types
user [options] <action> create/update users, or generate a token (auth)
help [command] display help for command
``` ```
## Starting an instance (`run`) ## Starting an instance (`run`)
@@ -349,6 +349,91 @@ Note that we didn't add relational fields directly to the entity, but instead de
manually. manually.
</Callout> </Callout>
### System entities
There are multiple system entities which are added depending on if the module is enabled:
- `users`: if authentication is enabled
- `media`: if media is enabled and an adapter is configured
You can add additional fields to these entities. System-defined fields don't have to be repeated, those are automatically added to the entity, so don't worry about that. It's important though to match the system entities name, otherwise a new unrelated entity will be created.
If you'd like to connect your entities to system entities, you need them in the schema to access their reference when making relations. From the example above, if you'd like to connect the `posts` entity to the `users` entity, you can do so like this:
```typescript
import { em, entity, text, number, systemEntity } from "bknd";
const schema = em(
{
posts: entity("posts", {
title: text().required(),
slug: text().required(),
content: text(),
views: number(),
// don't add the foreign key field, it's automatically added
}),
comments: entity("comments", {
content: text(),
}),
// [!code highlight]
// add a `users` entity
users: systemEntity("users", { // [!code highlight]
// [!code highlight]
// optionally add additional fields
}) // [!code highlight]
},
// now you have access to the system entity "users"
({ relation, index }, { posts, comments, users }) => {
// ... other relations
relation(posts).manyToOne(users); // [!code highlight]
},
);
```
### Add media to an entity
If media is enabled, you can upload media directly or associate it with an entity. E.g. you may want to upload a cover image for a post, but also a gallery of images. Since a relation to the media entity is polymorphic, you have to:
1. add a virtual field to your entity (single `medium` or multiple `media`)
2. add the relation from the owning entity to the media entity
3. specify the mapped field name by using the `mappedBy` option
```typescript
import { em, entity, text, number, systemEntity, medium, media } from "bknd";
const schema = em(
{
posts: entity("posts", {
title: text().required(),
slug: text().required(),
content: text(),
views: number(),
// [!code highlight]
// `medium` represents a single media item
cover: medium(), // [!code highlight]
// [!code highlight]
// `media` represents a list of media items
gallery: media(), // [!code highlight]
}),
comments: entity("comments", {
content: text(),
}),
// [!code highlight]
// add the `media` entity
media: systemEntity("media", { // [!code highlight]
// [!code highlight]
// optionally add additional fields
}) // [!code highlight]
},
// now you have access to the system entity "media"
({ relation, index }, { posts, comments, media }) => {
// add the `cover` relation
relation(posts).polyToOne(media, { mappedBy: "cover" }); // [!code highlight]
// add the `gallery` relation
relation(posts).polyToMany(media, { mappedBy: "gallery" }); // [!code highlight]
},
);
```
### Type completion ### Type completion
To get type completion, there are two options: To get type completion, there are two options:
@@ -44,16 +44,8 @@ export default function UserAvatar() {
#### Props #### Props
- `initialItems?: xMediaFieldSchema[]`: Initial items to display, must be an array of media objects. <AutoTypeTable path="../app/src/ui/elements/media/DropzoneContainer.tsx" name="DropzoneContainerProps" />
- `entity?: { name: string; id: number; field: string }`: If given, the initial media items fetched will be from this entity.
- `query?: RepoQueryIn`: Query to filter the media items.
- `overwrite?: boolean`: If true, the media item will be overwritten on entity media uploads if limit was reached.
- `maxItems?: number`: Maximum number of media items that can be uploaded.
- `autoUpload?: boolean`: If true, the media items will be uploaded automatically.
- `onRejected?: (files: FileWithPath[]) => void`: Callback when a file is rejected.
- `onDeleted?: (file: FileState) => void`: Callback when a file is deleted.
- `onUploaded?: (file: FileState) => void`: Callback when a file is uploaded.
- `placeholder?: { show?: boolean; text?: string }`: Placeholder text to show when no media items are present.
#### Customize Rendering #### Customize Rendering
@@ -0,0 +1,4 @@
{
"title": "MCP",
"pages": ["overview", "tools-resources"]
}
@@ -0,0 +1,208 @@
---
title: "Overview"
description: "Built-in full featured MCP server."
tags: ["documentation"]
---
import { ImageZoom } from "fumadocs-ui/components/image-zoom";
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
<Callout type="warning">
The MCP server is currently experimental and may change in the future. During this period, it is disabled by default. To stabilize it, and make **bknd MCP native**, all configuration changes you can make today with the integrated Admin UI will be migrated to use the MCP server.
</Callout>
bknd includes a fully featured MCP server that can be used to interact with the bknd instance. It uses a lightweight MCP implementation that works in any environment bknd works in. Unlike other MCP servers, the exposed tools and resources are mainly dynamically generated from the schema, extracted from defined hono routes, and manually defined ones. This means exposed tools and resources are always up to date, and requires little overhead to maintain.
- Fully featured, always up to date MCP server natively integrated with bknd
- Integrated MCP UI accessible from the Admin UI
- Built-in MCP client directly usable from your app instance
- CLI command to run an MCP server on stdio transport
## Integrated MCP UI
<video controls poster="/content/mcp/v0.17_mcp_o_frame_12.5s.jpg">
<source src="/content/mcp/v0.17_mcp_o.mp4" type="video/mp4" />
</video>
Once enabled, you can access the MCP UI at `/mcp` or choose "MCP" from the top right user menu.
## Enable MCP
If you're using `initialConfig`, you can enable the MCP server by setting the `server.mcp.enabled` property to `true`.
```typescript
import type { BkndConfig } from "bknd";
export default {
initialConfig: {
server: {
mcp: {
enabled: true,
}
}
}
} satisfies BkndConfig;
```
Using the Admin UI, you can either navigate to `/settings/server` or click top right on the user menu, select "Settings", then "Server". Enable the MCP server by checking the "Enabled" checkbox under "Mcp".
## Using the MCP Client
The implementation is closely following the [MCP spec 2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18) powered by [jsonv-ts](https://github.com/jsonv-ts/jsonv-ts), therefore any spec compliant client will work. However, there is a built-in MCP client:
```typescript
import { McpClient } from "bknd/utils";
const client = new McpClient({
url: "http://localhost:1337/mcp",
});
```
Alternatively, similar to the `getApi` function, you can use the `getMcpClient` function to get the client from your app instance that doesn't travel through the network.
```typescript
import { createApp } from "bknd";
const app = createApp();
const client = app.getMcpClient();
```
Unlike the official spec requires, there is no initialization required, but supported. Without connecting, initialization or fetching the list of tools, you can directly call them. For example, you could fetch a list of `posts`:
```typescript
const result = await client.callTool({
name: "data_entity_read_many",
arguments: {
entity: "posts",
limit: 3,
select: ["id", "title"],
},
});
// {
// data: [
// { id: 1, title: "Post 1" },
// { id: 2, title: "Post 2" },
// { id: 3, title: "Post 3" }
// ],
// }
```
Refer to the [jsonv-ts docs](https://github.com/dswbx/jsonv-ts#mcp-client) for more information.
## STDIO Transport
To start an MCP server on stdio transport, you can use the `mcp` CLI command. This is useful when you want to use it with IDEs or other tools that support stdio transport.
```bash
npx bknd mcp
```
If you want have the Streamable HTTP endpoint disabled, you can still use the STDIO transport by passing the `--force` option.
```bash
npx bknd mcp --force
```
## Usage in external tools
You can also use the MCP server in external tools, such as VS Code, Cursor, or other IDEs that support MCP. This list is not exhaustive, and will be updated.
<Accordions type="single">
<Accordion title="Cursor">
Go to: `Settings` -> `Cursor Settings` -> `Tools & Integrations` -> `Add a custom MCP server`
Pasting the following config into your Cursor `~/.cursor/mcp.json` file is the recommended approach. You can also install in a specific project by creating `.cursor/mcp.json` in your project folder. See [Cursor MCP docs](https://docs.cursor.com/context/model-context-protocol) for more info.
```json tab="Local"
{
"mcpServers": {
"bknd": {
"command": "npx",
"args": ["-y", "bknd@latest", "mcp"]
}
}
}
```
```json tab="Remote"
{
"mcpServers": {
"bknd": {
"url": "http://localhost:1337/mcp"
}
}
}
```
</Accordion>
<Accordion title="VS Code">
Add this to your VS Code MCP config. See [VS Code MCP docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more info.
```json tab="Local"
{
"servers": {
"bknd": {
"type": "stdio",
"command": "npx",
"args": ["-y", "bknd@latest", "mcp"]
}
}
}
```
```json tab="Remote"
{
"servers": {
"bknd": {
"type": "http",
"url": "http://localhost:1337/mcp"
}
}
}
```
</Accordion>
<Accordion title="Claude Desktop">
Add this to your Claude Desktop `claude_desktop_config.json` file. See [Claude Desktop MCP docs](https://modelcontextprotocol.io/quickstart/user) for more info.
```json title="claude_desktop_config.json"
{
"mcpServers": {
"bknd": {
"command": "npx",
"args": ["-y", "bknd@latest", "mcp"]
}
}
}
```
</Accordion>
</Accordions>
If a tool you're using is not listed here, please let us know by [opening an issue](https://github.com/bknd-dev/bknd/issues/new) or [contacting us on Discord](https://discord.com/invite/Qjz9nNHYTB).
## Authentication
Both the Streamable HTTP and STDIO transport support authentication. The same authentication mechanism as the API is used, so permissions work the exact same way.
When using the Streamable HTTP transport, you can pass the `Authorization` header to the client.
```typescript
const client = new McpClient({
url: "http://localhost:1337/mcp",
headers: {
Authorization: `Bearer ${token}`,
},
});
```
When using the STDIO transport, you can pass an `--token` option to the CLI command.
```bash
npx bknd mcp --token <token>
```
Alternatively, you can also use the `BEARER_TOKEN` environment variable.
```bash
BEARER_TOKEN=<token> npx bknd mcp
```
@@ -199,6 +199,37 @@ To delete many records of an entity, use the `deleteMany` method:
const { data } = await api.data.deleteMany("posts", { views: { $lte: 1 } }); const { data } = await api.data.deleteMany("posts", { views: { $lte: 1 } });
``` ```
### `data.readManyByReference([entity], [id], [reference], [query])`
To retrieve records from a related entity by following a reference, use the `readManyByReference` method:
```ts
const { data } = await api.data.readManyByReference("posts", 1, "comments", {
limit: 5,
sort: "-created_at",
});
```
### `data.count([entity], [where])`
To count records in an entity that match certain criteria, use the `count` method:
```ts
const { data } = await api.data.count("posts", {
views: { $gt: 100 }
});
```
### `data.exists([entity], [where])`
To check if any records exist in an entity that match certain criteria, use the `exists` method:
```ts
const { data } = await api.data.exists("posts", {
title: "Hello, World!"
});
```
## Auth (`api.auth`) ## Auth (`api.auth`)
Access the `Auth` specific API methods at `api.auth`. If there is successful authentication, the Access the `Auth` specific API methods at `api.auth`. If there is successful authentication, the
@@ -241,3 +272,89 @@ To retrieve the current user, use the `me` method:
```ts ```ts
const { data } = await api.auth.me(); const { data } = await api.auth.me();
``` ```
## Media (`api.media`)
Access the `Media` specific API methods at `api.media`.
### `media.listFiles()`
To retrieve a list of all uploaded files, use the `listFiles` method:
```ts
const { data } = await api.media.listFiles();
// ^? FileListObject[]
```
### `media.getFile([filename])`
To retrieve a file as a readable stream, use the `getFile` method:
```ts
const { data } = await api.media.getFile("image.jpg");
// ^? ReadableStream<Uint8Array>
```
### `media.getFileStream([filename])`
To get a file stream directly, use the `getFileStream` method:
```ts
const stream = await api.media.getFileStream("image.jpg");
// ^? ReadableStream<Uint8Array>
```
### `media.download([filename])`
To download a file as a File object, use the `download` method:
```ts
const file = await api.media.download("image.jpg");
// ^? File
```
### `media.upload([item], [options])`
To upload a file, use the `upload` method. The item can be any of:
- `File` object
- `Request` object
- `Response` object
- `string` (URL)
- `ReadableStream`
- `Buffer`
- `Blob`
```ts
// Upload a File object
const { data } = await api.media.upload(item);
// Upload from a URL
const { data } = await api.media.upload("https://example.com/image.jpg");
// Upload with custom options
const { data } = await api.media.upload(item, {
filename: "custom-name.jpg",
});
```
### `media.uploadToEntity([entity], [id], [field], [item], [options])`
To upload a file directly to an entity field, use the `uploadToEntity` method:
```ts
const { data } = await api.media.uploadToEntity(
"posts",
1,
"image",
item
);
```
### `media.deleteFile([filename])`
To delete a file, use the `deleteFile` method:
```ts
const { data } = await api.media.deleteFile("image.jpg");
```
+979 -975
View File
File diff suppressed because it is too large Load Diff
+26 -24
View File
@@ -5,11 +5,12 @@ import type { MDXComponents } from "mdx/types";
import * as FilesComponents from "fumadocs-ui/components/files"; import * as FilesComponents from "fumadocs-ui/components/files";
import * as TabsComponents from "fumadocs-ui/components/tabs"; import * as TabsComponents from "fumadocs-ui/components/tabs";
import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
import { ImageZoom } from "fumadocs-ui/components/image-zoom";
import { import {
CalloutInfo, CalloutInfo,
CalloutPositive, CalloutPositive,
CalloutCaution, CalloutCaution,
CalloutDanger, CalloutDanger,
} from "./app/_components/Callout"; } from "./app/_components/Callout";
import { StackBlitz } from "./app/_components/StackBlitz"; import { StackBlitz } from "./app/_components/StackBlitz";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
@@ -18,29 +19,30 @@ import * as Twoslash from "fumadocs-twoslash/ui";
import { createGenerator } from "fumadocs-typescript"; import { createGenerator } from "fumadocs-typescript";
import { AutoTypeTable } from "fumadocs-typescript/ui"; import { AutoTypeTable } from "fumadocs-typescript/ui";
import { Wrapper } from "@/components/misc/Wrapper";
const generator = createGenerator({ const generator = createGenerator({
tsconfigPath: "../tsconfig.json", tsconfigPath: "../tsconfig.json",
}); });
export function getMDXComponents(components?: MDXComponents): MDXComponents { export function getMDXComponents(components?: MDXComponents): MDXComponents {
return { return {
...defaultMdxComponents, ...defaultMdxComponents,
...TabsComponents, ...TabsComponents,
...FilesComponents, ...FilesComponents,
...Twoslash, ...Twoslash,
Accordion, Accordion,
Accordions, Accordions,
CalloutInfo, CalloutInfo,
CalloutPositive, CalloutPositive,
CalloutCaution, CalloutCaution,
CalloutDanger, CalloutDanger,
StackBlitz, StackBlitz,
Icon, Icon,
APIPage: (props) => <APIPage {...openapi.getAPIPageProps(props)} />, Wrapper,
AutoTypeTable: (props) => ( img: (props) => <ImageZoom {...(props as any)} />,
<AutoTypeTable {...props} generator={generator} /> APIPage: (props) => <APIPage {...openapi.getAPIPageProps(props)} />,
), AutoTypeTable: (props) => <AutoTypeTable {...props} generator={generator} />,
...components, ...components,
}; };
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+2 -8
View File
@@ -6,18 +6,12 @@ import { writeFile, readFile } from "node:fs/promises";
const config = { const config = {
mcpConfig: "./mcp.json", mcpConfig: "./mcp.json",
outFile: "./content/docs/(documentation)/modules/server/mcp.mdx", outFile: "./content/docs/(documentation)/usage/mcp/tools-resources.mdx",
}; };
async function generate() { async function generate() {
console.info("Generating MCP documentation..."); console.info("Generating MCP documentation...");
try {
console.log("bun version", Bun.version);
} catch (e) {
console.log("bun failed");
}
await cleanup(); await cleanup();
const mcpConfig = JSON.parse(await readFile(config.mcpConfig, "utf-8")); const mcpConfig = JSON.parse(await readFile(config.mcpConfig, "utf-8"));
const document = await generateDocument(mcpConfig); const document = await generateDocument(mcpConfig);
@@ -34,7 +28,7 @@ async function generateDocument({
}) { }) {
return `--- return `---
title: "MCP" title: "MCP"
description: "Built-in full featured MCP server." description: "Tools & Resources of the built-in full featured MCP server."
tags: ["documentation"] tags: ["documentation"]
--- ---
import { JsonSchemaTypeTable } from '@/components/McpTool'; import { JsonSchemaTypeTable } from '@/components/McpTool';