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

thoughtspot / mcp-server / 16607548209

29 Jul 2025 09:11PM UTC coverage: 90.536% (-1.2%) from 91.706%
16607548209

Pull #54

github

web-flow
Merge 188b5de85 into 586b0d878
Pull Request #54: Png image

190 of 220 branches covered (86.36%)

Branch coverage included in aggregate %.

56 of 70 new or added lines in 7 files covered. (80.0%)

1 existing line in 1 file now uncovered.

671 of 731 relevant lines covered (91.79%)

137.93 hits per line

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

84.06
/src/utils.ts
1
import { type Span, SpanStatusCode } from '@opentelemetry/api';
2
import { getActiveSpan } from './metrics/tracing/tracing-utils';
3
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
import { McpAgent } from 'agents/mcp';
5
import type { BaseMCPServer, Context } from './servers/mcp-server-base';
6
import { instrumentDO, type ResolveConfigFn } from '@microlabs/otel-cf-workers';
7

8
export type Props = {
9
    accessToken: string;
10
    instanceUrl: string;
11
    clientName: {
12
        clientId: string;
13
        clientName: string;
14
        registrationDate: number;
15
    };
16
};
17

18
export class McpServerError extends Error {
19
    public readonly span?: Span;
20
    public readonly errorJson: any;
21
    public readonly statusCode: number;
22

23
    constructor(errorJson: any, statusCode: number) {
24
        // Extract message from error JSON or use a default message
25
        const message = typeof errorJson === 'string'
486✔
26
            ? errorJson
27
            : errorJson?.message || errorJson?.error || 'Unknown error occurred';
476✔
28

29
        super(message);
486✔
30

31
        this.name = 'McpServerError';
486✔
32
        this.span = getActiveSpan();
486✔
33
        this.errorJson = errorJson;
486✔
34
        this.statusCode = statusCode;
486✔
35

36
        // Set span status if span is provided
37
        if (this.span) {
486✔
38
            this.span.setStatus({
331✔
39
                code: SpanStatusCode.ERROR,
40
                message: this.message
41
            });
42

43
            // Record the exception in the span
44
            this.span.recordException(this);
331✔
45

46
            // Add error details as span attributes
47
            if (typeof errorJson === 'object' && errorJson !== null) {
331✔
48
                // Add relevant error details to span attributes
49
                if (errorJson.code) {
321✔
50
                    this.span.setAttribute('error.code', errorJson.code);
5✔
51
                }
52
                if (errorJson.type) {
321✔
53
                    this.span.setAttribute('error.type', errorJson.type);
5✔
54
                }
55
                if (errorJson.details) {
321✔
56
                    this.span.setAttribute('error.details', JSON.stringify(errorJson.details));
37✔
57
                }
58
            }
59

60
            this.span.setAttribute('error.status_code', this.statusCode);
331✔
61
        }
62

63
        console.error('Error:', this.message);
486✔
64

65
        // Ensure proper prototype chain for instanceof checks
66
        Object.setPrototypeOf(this, McpServerError.prototype);
486✔
67
    }
68

69
    /**
70
     * Convert the error to a JSON representation
71
     */
72
    toJSON() {
73
        return {
10✔
74
            name: this.name,
75
            message: this.message,
76
            statusCode: this.statusCode,
77
            errorJson: this.errorJson,
78
            stack: this.stack
79
        };
80
    }
81

82
    /**
83
     * Get a user-friendly error message
84
     */
85
    getUserMessage(): string {
86
        if (typeof this.errorJson === 'object' && this.errorJson?.userMessage) {
25✔
87
            return this.errorJson.userMessage;
5✔
88
        }
89
        return this.message;
20✔
90
    }
91
}
92

93
export function instrumentedMCPServer<T extends BaseMCPServer>(MCPServer: new (ctx: Context) => T, config: ResolveConfigFn) {
94
    const Agent = class extends McpAgent<Env, any, Props> {
56✔
95
        private _server: T | undefined;
96
        private _env: Env;
97

98
        // Lazy getter for server that creates it when first accessed
99
        // 
100
        // WHY THIS APPROACH:
101
        // Originally we passed 'this' directly as Context: `server = new MCPServer(this)`
102
        // This worked when Context was just { props: Props }, but broke when we added env.
103
        // 
104
        // PROBLEMS WITH ORIGINAL APPROACH:
105
        // 1. McpAgent's 'env' property is protected, but Context expects public
106
        // 2. TypeScript error: "Property 'env' is protected but public in Context"
107
        // 
108
        // WHY NOT CONSTRUCTOR CREATION:
109
        // We tried creating server in constructor: `new MCPServer({ props: this.props, env })`
110
        // But this.props is undefined during constructor - it gets set later by McpAgent._init()
111
        // Runtime error: "Cannot read properties of undefined (reading 'instanceUrl')"
112
        // 
113
        // SOLUTION - LAZY INITIALIZATION:
114
        // - Store env from constructor (available immediately)
115
        // - Create server only when first accessed (after props are set by McpAgent lifecycle)
116
        // - Combine both props and env into proper Context object
117
        get server(): T {
NEW
118
            if (!this._server) {
×
NEW
119
                const context: Context = {
×
120
                    props: this.props, // Available after McpAgent._init() sets it
121
                    env: this._env     // Stored from constructor
122
                };
NEW
123
                this._server = new MCPServer(context);
×
124
            }
NEW
125
            return this._server;
×
126
        }
127

128
        // Argument of type 'typeof ThoughtSpotMCPWrapper' is not assignable to parameter of type 'DOClass'.
129
        // Cannot assign a 'protected' constructor type to a 'public' constructor type.
130
        // Created to satisfy the DOClass type.
131
        // biome-ignore lint/complexity/noUselessConstructor: required for DOClass
132
        public constructor(state: DurableObjectState, env: Env) {
133
            super(state, env);
16✔
134
            // Store env for later use - props aren't available yet in constructor
135
            // McpAgent lifecycle: constructor → _init(props) → init()
136
            this._env = env;
16✔
137
        }
138

139
        async init() {
140
            // Access the server property to trigger lazy initialization
141
            // At this point, props have been set by McpAgent._init()
UNCOV
142
            await this.server.init();
×
143
        }
144
    }
145

146
    return instrumentDO(Agent, config);
56✔
147
}
148

149
export async function putInKV(key: string, value: any, env: Env) {
NEW
150
    if (env?.OAUTH_KV) {
×
NEW
151
        await env.OAUTH_KV.put(key, JSON.stringify(value), {
×
152
            expirationTtl: 60 * 60 * 3 // 3 hours
153
        });
154
    }
155
}
156

157
export async function getFromKV(key: string, env: Env) {
158
    console.log("[DEBUG] Getting from KV", key);
144✔
159
    if (env?.OAUTH_KV) {
144✔
160
        const value = await env.OAUTH_KV.get(key, { type: "json" });
128✔
161
        if (value) {
112✔
162
            return value;
96✔
163
        }
164
        return null;
16✔
165
    }
166
}
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

© 2026 Coveralls, Inc