diff --git a/main.ts b/main.ts index f225d16..f122c62 100644 --- a/main.ts +++ b/main.ts @@ -1,9 +1,24 @@ import { McpServer } from "@modelcontextprotocol/server"; import { serveStdio } from "@modelcontextprotocol/server/stdio"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import { Command } from "commander"; +import http from "node:http"; +import crypto from "node:crypto"; import { registerAuthTools } from "./tools/auth.ts"; import { registerEmailTools } from "./tools/email.ts"; import { registerFolderTools } from "./tools/folders.ts"; +const program = new Command() + .name("exchange-mcp") + .description("Exchange MCP Server — EWS integration via MCP") + .option("--transport ", "Transport: stdio or sse", "stdio") + .option("--token-hash ", "SHA-256 hash of bearer token for HTTP transport auth") + .option("--host ", "HTTP host", "127.0.0.1") + .option("--port ", "HTTP port", "8000"); + +program.parse(process.argv); +const options = program.opts(); + function buildServer(): McpServer { const server = new McpServer({ name: "exchange-mcp", @@ -17,4 +32,62 @@ function buildServer(): McpServer { return server; } -serveStdio(buildServer); \ No newline at end of file +async function main() { + const transportType: string = options.transport; + + if (transportType === "stdio") { + serveStdio(buildServer); + return; + } + + if (transportType === "sse") { + const server = buildServer(); + + const mcpTransport = new NodeStreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + }); + + await server.connect(mcpTransport); + + const httpServer = http.createServer(async (req, res) => { + if (options.tokenHash) { + const auth = req.headers.authorization || ""; + const token = auth.startsWith("Bearer ") ? auth.slice(7) : ""; + const hash = crypto.createHash("sha256").update(token).digest("hex"); + if (hash !== options.tokenHash) { + res.writeHead(401, { "Content-Type": "text/plain" }); + res.end("Unauthorized"); + return; + } + } + + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + + if (req.method === "OPTIONS") { + res.writeHead(200); + res.end(); + return; + } + + await mcpTransport.handleRequest(req, res); + }); + + const port = Number(options.port); + httpServer.listen(port, options.host); + + console.error( + `Exchange MCP Server running via SSE on http://${options.host}:${port}`, + ); + return; + } + + console.error(`Unsupported transport: ${transportType}. Use "stdio" or "sse".`); + process.exit(1); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 742b892..a0dd93b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "exchange-mcp", "version": "0.0.0", "dependencies": { + "@modelcontextprotocol/node": "^2.0.0-beta.2", "@modelcontextprotocol/server": "^2.0.0-beta.2", + "commander": "^15.0.0", "fast-xml-parser": "^5.2.0", "httpntlm": "^1.8.13", "zod": "^4.4.3" @@ -461,6 +463,39 @@ "node": ">=18" } }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/node": { + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0-beta.2.tgz", + "integrity": "sha512-1ZV1t98ZpwsoiSpIxKOxZCB2qMhDkh4XMQIWhQVDsCG2pVyhSqduxsFiwMd1R0333+KzJ+ELY3RIR+iEvM9A+g==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.0.0-beta.2", + "hono": "^4.11.4" + }, + "peerDependenciesMeta": { + "hono": { + "optional": true + } + } + }, "node_modules/@modelcontextprotocol/server": { "version": "2.0.0-beta.2", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.2.tgz", @@ -507,6 +542,15 @@ ], "license": "MIT" }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/des.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", @@ -613,6 +657,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/hono": { + "version": "4.12.28", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.28.tgz", + "integrity": "sha512-YwUvVpSF7m1yOblFPrU3Hbo8XhPheBoiyfGuII6z19LnOr6JpDnyyp7LFNrfV56wS8tpvtBFGRISHN02pDdLOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/httpntlm": { "version": "1.8.13", "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.8.13.tgz", diff --git a/package.json b/package.json index a39c2e4..c2baf3b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "format": "dprint fmt" }, "dependencies": { + "@modelcontextprotocol/node": "^2.0.0-beta.2", "@modelcontextprotocol/server": "^2.0.0-beta.2", + "commander": "^15.0.0", "fast-xml-parser": "^5.2.0", "httpntlm": "^1.8.13", "zod": "^4.4.3"