Skip to content

Commit 2fd3398

Browse files
authored
Add API server routes (#6)
* Add API server routes * newline imports * Remove relevant data + error handling
1 parent 7aaf956 commit 2fd3398

File tree

7 files changed

+199
-180
lines changed

7 files changed

+199
-180
lines changed

package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"wrangler": "^4.12.0"
3535
},
3636
"dependencies": {
37-
"@cloudflare/workers-oauth-provider": "^0.0.4",
37+
"@cloudflare/workers-oauth-provider": "^0.0.5",
3838
"@modelcontextprotocol/sdk": "^1.10.2",
3939
"@thoughtspot/rest-api-sdk": "^2.13.1",
4040
"agents": "^0.0.75",

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import OAuthProvider from "@cloudflare/workers-oauth-provider";
22
import { McpAgent } from "agents/mcp";
33
import handler from "./handlers";
44
import { Props } from "./utils";
5-
import { MCPServer } from "./mcp-server";
6-
5+
import { MCPServer } from "./servers/mcp-server";
6+
import { apiServer } from "./servers/api-server";
77

88
export class ThoughtSpotMCP extends McpAgent<Env, any, Props> {
99
server = new MCPServer(this);
@@ -17,6 +17,7 @@ export default new OAuthProvider({
1717
apiHandlers: {
1818
"/mcp": ThoughtSpotMCP.serve("/mcp") as any, // TODO: Remove 'any'
1919
"/sse": ThoughtSpotMCP.serveSSE("/sse") as any, // TODO: Remove 'any'
20+
"/api": apiServer as any, // TODO: Remove 'any'
2021
},
2122
defaultHandler: handler as any, // TODO: Remove 'any'
2223
authorizeEndpoint: "/authorize",

src/servers/api-server.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Hono } from 'hono'
2+
import { Props } from '../utils';
3+
import {
4+
createLiveboard,
5+
getAnswerForQuestion,
6+
getDataSources,
7+
getRelevantQuestions
8+
} from '../thoughtspot/thoughtspot-service';
9+
import { getThoughtSpotClient } from '../thoughtspot/thoughtspot-client';
10+
11+
const apiServer = new Hono<{ Bindings: Env & { props: Props } }>()
12+
13+
apiServer.post("/api/tools/relevant-questions", async (c) => {
14+
const { props } = c.executionCtx;
15+
const { query, datasourceIds, additionalContext } = await c.req.json();
16+
const client = getThoughtSpotClient(props.instanceUrl, props.accessToken);
17+
const questions = await getRelevantQuestions(query, datasourceIds, additionalContext || '', client);
18+
return c.json(questions);
19+
});
20+
21+
apiServer.post("/api/tools/get-answer", async (c) => {
22+
const { props } = c.executionCtx;
23+
const { question, datasourceId } = await c.req.json();
24+
const client = getThoughtSpotClient(props.instanceUrl, props.accessToken);
25+
const answer = await getAnswerForQuestion(question, datasourceId, false, client);
26+
return c.json(answer);
27+
});
28+
29+
apiServer.post("/api/tools/create-liveboard", async (c) => {
30+
const { props } = c.executionCtx;
31+
const { name, answers } = await c.req.json();
32+
const client = getThoughtSpotClient(props.instanceUrl, props.accessToken);
33+
const liveboardUrl = await createLiveboard(name, answers, client);
34+
return c.text(liveboardUrl);
35+
});
36+
37+
apiServer.get("/api/resources/datasources", async (c) => {
38+
const { props } = c.executionCtx;
39+
const client = getThoughtSpotClient(props.instanceUrl, props.accessToken);
40+
const datasources = await getDataSources(client);
41+
return c.json(datasources);
42+
});
43+
44+
apiServer.post("/api/rest/2.0/*", async (c) => {
45+
const { props } = c.executionCtx;
46+
const path = c.req.path;
47+
const method = c.req.method;
48+
const body = await c.req.json();
49+
return fetch(props.instanceUrl + path, {
50+
method,
51+
headers: {
52+
"Authorization": `Bearer ${props.accessToken}`,
53+
"Accept": "application/json",
54+
"Content-Type": "application/json",
55+
"User-Agent": "ThoughtSpot-ts-client",
56+
},
57+
body: JSON.stringify(body),
58+
});
59+
});
60+
61+
apiServer.get("/api/rest/2.0/*", async (c) => {
62+
const { props } = c.executionCtx;
63+
const path = c.req.path;
64+
const method = c.req.method;
65+
return fetch(props.instanceUrl + path, {
66+
method,
67+
headers: {
68+
"Authorization": `Bearer ${props.accessToken}`,
69+
"Accept": "application/json",
70+
"User-Agent": "ThoughtSpot-ts-client",
71+
}
72+
});
73+
});
74+
75+
export {
76+
apiServer,
77+
}

src/mcp-server.ts renamed to src/servers/mcp-server.ts

Lines changed: 42 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2-
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema, Tool, ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2+
import {
3+
CallToolRequestSchema,
4+
ListToolsRequestSchema,
5+
ToolSchema,
6+
ListResourcesRequestSchema,
7+
ReadResourceRequestSchema
8+
} from "@modelcontextprotocol/sdk/types.js";
39
import { z } from "zod";
410
import { zodToJsonSchema } from "zod-to-json-schema";
5-
import { Props } from "./utils";
6-
import { getRelevantData } from "./thoughtspot/relevant-data";
7-
import { getThoughtSpotClient } from "./thoughtspot/thoughtspot-client";
8-
import { DataSource, fetchTMLAndCreateLiveboard, getAnswerForQuestion, getDataSources, getRelevantQuestions } from "./thoughtspot/thoughtspot-service";
11+
import { Props } from "../utils";
12+
import { getThoughtSpotClient } from "../thoughtspot/thoughtspot-client";
13+
import {
14+
DataSource,
15+
fetchTMLAndCreateLiveboard,
16+
getAnswerForQuestion,
17+
getDataSources,
18+
getRelevantQuestions
19+
} from "../thoughtspot/thoughtspot-service";
920

1021

1122
const ToolInputSchema = ToolSchema.shape.inputSchema;
@@ -18,16 +29,8 @@ const GetRelevantQuestionsSchema = z.object({
1829
additionalContext: z.string()
1930
.describe("Additional context to add to the query, this might be older data returned for previous questions or any other relevant context that might help the system generate better questions.")
2031
.optional(),
21-
datasourceId: z.string()
22-
.describe("The datasource to get questions for, this is the id of the datasource to get data from")
23-
.optional()
24-
});
25-
26-
const GetRelevantDataSchema = z.object({
27-
query: z.string().describe("The query to get relevant data for, this could be a high level task or question the user is asking or hoping to get answered. You can pass the complete raw query as the system is smart to make sense of it."),
28-
datasourceId: z.string()
29-
.describe("The datasource to get data from, this is the id of the datasource to get data from")
30-
.optional()
32+
datasourceIds: z.array(z.string())
33+
.describe("The datasources to get questions for, this is the ids of the datasources to get data from")
3134
});
3235

3336
const GetAnswerSchema = z.object({
@@ -177,24 +180,29 @@ export class MCPServer extends Server {
177180

178181

179182
async callGetRelevantQuestions(request: z.infer<typeof CallToolRequestSchema>) {
180-
const { query, datasourceId: sourceId, additionalContext } = GetRelevantQuestionsSchema.parse(request.params.arguments);
183+
const { query, datasourceIds: sourceIds, additionalContext } = GetRelevantQuestionsSchema.parse(request.params.arguments);
181184
const client = getThoughtSpotClient(this.ctx.props.instanceUrl, this.ctx.props.accessToken);
182-
const progressToken = request.params._meta?.progressToken;
183-
let progress = 0;
184-
console.log("[DEBUG] Getting relevant questions for query: ", query, " and datasource: ", sourceId);
185+
console.log("[DEBUG] Getting relevant questions for query: ", query, " and datasource: ", sourceIds);
185186

186187
const relevantQuestions = await getRelevantQuestions(
187188
query,
188-
sourceId!,
189+
sourceIds!,
189190
additionalContext,
190191
client,
191192
);
192193

194+
if (relevantQuestions.error) {
195+
return {
196+
isError: true,
197+
content: [{ type: "text", text: "ERROR: " + relevantQuestions.error.message }],
198+
};
199+
}
200+
193201
return {
194-
content: [{
202+
content: relevantQuestions.questions.map(q => ({
195203
type: "text",
196-
text: relevantQuestions.map((question) => `- ${question}`).join("\n"),
197-
}],
204+
text: `Question: ${q.question}\nDatasourceId: ${q.datasourceId}`,
205+
})),
198206
};
199207
}
200208

@@ -206,6 +214,12 @@ export class MCPServer extends Server {
206214
console.log("[DEBUG] Getting answer for question: ", question, " and datasource: ", sourceId);
207215

208216
const answer = await getAnswerForQuestion(question, sourceId, false, client);
217+
if (answer.error) {
218+
return {
219+
isError: true,
220+
content: [{ type: "text", text: "ERROR: " + answer.error.message }],
221+
};
222+
}
209223

210224
return {
211225
content: [{
@@ -221,60 +235,19 @@ export class MCPServer extends Server {
221235
async callCreateLiveboard(request: z.infer<typeof CallToolRequestSchema>) {
222236
const { name, answers } = CreateLiveboardSchema.parse(request.params.arguments);
223237
const client = getThoughtSpotClient(this.ctx.props.instanceUrl, this.ctx.props.accessToken);
224-
const liveboardUrl = await fetchTMLAndCreateLiveboard(name, answers, client);
225-
return {
226-
content: [{
227-
type: "text",
228-
text: `Liveboard created successfully, you can view it at ${liveboardUrl}
229-
230-
Provide this url to the user as a link to view the liveboard in ThoughtSpot.`,
231-
}],
232-
};
233-
}
234-
235-
236-
async callGetRelevantData(request: z.infer<typeof CallToolRequestSchema>) {
237-
const { query, datasourceId: sourceId } = GetRelevantDataSchema.parse(request.params.arguments);
238-
const client = getThoughtSpotClient(this.ctx.props.instanceUrl, this.ctx.props.accessToken);
239-
const progressToken = request.params._meta?.progressToken;
240-
let progress = 0;
241-
console.log("[DEBUG] Getting relevant data for query: ", query, " and datasource: ", sourceId);
242-
243-
const relevantData = await getRelevantData({
244-
query,
245-
sourceId,
246-
shouldCreateLiveboard: true,
247-
notify: (data) => this.notification({
248-
method: "notifications/progress",
249-
params: {
250-
message: data,
251-
progressToken: progressToken,
252-
progress: Math.min(progress++ * 10, 100),
253-
total: 100,
254-
},
255-
}),
256-
client,
257-
});
258-
259-
if (relevantData.allAnswers.length === 0) {
238+
const liveboard = await fetchTMLAndCreateLiveboard(name, answers, client);
239+
if (liveboard.error) {
260240
return {
261241
isError: true,
262-
content: [{
263-
type: "text",
264-
text: "No relevant data found, please make sure the datasource is correct, and you have data download privileges in ThoughtSpot.",
265-
}],
242+
content: [{ type: "text", text: "ERROR: " + liveboard.error.message }],
266243
};
267244
}
268-
269245
return {
270246
content: [{
271247
type: "text",
272-
text: relevantData.allAnswers.map((answer) => `Question: ${answer.question}\nAnswer: ${answer.data}`).join("\n\n")
273-
}, {
274-
type: "text",
275-
text: `Dashboard Url: ${relevantData.liveboard}
248+
text: `Liveboard created successfully, you can view it at ${liveboard.url}
276249
277-
Use this url to view the dashboard/liveboard in ThoughtSpot which contains visualizations for the generated data. *Always* Present this url to the user as a link to view the data as a reference.`,
250+
Provide this url to the user as a link to view the liveboard in ThoughtSpot.`,
278251
}],
279252
};
280253
}

src/thoughtspot/relevant-data.ts

Lines changed: 0 additions & 63 deletions
This file was deleted.

0 commit comments

Comments
 (0)