{"_id":"@andersonbrdev/agentbridge","name":"@andersonbrdev/agentbridge","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@andersonbrdev/agentbridge","version":"0.1.0","description":"Define an AI agent tool once, call it in-process or serve it over MCP.","license":"MIT","author":{"name":"andsu-dev"},"type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"bun build ./src/index.ts --outdir dist --target node --format esm --packages=external && tsc --emitDeclarationOnly --outDir dist","test":"bun test","typecheck":"tsc --noEmit","example":"bun run examples/basic.ts","bench":"bun run examples/bench.ts"},"dependencies":{"@modelcontextprotocol/sdk":"^1.12.0","zod":"^3.24.0"},"devDependencies":{"@types/bun":"latest","typescript":"^5.7.0"},"gitHead":"c9e91ac8f4832052dc2f0d0494b10c28babb2c63","_id":"@andersonbrdev/agentbridge@0.1.0","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-UxhFlVNxphl8pw7xiYBSBYNar6JtA73G69ziZle/Q6cSkUF7YJzVFU2tGUwDJo1h3PDZSr1wWVq8YNlrp7Y3Iw==","shasum":"ff39f9f345939c57259bb50e532423fb187d79e6","tarball":"https://registry.npmjs.org/@andersonbrdev/agentbridge/-/agentbridge-0.1.0.tgz","fileCount":13,"unpackedSize":24325,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD9iywZK6IWe0sCTgHJEbjHBDCDcuKj9CcQZBXoWUmdLwIhAL1hPjvD5yzn/69mF23+c4PJ4maoH76wLk6OxFUX46HV"}]},"_npmUser":{"name":"andersonbrdev","email":"contato.medeirosdev@gmail.com"},"directories":{},"maintainers":[{"name":"andersonbrdev","email":"contato.medeirosdev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/agentbridge_0.1.0_1785928828333_0.07705695858303341"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-05T11:20:27.963Z","0.1.0":"2026-08-05T11:20:28.476Z","modified":"2026-08-05T11:20:28.727Z"},"maintainers":[{"name":"andersonbrdev","email":"contato.medeirosdev@gmail.com"}],"description":"Define an AI agent tool once, call it in-process or serve it over MCP.","author":{"name":"andsu-dev"},"license":"MIT","readme":"# agentbridge\n\n**Write your AI agent's tool once. Use it everywhere that agent shows up.**\n\n## The problem\n\nSay your product has an MCP server, so external agents (Claude, or whatever your users plug in) can call things like `search_creators` or `create_campaign`. Now say you're also building your own in-house copilot, a chat agent that lives inside your app and calls those exact same capabilities.\n\nYou now have two implementations of `search_creators`. One lives in the MCP server. One lives in your copilot's tool loop. They started out identical. Six months later, someone fixes a bug in one and forgets the other. Or worse, one of them forgets to check which tenant is asking, and now enterprise A can see enterprise B's data through whichever path nobody was looking at.\n\nThis isn't a hypothetical. It's the exact shape of bug that shows up whenever the same capability has two independent doors into it.\n\n## The fix\n\nDefine the tool once. Get both doors for free.\n\n```ts\nimport { z } from \"zod\";\nimport { defineTool, createToolCatalog } from \"agentbridge\";\n\nconst searchCreators = defineTool({\n  name: \"search_creators\",\n  schema: z.object({ niche: z.string() }),\n  handler: async (input, ctx) => {\n    // ctx.tenantId and ctx.jwt are already resolved, no way to forget them\n    return db.query.creators.findMany({ where: eq(creators.enterpriseId, ctx.tenantId) });\n  },\n});\n\nconst catalog = createToolCatalog({ tools: [searchCreators] });\n```\n\nNow call it directly, from your own copilot's loop. `call()` never throws for a tool failure, it returns `{ data, error }`, so a bad call is something you check, not something you have to remember to catch:\n\n```ts\nconst { data, error } = await catalog.call(\"search_creators\", { niche: \"beleza\" }, { tenantId, jwt });\nif (error) {\n  // error.code, error.message, error.retryable are all typed and stable\n  return handleFailure(error);\n}\nreturn data;\n```\n\nOr hand the whole catalog to an MCP client, like Claude Desktop or Claude Code:\n\n```ts\nawait catalog.stdio({ tenantId, jwt });\n```\n\nOr serve it remotely, over HTTP, resolving tenant fresh from each incoming request, works on Node, Bun, and Cloudflare Workers, since it's built on the standard `Request`/`Response`:\n\n```ts\nconst handle = catalog.http((req) => ({\n  tenantId: getTenantFromJWT(req.headers.get(\"authorization\")),\n  jwt: req.headers.get(\"authorization\") ?? \"\",\n}));\n\nBun.serve({ port: 3000, fetch: handle });\n```\n\nSame handler, same validation, same tenant context, every time. There's no second copy to drift.\n\n## See it work\n\n```bash\nbun install\nbun run example   # calls the same tool in-process and over a real MCP stdio server, prints both\nbun run bench     # throughput of the in-process path\n```\n\n`bun run example` defines `search_creators` once, calls it directly, then spawns a real MCP server and calls it again through an actual `@modelcontextprotocol/sdk` client over stdio. Both calls print the exact same result, that's the whole point, made visible.\n\n```\nin-process: { tenant: \"acme\", creators: [ \"@beleza_creator_1\", \"@beleza_creator_2\" ] }\nvia MCP:    { tenant: \"acme\", creators: [ \"@beleza_creator_1\", \"@beleza_creator_2\" ] }\n```\n\n`bun run bench` measures the in-process path's own overhead (Zod validation + dispatch, no MCP round-trip), on a laptop it lands around 4M calls/sec, ~0.24µs each. The library isn't the bottleneck in any real handler; the point of the benchmark is just to confirm that's true rather than assume it.\n\n## Catching cross-tenant leaks\n\nIf you're multi-tenant, `ctx.tenantId` reaching every handler isn't enough on its own, a handler can still leak another tenant's data through a bug (missing `WHERE`, wrong id passed down). Declare `tenantField` and the catalog checks every returned record before it leaves the tool, regardless of which door it went out:\n\n```ts\nconst listCreators = defineTool({\n  name: \"list_creators\",\n  schema: z.object({}),\n  tenantField: \"enterpriseId\", // field on the returned records that identifies the tenant\n  handler: async (input, ctx) => db.query.creators.findMany({ where: eq(creators.enterpriseId, ctx.tenantId) }),\n});\n```\n\nIf a handler ever returns a record whose `enterpriseId` doesn't match `ctx.tenantId`, the call comes back as `{ data: null, error }` with `error.code === \"TENANT_LEAK\"` instead of letting it reach the caller, MCP client or your own copilot. It's opt-in per tool and only catches leaks the output actually reveals (it can't see a leak baked into a scalar return value with no tenant field), a runtime backstop, not a substitute for correct queries.\n\n## What's specific to agents calling your tools, not humans\n\nAn agent doesn't click buttons, it can call a tool in a loop, and nobody's watching in real time when it does. The rest of the catalog's options exist for the failure modes that come from that:\n\n**Audit trail.** Every call fires `onCall`, whether it succeeded or not, with the input it was called with, the question \"what did the agent actually do to my data\" needs an answer, and it needs to not be a maybe.\n\n```ts\nconst catalog = createToolCatalog({\n  tools: [searchCreators],\n  hooks: {\n    onCall: (event) => logger.info(\"tool_call\", event),\n  }, // { tool, tenantId, input, durationMs, ok, error?, code? }\n});\n```\n\n**Rate limit per tenant.** A buggy agent retrying in a tight loop can do in seconds what a human couldn't do in a day. Opt in per tool:\n\n```ts\nconst searchCreators = defineTool({\n  name: \"search_creators\",\n  schema: z.object({ niche: z.string() }),\n  rateLimit: { max: 20, windowMs: 60_000 }, // per tenant, per tool\n  handler: async (input, ctx) => { /* ... */ },\n});\n```\n\n**Idempotency.** An agent that times out waiting for a response tends to just call again. Without protection, `create_campaign` runs twice. Opt in with `dedupe` and identical `(tool, tenant, input)` within the window returns the same result instead of re-executing. A failed call is never cached, a genuine retry after a real error re-runs the handler:\n\n```ts\nconst createCampaign = defineTool({\n  name: \"create_campaign\",\n  schema: z.object({ name: z.string() }),\n  dedupe: { windowMs: 5_000 },\n  handler: async (input, ctx) => { /* runs once, even if called twice at the same time */ },\n});\n```\n\n**Approval gate.** Some tools shouldn't fire just because an agent decided to call them. Mark a tool `requiresApproval: true` and wire up `hooks.onApprovalNeeded`, the call blocks until it returns `true`. No handler configured means the catalog fails closed, not open:\n\n```ts\nconst deleteCampaign = defineTool({\n  name: \"delete_campaign\",\n  schema: z.object({ id: z.string() }),\n  requiresApproval: true,\n  handler: async (input, ctx) => { /* only runs if approved */ },\n});\n\nconst catalog = createToolCatalog({\n  tools: [deleteCampaign],\n  hooks: {\n    onApprovalNeeded: async ({ tool, tenantId, input }) => askAHuman(tool, tenantId, input),\n  },\n});\n```\n\n**Visibility per tenant.** A tool a plan/tier doesn't have access to shouldn't just fail when called, it shouldn't be something the agent even knows exists. `visibleTo` hides it from `tools/list` entirely, and `call()` rejects it as `UNKNOWN_TOOL` rather than a permission error, so a hidden tool never leaks that it's there:\n\n```ts\nconst advancedAnalytics = defineTool({\n  name: \"advanced_analytics\",\n  schema: z.object({}),\n  visibleTo: (ctx) => isEnterprisePlan(ctx.tenantId),\n  handler: async (input, ctx) => { /* ... */ },\n});\n```\n\n**Shadow mode.** Before trusting a new tool with a real agent, watch what it would do first. `shadow: true` runs the full pipeline (rate limit, approval) but skips the real handler, only `onCall` fires, with `shadow: true` on the event. Flip it off once you trust it:\n\n```ts\nconst releasePayment = defineTool({\n  name: \"release_payment\",\n  schema: z.object({ amountCents: z.number() }),\n  shadow: true, // logs what it would have done, never actually runs\n  handler: async (input, ctx) => { /* real money movement, once you trust it */ },\n});\n```\n\n**Structured, retryable errors.** Every failure the catalog raises (unknown tool, bad input, tenant leak, rate limit, rejected approval) is a `ToolError` with a stable `.code` (`\"RATE_LIMITED\"`, `\"TENANT_LEAK\"`, `\"APPROVAL_REJECTED\"`, ...) and a `.retryable` flag, so an agent can decide to retry instead of guessing from prose. `RATE_LIMITED` also carries `.retryAfterMs`. Over MCP, the code and retry hint are prefixed onto the error text agents see.\n\nAll of the above are opt-in and additive, a tool or catalog with none of these fields set behaves exactly as it did before they existed.\n\n## Why this and not a bigger agent framework\n\n`agentbridge` doesn't orchestrate agents, doesn't manage conversations, and doesn't pick which LLM to call. It does one narrow thing: a tool is defined once and reachable from more than one caller. If you only ever call your tools from one place, you don't need this, a plain function is simpler and you should use that instead.\n\n## Scope today\n\n- `call(name, input, ctx)`, in-process invocation, schema-validated with Zod, returns `{ data, error }` instead of throwing.\n- `stdio(ctx)`, serves the whole catalog as a local MCP server over stdio, the way Claude Desktop and Claude Code expect.\n- `http(resolveTenant)`, a stateless Streamable HTTP handler (`(req: Request) => Promise<Response>`), tenant resolved fresh per request. No session kept between calls, each request gets its own `McpServer` instance, so one tenant's context never leaks into another's, even under concurrent load.\n\nNot built yet: session persistence for the HTTP transport (today it's stateless, every request re-initializes). Add it if you need long-lived streaming sessions instead of simple request/response.\n\n## Install\n\n```bash\nbun add agentbridge\n```\n","readmeFilename":"README.md","_rev":"1-abeb14393ddc525f9fafcb186de464a7"}