Skip to content

Commit 2ca3a69

Browse files
authored
Merge pull request #71 from Ompragash/feat/type-safety-logging
2 parents 8c5556c + e546489 commit 2ca3a69

File tree

9 files changed

+562
-54
lines changed

9 files changed

+562
-54
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Advanced reasoning and problem-solving using the `sonar-reasoning-pro` model. Pe
3333
1. Get your Perplexity API Key from the [API Portal](https://www.perplexity.ai/account/api/group)
3434
2. Set it as an environment variable: `PERPLEXITY_API_KEY=your_key_here`
3535
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.
3637

3738
### Claude Code
3839

src/http.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import express from "express";
44
import cors from "cors";
55
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
66
import { createPerplexityServer } from "./server.js";
7+
import { logger } from "./logger.js";
78

89
// Check for required API key
910
const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
1011
if (!PERPLEXITY_API_KEY) {
11-
console.error("Error: PERPLEXITY_API_KEY environment variable is required");
12+
logger.error("PERPLEXITY_API_KEY environment variable is required");
1213
process.exit(1);
1314
}
1415

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

6364
await transport.handleRequest(req, res, req.body);
6465
} catch (error) {
65-
console.error("Error handling MCP request:", error);
66+
logger.error("Error handling MCP request", { error: String(error) });
6667
if (!res.headersSent) {
6768
res.status(500).json({
6869
jsonrpc: "2.0",
@@ -84,10 +85,10 @@ app.get("/health", (req, res) => {
8485
* Start the HTTP server
8586
*/
8687
app.listen(PORT, BIND_ADDRESS, () => {
87-
console.log(`Perplexity MCP Server listening on http://${BIND_ADDRESS}:${PORT}/mcp`);
88-
console.log(`Allowed origins: ${ALLOWED_ORIGINS.join(", ")}`);
88+
logger.info(`Perplexity MCP Server listening on http://${BIND_ADDRESS}:${PORT}/mcp`);
89+
logger.info(`Allowed origins: ${ALLOWED_ORIGINS.join(", ")}`);
8990
}).on("error", (error) => {
90-
console.error("Server error:", error);
91+
logger.error("Server error", { error: String(error) });
9192
process.exit(1);
9293
});
9394

src/index.test.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe("Perplexity MCP Server", () => {
4848
});
4949

5050
it("should handle missing results array", () => {
51-
const mockData = {};
51+
const mockData = {} as any;
5252
const formatted = formatSearchResults(mockData);
5353
expect(formatted).toBe("No search results found.");
5454
});
@@ -248,6 +248,41 @@ describe("Perplexity MCP Server", () => {
248248
"Perplexity Search API error: 500 Internal Server Error"
249249
);
250250
});
251+
252+
it("should handle search timeout errors", async () => {
253+
process.env.PERPLEXITY_TIMEOUT_MS = "100";
254+
255+
global.fetch = vi.fn().mockImplementation((_url, options) => {
256+
return new Promise((resolve, reject) => {
257+
const signal = options?.signal as AbortSignal;
258+
259+
if (signal) {
260+
signal.addEventListener("abort", () => {
261+
reject(new DOMException("The operation was aborted.", "AbortError"));
262+
});
263+
}
264+
265+
setTimeout(() => {
266+
resolve({
267+
ok: true,
268+
json: async () => ({ results: [] }),
269+
} as Response);
270+
}, 200);
271+
});
272+
});
273+
274+
await expect(performSearch("test")).rejects.toThrow(
275+
"Request timeout"
276+
);
277+
});
278+
279+
it("should handle search network errors", async () => {
280+
global.fetch = vi.fn().mockRejectedValue(new Error("Network failure"));
281+
282+
await expect(performSearch("test")).rejects.toThrow(
283+
"Network error while calling Perplexity Search API"
284+
);
285+
});
251286
});
252287

253288
describe("API Response Validation", () => {
@@ -359,10 +394,10 @@ describe("Perplexity MCP Server", () => {
359394
} as Response);
360395

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

364-
expect(result).toBe("Response");
365-
expect(result).not.toContain("Citations:");
398+
await expect(performChatCompletion(messages)).rejects.toThrow(
399+
"Failed to parse JSON response"
400+
);
366401
});
367402
});
368403

@@ -588,7 +623,7 @@ describe("Perplexity MCP Server", () => {
588623
{ title: null, url: "https://example.com", snippet: undefined },
589624
{ title: "Valid", url: null, snippet: "snippet", date: undefined },
590625
],
591-
};
626+
} as any;
592627

593628
const formatted = formatSearchResults(mockData);
594629

src/logger.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Simple structured logger for the Perplexity MCP Server
3+
* Outputs to stderr to avoid interfering with STDIO transport
4+
*/
5+
6+
export enum LogLevel {
7+
DEBUG = 0,
8+
INFO = 1,
9+
WARN = 2,
10+
ERROR = 3,
11+
}
12+
13+
const LOG_LEVEL_NAMES: Record<LogLevel, string> = {
14+
[LogLevel.DEBUG]: "DEBUG",
15+
[LogLevel.INFO]: "INFO",
16+
[LogLevel.WARN]: "WARN",
17+
[LogLevel.ERROR]: "ERROR",
18+
};
19+
20+
/**
21+
* Gets the configured log level from environment variable
22+
* Defaults to ERROR to minimize noise in production
23+
*/
24+
function getLogLevel(): LogLevel {
25+
const level = process.env.PERPLEXITY_LOG_LEVEL?.toUpperCase();
26+
switch (level) {
27+
case "DEBUG":
28+
return LogLevel.DEBUG;
29+
case "INFO":
30+
return LogLevel.INFO;
31+
case "WARN":
32+
return LogLevel.WARN;
33+
case "ERROR":
34+
return LogLevel.ERROR;
35+
default:
36+
return LogLevel.ERROR;
37+
}
38+
}
39+
40+
const currentLogLevel = getLogLevel();
41+
42+
function safeStringify(obj: unknown): string {
43+
try {
44+
return JSON.stringify(obj);
45+
} catch {
46+
return "[Unstringifiable]";
47+
}
48+
}
49+
50+
/**
51+
* Formats a log message with timestamp and level
52+
*/
53+
function formatMessage(level: LogLevel, message: string, meta?: Record<string, unknown>): string {
54+
const timestamp = new Date().toISOString();
55+
const levelName = LOG_LEVEL_NAMES[level];
56+
57+
if (meta && Object.keys(meta).length > 0) {
58+
return `[${timestamp}] ${levelName}: ${message} ${safeStringify(meta)}`;
59+
}
60+
61+
return `[${timestamp}] ${levelName}: ${message}`;
62+
}
63+
64+
/**
65+
* Logs a message if the configured log level allows it
66+
*/
67+
function log(level: LogLevel, message: string, meta?: Record<string, unknown>): void {
68+
if (level >= currentLogLevel) {
69+
const formatted = formatMessage(level, message, meta);
70+
console.error(formatted); // Use stderr to avoid interfering with STDIO
71+
}
72+
}
73+
74+
/**
75+
* Structured logger interface
76+
*/
77+
export const logger = {
78+
debug(message: string, meta?: Record<string, unknown>): void {
79+
log(LogLevel.DEBUG, message, meta);
80+
},
81+
82+
info(message: string, meta?: Record<string, unknown>): void {
83+
log(LogLevel.INFO, message, meta);
84+
},
85+
86+
warn(message: string, meta?: Record<string, unknown>): void {
87+
log(LogLevel.WARN, message, meta);
88+
},
89+
90+
error(message: string, meta?: Record<string, unknown>): void {
91+
log(LogLevel.ERROR, message, meta);
92+
},
93+
};

0 commit comments

Comments
 (0)