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

mongodb-js / mongodb-mcp-server / 16377180973

18 Jul 2025 05:59PM UTC coverage: 80.518%. First build
16377180973

Pull #383

github

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

533 of 703 branches covered (75.82%)

Branch coverage included in aggregate %.

70 of 97 new or added lines in 7 files covered. (72.16%)

3104 of 3814 relevant lines covered (81.38%)

47.28 hits per line

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

65.22
/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
export class SessionStore {
2✔
6
    private sessions: {
2✔
7
        [sessionId: string]: {
8
            mcpServer: McpServer;
9
            transport: StreamableHTTPServerTransport;
10
            abortController: AbortController;
11
            abortTimeoutId: NodeJS.Timeout;
12
            notificationTimeoutId: NodeJS.Timeout;
13
        };
14
    } = {};
2✔
15

16
    constructor(
2✔
17
        private readonly idleTimeoutMS: number,
2✔
18
        private readonly notificationTimeoutMS: number
2✔
19
    ) {
2✔
20
        if (idleTimeoutMS <= 0) {
2!
NEW
21
            throw new Error("idleTimeoutMS must be greater than 0");
×
NEW
22
        }
×
23
        if (notificationTimeoutMS <= 0) {
2!
NEW
24
            throw new Error("notificationTimeoutMS must be greater than 0");
×
NEW
25
        }
×
26
        if (idleTimeoutMS <= notificationTimeoutMS) {
2!
NEW
27
            throw new Error("idleTimeoutMS must be greater than notificationTimeoutMS");
×
NEW
28
        }
×
29
    }
2✔
30

31
    getSession(sessionId: string): StreamableHTTPServerTransport | undefined {
2✔
32
        this.resetTimeout(sessionId);
6✔
33
        return this.sessions[sessionId]?.transport;
6✔
34
    }
6✔
35

36
    private resetTimeout(sessionId: string): void {
2✔
37
        const session = this.sessions[sessionId];
6✔
38
        if (!session) {
6!
NEW
39
            return;
×
NEW
40
        }
×
41

42
        if (session.abortTimeoutId) {
6✔
43
            clearTimeout(session.abortTimeoutId);
6✔
44
        }
6✔
45
        const abortTimeoutId = setTimeout(() => {
6✔
NEW
46
            session.abortController.abort();
×
47
        }, this.idleTimeoutMS);
6✔
48
        session.abortTimeoutId = abortTimeoutId;
6✔
49

50
        if (session.notificationTimeoutId) {
6✔
51
            clearTimeout(session.notificationTimeoutId);
6✔
52
        }
6✔
53
        const notificationTimeoutId = setTimeout(() => {
6✔
NEW
54
            this.sendNotification(sessionId);
×
55
        }, this.notificationTimeoutMS);
6✔
56
        session.notificationTimeoutId = notificationTimeoutId;
6✔
57
    }
6✔
58

59
    private sendNotification(sessionId: string): void {
2✔
NEW
60
        const session = this.sessions[sessionId];
×
NEW
61
        if (!session) {
×
NEW
62
            return;
×
NEW
63
        }
×
NEW
64
        const logger = new McpLogger(session.mcpServer);
×
NEW
65
        logger.info(
×
NEW
66
            LogId.streamableHttpTransportSessionCloseNotification,
×
NEW
67
            "sessionStore",
×
NEW
68
            "Session is about to be closed due to inactivity"
×
NEW
69
        );
×
NEW
70
    }
×
71

72
    setSession(sessionId: string, transport: StreamableHTTPServerTransport, mcpServer: McpServer): void {
2✔
73
        if (this.sessions[sessionId]) {
2!
74
            throw new Error(`Session ${sessionId} already exists`);
×
75
        }
×
76
        const abortController = new AbortController();
2✔
77
        const abortTimeoutId = setTimeout(() => {
2✔
NEW
78
            abortController.abort();
×
79
        }, this.idleTimeoutMS);
2✔
80
        const notificationTimeoutId = setTimeout(() => {
2✔
NEW
81
            this.sendNotification(sessionId);
×
82
        }, this.notificationTimeoutMS);
2✔
83
        this.sessions[sessionId] = { mcpServer, transport, abortController, abortTimeoutId, notificationTimeoutId };
2✔
84
        abortController.signal.onabort = async () => {
2✔
85
            await this.closeSession(sessionId);
2✔
86
        };
2✔
87
    }
2✔
88

89
    async closeSession(sessionId: string, closeTransport: boolean = true): Promise<void> {
2✔
90
        if (!this.sessions[sessionId]) {
2!
91
            throw new Error(`Session ${sessionId} not found`);
×
92
        }
×
93
        clearTimeout(this.sessions[sessionId].abortTimeoutId);
2✔
94
        clearTimeout(this.sessions[sessionId].notificationTimeoutId);
2✔
95
        if (closeTransport) {
2✔
96
            try {
2✔
97
                const logger = new McpLogger(this.sessions[sessionId].mcpServer);
2✔
98
                logger.info(
2✔
99
                    LogId.streamableHttpTransportSessionCloseNotification,
2✔
100
                    "sessionStore",
2✔
101
                    "Session closed, please reconnect"
2✔
102
                );
2✔
103
                await this.sessions[sessionId].transport.close();
2✔
104
            } catch (error) {
2!
105
                logger.error(
×
106
                    LogId.streamableHttpTransportSessionCloseFailure,
×
107
                    "streamableHttpTransport",
×
108
                    `Error closing transport ${sessionId}: ${error instanceof Error ? error.message : String(error)}`
×
109
                );
×
110
            }
×
111
        }
2✔
112
        delete this.sessions[sessionId];
2✔
113
    }
2✔
114

115
    async closeAllSessions(): Promise<void> {
2✔
116
        await Promise.all(Object.values(this.sessions).map((session) => session.abortController.abort()));
2✔
117
        this.sessions = {};
2✔
118
    }
2✔
119
}
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

© 2026 Coveralls, Inc