fix: coerce JSON-string tags to list in MemoryItem and MCP tools (#682)

MCP tool bridges sometimes serialize JSON arrays as strings during
transport, e.g. '["a", "b"]' arrives as the literal string '["a", "b"]'
instead of a native JSON array. This causes Pydantic to reject the
input with a validation error.

Add defensive coercion at two layers:

1. HTTP API (http.py): Pydantic field_validator on MemoryItem.tags
   with mode="before" that parses JSON strings back into lists.
2. MCP tools (mcp_tools.py): Same coercion in build_content_dict
   before tags reach the Pydantic model.

A plain non-JSON string is wrapped in a single-element list.
Correctly-formatted input is passed through unchanged.

Co-authored-by: Philipp <philipp@Philipps-MacBook-Pro.local>
This commit is contained in:
Philipp Oppolzer 2026-03-25 14:16:47 +01:00 committed by GitHub
parent 4285e94406
commit c5273f5fd4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 34 additions and 0 deletions

View file

@ -424,6 +424,27 @@ class MemoryItem(BaseModel):
default=None,
description="Optional tags for visibility scoping. Memories with tags can be filtered during recall.",
)
@field_validator("tags", mode="before")
@classmethod
def coerce_tags(cls, v):
"""Coerce JSON-string tags to list.
MCP tool bridges sometimes serialize JSON arrays as strings during
transport, e.g. '["a", "b"]' instead of ["a", "b"]. This validator
parses such strings back into lists so the retain call succeeds.
A plain non-JSON string is wrapped in a single-element list.
"""
if isinstance(v, str):
try:
parsed = json.loads(v)
if isinstance(parsed, list):
return parsed
except (json.JSONDecodeError, TypeError):
pass
return [v]
return v
observation_scopes: Literal["per_tag", "combined", "all_combinations"] | list[list[str]] | None = Field(
default=None,
title="ObservationScopes",

View file

@ -117,6 +117,19 @@ def build_content_dict(
Returns:
Tuple of (content_dict, error_message). error_message is None if successful.
"""
# Coerce tags from JSON string to list if needed.
# MCP tool bridges sometimes serialize JSON arrays as strings during
# transport, e.g. '["a", "b"]' instead of ["a", "b"].
if isinstance(tags, str):
try:
parsed = json.loads(tags)
if isinstance(parsed, list):
tags = parsed
except (json.JSONDecodeError, TypeError):
pass
if isinstance(tags, str):
tags = [tags]
content_dict: dict[str, Any] = {"content": content, "context": context}
if timestamp: