diff --git a/README.md b/README.md index 7908427..e05f42a 100644 --- a/README.md +++ b/README.md @@ -111,12 +111,51 @@ For any MCP-compatible client, use: npx @perplexity-ai/mcp-server ``` +### Proxy Setup (For Corporate Networks) + +If you are running this server at work—especially behind a company firewall or proxy—you may need to tell the program how to send its internet traffic through your network's proxy. Follow these steps: + +**1. Get your proxy details** + +- Ask your IT department for your HTTP(S) proxy address and port. +- You may also need a username and password. + +**2. Set the proxy environment variable** + +The easiest and most reliable way for Perplexity MCP is to use `PERPLEXITY_PROXY`. For example: + +```bash +export PERPLEXITY_PROXY=http://your-proxy-host:8080 +``` + +- If your proxy needs a username and password, use: + ```bash + export PERPLEXITY_PROXY=http://username:password@your-proxy-host:8080 + ``` + +**3. Alternate: Standard environment variables** + +If you'd rather use the standard variables, we support `HTTPS_PROXY` and `HTTP_PROXY`. + +> [!NOTE] +>The server checks proxy settings in this order: `PERPLEXITY_PROXY` → `HTTPS_PROXY` → `HTTP_PROXY`. If none are set, it connects directly to the internet. + ## Troubleshooting - **API Key Issues**: Ensure `PERPLEXITY_API_KEY` is set correctly - **Connection Errors**: Check your internet connection and API key validity - **Tool Not Found**: Make sure the package is installed and the command path is correct - **Timeout Errors**: For very long research queries, set `PERPLEXITY_TIMEOUT_MS` to a higher value +- **Proxy Issues**: If you're behind a corporate firewall and experience connection errors, you likely need to set up a proxy: + - Obtain your proxy server address and port from your IT department. + - Set the environment variable before running the server, e.g.: + - `export PERPLEXITY_PROXY=http://proxy-address:port` + - If authentication is needed: `export PERPLEXITY_PROXY=http://username:password@proxy-address:port` + - Typical proxy ports include 8080, 3128, or 80. + - The format for authenticated proxies is: + `http://username:password@proxy-host:port` + - Double-check the address, port, and credentials if connections fail or time out. + - If you continue to have issues, your firewall may be blocking traffic; ask IT if traffic for `api.perplexity.ai` is being restricted. For support, visit [community.perplexity.ai](https://community.perplexity.ai) or [file an issue](https://github.com/perplexityai/modelcontextprotocol/issues). diff --git a/index.test.ts b/index.test.ts index f0999c7..f7b98d8 100644 --- a/index.test.ts +++ b/index.test.ts @@ -665,4 +665,65 @@ describe("Perplexity MCP Server", () => { expect(resultKept).toContain("The answer is 4."); }); }); + + describe("Proxy Support", () => { + const originalEnv = process.env; + + beforeEach(() => { + // Reset environment variables + process.env = { ...originalEnv }; + delete process.env.PERPLEXITY_PROXY; + delete process.env.HTTPS_PROXY; + delete process.env.HTTP_PROXY; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it("should use native fetch when no proxy is configured", async () => { + const mockResponse = { + choices: [{ message: { content: "Test response" } }], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => mockResponse, + } as Response); + + const messages = [{ role: "user", content: "test" }]; + await performChatCompletion(messages); + + // Verify native fetch was called (not undici) + expect(global.fetch).toHaveBeenCalled(); + }); + + it("should read PERPLEXITY_PROXY environment variable", () => { + process.env.PERPLEXITY_PROXY = "http://proxy.example.com:8080"; + expect(process.env.PERPLEXITY_PROXY).toBe("http://proxy.example.com:8080"); + }); + + it("should prioritize PERPLEXITY_PROXY over HTTPS_PROXY", () => { + process.env.PERPLEXITY_PROXY = "http://perplexity-proxy.example.com:8080"; + process.env.HTTPS_PROXY = "http://https-proxy.example.com:8080"; + + // PERPLEXITY_PROXY should take precedence + expect(process.env.PERPLEXITY_PROXY).toBe("http://perplexity-proxy.example.com:8080"); + }); + + it("should fall back to HTTPS_PROXY when PERPLEXITY_PROXY is not set", () => { + delete process.env.PERPLEXITY_PROXY; + process.env.HTTPS_PROXY = "http://https-proxy.example.com:8080"; + + expect(process.env.HTTPS_PROXY).toBe("http://https-proxy.example.com:8080"); + }); + + it("should fall back to HTTP_PROXY when others are not set", () => { + delete process.env.PERPLEXITY_PROXY; + delete process.env.HTTPS_PROXY; + process.env.HTTP_PROXY = "http://http-proxy.example.com:8080"; + + expect(process.env.HTTP_PROXY).toBe("http://http-proxy.example.com:8080"); + }); + }); }); diff --git a/index.ts b/index.ts index 5e7fa33..be38ac3 100644 --- a/index.ts +++ b/index.ts @@ -7,6 +7,7 @@ import { ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; +import { fetch as undiciFetch, ProxyAgent } from "undici"; /** * Definition of the Perplexity Ask Tool. @@ -169,6 +170,45 @@ if (!PERPLEXITY_API_KEY) { 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. @@ -240,7 +280,7 @@ export async function performChatCompletion( let response; try { - response = await fetch(url.toString(), { + response = await proxyAwareFetch(url.toString(), { method: "POST", headers: { "Content-Type": "application/json", @@ -371,7 +411,7 @@ export async function performSearch( let response; try { - response = await fetch(url.toString(), { + response = await proxyAwareFetch(url.toString(), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/package-lock.json b/package-lock.json index f1cc2e0..49d7913 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.21.1", - "dotenv": "^16.6.1" + "dotenv": "^16.6.1", + "undici": "^6.20.0" }, "bin": { "perplexity-mcp": "dist/index.js" @@ -3160,6 +3161,15 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", + "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", diff --git a/package.json b/package.json index 9727059..33b771d 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.21.1", - "dotenv": "^16.6.1" + "dotenv": "^16.6.1", + "undici": "^6.20.0" }, "devDependencies": { "@types/node": "^20",