Custom HTTP tools
A custom HTTP tool gives one agent a tool backed by your own HTTP endpoint. When the agent’s model calls the tool during a job, BridgeMCP sends the call to your endpoint as a JSON request and returns your response to the model as the tool result. You write one HTTP handler, not an MCP server.
Where custom HTTP tools are available
- On one agent. Each tool belongs to a single agent in your organization. That agent’s model can call it while it runs jobs.
- In sub-agents. A sub-agent defined in the agent’s bundle can use the agent’s custom HTTP tools, unless its
AGENT.mdfrontmatter sets atoolslist that leaves them out. See the Agent Bundle specification. - Not in Virtual MCPs. Custom HTTP tools are not exposed through Virtual MCPs: they never appear in a Virtual MCP’s
tools/list, and MCP clients cannot call them. To give MCP clients access to your API, run an MCP server for it and add it as a connector. - With or without tool access. They work whether or not the agent has a Virtual MCP selected under Tool access.
- Loaded when a job starts. The agent’s enabled tools are loaded once at the start of each job, so creating, editing, enabling, disabling or deleting a tool affects jobs that start afterwards. A running job keeps the tools it started with.
Create a tool
- In the console, open Agents and select the agent.
- Open the Tools tab. Under Custom HTTP tools, select New tool.
- Fill in the fields below and select Save tool.
The fields:
- Name: 1 to 128 letters, digits, hyphens or underscores, unique within the agent. The model calls the tool by this name. If the name matches a built-in agent tool (
skill,taskor one of theworkspace_tools such asworkspace_read_file) or a tool from the agent’s Virtual MCP, the custom tool is left out of the job without notice, so give it a distinctive prefix such asacme_lookup_order. - Description: what the model reads to decide when and how to call the tool. Say what the tool does, when to use it and what each argument means.
- Endpoint URL: the single absolute
httporhttpsURL that every call is sent to, unchanged. It must be reachable from BridgeMCP. The URL is checked when the tool is called, not when you save it: other schemes, and hosts written as literal loopback, private or link-local IP addresses, are refused. Prefer HTTPS so arguments and responses are encrypted in transit. - Input schema (JSON Schema): a JSON Schema object describing the tool’s arguments. The model receives it as the tool’s parameter schema. BridgeMCP does not validate the model’s arguments against it, so validate them in your endpoint. The schema must be valid JSON; a blank schema is saved as
{}. - Signing secret: optional. When set, BridgeMCP signs every request so your endpoint can confirm it came from BridgeMCP (see Verify the signature). The secret is stored encrypted and never shown again.
- Enabled: only enabled tools are loaded into new jobs.
An input schema for an order lookup tool:
{
"type": "object",
"properties": {
"orderId": { "type": "string", "description": "Order ID, for example A-1042" }
},
"required": ["orderId"]
} Each saved tool appears as a card with an enabled or disabled badge, a signed badge when it has a signing secret, and Disable or Enable, Edit and Delete actions. The Tool access setting at the top of the same tab selects the agent’s Virtual MCP and is separate from custom HTTP tools.
The request BridgeMCP sends
The request is always an HTTP POST to the tool’s endpoint URL, with a JSON body and headers that include:
Content-Type: application/json; charset=utf-8X-Bridge-Signature, only when the tool has a signing secret
A tool has no settings for authentication headers, other custom headers or URL parameters, and every call goes to the endpoint URL unchanged. Use the signature to confirm that a call came from BridgeMCP.
A call to a tool named lookup_order whose endpoint URL is https://api.example.com/tools/lookup-order:
POST /tools/lookup-order HTTP/1.1
Host: api.example.com
Content-Type: application/json; charset=utf-8
X-Bridge-Signature: sha256=cde0d7b2bef7f025e88557d614a28f0730e540fdef0dc5dd2662f34b6ff9dd34
{"agentId":"01JR7M2K9Q4X8T6V3N5B1C0D2E","arguments":{"orderId":"A-1042"},"invocationId":"01K74Z9B3H6N2Q8R5T1V7W4X0Y","jobId":"01K74Z8Y2G5M1P7S4D9F3H6J0K","orgId":"01JQ3A8C5E1G7J2K9M4N6P0R3S","timestamp":"2026-10-09T14:03:27.1234567\u002B00:00","toolName":"lookup_order"} The body is one line of compact JSON with these fields:
| Field | Type | Meaning |
|---|---|---|
agentId | string | ID of the agent running the job. |
arguments | object | The arguments the model passed, with keys exactly as the model sent them. They are not validated against the input schema. |
invocationId | string | Unique ID for this call. Every call gets a new one, including a repeat of an earlier call. |
jobId | string | ID of the job the call belongs to. |
orgId | string | ID of the organization that owns the agent. |
timestamp | string | When BridgeMCP built the request, in UTC ISO 8601 with seven fractional digits and a +00:00 offset. |
toolName | string | The tool’s configured name. |
The IDs are 26-character ULID strings. Do not depend on field order, whitespace or escaping; parse the body with a JSON parser. BridgeMCP escapes characters such as +, <, & and non-ASCII letters as \uXXXX, so the timestamp’s +00:00 arrives as \u002B00:00. Any JSON parser decodes it.
Verify the signature
When a tool has a signing secret, X-Bridge-Signature is sha256= followed by the lowercase hex HMAC-SHA256 of the exact request body bytes, keyed with the UTF-8 bytes of the secret. To verify a request:
- Read the raw body bytes before parsing them. Re-serializing the parsed JSON does not reproduce the signed bytes, because escaping and formatting can differ.
- Compute the HMAC-SHA256 of those bytes with your secret, encode it as lowercase hex and prefix
sha256=. - Compare the result with the header in constant time. Reject the request if the header is missing or does not match.
- After parsing, reject requests whose
timestampis more than a few minutes away from your clock, to limit replays. You can also remember recentinvocationIdvalues and reject repeats.
To test your verifier, use the example request above: with the secret example-signing-secret, that exact body produces that exact X-Bridge-Signature header.
An endpoint for the lookup_order tool (Node.js 18 or later, Express 4.17 or later):
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const secret = process.env.BRIDGE_TOOL_SECRET;
const app = express();
// The signature covers the exact bytes BridgeMCP sent, so verify the raw body
// before parsing it. Re-serialized JSON does not reproduce those bytes.
function hasValidSignature(rawBody, header) {
if (!Buffer.isBuffer(rawBody) || typeof header !== "string") return false;
const expected = Buffer.from(
"sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex"),
);
const received = Buffer.from(header);
return received.length === expected.length && timingSafeEqual(received, expected);
}
app.post("/tools/lookup-order", express.raw({ type: "application/json" }), async (req, res) => {
if (!hasValidSignature(req.body, req.get("X-Bridge-Signature"))) {
return res.status(401).send("invalid signature");
}
const call = JSON.parse(req.body.toString("utf8"));
if (!(Math.abs(Date.now() - Date.parse(call.timestamp)) < 5 * 60 * 1000)) {
return res.status(401).send("stale request");
}
const orderId = call.arguments?.orderId;
if (typeof orderId !== "string") {
return res.json({ content: "orderId must be a string", isError: true });
}
const order = await findOrder(orderId); // your own lookup
if (!order) {
return res.json({ content: `No order ${orderId}`, isError: true });
}
return res.json({ content: `Order ${orderId} is ${order.status}`, isError: false });
});
app.listen(3000); Changing or removing the secret:
- When you edit a tool, leaving Signing secret blank keeps the current secret.
- A new secret applies to jobs that start after you save it. Jobs that are already running keep signing with the old secret, so accept both the old and the new secret until those jobs finish.
- There is no option to clear a secret. To stop signing, delete the tool and create it again without one.
- If a stored secret can no longer be decrypted, BridgeMCP sends the request without
X-Bridge-Signature. An endpoint that rejects requests with a missing header refuses those calls instead of trusting them.
What your endpoint returns
Respond with a 2xx status and a JSON object:
{ "content": "Order A-1042 is shipped", "isError": false } content(string) is the tool result the model reads. It is text only, so serialize structured data into the string, for example as JSON.isError(boolean) marks the result as a failure. When it istrue, the model receives an error that includes yourcontent.- Property names are matched case-insensitively and extra properties are ignored. A missing
contentis treated as an empty string and a missingisErrorasfalse. - A 2xx response with an empty body is an empty, successful result.
Errors, timeouts and retries
A failed call does not fail the job. BridgeMCP returns a tool result that starts with Error: custom tool '<name>', and the model decides what to do next.
| What happens | What the model receives |
|---|---|
| Your endpoint returns a non-2xx status. | An error with the status code and the first 500 characters of the response body. |
Your endpoint returns 2xx with "isError": true. | An error that includes your content. |
Your endpoint returns 2xx with a body that is not a JSON object, or whose content is not a string or isError is not a boolean. | A malformed response body error. |
| No complete response arrives within 30 seconds. | A timeout error. |
| The connection, DNS lookup or TLS handshake fails. | A request failed error with the reason. |
The endpoint URL is not an absolute http or https URL, or its host is a literal loopback, private or link-local IP address. | An invalid endpoint error. Nothing is sent. |
- 30 second timeout. Each call has 30 seconds to connect, send the request and read the full response. The limit is not configurable, so return quickly. For long-running work, start it in the background and return an acknowledgement.
- No automatic retries. BridgeMCP does not retry a failed call. The model may call the tool again, and every call has a new
invocationId, so make side effects safe to repeat, for example by keying them on the arguments. - Cancellation. If the job is cancelled or reaches its timeout, BridgeMCP cancels the request in flight.
- Visibility. Error bodies and
contentreach the model and are recorded in the job session, so keep secrets out of them.
Security notes
- Use HTTPS, set a signing secret and verify every request as described above.
- Treat
argumentsas untrusted input. The model chooses them, and job input such as a webhook payload can steer the model. - Authorize each call in your endpoint, for example by checking that
orgIdandagentIdare the organization and agent you expect. Both are covered by the signature, so you can rely on them once it verifies. - Scope each endpoint to the narrowest operation the agent needs.
Next steps
- Agents and jobs explains the job lifecycle, workspaces and sessions.
- Webhook triggers covers starting agent jobs from inbound events.
- Connectors and Virtual MCPs covers giving agents and MCP clients tools from MCP servers.