@feniix/bridgekit 0.9.5 → 0.11.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 +68 -1
- package/README.md +4 -38
- package/dist/src/adapters/mcp.js +1 -2
- package/dist/src/bin-wrapper-internal.d.ts +39 -0
- package/dist/src/bin-wrapper-internal.js +55 -0
- package/dist/src/bin-wrapper.d.ts +25 -0
- package/dist/src/bin-wrapper.js +26 -0
- package/dist/src/core/define-tool.d.ts +5 -6
- package/dist/src/core/execute-tool.d.ts +3 -3
- package/dist/src/index.d.ts +1 -1
- package/examples/README.md +14 -73
- package/llms.txt +3 -18
- package/package.json +5 -1
- package/dist/src/adapters/mcp-signal.d.ts +0 -9
- package/dist/src/adapters/mcp-signal.js +0 -15
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,74 @@ 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.
|
|
7
|
+
## [0.11.0] - 2026-05-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- New subpath export `@feniix/bridgekit/bin-wrapper` shipping
|
|
12
|
+
`runBinWrapper({ metaUrl, mcpEntry, buildScript, ... })`. Eliminates
|
|
13
|
+
the ~25-line "resolve dist path → spawn build if missing → import
|
|
14
|
+
and run" boilerplate that mixed source-loaded pi + compiled MCP
|
|
15
|
+
packages have been copy-pasting. Three downstream consumers
|
|
16
|
+
(pi-sequential-thinking, pi-exa, pi-code-reasoning) carry hand-rolled
|
|
17
|
+
versions today and can migrate in one-line replacements. Tested
|
|
18
|
+
against four canonical scenarios (entry present; entry built on
|
|
19
|
+
demand; build fails non-zero; build exits 0 with file still missing)
|
|
20
|
+
plus a negative for entry modules missing the required `runServer`
|
|
21
|
+
export. Resolves
|
|
22
|
+
[#6](https://github.com/feniix/bridgekit/issues/6).
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `signalFromExtra` (internal helper at `src/adapters/mcp-signal.ts`) removed.
|
|
27
|
+
The MCP SDK exposes `RequestHandlerExtra<ServerRequest, ServerNotification>`
|
|
28
|
+
with a guaranteed non-optional `signal: AbortSignal` field; the duck-typing
|
|
29
|
+
helper existed only because the contract was undocumented. `createMcpServer`'s
|
|
30
|
+
`tools/call` handler now reads `extra.signal` directly with a typed `extra`
|
|
31
|
+
parameter. No behavior change; cancellation propagation is unchanged. An
|
|
32
|
+
adversarial type-level pin in `src/adapters/mcp.typecheck.ts` fails closed
|
|
33
|
+
if the SDK ever changes `signal`'s type. Resolves
|
|
34
|
+
[#3](https://github.com/feniix/bridgekit/issues/3).
|
|
35
|
+
|
|
36
|
+
## [0.10.0] - 2026-05-27
|
|
37
|
+
|
|
38
|
+
### Removed (breaking)
|
|
39
|
+
|
|
40
|
+
- `PortableToolHost<TExtension extends string>` type alias — replaced by the
|
|
41
|
+
fixed `PortableToolBuiltInHost = "pi" | "mcp" | "test"` union.
|
|
42
|
+
- `<THost extends string>` generic parameter on `PortableTool`,
|
|
43
|
+
`PortableToolContext`, `definePortableTool`, `executePortableTool`, and
|
|
44
|
+
`validatePortableToolArgs`. Host is now a fixed literal union; adapters
|
|
45
|
+
outside `"pi" | "mcp"` should cast at their adapter boundary instead of
|
|
46
|
+
extending the type union.
|
|
47
|
+
|
|
48
|
+
Migration: drop the second generic argument from any of those types.
|
|
49
|
+
`PortableTool<typeof params, "custom">` becomes `PortableTool<typeof params>`.
|
|
50
|
+
`PortableToolContext<"custom">` becomes `PortableToolContext` (cast `host`
|
|
51
|
+
at the consumer if a custom literal is needed).
|
|
52
|
+
|
|
53
|
+
Cast pattern (custom literal at the adapter boundary):
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// PortableToolContext.host is fixed to PortableToolBuiltInHost.
|
|
57
|
+
// A direct `as "custom-runtime"` fails under strict (TS2352 — no overlap);
|
|
58
|
+
// cast through `unknown` instead.
|
|
59
|
+
const ctx: PortableToolContext = {
|
|
60
|
+
host: "custom-runtime" as unknown as PortableToolBuiltInHost,
|
|
61
|
+
};
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Caveat: the cast lies at the type-system boundary, so
|
|
65
|
+
`switch (ctx.host) { ... default: assertNever(ctx.host); }` patterns will
|
|
66
|
+
fall through on the custom literal at runtime. For custom dispatch, carry
|
|
67
|
+
the host identifier on an adapter-owned field rather than on `ctx.host`,
|
|
68
|
+
or do a runtime allowlist check before exhaustive narrowing.
|
|
69
|
+
|
|
70
|
+
No consumer in tree or in the three known downstream consumers
|
|
71
|
+
(pi-sequential-thinking, pi-exa, pi-code-reasoning) used the generic.
|
|
72
|
+
Resolves [#5](https://github.com/feniix/bridgekit/issues/5).
|
|
73
|
+
|
|
74
|
+
## [0.9.5] - 2026-05-27
|
|
8
75
|
|
|
9
76
|
### Documentation
|
|
10
77
|
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ This package is ESM-only and supports Node.js 22.19.0 or newer. Published module
|
|
|
63
63
|
|
|
64
64
|
## Why
|
|
65
65
|
|
|
66
|
-
- Define a tool once; ship it to pi and MCP
|
|
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
69
|
- Import-passive, `sideEffects: false`, three-entrypoint split (`.`, `./pi`, `./mcp`) so pi-only consumers do not pull the MCP SDK and vice versa.
|
|
@@ -80,17 +80,18 @@ import {
|
|
|
80
80
|
type PortableTool,
|
|
81
81
|
type PortableToolBuiltInHost,
|
|
82
82
|
type PortableToolContext,
|
|
83
|
-
type PortableToolHost,
|
|
84
83
|
type PortableToolResult,
|
|
85
84
|
type PortableValidationError,
|
|
86
85
|
} from "@feniix/bridgekit";
|
|
87
86
|
import { registerPiTools } from "@feniix/bridgekit/pi";
|
|
88
87
|
import { createMcpServer, runMcpStdioServer } from "@feniix/bridgekit/mcp";
|
|
88
|
+
import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
- Root entrypoint: host-neutral tool definitions, validation, and execution helpers.
|
|
92
92
|
- `/pi`: pi adapter only.
|
|
93
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.
|
|
94
95
|
|
|
95
96
|
Do not deep-import from `dist/` or `src/` in consuming packages.
|
|
96
97
|
|
|
@@ -232,41 +233,6 @@ Portable validation failures and portable `isError: true` results return `CallTo
|
|
|
232
233
|
|
|
233
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.
|
|
234
235
|
|
|
235
|
-
### Custom host typing
|
|
236
|
-
|
|
237
|
-
Default portable tools accept the built-in host union:
|
|
238
|
-
|
|
239
|
-
```ts
|
|
240
|
-
type BuiltIn = "pi" | "mcp" | "test";
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
Custom adapters opt in explicitly:
|
|
244
|
-
|
|
245
|
-
```ts
|
|
246
|
-
import { Type } from "typebox";
|
|
247
|
-
import { definePortableTool, type PortableToolHost } from "@feniix/bridgekit";
|
|
248
|
-
|
|
249
|
-
const params = Type.Object({ text: Type.String() });
|
|
250
|
-
|
|
251
|
-
type CustomHost = "custom-runtime";
|
|
252
|
-
|
|
253
|
-
export const customTool = definePortableTool<typeof params, CustomHost>({
|
|
254
|
-
name: "custom_echo",
|
|
255
|
-
title: "Custom Echo",
|
|
256
|
-
description: "Echoes text in a custom runtime.",
|
|
257
|
-
parameters: params,
|
|
258
|
-
execute(args, ctx) {
|
|
259
|
-
const host: CustomHost = ctx.host;
|
|
260
|
-
return { text: `${host}: ${args.text}` };
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
const hostValue: PortableToolHost<CustomHost> = "custom-runtime";
|
|
265
|
-
void hostValue;
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Use `PortableToolHost<CustomHost>` for values that may be either a built-in host or your extension. Use the `PortableTool`/`PortableToolContext` generic when a tool or adapter is custom-host-only.
|
|
269
|
-
|
|
270
236
|
## Best practices
|
|
271
237
|
|
|
272
238
|
Tool definition best practices:
|
|
@@ -306,5 +272,5 @@ Read these files in order:
|
|
|
306
272
|
|
|
307
273
|
1. `README.md` — public API, contracts, and best practices.
|
|
308
274
|
2. `llms.txt` — compact agent-facing usage rules and anti-patterns.
|
|
309
|
-
3. `examples/README.md` — copyable layouts for shared tools, pi extensions, MCP stdio servers, and
|
|
275
|
+
3. `examples/README.md` — copyable layouts for shared tools, pi extensions, MCP stdio servers, and per-host metadata via `hostExtras`.
|
|
310
276
|
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.
|
package/dist/src/adapters/mcp.js
CHANGED
|
@@ -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:
|
|
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
|
+
}
|
|
@@ -37,9 +37,8 @@ export interface PortableValidationError {
|
|
|
37
37
|
message: string;
|
|
38
38
|
}
|
|
39
39
|
export type PortableToolBuiltInHost = "pi" | "mcp" | "test";
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
host: THost;
|
|
40
|
+
export interface PortableToolContext {
|
|
41
|
+
host: PortableToolBuiltInHost;
|
|
43
42
|
signal?: AbortSignal;
|
|
44
43
|
progress?: (update: PortableToolResult) => void;
|
|
45
44
|
}
|
|
@@ -139,12 +138,12 @@ export interface PortableToolHostExtras {
|
|
|
139
138
|
pi?: PiHostExtras;
|
|
140
139
|
mcp?: McpHostExtras;
|
|
141
140
|
}
|
|
142
|
-
export interface PortableTool<TParams extends TSchema = TSchema
|
|
141
|
+
export interface PortableTool<TParams extends TSchema = TSchema> {
|
|
143
142
|
name: string;
|
|
144
143
|
title: string;
|
|
145
144
|
description: string;
|
|
146
145
|
parameters: TParams;
|
|
147
|
-
execute: (args: Static<TParams>, ctx: PortableToolContext
|
|
146
|
+
execute: (args: Static<TParams>, ctx: PortableToolContext) => PortableToolResult | Promise<PortableToolResult>;
|
|
148
147
|
/**
|
|
149
148
|
* Optional per-host metadata. Adapters consume the keys they recognise;
|
|
150
149
|
* unknown host namespaces are ignored. Absent → no behavior change.
|
|
@@ -156,4 +155,4 @@ export interface PortableTool<TParams extends TSchema = TSchema, THost extends s
|
|
|
156
155
|
*/
|
|
157
156
|
hostExtras?: PortableToolHostExtras;
|
|
158
157
|
}
|
|
159
|
-
export declare function definePortableTool<TParams extends TSchema
|
|
158
|
+
export declare function definePortableTool<TParams extends TSchema>(tool: PortableTool<TParams>): PortableTool<TParams>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { TSchema } from "typebox";
|
|
2
|
-
import type { PortableTool,
|
|
3
|
-
export declare function validatePortableToolArgs
|
|
2
|
+
import type { PortableTool, PortableToolContext, PortableToolResult, PortableValidationError } from "./define-tool.js";
|
|
3
|
+
export declare function validatePortableToolArgs(tool: PortableTool<TSchema>, args: unknown): {
|
|
4
4
|
ok: true;
|
|
5
5
|
} | {
|
|
6
6
|
ok: false;
|
|
7
7
|
errors: PortableValidationError[];
|
|
8
8
|
};
|
|
9
|
-
export declare function executePortableTool
|
|
9
|
+
export declare function executePortableTool(tool: PortableTool<TSchema>, args: unknown, ctx: PortableToolContext): Promise<PortableToolResult>;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { definePortableTool, type McpHostExtras, type PiHostExtras, type PortableTool, type PortableToolBuiltInHost, type PortableToolContext, type PortableToolErrorDetails, type
|
|
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";
|
|
2
2
|
export { executePortableTool, validatePortableToolArgs } from "./core/execute-tool.js";
|
|
3
3
|
export { isDomainFailure, isValidationFailure, type PortableDomainFailure, type PortableValidationFailure, } from "./core/result-guards.js";
|
package/examples/README.md
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
271
|
-
|
|
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:
|
|
@@ -283,53 +270,7 @@ MCP behavior:
|
|
|
283
270
|
|
|
284
271
|
---
|
|
285
272
|
|
|
286
|
-
## 4.
|
|
287
|
-
|
|
288
|
-
Default portable tools accept only built-in hosts: `"pi" | "mcp" | "test"`.
|
|
289
|
-
|
|
290
|
-
If you are writing a custom adapter, opt in explicitly so the handler can safely narrow `ctx.host`:
|
|
291
|
-
|
|
292
|
-
```ts
|
|
293
|
-
import { Type } from "typebox";
|
|
294
|
-
import {
|
|
295
|
-
definePortableTool,
|
|
296
|
-
executePortableTool,
|
|
297
|
-
type PortableTool,
|
|
298
|
-
type PortableToolContext,
|
|
299
|
-
type PortableToolHost,
|
|
300
|
-
} from "@feniix/bridgekit";
|
|
301
|
-
|
|
302
|
-
const params = Type.Object({ text: Type.String() });
|
|
303
|
-
|
|
304
|
-
type CustomHost = "custom-runtime";
|
|
305
|
-
type CustomTool = PortableTool<typeof params, CustomHost>;
|
|
306
|
-
|
|
307
|
-
const customTool = definePortableTool<typeof params, CustomHost>({
|
|
308
|
-
name: "custom_echo",
|
|
309
|
-
title: "Custom Echo",
|
|
310
|
-
description: "Echoes text in a custom runtime.",
|
|
311
|
-
parameters: params,
|
|
312
|
-
execute(args, ctx) {
|
|
313
|
-
const host: CustomHost = ctx.host;
|
|
314
|
-
return { text: `${host}: ${args.text}` };
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
async function runCustomTool(tool: CustomTool, text: string) {
|
|
319
|
-
const ctx: PortableToolContext<CustomHost> = { host: "custom-runtime" };
|
|
320
|
-
return executePortableTool(tool, { text }, ctx);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const hostValue: PortableToolHost<CustomHost> = "custom-runtime";
|
|
324
|
-
void hostValue;
|
|
325
|
-
void customTool;
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
Use `PortableToolHost<CustomHost>` for values that can be either a built-in host or your custom extension. Use `PortableToolContext<CustomHost>` or `PortableTool<Schema, CustomHost>` when a tool is custom-host-only.
|
|
329
|
-
|
|
330
|
-
---
|
|
331
|
-
|
|
332
|
-
## 5. Per-host metadata via `hostExtras`
|
|
273
|
+
## 4. Per-host metadata via `hostExtras`
|
|
333
274
|
|
|
334
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.
|
|
335
276
|
|
|
@@ -366,7 +307,7 @@ Best practices for `hostExtras`:
|
|
|
366
307
|
|
|
367
308
|
- Keep tool *behavior* host-neutral. `hostExtras` carries *data* the adapter reads on the tool's behalf; do not embed callbacks or runtime logic.
|
|
368
309
|
- Set only the fields the adapter recognises; unrecognised keys are silently ignored, but they add noise to the definition.
|
|
369
|
-
- For
|
|
310
|
+
- 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.
|
|
370
311
|
|
|
371
312
|
See `docs/rfc-host-extras.md` for the full design rationale (which fields qualify, why a top-level field beats a sidecar map, the closure rule for future additions).
|
|
372
313
|
|
|
@@ -446,7 +387,7 @@ per-consumer migration deltas.
|
|
|
446
387
|
|
|
447
388
|
---
|
|
448
389
|
|
|
449
|
-
##
|
|
390
|
+
## 5. Package checklist
|
|
450
391
|
|
|
451
392
|
For publishable tool packages:
|
|
452
393
|
|
package/llms.txt
CHANGED
|
@@ -6,7 +6,7 @@ Use this package when you need to define a tool once and expose it through both
|
|
|
6
6
|
|
|
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
|
-
3. `examples/README.md` — copyable end-to-end layouts for shared tools, pi extension wiring, MCP stdio server wiring, and
|
|
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
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.
|
|
11
11
|
|
|
12
12
|
## Import map
|
|
@@ -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
|
|
|
@@ -96,23 +98,6 @@ if (result.isError) {
|
|
|
96
98
|
}
|
|
97
99
|
```
|
|
98
100
|
|
|
99
|
-
## Custom host typing
|
|
100
|
-
|
|
101
|
-
Default tools accept only built-in hosts: `"pi" | "mcp" | "test"`.
|
|
102
|
-
For a custom adapter, opt in explicitly:
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
const customTool = definePortableTool<typeof params, "custom-host">({
|
|
106
|
-
// ...
|
|
107
|
-
execute(args, ctx) {
|
|
108
|
-
const host: "custom-host" = ctx.host;
|
|
109
|
-
return { text: host };
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Use `PortableToolHost<"custom-host">` when a value may be either a built-in host or that extension.
|
|
115
|
-
|
|
116
101
|
## Mixed source-loaded hosts and compiled MCP bins
|
|
117
102
|
|
|
118
103
|
Some hosts, such as pi in source-extension packages, may intentionally load TypeScript source while MCP clients launched from npm need compiled JavaScript. In that case:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feniix/bridgekit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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
|
-
}
|