Hmbown/CodeWhale · error
. must be a string
Error message
{context}.{field} must be a string What it means
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.
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.
Example fix
// before (server output)
{"name":"search","description":{"text":"Search things"}}
// after
{"name":"search","description":"Search things"} Defensive patterns
Strategy: type-guard
Validate before calling
fn field_is_string_or_absent(entry: &serde_json::Value, field: &str) -> bool {
match entry.get(field) {
None | Some(serde_json::Value::String(_)) => true,
_ => false,
}
} Type guard
fn as_optional_string(v: &serde_json::Value, field: &str) -> Option<Option<String>> {
match v.get(field) {
None => Some(None),
Some(serde_json::Value::String(s)) => Some(Some(s.clone())),
_ => None,
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- A managed, project or plugin connector already uses this…
- Absolute path should not warn
- active plugin registry is missing its pre-dotenv…
- app_denied
- {backoff.last_error}
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/4b7c36c60d7ea233.
Report an issue: GitHub.
Appendix: source
Thrown at crates/mcp/src/stdio_client.rs:126
let Some(priority) = priority.as_f64() else {
return false;
};
if !priority.is_finite() || !(0.0..=1.0).contains(&priority) {
return false;
}
}
true
}
fn optional_string_field(
fields: &serde_json::Map<String, Value>,
field: &str,
context: &str,
) -> Result<Option<String>> {
match fields.get(field) {
None => Ok(None),
Some(Value::String(value)) => Ok(Some(value.clone())),
Some(_) => bail!("{context}.{field} must be a string"),
}
}
fn parse_tool_entry(
server_name: &str,
tool: &Value,
index: usize,
allow_legacy_schema_omission: bool,
) -> Result<(McpToolDescriptor, Value)> {
let context = format!("MCP server '{server_name}': tools/list tools[{index}]");
let fields = tool
.as_object()
.with_context(|| format!("{context} must be an object"))?;
let tool_name = fields
.get("name")
.and_then(Value::as_str)
.with_context(|| format!("{context}.name must be a string"))?
.to_string();View on GitHub (pinned to 73e0f67d83)