/** * Entry point for running the MCP server. * Run with: npx mcp-pdf-server * Or: node dist/index.js [--stdio] [pdf-urls...] */ import fs from "node:fs"; import path from "node:path"; import { createMcpExpressApp } from "@modelcontextprotocol/express"; import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import type { McpServer } from "@modelcontextprotocol/server"; import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer, isArxivUrl, isFileUrl, normalizeArxivUrl, pathToFileUrl, fileUrlToPath, allowedLocalFiles, cliLocalFiles, DEFAULT_PDF, allowedLocalDirs, writeFlags, } from "./server.js"; /** * Starts an MCP server with Streamable HTTP transport in stateless mode. */ export async function startStreamableHTTPServer( createServer: () => McpServer, ): Promise { const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); res.on("close", () => { transport.close().catch(() => {}); server.close().catch(() => {}); }); try { await server.connect(transport); await transport.handleRequest(req, res, req.body); } catch (error) { console.error("MCP error:", error); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null, }); } } }); const httpServer = app.listen(port, (err) => { if (err) { console.error("Failed to start server:", err); process.exit(1); } console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { console.log("\nShutting down..."); httpServer.close(() => process.exit(0)); }; process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); } /** * Starts an MCP server with stdio transport. * * @param createServer - Factory function that creates a new McpServer instance. */ export async function startStdioServer( createServer: () => McpServer, ): Promise { await createServer().connect(new StdioServerTransport()); } function parseArgs(): { urls: string[]; stdio: boolean; useClientRoots: boolean; enableInteract: boolean; debug: boolean; } { const args = process.argv.slice(2); const urls: string[] = []; let stdio = false; let useClientRoots = false; let enableInteract = false; let debug = false; for (const arg of args) { if (arg === "--stdio") { stdio = true; } else if (arg === "--use-client-roots") { useClientRoots = true; } else if (arg === "--enable-interact") { // Force-enable interact for HTTP mode. Only use when running a // single long-lived server process (e.g. the e2e test harness) — // the command queue is in-memory per-process, so stateless // multi-instance deployments will drop commands. enableInteract = true; } else if (arg === "--debug") { debug = true; } else if (arg === "--writeable-uploads-root") { // Claude Desktop mounts attachments under a dir root named "uploads"; // by default we refuse to write there. This flag opts back in. writeFlags.allowUploadsRoot = true; } else if (!arg.startsWith("-")) { // Convert local paths to file:// URLs, normalize arxiv URLs let url = arg; if ( !arg.startsWith("http://") && !arg.startsWith("https://") && !arg.startsWith("file://") ) { url = pathToFileUrl(arg); } else if (isArxivUrl(arg)) { url = normalizeArxivUrl(arg); } urls.push(url); } } return { urls: urls.length > 0 ? urls : [DEFAULT_PDF], stdio, useClientRoots, enableInteract, debug, }; } async function main() { const { urls, stdio, useClientRoots, enableInteract, debug } = parseArgs(); // Register local files in whitelist for (const url of urls) { if (isFileUrl(url)) { const filePath = path.resolve(fileUrlToPath(url)); if (fs.existsSync(filePath)) { const s = fs.statSync(filePath); if (s.isFile()) { allowedLocalFiles.add(filePath); cliLocalFiles.add(filePath); console.error(`[pdf-server] Registered local file: ${filePath}`); } else if (s.isDirectory()) { allowedLocalDirs.add(filePath); console.error(`[pdf-server] Registered local directory: ${filePath}`); } } else { console.error(`[pdf-server] Warning: File not found: ${filePath}`); } } } console.error(`[pdf-server] Ready (${urls.length} URL(s) configured)`); if (stdio) { // stdio → client is local (e.g. Claude Desktop), roots are safe await startStdioServer(() => createServer({ enableInteract: true, useClientRoots: true, debug }), ); } else { // HTTP → client is remote, only honour roots with explicit opt-in if (!useClientRoots) { console.error( "[pdf-server] Client roots are ignored (default for remote transports). " + "Pass --use-client-roots to allow the client to expose local directories.", ); } await startStreamableHTTPServer(() => createServer({ useClientRoots, enableInteract, debug }), ); } } main().catch((e) => { console.error(e); process.exit(1); });