@perplexity-ai/mcp-server 0.5.0 → 0.5.2
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 +28 -6
- package/dist/http.js +6 -5
- package/dist/index.js +0 -1
- package/dist/logger.js +82 -0
- package/dist/server.js +32 -31
- package/dist/types.js +4 -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.5.
|
|
9
|
+
"version": "0.5.2"
|
|
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.5.
|
|
16
|
+
"version": "0.5.2",
|
|
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
|
|
|
@@ -33,6 +33,7 @@ Advanced reasoning and problem-solving using the `sonar-reasoning-pro` model. Pe
|
|
|
33
33
|
1. Get your Perplexity API Key from the [API Portal](https://www.perplexity.ai/account/api/group)
|
|
34
34
|
2. Set it as an environment variable: `PERPLEXITY_API_KEY=your_key_here`
|
|
35
35
|
3. (Optional) Set a timeout for requests: `PERPLEXITY_TIMEOUT_MS=600000`. The default is 5 minutes.
|
|
36
|
+
4. (Optional) Set log level for debugging: `PERPLEXITY_LOG_LEVEL=DEBUG|INFO|WARN|ERROR`. The default is ERROR.
|
|
36
37
|
|
|
37
38
|
### Claude Code
|
|
38
39
|
|
|
@@ -81,9 +82,9 @@ Or add to your `claude.json`:
|
|
|
81
82
|
}
|
|
82
83
|
```
|
|
83
84
|
|
|
84
|
-
### Cursor
|
|
85
|
+
### Cursor
|
|
85
86
|
|
|
86
|
-
Add to your `mcp.json` (Cursor)
|
|
87
|
+
Add to your `mcp.json` (Cursor):
|
|
87
88
|
|
|
88
89
|
```json
|
|
89
90
|
{
|
|
@@ -92,14 +93,35 @@ Add to your `mcp.json` (Cursor) or `.vscode/mcp.json` (VS Code):
|
|
|
92
93
|
"command": "npx",
|
|
93
94
|
"args": ["-y", "@perplexity-ai/mcp-server"],
|
|
94
95
|
"env": {
|
|
95
|
-
"PERPLEXITY_API_KEY": "your_key_here"
|
|
96
|
-
"PERPLEXITY_TIMEOUT_MS": "600000"
|
|
96
|
+
"PERPLEXITY_API_KEY": "your_key_here"
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
### VS Code
|
|
104
|
+
|
|
105
|
+
Add to your `.vscode/mcp.json` (VS Code):
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"servers": {
|
|
110
|
+
"perplexity": {
|
|
111
|
+
"type": "stdio",
|
|
112
|
+
"command": "npx",
|
|
113
|
+
"args": [
|
|
114
|
+
"-y",
|
|
115
|
+
"@perplexity-ai/mcp-server"
|
|
116
|
+
],
|
|
117
|
+
"env": {
|
|
118
|
+
"PERPLEXITY_API_KEY": "your_key_here"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
103
125
|
Or use the one-click install badges at the top of this README.
|
|
104
126
|
|
|
105
127
|
### Codex
|
package/dist/http.js
CHANGED
|
@@ -3,10 +3,11 @@ 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
|
+
import { logger } from "./logger.js";
|
|
6
7
|
// Check for required API key
|
|
7
8
|
const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
|
|
8
9
|
if (!PERPLEXITY_API_KEY) {
|
|
9
|
-
|
|
10
|
+
logger.error("PERPLEXITY_API_KEY environment variable is required");
|
|
10
11
|
process.exit(1);
|
|
11
12
|
}
|
|
12
13
|
const app = express();
|
|
@@ -53,7 +54,7 @@ app.all("/mcp", async (req, res) => {
|
|
|
53
54
|
await transport.handleRequest(req, res, req.body);
|
|
54
55
|
}
|
|
55
56
|
catch (error) {
|
|
56
|
-
|
|
57
|
+
logger.error("Error handling MCP request", { error: String(error) });
|
|
57
58
|
if (!res.headersSent) {
|
|
58
59
|
res.status(500).json({
|
|
59
60
|
jsonrpc: "2.0",
|
|
@@ -73,9 +74,9 @@ app.get("/health", (req, res) => {
|
|
|
73
74
|
* Start the HTTP server
|
|
74
75
|
*/
|
|
75
76
|
app.listen(PORT, BIND_ADDRESS, () => {
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
logger.info(`Perplexity MCP Server listening on http://${BIND_ADDRESS}:${PORT}/mcp`);
|
|
78
|
+
logger.info(`Allowed origins: ${ALLOWED_ORIGINS.join(", ")}`);
|
|
78
79
|
}).on("error", (error) => {
|
|
79
|
-
|
|
80
|
+
logger.error("Server error", { error: String(error) });
|
|
80
81
|
process.exit(1);
|
|
81
82
|
});
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,6 @@ async function main() {
|
|
|
16
16
|
const server = createPerplexityServer();
|
|
17
17
|
const transport = new StdioServerTransport();
|
|
18
18
|
await server.connect(transport);
|
|
19
|
-
console.error("Perplexity MCP Server running on stdio with Ask, Research, Reason, and Search tools");
|
|
20
19
|
}
|
|
21
20
|
catch (error) {
|
|
22
21
|
console.error("Fatal error running server:", error);
|
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,6 +1,7 @@
|
|
|
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
|
+
import { ChatCompletionResponseSchema, SearchResponseSchema } from "./validation.js";
|
|
4
5
|
// Retrieve the Perplexity API key from environment variables
|
|
5
6
|
const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
|
|
6
7
|
/**
|
|
@@ -9,7 +10,7 @@ const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
|
|
|
9
10
|
*
|
|
10
11
|
* @returns {string | undefined} The proxy URL if configured, undefined otherwise
|
|
11
12
|
*/
|
|
12
|
-
function getProxyUrl() {
|
|
13
|
+
export function getProxyUrl() {
|
|
13
14
|
return process.env.PERPLEXITY_PROXY ||
|
|
14
15
|
process.env.HTTPS_PROXY ||
|
|
15
16
|
process.env.HTTP_PROXY ||
|
|
@@ -23,32 +24,31 @@ function getProxyUrl() {
|
|
|
23
24
|
* @param {RequestInit} options - Fetch options
|
|
24
25
|
* @returns {Promise<Response>} The fetch response
|
|
25
26
|
*/
|
|
26
|
-
async function proxyAwareFetch(url, options = {}) {
|
|
27
|
+
export async function proxyAwareFetch(url, options = {}) {
|
|
27
28
|
const proxyUrl = getProxyUrl();
|
|
28
29
|
if (proxyUrl) {
|
|
29
30
|
// Use undici with ProxyAgent when proxy is configured
|
|
30
31
|
const proxyAgent = new ProxyAgent(proxyUrl);
|
|
31
|
-
const
|
|
32
|
+
const undiciOptions = {
|
|
32
33
|
...options,
|
|
33
34
|
dispatcher: proxyAgent,
|
|
34
|
-
}
|
|
35
|
+
};
|
|
36
|
+
const response = await undiciFetch(url, undiciOptions);
|
|
35
37
|
// Cast to native Response type for compatibility
|
|
36
38
|
return response;
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return fetch(url, options);
|
|
41
|
-
}
|
|
40
|
+
// Use native fetch when no proxy is configured
|
|
41
|
+
return fetch(url, options);
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
44
|
* Validates an array of message objects for chat completion tools.
|
|
45
45
|
* Ensures each message has a valid role and content field.
|
|
46
46
|
*
|
|
47
|
-
* @param {
|
|
47
|
+
* @param {unknown} messages - The messages to validate
|
|
48
48
|
* @param {string} toolName - The name of the tool calling this validation (for error messages)
|
|
49
49
|
* @throws {Error} If messages is not an array or if any message is invalid
|
|
50
50
|
*/
|
|
51
|
-
function validateMessages(messages, toolName) {
|
|
51
|
+
export function validateMessages(messages, toolName) {
|
|
52
52
|
if (!Array.isArray(messages)) {
|
|
53
53
|
throw new Error(`Invalid arguments for ${toolName}: 'messages' must be an array`);
|
|
54
54
|
}
|
|
@@ -72,14 +72,14 @@ function validateMessages(messages, toolName) {
|
|
|
72
72
|
* @param {string} content - The content to process
|
|
73
73
|
* @returns {string} The content with thinking tokens removed
|
|
74
74
|
*/
|
|
75
|
-
function stripThinkingTokens(content) {
|
|
75
|
+
export function stripThinkingTokens(content) {
|
|
76
76
|
return content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
79
|
* Performs a chat completion by sending a request to the Perplexity API.
|
|
80
80
|
* Appends citations to the returned message content if they exist.
|
|
81
81
|
*
|
|
82
|
-
* @param {
|
|
82
|
+
* @param {Message[]} messages - An array of message objects.
|
|
83
83
|
* @param {string} model - The model to use for the completion.
|
|
84
84
|
* @param {boolean} stripThinking - If true, removes <think>...</think> tags from the response.
|
|
85
85
|
* @returns {Promise<string>} The chat completion result with appended citations.
|
|
@@ -130,22 +130,24 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
|
|
|
130
130
|
}
|
|
131
131
|
throw new Error(`Perplexity API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
132
132
|
}
|
|
133
|
-
// Attempt to parse the JSON response from the API
|
|
134
133
|
let data;
|
|
135
134
|
try {
|
|
136
|
-
|
|
135
|
+
const json = await response.json();
|
|
136
|
+
data = ChatCompletionResponseSchema.parse(json);
|
|
137
137
|
}
|
|
138
|
-
catch (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error instanceof z.ZodError) {
|
|
140
|
+
const issues = error.issues;
|
|
141
|
+
if (issues.some(i => i.path.includes('message') || i.path.includes('content'))) {
|
|
142
|
+
throw new Error("Invalid API response: missing message content");
|
|
143
|
+
}
|
|
144
|
+
if (issues.some(i => i.path.includes('choices'))) {
|
|
145
|
+
throw new Error("Invalid API response: missing or empty choices array");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`Failed to parse JSON response from Perplexity API: ${error}`);
|
|
144
149
|
}
|
|
145
150
|
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
151
|
// Directly retrieve the main message content from the response
|
|
150
152
|
let messageContent = firstChoice.message.content;
|
|
151
153
|
// Strip thinking tokens if requested
|
|
@@ -164,7 +166,7 @@ export async function performChatCompletion(messages, model = "sonar-pro", strip
|
|
|
164
166
|
/**
|
|
165
167
|
* Formats search results from the Perplexity Search API into a readable string.
|
|
166
168
|
*
|
|
167
|
-
* @param {
|
|
169
|
+
* @param {SearchResponse} data - The search response data from the API.
|
|
168
170
|
* @returns {string} Formatted search results.
|
|
169
171
|
*/
|
|
170
172
|
export function formatSearchResults(data) {
|
|
@@ -206,10 +208,8 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
|
|
|
206
208
|
query: query,
|
|
207
209
|
max_results: maxResults,
|
|
208
210
|
max_tokens_per_page: maxTokensPerPage,
|
|
211
|
+
...(country && { country }),
|
|
209
212
|
};
|
|
210
|
-
if (country) {
|
|
211
|
-
body.country = country;
|
|
212
|
-
}
|
|
213
213
|
const controller = new AbortController();
|
|
214
214
|
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
215
215
|
let response;
|
|
@@ -245,10 +245,11 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
|
|
|
245
245
|
}
|
|
246
246
|
let data;
|
|
247
247
|
try {
|
|
248
|
-
|
|
248
|
+
const json = await response.json();
|
|
249
|
+
data = SearchResponseSchema.parse(json);
|
|
249
250
|
}
|
|
250
|
-
catch (
|
|
251
|
-
throw new Error(`Failed to parse JSON response from Perplexity Search API: ${
|
|
251
|
+
catch (error) {
|
|
252
|
+
throw new Error(`Failed to parse JSON response from Perplexity Search API: ${error}`);
|
|
252
253
|
}
|
|
253
254
|
return formatSearchResults(data);
|
|
254
255
|
}
|
|
@@ -261,7 +262,7 @@ export async function performSearch(query, maxResults = 10, maxTokensPerPage = 1
|
|
|
261
262
|
export function createPerplexityServer() {
|
|
262
263
|
const server = new McpServer({
|
|
263
264
|
name: "io.github.perplexityai/mcp-server",
|
|
264
|
-
version: "0.5.
|
|
265
|
+
version: "0.5.2",
|
|
265
266
|
});
|
|
266
267
|
// Register perplexity_ask tool
|
|
267
268
|
server.registerTool("perplexity_ask", {
|
package/dist/types.js
ADDED
|
@@ -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