Skip to content

feat: add Claude Code marketplace plugin and env var tool filtering#623

Open
mickey-mikey wants to merge 39 commits intotaylorwilsdon:mainfrom
mickey-mikey:feat/plugin-marketplace
Open

feat: add Claude Code marketplace plugin and env var tool filtering#623
mickey-mikey wants to merge 39 commits intotaylorwilsdon:mainfrom
mickey-mikey:feat/plugin-marketplace

Conversation

@mickey-mikey
Copy link
Copy Markdown
Contributor

@mickey-mikey mickey-mikey commented Mar 28, 2026

Description

Add Claude Code marketplace plugin manifests (.claude-plugin/) and WORKSPACE_MCP_* env var fallbacks in main.py so plugin users can configure OAuth credentials and tool filtering at install time via userConfig prompts.

userConfig fields (all prompted at install)

Field Type Sensitive Description
google_oauth_client_id string no OAuth 2.0 Client ID
google_oauth_client_secret string yes (keychain) OAuth 2.0 Client Secret
tools string no Comma-separated services: gmail, drive, etc. Empty for all.
tool_tier string no core, extended, or complete. Empty for all tools.
read_only string no true for read-only mode. Empty for full access.
permissions string no Space-separated service:level pairs. Empty for full access.

See #589 for the companion skill PR.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this change manually

Test plan

  • Add marketplace: /plugin marketplace add mickey-mikey/google_workspace_mcp#feat/plugin-marketplace
  • Install plugin: /plugin install google-workspace-mcp@google-workspace-mcp
  • Verify plugin prompts for all 6 fields
  • Verify OAuth flow via start_google_auth
  • Verify gmail,drive for tools loads only Gmail + Drive
  • Verify core for tool_tier loads only core tier
  • Verify true for read_only enables read-only mode
  • Verify gmail:send drive:readonly for permissions
  • Verify empty filtering fields load all tools with full access
  • Verify CLI args still take precedence over env vars

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have enabled "Allow edits from maintainers" for this pull request

Additional Notes

All userConfig fields are marked required: true intentionally — when required: false, the plugin framework skips the field at install and ${user_config.*} substitution fails at server startup. Users press Enter on optional fields to accept the empty default; main.py treats empty strings as no-ops.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Claude plugin adding Google Workspace integration with configurable OAuth credentials, selectable Workspace services, capability tiers, read-only mode, and per-service permission controls.
    • Optional HTTP endpoint that can run alongside stdio when a port is configured; otherwise falls back to stdio-only.
    • Improved startup/config validation with environment-variable fallbacks and clearer error handling.
  • Chores

    • Plugin registered to the marketplace (v1.18.0).

mickey-mikey and others added 2 commits March 28, 2026 14:54
Add marketplace deployment manifests and WORKSPACE_MCP_TOOLS/
WORKSPACE_MCP_TOOL_TIER env var fallbacks so plugin users can
restrict tool loading via ~/.claude/settings.json without needing
CLI args.

- .claude-plugin/plugin.json and marketplace.json
- Env var fallbacks in main.py for --tools and --tool-tier args

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Claude plugin manifests for google-workspace-mcp and refactors main.py to centralize service registration, derive CLI choices from that registry, add env-var fallbacks and validation, defer transport defaulting, generate dynamic service imports, and optionally run stdio concurrently with an HTTP /mcp endpoint.

Changes

Cohort / File(s) Summary
Marketplace manifest
\.claude-plugin/marketplace.json
Adds a marketplace manifest registering google-workspace-mcp with owner and descriptive metadata.
Plugin manifest
\.claude-plugin/plugin.json
Adds plugin manifest (v1.18.0) declaring userConfig for Google OAuth and runtime options (tools, tool_tier, read_only, permissions), maps these to WORKSPACE_MCP_* env vars for an MCP server mcpServers.google-workspace, forces OAUTHLIB_INSECURE_TRANSPORT="1", and sets WORKSPACE_MCP_HTTP_PORT="8001".
MCP runtime & CLI
main.py
Introduces SERVICE_MODULES registry and derived VALID_SERVICES; updates CLI --tools choices and defers --transport default; adds env-var fallbacks/validation for WORKSPACE_MCP_* (tools, tool_tier, read_only, permissions, transport); replaces hardcoded per-service imports with generated mapping; when WORKSPACE_MCP_HTTP_PORT is set, can run stdio asynchronously alongside a uvicorn /mcp HTTP endpoint (port-checked), otherwise preserves prior stdio behavior.

Sequence Diagram(s)

sequenceDiagram
  participant Env as Environment (WORKSPACE_MCP_*)
  participant CLI as main.py (CLI)
  participant Server as MCP Server
  participant HTTP as HTTP endpoint (uvicorn)
  participant STDIO as STDIO loop

  Env->>CLI: Provide WORKSPACE_MCP_* vars (tools, tool_tier, read_only, permissions, transport, http_port)
  CLI->>CLI: Parse args, apply env fallbacks, validate against SERVICE_MODULES / VALID_SERVICES
  CLI->>Server: Build service import mapping and runtime config
  alt transport == "streamable-http"
    CLI->>Server: Start server with streamable-http transport
    Server->>HTTP: Serve streamable HTTP endpoints
  else stdio transport
    CLI->>Server: Prepare stdio runtime
    alt WORKSPACE_MCP_HTTP_PORT set and port available
      par
        Server->>STDIO: run_stdio_async()
      and
        CLI->>HTTP: Start uvicorn on /mcp
      end
    else
      Server->>STDIO: run() (stdio-only)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through manifests, tidy and bright,
Env-vars hummed secrets deep in the night.
Services lined up, mapped with nimble paws,
Stdio chats while HTTP opens doors.
A carrot-clicked deploy — tiny hops and applause!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding Claude Code marketplace plugin manifests and implementing environment variable fallbacks for tool filtering.
Description check ✅ Passed The PR description follows the template structure with all required sections completed: Description, Type of Change, Testing, and Checklist. The description provides comprehensive context including userConfig fields, test plan, and implementation notes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@mickey-mikey mickey-mikey mentioned this pull request Mar 28, 2026
20 tasks
mickey-mikey and others added 6 commits March 28, 2026 15:20
Use plugin framework's userConfig to prompt users for OAuth
client ID and secret, passing them as env vars to the MCP server.
Sensitive secret is stored in system keychain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without required: true, the plugin framework doesn't prompt
for values on enable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Plugin users configure tools, tool_tier, read_only, and permissions
at install time via userConfig prompts. Values pass to main.py as
WORKSPACE_MCP_* env vars, read as fallbacks when CLI args aren't set.

- plugin.json: 6 userConfig fields (OAuth creds + 4 filtering options)
- main.py: env var fallback block after argparse for all 4 filtering options
- Boolean read_only uses type: boolean for toggle UI
- Empty optional values are ignored (falsy/not-in-valid-set checks)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…stituted

Optional fields are skipped at install and can't be substituted
in env vars. Making them required ensures the user is prompted
and empty values still work (main.py ignores empty env vars).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mickey-mikey mickey-mikey marked this pull request as ready for review March 28, 2026 05:43
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
main.py (2)

214-217: Invalid service names are silently ignored.

If a user misconfigures WORKSPACE_MCP_TOOLS with a typo (e.g., "gmail,calender,drive"), the invalid entry is silently dropped. Consider logging a debug/warning message for discoverability.

🔍 Optional: Add logging for invalid services
     if args.tools is None:
         _env_tools = os.getenv("WORKSPACE_MCP_TOOLS")
         if _env_tools:
-            _parsed = [t.strip() for t in _env_tools.split(",") if t.strip() in _VALID_SERVICES]
+            _raw = [t.strip() for t in _env_tools.split(",") if t.strip()]
+            _invalid = [t for t in _raw if t not in _VALID_SERVICES]
+            if _invalid:
+                logger.debug("Ignoring invalid services from WORKSPACE_MCP_TOOLS: %s", _invalid)
+            _parsed = [t for t in _raw if t in _VALID_SERVICES]
             if _parsed:
                 args.tools = _parsed
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` around lines 214 - 217, The parsing silently drops invalid service
names from WORKSPACE_MCP_TOOLS: when splitting _env_tools into _parsed against
_VALID_SERVICES and assigning to args.tools, any invalid tokens should be
surfaced; modify the block around _env_tools/_parsed/_VALID_SERVICES to compute
the invalid_items (tokens present in _env_tools but not in _VALID_SERVICES) and
emit a logger.warning or logger.debug with those invalid names and a short hint
about valid values, then continue to set args.tools = _parsed as before;
reference the variables _env_tools, _parsed, _VALID_SERVICES, and args.tools to
locate the change.

208-211: Consider extracting _VALID_SERVICES to avoid duplication.

This set duplicates the choices list from the --tools argument on lines 161-174. If the list of valid services changes, both locations must be updated.

♻️ Suggested refactor

Extract the services list to a module-level constant:

+# Valid service names for tool filtering
+VALID_SERVICES = frozenset({
+    "gmail", "drive", "calendar", "docs", "sheets", "chat",
+    "forms", "slides", "tasks", "contacts", "search", "appscript",
+})
+
 def main():
     ...
     parser.add_argument(
         "--tools",
         nargs="*",
-        choices=[
-            "gmail", "drive", "calendar", ...
-        ],
+        choices=sorted(VALID_SERVICES),
         ...
     )
     ...
     # Env var fallbacks
-    _VALID_SERVICES = {
-        "gmail", "drive", "calendar", "docs", "sheets", "chat",
-        "forms", "slides", "tasks", "contacts", "search", "appscript",
-    }
     if args.tools is None:
         _env_tools = os.getenv("WORKSPACE_MCP_TOOLS")
         if _env_tools:
-            _parsed = [t.strip() for t in _env_tools.split(",") if t.strip() in _VALID_SERVICES]
+            _parsed = [t.strip() for t in _env_tools.split(",") if t.strip() in VALID_SERVICES]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` around lines 208 - 211, Extract the duplicated services set into one
module-level constant (e.g., SERVICES or AVAILABLE_SERVICES) and use that single
symbol both for _VALID_SERVICES and for the choices passed to the --tools
argparse argument; update references in the file so _VALID_SERVICES is assigned
from that constant and the argparse --tools choices parameter uses the same
constant (convert to list/tuple as needed) to eliminate duplication and ensure a
single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude-plugin/plugin.json:
- Around line 23-50: The four filtering properties tools, tool_tier, read_only,
and permissions in the plugin JSON are incorrectly marked "required": true;
update each property (tools, tool_tier, read_only, permissions) to set
"required": false to match their "Leave empty for..." descriptions and other
Claude plugin conventions so these fields are treated as optional.

In `@main.py`:
- Around line 218-221: Tool tier environment variable comparison is
case-sensitive causing values like "CORE" to be ignored; normalize _env_tier
(e.g., call .lower() or .casefold()) before checking membership in
{"core","extended","complete"} and assign the normalized value to args.tool_tier
so it behaves consistently with the read_only normalization used elsewhere (see
args.tool_tier and WORKSPACE_MCP_TOOL_TIER references).

---

Nitpick comments:
In `@main.py`:
- Around line 214-217: The parsing silently drops invalid service names from
WORKSPACE_MCP_TOOLS: when splitting _env_tools into _parsed against
_VALID_SERVICES and assigning to args.tools, any invalid tokens should be
surfaced; modify the block around _env_tools/_parsed/_VALID_SERVICES to compute
the invalid_items (tokens present in _env_tools but not in _VALID_SERVICES) and
emit a logger.warning or logger.debug with those invalid names and a short hint
about valid values, then continue to set args.tools = _parsed as before;
reference the variables _env_tools, _parsed, _VALID_SERVICES, and args.tools to
locate the change.
- Around line 208-211: Extract the duplicated services set into one module-level
constant (e.g., SERVICES or AVAILABLE_SERVICES) and use that single symbol both
for _VALID_SERVICES and for the choices passed to the --tools argparse argument;
update references in the file so _VALID_SERVICES is assigned from that constant
and the argparse --tools choices parameter uses the same constant (convert to
list/tuple as needed) to eliminate duplication and ensure a single source of
truth.
🪄 Autofix (Beta)

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

Run ID: cacb7d9a-08be-46b8-b8ff-f160540a97df

📥 Commits

Reviewing files that changed from the base of the PR and between b85a953 and e7d0baf.

📒 Files selected for processing (3)
  • .claude-plugin/marketplace.json
  • .claude-plugin/plugin.json
  • main.py

Comment on lines +23 to +50
"tools": {
"title": "Services to enable",
"description": "Comma-separated list: gmail, drive, calendar, docs, sheets, chat, forms, slides, tasks, contacts, search, appscript. Leave empty for all.",
"type": "string",
"sensitive": false,
"required": true
},
"tool_tier": {
"title": "Tool tier",
"description": "core, extended, or complete. Leave empty for all tools.",
"type": "string",
"sensitive": false,
"required": true
},
"read_only": {
"title": "Read-only mode",
"description": "true to restrict to read-only scopes. Leave empty for full access.",
"type": "string",
"sensitive": false,
"required": true
},
"permissions": {
"title": "Per-service permissions",
"description": "Space-separated service:level pairs. Example: gmail:send drive:readonly. Gmail levels: readonly, organize, drafts, send, full. Other services: readonly, full. Mutually exclusive with read-only and tools. Leave empty for full access.",
"type": "string",
"sensitive": false,
"required": true
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Claude plugin userConfig required field empty string behavior

💡 Result:

In Claude Code plugins, the userConfig field in the .claude-plugin/plugin.json manifest declares user-configurable values that Claude Code prompts the user to provide when enabling the plugin. Each entry is an object with a description (prompt text) and optional sensitive flag (true for secrets like tokens). No sources explicitly document behavior for required fields or empty strings in userConfig. The schema shows simple string fields without type: "string", minLength, or required indicators, suggesting fields are optional or accept empty strings. Similar Claude Code validation issues (e.g., agents field expecting arrays, tool schemas needing required: []) imply strict Zod/JSON Schema enforcement. If a field were required and empty, it would likely fail validation (error like "Expected string, received undefined" or minLength violation), preventing plugin enablement. However, since userConfig prompts users interactively, empty input may be accepted as "" unless validated otherwise. No bugs report empty string failures specifically for userConfig. To test: Define "myfield": {"description": "Enter value", "sensitive": false}, enable plugin, submit empty—likely succeeds with empty config value, as no evidence of rejection.

Citations:


🏁 Script executed:

# Check if there are any other plugin.json files or documentation about required fields
find . -type f -name "plugin.json" -o -name "*plugin*" -type f | head -20

Repository: taylorwilsdon/google_workspace_mcp

Length of output: 106


🏁 Script executed:

# Search for any documentation or comments about required fields in plugin configuration
rg -i "required" --type json | head -20

Repository: taylorwilsdon/google_workspace_mcp

Length of output: 538


🏁 Script executed:

# Check the full structure of the plugin.json file to understand the context better
cat -n .claude-plugin/plugin.json

Repository: taylorwilsdon/google_workspace_mcp

Length of output: 3029


Change "required": false for optional filtering fields to resolve semantic inconsistency.

The filtering fields (tools, tool_tier, read_only, permissions) are marked "required": true but have descriptions saying "Leave empty for...". This creates a semantic contradiction—optional fields should use "required": false, as shown in other Claude plugin configurations.

Update lines 23–50 to change "required": true to "required": false for these four fields to clarify that they are optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude-plugin/plugin.json around lines 23 - 50, The four filtering
properties tools, tool_tier, read_only, and permissions in the plugin JSON are
incorrectly marked "required": true; update each property (tools, tool_tier,
read_only, permissions) to set "required": false to match their "Leave empty
for..." descriptions and other Claude plugin conventions so these fields are
treated as optional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Intentional. When required: false, the plugin framework skips the field at install time and then fails at server startup with "Missing required user configuration value" because ${user_config.*} substitution has no value to inject. Making them required: true with "Leave empty for..." descriptions is the current workaround — users just press Enter to accept the empty default, and main.py treats empty strings as no-ops.

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.

@mickey-mikey, thanks for the detailed explanation — that's a non-obvious but completely valid workaround. The framework's behavior of silently skipping required: false fields and then failing on unresolved ${user_config.*} substitutions at startup makes the required: true + "press Enter for empty" pattern the only practical option here. My original comment was incorrect given this constraint. The code is correct as-is.


✏️ Learnings added
Learnt from: mickey-mikey
Repo: taylorwilsdon/google_workspace_mcp PR: 623
File: .claude-plugin/plugin.json:23-50
Timestamp: 2026-03-28T06:03:32.583Z
Learning: In `.claude-plugin/plugin.json` for Claude Code plugins (google_workspace_mcp repository), optional filtering fields (`tools`, `tool_tier`, `read_only`, `permissions`) are intentionally marked `"required": true` even though their descriptions say "Leave empty for...". This is because the Claude plugin framework skips `required: false` fields at install time, causing `${user_config.*}` variable substitution to fail at server startup with "Missing required user configuration value". The workaround is to mark all fields `required: true` so users are prompted (and can press Enter to submit an empty string), while `main.py` treats empty strings as no-ops. This is a known limitation of the Claude plugin framework's userConfig substitution behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread main.py
mickey-mikey and others added 2 commits March 28, 2026 17:13
- Case-insensitive parsing for WORKSPACE_MCP_TOOLS and WORKSPACE_MCP_TOOL_TIER
- Log warning for invalid service names in WORKSPACE_MCP_TOOLS
- Extract VALID_SERVICES constant shared between argparse and env var fallbacks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
main.py (1)

49-52: Consider deriving VALID_SERVICES from tool_imports to prevent drift.

If a new service is added to tool_imports but not to VALID_SERVICES, users won't be able to specify it via --tools or env vars. Consider deriving the frozenset from the dict keys:

♻️ Optional refactor to prevent drift

Move VALID_SERVICES definition after tool_imports and derive it:

# After tool_imports dict definition around line 328
VALID_SERVICES: frozenset[str] = frozenset(tool_imports.keys())

This would require restructuring since tool_imports is currently inside main(). Alternatively, extract the service names list to a single location that both reference.

Also applies to: 315-328

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` around lines 49 - 52, VALID_SERVICES is a hardcoded frozenset that
can drift from the actual tools defined in tool_imports; move the authoritative
list to one place by deriving VALID_SERVICES from tool_imports (or extracting a
single SERVICE_NAMES list used by both) so that VALID_SERVICES =
frozenset(tool_imports.keys()) (ensure you relocate the VALID_SERVICES
definition to after tool_imports or lift tool_imports out of main() so both can
reference the same symbol), then update any uses that reference VALID_SERVICES
to the new derived constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@main.py`:
- Around line 49-52: VALID_SERVICES is a hardcoded frozenset that can drift from
the actual tools defined in tool_imports; move the authoritative list to one
place by deriving VALID_SERVICES from tool_imports (or extracting a single
SERVICE_NAMES list used by both) so that VALID_SERVICES =
frozenset(tool_imports.keys()) (ensure you relocate the VALID_SERVICES
definition to after tool_imports or lift tool_imports out of main() so both can
reference the same symbol), then update any uses that reference VALID_SERVICES
to the new derived constant.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 709e6151-78fa-4ee6-8d5e-20257c59023b

📥 Commits

Reviewing files that changed from the base of the PR and between a0f55f9 and d237248.

📒 Files selected for processing (1)
  • main.py

mickey-mikey and others added 7 commits March 28, 2026 17:25
Prevents drift between VALID_SERVICES, argparse choices, and tool_imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mickey-mikey
Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

✅ Actions performed

Reviews resumed.

mickey-mikey and others added 3 commits April 4, 2026 20:04
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allows plugin users to choose stdio or streamable-http at install time.
streamable-http enables workspace-cli access to the running server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
main.py (3)

319-332: Implicit order coupling between SERVICE_NAMES and lambda list.

The dict(zip(...)) pattern requires the lambda list order to match SERVICE_NAMES exactly. While this works correctly now, future edits to either location could silently mismatch services with modules. Consider adding a brief comment or keeping the explicit dict for clarity.

💡 Alternative: Retain explicit dict for safety

If readability and maintenance safety are priorities:

tool_imports = {
    "gmail": lambda: import_module("gmail.gmail_tools"),
    "drive": lambda: import_module("gdrive.drive_tools"),
    # ... etc
}

Or add a comment documenting the coupling:

# NOTE: Lambda order must match SERVICE_NAMES order exactly
tool_imports = dict(zip(SERVICE_NAMES, [
    lambda: import_module("gmail.gmail_tools"),
    # ...
]))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` around lines 319 - 332, The tool_imports dict uses
dict(zip(SERVICE_NAMES, [...])) which couples the lambda list order to
SERVICE_NAMES and can silently mismatch; change tool_imports to an explicit
mapping keyed by service name (e.g. keys like "gmail", "drive") mapping to the
corresponding lambda that calls import_module, or at minimum add a clear comment
above tool_imports noting that the lambda list order must exactly match
SERVICE_NAMES to prevent future maintenance errors; update references to
SERVICE_NAMES/tool_imports accordingly.

221-224: Consider adding .strip() for consistency with other env var handling.

Other env vars (like WORKSPACE_MCP_TOOL_TIER at line 214) strip whitespace before checking. While .split() handles leading/trailing whitespace implicitly, adding .strip() before the truthiness check would make the intent clearer and be consistent with the pattern used elsewhere.

♻️ Optional consistency fix
     if args.permissions is None:
-        _env_perms = os.getenv("WORKSPACE_MCP_PERMISSIONS")
-        if _env_perms:
+        _env_perms = os.getenv("WORKSPACE_MCP_PERMISSIONS", "").strip()
+        if _env_perms:
             args.permissions = _env_perms.split()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` around lines 221 - 224, The env var handling for
WORKSPACE_MCP_PERMISSIONS should mirror the pattern used elsewhere by stripping
whitespace first: when reading os.getenv("WORKSPACE_MCP_PERMISSIONS") into
_env_perms, call _env_perms = _env_perms.strip() (or check truthiness on
_env_perms.strip()) before using .split(), and then assign args.permissions =
_env_perms.split(); update the block around args.permissions and the _env_perms
variable to perform the .strip() so intent and behavior match other vars like
WORKSPACE_MCP_TOOL_TIER.

225-227: Transport env var can override explicit --transport stdio CLI argument.

The condition args.transport == "stdio" doesn't distinguish between the argparse default and an explicit CLI argument. If a user runs --transport stdio explicitly, WORKSPACE_MCP_TRANSPORT=streamable-http will still override it. Per PR objectives, CLI arguments should take precedence over env vars.

One fix is to check if --transport was explicitly provided by inspecting sys.argv or using a sentinel default:

🔧 Proposed fix using sentinel default
     parser.add_argument(
         "--transport",
         choices=["stdio", "streamable-http"],
-        default="stdio",
+        default=None,
         help="Transport mode: stdio (default) or streamable-http",
     )

Then update the fallback logic:

-    _env_transport = os.getenv("WORKSPACE_MCP_TRANSPORT", "").strip().lower()
-    if _env_transport in {"stdio", "streamable-http"} and args.transport == "stdio":
-        args.transport = _env_transport
+    if args.transport is None:
+        _env_transport = os.getenv("WORKSPACE_MCP_TRANSPORT", "").strip().lower()
+        args.transport = _env_transport if _env_transport in {"stdio", "streamable-http"} else "stdio"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` around lines 225 - 227, The current logic allows the
WORKSPACE_MCP_TRANSPORT env var to override an explicit CLI `--transport stdio`
because it only checks args.transport value; change this so env var only applies
when the CLI did not explicitly set --transport. Use either inspect sys.argv for
the presence of "--transport" or switch args to use a sentinel default (e.g.,
transport=None) and update argument parsing, then modify the block using
_env_transport and args.transport so it only assigns args.transport =
_env_transport when the CLI did not explicitly provide --transport; reference
symbols: _env_transport, WORKSPACE_MCP_TRANSPORT, args.transport, and sys.argv
or the sentinel default in argparse.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@main.py`:
- Around line 319-332: The tool_imports dict uses dict(zip(SERVICE_NAMES,
[...])) which couples the lambda list order to SERVICE_NAMES and can silently
mismatch; change tool_imports to an explicit mapping keyed by service name (e.g.
keys like "gmail", "drive") mapping to the corresponding lambda that calls
import_module, or at minimum add a clear comment above tool_imports noting that
the lambda list order must exactly match SERVICE_NAMES to prevent future
maintenance errors; update references to SERVICE_NAMES/tool_imports accordingly.
- Around line 221-224: The env var handling for WORKSPACE_MCP_PERMISSIONS should
mirror the pattern used elsewhere by stripping whitespace first: when reading
os.getenv("WORKSPACE_MCP_PERMISSIONS") into _env_perms, call _env_perms =
_env_perms.strip() (or check truthiness on _env_perms.strip()) before using
.split(), and then assign args.permissions = _env_perms.split(); update the
block around args.permissions and the _env_perms variable to perform the
.strip() so intent and behavior match other vars like WORKSPACE_MCP_TOOL_TIER.
- Around line 225-227: The current logic allows the WORKSPACE_MCP_TRANSPORT env
var to override an explicit CLI `--transport stdio` because it only checks
args.transport value; change this so env var only applies when the CLI did not
explicitly set --transport. Use either inspect sys.argv for the presence of
"--transport" or switch args to use a sentinel default (e.g., transport=None)
and update argument parsing, then modify the block using _env_transport and
args.transport so it only assigns args.transport = _env_transport when the CLI
did not explicitly provide --transport; reference symbols: _env_transport,
WORKSPACE_MCP_TRANSPORT, args.transport, and sys.argv or the sentinel default in
argparse.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7250c096-cba9-4dde-bc2c-21bce75c98c9

📥 Commits

Reviewing files that changed from the base of the PR and between f91ac9d and 33c2f07.

📒 Files selected for processing (2)
  • .claude-plugin/plugin.json
  • main.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • .claude-plugin/plugin.json

- Use sentinel default (None) for --transport so CLI args take precedence
  over WORKSPACE_MCP_TRANSPORT env var
- Add .strip() to WORKSPACE_MCP_PERMISSIONS for consistency
- Add order coupling comment to tool_imports dict

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
main.py (1)

49-54: Collapse the service registry to one source of truth.

The note at Line 319 is documenting a fragile invariant rather than enforcing it. SERVICE_NAMES/VALID_SERVICES now drive CLI and env validation, while the actual importer registry lives in a separate ordered list. If one side changes without the other, the process can accept a service name that has no matching importer. Defining the importer map once and deriving SERVICE_NAMES/VALID_SERVICES from it would remove that maintenance trap.

Also applies to: 167-170, 318-333

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` around lines 49 - 54, Replace the two separate registries by making
the importer registry the single source of truth: locate the importer registry
(the ordered list/mapper of service name -> importer) and keep that as the
canonical structure, then derive SERVICE_NAMES (tuple/sequence) and
VALID_SERVICES (frozenset) programmatically from its keys; update any CLI/env
validation code to reference VALID_SERVICES and any code expecting SERVICE_NAMES
to use the derived tuple so names cannot drift from the importer map. Ensure the
importer registry retains its ordering (use an ordered dict or list of tuples if
order is important) and remove the hardcoded SERVICE_NAMES/VALID_SERVICES
definitions so only the importer map is edited when adding/removing services.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@main.py`:
- Around line 202-227: The current env-var parsing can silently widen access
when non-empty malformed values are provided; update the checks for
WORKSPACE_MCP_TOOLS, WORKSPACE_MCP_TOOL_TIER, WORKSPACE_MCP_READ_ONLY and
WORKSPACE_MCP_TRANSPORT so that non-empty but invalid values cause a fail-closed
behavior (log an error and exit) instead of falling back to permissive defaults:
for args.tools, if _env_tools is non-empty and _parsed is empty while _raw
contains values, call logger.error and exit(1) rather than leaving args.tools as
None; for args.tool_tier, if _env_tier is non-empty and not in
{"core","extended","complete"}, error and exit; for args.read_only, if _env_ro
is non-empty and not in {"true","1","yes","false","0","no"} treat it as invalid,
log and exit; for args.transport, if _env_transport is non-empty and not in
{"stdio","streamable-http"} log and exit — keep the empty-string/no-op behavior
intact so only non-empty malformed values trigger the fail-closed path.

---

Nitpick comments:
In `@main.py`:
- Around line 49-54: Replace the two separate registries by making the importer
registry the single source of truth: locate the importer registry (the ordered
list/mapper of service name -> importer) and keep that as the canonical
structure, then derive SERVICE_NAMES (tuple/sequence) and VALID_SERVICES
(frozenset) programmatically from its keys; update any CLI/env validation code
to reference VALID_SERVICES and any code expecting SERVICE_NAMES to use the
derived tuple so names cannot drift from the importer map. Ensure the importer
registry retains its ordering (use an ordered dict or list of tuples if order is
important) and remove the hardcoded SERVICE_NAMES/VALID_SERVICES definitions so
only the importer map is edited when adding/removing services.
🪄 Autofix (Beta)

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

Run ID: 6cd141f6-e793-4191-a32f-ecf961a6635f

📥 Commits

Reviewing files that changed from the base of the PR and between 33c2f07 and 7cee576.

📒 Files selected for processing (1)
  • main.py

Comment thread main.py Outdated
mickey-mikey and others added 2 commits April 4, 2026 21:48
Plugin now starts server in HTTP mode and connects via URL, enabling
both Claude Code and workspace-cli to use the same server instance.

- mcpServers uses command+url pattern (start process, connect via HTTP)
- Hardcode --transport streamable-http in args
- Add OAUTHLIB_INSECURE_TRANSPORT=1 for localhost
- Remove transport from userConfig (always HTTP now)
- Fix CLI precedence: --transport uses sentinel default (None)
- Add .strip() to WORKSPACE_MCP_PERMISSIONS for consistency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude Code requires "type": "http" to connect via streamable-http
URL instead of defaulting to stdio.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mickey-mikey mickey-mikey marked this pull request as draft April 4, 2026 11:17
mickey-mikey and others added 3 commits April 4, 2026 22:17
In stdio mode, the server now also starts an HTTP listener on port 8001
so workspace-cli can connect to the same process. Both transports share
the same tools, permissions, and auth state.

- Plugin uses standard stdio command config (all plugins do this)
- Background uvicorn serves http_app() on port+1 via asyncio.gather
- If port is busy, HTTP silently skips (stdio still works)
- Reverted plugin.json to stdio (removed type:http experiment)
- Fix CLI precedence: --transport uses sentinel default
- Add .strip() to WORKSPACE_MCP_PERMISSIONS for consistency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HTTP listener only starts when WORKSPACE_MCP_HTTP_PORT is set.
Plugin.json sets it to 8001; CLI users are unaffected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Non-empty but invalid env vars now error and exit instead of silently
  falling back to permissive defaults (security: prevents access widening)
- Replace dual SERVICE_NAMES/tool_imports registries with single
  SERVICE_MODULES dict as source of truth
- Derive SERVICE_NAMES, VALID_SERVICES, and tool_imports from it

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mickey-mikey mickey-mikey marked this pull request as ready for review April 4, 2026 23:27
@mickey-mikey
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@main.py`:
- Line 567: The code converts _cli_port to int directly (http_port =
int(_cli_port)), which will raise an unhandled ValueError for non-numeric
WORKSPACE_MCP_HTTP_PORT; update the parsing to validate _cli_port before
conversion (e.g., check str.isdigit() or use a try/except around int(_cli_port))
and raise or log a clear, user-friendly error indicating invalid
WORKSPACE_MCP_HTTP_PORT and expected numeric port, ensuring the variable names
http_port and _cli_port are used in the fix and that failure path exits or falls
back safely.
🪄 Autofix (Beta)

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

Run ID: e6b15414-5096-4b72-bac8-383666c29881

📥 Commits

Reviewing files that changed from the base of the PR and between a7a0152 and 42babdd.

📒 Files selected for processing (3)
  • .claude-plugin/marketplace.json
  • .claude-plugin/plugin.json
  • main.py

Comment thread main.py Outdated
Non-numeric values now fail closed with a clear error instead of
an unhandled ValueError traceback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mickey-mikey
Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

✅ Actions performed

Reviews resumed.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
main.py (1)

573-573: Add a return annotation to _run_dual.

async def _run_dual() -> None: is enough here.

As per coding guidelines, "Use mandatory type hints (mypy strict), f-strings (avoid % formatting), and async/await; limit time.sleep."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` at line 573, Add an explicit return type annotation to the async
helper by changing the signature of _run_dual to async def _run_dual() -> None:,
and ensure the function does not return any value (replace any bare returns with
return None or remove them) so it satisfies mypy strict typing and async/await
conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@main.py`:
- Around line 573-592: The stdio/http run loop hangs because asyncio.gather
waits for both coroutines but uvicorn.Server.serve() keeps running after
server.run_stdio_async() completes; modify _run_dual to start the HTTP serve as
a background task (create_task on http_srv.serve()), await the stdio task
returned by server.run_stdio_async(), then signal the HTTP server to stop (set
http_srv.should_exit = True or call its graceful shutdown) and await the HTTP
task to finish before exiting; references: _run_dual, server.run_stdio_async,
uvicorn.Server(config) -> http_srv, http_srv.serve, and
asyncio.create_task/await.
- Around line 217-248: Before applying environment fallbacks, record which
mutually exclusive options were explicitly provided on the CLI (e.g., detect
presence of "--permissions", "--tools", "--read-only", "--tool-tier" in sys.argv
or via argparse 'parser.parse_known_args' metadata) and use that snapshot to
skip any env-branch that would conflict; specifically, if args.permissions was
supplied on the CLI, do not apply WORKSPACE_MCP_TOOLS or WORKSPACE_MCP_READ_ONLY
fallbacks to set args.tools or args.read_only, and if
args.tools/args.read_only/args.tool_tier were supplied on the CLI, do not apply
WORKSPACE_MCP_PERMISSIONS; update the branches that set args.tools,
args.tool_tier, args.read_only, and args.permissions to first check the recorded
CLI-supplied set and return early when a conflict would overwrite a CLI-provided
value.

---

Nitpick comments:
In `@main.py`:
- Line 573: Add an explicit return type annotation to the async helper by
changing the signature of _run_dual to async def _run_dual() -> None:, and
ensure the function does not return any value (replace any bare returns with
return None or remove them) so it satisfies mypy strict typing and async/await
conventions.
🪄 Autofix (Beta)

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

Run ID: bcf504e6-7fdb-4c6c-b1d6-9ddac0b6271c

📥 Commits

Reviewing files that changed from the base of the PR and between 42babdd and 9ad1289.

📒 Files selected for processing (1)
  • main.py

Comment thread main.py
Comment thread main.py Outdated
mickey-mikey and others added 3 commits April 5, 2026 10:03
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… shutdown

- Don't apply WORKSPACE_MCP_READ_ONLY env var when --permissions was
  passed on CLI, and vice versa, preventing conflicting mode errors.
- Replace asyncio.gather with create_task for HTTP server so the
  process exits when stdio completes instead of hanging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@taylorwilsdon taylorwilsdon self-assigned this Apr 6, 2026
@taylorwilsdon taylorwilsdon added the enhancement New feature or request label Apr 6, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
main.py (2)

693-706: HTTP task exceptions silently dropped; consider storing and checking the task.

The HTTP server task created at line 698 is never awaited or stored. If http_srv.serve() raises an exception during operation, it will be silently ignored (Python logs "Task exception was never retrieved" to stderr). Additionally, setting should_exit = True doesn't guarantee the server has fully shut down before the function returns.

🔧 Proposed fix to handle HTTP task lifecycle
                     http_srv = None
+                    http_task = None
                     if http_available:
                         app = server.http_app(path="/mcp")
                         config = uvicorn.Config(app, host=host, port=http_port, log_level="warning")
                         http_srv = uvicorn.Server(config)
-                        asyncio.create_task(http_srv.serve())
+                        http_task = asyncio.create_task(http_srv.serve())
                         safe_print(f"   workspace-cli endpoint: http://{host}:{http_port}/mcp")

                     await server.run_stdio_async()

                     if http_srv:
                         http_srv.should_exit = True
+                    if http_task:
+                        try:
+                            await asyncio.wait_for(http_task, timeout=5.0)
+                        except asyncio.TimeoutError:
+                            http_task.cancel()
+                        except Exception:
+                            pass  # HTTP errors non-critical in stdio mode
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` around lines 693 - 706, The HTTP server task created with
asyncio.create_task(http_srv.serve()) in _run_dual is never stored or awaited,
so exceptions from http_srv.serve() are lost and shutdown may not complete; fix
by assigning the task to a variable (e.g., http_task), attach exception handling
(await http_task or add a done callback to log/propagate exceptions), and on
shutdown set http_srv.should_exit = True and then await http_task (or call await
http_srv.wait_for_exit() if available) after await server.run_stdio_async() to
ensure the server has fully stopped; update references to http_srv, http_task,
server.http_app, uvicorn.Server, and _run_dual accordingly.

263-265: _cli_has_tools captured but never used; env permissions fallback missing guard.

_cli_has_tools is captured at line 263 but never referenced. The WORKSPACE_MCP_PERMISSIONS fallback at line 295 only checks not _cli_has_read_only, so running --tools gmail with WORKSPACE_MCP_PERMISSIONS=drive:full would apply the env permissions, then fail at line 317 with a confusing error implying the user combined the flags.

Consider also guarding against _cli_has_tools for consistency with how _cli_has_permissions guards WORKSPACE_MCP_READ_ONLY:

🔧 Proposed fix
-    if args.permissions is None and not _cli_has_read_only:
+    if args.permissions is None and not _cli_has_read_only and not _cli_has_tools:
         _env_perms = os.getenv("WORKSPACE_MCP_PERMISSIONS", "").strip()
         if _env_perms:
             args.permissions = _env_perms.split()

Also applies to: 295-298

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` around lines 263 - 265, The code currently captures _cli_has_tools
but never uses it, and the WORKSPACE_MCP_PERMISSIONS env fallback is only
guarded by _cli_has_read_only; update the guard logic so env fallbacks are only
applied when the corresponding CLI flags were not provided: use
_cli_has_permissions to gate applying WORKSPACE_MCP_PERMISSIONS and use
_cli_has_read_only to gate applying WORKSPACE_MCP_READ_ONLY, and also ensure
_cli_has_tools is considered (i.e., do not apply env permissions if the user
passed --tools) or remove _cli_has_tools if you decide it’s unnecessary; adjust
the conditional(s) around reading
WORKSPACE_MCP_PERMISSIONS/WORKSPACE_MCP_READ_ONLY to reference
_cli_has_permissions and _cli_has_tools/_cli_has_read_only appropriately.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@main.py`:
- Around line 693-706: The HTTP server task created with
asyncio.create_task(http_srv.serve()) in _run_dual is never stored or awaited,
so exceptions from http_srv.serve() are lost and shutdown may not complete; fix
by assigning the task to a variable (e.g., http_task), attach exception handling
(await http_task or add a done callback to log/propagate exceptions), and on
shutdown set http_srv.should_exit = True and then await http_task (or call await
http_srv.wait_for_exit() if available) after await server.run_stdio_async() to
ensure the server has fully stopped; update references to http_srv, http_task,
server.http_app, uvicorn.Server, and _run_dual accordingly.
- Around line 263-265: The code currently captures _cli_has_tools but never uses
it, and the WORKSPACE_MCP_PERMISSIONS env fallback is only guarded by
_cli_has_read_only; update the guard logic so env fallbacks are only applied
when the corresponding CLI flags were not provided: use _cli_has_permissions to
gate applying WORKSPACE_MCP_PERMISSIONS and use _cli_has_read_only to gate
applying WORKSPACE_MCP_READ_ONLY, and also ensure _cli_has_tools is considered
(i.e., do not apply env permissions if the user passed --tools) or remove
_cli_has_tools if you decide it’s unnecessary; adjust the conditional(s) around
reading WORKSPACE_MCP_PERMISSIONS/WORKSPACE_MCP_READ_ONLY to reference
_cli_has_permissions and _cli_has_tools/_cli_has_read_only appropriately.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 94366c73-5722-42a6-bf68-d1d433268184

📥 Commits

Reviewing files that changed from the base of the PR and between 80672d2 and 1ce39b4.

📒 Files selected for processing (1)
  • main.py

mickey-mikey and others added 3 commits April 9, 2026 12:58
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r task

- Add `_cli_has_tools` check to WORKSPACE_MCP_PERMISSIONS env fallback so
  explicit --tools on the CLI prevents env permissions from being applied
- Store asyncio task for http_srv.serve() and await it with a 5s timeout
  after signaling should_exit, preventing silent exception loss and ensuring
  clean shutdown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
main.py (1)

683-714: Consider wrapping the entire coroutine in try/finally for robust cleanup.

If server.run_stdio_async() raises an exception, the HTTP server cleanup code (lines 704-712) won't execute. While the outer except Exception at line 724 does call cleanup_oauth_callback_server() and then sys.exit(1), the http_task is technically orphaned until process exit.

This is unlikely to cause issues in practice (process exits anyway), but a try/finally would make the cleanup more robust and explicit.

♻️ Proposed refactor for explicit cleanup
                 async def _run_dual() -> None:
                     """Run stdio and HTTP transports concurrently."""
                     http_available = True
                     try:
                         with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                             s.bind((host, http_port))
                     except OSError:
                         logger.warning("Port %d in use, workspace-cli HTTP endpoint unavailable", http_port)
                         http_available = False

                     http_srv = None
                     http_task = None
                     if http_available:
                         app = server.http_app(path="/mcp")
                         config = uvicorn.Config(app, host=host, port=http_port, log_level="warning")
                         http_srv = uvicorn.Server(config)
                         http_task = asyncio.create_task(http_srv.serve())
                         safe_print(f"   workspace-cli endpoint: http://{host}:{http_port}/mcp")

-                    await server.run_stdio_async()
-
-                    if http_srv:
-                        http_srv.should_exit = True
-                    if http_task:
-                        try:
-                            await asyncio.wait_for(http_task, timeout=5.0)
-                        except (asyncio.TimeoutError, asyncio.CancelledError):
-                            http_task.cancel()
-                        except Exception:
-                            pass  # HTTP errors non-critical in stdio mode
+                    try:
+                        await server.run_stdio_async()
+                    finally:
+                        if http_srv:
+                            http_srv.should_exit = True
+                        if http_task:
+                            try:
+                                await asyncio.wait_for(http_task, timeout=5.0)
+                            except (asyncio.TimeoutError, asyncio.CancelledError):
+                                http_task.cancel()
+                            except Exception:
+                                pass  # HTTP errors non-critical in stdio mode
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` around lines 683 - 714, Wrap the core of async def _run_dual() in a
try/finally so that regardless of exceptions from server.run_stdio_async() you
always run the HTTP shutdown/cleanup: in the finally block set
http_srv.should_exit (if http_srv), await/wait_for http_task with a short
timeout and cancel if needed, and swallow expected
asyncio.TimeoutError/CancelledError; ensure http_task.cancel() is called and
awaited where appropriate so the http_task is not orphaned. Target symbols:
_run_dual, server.run_stdio_async, http_srv, http_task (and reuse the existing
exception-handling logic currently after server.run_stdio_async inside the
finally).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@main.py`:
- Around line 683-714: Wrap the core of async def _run_dual() in a try/finally
so that regardless of exceptions from server.run_stdio_async() you always run
the HTTP shutdown/cleanup: in the finally block set http_srv.should_exit (if
http_srv), await/wait_for http_task with a short timeout and cancel if needed,
and swallow expected asyncio.TimeoutError/CancelledError; ensure
http_task.cancel() is called and awaited where appropriate so the http_task is
not orphaned. Target symbols: _run_dual, server.run_stdio_async, http_srv,
http_task (and reuse the existing exception-handling logic currently after
server.run_stdio_async inside the finally).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d3bb2e7b-875c-485a-a178-caab16414976

📥 Commits

Reviewing files that changed from the base of the PR and between c04d92c and 4f4efb5.

📒 Files selected for processing (1)
  • main.py

mickey-mikey and others added 4 commits April 14, 2026 19:06
If run_stdio_async() raises, the HTTP server shutdown and task await now
still execute via the finally block, preventing the sidecar from being
orphaned on unexpected exits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants