Skip to content

fix: Copilot adapter validates remote transport_type (#791)#812

Merged
danielmeppiel merged 3 commits intomicrosoft:mainfrom
mvanhorn:fix-copilot-transport-validation-791
Apr 24, 2026
Merged

fix: Copilot adapter validates remote transport_type (#791)#812
danielmeppiel merged 3 commits intomicrosoft:mainfrom
mvanhorn:fix-copilot-transport-validation-791

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

Description

Resolves #791.

Mirrors PR #656 in the Copilot adapter. The Copilot client currently accepts any transport_type value (or none) from registry remotes without validation, so a registry entry returning transport_type: "grpc" would silently produce a garbage config instead of failing loud.

After this change, src/apm_cli/adapters/client/copilot.py:

  1. Selects the first remote with a non-empty URL (_select_remote_with_url, mirrors vscode.py:_select_remote_with_url), falling back to the original behavior when no remote has a URL so the downstream empty-URL error path is preserved.
  2. Reads transport_type, strips whitespace, defaults to "http" when missing/empty/whitespace-only, and raises ValueError for non-empty unrecognized transports with the same message shape as the VS Code adapter:

    Unsupported remote transport '{transport}' for Copilot. Server: {name}. Supported transports: http, sse, streamable-http.

  3. Still emits "type": "http" in the final Copilot CLI config (Copilot spec requirement for auth) and now strips the URL via .strip().

The helper is a @staticmethod on CopilotClientAdapter for now; promoting it to a shared base (per the issue's point #3) is left for a follow-up so this PR stays scoped.

Type of change

  • Bug fix

Testing

  • Tested locally
  • All existing tests pass (uv run pytest tests/unit/ -q → 3715 passed)
  • Added tests for new functionality

New test file tests/unit/test_copilot_adapter.py (parallel to test_vscode_adapter.py):

  • test_remote_missing_transport_type_defaults_to_http
  • test_remote_empty_transport_type_defaults_to_http
  • test_remote_none_transport_type_defaults_to_http
  • test_remote_whitespace_transport_type_defaults_to_http
  • test_remote_unsupported_transport_raises (asserts message includes transport name, server name, and "Copilot")
  • test_remote_supported_transports_do_not_raise (parametrized over http, sse, streamable-http)
  • test_remote_skips_entries_without_url
  • 3 direct unit tests on _select_remote_with_url

Total: 10 new tests, all passing. Full tests/unit/ suite passes unchanged.

Fixes #791

Mirror PR microsoft#656 in the Copilot adapter. Reject unrecognized remote
transports (e.g. 'grpc') with a clear ValueError, default to 'http'
when the registry omits transport_type, and skip remote entries without
URLs. Copilot CLI still emits 'type': 'http' for every remote per its
spec -- only the validation surface changes.

Adds a new tests/unit/test_copilot_adapter.py suite parallel to
tests/unit/test_vscode_adapter.py, covering missing/empty/whitespace/
None transport, the raise-on-unsupported path, the supported transport
set, and the _select_remote_with_url helper.

Fixes microsoft#791
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 22, 2026
@danielmeppiel danielmeppiel added panel-review Trigger the apm-review-panel gh-aw workflow and removed panel-review Trigger the apm-review-panel gh-aw workflow labels Apr 24, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict

Disposition: APPROVE (one optional follow-up; no required pre-merge actions)


Per-persona findings

Python Architect:

This is a routine bug-fix PR. One class diagram + one flow diagram.

1. OO / class diagram

classDiagram
    direction LR
    class MCPClientAdapter {
        <<Abstract>>
        +get_config_path() str
        +update_config(updates) void
        +get_current_config() dict
        +configure_mcp_server(...) bool
        +_infer_registry_name(package) str
        +_warn_input_variables(...) void
    }
    class CopilotClientAdapter {
        <<Adapter>>
        +supports_user_scope bool
        +_format_server_config(server_info, ...) dict
        +_select_remote_with_url(remotes) dict
        +_is_github_server(...) bool
        +_resolve_variable_placeholders(...) str
    }
    class VSCodeClientAdapter {
        <<Adapter>>
        +_format_server_config(server_info, ...) tuple
        +_select_remote_with_url(remotes) dict
        +_select_best_package(packages) dict
    }
    class SimpleRegistryClient {
        +find_server_by_reference(ref) dict
    }
    class GitHubTokenManager {
        +get_token_for_purpose(purpose) str
    }
    MCPClientAdapter <|-- CopilotClientAdapter
    MCPClientAdapter <|-- VSCodeClientAdapter
    CopilotClientAdapter *-- SimpleRegistryClient
    CopilotClientAdapter ..> GitHubTokenManager
    class CopilotClientAdapter:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
    note for CopilotClientAdapter "_select_remote_with_url is now\nbyte-identical in both adapters;\nnot yet on MCPClientAdapter"
Loading

2. Execution flow diagram

flowchart TD
    A["configure_mcp_server(server_url)\ncopilot.py:113"] --> B["registry_client.find_server_by_reference\n[NET] copilot.py:131"]
    B --> C["_format_server_config(server_info)\ncopilot.py:151"]
    C --> D{"_raw_stdio present?"}
    D -->|Yes| E["return stdio config\ncopilot.py:175"]
    D -->|No| F{"remotes list?"}
    F -->|Yes| G["_select_remote_with_url(remotes)\ncopilot.py:192 [NEW]"]
    G --> H{"usable remote found?"}
    H -->|Yes| I["remote = first with non-empty URL"]
    H -->|No| J["remote = remotes[0] fallback"]
    I --> K["transport = remote.get('transport_type').strip()\ncopilot.py:199 [NEW]"]
    J --> K
    K --> L{"transport empty?"}
    L -->|Yes| M["transport = 'http'\ncopilot.py:201 [NEW]"]
    L -->|No| N{"transport in\nsse/http/streamable-http?"}
    N -->|No| O["raise ValueError\ncopilot.py:203 [NEW]"]
    N -->|Yes| P["build config: type='http', url=url.strip()\ncopilot.py:208 [NEW]"]
    M --> P
    P --> Q{"is_github_server?"}
    Q -->|Yes| R["GitHubTokenManager.get_token_for_purpose\ncopilot.py:222 (unchanged)"]
    Q -->|No| S["skip auth headers"]
    R --> T["config headers = Bearer token (unchanged)"]
    T --> U["update_config\n[FS] copilot.py:143"]
    S --> U
    F -->|No| V["packages path / ValueError\ncopilot.py:263+"]
Loading

Design patterns

  • Used in this PR: Adapter -- CopilotClientAdapter._format_server_config() translates registry data (including the new transport_type field) into Copilot CLI-specific config, parallel to VSCodeClientAdapter. Pattern is annotated on the class diagram above.
  • Pragmatic suggestion: Push _select_remote_with_url() to MCPClientAdapter -- the method is now byte-identical in two subclasses (added to VS Code in fix: VS Code adapter fails to configure HTTP/SSE remote MCP servers (#654) #656, Copilot in this PR). A one-line move eliminates the next adapter having to copy it a third time. Tracked as a follow-up below.

CLI Logging Expert: No new logging regressions. The ValueError raised by the new transport validation is caught by the pre-existing except Exception as e: print(f"Error configuring MCP server: {e}") in configure_mcp_server() -- a pre-existing bare print() anti-pattern that the PR inherits, not introduces. The error message text is clear: it names the bad transport, the server, and the supported set. Minor gap: no "next action" pointer (e.g., "Check the server's registry entry or report to its maintainer"), but this is in the noise for an internal validation error unlikely to be seen by end users. No blocking issues.


DevX UX Expert: No CLI surface changes -- no new flags, commands, or help text. From the user perspective this converts a silent failure (garbage Copilot config written with whatever transport the registry sent) into an explicit, named error. That is the correct direction: "Failure mode is the product." Error text names the transport, the server, and the supported set -- all three things a developer needs to diagnose. No blockers.


Supply Chain Security Expert: The registry is an external data source. Old code silently accepted any transport_type value (or no value) and hardcoded type: "http" regardless; new code validates the field and raises ValueError for unrecognized values. This is an improvement -- the code now refuses to write a config derived from unexpected external data rather than silently absorbing it. URL .strip() prevents whitespace-only strings from being treated as valid endpoints. The _select_remote_with_url fallback to remotes[0] when all remotes lack URLs is correct: it preserves the existing empty-URL error path downstream. No path traversal, no token leakage, no auth scope changes. No blockers.


Auth Expert: Not activated -- the PR modifies src/apm_cli/adapters/client/copilot.py for transport_type validation and URL selection only; it does not touch auth.py, token_manager.py, azure_cli.py, github_host.py, or any other auth-chain file. The existing GitHubTokenManager call and Bearer token injection for GitHub MCP servers are unchanged.


OSS Growth Hacker: Parity fix with the VS Code adapter (#656). No standalone story hook, but it contributes to the "APM validates everything from the registry" reliability narrative. CHANGELOG entry is correctly placed in Fixed. Side-channel to CEO: two adapters have now independently added the same _select_remote_with_url() helper. A third adapter PR will copy it again. Pushing this to the base class is a contributor-funnel win -- new adapter authors get URL-selection logic for free, reducing "how do I add a new adapter" friction.


CEO arbitration

All specialists converge: this is a clean, correctly scoped parity fix from an external contributor. The Python Architect and Growth Hacker both independently flagged _select_remote_with_url() duplication, and I agree it belongs in MCPClientAdapter -- but that is a follow-up, not a blocker. Requiring a base-class refactor as a merge gate would penalize a well-tested external contribution for a 10-line helper. The PR fixes a real bug (#791), ships 179 lines of tests for 47 lines of production code, mirrors a validated pattern from #656, and has no security or UX regressions. Approve as-is; open a follow-up issue to push _select_remote_with_url() to the base class before a third adapter copies it.


Required actions before merge

None.


Optional follow-ups

  • Open a follow-up issue to move _select_remote_with_url() from CopilotClientAdapter and VSCodeClientAdapter into MCPClientAdapter. The method is now byte-identical in both subclasses; the base class is the right home so the next adapter gets it for free and future drift between implementations is eliminated.
  • Consider adding a "next action" hint to the ValueError message in _format_server_config() (e.g., "Check the server's registry entry or contact the registry maintainer") and surfacing it through CommandLogger rather than the bare print() in configure_mcp_server(). Pre-existing anti-pattern; out of scope for this PR.

Generated by PR Review Panel for issue #812 · ● 884.8K ·

@danielmeppiel danielmeppiel enabled auto-merge April 24, 2026 23:21
@danielmeppiel danielmeppiel disabled auto-merge April 24, 2026 23:21
@danielmeppiel danielmeppiel merged commit 2df8d30 into microsoft:main Apr 24, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

panel-review Trigger the apm-review-panel gh-aw workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Copilot adapter: add symmetric transport_type validation (mirror #656)

2 participants