@feniix/bridgekit 0.13.0 → 0.15.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 CHANGED
@@ -4,6 +4,39 @@ All notable changes to `@feniix/bridgekit` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.14.0] - 2026-05-28
8
+
9
+ ### Added
10
+
11
+ - `PortableTool` now carries an inferred success-result generic in addition to
12
+ its TypeBox parameter generic. `definePortableTool` and `executePortableTool`
13
+ preserve handler `structuredContent` types for direct/programmatic callers,
14
+ while validation failures remain `isError: true` results that narrow with
15
+ `isValidationFailure`.
16
+ - `runBinWrapper` now validates its string inputs before spawning or importing:
17
+ `mcpEntry` rejects absolute paths, `..` path segments, and NUL bytes;
18
+ `buildScript` rejects option-shaped or shell-shaped script names.
19
+ - CI and release checks now run `npm audit --omit=dev --audit-level=high`, and
20
+ Dependabot is configured for npm and GitHub Actions updates.
21
+
22
+ ### Changed
23
+
24
+ - TypeScript is now strict-plus: `noUncheckedIndexedAccess` and
25
+ `exactOptionalPropertyTypes` are enabled in addition to `strict`. The packed
26
+ consumer smoke test compiles installed declarations with the same flags.
27
+ - The Release workflow is manual-only (`workflow_dispatch`) with a `main`
28
+ branch guard; publishing no longer happens automatically after CI on `main`.
29
+ Third-party GitHub Actions are pinned by full commit SHA.
30
+ - Documentation now reflects the current pi default error-return behavior, the
31
+ install-time MCP SDK dependency footprint, pre-1.0 stability/support
32
+ expectations, and MCP stdio-safe `buildStdio` examples.
33
+
34
+ ### Fixed
35
+
36
+ - `isValidationFailure` now verifies every `validationErrors[]` entry has a
37
+ string `field` and `message` before narrowing to `PortableValidationFailure`.
38
+ - `package-lock.json` root metadata now matches `package.json#version`.
39
+
7
40
  ## [0.13.0] - 2026-05-27
8
41
 
9
42
  ### Added
package/README.md CHANGED
@@ -61,12 +61,18 @@ npm install @feniix/bridgekit typebox
61
61
 
62
62
  This package is ESM-only and supports Node.js 22.19.0 or newer. Published modules are import-passive and marked as side-effect free; tools are registered or servers are started only when the exported adapter functions are called.
63
63
 
64
+ ## Stability and support
65
+
66
+ BridgeKit is pre-1.0. Patch releases are intended to be compatible within the current minor, but `0.x.0` minor bumps may contain source-level breaking changes while the API is settling. Production consumers that need maximum stability should pin an exact version or a patch-only range and read `CHANGELOG.md` before bumping.
67
+
68
+ Issue triage is best-effort, with priority on packaging regressions, install failures, and SDK-compatibility breaks.
69
+
64
70
  ## Why
65
71
 
66
72
  - Define a tool once; ship it to pi and MCP without per-host forks.
67
73
  - Host-neutral tool files: no pi or MCP SDK imports in the tool definition itself.
68
74
  - TypeBox schemas pass through to MCP `inputSchema` directly — no JSON Schema conversion step.
69
- - Import-passive, `sideEffects: false`, four-entrypoint split (`.`, `./pi`, `./mcp`, `./bin-wrapper`) so pi-only consumers do not pull the MCP SDK and vice versa.
75
+ - Import-passive, `sideEffects: false`, four-entrypoint split (`.`, `./pi`, `./mcp`, `./bin-wrapper`) so ESM/bundler consumers that use only core or pi entrypoints do not import or bundle the MCP adapter code. The MCP SDK is still a normal package dependency today, so all npm installs include its transitive dependencies.
70
76
  - Conformance-tested public surface: a packed-install smoke test enforces the runtime export set, deep-import rejection, and type-level strictness against the installed declarations.
71
77
 
72
78
  ## API reference
@@ -77,6 +83,8 @@ import {
77
83
  executePortableTool,
78
84
  isDomainFailure,
79
85
  isValidationFailure,
86
+ type PiToolCallRenderer,
87
+ type PiToolResultRenderer,
80
88
  type PortableTool,
81
89
  type PortableToolBuiltInHost,
82
90
  type PortableToolContext,
@@ -95,6 +103,8 @@ import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
95
103
 
96
104
  Do not deep-import from `dist/` or `src/` in consuming packages.
97
105
 
106
+ `definePortableTool` infers both the TypeBox-backed argument type and the handler's success result shape. `executePortableTool` preserves that inferred success result for direct/programmatic callers, while validation failures are returned as `isError: true` results that can be narrowed with `isValidationFailure`.
107
+
98
108
  ### pi adapter
99
109
 
100
110
  ```ts
@@ -106,7 +116,7 @@ export default function extension(pi: Parameters<typeof registerPiTools>[0]) {
106
116
  }
107
117
  ```
108
118
 
109
- By default (`errorHandling: "return"`, as of 0.7) the pi adapter mirrors the MCP adapter: portable validation failures and portable `isError: true` results surface as `{ content, details, isError: true }` so consumers can branch on `result.isError` and narrow with `isValidationFailure` / `isDomainFailure`. Unexpected handler exceptions are caught and surfaced as `{ content: [{type:"text", text: message}], details: {}, isError: true }`, matching MCP. Success-path results include `isError: false` explicitly so consumers can use the same strict-equality checks across both adapters. `details` is populated from `structuredContent` first, then from `details`, then `{}`. Progress updates from `ctx.progress?.(...)` map to pi tool updates.
119
+ By default (`errorHandling: "return"`, as of 0.7) the pi adapter mirrors MCP's error-as-data posture: portable validation failures and portable `isError: true` results surface as `{ content, details, isError: true }` so consumers can branch on `result.isError`. The pi wire object exposes machine-readable data on `details` (sourced from `structuredContent` first, then `details`, then `{}`); use the result guards on `PortableToolResult` values at the portable seam, not directly on the pi wire object. Unexpected handler exceptions are caught and surfaced as `{ content: [{type:"text", text: message}], details: {}, isError: true }`, matching MCP. Success-path results include `isError: false` explicitly so consumers can use the same strict-equality checks across both adapters. Progress updates from `ctx.progress?.(...)` map to pi tool updates.
110
120
 
111
121
  The pre-0.7 behavior — throw `PortableToolExecutionError` on `isError` — is still available for one deprecation cycle:
112
122
 
@@ -130,12 +140,12 @@ try {
130
140
  // After (0.7+)
131
141
  const result = await piTool.execute(...);
132
142
  if (result.isError) {
133
- if (isValidationFailure(result)) {
143
+ if (result.details.kind === "validation" && Array.isArray(result.details.validationErrors)) {
134
144
  // validationErrors is Array<{ field: string; message: string }>.
135
- for (const { field, message } of result.structuredContent.validationErrors) {
145
+ for (const { field, message } of result.details.validationErrors) {
136
146
  // ...
137
147
  }
138
- } else if (isDomainFailure(result)) { /* handler-level error */ }
148
+ } else { /* handler-level error; details is whatever the handler returned */ }
139
149
  }
140
150
  ```
141
151
 
@@ -150,20 +160,28 @@ For **discriminated unions** (`Type.Union([Type.Object({tag: Literal("a"), …})
150
160
  For nested discriminated unions, `field` is the leaf segment of the JSON pointer (`"name"` for `/event/name`), not the full path. Consumers needing full path context for ambiguous prop names should track the active union branch out-of-band.
151
161
 
152
162
  The two modes expose the failure discriminator on different fields. In the
153
- new default `"return"` mode, prefer the result guards over inspecting a
154
- `kind` field directly — handler-emitted failures do not carry a synthesized
155
- `kind`. In the deprecated `"throw"` mode, the discriminator lives on
156
- `(err as PortableToolExecutionError).details.kind` (`"validation"` or
157
- `"domain"`). The guards operate on a `PortableToolResult`; they are not
158
- designed to be called on the pi adapter's wire object.
163
+ new default `"return"` mode, validation failures surface on the pi wire
164
+ object as `result.details.kind === "validation"`; handler-emitted failures
165
+ do not carry a synthesized `kind`. In the deprecated `"throw"` mode, the
166
+ discriminator lives on `(err as PortableToolExecutionError).details.kind`
167
+ (`"validation"` or `"domain"`). The guards operate on a `PortableToolResult`;
168
+ they are not designed to be called on the pi adapter's wire object.
159
169
 
160
170
  #### Per-host metadata via `hostExtras`
161
171
 
162
- `PortableTool.hostExtras` is an optional namespace for host-specific fields that should travel with the tool definition rather than a parallel sidecar map. The pi adapter reads `hostExtras.pi`; the MCP adapter reads `hostExtras.mcp`; each adapter ignores keys it does not recognise. Tools that omit `hostExtras` see no behavior change.
172
+ `PortableTool.hostExtras` is an optional namespace for host-specific fields that should travel with the tool definition rather than a parallel sidecar map. The pi adapter reads `hostExtras.pi`; the MCP adapter reads `hostExtras.mcp`; each adapter ignores keys it does not recognise. Tools that omit `hostExtras` see no behavior change. Pi TUI renderers use the exported `PiToolCallRenderer` / `PiToolResultRenderer` aliases; they are opaque pass-through callbacks, so bridgekit does not import pi-tui types or validate their return values.
173
+
174
+ Custom host adapters can reuse the core execution helpers, but the built-in `ctx.host` union is intentionally closed (`"pi" | "mcp" | "test"`). Custom adapters should cast at their boundary and carry custom dispatch state on adapter-owned fields; use module augmentation of `PortableToolHostExtras` for type-safe host-specific metadata.
163
175
 
164
176
  ```ts
165
177
  import { Type } from "typebox";
166
- import { definePortableTool } from "@feniix/bridgekit";
178
+ import { definePortableTool, type PiToolCallRenderer, type PiToolResultRenderer } from "@feniix/bridgekit";
179
+
180
+ const renderSummaryCall: PiToolCallRenderer = (args) => ({ type: "text", text: `summarise ${args.text}` });
181
+ const renderSummaryResult: PiToolResultRenderer = (result, options) => ({
182
+ type: "text",
183
+ text: options.expanded ? result.content?.[0]?.text : "summary ready",
184
+ });
167
185
 
168
186
  export const generateSummaryTool = definePortableTool({
169
187
  name: "generate_summary",
@@ -180,6 +198,9 @@ export const generateSummaryTool = definePortableTool({
180
198
  pendingMessage: "Summarising...",
181
199
  promptSnippet: "Use this tool when the user asks for a short summary.",
182
200
  promptGuidelines: ["Prefer < 80 chars.", "Strip markdown."],
201
+ // Optional pi TUI renderers, forwarded by identity to pi.registerTool.
202
+ renderCall: renderSummaryCall,
203
+ renderResult: renderSummaryResult,
183
204
  },
184
205
  mcp: {
185
206
  annotations: { readOnlyHint: true },
@@ -188,6 +209,8 @@ export const generateSummaryTool = definePortableTool({
188
209
  });
189
210
  ```
190
211
 
212
+ `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`.
213
+
191
214
  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"`.
192
215
 
193
216
  ### MCP adapter
@@ -231,7 +254,7 @@ Tool `parameters` must resolve to a JSON-Schema object at the top level. `Type.O
231
254
 
232
255
  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.
233
256
 
234
- The two adapters now read in parallel: invalid args and portable `isError` results return `{ isError: true }` from both hosts by default, and the same result-guard helpers (`isValidationFailure`, `isDomainFailure`) narrow them on either side.
257
+ The two adapters now read in parallel: invalid args and portable `isError` results return `{ isError: true }` from both hosts by default. The result-guard helpers (`isValidationFailure`, `isDomainFailure`) narrow `PortableToolResult` values at the portable seam; adapter wire values expose the same data through host-specific fields (`structuredContent` for MCP, `details` for pi).
235
258
 
236
259
  ### bin-wrapper (since 0.11.0)
237
260
 
@@ -245,21 +268,23 @@ await runBinWrapper({
245
268
  metaUrl: import.meta.url,
246
269
  mcpEntry: "dist/extensions/mcp-server.js",
247
270
  buildScript: "build:mcp",
271
+ // MCP stdio bins: keep build stdout off the JSON-RPC channel.
272
+ buildStdio: ["ignore", "inherit", "inherit"],
248
273
  });
249
274
  ```
250
275
 
251
276
  Options:
252
277
 
253
278
  - `metaUrl` (required): `import.meta.url` of the bin script. Used to locate the package root (the bin's parent directory).
254
- - `mcpEntry` (required): path to the compiled MCP entry, relative to the package root (e.g. `"dist/extensions/mcp-server.js"`).
255
- - `buildScript` (required): npm script to invoke when the entry is missing (e.g. `"build:mcp"`).
279
+ - `mcpEntry` (required): path to the compiled MCP entry, relative to the package root (e.g. `"dist/extensions/mcp-server.js"`). Absolute paths, `..` path segments, and NUL bytes are rejected.
280
+ - `buildScript` (required): npm script to invoke when the entry is missing (e.g. `"build:mcp"`). Names must be literal script identifiers containing only letters, numbers, `:`, `_`, `@`, `.`, or `-`, and must not start with `-`.
256
281
  - `buildTimeoutMs` (optional, default `60_000`): timeout for the build subprocess. Distinct "Build timed out…" diagnostic fires when exceeded.
257
282
  - `logPrefix` (optional, default `"bridgekit-bin"`): prefix on the "Failed to build…" diagnostic.
258
283
  - `buildStdio` (optional, default `"inherit"`): `stdio` mode passed to `spawnSync` when the build script runs. **MCP stdio server bins should pass `["ignore", "inherit", "inherit"]`** so the build subprocess's stdout cannot contaminate the parent's JSON-RPC framing channel (`process.stdout`). stderr stays inherited so build diagnostics remain visible.
259
284
 
260
285
  The MCP entry module must export `async function runServer(): Promise<void>`. Consumers with a different export name can alias on export.
261
286
 
262
- **Security: `mcpEntry` and `buildScript` must be literal strings in the caller's source.** The helper joins `mcpEntry` onto the resolved package root and dynamically `import()`s it, and it passes `buildScript` to `spawnSync` (with `shell: true` on Windows where `&`, `|`, and `^` are shell metacharacters). Sourcing either from CLI args or environment variables exposes arbitrary-file import and Windows command injection.
287
+ **Security: `mcpEntry` and `buildScript` must be literal strings in the caller's source.** The helper joins `mcpEntry` onto the resolved package root and dynamically `import()`s it, and it passes `buildScript` to `spawnSync` (with `shell: true` on Windows where `&`, `|`, and `^` are shell metacharacters). BridgeKit rejects absolute/traversing entry paths and shell-shaped script names, but sourcing either option from CLI args or environment variables is still outside the supported threat model.
263
288
 
264
289
  Behavior on missing entry:
265
290
 
@@ -297,8 +322,8 @@ Package and release checklist:
297
322
  - If an npm-launched bin depends on generated output, use `runBinWrapper` from `@feniix/bridgekit/bin-wrapper` (since 0.11.0) — it resolves the package-local generated entry, runs the package-local build when output is missing in workspace/local execution, preserves build failures, and distinguishes timeout from build error in its diagnostic. The bin script becomes a three-line invocation; no hand-rolled wrapper needed.
298
323
  - If a package keeps a source-loaded host entrypoint (for example a pi extension source file), use a package-local MCP build behind that wrapper and narrow the build to the MCP entrypoint plus shared host-neutral modules.
299
324
  - Declare a compatible Node engine (`>=22.19.0`) in downstream packages that expose BridgeKit-powered MCP bins.
300
- - Run `npm run check`, `npm run test`, `npm run pack:dry-run`, and `npm run package-smoke` before publishing.
301
- - Treat `docs/releasing.md` as the future release handoff; this repository is not configured for automated publish yet.
325
+ - Run `npm run check`, `npm test`, `npm run pack:dry-run`, `npm run package-smoke`, and `npm audit --omit=dev --audit-level=high` before publishing.
326
+ - Treat `docs/releasing.md` as the release handoff; publishing is manual via the Release workflow and npm trusted publishing.
302
327
 
303
328
  See `examples/README.md` for complete copyable examples.
304
329
 
@@ -182,7 +182,7 @@ export function createMcpServer(options) {
182
182
  });
183
183
  const server = new Server({ name: options.name, version: options.version }, {
184
184
  capabilities: { tools: { listChanged: false } },
185
- instructions: options.instructions,
185
+ ...(options.instructions !== undefined && { instructions: options.instructions }),
186
186
  });
187
187
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: mcpTools }));
188
188
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
@@ -1,5 +1,5 @@
1
1
  import type { TSchema } from "typebox";
2
- import type { PortableTool, PortableToolErrorDetails, PortableToolResult } from "../core/define-tool.js";
2
+ import type { PiToolCallRenderer, PiToolResultRenderer, PortableTool, PortableToolErrorDetails, PortableToolResult } from "../core/define-tool.js";
3
3
  type PiContent = {
4
4
  type: "text";
5
5
  text: string;
@@ -21,6 +21,8 @@ type PiToolDefinition = {
21
21
  execute(toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: (update: PiToolUpdate) => void, ctx?: unknown): Promise<PiToolResult>;
22
22
  promptSnippet?: string;
23
23
  promptGuidelines?: string[];
24
+ renderCall?: PiToolCallRenderer;
25
+ renderResult?: PiToolResultRenderer;
24
26
  };
25
27
  export type PiToolRegistration = {
26
28
  registerTool(tool: PiToolDefinition): unknown;
@@ -65,6 +65,9 @@ export function registerPiTools(pi, tools, options = {}) {
65
65
  // pi-coding-agent's registerTool input is `string[]` (mutable). The boundary
66
66
  // copy satisfies both sides without weakening either declared type.
67
67
  ...(piExtras?.promptGuidelines !== undefined && { promptGuidelines: [...piExtras.promptGuidelines] }),
68
+ // TUI renderers forwarded verbatim from hostExtras.pi.
69
+ ...(piExtras?.renderCall !== undefined && { renderCall: piExtras.renderCall }),
70
+ ...(piExtras?.renderResult !== undefined && { renderResult: piExtras.renderResult }),
68
71
  async execute(_toolCallId, params, signal, onUpdate, _ctx) {
69
72
  let result;
70
73
  try {
@@ -87,7 +90,7 @@ export function registerPiTools(pi, tools, options = {}) {
87
90
  }
88
91
  result = await executePortableTool(tool, params, {
89
92
  host: "pi",
90
- signal,
93
+ ...(signal !== undefined && { signal }),
91
94
  progress(update) {
92
95
  onUpdate?.({
93
96
  content: [{ type: "text", text: update.text }],
@@ -5,15 +5,41 @@
5
5
  // under inv-deep-imports-fail).
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { existsSync } from "node:fs";
8
- import { dirname, join, resolve } from "node:path";
8
+ import { dirname, isAbsolute, join, resolve } from "node:path";
9
9
  import { fileURLToPath, pathToFileURL } from "node:url";
10
10
  const DEFAULT_BUILD_TIMEOUT_MS = 60_000;
11
11
  const DEFAULT_LOG_PREFIX = "bridgekit-bin";
12
+ const SAFE_BUILD_SCRIPT_NAME = /^[A-Za-z0-9:_@.-]+$/;
13
+ function validateMcpEntry(mcpEntry) {
14
+ if (mcpEntry.length === 0) {
15
+ throw new Error("[bridgekit/bin-wrapper] mcpEntry must be a non-empty package-relative path.");
16
+ }
17
+ if (mcpEntry.includes("\0")) {
18
+ throw new Error("[bridgekit/bin-wrapper] mcpEntry must not contain NUL bytes.");
19
+ }
20
+ if (isAbsolute(mcpEntry) || /^[A-Za-z]:[\\/]/.test(mcpEntry)) {
21
+ throw new Error("[bridgekit/bin-wrapper] mcpEntry must be relative to the package root, not absolute.");
22
+ }
23
+ if (mcpEntry.split(/[\\/]+/).some((segment) => segment === "..")) {
24
+ throw new Error("[bridgekit/bin-wrapper] mcpEntry must not contain '..' path segments.");
25
+ }
26
+ }
27
+ function validateBuildScript(buildScript) {
28
+ if (buildScript.length === 0) {
29
+ throw new Error("[bridgekit/bin-wrapper] buildScript must be a non-empty npm script name.");
30
+ }
31
+ if (buildScript.startsWith("-") || !SAFE_BUILD_SCRIPT_NAME.test(buildScript)) {
32
+ throw new Error("[bridgekit/bin-wrapper] buildScript must be a literal npm script name containing only letters, " +
33
+ "numbers, ':', '_', '@', '.', or '-' (and must not start with '-').");
34
+ }
35
+ }
12
36
  export const defaultBinWrapperDeps = {
13
37
  spawnSync,
14
38
  exit: process.exit,
15
39
  };
16
40
  export async function runBinWrapperWithDeps(options, deps) {
41
+ validateMcpEntry(options.mcpEntry);
42
+ validateBuildScript(options.buildScript);
17
43
  const packageRoot = resolve(dirname(fileURLToPath(options.metaUrl)), "..");
18
44
  const entryPath = join(packageRoot, options.mcpEntry);
19
45
  if (!existsSync(entryPath)) {
@@ -42,6 +42,19 @@ export interface PortableToolContext {
42
42
  signal?: AbortSignal;
43
43
  progress?: (update: PortableToolResult) => void;
44
44
  }
45
+ /**
46
+ * Optional TUI renderer for the tool call line (before execution).
47
+ * Receives `(args, theme, context)` and returns a pi-tui Component.
48
+ * BridgeKit passes this through verbatim — no validation or wrapping.
49
+ */
50
+ export type PiToolCallRenderer = (args: any, theme: any, context: any) => any;
51
+ /**
52
+ * Optional TUI renderer for the tool result (after execution).
53
+ * Receives `(result, options, theme, context)` and returns a pi-tui Component.
54
+ * `options.expanded` is toggled by the user via Ctrl+O in pi's TUI.
55
+ * BridgeKit passes this through verbatim — no validation or wrapping.
56
+ */
57
+ export type PiToolResultRenderer = (result: any, options: any, theme: any, context: any) => any;
45
58
  /**
46
59
  * Pi-specific entries on `PortableTool.hostExtras`. Read only by the pi
47
60
  * adapter; the MCP adapter ignores this namespace.
@@ -83,6 +96,8 @@ export interface PiHostExtras {
83
96
  * once attached to a tool definition.
84
97
  */
85
98
  promptGuidelines?: readonly string[];
99
+ renderCall?: PiToolCallRenderer;
100
+ renderResult?: PiToolResultRenderer;
86
101
  }
87
102
  /**
88
103
  * MCP-specific entries on `PortableTool.hostExtras`. Read only by the MCP
@@ -138,12 +153,12 @@ export interface PortableToolHostExtras {
138
153
  pi?: PiHostExtras;
139
154
  mcp?: McpHostExtras;
140
155
  }
141
- export interface PortableTool<TParams extends TSchema = TSchema> {
156
+ export interface PortableTool<TParams extends TSchema = TSchema, TResult extends PortableToolResult = PortableToolResult> {
142
157
  name: string;
143
158
  title: string;
144
159
  description: string;
145
160
  parameters: TParams;
146
- execute: (args: Static<TParams>, ctx: PortableToolContext) => PortableToolResult | Promise<PortableToolResult>;
161
+ execute: (args: Static<TParams>, ctx: PortableToolContext) => TResult | Promise<TResult>;
147
162
  /**
148
163
  * Optional per-host metadata. Adapters consume the keys they recognise;
149
164
  * unknown host namespaces are ignored. Absent → no behavior change.
@@ -155,4 +170,4 @@ export interface PortableTool<TParams extends TSchema = TSchema> {
155
170
  */
156
171
  hostExtras?: PortableToolHostExtras;
157
172
  }
158
- export declare function definePortableTool<TParams extends TSchema>(tool: PortableTool<TParams>): PortableTool<TParams>;
173
+ export declare function definePortableTool<TParams extends TSchema, TResult extends PortableToolResult>(tool: PortableTool<TParams, TResult>): PortableTool<TParams, TResult>;
@@ -1,9 +1,22 @@
1
- import type { TSchema } from "typebox";
1
+ import type { Static, TSchema } from "typebox";
2
2
  import type { PortableTool, PortableToolContext, PortableToolResult, PortableValidationError } from "./define-tool.js";
3
- export declare function validatePortableToolArgs(tool: PortableTool<TSchema>, args: unknown): {
3
+ import type { PortableValidationFailure } from "./result-guards.js";
4
+ type PortableToolSuccess<TResult extends PortableToolResult> = TResult & {
5
+ details?: Record<string, unknown>;
6
+ isError?: boolean;
7
+ } & (TResult extends {
8
+ structuredContent?: infer TStructured;
9
+ } ? {
10
+ structuredContent?: TStructured;
11
+ } : {
12
+ structuredContent?: Record<string, unknown>;
13
+ });
14
+ export declare function validatePortableToolArgs<TParams extends TSchema>(tool: PortableTool<TParams>, args: unknown): {
4
15
  ok: true;
16
+ args: Static<TParams>;
5
17
  } | {
6
18
  ok: false;
7
19
  errors: PortableValidationError[];
8
20
  };
9
- export declare function executePortableTool(tool: PortableTool<TSchema>, args: unknown, ctx: PortableToolContext): Promise<PortableToolResult>;
21
+ export declare function executePortableTool<TParams extends TSchema, TResult extends PortableToolResult>(tool: PortableTool<TParams, TResult>, args: unknown, ctx: PortableToolContext): Promise<PortableToolSuccess<TResult> | PortableValidationFailure>;
22
+ export {};
@@ -151,17 +151,19 @@ function resolveSchemaAtPath(schema, instancePath) {
151
151
  return undefined;
152
152
  const obj = current;
153
153
  if (obj.properties && typeof obj.properties === "object" && segment in obj.properties) {
154
- current = obj.properties[segment];
155
- if (current === undefined)
154
+ const next = obj.properties[segment];
155
+ if (next === undefined)
156
156
  return undefined;
157
+ current = next;
157
158
  continue;
158
159
  }
159
160
  if (obj.items !== undefined && /^\d+$/.test(segment)) {
160
161
  if (Array.isArray(obj.items)) {
161
162
  const idx = Number(segment);
162
- if (idx >= obj.items.length)
163
+ const next = getArrayEntry(obj.items, idx);
164
+ if (next === undefined)
163
165
  return undefined;
164
- current = obj.items[idx];
166
+ current = next;
165
167
  }
166
168
  else {
167
169
  current = obj.items;
@@ -207,12 +209,26 @@ function branchDiscriminatorMatches(branch, value) {
207
209
  }
208
210
  return hasDiscriminator;
209
211
  }
212
+ function findActiveUnionBranch(branches, value) {
213
+ let match;
214
+ for (const [index, branch] of branches.entries()) {
215
+ if (!branchDiscriminatorMatches(branch, value))
216
+ continue;
217
+ if (match !== undefined)
218
+ return undefined;
219
+ match = { index, branch };
220
+ }
221
+ return match;
222
+ }
210
223
  function isSubsetOf(props, set) {
211
224
  for (const p of props)
212
225
  if (!set.has(p))
213
226
  return false;
214
227
  return true;
215
228
  }
229
+ function getArrayEntry(entries, index) {
230
+ return index >= 0 && index < entries.length ? entries[index] : undefined;
231
+ }
216
232
  function suppressSiblingErrorsUnderUnion(schema, value, errors) {
217
233
  // For unions of objects, TypeBox emits per-branch `required` /
218
234
  // `additionalProperties` errors at the union's path. The user only needs
@@ -253,17 +269,12 @@ function suppressSiblingErrorsUnderUnion(schema, value, errors) {
253
269
  continue;
254
270
  }
255
271
  const branchValue = Pointer.Get(value, path);
256
- const matchedIndices = [];
257
- for (let i = 0; i < branches.length; i++) {
258
- if (branchDiscriminatorMatches(branches[i], branchValue))
259
- matchedIndices.push(i);
260
- }
261
- if (matchedIndices.length !== 1) {
272
+ const activeMatch = findActiveUnionBranch(branches, branchValue);
273
+ if (!activeMatch) {
262
274
  resolutions.set(path, { kind: "no-active" });
263
275
  continue;
264
276
  }
265
- const activeIndex = matchedIndices[0];
266
- const active = branches[activeIndex];
277
+ const { branch: active, index: activeIndex } = activeMatch;
267
278
  const branchRequired = new Set(active.required ?? []);
268
279
  const branchProps = new Set(Object.keys(active.properties ?? {}));
269
280
  // Map of property-name -> set of disallowed values held by losing
@@ -275,7 +286,10 @@ function suppressSiblingErrorsUnderUnion(schema, value, errors) {
275
286
  for (let i = 0; i < branches.length; i++) {
276
287
  if (i === activeIndex)
277
288
  continue;
278
- for (const [key, propSchema] of Object.entries(branches[i].properties ?? {})) {
289
+ const branch = getArrayEntry(branches, i);
290
+ if (!branch)
291
+ continue;
292
+ for (const [key, propSchema] of Object.entries(branch.properties ?? {})) {
279
293
  const values = readDiscriminatorValues(propSchema);
280
294
  if (values.length === 0)
281
295
  continue;
@@ -404,7 +418,7 @@ function expandTypeBoxError(schema, error) {
404
418
  }
405
419
  export function validatePortableToolArgs(tool, args) {
406
420
  if (Check(tool.parameters, args)) {
407
- return { ok: true };
421
+ return { ok: true, args };
408
422
  }
409
423
  // TypeBox can emit multiple errors per offending field (e.g. union/anyOf
410
424
  // mismatches fire one error per failed branch). First, when an anyOf/oneOf
@@ -443,5 +457,5 @@ export async function executePortableTool(tool, args, ctx) {
443
457
  isError: true,
444
458
  };
445
459
  }
446
- return tool.execute(args, ctx);
460
+ return (await tool.execute(validation.args, ctx));
447
461
  }
@@ -29,25 +29,6 @@ export type PortableValidationFailure = PortableToolResult & {
29
29
  export type PortableDomainFailure = PortableToolResult & {
30
30
  isError: true;
31
31
  };
32
- /**
33
- * Type guard for results produced by `executePortableTool` when TypeBox
34
- * validation rejected the args.
35
- *
36
- * The guard matches on shape, not provenance: a tool handler that manually
37
- * returns `{ isError: true, structuredContent: { kind: "validation", tool, validationErrors } }`
38
- * will also satisfy this predicate. BridgeKit cannot authenticate the source of
39
- * a result on the wire, so handler-emitted validation-shaped failures are
40
- * indistinguishable from BridgeKit-emitted ones. If that ambiguity matters,
41
- * keep handler-emitted error shapes distinct from the validation discriminator.
42
- *
43
- * @example
44
- * const result = await executePortableTool(tool, args, ctx);
45
- * if (result.isError) {
46
- * if (isValidationFailure(result)) {
47
- * log.warn({ errors: result.structuredContent.validationErrors }, "bad args");
48
- * }
49
- * }
50
- */
51
32
  export declare function isValidationFailure(result: PortableToolResult): result is PortableValidationFailure;
52
33
  /**
53
34
  * Type guard for handler-emitted `isError: true` results that are not
@@ -17,15 +17,23 @@
17
17
  * }
18
18
  * }
19
19
  */
20
+ function isPortableValidationError(error) {
21
+ if (!error || typeof error !== "object")
22
+ return false;
23
+ const candidate = error;
24
+ return typeof candidate.field === "string" && typeof candidate.message === "string";
25
+ }
20
26
  export function isValidationFailure(result) {
21
27
  if (result.isError !== true)
22
28
  return false;
23
29
  const structured = result.structuredContent;
24
30
  if (!structured || typeof structured !== "object")
25
31
  return false;
26
- return (structured.kind === "validation" &&
27
- typeof structured.tool === "string" &&
28
- Array.isArray(structured.validationErrors));
32
+ const candidate = structured;
33
+ return (candidate.kind === "validation" &&
34
+ typeof candidate.tool === "string" &&
35
+ Array.isArray(candidate.validationErrors) &&
36
+ candidate.validationErrors.every(isPortableValidationError));
29
37
  }
30
38
  /**
31
39
  * Type guard for handler-emitted `isError: true` results that are not
@@ -1,3 +1,3 @@
1
- export { definePortableTool, type McpHostExtras, type PiHostExtras, type PortableTool, type PortableToolBuiltInHost, type PortableToolContext, type PortableToolErrorDetails, type PortableToolHostExtras, type PortableToolResult, type PortableValidationError, } from "./core/define-tool.js";
1
+ export { definePortableTool, type McpHostExtras, type PiHostExtras, type PiToolCallRenderer, type PiToolResultRenderer, type PortableTool, type PortableToolBuiltInHost, type PortableToolContext, type PortableToolErrorDetails, type PortableToolHostExtras, type PortableToolResult, type PortableValidationError, } from "./core/define-tool.js";
2
2
  export { executePortableTool, validatePortableToolArgs } from "./core/execute-tool.js";
3
3
  export { isDomainFailure, isValidationFailure, type PortableDomainFailure, type PortableValidationFailure, } from "./core/result-guards.js";
@@ -145,7 +145,8 @@ In `package.json`:
145
145
  pi behavior:
146
146
 
147
147
  - Valid portable results become pi tool results.
148
- - Portable results with `isError: true` reject with `PortableToolExecutionError`; tests should assert a rejected execution plus error `.details`.
148
+ - Invalid arguments and portable results with `isError: true` return `{ content, details, isError: true }` by default; tests should branch on `result.isError` and inspect `details`.
149
+ - The legacy throw mode is still available with `registerPiTools(pi, tools, { errorHandling: "throw" })`, but it is deprecated and should not be used for new code.
149
150
  - Progress updates from `ctx.progress?.(...)` map to pi updates.
150
151
 
151
152
  ---
@@ -209,7 +210,7 @@ In `package.json`:
209
210
  "node": ">=22.19.0"
210
211
  },
211
212
  "dependencies": {
212
- "@feniix/bridgekit": "^0.2.2",
213
+ "@feniix/bridgekit": "^0.14.0",
213
214
  "typebox": "^1.1.31"
214
215
  }
215
216
  }
@@ -235,7 +236,7 @@ For mixed source-loaded pi + compiled MCP packages, keep the pi source entrypoin
235
236
  "node": ">=22.19.0"
236
237
  },
237
238
  "dependencies": {
238
- "@feniix/bridgekit": "^0.2.2",
239
+ "@feniix/bridgekit": "^0.14.0",
239
240
  "typebox": "^1.1.31"
240
241
  }
241
242
  }
@@ -251,12 +252,14 @@ await runBinWrapper({
251
252
  metaUrl: import.meta.url,
252
253
  mcpEntry: "dist/extensions/mcp-server.js",
253
254
  buildScript: "build:mcp",
255
+ // MCP stdio bins: keep build stdout off the JSON-RPC channel.
256
+ buildStdio: ["ignore", "inherit", "inherit"],
254
257
  });
255
258
  ```
256
259
 
257
- `mcpEntry` and `buildScript` are the two options consumers typically vary. The MCP entry module must export `runServer(): Promise<void>` (the convention used throughout these examples). `buildTimeoutMs` (default `60_000`) and `logPrefix` (default `"bridgekit-bin"`) are available for tuning but rarely needed.
260
+ `mcpEntry` and `buildScript` are the two options consumers typically vary. The MCP entry module must export `runServer(): Promise<void>` (the convention used throughout these examples). `buildStdio`, `buildTimeoutMs` (default `60_000`), and `logPrefix` (default `"bridgekit-bin"`) are available for tuning but rarely needed outside MCP-stdio stdout isolation.
258
261
 
259
- Both `mcpEntry` and `buildScript` must be **literal strings** in your bin source. The helper joins `mcpEntry` onto the resolved package root and dynamically `import()`s it, and it passes `buildScript` to `spawnSync` (with `shell: true` on Windows where `&`, `|`, and `^` are shell metacharacters). Sourcing either from CLI args, environment variables, or other runtime input exposes arbitrary-file import and Windows command injection — the trusted-literal expectation is the threat model.
262
+ Both `mcpEntry` and `buildScript` must be **literal strings** in your bin source. The helper joins `mcpEntry` onto the resolved package root and dynamically `import()`s it, and it passes `buildScript` to `spawnSync` (with `shell: true` on Windows where `&`, `|`, and `^` are shell metacharacters). BridgeKit rejects absolute/traversing entry paths and shell-shaped script names, but sourcing either option from CLI args, environment variables, or other runtime input remains outside the supported threat model.
260
263
 
261
264
  Commit the wrapper with executable mode (`chmod +x bin/my-tools-mcp.js`) and verify `npm pack --dry-run --json` includes it with executable mode.
262
265
 
@@ -272,11 +275,17 @@ MCP behavior:
272
275
 
273
276
  ## 4. Per-host metadata via `hostExtras`
274
277
 
275
- When a tool needs host-specific metadata — pi's `pendingMessage` for a "Processing..." signal, MCP's annotations as advisory hints to clients — the canonical place is `PortableTool.hostExtras`. Each host has its own namespace; adapters read the keys they recognise and ignore the rest. Tools that omit `hostExtras` see no behavior change.
278
+ When a tool needs host-specific metadata — pi's `pendingMessage` for a "Processing..." signal, pi TUI renderers for custom collapsed/expanded display, or MCP's annotations as advisory hints to clients — the canonical place is `PortableTool.hostExtras`. Each host has its own namespace; adapters read the keys they recognise and ignore the rest. Tools that omit `hostExtras` see no behavior change.
276
279
 
277
280
  ```ts
278
281
  import { Type } from "typebox";
279
- import { definePortableTool } from "@feniix/bridgekit";
282
+ import { definePortableTool, type PiToolCallRenderer, type PiToolResultRenderer } from "@feniix/bridgekit";
283
+
284
+ const renderSummariseCall: PiToolCallRenderer = (args) => ({ type: "text", text: `summarise ${args.text}` });
285
+ const renderSummariseResult: PiToolResultRenderer = (result, options) => ({
286
+ type: "text",
287
+ text: options.expanded ? result.content?.[0]?.text : "summary ready",
288
+ });
280
289
 
281
290
  export const summariseTool = definePortableTool({
282
291
  name: "summarise",
@@ -293,6 +302,9 @@ export const summariseTool = definePortableTool({
293
302
  pendingMessage: "Summarising...",
294
303
  promptSnippet: "Use this tool when the user asks for a short summary.",
295
304
  promptGuidelines: ["Prefer < 80 chars.", "Strip markdown."],
305
+ // Opaque pi TUI callbacks. BridgeKit forwards these by identity.
306
+ renderCall: renderSummariseCall,
307
+ renderResult: renderSummariseResult,
296
308
  },
297
309
  mcp: {
298
310
  // Advisory hints clients may surface. Do not affect validation or
@@ -305,7 +317,7 @@ export const summariseTool = definePortableTool({
305
317
 
306
318
  Best practices for `hostExtras`:
307
319
 
308
- - Keep tool *behavior* host-neutral. `hostExtras` carries *data* the adapter reads on the tool's behalf; do not embed callbacks or runtime logic.
320
+ - Keep portable tool *behavior* host-neutral. `hostExtras` carries host presentation metadata; pi renderers should stay presentation-only and are forwarded verbatim, not invoked by BridgeKit.
309
321
  - Set only the fields the adapter recognises; unrecognised keys are silently ignored, but they add noise to the definition.
310
322
  - If you ship an adapter for a host outside `"pi" | "mcp"`, cast `ctx.host` at the adapter boundary (the host union is fixed since 0.10.0). For type-safe per-host metadata in your adapter's namespace, extend `PortableToolHostExtras` via `declare module "@feniix/bridgekit"` so consumers of your adapter get type safety on the new namespace.
311
323
 
@@ -398,8 +410,8 @@ For publishable tool packages:
398
410
  - Avoid `workspace:` or `file:` ranges in publishable package dependencies.
399
411
  - Avoid dangling `sourceMappingURL` comments: either publish maps and useful sources, or disable source maps for package builds.
400
412
  - Ensure the npm bin entrypoint starts with a shebang, is executable (`chmod +x` or equivalent), and appears in `npm pack --dry-run --json` with executable mode.
401
- - When a bin depends on generated output, prefer a checked-in wrapper under `bin/` over pointing directly at `dist/`; test existing output, missing output, failed builds, and successful builds that omit the expected file.
413
+ - When a bin depends on generated output, prefer a checked-in wrapper under `bin/` over pointing directly at `dist/`; for MCP stdio bins set `buildStdio: ["ignore", "inherit", "inherit"]`; test existing output, missing output, failed builds, and successful builds that omit the expected file.
402
414
  - If only the MCP bin needs compiled output, narrow its tsconfig to the MCP entrypoint and shared host-neutral modules instead of compiling unrelated host adapters.
403
415
  - Add a packed-install smoke test that installs tarballs into a temporary project.
404
- - For BridgeKit itself, run `npm run check`, `npm run test`, `npm run pack:dry-run`, and `npm run package-smoke` before release.
416
+ - For BridgeKit itself, run `npm run check`, `npm run test`, `npm run pack:dry-run`, `npm run package-smoke`, and `npm audit --omit=dev --audit-level=high` before release.
405
417
  - Keep imports side-effect free; registration and server startup should happen only in explicit entrypoints.
package/llms.txt CHANGED
@@ -26,7 +26,7 @@ import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper"; // optional: buil
26
26
  - Root entrypoint: host-neutral tool definitions, validation, and execution helpers.
27
27
  - `/pi`: pi adapter only.
28
28
  - `/mcp`: MCP server adapter only.
29
- - `/bin-wrapper`: optional helper for npm `bin` scripts that build a local compiled MCP entry on first invocation. `mcpEntry` and `buildScript` must be literal strings in the caller's source (they're the dynamic-import target and the `spawnSync` arg; sourcing them from env or CLI is an arbitrary-file-import and Windows command-injection vector).
29
+ - `/bin-wrapper`: optional helper for npm `bin` scripts that build a local compiled MCP entry on first invocation. `mcpEntry` and `buildScript` must be literal strings in the caller's source (they're the dynamic-import target and the `spawnSync` arg; bridgekit rejects absolute/traversing entry paths and shell-shaped script names, but sourcing them from env or CLI remains outside the supported threat model).
30
30
 
31
31
  Do not deep-import from `dist/` or `src/` in consumer code. Reading published declarations for documentation is fine; imports should use the package entrypoints above.
32
32
 
@@ -69,16 +69,17 @@ export function createTools() {
69
69
  - TypeBox validation runs before the portable handler. If you need custom guidance for missing fields or wrong primitive types, use a permissive schema plus domain validation; otherwise accept BridgeKit's structural validation result.
70
70
  - MCP stdio entrypoints should be testable: export a server-options factory and a run function, then start stdio only when executed as the package bin.
71
71
  - Keep package runtime Node >=22.19.0 and ESM-only.
72
+ - TypeScript is strict-plus: `strict`, `noUncheckedIndexedAccess`, and `exactOptionalPropertyTypes` are enabled.
72
73
 
73
74
  ## Host behavior
74
75
 
75
- - pi invalid arguments and portable `isError: true` results return `{ content, details, isError: true }` by default (as of 0.7). Unexpected handler exceptions are also caught in return mode and surfaced as `{ content, details: {}, isError: true }`. Branch on `result.isError` and narrow with `isValidationFailure` / `isDomainFailure` from `@feniix/bridgekit`.
76
- - MCP invalid arguments and portable `isError: true` results return `CallToolResult` with `isError: true`. Same guards narrow the result.
76
+ - pi invalid arguments and portable `isError: true` results return `{ content, details, isError: true }` by default (as of 0.7). Unexpected handler exceptions are also caught in return mode and surfaced as `{ content, details: {}, isError: true }`. Branch on `result.isError`; validation failures surface as `result.details.kind === "validation"`. The result guards from `@feniix/bridgekit` operate on `PortableToolResult` values at the portable seam, not directly on the pi wire object.
77
+ - MCP invalid arguments and portable `isError: true` results return `CallToolResult` with `isError: true`. Inspect `structuredContent` on MCP wire results, or use the guards on `PortableToolResult` values before/at the adapter seam.
77
78
  - MCP preserves `structuredContent`; pi maps `structuredContent` into the pi `details` field. Both adapters fall back to `details` for legacy/debug payloads when `structuredContent` is absent.
78
79
  - Validation errors and domain errors share the `{ field, message }` shape *inside* `validationErrors[]`. The array itself only appears on `kind: "validation"` — narrow with `isValidationFailure` (or check the discriminator) before iterating. `field` is derived from TypeBox's structured error data: required-property errors read `params.requiredProperties` (one entry per missing prop), additional-property errors read `params.additionalProperties` (one entry per offending key), `const`/`enum` errors carry the allowed value(s) in the message (`must equal "create"` / `must equal one of "create", "update"`), other errors take the last meaningful segment of the offending JSON pointer (`text`, not `/text`). Locale-independent and prop-names-with-commas-safe. Duplicate `(field, message)` pairs from union mismatches are deduplicated. As of 0.8.0; `path` no longer exists. As of 0.8.2: **discriminated unions** (`Type.Union([Type.Object({tag:Literal("a"),…}),Type.Object({tag:Literal("b"),…})])`) surface the *active branch's* missing-required hints when exactly one branch's discriminator is satisfied — instead of the bare `anyOf` summary. Recognized discriminator shapes: `Literal` (`const`), `enum`, and `Union of Literals` (`anyOf` of consts). Works inside `Type.Array(...)` per-element. When no branch matches (invalid discriminator), falls back to the 0.8.1 behavior (drop sibling `required`, keep `const`/`enum`/`anyOf` summary). Caveats: array-element fields can be numeric indices (`field: "0"`, path context lost); root-level schema failures use the sentinel `field: "(root)"` rather than empty string; for nested discriminated unions `field` is the leaf segment, not the full path.
79
80
  - The result guards take a `PortableToolResult` — the value `executePortableTool` returns at the seam between BridgeKit and a host. They are not designed to be called on the pi adapter's wire object (`{ content, details, isError }`), which exposes `details` instead of `structuredContent`.
80
81
  - `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.
81
- - `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 verbatim to pi's `registerTool` call; `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.
82
+ - `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.
82
83
 
83
84
  ### Result-guard usage
84
85
 
@@ -119,7 +120,7 @@ Some hosts, such as pi in source-extension packages, may intentionally load Type
119
120
  });
120
121
  ```
121
122
 
122
- `mcpEntry` and `buildScript` must be literal strings — sourcing them from CLI args or env vars opens arbitrary-file import and Windows command injection. The MCP entry module must export `async function runServer(): Promise<void>`. `buildStdio` defaults to `"inherit"`; MCP stdio server bins should override to `["ignore", "inherit", "inherit"]` (since 0.13.0) because `process.stdout` is the JSON-RPC channel and the build subprocess otherwise contaminates framing.
123
+ `mcpEntry` and `buildScript` must be literal strings — bridgekit rejects absolute/traversing entry paths and shell-shaped script names, but sourcing them from CLI args or env vars remains outside the supported threat model. The MCP entry module must export `async function runServer(): Promise<void>`. `buildStdio` defaults to `"inherit"`; MCP stdio server bins should override to `["ignore", "inherit", "inherit"]` (since 0.13.0) because `process.stdout` is the JSON-RPC channel and the build subprocess otherwise contaminates framing.
123
124
  - Narrow the MCP build to the MCP entrypoint and shared host-neutral modules; avoid compiling pi adapter entrypoints into the standalone MCP path.
124
125
  - Put runtime imports used by the compiled MCP bin in `dependencies`, not only peers or dev dependencies.
125
126
  - Declare BridgeKit's Node engine requirement in the downstream package (`>=22.19.0`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feniix/bridgekit",
3
- "version": "0.13.0",
3
+ "version": "0.15.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",
@@ -73,10 +73,10 @@
73
73
  "typebox": "^1.1.31"
74
74
  },
75
75
  "devDependencies": {
76
- "@biomejs/biome": "2.4.10",
76
+ "@biomejs/biome": "2.4.16",
77
77
  "@total-typescript/shoehorn": "^0.1.2",
78
- "@types/node": "^22.19.17",
79
- "typescript": "^5.5.0"
78
+ "@types/node": "^25.9.1",
79
+ "typescript": "^6.0.3"
80
80
  },
81
81
  "license": "MIT"
82
82
  }