diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2414b5c..00815ab 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,14 +6,14 @@ }, "metadata": { "description": "Official Perplexity AI plugin providing real-time web search, reasoning, and research capabilities", - "version": "0.4.1" + "version": "0.5.0" }, "plugins": [ { "name": "perplexity", "source": "./", "description": "Real-time web search, reasoning, and research through Perplexity's API", - "version": "0.4.1", + "version": "0.5.0", "author": { "name": "Perplexity AI", "email": "api@perplexity.ai" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 59d1a50..db4c2cd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,4 +22,6 @@ jobs: run: npm ci - name: Run tests + env: + PERPLEXITY_API_KEY: test-api-key run: npm test diff --git a/DOCKER.md b/DOCKER.md index f935499..358463f 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -17,20 +17,22 @@ docker build -t perplexity-mcp-server . ## Running the Container -### Basic Usage +### HTTP Mode (Default) -Run the container with your API key: +The Docker container runs in HTTP mode by default, making it accessible via HTTP requests: ```bash -docker run --rm -e PERPLEXITY_API_KEY=your_key_here perplexity-mcp-server +docker run --rm -p 8080:8080 -e PERPLEXITY_API_KEY=your_key_here perplexity-mcp-server ``` +The server will be accessible at `http://localhost:8080/mcp` + ### With Custom Timeout Set a custom timeout for requests (default is 5 minutes): ```bash -docker run --rm \ +docker run --rm -p 8080:8080 \ -e PERPLEXITY_API_KEY=your_key_here \ -e PERPLEXITY_TIMEOUT_MS=600000 \ perplexity-mcp-server @@ -41,7 +43,7 @@ docker run --rm \ If you're behind a corporate proxy, configure it: ```bash -docker run --rm \ +docker run --rm -p 8080:8080 \ -e PERPLEXITY_API_KEY=your_key_here \ -e PERPLEXITY_PROXY=https://your-proxy-host:8080 \ perplexity-mcp-server @@ -50,7 +52,7 @@ docker run --rm \ Or with authentication: ```bash -docker run --rm \ +docker run --rm -p 8080:8080 \ -e PERPLEXITY_API_KEY=your_key_here \ -e PERPLEXITY_PROXY=https://username:password@your-proxy-host:8080 \ perplexity-mcp-server @@ -64,34 +66,38 @@ Create a `.env` file: PERPLEXITY_API_KEY=your_key_here PERPLEXITY_TIMEOUT_MS=600000 PERPLEXITY_PROXY=https://your-proxy-host:8080 +PORT=8080 ``` Then run: ```bash -docker run --rm --env-file .env perplexity-mcp-server +docker run --rm -p 8080:8080 --env-file .env perplexity-mcp-server ``` ## Integration with MCP Clients -When using Docker with MCP clients, configure them to run the Docker container. For example, in Cursor/VS Code's `mcp.json`: +When using the HTTP Docker server, configure your MCP client to connect to the HTTP endpoint: ```json { "mcpServers": { "perplexity": { - "command": "docker", - "args": [ - "run", - "--rm", - "-i", - "-e", "PERPLEXITY_API_KEY=your_key_here", - "perplexity-mcp-server" - ] + "url": "http://localhost:8080/mcp" } } } ``` -> **Note**: Docker-based MCP server configuration may have limitations compared to direct `npx` usage. For most use cases, the `npx` method documented in the main README is recommended. +## STDIO Mode (Local Development) + +For local development with STDIO transport, you can still run the server locally without Docker: + +```bash +npm install +npm run build +PERPLEXITY_API_KEY=your_key_here npm start +``` + +> **Note**: The Docker image is optimized for HTTP mode deployment. For local STDIO usage, the `npx` method documented in the main README is recommended. diff --git a/Dockerfile b/Dockerfile index 836d223..a29dbf9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ COPY tsconfig.json ./ RUN --mount=type=cache,target=/root/.npm npm install --ignore-scripts -COPY . . +COPY src/ ./src/ RUN npm run build @@ -23,4 +23,6 @@ ENV NODE_ENV=production RUN npm ci --ignore-scripts --omit-dev -ENTRYPOINT ["node", "dist/index.js"] +EXPOSE 8080 + +ENTRYPOINT ["node", "dist/http.js"] diff --git a/README.md b/README.md index 32cf50c..2d3e8a2 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,40 @@ If you'd rather use the standard variables, we support `HTTPS_PROXY` and `HTTP_P > The server checks proxy settings in this order: `PERPLEXITY_PROXY` → `HTTPS_PROXY` → `HTTP_PROXY`. If none are set, it connects directly to the internet. > URLs must include `https://`. Typical ports are `8080`, `3128`, and `80`. + +### HTTP Server Deployment + +For cloud or shared deployments, you can run the server in HTTP mode: + +#### Environment Variables + +The HTTP server supports these configuration options: + +- **`PORT`** - HTTP server port (default: `8080`) +- **`BIND_ADDRESS`** - Network interface to bind to (default: `127.0.0.1` for local, use `0.0.0.0` for hosted) +- **`ALLOWED_ORIGINS`** - Comma-separated list of allowed CORS origins (default: `http://localhost:3000,http://127.0.0.1:3000`, use `*` for public service) +- **`PERPLEXITY_API_KEY`** - Your Perplexity API key (required) + +#### Using Docker + +```bash +docker build -t perplexity-mcp-server . +docker run -p 8080:8080 -e PERPLEXITY_API_KEY=your_key_here perplexity-mcp-server +``` + +The server will be accessible at `http://localhost:8080/mcp` + +#### Using Node.js Directly + +```bash +npm install +npm run build +npm run start:http +``` + +Connect your MCP client to: `http://localhost:8080/mcp` + + ## Troubleshooting - **API Key Issues**: Ensure `PERPLEXITY_API_KEY` is set correctly diff --git a/index.ts b/index.ts deleted file mode 100644 index 25c333c..0000000 --- a/index.ts +++ /dev/null @@ -1,611 +0,0 @@ -#!/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 { fetch as undiciFetch, ProxyAgent } from "undici"; - -/** - * 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: Tool = { - name: "perplexity_ask", - title: "Ask Perplexity", - 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"], - }, - annotations: { - readOnlyHint: true, - openWorldHint: true, - }, -}; - -/** - * Definition of the Perplexity Research Tool. - * This tool performs deep research queries using the Perplexity API. - */ -const PERPLEXITY_RESEARCH_TOOL: Tool = { - name: "perplexity_research", - title: "Deep 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", - }, - strip_thinking: { - type: "boolean", - description: "If true, removes ... tags and their content from the response to save context tokens. Default is false.", - }, - }, - required: ["messages"], - }, - annotations: { - readOnlyHint: true, - openWorldHint: true, - }, -}; - -/** - * Definition of the Perplexity Reason Tool. - * This tool performs reasoning queries using the Perplexity API. - */ -const PERPLEXITY_REASON_TOOL: Tool = { - name: "perplexity_reason", - title: "Advanced Reasoning", - 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", - }, - strip_thinking: { - type: "boolean", - description: "If true, removes ... tags and their content from the response to save context tokens. Default is false.", - }, - }, - required: ["messages"], - }, - annotations: { - readOnlyHint: true, - openWorldHint: true, - }, -}; - -/** - * Definition of the Perplexity Search Tool. - * This tool performs web search using the Perplexity Search API. - */ -const PERPLEXITY_SEARCH_TOOL: Tool = { - name: "perplexity_search", - title: "Search the Web", - description: - "Performs web search using the Perplexity Search API. " + - "Returns ranked search results with titles, URLs, snippets, and metadata. " + - "Perfect for finding up-to-date facts, news, or specific information.", - inputSchema: { - type: "object", - properties: { - query: { - type: "string", - description: "Search query string", - }, - max_results: { - type: "number", - description: "Maximum number of results to return (1-20, default: 10)", - minimum: 1, - maximum: 20, - }, - max_tokens_per_page: { - type: "number", - description: "Maximum tokens to extract per webpage (default: 1024)", - minimum: 256, - maximum: 2048, - }, - country: { - type: "string", - description: "ISO 3166-1 alpha-2 country code for regional results (e.g., 'US', 'GB')", - }, - }, - required: ["query"], - }, - annotations: { - readOnlyHint: true, - openWorldHint: true, - }, -}; - -// 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); -} - -/** - * Gets the proxy URL from environment variables. - * Checks PERPLEXITY_PROXY, HTTPS_PROXY, HTTP_PROXY in order. - * - * @returns {string | undefined} The proxy URL if configured, undefined otherwise - */ -function getProxyUrl(): string | undefined { - return process.env.PERPLEXITY_PROXY || - process.env.HTTPS_PROXY || - process.env.HTTP_PROXY || - undefined; -} - -/** - * Creates a proxy-aware fetch function. - * Uses undici with ProxyAgent when a proxy is configured, otherwise uses native fetch. - * - * @param {string} url - The URL to fetch - * @param {RequestInit} options - Fetch options - * @returns {Promise} The fetch response - */ -async function proxyAwareFetch(url: string, options: RequestInit = {}): Promise { - const proxyUrl = getProxyUrl(); - - if (proxyUrl) { - // Use undici with ProxyAgent when proxy is configured - const proxyAgent = new ProxyAgent(proxyUrl); - const response = await undiciFetch(url, { - ...options, - dispatcher: proxyAgent, - } as any); - // Cast to native Response type for compatibility - return response as unknown as Response; - } else { - // Use native fetch when no proxy is configured - return fetch(url, options); - } -} - -/** - * Validates an array of message objects for chat completion tools. - * Ensures each message has a valid role and content field. - * - * @param {any} messages - The messages to validate - * @param {string} toolName - The name of the tool calling this validation (for error messages) - * @throws {Error} If messages is not an array or if any message is invalid - */ -function validateMessages(messages: any, toolName: string): void { - if (!Array.isArray(messages)) { - throw new Error(`Invalid arguments for ${toolName}: 'messages' must be an array`); - } - - for (let i = 0; i < messages.length; i++) { - const msg = messages[i]; - if (!msg || typeof msg !== 'object') { - throw new Error(`Invalid message at index ${i}: must be an object`); - } - if (!msg.role || typeof msg.role !== 'string') { - throw new Error(`Invalid message at index ${i}: 'role' must be a string`); - } - if (msg.content === undefined || msg.content === null || typeof msg.content !== 'string') { - throw new Error(`Invalid message at index ${i}: 'content' must be a string`); - } - } -} - -/** - * Strips thinking tokens (content within ... tags) from the response. - * This helps reduce context usage when the thinking process is not needed. - * - * @param {string} content - The content to process - * @returns {string} The content with thinking tokens removed - */ -function stripThinkingTokens(content: string): string { - return content.replace(/[\s\S]*?<\/think>/g, '').trim(); -} - -/** - * 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. - * @param {boolean} stripThinking - If true, removes ... tags from the response. - * @returns {Promise} The chat completion result with appended citations. - * @throws Will throw an error if the API request fails. - */ -export async function performChatCompletion( - messages: Array<{ role: string; content: string }>, - model: string = "sonar-pro", - stripThinking: boolean = false -): Promise { - // Read timeout fresh each time to respect env var changes - const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10); - - // 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 - }; - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); - - let response; - try { - response = await proxyAwareFetch(url.toString(), { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${PERPLEXITY_API_KEY}`, - }, - body: JSON.stringify(body), - signal: controller.signal, - }); - clearTimeout(timeoutId); - } catch (error) { - clearTimeout(timeoutId); - if (error instanceof Error && error.name === "AbortError") { - throw new Error(`Request timeout: Perplexity API did not respond within ${TIMEOUT_MS}ms. Consider increasing PERPLEXITY_TIMEOUT_MS.`); - } - throw new Error(`Network error while calling Perplexity API: ${error}`); - } - - // Check for non-successful HTTP status - if (!response.ok) { - let errorText; - try { - errorText = await 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 = await response.json(); - } catch (jsonError) { - throw new Error(`Failed to parse JSON response from Perplexity API: ${jsonError}`); - } - - // Validate response structure - if (!data.choices || !Array.isArray(data.choices) || data.choices.length === 0) { - throw new Error("Invalid API response: missing or empty choices array"); - } - - const firstChoice = data.choices[0]; - if (!firstChoice.message || typeof firstChoice.message.content !== 'string') { - throw new Error("Invalid API response: missing message content"); - } - - // Directly retrieve the main message content from the response - let messageContent = firstChoice.message.content; - - // Strip thinking tokens if requested - if (stripThinking) { - messageContent = stripThinkingTokens(messageContent); - } - - // 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: string, index: number) => { - messageContent += `[${index + 1}] ${citation}\n`; - }); - } - - return messageContent; -} - -/** - * Formats search results from the Perplexity Search API into a readable string. - * - * @param {any} data - The search response data from the API. - * @returns {string} Formatted search results. - */ -export function formatSearchResults(data: any): string { - if (!data.results || !Array.isArray(data.results)) { - return "No search results found."; - } - - let formattedResults = `Found ${data.results.length} search results:\n\n`; - - data.results.forEach((result: any, index: number) => { - formattedResults += `${index + 1}. **${result.title}**\n`; - formattedResults += ` URL: ${result.url}\n`; - if (result.snippet) { - formattedResults += ` ${result.snippet}\n`; - } - if (result.date) { - formattedResults += ` Date: ${result.date}\n`; - } - formattedResults += `\n`; - }); - - return formattedResults; -} - -/** - * Performs a web search using the Perplexity Search API. - * - * @param {string} query - The search query string. - * @param {number} maxResults - Maximum number of results to return (1-20). - * @param {number} maxTokensPerPage - Maximum tokens to extract per webpage. - * @param {string} country - Optional ISO country code for regional results. - * @returns {Promise} The formatted search results. - * @throws Will throw an error if the API request fails. - */ -export async function performSearch( - query: string, - maxResults: number = 10, - maxTokensPerPage: number = 1024, - country?: string -): Promise { - // Read timeout fresh each time to respect env var changes - const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10); - - const url = new URL("https://api.perplexity.ai/search"); - const body: any = { - query: query, - max_results: maxResults, - max_tokens_per_page: maxTokensPerPage, - }; - - if (country) { - body.country = country; - } - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); - - let response; - try { - response = await proxyAwareFetch(url.toString(), { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${PERPLEXITY_API_KEY}`, - }, - body: JSON.stringify(body), - signal: controller.signal, - }); - clearTimeout(timeoutId); - } catch (error) { - clearTimeout(timeoutId); - if (error instanceof Error && error.name === "AbortError") { - throw new Error(`Request timeout: Perplexity Search API did not respond within ${TIMEOUT_MS}ms. Consider increasing PERPLEXITY_TIMEOUT_MS.`); - } - throw new Error(`Network error while calling Perplexity Search API: ${error}`); - } - - // Check for non-successful HTTP status - if (!response.ok) { - let errorText; - try { - errorText = await response.text(); - } catch (parseError) { - errorText = "Unable to parse error response"; - } - throw new Error( - `Perplexity Search API error: ${response.status} ${response.statusText}\n${errorText}` - ); - } - - let data; - try { - data = await response.json(); - } catch (jsonError) { - throw new Error(`Failed to parse JSON response from Perplexity Search API: ${jsonError}`); - } - - return formatSearchResults(data); -} - -// Initialize the server with tool metadata and capabilities -const server = new Server( - { - name: "io.github.perplexityai/mcp-server", - version: "0.4.1", - }, - { - capabilities: { - tools: {}, - }, - instructions: `You are the Perplexity MCP Server. Use these tools appropriately: - -- perplexity_search: For quick web searches when you need current information or facts. Returns ranked search results. - -- perplexity_ask: For general questions and conversational queries with real-time web search using the sonar-pro model. - -- perplexity_research: For deep, comprehensive research requiring thorough analysis using the sonar-deep-research model. Use this for complex topics that require detailed investigation. - -- perplexity_reason: For complex analytical tasks requiring advanced reasoning using the sonar-reasoning-pro model. Use this for logical problems, analysis, and decision-making. - -When using perplexity_research or perplexity_reason, consider setting strip_thinking=true to save context tokens if the reasoning process isn't needed in the final output.`, - } -); - -/** - * 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, async () => ({ - tools: [PERPLEXITY_ASK_TOOL, PERPLEXITY_RESEARCH_TOOL, PERPLEXITY_REASON_TOOL, PERPLEXITY_SEARCH_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, async (request) => { - try { - const { name, arguments: args } = request.params; - if (!args) { - throw new Error("No arguments provided"); - } - switch (name) { - case "perplexity_ask": { - validateMessages(args.messages, "perplexity_ask"); - const messages = args.messages as Array<{ role: string; content: string }>; - const result = await performChatCompletion(messages, "sonar-pro"); - return { - content: [{ type: "text", text: result }], - isError: false, - }; - } - case "perplexity_research": { - validateMessages(args.messages, "perplexity_research"); - const messages = args.messages as Array<{ role: string; content: string }>; - const stripThinking = typeof args.strip_thinking === "boolean" ? args.strip_thinking : false; - const result = await performChatCompletion(messages, "sonar-deep-research", stripThinking); - return { - content: [{ type: "text", text: result }], - isError: false, - }; - } - case "perplexity_reason": { - validateMessages(args.messages, "perplexity_reason"); - const messages = args.messages as Array<{ role: string; content: string }>; - const stripThinking = typeof args.strip_thinking === "boolean" ? args.strip_thinking : false; - const result = await performChatCompletion(messages, "sonar-reasoning-pro", stripThinking); - return { - content: [{ type: "text", text: result }], - isError: false, - }; - } - case "perplexity_search": { - if (typeof args.query !== "string") { - throw new Error("Invalid arguments for perplexity_search: 'query' must be a string"); - } - const { query, max_results, max_tokens_per_page, country } = args; - const maxResults = typeof max_results === "number" ? max_results : undefined; - const maxTokensPerPage = typeof max_tokens_per_page === "number" ? max_tokens_per_page : undefined; - const countryCode = typeof country === "string" ? country : undefined; - - const result = await performSearch( - query, - maxResults, - maxTokensPerPage, - countryCode - ); - 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 standard I/O 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, Reason, and Search 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/package-lock.json b/package-lock.json index 1949fab..81e18d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,30 @@ { "name": "@perplexity-ai/mcp-server", - "version": "0.4.1", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@perplexity-ai/mcp-server", - "version": "0.4.1", + "version": "0.5.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.21.1", - "undici": "^6.20.0" + "cors": "^2.8.5", + "express": "^4.21.2", + "undici": "^6.20.0", + "zod": "^3.25.46" }, "bin": { "perplexity-mcp": "dist/index.js" }, "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", "@types/node": "^20", "@vitest/coverage-v8": "^4.0.5", "shx": "^0.4.0", + "tsx": "^4.19.4", "typescript": "^5.9.3", "vitest": "^4.0.5" }, @@ -588,6 +594,245 @@ } } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -941,6 +1186,17 @@ "dev": true, "license": "MIT" }, + "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/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -952,6 +1208,26 @@ "assertion-error": "^2.0.1" } }, + "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/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -966,6 +1242,45 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "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": "20.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", @@ -976,6 +1291,53 @@ "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": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@vitest/coverage-v8": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.5.tgz", @@ -1120,34 +1482,13 @@ } }, "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "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-db": "^1.54.0" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { "node": ">= 0.6" @@ -1186,6 +1527,12 @@ } } }, + "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/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1209,23 +1556,69 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "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": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "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/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/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/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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": ">=18" + "node": ">= 0.8" } }, "node_modules/braces": { @@ -1290,9 +1683,9 @@ } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "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" @@ -1311,22 +1704,19 @@ } }, "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "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.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } + "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", @@ -1381,6 +1771,16 @@ "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/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1638,41 +2038,45 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" + "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": ">= 18" + "node": ">= 0.10.0" }, "funding": { "type": "opencollective", @@ -1694,27 +2098,21 @@ "express": ">= 4.11" } }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "node_modules/express/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": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" + "ms": "2.0.0" } }, + "node_modules/express/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/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1778,22 +2176,38 @@ } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" + "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/finalhandler/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/finalhandler/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/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1804,12 +2218,12 @@ } }, "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/fsevents": { @@ -1886,6 +2300,19 @@ "node": ">=6" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -2204,22 +2631,19 @@ } }, "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "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.8" + "node": ">= 0.6" } }, "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "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", - "engines": { - "node": ">=18" - }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -2234,6 +2658,15 @@ "node": ">= 8" } }, + "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/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -2248,6 +2681,39 @@ "node": ">=8.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/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -2284,9 +2750,9 @@ } }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "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" @@ -2400,14 +2866,10 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "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/pathe": { "version": "2.0.3", @@ -2499,12 +2961,12 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "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.1.0" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -2600,6 +3062,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -2669,6 +3141,16 @@ "node": ">= 18" } }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2730,61 +3212,66 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "license": "MIT", "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "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": ">= 18" + "node": ">= 0.8.0" } }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "node_modules/send/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", - "engines": { - "node": ">= 0.6" + "dependencies": { + "ms": "2.0.0" } }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "node_modules/send/node_modules/debug/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/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", - "dependencies": { - "mime-db": "^1.54.0" - }, "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "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": "^1.2.0" + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" }, "engines": { - "node": ">= 18" + "node": ">= 0.8.0" } }, "node_modules/setprototypeof": { @@ -3099,36 +3586,34 @@ "node": ">=0.6" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, "license": "MIT", "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "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": { - "mime-db": "^1.54.0" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" }, "engines": { "node": ">= 0.6" @@ -3173,6 +3658,15 @@ "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", @@ -3418,9 +3912,9 @@ "license": "ISC" }, "node_modules/zod": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 881f525..08b1afb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@perplexity-ai/mcp-server", - "version": "0.4.1", + "version": "0.5.0", "mcpName": "io.github.perplexityai/mcp-server", "description": "Real-time web search, reasoning, and research through Perplexity's API", "keywords": [ @@ -28,7 +28,9 @@ "perplexity-mcp": "dist/index.js" }, "files": [ - "dist", + "dist/*.js", + "!dist/*.test.js", + "!dist/vitest.config.js", "README.md", ".claude-plugin" ], @@ -36,18 +38,30 @@ "build": "tsc && shx chmod +x dist/*.js", "prepare": "npm run build", "watch": "tsc --watch", + "start": "node dist/index.js", + "start:http": "node dist/http.js", + "start:http:public": "BIND_ADDRESS=0.0.0.0 ALLOWED_ORIGINS=* node dist/http.js", + "dev": "tsx src/index.ts", + "dev:http": "tsx src/http.ts", + "dev:http:public": "BIND_ADDRESS=0.0.0.0 ALLOWED_ORIGINS=* tsx src/http.ts", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.21.1", - "undici": "^6.20.0" + "cors": "^2.8.5", + "express": "^4.21.2", + "undici": "^6.20.0", + "zod": "^3.25.46" }, "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", "@types/node": "^20", "@vitest/coverage-v8": "^4.0.5", "shx": "^0.4.0", + "tsx": "^4.19.4", "typescript": "^5.9.3", "vitest": "^4.0.5" }, diff --git a/server.json b/server.json index bf13ca5..6a0b815 100644 --- a/server.json +++ b/server.json @@ -3,16 +3,23 @@ "name": "io.github.perplexityai/mcp-server", "title": "Perplexity API Platform", "description": "Real-time web search, reasoning, and research through Perplexity's API", - "version": "0.4.1", + "version": "0.5.0", "packages": [ { "registryType": "npm", "identifier": "@perplexity-ai/mcp-server", - "version": "0.4.1", + "version": "0.5.0", "transport": { "type": "stdio" } } + ], + "remotes": [ + { + "type": "streamable-http", + "url": "https://mcp.perplexity.ai/mcp", + "description": "Hosted Perplexity MCP server with HTTP transport. Set PERPLEXITY_API_KEY environment variable." + } ] } diff --git a/smithery.yaml b/smithery.yaml new file mode 100644 index 0000000..9582cde --- /dev/null +++ b/smithery.yaml @@ -0,0 +1,7 @@ +runtime: "container" +build: + dockerfile: "Dockerfile" + dockerBuildPath: "." +startCommand: + type: "http" + diff --git a/src/http.ts b/src/http.ts new file mode 100644 index 0000000..b759a2c --- /dev/null +++ b/src/http.ts @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +import express from "express"; +import cors from "cors"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createPerplexityServer } from "./server.js"; + +// Check for required API key +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); +} + +const app = express(); +const PORT = parseInt(process.env.PORT || "8080", 10); +const BIND_ADDRESS = process.env.BIND_ADDRESS || "127.0.0.1"; +const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(",") || [ + "http://localhost:3000", + "http://127.0.0.1:3000", +]; + +// CORS configuration for browser-based MCP clients +app.use(cors({ + origin: (origin, callback) => { + if (!origin) return callback(null, true); + + if (ALLOWED_ORIGINS.includes("*")) { + return callback(null, true); + } + + if (ALLOWED_ORIGINS.includes(origin)) { + callback(null, true); + } else { + callback(new Error(`Origin ${origin} not allowed by CORS`)); + } + }, + exposedHeaders: ["Mcp-Session-Id", "mcp-protocol-version"], + allowedHeaders: ["Content-Type", "mcp-session-id"], +})); + +app.use(express.json()); + +const mcpServer = createPerplexityServer(); + +/** + * POST: client-to-server messages (requests, responses, notifications) + * GET: SSE stream for server-to-client messages (notifications, requests) + */ +app.all("/mcp", async (req, res) => { + try { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + + res.on('close', () => { + transport.close(); + }); + + await mcpServer.connect(transport); + + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error("Error handling MCP request:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal server error" }, + id: null, + }); + } + } +}); + +/** + * Health check endpoint + */ +app.get("/health", (req, res) => { + res.json({ status: "ok", service: "perplexity-mcp-server" }); +}); + +/** + * Start the HTTP server + */ +app.listen(PORT, BIND_ADDRESS, () => { + console.log(`Perplexity MCP Server listening on http://${BIND_ADDRESS}:${PORT}/mcp`); + console.log(`Allowed origins: ${ALLOWED_ORIGINS.join(", ")}`); +}).on("error", (error) => { + console.error("Server error:", error); + process.exit(1); +}); + diff --git a/index.test.ts b/src/index.test.ts similarity index 99% rename from index.test.ts rename to src/index.test.ts index f7b98d8..45f3eb9 100644 --- a/index.test.ts +++ b/src/index.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { formatSearchResults, performChatCompletion, performSearch } from "./index.js"; +import { formatSearchResults, performChatCompletion, performSearch } from "./server.js"; describe("Perplexity MCP Server", () => { let originalFetch: typeof global.fetch; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..cf9f9d4 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { createPerplexityServer } from "./server.js"; + +// Check for required API key +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); +} + +/** + * Initializes and runs the server using standard I/O for communication. + * Logs an error and exits if the server fails to start. + */ +async function main() { + try { + const server = createPerplexityServer(); + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Perplexity MCP Server running on stdio with Ask, Research, Reason, and Search tools"); + } catch (error) { + console.error("Fatal error running server:", error); + process.exit(1); + } +} + +// Start the server and catch any startup errors +main().catch((error) => { + console.error("Fatal error running server:", error); + process.exit(1); +}); + diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..2c91c58 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,449 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { fetch as undiciFetch, ProxyAgent } from "undici"; + +// Retrieve the Perplexity API key from environment variables +const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY; + +/** + * Gets the proxy URL from environment variables. + * Checks PERPLEXITY_PROXY, HTTPS_PROXY, HTTP_PROXY in order. + * + * @returns {string | undefined} The proxy URL if configured, undefined otherwise + */ +function getProxyUrl(): string | undefined { + return process.env.PERPLEXITY_PROXY || + process.env.HTTPS_PROXY || + process.env.HTTP_PROXY || + undefined; +} + +/** + * Creates a proxy-aware fetch function. + * Uses undici with ProxyAgent when a proxy is configured, otherwise uses native fetch. + * + * @param {string} url - The URL to fetch + * @param {RequestInit} options - Fetch options + * @returns {Promise} The fetch response + */ +async function proxyAwareFetch(url: string, options: RequestInit = {}): Promise { + const proxyUrl = getProxyUrl(); + + if (proxyUrl) { + // Use undici with ProxyAgent when proxy is configured + const proxyAgent = new ProxyAgent(proxyUrl); + const response = await undiciFetch(url, { + ...options, + dispatcher: proxyAgent, + } as any); + // Cast to native Response type for compatibility + return response as unknown as Response; + } else { + // Use native fetch when no proxy is configured + return fetch(url, options); + } +} + +/** + * Validates an array of message objects for chat completion tools. + * Ensures each message has a valid role and content field. + * + * @param {any} messages - The messages to validate + * @param {string} toolName - The name of the tool calling this validation (for error messages) + * @throws {Error} If messages is not an array or if any message is invalid + */ +function validateMessages(messages: any, toolName: string): void { + if (!Array.isArray(messages)) { + throw new Error(`Invalid arguments for ${toolName}: 'messages' must be an array`); + } + + for (let i = 0; i < messages.length; i++) { + const msg = messages[i]; + if (!msg || typeof msg !== 'object') { + throw new Error(`Invalid message at index ${i}: must be an object`); + } + if (!msg.role || typeof msg.role !== 'string') { + throw new Error(`Invalid message at index ${i}: 'role' must be a string`); + } + if (msg.content === undefined || msg.content === null || typeof msg.content !== 'string') { + throw new Error(`Invalid message at index ${i}: 'content' must be a string`); + } + } +} + +/** + * Strips thinking tokens (content within ... tags) from the response. + * This helps reduce context usage when the thinking process is not needed. + * + * @param {string} content - The content to process + * @returns {string} The content with thinking tokens removed + */ +function stripThinkingTokens(content: string): string { + return content.replace(/[\s\S]*?<\/think>/g, '').trim(); +} + +/** + * 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. + * @param {boolean} stripThinking - If true, removes ... tags from the response. + * @returns {Promise} The chat completion result with appended citations. + * @throws Will throw an error if the API request fails. + */ +export async function performChatCompletion( + messages: Array<{ role: string; content: string }>, + model: string = "sonar-pro", + stripThinking: boolean = false +): Promise { + if (!PERPLEXITY_API_KEY) { + throw new Error("PERPLEXITY_API_KEY environment variable is required"); + } + + // Read timeout fresh each time to respect env var changes + const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10); + + // Construct the API endpoint URL and request body + const url = new URL("https://api.perplexity.ai/chat/completions"); + const body = { + model: model, + messages: messages, + }; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); + + let response; + try { + response = await proxyAwareFetch(url.toString(), { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${PERPLEXITY_API_KEY}`, + }, + body: JSON.stringify(body), + signal: controller.signal, + }); + clearTimeout(timeoutId); + } catch (error) { + clearTimeout(timeoutId); + if (error instanceof Error && error.name === "AbortError") { + throw new Error(`Request timeout: Perplexity API did not respond within ${TIMEOUT_MS}ms. Consider increasing PERPLEXITY_TIMEOUT_MS.`); + } + throw new Error(`Network error while calling Perplexity API: ${error}`); + } + + // Check for non-successful HTTP status + if (!response.ok) { + let errorText; + try { + errorText = await 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 = await response.json(); + } catch (jsonError) { + throw new Error(`Failed to parse JSON response from Perplexity API: ${jsonError}`); + } + + // Validate response structure + if (!data.choices || !Array.isArray(data.choices) || data.choices.length === 0) { + throw new Error("Invalid API response: missing or empty choices array"); + } + + const firstChoice = data.choices[0]; + if (!firstChoice.message || typeof firstChoice.message.content !== 'string') { + throw new Error("Invalid API response: missing message content"); + } + + // Directly retrieve the main message content from the response + let messageContent = firstChoice.message.content; + + // Strip thinking tokens if requested + if (stripThinking) { + messageContent = stripThinkingTokens(messageContent); + } + + // 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: string, index: number) => { + messageContent += `[${index + 1}] ${citation}\n`; + }); + } + + return messageContent; +} + +/** + * Formats search results from the Perplexity Search API into a readable string. + * + * @param {any} data - The search response data from the API. + * @returns {string} Formatted search results. + */ +export function formatSearchResults(data: any): string { + if (!data.results || !Array.isArray(data.results)) { + return "No search results found."; + } + + let formattedResults = `Found ${data.results.length} search results:\n\n`; + + data.results.forEach((result: any, index: number) => { + formattedResults += `${index + 1}. **${result.title}**\n`; + formattedResults += ` URL: ${result.url}\n`; + if (result.snippet) { + formattedResults += ` ${result.snippet}\n`; + } + if (result.date) { + formattedResults += ` Date: ${result.date}\n`; + } + formattedResults += `\n`; + }); + + return formattedResults; +} + +/** + * Performs a web search using the Perplexity Search API. + * + * @param {string} query - The search query string. + * @param {number} maxResults - Maximum number of results to return (1-20). + * @param {number} maxTokensPerPage - Maximum tokens to extract per webpage. + * @param {string} country - Optional ISO country code for regional results. + * @returns {Promise} The formatted search results. + * @throws Will throw an error if the API request fails. + */ +export async function performSearch( + query: string, + maxResults: number = 10, + maxTokensPerPage: number = 1024, + country?: string +): Promise { + if (!PERPLEXITY_API_KEY) { + throw new Error("PERPLEXITY_API_KEY environment variable is required"); + } + + // Read timeout fresh each time to respect env var changes + const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10); + + const url = new URL("https://api.perplexity.ai/search"); + const body: any = { + query: query, + max_results: maxResults, + max_tokens_per_page: maxTokensPerPage, + }; + + if (country) { + body.country = country; + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); + + let response; + try { + response = await proxyAwareFetch(url.toString(), { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${PERPLEXITY_API_KEY}`, + }, + body: JSON.stringify(body), + signal: controller.signal, + }); + clearTimeout(timeoutId); + } catch (error) { + clearTimeout(timeoutId); + if (error instanceof Error && error.name === "AbortError") { + throw new Error(`Request timeout: Perplexity Search API did not respond within ${TIMEOUT_MS}ms. Consider increasing PERPLEXITY_TIMEOUT_MS.`); + } + throw new Error(`Network error while calling Perplexity Search API: ${error}`); + } + + // Check for non-successful HTTP status + if (!response.ok) { + let errorText; + try { + errorText = await response.text(); + } catch (parseError) { + errorText = "Unable to parse error response"; + } + throw new Error( + `Perplexity Search API error: ${response.status} ${response.statusText}\n${errorText}` + ); + } + + let data; + try { + data = await response.json(); + } catch (jsonError) { + throw new Error(`Failed to parse JSON response from Perplexity Search API: ${jsonError}`); + } + + return formatSearchResults(data); +} + +/** + * Creates and configures the Perplexity MCP server with all tools. + * This factory function is transport-agnostic and returns a configured server instance. + * + * @returns The configured MCP server instance + */ +export function createPerplexityServer() { + const server = new McpServer({ + name: "io.github.perplexityai/mcp-server", + version: "0.5.0", + }); + + // Register perplexity_ask tool + server.registerTool( + "perplexity_ask", + { + title: "Ask Perplexity", + description: "Engages in a conversation using the Sonar API. " + + "Accepts an array of messages (each with a role and content) " + + "and returns a chat completion response from the Perplexity model.", + inputSchema: { + messages: z.array(z.object({ + role: z.string().describe("Role of the message (e.g., system, user, assistant)"), + content: z.string().describe("The content of the message"), + })).describe("Array of conversation messages"), + }, + outputSchema: { + response: z.string().describe("The chat completion response"), + }, + annotations: { + readOnlyHint: true, + openWorldHint: true, + }, + }, + async ({ messages }) => { + validateMessages(messages, "perplexity_ask"); + const result = await performChatCompletion(messages, "sonar-pro"); + return { + content: [{ type: "text", text: result }], + structuredContent: { response: result }, + }; + } + ); + + // Register perplexity_research tool + server.registerTool( + "perplexity_research", + { + title: "Deep 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: { + messages: z.array(z.object({ + role: z.string().describe("Role of the message (e.g., system, user, assistant)"), + content: z.string().describe("The content of the message"), + })).describe("Array of conversation messages"), + strip_thinking: z.boolean().optional() + .describe("If true, removes ... tags and their content from the response to save context tokens. Default is false."), + }, + outputSchema: { + response: z.string().describe("The research response"), + }, + annotations: { + readOnlyHint: true, + openWorldHint: true, + }, + }, + async ({ messages, strip_thinking }) => { + validateMessages(messages, "perplexity_research"); + const stripThinking = typeof strip_thinking === "boolean" ? strip_thinking : false; + const result = await performChatCompletion(messages, "sonar-deep-research", stripThinking); + return { + content: [{ type: "text", text: result }], + structuredContent: { response: result }, + }; + } + ); + + // Register perplexity_reason tool + server.registerTool( + "perplexity_reason", + { + title: "Advanced Reasoning", + 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: { + messages: z.array(z.object({ + role: z.string().describe("Role of the message (e.g., system, user, assistant)"), + content: z.string().describe("The content of the message"), + })).describe("Array of conversation messages"), + strip_thinking: z.boolean().optional() + .describe("If true, removes ... tags and their content from the response to save context tokens. Default is false."), + }, + outputSchema: { + response: z.string().describe("The reasoning response"), + }, + annotations: { + readOnlyHint: true, + openWorldHint: true, + }, + }, + async ({ messages, strip_thinking }) => { + validateMessages(messages, "perplexity_reason"); + const stripThinking = typeof strip_thinking === "boolean" ? strip_thinking : false; + const result = await performChatCompletion(messages, "sonar-reasoning-pro", stripThinking); + return { + content: [{ type: "text", text: result }], + structuredContent: { response: result }, + }; + } + ); + + // Register perplexity_search tool + server.registerTool( + "perplexity_search", + { + title: "Search the Web", + description: "Performs web search using the Perplexity Search API. " + + "Returns ranked search results with titles, URLs, snippets, and metadata. " + + "Perfect for finding up-to-date facts, news, or specific information.", + inputSchema: { + query: z.string().describe("Search query string"), + max_results: z.number().min(1).max(20).optional() + .describe("Maximum number of results to return (1-20, default: 10)"), + max_tokens_per_page: z.number().min(256).max(2048).optional() + .describe("Maximum tokens to extract per webpage (default: 1024)"), + country: z.string().optional() + .describe("ISO 3166-1 alpha-2 country code for regional results (e.g., 'US', 'GB')"), + }, + outputSchema: { + results: z.string().describe("Formatted search results"), + }, + annotations: { + readOnlyHint: true, + openWorldHint: true, + }, + }, + async ({ query, max_results, max_tokens_per_page, country }) => { + const maxResults = typeof max_results === "number" ? max_results : 10; + const maxTokensPerPage = typeof max_tokens_per_page === "number" ? max_tokens_per_page : 1024; + const countryCode = typeof country === "string" ? country : undefined; + + const result = await performSearch(query, maxResults, maxTokensPerPage, countryCode); + return { + content: [{ type: "text", text: result }], + structuredContent: { results: result }, + }; + } + ); + + return server.server; +} + diff --git a/src/transport.test.ts b/src/transport.test.ts new file mode 100644 index 0000000..165d7f8 --- /dev/null +++ b/src/transport.test.ts @@ -0,0 +1,366 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { createPerplexityServer } from "./server.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import express from "express"; +import cors from "cors"; +import { Server } from "http"; + +describe("Transport Integration Tests", () => { + let originalFetch: typeof global.fetch; + let originalEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + originalFetch = global.fetch; + originalEnv = { ...process.env }; + process.env.PERPLEXITY_API_KEY = "test-api-key"; + }); + + afterEach(() => { + global.fetch = originalFetch; + process.env = originalEnv; + vi.restoreAllMocks(); + }); + + describe("Server Factory", () => { + it("should create a server with all tools registered", () => { + const server = createPerplexityServer(); + + expect(server).toBeDefined(); + // The server should be the underlying Server instance from McpServer + expect(typeof server.connect).toBe("function"); + expect(typeof server.close).toBe("function"); + }); + + it("should fail if PERPLEXITY_API_KEY is not set", () => { + delete process.env.PERPLEXITY_API_KEY; + + // The server creation itself doesn't fail, but tool calls should fail + const server = createPerplexityServer(); + expect(server).toBeDefined(); + }); + }); + + describe("STDIO Transport", () => { + it("should connect successfully to STDIO transport", async () => { + const server = createPerplexityServer(); + const transport = new StdioServerTransport(); + + // Mock the transport connection + const connectSpy = vi.spyOn(transport, 'start').mockResolvedValue(undefined); + const closeSpy = vi.spyOn(transport, 'close').mockImplementation(() => Promise.resolve()); + + await server.connect(transport); + + expect(connectSpy).toHaveBeenCalled(); + + // Clean up + transport.close(); + server.close(); + }); + + it("should handle STDIO transport errors gracefully", async () => { + const server = createPerplexityServer(); + const transport = new StdioServerTransport(); + + // Mock transport to throw error + vi.spyOn(transport, 'start').mockRejectedValue(new Error("Transport error")); + + await expect(server.connect(transport)).rejects.toThrow("Transport error"); + + server.close(); + }); + }); + + describe("HTTP Transport", () => { + let httpServer: Server; + let app: express.Application; + + beforeEach(() => { + app = express(); + app.use(cors({ + origin: "*", + exposedHeaders: ["Mcp-Session-Id", "mcp-protocol-version"], + allowedHeaders: ["Content-Type", "mcp-session-id"], + })); + app.use(express.json()); + }); + + afterEach(async () => { + if (httpServer) { + await new Promise((resolve) => { + httpServer.close(() => resolve()); + }); + } + }); + + it("should handle HTTP MCP requests with real transport", async () => { + // Set up proper MCP endpoint with real transport + app.post("/mcp", async (req, res) => { + try { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + + res.on("close", () => { + transport.close(); + }); + + const server = createPerplexityServer(); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error("Error handling MCP request:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal server error" }, + id: null, + }); + } + } + }); + + httpServer = app.listen(0); + const address = httpServer.address(); + const port = typeof address === 'object' && address ? address.port : 3000; + + // Make a real MCP tools/list request + const response = await fetch(`http://localhost:${port}/mcp`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "tools/list", + params: {} + }), + }); + + expect(response.ok).toBe(true); + const data = await response.json(); + + // Verify proper MCP response structure + expect(data.jsonrpc).toBe("2.0"); + expect(data.id).toBe(1); + expect(data.result).toBeDefined(); + expect(data.result.tools).toBeDefined(); + expect(data.result.tools).toHaveLength(4); + + // Verify all four tools are present + const toolNames = data.result.tools.map((t: any) => t.name); + expect(toolNames).toContain("perplexity_ask"); + expect(toolNames).toContain("perplexity_research"); + expect(toolNames).toContain("perplexity_reason"); + expect(toolNames).toContain("perplexity_search"); + + // Verify tool schema structure + expect(data.result.tools[0].inputSchema).toBeDefined(); + expect(data.result.tools[0].description).toBeDefined(); + }); + + it("should handle tool calls via HTTP with real transport", async () => { + // This test verifies the HTTP transport layer works correctly + // Tool execution logic is already tested in index.test.ts + + // Set up proper MCP endpoint with real transport + app.post("/mcp", async (req, res) => { + try { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + + res.on("close", () => { + transport.close(); + }); + + const server = createPerplexityServer(); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error("Error handling tool call:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal server error" }, + id: null, + }); + } + } + }); + + httpServer = app.listen(0); + const address = httpServer.address(); + const port = typeof address === 'object' && address ? address.port : 3000; + + // Test with an invalid tool call to verify error handling + const response = await fetch(`http://localhost:${port}/mcp`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { + name: "nonexistent_tool", + arguments: {} + } + }), + }); + + expect(response.ok).toBe(true); // MCP errors are 200 OK with error in body + const data = await response.json(); + + // Verify proper MCP error response structure + expect(data).toBeDefined(); + expect(data.jsonrpc).toBe("2.0"); + expect(data.id).toBe(2); + + // McpServer returns tool errors as result.isError, not top-level error + expect(data.result).toBeDefined(); + expect(data.result.isError).toBe(true); + expect(data.result.content[0].text).toContain("not found"); + }); + + it("should handle HTTP errors properly", async () => { + app.post("/mcp", async (req, res) => { + res.status(400).json({ + jsonrpc: "2.0", + error: { code: -32600, message: "Invalid Request" }, + id: null, + }); + }); + + httpServer = app.listen(0); + const address = httpServer.address(); + const port = typeof address === 'object' && address ? address.port : 3000; + + const response = await fetch(`http://localhost:${port}/mcp`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "invalid/method", + params: {} + }), + }); + + expect(response.status).toBe(400); + const data = await response.json(); + expect(data.error.message).toBe("Invalid Request"); + }); + + it("should require proper Accept headers", async () => { + app.post("/mcp", async (req, res) => { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + + try { + const server = createPerplexityServer(); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (error) { + res.status(406).json({ + jsonrpc: "2.0", + error: { code: -32000, message: "Not Acceptable" }, + id: null, + }); + } + }); + + httpServer = app.listen(0); + const address = httpServer.address(); + const port = typeof address === 'object' && address ? address.port : 3000; + + // Test without proper Accept header + const response = await fetch(`http://localhost:${port}/mcp`, { + method: "POST", + headers: { + "Content-Type": "application/json", + // Missing Accept header + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "tools/list", + params: {} + }), + }); + + expect(response.status).toBe(406); + }); + }); + + describe("Transport Comparison", () => { + it("should produce identical results for both transports", async () => { + const mockResponse = { + choices: [{ message: { content: "Identical response" } }] + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => mockResponse, + } as Response); + + // Test STDIO (we can't easily test the actual transport, but we can test the server) + const server1 = createPerplexityServer(); + const server2 = createPerplexityServer(); + + // Both should be identical server instances with same capabilities + expect(server1).toBeDefined(); + expect(server2).toBeDefined(); + + // Clean up + server1.close(); + server2.close(); + }); + }); + + describe("Health Check", () => { + let healthApp: express.Application; + let healthHttpServer: Server; + + beforeEach(() => { + healthApp = express(); + }); + + afterEach(async () => { + if (healthHttpServer) { + await new Promise((resolve) => { + healthHttpServer.close(() => resolve()); + }); + } + }); + + it("should provide health check endpoint for HTTP mode", async () => { + healthApp.get("/health", (req: express.Request, res: express.Response) => { + res.json({ status: "ok", service: "perplexity-mcp-server" }); + }); + + healthHttpServer = healthApp.listen(0); + const address = healthHttpServer.address(); + const port = typeof address === 'object' && address ? address.port : 3000; + + const response = await fetch(`http://localhost:${port}/health`); + expect(response.ok).toBe(true); + + const data = await response.json(); + expect(data.status).toBe("ok"); + expect(data.service).toBe("perplexity-mcp-server"); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index ba5c7b7..190e48e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "target": "ES2015", + "target": "ES2020", "module": "ESNext", "outDir": "./dist", - "rootDir": ".", + "rootDir": "./src", "strict": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, @@ -11,6 +11,6 @@ "moduleResolution": "node" }, "include": [ - "./**/*.ts" + "src/**/*.ts" ] }