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.
87 lines
3.0 KiB
YAML
87 lines
3.0 KiB
YAML
name: Build & push patched image
|
|
|
|
# Wraps ghcr.io/metatool-ai/metamcp:latest with our sed-patch to hide
|
|
# the OAuth 2.1 discovery endpoints, then pushes the resulting image
|
|
# to the git.jpaul.io registry.
|
|
#
|
|
# Runs on push to main, on the nightly cron so we automatically pick up
|
|
# upstream :latest updates, and on demand via workflow_dispatch. The
|
|
# Dockerfile has grep guards that fail the build loudly if upstream
|
|
# renames or restructures the OAuth routes, so a bad bump is loud —
|
|
# nightly failures show up as red workflow runs, not a silently broken
|
|
# unpatched image.
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths-ignore:
|
|
- "README.md"
|
|
- "LICENSE"
|
|
- ".gitignore"
|
|
# sql/ holds MetaMCP *config* (rows applied to its Postgres), which has
|
|
# nothing to do with the image contents — don't rebuild on those edits.
|
|
- "sql/**"
|
|
schedule:
|
|
# Every day at 08:00 UTC — pulls upstream :latest and rebuilds.
|
|
- cron: "0 8 * * *"
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
# Push to the plain-HTTP LAN endpoint (bypasses Cloudflare's body cap
|
|
# on multi-hundred-MB layers). Pull consumers use the FQDN over TLS.
|
|
REGISTRY_PUSH: 192.168.0.2:1234
|
|
REGISTRY_PULL: git.jpaul.io
|
|
IMAGE: justin/metamcp-patched
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: docker
|
|
container:
|
|
image: catthehacker/ubuntu:act-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
with:
|
|
driver-opts: image=moby/buildkit:latest
|
|
buildkitd-config-inline: |
|
|
[registry."192.168.0.2:1234"]
|
|
http = true
|
|
insecure = true
|
|
|
|
- name: Registry login (hand-written config — login-action 403s on plain HTTP)
|
|
shell: bash
|
|
run: |
|
|
AUTH=$(printf '%s' "justin:${{ secrets.REGISTRY_TOKEN }}" | base64 -w0)
|
|
mkdir -p ~/.docker
|
|
printf '{"auths":{"%s":{"auth":"%s"}}}' "${REGISTRY_PUSH}" "$AUTH" > ~/.docker/config.json
|
|
|
|
- name: Compute tag
|
|
id: tags
|
|
shell: bash
|
|
run: |
|
|
SHA="$(echo "${{ github.sha }}" | cut -c1-12)"
|
|
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Build & push
|
|
shell: bash
|
|
run: |
|
|
docker buildx build \
|
|
--push \
|
|
--pull \
|
|
-t "${REGISTRY_PUSH}/${IMAGE}:latest" \
|
|
-t "${REGISTRY_PUSH}/${IMAGE}:${{ steps.tags.outputs.sha }}" \
|
|
--label "org.opencontainers.image.source=https://git.jpaul.io/${IMAGE}" \
|
|
--label "org.opencontainers.image.description=MetaMCP with host-wide OAuth 2.1 discovery routes hidden" \
|
|
.
|
|
echo "Pushed ${REGISTRY_PULL}/${IMAGE}:{latest,${{ steps.tags.outputs.sha }}}"
|
|
|
|
- name: Link package to repo (idempotent)
|
|
shell: bash
|
|
run: |
|
|
curl -s -o /dev/null -w "link HTTP %{http_code}\n" \
|
|
-H "Authorization: token ${{ secrets.REGISTRY_TOKEN }}" \
|
|
-X POST "http://${REGISTRY_PUSH}/api/v1/packages/justin/container/metamcp-patched/-/link/metamcp-patched" || true
|