MCP server: HTTP transport with per-request auth forwarding, packaged as opensource-mcp#425
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a deployable MCP server surface for create-a-container that can be reached over HTTPS via the Manager (/mcp), with per-request Authorization forwarding end-to-end, and packages the MCP server as a standalone opensource-mcp OS package with vendored Python dependencies.
Changes:
- Add a new
opensource-mcppackaged MCP server (systemd unit +/etc/defaultconfig) and integrate it into the build/release pipeline. - Add an Express streaming reverse proxy middleware mounted at
/mcp, plus Jest coverage for header/body/query passthrough, SSE behavior, and upstream failure handling. - Update docs to describe local stdio vs built-in HTTP usage, plus the new “four deployable components” model.
Reviewed changes
Copilot reviewed 24 out of 26 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| mie-opensource-landing/docs/users/mcp-server.md | Documents stdio vs built-in HTTP MCP usage and hosting details. |
| mie-opensource-landing/docs/developers/release-pipeline.md | Updates release docs to include the new opensource-mcp component. |
| manager-control-program/uv.lock | Updates lock metadata to treat the project as editable/installable. |
| manager-control-program/README.md | Reframes usage, packaging details, and transport/auth configuration. |
| manager-control-program/pyproject.toml | Makes the project installable via an explicit build system (hatchling). |
| manager-control-program/manager_control_program/server.py | Adds HTTP transports and per-request Authorization forwarding. |
| manager-control-program/manager_control_program/init.py | Marks the package as importable/installable. |
| manager-control-program/Makefile | Adds packaging/staging logic to build opensource-mcp with vendored deps. |
| manager-control-program/contrib/systemd/opensource-mcp.service | New systemd unit to run the MCP server on loopback. |
| manager-control-program/contrib/preremove.sh | Stops/disables the unit on package removal. |
| manager-control-program/contrib/postinstall.sh | Enables (and restarts when possible) the MCP service post-install. |
| manager-control-program/contrib/default/opensource-mcp | Provides /etc/default runtime config for the MCP service. |
| manager-control-program/.gitignore | Ignores packaging outputs/staging artifacts. |
| manager-control-program/.fpm | Defines the opensource-mcp package metadata/dependencies/conffiles. |
| Makefile | Adds manager-control-program to the root components build list. |
| images/manager/Dockerfile | Installs opensource-mcp into the manager image. |
| images/manager/container-creator-init.service | Writes MCP_SERVER_URL into /etc/default/container-creator on first boot. |
| images/builder/Dockerfile | Updates builder image docs/comments for building 4 packages. |
| create-a-container/README.md | Notes opensource-server now depends on opensource-mcp and proxies /mcp. |
| create-a-container/middlewares/mcp-proxy.js | Implements a streaming reverse proxy for /mcp. |
| create-a-container/middlewares/tests/mcp-proxy.test.js | Adds Jest coverage for proxy passthrough and failure modes. |
| create-a-container/example.env | Documents MCP_SERVER_URL env var for enabling the proxy. |
| create-a-container/contrib/systemd/container-creator.service | Documents that /etc/default/container-creator now includes MCP config. |
| create-a-container/app.js | Mounts /mcp proxy early (before parsers/session/rate-limit/CSRF). |
| create-a-container/.fpm | Adds runtime dependency on opensource-mcp. |
| .github/workflows/release.yml | Includes opensource-mcp in release artifacts/flat APT metadata. |
Comments suppressed due to low confidence (1)
manager-control-program/manager_control_program/server.py:80
- Typo in comment:
awslabs.openapi_mpc_servershould beawslabs.openapi_mcp_server.
# awslabs.openapi_mpc_server.server:main function with the small exception
Comment on lines
+36
to
+42
| function filterHeaders(headers) { | ||
| const out = {}; | ||
| for (const [name, value] of Object.entries(headers)) { | ||
| if (!HOP_BY_HOP.has(name.toLowerCase())) out[name] = value; | ||
| } | ||
| return out; | ||
| } |
Comment on lines
+54
to
+55
| # No test suite yet; kept as a no-op so the repo-wide `make test` loop stays | ||
| # uniform if this component is ever added to the root COMPONENTS list. |
| if transport == "stdio": | ||
| # We default to Bearer auth which requires the user to have set the | ||
| # AUTH_TOKEN environment variable. I'm unsure if any other auth | ||
| # types work, but we leave that door open incase it's needed. |
…orwarding Adds SERVER_TRANSPORT=http|streamable-http|sse (default remains stdio). In HTTP mode the server no longer requires AUTH_TOKEN at startup: a client-level httpx auth hook reads each incoming MCP request's Authorization header from fastmcp's request context and forwards it on the outgoing API call, so every caller authenticates to the API as themselves. fastmcp's built-in header passthrough deliberately strips authorization, hence the explicit include. Setting AUTH_TYPE=bearer with AUTH_TOKEN alongside HTTP mode still works as a static fallback for requests without a header. stdio behavior is unchanged (bearer + AUTH_TOKEN required). Verified end-to-end against a stub API: two HTTP MCP clients with different bearer tokens each hit the API under their own token; a headerless client produced no Authorization header; stdio mode still fails fast without AUTH_TOKEN; invalid transports error clearly.
Without a [build-system], uv treats the project as virtual and never installs it into the venv, so the manager-control-program console script doesn't exist: the documented `uv run manager-control-program` and `uvx --from git+...` invocations fail with 'Failed to spawn'. Add a hatchling build-system and the missing package __init__.py. uv.lock: source flips from virtual to editable.
… HTTP mode The MCP Server page in mie-opensource-landing is the real setup guide: restructure it around the two ways to use the server (Option A: local stdio via uvx, Option B: connecting to a shared HTTP server with your own API key in the Authorization header) and add a hosting section for the new HTTP transport with its env vars and security caveats. Slim the component README to the house style used by create-a-container: component overview, defer-to-docs table, local development, environment variable reference, and how auth works per transport - dropping the MCP client config JSON it previously duplicated from the docs site.
…mcp to it Package manager-control-program inside the opensource-server package with vendored Python dependencies so it runs without uv: - manager-control-program gains the standard component Makefile; its install target exports the uv lockfile and installs prebuilt wheels (Debian 13 / amd64 / CPython 3.13) plus the app into /opt/opensource-server/manager-control-program. The target host needs only /usr/bin/python3. - New manager-control-program.service systemd unit runs it via python3 -m with PYTHONPATH at the vendored dir: HTTP transport on 127.0.0.1:8100, per-request Authorization forwarding, no AUTH_TOKEN. Optional overrides via /etc/default/manager-control-program. - create-a-container's install stages it, postinstall/preremove enable/disable it alongside the existing units. - The Manager reverse-proxies /mcp to it (MCP_SERVER_URL, set in container-creator.service): a dependency-free streaming proxy mounted before the body parsers/session/rate-limit/CSRF so MCP request bodies stream through untouched and SSE responses stay open. MCP clients connect at the Manager's public origin with TLS, e.g. https://manager.example.com/mcp. Verified: jest suite covers the proxy (headers/body/query passthrough, SSE content type, 502 when down, unmounted when unconfigured); built the real deb with fpm and checked contents (vendored tree, all three units, postinst); ran the staged tree on a bare CPython 3.13 behind the real proxy middleware with two MCP clients — each caller's token reached the stub API intact through both hops.
manager-control-program.service execs /usr/bin/python3, but the package never declared it, so on a fresh install without python3 the service failed to start. Pin the range to the vendored wheels' ABI: compiled wheels are cp313-only, so python3 >= 3.13 and < 3.14 (fpm translates the operators per package format; verified deb and rpm output). The Makefile comment cross-references the pin so a future PYTHON_VERSION bump updates both together.
…nsource-mcp package Split the MCP server out of the opensource-server package into a standalone opensource-mcp package instead of bundling it inside create-a-container's build. - manager-control-program now builds its own deb/rpm/apk: the Makefile gains a package target (stage into .pkg/buildroot, then fpm) and a .fpm declaring the opensource-mcp package, with the python3 >= 3.13, < 3.14 dependency pin moved here from create-a-container/.fpm (it tracks the vendored cp313 wheels' ABI). Own postinstall/preremove enable/disable the unit. - The systemd unit is renamed manager-control-program.service -> opensource-mcp.service, with its EnvironmentFile at /etc/default/opensource-mcp. - create-a-container no longer calls this component's install target or ships its units; it just --depends opensource-mcp. Its postinstall/preremove drop the MCP unit, and MCP_SERVER_URL still points the reverse proxy at the loopback service. - Root Makefile adds manager-control-program to COMPONENTS (ordered before create-a-container so the dependency builds first); the release workflow, builder Dockerfile, manager image, and docs move to a four-package model, and .gitignore covers the new build artifacts (.pkg/, *.deb/rpm/apk). Verified: `make deb` in manager-control-program produces opensource-mcp with the correct python3 dependency range, the opensource-mcp.service unit, and the vendored dependency tree; `make deb` in create-a-container produces opensource-server depending on opensource-mcp and no longer containing the vendored Python code.
…o /etc/default The units this branch added hardcoded configuration via Environment= lines, which admins can't change without editing the shipped unit. Move that config into /etc/default so it can be edited (and survives package upgrades), keeping only fixed runtime constants in the units. - opensource-mcp: ship /etc/default/opensource-mcp (a package config file via fpm --config-files) with API_BASE_URL and SERVER_TRANSPORT/HOST/PORT; the unit now keeps only PYTHONPATH and reads the (now required) EnvironmentFile. Makefile installs it. - container-creator: MCP_SERVER_URL moves from the unit into /etc/default/container-creator, appended by container-creator-init alongside the DB settings it already generates. - README/docs: describe the settings as living in the packaged config file rather than as inline unit overrides. Verified: opensource-mcp.deb registers /etc/default/opensource-mcp as a conffile carrying the four values and its unit body no longer contains them; container-creator's unit no longer sets MCP_SERVER_URL and the init service writes it into /etc/default/container-creator.
…ast the app-level CSRF guard The manager's own agent checks in over localhost during bootstrap before any site row or API key exists, so its POST /api/v1/agents carries neither a Bearer token nor a CSRF token. The agents router already exempts this via a route-level localhost bypass (checkinAuth), but the app-level csrfGuard in app.js runs first and only exempted Bearer-without-cookie requests — so the credential-less localhost check-in was rejected with 403 csrf_invalid before ever reaching the route. Mirror the route-level bypass in csrfGuard: skip CSRF for localhost requests (isLocalhostRequest also rejects proxied/remote clients via X-Real-IP / X-Forwarded-For, so this doesn't widen the trust surface). isLocalhostRequest is required lazily to avoid a load-order cycle with middlewares/index. Adds a regression test covering the localhost bootstrap check-in (not 403) and a proxied-remote credential-less check-in (still rejected).
…ount it into the dev compose stack The MCP server runs from /opt/opensource-server/manager-control-program (PYTHONPATH) with only the system python3 — the vendored Python deps must already be present there. In the dev compose stack the repo is mounted read-only into Proxmox and those deps aren't checked in, so nothing populated that path and opensource-mcp.service couldn't start from a fresh checkout. - make build now assembles the ready-to-run tree (app package + vendored wheels for Debian 13 / amd64 / CPython 3.13) into build/ instead of being a no-op. make install just copies build/ into DESTBIN, so the dev bind mount and the packaged install serve byte-for-byte the same layout. clean removes build/. (build/ is already gitignored.) - compose gains an `mcp` service that runs `make -C manager-control-program build` (repo mounted read-write, like the node/client services; the uv image lacks make, so it's apt-installed first). The proxmox service overlays ./manager-control-program/build onto /opt/opensource-server/manager-control-program read-only and depends on `mcp` completing, so the server stands up from scratch. Verified: `make build` then `make install DESTDIR=...` produce the same tree; it imports and runs under CPython 3.13 with only PYTHONPATH set; `make deb` still packages the same contents; and the `mcp` compose service populates build/ from a clean checkout.
…h in dev and the package Follow-up to assembling the runtime tree in build/: make that directory the actual runtime path in every context so it matches the git repo layout (manager-control-program/build). - opensource-mcp.service points WorkingDirectory and PYTHONPATH at /opt/opensource-server/manager-control-program/build. - make install copies build/ into DESTBIN/build (not flattened into DESTBIN), so the package ships the tree at /opt/opensource-server/manager-control-program/build — the same path the read-only repo mount exposes in the dev container. - Drop the proxmox overlay bind mount: the read-only repo mount already exposes manager-control-program/build at that path once the `mcp` service has built it. - Use the non-slim astral/uv image for the `mcp` service, which already ships make, dropping the apt-get step. Verified: the deb installs the tree at .../manager-control-program/build and the unit's PYTHONPATH matches; it imports and runs under CPython 3.13 from build/; the `mcp` compose service builds it from a clean checkout with no apt step.
…on failure The agent only logged a handful of one-line messages via console.*, and crucially discarded the output of `nginx -t` (run with stdio 'pipe'), so when nginx rejected a rendered config the journal showed "config test failed" with no indication of *why*. - New agent/src/log.ts: a minimal leveled logger (error/warn/info/debug) gated by LOG_LEVEL (default info; unknown values warn and fall back). error/warn go to stderr, info/debug to stdout, each line level-tagged so `journalctl -u opensource-agent` output is greppable. commandOutput() extracts the captured stdout/stderr from a failed execFileSync error. - apply.ts: run() now captures command output; on a failed config test (nginx -t / dnsmasq --test) the actual validator output is logged at error level. Added render/diff/write/test/reload trace logging (debug) plus info-level "applying"/"test passed"/"reloaded" milestones, and the reload-after-rollback failure is now logged instead of swallowed. - index.ts: log startup, per-pass check-in outcome (304 vs new config), and MAX_PASSES exhaustion; the top-level handler now logs the full stack, not just err.message. - api.ts: log the check-in request and distinguish a manager timeout from other network errors and from non-2xx responses. - state.ts: route the corrupt-state warning through the logger. - opensource-agent.service: document the optional LOG_LEVEL env var. Verified: tsc builds clean; default level hides debug and shows info/warn/error; a simulated nginx -t failure surfaces the emerg line; LOG_LEVEL=debug enables debug; unknown levels fall back to info.
runleveldev
force-pushed
the
mcp-http-auth-forwarding
branch
from
July 24, 2026 15:23
318ffb6 to
960e7ac
Compare
…ver path
MCP tool calls were returning the Manager's SPA HTML instead of JSON.
fastmcp builds each generated tool's request URL by joining the raw
OpenAPI path (e.g. "/sites") onto the httpx client's base_url and
ignores the spec's `servers` entry. Our spec declares
`servers: [{url: /api/v1}]`, so with base_url=API_BASE_URL the tools
requested http://host:3000/sites — which the Manager serves as the
single-page app's HTML catch-all — instead of /api/v1/sites.
Read servers[0].url from the fetched spec and append its path to the
client's base_url after create_mcp_server, so requests resolve to
/api/v1/<path>. A relative server url contributes its path directly; an
absolute one contributes only its path so API_BASE_URL's (loopback)
host is preserved; an empty/root server leaves base_url unchanged.
Verified against a stub API whose spec declares servers:/api/v1: the
client base_url becomes http://host/api/v1/ and a tool for "/sites"
resolves to /api/v1/sites.
cmyers-mieweb
approved these changes
Jul 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an HTTP transport to the
manager-control-programMCP server with per-request Authorization forwarding, and ships it to production as its ownopensource-mcpDebian package that the Manager reverse-proxies at/mcp.MCP clients connect at the Manager's public origin over TLS (e.g.
https://manager.example.com/mcp); each caller's Bearer API key flows through untouched to the create-a-container REST API, so every client acts under its own key with no sharedAUTH_TOKENat startup.What's in here
Server (
manager-control-program)SERVER_TRANSPORT=http|streamable-http|sse(default staysstdio). In HTTP mode anhttpx.Authhook reads each incoming MCP request'sAuthorizationheader from fastmcp's request context and forwards it on the outgoing API call.AUTH_TYPE=bearer+AUTH_TOKENstill works as a static fallback.stdiobehavior is unchanged.__init__.py) souvx/uv runwork.Packaging & deployment
manager-control-programnow builds a standaloneopensource-mcppackage (deb/rpm/apk) with all Python deps vendored (prebuilt CPython 3.13 wheels) — the target host needs onlypython3(no uv/pip/venv/network). Pinnedpython3 >= 3.13, < 3.14to match the cp313 wheels' ABI.opensource-servernow--depends opensource-mcpinstead of bundling it. Added to rootCOMPONENTS, the release workflow, builder Dockerfile, and manager image.opensource-mcp.serviceon127.0.0.1:8100./mcpbefore the body parsers/session/rate-limit/CSRF, so MCP request bodies stream through untouched and SSE responses stay open (MCP_SERVER_URL).Config in
/etc/default(not baked into units)opensource-mcpships/etc/default/opensource-mcpas a package config file carryingAPI_BASE_URLandSERVER_TRANSPORT/HOST/PORT; the unit keeps onlyPYTHONPATH.MCP_SERVER_URLmoved out ofcontainer-creator.serviceinto/etc/default/container-creator(written bycontainer-creator-initalongside the DB settings).Docs
uvx; Option B: shared HTTP server with your own API key) and added a hosting section; moved to the "four deployable components" model.Testing
MCP_SERVER_URLis unset.opensource-mcpandopensource-serverdebs with fpm and verified contents: vendored dependency tree,opensource-mcp.service,postinst/prerm, correctpython3dependency range,/etc/default/opensource-mcpregistered as a conffile, andopensource-serverdepending onopensource-mcpwithout any vendored Python code.AUTH_TOKEN.