Skip to content

Commit b69a00a

Browse files
authored
add open api spec for tools (#57)
* add open api spec for tools * address comments - export tool schemas from respective servers * fix conflicts * update README with instructions to connect to glean actions * add annotations for datasource discovery tool * return tool definition directly for open ai mcp server * add glean integration info to public docs * rename doc file from .txt to .md
1 parent 5739346 commit b69a00a

File tree

11 files changed

+350
-104
lines changed

11 files changed

+350
-104
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ For API usage, you would the token endpoints with a `secret_key` to generate the
157157
- `getRelevantQuestions`: Get relevant data questions from ThoughtSpot analytics based on a user query.
158158
- `getAnswer`: Get the answer to a specific question from ThoughtSpot analytics.
159159
- `createLiveboard`: Create a liveboard from a list of answers.
160+
- `getDataSourceSuggestions`: Get datasource suggestions for a given query.
160161
- **MCP Resources**:
161162
- `datasources`: List of ThoughtSpot Data models the user has access to.
162163

package-lock.json

Lines changed: 11 additions & 0 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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
},
4444
"dependencies": {
4545
"@cloudflare/workers-oauth-provider": "^0.0.5",
46+
"@hono/zod-validator": "^0.7.2",
4647
"@microlabs/otel-cf-workers": "^1.0.0-rc.52",
4748
"@modelcontextprotocol/sdk": "^1.15.1",
4849
"@thoughtspot/rest-api-sdk": "^2.13.1",

public/docs/glean-actions.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
- Glean actions
2+
3+
To configure the MCP server to be used via glean actions follow these steps:
4+
5+
1. Create a separate action for each the tools exposed through the MCP server
6+
2. Add open api spec for the tool that you are adding in the functionality section. The openapi spec for each tool is available on this url : https://agent.thoughtspot.app/openapi-spec/tools/{tool_name}. Tool name here would be the name given in the tool set below in [Features](#features). For example, Get relevant data questions tool has name getRelevantQuestions. Note: getDataSourceSuggestions is not yet available as openapi-spec as the feature is not yet Generally Available in ThoughtSpot for all customers. We will add this once it is available.
7+
3. Select authentication type as Oauth User while configuring the action
8+
4. Register the glean oauth server with TS MCP server
9+
10+
```bash
11+
curl 'https://agent.thoughtspot.app/register' \
12+
-H 'accept: */*' \
13+
-H 'accept-language: en-US,en;q=0.9' \
14+
--data-raw '{"redirect_uris":["${glean_callback_url}"],"token_endpoint_auth_method":"client_secret_basic","grant_types":["authorization_code","refresh_token"],"response_types":["code"],"client_name":"{company_glean_name}","client_uri":"${company_glean_uri}"}'
15+
```
16+
5. Add the client_id and client secret obtained from to the glean action auth section. Along with this add https://agent.thoughtspot.app/authorize in client url and https://agent.thoughtspot.app/token in authorize url.
17+
6. Save the spec and reload the action.
18+
7. Once the action is saved, we will get an option to run API test. It is recommended to run it one time to make sure everything is setup.
19+

src/api-schemas/open-api-spec.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { Hono } from 'hono';
2+
import { toolDefinitionsMCPServer } from '../servers/mcp-server';
3+
import { capitalize } from '../utils';
4+
5+
export const openApiSpecHandler = new Hono();
6+
7+
// Helper function to generate tool schema
8+
const generateToolSchema = (tool: typeof toolDefinitionsMCPServer[0]) => {
9+
const schemaName = `${capitalize(tool.name)}Request`;
10+
const generatedSchema = { ...tool.inputSchema } as any;
11+
generatedSchema.$schema = undefined;
12+
return { schemaName, schema: generatedSchema };
13+
};
14+
15+
// Helper function to generate response schema
16+
const generateResponseSchema = () => {
17+
return {
18+
type: 'object',
19+
description: 'Response from the API endpoint'
20+
};
21+
};
22+
23+
// Create individual endpoints for each tool
24+
for (const tool of toolDefinitionsMCPServer) {
25+
const { schemaName, schema } = generateToolSchema(tool);
26+
const responseSchema = generateResponseSchema();
27+
28+
openApiSpecHandler.get(`/tools/${tool.name}`, async (c) => {
29+
const toolSpec = {
30+
openapi: '3.0.0',
31+
info: {
32+
title: 'ThoughtSpot API',
33+
version: '1.0.0',
34+
description: 'API for interacting with ThoughtSpot services'
35+
},
36+
servers: [
37+
{
38+
url: '<TS_AGENT_URL>',
39+
description: 'ThoughtSpot agent url'
40+
}
41+
],
42+
paths: {
43+
[`/api/tools/${tool.name}`]: {
44+
post: {
45+
summary: tool.description,
46+
description: tool.description,
47+
operationId: tool.name,
48+
tags: ['Tools'],
49+
requestBody: {
50+
required: true,
51+
content: {
52+
'application/json': {
53+
schema: {
54+
$ref: `#/components/schemas/${schemaName}`
55+
}
56+
}
57+
}
58+
},
59+
responses: {
60+
'200': {
61+
description: 'Successful response',
62+
content: {
63+
'application/json': {
64+
schema: {
65+
$ref: `#/components/schemas/${capitalize(tool.name)}Response`
66+
}
67+
}
68+
}
69+
},
70+
'400': {
71+
description: 'Bad request - Invalid input parameters'
72+
},
73+
'401': {
74+
description: 'Unauthorized - Invalid or missing authentication'
75+
},
76+
'500': {
77+
description: 'Internal server error'
78+
}
79+
}
80+
}
81+
}
82+
},
83+
components: {
84+
schemas: {
85+
[schemaName]: schema,
86+
[`${capitalize(tool.name)}Response`]: responseSchema
87+
}
88+
}
89+
};
90+
91+
return c.json(toolSpec);
92+
});
93+
}
94+
95+
// Main OpenAPI spec endpoint that combines all tools
96+
openApiSpecHandler.get('/', async (c) => {
97+
const paths: Record<string, any> = {};
98+
const schemas: Record<string, any> = {};
99+
100+
// any tool added to the toolDefinitionsMCPServer will be added to the openapi spec automatically
101+
// the api server path should be /api/tools/<tool-name>
102+
for (const tool of toolDefinitionsMCPServer) {
103+
const { schemaName, schema } = generateToolSchema(tool);
104+
const responseSchema = generateResponseSchema();
105+
106+
schemas[schemaName] = schema;
107+
schemas[`${capitalize(tool.name)}Response`] = responseSchema;
108+
109+
paths[`/api/tools/${tool.name}`] = {
110+
post: {
111+
summary: tool.description,
112+
description: tool.description,
113+
operationId: tool.name,
114+
tags: ['Tools'],
115+
requestBody: {
116+
required: true,
117+
content: {
118+
'application/json': {
119+
schema: {
120+
$ref: `#/components/schemas/${schemaName}`
121+
}
122+
}
123+
}
124+
},
125+
responses: {
126+
'200': {
127+
description: 'Successful response',
128+
content: {
129+
'application/json': {
130+
schema: {
131+
$ref: `#/components/schemas/${capitalize(tool.name)}Response`
132+
}
133+
}
134+
}
135+
},
136+
'400': {
137+
description: 'Bad request - Invalid input parameters'
138+
},
139+
'401': {
140+
description: 'Unauthorized - Invalid or missing authentication'
141+
},
142+
'500': {
143+
description: 'Internal server error'
144+
}
145+
}
146+
}
147+
};
148+
}
149+
150+
const openApiDocument = {
151+
openapi: '3.0.0',
152+
info: {
153+
title: 'ThoughtSpot API',
154+
version: '1.0.0',
155+
description: 'API for interacting with ThoughtSpot services'
156+
},
157+
servers: [
158+
{
159+
url: '<TS_AGENT_URL>',
160+
description: 'ThoughtSpot agent url'
161+
}
162+
],
163+
paths: paths,
164+
components: {
165+
schemas: schemas
166+
}
167+
};
168+
169+
return c.json(openApiDocument);
170+
});

src/handlers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { any } from 'zod';
88
import { encodeBase64Url, decodeBase64Url } from 'hono/utils/encode';
99
import { getActiveSpan, WithSpan } from './metrics/tracing/tracing-utils';
1010
import { context, type Span, SpanStatusCode, trace } from "@opentelemetry/api";
11+
import { openApiSpecHandler } from './api-schemas/open-api-spec';
1112

1213
const app = new Hono<{ Bindings: Env & { OAUTH_PROVIDER: OAuthHelpers } }>()
1314

@@ -263,4 +264,6 @@ app.post("/store-token", async (c) => {
263264
}
264265
});
265266

267+
app.route('/openapi-spec', openApiSpecHandler);
268+
266269
export default app;

src/servers/api-server.ts

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Hono } from 'hono'
2+
import { zValidator } from '@hono/zod-validator'
23
import type { Props } from '../utils';
3-
import { McpServerError } from '../utils';
4-
import { getDataSources, ThoughtSpotService } from '../thoughtspot/thoughtspot-service';
4+
import { ThoughtSpotService } from '../thoughtspot/thoughtspot-service';
55
import { getThoughtSpotClient } from '../thoughtspot/thoughtspot-client';
66
import { getActiveSpan, WithSpan } from '../metrics/tracing/tracing-utils';
7-
import { context, type Span, SpanStatusCode, trace } from "@opentelemetry/api";
7+
import { CreateLiveboardSchema, GetAnswerSchema, GetRelevantQuestionsSchema } from './mcp-server';
88

99
const apiServer = new Hono<{ Bindings: Env & { props: Props } }>()
1010

@@ -35,9 +35,9 @@ class ApiHandler {
3535
}
3636

3737
@WithSpan('api-create-liveboard')
38-
async createLiveboard(props: Props, name: string, answers: any[]) {
38+
async createLiveboard(props: Props, name: string, answers: any[], noteTileParsedHtml: string) {
3939
const service = this.getThoughtSpotService(props);
40-
const result = await service.fetchTMLAndCreateLiveboard(name, answers);
40+
const result = await service.fetchTMLAndCreateLiveboard(name, answers, noteTileParsedHtml);
4141
return result.url || '';
4242
}
4343

@@ -88,25 +88,51 @@ class ApiHandler {
8888

8989
const handler = new ApiHandler();
9090

91-
apiServer.post("/api/tools/relevant-questions", async (c) => {
92-
const { props } = c.executionCtx;
93-
const { query, datasourceIds, additionalContext } = await c.req.json();
94-
const questions = await handler.getRelevantQuestions(props, query, datasourceIds, additionalContext);
95-
return c.json(questions);
96-
});
97-
98-
apiServer.post("/api/tools/get-answer", async (c) => {
99-
const { props } = c.executionCtx;
100-
const { question, datasourceId } = await c.req.json();
101-
const answer = await handler.getAnswer(props, question, datasourceId);
102-
return c.json(answer);
103-
});
91+
apiServer.post(
92+
"/api/tools/relevant-questions",
93+
zValidator('json', GetRelevantQuestionsSchema),
94+
async (c) => {
95+
const { props } = c.executionCtx;
96+
const { query, datasourceIds, additionalContext } = c.req.valid('json');
97+
const questions = await handler.getRelevantQuestions(props, query, datasourceIds, additionalContext);
98+
return c.json(questions);
99+
}
100+
);
101+
102+
apiServer.post(
103+
"/api/tools/get-answer",
104+
zValidator('json', GetAnswerSchema),
105+
async (c) => {
106+
const { props } = c.executionCtx;
107+
const { question, datasourceId } = c.req.valid('json');
108+
const answer = await handler.getAnswer(props, question, datasourceId);
109+
return c.json(answer);
110+
}
111+
);
112+
113+
apiServer.post(
114+
"/api/tools/create-liveboard",
115+
zValidator('json', CreateLiveboardSchema),
116+
async (c) => {
117+
const { props } = c.executionCtx;
118+
const { name, answers, noteTile } = c.req.valid('json');
119+
const liveboardUrl = await handler.createLiveboard(props, name, answers, noteTile);
120+
return c.text(liveboardUrl);
121+
}
122+
);
104123

105-
apiServer.post("/api/tools/create-liveboard", async (c) => {
124+
apiServer.get("/api/tools/ping", async (c) => {
106125
const { props } = c.executionCtx;
107-
const { name, answers } = await c.req.json();
108-
const liveboardUrl = await handler.createLiveboard(props, name, answers);
109-
return c.text(liveboardUrl);
126+
console.log("Received Ping request");
127+
if (props.accessToken && props.instanceUrl) {
128+
return c.json({
129+
content: [{ type: "text", text: "Pong" }],
130+
});
131+
}
132+
return c.json({
133+
isError: true,
134+
content: [{ type: "text", text: "ERROR: Not authenticated" }],
135+
});
110136
});
111137

112138
apiServer.get("/api/resources/datasources", async (c) => {

0 commit comments

Comments
 (0)