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

mongodb-js / mongodb-mcp-server / 16392751341

19 Jul 2025 09:07PM UTC coverage: 80.071%. First build
16392751341

Pull #383

github

web-flow
Merge 097aa049b into 5058fa637
Pull Request #383: feat: add client idle timeout [MCP-57]

535 of 704 branches covered (75.99%)

Branch coverage included in aggregate %.

73 of 116 new or added lines in 7 files covered. (62.93%)

3097 of 3832 relevant lines covered (80.82%)

47.09 hits per line

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

58.39
/src/common/sessionStore.ts
1
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
import logger, { LogId, McpLogger } from "./logger.js";
2✔
4

5
class TimeoutManager {
2✔
6
    private timeoutId: NodeJS.Timeout | undefined;
7
    public onerror?: (error: unknown) => void;
8

9
    constructor(
2✔
10
        private readonly callback: () => Promise<void> | void,
4✔
11
        private readonly timeoutMS: number
4✔
12
    ) {
4✔
13
        if (timeoutMS <= 0) {
4!
NEW
14
            throw new Error("timeoutMS must be greater than 0");
×
NEW
15
        }
×
16
        this.reset();
4✔
17
    }
4✔
18

19
    clear() {
2✔
20
        if (this.timeoutId) {
20✔
21
            clearTimeout(this.timeoutId);
16✔
22
            this.timeoutId = undefined;
16✔
23
        }
16✔
24
    }
20✔
25

26
    private async runCallback() {
2✔
NEW
27
        if (this.callback) {
×
NEW
28
            try {
×
NEW
29
                await this.callback();
×
NEW
30
            } catch (error: unknown) {
×
NEW
31
                this.onerror?.(error);
×
NEW
32
            }
×
NEW
33
        }
×
NEW
34
    }
×
35

36
    reset() {
2✔
37
        this.clear();
16✔
38
        this.timeoutId = setTimeout(() => {
16✔
NEW
39
            void this.runCallback().finally(() => {
×
NEW
40
                this.timeoutId = undefined;
×
NEW
41
            });
×
42
        }, this.timeoutMS);
16✔
43
    }
16✔
44
}
2✔
45

46
export class SessionStore {
2✔
47
    private sessions: {
2✔
48
        [sessionId: string]: {
49
            mcpServer: McpServer;
50
            transport: StreamableHTTPServerTransport;
51
            abortTimeout: TimeoutManager;
52
            notificationTimeout: TimeoutManager;
53
        };
54
    } = {};
2✔
55

56
    constructor(
2✔
57
        private readonly idleTimeoutMS: number,
2✔
58
        private readonly notificationTimeoutMS: number
2✔
59
    ) {
2✔
60
        if (idleTimeoutMS <= 0) {
2!
NEW
61
            throw new Error("idleTimeoutMS must be greater than 0");
×
NEW
62
        }
×
63
        if (notificationTimeoutMS <= 0) {
2!
NEW
64
            throw new Error("notificationTimeoutMS must be greater than 0");
×
NEW
65
        }
×
66
        if (idleTimeoutMS <= notificationTimeoutMS) {
2!
NEW
67
            throw new Error("idleTimeoutMS must be greater than notificationTimeoutMS");
×
NEW
68
        }
×
69
    }
2✔
70

71
    getSession(sessionId: string): StreamableHTTPServerTransport | undefined {
2✔
72
        this.resetTimeout(sessionId);
6✔
73
        return this.sessions[sessionId]?.transport;
6✔
74
    }
6✔
75

76
    private resetTimeout(sessionId: string): void {
2✔
77
        const session = this.sessions[sessionId];
6✔
78
        if (!session) {
6!
NEW
79
            return;
×
NEW
80
        }
×
81

82
        session.abortTimeout.reset();
6✔
83

84
        session.notificationTimeout.reset();
6✔
85
    }
6✔
86

87
    private sendNotification(sessionId: string): void {
2✔
NEW
88
        const session = this.sessions[sessionId];
×
NEW
89
        if (!session) {
×
NEW
90
            return;
×
NEW
91
        }
×
NEW
92
        const logger = new McpLogger(session.mcpServer);
×
NEW
93
        logger.info(
×
NEW
94
            LogId.streamableHttpTransportSessionCloseNotification,
×
NEW
95
            "sessionStore",
×
NEW
96
            "Session is about to be closed due to inactivity"
×
NEW
97
        );
×
NEW
98
    }
×
99

100
    setSession(sessionId: string, transport: StreamableHTTPServerTransport, mcpServer: McpServer): void {
2✔
101
        if (this.sessions[sessionId]) {
2!
102
            throw new Error(`Session ${sessionId} already exists`);
×
103
        }
×
104
        const abortTimeout = new TimeoutManager(async () => {
2✔
NEW
105
            const logger = new McpLogger(mcpServer);
×
NEW
106
            logger.info(
×
NEW
107
                LogId.streamableHttpTransportSessionCloseNotification,
×
NEW
108
                "sessionStore",
×
NEW
109
                "Session closed due to inactivity"
×
NEW
110
            );
×
111

NEW
112
            await this.closeSession(sessionId);
×
113
        }, this.idleTimeoutMS);
2✔
114
        const notificationTimeout = new TimeoutManager(
2✔
115
            () => this.sendNotification(sessionId),
2✔
116
            this.notificationTimeoutMS
2✔
117
        );
2✔
118
        this.sessions[sessionId] = { mcpServer, transport, abortTimeout, notificationTimeout };
2✔
119
    }
2✔
120

121
    async closeSession(sessionId: string, closeTransport: boolean = true): Promise<void> {
2✔
122
        if (!this.sessions[sessionId]) {
2!
123
            throw new Error(`Session ${sessionId} not found`);
×
124
        }
×
125
        this.sessions[sessionId].abortTimeout.clear();
2✔
126
        this.sessions[sessionId].notificationTimeout.clear();
2✔
127
        if (closeTransport) {
2✔
128
            try {
2✔
129
                await this.sessions[sessionId].transport.close();
2✔
130
            } catch (error) {
2!
131
                logger.error(
×
132
                    LogId.streamableHttpTransportSessionCloseFailure,
×
133
                    "streamableHttpTransport",
×
134
                    `Error closing transport ${sessionId}: ${error instanceof Error ? error.message : String(error)}`
×
135
                );
×
136
            }
×
137
        }
2✔
138
        delete this.sessions[sessionId];
2✔
139
    }
2✔
140

141
    async closeAllSessions(): Promise<void> {
2✔
142
        await Promise.all(Object.keys(this.sessions).map((sessionId) => this.closeSession(sessionId)));
2✔
143
    }
2✔
144
}
2✔
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