@nifrajs/mcp 1.0.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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `defineMcpWidget` — author an MCP Apps (`ui://`) interactive widget for a nifra MCP server.
3
+ *
4
+ * A widget is ONE self-contained HTML document, served as an MCP resource with MIME {@link UI_MIME}. The
5
+ * host renders it in a sandboxed iframe and pushes the linked tool's `structuredContent` in over the
6
+ * {@link ./bridge.ts} bridge — so the author writes plain markup + `mcpApp.onData(render)`, no transport
7
+ * glue. Link it to a tool with {@link ./tool.ts | defineMcpTool({ widget })}, which stamps the tool's
8
+ * `_meta.ui.resourceUri` from {@link McpWidget.meta}.
9
+ *
10
+ * SECURITY: `structuredContent` (and any theme) arriving over the bridge is UNTRUSTED — escape it before
11
+ * writing to `innerHTML`, or render with a framework that escapes (React/Preact, or `@nifrajs/mcp/react`).
12
+ * The sandboxed iframe limits blast radius, but a widget that builds markup by string concatenation can
13
+ * still XSS itself; the bridge passes data through, it does not sanitize.
14
+ */
15
+ import { type McpResource } from "./protocol.js";
16
+ export interface DefineMcpWidgetOptions {
17
+ /** The widget's resource URI — MUST use the `ui://` scheme, e.g. `ui://orders/table`. */
18
+ readonly uri: string;
19
+ /** Human-readable name (shown by hosts that list resources). */
20
+ readonly name: string;
21
+ readonly description?: string;
22
+ /** Document title (defaults to `name`). */
23
+ readonly title?: string;
24
+ /** Extra `<head>` content — `<style>`, `<meta>`, fonts. The bridge `<script>` is injected for you. */
25
+ readonly head?: string;
26
+ /** The widget body: markup plus a `<script>` that renders from `mcpApp.onData(...)` and may
27
+ * `mcpApp.callTool(...)`. The bridge global is defined before this runs. */
28
+ readonly html: string;
29
+ }
30
+ /** The MCP Apps `_meta.ui.resourceUri` link, emitted nested (current spec) AND flat (the deprecated
31
+ * `ui/resourceUri`, still read by some hosts) so a tool renders across the transition. */
32
+ export declare function uiResourceMeta(uri: string): Record<string, unknown>;
33
+ /** A widget: the resource to register on the server, its `ui://` URI, and the `_meta` link for its tool. */
34
+ export interface McpWidget {
35
+ readonly uri: string;
36
+ readonly resource: McpResource;
37
+ readonly meta: Record<string, unknown>;
38
+ }
39
+ /** Assemble the full self-contained widget document (bridge inlined in `<head>` so body scripts can use
40
+ * `mcpApp` immediately). */
41
+ export declare function widgetDocument(opts: DefineMcpWidgetOptions): string;
42
+ export declare function defineMcpWidget(opts: DefineMcpWidgetOptions): McpWidget;
43
+ //# sourceMappingURL=widget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget.d.ts","sourceRoot":"","sources":["../src/widget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,KAAK,WAAW,EAAW,MAAM,eAAe,CAAA;AAEzD,MAAM,WAAW,sBAAsB;IACrC,yFAAyF;IACzF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,sGAAsG;IACtG,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB;gFAC4E;IAC5E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED;0FAC0F;AAC1F,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEnE;AAED,4GAA4G;AAC5G,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAA;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACvC;AAMD;4BAC4B;AAC5B,wBAAgB,cAAc,CAAC,IAAI,EAAE,sBAAsB,GAAG,MAAM,CAgBnE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,SAAS,CAavE"}
package/dist/widget.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * `defineMcpWidget` — author an MCP Apps (`ui://`) interactive widget for a nifra MCP server.
3
+ *
4
+ * A widget is ONE self-contained HTML document, served as an MCP resource with MIME {@link UI_MIME}. The
5
+ * host renders it in a sandboxed iframe and pushes the linked tool's `structuredContent` in over the
6
+ * {@link ./bridge.ts} bridge — so the author writes plain markup + `mcpApp.onData(render)`, no transport
7
+ * glue. Link it to a tool with {@link ./tool.ts | defineMcpTool({ widget })}, which stamps the tool's
8
+ * `_meta.ui.resourceUri` from {@link McpWidget.meta}.
9
+ *
10
+ * SECURITY: `structuredContent` (and any theme) arriving over the bridge is UNTRUSTED — escape it before
11
+ * writing to `innerHTML`, or render with a framework that escapes (React/Preact, or `@nifrajs/mcp/react`).
12
+ * The sandboxed iframe limits blast radius, but a widget that builds markup by string concatenation can
13
+ * still XSS itself; the bridge passes data through, it does not sanitize.
14
+ */
15
+ import { bridgeScript } from "./bridge.js";
16
+ import { UI_MIME } from "./protocol.js";
17
+ /** The MCP Apps `_meta.ui.resourceUri` link, emitted nested (current spec) AND flat (the deprecated
18
+ * `ui/resourceUri`, still read by some hosts) so a tool renders across the transition. */
19
+ export function uiResourceMeta(uri) {
20
+ return { ui: { resourceUri: uri }, "ui/resourceUri": uri };
21
+ }
22
+ function escapeHtml(text) {
23
+ return text.replace(/[&<>]/g, (c) => (c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;"));
24
+ }
25
+ /** Assemble the full self-contained widget document (bridge inlined in `<head>` so body scripts can use
26
+ * `mcpApp` immediately). */
27
+ export function widgetDocument(opts) {
28
+ const title = escapeHtml(opts.title ?? opts.name);
29
+ return `<!doctype html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="utf-8" />
33
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
34
+ <title>${title}</title>
35
+ <script>${bridgeScript()}</script>
36
+ ${opts.head ?? ""}
37
+ </head>
38
+ <body>
39
+ ${opts.html}
40
+ </body>
41
+ </html>
42
+ `;
43
+ }
44
+ export function defineMcpWidget(opts) {
45
+ if (!opts.uri.startsWith("ui://")) {
46
+ throw new Error(`defineMcpWidget: uri must use the ui:// scheme (got "${opts.uri}")`);
47
+ }
48
+ const html = widgetDocument(opts);
49
+ const resource = {
50
+ uri: opts.uri,
51
+ name: opts.name,
52
+ ...(opts.description !== undefined ? { description: opts.description } : {}),
53
+ mimeType: UI_MIME,
54
+ read: () => Promise.resolve({ text: html, mimeType: UI_MIME }),
55
+ };
56
+ return { uri: opts.uri, resource, meta: uiResourceMeta(opts.uri) };
57
+ }
58
+ //# sourceMappingURL=widget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget.js","sourceRoot":"","sources":["../src/widget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAoB,OAAO,EAAE,MAAM,eAAe,CAAA;AAiBzD;0FAC0F;AAC1F,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAA;AAC5D,CAAC;AASD,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;AAC3F,CAAC;AAED;4BAC4B;AAC5B,MAAM,UAAU,cAAc,CAAC,IAA4B;IACzD,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,CAAA;IACjD,OAAO;;;;;SAKA,KAAK;UACJ,YAAY,EAAE;EACtB,IAAI,CAAC,IAAI,IAAI,EAAE;;;EAGf,IAAI,CAAC,IAAI;;;CAGV,CAAA;AACD,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAA4B;IAC1D,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,wDAAwD,IAAI,CAAC,GAAG,IAAI,CAAC,CAAA;IACvF,CAAC;IACD,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,CAAA;IACjC,MAAM,QAAQ,GAAgB;QAC5B,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,GAAG,CAAC,IAAI,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,QAAQ,EAAE,OAAO;QACjB,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;KAC/D,CAAA;IACD,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;AACpE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@nifrajs/mcp",
3
+ "version": "1.0.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
+ "keywords": [
6
+ "bun",
7
+ "typescript",
8
+ "nifra",
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "mcp-apps",
12
+ "mcp-ui",
13
+ "json-rpc",
14
+ "ai",
15
+ "tools",
16
+ "widgets"
17
+ ],
18
+ "type": "module",
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "bun": "./src/index.ts",
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ },
28
+ "./protocol": {
29
+ "bun": "./src/protocol.ts",
30
+ "types": "./dist/protocol.d.ts",
31
+ "default": "./dist/protocol.js"
32
+ },
33
+ "./http": {
34
+ "bun": "./src/http.ts",
35
+ "types": "./dist/http.d.ts",
36
+ "default": "./dist/http.js"
37
+ },
38
+ "./react": {
39
+ "bun": "./src/react.ts",
40
+ "types": "./dist/react.d.ts",
41
+ "default": "./dist/react.js"
42
+ }
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "src"
47
+ ],
48
+ "sideEffects": false,
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
51
+ },
52
+ "license": "MIT",
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/nifrajs/nifra.git",
59
+ "directory": "packages/mcp"
60
+ },
61
+ "bugs": {
62
+ "url": "https://github.com/nifrajs/nifra/issues"
63
+ },
64
+ "homepage": "https://github.com/nifrajs/nifra#readme"
65
+ }
package/src/bridge.ts ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The tiny, dependency-free client bridge injected into every `ui://` widget's HTML (MCP Apps / SEP-1865).
3
+ * It speaks the MCP-JSON-RPC-over-`postMessage` subset a host exposes to an embedded UI:
4
+ *
5
+ * host → widget : `ui/notifications/tool-result` (the tools/call result incl. `structuredContent`),
6
+ * `ui/notifications/tool-input` (the call arguments), and `ui/notifications/theme`
7
+ * (the host's design tokens) — pushed as JSON-RPC notifications.
8
+ * widget → host : `tools/call` requests (id-matched) so a button can re-invoke a tool through the host.
9
+ *
10
+ * Theming follows the shadcn/Tailwind semantic-token convention (`--background`, `--foreground`,
11
+ * `--primary`, `--card`, `--border`, `--radius`, …). The host pushes its resolved tokens; the bridge
12
+ * applies them to the document root, so a widget that styles with `var(--primary)` etc. matches the host
13
+ * app (e.g. a ShipNow-generated app injecting its theme preset) with zero per-widget work.
14
+ *
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)` — no postMessage plumbing.
17
+ *
18
+ * Returned as a string so {@link ./widget.ts} can inline it in the resource HTML (a widget ships as ONE
19
+ * self-contained document; the host renders it in a sandboxed iframe with no network of its own).
20
+ */
21
+
22
+ /** The author-facing global injected into a widget. Kept minimal and stable. */
23
+ export interface McpAppBridge {
24
+ /** Latest `structuredContent` the host pushed (or `null` before the first result). */
25
+ data: unknown
26
+ /** Register a callback for `structuredContent` — fired immediately if data already arrived, then on
27
+ * every later `ui/notifications/tool-result`. The common case: `mcpApp.onData(render)`. */
28
+ onData(cb: (data: unknown) => void): void
29
+ /** Register a callback for the raw tool-call arguments (`ui/notifications/tool-input`). */
30
+ onInput(cb: (args: unknown) => void): void
31
+ /** Latest theme the host pushed (`{ mode, tokens }`), or `null` before the first push. */
32
+ theme: unknown
33
+ /** Register a callback for host theme pushes. The bridge ALSO auto-applies the tokens to the document
34
+ * root (shadcn semantic vars + `color-scheme`/`data-theme`), so most widgets need no theme code. */
35
+ onTheme(cb: (theme: unknown) => void): void
36
+ /** Invoke a tool back through the host; resolves with the tools/call result. */
37
+ callTool(name: string, args?: Record<string, unknown>): Promise<unknown>
38
+ }
39
+
40
+ /** The bridge source, as a string for inlining in a `<script>`. Self-contained, no imports. */
41
+ export function bridgeScript(): string {
42
+ return BRIDGE_SOURCE
43
+ }
44
+
45
+ // Authored as a plain string (not a function we `.toString()`) so a bundler/minifier can't rename the
46
+ // global or strip it, and so it has zero dependency on this module's runtime. Intentionally ES5-ish for
47
+ // maximal host/iframe compatibility.
48
+ const BRIDGE_SOURCE = `(function () {
49
+ var pending = {};
50
+ var nextId = 1;
51
+ var dataCbs = [];
52
+ var inputCbs = [];
53
+ var themeCbs = [];
54
+ function applyTheme(theme) {
55
+ if (!theme || typeof theme !== "object") return;
56
+ var root = document.documentElement;
57
+ if (theme.mode === "light" || theme.mode === "dark") {
58
+ root.setAttribute("data-theme", theme.mode);
59
+ root.style.colorScheme = theme.mode;
60
+ }
61
+ var tokens = theme.tokens;
62
+ if (tokens && typeof tokens === "object") {
63
+ for (var k in tokens) {
64
+ if (Object.prototype.hasOwnProperty.call(tokens, k)) {
65
+ root.style.setProperty(k.charAt(0) === "-" ? k : "--" + k, String(tokens[k]));
66
+ }
67
+ }
68
+ }
69
+ }
70
+ var api = {
71
+ data: null,
72
+ theme: null,
73
+ onData: function (cb) {
74
+ if (typeof cb !== "function") return;
75
+ dataCbs.push(cb);
76
+ if (api.data !== null) { try { cb(api.data); } catch (e) {} }
77
+ },
78
+ onInput: function (cb) { if (typeof cb === "function") inputCbs.push(cb); },
79
+ onTheme: function (cb) {
80
+ if (typeof cb !== "function") return;
81
+ themeCbs.push(cb);
82
+ if (api.theme !== null) { try { cb(api.theme); } catch (e) {} }
83
+ },
84
+ callTool: function (name, args) {
85
+ var id = "w" + nextId++;
86
+ return new Promise(function (resolve, reject) {
87
+ pending[id] = { resolve: resolve, reject: reject };
88
+ post({ jsonrpc: "2.0", id: id, method: "tools/call", params: { name: name, arguments: args || {} } });
89
+ });
90
+ }
91
+ };
92
+ function post(msg) { try { (window.parent || window).postMessage(msg, "*"); } catch (e) {} }
93
+ function emit(list, value) { for (var i = 0; i < list.length; i++) { try { list[i](value); } catch (e) {} } }
94
+ window.addEventListener("message", function (event) {
95
+ // Only trust the embedding host (the parent window). A widget renders in a sandboxed iframe whose host
96
+ // posts via the parent (or a sandbox-proxy parent), so event.source === window.parent. Reject anything
97
+ // else — including a null source (synthetic/dispatched events) — so a sibling/opener frame or in-page
98
+ // script can't spoof tool-results, tool-call responses, or theme. (A top-level widget has no host.)
99
+ if (event.source !== window.parent || window.parent === window) return;
100
+ var msg = event.data;
101
+ if (!msg || typeof msg !== "object") return;
102
+ // A response to one of our tools/call requests (id-matched).
103
+ if (msg.id != null && pending[msg.id]) {
104
+ var p = pending[msg.id]; delete pending[msg.id];
105
+ if (msg.error) p.reject(msg.error); else p.resolve(msg.result);
106
+ return;
107
+ }
108
+ // A host → widget notification.
109
+ if (msg.method === "ui/notifications/tool-result") {
110
+ var params = msg.params || {};
111
+ var structured = params.structuredContent != null ? params.structuredContent : params;
112
+ api.data = structured;
113
+ emit(dataCbs, structured);
114
+ } else if (msg.method === "ui/notifications/tool-input") {
115
+ emit(inputCbs, (msg.params || {}).arguments);
116
+ } else if (msg.method === "ui/notifications/theme") {
117
+ var theme = msg.params || {};
118
+ api.theme = theme;
119
+ applyTheme(theme);
120
+ emit(themeCbs, theme);
121
+ }
122
+ });
123
+ window.mcpApp = api;
124
+ // Some hosts wait for the iframe to signal readiness before pushing data; harmless if ignored.
125
+ post({ jsonrpc: "2.0", method: "ui/notifications/ready" });
126
+ })();`
package/src/http.ts ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The transport core shared by every HTTP host of an MCP server — the dev/docs MCP in `@nifrajs/cli`
3
+ * (`nifra docs-mcp`), the site's edge worker, and a nifra app mounting `POST /mcp` via
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 — a bad body
6
+ * becomes a JSON-RPC parse error. Pass the tools (and optional {@link McpServerFeatures} for resources /
7
+ * prompts / the MCP Apps `ui://` widgets) so each host supplies its own corpus/source behind one core.
8
+ */
9
+
10
+ import {
11
+ handleRpc,
12
+ type JsonRpcRequest,
13
+ type McpServerFeatures,
14
+ type McpTool,
15
+ rpcError,
16
+ } from "./protocol.ts"
17
+
18
+ const CORS: Record<string, string> = {
19
+ "access-control-allow-origin": "*",
20
+ "access-control-allow-methods": "POST, GET, OPTIONS",
21
+ "access-control-allow-headers": "content-type",
22
+ }
23
+
24
+ const DEFAULT_MAX_BODY_BYTES = 1_000_000
25
+ const TEXT_DECODER = new TextDecoder()
26
+
27
+ export interface McpHttpOptions {
28
+ /** Maximum JSON-RPC request body size in bytes. Default 1 MB. */
29
+ readonly maxBodyBytes?: number
30
+ /** Resources / prompts / the MCP Apps UI extension served alongside the tools. */
31
+ readonly features?: McpServerFeatures
32
+ /** Shown on the GET health page so each host can describe itself. */
33
+ readonly health?: string
34
+ }
35
+
36
+ function parseContentLength(value: string): number | undefined {
37
+ if (value.length === 0) return undefined
38
+ let length = 0
39
+ for (let i = 0; i < value.length; i++) {
40
+ const digit = value.charCodeAt(i) - 48
41
+ if (digit < 0 || digit > 9) return undefined
42
+ length = length * 10 + digit
43
+ if (length > Number.MAX_SAFE_INTEGER) return Number.POSITIVE_INFINITY
44
+ }
45
+ return length
46
+ }
47
+
48
+ async function readJsonBounded(
49
+ request: Request,
50
+ maxBytes: number,
51
+ ): Promise<{ ok: true; value: unknown } | { ok: false; status: 400 | 413 }> {
52
+ const declared = request.headers.get("content-length")
53
+ if (declared !== null) {
54
+ const length = parseContentLength(declared)
55
+ if (length === undefined) return { ok: false, status: 400 }
56
+ if (length > maxBytes) return { ok: false, status: 413 }
57
+ }
58
+
59
+ const body = request.body
60
+ if (body === null) return { ok: false, status: 400 }
61
+
62
+ const reader = body.getReader()
63
+ const chunks: Uint8Array[] = []
64
+ let total = 0
65
+ for (;;) {
66
+ const { done, value } = await reader.read()
67
+ if (done) break
68
+ total += value.byteLength
69
+ if (total > maxBytes) {
70
+ await reader.cancel()
71
+ return { ok: false, status: 413 }
72
+ }
73
+ chunks.push(value)
74
+ }
75
+
76
+ const bytes = new Uint8Array(total)
77
+ let offset = 0
78
+ for (const chunk of chunks) {
79
+ bytes.set(chunk, offset)
80
+ offset += chunk.byteLength
81
+ }
82
+ try {
83
+ return { ok: true, value: JSON.parse(TEXT_DECODER.decode(bytes)) as unknown }
84
+ } catch {
85
+ return { ok: false, status: 400 }
86
+ }
87
+ }
88
+
89
+ /**
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 — a bad body becomes a
92
+ * JSON-RPC parse error. The dispatch is the shared, transport-agnostic {@link handleRpc}.
93
+ */
94
+ export async function respondMcpHttp(
95
+ request: Request,
96
+ tools: McpTool[],
97
+ serverInfo: { name: string; version: string },
98
+ options: McpHttpOptions = {},
99
+ ): Promise<Response> {
100
+ if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: CORS })
101
+ if (request.method === "GET") {
102
+ return new Response(
103
+ options.health ??
104
+ "MCP server — POST JSON-RPC 2.0 here (methods: initialize, tools/list, tools/call).",
105
+ { headers: { "content-type": "text/plain; charset=utf-8", ...CORS } },
106
+ )
107
+ }
108
+ if (request.method !== "POST") {
109
+ return new Response("method not allowed", {
110
+ status: 405,
111
+ headers: { allow: "POST, GET", ...CORS },
112
+ })
113
+ }
114
+ const parsed = await readJsonBounded(request, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES)
115
+ if (!parsed.ok) {
116
+ if (parsed.status === 413) {
117
+ return Response.json(rpcError(null, -32000, "payload too large"), {
118
+ status: 413,
119
+ headers: CORS,
120
+ })
121
+ }
122
+ return Response.json(rpcError(null, -32700, "parse error"), { status: 400, headers: CORS })
123
+ }
124
+ const message = parsed.value as JsonRpcRequest
125
+ const response = await handleRpc(message, tools, serverInfo, options.features ?? {}, {
126
+ signal: request.signal,
127
+ })
128
+ // A notification (no id) yields null — acknowledge with 204, no body.
129
+ if (response === null) return new Response(null, { status: 204, headers: CORS })
130
+ return Response.json(response, { headers: CORS })
131
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `@nifrajs/mcp` — build MCP servers (and MCP Apps) for a nifra app.
3
+ *
4
+ * - {@link ./protocol.ts} — the transport-agnostic JSON-RPC dispatch ({@link handleRpc}) + types, plus
5
+ * the MCP Apps (SEP-1865) extension (`structuredContent`, `_meta.ui.resourceUri`, the UI capability).
6
+ * - {@link ./http.ts} — {@link respondMcpHttp}, a Web `fetch` handler you mount at `POST /mcp`.
7
+ *
8
+ * Higher-level authoring helpers — {@link defineMcpWidget}, {@link defineMcpTool}, {@link createMcpServer}
9
+ * — build MCP Apps servers whose tool results render real UI.
10
+ */
11
+
12
+ export { bridgeScript, type McpAppBridge } from "./bridge.ts"
13
+ export { type McpHttpOptions, respondMcpHttp } from "./http.ts"
14
+ export * from "./protocol.ts"
15
+ export { type CreateMcpServerOptions, createMcpServer, type McpServer } from "./server.ts"
16
+ export {
17
+ type DefineMcpToolOptions,
18
+ defineMcpTool,
19
+ type McpToolHandlerResult,
20
+ type McpUiIntent,
21
+ } from "./tool.ts"
22
+ export {
23
+ type DefineMcpWidgetOptions,
24
+ defineMcpWidget,
25
+ type McpWidget,
26
+ uiResourceMeta,
27
+ widgetDocument,
28
+ } from "./widget.ts"