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
43 changes: 35 additions & 8 deletions crates/pakx-registry-client/src/official_mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ struct ServerListResponse {
}

#[derive(Debug, Clone, Deserialize)]
struct ServerRaw {
struct ServerCore {
/// Canonical id. The MCP Registry sends this as `name` (e.g.
/// `io.github.modelcontextprotocol/server-filesystem`); older or
/// alternate deployments may send `id`.
Expand All @@ -181,25 +181,52 @@ struct ServerRaw {
extra: serde_json::Map<String, Value>,
}

/// Wire format for a single server entry. The 2025-12-11 schema wraps
/// every entry in `{ "server": <core>, "_meta": {...} }`; older
/// deployments still send the flat core directly. Accept both.
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum ServerRaw {
Wrapped {
server: ServerCore,
#[serde(rename = "_meta", default)]
meta: Option<Value>,
},
Flat(ServerCore),
}

impl ServerRaw {
fn into_parts(self) -> (ServerCore, Option<Value>) {
match self {
Self::Wrapped { server, meta } => (server, meta),
Self::Flat(core) => (core, None),
}
}
}

#[derive(Debug, Clone, Deserialize)]
struct VersionDetail {
#[serde(default)]
version: Option<String>,
}

fn into_package(raw: ServerRaw) -> Package {
let version = raw
let (core, meta) = raw.into_parts();
let version = core
.version
.or_else(|| raw.version_detail.and_then(|v| v.version))
.or_else(|| core.version_detail.and_then(|v| v.version))
.unwrap_or_else(|| "0.0.0".to_string());
let install_hints = Value::Object(raw.extra);
let mut extra = core.extra;
if let Some(m) = meta {
extra.insert("_meta".to_owned(), m);
}
Package {
id: raw.name.clone(),
id: core.name.clone(),
source: RegistrySource::OfficialMcp,
name: raw.name,
name: core.name,
version,
description: raw.description,
install_hints,
description: core.description,
install_hints: Value::Object(extra),
}
}

Expand Down
73 changes: 73 additions & 0 deletions crates/pakx-registry-client/tests/official_mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,79 @@ async fn second_search_is_served_from_cache() {
// wiremock verifies the expect(1) on drop.
}

#[tokio::test]
async fn search_decodes_wrapped_schema() {
// 2025-12-11 schema wraps each list entry as `{server, _meta}`.
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v0/servers"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"servers": [
{
"server": {
"name": "io.github.modelcontextprotocol/server-filesystem",
"description": "Local fs",
"version": "1.2.3",
"remotes": [{ "type": "streamable-http", "url": "https://x" }]
},
"_meta": {
"io.modelcontextprotocol.registry/official": {
"status": "active",
"isLatest": true
}
}
},
{
"server": { "name": "io.github.acme/playwright", "version": "0.5.0" },
"_meta": {}
}
],
"next": null
})))
.mount(&server)
.await;

let temp = TempDir::new().unwrap();
let source = source_with_mock(&server.uri(), temp.path());

let results = source.search("").await.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(
results[0].id,
"io.github.modelcontextprotocol/server-filesystem"
);
assert_eq!(results[0].version, "1.2.3");
assert_eq!(results[0].description.as_deref(), Some("Local fs"));
// `_meta` is preserved on `install_hints` so the resolver can inspect it.
assert!(results[0].install_hints.get("_meta").is_some());
// `remotes` from the inner `server` is also preserved on `install_hints`.
assert!(results[0].install_hints.get("remotes").is_some());
assert_eq!(results[1].id, "io.github.acme/playwright");
}

#[tokio::test]
async fn fetch_decodes_wrapped_schema() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v0/servers/io.github.foo/bar"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"server": {
"name": "io.github.foo/bar",
"version_detail": { "version": "2.0.0" }
},
"_meta": { "io.modelcontextprotocol.registry/official": { "status": "active" } }
})))
.mount(&server)
.await;

let temp = TempDir::new().unwrap();
let source = source_with_mock(&server.uri(), temp.path());
let pkg = source.fetch("io.github.foo/bar").await.unwrap();
assert_eq!(pkg.id, "io.github.foo/bar");
assert_eq!(pkg.version, "2.0.0");
assert!(pkg.install_hints.get("_meta").is_some());
}

#[tokio::test]
async fn cache_ttl_expiry_refetches() {
let server = MockServer::start().await;
Expand Down
Loading