🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@feniix/bridgekit

Package Overview
Dependencies
Maintainers
1
Versions
23
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@feniix/bridgekit - npm Package Compare versions

Comparing version
0.10.0
to
0.11.0
+39
dist/src/bin-wrapper-internal.d.ts
import { type SpawnSyncOptions, type SpawnSyncReturns } from "node:child_process";
export type BinWrapperSpawnSync = (command: string, args: readonly string[], options: SpawnSyncOptions) => SpawnSyncReturns<Buffer>;
export interface BinWrapperOptions {
/**
* `import.meta.url` of the bin script. Used to locate the package root
* (the bin's parent directory). Passing the helper's own `import.meta.url`
* would resolve the wrong package, which is why the caller must supply this.
*/
metaUrl: string;
/**
* Path to the compiled MCP entry, relative to the package root.
*
* Must be a literal in the caller's source. The helper does not contain
* arbitrary paths — sourcing this from CLI args or env vars exposes the
* dynamic `import()` to attacker-controlled file paths.
*
* @example "dist/extensions/mcp-server.js"
*/
mcpEntry: string;
/**
* npm script to invoke when the entry is missing.
*
* Must be a literal in the caller's source. On Windows the helper sets
* `shell: true` for `spawnSync`, which makes `&`, `|`, and `^` shell
* metacharacters; sourcing this from CLI args or env vars opens a
* command-injection vector on Windows.
*
* @example "build:mcp"
*/
buildScript: string;
buildTimeoutMs?: number;
logPrefix?: string;
}
export interface BinWrapperDeps {
spawnSync: BinWrapperSpawnSync;
exit: (code: number) => never;
}
export declare const defaultBinWrapperDeps: BinWrapperDeps;
export declare function runBinWrapperWithDeps(options: BinWrapperOptions, deps: BinWrapperDeps): Promise<void>;
// Internal implementation of bin-wrapper. Ships in the published tarball
// (the public `bin-wrapper.js` imports from it at runtime) but is not
// reachable via package.json#exports — deep-importing this module by name
// fails with ERR_PACKAGE_PATH_NOT_EXPORTED (pinned by scripts/smoke-package.mjs
// under inv-deep-imports-fail).
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const DEFAULT_BUILD_TIMEOUT_MS = 60_000;
const DEFAULT_LOG_PREFIX = "bridgekit-bin";
export const defaultBinWrapperDeps = {
spawnSync,
exit: process.exit,
};
export async function runBinWrapperWithDeps(options, deps) {
const packageRoot = resolve(dirname(fileURLToPath(options.metaUrl)), "..");
const entryPath = join(packageRoot, options.mcpEntry);
if (!existsSync(entryPath)) {
const timeoutMs = options.buildTimeoutMs ?? DEFAULT_BUILD_TIMEOUT_MS;
const build = deps.spawnSync("npm", ["run", options.buildScript, "--silent"], {
cwd: packageRoot,
stdio: "inherit",
shell: process.platform === "win32",
timeout: timeoutMs,
});
// File presence is the load-bearing signal: a build that exits non-zero
// *after* emitting the entry (e.g. tsc with a post-emit diagnostic, or a
// build pipeline whose final step is non-fatal lint/test) is still
// recoverable. Only bail if the artifact we need is actually missing.
if (!existsSync(entryPath)) {
const prefix = options.logPrefix ?? DEFAULT_LOG_PREFIX;
// A child killed by the timeout returns status: null + signal set.
// Distinguish that from a build error so the operator sees the actual
// cause and knows about buildTimeoutMs.
if (build.signal !== null) {
console.error(`[${prefix}] Build timed out after ${timeoutMs}ms (signal ${build.signal}). ` +
`Raise buildTimeoutMs or run \`npm run ${options.buildScript}\` manually.`);
deps.exit(1);
}
else {
console.error(`[${prefix}] Failed to build the local MCP server. Run \`npm run ${options.buildScript}\` and try again.`);
// Propagate the build's non-zero status if present; fall back to 1
// when status===0 with missing entry or status===null without signal.
deps.exit(build.status !== null && build.status !== 0 ? build.status : 1);
}
}
}
const mod = (await import(pathToFileURL(entryPath).href));
if (typeof mod.runServer !== "function") {
throw new Error(`[bridgekit/bin-wrapper] entry "${options.mcpEntry}" does not export a runServer() function. ` +
`Make sure your MCP server module exports \`export async function runServer()\` at the top level.`);
}
await mod.runServer();
}
import { type BinWrapperOptions } from "./bin-wrapper-internal.js";
export type { BinWrapperOptions } from "./bin-wrapper-internal.js";
/**
* Helper for an npm `bin` script that needs to load a compiled MCP entrypoint
* and build it on first invocation (workspace/local execution) when the
* compiled output is missing.
*
* Replaces the ~25-line "resolve dist path -> spawn build if missing ->
* import and run" boilerplate shipped in `examples/README.md` since 0.4.x.
*
* The MCP entry module must export `runServer(): Promise<void>`. Consumers
* with a different name can re-export under that alias.
*
* @example
* ```ts
* #!/usr/bin/env node
* import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
* await runBinWrapper({
* metaUrl: import.meta.url,
* mcpEntry: "dist/extensions/mcp-server.js",
* buildScript: "build:mcp",
* });
* ```
*/
export declare function runBinWrapper(options: BinWrapperOptions): Promise<void>;
import { defaultBinWrapperDeps, runBinWrapperWithDeps } from "./bin-wrapper-internal.js";
/**
* Helper for an npm `bin` script that needs to load a compiled MCP entrypoint
* and build it on first invocation (workspace/local execution) when the
* compiled output is missing.
*
* Replaces the ~25-line "resolve dist path -> spawn build if missing ->
* import and run" boilerplate shipped in `examples/README.md` since 0.4.x.
*
* The MCP entry module must export `runServer(): Promise<void>`. Consumers
* with a different name can re-export under that alias.
*
* @example
* ```ts
* #!/usr/bin/env node
* import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
* await runBinWrapper({
* metaUrl: import.meta.url,
* mcpEntry: "dist/extensions/mcp-server.js",
* buildScript: "build:mcp",
* });
* ```
*/
export async function runBinWrapper(options) {
return runBinWrapperWithDeps(options, defaultBinWrapperDeps);
}
+30
-1

@@ -7,4 +7,33 @@ # Changelog

## [0.10.0] - Unreleased
## [0.11.0] - 2026-05-27
### Added
- New subpath export `@feniix/bridgekit/bin-wrapper` shipping
`runBinWrapper({ metaUrl, mcpEntry, buildScript, ... })`. Eliminates
the ~25-line "resolve dist path → spawn build if missing → import
and run" boilerplate that mixed source-loaded pi + compiled MCP
packages have been copy-pasting. Three downstream consumers
(pi-sequential-thinking, pi-exa, pi-code-reasoning) carry hand-rolled
versions today and can migrate in one-line replacements. Tested
against four canonical scenarios (entry present; entry built on
demand; build fails non-zero; build exits 0 with file still missing)
plus a negative for entry modules missing the required `runServer`
export. Resolves
[#6](https://github.com/feniix/bridgekit/issues/6).
### Changed
- `signalFromExtra` (internal helper at `src/adapters/mcp-signal.ts`) removed.
The MCP SDK exposes `RequestHandlerExtra<ServerRequest, ServerNotification>`
with a guaranteed non-optional `signal: AbortSignal` field; the duck-typing
helper existed only because the contract was undocumented. `createMcpServer`'s
`tools/call` handler now reads `extra.signal` directly with a typed `extra`
parameter. No behavior change; cancellation propagation is unchanged. An
adversarial type-level pin in `src/adapters/mcp.typecheck.ts` fails closed
if the SDK ever changes `signal`'s type. Resolves
[#3](https://github.com/feniix/bridgekit/issues/3).
## [0.10.0] - 2026-05-27
### Removed (breaking)

@@ -11,0 +40,0 @@

+1
-2

@@ -5,3 +5,2 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";

import { executePortableTool } from "../core/execute-tool.js";
import { signalFromExtra } from "./mcp-signal.js";
function toMcpResult(result) {

@@ -201,3 +200,3 @@ return {

host: "mcp",
signal: signalFromExtra(extra),
signal: extra.signal,
});

@@ -204,0 +203,0 @@ return toMcpResult(result);

@@ -244,32 +244,19 @@ # BridgeKit examples

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:
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:
```js
#!/usr/bin/env node
import { existsSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const serverPath = join(packageRoot, "dist", "extensions", "mcp-server.js");
await runBinWrapper({
metaUrl: import.meta.url,
mcpEntry: "dist/extensions/mcp-server.js",
buildScript: "build:mcp",
});
```
if (!existsSync(serverPath)) {
const build = spawnSync("npm", ["run", "build:mcp", "--silent"], {
cwd: packageRoot,
stdio: "inherit",
shell: process.platform === "win32",
timeout: 60_000,
});
`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.
if (build.status !== 0 || !existsSync(serverPath)) {
console.error("[my-tools] Failed to build the local MCP server. Run `npm run build:mcp` and try again.");
process.exit(build.status && build.status !== 0 ? build.status : 1);
}
}
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.
const { runServer } = await import(pathToFileURL(serverPath).href);
await runServer();
```
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.

@@ -276,0 +263,0 @@

@@ -23,2 +23,3 @@ # @feniix/bridgekit

import { createMcpServer, runMcpStdioServer } from "@feniix/bridgekit/mcp";
import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper"; // optional: built-in bin wrapper for mixed source-loaded pi + compiled MCP packages
```

@@ -29,2 +30,3 @@

- `/mcp`: MCP server adapter only.
- `/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).

@@ -31,0 +33,0 @@ 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.

{
"name": "@feniix/bridgekit",
"version": "0.10.0",
"version": "0.11.0",
"description": "BridgeKit defines TypeBox-backed tools once and adapts them to pi, MCP, and other hosts.",

@@ -40,2 +40,6 @@ "keywords": [

},
"./bin-wrapper": {
"types": "./dist/src/bin-wrapper.d.ts",
"import": "./dist/src/bin-wrapper.js"
},
"./package.json": "./package.json"

@@ -42,0 +46,0 @@ },

@@ -88,2 +88,3 @@ # BridgeKit

import { createMcpServer, runMcpStdioServer } from "@feniix/bridgekit/mcp";
import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
```

@@ -94,2 +95,3 @@

- `/mcp`: MCP server adapter only.
- `/bin-wrapper`: optional helper for npm `bin` scripts that need to build a local compiled MCP entry on first invocation.

@@ -96,0 +98,0 @@ Do not deep-import from `dist/` or `src/` in consuming packages.

/**
* Defensively pulls an `AbortSignal` out of MCP request `extra`. The MCP SDK
* does not formally type a `signal` on `extra`, so the shape is sniffed
* structurally.
*
* Validated against `@modelcontextprotocol/sdk` v1.x. Revalidate on any SDK
* major bump in case the cancellation channel moves or gains a typed surface.
*/
export declare function signalFromExtra(extra: unknown): AbortSignal | undefined;
/**
* Defensively pulls an `AbortSignal` out of MCP request `extra`. The MCP SDK
* does not formally type a `signal` on `extra`, so the shape is sniffed
* structurally.
*
* Validated against `@modelcontextprotocol/sdk` v1.x. Revalidate on any SDK
* major bump in case the cancellation channel moves or gains a typed surface.
*/
export function signalFromExtra(extra) {
if (!extra || typeof extra !== "object" || !("signal" in extra)) {
return undefined;
}
const signal = extra.signal;
return signal instanceof AbortSignal ? signal : undefined;
}