Skip to content

feat: add streamable http [MCP-42] #361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

feat: add streamable http [MCP-42] #361

wants to merge 10 commits into from

Conversation

fmenezes
Copy link
Collaborator

@fmenezes fmenezes commented Jul 14, 2025

Proposed changes

Introduces Streamable HTTP transport; No authentication or authorization was implemented.

Note: this is a feature branch, every single individual change was approved on each PR.

Checklist

@coveralls
Copy link
Collaborator

coveralls commented Jul 14, 2025

Pull Request Test Coverage Report for Build 16422846497

Details

  • 238 of 402 (59.2%) changed or added relevant lines in 10 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-1.6%) to 80.243%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/server.ts 24 36 66.67%
src/transports/stdio.ts 8 21 38.1%
src/index.ts 0 28 0.0%
src/common/sessionStore.ts 51 92 55.43%
src/transports/streamableHttp.ts 88 158 55.7%
Totals Coverage Status
Change from base Build 16373544917: -1.6%
Covered Lines: 3099
Relevant Lines: 3826

💛 - Coveralls

@fmenezes fmenezes changed the title feat: add streamable http [MCP-43] feat: add streamable http [MCP-42] Jul 15, 2025
@fmenezes fmenezes marked this pull request as ready for review July 21, 2025 10:59
@Copilot Copilot AI review requested due to automatic review settings July 21, 2025 10:59
@fmenezes fmenezes requested a review from a team as a code owner July 21, 2025 10:59
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces streamable HTTP transport functionality to the MongoDB MCP Server, allowing it to run as an HTTP server in addition to the existing stdio transport. The implementation includes session management with timeout handling and configurable logging options.

  • Adds StreamableHttpRunner with Express-based HTTP server and session management
  • Refactors transport layer with base class and configurable runner selection
  • Introduces comprehensive timeout management for HTTP sessions with cleanup

Reviewed Changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/transports/streamableHttp.ts New HTTP transport implementation with session management and Express server
src/transports/stdio.ts Refactored stdio transport into runner pattern with base class
src/transports/base.ts Abstract base class for transport runners
src/common/timeoutManager.ts New timeout management utility for session cleanup
src/common/sessionStore.ts HTTP session storage and lifecycle management
src/common/config.ts Enhanced configuration with HTTP and logging options
src/server.ts Refactored server initialization and validation logic
src/index.ts Simplified main entry point with transport runner selection
tests/integration/transports/streamableHttp.test.ts Integration tests for HTTP transport
tests/integration/transports/stdio.test.ts Integration tests for stdio transport
tests/unit/common/timeoutManager.test.ts Unit tests for timeout manager

Comment on lines 43 to 48
if (this.callback) {
try {
await this.callback();
} catch (error: unknown) {
this.onerror?.(error);
}
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runCallback method checks if 'this.callback' exists, but the callback is marked as 'readonly' and provided in the constructor, so this check is unnecessary and adds complexity.

Suggested change
if (this.callback) {
try {
await this.callback();
} catch (error: unknown) {
this.onerror?.(error);
}
try {
await this.callback();
} catch (error: unknown) {
this.onerror?.(error);

Copilot uses AI. Check for mistakes.

Comment on lines 52 to 53
const logger = new McpLogger(session.mcpServer);
logger.info(
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a new McpLogger instance for each notification is inefficient. Consider creating the logger once and reusing it, or passing it as a dependency.

Suggested change
const logger = new McpLogger(session.mcpServer);
logger.info(
session.logger.info(

Copilot uses AI. Check for mistakes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, we should create a logger and store it inside the our session map

Comment on lines 65 to 66
const logger = new McpLogger(mcpServer);
logger.info(
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a new McpLogger instance inside the timeout callback is inefficient and could be problematic if called multiple times. Consider creating the logger once and reusing it.

Suggested change
const logger = new McpLogger(mcpServer);
logger.info(
this.sessions[sessionId].logger.info(

Copilot uses AI. Check for mistakes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree

@blva blva requested a review from gagik July 21, 2025 11:02
Copy link
Collaborator

@gagik gagik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to be focused on some technical writing today but just some initial review on first part of the changes

private sendNotification(sessionId: string): void {
const session = this.sessions[sessionId];
if (!session) {
return;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe we should log a warning here?

Comment on lines 52 to 53
const logger = new McpLogger(session.mcpServer);
logger.info(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, we should create a logger and store it inside the our session map

Comment on lines 65 to 66
const logger = new McpLogger(mcpServer);
logger.info(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree

Comment on lines 53 to 55
* Resets the timeout.
*/
reset() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Resets the timeout.
*/
reset() {
* Restarts the timeout.
*/
restart() {

Just to better differentiate from clear as it's not clear that the initial state has the timeout started already

* Runs the callback function.
*/
private async runCallback() {
if (this.callback) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.callback is always defined right? this is not needed

if (timeoutMS <= 0) {
throw new Error("timeoutMS must be greater than 0");
}
this.reset();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should not have side-effects in constructors. I think this whole helper can be better represented as a single function.

type ManagedTimeout = {
     cancel: () => void;
     restart: () => void;
};

export function setManagedTimeout(
        callback: () => Promise<void> | void,
        timeoutMS: number,
        { onError }: { onError?: (error: unknown) => void }
) {
      const wrappedCallback = () => {
            try {
                await this.callback();
            } catch (error: unknown) {
                onError?.(error);
            }
      };
      let timeout = setTimeout(wrappedCallback, timeoutMS);
      const cancel = () => {
           cancelTimeout(timeout);
           timeout = undefined;
      };
      const restart = () => {
           cancel();
           timeout = setTimeout(wrappedCallback, timeoutMS);
      }
      return {
           cancel,
           restart
      };
}

@@ -186,6 +189,35 @@ export class Server {
}

private async validateConfig(): Promise<void> {
const transport = this.userConfig.transport as string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these type casts needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is the way I've found to validate it.

The problem here is as far as we are concerned, the possible values are predefined, so the literal types make sense, but the inputs are cli args or env vars, which are basically strings that can contain any values.

fmenezes added a commit that referenced this pull request Jul 21, 2025
@fmenezes fmenezes mentioned this pull request Jul 21, 2025
1 task
@fmenezes
Copy link
Collaborator Author

fmenezes commented Jul 21, 2025

@gagik I'm addressing all of these at #386, if you're happy with the changes, would you approve it?

thanks!

@fmenezes
Copy link
Collaborator Author

@gagik I've merged the PR according to your comments, would take another look?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants