Skip to content

Commit 45e06cb

Browse files
committed
add validation for API responses and message structure
- validate choices array exists and is non-empty - validate message content is string type - add message structure validation in tool handlers - export functions for testing prevents null pointer crashes on malformed API responses follows MCP security best practices for zero-trust validation
1 parent 5968215 commit 45e06cb

File tree

1 file changed

+54
-24
lines changed

1 file changed

+54
-24
lines changed

index.ts

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,32 @@ if (!PERPLEXITY_API_KEY) {
161161
process.exit(1);
162162
}
163163

164-
// Configure timeout for API requests (default: 5 minutes)
165-
// Can be overridden via PERPLEXITY_TIMEOUT_MS environment variable
166-
const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10);
164+
/**
165+
* Validates an array of message objects for chat completion tools.
166+
* Ensures each message has a valid role and content field.
167+
*
168+
* @param {any} messages - The messages to validate
169+
* @param {string} toolName - The name of the tool calling this validation (for error messages)
170+
* @throws {Error} If messages is not an array or if any message is invalid
171+
*/
172+
function validateMessages(messages: any, toolName: string): void {
173+
if (!Array.isArray(messages)) {
174+
throw new Error(`Invalid arguments for ${toolName}: 'messages' must be an array`);
175+
}
176+
177+
for (let i = 0; i < messages.length; i++) {
178+
const msg = messages[i];
179+
if (!msg || typeof msg !== 'object') {
180+
throw new Error(`Invalid message at index ${i}: must be an object`);
181+
}
182+
if (!msg.role || typeof msg.role !== 'string') {
183+
throw new Error(`Invalid message at index ${i}: 'role' must be a string`);
184+
}
185+
if (msg.content === undefined || msg.content === null || typeof msg.content !== 'string') {
186+
throw new Error(`Invalid message at index ${i}: 'content' must be a string`);
187+
}
188+
}
189+
}
167190

168191
/**
169192
* Performs a chat completion by sending a request to the Perplexity API.
@@ -174,17 +197,20 @@ const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10);
174197
* @returns {Promise<string>} The chat completion result with appended citations.
175198
* @throws Will throw an error if the API request fails.
176199
*/
177-
async function performChatCompletion(
200+
export async function performChatCompletion(
178201
messages: Array<{ role: string; content: string }>,
179202
model: string = "sonar-pro"
180203
): Promise<string> {
204+
// Read timeout fresh each time to respect env var changes
205+
const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10);
206+
181207
// Construct the API endpoint URL and request body
182208
const url = new URL("https://api.perplexity.ai/chat/completions");
183209
const body = {
184210
model: model, // Model identifier passed as parameter
185211
messages: messages,
186212
// Additional parameters can be added here if required (e.g., max_tokens, temperature, etc.)
187-
// See the Sonar API documentation for more details:
213+
// See the Sonar API documentation for more details:
188214
// https://docs.perplexity.ai/api-reference/chat-completions
189215
};
190216

@@ -232,8 +258,18 @@ async function performChatCompletion(
232258
throw new Error(`Failed to parse JSON response from Perplexity API: ${jsonError}`);
233259
}
234260

235-
// Directly retrieve the main message content from the response
236-
let messageContent = data.choices[0].message.content;
261+
// Validate response structure
262+
if (!data.choices || !Array.isArray(data.choices) || data.choices.length === 0) {
263+
throw new Error("Invalid API response: missing or empty choices array");
264+
}
265+
266+
const firstChoice = data.choices[0];
267+
if (!firstChoice.message || typeof firstChoice.message.content !== 'string') {
268+
throw new Error("Invalid API response: missing message content");
269+
}
270+
271+
// Directly retrieve the main message content from the response
272+
let messageContent = firstChoice.message.content;
237273

238274
// If citations are provided, append them to the message content
239275
if (data.citations && Array.isArray(data.citations) && data.citations.length > 0) {
@@ -252,7 +288,7 @@ async function performChatCompletion(
252288
* @param {any} data - The search response data from the API.
253289
* @returns {string} Formatted search results.
254290
*/
255-
function formatSearchResults(data: any): string {
291+
export function formatSearchResults(data: any): string {
256292
if (!data.results || !Array.isArray(data.results)) {
257293
return "No search results found.";
258294
}
@@ -284,12 +320,15 @@ function formatSearchResults(data: any): string {
284320
* @returns {Promise<string>} The formatted search results.
285321
* @throws Will throw an error if the API request fails.
286322
*/
287-
async function performSearch(
323+
export async function performSearch(
288324
query: string,
289325
maxResults: number = 10,
290326
maxTokensPerPage: number = 1024,
291327
country?: string
292328
): Promise<string> {
329+
// Read timeout fresh each time to respect env var changes
330+
const TIMEOUT_MS = parseInt(process.env.PERPLEXITY_TIMEOUT_MS || "300000", 10);
331+
293332
const url = new URL("https://api.perplexity.ai/search");
294333
const body: any = {
295334
query: query,
@@ -383,35 +422,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
383422
}
384423
switch (name) {
385424
case "perplexity_ask": {
386-
if (!Array.isArray(args.messages)) {
387-
throw new Error("Invalid arguments for perplexity_ask: 'messages' must be an array");
388-
}
389-
// Invoke the chat completion function with the provided messages
390-
const messages = args.messages;
425+
validateMessages(args.messages, "perplexity_ask");
426+
const messages = args.messages as Array<{ role: string; content: string }>;
391427
const result = await performChatCompletion(messages, "sonar-pro");
392428
return {
393429
content: [{ type: "text", text: result }],
394430
isError: false,
395431
};
396432
}
397433
case "perplexity_research": {
398-
if (!Array.isArray(args.messages)) {
399-
throw new Error("Invalid arguments for perplexity_research: 'messages' must be an array");
400-
}
401-
// Invoke the chat completion function with the provided messages using the deep research model
402-
const messages = args.messages;
434+
validateMessages(args.messages, "perplexity_research");
435+
const messages = args.messages as Array<{ role: string; content: string }>;
403436
const result = await performChatCompletion(messages, "sonar-deep-research");
404437
return {
405438
content: [{ type: "text", text: result }],
406439
isError: false,
407440
};
408441
}
409442
case "perplexity_reason": {
410-
if (!Array.isArray(args.messages)) {
411-
throw new Error("Invalid arguments for perplexity_reason: 'messages' must be an array");
412-
}
413-
// Invoke the chat completion function with the provided messages using the reasoning model
414-
const messages = args.messages;
443+
validateMessages(args.messages, "perplexity_reason");
444+
const messages = args.messages as Array<{ role: string; content: string }>;
415445
const result = await performChatCompletion(messages, "sonar-reasoning-pro");
416446
return {
417447
content: [{ type: "text", text: result }],

0 commit comments

Comments
 (0)