Skip to content

feat(mcp): expose content opportunities through MCP tools and REST endpoints#74

Merged
gkhngyk merged 1 commit into
ansvisor:mainfrom
Baqirrizvidev:fix/mcp-content-opportunities
May 22, 2026
Merged

feat(mcp): expose content opportunities through MCP tools and REST endpoints#74
gkhngyk merged 1 commit into
ansvisor:mainfrom
Baqirrizvidev:fix/mcp-content-opportunities

Conversation

@Baqirrizvidev

Copy link
Copy Markdown
Contributor

Exposes the content-opportunity layer of Ansvisor as MCP tools and parallel REST endpoints. Resolves #64 and resolves #65.

@gkhngyk

gkhngyk commented May 22, 2026

Copy link
Copy Markdown
Contributor

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 brands!inner + org filter on the detail query prevents cross-org access), and the diff is clean. The tool descriptions are well written too.

Just one thing to fix before merge — yarn lint flags 5 @typescript-eslint/no-explicit-any errors in web/src/lib/mcp/data.ts. Since these are jsonb columns, Record<string, unknown> is the type-safe replacement:

  • L396 brief: any | null;brief: Record<string, unknown> | null;
  • L429 source_data: Record<string, any> | null;source_data: Record<string, unknown> | null;
  • L430 brief: Record<string, any> | null;brief: Record<string, unknown> | null;
  • L464 source_data: any | null;source_data: Record<string, unknown> | null;
  • L465 brief: any | null;brief: Record<string, unknown> | null;

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 (invite/[token]/page.tsx, mobile-nav.tsx, sidebar.tsx, unused-vars, <img>) already exist on main and are unrelated to this PR — no need to touch them here.

Once the 5 anys are addressed, this is good to merge. Thanks again!

@gkhngyk gkhngyk merged commit 5f3304e into ansvisor:main May 22, 2026
1 of 2 checks passed
gkhngyk added a commit that referenced this pull request May 26, 2026
… 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.
gkhngyk added a commit that referenced this pull request May 26, 2026
…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.
gkhngyk added a commit that referenced this pull request May 26, 2026
…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.
gkhngyk added a commit that referenced this pull request May 28, 2026
…) + 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(mcp): expose get_content_opportunity tool + REST endpoint feat(mcp): expose list_content_opportunities tool + REST endpoint

2 participants