Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- VS Code adapter now configures remote MCP servers when `transport_type` is missing or empty in registry data, defaulting to `http` (#654)
- Propagate headers and environment variables through OpenCode MCP adapter with defensive copies to prevent mutation (#622)
### Changed

Expand Down
31 changes: 26 additions & 5 deletions src/apm_cli/adapters/client/vscode.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,17 +320,25 @@ def _format_server_config(self, server_info):
}
# Check for remotes (similar to Copilot adapter)
elif "remotes" in server_info and server_info["remotes"]:
remotes = server_info["remotes"]
remote = remotes[0] # Take the first remote
transport = remote.get("transport_type", "")
if transport in ("sse", "http", "streamable-http"):
remote = self._select_remote_with_url(server_info["remotes"])
if remote:
transport = (remote.get("transport_type") or "").strip()
# Default to "http" when transport_type is missing/empty,
# matching the Copilot adapter behavior (copilot.py:190-192).
if not transport:
transport = "http"
elif transport not in ("sse", "http", "streamable-http"):
raise ValueError(
f"Unsupported remote transport '{transport}' for VS Code. "
f"Server: {server_info.get('name', 'unknown')}. "
f"Supported transports: http, sse, streamable-http.")
headers = remote.get("headers", {})
# Normalize header list format to dict
if isinstance(headers, list):
headers = {h["name"]: h["value"] for h in headers if "name" in h and "value" in h}
server_config = {
"type": transport,
"url": remote.get("url", ""),
"url": remote["url"].strip(),
"headers": headers,
Comment thread
sergio-sisternes-epam marked this conversation as resolved.
}
input_vars.extend(
Expand Down Expand Up @@ -424,6 +432,19 @@ def _extract_package_args(package):

return []

@staticmethod
def _select_remote_with_url(remotes):
"""Return the first remote entry that has a non-empty URL.

Returns:
dict or None: The first usable remote, or None if none found.
"""
for remote in remotes:
url = (remote.get("url") or "").strip()
if url:
return remote
return None

def _select_best_package(self, packages):
"""Select the best package for VS Code installation from available packages.

Expand Down
117 changes: 117 additions & 0 deletions tests/unit/test_vscode_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,123 @@ def test_configure_self_defined_http_via_cache(self, mock_get_path):
config["servers"]["my-private-srv"]["url"], "http://localhost:8787/"
)

@patch("apm_cli.adapters.client.vscode.VSCodeClientAdapter.get_config_path")
def test_format_server_config_remote_missing_transport_type(self, mock_get_path):
"""Remote with no transport_type defaults to http (issue #654)."""
mock_get_path.return_value = self.temp_path
adapter = VSCodeClientAdapter()

server_info = {
"name": "atlassian-mcp-server",
"remotes": [{"url": "https://mcp.atlassian.com/v1/mcp"}],
}
config, inputs = adapter._format_server_config(server_info)

self.assertEqual(config["type"], "http")
self.assertEqual(config["url"], "https://mcp.atlassian.com/v1/mcp")
self.assertEqual(config["headers"], {})

@patch("apm_cli.adapters.client.vscode.VSCodeClientAdapter.get_config_path")
def test_format_server_config_remote_empty_transport_type(self, mock_get_path):
"""Remote with empty transport_type defaults to http."""
mock_get_path.return_value = self.temp_path
adapter = VSCodeClientAdapter()

server_info = {
"name": "remote-srv",
"remotes": [{"transport_type": "", "url": "https://example.com/mcp"}],
}
config, inputs = adapter._format_server_config(server_info)

self.assertEqual(config["type"], "http")
self.assertEqual(config["url"], "https://example.com/mcp")

@patch("apm_cli.adapters.client.vscode.VSCodeClientAdapter.get_config_path")
def test_format_server_config_remote_none_transport_type(self, mock_get_path):
"""Remote with transport_type=None defaults to http."""
mock_get_path.return_value = self.temp_path
adapter = VSCodeClientAdapter()

server_info = {
"name": "remote-srv",
"remotes": [{"transport_type": None, "url": "https://example.com/mcp"}],
}
config, inputs = adapter._format_server_config(server_info)

self.assertEqual(config["type"], "http")

@patch("apm_cli.adapters.client.vscode.VSCodeClientAdapter.get_config_path")
def test_format_server_config_remote_whitespace_transport_type(self, mock_get_path):
"""Remote with whitespace-only transport_type defaults to http."""
mock_get_path.return_value = self.temp_path
adapter = VSCodeClientAdapter()

server_info = {
"name": "remote-srv",
"remotes": [{"transport_type": " ", "url": "https://example.com/mcp"}],
}
config, inputs = adapter._format_server_config(server_info)

self.assertEqual(config["type"], "http")

@patch("apm_cli.adapters.client.vscode.VSCodeClientAdapter.get_config_path")
def test_format_server_config_remote_unsupported_transport_raises(self, mock_get_path):
"""Remote with an unrecognized transport_type raises ValueError."""
mock_get_path.return_value = self.temp_path
adapter = VSCodeClientAdapter()

server_info = {
"name": "future-srv",
"remotes": [{"transport_type": "grpc", "url": "https://example.com/mcp"}],
}
with self.assertRaises(ValueError) as ctx:
adapter._format_server_config(server_info)

self.assertIn("Unsupported remote transport", str(ctx.exception))
self.assertIn("grpc", str(ctx.exception))

@patch("apm_cli.adapters.client.vscode.VSCodeClientAdapter.get_config_path")
def test_format_server_config_remote_skips_entries_without_url(self, mock_get_path):
"""Remotes with empty URLs are skipped; first with a valid URL is used."""
mock_get_path.return_value = self.temp_path
adapter = VSCodeClientAdapter()

server_info = {
"name": "multi-remote",
"remotes": [
{"transport_type": "http", "url": ""},
{"transport_type": "sse", "url": "https://good.example.com/sse"},
],
}
config, inputs = adapter._format_server_config(server_info)

self.assertEqual(config["type"], "sse")
self.assertEqual(config["url"], "https://good.example.com/sse")

@patch("apm_cli.adapters.client.vscode.VSCodeClientAdapter.get_config_path")
def test_format_server_config_remote_default_http_preserves_headers(self, mock_get_path):
"""Defaulting to http still normalizes headers and extracts input vars."""
mock_get_path.return_value = self.temp_path
adapter = VSCodeClientAdapter()

server_info = {
"name": "header-srv",
"remotes": [
{
"url": "https://example.com/mcp",
"headers": [
{"name": "Authorization", "value": "${input:auth-token}"},
],
}
],
}
config, inputs = adapter._format_server_config(server_info)

self.assertEqual(config["type"], "http")
self.assertEqual(config["headers"], {"Authorization": "${input:auth-token}"})
self.assertTrue(len(inputs) > 0)
self.assertEqual(inputs[0]["id"], "auth-token")


class TestVSCodeSelectBestPackage(unittest.TestCase):
"""Test cases for _select_best_package logic."""
Expand Down
Loading