@nifrajs/mcp 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bridge.d.ts +3 -3
- package/dist/bridge.js +3 -3
- package/dist/http.d.ts +3 -3
- package/dist/http.js +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +5 -5
- package/dist/protocol.d.ts +5 -5
- package/dist/protocol.js +4 -4
- package/dist/react.d.ts +1 -1
- package/dist/react.js +7 -7
- package/dist/server.d.ts +5 -5
- package/dist/server.js +2 -2
- package/dist/tool.d.ts +5 -5
- package/dist/tool.js +2 -2
- package/dist/widget.d.ts +5 -5
- package/dist/widget.js +3 -3
- package/package.json +2 -2
- package/src/bridge.ts +4 -4
- package/src/http.ts +5 -5
- package/src/index.ts +5 -5
- package/src/protocol.ts +6 -6
- package/src/react.ts +7 -7
- package/src/server.ts +5 -5
- package/src/tool.ts +5 -5
- package/src/widget.ts +5 -5
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ hosts.
|
|
|
26
26
|
|
|
27
27
|
## For AI agents
|
|
28
28
|
|
|
29
|
-
Start with [`LLM.md`](./LLM.md)
|
|
29
|
+
Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
|
|
30
30
|
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
31
31
|
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
32
32
|
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
package/dist/bridge.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* host → widget : `ui/notifications/tool-result` (the tools/call result incl. `structuredContent`),
|
|
6
6
|
* `ui/notifications/tool-input` (the call arguments), and `ui/notifications/theme`
|
|
7
|
-
* (the host's design tokens)
|
|
7
|
+
* (the host's design tokens) - pushed as JSON-RPC notifications.
|
|
8
8
|
* widget → host : `tools/call` requests (id-matched) so a button can re-invoke a tool through the host.
|
|
9
9
|
*
|
|
10
10
|
* Theming follows the shadcn/Tailwind semantic-token convention (`--background`, `--foreground`,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* app with zero per-widget work.
|
|
14
14
|
*
|
|
15
15
|
* It exposes a small global the widget author scripts against, so authoring a widget is just
|
|
16
|
-
* `mcpApp.onData(render)` + (optionally) `mcpApp.callTool(name, args)`
|
|
16
|
+
* `mcpApp.onData(render)` + (optionally) `mcpApp.callTool(name, args)` - no postMessage plumbing.
|
|
17
17
|
*
|
|
18
18
|
* Returned as a string so {@link ./widget.ts} can inline it in the resource HTML (a widget ships as ONE
|
|
19
19
|
* self-contained document; the host renders it in a sandboxed iframe with no network of its own).
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
export interface McpAppBridge {
|
|
23
23
|
/** Latest `structuredContent` the host pushed (or `null` before the first result). */
|
|
24
24
|
data: unknown;
|
|
25
|
-
/** Register a callback for `structuredContent`
|
|
25
|
+
/** Register a callback for `structuredContent` - fired immediately if data already arrived, then on
|
|
26
26
|
* every later `ui/notifications/tool-result`. The common case: `mcpApp.onData(render)`. */
|
|
27
27
|
onData(cb: (data: unknown) => void): void;
|
|
28
28
|
/** Register a callback for the raw tool-call arguments (`ui/notifications/tool-input`). */
|
package/dist/bridge.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* host → widget : `ui/notifications/tool-result` (the tools/call result incl. `structuredContent`),
|
|
6
6
|
* `ui/notifications/tool-input` (the call arguments), and `ui/notifications/theme`
|
|
7
|
-
* (the host's design tokens)
|
|
7
|
+
* (the host's design tokens) - pushed as JSON-RPC notifications.
|
|
8
8
|
* widget → host : `tools/call` requests (id-matched) so a button can re-invoke a tool through the host.
|
|
9
9
|
*
|
|
10
10
|
* Theming follows the shadcn/Tailwind semantic-token convention (`--background`, `--foreground`,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* app with zero per-widget work.
|
|
14
14
|
*
|
|
15
15
|
* It exposes a small global the widget author scripts against, so authoring a widget is just
|
|
16
|
-
* `mcpApp.onData(render)` + (optionally) `mcpApp.callTool(name, args)`
|
|
16
|
+
* `mcpApp.onData(render)` + (optionally) `mcpApp.callTool(name, args)` - no postMessage plumbing.
|
|
17
17
|
*
|
|
18
18
|
* Returned as a string so {@link ./widget.ts} can inline it in the resource HTML (a widget ships as ONE
|
|
19
19
|
* self-contained document; the host renders it in a sandboxed iframe with no network of its own).
|
|
@@ -74,7 +74,7 @@ const BRIDGE_SOURCE = `(function () {
|
|
|
74
74
|
window.addEventListener("message", function (event) {
|
|
75
75
|
// Only trust the embedding host (the parent window). A widget renders in a sandboxed iframe whose host
|
|
76
76
|
// posts via the parent (or a sandbox-proxy parent), so event.source === window.parent. Reject anything
|
|
77
|
-
// else
|
|
77
|
+
// else - including a null source (synthetic/dispatched events) - so a sibling/opener frame or in-page
|
|
78
78
|
// script can't spoof tool-results, tool-call responses, or theme. (A top-level widget has no host.)
|
|
79
79
|
if (event.source !== window.parent || window.parent === window) return;
|
|
80
80
|
var msg = event.data;
|
package/dist/http.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The transport core shared by every HTTP host of an MCP server
|
|
2
|
+
* The transport core shared by every HTTP host of an MCP server - the dev/docs MCP in `@nifrajs/cli`
|
|
3
3
|
* (`nifra docs-mcp`), the site's edge worker, and a nifra app mounting `POST /mcp` via
|
|
4
4
|
* {@link ./server.ts}. {@link respondMcpHttp} is a plain Web `fetch` handler: POST a JSON-RPC body →
|
|
5
|
-
* JSON-RPC response; GET is a health page; OPTIONS is the CORS preflight. It never throws
|
|
5
|
+
* JSON-RPC response; GET is a health page; OPTIONS is the CORS preflight. It never throws - a bad body
|
|
6
6
|
* becomes a JSON-RPC parse error. Pass the tools (and optional {@link McpServerFeatures} for resources /
|
|
7
7
|
* prompts / the MCP Apps `ui://` widgets) so each host supplies its own corpus/source behind one core.
|
|
8
8
|
*/
|
|
@@ -17,7 +17,7 @@ export interface McpHttpOptions {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Handle one MCP request over HTTP against the given `tools`/`features`. POST a JSON-RPC body → JSON-RPC
|
|
20
|
-
* response; GET → a plain-text health page; OPTIONS → CORS preflight. Never throws
|
|
20
|
+
* response; GET → a plain-text health page; OPTIONS → CORS preflight. Never throws - a bad body becomes a
|
|
21
21
|
* JSON-RPC parse error. The dispatch is the shared, transport-agnostic {@link handleRpc}.
|
|
22
22
|
*/
|
|
23
23
|
export declare function respondMcpHttp(request: Request, tools: McpTool[], serverInfo: {
|
package/dist/http.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The transport core shared by every HTTP host of an MCP server
|
|
2
|
+
* The transport core shared by every HTTP host of an MCP server - the dev/docs MCP in `@nifrajs/cli`
|
|
3
3
|
* (`nifra docs-mcp`), the site's edge worker, and a nifra app mounting `POST /mcp` via
|
|
4
4
|
* {@link ./server.ts}. {@link respondMcpHttp} is a plain Web `fetch` handler: POST a JSON-RPC body →
|
|
5
|
-
* JSON-RPC response; GET is a health page; OPTIONS is the CORS preflight. It never throws
|
|
5
|
+
* JSON-RPC response; GET is a health page; OPTIONS is the CORS preflight. It never throws - a bad body
|
|
6
6
|
* becomes a JSON-RPC parse error. Pass the tools (and optional {@link McpServerFeatures} for resources /
|
|
7
7
|
* prompts / the MCP Apps `ui://` widgets) so each host supplies its own corpus/source behind one core.
|
|
8
8
|
*/
|
|
@@ -69,7 +69,7 @@ async function readJsonBounded(request, maxBytes) {
|
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
71
|
* Handle one MCP request over HTTP against the given `tools`/`features`. POST a JSON-RPC body → JSON-RPC
|
|
72
|
-
* response; GET → a plain-text health page; OPTIONS → CORS preflight. Never throws
|
|
72
|
+
* response; GET → a plain-text health page; OPTIONS → CORS preflight. Never throws - a bad body becomes a
|
|
73
73
|
* JSON-RPC parse error. The dispatch is the shared, transport-agnostic {@link handleRpc}.
|
|
74
74
|
*/
|
|
75
75
|
export async function respondMcpHttp(request, tools, serverInfo, options = {}) {
|
|
@@ -77,7 +77,7 @@ export async function respondMcpHttp(request, tools, serverInfo, options = {}) {
|
|
|
77
77
|
return new Response(null, { status: 204, headers: CORS });
|
|
78
78
|
if (request.method === "GET") {
|
|
79
79
|
return new Response(options.health ??
|
|
80
|
-
"MCP server
|
|
80
|
+
"MCP server - POST JSON-RPC 2.0 here (methods: initialize, tools/list, tools/call).", { headers: { "content-type": "text/plain; charset=utf-8", ...CORS } });
|
|
81
81
|
}
|
|
82
82
|
if (request.method !== "POST") {
|
|
83
83
|
return new Response("method not allowed", {
|
|
@@ -99,7 +99,7 @@ export async function respondMcpHttp(request, tools, serverInfo, options = {}) {
|
|
|
99
99
|
const response = await handleRpc(message, tools, serverInfo, options.features ?? {}, {
|
|
100
100
|
signal: request.signal,
|
|
101
101
|
});
|
|
102
|
-
// A notification (no id) yields null
|
|
102
|
+
// A notification (no id) yields null - acknowledge with 204, no body.
|
|
103
103
|
if (response === null)
|
|
104
104
|
return new Response(null, { status: 204, headers: CORS });
|
|
105
105
|
return Response.json(response, { headers: CORS });
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/mcp`
|
|
2
|
+
* `@nifrajs/mcp` - build MCP servers (and MCP Apps) for a nifra app.
|
|
3
3
|
*
|
|
4
|
-
* - {@link ./protocol.ts}
|
|
4
|
+
* - {@link ./protocol.ts} - the transport-agnostic JSON-RPC dispatch ({@link handleRpc}) + types, plus
|
|
5
5
|
* the MCP Apps (SEP-1865) extension (`structuredContent`, `_meta.ui.resourceUri`, the UI capability).
|
|
6
|
-
* - {@link ./http.ts}
|
|
6
|
+
* - {@link ./http.ts} - {@link respondMcpHttp}, a Web `fetch` handler you mount at `POST /mcp`.
|
|
7
7
|
*
|
|
8
|
-
* Higher-level authoring helpers
|
|
9
|
-
*
|
|
8
|
+
* Higher-level authoring helpers - {@link defineMcpWidget}, {@link defineMcpTool}, {@link createMcpServer}
|
|
9
|
+
* - build MCP Apps servers whose tool results render real UI.
|
|
10
10
|
*/
|
|
11
11
|
export { bridgeScript, type McpAppBridge } from "./bridge.js";
|
|
12
12
|
export { type McpHttpOptions, respondMcpHttp } from "./http.js";
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/mcp`
|
|
2
|
+
* `@nifrajs/mcp` - build MCP servers (and MCP Apps) for a nifra app.
|
|
3
3
|
*
|
|
4
|
-
* - {@link ./protocol.ts}
|
|
4
|
+
* - {@link ./protocol.ts} - the transport-agnostic JSON-RPC dispatch ({@link handleRpc}) + types, plus
|
|
5
5
|
* the MCP Apps (SEP-1865) extension (`structuredContent`, `_meta.ui.resourceUri`, the UI capability).
|
|
6
|
-
* - {@link ./http.ts}
|
|
6
|
+
* - {@link ./http.ts} - {@link respondMcpHttp}, a Web `fetch` handler you mount at `POST /mcp`.
|
|
7
7
|
*
|
|
8
|
-
* Higher-level authoring helpers
|
|
9
|
-
*
|
|
8
|
+
* Higher-level authoring helpers - {@link defineMcpWidget}, {@link defineMcpTool}, {@link createMcpServer}
|
|
9
|
+
* - build MCP Apps servers whose tool results render real UI.
|
|
10
10
|
*/
|
|
11
11
|
export { bridgeScript } from "./bridge.js";
|
|
12
12
|
export { respondMcpHttp } from "./http.js";
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pure MCP (Model Context Protocol) JSON-RPC dispatch
|
|
2
|
+
* The pure MCP (Model Context Protocol) JSON-RPC dispatch - no I/O, no `Bun.*`, no side effects, so it
|
|
3
3
|
* unit-tests cleanly. A transport (stdio in `@nifrajs/cli`'s `mcp.ts`, Streamable-HTTP in {@link ./http.ts})
|
|
4
4
|
* wires this to a byte stream; the tools/resources are injected, so the protocol logic is exercised
|
|
5
5
|
* without spawning or reading streams.
|
|
6
6
|
*
|
|
7
|
-
* Beyond base MCP this also implements the server half of **MCP Apps (SEP-1865)**
|
|
7
|
+
* Beyond base MCP this also implements the server half of **MCP Apps (SEP-1865)** - interactive `ui://`
|
|
8
8
|
* widgets: a tool links a UI resource via `_meta.ui.resourceUri`, `tools/call` returns `structuredContent`
|
|
9
9
|
* for the widget to render, and `initialize` advertises the `io.modelcontextprotocol/ui` extension. All of
|
|
10
|
-
* it is additive
|
|
10
|
+
* it is additive - a tool whose handler returns a plain `string` behaves exactly as before.
|
|
11
11
|
*/
|
|
12
12
|
export declare const PROTOCOL_VERSION = "2024-11-05";
|
|
13
13
|
/** The MIME type a UI resource MUST use so a host recognizes it as an MCP App widget (SEP-1865). */
|
|
@@ -23,7 +23,7 @@ export interface McpToolContext {
|
|
|
23
23
|
readonly progressToken?: ProgressToken;
|
|
24
24
|
readonly reportProgress: (progress: number, total?: number) => void;
|
|
25
25
|
}
|
|
26
|
-
/** A single content block in a tool result. Today only text
|
|
26
|
+
/** A single content block in a tool result. Today only text - the model-facing representation. */
|
|
27
27
|
export interface McpContentBlock {
|
|
28
28
|
readonly type: "text";
|
|
29
29
|
readonly text: string;
|
|
@@ -42,7 +42,7 @@ export interface McpToolResult {
|
|
|
42
42
|
}
|
|
43
43
|
/** A tool the agent can call. `handler` returns the text shown to the agent, or a rich {@link McpToolResult}
|
|
44
44
|
* (for MCP Apps: structured data + a `ui://` widget link). `_meta` is the per-tool descriptor metadata
|
|
45
|
-
* surfaced in `tools/list`
|
|
45
|
+
* surfaced in `tools/list` - for a widget tool it carries `ui.resourceUri`. */
|
|
46
46
|
/** MCP tool safety hints (`readOnlyHint`/`destructiveHint`/…) surfaced in `tools/list`, per the MCP spec. */
|
|
47
47
|
export interface McpToolAnnotations {
|
|
48
48
|
readonly title?: string;
|
package/dist/protocol.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pure MCP (Model Context Protocol) JSON-RPC dispatch
|
|
2
|
+
* The pure MCP (Model Context Protocol) JSON-RPC dispatch - no I/O, no `Bun.*`, no side effects, so it
|
|
3
3
|
* unit-tests cleanly. A transport (stdio in `@nifrajs/cli`'s `mcp.ts`, Streamable-HTTP in {@link ./http.ts})
|
|
4
4
|
* wires this to a byte stream; the tools/resources are injected, so the protocol logic is exercised
|
|
5
5
|
* without spawning or reading streams.
|
|
6
6
|
*
|
|
7
|
-
* Beyond base MCP this also implements the server half of **MCP Apps (SEP-1865)**
|
|
7
|
+
* Beyond base MCP this also implements the server half of **MCP Apps (SEP-1865)** - interactive `ui://`
|
|
8
8
|
* widgets: a tool links a UI resource via `_meta.ui.resourceUri`, `tools/call` returns `structuredContent`
|
|
9
9
|
* for the widget to render, and `initialize` advertises the `io.modelcontextprotocol/ui` extension. All of
|
|
10
|
-
* it is additive
|
|
10
|
+
* it is additive - a tool whose handler returns a plain `string` behaves exactly as before.
|
|
11
11
|
*/
|
|
12
12
|
export const PROTOCOL_VERSION = "2024-11-05";
|
|
13
13
|
/** The MIME type a UI resource MUST use so a host recognizes it as an MCP App widget (SEP-1865). */
|
|
@@ -251,7 +251,7 @@ export async function handleRpc(message, tools, serverInfo, features = {}, optio
|
|
|
251
251
|
});
|
|
252
252
|
}
|
|
253
253
|
catch {
|
|
254
|
-
// Don't surface the raw read error
|
|
254
|
+
// Don't surface the raw read error - it can leak a filesystem path. Return a generic message.
|
|
255
255
|
return rpcError(rid, -32000, `failed to read resource: ${String(uri)}`);
|
|
256
256
|
}
|
|
257
257
|
}
|
package/dist/react.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface ReactWidgetOptions {
|
|
|
13
13
|
/** Minify the client bundle (default true). */
|
|
14
14
|
readonly minify?: boolean;
|
|
15
15
|
}
|
|
16
|
-
/** Build a {@link McpWidget} from a React component. Async
|
|
16
|
+
/** Build a {@link McpWidget} from a React component. Async - it bundles the component at definition time
|
|
17
17
|
* (a one-time cost at server start); pass the result to `createMcpServer({ widgets })` / a tool's `widget`. */
|
|
18
18
|
export declare function reactWidget(opts: ReactWidgetOptions): Promise<McpWidget>;
|
|
19
19
|
//# sourceMappingURL=react.d.ts.map
|
package/dist/react.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `reactWidget`
|
|
2
|
+
* `reactWidget` - author an MCP Apps (`ui://`) widget from a React component instead of hand-written HTML.
|
|
3
3
|
*
|
|
4
4
|
* The component is bundled for the browser (via `Bun.build`) together with a bootstrap that mounts it and
|
|
5
|
-
* re-renders on every `structuredContent` the host pushes
|
|
5
|
+
* re-renders on every `structuredContent` the host pushes - `mcpApp.onData(data => root.render(<C {...data}/>))`.
|
|
6
6
|
* So a widget author writes a normal nifra/React component that takes the tool's structured data as props;
|
|
7
7
|
* the bridge wiring and bundling are handled here. Client-rendered (the widget shell is the resource the
|
|
8
8
|
* host fetches once; data arrives over the bridge), which matches how a host renders MCP App widgets.
|
|
9
9
|
*
|
|
10
|
-
* `react` + `react-dom` resolve from the CONSUMER's `node_modules` at bundle time
|
|
10
|
+
* `react` + `react-dom` resolve from the CONSUMER's `node_modules` at bundle time - they are NOT a
|
|
11
11
|
* dependency of `@nifrajs/mcp`. This entry is Bun-only (it calls `Bun.build`); import it from
|
|
12
12
|
* `@nifrajs/mcp/react`, not the package root, so the core stays runtime-agnostic and dependency-free.
|
|
13
13
|
*/
|
|
@@ -18,8 +18,8 @@ import { defineMcpWidget } from "./widget.js";
|
|
|
18
18
|
/** Bundle `component` + a bootstrap into a self-contained browser IIFE that renders on pushed data. */
|
|
19
19
|
async function bundleComponent(componentPath, minify) {
|
|
20
20
|
const dir = dirname(componentPath);
|
|
21
|
-
// The bootstrap must sit next to the component so its relative import
|
|
22
|
-
// resolution up the tree
|
|
21
|
+
// The bootstrap must sit next to the component so its relative import - and `react`/`react-dom`
|
|
22
|
+
// resolution up the tree - work against the consumer's node_modules. Unique name; removed in `finally`.
|
|
23
23
|
const bootPath = join(dir, `.nifra-mcp-widget.${randomBytes(6).toString("hex")}.tsx`);
|
|
24
24
|
const boot = `import { createElement } from "react"
|
|
25
25
|
import { createRoot } from "react-dom/client"
|
|
@@ -52,12 +52,12 @@ if (el) {
|
|
|
52
52
|
await rm(bootPath, { force: true });
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
/** Build a {@link McpWidget} from a React component. Async
|
|
55
|
+
/** Build a {@link McpWidget} from a React component. Async - it bundles the component at definition time
|
|
56
56
|
* (a one-time cost at server start); pass the result to `createMcpServer({ widgets })` / a tool's `widget`. */
|
|
57
57
|
export async function reactWidget(opts) {
|
|
58
58
|
const bundle = await bundleComponent(opts.component, opts.minify ?? true);
|
|
59
59
|
// Escape any `</script` the bundle may contain (inside string/regex literals) so it can't terminate
|
|
60
|
-
// this inline <script> early
|
|
60
|
+
// this inline <script> early - `<\/script` is equivalent JS but invisible to the HTML parser.
|
|
61
61
|
const safe = bundle.replace(/<\/(script)/gi, "<\\/$1");
|
|
62
62
|
return defineMcpWidget({
|
|
63
63
|
uri: opts.uri,
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `createMcpServer`
|
|
2
|
+
* `createMcpServer` - assemble a mountable MCP server for a nifra app, with MCP Apps (`ui://`) widgets.
|
|
3
3
|
*
|
|
4
4
|
* Pass tools (ideally from {@link ./tool.ts | defineMcpTool}) and widgets (from {@link ./widget.ts |
|
|
5
5
|
* defineMcpWidget}); the widgets' resources are registered and the `io.modelcontextprotocol/ui` capability
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* {@link McpServer.handle} dispatches a single JSON-RPC message directly (for headless verification / tests).
|
|
16
16
|
*
|
|
17
|
-
* SECURITY: this has NO built-in authentication
|
|
17
|
+
* SECURITY: this has NO built-in authentication - once mounted, every tool is callable by anyone who can
|
|
18
18
|
* reach the route (the CORS header is `*` and no credentials are used, so it is effectively public). That
|
|
19
19
|
* is fine for read-only/public tools; if any tool mutates state or returns private data, gate the route
|
|
20
20
|
* yourself (check an `Authorization` header / session in the nifra handler before calling `mcp.fetch`).
|
|
@@ -25,7 +25,7 @@ export interface CreateMcpServerOptions {
|
|
|
25
25
|
readonly name: string;
|
|
26
26
|
readonly version: string;
|
|
27
27
|
readonly tools?: readonly McpTool[];
|
|
28
|
-
/** MCP Apps widgets
|
|
28
|
+
/** MCP Apps widgets - their resources are registered and the UI capability is advertised. */
|
|
29
29
|
readonly widgets?: readonly McpWidget[];
|
|
30
30
|
/** Extra (non-widget) resources to expose. */
|
|
31
31
|
readonly resources?: readonly McpResource[];
|
|
@@ -42,9 +42,9 @@ export interface McpServer {
|
|
|
42
42
|
name: string;
|
|
43
43
|
version: string;
|
|
44
44
|
};
|
|
45
|
-
/** Web `fetch` handler
|
|
45
|
+
/** Web `fetch` handler - mount at `POST /mcp` (GET is a health page, OPTIONS the CORS preflight). */
|
|
46
46
|
fetch(request: Request): Promise<Response>;
|
|
47
|
-
/** Dispatch one JSON-RPC message directly (no HTTP)
|
|
47
|
+
/** Dispatch one JSON-RPC message directly (no HTTP) - for headless verification and unit tests. */
|
|
48
48
|
handle(message: JsonRpcRequest): Promise<JsonRpcResponse | null>;
|
|
49
49
|
}
|
|
50
50
|
export declare function createMcpServer(opts: CreateMcpServerOptions): McpServer;
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `createMcpServer`
|
|
2
|
+
* `createMcpServer` - assemble a mountable MCP server for a nifra app, with MCP Apps (`ui://`) widgets.
|
|
3
3
|
*
|
|
4
4
|
* Pass tools (ideally from {@link ./tool.ts | defineMcpTool}) and widgets (from {@link ./widget.ts |
|
|
5
5
|
* defineMcpWidget}); the widgets' resources are registered and the `io.modelcontextprotocol/ui` capability
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* {@link McpServer.handle} dispatches a single JSON-RPC message directly (for headless verification / tests).
|
|
16
16
|
*
|
|
17
|
-
* SECURITY: this has NO built-in authentication
|
|
17
|
+
* SECURITY: this has NO built-in authentication - once mounted, every tool is callable by anyone who can
|
|
18
18
|
* reach the route (the CORS header is `*` and no credentials are used, so it is effectively public). That
|
|
19
19
|
* is fine for read-only/public tools; if any tool mutates state or returns private data, gate the route
|
|
20
20
|
* yourself (check an `Authorization` header / session in the nifra handler before calling `mcp.fetch`).
|
package/dist/tool.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `defineMcpTool`
|
|
2
|
+
* `defineMcpTool` - author a tool for a nifra MCP server, optionally backed by an MCP Apps widget.
|
|
3
3
|
*
|
|
4
4
|
* The handler returns either a `string` (a plain text result, base MCP) or an object with `structuredContent`
|
|
5
5
|
* (the data a linked {@link ./widget.ts | widget} renders) and/or `text` (the model-facing summary). When a
|
|
6
6
|
* `widget` is passed, the tool advertises it via `_meta.ui.resourceUri` on both `tools/list` and each
|
|
7
|
-
* `tools/call` result, so the host knows to render the widget
|
|
7
|
+
* `tools/call` result, so the host knows to render the widget - see {@link ../protocol.ts}.
|
|
8
8
|
*/
|
|
9
9
|
import type { McpContentBlock, McpTool, McpToolContext } from "./protocol.js";
|
|
10
10
|
import type { InferOutput, StandardSchemaV1 } from "./standard.js";
|
|
@@ -15,7 +15,7 @@ export type McpToolHandlerResult = string | {
|
|
|
15
15
|
readonly text?: string;
|
|
16
16
|
/** Explicit content blocks (overrides `text`). */
|
|
17
17
|
readonly content?: readonly McpContentBlock[];
|
|
18
|
-
/** Structured data for the linked widget
|
|
18
|
+
/** Structured data for the linked widget - NOT added to the model's context. */
|
|
19
19
|
readonly structuredContent?: Record<string, unknown>;
|
|
20
20
|
readonly _meta?: Record<string, unknown>;
|
|
21
21
|
readonly isError?: boolean;
|
|
@@ -24,7 +24,7 @@ export type McpToolHandlerResult = string | {
|
|
|
24
24
|
* A render-intent hint for GENERATIVE hosts: how to present the result's
|
|
25
25
|
* `structuredContent` when the host renders its OWN themed UI rather than an iframe widget. The host maps
|
|
26
26
|
* the intent to a component in its design system (a shadcn/Tailwind table, form, metric card, …). Open
|
|
27
|
-
* union
|
|
27
|
+
* union - pick a known tag or a custom one.
|
|
28
28
|
*/
|
|
29
29
|
export type McpUiIntent = "table" | "list" | "cards" | "form" | "metric" | "detail" | "chart" | (string & {});
|
|
30
30
|
/** The default when no `input` schema is given: arguments arrive untyped and unvalidated. */
|
|
@@ -48,7 +48,7 @@ export interface DefineMcpToolOptions<S extends StandardSchemaV1 = UntypedArgs>
|
|
|
48
48
|
/** Link a widget so the tool's result renders as interactive UI in MCP Apps hosts (iframe). */
|
|
49
49
|
readonly widget?: McpWidget;
|
|
50
50
|
/** Render-intent for generative hosts that build their own themed UI from `structuredContent`. Lands
|
|
51
|
-
* in `_meta.ui.intent`. Independent of `widget`
|
|
51
|
+
* in `_meta.ui.intent`. Independent of `widget` - a tool can offer both (widget for MCP Apps hosts,
|
|
52
52
|
* intent for generative builders). */
|
|
53
53
|
readonly intent?: McpUiIntent;
|
|
54
54
|
readonly handler: (args: InferOutput<S>, context: McpToolContext) => McpToolHandlerResult | Promise<McpToolHandlerResult>;
|
package/dist/tool.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `defineMcpTool`
|
|
2
|
+
* `defineMcpTool` - author a tool for a nifra MCP server, optionally backed by an MCP Apps widget.
|
|
3
3
|
*
|
|
4
4
|
* The handler returns either a `string` (a plain text result, base MCP) or an object with `structuredContent`
|
|
5
5
|
* (the data a linked {@link ./widget.ts | widget} renders) and/or `text` (the model-facing summary). When a
|
|
6
6
|
* `widget` is passed, the tool advertises it via `_meta.ui.resourceUri` on both `tools/list` and each
|
|
7
|
-
* `tools/call` result, so the host knows to render the widget
|
|
7
|
+
* `tools/call` result, so the host knows to render the widget - see {@link ../protocol.ts}.
|
|
8
8
|
*/
|
|
9
9
|
import { formatIssues, jsonSchemaOf } from "./standard.js";
|
|
10
10
|
/** Build a tool's descriptor `_meta` from its widget link and/or render intent (both under `ui`). */
|
package/dist/widget.d.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `defineMcpWidget`
|
|
2
|
+
* `defineMcpWidget` - author an MCP Apps (`ui://`) interactive widget for a nifra MCP server.
|
|
3
3
|
*
|
|
4
4
|
* A widget is ONE self-contained HTML document, served as an MCP resource with MIME {@link UI_MIME}. The
|
|
5
5
|
* host renders it in a sandboxed iframe and pushes the linked tool's `structuredContent` in over the
|
|
6
|
-
* {@link ./bridge.ts} bridge
|
|
6
|
+
* {@link ./bridge.ts} bridge - so the author writes plain markup + `mcpApp.onData(render)`, no transport
|
|
7
7
|
* glue. Link it to a tool with {@link ./tool.ts | defineMcpTool({ widget })}, which stamps the tool's
|
|
8
8
|
* `_meta.ui.resourceUri` from {@link McpWidget.meta}.
|
|
9
9
|
*
|
|
10
|
-
* SECURITY: `structuredContent` (and any theme) arriving over the bridge is UNTRUSTED
|
|
10
|
+
* SECURITY: `structuredContent` (and any theme) arriving over the bridge is UNTRUSTED - escape it before
|
|
11
11
|
* writing to `innerHTML`, or render with a framework that escapes (React/Preact, or `@nifrajs/mcp/react`).
|
|
12
12
|
* The sandboxed iframe limits blast radius, but a widget that builds markup by string concatenation can
|
|
13
13
|
* still XSS itself; the bridge passes data through, it does not sanitize.
|
|
14
14
|
*/
|
|
15
15
|
import { type McpResource } from "./protocol.js";
|
|
16
16
|
export interface DefineMcpWidgetOptions {
|
|
17
|
-
/** The widget's resource URI
|
|
17
|
+
/** The widget's resource URI - MUST use the `ui://` scheme, e.g. `ui://orders/table`. */
|
|
18
18
|
readonly uri: string;
|
|
19
19
|
/** Human-readable name (shown by hosts that list resources). */
|
|
20
20
|
readonly name: string;
|
|
21
21
|
readonly description?: string;
|
|
22
22
|
/** Document title (defaults to `name`). */
|
|
23
23
|
readonly title?: string;
|
|
24
|
-
/** Extra `<head>` content
|
|
24
|
+
/** Extra `<head>` content - `<style>`, `<meta>`, fonts. The bridge `<script>` is injected for you. */
|
|
25
25
|
readonly head?: string;
|
|
26
26
|
/** The widget body: markup plus a `<script>` that renders from `mcpApp.onData(...)` and may
|
|
27
27
|
* `mcpApp.callTool(...)`. The bridge global is defined before this runs. */
|
package/dist/widget.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `defineMcpWidget`
|
|
2
|
+
* `defineMcpWidget` - author an MCP Apps (`ui://`) interactive widget for a nifra MCP server.
|
|
3
3
|
*
|
|
4
4
|
* A widget is ONE self-contained HTML document, served as an MCP resource with MIME {@link UI_MIME}. The
|
|
5
5
|
* host renders it in a sandboxed iframe and pushes the linked tool's `structuredContent` in over the
|
|
6
|
-
* {@link ./bridge.ts} bridge
|
|
6
|
+
* {@link ./bridge.ts} bridge - so the author writes plain markup + `mcpApp.onData(render)`, no transport
|
|
7
7
|
* glue. Link it to a tool with {@link ./tool.ts | defineMcpTool({ widget })}, which stamps the tool's
|
|
8
8
|
* `_meta.ui.resourceUri` from {@link McpWidget.meta}.
|
|
9
9
|
*
|
|
10
|
-
* SECURITY: `structuredContent` (and any theme) arriving over the bridge is UNTRUSTED
|
|
10
|
+
* SECURITY: `structuredContent` (and any theme) arriving over the bridge is UNTRUSTED - escape it before
|
|
11
11
|
* writing to `innerHTML`, or render with a framework that escapes (React/Preact, or `@nifrajs/mcp/react`).
|
|
12
12
|
* The sandboxed iframe limits blast radius, but a widget that builds markup by string concatenation can
|
|
13
13
|
* still XSS itself; the bridge passes data through, it does not sanitize.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/mcp",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Build MCP servers
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "Build MCP servers - and MCP Apps (interactive `ui://` widgets, SEP-1865) - for a nifra app. The transport-agnostic JSON-RPC core (also used by `@nifrajs/cli`'s dev MCP), a Streamable-HTTP handler you mount at `POST /mcp`, and `defineMcpWidget`/`defineMcpTool` for tool results that render real UI in MCP hosts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
7
7
|
"typescript",
|
package/src/bridge.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* host → widget : `ui/notifications/tool-result` (the tools/call result incl. `structuredContent`),
|
|
6
6
|
* `ui/notifications/tool-input` (the call arguments), and `ui/notifications/theme`
|
|
7
|
-
* (the host's design tokens)
|
|
7
|
+
* (the host's design tokens) - pushed as JSON-RPC notifications.
|
|
8
8
|
* widget → host : `tools/call` requests (id-matched) so a button can re-invoke a tool through the host.
|
|
9
9
|
*
|
|
10
10
|
* Theming follows the shadcn/Tailwind semantic-token convention (`--background`, `--foreground`,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* app with zero per-widget work.
|
|
14
14
|
*
|
|
15
15
|
* It exposes a small global the widget author scripts against, so authoring a widget is just
|
|
16
|
-
* `mcpApp.onData(render)` + (optionally) `mcpApp.callTool(name, args)`
|
|
16
|
+
* `mcpApp.onData(render)` + (optionally) `mcpApp.callTool(name, args)` - no postMessage plumbing.
|
|
17
17
|
*
|
|
18
18
|
* Returned as a string so {@link ./widget.ts} can inline it in the resource HTML (a widget ships as ONE
|
|
19
19
|
* self-contained document; the host renders it in a sandboxed iframe with no network of its own).
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
export interface McpAppBridge {
|
|
24
24
|
/** Latest `structuredContent` the host pushed (or `null` before the first result). */
|
|
25
25
|
data: unknown
|
|
26
|
-
/** Register a callback for `structuredContent`
|
|
26
|
+
/** Register a callback for `structuredContent` - fired immediately if data already arrived, then on
|
|
27
27
|
* every later `ui/notifications/tool-result`. The common case: `mcpApp.onData(render)`. */
|
|
28
28
|
onData(cb: (data: unknown) => void): void
|
|
29
29
|
/** Register a callback for the raw tool-call arguments (`ui/notifications/tool-input`). */
|
|
@@ -94,7 +94,7 @@ const BRIDGE_SOURCE = `(function () {
|
|
|
94
94
|
window.addEventListener("message", function (event) {
|
|
95
95
|
// Only trust the embedding host (the parent window). A widget renders in a sandboxed iframe whose host
|
|
96
96
|
// posts via the parent (or a sandbox-proxy parent), so event.source === window.parent. Reject anything
|
|
97
|
-
// else
|
|
97
|
+
// else - including a null source (synthetic/dispatched events) - so a sibling/opener frame or in-page
|
|
98
98
|
// script can't spoof tool-results, tool-call responses, or theme. (A top-level widget has no host.)
|
|
99
99
|
if (event.source !== window.parent || window.parent === window) return;
|
|
100
100
|
var msg = event.data;
|
package/src/http.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The transport core shared by every HTTP host of an MCP server
|
|
2
|
+
* The transport core shared by every HTTP host of an MCP server - the dev/docs MCP in `@nifrajs/cli`
|
|
3
3
|
* (`nifra docs-mcp`), the site's edge worker, and a nifra app mounting `POST /mcp` via
|
|
4
4
|
* {@link ./server.ts}. {@link respondMcpHttp} is a plain Web `fetch` handler: POST a JSON-RPC body →
|
|
5
|
-
* JSON-RPC response; GET is a health page; OPTIONS is the CORS preflight. It never throws
|
|
5
|
+
* JSON-RPC response; GET is a health page; OPTIONS is the CORS preflight. It never throws - a bad body
|
|
6
6
|
* becomes a JSON-RPC parse error. Pass the tools (and optional {@link McpServerFeatures} for resources /
|
|
7
7
|
* prompts / the MCP Apps `ui://` widgets) so each host supplies its own corpus/source behind one core.
|
|
8
8
|
*/
|
|
@@ -88,7 +88,7 @@ async function readJsonBounded(
|
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* Handle one MCP request over HTTP against the given `tools`/`features`. POST a JSON-RPC body → JSON-RPC
|
|
91
|
-
* response; GET → a plain-text health page; OPTIONS → CORS preflight. Never throws
|
|
91
|
+
* response; GET → a plain-text health page; OPTIONS → CORS preflight. Never throws - a bad body becomes a
|
|
92
92
|
* JSON-RPC parse error. The dispatch is the shared, transport-agnostic {@link handleRpc}.
|
|
93
93
|
*/
|
|
94
94
|
export async function respondMcpHttp(
|
|
@@ -101,7 +101,7 @@ export async function respondMcpHttp(
|
|
|
101
101
|
if (request.method === "GET") {
|
|
102
102
|
return new Response(
|
|
103
103
|
options.health ??
|
|
104
|
-
"MCP server
|
|
104
|
+
"MCP server - POST JSON-RPC 2.0 here (methods: initialize, tools/list, tools/call).",
|
|
105
105
|
{ headers: { "content-type": "text/plain; charset=utf-8", ...CORS } },
|
|
106
106
|
)
|
|
107
107
|
}
|
|
@@ -125,7 +125,7 @@ export async function respondMcpHttp(
|
|
|
125
125
|
const response = await handleRpc(message, tools, serverInfo, options.features ?? {}, {
|
|
126
126
|
signal: request.signal,
|
|
127
127
|
})
|
|
128
|
-
// A notification (no id) yields null
|
|
128
|
+
// A notification (no id) yields null - acknowledge with 204, no body.
|
|
129
129
|
if (response === null) return new Response(null, { status: 204, headers: CORS })
|
|
130
130
|
return Response.json(response, { headers: CORS })
|
|
131
131
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/mcp`
|
|
2
|
+
* `@nifrajs/mcp` - build MCP servers (and MCP Apps) for a nifra app.
|
|
3
3
|
*
|
|
4
|
-
* - {@link ./protocol.ts}
|
|
4
|
+
* - {@link ./protocol.ts} - the transport-agnostic JSON-RPC dispatch ({@link handleRpc}) + types, plus
|
|
5
5
|
* the MCP Apps (SEP-1865) extension (`structuredContent`, `_meta.ui.resourceUri`, the UI capability).
|
|
6
|
-
* - {@link ./http.ts}
|
|
6
|
+
* - {@link ./http.ts} - {@link respondMcpHttp}, a Web `fetch` handler you mount at `POST /mcp`.
|
|
7
7
|
*
|
|
8
|
-
* Higher-level authoring helpers
|
|
9
|
-
*
|
|
8
|
+
* Higher-level authoring helpers - {@link defineMcpWidget}, {@link defineMcpTool}, {@link createMcpServer}
|
|
9
|
+
* - build MCP Apps servers whose tool results render real UI.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export { bridgeScript, type McpAppBridge } from "./bridge.ts"
|
package/src/protocol.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pure MCP (Model Context Protocol) JSON-RPC dispatch
|
|
2
|
+
* The pure MCP (Model Context Protocol) JSON-RPC dispatch - no I/O, no `Bun.*`, no side effects, so it
|
|
3
3
|
* unit-tests cleanly. A transport (stdio in `@nifrajs/cli`'s `mcp.ts`, Streamable-HTTP in {@link ./http.ts})
|
|
4
4
|
* wires this to a byte stream; the tools/resources are injected, so the protocol logic is exercised
|
|
5
5
|
* without spawning or reading streams.
|
|
6
6
|
*
|
|
7
|
-
* Beyond base MCP this also implements the server half of **MCP Apps (SEP-1865)**
|
|
7
|
+
* Beyond base MCP this also implements the server half of **MCP Apps (SEP-1865)** - interactive `ui://`
|
|
8
8
|
* widgets: a tool links a UI resource via `_meta.ui.resourceUri`, `tools/call` returns `structuredContent`
|
|
9
9
|
* for the widget to render, and `initialize` advertises the `io.modelcontextprotocol/ui` extension. All of
|
|
10
|
-
* it is additive
|
|
10
|
+
* it is additive - a tool whose handler returns a plain `string` behaves exactly as before.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
export const PROTOCOL_VERSION = "2024-11-05"
|
|
@@ -28,7 +28,7 @@ export interface McpToolContext {
|
|
|
28
28
|
readonly reportProgress: (progress: number, total?: number) => void
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** A single content block in a tool result. Today only text
|
|
31
|
+
/** A single content block in a tool result. Today only text - the model-facing representation. */
|
|
32
32
|
export interface McpContentBlock {
|
|
33
33
|
readonly type: "text"
|
|
34
34
|
readonly text: string
|
|
@@ -49,7 +49,7 @@ export interface McpToolResult {
|
|
|
49
49
|
|
|
50
50
|
/** A tool the agent can call. `handler` returns the text shown to the agent, or a rich {@link McpToolResult}
|
|
51
51
|
* (for MCP Apps: structured data + a `ui://` widget link). `_meta` is the per-tool descriptor metadata
|
|
52
|
-
* surfaced in `tools/list`
|
|
52
|
+
* surfaced in `tools/list` - for a widget tool it carries `ui.resourceUri`. */
|
|
53
53
|
/** MCP tool safety hints (`readOnlyHint`/`destructiveHint`/…) surfaced in `tools/list`, per the MCP spec. */
|
|
54
54
|
export interface McpToolAnnotations {
|
|
55
55
|
readonly title?: string
|
|
@@ -375,7 +375,7 @@ export async function handleRpc(
|
|
|
375
375
|
],
|
|
376
376
|
})
|
|
377
377
|
} catch {
|
|
378
|
-
// Don't surface the raw read error
|
|
378
|
+
// Don't surface the raw read error - it can leak a filesystem path. Return a generic message.
|
|
379
379
|
return rpcError(rid, -32000, `failed to read resource: ${String(uri)}`)
|
|
380
380
|
}
|
|
381
381
|
}
|
package/src/react.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `reactWidget`
|
|
2
|
+
* `reactWidget` - author an MCP Apps (`ui://`) widget from a React component instead of hand-written HTML.
|
|
3
3
|
*
|
|
4
4
|
* The component is bundled for the browser (via `Bun.build`) together with a bootstrap that mounts it and
|
|
5
|
-
* re-renders on every `structuredContent` the host pushes
|
|
5
|
+
* re-renders on every `structuredContent` the host pushes - `mcpApp.onData(data => root.render(<C {...data}/>))`.
|
|
6
6
|
* So a widget author writes a normal nifra/React component that takes the tool's structured data as props;
|
|
7
7
|
* the bridge wiring and bundling are handled here. Client-rendered (the widget shell is the resource the
|
|
8
8
|
* host fetches once; data arrives over the bridge), which matches how a host renders MCP App widgets.
|
|
9
9
|
*
|
|
10
|
-
* `react` + `react-dom` resolve from the CONSUMER's `node_modules` at bundle time
|
|
10
|
+
* `react` + `react-dom` resolve from the CONSUMER's `node_modules` at bundle time - they are NOT a
|
|
11
11
|
* dependency of `@nifrajs/mcp`. This entry is Bun-only (it calls `Bun.build`); import it from
|
|
12
12
|
* `@nifrajs/mcp/react`, not the package root, so the core stays runtime-agnostic and dependency-free.
|
|
13
13
|
*/
|
|
@@ -34,8 +34,8 @@ export interface ReactWidgetOptions {
|
|
|
34
34
|
/** Bundle `component` + a bootstrap into a self-contained browser IIFE that renders on pushed data. */
|
|
35
35
|
async function bundleComponent(componentPath: string, minify: boolean): Promise<string> {
|
|
36
36
|
const dir = dirname(componentPath)
|
|
37
|
-
// The bootstrap must sit next to the component so its relative import
|
|
38
|
-
// resolution up the tree
|
|
37
|
+
// The bootstrap must sit next to the component so its relative import - and `react`/`react-dom`
|
|
38
|
+
// resolution up the tree - work against the consumer's node_modules. Unique name; removed in `finally`.
|
|
39
39
|
const bootPath = join(dir, `.nifra-mcp-widget.${randomBytes(6).toString("hex")}.tsx`)
|
|
40
40
|
const boot = `import { createElement } from "react"
|
|
41
41
|
import { createRoot } from "react-dom/client"
|
|
@@ -69,12 +69,12 @@ if (el) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/** Build a {@link McpWidget} from a React component. Async
|
|
72
|
+
/** Build a {@link McpWidget} from a React component. Async - it bundles the component at definition time
|
|
73
73
|
* (a one-time cost at server start); pass the result to `createMcpServer({ widgets })` / a tool's `widget`. */
|
|
74
74
|
export async function reactWidget(opts: ReactWidgetOptions): Promise<McpWidget> {
|
|
75
75
|
const bundle = await bundleComponent(opts.component, opts.minify ?? true)
|
|
76
76
|
// Escape any `</script` the bundle may contain (inside string/regex literals) so it can't terminate
|
|
77
|
-
// this inline <script> early
|
|
77
|
+
// this inline <script> early - `<\/script` is equivalent JS but invisible to the HTML parser.
|
|
78
78
|
const safe = bundle.replace(/<\/(script)/gi, "<\\/$1")
|
|
79
79
|
return defineMcpWidget({
|
|
80
80
|
uri: opts.uri,
|
package/src/server.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `createMcpServer`
|
|
2
|
+
* `createMcpServer` - assemble a mountable MCP server for a nifra app, with MCP Apps (`ui://`) widgets.
|
|
3
3
|
*
|
|
4
4
|
* Pass tools (ideally from {@link ./tool.ts | defineMcpTool}) and widgets (from {@link ./widget.ts |
|
|
5
5
|
* defineMcpWidget}); the widgets' resources are registered and the `io.modelcontextprotocol/ui` capability
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* {@link McpServer.handle} dispatches a single JSON-RPC message directly (for headless verification / tests).
|
|
16
16
|
*
|
|
17
|
-
* SECURITY: this has NO built-in authentication
|
|
17
|
+
* SECURITY: this has NO built-in authentication - once mounted, every tool is callable by anyone who can
|
|
18
18
|
* reach the route (the CORS header is `*` and no credentials are used, so it is effectively public). That
|
|
19
19
|
* is fine for read-only/public tools; if any tool mutates state or returns private data, gate the route
|
|
20
20
|
* yourself (check an `Authorization` header / session in the nifra handler before calling `mcp.fetch`).
|
|
@@ -37,7 +37,7 @@ export interface CreateMcpServerOptions {
|
|
|
37
37
|
readonly name: string
|
|
38
38
|
readonly version: string
|
|
39
39
|
readonly tools?: readonly McpTool[]
|
|
40
|
-
/** MCP Apps widgets
|
|
40
|
+
/** MCP Apps widgets - their resources are registered and the UI capability is advertised. */
|
|
41
41
|
readonly widgets?: readonly McpWidget[]
|
|
42
42
|
/** Extra (non-widget) resources to expose. */
|
|
43
43
|
readonly resources?: readonly McpResource[]
|
|
@@ -52,9 +52,9 @@ export interface McpServer {
|
|
|
52
52
|
readonly tools: McpTool[]
|
|
53
53
|
readonly features: McpServerFeatures
|
|
54
54
|
readonly serverInfo: { name: string; version: string }
|
|
55
|
-
/** Web `fetch` handler
|
|
55
|
+
/** Web `fetch` handler - mount at `POST /mcp` (GET is a health page, OPTIONS the CORS preflight). */
|
|
56
56
|
fetch(request: Request): Promise<Response>
|
|
57
|
-
/** Dispatch one JSON-RPC message directly (no HTTP)
|
|
57
|
+
/** Dispatch one JSON-RPC message directly (no HTTP) - for headless verification and unit tests. */
|
|
58
58
|
handle(message: JsonRpcRequest): Promise<JsonRpcResponse | null>
|
|
59
59
|
}
|
|
60
60
|
|
package/src/tool.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `defineMcpTool`
|
|
2
|
+
* `defineMcpTool` - author a tool for a nifra MCP server, optionally backed by an MCP Apps widget.
|
|
3
3
|
*
|
|
4
4
|
* The handler returns either a `string` (a plain text result, base MCP) or an object with `structuredContent`
|
|
5
5
|
* (the data a linked {@link ./widget.ts | widget} renders) and/or `text` (the model-facing summary). When a
|
|
6
6
|
* `widget` is passed, the tool advertises it via `_meta.ui.resourceUri` on both `tools/list` and each
|
|
7
|
-
* `tools/call` result, so the host knows to render the widget
|
|
7
|
+
* `tools/call` result, so the host knows to render the widget - see {@link ../protocol.ts}.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { McpContentBlock, McpTool, McpToolContext, McpToolResult } from "./protocol.ts"
|
|
@@ -20,7 +20,7 @@ export type McpToolHandlerResult =
|
|
|
20
20
|
readonly text?: string
|
|
21
21
|
/** Explicit content blocks (overrides `text`). */
|
|
22
22
|
readonly content?: readonly McpContentBlock[]
|
|
23
|
-
/** Structured data for the linked widget
|
|
23
|
+
/** Structured data for the linked widget - NOT added to the model's context. */
|
|
24
24
|
readonly structuredContent?: Record<string, unknown>
|
|
25
25
|
readonly _meta?: Record<string, unknown>
|
|
26
26
|
readonly isError?: boolean
|
|
@@ -30,7 +30,7 @@ export type McpToolHandlerResult =
|
|
|
30
30
|
* A render-intent hint for GENERATIVE hosts: how to present the result's
|
|
31
31
|
* `structuredContent` when the host renders its OWN themed UI rather than an iframe widget. The host maps
|
|
32
32
|
* the intent to a component in its design system (a shadcn/Tailwind table, form, metric card, …). Open
|
|
33
|
-
* union
|
|
33
|
+
* union - pick a known tag or a custom one.
|
|
34
34
|
*/
|
|
35
35
|
export type McpUiIntent =
|
|
36
36
|
| "table"
|
|
@@ -64,7 +64,7 @@ export interface DefineMcpToolOptions<S extends StandardSchemaV1 = UntypedArgs>
|
|
|
64
64
|
/** Link a widget so the tool's result renders as interactive UI in MCP Apps hosts (iframe). */
|
|
65
65
|
readonly widget?: McpWidget
|
|
66
66
|
/** Render-intent for generative hosts that build their own themed UI from `structuredContent`. Lands
|
|
67
|
-
* in `_meta.ui.intent`. Independent of `widget`
|
|
67
|
+
* in `_meta.ui.intent`. Independent of `widget` - a tool can offer both (widget for MCP Apps hosts,
|
|
68
68
|
* intent for generative builders). */
|
|
69
69
|
readonly intent?: McpUiIntent
|
|
70
70
|
readonly handler: (
|
package/src/widget.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `defineMcpWidget`
|
|
2
|
+
* `defineMcpWidget` - author an MCP Apps (`ui://`) interactive widget for a nifra MCP server.
|
|
3
3
|
*
|
|
4
4
|
* A widget is ONE self-contained HTML document, served as an MCP resource with MIME {@link UI_MIME}. The
|
|
5
5
|
* host renders it in a sandboxed iframe and pushes the linked tool's `structuredContent` in over the
|
|
6
|
-
* {@link ./bridge.ts} bridge
|
|
6
|
+
* {@link ./bridge.ts} bridge - so the author writes plain markup + `mcpApp.onData(render)`, no transport
|
|
7
7
|
* glue. Link it to a tool with {@link ./tool.ts | defineMcpTool({ widget })}, which stamps the tool's
|
|
8
8
|
* `_meta.ui.resourceUri` from {@link McpWidget.meta}.
|
|
9
9
|
*
|
|
10
|
-
* SECURITY: `structuredContent` (and any theme) arriving over the bridge is UNTRUSTED
|
|
10
|
+
* SECURITY: `structuredContent` (and any theme) arriving over the bridge is UNTRUSTED - escape it before
|
|
11
11
|
* writing to `innerHTML`, or render with a framework that escapes (React/Preact, or `@nifrajs/mcp/react`).
|
|
12
12
|
* The sandboxed iframe limits blast radius, but a widget that builds markup by string concatenation can
|
|
13
13
|
* still XSS itself; the bridge passes data through, it does not sanitize.
|
|
@@ -17,14 +17,14 @@ import { bridgeScript } from "./bridge.ts"
|
|
|
17
17
|
import { type McpResource, UI_MIME } from "./protocol.ts"
|
|
18
18
|
|
|
19
19
|
export interface DefineMcpWidgetOptions {
|
|
20
|
-
/** The widget's resource URI
|
|
20
|
+
/** The widget's resource URI - MUST use the `ui://` scheme, e.g. `ui://orders/table`. */
|
|
21
21
|
readonly uri: string
|
|
22
22
|
/** Human-readable name (shown by hosts that list resources). */
|
|
23
23
|
readonly name: string
|
|
24
24
|
readonly description?: string
|
|
25
25
|
/** Document title (defaults to `name`). */
|
|
26
26
|
readonly title?: string
|
|
27
|
-
/** Extra `<head>` content
|
|
27
|
+
/** Extra `<head>` content - `<style>`, `<meta>`, fonts. The bridge `<script>` is injected for you. */
|
|
28
28
|
readonly head?: string
|
|
29
29
|
/** The widget body: markup plus a `<script>` that renders from `mcpApp.onData(...)` and may
|
|
30
30
|
* `mcpApp.callTool(...)`. The bridge global is defined before this runs. */
|