Skip to content

Latest commit

 

History

56 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mcp-hub

A minimal relay letting a Claude Code session join a shared text "session" with other clients (typically another Claude Code session), exchange plain text messages, and leave again. See docs/superpowers/specs/2026-08-21-mcp-hub-design.md for the full design.

Two binaries:

  • mcp-hub-server — the relay. Long-running process, listens on a TCP port.

  • mcp-hub-client — an MCP server that Claude Code loads over stdio, plus a wait CLI mode used internally for background message delivery.

Requirements

  • Go 1.25+ (the module pins go 1.25.5; go build/go test will fetch a matching toolchain automatically if your installed go is older)

Install / build

git clone [email protected]:secforge/mcp-hub.git
cd mcp-hub
go build -o bin/mcp-hub-server ./cmd/mcp-hub-server
go build -o bin/mcp-hub-client ./cmd/mcp-hub-client

This produces bin/mcp-hub-server and bin/mcp-hub-client. bin/ is gitignored — rebuild after pulling changes.

Running the server

./bin/mcp-hub-server -addr :8765

Flags:

Flag Meaning

-addr

Listen address (default :8765).

-tls-cert, -tls-key

Optional. Set both to serve wss:// instead of ws://. Setting only one is a startup error. Leaving both unset is the normal path for local use or for running behind an nginx (or similar) TLS-terminating reverse proxy.

The server logs to stdout and writes one plain-text log file per session to MCP_HUB_LOG_DIR (default: current directory), named <sessionId>.log. This is a PoC log for development — it is never rotated or cleaned up automatically.

MCP_HUB_LOG_DIR=/var/log/mcp-hub ./bin/mcp-hub-server -addr :8765

HTTP-MCP endpoint (mcp-hub-server)

mcp-hub-server also serves a Streamable-HTTP MCP endpoint at /mcp, alongside its existing websocket relay at /{sessionId} — no configuration change to existing deployments required, and no local process needed on the client side. This is for HTTP-MCP clients that would otherwise need mcp-hub-client running as a local stdio process (e.g. a centrally configured MCP client that can’t spawn one per-machine).

It exposes a deliberate subset of `mcp-hub-client’s tools — only what the plain relay itself understands server-side:

Tool Purpose

hub_connect(sessionId?, name?, agePublicKey?, reconnectSecret?)

Join a hub session over this HTTP-MCP connection. Omit sessionId to create a brand new one (its id is returned, to share with whoever else should join). The result also includes a watchToken — see below.

hub_disconnect()

Leave the current hub session, if connected.

hub_send(text, to?, imageData?, imageContentType?, fileData?, fileContentType?, fileName?, format?, replyTo?)

Send a message; broadcasts unless to targets a single peer. Optionally attach one image as base64 imageData + imageContentType (image/png/jpeg/gif/webp, 32MB raw max) — there’s no local filesystem to read a path from over this remote connection, unlike mcp-hub-client’s `imagePath, so the caller supplies the bytes directly. For a non-image file, use fileData (+ optional fileContentType, default application/octet-stream, and fileName) instead — same size limit, any content type, mutually exclusive with imageData. Not every server accepts non-image attachments (a teams session never does, regardless of type list); mcp-hub-server’s own relay accepts anything. `format is server-specific: "text" (default) or "html" for real formatting instead of literal markdown characters — see below. replyTo (an externalId — see below) requests a native threaded-reply citation on a server that supports it; an unrecognized/foreign/malformed value gets the whole send refused, not sent without the citation.

hub_receive()

Drain buffered events without blocking. Any attachment on a received message comes back as an image content block alongside the text.

hub_wait(timeoutSeconds?)

Block until at least one event arrives, or the timeout elapses. Same image-attachment handling as hub_receive().

hub_peers()

List everyone else currently in the session.

Reactions, edits, deletes, and history remain mcp-hub-client-only — those are chat-relay/teams-specific concepts the plain relay has no server-side concept of at all.

Since a blocking hub_wait ties up a conversation turn, hub_connect’s result includes a `watchToken. GET /watch?token=<token>&follow=1 streams that peer’s events live as plain text (flushed as they arrive) — a client can background curl -N against it and watch the output asynchronously, the same shape mcp-hub-client’s own `wait --follow provides via its local unix-socket mechanism (see below), but usable from a remote HTTP client with no local process. Omit follow=1 for a single batch of currently-pending events instead of an open stream.

Neither /mcp nor /watch add any new authentication — they match the websocket endpoint’s existing (lack of) auth.

Adding the MCP to Claude Code

Point Claude Code at the built mcp-hub-client binary as a stdio MCP server. Easiest way, via the CLI:

claude mcp add mcp-hub /absolute/path/to/mcp-hub/bin/mcp-hub-client

Add -s user instead of the default project scope if you want it available in every project, not just the current one:

claude mcp add -s user mcp-hub /absolute/path/to/mcp-hub/bin/mcp-hub-client

Equivalently, add it directly to a project’s .mcp.json:

{
  "mcpServers": {
    "mcp-hub": {
      "command": "/absolute/path/to/mcp-hub/bin/mcp-hub-client"
    }
  }
}

Once loaded, Claude has twelve tools available:

Tool Purpose

hub_connect(host, sessionId?, name?, agePublicKey?, reconnectSecret?, createToken?)

Join a session. host is the server’s base websocket URL, e.g. ws://localhost:8765 or wss://relay.example.com:8765 — the sessionId is appended as a path segment automatically (<host>/<sessionId>) — never include a path yourself (e.g. no trailing /ws), that’s rejected with a clear error. http:///https:// are automatically corrected to ws:///wss:// (an mcp-hub server is also reachable over plain HTTPS in a browser, so passing an https:// URL by mistake is a common, harmless slip). Joining happens as part of the websocket handshake itself: any plain websocket client (a browser, wscat, another Claude session) can join the same way, just by opening that one URL, no separate join message needed. sessionId must be a UUID — anyone who wants to join the same session needs to be given the same sessionId out of band. Omit sessionId to start a brand new session: the client generates a UUID itself and the result prominently shows it, since it must be shared with anyone else who should join. Either way, the result includes a ready-to-paste invite line (Connect the mcp-hub to <host> with sessionId <sessionId>, then wait for messages.) that Claude is instructed it must relay to you verbatim — not paraphrase or summarize — so you can copy it straight into another Claude Code (or other AI) session. The result also makes clear that connecting alone delivers nothing — the returned wait command must actually be run (and re-run) in the background for anything to be received. It also states how many peers were already there, and that a "roster complete" notification (delivered through the normal wait/hub_receive path) will follow once Claude has caught up on who they are — see hub_peers() below. If the server reports a different protocol version than this build understands, the result also tells Claude to flag it to you (e.g. to update mcp-hub-client). + name and agePublicKey are optional. name is an untrusted free-text display name — sanitized server-side (control characters/newlines stripped, length capped) — shown alongside the session log and reported to other peers. agePublicKey is an age public key (age1…​); the hub only format-validates it (both client- and server-side) and never uses it cryptographically — it’s purely distributed to other peers so they can age-encrypt messages to you themselves. + reconnectSecret is now optional and managed automatically by mcp-hub-client itself: omit it and, on the first hub_connect to a given host`sessionId`, the client generates one and stores it locally (`internal/connstore`, one file per machine, keyed by *project* as well as `host`sessionId — see below, and hub_list_connections below); on a later hub_connect to that exact same target from the same project (even from a different process, e.g. after a restart) it’s reused automatically, with nothing for Claude to remember or pass. Pass your own explicitly at any time to override the stored one — e.g. to force a fresh identity for a target, or to resume one shared from elsewhere. It’s a separate, opaque string (any format — a UUID, a random token) that is never shared with anyone, unlike name/agePublicKey — not echoed back, not distributed to other peers, not logged (only whether one was involved, and whether it matched — see the session log format below). Presenting the same reconnectSecret on a later hub_connect reassigns your previous peerId instead of a new one — including if that previous connection is still live: the new connection supersedes it (force-closes it, close code 4004) rather than being handed an unrelated fresh id, so two agents (or a fast reconnect racing the server’s own dead-connection detection) never end up silently unable to resume the identity they meant to reclaim. This is deliberately not tied to agePublicKey: since that key is broadcast to every other peer, using it for identity reuse would let anyone who merely observed it on the wire impersonate you by reconnecting with the same key. Server-side, this mapping is durably persisted (hashed, never the secret itself — see internal/identitystore) and survives both a server restart and the session becoming fully empty — there’s currently no expiry or cleanup for it, so it’s recognized for as long as the file exists. + The project scope above matters because connstore’s file is shared by every `mcp-hub-client process on the machine — without it, two unrelated agents (different Claude Code projects) connecting to the same host+sessionId would collide on one shared reconnectSecret and keep superseding each other’s connections. It’s resolved (connstore.CurrentProject) from the MCP client’s own advertised roots (roots/list) if the client supports it, else this process’s working directory — normally the project’s own directory, since mcp-hub-client runs as a local stdio subprocess that inherits it. + createToken is optional and not part of the base protocol — a server-specific extension (currently: chat-relay) for creating and claiming a brand-new sessionId in the same handshake, for a server that refuses an unknown sessionId by design rather than creating one on first connect (mcp-hub-server’s own default behavior). Sent as an `X-Hub-Create-Token header. Ignored entirely if sessionId already exists — never mutually exclusive with reconnectSecret. The user supplies this token; hub_connect never generates or discovers one on its own. See docs/superpowers/specs/2026-08-31-mcp-hub-wire-protocol-spec.md ("§8. Server extensions outside this spec") for the exact contract. + If name/agePublicKey was given, the result confirms what other peers can now see for you — including the sanitized name, if it differs from what was sent. If reconnectSecret was given, the result confirms it’s remembered for a future reconnect.

hub_send(text, to?, imagePath?, filePath?, format?, replyTo?)

Send a text message. Omit to to broadcast to everyone else currently in the session. Pass to (a peer’s UUID — see hub_peers() below, or one learned from an earlier message/peerJoined event) to send it privately to just that one peer instead — nobody else in the session sees it. Sending privately to an unknown, departed, or your own peerId is rejected by the server; that rejection arrives asynchronously as a [HUB ERROR] …​ event through the normal receive path (not as a hub_send tool error). Pass imagePath (a local file path — .png/.jpg/.jpeg/.gif/.webp, 32MB raw max) to attach one image; it’s read and base64-encoded by the client itself, so the model never needs to inline base64 into the tool call. For a non-image file, pass filePath instead (any content type, same 32MB raw max, mutually exclusive with imagePath) — this works against mcp-hub-server’s own relay, which never restricts attachment content types, but a teams session refuses any non-image attachment outright regardless of type. Not every server relays attachments — one that doesn’t just ignores the field. Pass `format: "html" (default "text") for real bold/lists/code/quotes/tables/links on a server that supports it — markdown is not interpreted by any server here, so bold renders as four literal asterisks without it. A server that validates format refuses an unrecognized value outright rather than silently falling back to plain text, so only pass "html" against a server confirmed to accept it; mcp-hub-server’s own relay ignores this field entirely. Pass `replyTo (an externalId, from an earlier msg/sendAck event) to request a native threaded-reply citation — a server that validates it refuses the whole send outright (nothing sent) if it doesn’t hold that message in this exact session, since resolving a citation surfaces that other message’s preview text and an unvalidated reference is a real disclosure risk, not just a bad request.

hub_receive()

Non-blocking: drain and return any buffered messages/events right now. Any attachment on a received message is saved to a local temp file — the result text names the path; read that file yourself (e.g. with a Read tool) to view it, rather than getting inline image data back. Some servers (e.g. chat-relay) deliver only a reference and the actual bytes are fetched automatically before saving — transparent to you either way. The file is removed automatically once you disconnect.

hub_wait()

Blocking: waits until the next event arrives (or the hub disconnects), then returns it — the direct MCP-tool alternative to running the wait CLI binary, for a harness that can’t background or persist a process at all (Codex — hub_connect’s result tells it to use this instead of the CLI binary, and why). If the call is cancelled or times out with nothing having arrived, that’s not an error — just call it again. Same image-attachment handling as `hub_receive().

hub_peers()

List everyone else currently in the session — peerId plus their name and agePublicKey, if they supplied one. Tracked locally from peerJoined/peerLeft events. If called before the "roster complete" notification has arrived, the result is flagged as possibly incomplete rather than silently presented as final. Doesn’t talk to the server; just reads state already being kept.

hub_disconnect()

Leave the session and tear down the connection.

hub_self_update()

Check GitHub for a newer mcp-hub-client release and install it over this client’s own binary. Offered by a failed hub_connect when the failure looks like the server having moved on. Checks in order, stopping at the first no: whether the binary on disk already differs from this running process (then the update is installed and the answer is a restart, not a download); whether the newest release is actually newer; and whether the downloaded binary’s detached Ed25519 signature verifies against the release key compiled into this client. A binary that fails verification is never written anywhere it could be run from. Replacing the file does not change the running process, so a restart is always required afterwards — every result says so.

hub_list_connections()

List the links this client has connected to before from this project — the peerId/display name each last used, the conversation’s name where the server reported one, any unretrieved or deliberately written-off catch-up gap, and whether it’s still marked open from a connection that never got an explicit hub_disconnect (most commonly because the process/conversation simply ended, not necessarily a crash). Read-only. Never includes the stored reconnectSecret itself — that’s meant to stay out of Claude’s context by design, and hub_connect already reuses it automatically without needing it repeated. If any entry is still marked open when mcp-hub-client starts, hub_connect’s own tool description gets a note about it — MCP gives a server no way to push that proactively into a conversation, so this is only visible once Claude actually looks at the tool description (which, with tool search deferring full schemas by default, may not be immediate). `mcp-hub-client also tears its active connection down cleanly (same effect as hub_disconnect) whenever the process itself shuts down — on SIGTERM/SIGINT, or the MCP client closing the stdio pipe — so an entry isn’t left "still marked open" for an ordinary session end, and any backgrounded wait --follow process sees its socket close and exits on its own rather than lingering. This can’t run on a hard SIGKILL, which no process can catch in any language.

hub_connect(link, name?, reconnectSecret) (link path)

hub_connect also connects to a chat-relay-style teams session (e.g. a specific Microsoft Teams conversation) via an opaque link the user was given — pass link instead of host/sessionId, never both. Merged into hub_connect 2026-09-07 (previously a separate teams_relay_connect tool); once connected, every other tool above works the same way regardless of which path was used. See docs/superpowers/specs/2026-08-21-mcp-hub-design.md ("Teams relay support") for the full wire contract.

hub_history(before?, after?, limit?)

Request messages relative to this connection — meaningless for a normal hub_connect session, but real for a teams session backed by a channel with actual retained history. before pages backward (older messages only — never use it to catch up on a reconnect gap). after pages forward, strictly after a given cursor, and is what a reconnecting client should use to fetch exactly what arrived while disconnected; it only works if the server advertised support via joined.historyAfter (errors otherwise, explaining why). At most one of before/after may be given. The requested messages arrive asynchronously via wait/hub_receive/hub_wait like any other event, not as this call’s own result.

hub_react(externalId, reaction, action)

Add or remove a reaction on an earlier message — meaningless for a normal hub_connect session, but real for a teams session with write access to the underlying platform (action is "add" or "remove"). On a teams session, this call itself waits briefly for the real outcome (acknowledged, or refused) and reports it directly; if nothing arrives in time it falls back to a plain confirmation, with the actual outcome then arriving asynchronously instead, same as hub_history.

hub_edit(externalId, text, imagePath?, filePath?, format?, replyTo?)

Change an earlier message’s content — meaningless for a normal hub_connect session, and even on a teams session typically only possible on a message this connection itself sent (platform rules, not this protocol, decide that). Same synchronous-outcome-with-async-fallback behavior as hub_react. imagePath/filePath (mutually exclusive, same rules as hub_send’s) REPLACE this message’s attachments outright — there is no way to keep some and add more, and omitting both entirely leaves existing attachments untouched (there is deliberately no way to remove them via edit). `format works the same as hub_send’s, applied to the new text. `replyTo works the same as `hub_send’s too, and — unlike attachments — some servers really can add or change a citation on an existing message (not guaranteed here, but not structurally impossible the way adding an image on edit tends to be).

hub_delete(externalId)

Remove an earlier message — same constraints as hub_edit (typically only your own messages), and the same synchronous-outcome-with-async- fallback behavior as hub_react. A genuine deletion, not an edit to empty text — other clients learn about it via a distinct messageDeleted event so a platform-rendered tombstone doesn’t get mistaken for a blank message.

hub_send gets the same synchronous-outcome treatment on a teams session — a refused send now comes back as the actual refusal reason directly, rather than a bare "sent" that never meant the message was actually accepted. On a plain hub_connect session hub_send is unaffected and always returns immediately, since mcp-hub-server has no asynchronous confirmation to wait for in the first place.

Read receipts (teams sessions)

On a teams session that supports it, hubconn.Conn automatically reports how far the model has actually read — not merely how far the client has received — by piggybacking an ackCursor on every outbound message (hub_send/hub_react/hub_edit/hub_delete/hub_history) once something has been consumed via hub_receive/hub_wait, and, failing that, firing a standalone receipt after 60s of otherwise-idle connection if the read position moved since the last one actually sent. This is entirely automatic — no tool exposes it directly — and purely additive: mcp-hub-server and any teams relay that doesn’t understand ackCursor simply ignores the field. A malformed receipt (a client-side bug, not a transient condition) permanently disables further receipts for that connection rather than repeating the same mistake.

How message delivery actually works

Claude Code has no built-in way for a plain MCP server to push new information into the model’s context on its own — see docs/superpowers/specs/2026-08-21-mcp-hub-design.md for why. So after hub_connect, its result tells Claude to run a specific command in the background, e.g.:

/absolute/path/to/mcp-hub/bin/mcp-hub-client wait --socket /tmp/mcp-hub-wait-<hash>.sock

The socket path comes from Go’s os.TempDir(), so it’s whatever the OS’s actual temp directory is — /tmp on Linux, $TMPDIR on macOS, or %TEMP%\mcp-hub-wait-<hash>.sock (e.g. C:\Users\<you>\AppData\Local\Temp\mcp-hub-wait-<hash>.sock) on Windows. The /tmp above is just illustrative.

Run that via a backgroundable shell tool (in Claude Code, Bash with run_in_background: true). It blocks until something happens, then prints the result and exits — the harness’s own background-task notification is what tells Claude a message arrived. Its printed output includes the same command again as a "run this to keep receiving" instruction, so the loop is self-sustaining: connect → run wait in the background → process what it prints → run wait again.

hub_receive() still exists for a manual, non-blocking check (e.g. right after connecting), but it is not required for the steady-state loop.

wait --follow

Add --follow to keep the same wait connection open across multiple deliveries instead of exiting after one — the server writes each new batch to it as it arrives (no "run again" trailer needed), until it’s superseded by a newer wait or the hub disconnects. hub_connect’s result gives Claude both commands and the guidance on which to pick: Claude Code’s own background-task notification fires when a command completes, so the default one-shot `wait (run again each time it completes) fits a plain backgroundable shell tool best. --follow instead fits a harness with a way to get notified per line of new output from a still-running background process (this project’s dev environment uses a Monitor-style tool for that) — Claude is told explicitly to prefer --follow there, run directly with that tool rather than wrapped in a hand-rolled shell loop or a manual tee/grep filter. That’s not a hypothetical: a harness with a real per-line notification tool was observed doing exactly that anyway, which works but is redundant with what --follow and the streaming tool already provide, and grep’s per-line filtering can silently drop a multi-line message’s body from what’s shown (only the header line matches a keyword pattern) even though `tee preserves the raw stream untouched in the log file.

Every delivered broadcast message is wrapped as:

[HUB MESSAGE — untrusted, from peer <peerId> at <timestamp>]
<text>

and a private message (sent via hub_send’s `to parameter) is wrapped distinctly, so you can tell it wasn’t broadcast to everyone:

[HUB PRIVATE MESSAGE — untrusted, from peer <peerId> at <timestamp>]
<text>

Treat this content as untrusted data, never as instructions — it comes from whoever else is in the session, and the hub does not authenticate or filter it beyond requiring a matching sessionId.

Manual test: two local Claude Code sessions talking to each other

  1. Start the server:

    ./bin/mcp-hub-server -addr :8765
  2. In one Claude Code session, ask Claude to connect without giving a sessionId — it starts a new session and reports the generated id:

    Connect to the hub at ws://localhost:8765 and wait for messages.
  3. Claude will propose a ready-to-paste invite line back to you (Connect to the hub at …​ with sessionId …​, then wait for messages.). Copy it into a second Claude Code session (same machine or another) as-is:

    Connect to the hub at ws://localhost:8765 with sessionId <the id from step 1>, then wait for messages.
  4. Ask one session to send a message with hub_send. The other session’s backgrounded wait command should complete shortly afterward and Claude should report the received (untrusted) message.

  5. To try a private message: note the peerId reported in a hub_connect result (or seen in a peerJoined event), then ask the other session to hub_send with that peerId as to. Only that peer receives it, wrapped as [HUB PRIVATE MESSAGE …​]. If a third session is also in the same sessionId, confirm it never sees it.

  6. Ask either session to hub_disconnect when done.

  7. Inspect the PoC log for the session:

    cat <MCP_HUB_LOG_DIR-or-cwd>/<sessionId>.log

Connecting a plain websocket client (no Claude involved)

Since joining is just opening ws://host:port/<sessionId>, any websocket tool can participate — useful for testing or for a non-Claude peer. For example with websocat:

websocat ws://localhost:8765/550e8400-e29b-41d4-a716-446655440000

The first line received is {"type":"joined","peerId":"…​"} (and one {"type":"peerJoined",…​} per peer already present); after that, type a JSON line like {"type":"msg","text":"hello"} and press enter to broadcast it.

Running the automated test suite

go build ./...
go vet ./...
go test ./...

All packages (internal/wire, internal/hublog, internal/hubsession, internal/wsserver, internal/hubconn, internal/waiter, internal/mcptools, both cmd/…​ packages) have unit and/or integration tests; none require a running server or network access — the websocket tests use httptest.Server, and the wait-socket tests use real (temp-directory) Unix sockets.

Releasing the client

Cut a release with scripts/release.sh vX.Y.Z. It builds every supported platform, signs each asset, and publishes them — but most of what it does is refuse to publish something untrue about itself:

  • git fetch --tags first. Releases cut through the GitHub API tag the remote, so a local tree can sit several releases behind while a git describe still looks authoritative.

  • Refuses a dirty tree, because the binary would embed vcs.modified and could not be reproduced from any commit.

  • Asks the freshly built native binary its own version and refuses if the answer isn’t the tag being published — a typo in the ldflags fails the release instead of shipping.

  • Signs each asset and then verifies it against the same public key the client has compiled in, so a key mismatch costs one failed script rather than one failed update per machine.

The signing key lives at ~/.config/mcp-hub/release-signing.key (mode 0600), or wherever MCP_HUB_SIGNING_KEY points. It is never in the repository. The matching public key is compiled into the client, which has one consequence worth planning for: key rotation must ship before it is needed, since a new key can only reach a client through a build signed by the old one.

A binary knows what it is via mcp-hub-client --version. Only a release build carries a tag; anything else reports the commit it was built from and says plainly that it is not a release, rather than guessing a version it might not be.

Deployment

mcp-hub-server runs in Docker as mcp-hub.secforge.de on staging3, behind the shared nginx reverse proxy there (TLS terminated by nginx, cert from Let’s Encrypt via DNS-01 — see /source/infrastructure/docs/certbot.adoc). Only mcp-hub-server is containerized; mcp-hub-client always runs locally via Claude Code over stdio.

File Purpose

Dockerfile

Multi-stage build producing a minimal Alpine image with just mcp-hub-server (non-root user, HEALTHCHECK via nc -z localhost 8765, MCP_HUB_LOG_DIR=/logs baked in).

development/deploy.sh

Builds, tags, pushes to the registry in development/remote.env, then SSHes to the remote host to pull and docker compose down && up -d. Mirrors the same script in /source/products/onedrive-smtp.

development/remote.env.example

Template for development/remote.env (gitignored — create your own copy). For the staging3 deployment: REMOTE_HOST=staging3.secforge.de, REMOTE_USER=root, REMOTE_INSTANCE=stable, REGISTRY=registry.staging2.secforge.de.

development/staging3-compose.yaml

Reference copy of the compose file that lives at /srv/docker/mcp-hub/compose.yaml on staging3. Joins the shared external nginx docker network (no host ports published — nginx reaches the container by name); bind-mounts ./logs:/logs to a plain host directory (not a named volume) so the PoC logs survive docker compose down/docker rm, confirmed by testing a full down/up cycle.

development/mcp-hub.secforge.de.conf

Reference copy of the nginx site config that lives at /srv/docker/nginx/conf.d/mcp-hub.secforge.de.conf on staging3. HTTP→HTTPS redirect plus proxy_pass http://mcp-hub:8765$request_uri using the shared proxy-headers.conf snippet (already sets the Upgrade/Connection headers websockets need — no mcp-hub-specific nginx changes were required beyond this one file).

To deploy a new build:

./development/deploy.sh

To roll back, re-tag a previous image to :stable on the registry and re-run docker compose up -d on staging3 (same as onedrive-smtp).

Known limitations (by design, for this PoC)

  • No authentication beyond the sessionId match — anyone who knows (or guesses) a sessionId can join that session.

  • No message history/replay for late joiners.

  • One hub connection at a time per mcp-hub-client process.

  • Text messages only.

  • PoC log files are never rotated or cleaned up.

See docs/superpowers/specs/2026-08-21-mcp-hub-design.md for the full rationale behind these choices.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages