[SCAL-323294] changes for fetch_data tool - #179
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces two new tools, search_objects and fetch_data, to the MCP server for searching and retrieving data from ThoughtSpot objects (Answers and Liveboards). It includes new schemas, gRPC-backed API clients, terminology mapping for synonyms, and comprehensive unit tests. The reviewer identified a critical issue in src/servers/mcp-server.ts where the asynchronous validateConnection method is not awaited in the Ping tool case, causing the check to always evaluate to true.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| case ToolName.Ping: { | ||
| if (this.ctx.props.accessToken && this.ctx.props.instanceUrl) { | ||
| if (!this.getThoughtSpotService(recorder).validateConnection()) { | ||
| if (!this.getThoughtSpotService(recorder).validateConnection()) { |
There was a problem hiding this comment.
The validateConnection method is asynchronous and returns a Promise<boolean>. Since it is not awaited here, the condition !promise will always evaluate to false because any Promise object is truthy. This effectively bypasses the connection validation check. Please add await to correctly evaluate the promise result.
| if (!this.getThoughtSpotService(recorder).validateConnection()) { | |
| if (!(await this.getThoughtSpotService(recorder).validateConnection())) { |
References
- When a method returns a Promise, ensure it is awaited to prevent race conditions and potential data loss, especially when subsequent operations depend on its completion.
2b7918b to
de4930a
Compare
|
@copilot review the pr |
Reviewed the PR and addressed one issue: |
352f265 to
6454d11
Compare
|
@copilot review the pr |
6454d11 to
ddaf1fa
Compare
| ), | ||
| columns: z.array(z.string()).describe("The column names, in order."), | ||
| data_rows: z | ||
| .array(z.array(z.unknown())) |
There was a problem hiding this comment.
Is there a way to add a proper type contract
There was a problem hiding this comment.
Tightened to string | number | boolean | null datatypes.
|
|
||
| // Round numeric cells to 2 decimals: collapses FP noise and trims payload. | ||
| // Non-numbers and non-finite values pass through untouched. | ||
| function roundCell(value: unknown): unknown { |
There was a problem hiding this comment.
This seems arbitrary, what if the customer wants more precision? I feel we should return the full value for now, and not prematurely optimize this.
There was a problem hiding this comment.
Removed — cells now returned at full precision.
| // treats it as "must hold the whole viz" and 500s when it's too small. | ||
| // Start at maxRows; on that error, bump to the required count and refetch, | ||
| // then cap rows client-side below. Attempts are bounded (a full Liveboard | ||
| // can report a larger viz on each retry) to avoid a runaway loop. |
There was a problem hiding this comment.
This seems like a hacky design, we should make the API call such that it is successful on the first try. Is there no way to do that using this API?
There was a problem hiding this comment.
Reworked to one call — Answers pass record_size = max_rows,
Liveboards request the max 32-bit int.
Verified on local instance.
349b5d8 to
cb87e5c
Compare
|
@copilot review the PR |
| description: [ | ||
| "Fetch the full data (columns and rows) of a saved Answer or Liveboard, identified by its GUID.", | ||
| "CALL THIS WHENEVER the user wants to explain, describe, summarize, analyze, interpret, or ask what a specific object contains or shows: any explanation of what an object contains must be grounded in the data this tool returns, so fetch it first with the object's `id`, then answer from the returned data.", | ||
| "Returns the object's data exactly as saved — it does not run new queries or change the object's question, filters, or columns. The result is shaped to the object's type: an Answer returns a single tabular result, a Liveboard returns one tabular result per visualization (each with its visualization id and name). To pull a single visualization pinned on a Liveboard, pass the Liveboard GUID as `object_id` and the visualization GUID in `visualization_ids`. Each result includes the column names and data rows. Use the optional `max_rows` to bound the rows returned per visualization (defaults to 25).", |
There was a problem hiding this comment.
This description is a bit misleading, since TS will execute the saved query against the latest state of the data, so the tool may return different data compared to when the user first saved the answer. It WILL also run a new database query, so not sure why this says it does not run new queries.
There was a problem hiding this comment.
updated tool definitions.
| type: z | ||
| .string() | ||
| .describe("The resolved object type: either 'ANSWER' or 'LIVEBOARD'."), | ||
| description: z.string().describe("The description of the object."), |
There was a problem hiding this comment.
There are a lot of fields here duplicated from the search_objects response. Let's make it more concise and only include truly NEW data which the agent doesn't already know.
There was a problem hiding this comment.
Trimmed output to just data. Removed id/name/type/description — all already known from search_objects.
| .describe( | ||
| "The object's data. A single entry for an Answer; one entry per visualization for a Liveboard.", | ||
| ), | ||
| request_id: z |
There was a problem hiding this comment.
Let's remove like last time
There was a problem hiding this comment.
Removed request_id. Still sent as x-request-id upstream for tracing.
| row_count: z | ||
| .number() | ||
| .optional() | ||
| .describe("Number of rows actually returned in `data_rows`."), |
There was a problem hiding this comment.
Isn't this redundant? The agent can see how many rows there are
There was a problem hiding this comment.
Removed — agent can count data_rows. Kept total_row_count (upstream total, not derivable when capped).
|
|
||
| // Only Answers and Liveboards expose fetchable data. | ||
| const ANSWER_TYPE = "ANSWER"; | ||
| const LIVEBOARD_TYPE = "LIVEBOARD"; |
There was a problem hiding this comment.
Done — enum ObjectType { Answer, Liveboard }.
| const headers = buildHeaders(token, undefined, undefined, { requestId }); | ||
|
|
||
| // Step 1: resolve the object's type — it decides the data endpoint. | ||
| const metaData = await postJson( |
There was a problem hiding this comment.
Why do we need to do /metadata/search again? I thought we already did this in search_objects tool and returned all the necessary details to directly call the relevant /data API here.
There was a problem hiding this comment.
Added optional object_type — pass the type from search_objects and we skip the lookup (one call). Kept optional so a bare GUID still resolves.
No description provided.