@feniix/bridgekit 0.18.0 → 0.19.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/CHANGELOG.md +54 -0
- package/README.md +60 -17
- package/dist/src/adapters/mcp-http.d.ts +15 -0
- package/dist/src/adapters/mcp-http.js +16 -0
- package/dist/src/adapters/mcp.d.ts +32 -1
- package/dist/src/adapters/mcp.js +130 -68
- package/dist/src/core/define-tool.d.ts +18 -3
- package/dist/src/core/output-schema.d.ts +1 -1
- package/dist/src/core/output-schema.js +1 -1
- package/dist/src/mcp.d.ts +2 -1
- package/dist/src/mcp.js +2 -1
- package/examples/README.md +5 -3
- package/llms.txt +36 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,60 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.19.0] - 2026-10-14
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Forward tool `icons` and `_meta` from `hostExtras.mcp`, with construction-time
|
|
14
|
+
snapshots and no new wire keys when absent (#128).
|
|
15
|
+
- Add `createMcpHttpHandler` for Web-standard HTTP serving of modern MCP.
|
|
16
|
+
Listener and security policy remain application-owned
|
|
17
|
+
(#126). HTTP options and handler types are exported from the MCP entrypoint.
|
|
18
|
+
|
|
19
|
+
- Export `createMcpServerFactory(options)` from the MCP entrypoint: validates and
|
|
20
|
+
snapshots tools once and returns a per-call `Server` factory for the SDK's
|
|
21
|
+
`createMcpHandler` / `serveStdio`, replacing `createMcpServer` for custom
|
|
22
|
+
serving.
|
|
23
|
+
|
|
24
|
+
### Changed (breaking)
|
|
25
|
+
|
|
26
|
+
- Serve only the modern MCP `2026-07-28` era. `runMcpStdioServer` and
|
|
27
|
+
`createMcpHttpHandler` reject legacy-era openings (`2025-11-25` and earlier)
|
|
28
|
+
with `-32022` naming the supported revisions, and log one `[bridgekit-mcp]`
|
|
29
|
+
stderr line per rejected request. Clients limited to the legacy era (SDK v1, or
|
|
30
|
+
v2 in `legacy` negotiation mode) can no longer connect. The SDK v2 client
|
|
31
|
+
defaults to `versionNegotiation.mode: "legacy"`, so a default-constructed
|
|
32
|
+
client is rejected: pin `{ mode: { pin: "2026-07-28" } }` or use `"auto"`.
|
|
33
|
+
`legacy` is removed from `CreateMcpHttpHandlerOptions` and ignored at runtime
|
|
34
|
+
for untyped callers. `createMcpServer` is deprecated as a direct
|
|
35
|
+
`Server.connect` target (a directly connected `Server` is legacy-only); use the
|
|
36
|
+
new `createMcpServerFactory(options)` with the SDK's `createMcpHandler` to
|
|
37
|
+
compose custom serving. Both the `tools/list` `outputSchema` and the
|
|
38
|
+
`tools/call` `structuredContent` of non-object outputs are no longer
|
|
39
|
+
`{ result }`-wrapped for any served client. The SDK v1 dev dependency and its
|
|
40
|
+
stdio interop test were removed (#131 retired). See `docs/mcp-v2-migration.md`.
|
|
41
|
+
- `runMcpStdioServer` now returns `Promise<McpStdioServerHandle>` with an
|
|
42
|
+
idempotent `close()` that aborts pending requests (#129). Awaiting startup
|
|
43
|
+
means stdio wiring is ready, not that a client has connected. Callers that
|
|
44
|
+
explicitly annotated `Promise<void>` must update their annotation; existing
|
|
45
|
+
callers that ignore the resolved value need no changes.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- Snapshot validated tool definitions once for HTTP and stdio serving, including
|
|
50
|
+
discarded discovery probes; later caller array/metadata mutations cannot leak onto the wire.
|
|
51
|
+
- Reject invalid MCP icon shapes at construction with tool-attributed
|
|
52
|
+
`BRIDGEKIT_MCP_INVALID_ICONS` errors; HTTP serving defaults to stderr diagnostics.
|
|
53
|
+
- Clarify that stdio `close()` aborts requests without awaiting asynchronous tool cleanup.
|
|
54
|
+
|
|
55
|
+
### Documentation
|
|
56
|
+
|
|
57
|
+
- Record the official conformance suite's HTTP-only server interface and the
|
|
58
|
+
resulting blocker for direct stdio conformance testing (#130).
|
|
59
|
+
- Record four passing official HTTP scenarios and the pinned harness's rejection
|
|
60
|
+
of `2026-07-28`; direct stdio/full modern conformance remains unverified (#130).
|
|
61
|
+
Those legacy-filtered results predate modern-only serving and no longer reproduce.
|
|
62
|
+
|
|
9
63
|
## [0.18.0] - 2026-10-06
|
|
10
64
|
|
|
11
65
|
### Added
|
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ import {
|
|
|
94
94
|
type PortableValidationError,
|
|
95
95
|
} from "@feniix/bridgekit";
|
|
96
96
|
import { registerPiTools } from "@feniix/bridgekit/pi";
|
|
97
|
-
import {
|
|
97
|
+
import { createMcpHttpHandler, runMcpStdioServer } from "@feniix/bridgekit/mcp";
|
|
98
98
|
import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
|
|
99
99
|
```
|
|
100
100
|
|
|
@@ -128,9 +128,8 @@ const echoTool = definePortableTool({
|
|
|
128
128
|
Any JSON Schema root is accepted except a top-level `$ref` (`Type.Ref` /
|
|
129
129
|
`Type.Cyclic`): objects, intersections of objects, arrays, primitives, `null` and
|
|
130
130
|
unions all work. Success calls must return matching `structuredContent`; legacy
|
|
131
|
-
`details` alone does not satisfy the schema. Non-object values reach
|
|
132
|
-
clients as-is;
|
|
133
|
-
the value as `{ result: ... }`. Pi receives the raw value on `structuredContent`
|
|
131
|
+
`details` alone does not satisfy the schema. Non-object values reach MCP
|
|
132
|
+
clients as-is (the served modern era; legacy-era clients are not served). Pi receives the raw value on `structuredContent`
|
|
134
133
|
and `{ result: value }` on renderer-facing `details`. Missing/invalid output throws a tool-attributed `TypeError` at the
|
|
135
134
|
portable seam and becomes an error result through the adapters. Argument/domain
|
|
136
135
|
failures are exempt from success schemas. Without `outputSchema`, existing result
|
|
@@ -252,6 +251,8 @@ export const generateSummaryTool = definePortableTool({
|
|
|
252
251
|
},
|
|
253
252
|
mcp: {
|
|
254
253
|
annotations: { readOnlyHint: true },
|
|
254
|
+
icons: [{ src: "https://example.com/summary.svg", mimeType: "image/svg+xml" }],
|
|
255
|
+
_meta: { "example.com/category": "text" },
|
|
255
256
|
},
|
|
256
257
|
},
|
|
257
258
|
});
|
|
@@ -259,10 +260,20 @@ export const generateSummaryTool = definePortableTool({
|
|
|
259
260
|
|
|
260
261
|
`renderCall` is invoked by pi before execution to render the call line. `renderResult` is invoked by pi after execution to render the result; its `options.expanded` flag tracks the user's Ctrl+O collapsed/expanded toggle. BridgeKit forwards both functions by identity to `pi.registerTool`.
|
|
261
262
|
|
|
263
|
+
MCP `icons` and `_meta` are forwarded only when supplied, including explicit empty
|
|
264
|
+
arrays/objects. At server construction, BridgeKit copies icons (including their
|
|
265
|
+
`sizes` arrays) and shallow-copies `_meta`; nested metadata values must remain
|
|
266
|
+
immutable. BridgeKit never fetches icon URLs. The pi adapter ignores both fields.
|
|
267
|
+
|
|
262
268
|
See `docs/rfc-host-extras.md` for the design rationale (which fields are in scope, why a top-level field beats a sidecar map, the closure rule for future additions). `PortableToolHostExtras` is module-augmentable for custom host adapters; declare your namespace via `declare module "@feniix/bridgekit"`.
|
|
263
269
|
|
|
264
270
|
### MCP adapter
|
|
265
271
|
|
|
272
|
+
`await runMcpStdioServer(options)` returns a `McpStdioServerHandle` with
|
|
273
|
+
idempotent `close(): Promise<void>` for explicit shutdown and cancellation of
|
|
274
|
+
pending requests. Resolution signals startup wiring, not client negotiation or
|
|
275
|
+
shutdown. Stdin EOF also cleans up the server.
|
|
276
|
+
|
|
266
277
|
```ts
|
|
267
278
|
import { realpathSync } from "node:fs";
|
|
268
279
|
import { resolve } from "node:path";
|
|
@@ -302,7 +313,7 @@ Tool `parameters` must resolve to a JSON-Schema object at the top level. `Type.O
|
|
|
302
313
|
|
|
303
314
|
Portable validation failures and portable `isError: true` results return `CallToolResult` with `isError: true`. `structuredContent` is preserved; `details` is used only as a fallback when `structuredContent` is absent. Exporting a server-options factory keeps MCP entrypoints import-passive and easy to test without starting stdio.
|
|
304
315
|
|
|
305
|
-
When a client sends `_meta.progressToken` on `tools/call`, each `ctx.progress?.(update)` becomes a `notifications/progress` notification: `progress` is a per-call counter starting at 1 (the spec requires it to increase), `message` is `update.text`, and `total` is omitted. Without a token `ctx.progress` is `undefined`, so tools should keep calling it optionally. Notifications are flushed before the result and skipped once the request is cancelled. Caveat: the official SDK clients defer notification handlers while handling responses synchronously, so a burst of progress calls issued synchronously right before returning can be dropped by the client when it arrives in the same stdio chunk as the result. BridgeKit writes them in order regardless; tools that emit progress over time are unaffected.
|
|
316
|
+
When a client sends `_meta.progressToken` on `tools/call`, each `ctx.progress?.(update)` becomes a `notifications/progress` notification: `progress` is a per-call counter starting at 1 (the spec requires it to increase), `message` is `update.text`, and `total` is omitted. Without a token `ctx.progress` is `undefined`, so tools should keep calling it optionally. Notifications are flushed before the result and skipped once the request is cancelled. Caveat: the official SDK clients defer notification handlers while handling responses synchronously, so a burst of progress calls issued synchronously right before returning can be dropped by the client when it arrives in the same stdio chunk as the result. BridgeKit writes them in order regardless; tools that emit progress over time are unaffected. Upstream: [typescript-sdk#2580](https://github.com/modelcontextprotocol/typescript-sdk/issues/2580) (fix pending in [typescript-sdk#2967](https://github.com/modelcontextprotocol/typescript-sdk/pull/2967)), tracked in [#132](https://github.com/feniix/bridgekit/issues/132).
|
|
306
317
|
|
|
307
318
|
The two adapters return `{ isError: true }` for argument/domain failures by default.
|
|
308
319
|
Use result guards on portable values; both wire formats now preserve `structuredContent`,
|
|
@@ -311,23 +322,55 @@ The guards match error/data shape, not provenance, so Pi wire failures also matc
|
|
|
311
322
|
at runtime. Raw Pi values still lack portable `text`; do not cast them to
|
|
312
323
|
`PortableToolResult` to use the typed guards.
|
|
313
324
|
|
|
325
|
+
#### Streamable HTTP
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
import { createMcpHttpHandler } from "@feniix/bridgekit/mcp";
|
|
329
|
+
|
|
330
|
+
const handler = createMcpHttpHandler(createMcpServerOptions(), {
|
|
331
|
+
responseMode: "auto", // preserves progress via SSE when needed
|
|
332
|
+
});
|
|
333
|
+
// Mount handler.fetch(request) in your Web-standard framework.
|
|
334
|
+
// Await handler.close() when the application shuts down.
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The handler serves the modern `2026-07-28` era only; legacy-era clients (`2025-11-25` and earlier)
|
|
338
|
+
receive an unsupported-protocol-version error (`-32022`, HTTP 400) naming the
|
|
339
|
+
supported revisions. It does not start a listener or install
|
|
340
|
+
authentication, authorization, Host/Origin validation, CORS, or TLS. **Enforce
|
|
341
|
+
those in your application before calling `fetch`**, especially before exposing a
|
|
342
|
+
local server to a network. SDK HTTP options are accepted as the second argument.
|
|
343
|
+
`responseMode: "json"` drops mid-call notifications; use `"auto"` for progress.
|
|
344
|
+
|
|
314
345
|
#### MCP SDK v2 and modern stdio
|
|
315
346
|
|
|
316
|
-
`runMcpStdioServer(options)` serves
|
|
317
|
-
`
|
|
318
|
-
startup wiring, not at shutdown. Stdin EOF
|
|
319
|
-
|
|
347
|
+
`runMcpStdioServer(options)` serves modern `2026-07-28` through SDK v2's
|
|
348
|
+
`serveStdio`; `2025-11-25` openings are answered with `-32022`. Its `Promise<McpStdioServerHandle>`
|
|
349
|
+
resolves after startup wiring, not at shutdown. Stdin EOF or `handle.close()`
|
|
350
|
+
closes the connection; long-running
|
|
351
|
+
tools must honor `ctx.signal` and release their own resources. `close()` aborts
|
|
352
|
+
requests but does **not** await handler completion or asynchronous abort cleanup;
|
|
353
|
+
coordinate tool cleanup before disposing shared application resources.
|
|
320
354
|
|
|
321
355
|
`createMcpServer` now returns `@modelcontextprotocol/server` SDK v2's low-level
|
|
322
356
|
`Server`. This is a source-level breaking change for consumers using SDK methods
|
|
323
|
-
or types
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
357
|
+
or types. `createMcpServer` is deprecated: a directly connected `Server`
|
|
358
|
+
can only speak the legacy era, which BridgeKit no longer serves. Use
|
|
359
|
+
`runMcpStdioServer` or `createMcpHttpHandler`. See [migration guidance](docs/mcp-v2-migration.md).
|
|
360
|
+
|
|
361
|
+
**Legacy clients are not served.** Clients limited to the legacy era
|
|
362
|
+
(`2025-11-25` and earlier: SDK v1 clients, or v2 clients in `legacy` negotiation
|
|
363
|
+
mode) cannot connect. The SDK v2 client defaults to
|
|
364
|
+
`versionNegotiation.mode: "legacy"`, so a default-constructed client is rejected;
|
|
365
|
+
pin `versionNegotiation: { mode: { pin: "2026-07-28" } }` or use `"auto"`.
|
|
366
|
+
HTTP is exposed through `createMcpHttpHandler`; tasks remain pending.
|
|
367
|
+
|
|
368
|
+
HTTP option and return types are available as `CreateMcpHttpHandlerOptions` and
|
|
369
|
+
`McpHttpHandler` from `@feniix/bridgekit/mcp`; consumers need not import SDK types.
|
|
370
|
+
HTTP errors default to `[bridgekit-mcp]` diagnostics on stderr; `onerror` overrides
|
|
371
|
+
that reporter. Tool definitions are static (`tools.listChanged: false`); the SDK
|
|
372
|
+
handler's `notify.toolsChanged()` is not supported by BridgeKit's static tools.
|
|
373
|
+
Modern HTTP and stdio support request cancellation normally.
|
|
331
374
|
|
|
332
375
|
### bin-wrapper (since 0.11.0)
|
|
333
376
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type CreateMcpHandlerOptions, type McpHttpHandler } from "@modelcontextprotocol/server";
|
|
2
|
+
import { type CreateMcpServerOptions } from "./mcp.js";
|
|
3
|
+
/**
|
|
4
|
+
* SDK handler options minus `legacy`: BridgeKit serves the modern
|
|
5
|
+
* `2026-07-28` era only, so legacy-era requests are always rejected.
|
|
6
|
+
*/
|
|
7
|
+
export type CreateMcpHttpHandlerOptions = Omit<CreateMcpHandlerOptions, "legacy">;
|
|
8
|
+
export type { McpHttpHandler };
|
|
9
|
+
/**
|
|
10
|
+
* Web-standard HTTP serving; no listener, authentication, or origin policy is
|
|
11
|
+
* installed. Applications must enforce those before calling `handler.fetch`.
|
|
12
|
+
* Serves the modern `2026-07-28` era only; `2025-11-25` clients receive the
|
|
13
|
+
* unsupported-protocol-version error naming the supported revisions.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createMcpHttpHandler(options: CreateMcpServerOptions, httpOptions?: CreateMcpHttpHandlerOptions): McpHttpHandler;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
2
|
+
import { createMcpServerFactory } from "./mcp.js";
|
|
3
|
+
/**
|
|
4
|
+
* Web-standard HTTP serving; no listener, authentication, or origin policy is
|
|
5
|
+
* installed. Applications must enforce those before calling `handler.fetch`.
|
|
6
|
+
* Serves the modern `2026-07-28` era only; `2025-11-25` clients receive the
|
|
7
|
+
* unsupported-protocol-version error naming the supported revisions.
|
|
8
|
+
*/
|
|
9
|
+
export function createMcpHttpHandler(options, httpOptions = {}) {
|
|
10
|
+
const factory = createMcpServerFactory(options);
|
|
11
|
+
return createMcpHandler(factory, {
|
|
12
|
+
...httpOptions,
|
|
13
|
+
legacy: "reject",
|
|
14
|
+
onerror: httpOptions.onerror ?? ((error) => process.stderr.write(`[bridgekit-mcp] ${error.message}\n`)),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -20,5 +20,36 @@ export interface CreateMcpServerOptions {
|
|
|
20
20
|
tools: readonly PortableTool<TSchema, PortableToolResult<unknown>>[];
|
|
21
21
|
instructions?: string;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Validate and snapshot the tools once, then return a factory that builds an
|
|
25
|
+
* isolated SDK `Server` per call. Pass it to the SDK's `createMcpHandler` or
|
|
26
|
+
* `serveStdio` for custom serving; unlike `createMcpServer`, validation and the
|
|
27
|
+
* tool snapshot happen at this call, not once per server.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createMcpServerFactory(options: CreateMcpServerOptions): () => Server;
|
|
30
|
+
/**
|
|
31
|
+
* Build a single low-level SDK `Server`.
|
|
32
|
+
*
|
|
33
|
+
* @deprecated A directly connected `Server` (`server.connect(transport)`) can
|
|
34
|
+
* only speak the legacy era, which BridgeKit's serving entrypoints no longer
|
|
35
|
+
* support. Use `runMcpStdioServer` or `createMcpHttpHandler`, or for custom
|
|
36
|
+
* serving pass `createMcpServerFactory(options)` to the SDK's
|
|
37
|
+
* `createMcpHandler(factory, { legacy: "reject" })`. Do not wrap this function
|
|
38
|
+
* in a factory: it revalidates and re-snapshots the tools on every call.
|
|
39
|
+
*/
|
|
23
40
|
export declare function createMcpServer(options: CreateMcpServerOptions): Server;
|
|
24
|
-
export
|
|
41
|
+
export interface McpStdioServerHandle {
|
|
42
|
+
/**
|
|
43
|
+
* Close the transport and abort in-flight requests. Safe to call repeatedly.
|
|
44
|
+
* Does not await tool handlers: asynchronous abort cleanup may outlive close().
|
|
45
|
+
* Coordinate shared-resource disposal with your tools before ending pools.
|
|
46
|
+
*/
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Install modern-era (`2026-07-28`) stdio serving and return a close handle.
|
|
51
|
+
* `2025-11-25` openings are answered with the unsupported-protocol-version
|
|
52
|
+
* error. Resolution means transport wiring is ready, not that a client has
|
|
53
|
+
* connected or shutdown.
|
|
54
|
+
*/
|
|
55
|
+
export declare function runMcpStdioServer(options: CreateMcpServerOptions): Promise<McpStdioServerHandle>;
|
package/dist/src/adapters/mcp.js
CHANGED
|
@@ -30,12 +30,12 @@ function toMcpObjectSchema(schema) {
|
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Render an output schema for `tools/list` and for `projectCallToolResult`.
|
|
33
|
-
* All-object `allOf` intersections still get `type: "object"` synthesized
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* (array, primitive, union) passes through by reference
|
|
38
|
-
*
|
|
33
|
+
* All-object `allOf` intersections still get `type: "object"` synthesized so
|
|
34
|
+
* existing `Type.Intersect` outputs keep their wire shape (the SDK's legacy-era
|
|
35
|
+
* `{ result: ... }` wrap, reachable only through a directly connected
|
|
36
|
+
* deprecated `createMcpServer`, keys purely on the advertised root `type`).
|
|
37
|
+
* Every other root (array, primitive, union) passes through by reference. The
|
|
38
|
+
* same object must feed both the listing and the projection.
|
|
39
39
|
*/
|
|
40
40
|
function toMcpOutputSchema(schema) {
|
|
41
41
|
const candidate = schema;
|
|
@@ -52,6 +52,24 @@ function toMcpOutputSchema(schema) {
|
|
|
52
52
|
const ERROR_CODE_NON_OBJECT_PARAMETERS = "BRIDGEKIT_MCP_NON_OBJECT_PARAMETERS";
|
|
53
53
|
const ERROR_CODE_REF_PARAMETERS = "BRIDGEKIT_MCP_REF_PARAMETERS";
|
|
54
54
|
const ERROR_CODE_DUPLICATE_TOOL_NAME = "BRIDGEKIT_MCP_DUPLICATE_TOOL_NAME";
|
|
55
|
+
function assertToolIcons(tool) {
|
|
56
|
+
const icons = tool.hostExtras?.mcp?.icons;
|
|
57
|
+
if (icons === undefined)
|
|
58
|
+
return;
|
|
59
|
+
if (!Array.isArray(icons) ||
|
|
60
|
+
icons.some((icon) => typeof icon !== "object" ||
|
|
61
|
+
icon === null ||
|
|
62
|
+
!("src" in icon) ||
|
|
63
|
+
typeof icon.src !== "string" ||
|
|
64
|
+
("mimeType" in icon && icon.mimeType !== undefined && typeof icon.mimeType !== "string") ||
|
|
65
|
+
("sizes" in icon &&
|
|
66
|
+
icon.sizes !== undefined &&
|
|
67
|
+
(!Array.isArray(icon.sizes) || icon.sizes.some((size) => typeof size !== "string"))) ||
|
|
68
|
+
("theme" in icon && icon.theme !== undefined && icon.theme !== "light" && icon.theme !== "dark"))) {
|
|
69
|
+
throwWithCode(`createMcpServer: tool "${tool.name}" has invalid MCP icons; expected an array of icons with string src, ` +
|
|
70
|
+
"optional string mimeType, string[] sizes, and light or dark theme.", "BRIDGEKIT_MCP_INVALID_ICONS");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
55
73
|
function assertObjectShapedParameters(tools) {
|
|
56
74
|
for (const tool of tools) {
|
|
57
75
|
if (!isObjectSchema(tool.parameters)) {
|
|
@@ -98,21 +116,32 @@ function assertUniqueToolNames(tools) {
|
|
|
98
116
|
seen.add(tool.name);
|
|
99
117
|
}
|
|
100
118
|
}
|
|
101
|
-
|
|
102
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Validate and snapshot the tools once, then return a factory that builds an
|
|
121
|
+
* isolated SDK `Server` per call. Pass it to the SDK's `createMcpHandler` or
|
|
122
|
+
* `serveStdio` for custom serving; unlike `createMcpServer`, validation and the
|
|
123
|
+
* tool snapshot happen at this call, not once per server.
|
|
124
|
+
*/
|
|
125
|
+
export function createMcpServerFactory(options) {
|
|
126
|
+
const { name, version, instructions } = options;
|
|
127
|
+
for (const tool of options.tools) {
|
|
103
128
|
assertPortableOutputSchema(tool, "createMcpServer", "BRIDGEKIT_MCP");
|
|
129
|
+
assertToolIcons(tool);
|
|
130
|
+
}
|
|
104
131
|
assertObjectShapedParameters(options.tools);
|
|
105
132
|
assertUniqueToolNames(options.tools);
|
|
106
133
|
// Build the dispatch map and the listing payload at construction so
|
|
107
134
|
// `tools/list` returns a pre-computed array and post-construction mutations
|
|
108
135
|
// to the caller's array cannot leak unvalidated schemas onto the wire.
|
|
109
136
|
const entries = options.tools.map((tool) => ({
|
|
110
|
-
tool,
|
|
137
|
+
tool: { ...tool },
|
|
111
138
|
outputSchema: tool.outputSchema !== undefined ? toMcpOutputSchema(tool.outputSchema) : undefined,
|
|
112
139
|
}));
|
|
113
140
|
const byName = new Map(entries.map((entry) => [entry.tool.name, entry]));
|
|
114
141
|
const mcpTools = entries.map(({ tool, outputSchema }) => {
|
|
115
142
|
const annotations = tool.hostExtras?.mcp?.annotations;
|
|
143
|
+
const icons = tool.hostExtras?.mcp?.icons;
|
|
144
|
+
const meta = tool.hostExtras?.mcp?._meta;
|
|
116
145
|
// MCP advisory hints from `hostExtras.mcp.annotations`. Two gates:
|
|
117
146
|
// 1. `annotations !== undefined` — a tool without hostExtras builds a
|
|
118
147
|
// Tool entry whose own-property keys are byte-identical to 0.8.x.
|
|
@@ -134,69 +163,102 @@ export function createMcpServer(options) {
|
|
|
134
163
|
inputSchema: toMcpObjectSchema(tool.parameters),
|
|
135
164
|
...(outputSchema !== undefined && { outputSchema }),
|
|
136
165
|
...(hasAnnotations ? { annotations: { ...annotations } } : {}),
|
|
166
|
+
...(icons !== undefined
|
|
167
|
+
? { icons: icons.map(({ sizes, ...icon }) => ({ ...icon, ...(sizes !== undefined && { sizes: [...sizes] }) })) }
|
|
168
|
+
: {}),
|
|
169
|
+
...(meta !== undefined ? { _meta: { ...meta } } : {}),
|
|
137
170
|
};
|
|
138
171
|
});
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
172
|
+
return () => {
|
|
173
|
+
const server = new Server({ name, version }, {
|
|
174
|
+
capabilities: { tools: { listChanged: false } },
|
|
175
|
+
...(instructions !== undefined && { instructions }),
|
|
176
|
+
});
|
|
177
|
+
server.setRequestHandler("tools/list", async () => ({ tools: mcpTools }));
|
|
178
|
+
server.setRequestHandler("tools/call", async (request, ctx) => {
|
|
179
|
+
const entry = byName.get(request.params.name);
|
|
180
|
+
if (!entry) {
|
|
181
|
+
return {
|
|
182
|
+
content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
|
|
183
|
+
isError: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// `ctx.progress` is only wired when the client asked for progress by
|
|
187
|
+
// sending `_meta.progressToken`; otherwise the context is byte-identical
|
|
188
|
+
// to the pre-progress shape. `progress` is a monotonic per-call counter
|
|
189
|
+
// (the spec requires it to increase) and `message` is the update text.
|
|
190
|
+
// Sends are skipped once the request is cancelled and awaited before the
|
|
191
|
+
// result goes out, so a client never sees a notification for a token it
|
|
192
|
+
// has already released. A failed send never fails the tool call.
|
|
193
|
+
const progressToken = ctx.mcpReq._meta?.progressToken;
|
|
194
|
+
const pendingNotifications = [];
|
|
195
|
+
let progressCount = 0;
|
|
196
|
+
const progress = progressToken === undefined
|
|
197
|
+
? undefined
|
|
198
|
+
: (update) => {
|
|
199
|
+
if (ctx.mcpReq.signal.aborted)
|
|
200
|
+
return;
|
|
201
|
+
progressCount += 1;
|
|
202
|
+
pendingNotifications.push(ctx.mcpReq
|
|
203
|
+
.notify({
|
|
204
|
+
method: "notifications/progress",
|
|
205
|
+
params: { progressToken, progress: progressCount, message: update.text },
|
|
206
|
+
})
|
|
207
|
+
.catch(() => undefined));
|
|
208
|
+
};
|
|
209
|
+
try {
|
|
210
|
+
const result = await executePortableTool(entry.tool, request.params.arguments ?? {}, {
|
|
211
|
+
host: "mcp",
|
|
212
|
+
signal: ctx.mcpReq.signal,
|
|
213
|
+
...(progress !== undefined && { progress }),
|
|
214
|
+
});
|
|
215
|
+
await Promise.all(pendingNotifications);
|
|
216
|
+
return server.projectCallToolResult(toMcpResult(result), entry.outputSchema);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
await Promise.all(pendingNotifications);
|
|
220
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
221
|
+
return {
|
|
222
|
+
content: [{ type: "text", text: message }],
|
|
223
|
+
isError: true,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
return server;
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Build a single low-level SDK `Server`.
|
|
232
|
+
*
|
|
233
|
+
* @deprecated A directly connected `Server` (`server.connect(transport)`) can
|
|
234
|
+
* only speak the legacy era, which BridgeKit's serving entrypoints no longer
|
|
235
|
+
* support. Use `runMcpStdioServer` or `createMcpHttpHandler`, or for custom
|
|
236
|
+
* serving pass `createMcpServerFactory(options)` to the SDK's
|
|
237
|
+
* `createMcpHandler(factory, { legacy: "reject" })`. Do not wrap this function
|
|
238
|
+
* in a factory: it revalidates and re-snapshots the tools on every call.
|
|
239
|
+
*/
|
|
240
|
+
export function createMcpServer(options) {
|
|
241
|
+
return createMcpServerFactory(options)();
|
|
194
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Install modern-era (`2026-07-28`) stdio serving and return a close handle.
|
|
245
|
+
* `2025-11-25` openings are answered with the unsupported-protocol-version
|
|
246
|
+
* error. Resolution means transport wiring is ready, not that a client has
|
|
247
|
+
* connected or shutdown.
|
|
248
|
+
*/
|
|
195
249
|
export async function runMcpStdioServer(options) {
|
|
196
|
-
// Validate eagerly
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
250
|
+
// Validate eagerly; the SDK asks the factory for a fresh server per
|
|
251
|
+
// discovery probe/connection, so none share handler state.
|
|
252
|
+
const factory = createMcpServerFactory(options);
|
|
253
|
+
const handle = serveStdio(factory, {
|
|
254
|
+
legacy: "reject",
|
|
200
255
|
onerror: (error) => process.stderr.write(`[bridgekit-mcp] ${error.message}\n`),
|
|
201
256
|
});
|
|
257
|
+
let closing;
|
|
258
|
+
return {
|
|
259
|
+
close: () => {
|
|
260
|
+
closing ??= handle.close();
|
|
261
|
+
return closing;
|
|
262
|
+
},
|
|
263
|
+
};
|
|
202
264
|
}
|
|
@@ -6,9 +6,9 @@ export interface PortableToolResult<TStructured = Record<string, unknown>> {
|
|
|
6
6
|
* Structured data for hosts that support it. Preferred by both pi and MCP
|
|
7
7
|
* adapters. Any JSON value is accepted (object, array, primitive, `null`);
|
|
8
8
|
* the default type parameter keeps the common object case ergonomic.
|
|
9
|
-
* Non-object values reach MCP `structuredContent` directly
|
|
10
|
-
* era
|
|
11
|
-
*
|
|
9
|
+
* Non-object values reach MCP `structuredContent` directly (BridgeKit
|
|
10
|
+
* serves only the modern era) and are wrapped as `{ result: value }` in pi's
|
|
11
|
+
* renderer-facing `details`.
|
|
12
12
|
*/
|
|
13
13
|
structuredContent?: TStructured;
|
|
14
14
|
/**
|
|
@@ -118,6 +118,21 @@ export interface PiHostExtras {
|
|
|
118
118
|
* @see PortableToolHostExtras
|
|
119
119
|
*/
|
|
120
120
|
export interface McpHostExtras {
|
|
121
|
+
/**
|
|
122
|
+
* Tool icons forwarded to tools/list. The array, icon objects, and sizes
|
|
123
|
+
* arrays are copied at server construction. BridgeKit does not fetch icons.
|
|
124
|
+
*/
|
|
125
|
+
icons?: readonly {
|
|
126
|
+
src: string;
|
|
127
|
+
mimeType?: string;
|
|
128
|
+
sizes?: readonly string[];
|
|
129
|
+
theme?: "light" | "dark";
|
|
130
|
+
}[];
|
|
131
|
+
/**
|
|
132
|
+
* Opaque MCP tool metadata. Shallow-copied at server construction;
|
|
133
|
+
* nested values remain caller-owned and must be treated as immutable.
|
|
134
|
+
*/
|
|
135
|
+
_meta?: Record<string, unknown>;
|
|
121
136
|
/**
|
|
122
137
|
* MCP tool annotations attached to `tools/list` entries. Hints clients may
|
|
123
138
|
* surface to users; do not affect validation or execution.
|
|
@@ -7,7 +7,7 @@ export declare function schemaTypeLabel(schema: unknown): string;
|
|
|
7
7
|
export declare function throwWithCode(message: string, code: string, ErrorType?: ErrorConstructor): never;
|
|
8
8
|
/**
|
|
9
9
|
* Output schemas may use any JSON Schema root (object, array, primitive,
|
|
10
|
-
* union);
|
|
10
|
+
* union); modern MCP clients receive non-object roots as-is. Only a
|
|
11
11
|
* top-level `$ref` is rejected: `tools/list` ships schemas by value and clients
|
|
12
12
|
* do not resolve references.
|
|
13
13
|
*/
|
|
@@ -40,7 +40,7 @@ export function throwWithCode(message, code, ErrorType = Error) {
|
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
42
|
* Output schemas may use any JSON Schema root (object, array, primitive,
|
|
43
|
-
* union);
|
|
43
|
+
* union); modern MCP clients receive non-object roots as-is. Only a
|
|
44
44
|
* top-level `$ref` is rejected: `tools/list` ships schemas by value and clients
|
|
45
45
|
* do not resolve references.
|
|
46
46
|
*/
|
package/dist/src/mcp.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { type CreateMcpServerOptions, createMcpServer, runMcpStdioServer, } from "./adapters/mcp.js";
|
|
1
|
+
export { type CreateMcpServerOptions, createMcpServer, createMcpServerFactory, type McpStdioServerHandle, runMcpStdioServer, } from "./adapters/mcp.js";
|
|
2
|
+
export { type CreateMcpHttpHandlerOptions, createMcpHttpHandler, type McpHttpHandler, } from "./adapters/mcp-http.js";
|
package/dist/src/mcp.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { createMcpServer, runMcpStdioServer, } from "./adapters/mcp.js";
|
|
1
|
+
export { createMcpServer, createMcpServerFactory, runMcpStdioServer, } from "./adapters/mcp.js";
|
|
2
|
+
export { createMcpHttpHandler, } from "./adapters/mcp-http.js";
|
package/examples/README.md
CHANGED
|
@@ -272,11 +272,13 @@ MCP behavior:
|
|
|
272
272
|
- Unexpected thrown errors become MCP tool errors with text content.
|
|
273
273
|
- Optional `outputSchema` (any root except top-level `$ref`) is listed for successful
|
|
274
274
|
structured results; matching `structuredContent` is required and validated. Error
|
|
275
|
-
results are exempt. Non-object values are wrapped as `{ result }`
|
|
275
|
+
results are exempt. Non-object values reach MCP clients as-is and are wrapped as `{ result }` on pi `details`.
|
|
276
276
|
- `ctx.progress?.(...)` becomes `notifications/progress` when the client sent a
|
|
277
277
|
`progressToken`; otherwise `ctx.progress` is `undefined`.
|
|
278
|
-
- The stdio runner serves
|
|
279
|
-
It
|
|
278
|
+
- The stdio runner serves modern `2026-07-28` clients through SDK v2; legacy-era clients (`2025-11-25` and earlier) are rejected.
|
|
279
|
+
It returns `Promise<McpStdioServerHandle>` after wiring is installed; stdin EOF or
|
|
280
|
+
`await handle.close()` shuts down the connection and aborts in-flight requests.
|
|
281
|
+
`close()` does not await asynchronous tool cleanup.
|
|
280
282
|
- The module stays import-passive and testable: tests can import `createMcpServerOptions()` without starting stdio.
|
|
281
283
|
|
|
282
284
|
---
|
package/llms.txt
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
isValidationFailure,
|
|
20
20
|
} from "@feniix/bridgekit";
|
|
21
21
|
import { registerPiTools } from "@feniix/bridgekit/pi";
|
|
22
|
-
import {
|
|
22
|
+
import { createMcpHttpHandler, runMcpStdioServer } from "@feniix/bridgekit/mcp";
|
|
23
23
|
import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper"; // optional: built-in bin wrapper for mixed source-loaded pi + compiled MCP packages
|
|
24
24
|
```
|
|
25
25
|
|
|
@@ -63,7 +63,7 @@ export function createTools() {
|
|
|
63
63
|
TypeBox schema except a top-level `$ref` (objects, intersections, arrays,
|
|
64
64
|
primitives, unions). Missing/invalid successful `structuredContent` throws
|
|
65
65
|
`TypeError`; argument/domain error results are exempt. Non-object values are
|
|
66
|
-
wrapped as `{ result }`
|
|
66
|
+
passed as-is to MCP clients and wrapped as `{ result }` on pi `details`.
|
|
67
67
|
Legacy `details` alone does not satisfy a declared output schema.
|
|
68
68
|
`definePortableTool` infers/checks schema-linked success data at compile time;
|
|
69
69
|
use literal `isError: true` for exempt domain failures. A third
|
|
@@ -100,12 +100,42 @@ export function createTools() {
|
|
|
100
100
|
Their runtime predicates are shape-based, so Pi structured failures also
|
|
101
101
|
match; that does not make a raw Pi result a valid portable typed value.
|
|
102
102
|
- MCP uses SDK v2's low-level `Server` and method-string handlers. Cancellation
|
|
103
|
-
reads `ctx.mcpReq.signal`. `runMcpStdioServer` serves
|
|
104
|
-
`2026-07-28` stdio
|
|
105
|
-
`
|
|
103
|
+
reads `ctx.mcpReq.signal`. `runMcpStdioServer` serves modern
|
|
104
|
+
`2026-07-28` stdio only (legacy-era openings are rejected with `-32022`);
|
|
105
|
+
`createMcpServer` is deprecated as a direct `Server.connect` target (legacy-only). The runner's
|
|
106
|
+
`Promise<McpStdioServerHandle>` resolves at startup, not EOF; call the
|
|
107
|
+
handle's idempotent `close()` to tear down explicitly. Migration details:
|
|
106
108
|
`docs/mcp-v2-migration.md`.
|
|
109
|
+
- `createMcpHttpHandler(options, httpOptions?)` returns the SDK's Web-standard
|
|
110
|
+
HTTP handler (`fetch`, `close`, `notify`, `bus`), modern era only.
|
|
111
|
+
It starts no listener. Applications must enforce authentication, authorization,
|
|
112
|
+
Host/Origin validation, TLS, and CORS before calling `fetch`.
|
|
107
113
|
- `registerPiTools(pi, tools, { errorHandling: "throw" })` opts into the pre-0.7 behavior where `isError` results throw `PortableToolExecutionError`. Selecting this value emits a `DeprecationWarning` (code `BRIDGEKIT_PI_THROW_DEPRECATED`) once per process; only the `"throw"` value is deprecated, not the option itself. Scheduled for removal in 1.0; do not adopt it for new code.
|
|
108
|
-
- `PortableTool.hostExtras` (0.9+) carries optional per-host metadata in namespaced sub-objects (`pi`, `mcp`, plus module-augmentable custom hosts). Each field is optional and zero-cost when absent — a tool without `hostExtras` produces byte-identical pi registration and MCP `tools/list` payloads to 0.8.x. `hostExtras.pi.pendingMessage` fires `onUpdate({ content, details: { status: "pending" } })` exactly once before TypeBox validation; `hostExtras.pi.promptSnippet` / `promptGuidelines` pass through to pi's `registerTool`; `hostExtras.pi.renderCall` / `renderResult` are opaque TUI callbacks typed by exported `PiToolCallRenderer` / `PiToolResultRenderer` aliases and forwarded by identity; `hostExtras.mcp.annotations` (`title` / `readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint`) attaches MCP advisory hints. Tool definitions stay host-neutral — host-specific metadata lives in its namespace, not on the tool's top-level fields.
|
|
114
|
+
- `PortableTool.hostExtras` (0.9+) carries optional per-host metadata in namespaced sub-objects (`pi`, `mcp`, plus module-augmentable custom hosts). Each field is optional and zero-cost when absent — a tool without `hostExtras` produces byte-identical pi registration and MCP `tools/list` payloads to 0.8.x. `hostExtras.pi.pendingMessage` fires `onUpdate({ content, details: { status: "pending" } })` exactly once before TypeBox validation; `hostExtras.pi.promptSnippet` / `promptGuidelines` pass through to pi's `registerTool`; `hostExtras.pi.renderCall` / `renderResult` are opaque TUI callbacks typed by exported `PiToolCallRenderer` / `PiToolResultRenderer` aliases and forwarded by identity; `hostExtras.mcp.annotations` (`title` / `readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint`) attaches MCP advisory hints. `hostExtras.mcp.icons` carries `src`, optional `mimeType`, `sizes`, and `theme` (`light`/`dark`); malformed icons fail at construction with `BRIDGEKIT_MCP_INVALID_ICONS`. `hostExtras.mcp._meta` carries opaque MCP metadata. Both serving factories snapshot these fields once at construction: icons and sizes are copied, `_meta` is shallow-copied (nested values remain shared). Empty icons/`_meta` are forwarded, while empty annotations are omitted. Tool definitions stay host-neutral — host-specific metadata lives in its namespace, not on the tool's top-level fields.
|
|
115
|
+
|
|
116
|
+
### Migrating from dual-era MCP serving
|
|
117
|
+
|
|
118
|
+
BridgeKit serves only the modern MCP `2026-07-28` era. Legacy-era openings
|
|
119
|
+
(`2025-11-25` and earlier) are rejected with `-32022`
|
|
120
|
+
(`data.supported: ["2026-07-28"]`); stdio stays open for a modern opening.
|
|
121
|
+
|
|
122
|
+
1. Pin clients: the SDK v2 client defaults to `versionNegotiation.mode: "legacy"`
|
|
123
|
+
and is rejected. Use `{ mode: { pin: "2026-07-28" } }` or `"auto"`.
|
|
124
|
+
2. SDK v1 clients and v2 clients in `legacy` mode can no longer connect; there is
|
|
125
|
+
no compatibility switch.
|
|
126
|
+
3. Replace `createMcpServer(options)` + `server.connect(transport)` with
|
|
127
|
+
`runMcpStdioServer(options)` or `createMcpHttpHandler(options, httpOptions?)`.
|
|
128
|
+
`createMcpServer` is deprecated for direct connection; for custom serving pass
|
|
129
|
+
`createMcpServerFactory(options)` to the SDK's `createMcpHandler(factory,
|
|
130
|
+
{ legacy: "reject" })`. Do not wrap `createMcpServer` in a factory: it
|
|
131
|
+
revalidates and re-snapshots tools on every call.
|
|
132
|
+
4. Remove `legacy` from HTTP options (removed from the type, ignored at runtime).
|
|
133
|
+
5. Non-object structured output reaches clients as-is, not `{ result }`-wrapped.
|
|
134
|
+
6. Each rejected request logs one `[bridgekit-mcp]` stderr line by default. The
|
|
135
|
+
HTTP handler's `onerror` option overrides it; `runMcpStdioServer` has no
|
|
136
|
+
reporter option and always writes to stderr.
|
|
137
|
+
|
|
138
|
+
Details: `docs/mcp-v2-migration.md` ("Dropping legacy-era serving").
|
|
109
139
|
|
|
110
140
|
### Result-guard usage
|
|
111
141
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feniix/bridgekit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "BridgeKit defines TypeBox-backed tools once and adapts them to pi, MCP, and other hosts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@biomejs/biome": "2.5.15",
|
|
77
77
|
"@modelcontextprotocol/client": "^2.3.1",
|
|
78
|
-
"@modelcontextprotocol/
|
|
78
|
+
"@modelcontextprotocol/node": "2.1.1",
|
|
79
79
|
"@total-typescript/shoehorn": "^0.1.2",
|
|
80
80
|
"@types/node": "^22.20.5",
|
|
81
81
|
"typescript": "^6.0.3"
|