MCP server for semantic ad matching and management via Qdrant vector database. Provides data plane (read-only LLM-facing ad matching) and control plane (admin collection management) operations.
Ad Injector MCP provides 11 tools with explicit Pydantic-backed schemas and response allowlisting. All tools have descriptions and registered via @mcp.tool() decorator. However, critical gaps exist: (1) tool naming lacks consistent verb patterns, 'ads_match', 'ads_explain', 'ads_health', 'ads_capabilities' are passable, but 'collection_ensure', 'collection_migrate', 'collection_info' are weaker (ensure/migrate are vague; info is generic); (2) parameter descriptions are present but often too brief (e.g., 'Ad ID to delete' vs 'The unique identifier for the ad to permanently remove'); (3) output schemas are documented as JSON via response allowlists (_shape_match_response, _shape_ads_get) but not formally declared in tool metadata; (4) error handling returns JSON errors but lacks recovery guidance (e.g., ads_explain returns {'error': 'match_id not found'} with no guidance on what to do next); (5) destructive tools (ads_delete, ads_bulk_disable) lack confirmation/dry-run patterns; (6) no tool annotations (readOnlyHint, destructiveHint) despite clear risk stratification in comments.
Set enabled=False for all ads matching filter_spec. Returns count updated.
Supported placements, constraint keys, embedding model, schema version.
Delete a single ad by ad_id.
Return audit trace for a prior match (why eligible/ineligible, filters, scores).
Retrieve a single ad by ad_id.
Liveness/readiness: Qdrant and embedding provider reachable.
Match ads by semantic context (read-only). Returns ranked candidates and match_id for explain.
Destructive tools (ads_delete, ads_bulk_disable) lack confirmation/dry-run patterns. ads_delete can permanently remove an ad with no reversibility or user confirmation step.
Tool annotations (readOnlyHint, destructiveHint, idempotentHint) are entirely absent. Code comments classify risks (WRITE, DESTRUCTIVE, READ_ONLY) but this metadata is not exposed in tool definitions, preventing agents from making risk-aware decisions.
Output schemas are implemented via response allowlists in Python (_shape_match_response, _shape_collection_info) but not formally documented as part of tool metadata. LLMs cannot see the expected output structure without calling the tool.
Inferred effective spec: <=2025-11-25.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-22 | D | 51 | <=2025-11-25 | v2 |
| 2026-03-09 | D | 53 | - | v1 |
Batch upsert ads into the collection. Each ad is embedded, vector-inserted, and indexed.
Ensure the ads collection exists with the given config.
Return metadata about the current ads collection.
Optional: run schema migrations / re-index between versions.
Error responses lack recovery guidance. ads_explain returns {'error': 'match_id not found'} with no hint to retry ads_match or check request_id. Agents have no actionable next step.
Naming inconsistency: 'collection_ensure', 'collection_migrate', 'collection_info' are vague. 'ensure' does not clearly signal 'create if missing'; 'migrate' could mean data migration or schema migration; 'info' is too generic. Better: 'create_ads_collection_if_missing', 'migrate_ads_schema', 'get_collection_metadata'.
Parameter descriptions are often too brief (<50 chars). 'Ad ID to delete' lacks context on what happens post-deletion or whether this is irreversible. Pattern: 'The unique identifier of the ad to permanently remove. WARNING: This operation cannot be undone. Consider using ads_bulk_disable for disabling without deletion.'
ads_bulk_disable filter_spec is an untyped object. No schema, no enum constraints, no examples of valid filters. LLMs will guess at valid keys and risk malformed queries. Pattern: Define filter_spec as a structured object with typed, documented fields.
No pagination support documented in ads_upsert or batch operations. If an agent tries to upsert 10,000 ads, there is no guidance on chunking, max batch size, or rate limits. Risk: oversized requests, timeouts, or service degradation.
ads_match returns match_id for explain, but the trace store (_trace_store) is in-memory with a 10k entry cap and no TTL. match_id is opaque; there is no guarantee an old match_id will be retrievable 10 minutes later. Documentation should clarify: match_id expires after X seconds or after 10k newer matches.