@feniix/bridgekit 0.14.0 → 0.16.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 CHANGED
@@ -4,6 +4,63 @@ 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
+ ## [Unreleased]
8
+
9
+ ## [0.16.0] - 2026-10-06
10
+
11
+ ### Added
12
+
13
+ - Optional object-shaped `PortableTool.outputSchema`. Successful calls must return
14
+ matching `structuredContent`; missing or invalid output throws a tool-attributed
15
+ `TypeError`. Argument and domain failures are exempt from success schemas.
16
+ - Pi registrations forward `outputSchema`, and Pi results preserve
17
+ `structuredContent` alongside the existing renderer-facing `details`.
18
+ - MCP listings forward object-shaped output schemas, including intersections;
19
+ invalid output schemas are rejected at server construction.
20
+ - Schema-declaring `definePortableTool` calls check successful structured data at
21
+ compile time while preserving inferred result unions; explicit annotations can
22
+ retain schemas with a third `PortableTool` generic. Domain failures are exempt.
23
+ Explicit legacy two-generic function calls intentionally erase schema linkage
24
+ and retain runtime validation; inferred calls are recommended for type checking.
25
+ - Output-schema construction failures carry stable diagnostic codes, with
26
+ reference/union-specific recipes and MCP constructor attribution.
27
+
28
+ ### Changed (breaking)
29
+
30
+ - MCP runtime now uses `@modelcontextprotocol/server` SDK v2. `createMcpServer`
31
+ returns a v2 `Server`; consumers using its low-level API must migrate method
32
+ registration/context types and must not mix v1 and v2 SDK instances.
33
+ - `runMcpStdioServer` now serves both legacy MCP clients and modern protocol
34
+ `2026-07-28` via `serveStdio`. Its `Promise<void>` startup signature is unchanged.
35
+ This change requires a pre-1.0 minor release, not a patch.
36
+
37
+ ### Fixed
38
+
39
+ - Discarded modern stdio discovery probes no longer contaminate legacy fallback:
40
+ each factory invocation gets a fresh server. SDK-reported opening errors are
41
+ diagnosed on stderr without contaminating protocol stdout.
42
+ - Pi validates all output schemas before registering tools, preventing partial
43
+ registration when a later tool has an invalid schema.
44
+ - `definePortableTool` retains metadata composition and already-annotated
45
+ two-generic inputs while still checking concrete schema-bearing handlers.
46
+ Packed declarations pin both valid and invalid replacement handlers.
47
+
48
+ ## [0.15.0] - 2026-07-12
49
+
50
+ ### Added
51
+
52
+ - `PiHostExtras` accepts `renderCall` and `renderResult`, pi's per-tool TUI
53
+ renderers for the call line and for the collapsed/expanded result view.
54
+ `registerPiTools` forwards both to `pi.registerTool` by identity; the core
55
+ never invokes them and does not import `pi-tui`. Contributed by
56
+ [@akaGelo](https://github.com/akaGelo) in
57
+ [#72](https://github.com/feniix/bridgekit/pull/72).
58
+
59
+ ### Changed
60
+
61
+ - `docs/rfc-host-extras.md` records the renderers as Gap E, in scope as a
62
+ registration-time pass-through.
63
+
7
64
  ## [0.14.0] - 2026-05-28
8
65
 
9
66
  ### Added
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  BridgeKit provides reusable TypeBox-backed tool definitions and adapters for exposing one tool implementation through pi, MCP, and other hosts.
4
4
 
5
+ More about why this exists and where it is useful: [feniix-hq.net/projects/bridgekit](https://feniix-hq.net/projects/bridgekit/).
6
+
5
7
  ## Quickstart
6
8
 
7
9
  ```ts
@@ -83,6 +85,8 @@ import {
83
85
  executePortableTool,
84
86
  isDomainFailure,
85
87
  isValidationFailure,
88
+ type PiToolCallRenderer,
89
+ type PiToolResultRenderer,
86
90
  type PortableTool,
87
91
  type PortableToolBuiltInHost,
88
92
  type PortableToolContext,
@@ -105,6 +109,48 @@ Do not deep-import from `dist/` or `src/` in consuming packages.
105
109
 
106
110
  ### pi adapter
107
111
 
112
+ #### Declared structured output
113
+
114
+ Add an optional object-shaped `outputSchema` to a portable tool to describe its
115
+ successful `structuredContent` for both Pi and MCP:
116
+
117
+ ```ts
118
+ const echoTool = definePortableTool({
119
+ name: "echo",
120
+ title: "Echo",
121
+ description: "Echo text.",
122
+ parameters: Type.Object({ text: Type.String() }),
123
+ outputSchema: Type.Object({ text: Type.String() }),
124
+ execute: (args) => ({ text: args.text, structuredContent: { text: args.text } }),
125
+ });
126
+ ```
127
+
128
+ Inlined object schemas and intersections of objects are accepted. Success calls
129
+ must return matching `structuredContent`; legacy `details` alone does not satisfy
130
+ the schema. Missing/invalid output throws a tool-attributed `TypeError` at the
131
+ portable seam and becomes an error result through the adapters. Argument/domain
132
+ failures are exempt from success schemas. Without `outputSchema`, existing result
133
+ behavior is unchanged.
134
+
135
+ `definePortableTool` infers the output schema and checks synchronous/asynchronous
136
+ successful data at compile time while preserving the handler's inferred result
137
+ type. Domain failures must use the literal `isError: true` discriminator.
138
+ Runtime validation still protects JavaScript callers and dynamically sourced data.
139
+ For explicitly annotated tools, use the third `PortableTool<TParams, TResult, TOutput>`
140
+ generic to retain schema checking; existing two-generic annotations deliberately
141
+ erase schema specificity.
142
+ Explicit legacy `definePortableTool<TParams, TResult>(...)` calls also erase the
143
+ schema, even with a concrete `outputSchema`. Prefer inferred calls for compile-time
144
+ schema checking; runtime output validation still applies to the explicit calls.
145
+ Existing annotated tools and metadata-only spreads remain accepted by
146
+ `definePortableTool`; concrete schema-bearing spreads still check replacement
147
+ handlers. Treat `parameters` and `outputSchema` as immutable after registration:
148
+ do not mutate their contents or replace either schema object.
149
+
150
+ Pi registrations forward the schema; Pi results now preserve `structuredContent`
151
+ directly for programmatic/codemode consumers, alongside the renderer-facing
152
+ `details` described below. The new fields are omitted when not supplied.
153
+
108
154
  ```ts
109
155
  import { registerPiTools } from "@feniix/bridgekit/pi";
110
156
  import { createTools } from "./tools.js";
@@ -167,13 +213,19 @@ they are not designed to be called on the pi adapter's wire object.
167
213
 
168
214
  #### Per-host metadata via `hostExtras`
169
215
 
170
- `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.
216
+ `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.
171
217
 
172
218
  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.
173
219
 
174
220
  ```ts
175
221
  import { Type } from "typebox";
176
- import { definePortableTool } from "@feniix/bridgekit";
222
+ import { definePortableTool, type PiToolCallRenderer, type PiToolResultRenderer } from "@feniix/bridgekit";
223
+
224
+ const renderSummaryCall: PiToolCallRenderer = (args) => ({ type: "text", text: `summarise ${args.text}` });
225
+ const renderSummaryResult: PiToolResultRenderer = (result, options) => ({
226
+ type: "text",
227
+ text: options.expanded ? result.content?.[0]?.text : "summary ready",
228
+ });
177
229
 
178
230
  export const generateSummaryTool = definePortableTool({
179
231
  name: "generate_summary",
@@ -190,6 +242,9 @@ export const generateSummaryTool = definePortableTool({
190
242
  pendingMessage: "Summarising...",
191
243
  promptSnippet: "Use this tool when the user asks for a short summary.",
192
244
  promptGuidelines: ["Prefer < 80 chars.", "Strip markdown."],
245
+ // Optional pi TUI renderers, forwarded by identity to pi.registerTool.
246
+ renderCall: renderSummaryCall,
247
+ renderResult: renderSummaryResult,
193
248
  },
194
249
  mcp: {
195
250
  annotations: { readOnlyHint: true },
@@ -198,6 +253,8 @@ export const generateSummaryTool = definePortableTool({
198
253
  });
199
254
  ```
200
255
 
256
+ `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`.
257
+
201
258
  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"`.
202
259
 
203
260
  ### MCP adapter
@@ -241,7 +298,30 @@ Tool `parameters` must resolve to a JSON-Schema object at the top level. `Type.O
241
298
 
242
299
  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.
243
300
 
244
- 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).
301
+ The two adapters return `{ isError: true }` for argument/domain failures by default.
302
+ Use result guards on portable values; both wire formats now preserve `structuredContent`,
303
+ and Pi also exposes its renderer-facing `details`.
304
+ The guards match error/data shape, not provenance, so Pi wire failures also match
305
+ at runtime. Raw Pi values still lack portable `text`; do not cast them to
306
+ `PortableToolResult` to use the typed guards.
307
+
308
+ #### MCP SDK v2 and modern stdio
309
+
310
+ `runMcpStdioServer(options)` serves both legacy MCP revisions and modern
311
+ `2026-07-28` through SDK v2's `serveStdio`. Its `Promise<void>` resolves after
312
+ startup wiring, not at shutdown. Stdin EOF closes the connection; long-running
313
+ tools must honor `ctx.signal` and release their own resources.
314
+
315
+ `createMcpServer` now returns `@modelcontextprotocol/server` SDK v2's low-level
316
+ `Server`. This is a source-level breaking change for consumers using SDK methods
317
+ or types, even though legacy clients work over the wire. A direct
318
+ `server.connect(StdioServerTransport)` remains legacy-only; use the runner for
319
+ modern serving. See [migration guidance](docs/mcp-v2-migration.md).
320
+
321
+ Some SDK v1 clients validate error data against cached success output schemas
322
+ in their convenience `callTool` method; their generic request API can preserve
323
+ that data. SDK v2 clients correctly exempt error results. HTTP and tasks remain
324
+ outside BridgeKit's runner support.
245
325
 
246
326
  ### bin-wrapper (since 0.11.0)
247
327
 
@@ -1,4 +1,4 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1
+ import { Server } from "@modelcontextprotocol/server";
2
2
  import type { TSchema } from "typebox";
3
3
  import type { PortableTool } from "../core/define-tool.js";
4
4
  export interface CreateMcpServerOptions {
@@ -14,7 +14,7 @@ export interface CreateMcpServerOptions {
14
14
  * The outer array is snapshotted at construction; pushing or removing
15
15
  * entries from the caller's `tools` array post-construction does not affect
16
16
  * `tools/list` or `tools/call`. Schemas inside each tool are held by
17
- * reference, not deep-cloned — treat `tool.parameters` as immutable once
17
+ * reference, not deep-cloned — treat `tool.parameters` and `tool.outputSchema` as immutable once
18
18
  * `createMcpServer` returns.
19
19
  */
20
20
  tools: readonly PortableTool<TSchema>[];
@@ -1,7 +1,7 @@
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";
1
+ import { Server } from "@modelcontextprotocol/server";
2
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
4
3
  import { executePortableTool } from "../core/execute-tool.js";
4
+ import { assertPortableOutputSchema, isObjectSchema, schemaTypeLabel, throwWithCode } from "../core/output-schema.js";
5
5
  function toMcpResult(result) {
6
6
  return {
7
7
  content: [{ type: "text", text: result.text }],
@@ -10,85 +10,22 @@ function toMcpResult(result) {
10
10
  };
11
11
  }
12
12
  /**
13
- * Returns true if `schema` resolves to a JSON-Schema object at the top level —
14
- * either `{"type": "object", ...}` directly, or an `allOf` composition whose
15
- * branches are all object schemas (as produced by `Type.Intersect`).
16
- *
17
- * The check walks the canonical JSON-Schema shape rather than poking at
18
- * TypeBox `Kind` symbols, mirroring how `executePortableTool` traverses
19
- * schemas. The MCP wire contract is "top-level object," so that's the
20
- * predicate, regardless of which TypeBox combinator built the schema.
21
- */
22
- function isObjectSchema(schema) {
23
- if (typeof schema !== "object" || schema === null)
24
- return false;
25
- const candidate = schema;
26
- // $ref takes structural precedence: a hybrid {type: "object", $ref: "..."}
27
- // is not an inlined object schema — it's a reference, and
28
- // assertObjectShapedParameters owns the rejection recipe. Return false so
29
- // the $ref branch runs.
30
- if (typeof candidate.$ref === "string")
31
- return false;
32
- if (candidate.type === "object")
33
- return true;
34
- if (Array.isArray(candidate.allOf) && candidate.allOf.length > 0) {
35
- return candidate.allOf.every((entry) => isObjectSchema(entry));
36
- }
37
- return false;
38
- }
39
- /**
40
- * Best-effort human-readable label for a non-object schema, used in error
41
- * messages. For `allOf` (TypeBox's `Intersect` lowering) we descend into the
42
- * branches and surface the first non-object branch by index — a bare `"allOf"`
43
- * label is misleading because the rejection is owned by one specific branch,
44
- * not the composition itself.
45
- *
46
- * The `$ref` check is first because `Type.Cyclic` produces
47
- * `{ $defs: {...}, $ref: "..." }` at the root; the `$ref` is the load-bearing
48
- * structural signal regardless of what else is set, and the recipe for that
49
- * shape (inline or split) is different from the generic `Type.Object(...)`
50
- * wrap recipe.
51
- */
52
- function schemaTypeLabel(schema) {
53
- if (typeof schema !== "object" || schema === null)
54
- return "unknown";
55
- const candidate = schema;
56
- if (typeof candidate.$ref === "string")
57
- return "$ref";
58
- if (typeof candidate.type === "string")
59
- return candidate.type;
60
- if (Array.isArray(candidate.anyOf))
61
- return "anyOf";
62
- if (Array.isArray(candidate.oneOf))
63
- return "oneOf";
64
- if (Array.isArray(candidate.allOf)) {
65
- if (candidate.allOf.length === 0)
66
- return "allOf (empty)";
67
- for (let i = 0; i < candidate.allOf.length; i++) {
68
- if (!isObjectSchema(candidate.allOf[i])) {
69
- return `allOf[${i}] resolves to type="${schemaTypeLabel(candidate.allOf[i])}"`;
70
- }
71
- }
72
- }
73
- return "unknown";
74
- }
75
- /**
76
- * Render `parameters` as MCP `inputSchema`. The MCP SDK Zod-validates that the
77
- * top-level schema has `type: "object"` on the client side of `tools/list`, so
13
+ * Render a validated object schema as MCP `inputSchema` or `outputSchema`.
14
+ * MCP clients require the top-level schema to have `type: "object"`, so
78
15
  * `Type.Intersect` (which TypeBox renders as `{ allOf: [...] }` with no top-
79
16
  * level `type`) needs `type: "object"` synthesized before transmission. This
80
17
  * is a no-op for `Type.Object` schemas, which already carry the field.
81
18
  *
82
- * The casts here are sound because `assertObjectShapedParameters` runs first
83
- * and rejects any schema whose top-level lowering isn't `type:"object"` or
84
- * `allOf` of objects — both shapes round-trip as MCP `Tool["inputSchema"]`.
19
+ * The casts here are sound because `assertObjectShapedParameters` or
20
+ * `assertPortableOutputSchema` runs first and rejects schemas whose top-level
21
+ * lowering isn't `type:"object"` or `allOf` of objects.
85
22
  */
86
- function toInputSchema(parameters) {
87
- const candidate = parameters;
23
+ function toMcpObjectSchema(schema) {
24
+ const candidate = schema;
88
25
  if (candidate.type === "object") {
89
- return parameters;
26
+ return schema;
90
27
  }
91
- return { type: "object", ...parameters };
28
+ return { type: "object", ...schema };
92
29
  }
93
30
  /**
94
31
  * Stable `error.code` values attached to `createMcpServer` construction
@@ -98,11 +35,6 @@ function toInputSchema(parameters) {
98
35
  const ERROR_CODE_NON_OBJECT_PARAMETERS = "BRIDGEKIT_MCP_NON_OBJECT_PARAMETERS";
99
36
  const ERROR_CODE_REF_PARAMETERS = "BRIDGEKIT_MCP_REF_PARAMETERS";
100
37
  const ERROR_CODE_DUPLICATE_TOOL_NAME = "BRIDGEKIT_MCP_DUPLICATE_TOOL_NAME";
101
- function throwWithCode(message, code) {
102
- const error = new Error(message);
103
- error.code = code;
104
- throw error;
105
- }
106
38
  function assertObjectShapedParameters(tools) {
107
39
  for (const tool of tools) {
108
40
  if (!isObjectSchema(tool.parameters)) {
@@ -150,6 +82,8 @@ function assertUniqueToolNames(tools) {
150
82
  }
151
83
  }
152
84
  export function createMcpServer(options) {
85
+ for (const tool of options.tools)
86
+ assertPortableOutputSchema(tool, "createMcpServer", "BRIDGEKIT_MCP");
153
87
  assertObjectShapedParameters(options.tools);
154
88
  assertUniqueToolNames(options.tools);
155
89
  // Build the dispatch map and the listing payload at construction so
@@ -176,7 +110,8 @@ export function createMcpServer(options) {
176
110
  name: tool.name,
177
111
  title: tool.title,
178
112
  description: tool.description,
179
- inputSchema: toInputSchema(tool.parameters),
113
+ inputSchema: toMcpObjectSchema(tool.parameters),
114
+ ...(tool.outputSchema !== undefined && { outputSchema: toMcpObjectSchema(tool.outputSchema) }),
180
115
  ...(hasAnnotations ? { annotations: { ...annotations } } : {}),
181
116
  };
182
117
  });
@@ -184,8 +119,8 @@ export function createMcpServer(options) {
184
119
  capabilities: { tools: { listChanged: false } },
185
120
  ...(options.instructions !== undefined && { instructions: options.instructions }),
186
121
  });
187
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: mcpTools }));
188
- server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
122
+ server.setRequestHandler("tools/list", async () => ({ tools: mcpTools }));
123
+ server.setRequestHandler("tools/call", async (request, ctx) => {
189
124
  const tool = byName.get(request.params.name);
190
125
  if (!tool) {
191
126
  return {
@@ -196,9 +131,9 @@ export function createMcpServer(options) {
196
131
  try {
197
132
  const result = await executePortableTool(tool, request.params.arguments ?? {}, {
198
133
  host: "mcp",
199
- signal: extra.signal,
134
+ signal: ctx.mcpReq.signal,
200
135
  });
201
- return toMcpResult(result);
136
+ return server.projectCallToolResult(toMcpResult(result), tool.outputSchema !== undefined ? toMcpObjectSchema(tool.outputSchema) : undefined);
202
137
  }
203
138
  catch (error) {
204
139
  const message = error instanceof Error ? error.message : String(error);
@@ -211,6 +146,10 @@ export function createMcpServer(options) {
211
146
  return server;
212
147
  }
213
148
  export async function runMcpStdioServer(options) {
214
- const server = createMcpServer(options);
215
- await server.connect(new StdioServerTransport());
149
+ // Validate eagerly, but each discarded probe/connection must own its server:
150
+ // a modern probe installs era-specific handlers before it can be discarded.
151
+ createMcpServer(options);
152
+ serveStdio(() => createMcpServer(options), {
153
+ onerror: (error) => process.stderr.write(`[bridgekit-mcp] ${error.message}\n`),
154
+ });
216
155
  }
@@ -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;
@@ -11,6 +11,7 @@ type PiToolUpdate = {
11
11
  type PiToolResult = {
12
12
  content: PiContent[];
13
13
  details: Record<string, unknown>;
14
+ structuredContent?: Record<string, unknown>;
14
15
  isError?: boolean;
15
16
  };
16
17
  type PiToolDefinition = {
@@ -18,9 +19,12 @@ type PiToolDefinition = {
18
19
  label: string;
19
20
  description: string;
20
21
  parameters: TSchema;
22
+ outputSchema?: TSchema;
21
23
  execute(toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: (update: PiToolUpdate) => void, ctx?: unknown): Promise<PiToolResult>;
22
24
  promptSnippet?: string;
23
25
  promptGuidelines?: string[];
26
+ renderCall?: PiToolCallRenderer;
27
+ renderResult?: PiToolResultRenderer;
24
28
  };
25
29
  export type PiToolRegistration = {
26
30
  registerTool(tool: PiToolDefinition): unknown;
@@ -1,4 +1,5 @@
1
1
  import { executePortableTool } from "../core/execute-tool.js";
2
+ import { assertPortableOutputSchema } from "../core/output-schema.js";
2
3
  import { isValidationFailure } from "../core/result-guards.js";
3
4
  function toPiDetails(result) {
4
5
  return result.structuredContent ?? result.details ?? {};
@@ -40,6 +41,8 @@ let throwModeDeprecationWarned = false;
40
41
  * registerPiTools(pi, tools, { errorHandling: "throw" });
41
42
  */
42
43
  export function registerPiTools(pi, tools, options = {}) {
44
+ for (const tool of tools)
45
+ assertPortableOutputSchema(tool);
43
46
  const errorHandling = options.errorHandling ?? "return";
44
47
  if (errorHandling === "throw" && !throwModeDeprecationWarned) {
45
48
  throwModeDeprecationWarned = true;
@@ -56,6 +59,7 @@ export function registerPiTools(pi, tools, options = {}) {
56
59
  label: tool.title,
57
60
  description: tool.description,
58
61
  parameters: tool.parameters,
62
+ ...(tool.outputSchema !== undefined && { outputSchema: tool.outputSchema }),
59
63
  // Pass-through pi-side metadata. Each field is gated on `!== undefined`
60
64
  // so tools without `hostExtras.pi` build a registration object whose
61
65
  // own-property keys are unchanged from the pre-0.9 shape.
@@ -65,6 +69,9 @@ export function registerPiTools(pi, tools, options = {}) {
65
69
  // pi-coding-agent's registerTool input is `string[]` (mutable). The boundary
66
70
  // copy satisfies both sides without weakening either declared type.
67
71
  ...(piExtras?.promptGuidelines !== undefined && { promptGuidelines: [...piExtras.promptGuidelines] }),
72
+ // TUI renderers forwarded verbatim from hostExtras.pi.
73
+ ...(piExtras?.renderCall !== undefined && { renderCall: piExtras.renderCall }),
74
+ ...(piExtras?.renderResult !== undefined && { renderResult: piExtras.renderResult }),
68
75
  async execute(_toolCallId, params, signal, onUpdate, _ctx) {
69
76
  let result;
70
77
  try {
@@ -113,6 +120,7 @@ export function registerPiTools(pi, tools, options = {}) {
113
120
  return {
114
121
  content: [{ type: "text", text: result.text }],
115
122
  details: toPiDetails(result),
123
+ ...(result.structuredContent !== undefined && { structuredContent: result.structuredContent }),
116
124
  isError: result.isError === true,
117
125
  };
118
126
  },
@@ -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,28 @@ export interface PortableToolHostExtras {
138
153
  pi?: PiHostExtras;
139
154
  mcp?: McpHostExtras;
140
155
  }
141
- export interface PortableTool<TParams extends TSchema = TSchema, TResult extends PortableToolResult = PortableToolResult> {
156
+ /** Successes satisfy the output schema; literal domain failures retain arbitrary data. */
157
+ type SchemaResult<TOutput extends TSchema> = (PortableToolResult & {
158
+ structuredContent: Exclude<Static<TOutput>, undefined> & Record<string, unknown>;
159
+ isError?: false;
160
+ }) | (PortableToolResult & {
161
+ isError: true;
162
+ });
163
+ type CheckedResult<TResult extends PortableToolResult, TOutput extends TSchema | undefined> = [TOutput] extends [
164
+ TSchema
165
+ ] ? TResult & SchemaResult<NoInfer<Extract<TOutput, TSchema>>> : TResult;
166
+ export interface PortableTool<TParams extends TSchema = TSchema, TResult extends PortableToolResult = PortableToolResult, TOutput extends TSchema | undefined = TSchema | undefined> {
142
167
  name: string;
143
168
  title: string;
144
169
  description: string;
145
170
  parameters: TParams;
146
- execute: (args: Static<TParams>, ctx: PortableToolContext) => TResult | Promise<TResult>;
171
+ /**
172
+ * Object-shaped TypeBox schema for successful structuredContent.
173
+ * Literal isError:true results are exempt. Treat this schema and parameters
174
+ * as immutable after registration, including replacing either schema object.
175
+ */
176
+ outputSchema?: TOutput;
177
+ execute: (args: Static<TParams>, ctx: PortableToolContext) => CheckedResult<TResult, TOutput> | Promise<CheckedResult<TResult, TOutput>>;
147
178
  /**
148
179
  * Optional per-host metadata. Adapters consume the keys they recognise;
149
180
  * unknown host namespaces are ignored. Absent → no behavior change.
@@ -155,4 +186,18 @@ export interface PortableTool<TParams extends TSchema = TSchema, TResult extends
155
186
  */
156
187
  hostExtras?: PortableToolHostExtras;
157
188
  }
158
- export declare function definePortableTool<TParams extends TSchema, TResult extends PortableToolResult>(tool: PortableTool<TParams, TResult>): PortableTool<TParams, TResult>;
189
+ export declare function definePortableTool<TParams extends TSchema, TOutput extends TSchema, TExecute extends (args: Static<TParams>, ctx: PortableToolContext) => SchemaResult<NoInfer<TOutput>> | Promise<SchemaResult<NoInfer<TOutput>>>>(tool: Omit<PortableTool<TParams, PortableToolResult, TOutput>, "execute" | "outputSchema"> & {
190
+ outputSchema: TOutput;
191
+ execute: TExecute;
192
+ }): PortableTool<TParams, Awaited<ReturnType<TExecute>>, TOutput>;
193
+ export declare function definePortableTool<TParams extends TSchema, TResult extends PortableToolResult>(tool: PortableTool<TParams, TResult> & {
194
+ outputSchema?: undefined;
195
+ }): PortableTool<TParams, TResult, undefined>;
196
+ export declare function definePortableTool<TParams extends TSchema, TOutput extends TSchema, TExecute extends (args: Static<TParams>, ctx: PortableToolContext) => SchemaResult<NoInfer<TOutput>> | Promise<SchemaResult<NoInfer<TOutput>>>>(tool: Omit<PortableTool<TParams, PortableToolResult, TOutput>, "execute" | "outputSchema"> & {
197
+ outputSchema?: TOutput;
198
+ execute: TExecute;
199
+ }): PortableTool<TParams, Awaited<ReturnType<TExecute>>, TOutput>;
200
+ export declare function definePortableTool<TParams extends TSchema, TResult extends PortableToolResult, TTool extends {
201
+ outputSchema?: TSchema | undefined;
202
+ } = PortableTool<TParams, TResult>>(tool: PortableTool<TParams, TResult> & TTool & (TSchema extends NonNullable<NoInfer<TTool>["outputSchema"]> ? unknown : never)): TTool;
203
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { Check, Errors, Pointer } from "typebox/value";
2
+ import { assertPortableOutputSchema } from "./output-schema.js";
2
3
  const ROOT_FIELD = "(root)";
3
4
  function fieldFromPath(instancePath) {
4
5
  return instancePath.split("/").filter(Boolean).at(-1) ?? ROOT_FIELD;
@@ -443,6 +444,7 @@ export function validatePortableToolArgs(tool, args) {
443
444
  return { ok: false, errors };
444
445
  }
445
446
  export async function executePortableTool(tool, args, ctx) {
447
+ assertPortableOutputSchema(tool);
446
448
  const validation = validatePortableToolArgs(tool, args);
447
449
  if (!validation.ok) {
448
450
  return {
@@ -457,5 +459,13 @@ export async function executePortableTool(tool, args, ctx) {
457
459
  isError: true,
458
460
  };
459
461
  }
460
- return (await tool.execute(validation.args, ctx));
462
+ const result = await tool.execute(validation.args, ctx);
463
+ if (tool.outputSchema !== undefined &&
464
+ result.isError !== true &&
465
+ (result.structuredContent === undefined || !Check(tool.outputSchema, result.structuredContent))) {
466
+ // A handler violating its declared success contract is a programmer error,
467
+ // not an argument failure. Keep inferred success/error result types intact.
468
+ throw new TypeError(`Invalid structured output for ${tool.name}: structuredContent must match outputSchema.`);
469
+ }
470
+ return result;
461
471
  }
@@ -0,0 +1,7 @@
1
+ import type { PortableTool } from "./define-tool.js";
2
+ /** Follow JSON Schema structure rather than TypeBox-specific symbols. */
3
+ export declare function isObjectSchema(schema: unknown): boolean;
4
+ /** Identify the first unsupported root/branch for an actionable schema diagnostic. */
5
+ export declare function schemaTypeLabel(schema: unknown): string;
6
+ export declare function throwWithCode(message: string, code: string, ErrorType?: ErrorConstructor): never;
7
+ export declare function assertPortableOutputSchema(tool: PortableTool, context?: string, codePrefix?: string): void;
@@ -0,0 +1,53 @@
1
+ /** Follow JSON Schema structure rather than TypeBox-specific symbols. */
2
+ export function isObjectSchema(schema) {
3
+ if (typeof schema !== "object" || schema === null)
4
+ return false;
5
+ const candidate = schema;
6
+ if (typeof candidate.$ref === "string")
7
+ return false;
8
+ if (candidate.type === "object")
9
+ return true;
10
+ return Array.isArray(candidate.allOf) && candidate.allOf.length > 0 && candidate.allOf.every(isObjectSchema);
11
+ }
12
+ /** Identify the first unsupported root/branch for an actionable schema diagnostic. */
13
+ export function schemaTypeLabel(schema) {
14
+ if (typeof schema !== "object" || schema === null)
15
+ return "unknown";
16
+ const candidate = schema;
17
+ if (typeof candidate.$ref === "string")
18
+ return "$ref";
19
+ if (typeof candidate.type === "string")
20
+ return candidate.type;
21
+ if (Array.isArray(candidate.anyOf))
22
+ return "anyOf";
23
+ if (Array.isArray(candidate.oneOf))
24
+ return "oneOf";
25
+ if (Array.isArray(candidate.allOf)) {
26
+ if (candidate.allOf.length === 0)
27
+ return "allOf (empty)";
28
+ for (let i = 0; i < candidate.allOf.length; i++) {
29
+ if (!isObjectSchema(candidate.allOf[i])) {
30
+ return `allOf[${i}] resolves to type="${schemaTypeLabel(candidate.allOf[i])}"`;
31
+ }
32
+ }
33
+ }
34
+ return "unknown";
35
+ }
36
+ export function throwWithCode(message, code, ErrorType = Error) {
37
+ const error = new ErrorType(message);
38
+ error.code = code;
39
+ throw error;
40
+ }
41
+ export function assertPortableOutputSchema(tool, context, codePrefix = "BRIDGEKIT") {
42
+ if (tool.outputSchema !== undefined && !isObjectSchema(tool.outputSchema)) {
43
+ const label = schemaTypeLabel(tool.outputSchema);
44
+ let recipe = "Use an inlined object schema or an intersection of object schemas.";
45
+ if (label.includes("$ref")) {
46
+ recipe += " Top-level $ref / Type.Cyclic is unsupported; inline the referenced shape or split recursive shapes.";
47
+ }
48
+ else if (label.includes("anyOf") || label.includes("oneOf")) {
49
+ recipe += " Top-level unions are unsupported; flatten branches into one object or expose separate tools.";
50
+ }
51
+ throwWithCode(`${context ? `${context}: ` : ""}Invalid outputSchema for ${tool.name} (type="${label}"): ${recipe}`, `${codePrefix}_${label.includes("$ref") ? "REF" : "NON_OBJECT"}_OUTPUT_SCHEMA`, TypeError);
52
+ }
53
+ }
@@ -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";
@@ -144,7 +144,8 @@ In `package.json`:
144
144
 
145
145
  pi behavior:
146
146
 
147
- - Valid portable results become pi tool results.
147
+ - Valid portable results become pi tool results with `structuredContent` preserved
148
+ alongside renderer-facing `details`. Optional `outputSchema` is forwarded.
148
149
  - 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
150
  - 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.
150
151
  - Progress updates from `ctx.progress?.(...)` map to pi updates.
@@ -269,17 +270,27 @@ MCP behavior:
269
270
  - `tools/call` validates arguments before invoking handlers.
270
271
  - Invalid arguments and portable `isError: true` results return MCP tool results with `isError: true`.
271
272
  - Unexpected thrown errors become MCP tool errors with text content.
273
+ - Optional object-shaped `outputSchema` is listed for successful structured results;
274
+ matching `structuredContent` is required and validated. Error results are exempt.
275
+ - The stdio runner serves legacy and modern `2026-07-28` clients through SDK v2.
276
+ It retains `Promise<void>` startup behavior; stdin EOF shuts down the connection.
272
277
  - The module stays import-passive and testable: tests can import `createMcpServerOptions()` without starting stdio.
273
278
 
274
279
  ---
275
280
 
276
281
  ## 4. Per-host metadata via `hostExtras`
277
282
 
278
- 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.
283
+ 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.
279
284
 
280
285
  ```ts
281
286
  import { Type } from "typebox";
282
- import { definePortableTool } from "@feniix/bridgekit";
287
+ import { definePortableTool, type PiToolCallRenderer, type PiToolResultRenderer } from "@feniix/bridgekit";
288
+
289
+ const renderSummariseCall: PiToolCallRenderer = (args) => ({ type: "text", text: `summarise ${args.text}` });
290
+ const renderSummariseResult: PiToolResultRenderer = (result, options) => ({
291
+ type: "text",
292
+ text: options.expanded ? result.content?.[0]?.text : "summary ready",
293
+ });
283
294
 
284
295
  export const summariseTool = definePortableTool({
285
296
  name: "summarise",
@@ -296,6 +307,9 @@ export const summariseTool = definePortableTool({
296
307
  pendingMessage: "Summarising...",
297
308
  promptSnippet: "Use this tool when the user asks for a short summary.",
298
309
  promptGuidelines: ["Prefer < 80 chars.", "Strip markdown."],
310
+ // Opaque pi TUI callbacks. BridgeKit forwards these by identity.
311
+ renderCall: renderSummariseCall,
312
+ renderResult: renderSummariseResult,
299
313
  },
300
314
  mcp: {
301
315
  // Advisory hints clients may surface. Do not affect validation or
@@ -308,7 +322,7 @@ export const summariseTool = definePortableTool({
308
322
 
309
323
  Best practices for `hostExtras`:
310
324
 
311
- - Keep tool *behavior* host-neutral. `hostExtras` carries *data* the adapter reads on the tool's behalf; do not embed callbacks or runtime logic.
325
+ - 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.
312
326
  - Set only the fields the adapter recognises; unrecognised keys are silently ignored, but they add noise to the definition.
313
327
  - 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.
314
328
 
package/llms.txt CHANGED
@@ -59,6 +59,18 @@ export function createTools() {
59
59
  - Keep portable tools host-neutral. Do not import pi or MCP SDKs from files that define tool behavior.
60
60
  - Tool `parameters` for MCP-compatible tools must resolve to a JSON-Schema object at the top level. `Type.Object(...)` is the common case; `Type.Intersect([Type.Object(...), Type.Object(...)])` is also accepted. Non-object top-level shapes throw at `createMcpServer` construction.
61
61
  - Return `{ text, structuredContent }` for successful results.
62
+ - Optional `outputSchema` declares successful structured data for both hosts: use
63
+ an inlined TypeBox object or object intersection. Missing/invalid successful
64
+ `structuredContent` throws `TypeError`; argument/domain error results are exempt.
65
+ Legacy `details` alone does not satisfy a declared output schema.
66
+ `definePortableTool` infers/checks schema-linked success data at compile time;
67
+ use literal `isError: true` for exempt domain failures. A third
68
+ `PortableTool<TParams, TResult, TOutput>` generic preserves the schema in explicit
69
+ annotations; legacy two-generic annotations erase it. Runtime checks still apply.
70
+ Explicit legacy `definePortableTool<TParams, TResult>(...)` calls also erase
71
+ schema inference, even for concrete schemas; prefer inferred calls for checking.
72
+ Existing tool annotations and metadata spreads remain composable. Treat both
73
+ `parameters` and `outputSchema` as immutable after registration (no mutation or replacement).
62
74
  - Use `isError: true` for domain/tool-level failures that should reach the model as tool output.
63
75
  - Throw only for unexpected programmer, adapter, or runtime failures.
64
76
  - Respect `ctx.signal` in long-running tools.
@@ -75,11 +87,21 @@ export function createTools() {
75
87
 
76
88
  - 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
89
  - 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.
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.
90
+ - Both adapters preserve `structuredContent`; Pi also maps it into renderer-facing
91
+ `details`. Both retain legacy `details` fallback. Output schemas are forwarded
92
+ to registrations/listings only when provided.
79
93
  - 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.
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`.
94
+ - Result guards take `PortableToolResult` values from `executePortableTool`, not
95
+ raw Pi wire objects (`content` instead of portable `text`).
96
+ Their runtime predicates are shape-based, so Pi structured failures also
97
+ match; that does not make a raw Pi result a valid portable typed value.
98
+ - MCP uses SDK v2's low-level `Server` and method-string handlers. Cancellation
99
+ reads `ctx.mcpReq.signal`. `runMcpStdioServer` serves legacy and modern
100
+ `2026-07-28` stdio; direct `Server.connect` remains legacy-only. The runner's
101
+ `Promise<void>` resolves at startup, not EOF. Migration details:
102
+ `docs/mcp-v2-migration.md`.
81
103
  - `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.
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 verbatim to pi's `registerTool` call; `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.
104
+ - `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.
83
105
 
84
106
  ### Result-guard usage
85
107
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feniix/bridgekit",
3
- "version": "0.14.0",
3
+ "version": "0.16.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",
@@ -23,7 +23,7 @@
23
23
  "bugs": {
24
24
  "url": "https://github.com/feniix/bridgekit/issues"
25
25
  },
26
- "homepage": "https://github.com/feniix/bridgekit#readme",
26
+ "homepage": "https://feniix-hq.net/projects/bridgekit/",
27
27
  "exports": {
28
28
  ".": {
29
29
  "types": "./dist/src/index.d.ts",
@@ -69,11 +69,13 @@
69
69
  "examples/README.md"
70
70
  ],
71
71
  "dependencies": {
72
- "@modelcontextprotocol/sdk": "^1.29.0",
72
+ "@modelcontextprotocol/server": "^2.3.1",
73
73
  "typebox": "^1.1.31"
74
74
  },
75
75
  "devDependencies": {
76
- "@biomejs/biome": "2.4.16",
76
+ "@modelcontextprotocol/client": "^2.3.1",
77
+ "@modelcontextprotocol/sdk": "^1.30.1",
78
+ "@biomejs/biome": "2.5.4",
77
79
  "@total-typescript/shoehorn": "^0.1.2",
78
80
  "@types/node": "^25.9.1",
79
81
  "typescript": "^6.0.3"