tools.ts.old•61.4 kB
/**
* Handlers for tool-related requests
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { createErrorResult } from "../utils/error-handler.js";
import { ValueMatchError } from "../errors/value-match-error.js";
import { parseResourceUri } from "../utils/uri-parser.js";
import { ResourceType, AttioListEntry, AttioRecord } from "../types/attio.js";
import { ListEntryFilters } from "../api/operations/index.js";
import { processListEntries } from "../utils/record-utils.js";
import axios from "axios";
// Import tool configurations and definitions
import {
companyToolConfigs,
companyToolDefinitions,
peopleToolConfigs,
peopleToolDefinitions,
listsToolConfigs,
listsToolDefinitions,
promptsToolConfigs,
promptsToolDefinitions,
recordToolConfigs,
recordToolDefinitions
} from "./tool-configs/index.js";
// Import resource-specific tool configuration
import {
RESOURCE_SPECIFIC_CREATE_TOOLS,
ResourceSpecificCreateTool,
RESOURCE_TYPE_MAP,
VALIDATION_RULES
} from "./tool-configs/resource-specific-tools.js";
// Import tool types
import {
ToolConfig,
SearchToolConfig,
AdvancedSearchToolConfig,
DetailsToolConfig,
NotesToolConfig,
CreateNoteToolConfig,
GetListsToolConfig,
GetListEntriesToolConfig,
ListActionToolConfig,
DateBasedSearchToolConfig
} from "./tool-types.js";
// Import record tool types
import {
RecordCreateToolConfig,
RecordGetToolConfig,
RecordUpdateToolConfig,
RecordDeleteToolConfig,
RecordListToolConfig,
RecordBatchCreateToolConfig,
RecordBatchUpdateToolConfig
} from "./tool-configs/records/index.js";
// Consolidated tool configurations from modular files
const TOOL_CONFIGS = {
[ResourceType.COMPANIES]: companyToolConfigs,
[ResourceType.PEOPLE]: peopleToolConfigs,
[ResourceType.LISTS]: listsToolConfigs,
[ResourceType.RECORDS]: recordToolConfigs,
// Add other resource types as needed
};
// Consolidated tool definitions from modular files
const TOOL_DEFINITIONS = {
[ResourceType.COMPANIES]: companyToolDefinitions,
[ResourceType.PEOPLE]: peopleToolDefinitions,
[ResourceType.LISTS]: listsToolDefinitions,
[ResourceType.RECORDS]: recordToolDefinitions,
// Add other resource types as needed
};
/**
* Find the tool config for a given tool name
*
* @param toolName - The name of the tool
* @returns Tool configuration or undefined if not found
*/
function findToolConfig(toolName: string): {
resourceType: ResourceType;
toolConfig: ToolConfig;
toolType: string;
} | undefined {
for (const resourceType of Object.values(ResourceType)) {
const resourceConfig = TOOL_CONFIGS[resourceType];
if (!resourceConfig) continue;
for (const [toolType, config] of Object.entries(resourceConfig)) {
if (config && config.name === toolName) {
return {
resourceType: resourceType as ResourceType,
toolConfig: config as ToolConfig,
toolType,
};
}
}
}
return undefined;
}
/**
* Registers tool-related request handlers with the server
*
* @param server - The MCP server instance
*/
export function registerToolHandlers(server: Server): void {
// Handler for listing available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
...TOOL_DEFINITIONS[ResourceType.COMPANIES],
...TOOL_DEFINITIONS[ResourceType.PEOPLE],
...TOOL_DEFINITIONS[ResourceType.LISTS]
],
};
});
// Handler for calling tools
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
try {
const toolInfo = findToolConfig(toolName);
if (!toolInfo) {
throw new Error(`Tool not found: ${toolName}`);
}
const { resourceType, toolConfig, toolType } = toolInfo;
// Handle search tools
if (toolType === 'search') {
const query = request.params.arguments?.query as string;
try {
const searchToolConfig = toolConfig as SearchToolConfig;
const results = await searchToolConfig.handler(query);
const formattedResults = searchToolConfig.formatResult(results);
return {
content: [
{
type: "text",
text: `Found ${results.length} ${resourceType}:\n${formattedResults}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`/objects/${resourceType}/records/query`,
"POST",
(error as any).response?.data || {}
);
}
}
// Handle searchByEmail tools
if (toolType === 'searchByEmail') {
const email = request.params.arguments?.email as string;
try {
const searchToolConfig = toolConfig as SearchToolConfig;
const results = await searchToolConfig.handler(email);
const formattedResults = searchToolConfig.formatResult(results);
return {
content: [
{
type: "text",
text: formattedResults,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`/objects/${resourceType}/records/query`,
"POST",
(error as any).response?.data || {}
);
}
}
// Handle searchByPhone tools
if (toolType === 'searchByPhone') {
const phone = request.params.arguments?.phone as string;
try {
const searchToolConfig = toolConfig as SearchToolConfig;
const results = await searchToolConfig.handler(phone);
const formattedResults = searchToolConfig.formatResult(results);
return {
content: [
{
type: "text",
text: formattedResults,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`/objects/${resourceType}/records/query`,
"POST",
(error as any).response?.data || {}
);
}
}
// Handle details tools
if (toolType === 'details') {
let id: string;
let uri: string;
// Check which parameter is provided
const directId = resourceType === ResourceType.COMPANIES
? request.params.arguments?.companyId as string
: request.params.arguments?.personId as string;
uri = request.params.arguments?.uri as string;
// Use either direct ID or URI, with priority to URI if both are provided
if (uri) {
try {
const [uriType, uriId] = parseResourceUri(uri);
if (uriType !== resourceType) {
throw new Error(`URI type mismatch: Expected ${resourceType}, got ${uriType}`);
}
id = uriId;
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Invalid URI format"),
uri,
"GET",
{ status: 400, message: "Invalid URI format" }
);
}
} else if (directId) {
id = directId;
// For logging purposes
uri = `attio://${resourceType}/${directId}`;
} else {
return createErrorResult(
new Error("Missing required parameter: uri or direct ID"),
`${resourceType}/details`,
"GET",
{ status: 400, message: "Missing required parameter: uri or companyId/personId" }
);
}
try {
const details = await toolConfig.handler(id);
// If a formatResult function exists, use it
if ('formatResult' in toolConfig && typeof toolConfig.formatResult === 'function') {
const formattedResult = toolConfig.formatResult(details);
return {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
}
// Otherwise, fall back to JSON stringification
return {
content: [
{
type: "text",
text: `${resourceType.slice(0, -1).charAt(0).toUpperCase() + resourceType.slice(1, -1)} details for ${id}:\n${JSON.stringify(details, null, 2)}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
uri,
"GET",
(error as any).response?.data || {}
);
}
}
// Handle getAttributes tools
if (toolType === 'getAttributes') {
if (resourceType === ResourceType.COMPANIES) {
const companyId = request.params.arguments?.companyId as string;
const attributeName = request.params.arguments?.attributeName as string | undefined;
try {
const result = await toolConfig.handler(companyId, attributeName);
const formattedResult = toolConfig.formatResult ? toolConfig.formatResult(result) : JSON.stringify(result, null, 2);
return {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`companies/${companyId}/attributes`,
"GET",
(error as any).response?.data || {}
);
}
}
}
// Handle json tools (same as details but with json tool type)
if (toolType === 'json') {
let id: string;
let uri: string;
// Check which parameter is provided
const directId = resourceType === ResourceType.COMPANIES
? request.params.arguments?.companyId as string
: request.params.arguments?.personId as string;
uri = request.params.arguments?.uri as string;
// Use either direct ID or URI, with priority to URI if both are provided
if (uri) {
try {
const [uriType, uriId] = parseResourceUri(uri);
if (uriType !== resourceType) {
throw new Error(`URI type mismatch: Expected ${resourceType}, got ${uriType}`);
}
id = uriId;
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Invalid URI format"),
uri,
"GET",
{ status: 400, message: "Invalid URI format" }
);
}
} else if (directId) {
id = directId;
// For logging purposes
uri = `attio://${resourceType}/${directId}`;
} else {
return createErrorResult(
new Error("Missing required parameter: uri or direct ID"),
`${resourceType}/json`,
"GET",
{ status: 400, message: "Missing required parameter: uri or companyId/personId" }
);
}
try {
const details = await toolConfig.handler(id);
// If a formatResult function exists, use it
if ('formatResult' in toolConfig && typeof toolConfig.formatResult === 'function') {
const formattedResult = toolConfig.formatResult(details);
return {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
}
// Otherwise, fall back to JSON stringification
return {
content: [
{
type: "text",
text: `${resourceType.slice(0, -1).charAt(0).toUpperCase() + resourceType.slice(1, -1)} JSON for ${id}:\n${JSON.stringify(details, null, 2)}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
uri,
"GET",
(error as any).response?.data || {}
);
}
}
// Handle basic info tools
if (toolType === 'basicInfo' || toolType === 'contactInfo' || toolType === 'businessInfo' || toolType === 'socialInfo') {
let id: string;
let uri: string;
// Check which parameter is provided
const directId = resourceType === ResourceType.COMPANIES
? request.params.arguments?.companyId as string
: request.params.arguments?.personId as string;
uri = request.params.arguments?.uri as string;
// Use either direct ID or URI, with priority to URI if both are provided
if (uri) {
try {
const [uriType, uriId] = parseResourceUri(uri);
if (uriType !== resourceType) {
throw new Error(`URI type mismatch: Expected ${resourceType}, got ${uriType}`);
}
id = uriId;
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Invalid URI format"),
uri,
"GET",
{ status: 400, message: "Invalid URI format" }
);
}
} else if (directId) {
id = directId;
uri = `attio://${resourceType}/${directId}`;
} else {
return createErrorResult(
new Error("Missing required parameter: uri or direct ID"),
`${resourceType}/${toolType}`,
"GET",
{ status: 400, message: "Missing required parameter: uri or companyId/personId" }
);
}
try {
const result = await toolConfig.handler(id);
if ('formatResult' in toolConfig && typeof toolConfig.formatResult === 'function') {
const formattedResult = toolConfig.formatResult(result);
return {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
uri,
"GET",
(error as any).response?.data || {}
);
}
}
// Handle fields tool
if (toolType === 'fields') {
const companyId = request.params.arguments?.companyId as string;
const fields = request.params.arguments?.fields as string[];
if (!companyId || !fields || !Array.isArray(fields)) {
return createErrorResult(
new Error("Missing required parameters: companyId and fields array"),
`${resourceType}/fields`,
"GET",
{ status: 400, message: "Missing required parameters: companyId and fields array" }
);
}
try {
const result = await toolConfig.handler(companyId, fields);
if ('formatResult' in toolConfig && typeof toolConfig.formatResult === 'function') {
const formattedResult = toolConfig.formatResult(result);
return {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`${resourceType}/fields/${companyId}`,
"GET",
(error as any).response?.data || {}
);
}
}
// Handle custom fields tool
if (toolType === 'customFields') {
const companyId = request.params.arguments?.companyId as string;
const customFieldNames = request.params.arguments?.customFieldNames;
if (!companyId) {
return createErrorResult(
new Error("Missing required parameter: companyId"),
`${resourceType}/custom-fields`,
"GET",
{ status: 400, message: "Missing required parameter: companyId" }
);
}
try {
const result = await toolConfig.handler(companyId, customFieldNames);
if ('formatResult' in toolConfig && typeof toolConfig.formatResult === 'function') {
const formattedResult = toolConfig.formatResult(result);
return {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`${resourceType}/custom-fields/${companyId}`,
"GET",
(error as any).response?.data || {}
);
}
}
// Handle discover attributes tool
if (toolType === 'discoverAttributes') {
try {
const result = await toolConfig.handler();
if ('formatResult' in toolConfig && typeof toolConfig.formatResult === 'function') {
const formattedResult = toolConfig.formatResult(result);
return {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`${resourceType}/discover-attributes`,
"GET",
(error as any).response?.data || {}
);
}
}
// Handle attributes tools
if (toolType === 'attributes') {
let companyId = request.params.arguments?.companyId as string;
const attributeName = request.params.arguments?.attributeName as string;
if (!companyId) {
return createErrorResult(
new Error("Missing required parameter: companyId"),
`${resourceType}/attributes`,
"GET",
{ status: 400, message: "Missing required parameter: companyId" }
);
}
try {
const result = await toolConfig.handler(companyId, attributeName);
// If a formatResult function exists, use it
if ('formatResult' in toolConfig && typeof toolConfig.formatResult === 'function') {
const formattedResult = toolConfig.formatResult(result);
return {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
}
// Otherwise, fall back to JSON stringification
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`${resourceType}/attributes/${companyId}`,
"GET",
(error as any).response?.data || {}
);
}
}
// Handle notes tools
if (toolType === 'notes') {
let id: string;
let uri: string;
// Check which parameter is provided
const directId = resourceType === ResourceType.COMPANIES
? request.params.arguments?.companyId as string
: request.params.arguments?.personId as string;
uri = request.params.arguments?.uri as string;
const limit = request.params.arguments?.limit as number || 10;
const offset = request.params.arguments?.offset as number || 0;
// Use either direct ID or URI
if (uri) {
try {
id = uri; // Pass the full URI to the handler which now handles URI parsing
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Invalid URI format"),
uri,
"GET",
{ status: 400, message: "Invalid URI format" }
);
}
} else if (directId) {
id = directId;
// For logging purposes
uri = `attio://${resourceType}/${directId}`;
} else {
return createErrorResult(
new Error("Missing required parameter: uri or direct ID"),
`${resourceType}/notes`,
"GET",
{ status: 400, message: "Missing required parameter: uri or companyId/personId" }
);
}
try {
if (process.env.NODE_ENV === 'development') {
console.error(`[tools.notes] Calling notes handler for ${resourceType} with: `, {
id,
limit,
offset
});
}
const notes = await toolConfig.handler(id, limit, offset);
return {
content: [
{
type: "text",
text: `Found ${notes.length} notes for ${resourceType.slice(0, -1)} ${id.includes('attio://') ? id.split('/').pop() : id}:\n${notes.length > 0 ? notes.map((note: any) => JSON.stringify(note)).join("----------\n") : "No notes found."}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
uri,
"GET",
(error as any).response?.data || {}
);
}
}
// Handle create note tools
if (toolType === 'createNote') {
const createNoteConfig = toolConfig as CreateNoteToolConfig;
const idParam = createNoteConfig.idParam;
if (!idParam) {
const configError = new Error('Missing idParam in tool configuration');
return createErrorResult(configError, 'tool-config', 'GET', { status: 400 });
}
let id: string;
let uri = request.params.arguments?.uri as string;
const directId = request.params.arguments?.[idParam] as string;
const noteTitle = request.params.arguments?.title || request.params.arguments?.noteTitle as string || 'Note';
const noteText = request.params.arguments?.content || request.params.arguments?.noteText as string;
if (!noteText) {
return createErrorResult(
new Error("Missing required parameter: content or noteText"),
"notes/create",
"POST",
{ status: 400, message: "Missing required parameter: content" }
);
}
// Use either direct ID or URI
if (uri) {
try {
id = uri; // Pass the full URI to the handler which now handles URI parsing
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Invalid URI format"),
uri,
"POST",
{ status: 400, message: "Invalid URI format" }
);
}
} else if (directId) {
id = directId;
// For logging purposes
uri = `attio://${resourceType}/${directId}`;
} else {
return createErrorResult(
new Error(`Missing required parameter: uri or ${idParam}`),
"notes/create",
"POST",
{ status: 400, message: `Missing required parameter: uri or ${idParam}` }
);
}
try {
if (process.env.NODE_ENV === 'development') {
console.error(`[tools.createNote] Creating note for ${resourceType} with: `, {
id,
title: noteTitle,
textLength: typeof noteText === 'string' ? noteText.length : 0
});
}
const response = await toolConfig.handler(id, noteTitle, noteText);
return {
content: [
{
type: "text",
text: `Note added to ${resourceType.slice(0, -1)} ${id.includes('attio://') ? id.split('/').pop() : id}: attio://notes/${response?.id?.note_id || 'unknown'}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
"/notes",
"POST",
(error as any).response?.data || {}
);
}
}
// Lists API tools
// Handle getLists tool
if (toolType === 'getLists') {
const objectSlug = request.params.arguments?.objectSlug as string;
const limit = request.params.arguments?.limit as number || 20;
try {
const lists = await toolConfig.handler(objectSlug, limit);
const getListsToolConfig = toolConfig as GetListsToolConfig;
const formattedResults = getListsToolConfig.formatResult
? getListsToolConfig.formatResult(lists)
: JSON.stringify(lists, null, 2);
return {
content: [
{
type: "text",
text: `Found ${lists.length} lists:\n${formattedResults}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
"/lists",
"GET",
(error as any).response?.data || {}
);
}
}
// Handle getListDetails tool
if (toolType === 'getListDetails') {
const listId = request.params.arguments?.listId as string;
try {
const list = await toolConfig.handler(listId);
return {
content: [
{
type: "text",
text: `List details for ${listId}:\n${JSON.stringify(list, null, 2)}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`/lists/${listId}`,
"GET",
(error as any).response?.data || {}
);
}
}
// Handle getListEntries tool
if (toolType === 'getListEntries') {
const listId = request.params.arguments?.listId as string;
// Ensure parameters are properly typed to match the handler expectations
// Convert parameters to the correct type and handle undefined values properly
let limit: number | undefined;
let offset: number | undefined;
// Only set the parameter values if they are explicitly provided in the request
if (request.params.arguments?.limit !== undefined && request.params.arguments?.limit !== null) {
limit = Number(request.params.arguments.limit);
}
if (request.params.arguments?.offset !== undefined && request.params.arguments?.offset !== null) {
offset = Number(request.params.arguments.offset);
}
if (process.env.NODE_ENV === 'development') {
console.error('[getListEntries Tool] Processing request with parameters:', {
listId,
limit,
offset,
request_limit_type: typeof request.params.arguments?.limit,
request_limit_value: request.params.arguments?.limit,
calculated_limit_type: typeof limit,
calculated_limit_value: limit
});
}
try {
// Pass parameters directly to the handler, letting it handle defaults
const entries = await toolConfig.handler(listId, limit, offset);
const getListEntriesToolConfig = toolConfig as GetListEntriesToolConfig;
// Use shared utility function to process entries and ensure record_id is available
const processedEntries = entries ? processListEntries(entries) : [];
const formattedResults = getListEntriesToolConfig.formatResult
? getListEntriesToolConfig.formatResult(processedEntries)
: JSON.stringify(processedEntries, null, 2);
return {
content: [
{
type: "text",
text: `Found ${processedEntries.length} entries in list ${listId}:\n${formattedResults}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`/lists/${listId}/entries`,
"GET",
(error as any).response?.data || {}
);
}
}
// Handle filterListEntries tool
if (toolType === 'filterListEntries') {
const listId = request.params.arguments?.listId as string;
const attributeSlug = request.params.arguments?.attributeSlug as string;
const condition = request.params.arguments?.condition as string;
const value = request.params.arguments?.value;
// Convert parameters to the correct type
let limit: number | undefined;
let offset: number | undefined;
if (request.params.arguments?.limit !== undefined && request.params.arguments?.limit !== null) {
limit = Number(request.params.arguments.limit);
}
if (request.params.arguments?.offset !== undefined && request.params.arguments?.offset !== null) {
offset = Number(request.params.arguments.offset);
}
if (process.env.NODE_ENV === 'development') {
console.error('[filterListEntries Tool] Processing request with parameters:', {
listId,
attributeSlug,
condition,
value,
limit,
offset
});
}
try {
const entries = await toolConfig.handler(listId, attributeSlug, condition, value, limit, offset);
const processedEntries = entries ? processListEntries(entries) : [];
const formattedResults = toolConfig.formatResult
? toolConfig.formatResult(processedEntries)
: JSON.stringify(processedEntries, null, 2);
return {
content: [
{
type: "text",
text: formattedResults,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`/lists/${listId}/entries/query`,
"POST",
(error as any).response?.data || {}
);
}
}
// Handle advancedFilterListEntries tool
if (toolType === 'advancedFilterListEntries') {
const listId = request.params.arguments?.listId as string;
const filters = request.params.arguments?.filters as any;
// Convert parameters to the correct type
let limit: number | undefined;
let offset: number | undefined;
if (request.params.arguments?.limit !== undefined && request.params.arguments?.limit !== null) {
limit = Number(request.params.arguments.limit);
}
if (request.params.arguments?.offset !== undefined && request.params.arguments?.offset !== null) {
offset = Number(request.params.arguments.offset);
}
if (process.env.NODE_ENV === 'development') {
console.error('[advancedFilterListEntries] Processing request with parameters:', {
listId,
filters: JSON.stringify(filters),
limit,
offset
});
}
try {
const entries = await toolConfig.handler(listId, filters, limit, offset);
const processedEntries = entries ? processListEntries(entries) : [];
const formattedResults = toolConfig.formatResult
? toolConfig.formatResult(processedEntries)
: JSON.stringify(processedEntries, null, 2);
return {
content: [
{
type: "text",
text: formattedResults,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`/lists/${listId}/entries/query`,
"POST",
(error as any).response?.data || {}
);
}
}
// Handle addRecordToList tool
if (toolType === 'addRecordToList') {
const listId = request.params.arguments?.listId as string;
const recordId = request.params.arguments?.recordId as string;
try {
const entry = await toolConfig.handler(listId, recordId);
return {
content: [
{
type: "text",
text: `Record ${recordId} added to list ${listId}. Entry ID: ${typeof entry.id === 'object' ? entry.id.entry_id : entry.id}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`/lists/${listId}/entries`,
"POST",
(error as any).response?.data || {}
);
}
}
// Handle removeRecordFromList tool
if (toolType === 'removeRecordFromList') {
const listId = request.params.arguments?.listId as string;
const entryId = request.params.arguments?.entryId as string;
try {
const success = await toolConfig.handler(listId, entryId);
return {
content: [
{
type: "text",
text: success
? `Successfully removed entry ${entryId} from list ${listId}`
: `Failed to remove entry ${entryId} from list ${listId}`,
},
],
isError: !success,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`/lists/${listId}/entries/${entryId}`,
"DELETE",
(error as any).response?.data || {}
);
}
}
// Handle record creation
if (toolType === 'create') {
// Resource-specific create tools like create-company don't use objectSlug
// They have a predefined resource type built into their implementation
if (RESOURCE_SPECIFIC_CREATE_TOOLS.includes(toolName as ResourceSpecificCreateTool)) {
const attributes = request.params.arguments?.attributes || {};
// Validate required attributes for specific tools
const validationError = VALIDATION_RULES[toolName as ResourceSpecificCreateTool]?.(attributes);
if (validationError) {
throw new Error(validationError);
}
try {
const result = await toolConfig.handler(attributes);
const formattedResult = toolConfig.formatResult ? toolConfig.formatResult(result) : JSON.stringify(result, null, 2);
return {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
} catch (error) {
const errorResource = RESOURCE_TYPE_MAP[toolName as ResourceSpecificCreateTool];
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`objects/${errorResource}/records`,
"POST",
(error as any).response?.data || {}
);
}
} else {
// Generic record creation that requires objectSlug
const objectSlug = request.params.arguments?.objectSlug as string;
const objectId = request.params.arguments?.objectId as string;
const attributes = request.params.arguments?.attributes || {};
try {
const recordCreateConfig = toolConfig as RecordCreateToolConfig;
const record = await recordCreateConfig.handler(objectSlug, attributes, objectId);
return {
content: [
{
type: "text",
text: `Record created successfully in ${objectSlug}:\nID: ${record.id?.record_id || 'unknown'}\n${JSON.stringify(record, null, 2)}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`objects/${objectSlug}/records`,
"POST",
(error as any).response?.data || {}
);
}
}
}
// Handle record retrieval
if (toolType === 'get') {
const objectSlug = request.params.arguments?.objectSlug as string;
const objectId = request.params.arguments?.objectId as string;
const recordId = request.params.arguments?.recordId as string;
const attributes = request.params.arguments?.attributes as string[];
try {
const recordGetConfig = toolConfig as RecordGetToolConfig;
const record = await recordGetConfig.handler(objectSlug, recordId, attributes, objectId);
return {
content: [
{
type: "text",
text: `Record details for ${objectSlug}/${recordId}:\n${JSON.stringify(record, null, 2)}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`objects/${objectSlug}/records/${recordId}`,
"GET",
(error as any).response?.data || {}
);
}
}
// Handle record update
if (toolType === 'update') {
// For company resources, handle the specific company parameters
if (resourceType === ResourceType.COMPANIES) {
const companyId = request.params.arguments?.companyId as string;
const attributes = request.params.arguments?.attributes || {};
try {
const updateConfig = toolConfig as ToolConfig;
const record = await updateConfig.handler(companyId, attributes);
// Check for formatResult function
if ('formatResult' in updateConfig && typeof updateConfig.formatResult === 'function') {
const formattedResult = updateConfig.formatResult(record);
return {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
}
return {
content: [
{
type: "text",
text: `Company updated successfully: ${record.values?.name?.[0]?.value || 'Unknown'} (ID: ${record.id?.record_id})`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`/objects/companies/records/${companyId}`,
"PATCH",
(error as any).response?.data || {}
);
}
} else {
// Default record update for other resource types
const objectSlug = request.params.arguments?.objectSlug as string;
const objectId = request.params.arguments?.objectId as string;
const recordId = request.params.arguments?.recordId as string;
const attributes = request.params.arguments?.attributes || {};
try {
const recordUpdateConfig = toolConfig as RecordUpdateToolConfig;
const record = await recordUpdateConfig.handler(objectSlug, recordId, attributes, objectId);
return {
content: [
{
type: "text",
text: `Record updated successfully in ${objectSlug}:\nID: ${record.id?.record_id || 'unknown'}\n${JSON.stringify(record, null, 2)}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`objects/${objectSlug}/records/${recordId}`,
"PATCH",
(error as any).response?.data || {}
);
}
}
}
// Handle company attribute update
if (toolType === 'updateAttribute') {
if (resourceType === ResourceType.COMPANIES) {
const companyId = request.params.arguments?.companyId as string;
const attributeName = request.params.arguments?.attributeName as string;
const attributeValue = request.params.arguments?.attributeValue;
try {
const updateAttributeConfig = toolConfig as ToolConfig;
const record = await updateAttributeConfig.handler(companyId, attributeName, attributeValue);
// Check for formatResult function
if ('formatResult' in updateAttributeConfig && typeof updateAttributeConfig.formatResult === 'function') {
const formattedResult = updateAttributeConfig.formatResult(record);
return {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
}
return {
content: [
{
type: "text",
text: `Company attribute updated: ${attributeName} for ${record.values?.name?.[0]?.value || 'Unknown'} (ID: ${record.id?.record_id})`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`/objects/companies/records/${companyId}`,
"PATCH",
(error as any).response?.data || {}
);
}
}
}
// Handle record deletion
if (toolType === 'delete') {
const objectSlug = request.params.arguments?.objectSlug as string;
const objectId = request.params.arguments?.objectId as string;
const recordId = request.params.arguments?.recordId as string;
try {
const recordDeleteConfig = toolConfig as RecordDeleteToolConfig;
const success = await recordDeleteConfig.handler(objectSlug, recordId, objectId);
return {
content: [
{
type: "text",
text: success ?
`Record ${recordId} deleted successfully from ${objectSlug}` :
`Failed to delete record ${recordId} from ${objectSlug}`,
},
],
isError: !success,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`objects/${objectSlug}/records/${recordId}`,
"DELETE",
(error as any).response?.data || {}
);
}
}
// Handle record listing
if (toolType === 'list') {
const objectSlug = request.params.arguments?.objectSlug as string;
const objectId = request.params.arguments?.objectId as string;
const options = { ...request.params.arguments };
// Remove non-option properties
delete options.objectSlug;
delete options.objectId;
try {
const recordListConfig = toolConfig as RecordListToolConfig;
const records = await recordListConfig.handler(objectSlug, options, objectId);
return {
content: [
{
type: "text",
text: `Found ${records.length} records in ${objectSlug}:\n${records.map((record: any) =>
`- ${record.values?.name?.[0]?.value || '[Unnamed]'} (ID: ${record.id?.record_id || 'unknown'})`).join('\n')}`,
},
],
isError: false,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`objects/${objectSlug}/records`,
"GET",
(error as any).response?.data || {}
);
}
}
// Handle batch record creation
if (toolType === 'batchCreate') {
const objectSlug = request.params.arguments?.objectSlug as string;
const objectId = request.params.arguments?.objectId as string;
const records = Array.isArray(request.params.arguments?.records) ? request.params.arguments?.records : [];
try {
const recordBatchCreateConfig = toolConfig as RecordBatchCreateToolConfig;
const result = await recordBatchCreateConfig.handler(objectSlug, records, objectId);
return {
content: [
{
type: "text",
text: `Batch create operation completed for ${objectSlug}:\n` +
`Total: ${result.summary.total}, Succeeded: ${result.summary.succeeded}, Failed: ${result.summary.failed}\n` +
`${result.results.map((r: any, i: number) =>
r.success
? `✅ Record ${i+1}: Created successfully (ID: ${r.data?.id?.record_id || 'unknown'})`
: `❌ Record ${i+1}: Failed - ${r.error?.message || 'Unknown error'}`
).join('\n')}`,
},
],
isError: result.summary.failed > 0,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`objects/${objectSlug}/records/batch`,
"POST",
(error as any).response?.data || {}
);
}
}
// Handle advancedSearch tools (for both people and companies)
if (toolType === 'searchByCreationDate' ||
toolType === 'searchByModificationDate' ||
toolType === 'searchByLastInteraction' ||
toolType === 'searchByActivity') {
try {
const advancedSearchConfig = toolConfig as AdvancedSearchToolConfig | DateBasedSearchToolConfig;
let results: AttioRecord[] = [];
// Parse or extract parameters based on tool type
if (toolType === 'searchByCreationDate' || toolType === 'searchByModificationDate') {
// Handle date range parameter
let dateRange = request.params.arguments?.dateRange;
const limit = Number(request.params.arguments?.limit) || 20;
const offset = Number(request.params.arguments?.offset) || 0;
// If dateRange is a string, try to parse it as JSON
if (typeof dateRange === 'string') {
try {
dateRange = JSON.parse(dateRange);
} catch (error) {
console.warn(`Failed to parse dateRange parameter: ${error instanceof Error ? error.message : 'Unknown error'}`);
// Continue with the string, the handler will need to handle it
}
}
// Create a properly typed filter object if it's not already one
let filter: ListEntryFilters = { filters: [] };
if (typeof dateRange === 'object' && dateRange !== null) {
filter = dateRange as ListEntryFilters;
} else {
filter = { filters: [{ attribute: { slug: 'created_at' }, condition: 'equals', value: dateRange }] };
}
const response = await advancedSearchConfig.handler(filter, limit, offset);
results = response || [];
}
else if (toolType === 'searchByLastInteraction') {
// Handle date range and interaction type parameters
let dateRange = request.params.arguments?.dateRange;
const interactionType = request.params.arguments?.interactionType;
const limit = Number(request.params.arguments?.limit) || 20;
const offset = Number(request.params.arguments?.offset) || 0;
// If dateRange is a string, try to parse it as JSON
if (typeof dateRange === 'string') {
try {
dateRange = JSON.parse(dateRange);
} catch (error) {
console.warn(`Failed to parse dateRange parameter: ${error instanceof Error ? error.message : 'Unknown error'}`);
// Continue with the string, the handler will need to handle it
}
}
// Create a properly typed filter object
let filter: ListEntryFilters = { filters: [] };
// Construct proper filter based on date range and interaction type
filter = {
filters: [
{ attribute: { slug: 'last_interaction' }, condition: 'equals', value: dateRange },
...(interactionType ? [{ attribute: { slug: 'interaction_type' }, condition: 'equals', value: interactionType }] : [])
]
};
const response = await advancedSearchConfig.handler(filter, limit, offset);
results = response || [];
}
else if (toolType === 'searchByActivity') {
// Handle activity filter parameter
let activityFilter = request.params.arguments?.activityFilter;
const limit = Number(request.params.arguments?.limit) || 20;
const offset = Number(request.params.arguments?.offset) || 0;
// If activityFilter is a string, try to parse it as JSON
if (typeof activityFilter === 'string') {
try {
activityFilter = JSON.parse(activityFilter);
} catch (error) {
console.warn(`Failed to parse activityFilter parameter: ${error instanceof Error ? error.message : 'Unknown error'}`);
// Continue with the string, the handler will need to handle it
}
}
// Create a properly typed filter object
let filter: ListEntryFilters = { filters: [] };
if (typeof activityFilter === 'object' && activityFilter !== null) {
filter = {
filters: [
{ attribute: { slug: 'created_at' }, condition: 'equals', value: activityFilter }
]
};
} else {
filter = { filters: [{ attribute: { slug: 'created_at' }, condition: 'equals', value: activityFilter }] };
}
const response = await advancedSearchConfig.handler(filter, limit, offset);
results = response || [];
}
// Format and return results
const formattedResults = advancedSearchConfig.formatResult(results);
return {
content: [
{
type: "text",
text: formattedResults,
},
],
isError: false,
};
} catch (error) {
let errorDetailsForCreateResult: any = {};
if (error instanceof ValueMatchError) {
// If it's a ValueMatchError, its message is already enhanced.
// We want to pass the original error's response data if available for full context in createErrorResult
if (axios.isAxiosError(error.originalError) && error.originalError.response) {
errorDetailsForCreateResult = error.originalError.response.data;
} else {
// Fallback if originalError is not an Axios error or has no response data
errorDetailsForCreateResult = error.details || {};
}
} else if (axios.isAxiosError(error) && error.response) {
errorDetailsForCreateResult = error.response.data;
} else if (error instanceof Error && (error as any).details) {
errorDetailsForCreateResult = (error as any).details;
} else {
// Minimal fallback
errorDetailsForCreateResult = { message: (error as Error)?.message || 'Unknown error details' };
}
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error caught in advancedSearch"),
`/objects/${resourceType}/records/query`,
"POST",
errorDetailsForCreateResult
);
}
}
// Handle batch record updates
if (toolType === 'batchUpdate') {
const objectSlug = request.params.arguments?.objectSlug as string;
const objectId = request.params.arguments?.objectId as string;
const records = Array.isArray(request.params.arguments?.records) ? request.params.arguments?.records : [];
try {
const recordBatchUpdateConfig = toolConfig as RecordBatchUpdateToolConfig;
const result = await recordBatchUpdateConfig.handler(objectSlug, records, objectId);
return {
content: [
{
type: "text",
text: `Batch update operation completed for ${objectSlug}:\n` +
`Total: ${result.summary.total}, Succeeded: ${result.summary.succeeded}, Failed: ${result.summary.failed}\n` +
`${result.results.map((r: any) =>
r.success
? `✅ Record ${r.id}: Updated successfully`
: `❌ Record ${r.id}: Failed - ${r.error?.message || 'Unknown error'}`
).join('\n')}`,
},
],
isError: result.summary.failed > 0,
};
} catch (error) {
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error"),
`objects/${objectSlug}/records/batch`,
"PATCH",
(error as any).response?.data || {}
);
}
}
// Handle advanced search tools
if (toolType === 'advancedSearch') {
const filters = request.params.arguments?.filters as any;
// Convert parameters to the correct type
let limit: number | undefined;
let offset: number | undefined;
if (request.params.arguments?.limit !== undefined && request.params.arguments?.limit !== null) {
limit = Number(request.params.arguments.limit);
}
if (request.params.arguments?.offset !== undefined && request.params.arguments?.offset !== null) {
offset = Number(request.params.arguments.offset);
}
// Import the attribute mapping utility
const { translateAttributeNamesInFilters } = await import("../utils/attribute-mapping/index.js");
// Translate any human-readable attribute names to their slug equivalents
// Pass resourceType for object-specific mappings
const translatedFilters = translateAttributeNamesInFilters(filters, resourceType);
if (process.env.NODE_ENV === 'development') {
console.error(`[advancedSearch ${resourceType}] Processing request with parameters:`, {
originalFilters: JSON.stringify(filters),
translatedFilters: JSON.stringify(translatedFilters),
limit,
offset
});
}
try {
const advancedSearchToolConfig = toolConfig as AdvancedSearchToolConfig | DateBasedSearchToolConfig;
const results = await advancedSearchToolConfig.handler(translatedFilters, limit, offset);
const formattedResults = advancedSearchToolConfig.formatResult(results);
return {
content: [
{
type: "text",
text: formattedResults,
},
],
isError: false,
};
} catch (error) {
let errorDetailsForCreateResult: any = {};
if (error instanceof ValueMatchError) {
// If it's a ValueMatchError, its message is already enhanced.
// We want to pass the original error's response data if available for full context in createErrorResult
if (axios.isAxiosError(error.originalError) && error.originalError.response) {
errorDetailsForCreateResult = error.originalError.response.data;
} else {
// Fallback if originalError is not an Axios error or has no response data
errorDetailsForCreateResult = error.details || {};
}
} else if (axios.isAxiosError(error) && error.response) {
errorDetailsForCreateResult = error.response.data;
} else if (error instanceof Error && (error as any).details) {
errorDetailsForCreateResult = (error as any).details;
} else {
// Minimal fallback
errorDetailsForCreateResult = { message: (error as Error)?.message || 'Unknown error details' };
}
return createErrorResult(
error instanceof Error ? error : new Error("Unknown error caught in advancedSearch"),
`/objects/${resourceType}/records/query`,
"POST",
errorDetailsForCreateResult
);
}
}
throw new Error(`Tool handler not implemented for tool type: ${toolType}`);
} catch (error) {
return {
content: [
{
type: "text",
text: `Error executing tool '${toolName}': ${(error as Error).message}`,
},
],
isError: true,
};
}
});
}