Error Handling in Attio MCP Server

October 3, 2025 · View on GitHub

This document describes the error handling system implemented in the Attio MCP server.

Enhanced Error Context Metadata

Enhanced API errors automatically capture field-specific metadata when the server can resolve it from Attio's attribute catalog. Every enhanced error context now includes:

  • fieldType – a human-readable type hint such as phone_number, email, or select
  • fieldMetadata – the Attio attribute configuration snapshot that was used to determine the field type
throw ErrorService.createFieldError({
  field: 'status',
  message: 'Invalid status value',
  resourceType: 'tasks',
  operation: 'update',
  attributeMetadataIndex,
});

The resulting error context contains both the type and metadata, enabling richer troubleshooting messages and downstream tooling:

{
  "field": "status",
  "fieldType": "select",
  "fieldMetadata": {
    "api_slug": "status",
    "field_type": "select",
    "options": ["new", "active", "completed"]
  }
}

When a field type is resolved, the error service emits a structured debug log to aid incident triage:

DEBUG ErrorService field-context {"field":"status","fieldType":"select","resourceType":"tasks","operation":"update"}

These additions make it easier to audit failed requests and reconcile validation errors with the exact Attio configuration that triggered them.

Creating Errors with Field Metadata

Prefer the ErrorService helpers when you need field-aware diagnostics:

  • ErrorService.createFieldError({ ... }) for field-specific validation failures
  • ErrorService.createValidationError({ ... }) for broader validation errors with optional field context

Both helpers automatically hydrate fieldType and fieldMetadata from attribute discovery results. See ErrorService Helpers for usage details and logging behavior.

Filter Validation Error Categories

As of version 0.0.2, the server implements a more granular categorization system for filter validation errors. This allows for more targeted error handling and better user feedback.

CategoryDescriptionExample Error
STRUCTUREBasic structure issues with filtersMissing filters array, filters not an array
ATTRIBUTEIssues with attribute specificationMissing attribute object, missing attribute.slug
CONDITIONIssues with filter conditionsInvalid condition value, unsupported condition
VALUEIssues with filter valuesInvalid value type, incompatible value for condition
TRANSFORMATIONIssues transforming filters to API formatAPI format transformation errors

Working with Filter Error Categories

When handling filter validation errors, you can use the error category to determine the type of issue:

try {
  const results = await advancedSearchCompanies(filters);
} catch (error) {
  if (error instanceof FilterValidationError) {
    switch (error.category) {
      case FilterErrorCategory.STRUCTURE:
        console.error('Basic filter structure issue:', error.message);
        // Show basic filter structure examples
        break;
      case FilterErrorCategory.ATTRIBUTE:
        console.error('Issue with filter attributes:', error.message);
        // Show attribute examples
        break;
      case FilterErrorCategory.CONDITION:
        console.error('Issue with filter conditions:', error.message);
        // Show condition examples
        break;
      case FilterErrorCategory.VALUE:
        console.error('Issue with filter values:', error.message);
        // Show value format examples
        break;
      case FilterErrorCategory.TRANSFORMATION:
        console.error(
          'Issue transforming filters to API format:',
          error.message
        );
        // Show transformation examples
        break;
    }
  } else {
    console.error('Other error:', error.message);
  }
}

Filter Error Examples

Structure Error

Filter object is required but was undefined or null

Example of valid filter structure:
{
  "filters": [
    {
      "attribute": { "slug": "name" },
      "condition": "contains",
      "value": "Company Inc"
    }
  ]
}

Attribute Error

Invalid filter structure at index 0: missing attribute.slug

Example of valid filter structure:
{
  "filters": [
    {
      "attribute": { "slug": "name" },
      "condition": "contains",
      "value": "Company Inc"
    }
  ]
}

Condition Error

Invalid filter structure at index 0: invalid condition 'not_valid'.
Valid conditions are: equals, contains, starts_with, ends_with, greater_than, less_than, is_empty, is_not_empty

Example of valid filter structure:
{
  "filters": [
    {
      "attribute": { "slug": "name" },
      "condition": "contains",
      "value": "Company Inc"
    }
  ]
}

MCP-Specific Error Handling

When using Claude with the Attio MCP server, you may encounter errors that are specific to the MCP integration. Here's how these are handled:

Common MCP Error Scenarios

  1. Authentication Failures

    • If your Attio API key is invalid or expired, Claude will receive an authentication error
    • Claude will prompt you to check your API key configuration
  2. Resource Not Found Errors

    • When referencing non-existent records, Claude receives a NOT_FOUND error
    • Claude will suggest alternative search approaches or creation of new records
  3. Rate Limiting

    • Attio API rate limits may cause temporary failures
    • The MCP server implements retry logic with exponential backoff
    • Claude will inform you if operations are being delayed due to rate limiting

Error Recovery Strategies

Claude implements several strategies to recover from errors:

  1. Alternative Search Methods

    • If a direct lookup fails, Claude may try broader search criteria
    • Example: If email search fails, Claude might try name search instead
  2. Graceful Degradation

    • When specific operations fail, Claude will continue with available functions
    • Example: If creating a note fails, Claude can still read existing notes
  3. User Guidance

    • Claude provides clear explanations of errors and suggests next steps
    • For persistent errors, Claude will recommend troubleshooting steps

Example Error Handling Dialog

User: "Update the contact information for john@nonexistentcompany.com"

Claude: "I wasn't able to find a contact with the email john@nonexistentcompany.com. Would you like me to:

  1. Search for contacts named John instead
  2. Create a new contact with this email address
  3. Check if there might be a different email address for this contact"

Error Types

The server categorizes errors into specific types to make error handling more predictable and actionable. These types are defined in the ErrorType enum:

export enum ErrorType {
  VALIDATION_ERROR = 'validation_error',
  API_ERROR = 'api_error',
  AUTHENTICATION_ERROR = 'authentication_error',
  RATE_LIMIT_ERROR = 'rate_limit_error',
  NETWORK_ERROR = 'network_error',
  NOT_FOUND_ERROR = 'not_found_error',
  SERVER_ERROR = 'server_error',
  UNKNOWN_ERROR = 'unknown_error',
}

Error Response Format

All error responses follow a consistent format:

interface ErrorResponse {
  error: {
    type: ErrorType;
    message: string;
    details?: any;
    code?: string;
    status?: number;
  };
}
  • type: The categorized error type
  • message: A human-readable error message
  • details: Optional detailed information about the error
  • code: Optional error code
  • status: HTTP status code associated with the error

Input Validation

The server uses a comprehensive validation utility to ensure all input meets the expected schema before processing. This helps catch and report invalid input early in the request lifecycle.

// Example validation schema
const personSchema = {
  type: 'object',
  required: ['firstName', 'email'],
  properties: {
    firstName: { type: 'string', minLength: 1 },
    lastName: { type: 'string' },
    email: { type: 'string', format: 'email' },
    age: { type: 'number', minimum: 0 },
  },
};

// Validate input against schema
const { valid, errors } = validateInput(input, personSchema);
if (!valid) {
  // Handle validation errors
}

API Call Retry Logic

The server implements automatic retry logic for API calls to handle transient failures. This is done using the callWithRetry function which supports:

  • Configurable maximum retry attempts
  • Exponential backoff with jitter
  • Intelligent retry decisions based on error type
// Example usage
const result = await callWithRetry(
  async () => {
    // API call that might fail
    return await api.get('/some/endpoint');
  },
  {
    maxRetries: 3,
    initialDelay: 1000,
    maxDelay: 10000,
  }
);

Default Retry Configuration

export const DEFAULT_RETRY_CONFIG: RetryConfig = {
  maxRetries: 3,
  initialDelay: 1000, // 1 second
  maxDelay: 10000, // 10 seconds
  useExponentialBackoff: true,
  retryableStatusCodes: [408, 429, 500, 502, 503, 504],
};

Standardized Response Formatting

All responses from the server follow standardized formats for consistency:

Success Responses

// Basic success response
{
  success: true,
  message: "Operation completed successfully",
  data: { ... }
}

// List response
{
  success: true,
  message: "Found 10 items",
  data: [ ... ],
  meta: {
    total: 10,
    page: 1,
    hasMore: false
  }
}

Error Responses

// Error response
{
  success: false,
  error: {
    type: "validation_error",
    message: "Invalid input provided",
    details: [
      { field: "email", message: "Must be a valid email address" }
    ]
  }
}

Best Practices

Creating Error Responses

Use the createErrorResult function to create standardized error responses:

import { createErrorResult, ErrorType } from '../utils/error-handler';

try {
  // Operation that might fail
} catch (error) {
  if (error.response?.status === 404) {
    return createErrorResult(ErrorType.NOT_FOUND_ERROR, 'Resource not found', {
      resourceId: id,
    });
  }

  // Generic error handling
  return createErrorResult(
    ErrorType.UNKNOWN_ERROR,
    'An unexpected error occurred',
    error
  );
}

Using the Retry Logic

Add retry logic to API calls that might fail due to transient issues:

import { callWithRetry } from '../api/attio-operations';

async function fetchUserData(userId) {
  return callWithRetry(
    async () => {
      // API call that might fail
      const response = await api.get(`/users/${userId}`);
      return response.data;
    },
    {
      maxRetries: 5,
      retryableStatusCodes: [429, 503],
    }
  );
}

Validation

Always validate inputs before processing:

import { validateInput } from '../utils/validation';

function processUserData(userData) {
  const schema = {
    type: 'object',
    required: ['name', 'email'],
    properties: {
      name: { type: 'string', minLength: 1 },
      email: { type: 'string', format: 'email' },
    },
  };

  const { valid, errors } = validateInput(userData, schema);
  if (!valid) {
    throw new Error(`Invalid user data: ${JSON.stringify(errors)}`);
  }

  // Process validated data
}

Testing

The error handling system includes comprehensive test coverage for:

  • Error type categorization
  • Response formatting
  • Input validation
  • Retry logic

Use these tests as examples when implementing error handling in new features.

API Client Error Handling Best Practices

When interacting with external APIs, such as the Attio API via client modules (e.g., src/api/attio-operations.ts), it's crucial to handle errors in a way that preserves detailed information for upstream processing and user feedback.

Key Principle: Preserve Original Error Context

The most common pitfall is catching an error from an API client library (like an AxiosError) and then throwing a new, generic Error object. This practice strips away valuable context from the original error, such as:

  • HTTP status code
  • API-specific error codes (e.g., responseData.code from Attio)
  • API-specific error messages and details (e.g., responseData.path, responseData.detail)

This loss of information prevents centralized error handlers (like those in src/utils/error-handler.ts or src/handlers/tools.ts) from creating rich, informative error messages for the end-user or for detailed logging.

When a function in an API client module (like src/api/attio-operations.ts) catches an error from an API call (typically an AxiosError or similar), it should re-throw the original error object.

Rationale:

  • Centralized Enhancement: Allows a single, higher-level error handler to inspect the original error (including its response.data or similar properties) and create a specific, user-friendly AttioApiError or a well-structured error response.
  • Detailed Logging: Ensures that all available details from the API provider are available for logging and debugging.
  • Consistent Error Structure: Promotes a consistent error structure throughout the application, as the centralized handler can normalize different API errors into a standard format.

Example

Consider a function in src/api/attio-operations.ts that makes an API call:

BAD: Masking the original error.

// In src/api/attio-operations.ts
import axios from 'axios';

async function fetchSomeData(id: string): Promise<any> {
  try {
    const response = await axios.get(
      `https://api.attio.com/v2/some-endpoint/${id}`
    );
    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error) && error.response) {
      // PROBLEM: Creates a new generic error, losing original Attio details.
      if (error.response.status === 404) {
        throw new Error(`Resource with ID ${id} not found.`);
      } else if (error.response.status === 400) {
        throw new Error(
          `Bad request for resource ${id}: ${error.response.data.message}`
        );
      }
    }
    // PROBLEM: Generic fallback, losing all specific API error info.
    throw new Error('An unexpected error occurred while fetching data.');
  }
}

GOOD: Re-throwing the original error for upstream handling.

// In src/api/attio-operations.ts
import axios from 'axios';

async function fetchSomeData(id: string): Promise<any> {
  try {
    const response = await axios.get(
      `https://api.attio.com/v2/some-endpoint/${id}`
    );
    return response.data;
  } catch (error) {
    // BEST PRACTICE: Re-throw the original error (or a minimally wrapped one if necessary,
    // ensuring the original error is attached for inspection).
    // The AxiosError object itself contains response.status, response.data, etc.
    throw error;
  }
}

// Upstream handler (e.g., in src/handlers/tools.ts or called by it)
// This handler can now access error.response.data from Attio.

/*
import { createAttioError } from '../utils/error-handler';
sync function handleFetchData(id: string) {
  try {
    return await fetchSomeData(id);
  } catch (error) {
    // createAttioError (or similar) can inspect the full AxiosError
    // and its error.response.data to create a detailed AttioApiError.
    const enhancedError = createAttioError(error);
    // Log enhancedError, return it as a structured JSON-RPC error, etc.
    console.error(enhancedError);
    throw enhancedError; // Or return a formatted error response
  }
}
*/

By consistently re-throwing original API errors, the application can leverage centralized error handling logic to provide more precise feedback and better diagnostic information.

Recent Updates and Improvements

Enhanced Attribute Error Handling (Issue #183)

The company attributes module has been updated with improved error handling to address issues when using the get-company-attributes tool. Key improvements include:

  1. Try/Catch Blocks in Core Logic

    • Added structured try/catch blocks in the getCompanyAttributes function
    • Implemented consistent error logging using the logAttributeError helper
    • Enhanced error messages with additional context (company ID, attribute name)
  2. Robust Result Formatting

    • Improved error handling in the tool formatter with comprehensive checks
    • Added fallback handling for unexpected result structures
    • Better error messages for end users with clear guidance on issues
  3. Error Detection and Prevention

    • Added type checking and validation for input parameters
    • Improved validation for attribute existence before access attempts
    • Implemented robust error propagation with context preservation

These improvements ensure that the get-company-attributes tool now provides clear, actionable error messages rather than generic "[object Object]" errors, making it much easier to troubleshoot issues when working with company attributes.

Field Type Metadata in Enhanced Errors (Issue #808)

The error handling system now preserves field type and metadata information throughout the error flow, enabling better diagnostics and contextual error messages.

Field Type in Error Context

The EnhancedApiErrorContext interface now includes fieldType and fieldMetadata properties:

interface EnhancedApiErrorContext {
  field?: string;
  fieldType?: string; // e.g., "phone_number", "email", "select"
  fieldMetadata?: unknown; // Attio field config snapshot
  validValues?: string[];
  // ...other context fields
}

Benefits

Better Debugging: Knowing the field type helps distinguish between different error scenarios:

  • Validation logic issue: Our code has a bug
  • Field type mismatch: User provided wrong format
  • API schema change: Attio changed field requirements

Contextual Error Messages: Errors now surface field type information automatically:

// Error with field type context
const error = new EnhancedApiError(
  'Invalid status value',
  400,
  '/objects/companies',
  'POST',
  {
    field: 'status',
    fieldType: 'select',
    validValues: ['new', 'active', 'won'],
  }
);

// Contextual message includes type information:
// "Field 'status' expects values of type 'select'.
//  Valid options for 'status' are: [new, active, won]."

Field Type Preservation

Field types are preserved throughout the error flow:

  1. Error Templates: Predefined templates set appropriate field types
ErrorTemplates.INVALID_SELECT_OPTION(
  'status',
  'invalid',
  ['new', 'active'],
  'companies'
);
// Sets fieldType: 'select'

ErrorTemplates.INVALID_UUID_FORMAT('not-a-uuid', 'companies');
// Sets fieldType: 'uuid'

ErrorTemplates.PHONE_NUMBER_FORMAT_ERROR('phone', 'people');
// Sets fieldType: 'phone_number'
  1. Axios Error Mapping: Field types extracted from API validation errors
// API response includes field_type or expected_type
{
  validation_errors: [
    {
      field: 'status',
      field_type: 'select',
      message: 'Invalid option provided',
    },
  ];
}

// ErrorService.fromAxios() preserves this as fieldType
const mapped = ErrorService.fromAxios(axiosError);
// mapped.details.validation_errors[0].fieldType === 'select'
  1. Error Enhancement: Field type context preserved when enhancing errors
const enhanced = ErrorEnhancer.ensureEnhanced(new Error('Validation failed'), {
  field: 'status',
  fieldType: 'select',
  validValues: ['open', 'won'],
});

// enhanced.context.fieldType === 'select'
// enhanced.getContextualMessage() includes type information

Using Field Metadata

The fieldMetadata property can store the complete Attio field configuration for deep diagnostics:

const error = new EnhancedApiError(
  'Validation failed',
  400,
  '/objects/companies',
  'POST',
  {
    field: 'custom_field',
    fieldType: 'text',
    fieldMetadata: {
      slug: 'custom_field',
      type: 'text',
      max_length: 255,
      required: true,
      // ...full field config from Attio
    },
  }
);

// Access field configuration for debugging
if (error.context?.fieldMetadata) {
  const config = error.context.fieldMetadata as Record<string, unknown>;
  console.log('Field max length:', config.max_length);
  console.log('Field required:', config.required);
}

Validation Error Formatting

Validation errors now include field type in formatted messages:

// ErrorService.fromAxios() formats validation errors with type info:
// "- Field "status" (type: select): Invalid option provided"
// "- Field "record_id" (type: uuid): Must be valid UUID format"

Testing Field Type Preservation

Tests verify field type metadata flows correctly:

it('should preserve field type metadata when enhancing errors', () => {
  const enhanced = new EnhancedApiError(
    'Invalid value',
    400,
    '/api/test',
    'POST',
    {
      field: 'status',
      fieldType: 'select',
      validValues: ['new', 'active'],
    }
  );

  expect(enhanced.context?.fieldType).toBe('select');
  expect(enhanced.getContextualMessage()).toContain("type 'select'");
});

ErrorService Helpers

Utility methods in ErrorService now hydrate error context directly from attribute discovery metadata:

const attributeMetadataIndex = buildAttributeMetadataIndex(attributes);

throw ErrorService.createFieldError({
  field: 'status',
  message: 'Invalid status value',
  resourceType: 'tasks',
  operation: 'update',
  attributeMetadataIndex,
});

throw ErrorService.createValidationError({
  message: 'Priority must be numeric',
  resourceType: 'tasks',
  operation: 'update',
  field: 'priority',
  attributeMetadataIndex,
  documentationHint: 'Use numeric values between 1-5',
});

Both helpers log a field-context debug entry showing the resolved fieldType, making it visible in structured logs for debugging.

Common Field Types

Field TypeDescriptionExample Error Template
selectSelect/dropdown fieldsINVALID_SELECT_OPTION
uuidUUID identifier fieldsINVALID_UUID_FORMAT
phone_numberPhone number fieldsPHONE_NUMBER_FORMAT_ERROR
emailEmail address fieldsGeneric with fieldType: 'email'
textText/string fieldsTASK_FIELD_MAPPING
numberNumeric fieldsGeneric with fieldType: 'number'
dateDate/datetime fieldsGeneric with fieldType: 'date'

Related: PR #829 implementation, Issue #808 enhancement request