Skip to main content
Glama

get-record-details

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)', },

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