{"record":{"id":"4b7c36c60d7ea233","repo":"Hmbown/CodeWhale","slug":"context-field-must-be-a-string","errorCode":null,"errorMessage":"{context}.{field} must be a string","messagePattern":"(.+?)\\.(.+?) must be a string","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/mcp/src/stdio_client.rs","lineNumber":126,"sourceCode":"        let Some(priority) = priority.as_f64() else {\n            return false;\n        };\n        if !priority.is_finite() || !(0.0..=1.0).contains(&priority) {\n            return false;\n        }\n    }\n    true\n}\n\nfn optional_string_field(\n    fields: &serde_json::Map<String, Value>,\n    field: &str,\n    context: &str,\n) -> Result<Option<String>> {\n    match fields.get(field) {\n        None => Ok(None),\n        Some(Value::String(value)) => Ok(Some(value.clone())),\n        Some(_) => bail!(\"{context}.{field} must be a string\"),\n    }\n}\n\nfn parse_tool_entry(\n    server_name: &str,\n    tool: &Value,\n    index: usize,\n    allow_legacy_schema_omission: bool,\n) -> Result<(McpToolDescriptor, Value)> {\n    let context = format!(\"MCP server '{server_name}': tools/list tools[{index}]\");\n    let fields = tool\n        .as_object()\n        .with_context(|| format!(\"{context} must be an object\"))?;\n    let tool_name = fields\n        .get(\"name\")\n        .and_then(Value::as_str)\n        .with_context(|| format!(\"{context}.name must be a string\"))?\n        .to_string();","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/mcp/src/stdio_client.rs#L108-L144","documentation":"optional_string_field extracts an optional string field from a catalog entry (tool or resource descriptor) during stdio parsing. If the field is present but is not a JSON string, parsing fails with this message naming the context and field. It enforces descriptor schema types during tools/resources list parsing.","triggerScenarios":"Calling list_tools/list_resources where a tool or resource entry contains e.g. \"description\": 42 or \"uri\": {\"...\"} — the field exists but is a number, object, array, bool, or null instead of a string.","commonSituations":"MCP server emits localized/multimodal description objects instead of plain strings; server bug serializing metadata with wrong types; schema drift between MCP spec versions where a field became structured content.","solutions":["Fix the MCP server to emit the field as a plain JSON string (or omit the field entirely, since it is optional).","Update the server to a version that conforms to the descriptor schema.","If you control the integration, wrap list parsing and skip/reject the offending entry with a clear server-side log."],"exampleFix":"// before (server output)\n{\"name\":\"search\",\"description\":{\"text\":\"Search things\"}}\n// after\n{\"name\":\"search\",\"description\":\"Search things\"}","handlingStrategy":"type-guard","validationCode":"fn field_is_string_or_absent(entry: &serde_json::Value, field: &str) -> bool {\n    match entry.get(field) {\n        None | Some(serde_json::Value::String(_)) => true,\n        _ => false,\n}\n}","typeGuard":"fn as_optional_string(v: &serde_json::Value, field: &str) -> Option<Option<String>> {\n    match v.get(field) {\n        None => Some(None),\n        Some(serde_json::Value::String(s)) => Some(Some(s.clone())),\n        _ => None,\n    }\n}","tryCatchPattern":null,"preventionTips":["Server-side: emit descriptor metadata as plain JSON strings, never localized/structured objects.","Validate a sample tools/list payload whenever a server version changes.","Coerce structured descriptions to strings at the server boundary before advertising."],"tags":["mcp","type-mismatch","descriptors"],"backgroundTag":"type-mismatch","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}