Skip to main content
Glama
core.tsβ€’16.3 kB
/** * Core dispatcher module - main tool execution dispatcher with modular operation handlers */ import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; import { ResourceType } from '@/types/attio.js'; // Import utilities import { initializeToolContext, logToolRequest, logToolSuccess, logToolError, logToolConfigError, } from '@/handlers/tools/dispatcher/logging.js'; // Import tool configurations import { findToolConfig } from '@/handlers/tools/registry.js'; // Type for format result function that may accept additional parameters type FormatResultFunction = | ((results: unknown) => string) | ((results: unknown, resourceType?: unknown, infoType?: unknown) => string); import { PerformanceTimer, OperationType, getLogContext, } from '@/utils/logger.js'; import { sanitizeMcpResponse } from '@/utils/json-serializer.js'; import { createSecureToolErrorResult } from '@/utils/secure-error-handler.js'; // Import operation handlers import { handleBasicSearch, handleSearchByEmail, handleSearchByPhone, handleSearchByDomain, handleSmartSearch, } from '@/handlers/tools/dispatcher/operations/search.js'; import { handleAdvancedSearch } from '@/handlers/tools/dispatcher/operations/advanced-search.js'; import { handleDetailsOperation } from '@/handlers/tools/dispatcher/operations/details.js'; import { handleNotesOperation, handleCreateNoteOperation, } from '@/handlers/tools/dispatcher/operations/notes.js'; import { handleGetListsOperation } from '@/handlers/tools/dispatcher/operations/lists.js'; // Import CRUD operation handlers import { handleCreateOperation, handleUpdateOperation, handleUpdateAttributeOperation, handleDeleteOperation, } from '@/handlers/tools/dispatcher/operations/crud.js'; // Import List operation handlers (additional operations from emergency fix) import { handleAddRecordToListOperation, handleRemoveRecordFromListOperation, handleUpdateListEntryOperation, handleGetListDetailsOperation, handleGetListEntriesOperation, handleFilterListEntriesOperation, handleAdvancedFilterListEntriesOperation, handleFilterListEntriesByParentOperation, handleFilterListEntriesByParentIdOperation, handleGetRecordListMembershipsOperation, } from '@/handlers/tools/dispatcher/operations/lists.js'; // Import Batch operation handlers import { handleBatchUpdateOperation, handleBatchCreateOperation, handleBatchDeleteOperation, handleBatchSearchOperation, handleBatchGetDetailsOperation, } from '@/handlers/tools/dispatcher/operations/batch.js'; // Import Record operation handlers import { handleListOperation, handleGetOperation, } from '@/handlers/tools/dispatcher/operations/records.js'; import { handleInfoOperation, handleFieldsOperation, handleGetAttributesOperation, handleDiscoverAttributesOperation, } from '@/handlers/tools/dispatcher/misc-operations.js'; // Import tool type definitions import { ToolConfig, SearchToolConfig, AdvancedSearchToolConfig, DetailsToolConfig, NotesToolConfig, CreateNoteToolConfig, GetListsToolConfig, } from '@/handlers/tool-types.js'; /** * Normalize error messages by stripping tool execution prefixes * This improves test compatibility and error message clarity */ import { canonicalizeResourceType } from '@/handlers/tools/dispatcher/utils.js'; /** * Execute a tool request and return formatted results * * @param request - The tool request to execute * @returns Tool execution result */ export async function executeToolRequest(request: CallToolRequest) { const toolName = request.params.name; // Initialize logging context for this tool execution initializeToolContext(toolName); let timer: PerformanceTimer | undefined; let toolType: string | undefined; // Note: Argument normalization is handled in the request handler (Issue #344) // This dispatcher expects normalized requests with proper arguments structure try { const toolInfo = findToolConfig(toolName); if (!toolInfo) { logToolConfigError(toolName, 'Tool configuration not found'); throw new Error(`Tool not found: ${toolName}`); } const { resourceType, toolConfig } = toolInfo; toolType = toolInfo.toolType; // Assign to outer scope variable // Start tool execution logging with performance tracking timer = logToolRequest(toolType, toolName, request); let result; // Handle Universal and General tools first (Issue #352) if (resourceType === 'UNIVERSAL') { // For universal tools, use the tool's own handler directly const args = request.params.arguments as Record<string, unknown>; // Canonicalize and freeze resource_type to prevent mutation if (args && 'resource_type' in args) { args.resource_type = canonicalizeResourceType(args.resource_type); Object.defineProperty(args, 'resource_type', { value: args.resource_type, writable: false, }); } // Universal tools have their own parameter validation and handling const rawResult = await (toolConfig as ToolConfig).handler( args as Record<string, unknown> ); // If a tool already returned an MCP-shaped object, stop double-wrapping const isMcpResponseLike = ( value: unknown ): value is { content: unknown; isError: boolean; } => typeof value === 'object' && value !== null && 'content' in value && 'isError' in value; if (isMcpResponseLike(rawResult)) { const sanitized = sanitizeMcpResponse(rawResult); logToolSuccess(toolName, toolType, sanitized, timer); return sanitized; // skip detection/formatting, it's already MCP } // Format the result using the tool's formatResult if available let formattedResult: string; if (rawResult === null || rawResult === undefined) { formattedResult = JSON.stringify(rawResult, null, 2); } else if (toolConfig.formatResult) { try { formattedResult = (toolConfig.formatResult as FormatResultFunction)( rawResult, args?.resource_type, args?.info_type ); } catch { formattedResult = (toolConfig.formatResult as FormatResultFunction)( rawResult ); } } else { formattedResult = JSON.stringify(rawResult, null, 2); } result = { content: [{ type: 'text', text: formattedResult }], isError: false, }; } else if (resourceType === 'GENERAL') { // For general tools, use the tool's own handler directly const args = request.params.arguments as Record<string, unknown>; let handlerArgs: unknown[] = []; // Map arguments based on tool type if ( toolType === 'linkPersonToCompany' || toolType === 'unlinkPersonFromCompany' ) { handlerArgs = [args?.personId, args?.companyId]; } else if (toolType === 'getPersonCompanies') { handlerArgs = [args?.personId]; } else if (toolType === 'getCompanyTeam') { handlerArgs = [args?.companyId]; } else { handlerArgs = [args]; } const rawResult = await (toolConfig as ToolConfig).handler( ...handlerArgs ); const formattedResult = toolConfig.formatResult?.(rawResult) || JSON.stringify(rawResult, null, 2); result = { content: [{ type: 'text', text: formattedResult }], isError: false, }; } else if (toolType === 'search') { result = await handleBasicSearch( request, toolConfig as SearchToolConfig, resourceType as ResourceType ); } else if (toolType === 'searchByEmail') { result = await handleSearchByEmail( request, toolConfig as SearchToolConfig, resourceType ); } else if (toolType === 'searchByPhone') { result = await handleSearchByPhone( request, toolConfig as SearchToolConfig, resourceType ); } else if (toolType === 'searchByCompany') { // Use the tool config's own handler and format the result const rawResult = await (toolConfig as ToolConfig).handler( request.params.arguments as unknown as Record<string, unknown> ); const formattedResult = toolConfig.formatResult?.(rawResult) || JSON.stringify(rawResult, null, 2); result = { content: [{ type: 'text', text: formattedResult }], isError: false, }; } else if (toolType === 'smartSearch') { result = await handleSmartSearch( request, toolConfig as SearchToolConfig, resourceType ); } else if (toolType === 'details') { result = await handleDetailsOperation( request, toolConfig as DetailsToolConfig, resourceType ); } else if (toolType === 'notes') { result = await handleNotesOperation( request, toolConfig as NotesToolConfig, resourceType ); } else if (toolType === 'createNote') { result = await handleCreateNoteOperation( request, toolConfig as CreateNoteToolConfig, resourceType ); } else if (toolType === 'getLists') { result = await handleGetListsOperation( request, toolConfig as GetListsToolConfig ); // Handle CRUD operations (from emergency fix) } else if (toolType === 'create') { result = await handleCreateOperation( request, toolConfig as ToolConfig, resourceType ); } else if (toolType === 'update') { result = await handleUpdateOperation( request, toolConfig as ToolConfig, resourceType ); } else if (toolType === 'updateAttribute') { result = await handleUpdateAttributeOperation( request, toolConfig as ToolConfig, resourceType ); } else if (toolType === 'delete') { result = await handleDeleteOperation( request, toolConfig as ToolConfig, resourceType ); // Handle additional info operations (from emergency fix) } else if ( toolType === 'basicInfo' || toolType === 'businessInfo' || toolType === 'contactInfo' || toolType === 'socialInfo' || toolType === 'json' ) { result = await handleInfoOperation(request, toolConfig, resourceType); } else if (toolType === 'fields') { result = await handleFieldsOperation(request, toolConfig, resourceType); } else if (toolType === 'getAttributes') { result = await handleGetAttributesOperation( request, toolConfig, resourceType ); } else if (toolType === 'discoverAttributes') { result = await handleDiscoverAttributesOperation(request, toolConfig); } else if (toolType === 'customFields') { result = await handleInfoOperation(request, toolConfig, resourceType); // Handle List operations (from emergency fix) } else if (toolType === 'addRecordToList') { result = await handleAddRecordToListOperation(request, toolConfig); } else if (toolType === 'removeRecordFromList') { result = await handleRemoveRecordFromListOperation(request, toolConfig); } else if (toolType === 'updateListEntry') { result = await handleUpdateListEntryOperation(request, toolConfig); } else if (toolType === 'getListDetails') { result = await handleGetListDetailsOperation(request, toolConfig); } else if (toolType === 'getListEntries') { result = await handleGetListEntriesOperation(request, toolConfig); } else if (toolType === 'filterListEntries') { result = await handleFilterListEntriesOperation(request, toolConfig); } else if (toolType === 'advancedFilterListEntries') { result = await handleAdvancedFilterListEntriesOperation( request, toolConfig ); } else if (toolType === 'filterListEntriesByParent') { result = await handleFilterListEntriesByParentOperation( request, toolConfig ); } else if (toolType === 'filterListEntriesByParentId') { result = await handleFilterListEntriesByParentIdOperation( request, toolConfig ); } else if (toolType === 'getRecordListMemberships') { result = await handleGetRecordListMembershipsOperation( request, toolConfig ); // Handle Batch operations (from emergency fix) } else if (toolType === 'batchUpdate') { result = await handleBatchUpdateOperation( request, toolConfig, resourceType ); } else if (toolType === 'batchCreate') { result = await handleBatchCreateOperation( request, toolConfig, resourceType ); } else if (toolType === 'batchDelete') { result = await handleBatchDeleteOperation( request, toolConfig, resourceType ); } else if (toolType === 'batchSearch') { result = await handleBatchSearchOperation( request, toolConfig, resourceType ); } else if (toolType === 'batchGetDetails') { result = await handleBatchGetDetailsOperation( request, toolConfig, resourceType ); // Handle other advanced search operations (from emergency fix) } else if (toolType === 'advancedSearch') { result = await handleAdvancedSearch( request, toolConfig as AdvancedSearchToolConfig, resourceType ); } else if (toolType === 'searchByDomain') { result = await handleSearchByDomain( request, toolConfig as SearchToolConfig, resourceType ); // Handle workspace member operations (Issue #684) } else if ( toolType === 'listWorkspaceMembers' || toolType === 'searchWorkspaceMembers' || toolType === 'getWorkspaceMember' ) { // Workspace member tools have simple handlers with formatResult const rawResult = await (toolConfig as ToolConfig).handler( request.params.arguments as unknown as Record<string, unknown> ); const formattedResult = toolConfig.formatResult?.(rawResult) || JSON.stringify(rawResult, null, 2); result = { content: [{ type: 'text', text: formattedResult }], isError: false, }; // Handle generic record operations } else if (toolType === 'list') { result = await handleListOperation(request, toolConfig as ToolConfig); } else if (toolType === 'get') { result = await handleGetOperation(request, toolConfig as ToolConfig); } else { // Placeholder for other operations - will be extracted to modules later throw new Error( `Tool handler not implemented for tool type: ${toolType}` ); } // Log successful execution logToolSuccess(toolName, toolType, result, timer); // Ensure the response is safely serializable const sanitizedResult = sanitizeMcpResponse(result); return sanitizedResult; } catch (error: unknown) { // Get additional error details for better debugging const errorDetails = { tool: toolName, errorType: error && typeof error === 'object' ? error.constructor.name : typeof error, stack: error instanceof Error ? error.stack : undefined, additionalInfo: error && typeof error === 'object' && 'details' in error ? (error as { details: unknown }).details : undefined, }; // Log error using enhanced structured logging const finalTimer = timer ? timer : new PerformanceTimer( 'dispatcher_error_fallback', toolName, OperationType.TOOL_EXECUTION ); logToolError( toolName, toolType || 'unknown_type_on_error', error, finalTimer, errorDetails ); const { correlationId, requestId, userId } = getLogContext(); return createSecureToolErrorResult(error, { module: 'handlers.tools.dispatcher', operation: `execute:${toolName}`, resourceType: toolType, correlationId, requestId, userId, }); } }

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