Skip to content

Commit 26a66ac

Browse files
committed
feat: add logger using winston
1 parent 4e123c9 commit 26a66ac

11 files changed

Lines changed: 883 additions & 166 deletions

File tree

package-lock.json

Lines changed: 426 additions & 134 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@
6868
"prettier": "^3.6.2",
6969
"tsx": "^4.20.5",
7070
"typescript": "^5.9.2",
71-
"vitest": "^3.2.4"
71+
"vitest": "^3.2.4",
72+
"winston": "^3.17.0"
7273
},
7374
"publishConfig": {
7475
"access": "public"

src/commands/mcp.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { parseKeyValue } from "../mcp/utils.js";
1010
import { addMCPServer, removeMCPServer } from "../mcp/config.js";
1111
import { getMCPClientManager } from "../mcp/manager.js";
1212
import type { MCPServerConfig, TransportType } from "../mcp/types.js";
13+
import { createLogger } from "../logger/index.js";
1314

1415
export function createMCPCommand(): Command {
1516
const mcp = new Command("mcp");
@@ -102,8 +103,14 @@ export function createMCPCommand(): Command {
102103
mcp
103104
.command("remove <name>")
104105
.description("Remove an MCP server")
105-
.action(async (name: string) => {
106+
.action(async function (this: Command, name: string) {
107+
const program = this.parent?.parent;
108+
const verbose = program?.opts<{ verbose?: boolean }>().verbose ?? false;
109+
const logger = createLogger({ silent: !verbose, level: "info" });
110+
106111
const mcpClientManager = getMCPClientManager();
112+
mcpClientManager.setLogger(logger);
113+
107114
await mcpClientManager.removeClient(name);
108115

109116
await removeMCPServer(name);

src/components/mcp/mcp-list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Table from "../ink-table.js";
88

99
export function MCPList({ verbose = false }: { verbose?: boolean }) {
1010
const { exit } = useApp();
11-
const { loading, rows, summary, error } = useMCPServers(exit);
11+
const { loading, rows, summary, error } = useMCPServers(exit, verbose);
1212

1313
// After data is ready and rendered, politely exit to finalize output
1414
useEffect(() => {

src/components/mcp/useMCPServers.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { useEffect, useState } from "react";
22
import type { Row, Summary } from "./types.js";
33
import { loadMCPConfig } from "../../mcp/config.js";
44
import { getMCPClientManager } from "../../mcp/manager.js";
5+
import { createLogger } from "../../logger/index.js";
56

6-
export function useMCPServers(exit: () => void) {
7+
export function useMCPServers(exit: () => void, verbose: boolean) {
78
const [loading, setLoading] = useState(true);
89
const [error, setError] = useState<string | null>(null);
910
const [rows, setRows] = useState<Row[]>([]);
@@ -17,8 +18,12 @@ export function useMCPServers(exit: () => void) {
1718
useEffect(() => {
1819
(async () => {
1920
try {
21+
const logger = createLogger({ silent: !verbose, level: "info" });
2022
const config = loadMCPConfig();
23+
2124
const manager = getMCPClientManager();
25+
manager.setLogger(logger);
26+
2227
const servers = Object.entries(config.servers || {});
2328

2429
if (servers.length === 0) return;

src/logger/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./logger.js";

src/logger/logger.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import fs from "node:fs";
2+
import winston from "winston";
3+
import { redactSensitiveData } from "../utils/masking.js";
4+
import path from "node:path";
5+
import chalk from "chalk";
6+
7+
const logLevels = {
8+
error: 0,
9+
warn: 1,
10+
info: 2,
11+
http: 3,
12+
verbose: 4,
13+
debug: 5,
14+
silly: 6,
15+
};
16+
17+
export interface LoggerOptions {
18+
level?: string;
19+
silent?: boolean;
20+
file?: string;
21+
}
22+
23+
type ChalkColor =
24+
| "red"
25+
| "green"
26+
| "yellow"
27+
| "blue"
28+
| "magenta"
29+
| "cyan"
30+
| "white"
31+
| "gray"
32+
| "redBright"
33+
| "greenBright"
34+
| "yellowBright"
35+
| "blueBright"
36+
| "magentaBright"
37+
| "cyanBright"
38+
| "whiteBright";
39+
40+
const levelColorMap: Record<string, (text: string) => string> = {
41+
error: chalk.red,
42+
warn: chalk.yellow,
43+
info: chalk.blue,
44+
http: chalk.cyan,
45+
verbose: chalk.magenta,
46+
debug: chalk.gray,
47+
silly: chalk.gray.dim,
48+
};
49+
50+
const maskFormat = winston.format((info) => {
51+
if (typeof info.message === "string") {
52+
info.message = redactSensitiveData(info.message);
53+
}
54+
return info;
55+
});
56+
57+
const fileFormat = winston.format.printf((info) => {
58+
const { level, message, timestamp, color, ...meta } = info as any;
59+
const metaKeys = Object.keys(meta || {});
60+
const metaStr = metaKeys.length ? ` ${JSON.stringify(meta)}` : "";
61+
return `${timestamp} [${String(level).toUpperCase()}]: ${message}${metaStr}`;
62+
});
63+
64+
const consoleFormat = winston.format.printf((info) => {
65+
const { level, message, timestamp, color, ...meta } = info as any;
66+
const colorize = levelColorMap[level] || chalk.white;
67+
let formattedMessage = message;
68+
69+
// Apply custom color if specified
70+
if (color && chalk[color as ChalkColor]) {
71+
formattedMessage = (chalk[color as ChalkColor] as any)(message);
72+
}
73+
74+
const metaKeys = Object.keys(meta || {});
75+
const metaStr = metaKeys.length ? ` ${JSON.stringify(meta)}` : "";
76+
77+
return `${chalk.dim(timestamp)} ${colorize(String(level).toUpperCase())}: ${formattedMessage}${metaStr}`;
78+
});
79+
80+
export class Logger {
81+
private logger: winston.Logger;
82+
private isSilent: boolean = false;
83+
84+
constructor(options: LoggerOptions = {}) {
85+
const level = options.level || "info";
86+
this.isSilent = options.silent || false;
87+
88+
const errorFormat = winston.format((info) => {
89+
if (info instanceof Error) {
90+
return Object.assign({}, info, {
91+
message: info.message,
92+
stack: info.stack,
93+
});
94+
}
95+
if (info.error instanceof Error) {
96+
info.message += `\n${info.error.stack}`;
97+
}
98+
return info;
99+
});
100+
101+
this.logger = winston.createLogger({
102+
levels: logLevels,
103+
level: level,
104+
format: winston.format.combine(
105+
errorFormat(),
106+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
107+
maskFormat(),
108+
),
109+
transports: this.createTransports(options.file),
110+
silent: this.isSilent,
111+
});
112+
113+
// Add colors to winston
114+
winston.addColors({
115+
error: "red",
116+
warn: "yellow",
117+
info: "blue",
118+
http: "cyan",
119+
verbose: "magenta",
120+
debug: "gray",
121+
silly: "gray",
122+
});
123+
}
124+
125+
error(message: string, meta?: any, color?: ChalkColor): void {
126+
this.logger.error(message, { ...meta, color });
127+
}
128+
129+
warn(message: string, meta?: any, color?: ChalkColor): void {
130+
this.logger.warn(message, { ...meta, color });
131+
}
132+
133+
info(message: string, meta?: any, color?: ChalkColor): void {
134+
this.logger.info(message, { ...meta, color });
135+
}
136+
137+
http(message: string, meta?: any, color?: ChalkColor): void {
138+
this.logger.http(message, { ...meta, color });
139+
}
140+
141+
verbose(message: string, meta?: any, color?: ChalkColor): void {
142+
this.logger.verbose(message, { ...meta, color });
143+
}
144+
145+
debug(message: string, meta?: any, color?: ChalkColor): void {
146+
this.logger.debug(message, { ...meta, color });
147+
}
148+
149+
silly(message: string, meta?: any, color?: ChalkColor): void {
150+
this.logger.silly(message, { ...meta, color });
151+
}
152+
153+
setSilent(silent: boolean): void {
154+
this.isSilent = silent;
155+
this.logger.silent = silent;
156+
}
157+
158+
getWinstonLogger(): winston.Logger {
159+
return this.logger;
160+
}
161+
162+
private createTransports(filePath?: string): winston.transport[] {
163+
const transports: winston.transport[] = [];
164+
165+
if (filePath) {
166+
// File transport
167+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
168+
transports.push(
169+
new winston.transports.File({
170+
filename: filePath,
171+
format: winston.format.combine(
172+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
173+
maskFormat(),
174+
fileFormat,
175+
),
176+
}),
177+
);
178+
} else {
179+
// Console transport
180+
transports.push(
181+
new winston.transports.Console({
182+
format: winston.format.combine(
183+
winston.format.timestamp({ format: "HH:mm:ss" }),
184+
maskFormat(),
185+
consoleFormat,
186+
),
187+
stderrLevels: Object.keys(logLevels), // Redirect all log levels to stderr
188+
}),
189+
);
190+
}
191+
192+
return transports;
193+
}
194+
}
195+
196+
export const createLogger = (options: LoggerOptions = {}): Logger => {
197+
return new Logger(options);
198+
};

0 commit comments

Comments
 (0)