Skip to main content
Glama
research.md13.2 kB
# Research: Deferred API Token Validation Patterns **Feature**: 006-more-mcp-compliance **Date**: 2025-10-02 ## Research Questions ### 1. TypeScript Lazy Initialization Patterns for API Clients **Decision**: Use singleton pattern with lazy initialization and cached validation state **Rationale**: - Singleton ensures single validation state across server lifetime - Lazy initialization defers expensive operations until first use - Cached state (boolean flag) eliminates redundant API calls per clarification session decision - Aligns with existing TodoistApiService architecture **Alternatives Considered**: - **Dependency injection with lifecycle hooks**: More complex, requires framework changes, overkill for simple state flag - **Proxy pattern with on-demand validation**: Additional abstraction layer, performance overhead - **Event-driven initialization**: Async complexity, harder to reason about failure modes **Implementation Pattern**: ```typescript class TokenValidator { private static validationState: 'not_validated' | 'valid' | 'invalid' = 'not_validated'; private static validationError?: Error; static async validateOnce(): Promise<void> { if (this.validationState !== 'not_validated') { if (this.validationState === 'invalid') throw this.validationError; return; // Already validated successfully } // Perform validation... } } ``` ### 2. MCP Server Initialization Without Required Configuration **Decision**: Optional configuration with runtime validation guards **Rationale**: - MCP protocol requires server to respond to `initialize` and `list_tools` without executing business logic - Configuration validation must be conditional, not at module load time - Error messages provided at tool execution boundary, not startup - Follows standard pattern for hosted/inspectable services (Docker health checks, Kubernetes probes) **Alternatives Considered**: - **Environment variable with default empty value**: Hides configuration errors, fails late - **Two-phase initialization (init + configure)**: Breaking change to existing deployment scripts - **Warning logs at startup**: Noise in logs, users ignore warnings **Implementation Pattern**: ```typescript // config/index.ts export function getConfig() { return { apiToken: process.env.TODOIST_API_TOKEN ?? null, // Allow null // ... other config }; } // services/todoist-api.ts constructor(config: Config) { this.config = config; // Do NOT validate token here } private ensureToken(): string { if (!this.config.apiToken) { throw new Error("Token missing. Set TODOIST_API_TOKEN environment variable"); } return this.config.apiToken; } ``` ### 3. Error Message Formatting for Actionable Guidance **Decision**: Structured error format with category, description, and action fields **Rationale**: - Matches MCP error response structure (code, message, data) - Clear separation of what happened vs. what to do - Consistent with clarification decision: "[Category]. [Next step]" - Enables client-side error handling/routing **Alternatives Considered**: - **Plain string messages**: Hard to parse, inconsistent formatting - **Error codes only**: Requires lookup table, not user-friendly - **Markdown formatted errors**: Overhead for simple errors, parsing complexity **Implementation Pattern**: ```typescript enum TokenErrorCategory { Missing = 'TOKEN_MISSING', Invalid = 'TOKEN_INVALID', AuthFailed = 'AUTH_FAILED', PermissionDenied = 'PERMISSION_DENIED' } function createTokenError(category: TokenErrorCategory): Error { const messages = { [TokenErrorCategory.Missing]: 'Token missing. Set TODOIST_API_TOKEN environment variable', [TokenErrorCategory.Invalid]: 'Token invalid. Verify token format', [TokenErrorCategory.AuthFailed]: 'Authentication failed. Verify token is valid at Todoist settings', [TokenErrorCategory.PermissionDenied]: 'Permission denied. Token lacks required scopes' }; return new Error(messages[category]); } ``` ### 4. Health Check Endpoint Design for Multi-State Systems **Decision**: Structured health check response with component status breakdown **Rationale**: - Health check always returns 200 OK (server is running) - Response body includes token availability and validation state as metadata - Enables monitoring tools to track degraded state without failing health checks - Aligns with Kubernetes liveness vs. readiness probe patterns **Alternatives Considered**: - **503 when token missing**: False negative, server is operational - **Separate /ready endpoint**: Additional complexity, not standard for MCP - **Boolean healthy field only**: Insufficient detail for debugging **Implementation Pattern**: ```typescript interface HealthCheckResponse { status: 'healthy'; components: { server: { status: 'operational' }; tokenValidation: { status: 'configured' | 'not_configured' | 'valid' | 'invalid'; validatedAt?: string; // ISO timestamp if validated }; }; timestamp: string; } ``` ### 5. Jest Testing Strategies for Initialization Lifecycle **Decision**: Contract tests with in-memory API service mocks for initialization flows **Rationale**: - No network calls in tests (fast, deterministic) - In-memory mock can simulate all token states (missing, invalid, valid) - Existing pattern in codebase (`tests/helpers/inMemoryTodoistApiService.ts`) - Easy to test edge cases (token removed mid-session, revocation) **Alternatives Considered**: - **Integration tests only**: Slow, requires actual Todoist API access - **Sinon/jest.mock for API service**: Harder to maintain, test coupling - **Test containers**: Overkill for stateless validation logic **Implementation Pattern**: ```typescript describe('Token validation lifecycle', () => { it('allows server startup without token', () => { delete process.env.TODOIST_API_TOKEN; const server = new TodoistMCPServer(); expect(server.initialize()).resolves.not.toThrow(); }); it('validates token on first tool call', async () => { process.env.TODOIST_API_TOKEN = 'test_token'; const result = await server.callTool('todoist_tasks', { action: 'list' }); expect(mockApi.validateToken).toHaveBeenCalledTimes(1); }); it('caches successful validation for session', async () => { await server.callTool('todoist_tasks', { action: 'list' }); await server.callTool('todoist_projects', { action: 'list' }); expect(mockApi.validateToken).toHaveBeenCalledTimes(1); // Only once }); }); ``` ## Architecture Decisions ### Validation State Machine ``` ┌─────────────────┐ │ not_validated │ (startup state) └────────┬────────┘ │ First tool call triggers validation │ ┌────────▼────────┐ │ validating │ (transient) └────────┬────────┘ │ ┌─────────┴─────────┐ │ │ ┌────────▼────────┐ ┌──────▼──────┐ │ valid │ │ invalid │ │ (cached forever)│ │ (persisted) │ └─────────────────┘ └─────────────┘ ``` **State Transitions**: - `not_validated` → `validating`: First tool invocation - `validating` → `valid`: Successful Todoist API response (e.g., 200 from GET /projects) - `validating` → `invalid`: Auth failure (401/403), network error, or missing token - `valid` → (no transitions): Cache persists until server restart - `invalid` → (no transitions): Error persists until server restart ### Token Validation Strategy **Validation Method**: Lightweight API call to Todoist (GET /api/v1/projects with limit=1) **Rationale**: - Minimal data transfer (single project metadata) - Validates both token existence and permissions - Uses existing TodoistApiService retry/rate-limit logic - Cached result eliminates overhead for subsequent calls ### Modified Components **1. Configuration Layer (`src/config/index.ts`)** - Remove token validation from `getConfig()` - Return nullable token: `apiToken: string | null` **2. API Service (`src/services/todoist-api.ts`)** - Add private `ensureToken(): string` guard method - Add public `validateToken(): Promise<void>` with caching - Inject `ensureToken()` call at top of all public methods **3. Server Initialization (`src/server.ts`)** - Remove config validation from constructor - Initialize with nullable token configuration **4. Tool Handlers (`src/tools/*.ts`)** - No changes required (validation happens in TodoistApiService layer) **5. Health Check Handler (new or modified)** - Return structured response with token state metadata - Never throw/fail on missing token ## Dependencies & Integration Points ### Existing Dependencies (Unchanged) - `@modelcontextprotocol/sdk@0.5.0`: MCP protocol implementation - `axios@1.6.2`: HTTP client for Todoist API - `zod@3.22.4`: Runtime schema validation - `jest@29.7.0`: Testing framework ### No New Dependencies Required - Validation logic uses existing axios instance - Health check uses existing MCP server infrastructure - State management with simple TypeScript class properties ### Integration Points - **MCP Protocol Handshake**: Must succeed without token (no changes to SDK usage) - **Todoist API**: Validation call uses existing endpoint (GET /api/v1/projects) - **Existing Tool Tests**: Must pass without modifications (backward compatibility) - **CI/CD Pipeline**: No build process changes (TypeScript compilation only) ## Performance Characteristics ### Startup Performance - **Before**: ~50ms (config load + token validation) - **After**: ~10ms (config load only) - **Improvement**: 80% faster startup, enables instant server inspection ### Runtime Performance - **First tool call**: +50ms (one-time validation overhead) - **Subsequent calls**: 0ms overhead (cached state) - **Memory footprint**: +8 bytes (single boolean flag) ### Failure Modes - **Token missing**: Instant error response (<1ms, no network call) - **Token invalid**: ~100ms (API call + error handling) - **Token revoked mid-session**: Silent degradation (cached valid state persists) ## Security Considerations ### Token Exposure Risks (Mitigated) - Token never logged in validation flow - Error messages never echo token value - Health check never exposes token (only validation state) ### Attack Vectors (No New Risks) - Deferred validation does not introduce timing attacks (same validation logic) - No token bypass possible (all tools still gated by validation) - Health check metadata does not leak sensitive info (boolean states only) ### Audit Trail - Log validation attempts with timestamp (INFO level) - Log validation failures with category only (ERROR level, no token) - Existing rate limit logging unchanged ## Testing Strategy ### Test Coverage Targets - **Unit Tests**: Token validation state machine (100% coverage) - **Contract Tests**: Server initialization without token (new scenarios) - **Integration Tests**: Full lifecycle flows (startup → tool call → caching) - **Edge Case Tests**: Token removal, revocation, format errors ### Test Scenarios (From Spec) 1. Server starts without token → initialization succeeds 2. Tool call without token → actionable error returned 3. Tool call with valid token → normal execution + validation cached 4. Tool call with invalid token → actionable error returned 5. Token added after startup → subsequent calls succeed 6. Health check without token → healthy with metadata 7. Token failure → category + next step in error message ### Test Data Requirements - Mock Todoist API responses for 200/401/403/500 scenarios - Environment variable fixtures (present/missing/invalid format) - Timing assertions for cache behavior (<1ms cache hit) ## Open Questions & Risks ### Addressed in Clarifications - ✅ Validation caching strategy (session-based) - ✅ Health check behavior (healthy with metadata) - ✅ Error message detail level (actionable format) ### Implementation Risks - **Risk**: Breaking existing deployments that rely on startup validation - **Mitigation**: Backward compatible - servers with tokens work identically - **Risk**: Users may not notice missing token until first tool use - **Mitigation**: Health check metadata provides early visibility - **Risk**: Cached validation hides mid-session token revocation - **Mitigation**: Documented in edge cases, acceptable tradeoff per clarification ### Follow-up Work (Out of Scope) - Token refresh mechanism (if Todoist implements token rotation) - Prometheus metrics for validation state transitions - Admin API to force re-validation without restart

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shayonpal/mcp-todoist'

If you have feedback or need assistance with the MCP directory API, please join our Discord server