fix(search_people): accept JSON-stringified network arrays from MCP clients#517
Conversation
Some MCP clients serialize array-typed arguments whose JSON Schema has no top-level type (the anyOf[array, null] rendering of list[str] | None) into a JSON string before dispatch, so ["F"] arrives as the string '["F"]' and pydantic rejects it with a list_type error. This makes the connection-degree filter unusable from those clients (e.g. Claude Desktop / Cowork); clients that send a native array work fine. Accept str | list[str] for network and normalize a string (JSON-decoding when possible, otherwise treating it as a single token) to a list before use. Native arrays are unaffected. Co-Authored-By: Oz <oz-agent@warp.dev>
Greptile SummaryAdds a pre-flight normalization step to
Confidence Score: 4/5Safe to merge — the normalization is additive and all existing call-sites that pass native lists or None are unaffected. The change is narrow and well-scoped: it only adds a pre-flight string-normalization step to a single tool, and the happy paths (native list, None, stringified array) all behave correctly. The edge cases around linkedin_mcp_server/tools/person.py — the new normalization block and the missing test coverage for it. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[MCP Client calls search_people] --> B{network type?}
B -- "list[str]" --> E[Pass through unchanged]
B -- "None" --> E
B -- "str" --> C[strip whitespace]
C -- "empty string" --> D[network = None]
C -- "non-empty" --> F{json.loads succeeds?}
F -- "Yes, returns list" --> G[network = parsed list]
F -- "Yes, returns non-list" --> H["network = [str(parsed)]"]
F -- "ValueError / JSONDecodeError" --> I["network = [raw_string]"]
G --> E
H --> E
I --> E
D --> J[extractor.search_people called]
E --> J
J --> K{FilterValidationError?}
K -- "Yes" --> L[Raise ToolError to client]
K -- "No" --> M[Return search results]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[MCP Client calls search_people] --> B{network type?}
B -- "list[str]" --> E[Pass through unchanged]
B -- "None" --> E
B -- "str" --> C[strip whitespace]
C -- "empty string" --> D[network = None]
C -- "non-empty" --> F{json.loads succeeds?}
F -- "Yes, returns list" --> G[network = parsed list]
F -- "Yes, returns non-list" --> H["network = [str(parsed)]"]
F -- "ValueError / JSONDecodeError" --> I["network = [raw_string]"]
G --> E
H --> E
I --> E
D --> J[extractor.search_people called]
E --> J
J --> K{FilterValidationError?}
K -- "Yes" --> L[Raise ToolError to client]
K -- "No" --> M[Return search results]
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
linkedin_mcp_server/tools/person.py:155-159
**JSON null/non-list decoded value produces unexpected network token**
When `json.loads(raw)` succeeds but returns `None` (e.g. input `'null'`) or a number/bool, the `else` branch produces `["None"]`, `["42"]`, etc., which are silently forwarded to the extractor as network filter values. `FilterValidationError` will eventually reject them, but the error message ("None is not a valid network filter") may be confusing. A quick `if parsed is None` guard would set `network = None` in that case and match user intent more naturally.
### Issue 2 of 2
linkedin_mcp_server/tools/person.py:146-159
**No unit tests for the new normalization paths**
The PR description lists four cases that were verified manually (`'["F"]'` → `["F"]`, bare `"F"` → `["F"]`, etc.) but no automated test covers them. The existing `test_search_people_with_network_and_company_filters` test only exercises `network=["F"]` (the native-list path). A few parametrized assertions in `tests/test_tools.py` would lock in the behaviour and catch any future regression from ruff auto-fixes or type-annotation changes.
Reviews (1): Last reviewed commit: "fix(search_people): accept JSON-stringif..." | Re-trigger Greptile |
| try: | ||
| parsed = json.loads(raw) | ||
| except ValueError: | ||
| parsed = raw | ||
| network = parsed if isinstance(parsed, list) else [str(parsed)] |
There was a problem hiding this comment.
JSON null/non-list decoded value produces unexpected network token
When json.loads(raw) succeeds but returns None (e.g. input 'null') or a number/bool, the else branch produces ["None"], ["42"], etc., which are silently forwarded to the extractor as network filter values. FilterValidationError will eventually reject them, but the error message ("None is not a valid network filter") may be confusing. A quick if parsed is None guard would set network = None in that case and match user intent more naturally.
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/tools/person.py
Line: 155-159
Comment:
**JSON null/non-list decoded value produces unexpected network token**
When `json.loads(raw)` succeeds but returns `None` (e.g. input `'null'`) or a number/bool, the `else` branch produces `["None"]`, `["42"]`, etc., which are silently forwarded to the extractor as network filter values. `FilterValidationError` will eventually reject them, but the error message ("None is not a valid network filter") may be confusing. A quick `if parsed is None` guard would set `network = None` in that case and match user intent more naturally.
How can I resolve this? If you propose a fix, please make it concise.| # Some MCP clients serialize array arguments whose JSON Schema has no | ||
| # top-level "type" (the anyOf[array, null] rendering of `list[str] | | ||
| # None`) into a JSON string before dispatch, so ["F"] can arrive as | ||
| # the string '["F"]'. Accept and normalize that to a list. | ||
| if isinstance(network, str): | ||
| raw = network.strip() | ||
| if not raw: | ||
| network = None | ||
| else: | ||
| try: | ||
| parsed = json.loads(raw) | ||
| except ValueError: | ||
| parsed = raw | ||
| network = parsed if isinstance(parsed, list) else [str(parsed)] |
There was a problem hiding this comment.
No unit tests for the new normalization paths
The PR description lists four cases that were verified manually ('["F"]' → ["F"], bare "F" → ["F"], etc.) but no automated test covers them. The existing test_search_people_with_network_and_company_filters test only exercises network=["F"] (the native-list path). A few parametrized assertions in tests/test_tools.py would lock in the behaviour and catch any future regression from ruff auto-fixes or type-annotation changes.
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/tools/person.py
Line: 146-159
Comment:
**No unit tests for the new normalization paths**
The PR description lists four cases that were verified manually (`'["F"]'` → `["F"]`, bare `"F"` → `["F"]`, etc.) but no automated test covers them. The existing `test_search_people_with_network_and_company_filters` test only exercises `network=["F"]` (the native-list path). A few parametrized assertions in `tests/test_tools.py` would lock in the behaviour and catch any future regression from ruff auto-fixes or type-annotation changes.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Some MCP clients serialize array-typed tool arguments whose JSON Schema has no top-level
type(theanyOf: [array, null]rendering oflist[str] | None) into a JSON string before dispatch. As a result,search_people'snetworkargument arrives as the string'["F"]'instead of the array["F"], and pydantic rejects it:This makes the 1st/2nd/3rd-degree connection filter unusable from affected clients (observed with Claude Desktop / "Cowork"). Clients that send a native array (e.g. the MCP Inspector) are unaffected.
Change
networknow acceptsstr | list[str] | None."F"). Native lists pass through unchanged.anyOf: [string, array<string>, null], so the stringified value also passes validation.Testing
network=["F"]->["F"](unchanged)network='["F"]'(stringified) ->["F"]network='["F","S"]'->["F","S"]network="F"(bare token) ->["F"]ruff check/ruff formatclean.Co-Authored-By: Oz oz-agent@warp.dev