From 24eefa5357f8745b4af4f3ab6e245800e7ad8f32 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 30 Aug 2025 14:06:13 +0200 Subject: [PATCH] mcp: added path config, register at /api path by default to work with frameworks --- app/src/modules/server/AppServer.ts | 1 + app/src/modules/server/SystemController.ts | 5 +++-- app/src/ui/components/display/Icon.tsx | 8 ++++++++ .../routes/tools/mcp/hooks/use-mcp-client.ts | 16 +++++++++++++++ app/src/ui/routes/tools/mcp/mcp.tsx | 3 ++- app/src/ui/routes/tools/mcp/tools.tsx | 20 ++++++++++++++----- app/src/ui/routes/tools/mcp/utils.ts | 12 ----------- .../(documentation)/usage/mcp/overview.mdx | 10 +++++----- examples/cloudflare-worker/bknd.config.ts | 2 +- examples/cloudflare-worker/config.ts | 2 +- 10 files changed, 52 insertions(+), 27 deletions(-) create mode 100644 app/src/ui/routes/tools/mcp/hooks/use-mcp-client.ts diff --git a/app/src/modules/server/AppServer.ts b/app/src/modules/server/AppServer.ts index 31a76c5..57af316 100644 --- a/app/src/modules/server/AppServer.ts +++ b/app/src/modules/server/AppServer.ts @@ -24,6 +24,7 @@ export const serverConfigSchema = $object( }), mcp: s.strictObject({ enabled: s.boolean({ default: false }), + path: s.string({ default: "/api/system/mcp" }), }), }, { diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts index e162612..25b20a0 100644 --- a/app/src/modules/server/SystemController.ts +++ b/app/src/modules/server/SystemController.ts @@ -60,8 +60,9 @@ export class SystemController extends Controller { register(app: App) { app.server.route("/api/system", this.getController()); + const config = app.modules.get("server").config; - if (!this.app.modules.get("server").config.mcp.enabled) { + if (!config.mcp.enabled) { return; } @@ -97,7 +98,7 @@ export class SystemController extends Controller { explainEndpoint: true, }, endpoint: { - path: "/mcp", + path: config.mcp.path as any, // @ts-ignore _init: isNode() ? { duplex: "half" } : {}, }, diff --git a/app/src/ui/components/display/Icon.tsx b/app/src/ui/components/display/Icon.tsx index 65692c9..91181b4 100644 --- a/app/src/ui/components/display/Icon.tsx +++ b/app/src/ui/components/display/Icon.tsx @@ -13,6 +13,14 @@ const Warning = ({ className, ...props }: IconProps) => ( /> ); +const Err = ({ className, ...props }: IconProps) => ( + +); + export const Icon = { Warning, + Err, }; diff --git a/app/src/ui/routes/tools/mcp/hooks/use-mcp-client.ts b/app/src/ui/routes/tools/mcp/hooks/use-mcp-client.ts new file mode 100644 index 0000000..5ad9d4b --- /dev/null +++ b/app/src/ui/routes/tools/mcp/hooks/use-mcp-client.ts @@ -0,0 +1,16 @@ +import { McpClient, type McpClientConfig } from "jsonv-ts/mcp"; +import { useBknd } from "ui/client/bknd"; + +const clients = new Map(); + +export function getClient(opts: McpClientConfig) { + if (!clients.has(JSON.stringify(opts))) { + clients.set(JSON.stringify(opts), new McpClient(opts)); + } + return clients.get(JSON.stringify(opts))!; +} + +export function useMcpClient() { + const { config } = useBknd(); + return getClient({ url: window.location.origin + config.server.mcp.path }); +} diff --git a/app/src/ui/routes/tools/mcp/mcp.tsx b/app/src/ui/routes/tools/mcp/mcp.tsx index 791358d..c06668b 100644 --- a/app/src/ui/routes/tools/mcp/mcp.tsx +++ b/app/src/ui/routes/tools/mcp/mcp.tsx @@ -17,6 +17,7 @@ export default function ToolsMcp() { const setFeature = useMcpStore((state) => state.setFeature); const content = useMcpStore((state) => state.content); const openSidebar = appShellStore((store) => store.toggleSidebar("default")); + const mcpPath = config.server.mcp.path; if (!config.server.mcp.enabled) { return ( @@ -39,7 +40,7 @@ export default function ToolsMcp() {
- {window.location.origin + "/mcp"} + {window.location.origin + mcpPath}
diff --git a/app/src/ui/routes/tools/mcp/tools.tsx b/app/src/ui/routes/tools/mcp/tools.tsx index 3b69003..d439fc1 100644 --- a/app/src/ui/routes/tools/mcp/tools.tsx +++ b/app/src/ui/routes/tools/mcp/tools.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState, useTransition } from "react"; -import { getClient, getTemplate } from "./utils"; +import { getTemplate } from "./utils"; import { useMcpStore } from "./state"; import { AppShell } from "ui/layouts/AppShell"; import { TbHistory, TbHistoryOff, TbRefresh } from "react-icons/tb"; @@ -10,9 +10,11 @@ import { Field, Form } from "ui/components/form/json-schema-form"; import { Button } from "ui/components/buttons/Button"; import * as Formy from "ui/components/form/Formy"; import { appShellStore } from "ui/store"; +import { Icon } from "ui/components/display/Icon"; +import { useMcpClient } from "./hooks/use-mcp-client"; export function Sidebar({ open, toggle }) { - const client = getClient(); + const client = useMcpClient(); const closeSidebar = appShellStore((store) => store.closeSidebar("default")); const tools = useMcpStore((state) => state.tools); const setTools = useMcpStore((state) => state.setTools); @@ -20,11 +22,18 @@ export function Sidebar({ open, toggle }) { const content = useMcpStore((state) => state.content); const [loading, setLoading] = useState(false); const [query, setQuery] = useState(""); + const [error, setError] = useState(null); const handleRefresh = useCallback(async () => { setLoading(true); - const res = await client.listTools(); - if (res) setTools(res.tools); + setError(null); + try { + const res = await client.listTools(); + if (res) setTools(res.tools); + } catch (e) { + console.error(e); + setError(String(e)); + } setLoading(false); }, []); @@ -39,6 +48,7 @@ export function Sidebar({ open, toggle }) { toggle={toggle} renderHeaderRight={() => (
+ {error && } {tools.length} @@ -88,7 +98,7 @@ export function Content() { const [result, setResult] = useState(null); const historyVisible = useMcpStore((state) => state.historyVisible); const setHistoryVisible = useMcpStore((state) => state.setHistoryVisible); - const client = getClient(); + const client = useMcpClient(); const jsonViewerTabsRef = useRef(null); const hasInputSchema = content?.inputSchema && Object.keys(content.inputSchema.properties ?? {}).length > 0; diff --git a/app/src/ui/routes/tools/mcp/utils.ts b/app/src/ui/routes/tools/mcp/utils.ts index 82d3df8..dfac3bc 100644 --- a/app/src/ui/routes/tools/mcp/utils.ts +++ b/app/src/ui/routes/tools/mcp/utils.ts @@ -1,17 +1,5 @@ -import { McpClient, type McpClientConfig } from "jsonv-ts/mcp"; import { Draft2019 } from "json-schema-library"; -const clients = new Map(); - -export function getClient( - { url, ...opts }: McpClientConfig = { url: window.location.origin + "/mcp" }, -) { - if (!clients.has(String(url))) { - clients.set(String(url), new McpClient({ url, ...opts })); - } - return clients.get(String(url))!; -} - export function getTemplate(schema: object) { if (!schema || schema === undefined || schema === null) return undefined; diff --git a/docs/content/docs/(documentation)/usage/mcp/overview.mdx b/docs/content/docs/(documentation)/usage/mcp/overview.mdx index 9085814..32f4354 100644 --- a/docs/content/docs/(documentation)/usage/mcp/overview.mdx +++ b/docs/content/docs/(documentation)/usage/mcp/overview.mdx @@ -24,7 +24,7 @@ bknd includes a fully featured MCP server that can be used to interact with the -Once enabled, you can access the MCP UI at `/mcp` or choose "MCP" from the top right user menu. +Once enabled, you can access the MCP UI at `/mcp`, or choose "MCP" from the top right user menu. ## Enable MCP @@ -54,7 +54,7 @@ The implementation is closely following the [MCP spec 2025-06-18](https://modelc import { McpClient } from "bknd/utils"; const client = new McpClient({ - url: "http://localhost:1337/mcp", + url: "http://localhost:1337/api/system/mcp", }); ``` @@ -128,7 +128,7 @@ Pasting the following config into your Cursor `~/.cursor/mcp.json` file is the r { "mcpServers": { "bknd": { - "url": "http://localhost:1337/mcp" + "url": "http://localhost:1337/api/system/mcp" } } } @@ -155,7 +155,7 @@ Add this to your VS Code MCP config. See [VS Code MCP docs](https://code.visuals "servers": { "bknd": { "type": "http", - "url": "http://localhost:1337/mcp" + "url": "http://localhost:1337/api/system/mcp" } } } @@ -188,7 +188,7 @@ When using the Streamable HTTP transport, you can pass the `Authorization` heade ```typescript const client = new McpClient({ - url: "http://localhost:1337/mcp", + url: "http://localhost:1337/api/system/mcp", headers: { Authorization: `Bearer ${token}`, }, diff --git a/examples/cloudflare-worker/bknd.config.ts b/examples/cloudflare-worker/bknd.config.ts index 88250c8..74948a8 100644 --- a/examples/cloudflare-worker/bknd.config.ts +++ b/examples/cloudflare-worker/bknd.config.ts @@ -5,7 +5,7 @@ * We're using separate files, so that "wrangler" doesn't get bundled with your worker. */ -import { withPlatformProxy } from "bknd/adapter/cloudflare"; +import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy"; import config from "./config.ts"; export default withPlatformProxy(config); diff --git a/examples/cloudflare-worker/config.ts b/examples/cloudflare-worker/config.ts index e352f16..0309982 100644 --- a/examples/cloudflare-worker/config.ts +++ b/examples/cloudflare-worker/config.ts @@ -2,7 +2,7 @@ import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare"; import { syncTypes } from "bknd/plugins"; import { writeFile } from "node:fs/promises"; -const isDev = !import.meta.env.PROD; +const isDev = import.meta.env && !import.meta.env.PROD; export default { d1: {