21a5cf5762
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4d136c4fb2 |
fix(mcp): RFC 9728 protected-resource metadata path + audience binding
All checks were successful
Deploy to Production / deploy (push) Successful in 1m31s
Codex/RFC review showed that Claude Desktop addresses the MCP resource as <PUBLIC_URL>/mcp (the streamable-HTTP endpoint) rather than the base URL. Per RFC 9728 the protected-resource metadata then lives at .well-known/oauth-protected-resource inserted between host and path: https://mcp.buildmymcpserver.com/.well-known/oauth-protected-resource/<slug>/mcp Runner template now: - publishes `resource: <PUBLIC_URL>/mcp` - sets WWW-Authenticate to the RFC 9728 well-known URL - serves /.well-known/oauth-protected-resource[/*] so the metadata answers at both the legacy and RFC paths during transition - accepts both audiences (<PUBLIC_URL>/mcp + <PUBLIC_URL>) during rollout so already-issued tokens keep working API: - resolveServerByResource() tries port first, then path segment (production path-routing), with a guard against treating "mcp" as a tenant slug - AS metadata advertises resource_parameter_supported: true nginx (scripts/setup-runner-tls.sh + scripts/bmm-mcp-runners.nginx): - new location matches /.well-known/oauth-protected-resource/<slug>/... and proxies to the slug's runner with the slug stripped, so the runner sees the local well-known path Docs (oauth + api-reference) updated to the RFC paths. |
||
|
|
d0f3c202eb |
fix(tls): pivot per-runner TLS to path-routing on single subdomain
All checks were successful
Deploy to Production / deploy (push) Successful in 54s
The per-subdomain approach (*.mcp.buildmymcpserver.com) failed at the Cloudflare edge — Universal SSL only covers ONE-level wildcards, so the TLS handshake on slug.mcp.buildmymcpserver.com hits SSL alert 40 handshake_failure. The two paths to fix that (CF Advanced Cert Manager at $10/mo, or a Let's-Encrypt wildcard via DNS-01 with certbot) both trade either money or ops for the URL aesthetic. Pivot to path-routing on the single subdomain mcp.buildmymcpserver.com, which IS covered by free Universal SSL. publicUrl format changes from https://<slug>.mcp.buildmymcpserver.com → https://mcp.buildmymcpserver.com/<slug> No recurring cost, works with the existing CF setup, MCP clients don't care about the URL shape (it comes from the wizard's install snippet). Code changes: - generator/lib/deploy.ts: * publicUrl computed as `${MCP_DOMAIN}/${slug}` instead of `${slug}.${MCP_DOMAIN}` * writeRunnerMapEntry writes one-line nginx snippet: if ($bmm_slug = "<slug>") { set $bmm_port <port>; } (was: a map-entry pair "<slug>.<MCP_DOMAIN> <port>;") - setup-runner-tls.sh: * nginx vhost is now single server_name mcp.buildmymcpserver.com * regex location captures (?<bmm_slug>...)(?<bmm_path>/.*)? * includes runner-map.combined inside the location block so the generated if-snippets set $bmm_port; unknown slug → 404 * proxy_pass strips the slug prefix: /<slug>/foo → 127.0.0.1:port/foo * Prereq docs updated: just A-record for mcp (no wildcard needed), same Origin CA cert reused * Added /health endpoint at vhost root for monitoring Systemd watcher + map dir + volume mounts unchanged — same file paths, just different snippet content. Re-running setup-runner-tls.sh on the host overwrites the wildcard vhost with the new path-based one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8c6f04f034 |
feat: oauth refresh-token grant + per-runner subdomain TLS plumbing
All checks were successful
Deploy to Production / deploy (push) Successful in 52s
OAUTH REFRESH-TOKEN
- oauth_tokens.subject column added (migration applied to prod DB): stores
the JWT sub claim from the original authorization so refreshes can
re-mint with the same identity without re-walking the (consumed) code.
- Authorization-code branch now writes subject AND uses a 30-day
expires_at for the row (was 1h — same as access token, which killed
refresh after 1h).
- New refresh_token grant branch:
* looks up token by refresh-hash + expiry
* client_id must match, client_secret verified if confidential
* RFC 8707: requested resource must equal stored resource
* OAuth 2.1 rotation: atomic UPDATE WHERE old_hash → new access JWT,
new refresh token, extended expiry; loser of a race sees invalid_grant
- Access TTL (1h) and refresh TTL (30d) extracted as constants.
Clients no longer have to re-authorize hourly. Closes Zb-001.
PER-RUNNER SUBDOMAIN TLS (Z1-002)
Code path:
- New MCP_DOMAIN env (e.g. "mcp.buildmymcpserver.com") + RUNNER_MAP_DIR
(default /var/runner-map) in generator config.
- deployContainer: writes /var/runner-map/<slug>.conf with content
"slug.MCP_DOMAIN port;" and computes publicUrl as
https://<slug>.<MCP_DOMAIN>. Falls back to http://host:port when
MCP_DOMAIN is unset (zero behaviour change until host is configured).
- stopContainer (both api/lib/docker.ts and generator/lib/deploy.ts) now
accepts an optional slug arg and removes the map fragment. Callers
(DELETE /v1/servers/:id, admin template takedown) updated.
Infra path (one-time host setup — Marco runs as root):
- scripts/setup-runner-tls.sh:
1. nginx vhost matching *.mcp.buildmymcpserver.com via regex →
reads slug→port from /opt/buildmymcpserver/runner-map.combined
2. systemd inotify service watches the map dir, combines fragments
on any change, reloads nginx
3. installs inotify-tools if missing, idempotent
- Prereqs documented at top: Cloudflare wildcard DNS proxied, Origin CA
cert for *.mcp.buildmymcpserver.com, SSL mode Full (strict).
- After running: edit docker-compose.prod.yml to mount the map dir into
api + generator, set MCP_DOMAIN in env, recreate containers.
Closes Zb-001 fully. Closes Z1-002 on the code side; one Marco-on-host
action away from closing it on the infra side.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|