Skip to content

Commit db23253

Browse files
authored
refactor: move http server (#377)
1 parent 17b149c commit db23253

File tree

9 files changed

+304
-198
lines changed

9 files changed

+304
-198
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"check:types": "tsc --noEmit --project tsconfig.json",
3030
"reformat": "prettier --write .",
3131
"generate": "./scripts/generate.sh",
32-
"test": "vitest --coverage"
32+
"test": "vitest --run --coverage"
3333
},
3434
"license": "Apache-2.0",
3535
"devDependencies": {

src/index.ts

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,23 @@
11
#!/usr/bin/env node
22

33
import logger, { LogId } from "./common/logger.js";
4-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
54
import { config } from "./common/config.js";
6-
import { Session } from "./common/session.js";
7-
import { Server } from "./server.js";
8-
import { packageInfo } from "./common/packageInfo.js";
9-
import { Telemetry } from "./telemetry/telemetry.js";
10-
import { createStdioTransport } from "./transports/stdio.js";
11-
import { createHttpTransport } from "./transports/streamableHttp.js";
5+
import { StdioRunner } from "./transports/stdio.js";
6+
import { StreamableHttpRunner } from "./transports/streamableHttp.js";
127

13-
try {
14-
const session = new Session({
15-
apiBaseUrl: config.apiBaseUrl,
16-
apiClientId: config.apiClientId,
17-
apiClientSecret: config.apiClientSecret,
18-
});
19-
20-
const transport = config.transport === "stdio" ? createStdioTransport() : await createHttpTransport();
21-
22-
const telemetry = Telemetry.create(session, config);
23-
24-
const mcpServer = new McpServer({
25-
name: packageInfo.mcpServerName,
26-
version: packageInfo.version,
27-
});
28-
29-
const server = new Server({
30-
mcpServer,
31-
session,
32-
telemetry,
33-
userConfig: config,
34-
});
8+
async function main() {
9+
const transportRunner = config.transport === "stdio" ? new StdioRunner() : new StreamableHttpRunner();
3510

3611
const shutdown = () => {
3712
logger.info(LogId.serverCloseRequested, "server", `Server close requested`);
3813

39-
server
14+
transportRunner
4015
.close()
4116
.then(() => {
42-
logger.info(LogId.serverClosed, "server", `Server closed successfully`);
4317
process.exit(0);
4418
})
45-
.catch((err: unknown) => {
46-
const error = err instanceof Error ? err : new Error(String(err));
47-
logger.error(LogId.serverCloseFailure, "server", `Error closing server: ${error.message}`);
19+
.catch((error: unknown) => {
20+
logger.error(LogId.serverCloseFailure, "server", `Error closing server: ${error as string}`);
4821
process.exit(1);
4922
});
5023
};
@@ -54,8 +27,22 @@ try {
5427
process.once("SIGTERM", shutdown);
5528
process.once("SIGQUIT", shutdown);
5629

57-
await server.connect(transport);
58-
} catch (error: unknown) {
30+
try {
31+
await transportRunner.start();
32+
} catch (error: unknown) {
33+
logger.emergency(LogId.serverStartFailure, "server", `Fatal error running server: ${error as string}`);
34+
try {
35+
await transportRunner.close();
36+
logger.error(LogId.serverClosed, "server", "Server closed");
37+
} catch (error: unknown) {
38+
logger.error(LogId.serverCloseFailure, "server", `Error closing server: ${error as string}`);
39+
} finally {
40+
process.exit(1);
41+
}
42+
}
43+
}
44+
45+
main().catch((error: unknown) => {
5946
logger.emergency(LogId.serverStartFailure, "server", `Fatal error running server: ${error as string}`);
6047
process.exit(1);
61-
}
48+
});

src/transports/base.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { config } from "../common/config.js";
2+
import { packageInfo } from "../common/packageInfo.js";
3+
import { Server } from "../server.js";
4+
import { Session } from "../common/session.js";
5+
import { Telemetry } from "../telemetry/telemetry.js";
6+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7+
8+
export abstract class TransportRunnerBase {
9+
protected setupServer(): Server {
10+
const session = new Session({
11+
apiBaseUrl: config.apiBaseUrl,
12+
apiClientId: config.apiClientId,
13+
apiClientSecret: config.apiClientSecret,
14+
});
15+
16+
const telemetry = Telemetry.create(session, config);
17+
18+
const mcpServer = new McpServer({
19+
name: packageInfo.mcpServerName,
20+
version: packageInfo.version,
21+
});
22+
23+
return new Server({
24+
mcpServer,
25+
session,
26+
telemetry,
27+
userConfig: config,
28+
});
29+
}
30+
31+
abstract start(): Promise<void>;
32+
33+
abstract close(): Promise<void>;
34+
}

src/transports/stdio.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import logger, { LogId } from "../common/logger.js";
2+
import { Server } from "../server.js";
3+
import { TransportRunnerBase } from "./base.js";
14
import { JSONRPCMessage, JSONRPCMessageSchema } from "@modelcontextprotocol/sdk/types.js";
25
import { EJSON } from "bson";
36
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -45,3 +48,24 @@ export function createStdioTransport(): StdioServerTransport {
4548

4649
return server;
4750
}
51+
52+
export class StdioRunner extends TransportRunnerBase {
53+
private server: Server | undefined;
54+
55+
async start() {
56+
try {
57+
this.server = this.setupServer();
58+
59+
const transport = createStdioTransport();
60+
61+
await this.server.connect(transport);
62+
} catch (error: unknown) {
63+
logger.emergency(LogId.serverStartFailure, "server", `Fatal error running server: ${error as string}`);
64+
process.exit(1);
65+
}
66+
}
67+
68+
async close(): Promise<void> {
69+
await this.server?.close();
70+
}
71+
}

src/transports/streamableHttp.ts

Lines changed: 75 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,92 @@
11
import express from "express";
22
import http from "http";
33
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4-
4+
import { TransportRunnerBase } from "./base.js";
55
import { config } from "../common/config.js";
66
import logger, { LogId } from "../common/logger.js";
77

88
const JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED = -32000;
9+
const JSON_RPC_ERROR_CODE_METHOD_NOT_ALLOWED = -32601;
910

10-
export async function createHttpTransport(): Promise<StreamableHTTPServerTransport> {
11-
const app = express();
12-
app.enable("trust proxy"); // needed for reverse proxy support
13-
app.use(express.urlencoded({ extended: true }));
14-
app.use(express.json());
11+
function promiseHandler(
12+
fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise<void>
13+
) {
14+
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
15+
fn(req, res, next).catch(next);
16+
};
17+
}
1518

16-
const transport = new StreamableHTTPServerTransport({
17-
sessionIdGenerator: undefined,
18-
});
19+
export class StreamableHttpRunner extends TransportRunnerBase {
20+
private httpServer: http.Server | undefined;
1921

20-
app.post("/mcp", async (req: express.Request, res: express.Response) => {
21-
try {
22-
await transport.handleRequest(req, res, req.body);
23-
} catch (error) {
24-
logger.error(
25-
LogId.streamableHttpTransportRequestFailure,
26-
"streamableHttpTransport",
27-
`Error handling request: ${error instanceof Error ? error.message : String(error)}`
28-
);
29-
res.status(400).json({
30-
jsonrpc: "2.0",
31-
error: {
32-
code: JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED,
33-
message: `failed to handle request`,
34-
data: error instanceof Error ? error.message : String(error),
35-
},
36-
});
37-
}
38-
});
22+
async start() {
23+
const app = express();
24+
app.enable("trust proxy"); // needed for reverse proxy support
25+
app.use(express.urlencoded({ extended: true }));
26+
app.use(express.json());
27+
28+
app.post(
29+
"/mcp",
30+
promiseHandler(async (req: express.Request, res: express.Response) => {
31+
const transport = new StreamableHTTPServerTransport({
32+
sessionIdGenerator: undefined,
33+
});
34+
35+
const server = this.setupServer();
3936

40-
app.get("/mcp", async (req: express.Request, res: express.Response) => {
41-
try {
42-
await transport.handleRequest(req, res, req.body);
43-
} catch (error) {
44-
logger.error(
45-
LogId.streamableHttpTransportRequestFailure,
46-
"streamableHttpTransport",
47-
`Error handling request: ${error instanceof Error ? error.message : String(error)}`
48-
);
49-
res.status(400).json({
37+
await server.connect(transport);
38+
39+
res.on("close", () => {
40+
Promise.all([transport.close(), server.close()]).catch((error: unknown) => {
41+
logger.error(
42+
LogId.streamableHttpTransportCloseFailure,
43+
"streamableHttpTransport",
44+
`Error closing server: ${error instanceof Error ? error.message : String(error)}`
45+
);
46+
});
47+
});
48+
49+
try {
50+
await transport.handleRequest(req, res, req.body);
51+
} catch (error) {
52+
logger.error(
53+
LogId.streamableHttpTransportRequestFailure,
54+
"streamableHttpTransport",
55+
`Error handling request: ${error instanceof Error ? error.message : String(error)}`
56+
);
57+
res.status(400).json({
58+
jsonrpc: "2.0",
59+
error: {
60+
code: JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED,
61+
message: `failed to handle request`,
62+
data: error instanceof Error ? error.message : String(error),
63+
},
64+
});
65+
}
66+
})
67+
);
68+
69+
app.get("/mcp", (req: express.Request, res: express.Response) => {
70+
res.status(405).json({
5071
jsonrpc: "2.0",
5172
error: {
52-
code: JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED,
53-
message: `failed to handle request`,
54-
data: error instanceof Error ? error.message : String(error),
73+
code: JSON_RPC_ERROR_CODE_METHOD_NOT_ALLOWED,
74+
message: `method not allowed`,
5575
},
5676
});
57-
}
58-
});
77+
});
5978

60-
app.delete("/mcp", async (req: express.Request, res: express.Response) => {
61-
try {
62-
await transport.handleRequest(req, res, req.body);
63-
} catch (error) {
64-
logger.error(
65-
LogId.streamableHttpTransportRequestFailure,
66-
"streamableHttpTransport",
67-
`Error handling request: ${error instanceof Error ? error.message : String(error)}`
68-
);
69-
res.status(400).json({
79+
app.delete("/mcp", (req: express.Request, res: express.Response) => {
80+
res.status(405).json({
7081
jsonrpc: "2.0",
7182
error: {
72-
code: JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED,
73-
message: `failed to handle request`,
74-
data: error instanceof Error ? error.message : String(error),
83+
code: JSON_RPC_ERROR_CODE_METHOD_NOT_ALLOWED,
84+
message: `method not allowed`,
7585
},
7686
});
77-
}
78-
});
87+
});
7988

80-
try {
81-
const server = await new Promise<http.Server>((resolve, reject) => {
89+
this.httpServer = await new Promise<http.Server>((resolve, reject) => {
8290
const result = app.listen(config.httpPort, config.httpHost, (err?: Error) => {
8391
if (err) {
8492
reject(err);
@@ -93,31 +101,17 @@ export async function createHttpTransport(): Promise<StreamableHTTPServerTranspo
93101
"streamableHttpTransport",
94102
`Server started on http://${config.httpHost}:${config.httpPort}`
95103
);
104+
}
96105

97-
transport.onclose = () => {
98-
logger.info(LogId.streamableHttpTransportCloseRequested, "streamableHttpTransport", `Closing server`);
99-
server.close((err?: Error) => {
106+
async close(): Promise<void> {
107+
await new Promise<void>((resolve, reject) => {
108+
this.httpServer?.close((err) => {
100109
if (err) {
101-
logger.error(
102-
LogId.streamableHttpTransportCloseFailure,
103-
"streamableHttpTransport",
104-
`Error closing server: ${err.message}`
105-
);
110+
reject(err);
106111
return;
107112
}
108-
logger.info(LogId.streamableHttpTransportCloseSuccess, "streamableHttpTransport", `Server closed`);
113+
resolve();
109114
});
110-
};
111-
112-
return transport;
113-
} catch (error: unknown) {
114-
const err = error instanceof Error ? error : new Error(String(error));
115-
logger.info(
116-
LogId.streamableHttpTransportStartFailure,
117-
"streamableHttpTransport",
118-
`Error starting server: ${err.message}`
119-
);
120-
121-
throw err;
115+
});
122116
}
123117
}

0 commit comments

Comments
 (0)