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/listonly shows tools enabled for the caller’s resource group; disabled tools return403on call. - Approvals — approval-gated tools return
202/428with anapproval_request_iduntil a group member approves; retry withX-Approval-Token. See the agent guide. - Guardrails —
mcp_inputchecks run on tool arguments (secrets, PII egress, banned patterns),mcp_outputchecks 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
sqlargument). - 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/callis logged with arguments preview, caller identity, and latency.
Raw JSON-RPC (curl)
Section titled “Raw JSON-RPC (curl)”Streamable-HTTP MCP is stateful: initialize first, then carry the returned Mcp-Session-Id.
# 1. Initialize — capture the mcp-session-id response headercurl -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 toolscurl -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 toolcurl -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: messagedata: {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"🌤️ Weather information for Berlin...\n🌡️ Current Temperature: 21.4°C..."}]}}Claude Desktop
Section titled “Claude Desktop”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:
{ "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:
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_..."MCP SDK clients
Section titled “MCP SDK clients”import asyncio, httpxfrom mcp import ClientSessionfrom 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())import { Client } from "@modelcontextprotocol/sdk/client/index.js";import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport( new URL("http://localhost:8100/v1/proxy/mcp/mcp-01KS2MQQS24X355MN7NK006Y28"), { requestInit: { headers: { Authorization: "Bearer sk_brutor_api_..." }, }, });
const client = new Client({ name: "my-client", version: "1.0.0" });await client.connect(transport);
const tools = await client.listTools();console.log(tools.tools.map((t) => t.name));
const result = await client.callTool({ name: "get_current_weather", arguments: { city: "Berlin" },});console.log(result.content);Virtual MCP: one endpoint, many servers
Section titled “Virtual MCP: one endpoint, many servers”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/listreturns 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/callis 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.
{ "mcpServers": { "brutor-tools": { "command": "npx", "args": [ "-y", "mcp-remote", "http://localhost:8100/v1/proxy/vmcp/vmcp-01JQZ3...", "--header", "Authorization: Bearer sk_brutor_api_..." ] } }}OAuth-backed servers
Section titled “OAuth-backed servers”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-resourceGET /v1/proxy/mcp/{server_id}/.well-known/openid-configurationThe 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.
Related
Section titled “Related”- Register your own MCP server — build and register the server side
- Build an agent — the full agent loop, including approval handling
- MCP & A2A API reference
