Skip to content

Load MCP Registry token from GCP Secret Manager - #625

Closed
wonderwhy-er wants to merge 1 commit into
mainfrom
feat/mcp-registry-token-from-gcp-secret
Closed

Load MCP Registry token from GCP Secret Manager#625
wonderwhy-er wants to merge 1 commit into
mainfrom
feat/mcp-registry-token-from-gcp-secret

Conversation

@wonderwhy-er

@wonderwhy-er wonderwhy-er commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Problem

The MCP Registry grants publish rights for io.github.wonderwhy-er/* based on the GitHub identity that authenticates, not on repo collaborator status. Since wonderwhy-er is a personal account rather than an org, there is nothing for a second maintainer to be a member of — @edgarsskore logging in with his own account gets io.github.edgarsskore/* and cannot publish this server.

That has made registry publishing a single-person bottleneck tied to one laptop's interactive login.

Approach

Share a scoped PAT (read:org + read:user) through GCP Secret Manager in the project we already use for telemetry (airy-runway-456110-g5), rather than passing a token around by hand.

Resolution order in publish-release.cjs:

  1. MCP_GITHUB_TOKEN env var — CI or ad-hoc override
  2. GCP Secret Manager (mcp-registry-pat) via gcloud — normal team path
  3. Interactive mcp-publisher login github — unchanged fallback

Secret location is overridable with MCP_SECRET_NAME / MCP_SECRET_PROJECT.

Changes

  • 🔑 getMcpToken() — resolves the PAT through the chain above, cached, never throws, never prints the token.
  • 🔐 mcpLogin() — prefers the shared token, falls back to the browser device flow.
  • ♻️ Pre-flight auto-recovery — a missing or expired registry session used to fail the release outright. When a shared token is available it now mints a fresh session and continues. Missing/expired session downgraded from error to warning accordingly.
  • 🔁 Step 6 retry — the existing 401 recovery path now goes through mcpLogin(), so it works unattended.
  • 🛡️ Token redactionexecSync embeds the full command in error.message, so a failed login printed the PAT verbatim. Caught during testing; the token is now scrubbed before the message surfaces.
  • 📖 --help — documents the auth chain and the per-member IAM grant.

Setup (not done by this PR)

# once, by @wonderwhy-er
gcloud secrets create mcp-registry-pat --project=airy-runway-456110-g5
printf '%s' "$PAT" | gcloud secrets versions add mcp-registry-pat \
  --data-file=- --project=airy-runway-456110-g5

# per team member
gcloud secrets add-iam-policy-binding mcp-registry-pat \
  --member="user:THEIR@EMAIL" \
  --role="roles/secretmanager.secretAccessor" \
  --project=airy-runway-456110-g5

Mint the PAT as fine-grained with an expiry and only read:org + read:user. Nothing else in the release path needs it.

Testing

Verified on macOS against the real mcp-publisher (v1.6.0):

  • node --check clean; npm run build passes
  • No credentials anywhere → pre-flight fails with actionable guidance naming both recovery routes, no interactive prompt triggered
  • Token present but invalid → auto-refresh attempted, fails cleanly with scope hint
  • Token redaction → 0 occurrences of the token in output on failure (was leaking before the fix)
  • --help renders the new section

Not yet exercised end-to-end with a real PAT, since the secret does not exist yet.

Notes

  • Trade-off worth naming: the registry still records wonderwhy-er as publisher regardless of who runs it. Secret Manager controls distribution and gives an access audit trail; it does not change attribution. Scoping the PAT tightly is what bounds the blast radius.
  • Rotation is manual — GitHub has no API to mint PATs, so Secret Manager rotation schedules do not help. The PAT expiry is the real backstop.
  • This does not preclude GitHub Actions later. Registry OIDC needs no secret at all and would make this redundant; if we ever move the repo into the desktop-commander org, revisit.
  • ⚠️ Unrelated, spotted in the working tree while building: src/server.ts:221 had a stray double-space (isWe lcomePageEligibleClient) that broke tsc, while line 226 still used the correct name. Reverted so the build would pass — that file is not part of this diff. Worth checking it was not mid-edit intentionally.
  • mcp-publisher is at v1.6.0 locally; v1.8.0 is available. Unrelated to this PR, but note CVE-2026-44428 (OIDC token replay across registry deployments) was fixed registry-side in 1.7.6.

Summary by CodeRabbit

  • New Features

    • Added flexible MCP Registry authentication using environment credentials, secure cloud-stored credentials, or interactive login.
    • Added automatic session refresh during release publishing when shared credentials are available.
    • Added credential setup guidance and authentication help text.
  • Bug Fixes

    • Improved handling of expired or missing sessions during publishing and retries.
    • Redacted authentication tokens from login failure messages.

The registry ties the io.github.wonderwhy-er/* namespace to the GitHub
identity that authenticates, not to repo collaborator status, so a second
maintainer cannot publish with their own account. Share a scoped PAT
(read:org + read:user) through GCP Secret Manager instead.

- Add getMcpToken(): MCP_GITHUB_TOKEN env -> gcloud secret -> null
- Add mcpLogin(): prefer shared token, fall back to interactive device flow
- Pre-flight now auto-refreshes an expired registry session when a shared
  token is available instead of failing the release
- Step 6 retry loop reuses mcpLogin() for its 401 recovery
- Redact the token from any login failure message; execSync embeds the
  full command in error.message, which would otherwise leak the PAT into
  terminal scrollback and CI logs
- Downgrade missing/expired session from error to warning, since it is now
  a recoverable condition
- Document the auth chain and the per-member IAM grant in --help

Secret location overridable via MCP_SECRET_NAME / MCP_SECRET_PROJECT.
Behaviour is unchanged when no token is present anywhere.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The release script adds MCP Registry credential resolution from environment variables or GCP Secret Manager. It refreshes missing or expired sessions during pre-flight checks and reuses shared authentication during publish retries.

Changes

MCP Registry authentication

Layer / File(s) Summary
Credential resolution and login
scripts/publish-release.cjs
The script resolves and caches credentials from MCP_GITHUB_TOKEN or GCP Secret Manager. It redacts tokens from login errors and documents authentication setup.
Pre-flight session refresh
scripts/publish-release.cjs
Missing and expired sessions produce warnings. Pre-flight checks refresh sessions when shared credentials exist and provide interactive-login guidance otherwise.
Publish retry authentication
scripts/publish-release.cjs
Publish retries use the shared mcpLogin() flow and updated credential guidance.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReleaseScript as publish-release.cjs
  participant SecretManager as GCP Secret Manager
  participant MCPPublisher as mcp-publisher
  participant Registry as MCP Registry

  ReleaseScript->>SecretManager: Resolve shared token when needed
  SecretManager-->>ReleaseScript: Return token
  ReleaseScript->>MCPPublisher: Refresh or perform login
  MCPPublisher-->>ReleaseScript: Return session status
  ReleaseScript->>Registry: Retry MCP publish
  Registry-->>ReleaseScript: Return publish result
Loading

Possibly related PRs

Suggested labels: size:L

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the primary feature: loading the MCP Registry token from GCP Secret Manager.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-registry-token-from-gcp-secret

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/publish-release.cjs`:
- Around line 142-145: Update the MCP_GITHUB_TOKEN handling in the token-loading
function to trim the environment value before checking it, and only cache and
return the normalized token when it is non-empty. Treat whitespace-only values
as absent so the existing GCP Secret Manager fallback executes.
- Around line 154-156: The release script currently interpolates
credential-derived values into shell command strings. Update the command
execution in the token retrieval flow and the mcp-publisher invocation to use
execFileSync or spawnSync with separate argv arguments, preserving existing
commands and behavior while preventing secrets from being shell-interpreted.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 28595034-9415-41b9-b74e-52e9e5c74a9a

📥 Commits

Reviewing files that changed from the base of the PR and between 9bd8422 and 1ff181c.

📒 Files selected for processing (1)
  • scripts/publish-release.cjs

Comment on lines +142 to +145
if (process.env.MCP_GITHUB_TOKEN) {
printInfo('Using MCP Registry token from MCP_GITHUB_TOKEN');
cachedMcpToken = process.env.MCP_GITHUB_TOKEN.trim();
return cachedMcpToken;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Ignore a whitespace-only environment token.

MCP_GITHUB_TOKEN=" " passes the current truthy check, then becomes an empty cached value. This suppresses the GCP Secret Manager fallback. Trim once before the condition and use the normalized value only when it is non-empty.

Proposed fix
-    if (process.env.MCP_GITHUB_TOKEN) {
+    const envToken = process.env.MCP_GITHUB_TOKEN?.trim();
+    if (envToken) {
         printInfo('Using MCP Registry token from MCP_GITHUB_TOKEN');
-        cachedMcpToken = process.env.MCP_GITHUB_TOKEN.trim();
+        cachedMcpToken = envToken;
         return cachedMcpToken;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (process.env.MCP_GITHUB_TOKEN) {
printInfo('Using MCP Registry token from MCP_GITHUB_TOKEN');
cachedMcpToken = process.env.MCP_GITHUB_TOKEN.trim();
return cachedMcpToken;
const envToken = process.env.MCP_GITHUB_TOKEN?.trim();
if (envToken) {
printInfo('Using MCP Registry token from MCP_GITHUB_TOKEN');
cachedMcpToken = envToken;
return cachedMcpToken;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/publish-release.cjs` around lines 142 - 145, Update the
MCP_GITHUB_TOKEN handling in the token-loading function to trim the environment
value before checking it, and only cache and return the normalized token when it
is non-empty. Treat whitespace-only values as absent so the existing GCP Secret
Manager fallback executes.

Comment on lines +154 to +156
const token = execSilent(
`gcloud secrets versions access latest --secret=${GCP_SECRET_NAME} --project=${GCP_PROJECT_ID} 2>/dev/null`,
{ ignoreError: true }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the local command wrapper and all credential-derived command construction.
rg -n -C 12 'function exec\b|const exec\b|let exec\b|exec\s*=' scripts/publish-release.cjs
rg -n -C 5 'gcloud secrets versions access|mcp-publisher login github' scripts/publish-release.cjs

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 7181


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the imports and top-level command construction to determine the exec wrapper behavior.
sed -n '1,110p' scripts/publish-release.cjs

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 2862


🌐 Web query:

Node child_process execSync string command shell option windows command argument vector array documentation

💡 Result:

In Node.js, the child_process.execSync(command, options) function is a synchronous method that executes a command by spawning a shell [1][2]. The shell option in execSync is used to specify the shell to execute the command with [1][3]. Unlike execFile, which executes a file directly, execSync is designed to run commands through a shell [1][4]. Key details regarding the shell option and command execution: 1. Behavior: When shell is enabled (default is true for exec/execSync), the provided command string is executed inside a shell [1][2]. 2. Default Shell: - On Unix, it defaults to /bin/sh [1][3]. - On Windows, it defaults to the value of process.env.ComSpec (typically cmd.exe) [1][3][5]. 3. Customization: A different shell can be specified as a string value for the shell option [1]. 4. Security Warning: Because execSync runs commands through a shell, passing unsanitized user input to the command string can lead to shell injection vulnerabilities [1][4]. 5. Argument Handling: Because execSync processes the entire command as a string, it does not support passing arguments as an array [2][6]. If you need to pass arguments securely, you should use child_process.execFileSync or child_process.spawnSync, which support passing arguments as a separate array and do not require shell invocation by default [2][4][6]. On Windows specifically, the windowsVerbatimArguments option (boolean) can be used to prevent automatic quoting or escaping of arguments when the shell is not used; however, when the shell is set to cmd.exe, this option is generally ignored or handled by the shell itself [3]. The combination of using execSync and a shell means that the command string is interpreted by the shell, making it distinct from methods that treat input as a direct argument vector [6].

Citations:


Do not pass credential-derived values through execSync command strings.

exec calls execSync(command) with a shell command string. The gcloud invocation at lines 154-156 interpolates secret overrides, and the mcp-publisher invocation at line 184 interpolates the resolved PAT. Use a non-shell process API, such as execFileSync/spawnSync, with argv so secret data is not interpreted by the shell.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/publish-release.cjs` around lines 154 - 156, The release script
currently interpolates credential-derived values into shell command strings.
Update the command execution in the token retrieval flow and the mcp-publisher
invocation to use execFileSync or spawnSync with separate argv arguments,
preserving existing commands and behavior while preventing secrets from being
shell-interpreted.

@wonderwhy-er
wonderwhy-er deleted the feat/mcp-registry-token-from-gcp-secret branch August 6, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant