diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 4c99433..edef9bf 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "reva-turbo", "displayName": "RevAOps Plugin", - "version": "2.1.4", + "version": "2.1.5", "description": "The Rev A Manufacturing PM copilot — full RFQ-to-delivery workflow with CRM, memory, and 48 skills. Paste your key in chat and you're in; bring your own CRM (HubSpot, Salesforce, Attio, Pipedrive) or use the bundled Nakatomi + AutoMem defaults.", "author": { "name": "Rev A Manufacturing / MrDula Solutions", diff --git a/plugin/VERSION b/plugin/VERSION index 7d2ed7c..cd57a8b 100644 --- a/plugin/VERSION +++ b/plugin/VERSION @@ -1 +1 @@ -2.1.4 +2.1.5 diff --git a/plugin/bin/reva-mcp-launch.sh b/plugin/bin/reva-mcp-launch.sh index 04a468c..652bc98 100755 --- a/plugin/bin/reva-mcp-launch.sh +++ b/plugin/bin/reva-mcp-launch.sh @@ -36,8 +36,12 @@ if [ -f "$CRED_FILE" ]; then fi # Fall back to the Rev A production router if the PM hasn't overridden it. -# The /mcp suffix matches the router's mount point in services/mcp-router. -: "${REVA_MCP_URL:=https://mcp-router-production-460a.up.railway.app/mcp}" +# The /mcp/ suffix (TRAILING SLASH is load-bearing) matches the router's +# mount point. Without the slash, Starlette's Mount issues a 307 redirect +# to /mcp/ and mcp-remote drops the POST body (and the Authorization +# header, if the redirect downgrades to HTTP behind Railway's proxy) — +# the classic "Missing session ID" install failure. +: "${REVA_MCP_URL:=https://mcp-router-production-460a.up.railway.app/mcp/}" : "${REVA_API_KEY:=}" # Hand off to mcp-remote. `-y` bypasses the npx prompt, which would hang diff --git a/plugin/scripts/desktop-install.sh b/plugin/scripts/desktop-install.sh index 4dc575a..df73374 100755 --- a/plugin/scripts/desktop-install.sh +++ b/plugin/scripts/desktop-install.sh @@ -32,7 +32,7 @@ set -uo pipefail REPO="mrdulasolutions/RevOps-RevAMfg" TAG="${REVA_RELEASE_TAG:-latest}" -DEFAULT_MCP_URL="https://mcp-router-production-460a.up.railway.app/mcp" +DEFAULT_MCP_URL="https://mcp-router-production-460a.up.railway.app/mcp/" REVA_MCP_URL="${REVA_MCP_URL:-$DEFAULT_MCP_URL}" # Pretty-ish output that still reads fine inside AppleScript's `do shell diff --git a/plugin/skills/revmyengine/SKILL.md b/plugin/skills/revmyengine/SKILL.md index da18d29..fa20a6c 100644 --- a/plugin/skills/revmyengine/SKILL.md +++ b/plugin/skills/revmyengine/SKILL.md @@ -1,7 +1,7 @@ --- name: revmyengine preamble-tier: 1 -version: 2.1.4 +version: 2.1.5 description: | REVA-TURBO master orchestrator for Rev A Manufacturing PM workflow. Routes requests to the correct sub-skill based on context. Chains the @@ -272,15 +272,41 @@ file was written, and wait. Extract the key from the PM's message (grep for `nk_[A-Za-z0-9_-]+`). If the user also provided a router URL, capture that too (look for an -`https://…/mcp` token); otherwise default to the Rev A production URL. +`https://…/mcp` token); otherwise default to +`https://mcp-router-production-460a.up.railway.app/mcp/` (trailing +slash is load-bearing — see note at end of section). -Write credentials to the file the MCP launcher reads on Desktop -startup (`bin/reva-mcp-launch.sh`): +#### ⚠️ CRITICAL: detect the execution environment BEFORE writing + +The credentials file must land on the **user's real Mac** at +`~/.reva-turbo/state/mcp-credentials.env` — that's the file Claude +Desktop's launcher reads on startup. **In Claude Cowork**, the `Bash` +tool writes to an ephemeral sandbox filesystem, *not* the real Mac — +credentials written via Bash inside Cowork never reach Desktop and +the plugin stays disconnected. + +Capability detection (check BEFORE writing creds): + +| Environment | Signal | Write creds via | +|---|---|---| +| **Claude Code** (runs on the Mac itself) | `Bash` tool present AND no `mcp__computer-use__*` / `mcp__Control_your_Mac__*` | Bash — writes land on the real Mac | +| **Claude Cowork** (sandboxed) | Both `Bash` AND `mcp__Control_your_Mac__osascript` present | `mcp__Control_your_Mac__osascript` — punches through the sandbox to the real Mac | +| **Claude Desktop with Bash** (rare) | `Bash` only, no osascript | Bash works; native macOS filesystem | +| **Claude Desktop, no Bash, osascript present** | `mcp__Control_your_Mac__osascript` only | osascript | +| **No shell-capable tool at all** | Filesystem MCP write tools only | Filesystem MCP writing to `$HOME/.reva-turbo/state/mcp-credentials.env` | + +**Rule of thumb:** *if `mcp__Control_your_Mac__osascript` is present, +prefer it over `Bash` for credential writes.* Cowork has both Bash +(sandbox) and osascript (real Mac) — if you use Bash you silently +miss the Mac, and the PM watches you "succeed" while nothing actually +works. + +#### Write creds via Bash (Claude Code / native Desktop) ```bash mkdir -p ~/.reva-turbo/state _KEY="NK_KEY_HERE" # replace with the extracted nk_... value -_URL="MCP_URL_HERE" # replace; default: https://mcp-router-production-460a.up.railway.app/mcp +_URL="MCP_URL_HERE" # default: https://mcp-router-production-460a.up.railway.app/mcp/ cat > ~/.reva-turbo/state/mcp-credentials.env < $HOME/.reva-turbo/state/mcp-credentials.env <<'EOF' +REVA_MCP_URL=https://mcp-router-production-460a.up.railway.app/mcp/ +REVA_API_KEY=NK_KEY_HERE +EOF +chmod 600 $HOME/.reva-turbo/state/mcp-credentials.env" +``` + +Substitute `NK_KEY_HERE` with the actual `nk_...` value **before** +sending to `mcp__Control_your_Mac__osascript`. Never log the key. + +#### Validate the key immediately (same environment) + +Hit the router's `/auth/me` endpoint — proxied through to Nakatomi, +returns the user record on a valid key. Use the **same execution +environment** you used to write the creds so you're validating against +the same network (e.g. osascript if you wrote via osascript, Bash if +you wrote via Bash): ```bash -_BASE="${_URL%/mcp}" +# Bash environments: +_BASE="${_URL%/mcp/}" curl -fsS -H "Authorization: Bearer $_KEY" "$_BASE/auth/me" | head -c 400 + +# osascript environment: +do shell script "curl -fsS -H 'Authorization: Bearer NK_KEY_HERE' 'https://mcp-router-production-460a.up.railway.app/auth/me' 2>&1 | head -c 400" ``` - If `curl` succeeds (HTTP 200, JSON with `email`/`workspace`): tell @@ -317,6 +364,18 @@ curl -fsS -H "Authorization: Bearer $_KEY" "$_BASE/auth/me" | head -c 400 - The file mode is 600 — belt-and-suspenders against any other process on the box. +**Why the trailing slash on `/mcp/` is load-bearing:** the router +mounts FastMCP under `/mcp`. A POST to `/mcp` (no slash) triggers +Starlette's auto-redirect to `/mcp/` as a 307. Some MCP clients +(notably `mcp-remote` ≤ 0.x) drop the request body when following a +307 POST — and if the origin is behind a proxy that doesn't pass +X-Forwarded-Proto through, the redirect target can come back `http://` +instead of `https://`, at which point the Authorization header gets +stripped on protocol downgrade. The router has proxy-header trust +enabled as of v2.1.5 and rewrites `/mcp` → `/mcp/` in middleware, so +both URLs now work server-side — but the credentials file should +always carry the trailing-slash form. Defense in depth. + Do not proceed to Steps 1–4 until the tool call succeeds. ## Trust Level Injection diff --git a/services/mcp-router/Dockerfile b/services/mcp-router/Dockerfile index 6d63318..7b090fc 100644 --- a/services/mcp-router/Dockerfile +++ b/services/mcp-router/Dockerfile @@ -14,4 +14,8 @@ COPY router ./router EXPOSE 8080 # Railway supplies $PORT; default to 8080 for local. -CMD ["sh", "-c", "uvicorn router.main:app --host 0.0.0.0 --port ${PORT:-8080}"] +# --proxy-headers + --forwarded-allow-ips='*' make uvicorn trust Railway's +# X-Forwarded-Proto so any redirect (e.g. Starlette's Mount trailing-slash +# redirect on /mcp → /mcp/) stays HTTPS instead of downgrading to HTTP and +# stripping the Authorization header. +CMD ["sh", "-c", "uvicorn router.main:app --host 0.0.0.0 --port ${PORT:-8080} --proxy-headers --forwarded-allow-ips='*'"] diff --git a/services/mcp-router/router/main.py b/services/mcp-router/router/main.py index cafbc15..a993d7f 100644 --- a/services/mcp-router/router/main.py +++ b/services/mcp-router/router/main.py @@ -52,6 +52,20 @@ async def lifespan(_: FastAPI): yield app = FastAPI(title="REVA MCP Router", version="0.1.0", lifespan=lifespan) + + # Normalize /mcp → /mcp/ IN-PLACE (no 307 redirect). Starlette's Mount + # would otherwise redirect any missing-trailing-slash request, and + # mcp-remote / other MCP clients drop the POST body (and Authorization + # header, if the redirect downgrades to HTTP) when following a 307. + # Rewriting the path at the ASGI layer bypasses that entirely — clients + # can POST to /mcp OR /mcp/ and both work. + @app.middleware("http") + async def normalize_mcp_trailing_slash(request, call_next): + if request.scope["path"] == "/mcp": + request.scope["path"] = "/mcp/" + request.scope["raw_path"] = b"/mcp/" + return await call_next(request) + app.mount("/mcp", mcp.streamable_http_app()) # Self-service signup (GET /signup HTML, POST /signup JSON) diff --git a/services/mcp-router/router/signup.py b/services/mcp-router/router/signup.py index 5da89ae..7517c44 100644 --- a/services/mcp-router/router/signup.py +++ b/services/mcp-router/router/signup.py @@ -491,7 +491,7 @@ def _extract_detail(resp: httpx.Response, fallback: str) -> str: