Building an agent

Wire Agent Handler into a customer-facing product so each customer connects their own third-party accounts.

This is the setup guide for Building an agent - your customers use your product, your product runs an agent, and each customer connects their own Salesforce, Slack, GitHub, and the rest. Agent Handler holds their credentials, scoped per Registered User, and your backend orchestrates everything.

If you’re building a tool for your own employees, see Context layer for employees instead. For a comparison, see Use cases.

What you’re building

By the end of this guide your stack will look like this:

  • Your backend creates one Registered User per customer user when they sign up for your product.
  • A Tool Pack in Agent Handler defines what tools your agent can call.
  • When a customer needs to authenticate a Connector, your frontend opens Link. Credentials get stored against the customer’s Registered User.
  • When the agent runs, your backend hits Agent Handler’s MCP endpoint with the Registered User’s ID in the URL and your API key in the header. The MCP URL never reaches the browser.

Step 1: Get your API key

Production keys live at Settings → API Keys. You start with one production key and any number of test keys; test keys are sandboxed and isolated from production data. Use a test key while you build; switch when you’re ready for real customers.

Never expose the API key client-side. It authorizes calls on behalf of every one of your customers’ Registered Users. Keep it in your backend secret store.

Step 2: Create a Tool Pack

Tool Packs decide what tools your agent can call. Create one in the Tool Packs dashboard, add the Connectors you need, and pick the specific tools to expose (most Connectors have 30–80 tools; you usually want a subset). Copy the pack’s ID from the URL - you’ll need it on every MCP call.

For one-vs-many packs, lifecycle, and overrides, see Tool Packs.

Step 3: Create a Registered User

Create one Registered User per customer user, the moment they sign up for your product. The origin_user_id you pass is your own user ID - that’s how you find this Registered User later.

$curl -X POST https://ah-api.merge.dev/api/v1/registered-users \
> -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "origin_user_id": "user_a3f9b2",
> "origin_user_name": "Alice Chen",
> "origin_user_email": "[email protected]",
> "shared_credential_group": {
> "origin_company_id": "company_acme",
> "origin_company_name": "Acme Inc."
> }
> }'
1{
2 "registered_user_id": "f9813dd5-e70b-484c-91d8-00acd6065b07"
3}

Store registered_user_id against your user record. The API call is idempotent on origin_user_id - calling it twice returns the existing Registered User rather than creating a duplicate.

The shared_credential_group is optional. Pass it when you want to Group users by tenant - typical in B2B products where one customer org has multiple users. See Companies and Groups for the details.

When a customer needs to authenticate a Connector, your frontend opens Link. There are two ways the flow gets triggered:

  • At agent runtime. The model tries to call a tool the user hasn’t authenticated. The Connector returns an authenticate_meta tool result with a link_token. Your frontend catches it and opens Link.
  • Pre-flight, from a settings page. The user clicks “Connect Slack” in your UI. Your backend generates a link token and your frontend opens Link.

Pre-flight is more common; users prefer to set up integrations once instead of being interrupted mid-conversation.

$curl -X POST https://ah-api.merge.dev/api/registered-users/$REGISTERED_USER_ID/link-token \
> -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "Connector": "slack" }'
1{
2 "link_token": "ltk_•••••",
3 "expires_at": "2026-05-04T18:30:00Z"
4}

Link tokens are short-lived (30 minutes) and single-use. Generate one per Link session.

Install the React component:

$npm install @mergeapi/react-agent-handler-link

Open Link with the token your backend just minted:

ConnectButton.tsx
1import { useAgentHandlerLink } from "@mergeapi/react-agent-handler-link";
2
3export function ConnectSlackButton({ linkToken }: { linkToken: string }) {
4 const { open, isReady } = useAgentHandlerLink({
5 linkToken,
6 onSuccess: () => {
7 // Credentials are now stored against the Registered User.
8 // Refresh your UI, or kick off the agent action.
9 },
10 onExit: (error) => {
11 // User closed Link or auth failed.
12 // `error` is null on a clean exit, populated on failure.
13 },
14 });
15
16 return (
17 <button disabled={!isReady} onClick={() => open()}>
18 Connect Slack
19 </button>
20 );
21}

For the authenticate_meta flow, intercept the tool call result in the agent’s response stream and feed the link_token field straight into the same component. See Link.

For email-driven authentication or any context where rendering a React component isn’t an option, use Magic Link instead - your backend mints a signed URL, you send it to the user, they authenticate in their own browser.

Step 5: Call MCP from your backend

When the agent runs, your backend constructs the MCP URL and calls Agent Handler:

https://ah-api.merge.dev/api/v1/tool-packs/<TOOL_PACK_ID>/registered-users/<REGISTERED_USER_ID>/mcp

A minimal Python client using the official MCP SDK:

agent_runtime.py
1import os
2from mcp.client.session import ClientSession
3from mcp.client.streamable_http import streamablehttp_client
4
5API_KEY = os.environ["MERGE_AGENT_HANDLER_API_KEY"]
6TOOL_PACK_ID = "tp_..."
7
8async def run_agent_for_user(registered_user_id: str, prompt: str) -> str:
9 url = (
10 f"https://ah-api.merge.dev/api/v1/tool-packs/{TOOL_PACK_ID}"
11 f"/registered-users/{registered_user_id}/mcp"
12 )
13 headers = {"Authorization": f"Bearer {API_KEY}"}
14
15 async with streamablehttp_client(url, headers=headers) as (read, write, _):
16 async with ClientSession(read, write) as session:
17 await session.initialize()
18 tools = await session.list_tools()
19 # Hand `tools` to your model; route any tool calls back through:
20 # await session.call_tool(name, arguments)
21 ...

If you’re building a custom MCP client (for example, in a language without an SDK), you’re speaking JSON-RPC 2.0 over HTTP. See MCP integration for the wire-level walkthrough.

What can go wrong

A few real failure modes worth anticipating.

Tool call returns 403 with unauthorized_tool. The tool isn’t in the Registered User’s Tool Pack. Check the Tool Pack contains the tool, and that you’re calling the right Tool Pack ID in the URL.

Tool call returns 401. Your API key is invalid, expired, or you’re using a test key against a production Registered User (or vice versa). Test and production data are isolated.

Tool call returns reauth_required. The user’s stored OAuth token is expired or revoked. Generate a fresh link token for the same Connector and reopen Link. The user re-authenticates; the call retries cleanly.

Tool call result is missing fields. The Security Gateway redacted them. Check the Alerts dashboard to see which rule fired. If you want the data through unredacted for one Tool Pack, add an override.

Link won’t open. Most often the link token expired (30-minute window) or it’s being reused. Generate a new token and open Link with that one.

For a fuller error catalog, see Troubleshooting.

Production checklist

Before you flip the switch on real customers:

  • Production API key created, rotated, and stored in your secret manager.
  • Tool Pack reviewed: the only tools in it are ones your agent should be able to call.
  • Standard Entity Rules configured for your data sensitivity profile (PII at minimum; PHI if you’re in healthcare; payment data if you handle cards).
  • Allowed callback origins set in Settings → API Keys for every domain that hosts your frontend.
  • Webhook subscriptions wired up if you want tool-call events in your own pipeline.
  • Tool-call logs spot-checked. You should be able to find a recent call in seconds.

Next

Read Tool Packs for the full tool-pack lifecycle, or jump to Security Gateway to set up your data rules.