{"record":{"id":"a806f3a59e427ac0","repo":"gitbutlerapp/gitbutler","slug":"mcp-apps-capability-is-an-object","errorCode":null,"errorMessage":"MCP Apps capability is an object","messagePattern":"MCP Apps capability is an object","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but/src/command/mcp/mod.rs","lineNumber":245,"sourceCode":"        Ok(match result {\n            Ok(result) => result,\n            Err(err) => CallToolResult::error(vec![Content::text(format!(\n                \"Could not mark the review as ready: {err:#}\"\n            ))]),\n        })\n    }\n}\n\n#[tool_handler]\nimpl ServerHandler for Mcp {\n    fn get_info(&self) -> ServerInfo {\n        let mut extensions = ExtensionCapabilities::new();\n        extensions.insert(\n            \"io.modelcontextprotocol/ui\".to_owned(),\n            serde_json::from_value(json!({\n                \"mimeTypes\": [MCP_APP_MIME_TYPE]\n            }))\n            .expect(\"MCP Apps capability is an object\"),\n        );\n\n        ServerInfo {\n            instructions: Some(\n                \"Use gitbutler_workspace to inspect a repository's current GitButler workspace. Pass the active repository path when it is available; omit it only when the client is known to expose that repository as a filesystem root. After `but pr new`, call gitbutler_review_card with the returned review numbers so the user can see the created reviews.\"\n                    .into(),\n            ),\n            capabilities: ServerCapabilities::builder()\n                .enable_extensions_with(extensions)\n                .enable_resources()\n                .enable_tools()\n                .build(),\n            server_info: Implementation {\n                name: \"gitbutler\".into(),\n                title: Some(\"GitButler\".into()),\n                version: option_env!(\"VERSION\").unwrap_or(\"dev\").into(),\n                description: Some(\"GitButler workspace tools and views\".into()),\n                icons: None,","sourceCodeStart":227,"sourceCodeEnd":263,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/2497b8007aa4a1922dae9a805b32ffe5b5037785/crates/but/src/command/mcp/mod.rs#L227-L263","documentation":"When the MCP server advertises itself (ServerHandler::get_info), but inserts the MCP Apps UI capability into ExtensionCapabilities by deserializing a hardcoded JSON literal ({\"mimeTypes\":[MCP_APP_MIME_TYPE]}) into rmcp's capability value type. Since the input is a compile-time constant, the expect can only fail when the installed rmcp crate's extension-capability type no longer accepts that shape — i.e. a dependency upgrade changed the schema.","triggerScenarios":"Starting but's MCP server mode (an MCP client spawning `but mcp`, or any path calling Mcp::get_info) after the rmcp dependency was upgraded/changed so that ExtensionCapabilities::insert's value type rejects an object with a mimeTypes array.","commonSituations":"cargo update or a version bump of the rmcp SDK in the workspace; building against a fork/patched rmcp; drift between rmcp and the MCP Apps (io.modelcontextprotocol/ui) spec revision.","solutions":["Align rmcp with the version but was built against — check Cargo.lock for the pinned rmcp and restore it (e.g. git checkout Cargo.lock or cargo update -p rmcp --precise <ver>)","If the upgrade is intentional, update the json! literal to the capability shape the new rmcp expects","Pin rmcp exactly (rmcp = \"=x.y.z\") so silent drift cannot reintroduce this","Add a smoke test asserting get_info() completes without panicking after dependency changes"],"exampleFix":"// before — Cargo.toml allows drift\nrmcp = \"0.x\"\n\n// after — exact pin so the capability literal and the type stay in sync\nrmcp = \"=0.x.y\"","handlingStrategy":"validation","validationCode":"// after any rmcp/cargo update, verify server info construction before shipping\n#[test]\nfn mcp_server_info_smoke() {\n    let mcp = /* construct Mcp handler */;\n    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| mcp.get_info()));\n    assert!(result.is_ok(), \"get_info panicked — extension capability schema drifted\");\n}","typeGuard":null,"tryCatchPattern":"// degrade gracefully: serve without extensions if the capability literal no longer parses\nlet extensions = std::panic::catch_unwind(AssertUnwindSafe(build_extensions))\n    .unwrap_or_default(); // proceed with no advertised UI capability","preventionTips":["Pin rmcp to an exact version (=x.y.z) so capability schemas cannot drift silently","Run an MCP handshake smoke test (initialize request) in CI after every dependency bump","Review rmcp changelogs for ExtensionCapabilities/extension-value type changes before upgrading","Report but-version + rmcp-version together when this panic appears — it is a version-combination bug"],"tags":["rust","mcp","rmcp","serde","dependency-drift","server-startup"],"backgroundTag":"serde-deserialization-schema-mismatch","analyzedSha":"2497b8007aa4a1922dae9a805b32ffe5b5037785","analyzedAt":"2026-08-17T00:30:25.648Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}