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

thoughtspot / mcp-server / 16633091239

30 Jul 2025 08:30PM UTC coverage: 92.389% (-0.09%) from 92.477%
16633091239

Pull #54

github

web-flow
Merge 25cca711a into a8f556ab2
Pull Request #54: Png image

194 of 220 branches covered (88.18%)

Branch coverage included in aggregate %.

50 of 58 new or added lines in 7 files covered. (86.21%)

4 existing lines in 2 files now uncovered.

680 of 726 relevant lines covered (93.66%)

140.7 hits per line

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

91.3
/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
    hostName: string;
17
};
18

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

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

30
        super(message);
661✔
31

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

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

44
            // Record the exception in the span
45
            this.span.recordException(this);
351✔
46

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

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

64
        console.error('Error:', this.message);
661✔
65

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

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

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

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

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

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

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

147
    return instrumentDO(Agent, config);
104✔
148
}
149

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

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