---
title: Tool Router
description: Route tool calls and raw HTTP requests through a managed session. Auth is injected server-side, so your agent never touches provider credentials.
---

import { Callout } from "fumadocs-ui/components/callout";

The **Tool Router** is the layer that turns a CodeSpar session into a live tool-access endpoint. Once a session exists, your agent can:

- **Execute registered tools** via `session.execute(tool, params)`, the standard path for anything in the catalog.
- **Proxy raw HTTP calls** via `session.proxyExecute(...)`, for endpoints that don't have a pre-defined tool yet, or when you need provider-specific fields the meta-tools don't expose.
- **Stream as MCP**: the session URL is consumable by any MCP-compatible client (Claude Desktop, Cursor, VS Code).

In every case, **credentials stay on the server**. Your agent sees only the session id; the backend injects the right API key, OAuth token, or certificate per provider and logs the call.

## Why a router

Most commerce workflows touch 3 to 6 providers. Without a router, each call forces the agent code to:

1. Hold the provider credential (security and compliance headache).
2. Format the request in the provider's native shape.
3. Handle rate limits, retries, and credential rotation per provider.
4. Log, audit, and bill the call manually.

With the router, the agent holds one short-lived CodeSpar API key, calls `session.execute` or `session.proxyExecute`, and the backend takes care of the rest.

## Three ways to call a tool

Connect as many providers as the workflow needs: connecting more widens the set the router can pick from and fail over to, and naming `<server>/<tool>` pins one call to one provider. What `execute` can reach is the project's connections; the session's `servers` list gates `proxyExecute` only.

### 1. Meta-tool (the default)

```ts
const result = await session.execute("codespar_pay", {
  method: "pix",
  amount: 15000, // minor units: R$ 150,00
  currency: "BRL",
  description: "Order #1234",
  recipient: "maria@example.com", // a Pix key
});
```

The [14 meta-tools](/docs/concepts/meta-tools) route to a provider the router picks per call, from the providers this project has connected whose catalog line matches the rail, currency and country, and whose money direction matches the meta-tool it was called on. The agent never names the provider: `method`, `rail`, `country` and `currency` are what steer it. The same `codespar_charge` reaches Asaas for `method: "pix"` in BR and Stripe ACP for `method: "card"` in US; when nothing the project has connected covers the combination, the call comes back `router error: eligibility_empty` instead of guessing. The router freezes a fallback list at decision time, so a provider that degrades is retried on the next one with the same idempotency key.

### 2. Raw provider tool

```ts
const result = await session.execute("asaas/create_payment", {
  customer: "cus_000006543210",
  billingType: "PIX",
  value: 249.0,
  dueDate: "2026-10-15",
});
```

The name is `<server>/<tool>` exactly as the catalog spells it, and the body is the provider's own field shape, not ours. Use it when the meta-tool abstraction is too loose. Credentials are still injected server-side, and a provider the project has not connected is refused with `not_connected` before any request leaves.

### 3. Proxy execute (raw HTTP)

```ts
const result = await session.proxyExecute({
  server: "stripe",
  endpoint: "/v1/charges",
  method: "POST",
  body: {
    amount: 1000,
    currency: "usd",
    source: "tok_visa",
  },
});

if (result.status === 201) {
  console.log("charge id:", (result.data as { id: string }).id);
}
```

For anything not yet covered by a registered tool: new provider endpoints, beta APIs, or one-off integrations. The backend still injects auth for the named `server`, rate-limits, and writes an audit log.

<Callout type="warn">
Don't put API keys, bearer tokens, or signed headers in `request.headers`. The backend injects provider credentials automatically. Headers you pass are forwarded as request metadata only (e.g. `Idempotency-Key`, `X-Trace-Id`).
</Callout>

## Request shape

```ts
interface ProxyRequest {
  server: string;                 // e.g. "stripe", "asaas"
  endpoint: string;               // e.g. "/v1/charges"
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
  body?: unknown;                 // JSON body (ignored for GET)
  params?: Record<string, string | number | boolean>;  // query string
  headers?: Record<string, string>;  // request metadata, NOT auth
}
```

## Response shape

```ts
interface ProxyResult {
  status: number;                     // upstream HTTP status
  data: unknown;                      // parsed JSON, or string if non-JSON
  headers: Record<string, string>;    // response headers, lowercased
  duration: number;                   // upstream call duration (ms)
  proxy_call_id?: string;             // log cross-reference id
}
```

On a non-2xx backend response (your session expired, the server isn't connected, rate limit), `proxyExecute` throws. The `status` field on the result object is the **upstream** status: a proxied `401` from Stripe is a successful proxy call that returned 401.

## MCP endpoint

The router sits behind MCP too. A client connects to the [hosted server](/docs/concepts/meta-tools) with one URL and gets the same 14 meta-tools; the session is provisioned on the `initialize` handshake and every `tools/call` goes through the router exactly as an SDK `session.execute()` does. A session you created through the SDK also answers JSON-RPC at `POST /v1/sessions/:id/mcp`, which is the route `session.tools()` uses.

## Observability

Every call through the router, whether `execute`, `proxyExecute`, or MCP-routed, is logged with:

- The session id, user id, and account id
- The tool or endpoint called and the parameters
- Upstream latency, status, and byte count
- Retry attempts and final outcome

Read them back with `GET /v1/sessions/:id/tool-calls`, which returns the newest rows for a session (see [Debugging](/docs/debugging)). There is no log-streaming endpoint on the API today.

## Operating the router

Three dashboard pages cover day-to-day router operations.

### Health rollup

**/dashboard/health** is the at-a-glance status surface. It rolls up six checks: database, credential store, embeddings coverage for discovery, FX-rate freshness, router telemetry activity, and your provider connections. An overall status is derived from them:

- `down`: the database or credential store is unreachable. The router cannot operate.
- `degraded`: a non-critical check is off nominal, e.g. stale FX rates, idle telemetry, or partial connections. The router operates with reduced quality.
- `healthy`: every check is nominal.

Status transitions emit `system.health.degraded` and `system.health.recovered` events through the standard [triggers](/docs/concepts/triggers) pipeline, so you can page on them instead of watching the dashboard. The page deep-links to **/dashboard/triggers** with the event filter pre-filled.

### Per-provider metrics

**/dashboard/router** shows one row per provider and canonical tool pair: attempts, success rate, mean upstream latency over the last 24 hours (an arithmetic mean, not a median), the last error code, and a 24-hour latency sparkline. It answers "is it the provider or my dispatch" during an incident. On a healthy rail you should see success rates of 99% or better and an empty last-error column. The same error code repeating row after row usually means credential drift or a provider-side breaking change; confirm on the connections panel of **/dashboard/health** first, then the provider's status page. Zero attempts on a rail you expect traffic on usually means the tenant has no connection for that provider, so the dispatcher cannot pick it.

### Candidates triage

**/dashboard/router-candidates** is the triage surface for adding new rails to the router. It consumes a classifier-generated report of catalog tools that look like routing candidates for a meta-tool, and lets you mark each candidate as claimed (you will hand-curate the transforms and add the rail), deferred (revisit next sweep), or rejected (wrong classification), then export the claimed set as JSON for hand-curation. The page is browser-only: the report you drop on it never round-trips through the backend, and triage state persists in `localStorage` keyed by the report's checksum, so a reload does not erase your progress.

## Next steps

<NextStepsGrid items={[
  { label: "CONCEPT", title: "Sessions", description: "How a session is created, scoped, and managed.", href: "/docs/concepts/sessions" },
  { label: "CONCEPT", title: "Authentication", description: "Connect Links and provider OAuth flows.", href: "/docs/concepts/authentication" },
  { label: "CONCEPT", title: "Tools & Meta-Tools", description: "The 14 meta-tools and the raw provider catalogs behind them.", href: "/docs/concepts/tools" },
  { label: "COOKBOOK", title: "Pix Payment Agent", description: "A Pix charge routed through the router, end to end.", href: "/docs/cookbooks/pix-payment-agent" },
]} />
