@feniix/bridgekit 0.2.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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/src/adapters/mcp-signal.d.ts +1 -0
- package/dist/src/adapters/mcp-signal.js +7 -0
- package/dist/src/adapters/mcp.d.ts +11 -0
- package/dist/src/adapters/mcp.js +55 -0
- package/dist/src/adapters/pi.d.ts +31 -0
- package/dist/src/adapters/pi.js +44 -0
- package/dist/src/core/define-tool.d.ts +26 -0
- package/dist/src/core/define-tool.js +3 -0
- package/dist/src/core/execute-tool.d.ts +15 -0
- package/dist/src/core/execute-tool.js +29 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/mcp.d.ts +1 -0
- package/dist/src/mcp.js +1 -0
- package/dist/src/pi.d.ts +1 -0
- package/dist/src/pi.js +1 -0
- package/examples/README.md +193 -0
- package/llms.txt +88 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Otaegui
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# BridgeKit
|
|
2
|
+
|
|
3
|
+
BridgeKit provides reusable TypeBox-backed tool definitions and adapters for exposing one tool implementation through pi, MCP, and other hosts.
|
|
4
|
+
|
|
5
|
+
## Runtime support
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
## For coding agents
|
|
10
|
+
|
|
11
|
+
Read these files in order:
|
|
12
|
+
|
|
13
|
+
1. `README.md` — public API, contracts, and best practices.
|
|
14
|
+
2. `llms.txt` — compact agent-facing usage rules and anti-patterns.
|
|
15
|
+
3. `examples/README.md` — copyable layouts for shared tools, pi extensions, MCP stdio servers, and custom hosts.
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
## Entrypoints
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
definePortableTool,
|
|
23
|
+
executePortableTool,
|
|
24
|
+
type PortableTool,
|
|
25
|
+
type PortableToolBuiltInHost,
|
|
26
|
+
type PortableToolContext,
|
|
27
|
+
type PortableToolHost,
|
|
28
|
+
type PortableToolResult,
|
|
29
|
+
type PortableValidationError,
|
|
30
|
+
} from "@feniix/bridgekit";
|
|
31
|
+
import { registerPiTools } from "@feniix/bridgekit/pi";
|
|
32
|
+
import { createMcpServer, runMcpStdioServer } from "@feniix/bridgekit/mcp";
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- Root entrypoint: host-neutral tool definitions, validation, and execution helpers.
|
|
36
|
+
- `/pi`: pi adapter only.
|
|
37
|
+
- `/mcp`: MCP server adapter only.
|
|
38
|
+
|
|
39
|
+
Do not deep-import from `dist/` or `src/` in consuming packages.
|
|
40
|
+
|
|
41
|
+
## Core tools
|
|
42
|
+
|
|
43
|
+
Define tools once in host-neutral files:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Type } from "typebox";
|
|
47
|
+
import { definePortableTool } from "@feniix/bridgekit";
|
|
48
|
+
|
|
49
|
+
export const echoTool = definePortableTool({
|
|
50
|
+
name: "echo",
|
|
51
|
+
title: "Echo",
|
|
52
|
+
description: "Echo text.",
|
|
53
|
+
parameters: Type.Object({ text: Type.String() }),
|
|
54
|
+
execute(args, ctx) {
|
|
55
|
+
return {
|
|
56
|
+
text: args.text,
|
|
57
|
+
structuredContent: { text: args.text, host: ctx.host },
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Tool definition best practices:
|
|
64
|
+
|
|
65
|
+
- Keep tool files host-neutral: no pi imports, no MCP SDK imports.
|
|
66
|
+
- Use TypeBox `Type.Object(...)` schemas so MCP can expose input schemas directly.
|
|
67
|
+
- Return `text` for model-visible output and `structuredContent` for machine-readable data.
|
|
68
|
+
- Use `isError: true` for expected/domain failures that should be represented as tool output.
|
|
69
|
+
- Throw only for unexpected programmer, adapter, or runtime failures.
|
|
70
|
+
- Respect `ctx.signal` in long-running tools.
|
|
71
|
+
- Use `ctx.progress?.(...)` for incremental updates.
|
|
72
|
+
- Keep modules import-passive; do not register tools or start servers at import time.
|
|
73
|
+
|
|
74
|
+
## pi adapter
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { registerPiTools } from "@feniix/bridgekit/pi";
|
|
78
|
+
import { echoTool } from "./tools.js";
|
|
79
|
+
|
|
80
|
+
export default function extension(pi: Parameters<typeof registerPiTools>[0]) {
|
|
81
|
+
registerPiTools(pi, [echoTool]);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
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.
|
|
86
|
+
|
|
87
|
+
## MCP adapter
|
|
88
|
+
|
|
89
|
+
```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
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
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
|
+
|
|
103
|
+
MCP invalid input and portable `isError: true` results return `CallToolResult` with `isError: true`.
|
|
104
|
+
|
|
105
|
+
## Custom host typing
|
|
106
|
+
|
|
107
|
+
Default portable tools accept the built-in host union:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
type BuiltIn = "pi" | "mcp" | "test";
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Custom adapters opt in explicitly:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { Type } from "typebox";
|
|
117
|
+
import { definePortableTool, type PortableToolHost } from "@feniix/bridgekit";
|
|
118
|
+
|
|
119
|
+
const params = Type.Object({ text: Type.String() });
|
|
120
|
+
|
|
121
|
+
type CustomHost = "custom-runtime";
|
|
122
|
+
|
|
123
|
+
export const customTool = definePortableTool<typeof params, CustomHost>({
|
|
124
|
+
name: "custom_echo",
|
|
125
|
+
title: "Custom Echo",
|
|
126
|
+
description: "Echoes text in a custom runtime.",
|
|
127
|
+
parameters: params,
|
|
128
|
+
execute(args, ctx) {
|
|
129
|
+
const host: CustomHost = ctx.host;
|
|
130
|
+
return { text: `${host}: ${args.text}` };
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const hostValue: PortableToolHost<CustomHost> = "custom-runtime";
|
|
135
|
+
void hostValue;
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
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.
|
|
139
|
+
|
|
140
|
+
## Package and release checklist
|
|
141
|
+
|
|
142
|
+
- Publish compiled JavaScript plus generated `.d.ts` declarations, not source as runtime code.
|
|
143
|
+
- Keep `exports`, `main`, and `types` aligned with built files.
|
|
144
|
+
- Keep runtime imports in `dependencies`.
|
|
145
|
+
- Avoid `workspace:` or `file:` dependency ranges in publishable packages.
|
|
146
|
+
- Avoid dangling `sourceMappingURL` comments: publish maps and useful sources together, or disable source maps for package builds.
|
|
147
|
+
- Run `npm run check`, `npm run test`, `npm run pack:dry-run`, and `npm run package-smoke` before publishing.
|
|
148
|
+
- Treat `docs/releasing.md` as the future release handoff; this repository is not configured for automated publish yet.
|
|
149
|
+
|
|
150
|
+
See `examples/README.md` for complete copyable examples.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function signalFromExtra(extra: unknown): AbortSignal | undefined;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import type { TObject } from "typebox";
|
|
3
|
+
import type { PortableTool } from "../core/define-tool.js";
|
|
4
|
+
export interface CreateMcpServerOptions {
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
tools: readonly PortableTool<TObject>[];
|
|
8
|
+
instructions?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function createMcpServer(options: CreateMcpServerOptions): Server;
|
|
11
|
+
export declare function runMcpStdioServer(options: CreateMcpServerOptions): Promise<void>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { executePortableTool } from "../core/execute-tool.js";
|
|
5
|
+
import { signalFromExtra } from "./mcp-signal.js";
|
|
6
|
+
function toMcpResult(result) {
|
|
7
|
+
return {
|
|
8
|
+
content: [{ type: "text", text: result.text }],
|
|
9
|
+
structuredContent: result.structuredContent ?? result.details,
|
|
10
|
+
isError: result.isError ?? false,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function createMcpServer(options) {
|
|
14
|
+
const byName = new Map(options.tools.map((tool) => [tool.name, tool]));
|
|
15
|
+
const server = new Server({ name: options.name, version: options.version }, {
|
|
16
|
+
capabilities: { tools: { listChanged: false } },
|
|
17
|
+
instructions: options.instructions,
|
|
18
|
+
});
|
|
19
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
20
|
+
tools: options.tools.map((tool) => ({
|
|
21
|
+
name: tool.name,
|
|
22
|
+
title: tool.title,
|
|
23
|
+
description: tool.description,
|
|
24
|
+
inputSchema: tool.parameters,
|
|
25
|
+
})),
|
|
26
|
+
}));
|
|
27
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
28
|
+
const tool = byName.get(request.params.name);
|
|
29
|
+
if (!tool) {
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
|
|
32
|
+
isError: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const result = await executePortableTool(tool, request.params.arguments ?? {}, {
|
|
37
|
+
host: "mcp",
|
|
38
|
+
signal: signalFromExtra(extra),
|
|
39
|
+
});
|
|
40
|
+
return toMcpResult(result);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text", text: message }],
|
|
46
|
+
isError: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return server;
|
|
51
|
+
}
|
|
52
|
+
export async function runMcpStdioServer(options) {
|
|
53
|
+
const server = createMcpServer(options);
|
|
54
|
+
await server.connect(new StdioServerTransport());
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { TSchema } from "typebox";
|
|
2
|
+
import type { PortableTool, PortableToolResult } from "../core/define-tool.js";
|
|
3
|
+
type PiContent = {
|
|
4
|
+
type: "text";
|
|
5
|
+
text: string;
|
|
6
|
+
};
|
|
7
|
+
type PiToolUpdate = {
|
|
8
|
+
content: PiContent[];
|
|
9
|
+
details: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
type PiToolResult = {
|
|
12
|
+
content: PiContent[];
|
|
13
|
+
details: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
type PiToolDefinition = {
|
|
16
|
+
name: string;
|
|
17
|
+
label: string;
|
|
18
|
+
description: string;
|
|
19
|
+
parameters: TSchema;
|
|
20
|
+
execute(toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: (update: PiToolUpdate) => void, ctx?: unknown): Promise<PiToolResult>;
|
|
21
|
+
};
|
|
22
|
+
export type PiToolRegistration = {
|
|
23
|
+
registerTool(tool: PiToolDefinition): unknown;
|
|
24
|
+
};
|
|
25
|
+
export declare class PortableToolExecutionError extends Error {
|
|
26
|
+
readonly details: Record<string, unknown>;
|
|
27
|
+
constructor(result: PortableToolResult);
|
|
28
|
+
}
|
|
29
|
+
export declare function isPortableToolExecutionError(error: unknown): error is PortableToolExecutionError;
|
|
30
|
+
export declare function registerPiTools(pi: PiToolRegistration, tools: readonly PortableTool<TSchema>[]): void;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { executePortableTool } from "../core/execute-tool.js";
|
|
2
|
+
function toPiDetails(result) {
|
|
3
|
+
return result.structuredContent ?? result.details ?? {};
|
|
4
|
+
}
|
|
5
|
+
export class PortableToolExecutionError extends Error {
|
|
6
|
+
details;
|
|
7
|
+
constructor(result) {
|
|
8
|
+
super(result.text);
|
|
9
|
+
this.name = "PortableToolExecutionError";
|
|
10
|
+
this.details = toPiDetails(result);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function isPortableToolExecutionError(error) {
|
|
14
|
+
return error instanceof PortableToolExecutionError;
|
|
15
|
+
}
|
|
16
|
+
export function registerPiTools(pi, tools) {
|
|
17
|
+
for (const tool of tools) {
|
|
18
|
+
pi.registerTool({
|
|
19
|
+
name: tool.name,
|
|
20
|
+
label: tool.title,
|
|
21
|
+
description: tool.description,
|
|
22
|
+
parameters: tool.parameters,
|
|
23
|
+
async execute(_toolCallId, params, signal, onUpdate, _ctx) {
|
|
24
|
+
const result = await executePortableTool(tool, params, {
|
|
25
|
+
host: "pi",
|
|
26
|
+
signal,
|
|
27
|
+
progress(update) {
|
|
28
|
+
onUpdate?.({
|
|
29
|
+
content: [{ type: "text", text: update.text }],
|
|
30
|
+
details: toPiDetails(update),
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (result.isError) {
|
|
35
|
+
throw new PortableToolExecutionError(result);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: result.text }],
|
|
39
|
+
details: toPiDetails(result),
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Static, TSchema } from "typebox";
|
|
2
|
+
export interface PortableToolResult {
|
|
3
|
+
/** Plain text sent back to the model in every host. */
|
|
4
|
+
text: string;
|
|
5
|
+
/** Structured data for hosts that support it. Preferred by both pi and MCP adapters. */
|
|
6
|
+
structuredContent?: Record<string, unknown>;
|
|
7
|
+
/** Legacy/adapter debug details used only when structuredContent is absent. */
|
|
8
|
+
details?: Record<string, unknown>;
|
|
9
|
+
/** Tool-level error flag. Throw for unexpected adapter/runtime failures. */
|
|
10
|
+
isError?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type PortableToolBuiltInHost = "pi" | "mcp" | "test";
|
|
13
|
+
export type PortableToolHost<TExtension extends string = never> = PortableToolBuiltInHost | TExtension;
|
|
14
|
+
export interface PortableToolContext<THost extends string = PortableToolBuiltInHost> {
|
|
15
|
+
host: THost;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
progress?: (update: PortableToolResult) => void;
|
|
18
|
+
}
|
|
19
|
+
export interface PortableTool<TParams extends TSchema = TSchema, THost extends string = PortableToolBuiltInHost> {
|
|
20
|
+
name: string;
|
|
21
|
+
title: string;
|
|
22
|
+
description: string;
|
|
23
|
+
parameters: TParams;
|
|
24
|
+
execute: (args: Static<TParams>, ctx: PortableToolContext<THost>) => PortableToolResult | Promise<PortableToolResult>;
|
|
25
|
+
}
|
|
26
|
+
export declare function definePortableTool<TParams extends TSchema, THost extends string = PortableToolBuiltInHost>(tool: PortableTool<TParams, THost>): PortableTool<TParams, THost>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TSchema } from "typebox";
|
|
2
|
+
import type { PortableTool, PortableToolBuiltInHost, PortableToolContext, PortableToolResult } from "./define-tool.js";
|
|
3
|
+
type NoInferPortable<T> = [T][T extends unknown ? 0 : never];
|
|
4
|
+
export interface PortableValidationError {
|
|
5
|
+
path: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function validatePortableToolArgs<THost extends string = PortableToolBuiltInHost>(tool: PortableTool<TSchema, THost>, args: unknown): {
|
|
9
|
+
ok: true;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
errors: PortableValidationError[];
|
|
13
|
+
};
|
|
14
|
+
export declare function executePortableTool<THost extends string = PortableToolBuiltInHost>(tool: PortableTool<TSchema, THost>, args: unknown, ctx: PortableToolContext<NoInferPortable<THost>>): Promise<PortableToolResult>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Check, Errors } from "typebox/value";
|
|
2
|
+
export function validatePortableToolArgs(tool, args) {
|
|
3
|
+
if (Check(tool.parameters, args)) {
|
|
4
|
+
return { ok: true };
|
|
5
|
+
}
|
|
6
|
+
return {
|
|
7
|
+
ok: false,
|
|
8
|
+
errors: [...Errors(tool.parameters, args)].map((error) => ({
|
|
9
|
+
path: error.instancePath || "/",
|
|
10
|
+
message: error.message,
|
|
11
|
+
})),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export async function executePortableTool(tool, args, ctx) {
|
|
15
|
+
const validation = validatePortableToolArgs(tool, args);
|
|
16
|
+
if (!validation.ok) {
|
|
17
|
+
return {
|
|
18
|
+
text: `Invalid arguments for ${tool.name}: ${validation.errors
|
|
19
|
+
.map((error) => `${error.path} ${error.message}`)
|
|
20
|
+
.join("; ")}`,
|
|
21
|
+
structuredContent: {
|
|
22
|
+
tool: tool.name,
|
|
23
|
+
validationErrors: validation.errors,
|
|
24
|
+
},
|
|
25
|
+
isError: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return tool.execute(args, ctx);
|
|
29
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { definePortableTool, type PortableTool, type PortableToolBuiltInHost, type PortableToolContext, type PortableToolHost, type PortableToolResult, } from "./core/define-tool.js";
|
|
2
|
+
export { executePortableTool, type PortableValidationError, validatePortableToolArgs, } from "./core/execute-tool.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type CreateMcpServerOptions, createMcpServer, runMcpStdioServer, } from "./adapters/mcp.js";
|
package/dist/src/mcp.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createMcpServer, runMcpStdioServer, } from "./adapters/mcp.js";
|
package/dist/src/pi.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isPortableToolExecutionError, type PiToolRegistration, PortableToolExecutionError, registerPiTools, } from "./adapters/pi.js";
|
package/dist/src/pi.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isPortableToolExecutionError, PortableToolExecutionError, registerPiTools, } from "./adapters/pi.js";
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# BridgeKit examples
|
|
2
|
+
|
|
3
|
+
These examples show the recommended layout for defining portable tools once and wiring them into pi and MCP hosts.
|
|
4
|
+
|
|
5
|
+
## Recommended package layout
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
my-tools-package/
|
|
9
|
+
package.json
|
|
10
|
+
src/
|
|
11
|
+
tools.ts # host-neutral portable tools
|
|
12
|
+
pi-extension.ts # pi adapter wiring
|
|
13
|
+
mcp-server.ts # MCP stdio server wiring
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Keep `src/tools.ts` free of pi and MCP imports. Host-specific imports belong only in adapter entrypoints.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. Define shared portable tools
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// src/tools.ts
|
|
24
|
+
import { Type } from "typebox";
|
|
25
|
+
import { definePortableTool } from "@feniix/bridgekit";
|
|
26
|
+
|
|
27
|
+
const reverseParams = Type.Object({
|
|
28
|
+
text: Type.String({ description: "Text to reverse." }),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const reverseTextTool = definePortableTool({
|
|
32
|
+
name: "reverse_text",
|
|
33
|
+
title: "Reverse Text",
|
|
34
|
+
description: "Reverse the supplied text.",
|
|
35
|
+
parameters: reverseParams,
|
|
36
|
+
execute(args, ctx) {
|
|
37
|
+
if (ctx.signal?.aborted) {
|
|
38
|
+
return {
|
|
39
|
+
text: "Reverse text was cancelled.",
|
|
40
|
+
structuredContent: { cancelled: true },
|
|
41
|
+
isError: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const output = [...args.text].reverse().join("");
|
|
46
|
+
return {
|
|
47
|
+
text: output,
|
|
48
|
+
structuredContent: {
|
|
49
|
+
input: args.text,
|
|
50
|
+
output,
|
|
51
|
+
host: ctx.host,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const tools = [reverseTextTool];
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Best practices shown here:
|
|
61
|
+
|
|
62
|
+
- The schema is the single source of truth for argument validation.
|
|
63
|
+
- The handler returns portable `{ text, structuredContent }` data.
|
|
64
|
+
- The handler observes `ctx.signal` without importing a host SDK.
|
|
65
|
+
- The file has no import-time registration or server startup.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 2. Register tools in a pi extension
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// src/pi-extension.ts
|
|
73
|
+
import { registerPiTools } from "@feniix/bridgekit/pi";
|
|
74
|
+
import { tools } from "./tools.js";
|
|
75
|
+
|
|
76
|
+
export default function extension(pi: Parameters<typeof registerPiTools>[0]) {
|
|
77
|
+
registerPiTools(pi, tools);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
In `package.json`:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"type": "module",
|
|
86
|
+
"pi": {
|
|
87
|
+
"extensions": ["./dist/src/pi-extension.js"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
pi behavior:
|
|
93
|
+
|
|
94
|
+
- Valid portable results become pi tool results.
|
|
95
|
+
- Portable results with `isError: true` reject with `PortableToolExecutionError`.
|
|
96
|
+
- Progress updates from `ctx.progress?.(...)` map to pi updates.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 3. Serve the same tools over MCP stdio
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
#!/usr/bin/env node
|
|
104
|
+
// 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
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
In `package.json`:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"type": "module",
|
|
121
|
+
"bin": {
|
|
122
|
+
"my-tools-mcp": "./dist/src/mcp-server.js"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
MCP behavior:
|
|
128
|
+
|
|
129
|
+
- `tools/list` exposes TypeBox schemas directly as JSON Schema.
|
|
130
|
+
- `tools/call` validates arguments before invoking handlers.
|
|
131
|
+
- Invalid arguments and portable `isError: true` results return MCP tool results with `isError: true`.
|
|
132
|
+
- Unexpected thrown errors become MCP tool errors with text content.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 4. Use custom host typing for custom adapters
|
|
137
|
+
|
|
138
|
+
Default portable tools accept only built-in hosts: `"pi" | "mcp" | "test"`.
|
|
139
|
+
|
|
140
|
+
If you are writing a custom adapter, opt in explicitly so the handler can safely narrow `ctx.host`:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { Type } from "typebox";
|
|
144
|
+
import {
|
|
145
|
+
definePortableTool,
|
|
146
|
+
executePortableTool,
|
|
147
|
+
type PortableTool,
|
|
148
|
+
type PortableToolContext,
|
|
149
|
+
type PortableToolHost,
|
|
150
|
+
} from "@feniix/bridgekit";
|
|
151
|
+
|
|
152
|
+
const params = Type.Object({ text: Type.String() });
|
|
153
|
+
|
|
154
|
+
type CustomHost = "custom-runtime";
|
|
155
|
+
type CustomTool = PortableTool<typeof params, CustomHost>;
|
|
156
|
+
|
|
157
|
+
const customTool = definePortableTool<typeof params, CustomHost>({
|
|
158
|
+
name: "custom_echo",
|
|
159
|
+
title: "Custom Echo",
|
|
160
|
+
description: "Echoes text in a custom runtime.",
|
|
161
|
+
parameters: params,
|
|
162
|
+
execute(args, ctx) {
|
|
163
|
+
const host: CustomHost = ctx.host;
|
|
164
|
+
return { text: `${host}: ${args.text}` };
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
async function runCustomTool(tool: CustomTool, text: string) {
|
|
169
|
+
const ctx: PortableToolContext<CustomHost> = { host: "custom-runtime" };
|
|
170
|
+
return executePortableTool(tool, { text }, ctx);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const hostValue: PortableToolHost<CustomHost> = "custom-runtime";
|
|
174
|
+
void hostValue;
|
|
175
|
+
void customTool;
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
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.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 5. Package checklist
|
|
183
|
+
|
|
184
|
+
For publishable tool packages:
|
|
185
|
+
|
|
186
|
+
- Compile to JavaScript and declarations before packing.
|
|
187
|
+
- Use `exports` to expose only supported entrypoints.
|
|
188
|
+
- Keep runtime imports in `dependencies`, not only dev dependencies.
|
|
189
|
+
- Avoid `workspace:` or `file:` ranges in publishable package dependencies.
|
|
190
|
+
- Avoid dangling `sourceMappingURL` comments: either publish maps and useful sources, or disable source maps for package builds.
|
|
191
|
+
- Add a packed-install smoke test that installs tarballs into a temporary project.
|
|
192
|
+
- For BridgeKit itself, run `npm run check`, `npm run test`, `npm run pack:dry-run`, and `npm run package-smoke` before release.
|
|
193
|
+
- Keep imports side-effect free; registration and server startup should happen only in explicit entrypoints.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @feniix/bridgekit
|
|
2
|
+
|
|
3
|
+
Use this package when you need to define a tool once and expose it through both pi and MCP hosts.
|
|
4
|
+
|
|
5
|
+
## Read order for agents
|
|
6
|
+
|
|
7
|
+
1. `README.md` — public API, contracts, best practices, packaging notes.
|
|
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 custom hosts.
|
|
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
|
+
|
|
12
|
+
## Import map
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { definePortableTool, executePortableTool } from "@feniix/bridgekit";
|
|
16
|
+
import { registerPiTools } from "@feniix/bridgekit/pi";
|
|
17
|
+
import { createMcpServer, runMcpStdioServer } from "@feniix/bridgekit/mcp";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- Root entrypoint: host-neutral tool definitions, validation, and execution helpers.
|
|
21
|
+
- `/pi`: pi adapter only.
|
|
22
|
+
- `/mcp`: MCP server adapter only.
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
## Core pattern
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { Type } from "typebox";
|
|
30
|
+
import { definePortableTool } from "@feniix/bridgekit";
|
|
31
|
+
|
|
32
|
+
export const echoTool = definePortableTool({
|
|
33
|
+
name: "echo",
|
|
34
|
+
title: "Echo",
|
|
35
|
+
description: "Echo text.",
|
|
36
|
+
parameters: Type.Object({ text: Type.String() }),
|
|
37
|
+
execute(args, ctx) {
|
|
38
|
+
return {
|
|
39
|
+
text: args.text,
|
|
40
|
+
structuredContent: { text: args.text, host: ctx.host },
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Best practices
|
|
47
|
+
|
|
48
|
+
- Keep portable tools host-neutral. Do not import pi or MCP SDKs from files that define tool behavior.
|
|
49
|
+
- Use TypeBox `Type.Object(...)` schemas for MCP-compatible tools.
|
|
50
|
+
- Return `{ text, structuredContent }` for successful results.
|
|
51
|
+
- Use `isError: true` for domain/tool-level failures that should reach the model as tool output.
|
|
52
|
+
- Throw only for unexpected programmer, adapter, or runtime failures.
|
|
53
|
+
- Respect `ctx.signal` in long-running tools.
|
|
54
|
+
- Use `ctx.progress?.(...)` for incremental progress updates when the host supports them.
|
|
55
|
+
- 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.
|
|
57
|
+
- Keep package runtime Node >=22.19.0 and ESM-only.
|
|
58
|
+
|
|
59
|
+
## Host behavior
|
|
60
|
+
|
|
61
|
+
- pi invalid arguments and portable `isError: true` results throw `PortableToolExecutionError`, because pi expects native tool failures.
|
|
62
|
+
- MCP invalid arguments and portable `isError: true` results return `CallToolResult` with `isError: true`.
|
|
63
|
+
- 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
|
+
|
|
65
|
+
## Custom host typing
|
|
66
|
+
|
|
67
|
+
Default tools accept only built-in hosts: `"pi" | "mcp" | "test"`.
|
|
68
|
+
For a custom adapter, opt in explicitly:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const customTool = definePortableTool<typeof params, "custom-host">({
|
|
72
|
+
// ...
|
|
73
|
+
execute(args, ctx) {
|
|
74
|
+
const host: "custom-host" = ctx.host;
|
|
75
|
+
return { text: host };
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Use `PortableToolHost<"custom-host">` when a value may be either a built-in host or that extension.
|
|
81
|
+
|
|
82
|
+
## Anti-patterns
|
|
83
|
+
|
|
84
|
+
- Do not create a separate pi implementation and MCP implementation for the same logic.
|
|
85
|
+
- Do not make tool files read package metadata, environment variables, files, or network resources at import time.
|
|
86
|
+
- Do not expose unsupported high-level MCP helpers such as `registerMcpTools` unless tests prove compatibility with the installed MCP SDK.
|
|
87
|
+
- Do not return host-specific response shapes from portable tool handlers.
|
|
88
|
+
- Do not use `workspace:` or `file:` dependency ranges for packages intended to install from npm.
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@feniix/bridgekit",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "BridgeKit defines TypeBox-backed tools once and adapts them to pi, MCP, and other hosts.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi",
|
|
7
|
+
"mcp",
|
|
8
|
+
"bridgekit",
|
|
9
|
+
"portable-tools",
|
|
10
|
+
"typebox"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/src/index.js",
|
|
14
|
+
"types": "./dist/src/index.d.ts",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22.19.0"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/feniix/bridgekit.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/feniix/bridgekit/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/feniix/bridgekit#readme",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/src/index.d.ts",
|
|
30
|
+
"import": "./dist/src/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./pi": {
|
|
33
|
+
"types": "./dist/src/pi.d.ts",
|
|
34
|
+
"import": "./dist/src/pi.js"
|
|
35
|
+
},
|
|
36
|
+
"./mcp": {
|
|
37
|
+
"types": "./dist/src/mcp.d.ts",
|
|
38
|
+
"import": "./dist/src/mcp.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"lint": "biome check .",
|
|
44
|
+
"lint:fix": "biome check --write .",
|
|
45
|
+
"check": "npm run lint && npm run typecheck",
|
|
46
|
+
"clean": "node scripts/clean-package-dist.mjs",
|
|
47
|
+
"typecheck": "tsc -b tsconfig.json --pretty false",
|
|
48
|
+
"build": "npm run clean && tsc -b tsconfig.json",
|
|
49
|
+
"test": "npm run build && node scripts/run-built-tests.mjs",
|
|
50
|
+
"test:coverage": "npm run build && node scripts/run-built-tests-coverage.mjs",
|
|
51
|
+
"verify:dist": "node scripts/verify-bridgekit-dist.mjs",
|
|
52
|
+
"pack:dry-run": "npm pack --dry-run --json",
|
|
53
|
+
"package-smoke": "node scripts/smoke-package.mjs",
|
|
54
|
+
"prepack": "npm run build && npm run verify:dist"
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"dist/**/*.js",
|
|
58
|
+
"dist/**/*.d.ts",
|
|
59
|
+
"!dist/**/*.test.*",
|
|
60
|
+
"!dist/**/*.typecheck.*",
|
|
61
|
+
"!dist/**/*.map",
|
|
62
|
+
"!dist/tsconfig.tsbuildinfo",
|
|
63
|
+
"README.md",
|
|
64
|
+
"llms.txt",
|
|
65
|
+
"examples/README.md"
|
|
66
|
+
],
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
69
|
+
"typebox": "^1.1.31"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@biomejs/biome": "2.4.10",
|
|
73
|
+
"@total-typescript/shoehorn": "^0.1.2",
|
|
74
|
+
"@types/node": "^22.19.17",
|
|
75
|
+
"typescript": "^5.5.0"
|
|
76
|
+
},
|
|
77
|
+
"license": "MIT"
|
|
78
|
+
}
|