Skip to content

Commit f87c162

Browse files
committed
Add logging
*Add logging * Update README * Add mock 'fetch' factory for the API * Tweak example log messages
1 parent 3a07f34 commit f87c162

File tree

8 files changed

+653
-15
lines changed

8 files changed

+653
-15
lines changed

README.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,110 @@ const client = new McpdClient({
112112
});
113113
```
114114

115+
### Logging
116+
117+
The SDK includes optional logging for warnings about unhealthy or non-existent servers that are skipped during operations.
118+
119+
**Important:** Logging is disabled by default. Only enable logging in non-MCP-server contexts. MCP servers using stdio transport for JSON-RPC communication should never enable logging, as it will contaminate stdout/stderr and break the protocol.
120+
121+
#### Using Environment Variable
122+
123+
Set the `MCPD_LOG_LEVEL` environment variable to control logging:
124+
125+
```bash
126+
# Valid levels: trace, debug, info, warn, error, off (default)
127+
export MCPD_LOG_LEVEL=warn
128+
```
129+
130+
**Available Log Levels:**
131+
132+
| Level | Description |
133+
| ------- | --------------------------------------------------------------- |
134+
| `trace` | Verbose information (includes `debug`, `info`, `warn`, `error`) |
135+
| `debug` | Debug information (includes `info`, `warn`, `error`) |
136+
| `info` | General informational messages (includes `warn`, `error`) |
137+
| `warn` | Warning messages only (includes `error`) |
138+
| `error` | Error messages only |
139+
| `off` | (...or unset) Logging disabled (default) |
140+
141+
```typescript
142+
// Logging is automatically enabled based on MCPD_LOG_LEVEL
143+
const client = new McpdClient({
144+
apiEndpoint: "http://localhost:8090",
145+
});
146+
```
147+
148+
#### Using Custom Logger
149+
150+
For advanced use cases, inject your own logger implementation.
151+
152+
**Partial Logger Support:** You can provide only the methods you want to customize. Any omitted methods will fall back to the default logger, which respects `MCPD_LOG_LEVEL`.
153+
154+
```typescript
155+
import { McpdClient } from "@mozilla-ai/mcpd";
156+
157+
// Full custom logger
158+
const client = new McpdClient({
159+
apiEndpoint: "http://localhost:8090",
160+
logger: {
161+
trace: (...args) => myLogger.trace(args),
162+
debug: (...args) => myLogger.debug(args),
163+
info: (...args) => myLogger.info(args),
164+
warn: (...args) => myLogger.warn(args),
165+
error: (...args) => myLogger.error(args),
166+
},
167+
});
168+
169+
// Partial logger: custom warn/error, default (MCPD_LOG_LEVEL-aware) for others
170+
const client2 = new McpdClient({
171+
apiEndpoint: "http://localhost:8090",
172+
logger: {
173+
warn: (msg) => console.warn(`[MCPD] ${msg}`),
174+
error: (msg) => console.error(`[MCPD] ${msg}`),
175+
// trace, debug, info use default logger (respects MCPD_LOG_LEVEL)
176+
},
177+
});
178+
```
179+
180+
#### Disabling Logging
181+
182+
To disable logging, simply ensure `MCPD_LOG_LEVEL` is unset or set to `off` (the default):
183+
184+
```typescript
185+
// Logging is disabled by default (no configuration needed)
186+
const client = new McpdClient({
187+
apiEndpoint: "http://localhost:8090",
188+
});
189+
```
190+
191+
If you need to disable logging even when `MCPD_LOG_LEVEL` is set (rare case), provide a custom logger with no-op implementations:
192+
193+
```typescript
194+
// Override MCPD_LOG_LEVEL to force disable
195+
const client = new McpdClient({
196+
apiEndpoint: "http://localhost:8090",
197+
logger: {
198+
trace: () => {},
199+
debug: () => {},
200+
info: () => {},
201+
warn: () => {},
202+
error: () => {},
203+
},
204+
});
205+
```
206+
207+
When logging is enabled, warnings are emitted for:
208+
209+
- Unhealthy servers that are skipped (e.g., status `timeout`, `unreachable`)
210+
- Non-existent servers specified in filter options
211+
212+
Example warning messages:
213+
214+
```
215+
Skipping unhealthy server 'time' with status 'timeout'
216+
Skipping non-existent server 'unknown'
217+
```
218+
115219
### Core Methods
116220

117221
#### `client.listServers()`

examples/langchain/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ async function main() {
106106
} catch (error) {
107107
if (error instanceof McpdError) {
108108
console.error('------------------------------');
109-
console.error(`[MCPD ERROR] ${error.message}`);
109+
console.error(`[mcpd ERROR] ${error.message}`);
110110
console.error('------------------------------');
111111
} else if (error.code === 'ECONNREFUSED') {
112112
console.error('------------------------------');

examples/vercel-ai/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ async function main() {
9797
} catch (error) {
9898
if (error instanceof McpdError) {
9999
console.error('------------------------------');
100-
console.error(`[MCPD ERROR] ${error.message}`);
100+
console.error(`[mcpd ERROR] ${error.message}`);
101101
console.error('------------------------------');
102102
} else if (error.code === 'ECONNREFUSED') {
103103
console.error('------------------------------');

src/client.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { createCache } from "./utils/cache";
4343
import { ServersNamespace } from "./dynamicCaller";
4444
import { FunctionBuilder, type AgentFunction } from "./functionBuilder";
4545
import { API_PATHS } from "./apiPaths";
46+
import { createLogger, type Logger } from "./logger";
4647

4748
/**
4849
* Maximum number of server health entries to cache.
@@ -94,6 +95,7 @@ export class McpdClient {
9495
readonly #timeout: number;
9596
readonly #serverHealthCache: LRUCache<string, ServerHealth | Error>;
9697
readonly #functionBuilder: FunctionBuilder;
98+
readonly #logger: Logger;
9799
readonly #cacheableExceptions = new Set([
98100
ServerNotFoundError,
99101
ServerUnhealthyError,
@@ -111,19 +113,22 @@ export class McpdClient {
111113
* @param options - Configuration options for the client
112114
*/
113115
constructor(options: McpdClientOptions) {
114-
// Remove trailing slash from endpoint
116+
// Remove trailing slash from endpoint.
115117
this.#endpoint = options.apiEndpoint.replace(/\/$/, "");
116118
this.#apiKey = options.apiKey;
117119
this.#timeout = options.timeout ?? 30000;
118120

119-
// Setup health cache
120-
const healthCacheTtlMs = (options.healthCacheTtl ?? 10) * 1000; // Convert to milliseconds
121+
// Setup health cache.
122+
const healthCacheTtlMs = (options.healthCacheTtl ?? 10) * 1000; // Convert to milliseconds.
121123
this.#serverHealthCache = createCache({
122124
max: SERVER_HEALTH_CACHE_MAXSIZE,
123125
ttl: healthCacheTtlMs,
124126
});
125127

126-
// Initialize servers namespace and function builder with injected functions
128+
// Setup logger (the default logger uses MCPD_LOG_LEVEL).
129+
this.#logger = createLogger(options.logger);
130+
131+
// Initialize servers namespace and function builder with injected functions.
127132
this.servers = new ServersNamespace({
128133
performCall: this.#performCall.bind(this),
129134
getTools: this.#getToolsByServer.bind(this),
@@ -608,20 +613,33 @@ export class McpdClient {
608613
* This helper fetches server names (if not provided) and filters to only healthy servers.
609614
* Used by getToolSchemas(), getPrompts(), and agentTools() to avoid timeouts on failed servers.
610615
*
611-
* @param servers - Optional array of server names. If not provided, fetches all servers.
612-
* @returns Array of healthy server names
613-
* @internal
616+
* If logging is enabled, warnings are logged for servers that are skipped
617+
* due to unhealthy status or non-existence.
618+
*
619+
* @param servers - Optional list of server names to filter. If not provided,
620+
* checks health of all servers.
621+
* @returns List of server names with 'ok' health status.
614622
*/
615623
async #getHealthyServers(servers?: string[]): Promise<string[]> {
616-
// Get server names if not provided.
617-
const serverNames =
618-
servers && servers.length > 0 ? servers : await this.listServers();
619-
620-
// Get health status and filter to healthy servers.
624+
const serverNames = servers?.length ? servers : await this.listServers();
621625
const healthMap = await this.getServerHealth();
626+
622627
return serverNames.filter((name) => {
623628
const health = healthMap[name];
624-
return health && HealthStatusHelpers.isHealthy(health.status);
629+
630+
if (!health) {
631+
this.#logger.warn(`Skipping non-existent server '${name}'`);
632+
return false;
633+
}
634+
635+
if (!HealthStatusHelpers.isHealthy(health.status)) {
636+
this.#logger.warn(
637+
`Skipping unhealthy server '${name}' with status '${health.status}'`,
638+
);
639+
return false;
640+
}
641+
642+
return true;
625643
});
626644
}
627645

src/logger.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* Internal logging infrastructure for the mcpd SDK.
3+
*
4+
* This module provides a logging shim controlled by the MCPD_LOG_LEVEL environment
5+
* variable.
6+
*
7+
* Logging is disabled by default.
8+
*
9+
* NOTE: It is recommended that you only enable MCPD_LOG_LEVEL in non-MCP-server contexts.
10+
* MCP servers using stdio transport for JSON-RPC communication should avoid enabling logging
11+
* to avoid contaminating stdout/stderr.
12+
*/
13+
14+
/**
15+
* Valid log levels for MCPD_LOG_LEVEL environment variable.
16+
*/
17+
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "off";
18+
19+
/**
20+
* Logger interface defining the SDK's logging contract.
21+
*
22+
* Custom loggers must implement all methods. Each method accepts a message
23+
* and optional arguments for string formatting.
24+
*/
25+
export interface Logger {
26+
/**
27+
* Log a trace-level message (most verbose).
28+
*
29+
* @param args - Message and optional formatting arguments.
30+
*/
31+
trace(...args: unknown[]): void;
32+
33+
/**
34+
* Log a debug-level message.
35+
*
36+
* @param args - Message and optional formatting arguments.
37+
*/
38+
debug(...args: unknown[]): void;
39+
40+
/**
41+
* Log an info-level message.
42+
*
43+
* @param args - Message and optional formatting arguments.
44+
*/
45+
info(...args: unknown[]): void;
46+
47+
/**
48+
* Log a warning-level message.
49+
*
50+
* @param args - Message and optional formatting arguments.
51+
*/
52+
warn(...args: unknown[]): void;
53+
54+
/**
55+
* Log an error-level message.
56+
*
57+
* @param args - Message and optional formatting arguments.
58+
*/
59+
error(...args: unknown[]): void;
60+
}
61+
62+
// Numeric ranks for log levels (lower = more verbose).
63+
const ranks: Record<LogLevel, number> = {
64+
trace: 5,
65+
debug: 10,
66+
info: 20,
67+
warn: 30,
68+
error: 40,
69+
off: 1000,
70+
};
71+
72+
// Attempts to resolve log level from a (case insensitive) environment variable value.
73+
// Defaults to "off" if unrecognized.
74+
function resolve(raw: string | undefined): LogLevel {
75+
const candidate = raw?.toLowerCase() as LogLevel | undefined;
76+
return candidate && candidate in ranks ? candidate : "off";
77+
}
78+
79+
// Lazily resolve the level at call time to support testing.
80+
function getLevel(): LogLevel {
81+
return resolve(
82+
typeof process !== "undefined" ? process.env.MCPD_LOG_LEVEL : undefined,
83+
);
84+
}
85+
86+
// Default logger implementation using console methods.
87+
function defaultLogger(): Logger {
88+
const OFF: LogLevel = "off";
89+
90+
return {
91+
trace: (...args) => {
92+
const lvl = getLevel();
93+
if (lvl !== OFF && ranks[lvl] <= ranks.trace) console.trace(...args);
94+
},
95+
debug: (...args) => {
96+
const lvl = getLevel();
97+
if (lvl !== OFF && ranks[lvl] <= ranks.debug) console.debug(...args);
98+
},
99+
info: (...args) => {
100+
const lvl = getLevel();
101+
if (lvl !== OFF && ranks[lvl] <= ranks.info) console.info(...args);
102+
},
103+
warn: (...args) => {
104+
const lvl = getLevel();
105+
if (lvl !== OFF && ranks[lvl] <= ranks.warn) console.warn(...args);
106+
},
107+
error: (...args) => {
108+
const lvl = getLevel();
109+
if (lvl !== OFF && ranks[lvl] <= ranks.error) console.error(...args);
110+
},
111+
};
112+
}
113+
114+
/**
115+
* Create a logger, optionally using a custom implementation.
116+
*
117+
* This function allows SDK users to inject their own logger implementation.
118+
* Supports partial implementations - any omitted methods will fall back to the
119+
* default logger, which respects the MCPD_LOG_LEVEL environment variable.
120+
*
121+
* @param impl - Optional custom Logger implementation or partial implementation.
122+
* If not provided, uses default logger controlled by MCPD_LOG_LEVEL.
123+
* If partially provided, custom methods are used and omitted methods
124+
* fall back to default logger (which respects MCPD_LOG_LEVEL).
125+
* @returns A Logger instance with all methods implemented.
126+
*
127+
* @example
128+
* ```typescript
129+
* // Use default logger (controlled by MCPD_LOG_LEVEL).
130+
* const logger = createLogger();
131+
*
132+
* // Partial logger: custom warn/error, default (MCPD_LOG_LEVEL-aware) for others.
133+
* const logger = createLogger({
134+
* warn: (msg) => myCustomLogger.warning(msg),
135+
* error: (msg) => myCustomLogger.error(msg),
136+
* // trace, debug, info fall back to default logger (respects MCPD_LOG_LEVEL)
137+
* });
138+
* ```
139+
*/
140+
export function createLogger(impl?: Partial<Logger>): Logger {
141+
const base = defaultLogger();
142+
return {
143+
trace: impl?.trace ?? base.trace,
144+
debug: impl?.debug ?? base.debug,
145+
info: impl?.info ?? base.info,
146+
warn: impl?.warn ?? base.warn,
147+
error: impl?.error ?? base.error,
148+
};
149+
}

0 commit comments

Comments
 (0)