Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ The `loggers` configuration option controls where logs are sent. You can specify

**Default:** `disk,mcp` (logs are written to disk and sent to the MCP client).

You can combine multiple loggers, e.g. `--loggers disk,stderr` or `export MDB_MCP_LOGGERS="mcp,stderr"`.
You can combine multiple loggers, e.g. `--loggers disk stderr` or `export MDB_MCP_LOGGERS="mcp,stderr"`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

note: we should probably open a DOCSP ticket to add this to our docs

Copy link
Collaborator Author

Choose a reason for hiding this comment

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


##### Example: Set logger via environment variable

Expand All @@ -335,7 +335,7 @@ export MDB_MCP_LOGGERS="disk,stderr"
##### Example: Set logger via command-line argument

```shell
npx -y mongodb-mcp-server --loggers mcp,stderr
npx -y mongodb-mcp-server --loggers mcp stderr
```

##### Log File Location
Expand Down
2 changes: 1 addition & 1 deletion src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,6 @@ function SNAKE_CASE_toCamelCase(str: string): string {
// Reads the cli args and parses them into a UserConfig object.
function getCliConfig() {
return argv(process.argv.slice(2), {
array: ["disabledTools"],
array: ["disabledTools", "loggers"],
}) as unknown as Partial<UserConfig>;
}
11 changes: 6 additions & 5 deletions src/common/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ export const LogId = {
toolUpdateFailure: mongoLogId(1_005_001),

streamableHttpTransportStarted: mongoLogId(1_006_001),
streamableHttpTransportSessionInitialized: mongoLogId(1_006_002),
streamableHttpTransportRequestFailure: mongoLogId(1_006_003),
streamableHttpTransportCloseRequested: mongoLogId(1_006_004),
streamableHttpTransportCloseSuccess: mongoLogId(1_006_005),
streamableHttpTransportCloseFailure: mongoLogId(1_006_006),
streamableHttpTransportStartFailure: mongoLogId(1_006_002),
streamableHttpTransportSessionInitialized: mongoLogId(1_006_003),
streamableHttpTransportRequestFailure: mongoLogId(1_006_004),
streamableHttpTransportCloseRequested: mongoLogId(1_006_005),
streamableHttpTransportCloseSuccess: mongoLogId(1_006_006),
streamableHttpTransportCloseFailure: mongoLogId(1_006_007),
} as const;

export abstract class LoggerBase {
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ try {
apiClientSecret: config.apiClientSecret,
});

const transport = config.transport === "stdio" ? createStdioTransport() : createHttpTransport();
const transport = config.transport === "stdio" ? createStdioTransport() : await createHttpTransport();

const telemetry = Telemetry.create(session, config);

Expand Down
56 changes: 38 additions & 18 deletions src/transports/streamableHttp.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import express from "express";
import http from "http";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

import { config } from "../common/config.js";
import logger, { LogId } from "../common/logger.js";

const JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED = -32000;

export function createHttpTransport(): StreamableHTTPServerTransport {
export async function createHttpTransport(): Promise<StreamableHTTPServerTransport> {
const app = express();
app.enable("trust proxy"); // needed for reverse proxy support
app.use(express.urlencoded({ extended: true }));
Expand Down Expand Up @@ -76,28 +77,47 @@ export function createHttpTransport(): StreamableHTTPServerTransport {
}
});

const server = app.listen(config.httpPort, config.httpHost, () => {
try {
const server = await new Promise<http.Server>((resolve, reject) => {
const result = app.listen(config.httpPort, config.httpHost, (err?: Error) => {
if (err) {
reject(err);
return;
}
resolve(result);
});
});

logger.info(
LogId.streamableHttpTransportStarted,
"streamableHttpTransport",
`Server started on http://${config.httpHost}:${config.httpPort}`
);
});

transport.onclose = () => {
logger.info(LogId.streamableHttpTransportCloseRequested, "streamableHttpTransport", `Closing server`);
server.close((err?: Error) => {
if (err) {
logger.error(
LogId.streamableHttpTransportCloseFailure,
"streamableHttpTransport",
`Error closing server: ${err.message}`
);
return;
}
logger.info(LogId.streamableHttpTransportCloseSuccess, "streamableHttpTransport", `Server closed`);
});
};
transport.onclose = () => {
logger.info(LogId.streamableHttpTransportCloseRequested, "streamableHttpTransport", `Closing server`);
server.close((err?: Error) => {
if (err) {
logger.error(
LogId.streamableHttpTransportCloseFailure,
"streamableHttpTransport",
`Error closing server: ${err.message}`
);
return;
}
logger.info(LogId.streamableHttpTransportCloseSuccess, "streamableHttpTransport", `Server closed`);
});
};

return transport;
} catch (error: unknown) {
const err = error instanceof Error ? error : new Error(String(error));
logger.info(
LogId.streamableHttpTransportStartFailure,
"streamableHttpTransport",
`Error starting server: ${err.message}`
);

return transport;
throw err;
}
}
77 changes: 77 additions & 0 deletions tests/unit/transports/streamableHttp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { createHttpTransport } from "../../../src/transports/streamableHttp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
describe("streamableHttpTransport", () => {
let transport: StreamableHTTPServerTransport;
const mcpServer = new McpServer({
name: "test",
version: "1.0.0",
});
beforeAll(async () => {
transport = await createHttpTransport();
mcpServer.registerTool(
"hello",
{
title: "Hello Tool",
description: "Say hello",
inputSchema: { name: z.string() },
},
({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }],
})
);
await mcpServer.connect(transport);
});

afterAll(async () => {
await mcpServer.close();
});

describe("client connects successfully", () => {
let client: StreamableHTTPClientTransport;
beforeAll(async () => {
client = new StreamableHTTPClientTransport(new URL("http://127.0.0.1:3000/mcp"));
await client.start();
});

afterAll(async () => {
await client.close();
});

it("handles requests and sends responses", async () => {
client.onmessage = (message: JSONRPCMessage) => {
const messageResult = message as
| {
result?: {
tools: {
name: string;
description: string;
}[];
};
}
| undefined;

expect(message.jsonrpc).toBe("2.0");
expect(messageResult).toBeDefined();
expect(messageResult?.result?.tools).toBeDefined();
expect(messageResult?.result?.tools.length).toBe(1);
expect(messageResult?.result?.tools[0]?.name).toBe("hello");
expect(messageResult?.result?.tools[0]?.description).toBe("Say hello");
};

await client.send({
jsonrpc: "2.0",
id: 1,
method: "tools/list",
params: {
_meta: {
progressToken: 1,
},
},
});
});
});
});
Loading