@feniix/bridgekit 0.10.0 → 0.12.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,7 +4,65 @@ 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.10.0] - Unreleased
7
+ ## [0.12.0] - 2026-05-27
8
+
9
+ ### Documentation
10
+
11
+ - `README.md` gets a full `bin-wrapper` adapter section under "API
12
+ reference" — usage example, options breakdown, `runServer`
13
+ contract, trusted-literal security note, and the missing-entry
14
+ behavior matrix. Previously the subpath had a single line in the
15
+ entrypoint table and zero body documentation. Closes the largest
16
+ gap surfaced by the post-0.11.0 doc audit.
17
+ - `README.md` "Why" bullet and "Packaging" section updated for the
18
+ four-entrypoint shape and the `runBinWrapper` recommendation
19
+ (replaces the pre-0.11.0 manual checked-in-wrapper recipe).
20
+ - `llms.txt` rewrites the "Mixed source-loaded hosts and compiled
21
+ MCP bins" section to lead with `runBinWrapper`. The 8-bullet
22
+ manual-wrapper recipe is demoted to a fallback bullet for
23
+ authors writing their own wrapper.
24
+ - `dist/src/bin-wrapper.d.ts` added to the published-declarations
25
+ lists in `README.md` and `llms.txt`.
26
+ - `docs/README.md` lists `docs/rfc-host-extras.md` in the
27
+ maintainer-docs catalog. The RFC is JSDoc-linked from
28
+ `src/core/define-tool.ts` and was missing from the catalog.
29
+ - `src/adapters/mcp.typecheck.ts` fixes a wrong version stamp on
30
+ the `signalFromExtra` adversarial-pin comment (was `0.10.0 (#3)`,
31
+ should be `0.11.0 (#3)`). The pin's code is correct; only the
32
+ comment was wrong.
33
+
34
+ No API or behavior changes.
35
+
36
+ ## [0.11.0] - 2026-05-27
37
+
38
+ ### Added
39
+
40
+ - New subpath export `@feniix/bridgekit/bin-wrapper` shipping
41
+ `runBinWrapper({ metaUrl, mcpEntry, buildScript, ... })`. Eliminates
42
+ the ~25-line "resolve dist path → spawn build if missing → import
43
+ and run" boilerplate that mixed source-loaded pi + compiled MCP
44
+ packages have been copy-pasting. Three downstream consumers
45
+ (pi-sequential-thinking, pi-exa, pi-code-reasoning) carry hand-rolled
46
+ versions today and can migrate in one-line replacements. Tested
47
+ against four canonical scenarios (entry present; entry built on
48
+ demand; build fails non-zero; build exits 0 with file still missing)
49
+ plus a negative for entry modules missing the required `runServer`
50
+ export. Resolves
51
+ [#6](https://github.com/feniix/bridgekit/issues/6).
52
+
53
+ ### Changed
54
+
55
+ - `signalFromExtra` (internal helper at `src/adapters/mcp-signal.ts`) removed.
56
+ The MCP SDK exposes `RequestHandlerExtra<ServerRequest, ServerNotification>`
57
+ with a guaranteed non-optional `signal: AbortSignal` field; the duck-typing
58
+ helper existed only because the contract was undocumented. `createMcpServer`'s
59
+ `tools/call` handler now reads `extra.signal` directly with a typed `extra`
60
+ parameter. No behavior change; cancellation propagation is unchanged. An
61
+ adversarial type-level pin in `src/adapters/mcp.typecheck.ts` fails closed
62
+ if the SDK ever changes `signal`'s type. Resolves
63
+ [#3](https://github.com/feniix/bridgekit/issues/3).
64
+
65
+ ## [0.10.0] - 2026-05-27
8
66
 
9
67
  ### Removed (breaking)
10
68
 
package/README.md CHANGED
@@ -66,7 +66,7 @@ This package is ESM-only and supports Node.js 22.19.0 or newer. Published module
66
66
  - Define a tool once; ship it to pi and MCP without per-host forks.
67
67
  - Host-neutral tool files: no pi or MCP SDK imports in the tool definition itself.
68
68
  - TypeBox schemas pass through to MCP `inputSchema` directly — no JSON Schema conversion step.
69
- - Import-passive, `sideEffects: false`, three-entrypoint split (`.`, `./pi`, `./mcp`) so pi-only consumers do not pull the MCP SDK and vice versa.
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.
70
70
  - 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
71
 
72
72
  ## API reference
@@ -85,11 +85,13 @@ import {
85
85
  } from "@feniix/bridgekit";
86
86
  import { registerPiTools } from "@feniix/bridgekit/pi";
87
87
  import { createMcpServer, runMcpStdioServer } from "@feniix/bridgekit/mcp";
88
+ import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
88
89
  ```
89
90
 
90
91
  - Root entrypoint: host-neutral tool definitions, validation, and execution helpers.
91
92
  - `/pi`: pi adapter only.
92
93
  - `/mcp`: MCP server adapter only.
94
+ - `/bin-wrapper`: optional helper for npm `bin` scripts that need to build a local compiled MCP entry on first invocation.
93
95
 
94
96
  Do not deep-import from `dist/` or `src/` in consuming packages.
95
97
 
@@ -231,6 +233,41 @@ Portable validation failures and portable `isError: true` results return `CallTo
231
233
 
232
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.
233
235
 
236
+ ### bin-wrapper (since 0.11.0)
237
+
238
+ The `bin-wrapper` subpath ships a helper for npm `bin` scripts that load a compiled MCP entrypoint and build it on first invocation when the compiled output is missing. Replaces the ~25-line "resolve dist path → spawn build if missing → import and run" boilerplate that downstream consumers with mixed source-loaded pi + compiled MCP packages previously hand-rolled.
239
+
240
+ ```js
241
+ #!/usr/bin/env node
242
+ import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
243
+
244
+ await runBinWrapper({
245
+ metaUrl: import.meta.url,
246
+ mcpEntry: "dist/extensions/mcp-server.js",
247
+ buildScript: "build:mcp",
248
+ });
249
+ ```
250
+
251
+ Options:
252
+
253
+ - `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"`).
256
+ - `buildTimeoutMs` (optional, default `60_000`): timeout for the build subprocess. Distinct "Build timed out…" diagnostic fires when exceeded.
257
+ - `logPrefix` (optional, default `"bridgekit-bin"`): prefix on the "Failed to build…" diagnostic.
258
+
259
+ The MCP entry module must export `async function runServer(): Promise<void>`. Consumers with a different export name can alias on export.
260
+
261
+ **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.
262
+
263
+ Behavior on missing entry:
264
+
265
+ - Build emits the entry → import and call `runServer()`.
266
+ - Build exits non-zero but the entry exists → recover (import and call `runServer()`).
267
+ - Build exits 0 but the entry is still missing → exit with code 1, generic "Failed to build…" diagnostic.
268
+ - Build exits non-zero with entry missing → exit with the build's status code, generic diagnostic.
269
+ - Build killed by timeout (SIGTERM) → exit 1, distinct timeout diagnostic naming `buildTimeoutMs`.
270
+
234
271
  ## Best practices
235
272
 
236
273
  Tool definition best practices:
@@ -256,7 +293,7 @@ Package and release checklist:
256
293
  - Avoid `workspace:` or `file:` dependency ranges in publishable packages.
257
294
  - Avoid dangling `sourceMappingURL` comments: publish maps and useful sources together, or disable source maps for package builds.
258
295
  - For MCP stdio bins, ensure the executable entrypoint starts with a Node shebang, has executable mode (`chmod +x` or equivalent), and is included by `npm pack --dry-run --json`.
259
- - If an npm-launched bin depends on generated output, prefer a checked-in wrapper under `bin/` over pointing `bin` directly at `dist/`; the wrapper should resolve the package-local generated file and may run the package-local build for workspace/local execution.
296
+ - 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.
260
297
  - 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.
261
298
  - Declare a compatible Node engine (`>=22.19.0`) in downstream packages that expose BridgeKit-powered MCP bins.
262
299
  - Run `npm run check`, `npm run test`, `npm run pack:dry-run`, and `npm run package-smoke` before publishing.
@@ -270,5 +307,5 @@ Read these files in order:
270
307
 
271
308
  1. `README.md` — public API, contracts, and best practices.
272
309
  2. `llms.txt` — compact agent-facing usage rules and anti-patterns.
273
- 3. `examples/README.md` — copyable layouts for shared tools, pi extensions, MCP stdio servers, and per-host metadata via `hostExtras`.
274
- 4. Published declarations such as `dist/src/index.d.ts`, `dist/src/pi.d.ts`, and `dist/src/mcp.d.ts` — canonical installed-package type contracts. In a source checkout, the matching `src/` files contain the same implementation context.
310
+ 3. `examples/README.md` — copyable layouts for shared tools, pi extensions, MCP stdio servers, per-host metadata via `hostExtras`, and bin wrappers via `runBinWrapper`.
311
+ 4. Published declarations such as `dist/src/index.d.ts`, `dist/src/pi.d.ts`, `dist/src/mcp.d.ts`, and `dist/src/bin-wrapper.d.ts` — canonical installed-package type contracts. In a source checkout, the matching `src/` files contain the same implementation context.
@@ -2,7 +2,6 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { executePortableTool } from "../core/execute-tool.js";
5
- import { signalFromExtra } from "./mcp-signal.js";
6
5
  function toMcpResult(result) {
7
6
  return {
8
7
  content: [{ type: "text", text: result.text }],
@@ -197,7 +196,7 @@ export function createMcpServer(options) {
197
196
  try {
198
197
  const result = await executePortableTool(tool, request.params.arguments ?? {}, {
199
198
  host: "mcp",
200
- signal: signalFromExtra(extra),
199
+ signal: extra.signal,
201
200
  });
202
201
  return toMcpResult(result);
203
202
  }
@@ -0,0 +1,39 @@
1
+ import { type SpawnSyncOptions, type SpawnSyncReturns } from "node:child_process";
2
+ export type BinWrapperSpawnSync = (command: string, args: readonly string[], options: SpawnSyncOptions) => SpawnSyncReturns<Buffer>;
3
+ export interface BinWrapperOptions {
4
+ /**
5
+ * `import.meta.url` of the bin script. Used to locate the package root
6
+ * (the bin's parent directory). Passing the helper's own `import.meta.url`
7
+ * would resolve the wrong package, which is why the caller must supply this.
8
+ */
9
+ metaUrl: string;
10
+ /**
11
+ * Path to the compiled MCP entry, relative to the package root.
12
+ *
13
+ * Must be a literal in the caller's source. The helper does not contain
14
+ * arbitrary paths — sourcing this from CLI args or env vars exposes the
15
+ * dynamic `import()` to attacker-controlled file paths.
16
+ *
17
+ * @example "dist/extensions/mcp-server.js"
18
+ */
19
+ mcpEntry: string;
20
+ /**
21
+ * npm script to invoke when the entry is missing.
22
+ *
23
+ * Must be a literal in the caller's source. On Windows the helper sets
24
+ * `shell: true` for `spawnSync`, which makes `&`, `|`, and `^` shell
25
+ * metacharacters; sourcing this from CLI args or env vars opens a
26
+ * command-injection vector on Windows.
27
+ *
28
+ * @example "build:mcp"
29
+ */
30
+ buildScript: string;
31
+ buildTimeoutMs?: number;
32
+ logPrefix?: string;
33
+ }
34
+ export interface BinWrapperDeps {
35
+ spawnSync: BinWrapperSpawnSync;
36
+ exit: (code: number) => never;
37
+ }
38
+ export declare const defaultBinWrapperDeps: BinWrapperDeps;
39
+ export declare function runBinWrapperWithDeps(options: BinWrapperOptions, deps: BinWrapperDeps): Promise<void>;
@@ -0,0 +1,55 @@
1
+ // Internal implementation of bin-wrapper. Ships in the published tarball
2
+ // (the public `bin-wrapper.js` imports from it at runtime) but is not
3
+ // reachable via package.json#exports — deep-importing this module by name
4
+ // fails with ERR_PACKAGE_PATH_NOT_EXPORTED (pinned by scripts/smoke-package.mjs
5
+ // under inv-deep-imports-fail).
6
+ import { spawnSync } from "node:child_process";
7
+ import { existsSync } from "node:fs";
8
+ import { dirname, join, resolve } from "node:path";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ const DEFAULT_BUILD_TIMEOUT_MS = 60_000;
11
+ const DEFAULT_LOG_PREFIX = "bridgekit-bin";
12
+ export const defaultBinWrapperDeps = {
13
+ spawnSync,
14
+ exit: process.exit,
15
+ };
16
+ export async function runBinWrapperWithDeps(options, deps) {
17
+ const packageRoot = resolve(dirname(fileURLToPath(options.metaUrl)), "..");
18
+ const entryPath = join(packageRoot, options.mcpEntry);
19
+ if (!existsSync(entryPath)) {
20
+ const timeoutMs = options.buildTimeoutMs ?? DEFAULT_BUILD_TIMEOUT_MS;
21
+ const build = deps.spawnSync("npm", ["run", options.buildScript, "--silent"], {
22
+ cwd: packageRoot,
23
+ stdio: "inherit",
24
+ shell: process.platform === "win32",
25
+ timeout: timeoutMs,
26
+ });
27
+ // File presence is the load-bearing signal: a build that exits non-zero
28
+ // *after* emitting the entry (e.g. tsc with a post-emit diagnostic, or a
29
+ // build pipeline whose final step is non-fatal lint/test) is still
30
+ // recoverable. Only bail if the artifact we need is actually missing.
31
+ if (!existsSync(entryPath)) {
32
+ const prefix = options.logPrefix ?? DEFAULT_LOG_PREFIX;
33
+ // A child killed by the timeout returns status: null + signal set.
34
+ // Distinguish that from a build error so the operator sees the actual
35
+ // cause and knows about buildTimeoutMs.
36
+ if (build.signal !== null) {
37
+ console.error(`[${prefix}] Build timed out after ${timeoutMs}ms (signal ${build.signal}). ` +
38
+ `Raise buildTimeoutMs or run \`npm run ${options.buildScript}\` manually.`);
39
+ deps.exit(1);
40
+ }
41
+ else {
42
+ console.error(`[${prefix}] Failed to build the local MCP server. Run \`npm run ${options.buildScript}\` and try again.`);
43
+ // Propagate the build's non-zero status if present; fall back to 1
44
+ // when status===0 with missing entry or status===null without signal.
45
+ deps.exit(build.status !== null && build.status !== 0 ? build.status : 1);
46
+ }
47
+ }
48
+ }
49
+ const mod = (await import(pathToFileURL(entryPath).href));
50
+ if (typeof mod.runServer !== "function") {
51
+ throw new Error(`[bridgekit/bin-wrapper] entry "${options.mcpEntry}" does not export a runServer() function. ` +
52
+ `Make sure your MCP server module exports \`export async function runServer()\` at the top level.`);
53
+ }
54
+ await mod.runServer();
55
+ }
@@ -0,0 +1,25 @@
1
+ import { type BinWrapperOptions } from "./bin-wrapper-internal.js";
2
+ export type { BinWrapperOptions } from "./bin-wrapper-internal.js";
3
+ /**
4
+ * Helper for an npm `bin` script that needs to load a compiled MCP entrypoint
5
+ * and build it on first invocation (workspace/local execution) when the
6
+ * compiled output is missing.
7
+ *
8
+ * Replaces the ~25-line "resolve dist path -> spawn build if missing ->
9
+ * import and run" boilerplate shipped in `examples/README.md` since 0.4.x.
10
+ *
11
+ * The MCP entry module must export `runServer(): Promise<void>`. Consumers
12
+ * with a different name can re-export under that alias.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * #!/usr/bin/env node
17
+ * import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
18
+ * await runBinWrapper({
19
+ * metaUrl: import.meta.url,
20
+ * mcpEntry: "dist/extensions/mcp-server.js",
21
+ * buildScript: "build:mcp",
22
+ * });
23
+ * ```
24
+ */
25
+ export declare function runBinWrapper(options: BinWrapperOptions): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { defaultBinWrapperDeps, runBinWrapperWithDeps } from "./bin-wrapper-internal.js";
2
+ /**
3
+ * Helper for an npm `bin` script that needs to load a compiled MCP entrypoint
4
+ * and build it on first invocation (workspace/local execution) when the
5
+ * compiled output is missing.
6
+ *
7
+ * Replaces the ~25-line "resolve dist path -> spawn build if missing ->
8
+ * import and run" boilerplate shipped in `examples/README.md` since 0.4.x.
9
+ *
10
+ * The MCP entry module must export `runServer(): Promise<void>`. Consumers
11
+ * with a different name can re-export under that alias.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * #!/usr/bin/env node
16
+ * import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
17
+ * await runBinWrapper({
18
+ * metaUrl: import.meta.url,
19
+ * mcpEntry: "dist/extensions/mcp-server.js",
20
+ * buildScript: "build:mcp",
21
+ * });
22
+ * ```
23
+ */
24
+ export async function runBinWrapper(options) {
25
+ return runBinWrapperWithDeps(options, defaultBinWrapperDeps);
26
+ }
@@ -241,36 +241,23 @@ For mixed source-loaded pi + compiled MCP packages, keep the pi source entrypoin
241
241
  }
242
242
  ```
243
243
 
244
- The wrapper should resolve the generated MCP server relative to the installed package, build it when missing in local/workspace execution, and preserve build failures:
244
+ The wrapper should resolve the generated MCP server relative to the installed package, build it when missing in local/workspace execution, and preserve build failures. BridgeKit ships this as a built-in helper under the `/bin-wrapper` subpath:
245
245
 
246
246
  ```js
247
247
  #!/usr/bin/env node
248
- import { existsSync } from "node:fs";
249
- import { spawnSync } from "node:child_process";
250
- import { dirname, join, resolve } from "node:path";
251
- import { fileURLToPath, pathToFileURL } from "node:url";
252
-
253
- const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
254
- const serverPath = join(packageRoot, "dist", "extensions", "mcp-server.js");
255
-
256
- if (!existsSync(serverPath)) {
257
- const build = spawnSync("npm", ["run", "build:mcp", "--silent"], {
258
- cwd: packageRoot,
259
- stdio: "inherit",
260
- shell: process.platform === "win32",
261
- timeout: 60_000,
262
- });
263
-
264
- if (build.status !== 0 || !existsSync(serverPath)) {
265
- console.error("[my-tools] Failed to build the local MCP server. Run `npm run build:mcp` and try again.");
266
- process.exit(build.status && build.status !== 0 ? build.status : 1);
267
- }
268
- }
248
+ import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
269
249
 
270
- const { runServer } = await import(pathToFileURL(serverPath).href);
271
- await runServer();
250
+ await runBinWrapper({
251
+ metaUrl: import.meta.url,
252
+ mcpEntry: "dist/extensions/mcp-server.js",
253
+ buildScript: "build:mcp",
254
+ });
272
255
  ```
273
256
 
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.
258
+
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.
260
+
274
261
  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.
275
262
 
276
263
  MCP behavior:
package/llms.txt CHANGED
@@ -7,7 +7,7 @@ Use this package when you need to define a tool once and expose it through both
7
7
  1. `README.md` — public API, contracts, best practices, packaging notes.
8
8
  2. `llms.txt` — compact agent-facing usage rules and anti-patterns.
9
9
  3. `examples/README.md` — copyable end-to-end layouts for shared tools, pi extension wiring, MCP stdio server wiring, and per-host metadata via `hostExtras`.
10
- 4. Published declarations such as `dist/src/index.d.ts`, `dist/src/pi.d.ts`, and `dist/src/mcp.d.ts` — canonical installed-package type contracts. In a source checkout, the matching `src/` files contain implementation context.
10
+ 4. Published declarations such as `dist/src/index.d.ts`, `dist/src/pi.d.ts`, `dist/src/mcp.d.ts`, and `dist/src/bin-wrapper.d.ts` — canonical installed-package type contracts. In a source checkout, the matching `src/` files contain implementation context.
11
11
 
12
12
  ## Import map
13
13
 
@@ -20,11 +20,13 @@ import {
20
20
  } from "@feniix/bridgekit";
21
21
  import { registerPiTools } from "@feniix/bridgekit/pi";
22
22
  import { createMcpServer, runMcpStdioServer } from "@feniix/bridgekit/mcp";
23
+ import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper"; // optional: built-in bin wrapper for mixed source-loaded pi + compiled MCP packages
23
24
  ```
24
25
 
25
26
  - Root entrypoint: host-neutral tool definitions, validation, and execution helpers.
26
27
  - `/pi`: pi adapter only.
27
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).
28
30
 
29
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.
30
32
 
@@ -102,12 +104,24 @@ Some hosts, such as pi in source-extension packages, may intentionally load Type
102
104
 
103
105
  - Keep the host source entrypoint if that is the package convention, e.g. `pi.extensions: ["./extensions/index.ts"]`.
104
106
  - Add a package-local MCP build and point npm `bin` at a checked-in wrapper under `bin/`, not directly at generated `dist/` output.
105
- - The wrapper should resolve the package-local generated MCP server, run the package-local build when output is missing in workspace/local execution, and preserve non-zero build failures.
107
+ - **Use the built-in `runBinWrapper` helper** (since 0.11.0) instead of a hand-rolled wrapper. It resolves the package-local generated entry, runs the package-local build when output is missing in workspace/local execution, preserves non-zero build failures, recovers when the build emits the entry despite a non-zero exit, and emits a distinct timeout diagnostic when `spawnSync` is killed by `buildTimeoutMs`:
108
+
109
+ ```js
110
+ #!/usr/bin/env node
111
+ import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
112
+ await runBinWrapper({
113
+ metaUrl: import.meta.url,
114
+ mcpEntry: "dist/extensions/mcp-server.js",
115
+ buildScript: "build:mcp",
116
+ });
117
+ ```
118
+
119
+ `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>`.
106
120
  - Narrow the MCP build to the MCP entrypoint and shared host-neutral modules; avoid compiling pi adapter entrypoints into the standalone MCP path.
107
121
  - Put runtime imports used by the compiled MCP bin in `dependencies`, not only peers or dev dependencies.
108
122
  - Declare BridgeKit's Node engine requirement in the downstream package (`>=22.19.0`).
109
- - Ensure the checked-in wrapper is executable (`chmod +x` or equivalent) and verify the mode with `npm pack --dry-run --json`.
110
- - Test wrapper behavior for existing output, missing output, failed builds, and builds that exit successfully without creating the expected file.
123
+ - Ensure the bin script is executable (`chmod +x` or equivalent) and verify the mode with `npm pack --dry-run --json`.
124
+ - If you write your own wrapper instead of using `runBinWrapper`, test the four behavior modes: existing output, missing output that the build creates, failed builds (non-zero exit), and builds that exit successfully without creating the expected file.
111
125
 
112
126
  ## Anti-patterns
113
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feniix/bridgekit",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",
@@ -37,6 +37,10 @@
37
37
  "types": "./dist/src/mcp.d.ts",
38
38
  "import": "./dist/src/mcp.js"
39
39
  },
40
+ "./bin-wrapper": {
41
+ "types": "./dist/src/bin-wrapper.d.ts",
42
+ "import": "./dist/src/bin-wrapper.js"
43
+ },
40
44
  "./package.json": "./package.json"
41
45
  },
42
46
  "scripts": {
@@ -1,9 +0,0 @@
1
- /**
2
- * Defensively pulls an `AbortSignal` out of MCP request `extra`. The MCP SDK
3
- * does not formally type a `signal` on `extra`, so the shape is sniffed
4
- * structurally.
5
- *
6
- * Validated against `@modelcontextprotocol/sdk` v1.x. Revalidate on any SDK
7
- * major bump in case the cancellation channel moves or gains a typed surface.
8
- */
9
- export declare function signalFromExtra(extra: unknown): AbortSignal | undefined;
@@ -1,15 +0,0 @@
1
- /**
2
- * Defensively pulls an `AbortSignal` out of MCP request `extra`. The MCP SDK
3
- * does not formally type a `signal` on `extra`, so the shape is sniffed
4
- * structurally.
5
- *
6
- * Validated against `@modelcontextprotocol/sdk` v1.x. Revalidate on any SDK
7
- * major bump in case the cancellation channel moves or gains a typed surface.
8
- */
9
- export function signalFromExtra(extra) {
10
- if (!extra || typeof extra !== "object" || !("signal" in extra)) {
11
- return undefined;
12
- }
13
- const signal = extra.signal;
14
- return signal instanceof AbortSignal ? signal : undefined;
15
- }