> This page is for Agent Handler.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.merge.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.merge.dev/_mcp/server.

# Custom MCP servers

> How to register a remote MCP server with Agent Handler, from the dashboard or the API, so its tools show up alongside the built-in Connectors.

When the Connector catalog doesn't cover what you need (an in-house service, a niche SaaS, a tool you've built yourself), you can register a remote MCP server as a custom Connector. Agent Handler discovers its tools automatically and exposes them through the same MCP URL your agent already calls.

The trade-off: you operate the MCP server yourself. Agent Handler doesn't host third-party code; you point it at your endpoint and Agent Handler proxies calls through.

You can register a server from the dashboard or from the API. Use the dashboard for a one-off server. Use the API when you provision Connectors from a script or per environment, or keep them in version control, so a new environment gets the same servers without anyone clicking through a form.

## What's supported

* Remote MCP servers reachable at a public HTTPS URL that complete an MCP `initialize` handshake.
* MCP protocol revisions `2025-03-26` and `2024-11-05`. Agent Handler offers `2025-03-26` first and falls back to `2024-11-05` once for a server that won't negotiate it.
* Static authentication, a single bearer token or API key configured once at Connector creation.
* OAuth 2.0 with PKCE, when enabled for your organization. A Connector can then authenticate each user through the Link flow instead of a shared token, and Agent Handler can register an OAuth client with your server automatically through dynamic client registration (RFC 7591). This is gated per organization, so contact the Merge team to turn it on. OAuth Connectors are registered from the dashboard only; the API accepts no authentication or a static header.

## What's not supported

* **Local MCP servers.** CLI-launched servers (`npx <package>`) need to run on the same machine as the agent. Agent Handler is server-side and can't reach them.

## Registering a server from the dashboard

1. Open [Connectors](https://ah.merge.dev/connectors) and click **Add new**.
2. Fill in:
   * **Name.** Unique across your org's Connectors, and not one of the Connectors Merge already ships. The check is global rather than per organization, so `salesforce` is taken even if you have never enabled it.
   * **Remote MCP server address.** Your server's HTTPS endpoint.
   * **Authorization token.** The bearer token your server expects. Check your server's docs for the format.
3. Save. Agent Handler runs `initialize` against the server, then `tools/list`, and populates the Connector's tool list.

If the initial discovery fails, the Connector is created but with zero tools, and the dashboard shows the error. Common causes: HTTPS isn't reachable from Agent Handler's network, the token format is wrong, the server never completes `initialize`, or it negotiates a protocol revision Agent Handler doesn't support.

## Registering a server with the API

`POST /api/v1/connectors/` registers a remote MCP server in one call. It needs an [access key](/merge-agent-handler/administer/access-keys) with the `management:all` scope.

The API is stricter than the dashboard about failures. Agent Handler completes the MCP handshake and `tools/list` before it writes anything, so the call either returns a Connector with its tools already discovered or creates nothing. A script never has to clean up a half-registered Connector with zero tools.

### A server with no authentication

```bash
curl -X POST https://ah-api.merge.dev/api/v1/connectors/ \
  -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Docs",
    "url": "https://mcp.acme.com/mcp"
  }'
```

`auth` defaults to `{"type": "none"}`, so you can leave it out.

### A server that expects an API key in a header

Send the key in `auth.header`. Agent Handler stores the value encrypted at the organization level and never returns it, and every user of the Connector reaches the server with that one key.

```bash
curl -X POST https://ah-api.merge.dev/api/v1/connectors/ \
  -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Docs",
    "slug": "acme_docs",
    "url": "https://mcp.acme.com/mcp",
    "auth": {
      "type": "secret",
      "header": {
        "key": "Authorization",
        "value": "Bearer acme_live_8f2c1e"
      }
    }
  }'
```

The same request in Python:

**`register_mcp_server.py`**

```python register_mcp_server.py
import os

import requests

response = requests.post(
    "https://ah-api.merge.dev/api/v1/connectors/",
    headers={"Authorization": f"Bearer {os.environ['MERGE_AGENT_HANDLER_API_KEY']}"},
    json={
        "name": "Acme Docs",
        "slug": "acme_docs",
        "url": "https://mcp.acme.com/mcp",
        "auth": {
            "type": "secret",
            "header": {"key": "Authorization", "value": os.environ["ACME_MCP_TOKEN"]},
        },
    },
    timeout=30,
)
response.raise_for_status()
connector = response.json()
print(connector["slug"], [tool["name"] for tool in connector["tools"]])
```

A `201` returns the Connector with its discovered tools:

```json
{
  "id": "084fce8e-43d3-472a-8227-e28553e73aa0",
  "name": "Acme Docs",
  "slug": "acme_docs",
  "description": "",
  "source_url": "https://mcp.acme.com/mcp",
  "categories": [],
  "auth_options": [],
  "source": "external_mcp",
  "tools": [
    { "name": "search_docs", "description": "Search the Acme documentation corpus.", "credit_type": "simple" },
    { "name": "get_page", "description": "Fetch a single documentation page by id.", "credit_type": "simple" }
  ]
}
```

### Request fields

| Field     | Required | Notes                                                                                                                                                                                                                                                       |
| --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`    | Yes      | Display name, up to 100 characters                                                                                                                                                                                                                          |
| `url`     | Yes      | Your server's HTTPS endpoint. It can't point at localhost, a private network, or a reserved IP address, and the API has no call to change it after registration                                                                                             |
| `slug`    | No       | The Connector's identifier: 1 to 255 lowercase letters, digits, or underscores. Derived from `name` when you leave it out, so `Acme Docs` becomes `acme_docs`. Send one when your code needs a stable identifier that won't change if you rename the server |
| `auth`    | No       | `{"type": "none"}` (the default) or `{"type": "secret", "header": {"key": ..., "value": ...}}`. `oauth` returns a `400`                                                                                                                                     |
| `headers` | No       | Up to 20 extra non-secret headers sent on every request to your server. They're write-only, so keep your own record of them. Don't repeat the `auth.header` key here                                                                                        |

The slug follows the same naming rules as the dashboard. It can't match a Connector Merge already ships or a server in the [generic MCP catalog](/merge-agent-handler/connectors/generic/all-generic-mcp-servers). To use a catalog server, add it from the Connectors page instead.

### Retries and repeat calls

The endpoint is safe to retry after a timeout you can't confirm. An identical repeat call for a Connector with no authentication returns `200` and the existing Connector rather than creating a second one. Anything else that targets an existing slug returns `409`, including a repeat of a `secret` Connector, because Agent Handler can't tell a retry from a key rotation and won't report a rotation that didn't happen.

| Status | Meaning                                                                                                                                                  | What to do                                                                            |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `201`  | Registered, tools discovered                                                                                                                             | Add the Connector to a Tool Pack                                                      |
| `200`  | An identical Connector already exists                                                                                                                    | Nothing; you have the existing Connector                                              |
| `400`  | A field failed validation, and the body names the field. Or the handshake with your server failed, and the `error` field says why                        | Fix the input or the server. Each request in the handshake times out after 10 seconds |
| `409`  | The slug is taken by a different configuration, or this server is already registered under another slug. The `connectors` field lists the slugs involved | Delete the existing Connector first, or reuse it                                      |

Registration is limited to 60 requests a minute per organization. Each successful registration is recorded in the [audit trail](/merge-agent-handler/observe/audit-trail) as `CONNECTOR_IMPORTED`.

The full schema is in the API reference: [Register a remote MCP server](/merge-agent-handler/agent-handler/connectors/connectors-create).

## Using the custom Connector

Once tools are populated, the Connector behaves like any built-in:

* Add it to a [Tool Pack](/merge-agent-handler/build/tools/tool-packs).
* Apply [description overrides](/merge-agent-handler/build/tools/tool-description-overrides) and [input overrides](/merge-agent-handler/build/tools/tool-input-overrides) per pack.
* Calls flow through the [Security Gateway](/merge-agent-handler/secure/security-gateway) like any other tool call.
* Show up in [Tool Call Logs](/merge-agent-handler/observe/tool-call-logs) the same way.

The only difference is that responses come from your server, not from a third-party API Agent Handler manages.

## Tool discovery and refresh

Agent Handler refreshes the tool list periodically. When you add or remove tools on your server, they show up in Agent Handler within a few minutes.

For a manual refresh, open the Connector and click **Resync**. Agent Handler re-queries `tools/list` immediately.

## Rotating the auth token

When you rotate the static token on your server, update Agent Handler's stored copy at the same time.

From the dashboard:

1. Open the Connector → **Application Credentials**.
2. Replace the token.
3. Save.

The API has no update call for a registered server. Sending the same registration again with a new key returns `409` rather than replacing the old key. Rotating through the API means deleting the Connector and registering it again, and deleting takes the Connector out of its Tool Packs (see the next section), so you add it back afterward. For a Connector already in use, the dashboard is the shorter path. Use the API path for servers your own provisioning code owns end to end.

Token rotation isn't graceful. Calls in flight at the moment of rotation fail if they hit Agent Handler with the old token still loaded. For high-volume Connectors, time the rotation for low-traffic windows or accept brief failure.

## Deleting a server

`DELETE /api/v1/connectors/{slug}/` removes a custom Connector and the credentials stored for it, and returns `204`. Only Connectors your organization registered can be deleted this way. A Connector Merge ships, or one that belongs to another organization, returns `404`.

```bash
curl -X DELETE https://ah-api.merge.dev/api/v1/connectors/acme_docs/ \
  -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY"
```

> **Deleting takes the Connector out of its Tool Packs**
>
> A successful delete also removes the Connector from every Tool Pack that included it, and agents using those packs lose its tools. Check which packs use the Connector before you delete it.

The exception is a Tool Pack that sets an auth scope on the Connector. Then the delete is refused with `409`, and the response names the packs holding it:

```json
{
  "error": "The connector acme_docs is still in the tool packs Support agent. Remove it with DELETE /api/v1/tool-packs/{tool_pack_id}/connectors/acme_docs/ for each one, then delete the connector.",
  "tool_packs": [
    { "id": "5b1d6f0e-2c4a-4e8b-9a7d-3f6c2e1b8a90", "name": "Support agent" }
  ]
}
```

Remove it from each listed pack with [Delete a Connector from a Tool Pack](/merge-agent-handler/agent-handler/tool-packs/delete-connector), then repeat the delete. Each deletion is recorded in the audit trail as `CONNECTOR_DELETED`. The schema is in the API reference: [Delete a remote MCP server](/merge-agent-handler/agent-handler/connectors/connectors-destroy).

## When this isn't the right fit

Custom MCP servers are good for internal tools that already speak MCP and for niche third parties where you can run a thin MCP shim in front of their API. Don't reach for it to wrap heavy custom logic that should live in your agent, or in place of [requesting a Connector](https://www.merge.dev/get-in-touch) for a third-party SaaS we'd build anyway. Per-user OAuth is not a reason to avoid a custom server either, since a custom Connector can authenticate each user through Link once the capability is enabled for your organization.

## Next

Forward Agent Handler events to your own pipeline with [Webhooks](/merge-agent-handler/observe/webhooks).