
Research
/Security News
Miasma Mini Shai-Hulud Hits ImmobiliareLabs npm Packages
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.
@feniix/bridgekit
Advanced tools
BridgeKit defines TypeBox-backed tools once and adapts them to pi, MCP, and other hosts.
BridgeKit provides reusable TypeBox-backed tool definitions and adapters for exposing one tool implementation through pi, MCP, and other hosts.
// src/tools.ts
import { Type } from "typebox";
import { definePortableTool } from "@feniix/bridgekit";
export const echoTool = definePortableTool({
name: "echo",
title: "Echo",
description: "Echo text back to the caller.",
parameters: Type.Object({ text: Type.String() }),
execute(args, ctx) {
return {
text: args.text,
structuredContent: { text: args.text, host: ctx.host },
};
},
hostExtras: {
pi: { pendingMessage: "Echoing..." },
mcp: { annotations: { readOnlyHint: true } },
},
});
export function createTools() {
return [echoTool];
}
// src/mcp-server.ts
import { runMcpStdioServer } from "@feniix/bridgekit/mcp";
import { createTools } from "./tools.js";
await runMcpStdioServer({
name: "my-tools",
version: "0.1.0",
tools: createTools(),
});
// src/pi-extension.ts
import { registerPiTools } from "@feniix/bridgekit/pi";
import { createTools } from "./tools.js";
export default function extension(pi: Parameters<typeof registerPiTools>[0]) {
registerPiTools(pi, createTools());
}
npm install @feniix/bridgekit typebox
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.
inputSchema directly — no JSON Schema conversion step.sideEffects: false, three-entrypoint split (., ./pi, ./mcp) so pi-only consumers do not pull the MCP SDK and vice versa.import {
definePortableTool,
executePortableTool,
isDomainFailure,
isValidationFailure,
type PortableTool,
type PortableToolBuiltInHost,
type PortableToolContext,
type PortableToolHost,
type PortableToolResult,
type PortableValidationError,
} from "@feniix/bridgekit";
import { registerPiTools } from "@feniix/bridgekit/pi";
import { createMcpServer, runMcpStdioServer } from "@feniix/bridgekit/mcp";
/pi: pi adapter only./mcp: MCP server adapter only.Do not deep-import from dist/ or src/ in consuming packages.
import { registerPiTools } from "@feniix/bridgekit/pi";
import { createTools } from "./tools.js";
export default function extension(pi: Parameters<typeof registerPiTools>[0]) {
registerPiTools(pi, createTools());
}
By default (errorHandling: "return", as of 0.7) the pi adapter mirrors the MCP adapter: portable validation failures and portable isError: true results surface as { content, details, isError: true } so consumers can branch on result.isError and narrow with isValidationFailure / isDomainFailure. Unexpected handler exceptions are caught and surfaced as { content: [{type:"text", text: message}], details: {}, isError: true }, matching MCP. Success-path results include isError: false explicitly so consumers can use the same strict-equality checks across both adapters. details is populated from structuredContent first, then from details, then {}. Progress updates from ctx.progress?.(...) map to pi tool updates.
The pre-0.7 behavior — throw PortableToolExecutionError on isError — is still available for one deprecation cycle:
registerPiTools(pi, createTools(), { errorHandling: "throw" });
Selecting errorHandling: "throw" emits a DeprecationWarning (code BRIDGEKIT_PI_THROW_DEPRECATED) once per process. Only the "throw" value is deprecated; the errorHandling option itself remains. Migrate by switching to the default and branching on the returned result:
// Before (0.6 and earlier)
try {
const result = await piTool.execute(...);
} catch (err) {
if (isPortableToolExecutionError(err)) {
if (err.details.kind === "validation") { /* TypeBox errors */ }
else { /* domain failure */ }
}
}
// After (0.7+)
const result = await piTool.execute(...);
if (result.isError) {
if (isValidationFailure(result)) {
// validationErrors is Array<{ field: string; message: string }>.
for (const { field, message } of result.structuredContent.validationErrors) {
// ...
}
} else if (isDomainFailure(result)) { /* handler-level error */ }
}
PortableValidationError exposes { field, message }. field is derived from TypeBox's structured error data — required-property errors read params.requiredProperties (so a prop named "a,b" survives intact and the value is locale-independent); other errors take the last meaningful segment of the offending JSON pointer (text, not /text). One error is emitted per missing required property, and duplicate (field, message) pairs (e.g. from union mismatches) are deduplicated. Validation and domain errors share this { field, message } shape, so a consumer reading .field does not need to branch on which kind of failure produced the entry. (path was the pre-0.8.0 name; it has been removed.)
For array-element validation, field is the leaf segment, which can be a numeric index (e.g. field: "0") and loses path context. For root-level schema failures with empty instancePath (e.g. null passed to a Type.Object schema), field is the sentinel "(root)".
For discriminated unions (Type.Union([Type.Object({tag: Literal("a"), …}), Type.Object({tag: Literal("b"), …})])), when exactly one branch's discriminator matches the input, BridgeKit surfaces only that branch's missing-required hints (as of 0.8.2). Recognized discriminator shapes: Literal (const), enum, and Union of Literals (anyOf of consts). Resolution works inside Type.Array(...) per-element. When no branch matches (invalid discriminator), the anyOf summary and const/enum errors survive so consumers can identify the failed discriminator and pick a valid value.
const/enum error messages carry the allowed value(s) directly (must equal "create" / must equal one of "create", "update") so an agent can pick a valid discriminator on retry.
For nested discriminated unions, field is the leaf segment of the JSON pointer ("name" for /event/name), not the full path. Consumers needing full path context for ambiguous prop names should track the active union branch out-of-band.
The two modes expose the failure discriminator on different fields. In the
new default "return" mode, prefer the result guards over inspecting a
kind field directly — handler-emitted failures do not carry a synthesized
kind. In the deprecated "throw" mode, the discriminator lives on
(err as PortableToolExecutionError).details.kind ("validation" or
"domain"). The guards operate on a PortableToolResult; they are not
designed to be called on the pi adapter's wire object.
hostExtrasPortableTool.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.
import { Type } from "typebox";
import { definePortableTool } from "@feniix/bridgekit";
export const generateSummaryTool = definePortableTool({
name: "generate_summary",
title: "Generate Summary",
description: "Summarise a block of text.",
parameters: Type.Object({ text: Type.String() }),
execute(args) {
return { text: args.text.slice(0, 80) };
},
hostExtras: {
pi: {
// Fires once before TypeBox validation runs. Lets the pi host show a
// "Processing..." signal without a custom registration wrapper.
pendingMessage: "Summarising...",
promptSnippet: "Use this tool when the user asks for a short summary.",
promptGuidelines: ["Prefer < 80 chars.", "Strip markdown."],
},
mcp: {
annotations: { readOnlyHint: true },
},
},
});
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".
import { realpathSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { type CreateMcpServerOptions, runMcpStdioServer } from "@feniix/bridgekit/mcp";
import { createTools } from "./tools.js";
export function createMcpServerOptions(): CreateMcpServerOptions {
return {
name: "my-tools",
version: "0.1.0",
tools: createTools(),
instructions: "Use these tools when text needs processing.",
};
}
export async function runServer(): Promise<void> {
await runMcpStdioServer(createMcpServerOptions());
}
function realpathIfPossible(path: string): string {
try {
return realpathSync(path);
} catch {
return path;
}
}
if (process.argv[1] && realpathIfPossible(resolve(process.argv[1])) === realpathIfPossible(fileURLToPath(import.meta.url))) {
await runServer();
}
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.
Tool parameters must resolve to a JSON-Schema object at the top level. Type.Object(...) is the common case; Type.Intersect([Type.Object(...), Type.Object(...)]) of object schemas is also accepted (its allOf lowering is recognised, and type: "object" is synthesised onto the tools/list response so MCP clients that validate the inputSchema shape stay happy). Non-object top-level schemas (Type.String(), Type.Union([Type.Object(...), Type.Object(...)]), etc.) throw at server construction with a named-tool error so the failure surfaces at adapter setup, not at first tools/call.
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.
The two adapters now read in parallel: invalid args and portable isError results return { isError: true } from both hosts by default, and the same result-guard helpers (isValidationFailure, isDomainFailure) narrow them on either side.
Default portable tools accept the built-in host union:
type BuiltIn = "pi" | "mcp" | "test";
Custom adapters opt in explicitly:
import { Type } from "typebox";
import { definePortableTool, type PortableToolHost } from "@feniix/bridgekit";
const params = Type.Object({ text: Type.String() });
type CustomHost = "custom-runtime";
export const customTool = definePortableTool<typeof params, CustomHost>({
name: "custom_echo",
title: "Custom Echo",
description: "Echoes text in a custom runtime.",
parameters: params,
execute(args, ctx) {
const host: CustomHost = ctx.host;
return { text: `${host}: ${args.text}` };
},
});
const hostValue: PortableToolHost<CustomHost> = "custom-runtime";
void hostValue;
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.
Tool definition best practices:
Type.Object(...) schemas so MCP can expose input schemas directly.text for model-visible output and structuredContent for machine-readable data.isError: true for expected/domain failures that should be represented as tool output.ctx.signal in long-running tools.ctx.progress?.(...) for incremental updates.createTools() factory instead of a module-level singleton so each host runtime gets isolated state.execute; use a permissive schema plus domain validation if you need custom guidance for structurally invalid input.Package and release checklist:
.d.ts declarations for runtime entrypoints.exports, main, and types aligned with built files.dependencies.workspace: or file: dependency ranges in publishable packages.sourceMappingURL comments: publish maps and useful sources together, or disable source maps for package builds.chmod +x or equivalent), and is included by npm pack --dry-run --json.bin/ over pointing bin directly at dist/; the wrapper should resolve the package-local generated file and may run the package-local build for workspace/local execution.>=22.19.0) in downstream packages that expose BridgeKit-powered MCP bins.npm run check, npm run test, npm run pack:dry-run, and npm run package-smoke before publishing.docs/releasing.md as the future release handoff; this repository is not configured for automated publish yet.See examples/README.md for complete copyable examples.
Read these files in order:
README.md — public API, contracts, and best practices.llms.txt — compact agent-facing usage rules and anti-patterns.examples/README.md — copyable layouts for shared tools, pi extensions, MCP stdio servers, and custom hosts.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.FAQs
BridgeKit defines TypeBox-backed tools once and adapts them to pi, MCP, and other hosts.
The npm package @feniix/bridgekit receives a total of 60 weekly downloads. As such, @feniix/bridgekit popularity was classified as not popular.
We found that @feniix/bridgekit demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.

Security News
Rolldown paused Rust React Compiler integration after a 5MB binary size increase raised concerns about shipping React-specific code to all Vite users.

Security News
/Research
Mini Shai-Hulud expands into the Go ecosystem after hitting LeoPlatform npm packages and targeting GitHub Actions workflows.