Twosion MCP
The tools this server exposes over MCP. Each description is quoted verbatim — it is the whole of what a model knows about when to reach for the tool. Types, defaults and required-ness come from the same schema that validates the model's arguments. Calling these needs an access token; reading this page does not.
Connecting
Every host needs the same one value — the server URL. Authentication is OAuth 2.1 with PKCE against Auth0, discovered from this server's protected-resource metadata, so there is nothing else to configure. Hosts identify themselves with a Client ID Metadata Document (CIMD) where they publish one. The per-client guides in docs/clients/ cover the Auth0 side in full.
https://mcp.twosionflexa.aws.tveyes.com/mcpClaude Code
Add the server, then run /mcp and complete the browser sign-in.
claude mcp add --transport http twosion-mcp https://mcp.twosionflexa.aws.tveyes.com/mcpClaude Desktop & claude.ai
Settings → Connectors → Add custom connector, and paste the server URL above. claude_desktop_config.json is for local stdio servers only — an HTTP entry there is rejected as an invalid server configuration. These surfaces also connect from Anthropic's servers rather than from your machine, so the URL must be publicly reachable; a localhost URL will not work.
VS Code
Add an MCP server with the URL above. VS Code identifies itself with a Client ID Metadata Document, so nothing needs registering on the Auth0 tenant first.
Cursor, MCP Inspector
Neither publishes a CIMD document, and Dynamic Client Registration is disabled on this tenant, so each needs a static Auth0 Application registered once with the exact callback URL its build emits. An Auth0 administrator does this; the per-client guides in docs/clients/ have the field-by-field steps.
Tools
- briefing_get_latest
- coverage_analyze_mentions
- coverage_analyze_stories
- coverage_compare_queries
- coverage_get_media_item
- coverage_get_story
- coverage_search_mentions
- coverage_search_options
- report_list_recent
- system_ping
- watchlist_get_movers
- watchlist_get_summary
- watchlist_get_term_summaries
briefing_get_latest
#Get the most recently generated daily briefing for the current account.
Twosion generates at most one briefing at a time; there is no history and no way to request a specific past date -- this always returns the latest one. Use `detail="executive"` for a fast headline-and-sentiment digest, or the default `detail="full"` for per-watchlist-term themes, tone, top stories, findings, and recommendations.
Example: "What's today's briefing?" or "give me a quick digest" -> detail="executive". "Give me the full briefing with recommendations" -> omit detail, or pass detail="full" explicitly.
Use watchlist_get_term_summaries instead when the user only wants one or a few specific watch terms rather than the whole account. Raises an error if no briefing has been generated yet for this account.
| Field | Type | Default | What the model is told | |
|---|---|---|---|---|
| detail | full | executive | optional | "full" | `executive` returns only the headline, key points, and sentiment -- cheaper, and tolerant of partial generation failure. `full` also returns per-watchlist-term summaries, findings, and recommendations. |
coverage_analyze_mentions
#Aggregate mention counts, audience, demographics and geography for a topic -- not individual matches.
Use this when the user wants a total, a trend, or a demographic breakdown ("how much coverage", "gender/age split") rather than a list of items -- use coverage_search_mentions for that. Same query, matchMode, exclude, date range, and filter semantics as coverage_search_mentions (filter values come from coverage_search_options; never guess one). All filters must match; values within one filter match any.
Use queryString (instead of query) when recall matters: expand the topic into OR'd synonyms, abbreviations and spelling variants, e.g. (Tesla OR "Model Y") AND (recall OR retira OR NHTSA). Transcripts are speech-to-text, so exact long phrases often miss -- prefer "short phrase"~5. If a queryString returns nothing, broaden it (the response carries a hint) rather than reporting "no coverage".
Set watchlistId to analyze one watchlist's own query -- query becomes optional and, when given, narrows it. Get the id from watchlist_get_summary; never guess one.
Returns: - mentions: total, per-content-type counts, daily series. - demographics.tv: male share and age brackets. demographics.podcast: male share, median age/income, age brackets, and affinity lists (countries, cities, interests, brands, ...) whose `score` is an average across podcasts, NOT a share -- don't present as percent. - audience: TV/radio impressions and ad value (local/national), podcast/YouTube impressions, daily series. These are summed per clip -- cumulative impressions, not unique people; say so. - geography: top countries/markets by hits. Countries overlap with their markets, so don't add hits across rows. Sections with no data are omitted.
Example: "How much TV coverage did our Tesla recall get, and what's the audience skew?" -> query="Tesla recall".
| Field | Type | Default | What the model is told | |
|---|---|---|---|---|
| query | string | optional | "" | Ordinary words or a phrase to analyze in media coverage. Optional when watchlistId is set -- it then narrows the watchlist's own query. |
| queryString | string | optional | null | Advanced alternative to query: an Elasticsearch query string over transcripts. Quote phrases and short terms ("Model Y", "F-22"); OR synonyms and spelling variants; AND/NOT in capitals; () to group; "phrase"~N for words within N of each other; trailing wildcards only (recall*). No field:value syntax -- use the filters. Not allowed with watchlistId. Use instead of query, never both. |
| matchMode | all | any | phrase | optional | "all" | Require all words, any word, or the complete phrase. |
| exclude | string[] | optional | null | Words or phrases that must not appear in matching coverage. |
| startDate | string | optional | null | First date to analyze, inclusive. |
| endDate | string | optional | null | Last date to analyze, inclusive. |
| watchlistId | string | optional | null | Analyze this watchlist's own query instead of a free-text query. Get the id from watchlist_get_summary. |
| stations | string[] | optional | null | Station values from coverage_search_options (category=stations). |
| markets | string[] | optional | null | Market values from coverage_search_options (category=markets). |
| countries | string[] | optional | null | Country values from coverage_search_options (category=countries). |
| mediaTypes | string[] | optional | null | Media-type values from coverage_search_options (category=mediaTypes). |
| programs | string[] | optional | null | Program-name values from coverage_search_options (category=programs). |
| languages | string[] | optional | null | Language values from coverage_search_options (category=languages). |
| states | string[] | optional | null | State values from coverage_search_options (category=states). Broadcast only -- podcasts and YouTube have no state. |
| channels | string[] | optional | null | Podcast or YouTube channel ids: a podcast from coverage_search_options (category=podcasts), or a match's sourceId. Never guess one. |
| sections | mentions | demographics | audience | geography[] | optional | null | Which top-level sections to return (mentions, demographics, audience, geography); omit for everything. Every section is always fetched upstream, so this does not reduce cost or latency -- it only trims the response sent back. The resolved request is always included. |
| geoTopN | integer | optional | 15 | How many countries/markets to return in geography, by hits. |
coverage_analyze_stories
#Get an AI-synthesized narrative, sentiment, and trend analysis for a topic.
This is an expensive, always-live LLM analysis -- it re-embeds and re-clusters matching documents on every call, with no caching. Prefer a cheaper tool first: use coverage_search_mentions when the user wants a list of actual matching clips, not a synthesis. For one watch term, use watchlist_get_term_summaries first -- it reuses a cached briefing summary when fresh; escalate to this tool only when the user explicitly wants tone, sentiment, or trend detail the cheap tool doesn't return. To compare several watchlists against each other (share of voice, overlap, velocity), use coverage_compare_queries instead.
Example: "What's the story/sentiment around Tesla pricing?" (ad hoc) -> query="Tesla pricing". Example: "What's the sentiment on my Tesla watchlist?" (known watch term, deep dive) -> watchlistId="<id from watchlist_get_summary>"; never guess a watchlistId.
Returns rule-based key findings (statements), a tone/sentiment breakdown, country facets, a daily trend, and sample matching stories -- each with its own tone, audience, and the ids of clips it groups as duplicates. Pass a story's memberIds to coverage_get_story to list every clip in it.
| Field | Type | Default | What the model is told | |
|---|---|---|---|---|
| query | string | optional | null | Ad hoc topic to analyze. Unlike coverage_search_mentions, this is raw Elasticsearch querystring syntax, not translated -- wrap multi-word phrases in double quotes (e.g. "trade tariffs") to match the exact phrase; unquoted multi-word input may match individual words rather than the phrase. Exactly one of query/watchlistId is required -- unlike coverage_search_mentions, this tool does not narrow within a watchlist's own query; it's one or the other. |
| watchlistId | string | optional | null | Watch term id from watchlist_get_summary; never guess one. Exactly one of query/watchlistId is required. |
| startDate | string | optional | null | First date to analyze, inclusive. |
| endDate | string | optional | null | Last date to analyze, inclusive. |
| countries | string[] | optional | null | Plain ISO country codes, e.g. ["US", "GB"] -- not resolved via coverage_search_options; that tool's values are for a different filter dimension. |
| mediaTypes | string[] | optional | null | Plain media-type strings: "TV", "RADIO" (must be exact uppercase -- "Radio" matches nothing), "podcast", or "youtube" (case-insensitive). |
| markets | integer[] | optional | null | Market ids -- the numeric `value` coverage_search_options returns for category=markets. |
| states | string[] | optional | null | State codes, e.g. ["NV"] or ["US-NV"]. Expanded to every market in that state; combined with markets as one location filter (either matches). Broadcast only -- podcasts and YouTube have no state. |
| stations | string[] | optional | null | Station names -- the `name` (not the `value`) coverage_search_options returns for category=stations. This tool filters on station name. |
| languages | string[] | optional | null | ISO-639-1 language codes, e.g. ["en", "es"]. |
| newsOnly | boolean | optional | false | Restrict to news programs only. |
| maxStories | integer | optional | 20 | Maximum sample stories to return. |
| trendDays | integer | optional | 30 | Days of trend history before startDate. The trend spans this lookback plus the analyzed range. |
| sections | statements | tone | countries | trend | results | stats[] | optional | null | Which top-level sections to return; omit for everything. This trims only the response sent back -- the same analysis always runs upstream regardless of this value, so it does not reduce cost or latency. Use it to cut response size, e.g. sections=["results"] for just the story list with no findings/tone/trend commentary. |
coverage_compare_queries
#Compare 2 or more watchlists against each other over the same date range.
Use this instead of calling coverage_analyze_stories once per watchlist when the user wants a side-by-side comparison, not each watchlist's own analysis -- "which of my watchlists is trending" or "how much overlap is there between X and Y" need this tool, not several separate calls.
Returns, per watchlist: share of voice (hit count and audience, as a percentage of the combined total across all given watchlists) and velocity (spiking/rising/stable/declining/new); per watchlist pair: overlap (shared coverage and lift over chance); and week-over-week change across the combined set. Does not include each watchlist's own findings/tone/trend -- call coverage_analyze_stories(watchlistId=...) per watchlist for that depth.
Example: "Is Tesla or Rivian getting more coverage this month?" -> watchlistIds=["<Tesla id>", "<Rivian id>"]; get both ids from watchlist_get_summary first, never guess them.
| Field | Type | Default | What the model is told | |
|---|---|---|---|---|
| watchlistIds | string[] | required | — | Watch term ids from watchlist_get_summary; never guess one. 1-25 ids -- with a single id this degenerates to that query's own stats, no comparison happens. |
| startDate | string | optional | null | First date to compare, inclusive. |
| endDate | string | optional | null | Last date to compare, inclusive. |
| countries | string[] | optional | null | Plain ISO country codes, e.g. ["US", "GB"]. |
| mediaTypes | string[] | optional | null | Plain media-type strings: "TV", "RADIO" (exact uppercase), "podcast", or "youtube" (case-insensitive). |
| newsOnly | boolean | optional | false | Restrict to news programs only. |
| maxStoriesPerQuery | integer | optional | 10 | Maximum sample stories per watchlist (not used by this tool's own output; affects upstream cost only). |
| maxTopStories | integer | optional | 5 | Maximum watchlist-level top stories (not used by this tool's own output; affects upstream cost only). |
coverage_get_media_item
#Get the full transcript and detail for one media item already found via coverage_search_mentions.
Use this after coverage_search_mentions to read the complete transcript for one specific hit -- the search tool only returns short matching excerpts (snippets), not the full text. Pass the eventId exactly as coverage_search_mentions returned it; never guess or fabricate one.
Example: user asks "show me the full transcript for that first result" -> call this tool with eventId="<id from the earlier search response>".
| Field | Type | Default | What the model is told | |
|---|---|---|---|---|
| eventId | string | required | — | The eventId from a coverage_search_mentions match -- never guess one. |
coverage_get_story
#List every clip in one story from coverage_analyze_stories.
A story groups duplicate coverage of the same segment across stations; coverage_analyze_stories returns only its lead clip plus memberIds. Pass those memberIds here to see each clip -- station, market, air time, viewership, and excerpts -- in the same shape coverage_search_mentions returns, highest viewership first.
missingClipIds lists ids that didn't come back (usually older than the user's retention window); report them rather than guessing.
Example: "Which stations ran that second story?" -> clipIds=<that story's memberIds>.
| Field | Type | Default | What the model is told | |
|---|---|---|---|---|
| clipIds | string[] | required | — | A story's memberIds exactly as coverage_analyze_stories returned them -- never guess one. |
coverage_search_mentions
#Search the user's authorized media coverage using ordinary words or phrases.
Use this for ad hoc topics, especially when the topic is not already in the user's watchlists. Choose whether all words, any word, or the complete phrase must match; exclude unwanted terms; set an inclusive date range; sort by recency or PowerSearch viewership; group duplicate coverage; and request up to 50 matches.
Example: "Show me recent coverage about Tesla pricing" -> query="Tesla pricing". Example: "Find the exact phrase 'artificial intelligence', not crypto" -> query="artificial intelligence", matchMode="phrase", exclude=["crypto"].
Use queryString (instead of query) when recall matters: expand the topic into OR'd synonyms, abbreviations and spelling variants, e.g. (Tesla OR "Model Y") AND (recall OR retira OR NHTSA). Transcripts are speech-to-text, so exact long phrases often miss -- prefer "short phrase"~5. If a queryString returns nothing, broaden it (the response carries a hint) rather than reporting "no coverage".
Set watchlistId to search within one watchlist's own query instead of all coverage -- query becomes optional in that case (an empty query returns everything the watchlist already matches). Get the id from watchlist_get_summary; never guess one.
Narrow further with stations, markets, countries, states, mediaTypes, programs, languages, or channels (podcast/YouTube source ids). Every filter given must match; values within one filter match any. channels can't be combined with the TV/radio-only filters (stations, markets, states, programs) -- make separate calls. These fields need the exact value coverage_search_options returns, not a free-text name -- never guess a filter value; look it up first (e.g. category="stations", query="CNN").
Results include stable event IDs, source details, dates, viewership when available, and short matching excerpts. Pass `nextCursor` back as `cursor` to continue the same search. Do not modify or reuse a cursor with different search criteria. This tool returns individual matches, not daily trends or aggregate statistics -- use coverage_analyze_stories instead when the user wants a synthesized narrative, sentiment, or trend answer rather than a list of items (same query or watchlistId scoping, different question shape). Example: "show me articles about Tesla" -> this tool; "what's the sentiment on Tesla coverage?" -> coverage_analyze_stories.
| Field | Type | Default | What the model is told | |
|---|---|---|---|---|
| query | string | optional | "" | Ordinary words or a phrase to find in media coverage. Optional when watchlistId is set -- an empty query returns everything the watchlist's own definition already matches. |
| queryString | string | optional | null | Advanced alternative to query: an Elasticsearch query string over transcripts. Quote phrases and short terms ("Model Y", "F-22"); OR synonyms and spelling variants; AND/NOT in capitals; () to group; "phrase"~N for words within N of each other; trailing wildcards only (recall*). No field:value syntax -- use the filters. Not allowed with watchlistId. Use instead of query, never both. |
| matchMode | all | any | phrase | optional | "all" | Require all words, any word, or the complete phrase. |
| exclude | string[] | optional | null | Words or phrases that must not appear in matching coverage. |
| startDate | string | optional | null | First date to search, inclusive. |
| endDate | string | optional | null | Last date to search, inclusive. |
| sort | newest | oldest | highest_viewership | lowest_viewership | optional | "newest" | Order by date or viewership. |
| limit | integer | optional | 10 | Maximum matches in this page. |
| cursor | string | optional | null | Opaque nextCursor from the preceding page of this search. |
| groupDuplicates | boolean | optional | true | Group duplicate coverage records before paging. |
| watchlistId | string | optional | null | Scope the search to this watchlist's own query instead of searching all authorized coverage. Get the id from watchlist_get_summary. |
| stations | string[] | optional | null | Station values from coverage_search_options (category=stations). |
| markets | string[] | optional | null | Market values from coverage_search_options (category=markets). |
| countries | string[] | optional | null | Country values from coverage_search_options (category=countries). |
| mediaTypes | string[] | optional | null | Media-type values from coverage_search_options (category=mediaTypes). |
| programs | string[] | optional | null | Program-name values from coverage_search_options (category=programs). |
| languages | string[] | optional | null | Language values from coverage_search_options (category=languages). |
| states | string[] | optional | null | State values from coverage_search_options (category=states). Broadcast only -- podcasts and YouTube have no state. |
| channels | string[] | optional | null | Podcast or YouTube channel ids: a podcast from coverage_search_options (category=podcasts), or a match's sourceId. Never guess one. |
Examples
An exact phrase, newest first. Prefer phrase over the default all for multi-word topics: all matches each word independently, so “Tesla pricing” also returns segments about rate hikes being “priced in” that mention Tesla elsewhere.
{
"query": "Tesla pricing",
"matchMode": "phrase",
"limit": 10
}A fixed window. Both dates are inclusive, and the default window is only the last few days — widen it before concluding a topic has no coverage.
{
"query": "price cut",
"matchMode": "phrase",
"startDate": "2026-08-18",
"endDate": "2026-09-17"
}Biggest audience first, with advertising read-outs filtered out. exclude drops matches containing any of these terms.
{
"query": "electric vehicle",
"matchMode": "all",
"exclude": [
"insurance",
"sponsored"
],
"sort": "highest_viewership",
"limit": 25
}The next page. Pass back the nextCursor from the previous response and repeat every other argument unchanged — a cursor is only valid for the search that produced it.
{
"query": "Tesla pricing",
"matchMode": "phrase",
"cursor": "UzE3ODk1Nzc0NzUwMzA="
}coverage_search_options
#Resolve a human-readable name into the filter values coverage_search_mentions accepts.
Use this before calling coverage_search_mentions with stations, markets, countries, states, mediaTypes, programs, languages, or channels (use category="podcasts" for a podcast channel) -- those fields need the exact `value` this tool returns, not a free-text name; never guess a value instead of looking it up. Each result also carries a display `name` so you can confirm the right match before filtering.
Example: user asks for "Tesla coverage from CNN in Texas" -- call this tool twice, once with category="stations", query="CNN" and once with category="states", query="Texas", then pass the returned values into coverage_search_mentions's stations and states fields.
| Field | Type | Default | What the model is told | |
|---|---|---|---|---|
| category | stations | markets | countries | mediaTypes | programs | languages | states | podcasts | required | — | Which filter dimension to search: stations, markets, countries, states, mediaTypes, programs, languages, or podcasts. |
| query | string | required | — | Search text, e.g. a station call sign, market name, or country. |
| limit | integer | optional | 10 | Maximum candidates to return. |
report_list_recent
#List the current user's reports, newest first.
A report is a saved, curated set of clips. Use this to find a report's id when the user refers to one by name ("my Q3 Tesla report"); never guess a report id. Returns id, title, description, creation time, status, type, clip count, and whether it is shared.
Saved queries (watchlists) are not reports -- list those with watchlist_get_summary.
| Field | Type | Default | What the model is told | |
|---|---|---|---|---|
| limit | integer | optional | 20 | Maximum reports to return. |
| status | all | archived | scheduled | optional | "all" | all, archived, or scheduled reports. |
system_ping
#Health-check tool: echoes the given message back. Replace with real tools.
| Field | Type | Default | What the model is told | |
|---|---|---|---|---|
| message | string | optional | "pong" | Text to echo back. |
Examples
Confirms the session and token are working.
{
"message": "hello"
}watchlist_get_movers
#Find which watch terms are trending up or down across the whole account, ranked by mention-volume change between two equal-length windows (the last `windowDays` vs. the `windowDays` before that).
Use this for "what's spiking," "what's trending," or "what moved this week" questions about the account as a whole. This is different from watchlist_get_term_summaries, which gives a narrative/thematic summary of one specific term you already know you care about -- use this tool first to discover *which* terms are worth a closer look, then pass the returned id into watchlist_get_term_summaries(queryIds=[...]), coverage_search_mentions(watchlistId=...), or coverage_analyze_stories(watchlistId=...) for detail on any one of them.
Example: user asks "what's trending in my watchlists this week?" -- call this tool with windowDays=7 (the default), then look at the risers list.
Each entry's `dailySeries` gives a day-by-day hit count for that term across the comparison window, useful for describing the shape of a spike.
| Field | Type | Default | What the model is told | |
|---|---|---|---|---|
| top | integer | optional | 1 | Number of risers and decliners to return (each side, not combined). The le=50 cap is enforced by this MCP server, not the REST API, to keep results small enough for an LLM to read. |
| windowDays | integer | optional | 7 | Size of the current and prior comparison windows, in days. The le=90 cap is enforced by this MCP server, not the REST API. |
watchlist_get_summary
#Get an overview of the current user's watchlists.
Call this first when the user references "my watchlists" or a saved topic by name -- it resolves a title like "Tesla" into the id that coverage_search_mentions (watchlistId), watchlist_get_term_summaries, and briefing_get_latest all key off. Shows how watchlists are organized into groups and standalone queries, review aggregate counts, and see which watchlists have configured alerts. The response preserves empty groups and duplicate titles -- when a watchlist ID is present, use it instead of the title to identify that watchlist reliably in follow-up workflows, since titles can repeat.
Example: user asks "what's new with my Tesla watchlist?" -- call this tool with no arguments, find the entry titled "Tesla", and use its id in the next call.
This summary does not include media-match counts, trends, query definitions, alert destinations, or alert schedules -- use watchlist_get_term_summaries for match counts and AI-generated themes, or briefing_get_latest for a full daily digest across every watchlist.
Takes no arguments.
Examples
Takes no arguments; the watchlists are the caller's own.
{}watchlist_get_term_summaries
#Get AI-generated summaries of the account's watch terms: themes, dominant tone, notability, top stories and stations, and overlap with other terms.
Use this for "what's going on with this watchlist" or "summarize my X watchlist" questions about one or a few specific terms. Get term ids from watchlist_get_summary first -- never guess or fabricate a queryId; an unrecognized or unowned id is silently dropped rather than erroring, so a guessed id just produces a thinner result you could mistake for "no coverage." Omit queryIds to get every watch term in the account at once.
Example: user asks "how's my Tesla watchlist doing?" -- call watchlist_get_summary to find the Tesla term's id, then call this tool with queryIds set to that one id.
Works even when no daily briefing has ever been generated -- falls back to a live-computed summary in that case, though theme/tone/notability fields stay at their defaults until a briefing exists. Use briefing_get_latest instead for a full-account daily digest with sentiment, findings, and recommendations across every term together. If this summary isn't rich enough -- the user wants deeper tone/sentiment/trend analysis for just this one term -- escalate to coverage_analyze_stories(watchlistId=...); that tool is more expensive (always-live, no caching) so prefer this one first.
| Field | Type | Default | What the model is told | |
|---|---|---|---|---|
| queryIds | string[] | optional | null | Specific watchlist term ids to summarize, from watchlist_get_summary. Omitted or empty returns every watch term in the account. |