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