@feniix/bridgekit 0.2.2 → 0.4.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/README.md CHANGED
@@ -58,6 +58,10 @@ export const echoTool = definePortableTool({
58
58
  };
59
59
  },
60
60
  });
61
+
62
+ export function createTools() {
63
+ return [echoTool];
64
+ }
61
65
  ```
62
66
 
63
67
  Tool definition best practices:
@@ -70,37 +74,60 @@ Tool definition best practices:
70
74
  - Respect `ctx.signal` in long-running tools.
71
75
  - Use `ctx.progress?.(...)` for incremental updates.
72
76
  - Keep modules import-passive; do not register tools or start servers at import time.
77
+ - For stateful tools, export a `createTools()` factory instead of a module-level singleton so each host runtime gets isolated state.
78
+ - TypeBox validation happens before `execute`; use a permissive schema plus domain validation if you need custom guidance for structurally invalid input.
73
79
 
74
80
  ## pi adapter
75
81
 
76
82
  ```ts
77
83
  import { registerPiTools } from "@feniix/bridgekit/pi";
78
- import { echoTool } from "./tools.js";
84
+ import { createTools } from "./tools.js";
79
85
 
80
86
  export default function extension(pi: Parameters<typeof registerPiTools>[0]) {
81
- registerPiTools(pi, [echoTool]);
87
+ registerPiTools(pi, createTools());
82
88
  }
83
89
  ```
84
90
 
85
- Portable validation failures reject with `PortableToolExecutionError` in pi so the host sees a native tool failure. Progress updates from `ctx.progress?.(...)` map to pi tool updates.
91
+ Portable validation failures and portable results with `isError: true` reject with `PortableToolExecutionError` in pi so the host sees a native tool failure. The error message is the portable `text`, and `.details` is populated from `structuredContent` or `details`. Progress updates from `ctx.progress?.(...)` map to pi tool updates.
86
92
 
87
93
  ## MCP adapter
88
94
 
89
95
  ```ts
90
- import { runMcpStdioServer } from "@feniix/bridgekit/mcp";
91
- import { echoTool } from "./tools.js";
92
-
93
- await runMcpStdioServer({
94
- name: "my-tools",
95
- version: "0.1.0",
96
- tools: [echoTool],
97
- instructions: "Use these tools when text needs processing.",
98
- });
96
+ import { realpathSync } from "node:fs";
97
+ import { resolve } from "node:path";
98
+ import { fileURLToPath } from "node:url";
99
+ import { type CreateMcpServerOptions, runMcpStdioServer } from "@feniix/bridgekit/mcp";
100
+ import { createTools } from "./tools.js";
101
+
102
+ export function createMcpServerOptions(): CreateMcpServerOptions {
103
+ return {
104
+ name: "my-tools",
105
+ version: "0.1.0",
106
+ tools: createTools(),
107
+ instructions: "Use these tools when text needs processing.",
108
+ };
109
+ }
110
+
111
+ export async function runServer(): Promise<void> {
112
+ await runMcpStdioServer(createMcpServerOptions());
113
+ }
114
+
115
+ function realpathIfPossible(path: string): string {
116
+ try {
117
+ return realpathSync(path);
118
+ } catch {
119
+ return path;
120
+ }
121
+ }
122
+
123
+ if (process.argv[1] && realpathIfPossible(resolve(process.argv[1])) === realpathIfPossible(fileURLToPath(import.meta.url))) {
124
+ await runServer();
125
+ }
99
126
  ```
100
127
 
101
128
  The MCP adapter uses low-level `tools/list` and `tools/call` handlers so TypeBox schemas are exposed as JSON Schema directly. It intentionally does not expose a high-level `registerMcpTools` helper.
102
129
 
103
- MCP invalid input and portable `isError: true` results return `CallToolResult` with `isError: true`.
130
+ MCP invalid input and portable `isError: true` results return `CallToolResult` with `isError: true`. Exporting a server-options factory keeps MCP entrypoints import-passive and easy to test without starting stdio.
104
131
 
105
132
  ## Custom host typing
106
133
 
@@ -139,11 +166,14 @@ Use `PortableToolHost<CustomHost>` for values that may be either a built-in host
139
166
 
140
167
  ## Package and release checklist
141
168
 
142
- - Publish compiled JavaScript plus generated `.d.ts` declarations, not source as runtime code.
169
+ - Publish compiled JavaScript plus generated `.d.ts` declarations for runtime entrypoints.
143
170
  - Keep `exports`, `main`, and `types` aligned with built files.
144
171
  - Keep runtime imports in `dependencies`.
145
172
  - Avoid `workspace:` or `file:` dependency ranges in publishable packages.
146
173
  - Avoid dangling `sourceMappingURL` comments: publish maps and useful sources together, or disable source maps for package builds.
174
+ - For MCP stdio bins, ensure the emitted JavaScript starts with a Node shebang, has executable mode (`chmod +x` or equivalent), and is included by `npm pack --dry-run --json`.
175
+ - If a package keeps a source-loaded host entrypoint (for example a pi extension source file), use a package-local MCP build for the npm-launched bin and narrow that build to the MCP entrypoint plus shared host-neutral modules.
176
+ - Declare a compatible Node engine (`>=22.19.0`) in downstream packages that expose BridgeKit-powered MCP bins.
147
177
  - Run `npm run check`, `npm run test`, `npm run pack:dry-run`, and `npm run package-smoke` before publishing.
148
178
  - Treat `docs/releasing.md` as the future release handoff; this repository is not configured for automated publish yet.
149
179
 
@@ -15,6 +15,18 @@ my-tools-package/
15
15
 
16
16
  Keep `src/tools.ts` free of pi and MCP imports. Host-specific imports belong only in adapter entrypoints.
17
17
 
18
+ Some packages have a host that intentionally loads TypeScript source directly, while MCP clients launched from npm need compiled JavaScript. For that mixed mode, keep the same boundaries but use a package-local MCP build:
19
+
20
+ ```text
21
+ my-pi-extension-package/
22
+ package.json
23
+ extensions/
24
+ tools.ts # host-neutral portable tools
25
+ index.ts # source-loaded pi adapter wiring
26
+ mcp-server.ts # compiled MCP stdio server wiring
27
+ tsconfig.mcp.json # emits mcp-server.ts + shared modules to dist/
28
+ ```
29
+
18
30
  ---
19
31
 
20
32
  ## 1. Define shared portable tools
@@ -54,14 +66,17 @@ export const reverseTextTool = definePortableTool({
54
66
  },
55
67
  });
56
68
 
57
- export const tools = [reverseTextTool];
69
+ export function createTools() {
70
+ return [reverseTextTool];
71
+ }
58
72
  ```
59
73
 
60
74
  Best practices shown here:
61
75
 
62
- - The schema is the single source of truth for argument validation.
76
+ - The schema is the single source of truth for structural argument validation.
63
77
  - The handler returns portable `{ text, structuredContent }` data.
64
78
  - The handler observes `ctx.signal` without importing a host SDK.
79
+ - The `createTools()` factory gives stateful tools a fresh runtime per host instance; stateless packages may still return the same definitions.
65
80
  - The file has no import-time registration or server startup.
66
81
 
67
82
  ---
@@ -71,10 +86,10 @@ Best practices shown here:
71
86
  ```ts
72
87
  // src/pi-extension.ts
73
88
  import { registerPiTools } from "@feniix/bridgekit/pi";
74
- import { tools } from "./tools.js";
89
+ import { createTools } from "./tools.js";
75
90
 
76
91
  export default function extension(pi: Parameters<typeof registerPiTools>[0]) {
77
- registerPiTools(pi, tools);
92
+ registerPiTools(pi, createTools());
78
93
  }
79
94
  ```
80
95
 
@@ -92,7 +107,7 @@ In `package.json`:
92
107
  pi behavior:
93
108
 
94
109
  - Valid portable results become pi tool results.
95
- - Portable results with `isError: true` reject with `PortableToolExecutionError`.
110
+ - Portable results with `isError: true` reject with `PortableToolExecutionError`; tests should assert a rejected execution plus error `.details`.
96
111
  - Progress updates from `ctx.progress?.(...)` map to pi updates.
97
112
 
98
113
  ---
@@ -102,15 +117,42 @@ pi behavior:
102
117
  ```ts
103
118
  #!/usr/bin/env node
104
119
  // src/mcp-server.ts
105
- import { runMcpStdioServer } from "@feniix/bridgekit/mcp";
106
- import { tools } from "./tools.js";
107
-
108
- await runMcpStdioServer({
109
- name: "my-tools",
110
- version: "0.1.0",
111
- tools,
112
- instructions: "Use these tools when text needs lightweight transformation.",
113
- });
120
+ import { realpathSync } from "node:fs";
121
+ import { resolve } from "node:path";
122
+ import { fileURLToPath } from "node:url";
123
+ import { type CreateMcpServerOptions, runMcpStdioServer } from "@feniix/bridgekit/mcp";
124
+ import { createTools } from "./tools.js";
125
+
126
+ export function createMcpServerOptions(): CreateMcpServerOptions {
127
+ return {
128
+ name: "my-tools",
129
+ version: "0.1.0",
130
+ tools: createTools(),
131
+ instructions: "Use these tools when text needs lightweight transformation.",
132
+ };
133
+ }
134
+
135
+ export async function runServer(): Promise<void> {
136
+ await runMcpStdioServer(createMcpServerOptions());
137
+ }
138
+
139
+ function realpathIfPossible(path: string): string {
140
+ try {
141
+ return realpathSync(path);
142
+ } catch {
143
+ return path;
144
+ }
145
+ }
146
+
147
+ function isMainModule(): boolean {
148
+ const entrypoint = process.argv[1];
149
+ if (!entrypoint) return false;
150
+ return realpathIfPossible(resolve(entrypoint)) === realpathIfPossible(fileURLToPath(import.meta.url));
151
+ }
152
+
153
+ if (isMainModule()) {
154
+ await runServer();
155
+ }
114
156
  ```
115
157
 
116
158
  In `package.json`:
@@ -120,6 +162,43 @@ In `package.json`:
120
162
  "type": "module",
121
163
  "bin": {
122
164
  "my-tools-mcp": "./dist/src/mcp-server.js"
165
+ },
166
+ "scripts": {
167
+ "build": "tsc -b && chmod +x dist/src/mcp-server.js",
168
+ "prepack": "npm run build"
169
+ },
170
+ "engines": {
171
+ "node": ">=22.19.0"
172
+ },
173
+ "dependencies": {
174
+ "@feniix/bridgekit": "^0.2.2",
175
+ "typebox": "^1.1.31"
176
+ }
177
+ }
178
+ ```
179
+
180
+ For mixed source-loaded pi + compiled MCP packages, keep the pi source entrypoint and point only the npm `bin` at emitted JavaScript:
181
+
182
+ ```json
183
+ {
184
+ "type": "module",
185
+ "pi": {
186
+ "extensions": ["./extensions/index.ts"]
187
+ },
188
+ "bin": {
189
+ "my-tools-mcp": "./dist/extensions/mcp-server.js"
190
+ },
191
+ "files": ["extensions/", "dist/", "README.md", "LICENSE"],
192
+ "scripts": {
193
+ "build:mcp": "tsc --project tsconfig.mcp.json && chmod +x dist/extensions/mcp-server.js",
194
+ "prepack": "npm run build:mcp"
195
+ },
196
+ "engines": {
197
+ "node": ">=22.19.0"
198
+ },
199
+ "dependencies": {
200
+ "@feniix/bridgekit": "^0.2.2",
201
+ "typebox": "^1.1.31"
123
202
  }
124
203
  }
125
204
  ```
@@ -130,6 +209,7 @@ MCP behavior:
130
209
  - `tools/call` validates arguments before invoking handlers.
131
210
  - Invalid arguments and portable `isError: true` results return MCP tool results with `isError: true`.
132
211
  - Unexpected thrown errors become MCP tool errors with text content.
212
+ - The module stays import-passive and testable: tests can import `createMcpServerOptions()` without starting stdio.
133
213
 
134
214
  ---
135
215
 
@@ -183,11 +263,14 @@ Use `PortableToolHost<CustomHost>` for values that can be either a built-in host
183
263
 
184
264
  For publishable tool packages:
185
265
 
186
- - Compile to JavaScript and declarations before packing.
266
+ - Compile runtime entrypoints to JavaScript and declarations before packing.
187
267
  - Use `exports` to expose only supported entrypoints.
188
268
  - Keep runtime imports in `dependencies`, not only dev dependencies.
269
+ - Declare Node `>=22.19.0` when publishing BridgeKit-powered MCP bins.
189
270
  - Avoid `workspace:` or `file:` ranges in publishable package dependencies.
190
271
  - Avoid dangling `sourceMappingURL` comments: either publish maps and useful sources, or disable source maps for package builds.
272
+ - Ensure MCP bin output starts with a shebang, is executable (`chmod +x` or equivalent), and appears in `npm pack --dry-run --json` with executable mode.
273
+ - 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.
191
274
  - Add a packed-install smoke test that installs tarballs into a temporary project.
192
275
  - For BridgeKit itself, run `npm run check`, `npm run test`, `npm run pack:dry-run`, and `npm run package-smoke` before release.
193
276
  - Keep imports side-effect free; registration and server startup should happen only in explicit entrypoints.
package/llms.txt CHANGED
@@ -41,6 +41,10 @@ export const echoTool = definePortableTool({
41
41
  };
42
42
  },
43
43
  });
44
+
45
+ export function createTools() {
46
+ return [echoTool];
47
+ }
44
48
  ```
45
49
 
46
50
  ## Best practices
@@ -53,12 +57,15 @@ export const echoTool = definePortableTool({
53
57
  - Respect `ctx.signal` in long-running tools.
54
58
  - Use `ctx.progress?.(...)` for incremental progress updates when the host supports them.
55
59
  - Wire hosts explicitly: pi registration and MCP server startup are separate adapter calls.
56
- - Keep modules import-passive. Do not register tools or start servers at import time.
60
+ - Keep modules import-passive. Do not register tools, start servers, read package metadata, read env vars, or touch files at import time.
61
+ - For stateful tools, export a `createTools()` factory instead of a module-level `tools` singleton so each pi extension instance or MCP stdio process gets isolated state.
62
+ - 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.
63
+ - 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.
57
64
  - Keep package runtime Node >=22.19.0 and ESM-only.
58
65
 
59
66
  ## Host behavior
60
67
 
61
- - pi invalid arguments and portable `isError: true` results throw `PortableToolExecutionError`, because pi expects native tool failures.
68
+ - pi invalid arguments and portable `isError: true` results throw `PortableToolExecutionError`, because pi expects native tool failures. Tests should assert rejected execution plus `.details`, not a returned `{ isError: true }` object.
62
69
  - MCP invalid arguments and portable `isError: true` results return `CallToolResult` with `isError: true`.
63
70
  - 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.
64
71
 
@@ -79,10 +86,22 @@ const customTool = definePortableTool<typeof params, "custom-host">({
79
86
 
80
87
  Use `PortableToolHost<"custom-host">` when a value may be either a built-in host or that extension.
81
88
 
89
+ ## Mixed source-loaded hosts and compiled MCP bins
90
+
91
+ 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:
92
+
93
+ - Keep the host source entrypoint if that is the package convention, e.g. `pi.extensions: ["./extensions/index.ts"]`.
94
+ - Add a package-local MCP build whose `bin` points at emitted JavaScript, e.g. `./dist/extensions/mcp-server.js`.
95
+ - Narrow the MCP build to the MCP entrypoint and shared host-neutral modules; avoid compiling pi adapter entrypoints into the standalone MCP path.
96
+ - Put runtime imports used by the compiled MCP bin in `dependencies`, not only peers or dev dependencies.
97
+ - Declare BridgeKit's Node engine requirement in the downstream package (`>=22.19.0`).
98
+ - Ensure the built MCP bin is executable (`chmod +x` or equivalent) and verify the mode with `npm pack --dry-run --json`.
99
+
82
100
  ## Anti-patterns
83
101
 
84
102
  - Do not create a separate pi implementation and MCP implementation for the same logic.
85
103
  - Do not make tool files read package metadata, environment variables, files, or network resources at import time.
104
+ - Do not start MCP stdio at module top level in files that tests need to import.
86
105
  - Do not expose unsupported high-level MCP helpers such as `registerMcpTools` unless tests prove compatibility with the installed MCP SDK.
87
106
  - Do not return host-specific response shapes from portable tool handlers.
88
107
  - Do not use `workspace:` or `file:` dependency ranges for packages intended to install from npm.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feniix/bridgekit",
3
- "version": "0.2.2",
3
+ "version": "0.4.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",