ext-apps/examples/pdf-server/main.ts
Olivier Chafik 023f1fd233
fix: misc fixes for pdf-server, map-server, and say-server (#329)
* fix(map-server): simplify location update message format

* fix(say-server): pass host to streamable_http_app for non-localhost deployments

* refactor(pdf-server): simplify to stateless range-query architecture

- Remove caching/indexing in favor of direct HTTP Range requests
- Use HEAD requests to get file size (no full download)
- Use Range requests to proxy byte chunks (no caching)
- Local files: fs.read with offset/length
- Remote files: HTTP Range header
- Zero memory footprint (no PDF caching)
- Truly stateless (no session management needed)
- Single server.ts (~350 lines) + main.ts CLI entry point
- Instant startup (no preloading)
- Security: whitelist local files, allowed origins

Removed files:
- src/pdf-indexer.ts
- src/pdf-loader.ts
- src/types.ts

* feat(pdf-server): add dark mode support

- Add CSS variables with light-dark() function for automatic theme switching
- Sync theme with host via applyDocumentTheme()
- Apply host CSS variables via applyHostStyleVariables()

* feat(pdf-server): add list_pdfs tool back

Lists local files and allowed remote origins.
Also moves CHUNK_SIZE to module level constant.

* feat(pdf-server): improve tool descriptions and increase context limit

- display_pdf: list all allowed domains in description
- Increase MAX_MODEL_CONTEXT_LENGTH to 15000
- Move CHUNK_SIZE to module level constant

* chore(say-server): remove deployment docs from repo

Keep server.py fix for non-localhost deployments.
Deployment docs kept locally but not in repo.

* refactor(pdf-server): simplify updateModelContext format

- Add widget ID for multi-widget disambiguation
- Use concise header: 'PDF viewer (id) | title | Current Page: X/Y'
- Add 'Page content:' prefix before text
- Keep selection markers (<pdf-selection>...</pdf-selection>)

* refactor: rename widgetUUID to viewUUID for consistency

Aligns with main's standardization on 'view' terminology (PR #325).
Also dynamically generate allowed domains in display_pdf description.

* viewUUID

* fix(map-server): fix syntax error in location update message

* fix(pdf-server): restore package.json, improve server docs

- Restore --external pdfjs-dist in build
- Rewrite server.ts header for newcomers (list tools, not implementation details)
- Minimize main.ts diff

* chore(pdf-server): remove unused addAllowedOrigin/addAllowedLocalFile

* docs(pdf-server): update README for new architecture

* docs(pdf-server): restore full README, add theming section, remove get_pdf_info

* rm

* fix(pdf-server): restore createMcpExpressApp and error handling

- Use createMcpExpressApp for DNS rebinding protection
- Restore error callback in app.listen()

* fix(say-server): pass host to streamable_http_app for non-localhost deployments

* chore(say-server): remove deployment docs from repo

* widgetId->tool id

* fix(map-server): add null check for center in location update

chore(pdf-server): dedupe and sort allowed origins

* fix(map-server): use transparent background for rounded corners

* prettier
2026-01-23 12:35:07 +00:00

145 lines
3.8 KiB
TypeScript

/**
* 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 { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import {
createServer,
isArxivUrl,
isFileUrl,
normalizeArxivUrl,
pathToFileUrl,
fileUrlToPath,
allowedLocalFiles,
allowedRemoteOrigins,
DEFAULT_PDF,
} from "./server.js";
export interface ServerOptions {
port: number;
name?: string;
}
/**
* Starts an MCP server with Streamable HTTP transport in stateless mode.
*/
export async function startServer(
createServer: () => McpServer,
options: ServerOptions,
): Promise<void> {
const { port, name = "MCP Server" } = options;
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 StreamableHTTPServerTransport({
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(`${name} 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);
}
function parseArgs(): { urls: string[]; stdio: boolean } {
const args = process.argv.slice(2);
const urls: string[] = [];
let stdio = false;
for (const arg of args) {
if (arg === "--stdio") {
stdio = 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 };
}
async function main() {
const { urls, stdio } = parseArgs();
// Register local files in whitelist
for (const url of urls) {
if (isFileUrl(url)) {
const filePath = fileUrlToPath(url);
if (fs.existsSync(filePath)) {
allowedLocalFiles.add(filePath);
console.error(`[pdf-server] Registered local file: ${filePath}`);
} else {
console.error(`[pdf-server] Warning: File not found: ${filePath}`);
}
}
}
console.error(`[pdf-server] Ready (${urls.length} URL(s) configured)`);
console.error(
`[pdf-server] Allowed origins: ${[...allowedRemoteOrigins].join(", ")}`,
);
if (stdio) {
await createServer().connect(new StdioServerTransport());
} else {
const port = parseInt(process.env.PORT ?? "3120", 10);
await startServer(createServer, { port, name: "PDF Server" });
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});