Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Advanced reasoning and problem-solving using the `sonar-reasoning-pro` model. Pe
1. Get your Perplexity API Key from the [API Portal](https://www.perplexity.ai/account/api/group)
2. Set it as an environment variable: `PERPLEXITY_API_KEY=your_key_here`
3. (Optional) Set a timeout for requests: `PERPLEXITY_TIMEOUT_MS=600000`. The default is 5 minutes.
4. (Optional) Set log level for debugging: `PERPLEXITY_LOG_LEVEL=DEBUG|INFO|WARN|ERROR`. The default is ERROR.

### Claude Code

Expand Down
11 changes: 6 additions & 5 deletions src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import express from "express";
import cors from "cors";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createPerplexityServer } from "./server.js";
import { logger } from "./logger.js";

// Check for required API key
const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
if (!PERPLEXITY_API_KEY) {
console.error("Error: PERPLEXITY_API_KEY environment variable is required");
logger.error("PERPLEXITY_API_KEY environment variable is required");
process.exit(1);
}

Expand Down Expand Up @@ -62,7 +63,7 @@ app.all("/mcp", async (req, res) => {

await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("Error handling MCP request:", error);
logger.error("Error handling MCP request", { error: String(error) });
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
Expand All @@ -84,10 +85,10 @@ app.get("/health", (req, res) => {
* Start the HTTP server
*/
app.listen(PORT, BIND_ADDRESS, () => {
console.log(`Perplexity MCP Server listening on http://${BIND_ADDRESS}:${PORT}/mcp`);
console.log(`Allowed origins: ${ALLOWED_ORIGINS.join(", ")}`);
logger.info(`Perplexity MCP Server listening on http://${BIND_ADDRESS}:${PORT}/mcp`);
logger.info(`Allowed origins: ${ALLOWED_ORIGINS.join(", ")}`);
}).on("error", (error) => {
console.error("Server error:", error);
logger.error("Server error", { error: String(error) });
process.exit(1);
});

24 changes: 12 additions & 12 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe("Perplexity MCP Server", () => {
});

it("should handle missing results array", () => {
const mockData = {};
const mockData = {} as any;
const formatted = formatSearchResults(mockData);
expect(formatted).toBe("No search results found.");
});
Expand Down Expand Up @@ -260,7 +260,7 @@ describe("Perplexity MCP Server", () => {
const messages = [{ role: "user", content: "test" }];

await expect(performChatCompletion(messages)).rejects.toThrow(
"missing or empty choices array"
"Invalid API response"
);
});

Expand All @@ -273,7 +273,7 @@ describe("Perplexity MCP Server", () => {
const messages = [{ role: "user", content: "test" }];

await expect(performChatCompletion(messages)).rejects.toThrow(
"missing message content"
"Invalid API response"
);
});

Expand All @@ -286,7 +286,7 @@ describe("Perplexity MCP Server", () => {
const messages = [{ role: "user", content: "test" }];

await expect(performChatCompletion(messages)).rejects.toThrow(
"missing or empty choices array"
"Invalid API response"
);
});

Expand All @@ -299,7 +299,7 @@ describe("Perplexity MCP Server", () => {
const messages = [{ role: "user", content: "test" }];

await expect(performChatCompletion(messages)).rejects.toThrow(
"missing message content"
"Invalid API response"
);
});

Expand All @@ -312,7 +312,7 @@ describe("Perplexity MCP Server", () => {
const messages = [{ role: "user", content: "test" }];

await expect(performChatCompletion(messages)).rejects.toThrow(
"missing or empty choices array"
"Invalid API response"
);
});

Expand All @@ -325,7 +325,7 @@ describe("Perplexity MCP Server", () => {
const messages = [{ role: "user", content: "test" }];

await expect(performChatCompletion(messages)).rejects.toThrow(
"missing message content"
"Invalid API response"
);
});

Expand Down Expand Up @@ -359,10 +359,10 @@ describe("Perplexity MCP Server", () => {
} as Response);

const messages = [{ role: "user", content: "test" }];
const result = await performChatCompletion(messages);

expect(result).toBe("Response");
expect(result).not.toContain("Citations:");
await expect(performChatCompletion(messages)).rejects.toThrow(
"Invalid API response"
);
});
});

Expand All @@ -378,7 +378,7 @@ describe("Perplexity MCP Server", () => {
const messages = [{ role: "user", content: "test" }];

await expect(performChatCompletion(messages)).rejects.toThrow(
"Failed to parse JSON response"
"Invalid API response"
);
});

Expand Down Expand Up @@ -588,7 +588,7 @@ describe("Perplexity MCP Server", () => {
{ title: null, url: "https://example.com", snippet: undefined },
{ title: "Valid", url: null, snippet: "snippet", date: undefined },
],
};
} as any;

const formatted = formatSearchResults(mockData);

Expand Down
93 changes: 93 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Simple structured logger for the Perplexity MCP Server
* Outputs to stderr to avoid interfering with STDIO transport
*/

export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}

const LOG_LEVEL_NAMES: Record<LogLevel, string> = {
[LogLevel.DEBUG]: "DEBUG",
[LogLevel.INFO]: "INFO",
[LogLevel.WARN]: "WARN",
[LogLevel.ERROR]: "ERROR",
};

/**
* Gets the configured log level from environment variable
* Defaults to ERROR to minimize noise in production
*/
function getLogLevel(): LogLevel {
const level = process.env.PERPLEXITY_LOG_LEVEL?.toUpperCase();
switch (level) {
case "DEBUG":
return LogLevel.DEBUG;
case "INFO":
return LogLevel.INFO;
case "WARN":
return LogLevel.WARN;
case "ERROR":
return LogLevel.ERROR;
default:
return LogLevel.ERROR;
}
}

const currentLogLevel = getLogLevel();

function safeStringify(obj: unknown): string {
try {
return JSON.stringify(obj);
} catch {
return "[Unstringifiable]";
}
}

/**
* Formats a log message with timestamp and level
*/
function formatMessage(level: LogLevel, message: string, meta?: Record<string, unknown>): string {
const timestamp = new Date().toISOString();
const levelName = LOG_LEVEL_NAMES[level];

if (meta && Object.keys(meta).length > 0) {
return `[${timestamp}] ${levelName}: ${message} ${safeStringify(meta)}`;
}

return `[${timestamp}] ${levelName}: ${message}`;
}

/**
* Logs a message if the configured log level allows it
*/
function log(level: LogLevel, message: string, meta?: Record<string, unknown>): void {
if (level >= currentLogLevel) {
const formatted = formatMessage(level, message, meta);
console.error(formatted); // Use stderr to avoid interfering with STDIO
}
}

/**
* Structured logger interface
*/
export const logger = {
debug(message: string, meta?: Record<string, unknown>): void {
log(LogLevel.DEBUG, message, meta);
},

info(message: string, meta?: Record<string, unknown>): void {
log(LogLevel.INFO, message, meta);
},

warn(message: string, meta?: Record<string, unknown>): void {
log(LogLevel.WARN, message, meta);
},

error(message: string, meta?: Record<string, unknown>): void {
log(LogLevel.ERROR, message, meta);
},
};
Loading