feat(mcp): expose content opportunities through MCP tools and REST endpoints#74
Conversation
|
Thanks for this, @Baqirrizvidev — really nice work! 🙌 It follows the existing MCP conventions closely (per-resource REST routes, the data-layer ownership-check pattern, tool registration), the multi-tenant isolation is correct (org ownership checked before returning data, and the Just one thing to fix before merge —
Behavior stays the same (only a null check / raw passthrough), so this is purely a typing change. Note: the other lint errors/warnings you may see ( Once the 5 |
… endpoint (#109) Adds the third content-opportunity MCP tool (after list_content_opportunities and get_content_opportunity in #74), plus the parallel REST endpoint at POST /api/mcp/content-opportunities/[id]/brief. Auth boundary follows the existing cron / trigger-tracking pattern: the web MCP layer authenticates the ans_ API key, does the org-ownership check via supabaseAdmin *first*, and only then proxies to a new internal aeo-server endpoint guarded by CRON_SECRET. A wrong-org or missing id returns 404 before any outbound call, so no LLM cost or data leak for ids the caller doesn't own. The MCP path always passes force=true to bypass the brief-already-exists early-return on the dashboard route — MCP callers explicitly want a fresh brief; cached reads stay on get_content_opportunity. Server side: - server/src/routes/content.js: extract generateBriefForOpportunity(id, { force, model }) as the shared core; existing POST /api/content/:id/brief keeps its current cache-on-existing-brief behavior by calling it without force - server/src/server.js: new POST /api/internal/content/:id/brief mounted before the session-JWT middleware, CRON_SECRET-authenticated, always delegates to the shared core Web side: - web/src/lib/mcp/data.ts: generateBriefFor(auth, opportunityId) — wrong-org → null → 404, otherwise POSTs to the internal endpoint with Authorization: Bearer $CRON_SECRET (same pattern as /api/cron/daily-tracking) - web/src/lib/mcp/server.ts: registers generate_content_brief with a strong usage guard in the description (LLM cost + always re-generates) - web/src/app/api/mcp/content-opportunities/[id]/brief/route.ts: POST handler sharing the same generateBriefFor data function Closes #66.
…ions Closes the content-opportunity MCP loop (after #74 list/get and #109's generate_brief) by letting MCP-driven flows move an opportunity between new / sent / in_progress / done / dismissed without a dashboard round-trip. Pattern follows #109: ownership-check-first in the web data layer via the same brands!inner join. A wrong-org or missing id returns null (→ 404) with no UPDATE issued, so an attacker can't probe ids and can't flip rows they don't own. The shared CONTENT_OPPORTUNITY_STATUSES tuple is reused by both the zod enum (MCP) and the REST handler's manual validation, so the surfaces can't drift. - web/src/lib/mcp/data.ts: updateOpportunityStatusFor(auth, id, status) + exported CONTENT_OPPORTUNITY_STATUSES tuple + ContentOpportunityStatus type - web/src/lib/mcp/server.ts: registers update_opportunity_status with zod enum + per-status description so the model knows valid values without guessing - web/src/app/api/mcp/content-opportunities/[id]/status/route.ts: PATCH handler sharing the same data function; manual enum validation returns 400 with the valid list Closes #67.
…ions (#110) Closes the content-opportunity MCP loop (after #74 list/get and #109's generate_brief) by letting MCP-driven flows move an opportunity between new / sent / in_progress / done / dismissed without a dashboard round-trip. Pattern follows #109: ownership-check-first in the web data layer via the same brands!inner join. A wrong-org or missing id returns null (→ 404) with no UPDATE issued, so an attacker can't probe ids and can't flip rows they don't own. The shared CONTENT_OPPORTUNITY_STATUSES tuple is reused by both the zod enum (MCP) and the REST handler's manual validation, so the surfaces can't drift. - web/src/lib/mcp/data.ts: updateOpportunityStatusFor(auth, id, status) + exported CONTENT_OPPORTUNITY_STATUSES tuple + ContentOpportunityStatus type - web/src/lib/mcp/server.ts: registers update_opportunity_status with zod enum + per-status description so the model knows valid values without guessing - web/src/app/api/mcp/content-opportunities/[id]/status/route.ts: PATCH handler sharing the same data function; manual enum validation returns 400 with the valid list Closes #67.
…) + REST endpoint (#116) Adds the third A-group MCP read tool from the assistant-readiness list, after the content-opportunity tools shipped in #74 / #109 / #110. Combines two existing RPCs (competitor_aggregates and share_of_voice_aggregates, both DB-side aggregated since #114) so the tool answers "how do I compare to my competitors?" and "who's gaining share of voice?" in a single round trip. Ownership-check first via supabaseAdmin (wrong-org brand → null → 404, no RPC fire). Snapshot shape — deltas are out of scope for V1 and consistent with the existing get_visibility_summary / list_topics / list_prompts shape; a caller that wants a delta issues a second call with an earlier window. - web/src/lib/mcp/data.ts: getCompetitorComparisonFor(auth, params) — parallel RPC calls, computes brand + per-competitor averages (when-present semantics matching the UI), and per-(model_used, platform) SoV split. Uses the same competitor display-name fallback pattern as the existing competitor logic (name ?? competitor_id) so unnamed competitors don't collide. - web/src/lib/mcp/server.ts: registers the get_competitor_comparison tool with a description that covers both questions the tool answers and the snapshot-vs-delta caveat. - web/src/app/api/mcp/competitor-comparison/route.ts: GET handler sharing the same data fn; query params mirror the MCP tool args. Verified end-to-end against the largest brand in the cloud project (5054 prompt result appearances, 7 competitors, 10 distinct model/ platform combos) — returned shape matches the documented interface, SoV totals + per-platform breakdown reconcile with the existing UI. Closes #98.
Exposes the content-opportunity layer of Ansvisor as MCP tools and parallel REST endpoints. Resolves #64 and resolves #65.