# @feniix/bridgekit

Use this package when you need to define a tool once and expose it through both pi and MCP hosts.

## Read order for agents

1. `README.md` — public API, contracts, best practices, packaging notes.
2. `llms.txt` — compact agent-facing usage rules and anti-patterns.
3. `examples/README.md` — copyable end-to-end layouts for shared tools, pi extension wiring, MCP stdio server wiring, and custom hosts.
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.

## Import map

```ts
import {
  definePortableTool,
  executePortableTool,
  isDomainFailure,
  isValidationFailure,
} from "@feniix/bridgekit";
import { registerPiTools } from "@feniix/bridgekit/pi";
import { createMcpServer, runMcpStdioServer } from "@feniix/bridgekit/mcp";
```

- Root entrypoint: host-neutral tool definitions, validation, and execution helpers.
- `/pi`: pi adapter only.
- `/mcp`: MCP server adapter only.

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.

## Core pattern

```ts
import { Type } from "typebox";
import { definePortableTool } from "@feniix/bridgekit";

export const echoTool = definePortableTool({
  name: "echo",
  title: "Echo",
  description: "Echo text.",
  parameters: Type.Object({ text: Type.String() }),
  execute(args, ctx) {
    return {
      text: args.text,
      structuredContent: { text: args.text, host: ctx.host },
    };
  },
});

export function createTools() {
  return [echoTool];
}
```

## Best practices

- Keep portable tools host-neutral. Do not import pi or MCP SDKs from files that define tool behavior.
- 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.
- Return `{ text, structuredContent }` for successful results.
- Use `isError: true` for domain/tool-level failures that should reach the model as tool output.
- Throw only for unexpected programmer, adapter, or runtime failures.
- Respect `ctx.signal` in long-running tools.
- Use `ctx.progress?.(...)` for incremental progress updates when the host supports them.
- Wire hosts explicitly: pi registration and MCP server startup are separate adapter calls.
- Keep modules import-passive. Do not register tools, start servers, read package metadata, read env vars, or touch files at import time.
- For stateful tools, export a `createTools()` factory instead of a module-level `tools` singleton so each pi extension instance or MCP stdio process gets isolated state.
- TypeBox validation runs before the portable handler. If you need custom guidance for missing fields or wrong primitive types, use a permissive schema plus domain validation; otherwise accept BridgeKit's structural validation result.
- MCP stdio entrypoints should be testable: export a server-options factory and a run function, then start stdio only when executed as the package bin.
- Keep package runtime Node >=22.19.0 and ESM-only.

## Host behavior

- 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` and narrow with `isValidationFailure` / `isDomainFailure` from `@feniix/bridgekit`.
- MCP invalid arguments and portable `isError: true` results return `CallToolResult` with `isError: true`. Same guards narrow the result.
- 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.
- 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.
- 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`.
- `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.
- `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.

### Result-guard usage

```ts
const result = await executePortableTool(tool, args, { host: "test" });
if (result.isError) {
  if (isValidationFailure(result)) {
    // validationErrors is typed: Array<{ field: string; message: string }>.
    for (const { field, message } of result.structuredContent.validationErrors) {
      log.warn({ tool: result.structuredContent.tool, field, message }, "bad args");
    }
  } else if (isDomainFailure(result)) {
    // Handler-emitted failure; result.structuredContent is whatever the
    // handler chose. No synthesized `kind` discriminator.
    metrics.increment("tool.domain_error", { tool: tool.name });
  }
}
```

## Custom host typing

Default tools accept only built-in hosts: `"pi" | "mcp" | "test"`.
For a custom adapter, opt in explicitly:

```ts
const customTool = definePortableTool<typeof params, "custom-host">({
  // ...
  execute(args, ctx) {
    const host: "custom-host" = ctx.host;
    return { text: host };
  },
});
```

Use `PortableToolHost<"custom-host">` when a value may be either a built-in host or that extension.

## Mixed source-loaded hosts and compiled MCP bins

Some hosts, such as pi in source-extension packages, may intentionally load TypeScript source while MCP clients launched from npm need compiled JavaScript. In that case:

- Keep the host source entrypoint if that is the package convention, e.g. `pi.extensions: ["./extensions/index.ts"]`.
- Add a package-local MCP build and point npm `bin` at a checked-in wrapper under `bin/`, not directly at generated `dist/` output.
- The wrapper should resolve the package-local generated MCP server, run the package-local build when output is missing in workspace/local execution, and preserve non-zero build failures.
- Narrow the MCP build to the MCP entrypoint and shared host-neutral modules; avoid compiling pi adapter entrypoints into the standalone MCP path.
- Put runtime imports used by the compiled MCP bin in `dependencies`, not only peers or dev dependencies.
- Declare BridgeKit's Node engine requirement in the downstream package (`>=22.19.0`).
- Ensure the checked-in wrapper is executable (`chmod +x` or equivalent) and verify the mode with `npm pack --dry-run --json`.
- Test wrapper behavior for existing output, missing output, failed builds, and builds that exit successfully without creating the expected file.

## Anti-patterns

- Do not create a separate pi implementation and MCP implementation for the same logic.
- Do not make tool files read package metadata, environment variables, files, or network resources at import time.
- Do not start MCP stdio at module top level in files that tests need to import.
- Do not expose unsupported high-level MCP helpers such as `registerMcpTools` unless tests prove compatibility with the installed MCP SDK.
- Do not return host-specific response shapes from portable tool handlers.
- Do not use `workspace:` or `file:` dependency ranges for packages intended to install from npm.
