Appearance
Fresh 2026
MCP integration
How to connect agents to Agent Handler over MCP: config-file setup for Claude Desktop, Cursor, Windsurf, and VS Code, plus a reference custom client.
Agent Handler exposes its tools over MCP. Most agent runtimes - Claude Desktop, Cursor, Windsurf, VS Code, ChatGPT - speak MCP natively, so connecting them is a matter of pasting a URL into a config file. If you're building your own agent runtime, this page also covers the wire protocol.
Prerequisites
You need three things, all from your dashboard.
- Tool Pack ID. From the Tool Packs page; copy from the URL of the pack you want to expose.
- Registered User ID. From the Registered Users page; use a test user for development.
- Access Key. Production key for production users, test key for test users - they don't mix. See Access Keys.
The MCP URL
Every MCP connection points at this URL pattern:
https://ah-api.merge.dev/api/v1/tool-packs//registered-users//mcpThe URL identifies what tools (the Tool Pack) and whose credentials (the Registered User). The Access Key in the Authorization header authorizes the call. All three values are needed on every connection.
For the Agent Handler for Employees setup, there's a simplified URL that handles Tool Pack and user resolution through SSO instead.
Connect an MCP client
Open Settings → Developer → Edit Config and paste:
json
{
"mcpServers": {
"agent-handler": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://ah-api.merge.dev/api/v1/tool-packs//registered-users//mcp",
"--header",
"Authorization: Bearer ${AUTH_TOKEN}"
],
"env": {
"AUTH_TOKEN": ""
}
}
}
}Restart Claude Desktop. The first launch downloads mcp-remote (Node 20+ required).
Edit ~/.cursor/mcp.json (global) or .cursor/mcp.json (project):
json
{
"mcpServers": {
"agent-handler": {
"url": "https://ah-api.merge.dev/api/v1/tool-packs//registered-users//mcp",
"headers": {
"Authorization": "Bearer "
}
}
}
}Reload Cursor. Verify in Cursor Settings → MCP.
Edit ~/.codeium/windsurf/mcp_config.json:
json
{
"mcpServers": {
"agent-handler": {
"serverUrl": "https://ah-api.merge.dev/api/v1/tool-packs//registered-users//mcp",
"headers": {
"Authorization": "Bearer "
}
}
}
}Edit .vscode/mcp.json (workspace) or run MCP: Open User Configuration for global:
json
{
"servers": {
"agent-handler": {
"type": "http",
"url": "https://ah-api.merge.dev/api/v1/tool-packs//registered-users//mcp",
"headers": {
"Authorization": "Bearer "
}
}
}
}Most MCP clients accept a variant of:
json
{
"mcpServers": {
"agent-handler": {
"url": "https://ah-api.merge.dev/api/v1/tool-packs//registered-users//mcp",
"headers": {
"Authorization": "Bearer "
}
}
}
}If your client doesn't support custom headers, use the mcp-remote wrapper as a proxy (see the Claude Desktop tab - same pattern applies elsewhere).
Build a custom MCP client (SDK)
Use the official MCP SDK when your agent is a custom runtime. Anthropic ships an SDK in both Python and TypeScript - both handle the JSON-RPC framing, session ID generation, and streaming response handling.
python
import asyncio
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client
API_KEY = ""
TOOL_PACK_ID = ""
REGISTERED_USER_ID = ""
async def run():
url = (
f"https://ah-api.merge.dev/api/v1/tool-packs/{TOOL_PACK_ID}"
f"/registered-users/{REGISTERED_USER_ID}/mcp"
)
headers = {"Authorization": f"Bearer {API_KEY}"}
async with streamablehttp_client(url, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print(tools)
result = await session.call_tool(
"weather__get_forecast",
{"location": "Stockholm", "days": 3},
)
print(result)
asyncio.run(run())typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const API_KEY = "";
const TOOL_PACK_ID = "";
const REGISTERED_USER_ID = "";
const url = new URL(
`https://ah-api.merge.dev/api/v1/tool-packs/${TOOL_PACK_ID}` +
`/registered-users/${REGISTERED_USER_ID}/mcp`,
);
const transport = new StreamableHTTPClientTransport(url, {
requestInit: {
headers: { Authorization: `Bearer ${API_KEY}` },
},
});
const client = new Client({ name: "agent-runtime", version: "1.0.0" });
await client.connect(transport);
const tools = await client.listTools();
console.log(tools);
const result = await client.callTool({
name: "weather__get_forecast",
arguments: { location: "Stockholm", days: 3 },
});
console.log(result);For custom transport behavior or a runtime that has no SDK, build directly against the wire protocol below.
Build a custom MCP client (raw protocol)
MCP is JSON-RPC 2.0 over HTTP. Three core methods cover the agent surface.
python
import uuid
import aiohttp
class MCPClient:
def __init__(self, url: str, api_key: str):
self.url = url
self.session_id = str(uuid.uuid4())
self.request_id = 0
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"Mcp-Session-Id": self.session_id,
}
async def _send(self, method: str, params: dict | None = None) -> dict:
self.request_id += 1
payload = {"jsonrpc": "2.0", "id": self.request_id, "method": method}
if params:
payload["params"] = params
async with aiohttp.ClientSession() as http:
async with http.post(self.url, headers=self.headers, json=payload) as resp:
if "Mcp-Session-Id" in resp.headers:
self.session_id = resp.headers["Mcp-Session-Id"]
self.headers["Mcp-Session-Id"] = self.session_id
resp.raise_for_status()
return await resp.json()
async def initialize(self):
return await self._send("initialize", {"protocolVersion": "2024-11-05"})
async def list_tools(self):
return await self._send("tools/list")
async def call_tool(self, name: str, arguments: dict):
return await self._send("tools/call", {"name": name, "arguments": arguments})typescript
import { randomUUID } from "node:crypto";
export class MCPClient {
private sessionId: string;
private requestId = 0;
private headers: Record;
constructor(
private url: string,
apiKey: string,
) {
this.sessionId = randomUUID();
this.headers = {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
"Mcp-Session-Id": this.sessionId,
};
}
private async send(method: string, params?: Record) {
this.requestId += 1;
const payload: Record = {
jsonrpc: "2.0",
id: this.requestId,
method,
};
if (params) payload.params = params;
const resp = await fetch(this.url, {
method: "POST",
headers: this.headers,
body: JSON.stringify(payload),
});
const newSessionId = resp.headers.get("Mcp-Session-Id");
if (newSessionId) {
this.sessionId = newSessionId;
this.headers["Mcp-Session-Id"] = newSessionId;
}
if (!resp.ok) {
throw new Error(`MCP request failed: ${resp.status}`);
}
return resp.json();
}
initialize() {
return this.send("initialize", { protocolVersion: "2024-11-05" });
}
listTools() {
return this.send("tools/list");
}
callTool(name: string, args: Record) {
return this.send("tools/call", { name, arguments: args });
}
}The session ID rotates on Agent Handler's side - read it back from the response header and use the new value for subsequent requests.
For streaming responses (large tool outputs), set Accept: text/event-stream and parse the response as Server-Sent Events. The SDKs handle this automatically; the raw clients above don't.
Custom headers
Any header you send with an X- prefix is captured as metadata on the tool call and shown in the Tool Call Logs. Useful for tracing - set X-Chat-Id to your session ID and you can filter logs to one conversation. See Custom headers for MCP.
Common issues
- Tools not appearing in the client. Restart the client after editing config. Some clients cache aggressively; a hard restart usually fixes it. Check the client's MCP log for connection errors.
401on every call. Double-check theAuthorizationheader format (Beareris required and case-sensitive) and that the API key matches the Registered User's environment.- Session ID mismatch errors. Capture and re-use the session ID from response headers. Don't generate a new one per request.
For a full troubleshooting catalog, see Troubleshooting.
Next
For command-line tool search and execution, use the Merge CLI.