Skip to content

Use MCP Tools Through the Gateway

Every MCP server registered in Brutor gets a governed endpoint on the Core Proxy:

http://localhost:8100/v1/proxy/mcp/{server_id}

Any client that speaks MCP streamable HTTP can connect to it — Claude Desktop, Goose, Cursor, your own SDK code. The client neither knows nor cares that a gateway is in the middle; it just needs the URL and an auth header. Find a server’s ID/endpoint in Admin UI → Resources → MCP Servers → Configuration (the endpoint link on each server).

URL pattern POST http://localhost:8100/v1/proxy/mcp/{server_id} (JSON-RPC 2.0); GET with Accept: text/event-stream opens the SSE listener leg
Transport MCP streamable HTTP (stateful — carry the Mcp-Session-Id from initialize on every subsequent call)
Auth Authorization: Bearer sk_brutor_api_..., X-API-Key: sk_brutor_api_..., or a portal JWT

What governance applies on the way through

Section titled “What governance applies on the way through”
  • Tool filters — the client’s tools/list only shows tools enabled for the caller’s resource group; disabled tools return 403 on call.
  • Approvals — approval-gated tools return 202/428 with an approval_request_id until a group member approves; retry with X-Approval-Token. See the agent guide.
  • Guardrailsmcp_input checks run on tool arguments (secrets, PII egress, banned patterns), mcp_output checks on results (PII redaction before the model sees the data). See Guardrails.
  • Argument policies — analyzer rules on specific arguments (e.g. deny SQL writes on a sql argument).
  • OAuth injection — for OAuth-backed servers, the gateway injects the user’s real upstream token server-side. See OAuth for MCP tools.
  • Audit + metering — every tools/call is logged with arguments preview, caller identity, and latency.

Streamable-HTTP MCP is stateful: initialize first, then carry the returned Mcp-Session-Id.

Terminal window
# 1. Initialize — capture the mcp-session-id response header
curl -s -D - http://localhost:8100/v1/proxy/mcp/mcp-01KS2MQQS24X355MN7NK006Y28 \
-H "Authorization: Bearer sk_brutor_api_..." \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18",
"clientInfo":{"name":"curl","version":"1.0"},
"capabilities":{}}}'
# → header: mcp-session-id: 0b0cfcef9f2d4aa3920482a49441da22
# 2. List tools
curl -s http://localhost:8100/v1/proxy/mcp/mcp-01KS2MQQS24X355MN7NK006Y28 \
-H "Authorization: Bearer sk_brutor_api_..." \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: 0b0cfcef9f2d4aa3920482a49441da22" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
# 3. Call a tool
curl -s http://localhost:8100/v1/proxy/mcp/mcp-01KS2MQQS24X355MN7NK006Y28 \
-H "Authorization: Bearer sk_brutor_api_..." \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: 0b0cfcef9f2d4aa3920482a49441da22" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"get_current_weather","arguments":{"city":"Berlin"}}}'

Response (SSE-framed JSON-RPC):

event: message
data: {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"🌤️ Weather information for Berlin...\n🌡️ Current Temperature: 21.4°C..."}]}}

Claude Desktop launches stdio MCP servers from claude_desktop_config.json; the standard way to attach a remote streamable-HTTP server is the mcp-remote bridge, passing the gateway URL and your API key header:

claude_desktop_config.json
{
"mcpServers": {
"brutor-hubspot": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://localhost:8100/v1/proxy/mcp/mcp-01KS2MQQS24X355MN7NK006Y28",
"--header",
"Authorization: Bearer sk_brutor_api_..."
]
}
}
}

Restart Claude Desktop; the server’s tools appear in the tools menu. Every call Claude Desktop now makes to that server flows through the gateway — filtered, guarded, logged.

Add the gateway endpoint as a streamable_http extension in ~/.config/goose/config.yaml:

~/.config/goose/config.yaml
extensions:
brutor-hubspot:
enabled: true
type: streamable_http
name: brutor-hubspot
uri: http://localhost:8100/v1/proxy/mcp/mcp-01KS2MQQS24X355MN7NK006Y28
headers:
Authorization: "Bearer sk_brutor_api_..."
import asyncio, httpx
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
GATEWAY_MCP = "http://localhost:8100/v1/proxy/mcp/mcp-01KS2MQQS24X355MN7NK006Y28"
API_KEY = "sk_brutor_api_..."
async def main():
headers = {"Authorization": f"Bearer {API_KEY}"}
async with httpx.AsyncClient(headers=headers, timeout=None) as http_client:
async with streamable_http_client(GATEWAY_MCP, http_client=http_client) as (r, w, _):
async with ClientSession(r, w) as session:
await session.initialize()
tools = await session.list_tools()
print([t.name for t in tools.tools])
result = await session.call_tool(
"get_current_weather", arguments={"city": "Berlin"}
)
print(result.content[0].text)
asyncio.run(main())

A Virtual MCP server (Admin UI → Resources → MCP Servers → Virtual Servers) aggregates the tools of multiple upstream MCP servers behind a single endpoint:

http://localhost:8100/v1/proxy/vmcp/{virtual_server_id}
  • tools/list returns the union of upstream tools the caller is allowed to use, with operator-chosen exposed names and description overrides (names are namespaced per upstream server to avoid collisions).
  • tools/call is looked up by exposed name and delegated to the originating upstream through the full /v1/proxy/mcp/{server_id} pipeline — so per-server OAuth, guardrails, approvals, and logging all apply exactly as if you’d called the upstream directly.

For a client this means: configure one URL and get your whole governed tool estate — instead of one config entry per server.

claude_desktop_config.json — one entry for everything
{
"mcpServers": {
"brutor-tools": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://localhost:8100/v1/proxy/vmcp/vmcp-01JQZ3...",
"--header",
"Authorization: Bearer sk_brutor_api_..."
]
}
}
}

Servers whose upstream requires user OAuth (HubSpot, Slack, Jira…) publish discovery metadata through the gateway so MCP clients can drive the flow themselves:

GET /v1/proxy/mcp/{server_id}/.well-known/oauth-protected-resource
GET /v1/proxy/mcp/{server_id}/.well-known/openid-configuration

The real upstream tokens stay encrypted inside the gateway — see OAuth for MCP tools.

Protocol versions: nothing for you to change

Section titled “Protocol versions: nothing for you to change”

The gateway speaks both MCP eras — the legacy initialize handshake and the stateless 2026-07-28 dialect (server/discover, per-request _meta, routing headers). Your client uses whichever its SDK speaks; sessions keep working for legacy clients, and stateless clients need no session at all once MCP_STATELESS_ENABLED is on. One rule applies to everyone: if a client sends Mcp-Method/Mcp-Name headers, they must match the request body — mismatches are rejected (400, JSON-RPC -32020). See protocol versions & dialects.