@@ -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