• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

thoughtspot / mcp-server / 15691145450

16 Jun 2025 08:23PM UTC coverage: 19.595% (-0.04%) from 19.63%
15691145450

Pull #31

github

web-flow
Merge f989fca49 into b46edf0d4
Pull Request #31: Add locale header for every request with ThoughtSpot Rest client

9 of 98 branches covered (9.18%)

Branch coverage included in aggregate %.

3 of 8 new or added lines in 1 file covered. (37.5%)

78 of 346 relevant lines covered (22.54%)

0.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

13.33
/src/handlers.ts
1
import type { AuthRequest, OAuthHelpers } from '@cloudflare/workers-oauth-provider'
2
import { Hono } from 'hono'
3
import type { Props } from './utils';
4
import { parseRedirectApproval, renderApprovalDialog } from './oauth-manager/oauth-utils';
5
import { renderTokenCallback } from './oauth-manager/token-utils';
6
import { any } from 'zod';
7
import { encodeBase64Url, decodeBase64Url } from 'hono/utils/encode';
8

9

10

11
const app = new Hono<{ Bindings: Env & { OAUTH_PROVIDER: OAuthHelpers } }>()
2✔
12

13
app.get("/", async (c) => {
2✔
14
    return c.env.ASSETS.fetch('/index.html');
×
15
});
16

17
app.get("/hello", async (c) => {
2✔
18
    return c.json({ message: "Hello, World!" });
2✔
19
});
20

21
app.get("/authorize", async (c) => {
2✔
22
    const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw);
×
23
    const { clientId } = oauthReqInfo
×
24
    if (!clientId) {
×
25
        return c.text('Invalid request', 400)
×
26
    }
27
    return renderApprovalDialog(c.req.raw, {
×
28
        client: await c.env.OAUTH_PROVIDER.lookupClient(clientId),
29
        server: {
30
            name: "ThoughtSpot MCP Server",
31
            logo: "https://avatars.githubusercontent.com/u/8906680?s=200&v=4",
32
            description: 'MCP Server for ThoughtSpot Agent', // optional
33
        },
34
        state: { oauthReqInfo }, // arbitrary data that flows through the form submission below
35
    })
36
})
37

38
app.post("/authorize", async (c) => {
2✔
39
    // Validates form submission and extracts state
40
    const { state, instanceUrl } = await parseRedirectApproval(c.req.raw)
×
41
    if (!state.oauthReqInfo) {
×
42
        return c.text('Invalid request', 400)
×
43
    }
44

45
    if (!instanceUrl) {
×
46
        return new Response('Missing instance URL', { status: 400 });
×
47
    }
48

49
    // Construct the redirect URL to v1/saml
50
    const redirectUrl = new URL('callosum/v1/saml/login', instanceUrl);
×
51

52

53
    // TODO(shikhar.bhargava): remove this once we have a proper callback URL
54
    // the proper callback URL is the one /callosum/v1/v2/auth/token/authroize endpoint
55
    // which gives the encrypted token to the client. Also with that it will have the 
56
    // redirect URL as query params = new URL("/callback", c.req.url).href to 
57
    // send the user back to callback endpoint.
58
    // The callback endpoint will get the encrypted token and decrypt it to get the user's access token.
59

60
    // const targetURLAuthorize = new URL("callosum/v1/v2/auth/token/authorize", instanceUrl);
61
    // targetURLAuthorize.searchParams.append('validity_time_in_sec', "86400");
62
    // const targetURLCallbackPath = new URL("/callback", c.req.url);
63
    // targetURLCallbackPath.searchParams.append('instanceUrl', instanceUrl);
64
    // targetURLAuthorize.searchParams.append('redirect_url', btoa(targetURLCallbackPath.toString()));
65
    // const encodedState = btoa(JSON.stringify(state.oauthReqInfo));
66
    // targetURLAuthorize.searchParams.append('state', encodedState);
67
    // targetURLAuthorize.searchParams.append('token_encryption_key', "1234567812345678");
68
    // targetURLAuthorize.searchParams.append('encryption_algorithm', 'AES');
69
    // redirectUrl.searchParams.append('targetURLPath', targetURLAuthorize.href);
70

71
    const targetURLPath = new URL("/callback", c.req.url);
×
72
    targetURLPath.searchParams.append('instanceUrl', instanceUrl);
×
73
    const encodedState = encodeBase64Url(new TextEncoder().encode(JSON.stringify(state.oauthReqInfo)).buffer);
×
74
    targetURLPath.searchParams.append('oauthReqInfo', encodedState);
×
75
    redirectUrl.searchParams.append('targetURLPath', targetURLPath.href);
×
76
    console.log("redirectUrl", redirectUrl.toString());
×
77

78
    return Response.redirect(redirectUrl.toString());
×
79
})
80

81
app.get("/callback", async (c) => {
2✔
82

83
    // TODO(shikhar.bhargava): remove this once we have a proper callback URL
84
    // With the proper callback URL, we will get the encrypted token in the query params
85
    // along with it we will get the instanceUrl and the state (oauthReqInfo).
86
    // and we will decrypt the token to get the user's access token and complete the authorization.
87
    // const encodedOauthReqInfo = c.req.query('state');
88

89
    const instanceUrl = c.req.query('instanceUrl');
×
90
    const encodedOauthReqInfo = c.req
×
91
        .query('oauthReqInfo')
92
        // Added as a workaround for https://thoughtspot.atlassian.net/browse/SCAL-258056
93
        ?.replace('/10023.html', '');
94
    if (!instanceUrl) {
×
95
        return c.text('Missing instance URL', 400);
×
96
    }
97
    if (!encodedOauthReqInfo) {
×
98
        return c.text('Missing OAuth request info', 400);
×
99
    }
100

101
    try {
×
102
        const decodedOAuthReqInfo = JSON.parse(new TextDecoder().decode(decodeBase64Url(encodedOauthReqInfo)));
×
103
        return new Response(renderTokenCallback(instanceUrl, decodedOAuthReqInfo), {
×
104
            headers: {
105
                'Content-Type': 'text/html',
106
            },
107
        });
108
    } catch (error) {
109
        console.error('Error decoding OAuth request info:', error);
×
110
        return c.text('Invalid OAuth request info format', 400);
×
111
    }
112
})
113

114
app.post("/store-token", async (c) => {
2✔
115
    const { token, oauthReqInfo, instanceUrl } = await c.req.json();
×
116
    if (!token || !oauthReqInfo || !instanceUrl) {
×
117
        return c.text('Missing token or OAuth request info or instanceUrl', 400);
×
118
    }
119

120
    const { clientId } = oauthReqInfo;
×
121
    const clientName = await c.env.OAUTH_PROVIDER.lookupClient(clientId);
×
122

123
    // Complete the authorization with the provided information
124
    const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
×
125
        request: oauthReqInfo,
126
        userId: "default", // Using a default user ID since username is not required
127
        metadata: {
128
            label: "default",
129
        },
130
        scope: oauthReqInfo.scope,
131
        props: {
132
            accessToken: token.data.token,
133
            instanceUrl: instanceUrl,
134
            clientName: clientName,
135
        } as Props,
136
    });
137

138
    // Add CORS headers to the response
139
    return new Response(JSON.stringify({
×
140
        redirectTo: redirectTo
141
    }), {
142
        status: 200,
143
        headers: {
144
            'Content-Type': 'application/json'
145
        }
146
    });
147
});
148

149
export default app;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc