Skip to content

Commit f6ac9c5

Browse files
author
Peter
committed
fix: resolve MCP client parameter validation compatibility issues
Claude Code cannot handle anyOf JSON schema patterns, causing validation failures (HTTP 422 errors) when using the docsrs-mcp server. Changes: - Replace all anyOf patterns with string types in MCP manifest - All numeric parameters (k, concurrency, count, etc.) now use string type - All boolean parameters (force, has_examples, deprecated) use string type - Pydantic field validators handle type coercion from strings - Add module filtering in get_crate_summary to reduce noise - Add comprehensive test suite for string parameter validation This maintains full compatibility with both Claude Code MCP client and REST API while ensuring proper type validation and coercion.
1 parent 90f813c commit f6ac9c5

File tree

3 files changed

+446
-23
lines changed

3 files changed

+446
-23
lines changed

UsefulInformation.json

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"projectName": "docsrs-mcp",
3-
"lastUpdated": "2025-08-13",
3+
"lastUpdated": "2025-08-15",
44
"purpose": "Track errors, solutions, and lessons learned during development",
55
"categories": {
66
"errorSolutions": {
@@ -1606,6 +1606,19 @@
16061606
"debuggingTechnique": "Test with both Claude Code MCP client and REST API to identify client-specific validation differences",
16071607
"bestPractice": "Design MCP schemas for the most restrictive client (Claude Code) and handle complexity in server-side validators. Field validators can handle the complexity that schemas can't.",
16081608
"impact": "Enables full compatibility with Claude Code MCP client while maintaining type safety and validation through Pydantic"
1609+
},
1610+
{
1611+
"error": "Module structure search results contaminated with internal/dependency modules",
1612+
"rootCause": "Search results include internal implementation details like std::, core::, deps:: modules that are not relevant to user queries about crate API structure",
1613+
"solution": "Implement post-processing filters to remove internal and dependency modules from search results. Filter patterns: ['::__', '_internal', 'deps::', 'std::', 'core::'] to focus on public API modules only",
1614+
"context": "Module structure queries returning too much noise from internal Rust standard library and dependency modules",
1615+
"lesson": "Search results need post-processing to remove irrelevant internal modules and focus on user-relevant public API surface",
1616+
"pattern": "Apply filtering patterns after search but before returning results to users",
1617+
"dateEncountered": "2025-08-13",
1618+
"relatedFiles": ["src/docsrs_mcp/search.py", "src/docsrs_mcp/filters.py"],
1619+
"codeExample": "# Filter internal modules from results\nfiltered_modules = [\n module for module in modules \n if not any(pattern in module.path for pattern in \n ['::__', '_internal', 'deps::', 'std::', 'core::'])\n]",
1620+
"debuggingTechnique": "Compare raw search results with filtered results to verify internal modules are properly excluded",
1621+
"impact": "Improves search result quality by focusing on relevant public API modules instead of internal implementation details"
16091622
}
16101623
]
16111624
},
@@ -2387,6 +2400,25 @@
23872400
"booleanPattern": "String to boolean conversion with comprehensive value recognition (true/false, 1/0, yes/no, on/off, t/y)",
23882401
"errorHandling": "Provide sensible defaults for invalid inputs rather than strict validation failures"
23892402
}
2403+
},
2404+
{
2405+
"error": "MCP client validation compatibility issues with anyOf schema patterns",
2406+
"rootCause": "Claude Code MCP client cannot handle anyOf JSON schema patterns, causing 'Input validation error: not valid under any of the given schemas' failures despite correct implementation",
2407+
"solution": "Replace all anyOf patterns with simple string-only schema types in MCP manifest. Use Pydantic field validators with mode='before' for comprehensive type coercion from strings to proper types (int, float, bool). All numeric and boolean parameters now use string type in schema with validation handling the conversion",
2408+
"context": "MCP parameter validation failing across all tools despite implementing proper anyOf schema patterns for type flexibility",
2409+
"implementation": [
2410+
"Changed all numeric parameters to 'type': 'string' in MCP manifest schema generation",
2411+
"Changed all boolean parameters to 'type': 'string' in MCP manifest schema generation",
2412+
"Enhanced field validators to handle string-to-int, string-to-float, and string-to-bool conversion",
2413+
"Added module filtering in get_crate_summary to reduce noise from internal implementation details",
2414+
"Maintained all existing validation logic and bounds checking in Pydantic validators"
2415+
],
2416+
"pattern": "Use string-only schemas in MCP manifests combined with robust Pydantic field validators for type coercion. This pattern works around MCP client schema limitations while maintaining server-side type safety",
2417+
"dateEncountered": "2025-08-15",
2418+
"relatedFiles": ["src/docsrs_mcp/app.py", "src/docsrs_mcp/models.py"],
2419+
"codeExample": "# Schema generation - use string only\n\"k\": {\n \"type\": \"string\",\n \"description\": \"Number of results (integer between 1-100)\"\n}\n\n# Field validator handles conversion\n@field_validator('k', mode='before')\n@classmethod\ndef coerce_k(cls, v):\n return coerce_to_int_with_bounds(v, min_val=1, max_val=100)",
2420+
"lesson": "MCP clients may have stricter schema validation than servers expect. Always test with actual MCP clients, not just REST endpoints. Use simple schema types with complex validation logic rather than complex schemas with simple validation",
2421+
"preventionPattern": "For MCP compatibility: simple schemas + complex validators > complex schemas + simple validators"
23902422
}
23912423
]
23922424
},
@@ -2758,6 +2790,43 @@
27582790
"issue": "uvloop not compatible with Windows",
27592791
"impact": "Performance degradation on Windows",
27602792
"workaround": "Fallback to standard asyncio event loop on Windows"
2793+
},
2794+
"claudeCodeMcpCompatibility": {
2795+
"issue": "Claude Code does NOT support anyOf patterns in MCP schemas unlike Claude Desktop",
2796+
"impact": "Parameter validation fails when schemas use anyOf patterns for type flexibility",
2797+
"workaround": "Use string-only parameter types in MCP manifest with Pydantic field validators for coercion",
2798+
"details": {
2799+
"root_cause": "Claude Code implements stricter JSON Schema validation that rejects anyOf patterns entirely",
2800+
"solution_pattern": "Define parameters as type: 'string' in manifest, use @field_validator(mode='before') for type conversion",
2801+
"affected_tools": "All MCP tools with numeric or boolean parameters that need type flexibility"
2802+
}
2803+
},
2804+
"versionComparisonErrors": {
2805+
"issue": "PointerToNowhere errors in version comparison _map_item_type function",
2806+
"impact": "Version comparison tools fail with null reference errors",
2807+
"workaround": "Add defensive null checking before accessing item type properties",
2808+
"details": {
2809+
"root_cause": "Missing null checks when processing version comparison items",
2810+
"solution_pattern": "Check if item exists and has required properties before mapping types"
2811+
}
2812+
},
2813+
"preIngestionValidation": {
2814+
"issue": "Pre-ingestion tool parameters lack proper anyOf patterns causing HTTP 422 errors",
2815+
"impact": "Tool parameter validation fails when parameters sent as strings instead of expected types",
2816+
"workaround": "Update tool schemas to use string types with validators, not anyOf patterns for Claude Code compatibility",
2817+
"details": {
2818+
"root_cause": "Claude Code strict schema validation incompatible with anyOf type patterns",
2819+
"solution_pattern": "Use consistent string-type parameters with runtime type coercion in validators"
2820+
}
2821+
},
2822+
"moduleStructureNoise": {
2823+
"issue": "Module structure results include internal/dependency modules creating noise",
2824+
"impact": "Search results contaminated with irrelevant internal implementation details",
2825+
"workaround": "Filter internal and dependency modules from search results to focus on public API",
2826+
"details": {
2827+
"filter_patterns": ["::__", "_internal", "deps::", "std::", "core::"],
2828+
"solution_pattern": "Apply post-processing filters to remove non-user-relevant modules from results"
2829+
}
27612830
}
27622831
},
27632832
"debuggingTips": {
@@ -2770,7 +2839,12 @@
27702839
"Use inline enum definitions instead of $ref for simple value constraints in MCP schemas",
27712840
"Monitor internal progress tracking and expose through health endpoints for user visibility",
27722841
"Test file validation with various case combinations to identify platform-specific issues",
2773-
"Always implement case-insensitive comparison for configuration file validation"
2842+
"Always implement case-insensitive comparison for configuration file validation",
2843+
"CRITICAL: Claude Code does NOT support anyOf patterns - use string-only types with validators",
2844+
"Test tools with both Claude Code and Claude Desktop to identify client-specific compatibility issues",
2845+
"Add defensive null checking in version comparison functions to prevent PointerToNowhere errors",
2846+
"Filter internal/dependency modules from search results using pattern matching",
2847+
"For Claude Code compatibility: avoid anyOf, use string parameters with @field_validator coercion"
27742848
],
27752849
"vectorSearch": [
27762850
"Check embedding dimensions match (384 for BAAI/bge-small-en-v1.5)",

src/docsrs_mcp/app.py

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -478,8 +478,9 @@ async def get_mcp_manifest(request: Request):
478478
"description": "Name of the crate to query (supports stdlib: std, core, alloc)",
479479
},
480480
"version": {
481-
"anyOf": [{"type": "string"}, {"type": "null"}],
482-
"description": "Specific version (default: latest)",
481+
"type": "string",
482+
"description": "Specific version or 'latest' (default: latest). Pass null or empty string for latest",
483+
"examples": ["1.0.104", "latest", ""],
483484
},
484485
},
485486
"required": ["crate_name"],
@@ -513,8 +514,9 @@ async def get_mcp_manifest(request: Request):
513514
},
514515
"query": {"type": "string", "description": "Search query text"},
515516
"version": {
516-
"anyOf": [{"type": "string"}, {"type": "null"}],
517-
"description": "Specific version (default: latest)",
517+
"type": "string",
518+
"description": "Specific version or 'latest' (default: latest). Pass null or empty string for latest",
519+
"examples": ["1.0.104", "latest", ""],
518520
},
519521
"k": {
520522
"type": "string",
@@ -591,8 +593,9 @@ async def get_mcp_manifest(request: Request):
591593
"description": "Full path to the item (e.g., 'tokio::spawn')",
592594
},
593595
"version": {
594-
"anyOf": [{"type": "string"}, {"type": "null"}],
595-
"description": "Specific version (default: latest)",
596+
"type": "string",
597+
"description": "Specific version or 'latest' (default: latest). Pass null or empty string for latest",
598+
"examples": ["1.0.104", "latest", ""],
596599
},
597600
},
598601
"required": ["crate_name", "item_path"],
@@ -629,8 +632,9 @@ async def get_mcp_manifest(request: Request):
629632
"description": "Search query for finding relevant code examples",
630633
},
631634
"version": {
632-
"anyOf": [{"type": "string"}, {"type": "null"}],
633-
"description": "Specific version (default: latest)",
635+
"type": "string",
636+
"description": "Specific version or 'latest' (default: latest). Pass null or empty string for latest",
637+
"examples": ["1.0.104", "latest", ""],
634638
},
635639
"k": {
636640
"type": "string",
@@ -673,8 +677,9 @@ async def get_mcp_manifest(request: Request):
673677
"description": "Name of the crate to get module tree for",
674678
},
675679
"version": {
676-
"anyOf": [{"type": "string"}, {"type": "null"}],
677-
"description": "Specific version (default: latest)",
680+
"type": "string",
681+
"description": "Specific version or 'latest' (default: latest). Pass null or empty string for latest",
682+
"examples": ["1.0.104", "latest", ""],
678683
},
679684
},
680685
"required": ["crate_name"],
@@ -1065,20 +1070,88 @@ async def get_crate_summary(request: Request, params: GetCrateSummaryRequest):
10651070

10661071
name, version, description, repository, documentation = row
10671072

1068-
# Fetch modules with hierarchy information
1073+
# Fetch modules with hierarchy information, filtering out noise
1074+
# Filter criteria:
1075+
# 1. Exclude build artifacts (target/, .cargo/, out/, build/)
1076+
# 2. Exclude test modules unless explicitly needed
1077+
# 3. Limit depth to meaningful modules (depth <= 3)
1078+
# 4. Exclude empty re-export modules (item_count < 2 and depth > 0)
10691079
cursor = await db.execute(
1070-
"SELECT name, path, parent_id, depth, item_count FROM modules WHERE crate_id = 1"
1080+
"""
1081+
SELECT name, path, parent_id, depth, item_count
1082+
FROM modules
1083+
WHERE crate_id = 1
1084+
AND depth <= 3
1085+
AND (item_count >= 2 OR depth = 0)
1086+
AND path NOT LIKE '%target::%'
1087+
AND path NOT LIKE '%.cargo::%'
1088+
AND path NOT LIKE '%build::%'
1089+
AND path NOT LIKE '%out::%'
1090+
AND path NOT LIKE '%tests::%'
1091+
AND path NOT LIKE '%benches::%'
1092+
AND path NOT LIKE '%examples::%'
1093+
AND path NOT LIKE '%deps::%'
1094+
AND path NOT LIKE '%__pycache__%'
1095+
ORDER BY depth ASC, path ASC
1096+
"""
10711097
)
1072-
modules = [
1073-
CrateModule(
1074-
name=row[0],
1075-
path=row[1],
1076-
parent_id=row[2],
1077-
depth=row[3],
1078-
item_count=row[4],
1098+
1099+
all_rows = await cursor.fetchall()
1100+
1101+
# Additional filtering in Python for more complex patterns
1102+
filtered_modules = []
1103+
for row in all_rows:
1104+
name, path, parent_id, depth, item_count = row
1105+
1106+
# Skip internal/private modules (often start with underscore)
1107+
if name.startswith("_") and depth > 0:
1108+
continue
1109+
1110+
# Skip generated modules
1111+
if any(
1112+
pattern in path.lower()
1113+
for pattern in ["generated", "autogen", ".pb.", "proto"]
1114+
):
1115+
continue
1116+
1117+
# Include the module
1118+
filtered_modules.append(
1119+
CrateModule(
1120+
name=name,
1121+
path=path,
1122+
parent_id=parent_id,
1123+
depth=depth,
1124+
item_count=item_count,
1125+
)
10791126
)
1080-
for row in await cursor.fetchall()
1081-
]
1127+
1128+
# If we filtered out too many modules, include some key ones back
1129+
if len(filtered_modules) < 5 and len(all_rows) > 10:
1130+
# Fall back to top-level modules and those with significant content
1131+
cursor = await db.execute(
1132+
"""
1133+
SELECT name, path, parent_id, depth, item_count
1134+
FROM modules
1135+
WHERE crate_id = 1
1136+
AND (depth <= 1 OR item_count >= 5)
1137+
AND path NOT LIKE '%target::%'
1138+
AND path NOT LIKE '%.cargo::%'
1139+
ORDER BY depth ASC, item_count DESC
1140+
LIMIT 20
1141+
"""
1142+
)
1143+
filtered_modules = [
1144+
CrateModule(
1145+
name=row[0],
1146+
path=row[1],
1147+
parent_id=row[2],
1148+
depth=row[3],
1149+
item_count=row[4],
1150+
)
1151+
for row in await cursor.fetchall()
1152+
]
1153+
1154+
modules = filtered_modules
10821155

10831156
return GetCrateSummaryResponse(
10841157
name=name,

0 commit comments

Comments
 (0)