Skip to main content
Glama
kesslerio

Attio MCP Server

by kesslerio

get-record-details

Read-onlyIdempotent

Retrieve detailed information for CRM records including companies, people, lists, tasks, deals, and notes by specifying record ID and desired fields.

Instructions

Get detailed information for any record type

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
fieldsNoFields to include
record_idYesRecord ID to retrieve
resource_typeYesType of resource to operate on (companies, people, lists, records, tasks)

Implementation Reference

  • Core handler function that implements the get-record-details tool logic. Handles retrieval for all universal resource types (companies, people, lists, records, deals, tasks, notes), including UUID validation, 404 caching, performance tracking, field filtering, and enhanced error handling with EnhancedApiError.
    static async getRecordDetails(
      params: UniversalRecordDetailsParams
    ): Promise<AttioRecord> {
      const { resource_type, record_id, fields } = params;
    
      // NOTE: E2E tests should use real API by default. Mock shortcuts are reserved for offline smoke tests.
    
      // Start performance tracking
      const perfId = enhancedPerformanceTracker.startOperation(
        'get-record-details',
        'get',
        { resourceType: resource_type, recordId: record_id }
      );
    
      // Enhanced UUID validation using ValidationService (Issue #416)
      const validationStart = performance.now();
    
      // Early ID validation for performance tests - provide exact expected error message
      if (
        !record_id ||
        typeof record_id !== 'string' ||
        record_id.trim().length === 0
      ) {
        enhancedPerformanceTracker.endOperation(
          perfId,
          false,
          'Invalid record identifier format',
          400
        );
        throw new Error('Invalid record identifier format');
      }
    
      // Validate UUID format with clear error distinction
      // In mock/offline mode, allow known mock/test ID patterns but still reject obvious invalid formats
      try {
        if (shouldUseMockData()) {
          const isHex24 = /^[0-9a-f]{24}$/i.test(record_id);
          const isMockish =
            /^(mock-|comp_|person_|list_|deal_|task_|note_|rec_|record_)/i.test(
              record_id
            );
          // Local UUID v4 format check to avoid relying on mocked module exports in tests
          const looksLikeUuidV4 =
            /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
              record_id
            );
          const looksValid = isHex24 || isMockish || looksLikeUuidV4;
          if (!looksValid) {
            enhancedPerformanceTracker.endOperation(
              perfId,
              false,
              'Invalid record identifier format',
              400
            );
            throw new Error('Invalid record identifier format');
          }
        } else {
          ValidationService.validateUUID(record_id, resource_type, 'GET', perfId);
        }
      } catch (validationError) {
        enhancedPerformanceTracker.endOperation(
          perfId,
          false,
          'Invalid UUID format validation error',
          400
        );
        // For performance tests, preserve the original validation error message
        // This allows error.message to contain "Invalid record identifier format"
        if (validationError instanceof Error) {
          throw validationError; // Preserve original EnhancedApiError with .message property
        }
        // Fallback for non-Error cases
        throw new Error('Invalid record identifier format');
      }
    
      enhancedPerformanceTracker.markTiming(
        perfId,
        'validation',
        performance.now() - validationStart
      );
    
      // Check 404 cache using CachingService
      if (CachingService.isCached404(resource_type, record_id)) {
        enhancedPerformanceTracker.endOperation(
          perfId,
          false,
          'Cached 404 response',
          404,
          { cached: true }
        );
        // Use EnhancedApiError for consistent error handling
        throw createRecordNotFoundError(record_id, resource_type);
      }
    
      // Track API call timing
      const apiStart = enhancedPerformanceTracker.markApiStart(perfId);
      let result: AttioRecord;
    
      try {
        result = await this.retrieveRecordByType(resource_type, record_id);
    
        enhancedPerformanceTracker.markApiEnd(perfId, apiStart);
        enhancedPerformanceTracker.endOperation(perfId, true, undefined, 200);
    
        // Apply field filtering if fields parameter was provided
        if (fields && fields.length > 0) {
          const filteredResult = this.filterResponseFields(result, fields);
          // Ensure the filtered result maintains AttioRecord structure
          return {
            id: result.id,
            created_at: result.created_at,
            updated_at: result.updated_at,
            values:
              (filteredResult.values as Record<string, unknown>) || result.values,
          } as unknown as AttioRecord;
        }
        return result;
      } catch (apiError: unknown) {
        enhancedPerformanceTracker.markApiEnd(perfId, apiStart);
    
        // Handle EnhancedApiError instances directly - preserve them through the chain
        if (isEnhancedApiError(apiError)) {
          // Cache 404 responses using CachingService
          if (apiError.statusCode === 404) {
            CachingService.cache404Response(resource_type, record_id);
          }
    
          enhancedPerformanceTracker.endOperation(
            perfId,
            false,
            apiError.message,
            apiError.statusCode
          );
    
          // Re-throw EnhancedApiError as-is - make message enumerable for vitest
          throw withEnumerableMessage(apiError);
        }
    
        // Enhanced error handling for Issues #415, #416, #417
        const errorObj = apiError as Record<string, unknown>;
        const statusCode =
          ((errorObj?.response as Record<string, unknown>)?.status as number) ||
          (errorObj?.statusCode as number) ||
          500;
    
        if (
          statusCode === 404 ||
          (apiError instanceof Error && apiError.message.includes('not found'))
        ) {
          // Cache 404 responses using CachingService
          CachingService.cache404Response(resource_type, record_id);
    
          enhancedPerformanceTracker.endOperation(
            perfId,
            false,
            'Record not found',
            404
          );
    
          // URS suite expects createRecordNotFoundError for generic 404s
          throw createRecordNotFoundError(record_id, resource_type);
        }
    
        if (statusCode === 400) {
          enhancedPerformanceTracker.endOperation(
            perfId,
            false,
            'Invalid request',
            400
          );
    
          // Create and throw enhanced error
          const error = new Error(`Invalid record_id format: ${record_id}`);
          (error as unknown as Record<string, unknown>).statusCode = 400;
          throw ensureEnhanced(error, {
            endpoint: `/${resource_type}/${record_id}`,
            method: 'GET',
            resourceType: resource_type,
            recordId: record_id,
          });
        }
    
        // Check if this is our structured HTTP response before enhancing
        if (
          apiError &&
          typeof apiError === 'object' &&
          'status' in apiError &&
          'body' in apiError
        ) {
          // Convert legacy HTTP response to EnhancedApiError
          const errorObj = apiError as Record<string, unknown>;
          const message = String(
            (errorObj.body as Record<string, unknown>)?.message || 'HTTP error'
          );
          const status = Number(errorObj.status) || 500;
          enhancedPerformanceTracker.endOperation(perfId, false, message, status);
          const error = new Error(message);
          (error as unknown as Record<string, unknown>).statusCode = status;
          throw ensureEnhanced(error, {
            endpoint: `/${resource_type}/${record_id}`,
            method: 'GET',
            resourceType: resource_type,
            recordId: record_id,
          });
        }
    
        // For HTTP errors, use ErrorEnhancer to auto-enhance
        if (Number.isFinite(statusCode)) {
          const error =
            apiError instanceof Error ? apiError : new Error(String(apiError));
          const enhancedError = ErrorEnhancer.autoEnhance(
            error,
            resource_type,
            'get-record-details',
            record_id
          );
          enhancedPerformanceTracker.endOperation(
            perfId,
            false,
            // Issue #425: Use safe error message extraction
            ErrorEnhancer.getErrorMessage(enhancedError),
            statusCode
          );
          throw enhancedError;
        }
    
        // Fallback for any other uncaught errors
        const fallbackMessage =
          apiError instanceof Error ? apiError.message : String(apiError);
        enhancedPerformanceTracker.endOperation(
          perfId,
          false,
          fallbackMessage,
          500
        );
        // Always throw a standard Error object for consistent handling by the dispatcher
        throw new Error(
          `Failed to retrieve record ${record_id}: ${fallbackMessage}`
        );
      }
    }
  • Tool configuration for 'records_get_details' (target of 'get-record-details' alias), including handler wrapper, result formatter, structured output, and reference to input schema.
    export const getRecordDetailsConfig: UniversalToolConfig<
      UniversalRecordDetailsParams,
      AttioRecord
    > = {
      name: 'records_get_details',
      handler: async (
        params: UniversalRecordDetailsParams
      ): Promise<AttioRecord> => {
        try {
          const sanitizedParams = validateUniversalToolParams(
            'records_get_details',
            params
          );
          return await handleUniversalGetDetails(sanitizedParams);
        } catch (error: unknown) {
          return await handleSearchError(
            error,
            params.resource_type,
            params as unknown as Record<string, unknown>
          );
        }
      },
      formatResult: (record: AttioRecord, ...args: unknown[]): string => {
        const resourceType = args[0] as UniversalResourceType | undefined;
        if (!record) {
          return 'Record not found';
        }
    
        const resourceTypeName = resourceType
          ? getSingularResourceType(resourceType)
          : 'record';
        const name = UniversalUtilityService.extractDisplayName(
          record.values || {}
        );
        const id = String(record.id?.record_id || 'unknown');
    
        let details = `${resourceTypeName.charAt(0).toUpperCase() + resourceTypeName.slice(1)}: ${name}\nID: ${id}\n\n`;
    
        if (record.values) {
          let fieldOrder = [
            'email',
            'domains',
            'phone',
            'description',
            'categories',
            'primary_location',
          ];
    
          if (resourceType === UniversalResourceType.PEOPLE) {
            fieldOrder = [
              'email_addresses',
              'phone_numbers',
              'job_title',
              'description',
              'primary_location',
            ];
    
            if (
              record.values.associated_company &&
              Array.isArray(record.values.associated_company)
            ) {
              const companies = (
                record.values.associated_company as Record<string, unknown>[]
              )
                .map(
                  (c: Record<string, unknown>) =>
                    c.target_record_name || c.name || c.value
                )
                .filter(Boolean);
              if (companies.length > 0) {
                details += `Company: ${companies.join(', ')}\n`;
              }
            }
          }
    
          fieldOrder.forEach((field) => {
            const value =
              record.values?.[field] &&
              Array.isArray(record.values[field]) &&
              (record.values[field] as { value: string }[])[0]?.value;
            if (value) {
              const displayField =
                field.charAt(0).toUpperCase() + field.slice(1).replace(/_/g, ' ');
              details += `${displayField}: ${value}\n`;
            }
          });
    
          if (record.values?.domains && Array.isArray(record.values.domains)) {
            const domains = (record.values.domains as { domain?: string }[])
              .map((d: { domain?: string }) => d.domain)
              .filter(Boolean);
            if (domains.length > 0) {
              details += `Domains: ${domains.join(', ')}\n`;
            }
          }
          if (resourceType === UniversalResourceType.PEOPLE) {
            if (
              record.values.email_addresses &&
              Array.isArray(record.values.email_addresses)
            ) {
              const emails = (
                record.values.email_addresses as Record<string, unknown>[]
              )
                .map((e: Record<string, unknown>) => e.email_address || e.value)
                .filter(Boolean);
              if (emails.length > 0) {
                details += `Email: ${emails.join(', ')}\n`;
              }
            }
    
            if (
              record.values.phone_numbers &&
              Array.isArray(record.values.phone_numbers)
            ) {
              const phones = (
                record.values.phone_numbers as Record<string, unknown>[]
              )
                .map((p: Record<string, unknown>) => p.phone_number || p.value)
                .filter(Boolean);
              if (phones.length > 0) {
                details += `Phone: ${phones.join(', ')}\n`;
              }
            }
          }
    
          if (
            record.values.created_at &&
            Array.isArray(record.values.created_at) &&
            (record.values.created_at as { value: string }[])[0]?.value
          ) {
            details += `Created at: ${(record.values.created_at as { value: string }[])[0].value}\n`;
          }
        }
    
        return details.trim();
      },
      structuredOutput: (
        record: AttioRecord,
        resourceType?: string
      ): Record<string, unknown> => {
        if (!record) return {};
    
        const result: Record<string, unknown> = { ...record };
    
        // Normalize company name to string for consistency
        if (resourceType === 'companies' && record.values) {
          const values = record.values as Record<string, unknown>;
          const nameArray = values.name;
          if (Array.isArray(nameArray) && nameArray[0]?.value) {
            result.values = {
              ...values,
              name: nameArray[0].value,
            };
          }
        }
    
        return result;
      },
    };
    
    export const getRecordDetailsDefinition = {
      name: 'records_get_details',
      description: formatToolDescription({
        capability: 'Fetch a single record with enriched attribute formatting.',
        boundaries:
          'search or filter result sets; use records.search* tools instead.',
        constraints:
          'Requires resource_type and record_id; optional fields filter output.',
        recoveryHint: 'Validate record IDs with records.search before retrying.',
      }),
      inputSchema: getRecordDetailsSchema,
      annotations: {
        readOnlyHint: true,
        idempotentHint: true,
      },
    };
  • TypeScript interface defining input parameters for get-record-details: resource_type (enum), record_id (string), optional fields array for filtering.
    export interface UniversalRecordDetailsParams {
      resource_type: UniversalResourceType;
      record_id: string;
      fields?: string[];
    }
  • Registers the records_get_details tool config in the core operations map, which is merged into universalToolConfigs.
    export const coreOperationsToolConfigs = {
      'create-note': createNoteConfig,
      'list-notes': listNotesConfig,
      records_search: searchRecordsConfig,
      records_get_details: getRecordDetailsConfig,
      'create-record': createRecordConfig,
      'update-record': updateRecordConfig,
      'delete-record': deleteRecordConfig,
      records_get_attributes: getAttributesConfig,
      records_discover_attributes: discoverAttributesConfig,
      records_get_info: getDetailedInfoConfig,
    };
  • Defines 'get-record-details' as an alias that resolves to the canonical 'records_get_details' tool.
    'get-record-details': {
      target: 'records_get_details',
      reason: 'Phase 1 search tool rename (#776)',
      since: SINCE_PHASE_1,
      removal: 'v1.x (TBD)',
    },
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations provide readOnlyHint=true and idempotentHint=true, indicating a safe, repeatable read operation. The description adds value by specifying 'detailed information for any record type', which hints at broader scope beyond basic retrieval, but doesn't disclose rate limits, authentication needs, or error behaviors. No contradiction with annotations exists.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence with zero wasted words. It's front-loaded with the core purpose, making it easy to parse quickly. No unnecessary elaboration or redundancy is present.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given annotations cover safety and idempotency, and schema covers parameters fully, the description is minimally adequate. However, with no output schema and siblings offering similar functions, it lacks details on return format, error handling, or differentiation, leaving gaps in context for a retrieval tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so parameters are well-documented in the schema. The description adds no additional meaning about parameters beyond implying 'any record type' aligns with the resource_type enum. Baseline 3 is appropriate as the schema carries the burden, but the description doesn't enhance parameter understanding.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description 'Get detailed information for any record type' clearly states the verb ('Get') and resource ('detailed information for any record type'), but it's vague about what 'detailed information' entails and doesn't distinguish it from siblings like 'get-detailed-info' or 'get-list-details'. It provides a basic purpose but lacks specificity.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description offers no guidance on when to use this tool versus alternatives. With siblings like 'get-detailed-info', 'search-records', and 'fetch' that might overlap in functionality, there's no indication of context, prerequisites, or exclusions. It leaves the agent to guess based on the tool name alone.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/kesslerio/attio-mcp-server'

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