From 317276bfa4d732eaf15dec64924ba7f7c179f5d7 Mon Sep 17 00:00:00 2001 From: GeassV Date: Fri, 26 Sep 2025 16:05:40 +0800 Subject: [PATCH 1/2] rewrite with sse protocol --- perplexity-ask/dist/index.js | 300 ++++++++ perplexity-ask/dist/sse-server.js | 203 ++++++ perplexity-ask/dist/sse-transport.js | 125 ++++ perplexity-ask/index.ts | 12 +- perplexity-ask/package-lock.json | 993 ++++++++++++++++++++++++++- perplexity-ask/package.json | 6 +- perplexity-ask/sse-server.ts | 212 ++++++ perplexity-ask/test-client.html | 223 ++++++ 8 files changed, 2055 insertions(+), 19 deletions(-) create mode 100755 perplexity-ask/dist/index.js create mode 100755 perplexity-ask/dist/sse-server.js create mode 100755 perplexity-ask/dist/sse-transport.js create mode 100644 perplexity-ask/sse-server.ts create mode 100644 perplexity-ask/test-client.html diff --git a/perplexity-ask/dist/index.js b/perplexity-ask/dist/index.js new file mode 100755 index 0000000..029511b --- /dev/null +++ b/perplexity-ask/dist/index.js @@ -0,0 +1,300 @@ +#!/usr/bin/env node +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; +import { createSSEServer } from "./sse-server.js"; +/** + * Definition of the Perplexity Ask Tool. + * This tool accepts an array of messages and returns a chat completion response + * from the Perplexity API, with citations appended to the message if provided. + */ +const PERPLEXITY_ASK_TOOL = { + name: "perplexity_ask", + description: "Engages in a conversation using the Sonar API. " + + "Accepts an array of messages (each with a role and content) " + + "and returns a ask completion response from the Perplexity model.", + inputSchema: { + type: "object", + properties: { + messages: { + type: "array", + items: { + type: "object", + properties: { + role: { + type: "string", + description: "Role of the message (e.g., system, user, assistant)", + }, + content: { + type: "string", + description: "The content of the message", + }, + }, + required: ["role", "content"], + }, + description: "Array of conversation messages", + }, + }, + required: ["messages"], + }, +}; +/** + * Definition of the Perplexity Research Tool. + * This tool performs deep research queries using the Perplexity API. + */ +const PERPLEXITY_RESEARCH_TOOL = { + name: "perplexity_research", + description: "Performs deep research using the Perplexity API. " + + "Accepts an array of messages (each with a role and content) " + + "and returns a comprehensive research response with citations.", + inputSchema: { + type: "object", + properties: { + messages: { + type: "array", + items: { + type: "object", + properties: { + role: { + type: "string", + description: "Role of the message (e.g., system, user, assistant)", + }, + content: { + type: "string", + description: "The content of the message", + }, + }, + required: ["role", "content"], + }, + description: "Array of conversation messages", + }, + }, + required: ["messages"], + }, +}; +/** + * Definition of the Perplexity Reason Tool. + * This tool performs reasoning queries using the Perplexity API. + */ +const PERPLEXITY_REASON_TOOL = { + name: "perplexity_reason", + description: "Performs reasoning tasks using the Perplexity API. " + + "Accepts an array of messages (each with a role and content) " + + "and returns a well-reasoned response using the sonar-reasoning-pro model.", + inputSchema: { + type: "object", + properties: { + messages: { + type: "array", + items: { + type: "object", + properties: { + role: { + type: "string", + description: "Role of the message (e.g., system, user, assistant)", + }, + content: { + type: "string", + description: "The content of the message", + }, + }, + required: ["role", "content"], + }, + description: "Array of conversation messages", + }, + }, + required: ["messages"], + }, +}; +// Retrieve the Perplexity API key from environment variables +const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY; +if (!PERPLEXITY_API_KEY) { + console.error("Error: PERPLEXITY_API_KEY environment variable is required"); + process.exit(1); +} +/** + * Performs a chat completion by sending a request to the Perplexity API. + * Appends citations to the returned message content if they exist. + * + * @param {Array<{ role: string; content: string }>} messages - An array of message objects. + * @param {string} model - The model to use for the completion. + * @returns {Promise} The chat completion result with appended citations. + * @throws Will throw an error if the API request fails. + */ +function performChatCompletion(messages_1) { + return __awaiter(this, arguments, void 0, function* (messages, model = "sonar-pro") { + // Construct the API endpoint URL and request body + const url = new URL("https://api.perplexity.ai/chat/completions"); + const body = { + model: model, // Model identifier passed as parameter + messages: messages, + // Additional parameters can be added here if required (e.g., max_tokens, temperature, etc.) + // See the Sonar API documentation for more details: + // https://docs.perplexity.ai/api-reference/chat-completions + }; + let response; + try { + response = yield fetch(url.toString(), { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${PERPLEXITY_API_KEY}`, + }, + body: JSON.stringify(body), + }); + } + catch (error) { + throw new Error(`Network error while calling Perplexity API: ${error}`); + } + // Check for non-successful HTTP status + if (!response.ok) { + let errorText; + try { + errorText = yield response.text(); + } + catch (parseError) { + errorText = "Unable to parse error response"; + } + throw new Error(`Perplexity API error: ${response.status} ${response.statusText}\n${errorText}`); + } + // Attempt to parse the JSON response from the API + let data; + try { + data = yield response.json(); + } + catch (jsonError) { + throw new Error(`Failed to parse JSON response from Perplexity API: ${jsonError}`); + } + // Directly retrieve the main message content from the response + let messageContent = data.choices[0].message.content; + // If citations are provided, append them to the message content + if (data.citations && Array.isArray(data.citations) && data.citations.length > 0) { + messageContent += "\n\nCitations:\n"; + data.citations.forEach((citation, index) => { + messageContent += `[${index + 1}] ${citation}\n`; + }); + } + return messageContent; + }); +} +// Initialize the server with tool metadata and capabilities +const server = new Server({ + name: "example-servers/perplexity-ask", + version: "0.1.0", +}, { + capabilities: { + tools: {}, + }, +}); +/** + * Registers a handler for listing available tools. + * When the client requests a list of tools, this handler returns all available Perplexity tools. + */ +server.setRequestHandler(ListToolsRequestSchema, () => __awaiter(void 0, void 0, void 0, function* () { + return ({ + tools: [PERPLEXITY_ASK_TOOL, PERPLEXITY_RESEARCH_TOOL, PERPLEXITY_REASON_TOOL], + }); +})); +/** + * Registers a handler for calling a specific tool. + * Processes requests by validating input and invoking the appropriate tool. + * + * @param {object} request - The incoming tool call request. + * @returns {Promise} The response containing the tool's result or an error. + */ +server.setRequestHandler(CallToolRequestSchema, (request) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { name, arguments: args } = request.params; + if (!args) { + throw new Error("No arguments provided"); + } + switch (name) { + case "perplexity_ask": { + if (!Array.isArray(args.messages)) { + throw new Error("Invalid arguments for perplexity_ask: 'messages' must be an array"); + } + // Invoke the chat completion function with the provided messages + const messages = args.messages; + const result = yield performChatCompletion(messages, "sonar-pro"); + return { + content: [{ type: "text", text: result }], + isError: false, + }; + } + case "perplexity_research": { + if (!Array.isArray(args.messages)) { + throw new Error("Invalid arguments for perplexity_research: 'messages' must be an array"); + } + // Invoke the chat completion function with the provided messages using the deep research model + const messages = args.messages; + const result = yield performChatCompletion(messages, "sonar-deep-research"); + return { + content: [{ type: "text", text: result }], + isError: false, + }; + } + case "perplexity_reason": { + if (!Array.isArray(args.messages)) { + throw new Error("Invalid arguments for perplexity_reason: 'messages' must be an array"); + } + // Invoke the chat completion function with the provided messages using the reasoning model + const messages = args.messages; + const result = yield performChatCompletion(messages, "sonar-reasoning-pro"); + return { + content: [{ type: "text", text: result }], + isError: false, + }; + } + default: + // Respond with an error if an unknown tool is requested + return { + content: [{ type: "text", text: `Unknown tool: ${name}` }], + isError: true, + }; + } + } + catch (error) { + // Return error details in the response + return { + content: [ + { + type: "text", + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } +})); +/** + * Initializes and runs the server using SSE for communication. + * Logs an error and exits if the server fails to start. + */ +function runServer() { + return __awaiter(this, void 0, void 0, function* () { + try { + const PORT = process.env.PORT || 3001; + const sseServer = createSSEServer(server); + sseServer.listen(PORT, () => { + console.error(`Perplexity MCP Server running on SSE and listening on port ${PORT} with Ask, Research, and Reason tools`); + }); + } + catch (error) { + console.error("Fatal error running server:", error); + process.exit(1); + } + }); +} +// Start the server and catch any startup errors +runServer().catch((error) => { + console.error("Fatal error running server:", error); + process.exit(1); +}); diff --git a/perplexity-ask/dist/sse-server.js b/perplexity-ask/dist/sse-server.js new file mode 100755 index 0000000..671086b --- /dev/null +++ b/perplexity-ask/dist/sse-server.js @@ -0,0 +1,203 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +import express from 'express'; +import cors from 'cors'; +export function createSSEServer(mcpServer) { + const app = express(); + const clients = new Map(); + app.use(cors({ + origin: '*', + methods: ['GET', 'POST', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'Cache-Control', 'X-Client-ID'], + })); + app.use(express.json()); + // SSE endpoint for clients to receive messages + app.get('/events', (req, res) => { + const clientId = req.query.clientId || `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'Cache-Control, X-Client-ID', + }); + clients.set(clientId, res); + // Send initial connection message + res.write(`data: ${JSON.stringify({ type: 'connected', clientId })}\n\n`); + // Keep connection alive with periodic heartbeat + const heartbeat = setInterval(() => { + if (clients.has(clientId)) { + try { + res.write(`data: ${JSON.stringify({ type: 'heartbeat', timestamp: Date.now() })}\n\n`); + } + catch (error) { + console.error(`Heartbeat failed for client ${clientId}:`, error); + clearInterval(heartbeat); + clients.delete(clientId); + } + } + else { + clearInterval(heartbeat); + } + }, 30000); + // Handle client disconnect + req.on('close', () => { + clearInterval(heartbeat); + clients.delete(clientId); + console.error(`Client ${clientId} disconnected`); + }); + req.on('error', (error) => { + console.error(`Client ${clientId} error:`, error); + clearInterval(heartbeat); + clients.delete(clientId); + }); + }); + // Endpoint for clients to send JSON-RPC requests + app.post('/message', (req, res) => __awaiter(this, void 0, void 0, function* () { + try { + const message = req.body; + console.error(`Received message:`, JSON.stringify(message, null, 2)); + // Handle JSON-RPC request + if ('method' in message && 'id' in message) { + const request = message; + try { + let response; + if (request.method === 'tools/list') { + // Manually call the handler we registered in the main server + const handlers = mcpServer._requestHandlers; + const listHandler = handlers.get('tools/list'); + if (listHandler) { + response = yield listHandler({ + method: 'tools/list', + params: request.params || {} + }); + } + else { + throw new Error('No handler registered for tools/list'); + } + } + else if (request.method === 'tools/call') { + const handlers = mcpServer._requestHandlers; + const callHandler = handlers.get('tools/call'); + if (callHandler) { + response = yield callHandler({ + method: 'tools/call', + params: request.params + }); + } + else { + throw new Error('No handler registered for tools/call'); + } + } + else { + throw new Error(`Unknown method: ${request.method}`); + } + const jsonrpcResponse = { + jsonrpc: '2.0', + id: request.id, + result: response + }; + // Send response to the specific client that made the request + const clientId = req.headers['x-client-id']; + const responseData = JSON.stringify(jsonrpcResponse); + if (clientId && clients.has(clientId)) { + try { + clients.get(clientId).write(`data: ${responseData}\n\n`); + } + catch (error) { + console.error(`Error sending to client ${clientId}:`, error); + clients.delete(clientId); + } + } + else { + // If specific client not found, send to all clients + clients.forEach((client, cId) => { + try { + client.write(`data: ${responseData}\n\n`); + } + catch (error) { + console.error(`Error sending to client ${cId}:`, error); + clients.delete(cId); + } + }); + } + } + catch (error) { + console.error('Error processing request:', error); + // Send error response + const errorResponse = { + jsonrpc: '2.0', + id: request.id, + error: { + code: -32603, + message: error instanceof Error ? error.message : 'Internal error', + data: error instanceof Error ? error.stack : undefined + } + }; + const responseData = JSON.stringify(errorResponse); + const clientId = req.headers['x-client-id']; + if (clientId && clients.has(clientId)) { + try { + clients.get(clientId).write(`data: ${responseData}\n\n`); + } + catch (sendError) { + console.error(`Error sending error to client ${clientId}:`, sendError); + clients.delete(clientId); + } + } + else { + // If specific client not found, send to all clients + clients.forEach((client, cId) => { + try { + client.write(`data: ${responseData}\n\n`); + } + catch (sendError) { + console.error(`Error sending error to client ${cId}:`, sendError); + clients.delete(cId); + } + }); + } + } + } + res.status(200).json({ status: 'received' }); + } + catch (error) { + console.error('Error processing message:', error); + res.status(400).json({ error: 'Invalid message format' }); + } + })); + // Health check endpoint + app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + connectedClients: clients.size, + endpoints: { + events: '/events', + message: '/message', + health: '/health' + } + }); + }); + // Root endpoint with basic info + app.get('/', (req, res) => { + res.json({ + name: 'Perplexity MCP SSE Server', + version: '1.0.0', + endpoints: { + events: '/events - SSE endpoint for receiving responses', + message: '/message - POST endpoint for sending JSON-RPC requests', + health: '/health - Health check endpoint' + }, + connectedClients: clients.size + }); + }); + return app; +} diff --git a/perplexity-ask/dist/sse-transport.js b/perplexity-ask/dist/sse-transport.js new file mode 100755 index 0000000..4d3a9aa --- /dev/null +++ b/perplexity-ask/dist/sse-transport.js @@ -0,0 +1,125 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +import express from 'express'; +import cors from 'cors'; +export class SSEServerTransport { + constructor(port = 3001) { + this.clients = new Map(); + this.messageHandlers = []; + this.port = port; + this.app = express(); + this.setupRoutes(); + } + setupRoutes() { + this.app.use(cors({ + origin: '*', + methods: ['GET', 'POST', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'Cache-Control'], + })); + this.app.use(express.json()); + // SSE endpoint for receiving responses + this.app.get('/events', (req, res) => { + const clientId = req.query.clientId || `client_${Date.now()}`; + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'Cache-Control', + }); + this.clients.set(clientId, res); + // Send initial connection message + res.write(`data: ${JSON.stringify({ type: 'connected', clientId })}\n\n`); + // Handle client disconnect + req.on('close', () => { + this.clients.delete(clientId); + }); + }); + // Endpoint for receiving JSON-RPC messages from clients + this.app.post('/message', (req, res) => { + try { + const message = req.body; + // Notify all registered message handlers + this.messageHandlers.forEach(handler => { + try { + handler(message); + } + catch (error) { + console.error('Error in message handler:', error); + } + }); + res.status(200).json({ status: 'received' }); + } + catch (error) { + console.error('Error processing message:', error); + res.status(400).json({ error: 'Invalid message format' }); + } + }); + } + start() { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => { + try { + this.server = this.app.listen(this.port, () => { + console.error(`SSE server running on port ${this.port}`); + resolve(); + }); + } + catch (error) { + reject(error); + } + }); + }); + } + close() { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve) => { + if (this.server) { + this.server.close(() => { + resolve(); + }); + } + else { + resolve(); + } + }); + }); + } + send(message) { + return new Promise((resolve) => { + const messageData = JSON.stringify(message); + // Send message to all connected clients + this.clients.forEach((client, clientId) => { + try { + client.write(`data: ${messageData}\n\n`); + } + catch (error) { + console.error(`Error sending message to client ${clientId}:`, error); + this.clients.delete(clientId); + } + }); + resolve(); + }); + } + onMessage(handler) { + this.messageHandlers.push(handler); + } + onClose(handler) { + // Handle server close events if needed + process.on('SIGINT', handler); + process.on('SIGTERM', handler); + } + onError(handler) { + // Handle server errors + if (this.server) { + this.server.on('error', handler); + } + } +} diff --git a/perplexity-ask/index.ts b/perplexity-ask/index.ts index f5e5d6d..d4b288e 100644 --- a/perplexity-ask/index.ts +++ b/perplexity-ask/index.ts @@ -1,12 +1,12 @@ #!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; +import { createSSEServer } from "./sse-server.js"; /** * Definition of the Perplexity Ask Tool. @@ -289,14 +289,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }); /** - * Initializes and runs the server using standard I/O for communication. + * Initializes and runs the server using SSE for communication. * Logs an error and exits if the server fails to start. */ async function runServer() { try { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Perplexity MCP Server running on stdio with Ask, Research, and Reason tools"); + const PORT = process.env.PORT || 3001; + const sseServer = createSSEServer(server); + sseServer.listen(PORT, () => { + console.error(`Perplexity MCP Server running on SSE and listening on port ${PORT} with Ask, Research, and Reason tools`); + }); } catch (error) { console.error("Fatal error running server:", error); process.exit(1); diff --git a/perplexity-ask/package-lock.json b/perplexity-ask/package-lock.json index f8e1c2d..4f0857e 100644 --- a/perplexity-ask/package-lock.json +++ b/perplexity-ask/package-lock.json @@ -1,23 +1,32 @@ { - "name": "@modelcontextprotocol/server-perplexity-ask", + "name": "server-perplexity-ask", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@modelcontextprotocol/server-perplexity-ask", + "name": "server-perplexity-ask", "version": "0.1.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.1" + "@modelcontextprotocol/sdk": "^1.0.1", + "axios": "^1.6.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2" }, "bin": { "mcp-server-perplexity-ask": "dist/index.js" }, "devDependencies": { - "@types/node": "^22", + "@types/cors": "^2.8.19", + "@types/express": "^4.17.17", + "@types/node": "^20", "shx": "^0.3.4", "typescript": "^5.6.2" + }, + "engines": { + "node": ">=18" } }, "node_modules/@modelcontextprotocol/sdk": { @@ -31,14 +40,158 @@ "zod": "^3.23.8" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "22.13.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", - "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "version": "20.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", + "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" } }, "node_modules/balanced-match": { @@ -48,6 +201,57 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -68,6 +272,47 @@ "node": ">= 0.8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -75,6 +320,18 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -84,6 +341,52 @@ "node": ">= 0.6" } }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -93,6 +396,235 @@ "node": ">= 0.8" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -104,12 +636,48 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -132,11 +700,49 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -201,6 +807,15 @@ "node": ">= 0.10" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -217,6 +832,75 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -240,6 +924,54 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -250,6 +982,15 @@ "wrappy": "1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -267,6 +1008,55 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/raw-body": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", @@ -315,12 +1105,86 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -362,6 +1226,78 @@ "node": ">=6" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -393,6 +1329,19 @@ "node": ">=0.6" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.8.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", @@ -408,9 +1357,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -423,6 +1372,24 @@ "node": ">= 0.8" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/perplexity-ask/package.json b/perplexity-ask/package.json index 489e647..217189a 100644 --- a/perplexity-ask/package.json +++ b/perplexity-ask/package.json @@ -34,9 +34,13 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.0.1", "axios": "^1.6.2", - "dotenv": "^16.3.1" + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2" }, "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^4.17.17", "@types/node": "^20", "shx": "^0.3.4", "typescript": "^5.6.2" diff --git a/perplexity-ask/sse-server.ts b/perplexity-ask/sse-server.ts new file mode 100644 index 0000000..2a70eae --- /dev/null +++ b/perplexity-ask/sse-server.ts @@ -0,0 +1,212 @@ +import express from 'express'; +import cors from 'cors'; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { + JSONRPCMessage, + JSONRPCRequest, + ListToolsRequestSchema, + CallToolRequestSchema +} from "@modelcontextprotocol/sdk/types.js"; + +export function createSSEServer(mcpServer: Server) { + const app = express(); + const clients = new Map(); + + app.use(cors({ + origin: '*', + methods: ['GET', 'POST', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'Cache-Control', 'X-Client-ID'], + })); + + app.use(express.json()); + + // SSE endpoint for clients to receive messages + app.get('/events', (req, res) => { + const clientId = req.query.clientId as string || `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'Cache-Control, X-Client-ID', + }); + + clients.set(clientId, res); + + // Send initial connection message + res.write(`data: ${JSON.stringify({ type: 'connected', clientId })}\n\n`); + + // Keep connection alive with periodic heartbeat + const heartbeat = setInterval(() => { + if (clients.has(clientId)) { + try { + res.write(`data: ${JSON.stringify({ type: 'heartbeat', timestamp: Date.now() })}\n\n`); + } catch (error) { + console.error(`Heartbeat failed for client ${clientId}:`, error); + clearInterval(heartbeat); + clients.delete(clientId); + } + } else { + clearInterval(heartbeat); + } + }, 30000); + + // Handle client disconnect + req.on('close', () => { + clearInterval(heartbeat); + clients.delete(clientId); + console.error(`Client ${clientId} disconnected`); + }); + + req.on('error', (error) => { + console.error(`Client ${clientId} error:`, error); + clearInterval(heartbeat); + clients.delete(clientId); + }); + }); + + // Endpoint for clients to send JSON-RPC requests + app.post('/message', async (req, res) => { + try { + const message = req.body as JSONRPCMessage; + console.error(`Received message:`, JSON.stringify(message, null, 2)); + + // Handle JSON-RPC request + if ('method' in message && 'id' in message) { + const request = message as JSONRPCRequest; + + try { + let response; + + if (request.method === 'tools/list') { + // Manually call the handler we registered in the main server + const handlers = (mcpServer as any)._requestHandlers; + const listHandler = handlers.get('tools/list'); + if (listHandler) { + response = await listHandler({ + method: 'tools/list', + params: request.params || {} + }); + } else { + throw new Error('No handler registered for tools/list'); + } + } else if (request.method === 'tools/call') { + const handlers = (mcpServer as any)._requestHandlers; + const callHandler = handlers.get('tools/call'); + if (callHandler) { + response = await callHandler({ + method: 'tools/call', + params: request.params + }); + } else { + throw new Error('No handler registered for tools/call'); + } + } else { + throw new Error(`Unknown method: ${request.method}`); + } + + const jsonrpcResponse = { + jsonrpc: '2.0', + id: request.id, + result: response + }; + + // Send response to the specific client that made the request + const clientId = req.headers['x-client-id'] as string; + const responseData = JSON.stringify(jsonrpcResponse); + + if (clientId && clients.has(clientId)) { + try { + clients.get(clientId)!.write(`data: ${responseData}\n\n`); + } catch (error) { + console.error(`Error sending to client ${clientId}:`, error); + clients.delete(clientId); + } + } else { + // If specific client not found, send to all clients + clients.forEach((client, cId) => { + try { + client.write(`data: ${responseData}\n\n`); + } catch (error) { + console.error(`Error sending to client ${cId}:`, error); + clients.delete(cId); + } + }); + } + + } catch (error) { + console.error('Error processing request:', error); + // Send error response + const errorResponse = { + jsonrpc: '2.0', + id: request.id, + error: { + code: -32603, + message: error instanceof Error ? error.message : 'Internal error', + data: error instanceof Error ? error.stack : undefined + } + }; + + const responseData = JSON.stringify(errorResponse); + const clientId = req.headers['x-client-id'] as string; + + if (clientId && clients.has(clientId)) { + try { + clients.get(clientId)!.write(`data: ${responseData}\n\n`); + } catch (sendError) { + console.error(`Error sending error to client ${clientId}:`, sendError); + clients.delete(clientId); + } + } else { + // If specific client not found, send to all clients + clients.forEach((client, cId) => { + try { + client.write(`data: ${responseData}\n\n`); + } catch (sendError) { + console.error(`Error sending error to client ${cId}:`, sendError); + clients.delete(cId); + } + }); + } + } + } + + res.status(200).json({ status: 'received' }); + + } catch (error) { + console.error('Error processing message:', error); + res.status(400).json({ error: 'Invalid message format' }); + } + }); + + // Health check endpoint + app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + connectedClients: clients.size, + endpoints: { + events: '/events', + message: '/message', + health: '/health' + } + }); + }); + + // Root endpoint with basic info + app.get('/', (req, res) => { + res.json({ + name: 'Perplexity MCP SSE Server', + version: '1.0.0', + endpoints: { + events: '/events - SSE endpoint for receiving responses', + message: '/message - POST endpoint for sending JSON-RPC requests', + health: '/health - Health check endpoint' + }, + connectedClients: clients.size + }); + }); + + return app; +} \ No newline at end of file diff --git a/perplexity-ask/test-client.html b/perplexity-ask/test-client.html new file mode 100644 index 0000000..ca1eb0f --- /dev/null +++ b/perplexity-ask/test-client.html @@ -0,0 +1,223 @@ + + + + + + Perplexity MCP SSE Test Client + + + +
+

Perplexity MCP SSE Test Client

+ +
+

Connection Status

+
Disconnected
+ + +
+ +
+

Test Tools List

+ +
+ +
+

Test Tool Call

+ +
+ + +
+ +
+

Response Log

+
+ +
+
+ + + + \ No newline at end of file From 91a6ad56d8066e17d6cd09fd2aa6b6514d71fc91 Mon Sep 17 00:00:00 2001 From: GeassV Date: Fri, 26 Sep 2025 16:37:39 +0800 Subject: [PATCH 2/2] modify with official sse transport protocol --- perplexity-ask/SSE-README.md | 131 +++++++++++ perplexity-ask/dist/index.js | 48 +++- perplexity-ask/index.ts | 61 ++++- perplexity-ask/sse-server.ts | 212 ------------------ perplexity-ask/test-client-official.html | 273 +++++++++++++++++++++++ 5 files changed, 507 insertions(+), 218 deletions(-) create mode 100644 perplexity-ask/SSE-README.md delete mode 100644 perplexity-ask/sse-server.ts create mode 100644 perplexity-ask/test-client-official.html diff --git a/perplexity-ask/SSE-README.md b/perplexity-ask/SSE-README.md new file mode 100644 index 0000000..8e13e64 --- /dev/null +++ b/perplexity-ask/SSE-README.md @@ -0,0 +1,131 @@ +# Perplexity MCP SSE Server + +This is the Server-Sent Events (SSE) version of the Perplexity MCP server, converted from the original stdio version. + +## Changes Made + +### 1. Transport Layer +- **Before**: Used `StdioServerTransport` for stdio communication +- **After**: Uses official `SSEServerTransport` from MCP SDK for HTTP/SSE communication + +### 2. Server Architecture +- **HTTP Server**: Now runs as an Express.js server on a configurable port (default: 3001) +- **SSE Endpoint**: `/sse` - for establishing Server-Sent Events connection +- **Message Endpoint**: `/message` - for sending JSON-RPC requests via HTTP POST +- **Health Check**: `/health` - for monitoring server status + +### 3. Dependencies Added +- `express`: HTTP server framework +- `cors`: Cross-Origin Resource Sharing support +- `@types/express` and `@types/cors`: TypeScript type definitions + +## Usage + +### Starting the Server + +```bash +# Build the project +npm run build + +# Start with environment variables +PERPLEXITY_API_KEY=your_api_key PORT=3001 node dist/index.js +``` + +### API Endpoints + +#### 1. SSE Connection +``` +GET /sse +``` +Establishes a Server-Sent Events connection. The server will send an `endpoint` event with the message URL including session ID. + +#### 2. Send Messages +``` +POST /message?sessionId= +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/list", + "params": {} +} +``` + +#### 3. Health Check +``` +GET /health +``` +Returns server status and connection information. + +### Client Implementation + +1. **Connect to SSE**: + ```javascript + const eventSource = new EventSource('http://localhost:3001/sse'); + ``` + +2. **Listen for endpoint URL**: + ```javascript + eventSource.addEventListener('endpoint', function(event) { + const messageUrl = decodeURI(event.data); + // Use this URL for sending messages + }); + ``` + +3. **Send messages**: + ```javascript + fetch(messageUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(jsonRpcMessage) + }); + ``` + +4. **Receive responses**: + ```javascript + eventSource.addEventListener('message', function(event) { + const response = JSON.parse(event.data); + // Handle JSON-RPC response + }); + ``` + +### Test Client + +Use the provided HTML test clients: + +- `test-client-official.html` - Uses the official MCP SSE transport protocol +- `test-client.html` - Uses the previous custom implementation (deprecated) + +Open either file in a web browser and test the SSE functionality. + +### Available Tools + +The server provides the same three Perplexity tools as the original: + +1. **perplexity_ask** - General chat completion using sonar-pro model +2. **perplexity_research** - Deep research using sonar-deep-research model +3. **perplexity_reason** - Reasoning tasks using sonar-reasoning-pro model + +### Environment Variables + +- `PERPLEXITY_API_KEY` (required) - Your Perplexity API key +- `PORT` (optional) - Server port, defaults to 3001 + +### Advantages of SSE Version + +1. **HTTP-based**: Can be accessed from web browsers and standard HTTP clients +2. **Real-time**: Server-Sent Events provide real-time communication +3. **Scalable**: Can handle multiple concurrent clients +4. **Stateful**: Each client gets a unique session for proper message routing +5. **Standard**: Uses official MCP SDK SSE transport implementation +6. **CORS-enabled**: Supports cross-origin requests for web applications + +## Migration from stdio + +If you were using the stdio version, you'll need to: + +1. Update your client to use HTTP/SSE instead of stdio +2. Handle the session-based communication model +3. Use the new endpoints for connecting and messaging +4. Update any deployment configurations to expose HTTP ports instead of stdio \ No newline at end of file diff --git a/perplexity-ask/dist/index.js b/perplexity-ask/dist/index.js index 029511b..9ce52b4 100755 --- a/perplexity-ask/dist/index.js +++ b/perplexity-ask/dist/index.js @@ -9,8 +9,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; -import { createSSEServer } from "./sse-server.js"; +import express from 'express'; +import cors from 'cors'; /** * Definition of the Perplexity Ask Tool. * This tool accepts an array of messages and returns a chat completion response @@ -282,9 +284,49 @@ function runServer() { return __awaiter(this, void 0, void 0, function* () { try { const PORT = process.env.PORT || 3001; - const sseServer = createSSEServer(server); - sseServer.listen(PORT, () => { + const app = express(); + app.use(cors()); + app.use(express.json()); + const transports = new Map(); + // SSE endpoint + app.get('/sse', (req, res) => __awaiter(this, void 0, void 0, function* () { + const transport = new SSEServerTransport('/message', res); + yield transport.start(); + transports.set(transport.sessionId, transport); + // Connect the server to this transport + yield server.connect(transport); + transport.onclose = () => { + transports.delete(transport.sessionId); + console.error(`Client disconnected: ${transport.sessionId}`); + }; + console.error(`Client connected: ${transport.sessionId}`); + })); + // Message endpoint + app.post('/message', (req, res) => __awaiter(this, void 0, void 0, function* () { + const sessionId = req.query.sessionId; + const transport = transports.get(sessionId); + if (!transport) { + return res.status(404).send('Session not found'); + } + yield transport.handlePostMessage(req, res); + })); + // Health check endpoint + app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + connectedClients: transports.size, + endpoints: { + sse: '/sse', + message: '/message', + health: '/health' + } + }); + }); + app.listen(PORT, () => { console.error(`Perplexity MCP Server running on SSE and listening on port ${PORT} with Ask, Research, and Reason tools`); + console.error(`SSE endpoint: http://localhost:${PORT}/sse`); + console.error(`Message endpoint: http://localhost:${PORT}/message`); }); } catch (error) { diff --git a/perplexity-ask/index.ts b/perplexity-ask/index.ts index d4b288e..28acd29 100644 --- a/perplexity-ask/index.ts +++ b/perplexity-ask/index.ts @@ -1,12 +1,14 @@ #!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; -import { createSSEServer } from "./sse-server.js"; +import express from 'express'; +import cors from 'cors'; /** * Definition of the Perplexity Ask Tool. @@ -295,10 +297,63 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { async function runServer() { try { const PORT = process.env.PORT || 3001; - const sseServer = createSSEServer(server); - sseServer.listen(PORT, () => { + const app = express(); + + app.use(cors()); + app.use(express.json()); + + const transports = new Map(); + + // SSE endpoint + app.get('/sse', async (req, res) => { + const transport = new SSEServerTransport('/message', res); + await transport.start(); + + transports.set(transport.sessionId, transport); + + // Connect the server to this transport + await server.connect(transport); + + transport.onclose = () => { + transports.delete(transport.sessionId); + console.error(`Client disconnected: ${transport.sessionId}`); + }; + + console.error(`Client connected: ${transport.sessionId}`); + }); + + // Message endpoint + app.post('/message', async (req, res) => { + const sessionId = req.query.sessionId as string; + const transport = transports.get(sessionId); + + if (!transport) { + return res.status(404).send('Session not found'); + } + + await transport.handlePostMessage(req, res); + }); + + // Health check endpoint + app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + connectedClients: transports.size, + endpoints: { + sse: '/sse', + message: '/message', + health: '/health' + } + }); + }); + + app.listen(PORT, () => { console.error(`Perplexity MCP Server running on SSE and listening on port ${PORT} with Ask, Research, and Reason tools`); + console.error(`SSE endpoint: http://localhost:${PORT}/sse`); + console.error(`Message endpoint: http://localhost:${PORT}/message`); }); + } catch (error) { console.error("Fatal error running server:", error); process.exit(1); diff --git a/perplexity-ask/sse-server.ts b/perplexity-ask/sse-server.ts deleted file mode 100644 index 2a70eae..0000000 --- a/perplexity-ask/sse-server.ts +++ /dev/null @@ -1,212 +0,0 @@ -import express from 'express'; -import cors from 'cors'; -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { - JSONRPCMessage, - JSONRPCRequest, - ListToolsRequestSchema, - CallToolRequestSchema -} from "@modelcontextprotocol/sdk/types.js"; - -export function createSSEServer(mcpServer: Server) { - const app = express(); - const clients = new Map(); - - app.use(cors({ - origin: '*', - methods: ['GET', 'POST', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization', 'Cache-Control', 'X-Client-ID'], - })); - - app.use(express.json()); - - // SSE endpoint for clients to receive messages - app.get('/events', (req, res) => { - const clientId = req.query.clientId as string || `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Headers': 'Cache-Control, X-Client-ID', - }); - - clients.set(clientId, res); - - // Send initial connection message - res.write(`data: ${JSON.stringify({ type: 'connected', clientId })}\n\n`); - - // Keep connection alive with periodic heartbeat - const heartbeat = setInterval(() => { - if (clients.has(clientId)) { - try { - res.write(`data: ${JSON.stringify({ type: 'heartbeat', timestamp: Date.now() })}\n\n`); - } catch (error) { - console.error(`Heartbeat failed for client ${clientId}:`, error); - clearInterval(heartbeat); - clients.delete(clientId); - } - } else { - clearInterval(heartbeat); - } - }, 30000); - - // Handle client disconnect - req.on('close', () => { - clearInterval(heartbeat); - clients.delete(clientId); - console.error(`Client ${clientId} disconnected`); - }); - - req.on('error', (error) => { - console.error(`Client ${clientId} error:`, error); - clearInterval(heartbeat); - clients.delete(clientId); - }); - }); - - // Endpoint for clients to send JSON-RPC requests - app.post('/message', async (req, res) => { - try { - const message = req.body as JSONRPCMessage; - console.error(`Received message:`, JSON.stringify(message, null, 2)); - - // Handle JSON-RPC request - if ('method' in message && 'id' in message) { - const request = message as JSONRPCRequest; - - try { - let response; - - if (request.method === 'tools/list') { - // Manually call the handler we registered in the main server - const handlers = (mcpServer as any)._requestHandlers; - const listHandler = handlers.get('tools/list'); - if (listHandler) { - response = await listHandler({ - method: 'tools/list', - params: request.params || {} - }); - } else { - throw new Error('No handler registered for tools/list'); - } - } else if (request.method === 'tools/call') { - const handlers = (mcpServer as any)._requestHandlers; - const callHandler = handlers.get('tools/call'); - if (callHandler) { - response = await callHandler({ - method: 'tools/call', - params: request.params - }); - } else { - throw new Error('No handler registered for tools/call'); - } - } else { - throw new Error(`Unknown method: ${request.method}`); - } - - const jsonrpcResponse = { - jsonrpc: '2.0', - id: request.id, - result: response - }; - - // Send response to the specific client that made the request - const clientId = req.headers['x-client-id'] as string; - const responseData = JSON.stringify(jsonrpcResponse); - - if (clientId && clients.has(clientId)) { - try { - clients.get(clientId)!.write(`data: ${responseData}\n\n`); - } catch (error) { - console.error(`Error sending to client ${clientId}:`, error); - clients.delete(clientId); - } - } else { - // If specific client not found, send to all clients - clients.forEach((client, cId) => { - try { - client.write(`data: ${responseData}\n\n`); - } catch (error) { - console.error(`Error sending to client ${cId}:`, error); - clients.delete(cId); - } - }); - } - - } catch (error) { - console.error('Error processing request:', error); - // Send error response - const errorResponse = { - jsonrpc: '2.0', - id: request.id, - error: { - code: -32603, - message: error instanceof Error ? error.message : 'Internal error', - data: error instanceof Error ? error.stack : undefined - } - }; - - const responseData = JSON.stringify(errorResponse); - const clientId = req.headers['x-client-id'] as string; - - if (clientId && clients.has(clientId)) { - try { - clients.get(clientId)!.write(`data: ${responseData}\n\n`); - } catch (sendError) { - console.error(`Error sending error to client ${clientId}:`, sendError); - clients.delete(clientId); - } - } else { - // If specific client not found, send to all clients - clients.forEach((client, cId) => { - try { - client.write(`data: ${responseData}\n\n`); - } catch (sendError) { - console.error(`Error sending error to client ${cId}:`, sendError); - clients.delete(cId); - } - }); - } - } - } - - res.status(200).json({ status: 'received' }); - - } catch (error) { - console.error('Error processing message:', error); - res.status(400).json({ error: 'Invalid message format' }); - } - }); - - // Health check endpoint - app.get('/health', (req, res) => { - res.json({ - status: 'healthy', - timestamp: new Date().toISOString(), - connectedClients: clients.size, - endpoints: { - events: '/events', - message: '/message', - health: '/health' - } - }); - }); - - // Root endpoint with basic info - app.get('/', (req, res) => { - res.json({ - name: 'Perplexity MCP SSE Server', - version: '1.0.0', - endpoints: { - events: '/events - SSE endpoint for receiving responses', - message: '/message - POST endpoint for sending JSON-RPC requests', - health: '/health - Health check endpoint' - }, - connectedClients: clients.size - }); - }); - - return app; -} \ No newline at end of file diff --git a/perplexity-ask/test-client-official.html b/perplexity-ask/test-client-official.html new file mode 100644 index 0000000..01b35a5 --- /dev/null +++ b/perplexity-ask/test-client-official.html @@ -0,0 +1,273 @@ + + + + + + Perplexity MCP SSE Test Client (Official Transport) + + + +
+

Perplexity MCP SSE Test Client (Official Transport)

+ +
+

Connection Status

+
Disconnected
+
+ + +
+ +
+

Test Tools List

+ +
+ +
+

Test Tool Call

+ +
+ + +
+ +
+

Response Log

+
+ +
+
+ + + + \ No newline at end of file