Skip to content

feat(get-data): Spotter answer data (always on, with expired-answer reload) and iframe suppression flag - #227

Merged
mouryab merged 10 commits into
mainfrom
spotter-answer-data-iframe-suppression-v3
Nov 5, 2026
Merged

mouryab merged 10 commits into
mainfrom
spotter-answer-data-iframe-suppression-v3

Conversation

@mouryab

@mouryab mouryab commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

What

get_data can now read the rows behind an answer from Spotter (the Analytics Agent), including answers whose session has expired. This is always on. A separate opt-in URL flag, suppress-iframes=true, removes iframes so Claude builds its own visuals from the rows. Design doc: "Spotter answer data in getData + iframe suppression".

Spotter answer data (always on)

  • get_data accepts object_type: "SPOTTER_ANSWER", with object_id set to the answer_id from get_session_updates. visualization_ids is ignored.
  • Rows come from exportAnswerReport as CSV, using the answer's session id and generation number. They are parsed into the existing get_data result shape:
    • Empty cells become null.
    • Plain decimals and E-notation become numbers. The export writes large values as 1.093225231E7.
    • Leading-zero IDs, integers above 2^53 and other formatted text stay as strings.
    • total_row_count is the full row count.
  • The export runs as the calling user, so permissions and RLS apply. max_rows works as it does for saved Answers (default 25).
  • The cluster's data-extract watermark is skipped. It is a "Data extract produced by …" block before the header and is recognised only by that text, so single-column answers with empty cells keep all their rows.
  • By default the get_data description keeps Claude iframe-first. Claude shows Spotter's visualization and fetches rows only when the user asks to chart, reshape, combine or build a dashboard.

Expired answers

  • An answer's session ids expire after about 8 hours. answer_id now also carries conversation_id and message_id, which last as long as the conversation does. Existing readers that only use session_id and gen_no, such as the dashboard tools, are unaffected.
  • When the export fails, get_data reloads the answer and exports again with the fresh ids. The reload is POST /conversation/v2/{conversation_id}/message/{message_id}/load/public, the same call the Visual Embed SDK makes to replay a stored answer. It re-runs the query, so it can be slow.
  • There is no reload on 401/403, or for an answer_id issued before this change.
  • If the conversation itself is gone, the error asks Claude to put the question to Spotter again.
  • A 403 gets a permission hint instead.

suppress-iframes=true (opt-in, undocumented)

  • iframe_url is removed from get_session_updates results and from that tool's output schema. The schema no longer mentions iframes, embeds or visualizations.
  • Embed links and <iframe> tags are stripped from update text, including thinking text and content in raw mode.
  • frame_url is removed from V1 getAnswer.
  • The get_data description tells Claude that answers have no visual of their own, so it should build one from the returned rows whenever that would help.
URL Answer data Iframes Claude charts it itself?
(no flags) On Shown Only when asked
suppress-iframes=true On Hidden Yes, whenever it helps

Only the exact value true turns suppression on. With it unset, iframes behave exactly as before.

Why

Customers can put this server behind their own proxy and Claude client. Today Claude can show a Spotter answer but can't read its rows, and there is no way to turn iframes off. Answer data is on for everyone, for the doc's default-user story: "turn that into a Claude chart I can tweak". An earlier enable-spotter-answer-data off switch was dropped. If a URL still carries it, the server ignores it.

Implementation notes

  • suppress-iframes is parsed in both props hooks in src/index.ts.
  • Tool-text and schema changes are made in applyUrlFlagToolOverrides (tool-definitions.ts). Text is stripped when get_session_updates reads the stored updates (stripEmbeddedVisualizations).
  • answer_id is built in buildAnswerId (streaming-utils.ts).
  • The reload is loadSpotter3Answer in spotter-3-client.ts, recorded as the load_agent_conversation_answer upstream operation and the reload-spotter-answer span.
  • The README has a new "URL flags" section. suppress-iframes is left out of it on purpose.
  • No new API version was added.

Testing

  • Unit tests cover:
    • the CSV parser: watermark, single-column answers with empty cells, number handling including E-notation;
    • embed stripping;
    • the tool list in each mode, and URL parsing, including that the old flag is ignored;
    • the SPOTTER_ANSWER call paths: success, bad answer_id, export failure, 403;
    • expired-answer reload: success, no reload on 403, old-style answer_id, conversation gone;
    • loadSpotter3Answer and getAnswer.
  • Full suite: 1863 passed. Coverage is 93.9%. Lint is clean.
  • Ran locally with wrangler dev against champagne-master-aws staging, in the default and suppress modes. Real rows came back. A simulated expired answer (a dead session_id with real conversation ids) was reloaded through load/public and returned the same rows.
  • Ran on a deployed connector with suppress-iframes=true from Claude ("which Campaign Channel drives the highest Net Sales across each age_bucket"):
    • The answer_id carried the conversation ids, and there was no iframe_url or embed text.
    • get_data returned all 42 rows, matching Spotter's text, and Claude charted them.
    • That run found the E-notation bug, which is fixed in this PR and verified on a redeploy.
    • The flag removal itself has not been redeployed yet.

Known gaps / follow-ups

  • Reload latency: a reload re-runs the query, which can take 45s or more on a busy cluster. A client with a short timeout may give up.
  • Iframe-first by default: every client now sees SPOTTER_ANSWER. We rely on the tool description to keep models iframe-first when iframes are shown, which still needs a check in Claude. Whether Claude charts answers without being asked under suppress-iframes also needs a fresh-chat check.
  • Memory: the CSV export loads the whole answer before trimming to max_rows.
  • Permission gap: with suppression on, a user without the data-download privilege gets neither a visual nor data.
  • Skill file: line 12 of skills/analyzing-data-with-thoughtspot/SKILL.md ("Don't generate your own charts") conflicts with the suppressed flow.

… flags

Let Claude read the rows behind an unsaved Analytics Agent answer, and let
callers such as TriNet's proxy strip embedded visualizations from tool
output. Both are behind undocumented URL flags and off by default.

- enable-spotter-answer-data=true: get_data accepts object_type
  SPOTTER_ANSWER with the answer_id from get_session_updates. Rows come
  from the answer's CSV export (session + generation number) and are parsed
  into the get_data shape, skipping the data-extract watermark preamble.
  The description keeps the iframe-first default.
- suppress-iframes=true: implies the above, drops iframe_url from session
  updates and its schema, strips embed links and iframe tags from update
  text (thinking included), drops frame_url from getAnswer, and steers
  Claude to build visuals from get_data.
…frame-suppression

# Conflicts:
#	src/servers/mcp-server.ts
#	src/servers/tool-definitions.ts
#	src/services/clients/get-data/spotter-answer-data.ts
#	test/streaming-utils.spec.ts
- Strip the watermark preamble only when it reads "Data extract…", so a
  single-column answer with an empty cell no longer loses rows
- Coerce a cell to a number only when it round-trips exactly, keeping
  leading-zero IDs, >2^53 integers and formatted text as strings
- Give a permission hint instead of the expiry hint on 403
- Drop the unused title parameter
…ault

- answer_id now also carries conversation_id and message_id. These outlive
  the answer's session ids (about 8 hours), and existing readers ignore them
- When the CSV export fails, get_data loads the answer again through
  /conversation/v2/{conv}/message/{id}/load/public (the call the Visual
  Embed SDK uses to replay answers) and exports with the fresh ids.
  Not on 401/403, or for answer_ids without the conversation ids
- get_data on SPOTTER_ANSWER is on by default; enable-spotter-answer-data=false
  turns it off. suppress-iframes stays opt-in and forces it on
@mouryab
mouryab requested a review from a team as a code owner November 2, 2026 18:32

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for fetching the underlying data of unsaved Analytics Agent (Spotter) answers via the get_data tool, using the new SPOTTER_ANSWER object type. It adds two new URL query parameters: enable-spotter-answer-data (to control this feature) and suppress-iframes (to omit embedded visualization iframes and links from responses, relying instead on raw data visualization). To support this, the PR implements CSV parsing and watermark stripping for exported reports, adds a mechanism to reload expired answer sessions using conversation and message IDs, and updates the MCP server schemas and tool definitions dynamically based on these flags. Comprehensive unit and integration tests have been added to verify these behaviors. I have no feedback to provide as no review comments were submitted.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://spotter-answer-data-iframe-suppression-v-thoughtspot-mcp-server.thoughtspot-485.workers.dev (commit 028d955)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://4f555e95-thoughtspot-mcp-server.thoughtspot-485.workers.dev 028d955 2026-11-05T01:47:45.847Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://e6231bd2-thoughtspot-mcp-server.thoughtspot-485.workers.dev f2d777d 2026-11-04T20:19:00.247Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://a62cbde0-thoughtspot-mcp-server.thoughtspot-485.workers.dev 308de5e 2026-11-03T02:32:44.594Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://4694e881-thoughtspot-mcp-server.thoughtspot-485.workers.dev 9f3ac97 2026-11-02T20:34:16.789Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://2a1711ee-thoughtspot-mcp-server.thoughtspot-485.workers.dev 784066a 2026-11-02T18:31:19.234Z Visit the dashboard ↗

The answer export writes large values as "1.093225231E7", which the
round-trip check left as strings. Accept plain decimals and E-notation
instead, still keeping leading-zero IDs and integers beyond 2^53 as text.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
get-data-mcp-server 028d955 Nov 05 2026, 01:47 AM

Remove the enable-spotter-answer-data URL flag. get_data now always
accepts SPOTTER_ANSWER, so the flag-off rejection path, prop and span
attribute go too. suppress-iframes is unchanged.
@mouryab mouryab changed the title Spotter answer data iframe suppression v3 feat(get-data): Spotter answer data (always on, with expired-answer reload) and iframe suppression flag Nov 3, 2026
Comment thread README.md Resolved
Comment thread src/index.ts Outdated
const enableRawSessionUpdates =
url.searchParams.get("enable-raw-session-updates") === "true";
props.enableRawSessionUpdates = enableRawSessionUpdates;
props.suppressIframes = url.searchParams.get("suppress-iframes") === "true";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: make this a local var to avoid computing it twice, similar to what we did for enableRawSessionUpdates

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6c94584 — suppressIframes is now a local in both the bearer and OAuth paths, same as enableRawSessionUpdates.

Comment thread src/servers/tool-definitions.ts Resolved
Comment thread src/servers/tool-definitions.ts Resolved
Comment thread src/servers/tool-definitions.ts Resolved
Comment thread src/servers/mcp-server.ts Outdated
{
session_updates: messagesState.messages,
session_updates: this.ctx.props.suppressIframes
? stripEmbeddedVisualizations(messagesState.messages)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of removing the fields after the fact, we should just not generate the field in the first place. The iframe_url is created inside MCP Server code in src/streaming-utils.ts around line 170. Let's pass the suppressIframes flag and skip generating that field if we don't want it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. suppressIframes now flows from send_session_message → sendAgentConversationMessageStreaming → processSendAgentConversationMessageStreamingResponse, so iframe_url is never built. get_session_updates returns stored messages as they are.

One thing I kept: stripping embed links from message text (text / raw content), now done at write time alongside the iframe_url skip. In raw mode there's never an iframe_url, so text stripping is the only thing the flag does there. Do you know whether Spotter's text ever actually contains #/embed/ / tsmcp=true links? If it never does, I'll drop the stripping and just skip the field.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spotter's text never sends us embed links so we don't need to do any postprocessing

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 80c6179. The flag now only skips building iframe_url.

Comment thread src/services/thoughtspot-service.ts Outdated
generation_number: generationNumber,
file_format: "CSV",
});
return file.text();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do file.text() outside of observeUpstreamCall wrapper? It's not really part of the upstream call - it's postprocessing

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — moved out of observeUpstreamCall, matching the existing getAnswerData.

Comment thread src/services/thoughtspot-service.ts Resolved
Address review on #227 now that Spotter answer data is always on:

- Fold SPOTTER_ANSWER into GET_DATA_SUPPORTED_TYPES and GetDataInputSchema;
  move the answer_id/iframe_url wording into ConversationUpdateSchema and
  drop the flag-specific schemas and applyUrlFlagToolOverrides. With
  suppress-iframes, listTools only swaps one sentence of get_data's description.
- Skip building iframe_url at stream-processing time instead of stripping it
  in get_session_updates; embed links in message text are stripped there too.
- Keep Spotter answer CSV cells as strings, read the export body outside
  observeUpstreamCall, and raise SpotterAnswerCsvError for an unusable CSV
  so it is reported directly instead of triggering an expired-answer reload.
- Revert the v1 getAnswer frame_url change, use a local var for the
  suppress-iframes flag, drop the README URL-flags section (belongs in docs),
  and fix the raw get_session_updates answer_id description.
@mouryab
mouryab force-pushed the spotter-answer-data-iframe-suppression-v3 branch from 18348f7 to f2d777d Compare November 4, 2026 20:17
Comment thread src/streaming-utils.ts Outdated
await appendStoredMessages(
conversationId,
suppressIframes
? newMessages.map(stripEmbeddedVisualizationLinks)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This extra postprocessing is not needed, we don't get embed iframe links in the Spotter text stream

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 80c6179. The flag now only skips building iframe_url.

Comment thread src/streaming-utils.ts Outdated
);
};

// A ThoughtSpot embedded-visualization URL: the MCP embed marker or an embed route.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this below is not needed

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 80c6179.

@@ -1,11 +1,21 @@
// An unsaved answer from the Analytics Agent (Spotter), addressed by the
// `answer_id` that get_session_updates returns.
export const SPOTTER_ANSWER_TYPE = "SPOTTER_ANSWER";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call it ANALYTICS_AGENT_ANSWER to get ahead of whitelabeling concerns. We should not expose TS-branded names to the host agent.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to ANALYTICS_AGENT_ANSWER everywhere the host agent sees it, in 028d955.

Spotter's text never carries #/embed/ or tsmcp=true links, so the
suppress-iframes flag now only skips building iframe_url on answers.
Avoid exposing TS-branded names to the host agent ahead of whitelabeling:
the get_data object_type value, its descriptions and error messages now use
ANALYTICS_AGENT_ANSWER.
@mouryab
mouryab merged commit 6b1a399 into main Nov 5, 2026
5 checks passed
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.

3 participants