Skip to content
Merged
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 @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Breaking:** MCP registry client now speaks the official MCP Registry v0.1 spec. **Self-hosted registries must serve `/v0.1/` paths -- registries serving only the legacy `/v0/` paths will return 404.** The public registry at `api.mcp.github.com` and the official `registry.modelcontextprotocol.io` are unaffected. Python API: `SimpleRegistryClient.get_server_info(server_id)` is renamed to `get_server(server_name, version="latest")` (the parameter is now a server *name* per the spec, not a UUID); the old name remains for one minor as a `DeprecationWarning` shim. The legacy UUID strategy in `find_server_by_reference` is removed -- the spec keys per-server lookup on serverName. v0.1 package fields (`identifier`, `registryType`, `runtimeHint`, `packageArguments`, `runtimeArguments`, `environmentVariables`) are normalized at the registry boundary to the snake_case shape adapters consume. Thanks @fassmus for the report. (#1337, closes #1210)
- `--marketplace-output PATH` is now hidden from `--help` and emits a stderr deprecation warning; it auto-translates to `--marketplace-path claude=PATH`. Removal tracked in #1318. (#1317)
- `extends: org` now correctly layers `dependencies.require` and `dependencies.deny` from the parent policy when the child omits the `dependencies:` block entirely; `None` signals "no opinion" (transparent) while `[]` signals explicit override. (#1290)
- CI self-check job now uses `setup-only: true` + `apm audit --ci --no-drift` so managed files are not overwritten by `apm install` before `content-integrity` runs; documented the audit-only CI pattern and the install-before-audit blind spot in the enterprise and CI/CD guides. (#1291)
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/reference/cli/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ export MCP_REGISTRY_URL=https://mcp.internal.example.com
apm mcp list
```

The registry must implement the [MCP Registry v0.1 spec](https://github.com/modelcontextprotocol/registry) (apm calls `/v0.1/servers/...`). Registries serving only the legacy `/v0/` paths will return 404.

## Related

- [`apm install`](../install/) -- canonical MCP install path.
Expand Down
2 changes: 1 addition & 1 deletion packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ To build the marketplace, run `apm pack` (it reads `apm.yml` and writes `.claude
| `apm mcp search QUERY` | Search MCP registry | `--limit N` |
| `apm mcp show SERVER` | Show server details | -- |

Set `MCP_REGISTRY_URL` (default `https://api.mcp.github.com`) to point all `apm mcp` commands and `apm install --mcp` at a custom MCP registry. The URL is validated at startup and must use `https://`; set `MCP_REGISTRY_ALLOW_HTTP=1` to opt in to plaintext `http://` for development. When the override is set and the registry is unreachable during install pre-flight, APM fails closed.
Set `MCP_REGISTRY_URL` (default `https://api.mcp.github.com`) to point all `apm mcp` commands and `apm install --mcp` at a custom MCP registry. The URL is validated at startup and must use `https://`; set `MCP_REGISTRY_ALLOW_HTTP=1` to opt in to plaintext `http://` for development. The registry must implement the [MCP Registry v0.1 spec](https://github.com/modelcontextprotocol/registry) (apm calls `/v0.1/servers/...`); legacy `/v0/`-only registries will return 404. When the override is set and the registry is unreachable during install pre-flight, APM fails closed.

## Runtime management (experimental)

Expand Down
270 changes: 206 additions & 64 deletions src/apm_cli/registry/client.py

Large diffs are not rendered by default.

92 changes: 62 additions & 30 deletions tests/integration/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,17 @@ def safe_rmdir(path):


class TestMCPRegistry:
"""Test the MCP registry client with the demo registry."""
"""Test the MCP registry client end-to-end against the public GitHub MCP Registry.

Previously targeted the legacy ``demo.registry.azure-mcp.net`` host
which only served the non-spec ``/v0/`` API (issue #1210). Retargeted
to ``api.mcp.github.com`` which is MCP Registry v0.1 spec-compliant.
"""

def setup_method(self):
"""Set up test environment."""
self.registry_client = SimpleRegistryClient("https://demo.registry.azure-mcp.net")
self.registry_url = "https://api.mcp.github.com"
self.registry_client = SimpleRegistryClient(self.registry_url)

# Create a temporary directory for tests
self.test_dir = tempfile.TemporaryDirectory()
Expand All @@ -62,7 +68,7 @@ def teardown_method(self):

# Leave the temp tree before unlinking it. Otherwise cwd can still
# reference the directory inode and os.getcwd() raises FileNotFoundError
# on POSIX breaking later tests on the same xdist worker.
# on POSIX -- breaking later tests on the same xdist worker.
with contextlib.suppress(FileNotFoundError, OSError):
os.chdir(tempfile.gettempdir())

Expand All @@ -76,49 +82,75 @@ def teardown_method(self):

def test_list_servers(self):
"""Test listing servers from the registry."""
servers, _ = self.registry_client.list_servers()
servers, _ = self.registry_client.list_servers(limit=5)
assert isinstance(servers, list), "Server list should be a list"
assert len(servers) > 0, "Demo registry should have some servers"
assert len(servers) > 0, "Public registry should have some servers"

def test_get_server_info(self):
"""Test getting server details for a specific server."""
# Get the first server from the list
servers, _ = self.registry_client.list_servers()
def test_get_server(self):
"""Test getting server details for a specific server (v0.1: keyed by name)."""
servers, _ = self.registry_client.list_servers(limit=5)
if not servers:
pytest.skip("No servers available in the demo registry")
pytest.skip("No servers available in the registry")

server_id = servers[0]["id"]
server_info = self.registry_client.get_server_info(server_id)
server_name = servers[0]["name"]
server_info = self.registry_client.get_server(server_name)

assert server_info is not None, f"Server info for {server_id} should be retrievable"
assert server_info is not None, f"Server info for {server_name} should be retrievable"
assert "name" in server_info, "Server info should include name"
assert "id" in server_info, "Server info should include id"
assert server_info["name"] == server_name

def test_vscode_adapter_with_registry(self):
"""Test VSCode adapter with registry integration."""
# Create a VSCode adapter
adapter = VSCodeClientAdapter("https://demo.registry.azure-mcp.net")
adapter = VSCodeClientAdapter(self.registry_url)

# Get a list of servers
servers, _ = self.registry_client.list_servers()
# Walk a small page to find a server whose packages map to a VSCode-supported
# transport (npm, pypi, docker). The first registry entry isn't guaranteed
# to be VSCode-compatible (e.g. uvx-only servers are skipped).
servers, _ = self.registry_client.list_servers(limit=20)
if not servers:
pytest.skip("No servers available in the demo registry")

# Configure the first server
server_id = servers[0]["id"]
result = adapter.configure_mcp_server(server_id)
pytest.skip("No servers available in the registry")

configured = False
chosen_name = None
last_error: Exception | None = None
# Known unsupported-server failures: registry shapes the adapter
# actively skips (uvx-only, mcpb, etc.) raise ValueError or KeyError.
# Anything else is captured and re-raised at the end so a registry
# contract regression (e.g. v0.1 packages with `identifier` instead
# of `name`) cannot silently disguise itself as "no compatible
# server" -- which is the regression #1210 was filed against.
_expected_skip_errors = (ValueError, KeyError, TypeError)
for s in servers:
name = s.get("name")
if not name:
continue
try:
if adapter.configure_mcp_server(name) is True:
configured = True
chosen_name = name
break
except _expected_skip_errors:
continue
except Exception as exc:
last_error = exc
continue
Comment on lines +127 to +136

if not configured:
if last_error is not None:
raise AssertionError(
"VSCode adapter could not configure any of the first 20 registry "
"servers and the last failure was an unexpected exception (likely a "
"registry contract regression): "
f"{type(last_error).__name__}: {last_error}"
) from last_error
pytest.skip("No VSCode-compatible server found in the first 20 registry entries")

assert result is True, f"Should be able to configure server {server_id}"

# Check the generated configuration file
config_path = os.path.join(self.test_dir.name, ".vscode", "mcp.json")
assert os.path.exists(config_path), "Configuration file should be created"

with open(config_path, encoding="utf-8") as f:
config = json.load(f)

assert "servers" in config, "Config should have servers section"

# The server name in the config will be the server_id unless a name was specified
assert server_id in config["servers"], f"Config should include {server_id}"
assert "type" in config["servers"][server_id], "Server config should have type"
assert chosen_name in config["servers"], f"Config should include {chosen_name}"
assert "type" in config["servers"][chosen_name], "Server config should have type"
Loading
Loading