mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
211
docs/content/docs/(documentation)/extending/plugins.mdx
Normal file
211
docs/content/docs/(documentation)/extending/plugins.mdx
Normal file
@@ -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>({
|
||||
// ...
|
||||
onBuilt: async (app) => {
|
||||
// [!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. |
|
||||
| `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. |
|
||||
| `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`
|
||||
|
||||
@@ -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 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
|
||||
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,16 +15,18 @@
|
||||
"./usage/sdk",
|
||||
"./usage/react",
|
||||
"./usage/elements",
|
||||
"./usage/mcp/",
|
||||
"---Extending---",
|
||||
"./extending/config",
|
||||
"./extending/events",
|
||||
"./extending/plugins",
|
||||
"---Integration---",
|
||||
"./integration/introduction",
|
||||
"./integration/(frameworks)/",
|
||||
"./integration/(runtimes)/",
|
||||
"---Modules---",
|
||||
"./modules/overview",
|
||||
"./modules/server",
|
||||
"./modules/server/",
|
||||
"./modules/data",
|
||||
"./modules/auth",
|
||||
"./modules/media",
|
||||
|
||||
@@ -16,22 +16,24 @@ Here is the output:
|
||||
$ npx bknd
|
||||
Usage: bknd [options] [command]
|
||||
|
||||
⚡ bknd cli v0.16.0
|
||||
⚡ bknd cli v0.17.0
|
||||
|
||||
Options:
|
||||
-V, --version output the version number
|
||||
-h, --help display help for command
|
||||
|
||||
Commands:
|
||||
user <action> create/update users, or generate a token (auth)
|
||||
types [options] generate types
|
||||
schema [options] get schema
|
||||
run [options] run an instance
|
||||
debug <subject> debug bknd
|
||||
create [options] create a new project
|
||||
copy-assets [options] copy static assets
|
||||
config [options] get default config
|
||||
help [command] display help for command
|
||||
config [options] get app config
|
||||
copy-assets [options] copy static assets
|
||||
create [options] create a new project
|
||||
debug <subject> debug bknd
|
||||
mcp [options] mcp server stdio transport
|
||||
run [options] run an instance
|
||||
schema [options] get schema
|
||||
sync [options] sync database
|
||||
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`)
|
||||
|
||||
@@ -349,6 +349,91 @@ Note that we didn't add relational fields directly to the entity, but instead de
|
||||
manually.
|
||||
</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
|
||||
|
||||
To get type completion, there are two options:
|
||||
|
||||
@@ -44,16 +44,8 @@ export default function UserAvatar() {
|
||||
|
||||
#### Props
|
||||
|
||||
- `initialItems?: xMediaFieldSchema[]`: Initial items to display, must be an array of media objects.
|
||||
- `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.
|
||||
<AutoTypeTable path="../app/src/ui/elements/media/DropzoneContainer.tsx" name="DropzoneContainerProps" />
|
||||
|
||||
|
||||
#### Customize Rendering
|
||||
|
||||
|
||||
4
docs/content/docs/(documentation)/usage/mcp/meta.json
Normal file
4
docs/content/docs/(documentation)/usage/mcp/meta.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "MCP",
|
||||
"pages": ["overview", "tools-resources"]
|
||||
}
|
||||
208
docs/content/docs/(documentation)/usage/mcp/overview.mdx
Normal file
208
docs/content/docs/(documentation)/usage/mcp/overview.mdx
Normal file
@@ -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/api/system/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/api/system/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/api/system/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/api/system/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
|
||||
```
|
||||
355
docs/content/docs/(documentation)/usage/mcp/tools-resources.mdx
Normal file
355
docs/content/docs/(documentation)/usage/mcp/tools-resources.mdx
Normal file
File diff suppressed because one or more lines are too long
@@ -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 } });
|
||||
```
|
||||
|
||||
### `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`)
|
||||
|
||||
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
|
||||
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");
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user