One tool definition. In-process calls, MCP over stdio, and remote HTTP — with the same validation, context, and safeguards.
An AI agent can reach the same capability through different doors: an in-process copilot, an MCP server, or a remote API.
When each door has its own implementation, the code eventually drifts. A validation rule is updated in one place but not another. A tenant check is forgotten. A retry creates the same campaign twice.
AgentBridge gives those doors one shared implementation:
┌──────────────────┐
│ Tool definition │
│ schema + handler│
└────────┬─────────┘
│
┌──────────────┼──────────────┐
│ │ │
In-process MCP HTTP
copilot stdio Streamable HTTP
Define the tool once. The catalog takes care of schema validation, tenant context, structured errors, and delivery through the interface your agent needs.
bun add @andersonbrdev/agentbridge
# or
npm install @andersonbrdev/agentbridge
import { z } from "zod";
import { createToolCatalog, defineTool } from "@andersonbrdev/agentbridge";
const searchCreators = defineTool({
name: "search_creators",
schema: z.object({ niche: z.string() }),
handler: async (input, ctx) => {
return db.query.creators.findMany({
where: eq(creators.enterpriseId, ctx.tenantId),
});
},
});
const catalog = createToolCatalog({ tools: [searchCreators] });
call() returns a typed result instead of throwing for tool failures:
const { data, error } = await catalog.call(
"search_creators",
{ niche: "beleza" },
{ tenantId, jwt },
);
if (error) {
return handleFailure(error);
}
return data;
await catalog.stdio({ tenantId, jwt });
Claude Desktop, Claude Code, and other MCP clients can now use the same tool implementation.
const handle = catalog.http((req) => ({
tenantId: getTenantFromJWT(req.headers.get("authorization")),
jwt: req.headers.get("authorization") ?? "",
}));
Bun.serve({ port: 3000, fetch: handle });
The tenant context is resolved from every request. The handler uses standard Request/Response APIs and works with Node, Bun, and Cloudflare Workers.
Use the same validated tool from your own copilot, a local MCP server, or a remote HTTP endpoint. There is no second copy to drift.
Pass tenant context to every handler and optionally validate returned records before they leave the tool:
const listCreators = defineTool({
name: "list_creators",
schema: z.object({}),
tenantField: "enterpriseId",
handler: async (input, ctx) => {
return db.query.creators.findMany({
where: eq(creators.enterpriseId, ctx.tenantId),
});
},
});
If a returned record belongs to another tenant, the call fails with TENANT_LEAK before the data reaches the caller.
Every call can be recorded with onCall, including its tool, tenant, input, duration, result, and error code.
Protect your system from runaway agent loops and duplicate side effects:
rateLimit: { max: 20, windowMs: 60_000 }
dedupe: { windowMs: 5_000 }
Identical calls are coalesced within the configured deduplication window. Failed calls are never cached.
Require human approval before sensitive tools run:
requiresApproval: true
Without an approval hook, the catalog fails closed.
Hide tools that a plan or tenant cannot use. Hidden tools do not appear in tools/list and are rejected as UNKNOWN_TOOL when called directly.
Test how an agent would use a tool without running its real handler:
shadow: true
The full pipeline still runs, but only the audit hook fires.
Failures use stable codes such as RATE_LIMITED, TENANT_LEAK, and APPROVAL_REJECTED. Agents can use the retryable flag and, for rate limits, retryAfterMs.
bun install
bun run example # in-process call + real MCP stdio call
bun run bench # measures in-process dispatch overhead
The example defines search_creators once, calls it directly, and then calls it through a real MCP client. Both paths return the same result.
in-process: { tenant: "acme", creators: ["@beleza_creator_1", "@beleza_creator_2"] }
via MCP: { tenant: "acme", creators: ["@beleza_creator_1", "@beleza_creator_2"] }
Available today:
call(name, input, ctx)— in-process, schema-validated invocation.stdio(ctx)— local MCP server over stdio.http(resolveTenant)— stateless Streamable HTTP handler.
The HTTP transport is stateless. Each request gets a fresh MCP server instance, so tenant context is not shared between concurrent requests.
AgentBridge does not orchestrate agents, manage conversations, or choose an LLM. It solves one focused problem: defining a tool once and making it safely reachable from multiple callers.
If a tool has only one caller, a plain function may be all you need.
Issues, pull requests, tests, documentation, and ideas are welcome.
git clone https://github.com/Andsu-dev/agentbridge.git
cd agentbridge
bun install
bun test
Please open an issue before a larger change so we can align on the direction first.
MIT © Anderson BR Dev
Built in public by @andersonbrdev.