Files
exchange-mcp/main.ts
T
2026-07-09 13:40:52 +00:00

143 lines
4.0 KiB
TypeScript

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { Command } from "commander";
import crypto from "node:crypto";
import http from "node:http";
import { initDataDir } from "./ews_client.ts";
import { registerAuthTools } from "./tools/auth.ts";
import { registerAvailabilityTools } from "./tools/availability.ts";
import { registerCalendarTools } from "./tools/calendar.ts";
import { registerEmailTools } from "./tools/email.ts";
import { registerFolderTools } from "./tools/folders.ts";
import { registerPeopleTools } from "./tools/people.ts";
const program = new Command()
.name("exchange-mcp")
.description("Exchange MCP Server — EWS integration via MCP")
.option("--transport <type>", "Transport: stdio or sse", "stdio")
.option(
"--token-hash <hash>",
"SHA-256 hash of bearer token for HTTP transport auth",
)
.option("--host <host>", "HTTP host", "127.0.0.1")
.option("--port <port>", "HTTP port", "8000")
.option("--data-dir <path>", "Data directory for config.json", "./data");
program.parse(process.argv);
const options = program.opts();
initDataDir(options.dataDir);
function buildServer(): McpServer {
const server = new McpServer({
name: "exchange-mcp",
version: "1.0.0",
});
registerAuthTools(server);
registerEmailTools(server);
registerFolderTools(server);
registerCalendarTools(server);
registerAvailabilityTools(server);
registerPeopleTools(server);
return server;
}
async function runStdio() {
const server = buildServer();
const transport = new StdioServerTransport();
await server.connect(transport);
}
async function runSSE() {
const transports = new Map<string, SSEServerTransport>();
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;
}
const url = new URL(req.url || "/", `http://${req.headers.host}`);
if (req.method === "GET") {
const transport = new SSEServerTransport("/sse", res);
const server = buildServer();
await server.connect(transport);
transports.set(transport.sessionId, transport);
res.on("close", () => {
transports.delete(transport.sessionId);
});
return;
}
if (req.method === "POST" && url.pathname === "/sse") {
const sessionId = url.searchParams.get("sessionId") || "";
const transport = transports.get(sessionId);
if (!transport) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Session not found");
return;
}
await transport.handlePostMessage(req, res);
return;
}
res.writeHead(404);
res.end("Not found");
});
const port = Number(options.port);
httpServer.listen(port, options.host);
console.error(
`Exchange MCP Server running via SSE on http://${options.host}:${port}`,
);
}
async function main() {
const transportType: string = options.transport;
switch (transportType) {
case "stdio":
await runStdio();
break;
case "sse":
await runSSE();
break;
default:
console.error(
`Unsupported transport: "${transportType}". Use "stdio" or "sse".`,
);
process.exit(1);
}
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});