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

  1. Fix the MCP server to emit the field as a plain JSON string (or omit the field entirely, since it is optional).
  2. Update the server to a version that conforms to the descriptor schema.
  3. 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

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


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)