# Defensive Server Patterns
This document describes the defensive programming patterns implemented in ClickUp-MCP to ensure robust server initialization, configuration handling, and resource cleanup.
## Table of Contents
- [Server Context Registration](#server-context-registration)
- [Config Normalization](#config-normalization)
- [Test Resource Management](#test-resource-management)
- [Diagnostic Logging](#diagnostic-logging)
## Server Context Registration
### Problem
The MCP server context must be reliably registered and accessible after `createServer()` is called. Without proper registration, tests and runtime code may encounter "Server context not available" errors.
### Solution
The server factory now:
1. Registers context immediately after server creation using `Reflect.set()`
2. Verifies registration succeeded by reading the value back
3. Logs successful registration with context details
4. Throws a descriptive error if registration fails
```typescript
// In src/server/factory.ts
Reflect.set(server, serverContextSymbol, context)
// Verify context was registered successfully
const verifyCtx = Reflect.get(server, serverContextSymbol) as ServerContext | undefined;
if (!verifyCtx) {
logger.error("context_registration_failed", {
message: "Failed to register server context - this indicates a critical initialization error"
});
throw new Error("Failed to register server context on MCP server instance");
}
logger.info("context_registered", {
toolCount: context.tools.length,
sessionConfigured: !!(context.session.apiToken && context.session.defaultTeamId),
connectionId: defaultConnectionId
});
```
### Enhanced Error Messages
The `getServerContext()` function now provides detailed diagnostics when context is missing:
```typescript
export function getServerContext(server: Server): ServerContext {
const ctx = Reflect.get(server, serverContextSymbol) as ServerContext | undefined;
if (!ctx) {
const serverType = typeof server;
const hasSymbol = serverContextSymbol in (server as object);
const symbolValue = hasSymbol ? Reflect.get(server, serverContextSymbol) : undefined;
throw new Error(
`Server context not available. Server type: ${serverType}, ` +
`has symbol: ${hasSymbol}, symbol value type: ${typeof symbolValue}. ` +
`This usually means the server was not created with createServer() or ` +
`context registration failed during initialization.`
);
}
return ctx;
}
```
## Config Normalization
### Problem
Strict Zod validation can cause initialization failures when malformed or invalid config is provided. This differs from Python MCP behavior which is more permissive.
### Solution
Config normalization is now defensive at multiple levels:
#### 1. Smithery Config Parsing (index.ts)
```typescript
if (context?.config) {
try {
overrides = smitheryConfigSchema.parse(context.config)
console.log("config_normalized", {
message: "Successfully parsed Smithery config",
hasApiToken: !!overrides.apiToken,
hasDefaultTeamId: overrides.defaultTeamId !== undefined,
// ... other fields
});
} catch (err) {
// Never throw - log and continue with base config
console.warn("config_normalization_warning", {
message: "Config parsing failed, using base config from environment",
error: errorDetails,
issues: zodIssues
});
overrides = undefined;
}
}
```
#### 2. Session Config Normalization (schema.ts)
Handles invalid numeric values like NaN and Infinity:
```typescript
export function toSessionConfig(config: AppConfig, envSource?: EnvSource): SessionConfig {
// Defensive: handle NaN and invalid team IDs from config
let defaultTeamId = config.defaultTeamId;
if (defaultTeamId !== undefined && (!Number.isFinite(defaultTeamId) || Number.isNaN(defaultTeamId))) {
console.warn("session_config_normalization", {
message: "Invalid defaultTeamId in config, attempting fallback to environment",
configValue: defaultTeamId
});
defaultTeamId = undefined;
}
// Fallback to environment if config value was invalid
if (defaultTeamId === undefined) {
defaultTeamId = parseOptionalInteger(readEnv(env, "CLICKUP_DEFAULT_TEAM_ID"));
}
// Defensive: ensure defaultTeamId is either a valid finite number or undefined
const normalizedTeamId = Number.isFinite(defaultTeamId ?? NaN) ? defaultTeamId : undefined;
return {
apiToken,
authScheme: config.authScheme ?? "auto",
baseUrl,
defaultTeamId: normalizedTeamId,
requestTimeout,
defaultHeaders
};
}
```
#### 3. Config Merge Logging
Track where each config value came from for debugging:
```typescript
function mergeAppConfig(base: AppConfig, overrides?: SmitheryConfig): AppConfig {
if (!overrides) return base
const merged = { ...base, ...overrides };
console.log("config_merged", {
message: "Merged Smithery config with base config",
apiTokenSource: overrides.apiToken ? "smithery" : base.apiToken ? "env" : "none",
defaultTeamIdSource: overrides.defaultTeamId !== undefined ? "smithery" : base.defaultTeamId !== undefined ? "env" : "none",
// ... other fields
});
return merged;
}
```
### Handled Edge Cases
- `NaN` values → normalized to `undefined`
- `Infinity` / `-Infinity` → normalized to `undefined`
- Wrong type (string instead of number) → transformed or defaulted
- Missing fields → fallback chain: config → env → default
- Extra unknown fields → stripped by Zod `.strip()`
- Null values → transformed to appropriate defaults
## Test Resource Management
### Problem
Tests may experience:
- Port collisions (`EADDRINUSE`) when multiple tests use hardcoded ports
- Resource leaks when `.close()` is not called or fails
- Brittle teardown when server shape is unexpected
### Solution
Created defensive test utilities in `tests/helpers/server.ts`:
#### 1. Dynamic Port Allocation
```typescript
export function getDynamicPort(): number {
return 0; // Always use OS-assigned random port
}
// Usage in tests
const http = await startHttpBridge(server, { port: getDynamicPort() });
```
#### 2. Defensive Server Cleanup
```typescript
export async function closeServerDefensively(server: unknown): Promise<void> {
if (!server) return;
if (typeof server === "object" && "close" in server && typeof server.close === "function") {
try {
await server.close();
} catch (error) {
console.warn("server_close_warning", {
message: "Error while closing server",
error: errorMsg
});
}
} else {
console.warn("server_close_skipped", {
message: "Server does not have a .close() method",
serverType: typeof server,
hasClose: server && typeof server === "object" ? "close" in server : false
});
}
}
```
#### 3. Multi-Resource Cleanup
```typescript
export function createCleanupHandler(...cleanupFns: Array<() => Promise<void>>): () => Promise<void> {
return async () => {
const errors: Error[] = [];
for (const fn of cleanupFns) {
try {
await fn();
} catch (error) {
if (error instanceof Error) {
errors.push(error);
}
}
}
if (errors.length > 0) {
console.warn("cleanup_errors", {
message: `${errors.length} error(s) during cleanup`,
errors: errors.map(e => e.message)
});
}
};
}
// Usage in tests
const servers = [...]; // create multiple servers
const cleanup = createCleanupHandler(
...servers.flatMap(({ server, http }) => [
() => closeHttpBridgeDefensively(http),
() => closeServerDefensively(server)
])
);
await cleanup();
```
### Test Pattern Example
```typescript
it("example test with defensive cleanup", async () => {
const server = await createServer();
const http = await startHttpBridge(server, { port: getDynamicPort() });
try {
await waitForServerReady(server);
// Test code here
const response = await fetch(`http://127.0.0.1:${http.port}/healthz`);
expect(response.status).toBe(200);
} finally {
// Defensive cleanup - never throws, always logs
await closeHttpBridgeDefensively(http);
await closeServerDefensively(server);
}
});
```
## Diagnostic Logging
### Startup Diagnostics
The server prints comprehensive diagnostics on startup (in `factory.ts`):
```json
{
"timestamp": "2025-11-06T23:47:43.225Z",
"server": {
"name": "ClickUp-MCP",
"version": "0.1.0"
},
"configuration": {
"resolved": {
"apiToken": "test...oken",
"defaultTeamId": 1,
"primaryLanguage": "en-US",
"baseUrl": "https://api.clickup.com/api/v2",
"requestTimeoutMs": 30000,
"defaultHeadersJson": "<provided>"
},
"session": {
"apiToken": "test...oken",
"defaultTeamId": 1,
"authScheme": "auto",
"baseUrl": "https://api.clickup.com/api/v2",
"requestTimeout": 30,
"defaultHeaders": "Accept-Language"
},
"environment": {
"CLICKUP_TOKEN": "test...oken",
"CLICKUP_DEFAULT_TEAM_ID": "1",
"CLICKUP_PRIMARY_LANGUAGE": "en-US",
"CLICKUP_BASE_URL": "<not set>",
"CLICKUP_AUTH_SCHEME": "auto",
"REQUEST_TIMEOUT_MS": "30000",
"DEFAULT_HEADERS_JSON": "<provided>",
"MCP_TRANSPORT": "http",
"LOG_LEVEL": "info"
}
}
}
```
### Key Log Events
- `context_registered` - Server context successfully registered
- `context_registration_failed` - Critical error during context registration
- `config_normalized` - Smithery config successfully parsed
- `config_normalization_warning` - Config parsing failed, using fallback
- `config_merged` - Shows source of each config value (smithery/env/default)
- `session_config_normalization` - Invalid values normalized (NaN → undefined)
- `server_close_warning` - Error during server cleanup
- `server_close_skipped` - Server missing .close() method
- `cleanup_errors` - Errors during multi-resource cleanup
## Best Practices
### For Application Code
1. Always use `createServer()` from factory.ts
2. Never assume config values are present - check for undefined
3. Use structured logging for config changes
4. Handle initialization errors gracefully
### For Test Code
1. Always use `getDynamicPort()` for HTTP servers (returns 0)
2. Use defensive cleanup helpers in `finally` blocks
3. Use `createCleanupHandler()` for multiple resources
4. Never assume `.close()` exists - use defensive helpers
5. Check logs for warnings during teardown
### For Config Validation
1. Never throw during initialization
2. Log normalization events for debugging
3. Implement fallback chains: config → env → default
4. Strip invalid values rather than failing
## Migration Guide
### Updating Existing Tests
Before:
```typescript
it("test", async () => {
const server = await createServer();
const http = await startHttpBridge(server, { port: 3000 });
// test code
await http.close();
await server.close();
});
```
After:
```typescript
import { closeServerDefensively, closeHttpBridgeDefensively, getDynamicPort } from "./helpers/server.js";
it("test", async () => {
const server = await createServer();
const http = await startHttpBridge(server, { port: getDynamicPort() });
try {
// test code
} finally {
await closeHttpBridgeDefensively(http);
await closeServerDefensively(server);
}
});
```
### Benefits
- No port collisions
- No resource leaks
- Better error messages
- Resilient to server shape changes
- Matches Python MCP permissiveness