sql: per-caller usage attribution via dedicated endpoint chains
Build & push patched image / build (push) Successful in 1m8s

MetaMCP proxies with a fresh outbound connection and forwards no client
identity — not User-Agent, not X-MCP-Client, not initialize.clientInfo.
A downstream server that logs client identity therefore sees only
MetaMCP's own Node UA, and every client behind the gateway collapses
into one undifferentiated bucket. Verified by probe: three distinct
identity values in, {"name": "node", "user_agent": "node"} logged out.

Not patchable at reasonable cost: downstream connections are pooled and
pre-warmed (idleSessions[serverUuid] / createIdleSessionAsync), so one
connection serves many inbound callers and headers bind at connection
creation. No per-request injection point exists without disabling
pooling, re-keying the pool by (server, caller), or threading
AsyncLocalStorage through the transport.

But MetaMCP already supports static per-server custom headers —
mcp_servers.headers is jsonb and flows into the outbound
requestInit.headers. So give each caller its own endpoint → namespace →
server chain, pointing at the SAME backend URL, differing only in a
stamped X-MCP-Client header. Same container, same corpus, nothing
duplicated downstream.

Adds sql/zsupport-endpoint.sql (the worked example, applied to the live
gateway 2026-07-27 — zSupport now attributes correctly) and a README
section covering the pattern and its two gotchas: the UNIQUE (name,
user_id) constraint forcing a new server name, and the resulting change
of tool-name prefix that any client allow-list must track.

sql/** added to the workflow's paths-ignore — config, not image.
This commit is contained in:
2026-07-27 10:44:36 -04:00
parent d5cd602d85
commit 26b27b2d94
3 changed files with 109 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
-- Dedicated MetaMCP chain for the zSupport portal.
--
-- Why: MetaMCP proxies with a fresh outbound connection and forwards neither
-- the caller's User-Agent nor X-MCP-Client, so every client behind the gateway
-- lands in one undifferentiated "node" bucket in zerto-docs' usage log.
-- MetaMCP DOES support static per-server custom headers (mcp_servers.headers,
-- already used by the OB1 server for x-brain-key), and those flow into the
-- outbound requestInit.headers. So a second "view" of the same backend that
-- stamps X-MCP-Client gives us attribution with no code change on either side
-- (docs_mcp/usage.py already prefers X-MCP-Client over User-Agent).
--
-- Same backend container, same corpus, same index — only the stamped header
-- differs. Nothing is duplicated on the zerto-docs side.
--
-- To revert: DELETE the four rows (endpoint, mapping, namespace, server).
BEGIN;
-- 1. Server row — the header lives here (endpoints/namespaces have no such
-- column). Name can't be "zerto-docs": UNIQUE (name, user_id).
INSERT INTO mcp_servers (name, type, url, headers, user_id)
SELECT 'zerto-docs-zsupport', 'STREAMABLE_HTTP', url,
'{"X-MCP-Client": "zsupport/1.0"}'::jsonb, user_id
FROM mcp_servers WHERE name = 'zerto-docs';
-- 2. Namespace
INSERT INTO namespaces (name, description, user_id)
SELECT 'zerto-docs-zsupport',
'zerto-docs corpus, dedicated view for the zSupport portal (adds X-MCP-Client for usage attribution)',
user_id
FROM namespaces WHERE name = 'zerto-docs';
-- 3. Map the server into the namespace
INSERT INTO namespace_server_mappings (namespace_uuid, mcp_server_uuid, status)
SELECT n.uuid, s.uuid, 'ACTIVE'
FROM namespaces n, mcp_servers s
WHERE n.name = 'zerto-docs-zsupport' AND s.name = 'zerto-docs-zsupport';
-- 4. Public endpoint — mirrors zerto-docs (no api-key auth, no oauth).
INSERT INTO endpoints (name, description, namespace_uuid,
enable_api_key_auth, use_query_param_auth, enable_oauth, user_id)
SELECT 'zerto-docs-zsupport',
'Dedicated zerto-docs endpoint for the zSupport portal',
n.uuid, false, false, false, e.user_id
FROM namespaces n, endpoints e
WHERE n.name = 'zerto-docs-zsupport' AND e.name = 'zerto-docs';
COMMIT;