Source: https://docs.cumuluslabs.io/sdk-server
Content version: 0c3e6260

# Customer HTTP adapters

An agent tool describes an action; your application implements it. Import the HTTP adapters from `@cumulus/sdk/server` to turn that implementation into an authenticated endpoint with input validation, a deadline and output validation.

For example, an agent can ask your service desk for repair guidance. Cumulus sends the tool arguments to your server, your handler returns a result, and the agent uses that result in its next response. Your database and business rules stay in your application.

## Implement and serve a tool

Save this as `tools-server.mjs` in a project with SDK 0.3.4 installed. It defines a read-only contract and serves its implementation on Node's HTTP server.

```javascript
import { createServer } from 'node:http';
import { defineTool } from '@cumulus/sdk/agent';
import { implementTool, createToolHandler, nodeHandler } from '@cumulus/sdk/server';

const guide = defineTool({
  key: 'service_guide',
  description: 'Read service guidance for a repair request.',
  input: {
    type: 'object',
    properties: { topic: { type: 'string', enum: ['repair'] } },
    required: ['topic'], additionalProperties: false,
  },
  output: {
    type: 'object',
    properties: { guidance: { type: 'string' } },
    required: ['guidance'], additionalProperties: false,
  },
  execution: {
    type: 'http', binding: 'customer_api',
    path: '/cumulus/tools/guide', method: 'POST', timeoutMs: 3000,
    idempotency: { mode: 'effect_key' }, onUncertain: 'failure_edge',
  },
});

const implementation = implementTool(guide, async ({ topic }, context) => {
  // Replace this read-only lookup with your application service.
  context.signal.throwIfAborted();
  return { guidance: `For ${topic}, ask which equipment needs attention.` };
});

const handleGuide = createToolHandler(implementation, {
  auth: {
    scheme: 'bearer',
    secret: process.env.CUSTOMER_TOOL_SECRET,
    integration: 'service_guide',
  },
});

createServer(nodeHandler(async request => {
  if (new URL(request.url).pathname !== '/cumulus/tools/guide') {
    return new Response('Not found', { status: 404 });
  }
  return handleGuide(request);
})).listen(8787, '127.0.0.1');
```

Set `CUSTOMER_TOOL_SECRET` to a development secret of at least 16 characters, then run `node tools-server.mjs`. Send a local request in another terminal with the same secret:

```bash
curl --fail-with-body http://127.0.0.1:8787/cumulus/tools/guide \
  -H "Authorization: Bearer $CUSTOMER_TOOL_SECRET" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: example-effect-0001' \
  -d '{"args":{"topic":"repair"},"call":{"call_id":"example-conversation","run_id":"example-run","agent_id":"example-agent"}}'
```

The response is `{"guidance":"For repair, ask which equipment needs attention."}`. Remove the bearer header to get `401`; send a different topic to get `400`. These requests exercise your endpoint without placing a call.

In TypeScript, `implementTool` infers argument and result types from the tool's schemas. Reuse the same tool definition in your graph and server so they agree on the contract.

To receive Cumulus requests, deploy this route behind public HTTPS on port 443 and bind `customer_api` to it. Give the server the secret bytes through your secret manager; give Cumulus the matching pinned secret reference. See [bindings](/docs/sdk-bindings). With Express, mount `nodeHandler` before `express.json()` so signature verification receives the original body.

## Understand the request context

POST tools receive `{ args, call: { call_id, run_id?, agent_id, metadata? } }`. GET tools receive query arguments; their context has no conversation, run or agent ID, and `metadata` is empty. The adapter passes your handler this context:

| Field | Use |
| --- | --- |
| `integration` | The integration name configured on your server. |
| `conversationId` | The request's `call_id`; conversation correlation. |
| `runId` | The optional `run_id`; use this for run API lookups. |
| `agentId`, `metadata` | Request context for your workflow. |
| `effectId` | The `Idempotency-Key` header, required for `effect_key` tools. |
| `signal` | Cancellation and the tool deadline; pass it to work that supports cancellation. |

Authentication establishes who can call the endpoint. Your business service must still authorize the account, booking or payment being changed. Metadata and a caller's phone number do not establish that authorization. If your workflow needs an authoritative run lookup, reject requests without `runId`.

## Handle effects and failures

The read-only lookup above can safely return the same result again. A tool that books a visit needs durable effect handling: save its intent and outcome under `(integration, effectId)`, reject reuse with different input, and return the saved result for a duplicate. The [tutorial application](/docs/sdk-tutorial) includes a SQLite repository that retains duplicate results across restarts.

The adapter requires an effect key; it does not store or deduplicate business work for you. A timeout, cancellation or invalid output can occur after an external operation succeeded. Reconcile that operation before executing it again.

| Response | Meaning |
| --- | --- |
| `405` | The method does not match the tool contract. |
| `401` | Authentication failed. |
| `400` | The envelope, identity or arguments are invalid. |
| `500` | Execution failed or its outcome is uncertain. |

The default body limit is 65,536 bytes; `maxBodyBytes` changes it. Avoid logging raw bodies or provider errors containing private data.

## Receive initial context and lifecycle events

Use `createInboundContextHandler(resolve, { auth, variables })` for initial string variables on an inbound call. The callback receives the full event envelope and a cancellation signal:

```json
{
  "event": "call_inbound",
  "call_inbound": {
    "agent_id": "example-agent",
    "agent_version": 1,
    "from_number": "+12025550123",
    "to_number": "+12025550124"
  }
}
```

For example, this handler returns a declared context variable. Add its route to the Node server above:

```javascript
import { createInboundContextHandler } from '@cumulus/sdk/server';

const handleContext = createInboundContextHandler(async (input, signal) => {
  signal.throwIfAborted();
  // Read caller and destination from input.call_inbound for your authorized lookup.
  return { dynamic_variables: { demo_channel: 'phone' } };
}, {
  auth: {
    scheme: 'bearer', secret: process.env.CUSTOMER_TOOL_SECRET,
    integration: 'service_guide',
  },
  variables: ['demo_channel'],
});
```

Route `/cumulus/context` to `handleContext(request)` before the server's not-found response. The response is `{ "call_inbound": { "dynamic_variables": { "demo_channel": "phone" } } }`. Return only declared string variables; the resolver can also return `{ reject: true }` to reject the inbound call. The default deadline is three seconds; `timeoutMs` changes it.

Use `createEventHandler({ secret, consume })` for signed lifecycle notifications. It verifies `x-talos-signature` against the raw body and awaits `consume` before acknowledging. Store the event and deduplicate its ID before returning. If processing needs a background job, save that job intent in the same transaction. The signing secret needs at least 32 characters; the default freshness window is five minutes. Set `maxBodyBytes: 262144` when creating this handler to accept the full lifecycle-event size supported by Cumulus; the adapter defaults to 65,536 bytes. See the complete [HTTP callback contract](/docs/sdk-http-contracts#lifecycle-events), including raw-byte signature verification.

`run.ended`, `recording.updated` and `analysis.updated` arrive independently. Fetch each corresponding resource when your workflow needs it. The [tutorial server](/docs/sdk-tutorial) combines tool, context and event routes. Private keypad input uses the [dedicated tokenization adapter](/docs/sdk-private).

## Adapter reference

| Export | Responsibility |
| --- | --- |
| `implementTool`, `createToolHandler` | Connect and serve a tool contract. |
| `createInboundContextHandler` | Serve declared initial context variables. |
| `createEventHandler`, `verifyWebhook` | Receive or verify Cumulus lifecycle events. |
| `verifyRetellSignature` | Verify the separate Retell-compatible tool signature. |
| `createTokenizationHandler` | Serve the private capture contract. |
| `nodeHandler`, `readBounded` | Adapt to Node HTTP; read a bounded body. |
| `AuthenticationError`, `ToolOutcomeUncertain`, `TokenizationDeclined` | Distinguish authentication failure, uncertain execution and definite vault refusal. |
