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

A tools/call request settles access and cost first — authenticate, server access, quotas, rate limits, data residency — then five independent policy controls decide what the call may do: capability filter, guardrails, semantic policy, argument policy and human approval. Only then does the OAuth proxy attach a vaulted token, the MCP server run the tool, and output guardrails scan the result. Every call becomes one log row feeding the run ledger.

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. 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.