From 739d30513e977d3055ea919bcd9f42f5a81687be Mon Sep 17 00:00:00 2001 From: Vasyl Vdovychenko Date: Wed, 17 Jun 2026 10:09:12 -0400 Subject: [PATCH] ci(ai): bring up remote MCP server in prod deploy + health check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mcp-server Docker service is profile-gated (profiles:[mcp]) so CI's bare 'compose up' (docker/e2e jobs) never builds/runs it — but that also meant the prod deploy skipped it, leaving nginx /mcp pointing at nothing. - deploy.yml now runs 'docker compose --profile mcp ...' so prod opts the service in (CI still excludes it; the nginx /mcp block already syncs via the existing Sync-nginx step). - New 'Health check MCP server' deploy step (fail-loud): mcp-server /health up AND a real MCP initialize POST through nginx /mcp returns 200. After merge+deploy, https://textstack.app/mcp is the remote streamable-HTTP MCP endpoint (ChatGPT/Cursor/remote Claude), per-request device-flow-JWT auth. http transport verified locally (/health 200, POST /mcp initialize → text/event-stream 200). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/deploy.yml | 17 ++++++++++++++++- CHANGELOG.md | 4 ++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3aeddae2..027f4b8b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -193,7 +193,10 @@ jobs: # the default compose file (that would break the ci/docker job). run: | cd $PROJECT_DIR - docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d --build + # --profile mcp brings up the remote MCP server (mcp-server) in prod. + # It's profile-gated so CI's bare `compose up` (docker/e2e jobs) never + # builds/runs it; prod opts in here. nginx /mcp is synced below. + docker compose --profile mcp -f docker-compose.yml -f docker-compose.gpu.yml up -d --build - name: Sync nginx config run: | @@ -228,6 +231,18 @@ jobs: curl -sf http://localhost:80 || exit 1 echo "Frontend health check passed" + - name: Health check MCP server + run: | + # mcp-server is localhost-only on :8090; nginx fronts /mcp. + curl -sf http://localhost:8090/health || { echo "FAIL: mcp-server /health down"; exit 1; } + echo "mcp-server /health OK" + # MCP streamable endpoint through nginx (Host-routed, like a real client). + CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "Host: textstack.app" \ + -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"deploy-check","version":"1"}}}' \ + http://localhost/mcp) + [ "$CODE" = "200" ] && echo "nginx /mcp → MCP initialize 200 OK" || { echo "FAIL: /mcp returned $CODE"; exit 1; } + - name: Verify SEO setup run: | echo "=== SPA serves real users ===" diff --git a/CHANGELOG.md b/CHANGELOG.md index 706d7bb1..3ea78605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Phase 8 — enable remote MCP in the prod deploy (AI-049 follow-up) (2026-06-17) + +Makes the remote MCP endpoint actually go live. The `mcp-server` Docker service is profile-gated (`profiles:[mcp]`) so CI's bare `compose up` never builds/runs it — which also meant the prod deploy didn't bring it up. Fixed: `deploy.yml` now runs `docker compose --profile mcp ...` so prod opts the service in (CI still excludes it). The nginx `/mcp` block already syncs via the existing "Sync nginx config" step. Added a **Health check MCP server** deploy step (fail-loud): asserts `mcp-server` `/health` is up AND a real MCP `initialize` POST through nginx `/mcp` returns 200. After this merges + deploys, `https://textstack.app/mcp` is the remote streamable-HTTP MCP endpoint (for ChatGPT / Cursor / remote Claude), authenticated per-request via the device-flow JWT. Verified the http transport locally (`/health`→200, `POST /mcp` initialize→`text/event-stream` 200). + ### Phase 8 — MCP bridge: preserve API path prefix on relative URLs (AI-049) (2026-06-16) - **Fix.** The MCP bridge dropped the API **path prefix** (e.g. `/api`) on every request when `TEXTSTACK_API_URL` carried one (`https://textstack.app/api`). `TextStackApiClient` built relative URLs with a LEADING slash (`/search`, `/me/highlights/{id}`, `/books/{id}/ask`) and `HttpClient.BaseAddress` had NO trailing slash, so .NET resolved the leading-slash relative URI from the HOST ROOT and dropped `/api` — the bridge hit the SPA (`https://textstack.app/search`, 301 → HTML → `JsonException` → "upstream unavailable") instead of the API. Silently green in unit tests (fake handler ignores `BaseAddress`) and in prod-internal (`http://api:8080`, no prefix); confirmed live against prod. Fixed two ways: `McpBridgeCore.BaseUri(...)` now trailing-slash-normalizes the base (idempotent — no double slash), and `TextStackApiClient` + `DeviceFlowTokenProvider` build relative URIs leading-slash-stripped (`auth/device/code`, `auth/device/token`, `auth/refresh-mobile`), so the prefix segment survives instead of being treated as a replaceable file.