mirror of
https://github.com/langchain-ai/langchainjs-mcp-adapters.git
synced 2026-07-01 12:27:48 -04:00
break: remove winston in favor of the debug package (#25)
* break: remove winston in favor of the debug package * fix: add missing test coverage dev dependency
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
[](https://www.npmjs.com/package/@langchain/mcp-adapters)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
This library provides a lightweight wrapper that makes [Anthropic Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) tools compatible with [LangChain.js](https://github.com/langchain-ai/langchainjs) and [LangGraph.js](https://github.com/langchain-ai/langgraphjs).
|
||||
This library provides a lightweight wrapper that makes[Anthropic Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) tools compatible with [LangChain.js](https://github.com/langchain-ai/langchainjs) and [LangGraph.js](https://github.com/langchain-ai/langgraphjs).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -25,7 +25,6 @@ This library provides a lightweight wrapper that makes [Anthropic Model Context
|
||||
- Optimized for OpenAI, Anthropic, and Google models
|
||||
|
||||
- 🛠️ **Development Features**
|
||||
- Comprehensive logging system
|
||||
- Flexible configuration options
|
||||
- Robust error handling
|
||||
|
||||
@@ -491,33 +490,30 @@ When using in browsers:
|
||||
|
||||
### Debug Logging
|
||||
|
||||
Logging is disabled by default for optimal performance. Enable logging when needed for diagnostics:
|
||||
This package makes use of the [debug](https://www.npmjs.com/package/debug) package for debug logging.
|
||||
|
||||
```typescript
|
||||
import { enableLogging, disableLogging } from '@langchain/mcp-adapters';
|
||||
Logging is disabled by default, and can be enabled by setting the `DEBUG` environment variable as per
|
||||
the instructions in the debug package.
|
||||
|
||||
// Enable logging at info level (default)
|
||||
enableLogging();
|
||||
To output all debug logs from this package:
|
||||
|
||||
// Or specify a specific level
|
||||
enableLogging('debug'); // Most verbose
|
||||
enableLogging('info'); // General information
|
||||
enableLogging('warn'); // Warnings only
|
||||
enableLogging('error'); // Errors only
|
||||
|
||||
// Disable logging when done
|
||||
disableLogging();
|
||||
```bash
|
||||
DEBUG='@langchain/mcp-adapters:*'
|
||||
```
|
||||
|
||||
You can also access the logger directly:
|
||||
To output debug logs only from the `client` module:
|
||||
|
||||
```typescript
|
||||
import { logger } from '@langchain/mcp-adapters';
|
||||
|
||||
// Advanced logging configuration
|
||||
logger.level = 'debug';
|
||||
```bash
|
||||
DEBUG='@langchain/mcp-adapters:client'
|
||||
```
|
||||
|
||||
To output debug logs only from the `tools` module:
|
||||
|
||||
```bash
|
||||
DEBUG='@langchain/mcp-adapters:tools'
|
||||
```
|
||||
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
// Mock fs and path modules
|
||||
vi.mock('fs', () => ({
|
||||
existsSync: vi.fn(),
|
||||
mkdirSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
unlinkSync: vi.fn(),
|
||||
accessSync: vi.fn(),
|
||||
}));
|
||||
vi.mock('path', () => ({
|
||||
join: vi.fn(),
|
||||
}));
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
const winston = await import('winston');
|
||||
|
||||
describe('Logger', () => {
|
||||
// Store original console.warn implementation
|
||||
const originalConsoleWarn = console.warn;
|
||||
let consoleWarnMock: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear module cache to ensure logger is reinitialized
|
||||
vi.resetModules();
|
||||
|
||||
// Mock console.warn to capture warnings
|
||||
consoleWarnMock = vi.spyOn(console, 'warn').mockImplementation((..._args) => {});
|
||||
|
||||
// Configure path.join to return predictable paths
|
||||
(path.join as any).mockImplementation((...args: string[]) => args.join('/'));
|
||||
|
||||
// Reset fs mock implementation
|
||||
(fs.existsSync as any).mockReset();
|
||||
(fs.mkdirSync as any).mockReset();
|
||||
(fs.writeFileSync as any).mockReset();
|
||||
(fs.unlinkSync as any).mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore console.warn
|
||||
consoleWarnMock.mockRestore();
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
test('should fallback to console-only logging when directory creation fails', async () => {
|
||||
// Mock fs.existsSync to return false (directory doesn't exist)
|
||||
(fs.existsSync as any).mockReturnValue(false);
|
||||
|
||||
// Mock fs.mkdirSync to throw an error
|
||||
(fs.mkdirSync as any).mockImplementation(() => {
|
||||
throw new Error('Permission denied');
|
||||
});
|
||||
|
||||
// Import logger (after mocks are set up)
|
||||
const logger = (await import('../src/logger.js')).default;
|
||||
|
||||
// Verify console warning was logged
|
||||
expect(consoleWarnMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Unable to set up file logging')
|
||||
);
|
||||
expect(consoleWarnMock).toHaveBeenCalledWith('Falling back to console logging only');
|
||||
|
||||
// Ensure logger was created with only console transport
|
||||
expect(logger.transports.length).toBe(1);
|
||||
expect(logger.transports[0]).toBeInstanceOf(winston.transports.Console);
|
||||
});
|
||||
|
||||
test('should fallback to console-only logging when write permission test fails', async () => {
|
||||
// Mock fs.existsSync to return true (directory exists)
|
||||
(fs.existsSync as any).mockReturnValue(true);
|
||||
|
||||
// Mock fs.writeFileSync to throw an error
|
||||
(fs.writeFileSync as any).mockImplementation(() => {
|
||||
throw new Error('Permission denied');
|
||||
});
|
||||
|
||||
// Import logger (after mocks are set up)
|
||||
const logger = (await import('../src/logger.js')).default;
|
||||
|
||||
// Verify console warning was logged
|
||||
expect(consoleWarnMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Unable to set up file logging')
|
||||
);
|
||||
expect(consoleWarnMock).toHaveBeenCalledWith('Falling back to console logging only');
|
||||
|
||||
// Ensure logger was created with only console transport
|
||||
expect(logger.transports.length).toBe(1);
|
||||
expect(logger.transports[0]).toBeInstanceOf(winston.transports.Console);
|
||||
});
|
||||
|
||||
test('should set up file transports when permissions are available', async () => {
|
||||
// Mock all the file operations to succeed
|
||||
(fs.mkdirSync as any).mockImplementation(() => true);
|
||||
(fs.accessSync as any).mockImplementation(() => true);
|
||||
(fs.existsSync as any).mockReturnValue(true);
|
||||
(fs.writeFileSync as any).mockImplementation(() => undefined);
|
||||
|
||||
// Import logger directly
|
||||
const loggerModule = await import('../src/logger.js');
|
||||
const logger = loggerModule.default;
|
||||
|
||||
// Just verify logger was created - don't worry about warnings
|
||||
expect(logger).toBeDefined();
|
||||
expect(typeof logger.debug).toBe('function');
|
||||
expect(typeof logger.info).toBe('function');
|
||||
expect(typeof logger.warn).toBe('function');
|
||||
expect(typeof logger.error).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// MCP client imports
|
||||
import { MultiServerMCPClient, enableLogging } from '../src/index.js';
|
||||
import { MultiServerMCPClient } from '../src/index.js';
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config();
|
||||
@@ -63,9 +63,6 @@ async function runExample() {
|
||||
let client: MultiServerMCPClient | null = null;
|
||||
|
||||
try {
|
||||
// Enable logging for better visibility
|
||||
enableLogging('info');
|
||||
|
||||
// Create the multiple servers configuration file
|
||||
createMultipleServersConfigFile();
|
||||
|
||||
|
||||
Generated
+639
-261
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -43,7 +43,7 @@
|
||||
"dependencies": {
|
||||
"@dmitryrechkin/json-schema-to-zod": "^1.0.1",
|
||||
"@modelcontextprotocol/sdk": "^1.7.0",
|
||||
"winston": "^3.17.0"
|
||||
"debug": "^4.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^0.3.40"
|
||||
@@ -55,6 +55,7 @@
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@langchain/langgraph": "^0.2.56",
|
||||
"@langchain/openai": "^0.4.4",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^22.13.10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.3.1",
|
||||
|
||||
+56
-45
@@ -5,7 +5,14 @@ import { StructuredToolInterface } from '@langchain/core/tools';
|
||||
import { loadMcpTools } from './tools.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import logger from './logger.js';
|
||||
import debug from 'debug';
|
||||
|
||||
const {
|
||||
default: { name: packageName },
|
||||
} = await import('../package.json');
|
||||
const moduleName = 'client';
|
||||
|
||||
const debugLog = debug(`${packageName}:${moduleName}`);
|
||||
|
||||
/**
|
||||
* Configuration for stdio transport connection
|
||||
@@ -119,14 +126,16 @@ export class MultiServerMCPClient {
|
||||
try {
|
||||
const defaultConfigPath = path.join(process.cwd(), 'mcp.json');
|
||||
if (fs.existsSync(defaultConfigPath)) {
|
||||
logger.info(`Found default configuration at ${defaultConfigPath}, loading automatically`);
|
||||
debugLog(
|
||||
`INFO: Found default configuration at ${defaultConfigPath}, loading automatically`
|
||||
);
|
||||
const config = MultiServerMCPClient.loadConfigFromFile(defaultConfigPath);
|
||||
return MultiServerMCPClient.processConnections(config.servers);
|
||||
} else {
|
||||
logger.debug('No default mcp.json found in root directory');
|
||||
debugLog(`INFO: No default mcp.json found in root directory`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to load default configuration: ${error}`);
|
||||
debugLog(`WARN: Failed to load default configuration: ${error}`);
|
||||
// Do not throw here, just continue with no configs
|
||||
}
|
||||
}
|
||||
@@ -143,7 +152,7 @@ export class MultiServerMCPClient {
|
||||
|
||||
// Validate that config has a servers property
|
||||
if (!config || typeof config !== 'object' || !('servers' in config)) {
|
||||
logger.error(`Invalid MCP configuration from ${configPath}: missing 'servers' property`);
|
||||
debugLog(`ERROR: Invalid MCP configuration from ${configPath}: missing 'servers' property`);
|
||||
throw new MCPClientError(`Invalid MCP configuration: missing 'servers' property`);
|
||||
}
|
||||
|
||||
@@ -178,7 +187,7 @@ export class MultiServerMCPClient {
|
||||
if (envValue) {
|
||||
config.env[key] = envValue;
|
||||
} else {
|
||||
logger.warn(`Environment variable ${envVar} not found for server "${serverName}"`);
|
||||
debugLog(`WARN: Environment variable ${envVar} not found for server "${serverName}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,7 +233,7 @@ export class MultiServerMCPClient {
|
||||
|
||||
for (const [serverName, config] of Object.entries(connections)) {
|
||||
if (typeof config !== 'object' || config === null) {
|
||||
logger.warn(`Invalid configuration for server "${serverName}". Skipping.`);
|
||||
debugLog(`WARN: Invalid configuration for server "${serverName}". Skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -532,10 +541,10 @@ export class MultiServerMCPClient {
|
||||
client.connections = MultiServerMCPClient.processConnections(config.servers);
|
||||
}
|
||||
|
||||
logger.info(`Loaded MCP configuration from ${configPath}`);
|
||||
debugLog(`INFO: Loaded MCP configuration from ${configPath}`);
|
||||
return client;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to load MCP configuration from ${configPath}: ${error}`);
|
||||
debugLog(`ERROR: Failed to load MCP configuration from ${configPath}: ${error}`);
|
||||
throw new MCPClientError(`Failed to load MCP configuration: ${error}`);
|
||||
}
|
||||
}
|
||||
@@ -548,12 +557,12 @@ export class MultiServerMCPClient {
|
||||
*/
|
||||
async initializeConnections(): Promise<Map<string, StructuredToolInterface[]>> {
|
||||
if (!this.connections || Object.keys(this.connections).length === 0) {
|
||||
logger.warn('No connections to initialize');
|
||||
debugLog(`WARN: No connections to initialize`);
|
||||
return new Map();
|
||||
}
|
||||
|
||||
for (const [serverName, connection] of Object.entries(this.connections)) {
|
||||
logger.info(`Initializing connection to server "${serverName}"...`);
|
||||
debugLog(`INFO: Initializing connection to server "${serverName}"...`);
|
||||
|
||||
if (connection.transport === 'stdio') {
|
||||
await this.initializeStdioConnection(serverName, connection);
|
||||
@@ -580,8 +589,8 @@ export class MultiServerMCPClient {
|
||||
): Promise<void> {
|
||||
const { command, args, env, restart } = connection;
|
||||
|
||||
logger.debug(
|
||||
`Creating stdio transport for server "${serverName}" with command: ${command} ${args.join(' ')}`
|
||||
debugLog(
|
||||
`DEBUG: Creating stdio transport for server "${serverName}" with command: ${command} ${args.join(' ')}`
|
||||
);
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
@@ -614,7 +623,7 @@ export class MultiServerMCPClient {
|
||||
this.clients.set(serverName, client);
|
||||
|
||||
const cleanup = async () => {
|
||||
logger.debug(`Closing stdio transport for server "${serverName}"`);
|
||||
debugLog(`DEBUG: Closing stdio transport for server "${serverName}"`);
|
||||
await transport.close();
|
||||
};
|
||||
|
||||
@@ -641,7 +650,7 @@ export class MultiServerMCPClient {
|
||||
|
||||
// Only attempt restart if we haven't cleaned up
|
||||
if (this.clients.has(serverName)) {
|
||||
logger.info(`Process for server "${serverName}" exited, attempting to restart...`);
|
||||
debugLog(`INFO: Process for server "${serverName}" exited, attempting to restart...`);
|
||||
await this.attemptReconnect(serverName, connection, restart.maxAttempts, restart.delayMs);
|
||||
}
|
||||
};
|
||||
@@ -656,7 +665,7 @@ export class MultiServerMCPClient {
|
||||
): Promise<void> {
|
||||
const { url, headers, useNodeEventSource, reconnect } = connection;
|
||||
|
||||
logger.debug(`Creating SSE transport for server "${serverName}" with URL: ${url}`);
|
||||
debugLog(`DEBUG: Creating SSE transport for server "${serverName}" with URL: ${url}`);
|
||||
|
||||
try {
|
||||
const transport = await this.createSSETransport(serverName, url, headers, useNodeEventSource);
|
||||
@@ -684,7 +693,7 @@ export class MultiServerMCPClient {
|
||||
this.clients.set(serverName, client);
|
||||
|
||||
const cleanup = async () => {
|
||||
logger.debug(`Closing SSE transport for server "${serverName}"`);
|
||||
debugLog(`DEBUG: Closing SSE transport for server "${serverName}"`);
|
||||
await transport.close();
|
||||
};
|
||||
|
||||
@@ -714,7 +723,7 @@ export class MultiServerMCPClient {
|
||||
return new SSEClientTransport(new URL(url));
|
||||
}
|
||||
|
||||
logger.debug(`Using custom headers for SSE transport to server "${serverName}"`);
|
||||
debugLog(`DEBUG: Using custom headers for SSE transport to server "${serverName}"`);
|
||||
|
||||
// If useNodeEventSource is true, try Node.js implementations
|
||||
if (useNodeEventSource) {
|
||||
@@ -722,11 +731,11 @@ export class MultiServerMCPClient {
|
||||
}
|
||||
|
||||
// For browser environments, use the basic requestInit approach
|
||||
logger.debug(
|
||||
`Using browser EventSource for server "${serverName}". Headers may not be applied correctly.`
|
||||
debugLog(
|
||||
`DEBUG: Using browser EventSource for server "${serverName}". Headers may not be applied correctly.`
|
||||
);
|
||||
logger.debug(
|
||||
`For better headers support in browsers, consider using a custom SSE implementation.`
|
||||
debugLog(
|
||||
`DEBUG: For better headers support in browsers, consider using a custom SSE implementation.`
|
||||
);
|
||||
|
||||
return new SSEClientTransport(new URL(url), {
|
||||
@@ -748,8 +757,8 @@ export class MultiServerMCPClient {
|
||||
const ExtendedEventSourceModule = await import('extended-eventsource');
|
||||
const ExtendedEventSource = ExtendedEventSourceModule.EventSource;
|
||||
|
||||
logger.debug(`Using Extended EventSource for server "${serverName}"`);
|
||||
logger.debug(`Setting headers for Extended EventSource: ${JSON.stringify(headers)}`);
|
||||
debugLog(`DEBUG: Using Extended EventSource for server "${serverName}"`);
|
||||
debugLog(`DEBUG: Setting headers for Extended EventSource: ${JSON.stringify(headers)}`);
|
||||
|
||||
// Override the global EventSource with the extended implementation
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -763,8 +772,8 @@ export class MultiServerMCPClient {
|
||||
});
|
||||
} catch (extendedError) {
|
||||
// Fall back to standard eventsource if extended-eventsource is not available
|
||||
logger.debug(
|
||||
`Extended EventSource not available, falling back to standard EventSource: ${extendedError}`
|
||||
debugLog(
|
||||
`DEBUG: Extended EventSource not available, falling back to standard EventSource: ${extendedError}`
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -772,8 +781,8 @@ export class MultiServerMCPClient {
|
||||
const EventSourceModule = await import('eventsource');
|
||||
const EventSource = EventSourceModule.default;
|
||||
|
||||
logger.debug(`Using Node.js EventSource for server "${serverName}"`);
|
||||
logger.debug(`Setting headers for EventSource: ${JSON.stringify(headers)}`);
|
||||
debugLog(`DEBUG: Using Node.js EventSource for server "${serverName}"`);
|
||||
debugLog(`DEBUG: Setting headers for EventSource: ${JSON.stringify(headers)}`);
|
||||
|
||||
// Override the global EventSource with the Node.js implementation
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -786,8 +795,8 @@ export class MultiServerMCPClient {
|
||||
requestInit: { headers },
|
||||
});
|
||||
} catch (nodeError) {
|
||||
logger.warn(
|
||||
`Failed to load EventSource packages for server "${serverName}". Headers may not be applied to SSE connection: ${nodeError}`
|
||||
debugLog(
|
||||
`WARN: Failed to load EventSource packages for server "${serverName}". Headers may not be applied to SSE connection: ${nodeError}`
|
||||
);
|
||||
|
||||
// Last resort fallback
|
||||
@@ -816,7 +825,9 @@ export class MultiServerMCPClient {
|
||||
|
||||
// Only attempt reconnect if we haven't cleaned up
|
||||
if (this.clients.has(serverName)) {
|
||||
logger.info(`SSE connection for server "${serverName}" closed, attempting to reconnect...`);
|
||||
debugLog(
|
||||
`INFO: SSE connection for server "${serverName}" closed, attempting to reconnect...`
|
||||
);
|
||||
await this.attemptReconnect(
|
||||
serverName,
|
||||
connection,
|
||||
@@ -832,10 +843,10 @@ export class MultiServerMCPClient {
|
||||
*/
|
||||
private async loadToolsForServer(serverName: string, client: Client): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Loading tools for server "${serverName}"...`);
|
||||
debugLog(`DEBUG: Loading tools for server "${serverName}"...`);
|
||||
const tools = await loadMcpTools(client);
|
||||
this.serverNameToTools.set(serverName, tools);
|
||||
logger.info(`Successfully loaded ${tools.length} tools from server "${serverName}"`);
|
||||
debugLog(`INFO: Successfully loaded ${tools.length} tools from server "${serverName}"`);
|
||||
} catch (error) {
|
||||
throw new MCPClientError(`Failed to load tools from server "${serverName}": ${error}`);
|
||||
}
|
||||
@@ -864,8 +875,8 @@ export class MultiServerMCPClient {
|
||||
|
||||
while (!connected && (maxAttempts === undefined || attempts < maxAttempts)) {
|
||||
attempts++;
|
||||
logger.info(
|
||||
`Reconnection attempt ${attempts}${maxAttempts ? `/${maxAttempts}` : ''} for server "${serverName}"`
|
||||
debugLog(
|
||||
`INFO: Reconnection attempt ${attempts}${maxAttempts ? `/${maxAttempts}` : ''} for server "${serverName}"`
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -884,17 +895,17 @@ export class MultiServerMCPClient {
|
||||
// Check if connected
|
||||
if (this.clients.has(serverName)) {
|
||||
connected = true;
|
||||
logger.info(`Successfully reconnected to server "${serverName}"`);
|
||||
debugLog(`INFO: Successfully reconnected to server "${serverName}"`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to reconnect to server "${serverName}" (attempt ${attempts}): ${error}`
|
||||
debugLog(
|
||||
`ERROR: Failed to reconnect to server "${serverName}" (attempt ${attempts}): ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
logger.error(`Failed to reconnect to server "${serverName}" after ${attempts} attempts`);
|
||||
debugLog(`ERROR: Failed to reconnect to server "${serverName}" after ${attempts} attempts`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -965,13 +976,13 @@ export class MultiServerMCPClient {
|
||||
* Close all connections.
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
logger.info('Closing all MCP connections...');
|
||||
debugLog(`INFO: Closing all MCP connections...`);
|
||||
|
||||
for (const cleanup of this.cleanupFunctions) {
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (error) {
|
||||
logger.error(`Error during cleanup: ${error}`);
|
||||
debugLog(`ERROR: Error during cleanup: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -980,7 +991,7 @@ export class MultiServerMCPClient {
|
||||
this.serverNameToTools.clear();
|
||||
this.transportInstances.clear();
|
||||
|
||||
logger.info('All MCP connections closed');
|
||||
debugLog(`INFO: All MCP connections closed`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1077,10 +1088,10 @@ export class MultiServerMCPClient {
|
||||
this.connections = MultiServerMCPClient.processConnections(config.servers);
|
||||
}
|
||||
|
||||
logger.info(`Added MCP configuration from ${configPath}`);
|
||||
debugLog(`INFO: Added MCP configuration from ${configPath}`);
|
||||
return this;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to add MCP configuration from ${configPath}: ${error}`);
|
||||
debugLog(`ERROR: Failed to add MCP configuration from ${configPath}: ${error}`);
|
||||
throw new MCPClientError(`Failed to add MCP configuration: ${error}`);
|
||||
}
|
||||
}
|
||||
@@ -1104,7 +1115,7 @@ export class MultiServerMCPClient {
|
||||
this.connections = processedConnections;
|
||||
}
|
||||
|
||||
logger.info(`Added ${Object.keys(processedConnections).length} connections to client`);
|
||||
debugLog(`INFO: Added ${Object.keys(processedConnections).length} connections to client`);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export { MultiServerMCPClient } from './client.js';
|
||||
export { logger, enableLogging, disableLogging } from './logger.js';
|
||||
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
import * as winston from 'winston';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* Logging levels:
|
||||
* error: 0 - Severe errors that cause the application to crash or malfunction
|
||||
* warn: 1 - Warnings that don't stop the application but should be addressed
|
||||
* info: 2 - General information about application operation
|
||||
* http: 3 - HTTP request/response information
|
||||
* debug: 4 - Detailed debugging information
|
||||
*/
|
||||
const levels = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
http: 3,
|
||||
debug: 4,
|
||||
};
|
||||
|
||||
/**
|
||||
* By default, logging is set to the silent level unless explicitly enabled
|
||||
* This makes logging opt-in rather than enabled by default
|
||||
*/
|
||||
const defaultLevel = 'silent';
|
||||
|
||||
/**
|
||||
* Define colors for each log level to improve readability in the console.
|
||||
*/
|
||||
const colors = {
|
||||
error: 'red',
|
||||
warn: 'yellow',
|
||||
info: 'green',
|
||||
http: 'magenta',
|
||||
debug: 'white',
|
||||
};
|
||||
|
||||
// Add colors to Winston
|
||||
winston.addColors(colors);
|
||||
|
||||
/**
|
||||
* Define the format for log messages.
|
||||
* We include a timestamp, colorize the output, and format the message.
|
||||
*/
|
||||
const format = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
|
||||
winston.format.colorize({ all: true }),
|
||||
winston.format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
|
||||
);
|
||||
|
||||
/**
|
||||
* Define the transports for log messages.
|
||||
* Always log to console, and only log to files if we have permission.
|
||||
*/
|
||||
// Base transports array (always include console)
|
||||
const transports: winston.transport[] = [
|
||||
// Console transport
|
||||
new winston.transports.Console(),
|
||||
];
|
||||
|
||||
// Attempt to add file transports only if we have write permissions
|
||||
try {
|
||||
const logsDir = path.join(process.cwd(), 'logs');
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Test write permissions with a small file
|
||||
const testFile = path.join(logsDir, '.permissions-test');
|
||||
fs.writeFileSync(testFile, 'test');
|
||||
fs.unlinkSync(testFile);
|
||||
|
||||
// If we reach here, we have write permissions - add file transports
|
||||
transports.push(
|
||||
// File transport for errors
|
||||
new winston.transports.File({
|
||||
filename: path.join(logsDir, 'error.log'),
|
||||
level: 'error',
|
||||
}),
|
||||
|
||||
// File transport for all logs
|
||||
new winston.transports.File({
|
||||
filename: path.join(logsDir, 'all.log'),
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
// If any error occurs during the file operations, log to console only
|
||||
console.warn(
|
||||
`Unable to set up file logging: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
console.warn('Falling back to console logging only');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the logger instance with our configuration.
|
||||
* By default, logging is disabled (silent) but can be enabled by setting the level.
|
||||
*/
|
||||
export const logger = winston.createLogger({
|
||||
level: defaultLevel, // Start with silent logging by default
|
||||
levels,
|
||||
format,
|
||||
transports,
|
||||
});
|
||||
|
||||
/**
|
||||
* Enable logging at the specified level.
|
||||
*
|
||||
* @param level - The log level to enable ('error', 'warn', 'info', 'http', 'debug')
|
||||
*/
|
||||
export function enableLogging(level: keyof typeof levels | 'silent' = 'info'): void {
|
||||
logger.level = level;
|
||||
logger.debug(`Logging enabled at level: ${level}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable all logging.
|
||||
*/
|
||||
export function disableLogging(): void {
|
||||
logger.level = 'silent';
|
||||
}
|
||||
|
||||
export default logger;
|
||||
+15
-8
@@ -6,7 +6,14 @@ import {
|
||||
} from '@langchain/core/tools';
|
||||
import { JSONSchema, JSONSchemaToZod } from '@dmitryrechkin/json-schema-to-zod';
|
||||
|
||||
import logger from './logger.js';
|
||||
import debug from 'debug';
|
||||
|
||||
const {
|
||||
default: { name: packageName },
|
||||
} = await import('../package.json');
|
||||
const moduleName = 'tools';
|
||||
|
||||
const debugLog = debug(`${packageName}:${moduleName}`);
|
||||
|
||||
interface TextContent {
|
||||
type: 'text';
|
||||
@@ -67,7 +74,7 @@ function _convertCallToolResult(
|
||||
|
||||
// Check for errors
|
||||
if (result.isError) {
|
||||
logger.error('MCP tool returned an error result');
|
||||
debugLog('ERROR: MCP tool returned an error result');
|
||||
throw new ToolException(
|
||||
typeof finalTextOutput === 'string' ? finalTextOutput : textOutput.join('\n')
|
||||
);
|
||||
@@ -96,7 +103,7 @@ async function _callTool(
|
||||
args: Record<string, unknown>
|
||||
): Promise<string | [string | string[], NonTextContent[] | null]> {
|
||||
try {
|
||||
logger.info(`Calling tool ${name}(${JSON.stringify(args)})`);
|
||||
debugLog(`INFO: Calling tool ${name}(${JSON.stringify(args)})`);
|
||||
const result = await client.callTool({
|
||||
name,
|
||||
arguments: args,
|
||||
@@ -108,7 +115,7 @@ async function _callTool(
|
||||
content: result.content || [],
|
||||
});
|
||||
|
||||
logger.info(`Tool ${name} returned: ${JSON.stringify({ textContent, nonTextContent })}`);
|
||||
debugLog(`INFO: Tool ${name} returned: ${JSON.stringify({ textContent, nonTextContent })}`);
|
||||
|
||||
// Return based on the response format
|
||||
if (responseFormat === 'content_and_artifact') {
|
||||
@@ -118,7 +125,7 @@ async function _callTool(
|
||||
// Default to returning just the text content
|
||||
return typeof textContent === 'string' ? textContent : textContent.join('\n');
|
||||
} catch (error) {
|
||||
logger.error(`Error calling tool ${name}: ${String(error)}`);
|
||||
debugLog(`ERROR: Error calling tool ${name}: ${String(error)}`);
|
||||
if (error instanceof ToolException) {
|
||||
throw error;
|
||||
}
|
||||
@@ -140,7 +147,7 @@ export async function loadMcpTools(
|
||||
): Promise<StructuredToolInterface[]> {
|
||||
// Get tools in a single operation
|
||||
const toolsResponse = await client.listTools();
|
||||
logger.info(`Found ${toolsResponse.tools?.length || 0} MCP tools`);
|
||||
debugLog(`INFO: Found ${toolsResponse.tools?.length || 0} MCP tools`);
|
||||
|
||||
// Filter out tools without names and convert in a single map operation
|
||||
return (toolsResponse.tools || [])
|
||||
@@ -156,10 +163,10 @@ export async function loadMcpTools(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
func: _callTool.bind(null, client, tool.name, responseFormat) as any,
|
||||
});
|
||||
logger.debug(`Successfully loaded tool: ${dst.name}`);
|
||||
debugLog(`INFO: Successfully loaded tool: ${dst.name}`);
|
||||
return dst;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to load tool "${tool.name}":`, error);
|
||||
debugLog(`ERROR: Failed to load tool "${tool.name}":`, error);
|
||||
if (throwOnLoadError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"downlevelIteration": true,
|
||||
"noEmit": true // we just want type checking - no need to output for inclusion in the published module
|
||||
"noEmit": true, // we just want type checking - no need to output for inclusion in the published module
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["examples/**/*", "src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
|
||||
+2
-1
@@ -10,7 +10,8 @@
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"downlevelIteration": true
|
||||
"downlevelIteration": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "examples/**/*"]
|
||||
|
||||
Reference in New Issue
Block a user