@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 +33 -0
- package/README.md +44 -19
- package/dist/src/adapters/mcp.js +1 -1
- package/dist/src/adapters/pi.d.ts +3 -1
- package/dist/src/adapters/pi.js +4 -1
- package/dist/src/bin-wrapper-internal.js +27 -1
- package/dist/src/core/define-tool.d.ts +18 -3
- package/dist/src/core/execute-tool.d.ts +16 -3
- package/dist/src/core/execute-tool.js +29 -15
- package/dist/src/core/result-guards.d.ts +0 -19
- package/dist/src/core/result-guards.js +11 -3
- package/dist/src/index.d.ts +1 -1
- package/examples/README.md +22 -10
- package/llms.txt +6 -5
- package/package.json +4 -4
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
|
|
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
|
|
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 (
|
|
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.
|
|
145
|
+
for (const { field, message } of result.details.validationErrors) {
|
|
136
146
|
// ...
|
|
137
147
|
}
|
|
138
|
-
} else
|
|
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,
|
|
154
|
-
`kind
|
|
155
|
-
`kind`. In the deprecated `"throw"` mode, the
|
|
156
|
-
`(err as PortableToolExecutionError).details.kind`
|
|
157
|
-
`"domain"`). The guards operate on a `PortableToolResult`;
|
|
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
|
|
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).
|
|
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
|
|
301
|
-
- Treat `docs/releasing.md` as the
|
|
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
|
|
package/dist/src/adapters/mcp.js
CHANGED
|
@@ -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;
|
package/dist/src/adapters/pi.js
CHANGED
|
@@ -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) =>
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
155
|
-
if (
|
|
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
|
-
|
|
163
|
+
const next = getArrayEntry(obj.items, idx);
|
|
164
|
+
if (next === undefined)
|
|
163
165
|
return undefined;
|
|
164
|
-
current =
|
|
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
|
|
257
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
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 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";
|
package/examples/README.md
CHANGED
|
@@ -145,7 +145,8 @@ In `package.json`:
|
|
|
145
145
|
pi behavior:
|
|
146
146
|
|
|
147
147
|
- Valid portable results become pi tool results.
|
|
148
|
-
-
|
|
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.
|
|
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.
|
|
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).
|
|
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
|
|
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`,
|
|
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
|
|
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
|
|
76
|
-
- MCP invalid arguments and portable `isError: true` results return `CallToolResult` with `isError: true`.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
76
|
+
"@biomejs/biome": "2.4.16",
|
|
77
77
|
"@total-typescript/shoehorn": "^0.1.2",
|
|
78
|
-
"@types/node": "^
|
|
79
|
-
"typescript": "^
|
|
78
|
+
"@types/node": "^25.9.1",
|
|
79
|
+
"typescript": "^6.0.3"
|
|
80
80
|
},
|
|
81
81
|
"license": "MIT"
|
|
82
82
|
}
|