@kkaminsk/modelcontextprotocol 0.2.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/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/buildCommonOptions.test.js +183 -0
- package/dist/formatMultiQueryResults.test.js +88 -0
- package/dist/formatSearchResults.test.js +55 -0
- package/dist/index.js +1224 -0
- package/dist/utils.js +124 -0
- package/dist/vitest.config.js +15 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import { readFileSync } from "fs";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
import { dirname, join } from "path";
|
|
17
|
+
// Import shared utilities and types from utils module
|
|
18
|
+
import { DEFAULT_TIMEOUT_MS, MAX_DOMAIN_FILTERS, MAX_BATCH_QUERIES, DEFAULT_MODEL, buildCommonOptions, formatSearchResults, formatMultiQueryResults, } from "./utils.js";
|
|
19
|
+
// Read version from package.json with fallback for test environments
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = dirname(__filename);
|
|
22
|
+
let PACKAGE_VERSION = "0.0.0";
|
|
23
|
+
try {
|
|
24
|
+
// Try reading from parent directory (for compiled dist/index.js)
|
|
25
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
26
|
+
PACKAGE_VERSION = packageJson.version;
|
|
27
|
+
}
|
|
28
|
+
catch (_a) {
|
|
29
|
+
try {
|
|
30
|
+
// Try reading from same directory (for source index.ts in tests)
|
|
31
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
|
|
32
|
+
PACKAGE_VERSION = packageJson.version;
|
|
33
|
+
}
|
|
34
|
+
catch (_b) {
|
|
35
|
+
// Fallback version if package.json cannot be read
|
|
36
|
+
PACKAGE_VERSION = "0.0.0";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Utility Functions
|
|
40
|
+
/**
|
|
41
|
+
* Performs a fetch request with timeout and standardized error handling.
|
|
42
|
+
* Consolidates AbortController setup, timeout handling, and error wrapping.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} url - The URL to fetch.
|
|
45
|
+
* @param {RequestInit} options - Fetch options (method, headers, body, etc.).
|
|
46
|
+
* @param {string} apiName - Name of the API for error messages.
|
|
47
|
+
* @returns {Promise<Response>} The fetch response.
|
|
48
|
+
* @throws Will throw an error on timeout, network failure, or HTTP error.
|
|
49
|
+
*/
|
|
50
|
+
function fetchWithTimeout(url_1, options_1) {
|
|
51
|
+
return __awaiter(this, arguments, void 0, function* (url, options, apiName = "Perplexity API") {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
54
|
+
let response;
|
|
55
|
+
try {
|
|
56
|
+
response = yield fetch(url, Object.assign(Object.assign({}, options), { headers: Object.assign({ "Content-Type": "application/json", "Authorization": `Bearer ${PERPLEXITY_API_KEY}` }, options.headers), signal: controller.signal }));
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
clearTimeout(timeoutId);
|
|
61
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
62
|
+
throw new Error(`Request timeout: ${apiName} did not respond within ${TIMEOUT_MS}ms. Consider increasing PERPLEXITY_TIMEOUT_MS.`);
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`Network error while calling ${apiName}: ${error}`);
|
|
65
|
+
}
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
let errorText;
|
|
68
|
+
try {
|
|
69
|
+
errorText = yield response.text();
|
|
70
|
+
}
|
|
71
|
+
catch (_a) {
|
|
72
|
+
errorText = "Unable to parse error response";
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`${apiName} error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
75
|
+
}
|
|
76
|
+
return response;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Definition of the Perplexity Ask Tool.
|
|
81
|
+
* This tool accepts an array of messages and returns a chat completion response
|
|
82
|
+
* from the Perplexity API, with citations appended to the message if provided.
|
|
83
|
+
*/
|
|
84
|
+
const PERPLEXITY_ASK_TOOL = {
|
|
85
|
+
name: "perplexity_ask",
|
|
86
|
+
description: "Real-time AI-powered answers with web search. " +
|
|
87
|
+
"Supports sonar (fast/cheap) and sonar-pro (high quality) models. " +
|
|
88
|
+
"Accepts an array of messages and returns a completion response with citations.",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
messages: {
|
|
93
|
+
type: "array",
|
|
94
|
+
items: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
role: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: "Role of the message (e.g., system, user, assistant)",
|
|
100
|
+
},
|
|
101
|
+
content: {
|
|
102
|
+
type: "string",
|
|
103
|
+
description: "The content of the message",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
required: ["role", "content"],
|
|
107
|
+
},
|
|
108
|
+
description: "Array of conversation messages",
|
|
109
|
+
},
|
|
110
|
+
model: {
|
|
111
|
+
type: "string",
|
|
112
|
+
enum: ["sonar", "sonar-pro"],
|
|
113
|
+
description: "Model to use: 'sonar' for fast/cost-effective queries, 'sonar-pro' for higher quality (default)",
|
|
114
|
+
},
|
|
115
|
+
search_domain_filter: {
|
|
116
|
+
type: "array",
|
|
117
|
+
items: { type: "string" },
|
|
118
|
+
maxItems: 20,
|
|
119
|
+
description: "Domain filter list. Prefix with '-' to exclude (denylist), otherwise include (allowlist). Maximum 20 domains. Examples: ['wikipedia.org', 'github.com'] for allowlist, ['-reddit.com', '-twitter.com'] for denylist.",
|
|
120
|
+
},
|
|
121
|
+
temperature: {
|
|
122
|
+
type: "number",
|
|
123
|
+
minimum: 0,
|
|
124
|
+
maximum: 2,
|
|
125
|
+
description: "Controls randomness. 0 = deterministic, 2 = maximum creativity. Default: 0.2",
|
|
126
|
+
},
|
|
127
|
+
max_tokens: {
|
|
128
|
+
type: "integer",
|
|
129
|
+
minimum: 1,
|
|
130
|
+
description: "Maximum tokens in the response. Model-specific limits apply.",
|
|
131
|
+
},
|
|
132
|
+
top_p: {
|
|
133
|
+
type: "number",
|
|
134
|
+
minimum: 0,
|
|
135
|
+
maximum: 1,
|
|
136
|
+
description: "Nucleus sampling threshold. Default: 0.9",
|
|
137
|
+
},
|
|
138
|
+
top_k: {
|
|
139
|
+
type: "integer",
|
|
140
|
+
minimum: 0,
|
|
141
|
+
description: "Top-k sampling. 0 = disabled. Default: 0",
|
|
142
|
+
},
|
|
143
|
+
search_mode: {
|
|
144
|
+
type: "string",
|
|
145
|
+
enum: ["web", "academic", "sec"],
|
|
146
|
+
description: "Source type filter: 'web' for general internet (default), 'academic' for scholarly articles and papers, 'sec' for SEC filings and financial documents",
|
|
147
|
+
},
|
|
148
|
+
search_recency_filter: {
|
|
149
|
+
type: "string",
|
|
150
|
+
enum: ["day", "week", "month", "year"],
|
|
151
|
+
description: "Filter results by recency. 'day' = last 24 hours, 'week' = last 7 days, 'month' = last 30 days, 'year' = last 365 days.",
|
|
152
|
+
},
|
|
153
|
+
search_after_date: {
|
|
154
|
+
type: "string",
|
|
155
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
156
|
+
description: "Only include results published after this date. Format: MM/DD/YYYY",
|
|
157
|
+
},
|
|
158
|
+
search_before_date: {
|
|
159
|
+
type: "string",
|
|
160
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
161
|
+
description: "Only include results published before this date. Format: MM/DD/YYYY",
|
|
162
|
+
},
|
|
163
|
+
last_updated_after: {
|
|
164
|
+
type: "string",
|
|
165
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
166
|
+
description: "Only include results last updated after this date. Format: MM/DD/YYYY",
|
|
167
|
+
},
|
|
168
|
+
last_updated_before: {
|
|
169
|
+
type: "string",
|
|
170
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
171
|
+
description: "Only include results last updated before this date. Format: MM/DD/YYYY",
|
|
172
|
+
},
|
|
173
|
+
stream: {
|
|
174
|
+
type: "boolean",
|
|
175
|
+
description: "Enable streaming responses. When true, response is delivered incrementally. Default: false",
|
|
176
|
+
},
|
|
177
|
+
return_images: {
|
|
178
|
+
type: "boolean",
|
|
179
|
+
description: "Include relevant images from search results in the response. Default: false",
|
|
180
|
+
},
|
|
181
|
+
return_related_questions: {
|
|
182
|
+
type: "boolean",
|
|
183
|
+
description: "Include related question suggestions for follow-up queries. Default: false",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
required: ["messages"],
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Definition of the Perplexity Research Tool.
|
|
191
|
+
* This tool performs deep research queries using the Perplexity API.
|
|
192
|
+
*/
|
|
193
|
+
const PERPLEXITY_RESEARCH_TOOL = {
|
|
194
|
+
name: "perplexity_research",
|
|
195
|
+
description: "Performs deep research using the Perplexity API. " +
|
|
196
|
+
"Accepts an array of messages (each with a role and content) " +
|
|
197
|
+
"and returns a comprehensive research response with citations.",
|
|
198
|
+
inputSchema: {
|
|
199
|
+
type: "object",
|
|
200
|
+
properties: {
|
|
201
|
+
messages: {
|
|
202
|
+
type: "array",
|
|
203
|
+
items: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
role: {
|
|
207
|
+
type: "string",
|
|
208
|
+
description: "Role of the message (e.g., system, user, assistant)",
|
|
209
|
+
},
|
|
210
|
+
content: {
|
|
211
|
+
type: "string",
|
|
212
|
+
description: "The content of the message",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
required: ["role", "content"],
|
|
216
|
+
},
|
|
217
|
+
description: "Array of conversation messages",
|
|
218
|
+
},
|
|
219
|
+
reasoning_effort: {
|
|
220
|
+
type: "string",
|
|
221
|
+
enum: ["low", "medium", "high"],
|
|
222
|
+
description: "Controls research depth: 'low' for quick overviews, 'medium' for balanced research (default), 'high' for comprehensive deep research",
|
|
223
|
+
},
|
|
224
|
+
search_domain_filter: {
|
|
225
|
+
type: "array",
|
|
226
|
+
items: { type: "string" },
|
|
227
|
+
maxItems: 20,
|
|
228
|
+
description: "Domain filter list. Prefix with '-' to exclude (denylist), otherwise include (allowlist). Maximum 20 domains. Examples: ['wikipedia.org', 'github.com'] for allowlist, ['-reddit.com', '-twitter.com'] for denylist.",
|
|
229
|
+
},
|
|
230
|
+
temperature: {
|
|
231
|
+
type: "number",
|
|
232
|
+
minimum: 0,
|
|
233
|
+
maximum: 2,
|
|
234
|
+
description: "Controls randomness. 0 = deterministic, 2 = maximum creativity. Default: 0.2",
|
|
235
|
+
},
|
|
236
|
+
max_tokens: {
|
|
237
|
+
type: "integer",
|
|
238
|
+
minimum: 1,
|
|
239
|
+
description: "Maximum tokens in the response. Model-specific limits apply.",
|
|
240
|
+
},
|
|
241
|
+
top_p: {
|
|
242
|
+
type: "number",
|
|
243
|
+
minimum: 0,
|
|
244
|
+
maximum: 1,
|
|
245
|
+
description: "Nucleus sampling threshold. Default: 0.9",
|
|
246
|
+
},
|
|
247
|
+
top_k: {
|
|
248
|
+
type: "integer",
|
|
249
|
+
minimum: 0,
|
|
250
|
+
description: "Top-k sampling. 0 = disabled. Default: 0",
|
|
251
|
+
},
|
|
252
|
+
search_mode: {
|
|
253
|
+
type: "string",
|
|
254
|
+
enum: ["web", "academic", "sec"],
|
|
255
|
+
description: "Source type filter: 'web' for general internet (default), 'academic' for scholarly articles and papers, 'sec' for SEC filings and financial documents",
|
|
256
|
+
},
|
|
257
|
+
search_recency_filter: {
|
|
258
|
+
type: "string",
|
|
259
|
+
enum: ["day", "week", "month", "year"],
|
|
260
|
+
description: "Filter results by recency. 'day' = last 24 hours, 'week' = last 7 days, 'month' = last 30 days, 'year' = last 365 days.",
|
|
261
|
+
},
|
|
262
|
+
search_after_date: {
|
|
263
|
+
type: "string",
|
|
264
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
265
|
+
description: "Only include results published after this date. Format: MM/DD/YYYY",
|
|
266
|
+
},
|
|
267
|
+
search_before_date: {
|
|
268
|
+
type: "string",
|
|
269
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
270
|
+
description: "Only include results published before this date. Format: MM/DD/YYYY",
|
|
271
|
+
},
|
|
272
|
+
last_updated_after: {
|
|
273
|
+
type: "string",
|
|
274
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
275
|
+
description: "Only include results last updated after this date. Format: MM/DD/YYYY",
|
|
276
|
+
},
|
|
277
|
+
last_updated_before: {
|
|
278
|
+
type: "string",
|
|
279
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
280
|
+
description: "Only include results last updated before this date. Format: MM/DD/YYYY",
|
|
281
|
+
},
|
|
282
|
+
return_images: {
|
|
283
|
+
type: "boolean",
|
|
284
|
+
description: "Include relevant images from search results in the response. Default: false",
|
|
285
|
+
},
|
|
286
|
+
return_related_questions: {
|
|
287
|
+
type: "boolean",
|
|
288
|
+
description: "Include related question suggestions for follow-up queries. Default: false",
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
required: ["messages"],
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* Definition of the Perplexity Reason Tool.
|
|
296
|
+
* This tool performs reasoning queries using the Perplexity API.
|
|
297
|
+
*/
|
|
298
|
+
const PERPLEXITY_REASON_TOOL = {
|
|
299
|
+
name: "perplexity_reason",
|
|
300
|
+
description: "Performs reasoning tasks using the Perplexity API. " +
|
|
301
|
+
"Accepts an array of messages (each with a role and content) " +
|
|
302
|
+
"and returns a well-reasoned response using the sonar-reasoning-pro model.",
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: "object",
|
|
305
|
+
properties: {
|
|
306
|
+
messages: {
|
|
307
|
+
type: "array",
|
|
308
|
+
items: {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {
|
|
311
|
+
role: {
|
|
312
|
+
type: "string",
|
|
313
|
+
description: "Role of the message (e.g., system, user, assistant)",
|
|
314
|
+
},
|
|
315
|
+
content: {
|
|
316
|
+
type: "string",
|
|
317
|
+
description: "The content of the message",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
required: ["role", "content"],
|
|
321
|
+
},
|
|
322
|
+
description: "Array of conversation messages",
|
|
323
|
+
},
|
|
324
|
+
search_domain_filter: {
|
|
325
|
+
type: "array",
|
|
326
|
+
items: { type: "string" },
|
|
327
|
+
maxItems: 20,
|
|
328
|
+
description: "Domain filter list. Prefix with '-' to exclude (denylist), otherwise include (allowlist). Maximum 20 domains. Examples: ['wikipedia.org', 'github.com'] for allowlist, ['-reddit.com', '-twitter.com'] for denylist.",
|
|
329
|
+
},
|
|
330
|
+
temperature: {
|
|
331
|
+
type: "number",
|
|
332
|
+
minimum: 0,
|
|
333
|
+
maximum: 2,
|
|
334
|
+
description: "Controls randomness. 0 = deterministic, 2 = maximum creativity. Default: 0.2",
|
|
335
|
+
},
|
|
336
|
+
max_tokens: {
|
|
337
|
+
type: "integer",
|
|
338
|
+
minimum: 1,
|
|
339
|
+
description: "Maximum tokens in the response. Model-specific limits apply.",
|
|
340
|
+
},
|
|
341
|
+
top_p: {
|
|
342
|
+
type: "number",
|
|
343
|
+
minimum: 0,
|
|
344
|
+
maximum: 1,
|
|
345
|
+
description: "Nucleus sampling threshold. Default: 0.9",
|
|
346
|
+
},
|
|
347
|
+
top_k: {
|
|
348
|
+
type: "integer",
|
|
349
|
+
minimum: 0,
|
|
350
|
+
description: "Top-k sampling. 0 = disabled. Default: 0",
|
|
351
|
+
},
|
|
352
|
+
search_mode: {
|
|
353
|
+
type: "string",
|
|
354
|
+
enum: ["web", "academic", "sec"],
|
|
355
|
+
description: "Source type filter: 'web' for general internet (default), 'academic' for scholarly articles and papers, 'sec' for SEC filings and financial documents",
|
|
356
|
+
},
|
|
357
|
+
search_recency_filter: {
|
|
358
|
+
type: "string",
|
|
359
|
+
enum: ["day", "week", "month", "year"],
|
|
360
|
+
description: "Filter results by recency. 'day' = last 24 hours, 'week' = last 7 days, 'month' = last 30 days, 'year' = last 365 days.",
|
|
361
|
+
},
|
|
362
|
+
search_after_date: {
|
|
363
|
+
type: "string",
|
|
364
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
365
|
+
description: "Only include results published after this date. Format: MM/DD/YYYY",
|
|
366
|
+
},
|
|
367
|
+
search_before_date: {
|
|
368
|
+
type: "string",
|
|
369
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
370
|
+
description: "Only include results published before this date. Format: MM/DD/YYYY",
|
|
371
|
+
},
|
|
372
|
+
last_updated_after: {
|
|
373
|
+
type: "string",
|
|
374
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
375
|
+
description: "Only include results last updated after this date. Format: MM/DD/YYYY",
|
|
376
|
+
},
|
|
377
|
+
last_updated_before: {
|
|
378
|
+
type: "string",
|
|
379
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
380
|
+
description: "Only include results last updated before this date. Format: MM/DD/YYYY",
|
|
381
|
+
},
|
|
382
|
+
stream: {
|
|
383
|
+
type: "boolean",
|
|
384
|
+
description: "Enable streaming responses. When true, response is delivered incrementally. Default: false",
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
required: ["messages"],
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Definition of the Perplexity Search Tool.
|
|
392
|
+
* This tool performs web search using the Perplexity Search API.
|
|
393
|
+
*/
|
|
394
|
+
const PERPLEXITY_SEARCH_TOOL = {
|
|
395
|
+
name: "perplexity_search",
|
|
396
|
+
description: "Performs web search using the Perplexity Search API. " +
|
|
397
|
+
"Supports single query or batch of up to 5 queries. " +
|
|
398
|
+
"Returns ranked search results with titles, URLs, snippets, and metadata.",
|
|
399
|
+
inputSchema: {
|
|
400
|
+
type: "object",
|
|
401
|
+
properties: {
|
|
402
|
+
query: {
|
|
403
|
+
oneOf: [
|
|
404
|
+
{ type: "string", description: "Single search query" },
|
|
405
|
+
{
|
|
406
|
+
type: "array",
|
|
407
|
+
items: { type: "string" },
|
|
408
|
+
minItems: 1,
|
|
409
|
+
maxItems: 5,
|
|
410
|
+
description: "Array of up to 5 search queries"
|
|
411
|
+
}
|
|
412
|
+
],
|
|
413
|
+
description: "Search query or array of queries (max 5)",
|
|
414
|
+
},
|
|
415
|
+
max_results: {
|
|
416
|
+
type: "number",
|
|
417
|
+
description: "Maximum number of results to return (1-20, default: 10)",
|
|
418
|
+
minimum: 1,
|
|
419
|
+
maximum: 20,
|
|
420
|
+
},
|
|
421
|
+
max_tokens_per_page: {
|
|
422
|
+
type: "number",
|
|
423
|
+
description: "Maximum tokens to extract per webpage (default: 1024)",
|
|
424
|
+
minimum: 256,
|
|
425
|
+
maximum: 2048,
|
|
426
|
+
},
|
|
427
|
+
country: {
|
|
428
|
+
type: "string",
|
|
429
|
+
description: "ISO 3166-1 alpha-2 country code for regional results (e.g., 'US', 'GB')",
|
|
430
|
+
},
|
|
431
|
+
search_domain_filter: {
|
|
432
|
+
type: "array",
|
|
433
|
+
items: { type: "string" },
|
|
434
|
+
maxItems: 20,
|
|
435
|
+
description: "Domain filter list. Prefix with '-' to exclude (denylist), otherwise include (allowlist). Maximum 20 domains. Examples: ['wikipedia.org', 'github.com'] for allowlist, ['-reddit.com', '-twitter.com'] for denylist.",
|
|
436
|
+
},
|
|
437
|
+
search_recency_filter: {
|
|
438
|
+
type: "string",
|
|
439
|
+
enum: ["day", "week", "month", "year"],
|
|
440
|
+
description: "Filter results by recency. 'day' = last 24 hours, 'week' = last 7 days, 'month' = last 30 days, 'year' = last 365 days.",
|
|
441
|
+
},
|
|
442
|
+
search_after_date: {
|
|
443
|
+
type: "string",
|
|
444
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
445
|
+
description: "Only include results published after this date. Format: MM/DD/YYYY",
|
|
446
|
+
},
|
|
447
|
+
search_before_date: {
|
|
448
|
+
type: "string",
|
|
449
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
450
|
+
description: "Only include results published before this date. Format: MM/DD/YYYY",
|
|
451
|
+
},
|
|
452
|
+
last_updated_after: {
|
|
453
|
+
type: "string",
|
|
454
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
455
|
+
description: "Only include results last updated after this date. Format: MM/DD/YYYY",
|
|
456
|
+
},
|
|
457
|
+
last_updated_before: {
|
|
458
|
+
type: "string",
|
|
459
|
+
pattern: "^\\d{2}/\\d{2}/\\d{4}$",
|
|
460
|
+
description: "Only include results last updated before this date. Format: MM/DD/YYYY",
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
required: ["query"],
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
/**
|
|
467
|
+
* Definition of the Perplexity Async Research Tool.
|
|
468
|
+
* This tool starts an async deep research job and returns a request_id for polling.
|
|
469
|
+
*/
|
|
470
|
+
const PERPLEXITY_RESEARCH_ASYNC_TOOL = {
|
|
471
|
+
name: "perplexity_research_async",
|
|
472
|
+
description: "Start an async deep research job. Returns a request_id to poll for results. " +
|
|
473
|
+
"Use for complex research that may take several minutes. " +
|
|
474
|
+
"Poll with perplexity_research_status to check progress and retrieve results.",
|
|
475
|
+
inputSchema: {
|
|
476
|
+
type: "object",
|
|
477
|
+
properties: {
|
|
478
|
+
messages: {
|
|
479
|
+
type: "array",
|
|
480
|
+
items: {
|
|
481
|
+
type: "object",
|
|
482
|
+
properties: {
|
|
483
|
+
role: {
|
|
484
|
+
type: "string",
|
|
485
|
+
description: "Role of the message (e.g., system, user, assistant)",
|
|
486
|
+
},
|
|
487
|
+
content: {
|
|
488
|
+
type: "string",
|
|
489
|
+
description: "The content of the message",
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
required: ["role", "content"],
|
|
493
|
+
},
|
|
494
|
+
description: "Array of conversation messages",
|
|
495
|
+
},
|
|
496
|
+
reasoning_effort: {
|
|
497
|
+
type: "string",
|
|
498
|
+
enum: ["low", "medium", "high"],
|
|
499
|
+
description: "Controls research depth: 'low' for quick overviews, 'medium' for balanced research (default), 'high' for comprehensive deep research",
|
|
500
|
+
},
|
|
501
|
+
search_domain_filter: {
|
|
502
|
+
type: "array",
|
|
503
|
+
items: { type: "string" },
|
|
504
|
+
maxItems: 20,
|
|
505
|
+
description: "Domain filter list. Prefix with '-' to exclude (denylist), otherwise include (allowlist). Maximum 20 domains.",
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
required: ["messages"],
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
/**
|
|
512
|
+
* Definition of the Perplexity Research Status Tool.
|
|
513
|
+
* This tool checks the status of an async research job and retrieves results when complete.
|
|
514
|
+
*/
|
|
515
|
+
const PERPLEXITY_RESEARCH_STATUS_TOOL = {
|
|
516
|
+
name: "perplexity_research_status",
|
|
517
|
+
description: "Check status of an async research job and retrieve results when complete. " +
|
|
518
|
+
"Use the request_id returned from perplexity_research_async.",
|
|
519
|
+
inputSchema: {
|
|
520
|
+
type: "object",
|
|
521
|
+
properties: {
|
|
522
|
+
request_id: {
|
|
523
|
+
type: "string",
|
|
524
|
+
description: "The request_id returned from perplexity_research_async",
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
required: ["request_id"],
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
// Retrieve the Perplexity API key from environment variables
|
|
531
|
+
const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
|
|
532
|
+
if (!PERPLEXITY_API_KEY) {
|
|
533
|
+
console.error("Error: PERPLEXITY_API_KEY environment variable is required");
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
// Configure timeout for API requests (default: DEFAULT_TIMEOUT_MS = 5 minutes)
|
|
537
|
+
// Can be overridden via PERPLEXITY_TIMEOUT_MS environment variable
|
|
538
|
+
const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || String(DEFAULT_TIMEOUT_MS), 10);
|
|
539
|
+
/**
|
|
540
|
+
* Performs a chat completion by sending a request to the Perplexity API.
|
|
541
|
+
* Appends citations to the returned message content if they exist.
|
|
542
|
+
*
|
|
543
|
+
* @param {Array<{ role: string; content: string }>} messages - An array of message objects.
|
|
544
|
+
* @param {string} model - The model to use for the completion.
|
|
545
|
+
* @param {object} options - Additional options for the API request.
|
|
546
|
+
* @param {string} options.reasoning_effort - Controls research depth for sonar-deep-research model.
|
|
547
|
+
* @param {string[]} options.search_domain_filter - Domain filter list for search results.
|
|
548
|
+
* @param {number} options.temperature - Controls randomness (0-2).
|
|
549
|
+
* @param {number} options.max_tokens - Maximum tokens in the response.
|
|
550
|
+
* @param {number} options.top_p - Nucleus sampling threshold (0-1).
|
|
551
|
+
* @param {number} options.top_k - Top-k sampling (0 = disabled).
|
|
552
|
+
* @param {string} options.search_mode - Source type filter (web, academic, sec).
|
|
553
|
+
* @param {string} options.search_recency_filter - Filter by recency (day, week, month, year).
|
|
554
|
+
* @param {string} options.search_after_date - Only results after this date (MM/DD/YYYY).
|
|
555
|
+
* @param {string} options.search_before_date - Only results before this date (MM/DD/YYYY).
|
|
556
|
+
* @param {string} options.last_updated_after - Only results updated after this date (MM/DD/YYYY).
|
|
557
|
+
* @param {string} options.last_updated_before - Only results updated before this date (MM/DD/YYYY).
|
|
558
|
+
* @returns {Promise<string>} The chat completion result with appended citations.
|
|
559
|
+
* @throws Will throw an error if the API request fails.
|
|
560
|
+
*/
|
|
561
|
+
function performChatCompletion(messages_1) {
|
|
562
|
+
return __awaiter(this, arguments, void 0, function* (messages, model = DEFAULT_MODEL, options) {
|
|
563
|
+
// Construct the API endpoint URL and request body
|
|
564
|
+
const url = new URL("https://api.perplexity.ai/chat/completions");
|
|
565
|
+
const body = {
|
|
566
|
+
model: model, // Model identifier passed as parameter
|
|
567
|
+
messages: messages,
|
|
568
|
+
};
|
|
569
|
+
// Add reasoning_effort if provided (applicable for sonar-deep-research model)
|
|
570
|
+
if (options === null || options === void 0 ? void 0 : options.reasoning_effort) {
|
|
571
|
+
body.reasoning_effort = options.reasoning_effort;
|
|
572
|
+
}
|
|
573
|
+
// Add search_domain_filter if provided
|
|
574
|
+
if ((options === null || options === void 0 ? void 0 : options.search_domain_filter) && options.search_domain_filter.length > 0) {
|
|
575
|
+
if (options.search_domain_filter.length > MAX_DOMAIN_FILTERS) {
|
|
576
|
+
throw new Error(`search_domain_filter cannot exceed ${MAX_DOMAIN_FILTERS} domains`);
|
|
577
|
+
}
|
|
578
|
+
body.search_domain_filter = options.search_domain_filter;
|
|
579
|
+
}
|
|
580
|
+
// Add generation parameters if provided
|
|
581
|
+
if ((options === null || options === void 0 ? void 0 : options.temperature) !== undefined) {
|
|
582
|
+
if (options.temperature < 0 || options.temperature > 2) {
|
|
583
|
+
throw new Error("temperature must be between 0 and 2");
|
|
584
|
+
}
|
|
585
|
+
body.temperature = options.temperature;
|
|
586
|
+
}
|
|
587
|
+
if ((options === null || options === void 0 ? void 0 : options.max_tokens) !== undefined) {
|
|
588
|
+
if (options.max_tokens < 1) {
|
|
589
|
+
throw new Error("max_tokens must be at least 1");
|
|
590
|
+
}
|
|
591
|
+
body.max_tokens = options.max_tokens;
|
|
592
|
+
}
|
|
593
|
+
if ((options === null || options === void 0 ? void 0 : options.top_p) !== undefined) {
|
|
594
|
+
if (options.top_p < 0 || options.top_p > 1) {
|
|
595
|
+
throw new Error("top_p must be between 0 and 1");
|
|
596
|
+
}
|
|
597
|
+
body.top_p = options.top_p;
|
|
598
|
+
}
|
|
599
|
+
if ((options === null || options === void 0 ? void 0 : options.top_k) !== undefined) {
|
|
600
|
+
if (options.top_k < 0) {
|
|
601
|
+
throw new Error("top_k must be non-negative");
|
|
602
|
+
}
|
|
603
|
+
body.top_k = options.top_k;
|
|
604
|
+
}
|
|
605
|
+
// Add search_mode if provided
|
|
606
|
+
if (options === null || options === void 0 ? void 0 : options.search_mode) {
|
|
607
|
+
body.search_mode = options.search_mode;
|
|
608
|
+
}
|
|
609
|
+
// Add date filtering parameters if provided
|
|
610
|
+
if (options === null || options === void 0 ? void 0 : options.search_recency_filter) {
|
|
611
|
+
body.search_recency_filter = options.search_recency_filter;
|
|
612
|
+
}
|
|
613
|
+
if (options === null || options === void 0 ? void 0 : options.search_after_date) {
|
|
614
|
+
body.search_after_date_filter = options.search_after_date;
|
|
615
|
+
}
|
|
616
|
+
if (options === null || options === void 0 ? void 0 : options.search_before_date) {
|
|
617
|
+
body.search_before_date_filter = options.search_before_date;
|
|
618
|
+
}
|
|
619
|
+
if (options === null || options === void 0 ? void 0 : options.last_updated_after) {
|
|
620
|
+
body.last_updated_after = options.last_updated_after;
|
|
621
|
+
}
|
|
622
|
+
if (options === null || options === void 0 ? void 0 : options.last_updated_before) {
|
|
623
|
+
body.last_updated_before = options.last_updated_before;
|
|
624
|
+
}
|
|
625
|
+
// Add return_images and return_related_questions if provided
|
|
626
|
+
if (options === null || options === void 0 ? void 0 : options.return_images) {
|
|
627
|
+
body.return_images = true;
|
|
628
|
+
}
|
|
629
|
+
if (options === null || options === void 0 ? void 0 : options.return_related_questions) {
|
|
630
|
+
body.return_related_questions = true;
|
|
631
|
+
}
|
|
632
|
+
const response = yield fetchWithTimeout(url.toString(), { method: "POST", body: JSON.stringify(body) }, "Perplexity API");
|
|
633
|
+
// Attempt to parse the JSON response from the API
|
|
634
|
+
let data;
|
|
635
|
+
try {
|
|
636
|
+
data = yield response.json();
|
|
637
|
+
}
|
|
638
|
+
catch (jsonError) {
|
|
639
|
+
throw new Error(`Failed to parse JSON response from Perplexity API: ${jsonError}`);
|
|
640
|
+
}
|
|
641
|
+
// Directly retrieve the main message content from the response
|
|
642
|
+
let messageContent = data.choices[0].message.content;
|
|
643
|
+
// If citations are provided, append them to the message content
|
|
644
|
+
if (data.citations && data.citations.length > 0) {
|
|
645
|
+
messageContent += "\n\nCitations:\n";
|
|
646
|
+
data.citations.forEach((citation, index) => {
|
|
647
|
+
messageContent += `[${index + 1}] ${citation}\n`;
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
// If images are provided, append them to the message content
|
|
651
|
+
if (data.images && data.images.length > 0) {
|
|
652
|
+
messageContent += "\n\nImages:\n";
|
|
653
|
+
data.images.forEach((img, index) => {
|
|
654
|
+
messageContent += `[${index + 1}] ${img.url} (${img.width}x${img.height}) - Source: ${img.origin_url}\n`;
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
// If related questions are provided, append them to the message content
|
|
658
|
+
if (data.related_questions && data.related_questions.length > 0) {
|
|
659
|
+
messageContent += "\n\nRelated Questions:\n";
|
|
660
|
+
data.related_questions.forEach((question, index) => {
|
|
661
|
+
messageContent += `${index + 1}. ${question}\n`;
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
return messageContent;
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Performs a streaming chat completion by sending a request to the Perplexity API.
|
|
669
|
+
* Streams content incrementally and returns the full response with citations.
|
|
670
|
+
*
|
|
671
|
+
* @param {Array<{ role: string; content: string }>} messages - An array of message objects.
|
|
672
|
+
* @param {string} model - The model to use for the completion.
|
|
673
|
+
* @param {object} options - Additional options for the API request.
|
|
674
|
+
* @returns {Promise<string>} The chat completion result with appended citations.
|
|
675
|
+
* @throws Will throw an error if the API request fails.
|
|
676
|
+
*/
|
|
677
|
+
function performStreamingChatCompletion(messages_1) {
|
|
678
|
+
return __awaiter(this, arguments, void 0, function* (messages, model = DEFAULT_MODEL, options) {
|
|
679
|
+
var _a, _b, _c, _d;
|
|
680
|
+
const url = new URL("https://api.perplexity.ai/chat/completions");
|
|
681
|
+
const body = {
|
|
682
|
+
model: model,
|
|
683
|
+
messages: messages,
|
|
684
|
+
stream: true,
|
|
685
|
+
};
|
|
686
|
+
// Add search_domain_filter if provided
|
|
687
|
+
if ((options === null || options === void 0 ? void 0 : options.search_domain_filter) && options.search_domain_filter.length > 0) {
|
|
688
|
+
if (options.search_domain_filter.length > MAX_DOMAIN_FILTERS) {
|
|
689
|
+
throw new Error(`search_domain_filter cannot exceed ${MAX_DOMAIN_FILTERS} domains`);
|
|
690
|
+
}
|
|
691
|
+
body.search_domain_filter = options.search_domain_filter;
|
|
692
|
+
}
|
|
693
|
+
// Add generation parameters if provided
|
|
694
|
+
if ((options === null || options === void 0 ? void 0 : options.temperature) !== undefined) {
|
|
695
|
+
if (options.temperature < 0 || options.temperature > 2) {
|
|
696
|
+
throw new Error("temperature must be between 0 and 2");
|
|
697
|
+
}
|
|
698
|
+
body.temperature = options.temperature;
|
|
699
|
+
}
|
|
700
|
+
if ((options === null || options === void 0 ? void 0 : options.max_tokens) !== undefined) {
|
|
701
|
+
if (options.max_tokens < 1) {
|
|
702
|
+
throw new Error("max_tokens must be at least 1");
|
|
703
|
+
}
|
|
704
|
+
body.max_tokens = options.max_tokens;
|
|
705
|
+
}
|
|
706
|
+
if ((options === null || options === void 0 ? void 0 : options.top_p) !== undefined) {
|
|
707
|
+
if (options.top_p < 0 || options.top_p > 1) {
|
|
708
|
+
throw new Error("top_p must be between 0 and 1");
|
|
709
|
+
}
|
|
710
|
+
body.top_p = options.top_p;
|
|
711
|
+
}
|
|
712
|
+
if ((options === null || options === void 0 ? void 0 : options.top_k) !== undefined) {
|
|
713
|
+
if (options.top_k < 0) {
|
|
714
|
+
throw new Error("top_k must be non-negative");
|
|
715
|
+
}
|
|
716
|
+
body.top_k = options.top_k;
|
|
717
|
+
}
|
|
718
|
+
// Add search_mode if provided
|
|
719
|
+
if (options === null || options === void 0 ? void 0 : options.search_mode) {
|
|
720
|
+
body.search_mode = options.search_mode;
|
|
721
|
+
}
|
|
722
|
+
// Add date filtering parameters if provided
|
|
723
|
+
if (options === null || options === void 0 ? void 0 : options.search_recency_filter) {
|
|
724
|
+
body.search_recency_filter = options.search_recency_filter;
|
|
725
|
+
}
|
|
726
|
+
if (options === null || options === void 0 ? void 0 : options.search_after_date) {
|
|
727
|
+
body.search_after_date_filter = options.search_after_date;
|
|
728
|
+
}
|
|
729
|
+
if (options === null || options === void 0 ? void 0 : options.search_before_date) {
|
|
730
|
+
body.search_before_date_filter = options.search_before_date;
|
|
731
|
+
}
|
|
732
|
+
if (options === null || options === void 0 ? void 0 : options.last_updated_after) {
|
|
733
|
+
body.last_updated_after = options.last_updated_after;
|
|
734
|
+
}
|
|
735
|
+
if (options === null || options === void 0 ? void 0 : options.last_updated_before) {
|
|
736
|
+
body.last_updated_before = options.last_updated_before;
|
|
737
|
+
}
|
|
738
|
+
// Add return_images and return_related_questions if provided
|
|
739
|
+
if (options === null || options === void 0 ? void 0 : options.return_images) {
|
|
740
|
+
body.return_images = true;
|
|
741
|
+
}
|
|
742
|
+
if (options === null || options === void 0 ? void 0 : options.return_related_questions) {
|
|
743
|
+
body.return_related_questions = true;
|
|
744
|
+
}
|
|
745
|
+
const response = yield fetchWithTimeout(url.toString(), { method: "POST", body: JSON.stringify(body) }, "Perplexity API");
|
|
746
|
+
// Read the streaming response
|
|
747
|
+
const reader = (_a = response.body) === null || _a === void 0 ? void 0 : _a.getReader();
|
|
748
|
+
if (!reader) {
|
|
749
|
+
throw new Error("No response body available for streaming");
|
|
750
|
+
}
|
|
751
|
+
const decoder = new TextDecoder();
|
|
752
|
+
let buffer = "";
|
|
753
|
+
let fullContent = "";
|
|
754
|
+
let citations = [];
|
|
755
|
+
let images = [];
|
|
756
|
+
let relatedQuestions = [];
|
|
757
|
+
try {
|
|
758
|
+
while (true) {
|
|
759
|
+
const { done, value } = yield reader.read();
|
|
760
|
+
if (done)
|
|
761
|
+
break;
|
|
762
|
+
buffer += decoder.decode(value, { stream: true });
|
|
763
|
+
const lines = buffer.split("\n");
|
|
764
|
+
buffer = lines.pop() || "";
|
|
765
|
+
for (const line of lines) {
|
|
766
|
+
if (line.startsWith("data: ")) {
|
|
767
|
+
const data = line.slice(6).trim();
|
|
768
|
+
if (data === "[DONE]") {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
try {
|
|
772
|
+
const parsed = JSON.parse(data);
|
|
773
|
+
const content = (_d = (_c = (_b = parsed.choices) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.delta) === null || _d === void 0 ? void 0 : _d.content;
|
|
774
|
+
if (content) {
|
|
775
|
+
fullContent += content;
|
|
776
|
+
}
|
|
777
|
+
// Capture citations from the final message if available
|
|
778
|
+
if (parsed.citations && Array.isArray(parsed.citations)) {
|
|
779
|
+
citations = parsed.citations;
|
|
780
|
+
}
|
|
781
|
+
// Capture images from the final message if available
|
|
782
|
+
if (parsed.images && Array.isArray(parsed.images)) {
|
|
783
|
+
images = parsed.images;
|
|
784
|
+
}
|
|
785
|
+
// Capture related questions from the final message if available
|
|
786
|
+
if (parsed.related_questions && Array.isArray(parsed.related_questions)) {
|
|
787
|
+
relatedQuestions = parsed.related_questions;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
catch (parseError) {
|
|
791
|
+
// Log invalid JSON for debugging but continue processing
|
|
792
|
+
console.error(`[Stream] Failed to parse SSE data: ${data.substring(0, 100)}`, parseError);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
finally {
|
|
799
|
+
reader.releaseLock();
|
|
800
|
+
}
|
|
801
|
+
// Append citations if available
|
|
802
|
+
if (citations.length > 0) {
|
|
803
|
+
fullContent += "\n\nCitations:\n";
|
|
804
|
+
citations.forEach((citation, index) => {
|
|
805
|
+
fullContent += `[${index + 1}] ${citation}\n`;
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
// Append images if available
|
|
809
|
+
if (images.length > 0) {
|
|
810
|
+
fullContent += "\n\nImages:\n";
|
|
811
|
+
images.forEach((img, index) => {
|
|
812
|
+
fullContent += `[${index + 1}] ${img.url} (${img.width}x${img.height}) - Source: ${img.origin_url}\n`;
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
// Append related questions if available
|
|
816
|
+
if (relatedQuestions.length > 0) {
|
|
817
|
+
fullContent += "\n\nRelated Questions:\n";
|
|
818
|
+
relatedQuestions.forEach((question, index) => {
|
|
819
|
+
fullContent += `${index + 1}. ${question}\n`;
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
return fullContent;
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Performs a single web search using the Perplexity Search API.
|
|
827
|
+
*
|
|
828
|
+
* @param {string} query - The search query string.
|
|
829
|
+
* @param {number} maxResults - Maximum number of results to return (1-20).
|
|
830
|
+
* @param {number} maxTokensPerPage - Maximum tokens to extract per webpage.
|
|
831
|
+
* @param {string} country - Optional ISO country code for regional results.
|
|
832
|
+
* @param {string[]} searchDomainFilter - Domain filter list for search results.
|
|
833
|
+
* @param {object} dateFilters - Optional date filtering parameters.
|
|
834
|
+
* @returns {Promise<PerplexitySearchResponse>} The raw search results data.
|
|
835
|
+
* @throws Will throw an error if the API request fails.
|
|
836
|
+
*/
|
|
837
|
+
function performSingleSearch(query_1) {
|
|
838
|
+
return __awaiter(this, arguments, void 0, function* (query, maxResults = 10, maxTokensPerPage = 1024, country, searchDomainFilter, dateFilters) {
|
|
839
|
+
const url = new URL("https://api.perplexity.ai/search");
|
|
840
|
+
const body = {
|
|
841
|
+
query: query,
|
|
842
|
+
max_results: maxResults,
|
|
843
|
+
max_tokens_per_page: maxTokensPerPage,
|
|
844
|
+
};
|
|
845
|
+
if (country) {
|
|
846
|
+
body.country = country;
|
|
847
|
+
}
|
|
848
|
+
if (searchDomainFilter && searchDomainFilter.length > 0) {
|
|
849
|
+
if (searchDomainFilter.length > MAX_DOMAIN_FILTERS) {
|
|
850
|
+
throw new Error(`search_domain_filter cannot exceed ${MAX_DOMAIN_FILTERS} domains`);
|
|
851
|
+
}
|
|
852
|
+
body.search_domain_filter = searchDomainFilter;
|
|
853
|
+
}
|
|
854
|
+
// Add date filtering parameters if provided
|
|
855
|
+
if (dateFilters === null || dateFilters === void 0 ? void 0 : dateFilters.search_recency_filter) {
|
|
856
|
+
body.search_recency_filter = dateFilters.search_recency_filter;
|
|
857
|
+
}
|
|
858
|
+
if (dateFilters === null || dateFilters === void 0 ? void 0 : dateFilters.search_after_date) {
|
|
859
|
+
body.search_after_date_filter = dateFilters.search_after_date;
|
|
860
|
+
}
|
|
861
|
+
if (dateFilters === null || dateFilters === void 0 ? void 0 : dateFilters.search_before_date) {
|
|
862
|
+
body.search_before_date_filter = dateFilters.search_before_date;
|
|
863
|
+
}
|
|
864
|
+
if (dateFilters === null || dateFilters === void 0 ? void 0 : dateFilters.last_updated_after) {
|
|
865
|
+
body.last_updated_after = dateFilters.last_updated_after;
|
|
866
|
+
}
|
|
867
|
+
if (dateFilters === null || dateFilters === void 0 ? void 0 : dateFilters.last_updated_before) {
|
|
868
|
+
body.last_updated_before = dateFilters.last_updated_before;
|
|
869
|
+
}
|
|
870
|
+
const response = yield fetchWithTimeout(url.toString(), { method: "POST", body: JSON.stringify(body) }, "Perplexity Search API");
|
|
871
|
+
let data;
|
|
872
|
+
try {
|
|
873
|
+
data = yield response.json();
|
|
874
|
+
}
|
|
875
|
+
catch (jsonError) {
|
|
876
|
+
throw new Error(`Failed to parse JSON response from Perplexity Search API: ${jsonError}`);
|
|
877
|
+
}
|
|
878
|
+
return data;
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Performs web search using the Perplexity Search API.
|
|
883
|
+
* Supports single query or batch of up to 5 queries.
|
|
884
|
+
*
|
|
885
|
+
* @param {string | string[]} query - The search query string or array of queries (max 5).
|
|
886
|
+
* @param {number} maxResults - Maximum number of results per query to return (1-20).
|
|
887
|
+
* @param {number} maxTokensPerPage - Maximum tokens to extract per webpage.
|
|
888
|
+
* @param {string} country - Optional ISO country code for regional results.
|
|
889
|
+
* @param {string[]} searchDomainFilter - Domain filter list for search results.
|
|
890
|
+
* @param {object} dateFilters - Optional date filtering parameters.
|
|
891
|
+
* @returns {Promise<string>} The formatted search results.
|
|
892
|
+
* @throws Will throw an error if the API request fails or query count exceeds 5.
|
|
893
|
+
*/
|
|
894
|
+
function performSearch(query_1) {
|
|
895
|
+
return __awaiter(this, arguments, void 0, function* (query, maxResults = 10, maxTokensPerPage = 1024, country, searchDomainFilter, dateFilters) {
|
|
896
|
+
const queries = Array.isArray(query) ? query : [query];
|
|
897
|
+
if (queries.length === 0) {
|
|
898
|
+
throw new Error("At least one query is required");
|
|
899
|
+
}
|
|
900
|
+
if (queries.length > MAX_BATCH_QUERIES) {
|
|
901
|
+
throw new Error(`Maximum ${MAX_BATCH_QUERIES} queries per request`);
|
|
902
|
+
}
|
|
903
|
+
// Single query - return simple format
|
|
904
|
+
if (queries.length === 1) {
|
|
905
|
+
const data = yield performSingleSearch(queries[0], maxResults, maxTokensPerPage, country, searchDomainFilter, dateFilters);
|
|
906
|
+
return formatSearchResults(data);
|
|
907
|
+
}
|
|
908
|
+
// Multiple queries - execute in parallel and format grouped results
|
|
909
|
+
const results = yield Promise.all(queries.map((q) => __awaiter(this, void 0, void 0, function* () {
|
|
910
|
+
try {
|
|
911
|
+
const data = yield performSingleSearch(q, maxResults, maxTokensPerPage, country, searchDomainFilter, dateFilters);
|
|
912
|
+
return { query: q, data, error: null };
|
|
913
|
+
}
|
|
914
|
+
catch (error) {
|
|
915
|
+
return { query: q, data: null, error: error instanceof Error ? error.message : String(error) };
|
|
916
|
+
}
|
|
917
|
+
})));
|
|
918
|
+
return formatMultiQueryResults(results);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Starts an async deep research job using the Perplexity Async API.
|
|
923
|
+
* Returns a request_id that can be used to poll for results.
|
|
924
|
+
*
|
|
925
|
+
* @param {Array<{ role: string; content: string }>} messages - An array of message objects.
|
|
926
|
+
* @param {object} options - Additional options for the async research.
|
|
927
|
+
* @param {string} options.reasoning_effort - Controls research depth (low, medium, high).
|
|
928
|
+
* @param {string[]} options.search_domain_filter - Domain filter list for search results.
|
|
929
|
+
* @returns {Promise<{ request_id: string; status: string }>} The async job info.
|
|
930
|
+
* @throws Will throw an error if the API request fails.
|
|
931
|
+
*/
|
|
932
|
+
function startAsyncResearch(messages, options) {
|
|
933
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
934
|
+
const url = new URL("https://api.perplexity.ai/async/chat/completions");
|
|
935
|
+
const body = {
|
|
936
|
+
model: "sonar-deep-research",
|
|
937
|
+
messages: messages,
|
|
938
|
+
};
|
|
939
|
+
if (options === null || options === void 0 ? void 0 : options.reasoning_effort) {
|
|
940
|
+
body.reasoning_effort = options.reasoning_effort;
|
|
941
|
+
}
|
|
942
|
+
if ((options === null || options === void 0 ? void 0 : options.search_domain_filter) && options.search_domain_filter.length > 0) {
|
|
943
|
+
if (options.search_domain_filter.length > MAX_DOMAIN_FILTERS) {
|
|
944
|
+
throw new Error(`search_domain_filter cannot exceed ${MAX_DOMAIN_FILTERS} domains`);
|
|
945
|
+
}
|
|
946
|
+
body.search_domain_filter = options.search_domain_filter;
|
|
947
|
+
}
|
|
948
|
+
const response = yield fetchWithTimeout(url.toString(), { method: "POST", body: JSON.stringify(body) }, "Perplexity Async API");
|
|
949
|
+
let data;
|
|
950
|
+
try {
|
|
951
|
+
data = yield response.json();
|
|
952
|
+
}
|
|
953
|
+
catch (jsonError) {
|
|
954
|
+
throw new Error(`Failed to parse JSON response from Perplexity Async API: ${jsonError}`);
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
request_id: data.request_id,
|
|
958
|
+
status: data.status || "pending",
|
|
959
|
+
};
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Gets the status and results of an async research job.
|
|
964
|
+
*
|
|
965
|
+
* @param {string} requestId - The request_id from startAsyncResearch.
|
|
966
|
+
* @returns {Promise<object>} The async job status and results.
|
|
967
|
+
* @throws Will throw an error if the API request fails.
|
|
968
|
+
*/
|
|
969
|
+
function getAsyncResearchStatus(requestId) {
|
|
970
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
971
|
+
const url = new URL(`https://api.perplexity.ai/async/chat/completions/${requestId}`);
|
|
972
|
+
const response = yield fetchWithTimeout(url.toString(), { method: "GET" }, "Perplexity Async API");
|
|
973
|
+
let data;
|
|
974
|
+
try {
|
|
975
|
+
data = yield response.json();
|
|
976
|
+
}
|
|
977
|
+
catch (jsonError) {
|
|
978
|
+
throw new Error(`Failed to parse JSON response from Perplexity Async API: ${jsonError}`);
|
|
979
|
+
}
|
|
980
|
+
const result = {
|
|
981
|
+
request_id: data.request_id,
|
|
982
|
+
status: data.status,
|
|
983
|
+
};
|
|
984
|
+
if (data.created_at)
|
|
985
|
+
result.created_at = data.created_at;
|
|
986
|
+
if (data.completed_at)
|
|
987
|
+
result.completed_at = data.completed_at;
|
|
988
|
+
if (data.error)
|
|
989
|
+
result.error = data.error;
|
|
990
|
+
// Format completed results with citations
|
|
991
|
+
if (data.status === "completed" && data.choices && data.choices[0]) {
|
|
992
|
+
let messageContent = data.choices[0].message.content;
|
|
993
|
+
if (data.citations && data.citations.length > 0) {
|
|
994
|
+
messageContent += "\n\nCitations:\n";
|
|
995
|
+
data.citations.forEach((citation, index) => {
|
|
996
|
+
messageContent += `[${index + 1}] ${citation}\n`;
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
result.result = messageContent;
|
|
1000
|
+
}
|
|
1001
|
+
return result;
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
// Initialize the server with tool metadata and capabilities
|
|
1005
|
+
const server = new Server({
|
|
1006
|
+
name: "@perplexity-ai/mcp-server",
|
|
1007
|
+
version: PACKAGE_VERSION,
|
|
1008
|
+
}, {
|
|
1009
|
+
capabilities: {
|
|
1010
|
+
tools: {},
|
|
1011
|
+
},
|
|
1012
|
+
});
|
|
1013
|
+
/**
|
|
1014
|
+
* Registers a handler for listing available tools.
|
|
1015
|
+
* When the client requests a list of tools, this handler returns all available Perplexity tools.
|
|
1016
|
+
*/
|
|
1017
|
+
server.setRequestHandler(ListToolsRequestSchema, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1018
|
+
return ({
|
|
1019
|
+
tools: [
|
|
1020
|
+
PERPLEXITY_ASK_TOOL,
|
|
1021
|
+
PERPLEXITY_RESEARCH_TOOL,
|
|
1022
|
+
PERPLEXITY_REASON_TOOL,
|
|
1023
|
+
PERPLEXITY_SEARCH_TOOL,
|
|
1024
|
+
PERPLEXITY_RESEARCH_ASYNC_TOOL,
|
|
1025
|
+
PERPLEXITY_RESEARCH_STATUS_TOOL,
|
|
1026
|
+
],
|
|
1027
|
+
});
|
|
1028
|
+
}));
|
|
1029
|
+
/**
|
|
1030
|
+
* Registers a handler for calling a specific tool.
|
|
1031
|
+
* Processes requests by validating input and invoking the appropriate tool.
|
|
1032
|
+
*
|
|
1033
|
+
* @param {object} request - The incoming tool call request.
|
|
1034
|
+
* @returns {Promise<object>} The response containing the tool's result or an error.
|
|
1035
|
+
*/
|
|
1036
|
+
server.setRequestHandler(CallToolRequestSchema, (request) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1037
|
+
try {
|
|
1038
|
+
const { name, arguments: args } = request.params;
|
|
1039
|
+
if (!args) {
|
|
1040
|
+
throw new Error("No arguments provided");
|
|
1041
|
+
}
|
|
1042
|
+
switch (name) {
|
|
1043
|
+
case "perplexity_ask": {
|
|
1044
|
+
if (!Array.isArray(args.messages)) {
|
|
1045
|
+
throw new Error("Invalid arguments for perplexity_ask: 'messages' must be an array");
|
|
1046
|
+
}
|
|
1047
|
+
const messages = args.messages;
|
|
1048
|
+
const model = typeof args.model === "string" && ["sonar", "sonar-pro"].includes(args.model)
|
|
1049
|
+
? args.model
|
|
1050
|
+
: DEFAULT_MODEL;
|
|
1051
|
+
const useStreaming = args.stream === true;
|
|
1052
|
+
const options = buildCommonOptions(args);
|
|
1053
|
+
const result = useStreaming
|
|
1054
|
+
? yield performStreamingChatCompletion(messages, model, Object.keys(options).length > 0 ? options : undefined)
|
|
1055
|
+
: yield performChatCompletion(messages, model, Object.keys(options).length > 0 ? options : undefined);
|
|
1056
|
+
return {
|
|
1057
|
+
content: [{ type: "text", text: result }],
|
|
1058
|
+
isError: false,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
case "perplexity_research": {
|
|
1062
|
+
if (!Array.isArray(args.messages)) {
|
|
1063
|
+
throw new Error("Invalid arguments for perplexity_research: 'messages' must be an array");
|
|
1064
|
+
}
|
|
1065
|
+
const messages = args.messages;
|
|
1066
|
+
const reasoningEffort = typeof args.reasoning_effort === "string" &&
|
|
1067
|
+
["low", "medium", "high"].includes(args.reasoning_effort)
|
|
1068
|
+
? args.reasoning_effort
|
|
1069
|
+
: undefined;
|
|
1070
|
+
const commonOptions = buildCommonOptions(args);
|
|
1071
|
+
const options = Object.assign({}, commonOptions);
|
|
1072
|
+
if (reasoningEffort)
|
|
1073
|
+
options.reasoning_effort = reasoningEffort;
|
|
1074
|
+
const result = yield performChatCompletion(messages, "sonar-deep-research", Object.keys(options).length > 0 ? options : undefined);
|
|
1075
|
+
return {
|
|
1076
|
+
content: [{ type: "text", text: result }],
|
|
1077
|
+
isError: false,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
case "perplexity_reason": {
|
|
1081
|
+
if (!Array.isArray(args.messages)) {
|
|
1082
|
+
throw new Error("Invalid arguments for perplexity_reason: 'messages' must be an array");
|
|
1083
|
+
}
|
|
1084
|
+
const messages = args.messages;
|
|
1085
|
+
const useStreaming = args.stream === true;
|
|
1086
|
+
const options = buildCommonOptions(args);
|
|
1087
|
+
const result = useStreaming
|
|
1088
|
+
? yield performStreamingChatCompletion(messages, "sonar-reasoning-pro", Object.keys(options).length > 0 ? options : undefined)
|
|
1089
|
+
: yield performChatCompletion(messages, "sonar-reasoning-pro", Object.keys(options).length > 0 ? options : undefined);
|
|
1090
|
+
return {
|
|
1091
|
+
content: [{ type: "text", text: result }],
|
|
1092
|
+
isError: false,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
case "perplexity_search": {
|
|
1096
|
+
// Validate query: must be string or array of strings
|
|
1097
|
+
const isValidQuery = typeof args.query === "string" ||
|
|
1098
|
+
(Array.isArray(args.query) && args.query.every((q) => typeof q === "string"));
|
|
1099
|
+
if (!isValidQuery) {
|
|
1100
|
+
throw new Error("Invalid arguments for perplexity_search: 'query' must be a string or array of strings");
|
|
1101
|
+
}
|
|
1102
|
+
// Validate array length if array
|
|
1103
|
+
if (Array.isArray(args.query) && args.query.length > MAX_BATCH_QUERIES) {
|
|
1104
|
+
throw new Error(`Invalid arguments for perplexity_search: maximum ${MAX_BATCH_QUERIES} queries allowed`);
|
|
1105
|
+
}
|
|
1106
|
+
const query = args.query;
|
|
1107
|
+
const { max_results, max_tokens_per_page, country } = args;
|
|
1108
|
+
const maxResults = typeof max_results === "number" ? max_results : undefined;
|
|
1109
|
+
const maxTokensPerPage = typeof max_tokens_per_page === "number" ? max_tokens_per_page : undefined;
|
|
1110
|
+
const countryCode = typeof country === "string" ? country : undefined;
|
|
1111
|
+
const searchDomainFilter = Array.isArray(args.search_domain_filter)
|
|
1112
|
+
? args.search_domain_filter.filter((d) => typeof d === "string")
|
|
1113
|
+
: undefined;
|
|
1114
|
+
const dateFilters = {};
|
|
1115
|
+
if (typeof args.search_recency_filter === "string" && ["day", "week", "month", "year"].includes(args.search_recency_filter)) {
|
|
1116
|
+
dateFilters.search_recency_filter = args.search_recency_filter;
|
|
1117
|
+
}
|
|
1118
|
+
if (typeof args.search_after_date === "string")
|
|
1119
|
+
dateFilters.search_after_date = args.search_after_date;
|
|
1120
|
+
if (typeof args.search_before_date === "string")
|
|
1121
|
+
dateFilters.search_before_date = args.search_before_date;
|
|
1122
|
+
if (typeof args.last_updated_after === "string")
|
|
1123
|
+
dateFilters.last_updated_after = args.last_updated_after;
|
|
1124
|
+
if (typeof args.last_updated_before === "string")
|
|
1125
|
+
dateFilters.last_updated_before = args.last_updated_before;
|
|
1126
|
+
const result = yield performSearch(query, maxResults, maxTokensPerPage, countryCode, searchDomainFilter, Object.keys(dateFilters).length > 0 ? dateFilters : undefined);
|
|
1127
|
+
return {
|
|
1128
|
+
content: [{ type: "text", text: result }],
|
|
1129
|
+
isError: false,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
case "perplexity_research_async": {
|
|
1133
|
+
if (!Array.isArray(args.messages)) {
|
|
1134
|
+
throw new Error("Invalid arguments for perplexity_research_async: 'messages' must be an array");
|
|
1135
|
+
}
|
|
1136
|
+
const messages = args.messages;
|
|
1137
|
+
const reasoningEffort = typeof args.reasoning_effort === "string" &&
|
|
1138
|
+
["low", "medium", "high"].includes(args.reasoning_effort)
|
|
1139
|
+
? args.reasoning_effort
|
|
1140
|
+
: undefined;
|
|
1141
|
+
const searchDomainFilter = Array.isArray(args.search_domain_filter)
|
|
1142
|
+
? args.search_domain_filter.filter((d) => typeof d === "string")
|
|
1143
|
+
: undefined;
|
|
1144
|
+
const options = {};
|
|
1145
|
+
if (reasoningEffort)
|
|
1146
|
+
options.reasoning_effort = reasoningEffort;
|
|
1147
|
+
if (searchDomainFilter && searchDomainFilter.length > 0)
|
|
1148
|
+
options.search_domain_filter = searchDomainFilter;
|
|
1149
|
+
const result = yield startAsyncResearch(messages, Object.keys(options).length > 0 ? options : undefined);
|
|
1150
|
+
return {
|
|
1151
|
+
content: [{
|
|
1152
|
+
type: "text",
|
|
1153
|
+
text: `Async research job started.\n\nRequest ID: ${result.request_id}\nStatus: ${result.status}\n\nUse perplexity_research_status with this request_id to check progress and retrieve results.`
|
|
1154
|
+
}],
|
|
1155
|
+
isError: false,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
case "perplexity_research_status": {
|
|
1159
|
+
if (typeof args.request_id !== "string") {
|
|
1160
|
+
throw new Error("Invalid arguments for perplexity_research_status: 'request_id' must be a string");
|
|
1161
|
+
}
|
|
1162
|
+
const statusResult = yield getAsyncResearchStatus(args.request_id);
|
|
1163
|
+
let responseText = `Request ID: ${statusResult.request_id}\nStatus: ${statusResult.status}`;
|
|
1164
|
+
if (statusResult.created_at)
|
|
1165
|
+
responseText += `\nCreated: ${statusResult.created_at}`;
|
|
1166
|
+
if (statusResult.completed_at)
|
|
1167
|
+
responseText += `\nCompleted: ${statusResult.completed_at}`;
|
|
1168
|
+
if (statusResult.status === "failed" && statusResult.error) {
|
|
1169
|
+
responseText += `\nError: ${statusResult.error}`;
|
|
1170
|
+
}
|
|
1171
|
+
if (statusResult.status === "completed" && statusResult.result) {
|
|
1172
|
+
responseText += `\n\n--- Research Results ---\n\n${statusResult.result}`;
|
|
1173
|
+
}
|
|
1174
|
+
if (statusResult.status === "pending" || statusResult.status === "processing") {
|
|
1175
|
+
responseText += `\n\nThe research is still in progress. Please poll again in a few seconds.`;
|
|
1176
|
+
}
|
|
1177
|
+
return {
|
|
1178
|
+
content: [{ type: "text", text: responseText }],
|
|
1179
|
+
isError: false,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
default:
|
|
1183
|
+
// Respond with an error if an unknown tool is requested
|
|
1184
|
+
return {
|
|
1185
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
1186
|
+
isError: true,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
catch (error) {
|
|
1191
|
+
// Return error details in the response
|
|
1192
|
+
return {
|
|
1193
|
+
content: [
|
|
1194
|
+
{
|
|
1195
|
+
type: "text",
|
|
1196
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1197
|
+
},
|
|
1198
|
+
],
|
|
1199
|
+
isError: true,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
}));
|
|
1203
|
+
/**
|
|
1204
|
+
* Initializes and runs the server using standard I/O for communication.
|
|
1205
|
+
* Logs an error and exits if the server fails to start.
|
|
1206
|
+
*/
|
|
1207
|
+
function runServer() {
|
|
1208
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1209
|
+
try {
|
|
1210
|
+
const transport = new StdioServerTransport();
|
|
1211
|
+
yield server.connect(transport);
|
|
1212
|
+
console.error("Perplexity MCP Server running on stdio with Ask, Research, Reason, Search, and Async Research tools");
|
|
1213
|
+
}
|
|
1214
|
+
catch (error) {
|
|
1215
|
+
console.error("Fatal error running server:", error);
|
|
1216
|
+
process.exit(1);
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
// Start the server and catch any startup errors
|
|
1221
|
+
runServer().catch((error) => {
|
|
1222
|
+
console.error("Fatal error running server:", error);
|
|
1223
|
+
process.exit(1);
|
|
1224
|
+
});
|