Skip to main content
Glama
kesslerio

Attio MCP Server

by kesslerio

search-by-timeframe

Read-onlyIdempotent

Search Attio CRM records by creation, modification, or last interaction dates to filter companies, people, tasks, and other resources within specific time periods.

Instructions

Search records by temporal criteria (creation, modification, interaction dates)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
end_dateNoEnd date (ISO 8601 format)
limitNoMaximum number of results to return
offsetNoNumber of results to skip for pagination
resource_typeYesType of resource to operate on (companies, people, lists, records, tasks)
start_dateNoStart date (ISO 8601 format)
timeframe_typeNoTimeframe filter type

Implementation Reference

  • Primary handler implementation for the records_search_by_timeframe tool (aliased as search-by-timeframe). Validates params, processes relative dates, builds Attio API date filters based on timeframe_type or date_field, executes search via handleUniversalSearch, and formats results with timestamps.
    export const searchByTimeframeConfig: UniversalToolConfig<
      TimeframeSearchParams,
      AttioRecord[]
    > = {
      name: 'records_search_by_timeframe',
      handler: async (params: TimeframeSearchParams): Promise<AttioRecord[]> => {
        try {
          const sanitizedParams = validateUniversalToolParams(
            'records_search_by_timeframe',
            params
          );
    
          const {
            resource_type,
            timeframe_type,
            start_date,
            end_date,
            relative_range,
            invert_range,
            date_field,
            limit,
            offset,
          } = sanitizedParams;
    
          // Process relative_range parameter if provided (Issue #475)
          let processedStartDate = start_date;
          let processedEndDate = end_date;
    
          if (relative_range) {
            // Import the timeframe utility to convert relative ranges
            const { getRelativeTimeframeRange } = await import(
              '@utils/filters/timeframe-utils.js'
            );
    
            try {
              const range = getRelativeTimeframeRange(
                relative_range as RelativeTimeframe
              );
              processedStartDate = range.startDate;
              processedEndDate = range.endDate;
            } catch {
              throw new Error(
                `Invalid relative_range '${relative_range}'. Supported options: today, yesterday, this_week, last_week, this_month, last_month, last_7_days, last_14_days, last_30_days, last_90_days`
              );
            }
          }
    
          // Validate that at least one date is provided (after processing relative_range)
          if (!processedStartDate && !processedEndDate) {
            throw new Error(
              'At least one date (start_date or end_date) is required for timeframe search'
            );
          }
    
          // Determine the timestamp field to filter on (Issue #475)
          // Use date_field if provided, otherwise fall back to timeframe_type logic
          let timestampField: string;
          if (date_field) {
            // Map date_field directly to proper field name
            switch (date_field) {
              case 'created_at':
                timestampField = mapFieldName('created_at');
                break;
              case 'updated_at':
                timestampField = mapFieldName('modified_at'); // Map updated_at to modified_at
                break;
              case 'modified_at':
                timestampField = mapFieldName('modified_at');
                break;
              default:
                throw new Error(`Unsupported date_field: ${date_field}`);
            }
          } else {
            // Fallback to original timeframe_type logic
            const effectiveTimeframeType = timeframe_type || TimeframeType.MODIFIED;
            switch (effectiveTimeframeType) {
              case TimeframeType.CREATED:
                timestampField = mapFieldName('created_at');
                break;
              case TimeframeType.MODIFIED:
                timestampField = mapFieldName('modified_at');
                break;
              case TimeframeType.LAST_INTERACTION:
                timestampField = mapFieldName('modified_at');
                break;
              default:
                throw new Error(
                  `Unsupported timeframe type: ${effectiveTimeframeType}`
                );
            }
          }
    
          // Build the date filter using proper Attio API v2 filter syntax
          // Use normalized operators with $ prefix
          const dateFilters: Record<string, unknown>[] = [];
    
          const coerceIso = (
            d?: string,
            endBoundary = false
          ): string | undefined => {
            if (!d) return undefined;
            // If date-only (YYYY-MM-DD), expand to full UTC boundary
            if (/^\d{4}-\d{2}-\d{2}$/.test(d)) {
              return endBoundary ? `${d}T23:59:59.999Z` : `${d}T00:00:00Z`;
            }
            return d;
          };
    
          const startIso = coerceIso(processedStartDate, false);
          const endIso = coerceIso(processedEndDate, true);
    
          // Handle invert_range logic (Issue #475)
          if (invert_range) {
            // For inverted searches, we want records that were NOT updated in the timeframe
            // This means records older than the start date OR newer than the end date
            if (startIso && endIso) {
              // For a range inversion, we want records outside the range
              // This is typically records older than the start date (before the timeframe)
              dateFilters.push({
                attribute: { slug: timestampField },
                condition: normalizeOperator('lt'), // Less than start date
                value: startIso,
              });
            } else if (startIso) {
              // Only start date - invert to find records older than this date
              dateFilters.push({
                attribute: { slug: timestampField },
                condition: normalizeOperator('lt'),
                value: startIso,
              });
            } else if (endIso) {
              // Only end date - invert to find records newer than this date
              dateFilters.push({
                attribute: { slug: timestampField },
                condition: normalizeOperator('gt'),
                value: endIso,
              });
            }
          } else {
            // Normal (non-inverted) logic
            if (startIso) {
              dateFilters.push({
                attribute: { slug: timestampField },
                condition: normalizeOperator('gte'), // Normalize to $gte
                value: startIso,
              });
            }
    
            if (endIso) {
              dateFilters.push({
                attribute: { slug: timestampField },
                condition: normalizeOperator('lte'), // Normalize to $lte
                value: endIso,
              });
            }
          }
    
          // Create the filter object with the expected structure (legacy compatibility)
          const filters = { filters: dateFilters } as Record<string, unknown>;
    
          // Use the universal search handler; pass timeframe params explicitly so the
          // UniversalSearchService can FORCE Query API routing for date comparisons
          return await handleUniversalSearch({
            resource_type,
            query: '',
            filters,
            // Force timeframe routing parameters
            timeframe_attribute: timestampField,
            start_date: startIso,
            end_date: endIso,
            date_operator: 'between',
            limit: limit || 20,
            offset: offset || 0,
          });
        } catch (error: unknown) {
          throw ErrorService.createUniversalError(
            'records_search_by_timeframe',
            `${params.resource_type}:${params.timeframe_type || 'undefined'}`,
            error
          );
        }
      },
      formatResult: (results: AttioRecord[], ...args: unknown[]) => {
        const timeframeType = args[0] as TimeframeType | undefined;
        const resourceType = args[1] as UniversalResourceType | undefined;
        if (!Array.isArray(results)) {
          return 'Found 0 records (timeframe search)\nTip: Ensure your workspace has data in the requested date range.';
        }
    
        const timeframeName = timeframeType
          ? timeframeType.replace(/_/g, ' ')
          : 'timeframe';
        const resourceCount = results.length;
        const resourceTypeName = resourceType
          ? resourceCount === 1
            ? formatResourceType(resourceType)
            : getPluralResourceType(resourceType)
          : resourceCount === 1
            ? 'record'
            : 'records';
    
        return `Found ${results.length} ${resourceTypeName} by ${timeframeName}:\n${results
          .map((record: Record<string, unknown>, index: number) => {
            const values = record.values as Record<string, unknown>;
            const name =
              (values?.name as Record<string, unknown>[])?.[0]?.value ||
              (values?.name as Record<string, unknown>[])?.[0]?.full_name ||
              (values?.full_name as Record<string, unknown>[])?.[0]?.value ||
              (values?.title as Record<string, unknown>[])?.[0]?.value ||
              'Unnamed';
            const recordId = record.id as Record<string, unknown>;
            const id = recordId?.record_id || 'unknown';
    
            // Try to show relevant date information
            const created = safeExtractTimestamp(record.created_at);
            const modified = safeExtractTimestamp(record.updated_at);
            let dateInfo = '';
    
            if (timeframeType === TimeframeType.CREATED && created !== 'unknown') {
              dateInfo = ` (created: ${new Date(created).toLocaleDateString()})`;
            } else if (
              timeframeType === TimeframeType.MODIFIED &&
              modified !== 'unknown'
            ) {
              dateInfo = ` (modified: ${new Date(modified).toLocaleDateString()})`;
            }
    
            return `${index + 1}. ${name}${dateInfo} (ID: ${id})`;
          })
          .join('\n')}`;
      },
    };
  • Zod/JSON schema for input validation of the search-by-timeframe tool parameters, including resource_type, timeframe_type, start_date, end_date, and pagination.
    export const searchByTimeframeSchema = {
      type: 'object' as const,
      properties: {
        resource_type: resourceTypeProperty,
        timeframe_type: {
          type: 'string' as const,
          enum: Object.values(TimeframeType),
          description: 'Timeframe filter type',
        },
        start_date: {
          type: 'string' as const,
          format: 'date',
          description: 'Start date (ISO 8601 format)',
        },
        end_date: {
          type: 'string' as const,
          format: 'date',
          description: 'End date (ISO 8601 format)',
        },
        ...paginationProperties,
      },
      required: ['resource_type' as const],
      additionalProperties: false,
      examples: [
        {
          resource_type: 'deals',
          timeframe_type: TimeframeType.CREATED,
          start_date: '2025-01-01',
          end_date: '2025-01-31',
        },
      ],
    };
  • MCP tool definition registration for records_search_by_timeframe, including description, inputSchema reference, and annotations. Exported via advancedOperationsToolDefinitions and included in universalToolDefinitions.
    records_search_by_timeframe: {
      name: 'records_search_by_timeframe',
      description: formatToolDescription({
        capability:
          'Filter records by creation, update, or interaction timeframes.',
        boundaries: 'modify lifecycle state or scheduling follow-ups.',
        constraints:
          'Requires resource_type; provide timeframe or explicit date boundaries.',
        recoveryHint:
          'Call records.search if timeframe filters are too restrictive.',
      }),
      inputSchema: searchByTimeframeSchema,
      annotations: {
        readOnlyHint: true,
        idempotentHint: true,
      },
    },
  • Tool alias registration mapping 'search-by-timeframe' to the canonical 'records_search_by_timeframe' tool.
    'search-by-timeframe': {
      target: 'records_search_by_timeframe',
      reason: 'Phase 1 search tool rename (#776)',
      since: SINCE_PHASE_1,
      removal: 'v1.x (TBD)',
    },
  • Helper mapping for deprecated tool names to timeframe_type values used in migration params for search-by-timeframe.
     * Timeframe type mappings for search-by-timeframe universal tool
     */
    export const timeframeTypeMappings: Record<string, string> = {
      'search-people-by-creation-date': 'created',
      'search-people-by-modification-date': 'modified',
      'search-people-by-last-interaction': 'last_interaction',
    };
Behavior3/5

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

Annotations already declare readOnlyHint=true and idempotentHint=true, so the agent knows this is a safe, repeatable read operation. The description adds context about the temporal nature of the search but doesn't disclose additional behavioral traits like pagination behavior (implied by offset/limit), rate limits, authentication requirements, or what happens with empty results. It doesn't contradict annotations.

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 that immediately conveys the core functionality. It's appropriately sized for a search tool with good schema documentation, with zero wasted words or redundant information.

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 the tool's moderate complexity (6 parameters, temporal filtering logic), 100% schema coverage, and read-only/idempotent annotations, the description is minimally adequate. However, without an output schema and with multiple similar search tools in the sibling set, it should provide more contextual differentiation and guidance about result format or when to prefer this over other search methods.

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 the schema already fully documents all 6 parameters with descriptions, formats, enums, and constraints. The description adds minimal value beyond what's in the schema - it mentions 'temporal criteria' which aligns with timeframe_type but doesn't provide additional semantic context about parameter interactions or usage patterns.

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

Purpose4/5

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

The description clearly states the tool's purpose: 'Search records by temporal criteria' with specific examples of criteria types (creation, modification, interaction dates). It uses a specific verb ('Search') and resource ('records'), but doesn't explicitly distinguish it from sibling tools like 'search', 'search-by-content', or 'search-records' beyond the temporal focus.

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 provides no guidance on when to use this tool versus alternatives. With multiple search-related sibling tools (search, search-by-content, search-by-relationship, search-records, advanced-search), there's no indication of when temporal filtering is preferred over other search methods or what distinguishes this from general search tools.

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