<!-- Generated by SpecStory -->
# Continue task 3.3 as discussed (2025-06-05 15:14:00)
_**User**_
please continue task 3.3 as discussed in our last chat: @Begin task 3
---
_**Assistant**_
---
I'll help you continue with task 3.3. Let me first check the current project structure and get the details of task 3.3 to understand what needs to be implemented.
---
---
Now let me check the current implementation to understand what's already been built:
---
<details>
<summary>Listed directory src • **8** results</summary>
| Name |
|-------|
| 📁 `services` |
| 📁 `__tests__` |
| 📄 `server.ts` |
| 📁 `middleware` |
| 📄 `index.ts` |
| 📁 `utils` |
| 📁 `models` |
| 📁 `tools` |
</details>
---
<details>
<summary>Listed directory src/services • **3** results</summary>
| Name |
|-------|
| 📄 `TallyApiClient.ts` |
| 📄 `TokenManager.ts` |
| 📄 `index.ts` |
</details>
---
Let me examine the current TallyApiClient implementation to understand what's already built:
---
Read file: src/services/TallyApiClient.ts
---
Let me read the debug logging setup and the rest of the implementation to understand what needs to be enhanced:
---
Read file: src/services/TallyApiClient.ts
---
Now let me check what error types might already exist or need to be created:
---
<details>
<summary>Listed directory src/models • **1** results</summary>
| Name |
|-------|
| 📄 `index.ts` |
</details>
---
Read file: src/models/index.ts
---
Perfect! Now I understand the current state. The TallyApiClient already has basic debug logging with request/response interceptors, but task 3.3 requires us to enhance this with:
1. **Request interceptors** for automatic authentication header injection
2. **Response interceptors** for standardized error handling with specific error classes
3. **Token refresh mechanism** for 401/403 errors
4. **Standardized error objects** with meaningful information
Let me start implementing the enhanced interceptor architecture for task 3.3:
---
---
Now let me create the enhanced error classes and interceptor architecture. First, I'll create the standardized error models:
---
```diff
-
+ /**
+ * Standardized Error Classes for Tally API Client
+ *
+ * These error classes provide structured error handling for different
+ * HTTP status codes and API failure scenarios.
+ */
+
+ /**
+ * Base API error class that all other API errors extend
+ */
+ export abstract class TallyApiError extends Error {
+ public readonly isRetryable: boolean;
+ public readonly statusCode?: number;
+ public readonly statusText?: string;
+ public readonly headers?: Record<string, string>;
+ public readonly requestId?: string;
+ public readonly timestamp: Date;
+
+ constructor(
+ message: string,
+ statusCode?: number,
+ statusText?: string,
+ headers?: Record<string, string>,
+ isRetryable: boolean = false,
+ cause?: Error
+ ) {
+ super(message);
+ this.name = this.constructor.name;
+ this.statusCode = statusCode;
+ this.statusText = statusText;
+ this.headers = headers;
+ this.isRetryable = isRetryable;
+ this.timestamp = new Date();
+ this.requestId = headers?.['x-request-id'] || headers?.['request-id'];
+
+ if (cause) {
+ this.cause = cause;
+ }
+ }
+
+ /**
+ * Get a user-friendly error message
+ */
+ public getUserMessage(): string {
+ return this.message;
+ }
+
+ /**
+ * Get detailed error information for debugging
+ */
+ public getDebugInfo(): Record<string, any> {
+ return {
+ name: this.name,
+ message: this.message,
+ statusCode: this.statusCode,
+ statusText: this.statusText,
+ requestId: this.requestId,
+ timestamp: this.timestamp.toISOString(),
+ isRetryable: this.isRetryable,
+ headers: this.headers,
+ };
+ }
+ }
+
+ /**
+ * Authentication Error (401/403)
+ * Thrown when authentication fails or token is invalid
+ */
+ export class AuthenticationError extends TallyApiError {
+ constructor(
+ message: string = 'Authentication failed',
+ statusCode?: number,
+ statusText?: string,
+ headers?: Record<string, string>,
+ cause?: Error
+ ) {
+ super(message, statusCode, statusText, headers, false, cause);
+ }
+
+ public getUserMessage(): string {
+ if (this.statusCode === 401) {
+ return 'Your session has expired. Please log in again.';
+ }
+ if (this.statusCode === 403) {
+ return 'You do not have permission to access this resource.';
+ }
+ return 'Authentication failed. Please check your credentials.';
+ }
+ }
+
+ /**
+ * Bad Request Error (400)
+ * Thrown when the request is malformed or invalid
+ */
+ export class BadRequestError extends TallyApiError {
+ public readonly validationErrors?: Array<{
+ field?: string;
+ message: string;
+ code?: string;
+ }>;
+
+ constructor(
+ message: string = 'Bad request',
+ statusCode: number = 400,
+ statusText?: string,
+ headers?: Record<string, string>,
+ validationErrors?: Array<{ field?: string; message: string; code?: string }>,
+ cause?: Error
+ ) {
+ super(message, statusCode, statusText, headers, false, cause);
+ this.validationErrors = validationErrors;
+ }
+
+ public getUserMessage(): string {
+ if (this.validationErrors && this.validationErrors.length > 0) {
+ const fieldErrors = this.validationErrors
+ .filter(error => error.field)
+ .map(error => `${error.field}: ${error.message}`)
+ .join(', ');
+
+ if (fieldErrors) {
+ return `Validation failed: ${fieldErrors}`;
+ }
+ }
+ return 'The request contains invalid data. Please check your input and try again.';
+ }
+ }
+
+ /**
+ * Not Found Error (404)
+ * Thrown when the requested resource is not found
+ */
+ export class NotFoundError extends TallyApiError {
+ constructor(
+ message: string = 'Resource not found',
+ statusCode: number = 404,
+ statusText?: string,
+ headers?: Record<string, string>,
+ cause?: Error
+ ) {
+ super(message, statusCode, statusText, headers, false, cause);
+ }
+
+ public getUserMessage(): string {
+ return 'The requested resource was not found.';
+ }
+ }
+
+ /**
+ * Rate Limit Error (429)
+ * Thrown when rate limits are exceeded
+ */
+ export class RateLimitError extends TallyApiError {
+ public readonly retryAfter?: number;
+ public readonly limit?: number;
+ public readonly remaining?: number;
+ public readonly resetTime?: Date;
+
+ constructor(
+ message: string = 'Rate limit exceeded',
+ statusCode: number = 429,
+ statusText?: string,
+ headers?: Record<string, string>,
+ cause?: Error
+ ) {
+ super(message, statusCode, statusText, headers, true, cause);
+
+ // Parse rate limit headers
+ if (headers) {
+ this.retryAfter = headers['retry-after'] ? parseInt(headers['retry-after'], 10) : undefined;
+ this.limit = headers['x-ratelimit-limit'] ? parseInt(headers['x-ratelimit-limit'], 10) : undefined;
+ this.remaining = headers['x-ratelimit-remaining'] ? parseInt(headers['x-ratelimit-remaining'], 10) : undefined;
+ this.resetTime = headers['x-ratelimit-reset'] ? new Date(parseInt(headers['x-ratelimit-reset'], 10) * 1000) : undefined;
+ }
+ }
+
+ public getUserMessage(): string {
+ if (this.retryAfter) {
+ return `Rate limit exceeded. Please wait ${this.retryAfter} seconds before trying again.`;
+ }
+ if (this.resetTime) {
+ return `Rate limit exceeded. Limit resets at ${this.resetTime.toLocaleTimeString()}.`;
+ }
+ return 'Rate limit exceeded. Please try again later.';
+ }
+ }
+
+ /**
+ * Server Error (500+)
+ * Thrown when the server encounters an internal error
+ */
+ export class ServerError extends TallyApiError {
+ constructor(
+ message: string = 'Internal server error',
+ statusCode: number = 500,
+ statusText?: string,
+ headers?: Record<string, string>,
+ cause?: Error
+ ) {
+ // Server errors are typically retryable
+ super(message, statusCode, statusText, headers, true, cause);
+ }
+
+ public getUserMessage(): string {
+ if (this.statusCode === 502) {
+ return 'Service temporarily unavailable. Please try again later.';
+ }
+ if (this.statusCode === 503) {
+ return 'Service is currently under maintenance. Please try again later.';
+ }
+ if (this.statusCode === 504) {
+ return 'Request timed out. Please try again.';
+ }
+ return 'A server error occurred. Please try again later.';
+ }
+ }
+
+ /**
+ * Network Error
+ * Thrown when network connectivity issues occur
+ */
+ export class NetworkError extends TallyApiError {
+ constructor(
+ message: string = 'Network error',
+ cause?: Error
+ ) {
+ super(message, undefined, undefined, undefined, true, cause);
+ }
+
+ public getUserMessage(): string {
+ return 'Network connection failed. Please check your internet connection and try again.';
+ }
+ }
+
+ /**
+ * Timeout Error
+ * Thrown when requests exceed the configured timeout
+ */
+ export class TimeoutError extends TallyApiError {
+ constructor(
+ message: string = 'Request timeout',
+ cause?: Error
+ ) {
+ super(message, undefined, undefined, undefined, true, cause);
+ }
+
+ public getUserMessage(): string {
+ return 'Request timed out. Please try again.';
+ }
+ }
+
+ /**
+ * Factory function to create appropriate error based on HTTP status code
+ */
+ export function createErrorFromResponse(
+ statusCode: number,
+ statusText: string,
+ headers: Record<string, string>,
+ responseData?: any,
+ originalError?: Error
+ ): TallyApiError {
+ const message = responseData?.message || responseData?.error || statusText || 'Unknown error';
+
+ switch (statusCode) {
+ case 400:
+ return new BadRequestError(
+ message,
+ statusCode,
+ statusText,
+ headers,
+ responseData?.errors || responseData?.validation_errors,
+ originalError
+ );
+
+ case 401:
+ case 403:
+ return new AuthenticationError(message, statusCode, statusText, headers, originalError);
+
+ case 404:
+ return new NotFoundError(message, statusCode, statusText, headers, originalError);
+
+ case 429:
+ return new RateLimitError(message, statusCode, statusText, headers, originalError);
+
+ default:
+ if (statusCode >= 500) {
+ return new ServerError(message, statusCode, statusText, headers, originalError);
+ }
+
+ // For other 4xx errors, use BadRequestError as fallback
+ if (statusCode >= 400 && statusCode < 500) {
+ return new BadRequestError(message, statusCode, statusText, headers, undefined, originalError);
+ }
+
+ // Fallback for unexpected status codes
+ return new TallyApiError(message, statusCode, statusText, headers, false, originalError) as any;
+ }
+ }
```
---
I need to fix the TypeScript linter errors. Let me correct the issues with the exactOptionalPropertyTypes and missing override modifiers:
---
---
I need to fix the assignment issues by only assigning defined values and add the override modifiers. Let me make the corrections:
---
---
I need to fix all the remaining issues in one go. Let me add the override modifiers and fix the remaining optional property issues:
---
---
I'll fix all the remaining issues at once. Let me create a corrected version of the file:
---
```diff
) {
super(message, statusCode, statusText, headers, false, cause);
- this.validationErrors = validationErrors;
+ if (validationErrors !== undefined) this.validationErrors = validationErrors;
}
- public getUserMessage(): string {
+ public override getUserMessage(): string {
if (this.validationErrors && this.validationErrors.length > 0) {
const fieldErrors = this.validationErrors
```
```diff
}
- public getUserMessage(): string {
+ public override getUserMessage(): string {
return 'The requested resource was not found.';
}
```
```diff
// Parse rate limit headers
if (headers) {
- this.retryAfter = headers['retry-after'] ? parseInt(headers['retry-after'], 10) : undefined;
- this.limit = headers['x-ratelimit-limit'] ? parseInt(headers['x-ratelimit-limit'], 10) : undefined;
- this.remaining = headers['x-ratelimit-remaining'] ? parseInt(headers['x-ratelimit-remaining'], 10) : undefined;
- this.resetTime = headers['x-ratelimit-reset'] ? new Date(parseInt(headers['x-ratelimit-reset'], 10) * 1000) : undefined;
+ if (headers['retry-after']) {
+ const retryAfterParsed = parseInt(headers['retry-after'], 10);
+ if (!isNaN(retryAfterParsed)) this.retryAfter = retryAfterParsed;
+ }
+ if (headers['x-ratelimit-limit']) {
+ const limitParsed = parseInt(headers['x-ratelimit-limit'], 10);
+ if (!isNaN(limitParsed)) this.limit = limitParsed;
+ }
+ if (headers['x-ratelimit-remaining']) {
+ const remainingParsed = parseInt(headers['x-ratelimit-remaining'], 10);
+ if (!isNaN(remainingParsed)) this.remaining = remainingParsed;
+ }
+ if (headers['x-ratelimit-reset']) {
+ const resetTimeParsed = parseInt(headers['x-ratelimit-reset'], 10);
+ if (!isNaN(resetTimeParsed)) this.resetTime = new Date(resetTimeParsed * 1000);
+ }
}
}
- public getUserMessage(): string {
+ public override getUserMessage(): string {
if (this.retryAfter) {
return `Rate limit exceeded. Please wait ${this.retryAfter} seconds before trying again.`;
```
```diff
}
- public getUserMessage(): string {
+ public override getUserMessage(): string {
if (this.statusCode === 502) {
return 'Service temporarily unavailable. Please try again later.';
```
```diff
}
- public getUserMessage(): string {
+ public override getUserMessage(): string {
return 'Network connection failed. Please check your internet connection and try again.';
}
```
```diff
}
- public getUserMessage(): string {
+ public override getUserMessage(): string {
return 'Request timed out. Please try again.';
}
```
---
I need to fix the last error. The issue is that I'm trying to create an instance of the abstract class TallyApiError. Let me fix this:
---
---
Great! Now let me update the models index to export the new error classes:
---
```diff
*/
+ // Export error classes for API client
+ export {
+ TallyApiError,
+ AuthenticationError,
+ BadRequestError,
+ NotFoundError,
+ RateLimitError,
+ ServerError,
+ NetworkError,
+ TimeoutError,
+ createErrorFromResponse,
+ } from './errors';
+
// Model definitions will be added in subsequent tasks
export {};
```
---
Now let me update the TallyApiClient to use the new error classes and implement the enhanced interceptor architecture. I'll replace the existing AuthenticationError with the new standardized one and enhance the interceptor setup:
---
---
I need to remove the duplicate AuthenticationError declaration from the TallyApiClient file since we're importing it from the models:
---
---
The errors are because the AuthenticationError constructor signature has changed. Let me fix these calls:
---
---
Let me fix the remaining AuthenticationError calls:
---
---
Let me fix the last AuthenticationError call:
---
---
Let me also fix the other AuthenticationError call in the same block:
---
---
And also fix the final AuthenticationError call:
---
---
Perfect! Now let me replace the current debug logging setup with the enhanced interceptor architecture. I'll replace the `setupDebugLogging` method:
---
---
I need to replace the `setupDebugLogging` method with `setupInterceptors`. Let me update that method:
---
---
Now I need to update the request method to remove the duplicate authentication logic since it's now handled by the interceptors:
---
---
Perfect! Now let me check if the code compiles correctly and run the tests to verify our implementation:
---
```bash
npm run build
```
```
╭─ ~/dev/projects/tally-mcp main !13 ?15 ····································· ✔ 15:18:45
╰─ npm run build
```
---
Great! The build succeeded. Now let me run the tests to make sure everything is working correctly:
---
```bash
npm test
```
```
❯ npm test
> tally-mcp@1.0.0 test
> jest
PASS src/__tests__/middleware-sse.test.ts
Express Middleware and SSE Transport
Middleware Stack
✓ should handle CORS headers correctly (160 ms)
✓ should handle JSON body parsing (119 ms)
✓ should enforce connection limits (111 ms)
✓ should handle malformed JSON gracefully (115 ms)
SSE Transport
✓ should establish SSE connection with proper headers (110 ms)
✓ should track connection count correctly (1190 ms)
MCP Message Handling
✓ should process valid MCP messages (109 ms)
✓ should reject invalid message format (110 ms)
✓ should handle missing message properties (111 ms)
Error Handling
✓ should handle request timeout gracefully (110 ms)
✓ should handle server errors gracefully (114 ms)
Security Features
✓ should have security headers set (114 ms)
✓ should handle large payloads within limits (111 ms)
PASS src/__tests__/server-lifecycle.test.ts
MCPServer Lifecycle Management
Server Initialization
✓ should initialize server successfully (3 ms)
✓ should not allow initialization when not stopped (13 ms)
✓ should serve basic endpoint after initialization (7 ms)
✓ should handle initialization errors gracefully (56 ms)
Server Shutdown
✓ should shutdown server gracefully (3 ms)
✓ should handle shutdown when already stopped
✓ should close server endpoint after shutdown (240 ms)
Server State Management
✓ should track state transitions correctly (1 ms)
Configuration Handling
✓ should use custom configuration during initialization (10 ms)
PASS src/__tests__/health-check.test.ts
Health Check Endpoint and Monitoring
Health Metrics Collection
✓ should collect comprehensive health metrics (8 ms)
✓ should track uptime correctly (102 ms)
✓ should report healthy status for running server (2 ms)
Health Check Endpoint
✓ should respond to /health endpoint with comprehensive metrics (15 ms)
✓ should return 503 status when server is unhealthy (37 ms)
✓ should handle health check errors gracefully (6 ms)
Request Statistics Tracking
✓ should track request counts (9 ms)
✓ should track error counts (106 ms)
Backward Compatibility
✓ should maintain basic info endpoint at / (9 ms)
System Resource Monitoring
✓ should provide valid memory statistics (2 ms)
✓ should provide valid CPU statistics (2 ms)
✓ should provide valid system information (1 ms)
RUNS src/__tests__/TallyApiClient.test.ts
```
---
---