diff --git a/hindsight-api-slim/hindsight_api/api/http.py b/hindsight-api-slim/hindsight_api/api/http.py index 3c86f6f5..81a43f66 100644 --- a/hindsight-api-slim/hindsight_api/api/http.py +++ b/hindsight-api-slim/hindsight_api/api/http.py @@ -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", diff --git a/hindsight-api-slim/hindsight_api/mcp_tools.py b/hindsight-api-slim/hindsight_api/mcp_tools.py index faa8cb5a..6b5f2cce 100644 --- a/hindsight-api-slim/hindsight_api/mcp_tools.py +++ b/hindsight-api-slim/hindsight_api/mcp_tools.py @@ -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: