> Raw Markdown twin (generated at build time from the source Markdown). Rendered page: https://docs.gatellm.io/en/quickstart/mcp-proxy · Doc index: https://docs.gatellm.io/en/llms.txt


# Proxying external tools with MCP

The MCP (Model Context Protocol) gateway aggregates external tool servers into a single entry point: callers use one gateway access key to call multiple tools, and it supports tool ACLs and context budgets per key group. This chapter walks you through adding an external MCP server (a filesystem MCP example) and calling it from Claude Desktop via the gateway's `/mcp` endpoint to see the tools proxied.

## What you'll build

- An external MCP server connected to the gateway (e.g. filesystem MCP)
- A gateway access key that can call the gateway from any MCP client
- Call tools via the gateway from Claude Desktop and see the tool list + call results

## Prerequisites

- The gateway is running (`http://localhost:7890`) and you can log in to the Console
- `ENCRYPTION_KEY` is set — the access key saved below is encrypted before being stored; if it's not set, saving reports `encryption_key not set in config`. See [Docker single node → Prerequisites](/en/quickstart/docker-single-node.md#prereq)
- A running external MCP server using **streamable HTTP or SSE transport** (the example below uses `http://localhost:8000/mcp`).

  > The gateway does **not** support stdio / command / args / env / OAuth style MCP servers — only streamable HTTP / SSE / REST-bridge transports. The official `@modelcontextprotocol/server-*` packages are mostly stdio and cannot be connected directly.
  >
  > **No ready-made HTTP MCP server?** Wrap a stdio server into streamable HTTP with a bridge tool. For example, use [supergateway](https://github.com/supercorp-ai/supergateway) to bridge the filesystem server (one command starts it on port 8000 at path `/mcp`, matching the example below):
  >
  > ```bash
  > npx -y supergateway \
  >   --stdio "npx -y @modelcontextprotocol/server-filesystem /tmp" \
  >   --outputTransport streamableHttp
  > ```
  >
  > Once started, the endpoint is `http://localhost:8000/mcp`. It's the same with other HTTP/SSE MCP servers — just replace the endpoint below with its actual address.

## 1. Add an MCP server

Console → **MCP Servers** → **New**:

| Field | Value | Description |
|------|-----|------|
| Name | `files` | Unique; used as the tool prefix, regex `^[a-z0-9][a-z0-9_-]*$` |
| Description | filesystem MCP | For UI display |
| Transport (transport) | `streamable_http` | Or `sse` |
| Connection type (connection_type) | `stateless` | Stateless tool server |
| Endpoint (endpoint) | `http://localhost:8000/mcp` | The upstream tool server URL (for streamable HTTP, fill in the specific path) |
| Auth header (auth_header) | (leave empty if the upstream MCP requires no auth) | The upstream `Authorization` header (e.g. `Bearer ...`) |
| Tags (tags) | `["dev-tools"]` | Coarse-grained ACL filtering |
| Priority (priority) | `0` | 0–999, smaller = higher priority; used for context-budget trimming |
| Idle timeout (idle_timeout_secs) | `300` | Default 300 |
| Deferred loading (defer_loading) | `false` (default) | Set to true to load the tool schema only on first call |
| Health check (health_check) | (optional) e.g. `{ "path": "/health", "interval_secs": 60, "timeout_secs": 5 }` | Probes liveness periodically when configured. **`path` must be an endpoint the upstream actually provides** — if the upstream has no `/health` but you set it, probes keep failing, the server is judged dead, and its tools become invisible. Leave empty if unsure |
| Concurrency cap (max_concurrency) | (as needed) | Per-access-key concurrency cap for this server |
| Rate limit (rate_limit) | (as needed) | `requests_per_second` / `tokens_per_minute` |
| Enabled | ✓ | |

Save. The tool name format is `{server_name}.{tool_name}`, e.g. `files.read_file`, `files.list_directory`.

## 2. Configure the key group's MCP ACL

Tool permissions are configured in the **key group's** `mcp_tool_acl` (not on the MCP server). Console → **Access Keys** → **Key Groups** → Edit → MCP sub-tab (shown only when at least one MCP server exists):

| Field | Value |
|------|-----|
| `allowed_tools` | `["files.*"]` (allow all tools of the files server) |
| `denied_tools` | (leave empty) |
| `allowed_tags` | (leave empty) |

```json
{
  "name": "default",
  "models": ["*"],
  "mcp_tool_acl": {
    "allowed_tools": ["files.*"],
    "denied_tools": []
  }
}
```

ACL evaluation order: `denied` (highest) → `allowed_tools` → `allowed_tags` → default. The "default" at the end is **allow**: when both `allowed_tools` and `allowed_tags` are empty, anything not hit by `denied_tools` is allowed; once any allow list is configured, it switches to whitelist mode, allowing only hits.

## 3. Issue an access key

Console → **Access Keys** → **New**:

| Field | Value | Description |
|------|-----|------|
| Name | `my-mcp-key` | The key's identifier, used for management and audit |
| API key | Click "Generate" | Auto-generates a string; the credential clients send when calling |
| Group | `default` | Determines which models this key can access |
| Enabled | ✓ | |

## 4. Call from Claude Desktop

Claude Desktop's MCP configuration (macOS path `~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "gatellm": {
      "url": "http://localhost:7890/mcp",
      "headers": {
        "Authorization": "Bearer <your access key>"
      }
    }
  }
}
```

Restart Claude Desktop and ask Claude in the chat to "list the /tmp directory"; Claude will automatically call the `files.list_directory` tool, and the gateway proxies the request to the upstream MCP server.

## 5. Or use the Python mcp SDK

```python
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def main():
    async with streamablehttp_client(
        "http://localhost:7890/mcp",
        headers={"Authorization": "Bearer <your access key>"}
    ) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print([t.name for t in tools.tools])
            result = await session.call_tool("files.list_directory", {"path": "/tmp"})
            print(result)

asyncio.run(main())
```

## JSON-RPC methods

The gateway's `/mcp` endpoint supports:

| Method | Description |
|------|------|
| `initialize` | Handshake, exchange capabilities |
| `tools/list` | List available tools (filtered by that key group's MCP ACL), including the built-in `search_tools` meta-tool |
| `tools/call` | Call a tool, named as `{server_name}.{tool_name}` |
| `tools/search` | BM25 keyword search over tools (for locating when there are many tools) |

## FAQ

**Q: Claude Desktop doesn't show the tool list?**
Check: ① whether the MCP server is enabled; ② whether `defer_loading` is on (the schema loads only on first call, so you need to trigger a `tools/list`); ③ whether the key group's `mcp_tool_acl` filtered it out; ④ whether the upstream MCP server is actually alive; ⑤ if `health_check` is configured, whether `path` is a real upstream endpoint — if you set a path the upstream doesn't have, probes keep failing, the server is judged dead, and its tools become invisible (leave `health_check` empty if unsure).

**Q: Tool calls often time out?**
Adjust that server's `max_concurrency` (concurrency cap) and `rate_limit`, or check the upstream MCP server's own response speed. Each MCP server has independent circuit-breaker protection — consecutive failures **trip the breaker and pause forwarding to that server** (not "temporarily open"), and it's automatically allowed again after health recovers.

**Q: What does the `.` prefix in a tool name (e.g. `files.read_file`) mean?**
It's the two-part `{server_name}.{tool_name}` naming, avoiding collisions between same-named tools of different servers.

**Q: How do I make a key group unable to use MCP at all?**
In the key group's MCP sub-tab, leave `allowed_tools` empty and `allowed_tags` empty, or don't check any tools.

**Q: If an MCP server goes down, does it affect other tools?**
No. Each server has independent connection management and circuit-breaker protection. When one server is unavailable, its tools are excluded from the list and other servers keep working.

**Next**: [Onboard an external MCP server](/en/howto/onboard-external-mcp.md) for the full operations (add/list/detail/delete); [MCP configuration](/en/reference/mcp-config.md) for tool ACL, context budget, REST bridge, and other field details; [Endpoints · auth · protocol interop](/en/reference/endpoints.md) for the full endpoint list.
