@ruifung/codemode-bridge 1.0.3-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +378 -0
  3. package/dist/cli/commands.d.ts +70 -0
  4. package/dist/cli/commands.js +436 -0
  5. package/dist/cli/config-manager.d.ts +53 -0
  6. package/dist/cli/config-manager.js +142 -0
  7. package/dist/cli/index.d.ts +19 -0
  8. package/dist/cli/index.js +165 -0
  9. package/dist/executor/container-executor.d.ts +81 -0
  10. package/dist/executor/container-executor.js +351 -0
  11. package/dist/executor/executor-test-suite.d.ts +22 -0
  12. package/dist/executor/executor-test-suite.js +395 -0
  13. package/dist/executor/isolated-vm-executor.d.ts +78 -0
  14. package/dist/executor/isolated-vm-executor.js +368 -0
  15. package/dist/executor/vm2-executor.d.ts +21 -0
  16. package/dist/executor/vm2-executor.js +109 -0
  17. package/dist/executor/wrap-code.d.ts +52 -0
  18. package/dist/executor/wrap-code.js +80 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +6 -0
  21. package/dist/mcp/config.d.ts +44 -0
  22. package/dist/mcp/config.js +102 -0
  23. package/dist/mcp/e2e-bridge-test-suite.d.ts +28 -0
  24. package/dist/mcp/e2e-bridge-test-suite.js +429 -0
  25. package/dist/mcp/executor.d.ts +31 -0
  26. package/dist/mcp/executor.js +121 -0
  27. package/dist/mcp/mcp-adapter.d.ts +12 -0
  28. package/dist/mcp/mcp-adapter.js +49 -0
  29. package/dist/mcp/mcp-client.d.ts +85 -0
  30. package/dist/mcp/mcp-client.js +441 -0
  31. package/dist/mcp/oauth-handler.d.ts +33 -0
  32. package/dist/mcp/oauth-handler.js +138 -0
  33. package/dist/mcp/server.d.ts +25 -0
  34. package/dist/mcp/server.js +322 -0
  35. package/dist/mcp/token-persistence.d.ts +57 -0
  36. package/dist/mcp/token-persistence.js +131 -0
  37. package/dist/utils/logger.d.ts +44 -0
  38. package/dist/utils/logger.js +123 -0
  39. package/package.json +56 -0
@@ -0,0 +1,322 @@
1
+ /**
2
+ * MCP Server - Exposes the Code Mode bridge as an MCP server
3
+ *
4
+ * Architecture:
5
+ * - Upstream: Use official MCP SDK's Client to connect to and collect tools from other MCP servers
6
+ * - Orchestration: Pass collected tools to codemode SDK's createCodeTool()
7
+ * - Downstream: Use MCP SDK to expose the codemode tool via MCP protocol (stdio transport)
8
+ *
9
+ * This server:
10
+ * 1. Connects to upstream MCP servers using official MCP SDK Client
11
+ * 2. Collects tools from all upstream servers in native MCP format (JSON Schema)
12
+ * 3. Converts tools to ToolDescriptor format (with Zod schemas)
13
+ * 4. Uses @cloudflare/codemode SDK to create the "codemode" tool with those tools
14
+ * 5. Adapts the codemode SDK's AI SDK Tool to MCP protocol using a shim layer
15
+ * 6. Exposes the "codemode" tool via MCP protocol downstream
16
+ */
17
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
+ import { createCodeTool } from "@cloudflare/codemode/ai";
20
+ import { z } from "zod";
21
+ import { createExecutor } from "./executor.js";
22
+ import { adaptAISDKToolToMCP } from "./mcp-adapter.js";
23
+ import { MCPClient } from "./mcp-client.js";
24
+ import { logDebug, logError, logInfo, enableStderrBuffering } from "../utils/logger.js";
25
+ /**
26
+ * Convert JSON Schema to Zod schema
27
+ * MCP tools use JSON Schema, but createCodeTool expects Zod schemas
28
+ */
29
+ export function jsonSchemaToZod(schema) {
30
+ // Handle null/undefined
31
+ if (!schema) {
32
+ return z.object({}).strict();
33
+ }
34
+ // Handle object type
35
+ if (schema.type === "object" || !schema.type) {
36
+ const props = {};
37
+ if (schema.properties) {
38
+ for (const [key, prop] of Object.entries(schema.properties)) {
39
+ props[key] = jsonSchemaToZod(prop);
40
+ }
41
+ }
42
+ if (schema.required && Array.isArray(schema.required)) {
43
+ const required = new Set(schema.required);
44
+ const finalProps = {};
45
+ for (const [key, zodSchema] of Object.entries(props)) {
46
+ if (required.has(key)) {
47
+ finalProps[key] = zodSchema;
48
+ }
49
+ else {
50
+ finalProps[key] = zodSchema.optional();
51
+ }
52
+ }
53
+ return z.object(finalProps).strict();
54
+ }
55
+ // Make all fields optional if no required list
56
+ const optionalProps = {};
57
+ for (const [key, zodSchema] of Object.entries(props)) {
58
+ optionalProps[key] = zodSchema.optional();
59
+ }
60
+ return z.object(optionalProps).strict();
61
+ }
62
+ // Handle array type
63
+ if (schema.type === "array") {
64
+ const itemSchema = schema.items ? jsonSchemaToZod(schema.items) : z.any();
65
+ let arraySchema = z.array(itemSchema);
66
+ // Apply array constraints
67
+ if (typeof schema.minItems === "number") {
68
+ arraySchema = arraySchema.min(schema.minItems);
69
+ }
70
+ if (typeof schema.maxItems === "number") {
71
+ arraySchema = arraySchema.max(schema.maxItems);
72
+ }
73
+ return arraySchema;
74
+ }
75
+ // Handle string type
76
+ if (schema.type === "string") {
77
+ let stringSchema = z.string();
78
+ // Handle enum
79
+ if (schema.enum && Array.isArray(schema.enum)) {
80
+ return z.enum(schema.enum);
81
+ }
82
+ // Apply string format constraints
83
+ if (schema.format) {
84
+ switch (schema.format) {
85
+ case "email":
86
+ stringSchema = stringSchema.email();
87
+ break;
88
+ case "uuid":
89
+ stringSchema = stringSchema.uuid();
90
+ break;
91
+ case "url":
92
+ stringSchema = stringSchema.url();
93
+ break;
94
+ case "date-time":
95
+ stringSchema = stringSchema.datetime();
96
+ break;
97
+ }
98
+ }
99
+ // Apply string length constraints
100
+ if (typeof schema.minLength === "number") {
101
+ stringSchema = stringSchema.min(schema.minLength);
102
+ }
103
+ if (typeof schema.maxLength === "number") {
104
+ stringSchema = stringSchema.max(schema.maxLength);
105
+ }
106
+ if (schema.pattern) {
107
+ stringSchema = stringSchema.regex(new RegExp(schema.pattern));
108
+ }
109
+ return stringSchema;
110
+ }
111
+ // Handle number type
112
+ if (schema.type === "number") {
113
+ let numberSchema = z.number();
114
+ // Apply number constraints
115
+ if (typeof schema.minimum === "number") {
116
+ numberSchema = numberSchema.min(schema.minimum);
117
+ }
118
+ if (typeof schema.maximum === "number") {
119
+ numberSchema = numberSchema.max(schema.maximum);
120
+ }
121
+ if (typeof schema.multipleOf === "number") {
122
+ numberSchema = numberSchema.multipleOf(schema.multipleOf);
123
+ }
124
+ return numberSchema;
125
+ }
126
+ // Handle integer type
127
+ if (schema.type === "integer") {
128
+ let intSchema = z.number().int();
129
+ // Apply number constraints
130
+ if (typeof schema.minimum === "number") {
131
+ intSchema = intSchema.min(schema.minimum);
132
+ }
133
+ if (typeof schema.maximum === "number") {
134
+ intSchema = intSchema.max(schema.maximum);
135
+ }
136
+ if (typeof schema.multipleOf === "number") {
137
+ intSchema = intSchema.multipleOf(schema.multipleOf);
138
+ }
139
+ return intSchema;
140
+ }
141
+ // Handle boolean type
142
+ if (schema.type === "boolean") {
143
+ return z.boolean();
144
+ }
145
+ // Handle null type
146
+ if (schema.type === "null") {
147
+ return z.null();
148
+ }
149
+ // Handle anyOf (union types)
150
+ if (schema.anyOf && Array.isArray(schema.anyOf)) {
151
+ const schemas = schema.anyOf.map((s) => jsonSchemaToZod(s));
152
+ return z.union(schemas);
153
+ }
154
+ // Handle oneOf (discriminated union)
155
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
156
+ const schemas = schema.oneOf.map((s) => jsonSchemaToZod(s));
157
+ return z.union(schemas);
158
+ }
159
+ // Handle allOf (intersection types)
160
+ if (schema.allOf && Array.isArray(schema.allOf)) {
161
+ // Zod doesn't have native intersection for objects, so merge them
162
+ let merged = z.object({});
163
+ for (const subSchema of schema.allOf) {
164
+ const zodSchema = jsonSchemaToZod(subSchema);
165
+ merged = merged.and(zodSchema);
166
+ }
167
+ return merged;
168
+ }
169
+ // Handle enum for non-string types
170
+ if (schema.enum && Array.isArray(schema.enum)) {
171
+ if (schema.enum.length === 1) {
172
+ return z.literal(schema.enum[0]);
173
+ }
174
+ // Create union of literals
175
+ const literals = schema.enum.map((val) => z.literal(val));
176
+ return z.union(literals);
177
+ }
178
+ // Default to any
179
+ return z.any();
180
+ }
181
+ /**
182
+ * Convert native MCP tool definitions to ToolDescriptor format
183
+ * that createCodeTool() expects
184
+ */
185
+ function convertMCPToolToDescriptor(toolDef, client, toolName, serverName) {
186
+ return {
187
+ description: toolDef.description || "",
188
+ inputSchema: jsonSchemaToZod(toolDef.inputSchema),
189
+ execute: async (args) => {
190
+ // Log the tool invocation
191
+ logDebug(`Calling tool: ${serverName}__${toolName}`, {
192
+ component: 'Tool Execution',
193
+ server: serverName,
194
+ tool: toolName,
195
+ args: JSON.stringify(args)
196
+ });
197
+ try {
198
+ // Execute the tool on the upstream server using the MCP client
199
+ const result = await client.callTool(toolName, args);
200
+ // Log successful execution
201
+ logDebug(`Tool completed: ${serverName}__${toolName}`, {
202
+ component: 'Tool Execution',
203
+ server: serverName,
204
+ tool: toolName,
205
+ resultType: typeof result,
206
+ resultSize: JSON.stringify(result).length
207
+ });
208
+ return result;
209
+ }
210
+ catch (error) {
211
+ logDebug(`Tool failed: ${serverName}__${toolName}`, {
212
+ component: 'Tool Execution',
213
+ server: serverName,
214
+ tool: toolName,
215
+ error: error instanceof Error ? error.message : String(error)
216
+ });
217
+ throw error;
218
+ }
219
+ },
220
+ };
221
+ }
222
+ export async function startCodeModeBridgeServer(serverConfigs) {
223
+ // Enable buffering of stderr output from stdio tools during startup
224
+ enableStderrBuffering();
225
+ const mcp = new McpServer({
226
+ name: "codemode-bridge",
227
+ version: "1.0.0",
228
+ });
229
+ // Collect all tools from upstream MCP servers using official MCP SDK
230
+ const allToolDescriptors = {};
231
+ const toolsByServer = {}; // Track tools grouped by server
232
+ const mcpClients = []; // Keep track of clients for cleanup
233
+ let totalToolCount = 0;
234
+ // Initialize all connections in parallel
235
+ const connectionPromises = serverConfigs.map(async (config) => {
236
+ try {
237
+ // Create client for this upstream MCP server using official SDK
238
+ const client = new MCPClient(config);
239
+ // Connect to the upstream server
240
+ await client.connect();
241
+ mcpClients.push(client);
242
+ // Get tools from this server in native MCP format (JSON Schema)
243
+ const serverTools = await client.listTools();
244
+ const toolCount = serverTools.length;
245
+ totalToolCount += toolCount;
246
+ logDebug(`Server "${config.name}" has ${toolCount} tools`, { component: 'Bridge' });
247
+ // Track tools for this server
248
+ toolsByServer[config.name] = [];
249
+ // Namespace tools by server name to avoid conflicts
250
+ // e.g., kubernetes.get_pod -> kubernetes__get_pod
251
+ // Convert native MCP tools (JSON Schema) to ToolDescriptor format (Zod)
252
+ for (const tool of serverTools) {
253
+ const namespacedName = `${config.name}__${tool.name}`;
254
+ toolsByServer[config.name].push(namespacedName);
255
+ // Convert the native MCP tool to ToolDescriptor format
256
+ const descriptor = convertMCPToolToDescriptor(tool, client, tool.name, config.name);
257
+ allToolDescriptors[namespacedName] = descriptor;
258
+ }
259
+ return { config: config.name, toolCount, success: true };
260
+ }
261
+ catch (error) {
262
+ logError(`Failed to connect to "${config.name}"`, error instanceof Error ? error : { error: String(error) });
263
+ // Continue with other servers instead of failing completely
264
+ return { config: config.name, toolCount: 0, success: false };
265
+ }
266
+ });
267
+ // Wait for all connections to initialize in parallel
268
+ const results = await Promise.all(connectionPromises);
269
+ // Recalculate total tool count from results (in case totalToolCount wasn't updated due to timing)
270
+ totalToolCount = results.reduce((sum, result) => sum + (result?.toolCount || 0), 0);
271
+ logInfo(`Total: ${totalToolCount} tools from ${serverConfigs.length} server(s)`, { component: 'Bridge' });
272
+ // Log tools grouped by server
273
+ for (const [serverName, tools] of Object.entries(toolsByServer)) {
274
+ if (tools.length > 0) {
275
+ logInfo(`${serverName}: ${tools.join(', ')}`, { component: 'Bridge', server: serverName });
276
+ }
277
+ }
278
+ // Create the executor using the codemode SDK pattern
279
+ const { executor, info: executorInfo } = await createExecutor(30000); // 30 second timeout
280
+ // Create the codemode tool using the codemode SDK
281
+ // Pass ToolDescriptor format (with Zod schemas and execute functions)
282
+ logInfo(`Creating codemode tool with ${totalToolCount} tools from ${serverConfigs.length} server(s)`, { component: 'Bridge' });
283
+ const codemodeTool = createCodeTool({
284
+ tools: allToolDescriptors,
285
+ executor,
286
+ // Let the SDK auto-generate description from available tools
287
+ });
288
+ // Adapt the AI SDK Tool to MCP protocol format and register it
289
+ // The adaptAISDKToolToMCP function handles the protocol conversion
290
+ await adaptAISDKToolToMCP(mcp, codemodeTool);
291
+ // Register the status tool — returns executor mode, upstream servers, and tool counts
292
+ mcp.registerTool("status", {
293
+ description: "Get the current status of the codemode bridge: executor mode, upstream server connections, and available tools.",
294
+ inputSchema: z.object({}).strict(),
295
+ }, async () => {
296
+ const servers = Object.entries(toolsByServer).map(([name, tools]) => ({
297
+ name,
298
+ toolCount: tools.length,
299
+ tools,
300
+ }));
301
+ const status = {
302
+ executor: {
303
+ type: executorInfo.type,
304
+ reason: executorInfo.reason,
305
+ timeout: executorInfo.timeout,
306
+ },
307
+ servers,
308
+ totalTools: totalToolCount,
309
+ };
310
+ return {
311
+ content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
312
+ };
313
+ });
314
+ // Connect downstream MCP transport (what the client connects to)
315
+ const transport = new StdioServerTransport();
316
+ await mcp.connect(transport);
317
+ logInfo(`Ready on stdio transport`, { component: 'Bridge' });
318
+ logDebug(`Registering tool request handler`, {
319
+ component: 'Bridge'
320
+ });
321
+ logInfo(`Exposing 'eval' and 'status' tools`, { component: 'Bridge' });
322
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * OAuth2 Token Storage
3
+ *
4
+ * Persists OAuth tokens and client information to disk for reuse across sessions.
5
+ * Stored in ~/.config/codemode-bridge/mcp-tokens.json
6
+ */
7
+ import type { OAuthTokens, OAuthClientInformationMixed } from '@modelcontextprotocol/sdk/shared/auth.js';
8
+ /**
9
+ * Manages OAuth token storage for MCP server connections
10
+ */
11
+ export declare class TokenPersistence {
12
+ private configDir;
13
+ private tokenFile;
14
+ private storage;
15
+ constructor();
16
+ /**
17
+ * Load tokens from disk
18
+ */
19
+ private loadStorage;
20
+ /**
21
+ * Save storage to disk
22
+ */
23
+ private saveStorage;
24
+ /**
25
+ * Get stored client information for a server
26
+ */
27
+ getClientInformation(serverUrl: string): OAuthClientInformationMixed | undefined;
28
+ /**
29
+ * Save client information for a server
30
+ */
31
+ saveClientInformation(serverUrl: string, clientInfo: OAuthClientInformationMixed): void;
32
+ /**
33
+ * Get stored tokens for a server
34
+ */
35
+ getTokens(serverUrl: string): OAuthTokens | undefined;
36
+ /**
37
+ * Save tokens for a server
38
+ */
39
+ saveTokens(serverUrl: string, tokens: OAuthTokens): void;
40
+ /**
41
+ * Clear all tokens for a server (useful when revoked)
42
+ */
43
+ clearTokens(serverUrl: string): void;
44
+ /**
45
+ * Clear all information for a server
46
+ */
47
+ clearAll(serverUrl: string): void;
48
+ /**
49
+ * Check if a server has stored tokens (and whether they're expired)
50
+ * Returns { exists: boolean, isExpired: boolean }
51
+ */
52
+ getTokenStatus(serverUrl: string): {
53
+ exists: boolean;
54
+ isExpired: boolean;
55
+ };
56
+ }
57
+ export declare const tokenPersistence: TokenPersistence;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * OAuth2 Token Storage
3
+ *
4
+ * Persists OAuth tokens and client information to disk for reuse across sessions.
5
+ * Stored in ~/.config/codemode-bridge/mcp-tokens.json
6
+ */
7
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+ /**
11
+ * Manages OAuth token storage for MCP server connections
12
+ */
13
+ export class TokenPersistence {
14
+ constructor() {
15
+ this.storage = {};
16
+ this.configDir = join(homedir(), '.config', 'codemode-bridge');
17
+ this.tokenFile = join(this.configDir, 'mcp-tokens.json');
18
+ this.loadStorage();
19
+ }
20
+ /**
21
+ * Load tokens from disk
22
+ */
23
+ loadStorage() {
24
+ try {
25
+ if (existsSync(this.tokenFile)) {
26
+ const content = readFileSync(this.tokenFile, 'utf-8');
27
+ this.storage = JSON.parse(content);
28
+ }
29
+ }
30
+ catch (error) {
31
+ console.warn('Failed to load token storage:', error);
32
+ this.storage = {};
33
+ }
34
+ }
35
+ /**
36
+ * Save storage to disk
37
+ */
38
+ saveStorage() {
39
+ try {
40
+ // Ensure directory exists
41
+ if (!existsSync(this.configDir)) {
42
+ mkdirSync(this.configDir, { recursive: true });
43
+ }
44
+ // Write tokens file
45
+ writeFileSync(this.tokenFile, JSON.stringify(this.storage, null, 2));
46
+ }
47
+ catch (error) {
48
+ console.warn('Failed to save token storage:', error);
49
+ }
50
+ }
51
+ /**
52
+ * Get stored client information for a server
53
+ */
54
+ getClientInformation(serverUrl) {
55
+ return this.storage[serverUrl]?.clientInformation;
56
+ }
57
+ /**
58
+ * Save client information for a server
59
+ */
60
+ saveClientInformation(serverUrl, clientInfo) {
61
+ if (!this.storage[serverUrl]) {
62
+ this.storage[serverUrl] = { lastUpdated: Date.now() };
63
+ }
64
+ this.storage[serverUrl].clientInformation = clientInfo;
65
+ this.storage[serverUrl].lastUpdated = Date.now();
66
+ this.saveStorage();
67
+ }
68
+ /**
69
+ * Get stored tokens for a server
70
+ */
71
+ getTokens(serverUrl) {
72
+ const info = this.storage[serverUrl];
73
+ if (!info?.tokens) {
74
+ return undefined;
75
+ }
76
+ // Check if tokens have expired
77
+ const tokens = info.tokens;
78
+ const expiresAt = info.lastUpdated + ((tokens.expires_in || 3600) * 1000);
79
+ if (expiresAt < Date.now()) {
80
+ // Tokens expired - don't return them
81
+ // The OAuth provider will request new ones
82
+ delete info.tokens;
83
+ this.saveStorage();
84
+ return undefined;
85
+ }
86
+ return tokens;
87
+ }
88
+ /**
89
+ * Save tokens for a server
90
+ */
91
+ saveTokens(serverUrl, tokens) {
92
+ if (!this.storage[serverUrl]) {
93
+ this.storage[serverUrl] = { lastUpdated: Date.now() };
94
+ }
95
+ this.storage[serverUrl].tokens = tokens;
96
+ this.storage[serverUrl].lastUpdated = Date.now();
97
+ this.saveStorage();
98
+ }
99
+ /**
100
+ * Clear all tokens for a server (useful when revoked)
101
+ */
102
+ clearTokens(serverUrl) {
103
+ if (this.storage[serverUrl]) {
104
+ delete this.storage[serverUrl].tokens;
105
+ this.saveStorage();
106
+ }
107
+ }
108
+ /**
109
+ * Clear all information for a server
110
+ */
111
+ clearAll(serverUrl) {
112
+ delete this.storage[serverUrl];
113
+ this.saveStorage();
114
+ }
115
+ /**
116
+ * Check if a server has stored tokens (and whether they're expired)
117
+ * Returns { exists: boolean, isExpired: boolean }
118
+ */
119
+ getTokenStatus(serverUrl) {
120
+ const info = this.storage[serverUrl];
121
+ if (!info?.tokens) {
122
+ return { exists: false, isExpired: false };
123
+ }
124
+ const tokens = info.tokens;
125
+ const expiresAt = info.lastUpdated + ((tokens.expires_in || 3600) * 1000);
126
+ const isExpired = expiresAt < Date.now();
127
+ return { exists: true, isExpired };
128
+ }
129
+ }
130
+ // Singleton instance
131
+ export const tokenPersistence = new TokenPersistence();
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Logger utility using Winston
3
+ *
4
+ * Provides structured logging with debug mode support
5
+ * All logs go to stderr to avoid interfering with JSON-RPC protocol on stdout
6
+ */
7
+ import winston from 'winston';
8
+ /**
9
+ * Initialize the logger
10
+ */
11
+ export declare function initializeLogger(debug?: boolean): void;
12
+ /**
13
+ * Get the logger instance
14
+ */
15
+ export declare function getLogger(): winston.Logger;
16
+ /**
17
+ * Check if debug mode is enabled
18
+ */
19
+ export declare function isDebugEnabled(): boolean;
20
+ /**
21
+ * Enable buffering of stderr output from stdio tools
22
+ * Useful for deferring tool output until after startup is complete
23
+ */
24
+ export declare function enableStderrBuffering(): void;
25
+ /**
26
+ * Disable buffering and flush all buffered stderr messages
27
+ */
28
+ export declare function flushStderrBuffer(): void;
29
+ /**
30
+ * Log an info message
31
+ */
32
+ export declare function logInfo(message: string, meta?: Record<string, any>): void;
33
+ /**
34
+ * Log a debug message
35
+ */
36
+ export declare function logDebug(message: string, meta?: Record<string, any>): void;
37
+ /**
38
+ * Log a warning message
39
+ */
40
+ export declare function logWarn(message: string, meta?: Record<string, any>): void;
41
+ /**
42
+ * Log an error message
43
+ */
44
+ export declare function logError(message: string, error?: Error | Record<string, any>): void;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Logger utility using Winston
3
+ *
4
+ * Provides structured logging with debug mode support
5
+ * All logs go to stderr to avoid interfering with JSON-RPC protocol on stdout
6
+ */
7
+ import winston from 'winston';
8
+ import chalk from 'chalk';
9
+ let logger;
10
+ let debugMode = false;
11
+ let stderrBufferingEnabled = false;
12
+ let stderrBuffer = [];
13
+ /**
14
+ * Initialize the logger
15
+ */
16
+ export function initializeLogger(debug = false) {
17
+ debugMode = debug;
18
+ const level = debug ? 'debug' : 'info';
19
+ logger = winston.createLogger({
20
+ level,
21
+ format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), winston.format.printf(({ level, message, timestamp, component, suppressEarly, ...meta }) => {
22
+ // Color the level based on severity
23
+ let coloredLevel;
24
+ switch (level.toUpperCase()) {
25
+ case 'ERROR':
26
+ coloredLevel = chalk.red(`[${level.toUpperCase()}]`);
27
+ break;
28
+ case 'WARN':
29
+ coloredLevel = chalk.yellow(`[${level.toUpperCase()}]`);
30
+ break;
31
+ case 'INFO':
32
+ coloredLevel = chalk.green(`[${level.toUpperCase()}]`);
33
+ break;
34
+ case 'DEBUG':
35
+ coloredLevel = chalk.blue(`[${level.toUpperCase()}]`);
36
+ break;
37
+ default:
38
+ coloredLevel = `[${level.toUpperCase()}]`;
39
+ }
40
+ // Build the prefix with timestamp (debug only)
41
+ const timestampStr = debug ? `[${timestamp}] ` : '';
42
+ // Add component prefix if provided (colored in cyan)
43
+ const componentStr = component ? ` ${chalk.cyan(`[${component}]`)}` : '';
44
+ // Include remaining metadata if present (excluding suppressEarly which is internal)
45
+ const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';
46
+ return `${timestampStr}${coloredLevel}${componentStr} ${message}${metaStr}`.trim();
47
+ })),
48
+ transports: [
49
+ // Always write to stderr to avoid interfering with stdout (used for JSON-RPC protocol)
50
+ new winston.transports.Stream({ stream: process.stderr }),
51
+ ],
52
+ });
53
+ }
54
+ /**
55
+ * Get the logger instance
56
+ */
57
+ export function getLogger() {
58
+ if (!logger) {
59
+ initializeLogger();
60
+ }
61
+ return logger;
62
+ }
63
+ /**
64
+ * Check if debug mode is enabled
65
+ */
66
+ export function isDebugEnabled() {
67
+ return debugMode;
68
+ }
69
+ /**
70
+ * Enable buffering of stderr output from stdio tools
71
+ * Useful for deferring tool output until after startup is complete
72
+ */
73
+ export function enableStderrBuffering() {
74
+ stderrBufferingEnabled = true;
75
+ stderrBuffer = [];
76
+ }
77
+ /**
78
+ * Disable buffering and flush all buffered stderr messages
79
+ */
80
+ export function flushStderrBuffer() {
81
+ stderrBufferingEnabled = false;
82
+ const buffered = stderrBuffer;
83
+ stderrBuffer = [];
84
+ // Log all buffered messages directly without going through logInfo to avoid recursion
85
+ for (const { message, meta } of buffered) {
86
+ getLogger().info(message, meta);
87
+ }
88
+ }
89
+ /**
90
+ * Log an info message
91
+ */
92
+ export function logInfo(message, meta) {
93
+ // Buffer logs marked with suppressEarly during startup if buffering is enabled
94
+ if (stderrBufferingEnabled && meta?.suppressEarly) {
95
+ stderrBuffer.push({ message, meta });
96
+ }
97
+ else {
98
+ getLogger().info(message, meta);
99
+ }
100
+ }
101
+ /**
102
+ * Log a debug message
103
+ */
104
+ export function logDebug(message, meta) {
105
+ getLogger().debug(message, meta);
106
+ }
107
+ /**
108
+ * Log a warning message
109
+ */
110
+ export function logWarn(message, meta) {
111
+ getLogger().warn(message, meta);
112
+ }
113
+ /**
114
+ * Log an error message
115
+ */
116
+ export function logError(message, error) {
117
+ if (error instanceof Error) {
118
+ getLogger().error(message, { error: error.message, stack: error.stack });
119
+ }
120
+ else {
121
+ getLogger().error(message, error);
122
+ }
123
+ }