@perplexity-ai/mcp-server 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/README.md +46 -82
- package/dist/http.js +8 -21
- package/dist/index.js +1 -7
- package/dist/logger.js +82 -0
- package/dist/server.js +51 -117
- package/dist/types.js +1 -0
- package/dist/validation.js +38 -0
- package/package.json +1 -1
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Official Perplexity AI plugin providing real-time web search, reasoning, and research capabilities",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.6.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "perplexity",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Real-time web search, reasoning, and research through Perplexity's API",
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.6.0",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Perplexity AI",
|
|
19
19
|
"email": "api@perplexity.ai"
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Perplexity API Platform MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://cursor.com/en/install-mcp?name=perplexity&config=
|
|
3
|
+
[](https://cursor.com/en/install-mcp?name=perplexity&config=eyJ0eXBlIjoic3RkaW8iLCJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBwZXJwbGV4aXR5LWFpL21jcC1zZXJ2ZXIiXSwiZW52Ijp7IlBFUlBMRVhJVFlfQVBJX0tFWSI6IiJ9fQ==)
|
|
4
4
|
|
|
5
|
-
[](https://vscode.dev/redirect/mcp/install?name=perplexity&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40perplexity-ai%2Fmcp-server%22%5D%7D)
|
|
5
|
+
[](https://vscode.dev/redirect/mcp/install?name=perplexity&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40perplexity-ai%2Fmcp-server%22%5D%2C%22env%22%3A%7B%22PERPLEXITY_API_KEY%22%3A%22%22%7D%7D)
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/@perplexity-ai/mcp-server)
|
|
8
8
|
|
|
@@ -13,7 +13,7 @@ The official MCP server implementation for the Perplexity API Platform, providin
|
|
|
13
13
|
### **perplexity_search**
|
|
14
14
|
Direct web search using the Perplexity Search API. Returns ranked search results with metadata, perfect for finding current information.
|
|
15
15
|
|
|
16
|
-
### **perplexity_ask**
|
|
16
|
+
### **perplexity_ask**
|
|
17
17
|
General-purpose conversational AI with real-time web search using the `sonar-pro` model. Great for quick questions and everyday searches.
|
|
18
18
|
|
|
19
19
|
### **perplexity_research**
|
|
@@ -30,60 +30,37 @@ Advanced reasoning and problem-solving using the `sonar-reasoning-pro` model. Pe
|
|
|
30
30
|
## Configuration
|
|
31
31
|
|
|
32
32
|
### Get Your API Key
|
|
33
|
+
|
|
33
34
|
1. Get your Perplexity API Key from the [API Portal](https://www.perplexity.ai/account/api/group)
|
|
34
|
-
2.
|
|
35
|
-
3. (Optional) Set
|
|
35
|
+
2. Replace `your_key_here` in the configurations below with your API key
|
|
36
|
+
3. (Optional) Set timeout: `PERPLEXITY_TIMEOUT_MS=600000` (default: 5 minutes)
|
|
37
|
+
4. (Optional) Set log level: `PERPLEXITY_LOG_LEVEL=DEBUG|INFO|WARN|ERROR` (default: ERROR)
|
|
36
38
|
|
|
37
39
|
### Claude Code
|
|
38
40
|
|
|
39
|
-
#### Option 1: Install via Plugin (Recommended)
|
|
40
|
-
|
|
41
|
-
The easiest way to get started with Perplexity in Claude Code, set your API key:
|
|
42
|
-
```bash
|
|
43
|
-
export PERPLEXITY_API_KEY="your_key_here"
|
|
44
|
-
```
|
|
45
|
-
Then:
|
|
46
41
|
```bash
|
|
47
|
-
|
|
48
|
-
claude
|
|
49
|
-
|
|
50
|
-
# Add the Perplexity marketplace
|
|
51
|
-
/plugin marketplace add perplexityai/modelcontextprotocol
|
|
52
|
-
|
|
53
|
-
# Install the plugin
|
|
54
|
-
/plugin install perplexity
|
|
42
|
+
claude mcp add perplexity --env PERPLEXITY_API_KEY="your_key_here" -- npx -y @perplexity-ai/mcp-server
|
|
55
43
|
```
|
|
56
44
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Run in your terminal:
|
|
60
|
-
|
|
45
|
+
Or install via plugin:
|
|
61
46
|
```bash
|
|
62
|
-
|
|
47
|
+
export PERPLEXITY_API_KEY="your_key_here"
|
|
48
|
+
claude
|
|
49
|
+
# Then run: /plugin marketplace add perplexityai/modelcontextprotocol
|
|
50
|
+
# Then run: /plugin install perplexity
|
|
63
51
|
```
|
|
64
52
|
|
|
65
|
-
|
|
53
|
+
### Cursor, Claude Desktop & Windsurf
|
|
66
54
|
|
|
67
|
-
|
|
68
|
-
"mcpServers": {
|
|
69
|
-
"perplexity": {
|
|
70
|
-
"type": "stdio",
|
|
71
|
-
"command": "npx",
|
|
72
|
-
"args": [
|
|
73
|
-
"-y",
|
|
74
|
-
"perplexity-mcp"
|
|
75
|
-
],
|
|
76
|
-
"env": {
|
|
77
|
-
"PERPLEXITY_API_KEY": "your_key_here",
|
|
78
|
-
"PERPLEXITY_TIMEOUT_MS": "600000"
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
```
|
|
55
|
+
We recommend using the one-click install badge at the top of this README for Cursor.
|
|
83
56
|
|
|
84
|
-
|
|
57
|
+
For manual setup, all these clients use the same `mcpServers` format:
|
|
85
58
|
|
|
86
|
-
|
|
59
|
+
| Client | Config File |
|
|
60
|
+
|--------|-------------|
|
|
61
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
62
|
+
| Claude Desktop | `claude_desktop_config.json` |
|
|
63
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
|
|
87
64
|
|
|
88
65
|
```json
|
|
89
66
|
{
|
|
@@ -92,51 +69,42 @@ Add to your `mcp.json` (Cursor) or `.vscode/mcp.json` (VS Code):
|
|
|
92
69
|
"command": "npx",
|
|
93
70
|
"args": ["-y", "@perplexity-ai/mcp-server"],
|
|
94
71
|
"env": {
|
|
95
|
-
"PERPLEXITY_API_KEY": "your_key_here"
|
|
96
|
-
"PERPLEXITY_TIMEOUT_MS": "600000"
|
|
72
|
+
"PERPLEXITY_API_KEY": "your_key_here"
|
|
97
73
|
}
|
|
98
74
|
}
|
|
99
75
|
}
|
|
100
76
|
}
|
|
101
77
|
```
|
|
102
78
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
### Codex
|
|
79
|
+
### VS Code
|
|
106
80
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
codex mcp add perplexity --env PERPLEXITY_API_KEY=your_key_here -- npx -y @perplexity-ai/mcp-server
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### Claude Desktop
|
|
114
|
-
|
|
115
|
-
Add to your `claude_desktop_config.json`:
|
|
81
|
+
We recommend using the one-click install badge at the top of this README for VS Code, or for manual setup, add to `.vscode/mcp.json`:
|
|
116
82
|
|
|
117
83
|
```json
|
|
118
84
|
{
|
|
119
|
-
"
|
|
85
|
+
"servers": {
|
|
120
86
|
"perplexity": {
|
|
87
|
+
"type": "stdio",
|
|
121
88
|
"command": "npx",
|
|
122
89
|
"args": ["-y", "@perplexity-ai/mcp-server"],
|
|
123
90
|
"env": {
|
|
124
|
-
"PERPLEXITY_API_KEY": "your_key_here"
|
|
125
|
-
"PERPLEXITY_TIMEOUT_MS": "600000"
|
|
91
|
+
"PERPLEXITY_API_KEY": "your_key_here"
|
|
126
92
|
}
|
|
127
93
|
}
|
|
128
94
|
}
|
|
129
95
|
}
|
|
130
96
|
```
|
|
131
97
|
|
|
132
|
-
###
|
|
133
|
-
|
|
134
|
-
For any MCP-compatible client, use:
|
|
98
|
+
### Codex
|
|
135
99
|
|
|
136
100
|
```bash
|
|
137
|
-
npx @perplexity-ai/mcp-server
|
|
101
|
+
codex mcp add perplexity --env PERPLEXITY_API_KEY="your_key_here" -- npx -y @perplexity-ai/mcp-server
|
|
138
102
|
```
|
|
139
103
|
|
|
104
|
+
### Other MCP Clients
|
|
105
|
+
|
|
106
|
+
Most clients can be manually configured to use the `mcpServers` wrapper in their configuration file (like Cursor). If your client doesn't work, check its documentation for the correct wrapper format.
|
|
107
|
+
|
|
140
108
|
### Proxy Setup (For Corporate Networks)
|
|
141
109
|
|
|
142
110
|
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:
|
|
@@ -168,39 +136,34 @@ If you'd rather use the standard variables, we support `HTTPS_PROXY` and `HTTP_P
|
|
|
168
136
|
> The server checks proxy settings in this order: `PERPLEXITY_PROXY` → `HTTPS_PROXY` → `HTTP_PROXY`. If none are set, it connects directly to the internet.
|
|
169
137
|
> URLs must include `https://`. Typical ports are `8080`, `3128`, and `80`.
|
|
170
138
|
|
|
171
|
-
|
|
172
139
|
### HTTP Server Deployment
|
|
173
140
|
|
|
174
|
-
For cloud or shared deployments,
|
|
141
|
+
For cloud or shared deployments, run the server in HTTP mode.
|
|
175
142
|
|
|
176
143
|
#### Environment Variables
|
|
177
144
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
145
|
+
| Variable | Description | Default |
|
|
146
|
+
|----------|-------------|---------|
|
|
147
|
+
| `PERPLEXITY_API_KEY` | Your Perplexity API key | *Required* |
|
|
148
|
+
| `PORT` | HTTP server port | `8080` |
|
|
149
|
+
| `BIND_ADDRESS` | Network interface to bind to | `0.0.0.0` |
|
|
150
|
+
| `ALLOWED_ORIGINS` | CORS origins (comma-separated) | `*` |
|
|
184
151
|
|
|
185
|
-
####
|
|
152
|
+
#### Docker
|
|
186
153
|
|
|
187
154
|
```bash
|
|
188
155
|
docker build -t perplexity-mcp-server .
|
|
189
156
|
docker run -p 8080:8080 -e PERPLEXITY_API_KEY=your_key_here perplexity-mcp-server
|
|
190
157
|
```
|
|
191
158
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
#### Using Node.js Directly
|
|
159
|
+
#### Node.js
|
|
195
160
|
|
|
196
161
|
```bash
|
|
197
|
-
|
|
198
|
-
npm run build
|
|
199
|
-
npm run start:http
|
|
162
|
+
export PERPLEXITY_API_KEY=your_key_here
|
|
163
|
+
npm install && npm run build && npm run start:http
|
|
200
164
|
```
|
|
201
165
|
|
|
202
|
-
|
|
203
|
-
|
|
166
|
+
The server will be accessible at `http://localhost:8080/mcp`
|
|
204
167
|
|
|
205
168
|
## Troubleshooting
|
|
206
169
|
|
|
@@ -209,6 +172,7 @@ Connect your MCP client to: `http://localhost:8080/mcp`
|
|
|
209
172
|
- **Tool Not Found**: Make sure the package is installed and the command path is correct
|
|
210
173
|
- **Timeout Errors**: For very long research queries, set `PERPLEXITY_TIMEOUT_MS` to a higher value
|
|
211
174
|
- **Proxy Issues**: Verify your `PERPLEXITY_PROXY` or `HTTPS_PROXY` setup and ensure `api.perplexity.ai` isn't blocked by your firewall.
|
|
175
|
+
- **EOF / Initialize Errors**: Some strict MCP clients fail because `npx` writes installation messages to stdout. Use `npx -yq` instead of `npx -y` to suppress this output.
|
|
212
176
|
|
|
213
177
|
For support, visit [community.perplexity.ai](https://community.perplexity.ai) or [file an issue](https://github.com/perplexityai/modelcontextprotocol/issues).
|
|
214
178
|
|
package/dist/http.js
CHANGED
|
@@ -3,19 +3,16 @@ import express from "express";
|
|
|
3
3
|
import cors from "cors";
|
|
4
4
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
5
|
import { createPerplexityServer } from "./server.js";
|
|
6
|
-
|
|
6
|
+
import { logger } from "./logger.js";
|
|
7
7
|
const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
|
|
8
8
|
if (!PERPLEXITY_API_KEY) {
|
|
9
|
-
|
|
9
|
+
logger.error("PERPLEXITY_API_KEY environment variable is required");
|
|
10
10
|
process.exit(1);
|
|
11
11
|
}
|
|
12
12
|
const app = express();
|
|
13
13
|
const PORT = parseInt(process.env.PORT || "8080", 10);
|
|
14
|
-
const BIND_ADDRESS = process.env.BIND_ADDRESS || "
|
|
15
|
-
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(",") || [
|
|
16
|
-
"http://localhost:3000",
|
|
17
|
-
"http://127.0.0.1:3000",
|
|
18
|
-
];
|
|
14
|
+
const BIND_ADDRESS = process.env.BIND_ADDRESS || "0.0.0.0";
|
|
15
|
+
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(",") || ["*"];
|
|
19
16
|
// CORS configuration for browser-based MCP clients
|
|
20
17
|
app.use(cors({
|
|
21
18
|
origin: (origin, callback) => {
|
|
@@ -36,10 +33,6 @@ app.use(cors({
|
|
|
36
33
|
}));
|
|
37
34
|
app.use(express.json());
|
|
38
35
|
const mcpServer = createPerplexityServer();
|
|
39
|
-
/**
|
|
40
|
-
* POST: client-to-server messages (requests, responses, notifications)
|
|
41
|
-
* GET: SSE stream for server-to-client messages (notifications, requests)
|
|
42
|
-
*/
|
|
43
36
|
app.all("/mcp", async (req, res) => {
|
|
44
37
|
try {
|
|
45
38
|
const transport = new StreamableHTTPServerTransport({
|
|
@@ -53,7 +46,7 @@ app.all("/mcp", async (req, res) => {
|
|
|
53
46
|
await transport.handleRequest(req, res, req.body);
|
|
54
47
|
}
|
|
55
48
|
catch (error) {
|
|
56
|
-
|
|
49
|
+
logger.error("Error handling MCP request", { error: String(error) });
|
|
57
50
|
if (!res.headersSent) {
|
|
58
51
|
res.status(500).json({
|
|
59
52
|
jsonrpc: "2.0",
|
|
@@ -63,19 +56,13 @@ app.all("/mcp", async (req, res) => {
|
|
|
63
56
|
}
|
|
64
57
|
}
|
|
65
58
|
});
|
|
66
|
-
/**
|
|
67
|
-
* Health check endpoint
|
|
68
|
-
*/
|
|
69
59
|
app.get("/health", (req, res) => {
|
|
70
60
|
res.json({ status: "ok", service: "perplexity-mcp-server" });
|
|
71
61
|
});
|
|
72
|
-
/**
|
|
73
|
-
* Start the HTTP server
|
|
74
|
-
*/
|
|
75
62
|
app.listen(PORT, BIND_ADDRESS, () => {
|
|
76
|
-
|
|
77
|
-
|
|
63
|
+
logger.info(`Perplexity MCP Server listening on http://${BIND_ADDRESS}:${PORT}/mcp`);
|
|
64
|
+
logger.info(`Allowed origins: ${ALLOWED_ORIGINS.join(", ")}`);
|
|
78
65
|
}).on("error", (error) => {
|
|
79
|
-
|
|
66
|
+
logger.error("Server error", { error: String(error) });
|
|
80
67
|
process.exit(1);
|
|
81
68
|
});
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { createPerplexityServer } from "./server.js";
|
|
4
|
-
// Check for required API key
|
|
5
4
|
const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
|
|
6
5
|
if (!PERPLEXITY_API_KEY) {
|
|
7
6
|
console.error("Error: PERPLEXITY_API_KEY environment variable is required");
|
|
8
7
|
process.exit(1);
|
|
9
8
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Initializes and runs the server using standard I/O for communication.
|
|
12
|
-
* Logs an error and exits if the server fails to start.
|
|
13
|
-
*/
|
|
14
9
|
async function main() {
|
|
15
10
|
try {
|
|
16
|
-
const server = createPerplexityServer();
|
|
11
|
+
const server = createPerplexityServer("local-mcp");
|
|
17
12
|
const transport = new StdioServerTransport();
|
|
18
13
|
await server.connect(transport);
|
|
19
14
|
}
|
|
@@ -22,7 +17,6 @@ async function main() {
|
|
|
22
17
|
process.exit(1);
|
|
23
18
|
}
|
|
24
19
|
}
|
|
25
|
-
// Start the server and catch any startup errors
|
|
26
20
|
main().catch((error) => {
|
|
27
21
|
console.error("Fatal error running server:", error);
|
|
28
22
|
process.exit(1);
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple structured logger for the Perplexity MCP Server
|
|
3
|
+
* Outputs to stderr to avoid interfering with STDIO transport
|
|
4
|
+
*/
|
|
5
|
+
export var LogLevel;
|
|
6
|
+
(function (LogLevel) {
|
|
7
|
+
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
8
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
9
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
10
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
11
|
+
})(LogLevel || (LogLevel = {}));
|
|
12
|
+
const LOG_LEVEL_NAMES = {
|
|
13
|
+
[LogLevel.DEBUG]: "DEBUG",
|
|
14
|
+
[LogLevel.INFO]: "INFO",
|
|
15
|
+
[LogLevel.WARN]: "WARN",
|
|
16
|
+
[LogLevel.ERROR]: "ERROR",
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Gets the configured log level from environment variable
|
|
20
|
+
* Defaults to ERROR to minimize noise in production
|
|
21
|
+
*/
|
|
22
|
+
function getLogLevel() {
|
|
23
|
+
const level = process.env.PERPLEXITY_LOG_LEVEL?.toUpperCase();
|
|
24
|
+
switch (level) {
|
|
25
|
+
case "DEBUG":
|
|
26
|
+
return LogLevel.DEBUG;
|
|
27
|
+
case "INFO":
|
|
28
|
+
return LogLevel.INFO;
|
|
29
|
+
case "WARN":
|
|
30
|
+
return LogLevel.WARN;
|
|
31
|
+
case "ERROR":
|
|
32
|
+
return LogLevel.ERROR;
|
|
33
|
+
default:
|
|
34
|
+
return LogLevel.ERROR;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const currentLogLevel = getLogLevel();
|
|
38
|
+
function safeStringify(obj) {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.stringify(obj);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return "[Unstringifiable]";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Formats a log message with timestamp and level
|
|
48
|
+
*/
|
|
49
|
+
function formatMessage(level, message, meta) {
|
|
50
|
+
const timestamp = new Date().toISOString();
|
|
51
|
+
const levelName = LOG_LEVEL_NAMES[level];
|
|
52
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
53
|
+
return `[${timestamp}] ${levelName}: ${message} ${safeStringify(meta)}`;
|
|
54
|
+
}
|
|
55
|
+
return `[${timestamp}] ${levelName}: ${message}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Logs a message if the configured log level allows it
|
|
59
|
+
*/
|
|
60
|
+
function log(level, message, meta) {
|
|
61
|
+
if (level >= currentLogLevel) {
|
|
62
|
+
const formatted = formatMessage(level, message, meta);
|
|
63
|
+
console.error(formatted); // Use stderr to avoid interfering with STDIO
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Structured logger interface
|
|
68
|
+
*/
|
|
69
|
+
export const logger = {
|
|
70
|
+
debug(message, meta) {
|
|
71
|
+
log(LogLevel.DEBUG, message, meta);
|
|
72
|
+
},
|
|
73
|
+
info(message, meta) {
|
|
74
|
+
log(LogLevel.INFO, message, meta);
|
|
75
|
+
},
|
|
76
|
+
warn(message, meta) {
|
|
77
|
+
log(LogLevel.WARN, message, meta);
|
|
78
|
+
},
|
|
79
|
+
error(message, meta) {
|
|
80
|
+
log(LogLevel.ERROR, message, meta);
|
|
81
|
+
},
|
|
82
|
+
};
|
package/dist/server.js
CHANGED
|
@@ -1,54 +1,28 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { fetch as undiciFetch, ProxyAgent } from "undici";
|
|
4
|
-
|
|
4
|
+
import { ChatCompletionResponseSchema, SearchResponseSchema } from "./validation.js";
|
|
5
5
|
const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
|
|
6
|
-
|
|
7
|
-
* Gets the proxy URL from environment variables.
|
|
8
|
-
* Checks PERPLEXITY_PROXY, HTTPS_PROXY, HTTP_PROXY in order.
|
|
9
|
-
*
|
|
10
|
-
* @returns {string | undefined} The proxy URL if configured, undefined otherwise
|
|
11
|
-
*/
|
|
12
|
-
function getProxyUrl() {
|
|
6
|
+
export function getProxyUrl() {
|
|
13
7
|
return process.env.PERPLEXITY_PROXY ||
|
|
14
8
|
process.env.HTTPS_PROXY ||
|
|
15
9
|
process.env.HTTP_PROXY ||
|
|
16
10
|
undefined;
|
|
17
11
|
}
|
|
18
|
-
|
|
19
|
-
* Creates a proxy-aware fetch function.
|
|
20
|
-
* Uses undici with ProxyAgent when a proxy is configured, otherwise uses native fetch.
|
|
21
|
-
*
|
|
22
|
-
* @param {string} url - The URL to fetch
|
|
23
|
-
* @param {RequestInit} options - Fetch options
|
|
24
|
-
* @returns {Promise<Response>} The fetch response
|
|
25
|
-
*/
|
|
26
|
-
async function proxyAwareFetch(url, options = {}) {
|
|
12
|
+
export async function proxyAwareFetch(url, options = {}) {
|
|
27
13
|
const proxyUrl = getProxyUrl();
|
|
28
14
|
if (proxyUrl) {
|
|
29
|
-
// Use undici with ProxyAgent when proxy is configured
|
|
30
15
|
const proxyAgent = new ProxyAgent(proxyUrl);
|
|
31
|
-
const
|
|
16
|
+
const undiciOptions = {
|
|
32
17
|
...options,
|
|
33
18
|
dispatcher: proxyAgent,
|
|
34
|
-
}
|
|
35
|
-
|
|
19
|
+
};
|
|
20
|
+
const response = await undiciFetch(url, undiciOptions);
|
|
36
21
|
return response;
|
|
37
22
|
}
|
|
38
|
-
|
|
39
|
-
// Use native fetch when no proxy is configured
|
|
40
|
-
return fetch(url, options);
|
|
41
|
-
}
|
|
23
|
+
return fetch(url, options);
|
|
42
24
|
}
|
|
43
|
-
|
|
44
|
-
* Validates an array of message objects for chat completion tools.
|
|
45
|
-
* Ensures each message has a valid role and content field.
|
|
46
|
-
*
|
|
47
|
-
* @param {any} messages - The messages to validate
|
|
48
|
-
* @param {string} toolName - The name of the tool calling this validation (for error messages)
|
|
49
|
-
* @throws {Error} If messages is not an array or if any message is invalid
|
|
50
|
-
*/
|
|
51
|
-
function validateMessages(messages, toolName) {
|
|
25
|
+
export function validateMessages(messages, toolName) {
|
|
52
26
|
if (!Array.isArray(messages)) {
|
|
53
27
|
throw new Error(`Invalid arguments for ${toolName}: 'messages' must be an array`);
|
|
54
28
|
}
|
|
@@ -65,33 +39,15 @@ function validateMessages(messages, toolName) {
|
|
|
65
39
|
}
|
|
66
40
|
}
|
|
67
41
|
}
|
|
68
|
-
|
|
69
|
-
* Strips thinking tokens (content within <think>...</think> tags) from the response.
|
|
70
|
-
* This helps reduce context usage when the thinking process is not needed.
|
|
71
|
-
*
|
|
72
|
-
* @param {string} content - The content to process
|
|
73
|
-
* @returns {string} The content with thinking tokens removed
|
|
74
|
-
*/
|
|
75
|
-
function stripThinkingTokens(content) {
|
|
42
|
+
export function stripThinkingTokens(content) {
|
|
76
43
|
return content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
|
77
44
|
}
|
|
78
|
-
|
|
79
|
-
* Performs a chat completion by sending a request to the Perplexity API.
|
|
80
|
-
* Appends citations to the returned message content if they exist.
|
|
81
|
-
*
|
|
82
|
-
* @param {Array<{ role: string; content: string }>} messages - An array of message objects.
|
|
83
|
-
* @param {string} model - The model to use for the completion.
|
|
84
|
-
* @param {boolean} stripThinking - If true, removes <think>...</think> tags from the response.
|
|
85
|
-
* @returns {Promise<string>} The chat completion result with appended citations.
|
|
86
|
-
* @throws Will throw an error if the API request fails.
|
|
87
|
-
*/
|
|
88
|
-
export async function performChatCompletion(messages, model = "sonar-pro", stripThinking = false) {
|
|
45
|
+
export async function performChatCompletion(messages, model = "sonar-pro", stripThinking = false, serviceOrigin) {
|
|
89
46
|
if (!PERPLEXITY_API_KEY) {
|
|
90
47
|
throw new Error("PERPLEXITY_API_KEY environment variable is required");
|
|
91
48
|
}
|
|
92
49
|
// Read timeout fresh each time to respect env var changes
|
|
93
50
|
const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10);
|
|
94
|
-
// Construct the API endpoint URL and request body
|
|
95
51
|
const url = new URL("https://api.perplexity.ai/chat/completions");
|
|
96
52
|
const body = {
|
|
97
53
|
model: model,
|
|
@@ -101,12 +57,16 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
|
|
|
101
57
|
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
102
58
|
let response;
|
|
103
59
|
try {
|
|
60
|
+
const headers = {
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
"Authorization": `Bearer ${PERPLEXITY_API_KEY}`,
|
|
63
|
+
};
|
|
64
|
+
if (serviceOrigin) {
|
|
65
|
+
headers["X-Service"] = serviceOrigin;
|
|
66
|
+
}
|
|
104
67
|
response = await proxyAwareFetch(url.toString(), {
|
|
105
68
|
method: "POST",
|
|
106
|
-
headers
|
|
107
|
-
"Content-Type": "application/json",
|
|
108
|
-
"Authorization": `Bearer ${PERPLEXITY_API_KEY}`,
|
|
109
|
-
},
|
|
69
|
+
headers,
|
|
110
70
|
body: JSON.stringify(body),
|
|
111
71
|
signal: controller.signal,
|
|
112
72
|
});
|
|
@@ -119,7 +79,6 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
|
|
|
119
79
|
}
|
|
120
80
|
throw new Error(`Network error while calling Perplexity API: ${error}`);
|
|
121
81
|
}
|
|
122
|
-
// Check for non-successful HTTP status
|
|
123
82
|
if (!response.ok) {
|
|
124
83
|
let errorText;
|
|
125
84
|
try {
|
|
@@ -130,29 +89,28 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
|
|
|
130
89
|
}
|
|
131
90
|
throw new Error(`Perplexity API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
132
91
|
}
|
|
133
|
-
// Attempt to parse the JSON response from the API
|
|
134
92
|
let data;
|
|
135
93
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
catch (jsonError) {
|
|
139
|
-
throw new Error(`Failed to parse JSON response from Perplexity API: ${jsonError}`);
|
|
94
|
+
const json = await response.json();
|
|
95
|
+
data = ChatCompletionResponseSchema.parse(json);
|
|
140
96
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof z.ZodError) {
|
|
99
|
+
const issues = error.issues;
|
|
100
|
+
if (issues.some(i => i.path.includes('message') || i.path.includes('content'))) {
|
|
101
|
+
throw new Error("Invalid API response: missing message content");
|
|
102
|
+
}
|
|
103
|
+
if (issues.some(i => i.path.includes('choices'))) {
|
|
104
|
+
throw new Error("Invalid API response: missing or empty choices array");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`Failed to parse JSON response from Perplexity API: ${error}`);
|
|
144
108
|
}
|
|
145
109
|
const firstChoice = data.choices[0];
|
|
146
|
-
if (!firstChoice.message || typeof firstChoice.message.content !== 'string') {
|
|
147
|
-
throw new Error("Invalid API response: missing message content");
|
|
148
|
-
}
|
|
149
|
-
// Directly retrieve the main message content from the response
|
|
150
110
|
let messageContent = firstChoice.message.content;
|
|
151
|
-
// Strip thinking tokens if requested
|
|
152
111
|
if (stripThinking) {
|
|
153
112
|
messageContent = stripThinkingTokens(messageContent);
|
|
154
113
|
}
|
|
155
|
-
// If citations are provided, append them to the message content
|
|
156
114
|
if (data.citations && Array.isArray(data.citations) && data.citations.length > 0) {
|
|
157
115
|
messageContent += "\n\nCitations:\n";
|
|
158
116
|
data.citations.forEach((citation, index) => {
|
|
@@ -161,12 +119,6 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
|
|
|
161
119
|
}
|
|
162
120
|
return messageContent;
|
|
163
121
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Formats search results from the Perplexity Search API into a readable string.
|
|
166
|
-
*
|
|
167
|
-
* @param {any} data - The search response data from the API.
|
|
168
|
-
* @returns {string} Formatted search results.
|
|
169
|
-
*/
|
|
170
122
|
export function formatSearchResults(data) {
|
|
171
123
|
if (!data.results || !Array.isArray(data.results)) {
|
|
172
124
|
return "No search results found.";
|
|
@@ -185,17 +137,7 @@ export function formatSearchResults(data) {
|
|
|
185
137
|
});
|
|
186
138
|
return formattedResults;
|
|
187
139
|
}
|
|
188
|
-
|
|
189
|
-
* Performs a web search using the Perplexity Search API.
|
|
190
|
-
*
|
|
191
|
-
* @param {string} query - The search query string.
|
|
192
|
-
* @param {number} maxResults - Maximum number of results to return (1-20).
|
|
193
|
-
* @param {number} maxTokensPerPage - Maximum tokens to extract per webpage.
|
|
194
|
-
* @param {string} country - Optional ISO country code for regional results.
|
|
195
|
-
* @returns {Promise<string>} The formatted search results.
|
|
196
|
-
* @throws Will throw an error if the API request fails.
|
|
197
|
-
*/
|
|
198
|
-
export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1024, country) {
|
|
140
|
+
export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1024, country, serviceOrigin) {
|
|
199
141
|
if (!PERPLEXITY_API_KEY) {
|
|
200
142
|
throw new Error("PERPLEXITY_API_KEY environment variable is required");
|
|
201
143
|
}
|
|
@@ -206,20 +148,22 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
|
|
|
206
148
|
query: query,
|
|
207
149
|
max_results: maxResults,
|
|
208
150
|
max_tokens_per_page: maxTokensPerPage,
|
|
151
|
+
...(country && { country }),
|
|
209
152
|
};
|
|
210
|
-
if (country) {
|
|
211
|
-
body.country = country;
|
|
212
|
-
}
|
|
213
153
|
const controller = new AbortController();
|
|
214
154
|
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
215
155
|
let response;
|
|
216
156
|
try {
|
|
157
|
+
const headers = {
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
"Authorization": `Bearer ${PERPLEXITY_API_KEY}`,
|
|
160
|
+
};
|
|
161
|
+
if (serviceOrigin) {
|
|
162
|
+
headers["X-Service"] = serviceOrigin;
|
|
163
|
+
}
|
|
217
164
|
response = await proxyAwareFetch(url.toString(), {
|
|
218
165
|
method: "POST",
|
|
219
|
-
headers
|
|
220
|
-
"Content-Type": "application/json",
|
|
221
|
-
"Authorization": `Bearer ${PERPLEXITY_API_KEY}`,
|
|
222
|
-
},
|
|
166
|
+
headers,
|
|
223
167
|
body: JSON.stringify(body),
|
|
224
168
|
signal: controller.signal,
|
|
225
169
|
});
|
|
@@ -232,7 +176,6 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
|
|
|
232
176
|
}
|
|
233
177
|
throw new Error(`Network error while calling Perplexity Search API: ${error}`);
|
|
234
178
|
}
|
|
235
|
-
// Check for non-successful HTTP status
|
|
236
179
|
if (!response.ok) {
|
|
237
180
|
let errorText;
|
|
238
181
|
try {
|
|
@@ -245,25 +188,19 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
|
|
|
245
188
|
}
|
|
246
189
|
let data;
|
|
247
190
|
try {
|
|
248
|
-
|
|
191
|
+
const json = await response.json();
|
|
192
|
+
data = SearchResponseSchema.parse(json);
|
|
249
193
|
}
|
|
250
|
-
catch (
|
|
251
|
-
throw new Error(`Failed to parse JSON response from Perplexity Search API: ${
|
|
194
|
+
catch (error) {
|
|
195
|
+
throw new Error(`Failed to parse JSON response from Perplexity Search API: ${error}`);
|
|
252
196
|
}
|
|
253
197
|
return formatSearchResults(data);
|
|
254
198
|
}
|
|
255
|
-
|
|
256
|
-
* Creates and configures the Perplexity MCP server with all tools.
|
|
257
|
-
* This factory function is transport-agnostic and returns a configured server instance.
|
|
258
|
-
*
|
|
259
|
-
* @returns The configured MCP server instance
|
|
260
|
-
*/
|
|
261
|
-
export function createPerplexityServer() {
|
|
199
|
+
export function createPerplexityServer(serviceOrigin) {
|
|
262
200
|
const server = new McpServer({
|
|
263
201
|
name: "io.github.perplexityai/mcp-server",
|
|
264
|
-
version: "0.
|
|
202
|
+
version: "0.6.0",
|
|
265
203
|
});
|
|
266
|
-
// Register perplexity_ask tool
|
|
267
204
|
server.registerTool("perplexity_ask", {
|
|
268
205
|
title: "Ask Perplexity",
|
|
269
206
|
description: "Engages in a conversation using the Sonar API. " +
|
|
@@ -284,13 +221,12 @@ export function createPerplexityServer() {
|
|
|
284
221
|
},
|
|
285
222
|
}, async ({ messages }) => {
|
|
286
223
|
validateMessages(messages, "perplexity_ask");
|
|
287
|
-
const result = await performChatCompletion(messages, "sonar-pro");
|
|
224
|
+
const result = await performChatCompletion(messages, "sonar-pro", false, serviceOrigin);
|
|
288
225
|
return {
|
|
289
226
|
content: [{ type: "text", text: result }],
|
|
290
227
|
structuredContent: { response: result },
|
|
291
228
|
};
|
|
292
229
|
});
|
|
293
|
-
// Register perplexity_research tool
|
|
294
230
|
server.registerTool("perplexity_research", {
|
|
295
231
|
title: "Deep Research",
|
|
296
232
|
description: "Performs deep research using the Perplexity API. " +
|
|
@@ -314,13 +250,12 @@ export function createPerplexityServer() {
|
|
|
314
250
|
}, async ({ messages, strip_thinking }) => {
|
|
315
251
|
validateMessages(messages, "perplexity_research");
|
|
316
252
|
const stripThinking = typeof strip_thinking === "boolean" ? strip_thinking : false;
|
|
317
|
-
const result = await performChatCompletion(messages, "sonar-deep-research", stripThinking);
|
|
253
|
+
const result = await performChatCompletion(messages, "sonar-deep-research", stripThinking, serviceOrigin);
|
|
318
254
|
return {
|
|
319
255
|
content: [{ type: "text", text: result }],
|
|
320
256
|
structuredContent: { response: result },
|
|
321
257
|
};
|
|
322
258
|
});
|
|
323
|
-
// Register perplexity_reason tool
|
|
324
259
|
server.registerTool("perplexity_reason", {
|
|
325
260
|
title: "Advanced Reasoning",
|
|
326
261
|
description: "Performs reasoning tasks using the Perplexity API. " +
|
|
@@ -344,13 +279,12 @@ export function createPerplexityServer() {
|
|
|
344
279
|
}, async ({ messages, strip_thinking }) => {
|
|
345
280
|
validateMessages(messages, "perplexity_reason");
|
|
346
281
|
const stripThinking = typeof strip_thinking === "boolean" ? strip_thinking : false;
|
|
347
|
-
const result = await performChatCompletion(messages, "sonar-reasoning-pro", stripThinking);
|
|
282
|
+
const result = await performChatCompletion(messages, "sonar-reasoning-pro", stripThinking, serviceOrigin);
|
|
348
283
|
return {
|
|
349
284
|
content: [{ type: "text", text: result }],
|
|
350
285
|
structuredContent: { response: result },
|
|
351
286
|
};
|
|
352
287
|
});
|
|
353
|
-
// Register perplexity_search tool
|
|
354
288
|
server.registerTool("perplexity_search", {
|
|
355
289
|
title: "Search the Web",
|
|
356
290
|
description: "Performs web search using the Perplexity Search API. " +
|
|
@@ -376,7 +310,7 @@ export function createPerplexityServer() {
|
|
|
376
310
|
const maxResults = typeof max_results === "number" ? max_results : 10;
|
|
377
311
|
const maxTokensPerPage = typeof max_tokens_per_page === "number" ? max_tokens_per_page : 1024;
|
|
378
312
|
const countryCode = typeof country === "string" ? country : undefined;
|
|
379
|
-
const result = await performSearch(query, maxResults, maxTokensPerPage, countryCode);
|
|
313
|
+
const result = await performSearch(query, maxResults, maxTokensPerPage, countryCode, serviceOrigin);
|
|
380
314
|
return {
|
|
381
315
|
content: [{ type: "text", text: result }],
|
|
382
316
|
structuredContent: { results: result },
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const ChatMessageSchema = z.object({
|
|
3
|
+
content: z.string(),
|
|
4
|
+
role: z.string().optional(),
|
|
5
|
+
});
|
|
6
|
+
export const ChatChoiceSchema = z.object({
|
|
7
|
+
message: ChatMessageSchema,
|
|
8
|
+
finish_reason: z.string().optional(),
|
|
9
|
+
index: z.number().optional(),
|
|
10
|
+
});
|
|
11
|
+
export const TokenUsageSchema = z.object({
|
|
12
|
+
prompt_tokens: z.number().optional(),
|
|
13
|
+
completion_tokens: z.number().optional(),
|
|
14
|
+
total_tokens: z.number().optional(),
|
|
15
|
+
});
|
|
16
|
+
export const ChatCompletionResponseSchema = z.object({
|
|
17
|
+
choices: z.array(ChatChoiceSchema).min(1),
|
|
18
|
+
citations: z.array(z.string()).optional(),
|
|
19
|
+
usage: TokenUsageSchema.optional(),
|
|
20
|
+
id: z.string().optional(),
|
|
21
|
+
model: z.string().optional(),
|
|
22
|
+
created: z.number().optional(),
|
|
23
|
+
});
|
|
24
|
+
export const SearchResultSchema = z.object({
|
|
25
|
+
title: z.string(),
|
|
26
|
+
url: z.string(),
|
|
27
|
+
snippet: z.string().optional(),
|
|
28
|
+
date: z.string().optional(),
|
|
29
|
+
score: z.number().optional(),
|
|
30
|
+
});
|
|
31
|
+
export const SearchUsageSchema = z.object({
|
|
32
|
+
tokens: z.number().optional(),
|
|
33
|
+
});
|
|
34
|
+
export const SearchResponseSchema = z.object({
|
|
35
|
+
results: z.array(SearchResultSchema),
|
|
36
|
+
query: z.string().optional(),
|
|
37
|
+
usage: SearchUsageSchema.optional(),
|
|
38
|
+
});
|
package/package.json
CHANGED