Skip to content

Commit 34b7a78

Browse files
committed
refactor: move http server
1 parent 17b149c commit 34b7a78

File tree

6 files changed

+238
-175
lines changed

6 files changed

+238
-175
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: 28 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,45 @@
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-
});
8+
async function main() {
9+
const runner = config.transport === "stdio" ? new StdioRunner() : new StreamableHttpRunner();
1910

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-
});
35-
36-
const shutdown = () => {
11+
const shutdown = async () => {
3712
logger.info(LogId.serverCloseRequested, "server", `Server close requested`);
3813

39-
server
40-
.close()
41-
.then(() => {
42-
logger.info(LogId.serverClosed, "server", `Server closed successfully`);
43-
process.exit(0);
44-
})
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}`);
48-
process.exit(1);
49-
});
14+
try {
15+
const exitCode = await runner.close();
16+
process.exit(exitCode);
17+
} catch (error: unknown) {
18+
logger.error(LogId.serverCloseFailure, "server", `Error closing server: ${error as string}`);
19+
process.exit(1);
20+
}
5021
};
5122

5223
process.once("SIGINT", shutdown);
5324
process.once("SIGABRT", shutdown);
5425
process.once("SIGTERM", shutdown);
5526
process.once("SIGQUIT", shutdown);
5627

57-
await server.connect(transport);
58-
} catch (error: unknown) {
28+
try {
29+
await runner.run();
30+
} catch (error: unknown) {
31+
logger.emergency(LogId.serverStartFailure, "server", `Fatal error running server: ${error as string}`);
32+
try {
33+
await runner.close();
34+
} catch (error: unknown) {
35+
logger.error(LogId.serverCloseFailure, "server", `Error closing server: ${error as string}`);
36+
} finally {
37+
process.exit(1);
38+
}
39+
}
40+
}
41+
42+
main().catch((error: unknown) => {
5943
logger.emergency(LogId.serverStartFailure, "server", `Fatal error running server: ${error as string}`);
6044
process.exit(1);
61-
}
45+
});

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 Runner {
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 run(): Promise<void>;
32+
33+
abstract close(): Promise<number>;
34+
}

src/transports/stdio.ts

Lines changed: 34 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 { Runner } 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,34 @@ export function createStdioTransport(): StdioServerTransport {
4548

4649
return server;
4750
}
51+
52+
export class StdioRunner extends Runner {
53+
private server: Server | undefined;
54+
55+
async run() {
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<number> {
69+
logger.info(LogId.serverCloseRequested, "server", `Server close requested`);
70+
71+
try {
72+
await this.server?.close();
73+
logger.info(LogId.serverClosed, "server", `Server closed successfully`);
74+
return 0;
75+
} catch (error: unknown) {
76+
const err = error instanceof Error ? error : new Error(String(error));
77+
logger.error(LogId.serverCloseFailure, "server", `Error closing server: ${err.message}`);
78+
return 1;
79+
}
80+
}
81+
}

src/transports/streamableHttp.ts

Lines changed: 103 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,109 @@
11
import express from "express";
22
import http from "http";
33
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4-
4+
import { Runner } 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 Runner {
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 run() {
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());
3927

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({
50-
jsonrpc: "2.0",
51-
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),
55-
},
56-
});
57-
}
58-
});
28+
app.post(
29+
"/mcp",
30+
promiseHandler(async (req: express.Request, res: express.Response) => {
31+
const transport = new StreamableHTTPServerTransport({
32+
sessionIdGenerator: undefined,
33+
});
5934

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({
70-
jsonrpc: "2.0",
71-
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),
75-
},
76-
});
77-
}
78-
});
35+
const server = this.setupServer();
36+
37+
await server.connect(transport);
38+
39+
res.on("close", async () => {
40+
try {
41+
await transport.close();
42+
} catch (error: unknown) {
43+
logger.error(
44+
LogId.streamableHttpTransportCloseFailure,
45+
"streamableHttpTransport",
46+
`Error closing transport: ${error instanceof Error ? error.message : String(error)}`
47+
);
48+
}
49+
try {
50+
await server.close();
51+
} catch (error: unknown) {
52+
logger.error(
53+
LogId.streamableHttpTransportCloseFailure,
54+
"streamableHttpTransport",
55+
`Error closing server: ${error instanceof Error ? error.message : String(error)}`
56+
);
57+
}
58+
});
59+
60+
try {
61+
await transport.handleRequest(req, res, req.body);
62+
} catch (error) {
63+
logger.error(
64+
LogId.streamableHttpTransportRequestFailure,
65+
"streamableHttpTransport",
66+
`Error handling request: ${error instanceof Error ? error.message : String(error)}`
67+
);
68+
res.status(400).json({
69+
jsonrpc: "2.0",
70+
error: {
71+
code: JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED,
72+
message: `failed to handle request`,
73+
data: error instanceof Error ? error.message : String(error),
74+
},
75+
});
76+
}
77+
})
78+
);
7979

80-
try {
81-
const server = await new Promise<http.Server>((resolve, reject) => {
80+
app.get(
81+
"/mcp",
82+
promiseHandler(async (req: express.Request, res: express.Response) => {
83+
res.status(405).json({
84+
jsonrpc: "2.0",
85+
error: {
86+
code: JSON_RPC_ERROR_CODE_METHOD_NOT_ALLOWED,
87+
message: `method not allowed`,
88+
},
89+
});
90+
})
91+
);
92+
93+
app.delete(
94+
"/mcp",
95+
promiseHandler(async (req: express.Request, res: express.Response) => {
96+
res.status(405).json({
97+
jsonrpc: "2.0",
98+
error: {
99+
code: JSON_RPC_ERROR_CODE_METHOD_NOT_ALLOWED,
100+
message: `method not allowed`,
101+
},
102+
});
103+
})
104+
);
105+
106+
this.httpServer = await new Promise<http.Server>((resolve, reject) => {
82107
const result = app.listen(config.httpPort, config.httpHost, (err?: Error) => {
83108
if (err) {
84109
reject(err);
@@ -93,31 +118,16 @@ export async function createHttpTransport(): Promise<StreamableHTTPServerTranspo
93118
"streamableHttpTransport",
94119
`Server started on http://${config.httpHost}:${config.httpPort}`
95120
);
121+
}
96122

97-
transport.onclose = () => {
98-
logger.info(LogId.streamableHttpTransportCloseRequested, "streamableHttpTransport", `Closing server`);
99-
server.close((err?: Error) => {
100-
if (err) {
101-
logger.error(
102-
LogId.streamableHttpTransportCloseFailure,
103-
"streamableHttpTransport",
104-
`Error closing server: ${err.message}`
105-
);
106-
return;
107-
}
108-
logger.info(LogId.streamableHttpTransportCloseSuccess, "streamableHttpTransport", `Server closed`);
109-
});
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;
123+
async close(): Promise<number> {
124+
try {
125+
await this.httpServer?.close();
126+
return 0;
127+
} catch (error: unknown) {
128+
const err = error instanceof Error ? error : new Error(String(error));
129+
logger.error(LogId.serverCloseFailure, "server", `Error closing server: ${err.message}`);
130+
return 1;
131+
}
122132
}
123133
}

0 commit comments

Comments
 (0)