/**
* Tool: manage_person_emails
* Unified tool to manage email addresses on people (set, add, remove, clear)
*/
import type { Tool, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { createAttioClient } from '../attio-client.js';
import {
handleToolError,
createSuccessResponse,
} from '../utils/error-handler.js';
import { ConfigurationError, ValidationError } from '../utils/errors.js';
interface AttioPersonRecord {
id: {
workspace_id: string;
object_id: string;
record_id: string;
};
values: {
email_addresses?: Array<{ email_address: string }>;
[key: string]: unknown;
};
}
interface AttioPersonResponse {
data: AttioPersonRecord;
}
/**
* Tool definition for MCP
*/
export const managePersonEmailsTool: Tool = {
name: 'manage_person_emails',
description:
'Manage email addresses on a person. Supports four operations: "set" (replace all emails), "add" (append new emails), "remove" (delete specific emails), and "clear" (remove all emails).',
inputSchema: {
type: 'object',
properties: {
record_id: {
type: 'string',
description:
'The unique record ID of the person (e.g., from search_people or get_person)',
},
operation: {
type: 'string',
description:
'Operation to perform: "set" (replace all emails), "add" (append new emails), "remove" (delete specific emails), "clear" (remove all emails)',
enum: ['set', 'add', 'remove', 'clear'],
},
email_addresses: {
type: 'array',
items: {
type: 'string',
},
description:
'Array of email addresses to set, add, or remove. Not required for "clear" operation.',
},
},
required: ['record_id', 'operation'],
},
};
/**
* Handler function for manage_person_emails tool
*/
export async function handleManagePersonEmails(args: {
record_id: string;
operation: string;
email_addresses?: string[];
}): Promise<CallToolResult> {
try {
const apiKey = process.env['ATTIO_API_KEY'];
if (!apiKey) {
throw new ConfigurationError('ATTIO_API_KEY not configured');
}
const { record_id, operation, email_addresses } = args;
// Validate required parameters
if (!record_id || record_id.trim().length === 0) {
throw new ValidationError(
'record_id parameter is required and cannot be empty',
'record_id'
);
}
if (!operation || operation.trim().length === 0) {
throw new ValidationError(
'operation parameter is required and cannot be empty',
'operation'
);
}
// Validate operation
const validOperations = ['set', 'add', 'remove', 'clear'];
if (!validOperations.includes(operation)) {
throw new ValidationError(
`operation must be one of: ${validOperations.join(', ')}`,
'operation'
);
}
// Validate email_addresses parameter based on operation
if (operation !== 'clear') {
if (!email_addresses || email_addresses.length === 0) {
throw new ValidationError(
`email_addresses parameter is required for "${operation}" operation`,
'email_addresses'
);
}
}
const client = createAttioClient(apiKey);
// Handle clear operation (set emails to empty array)
if (operation === 'clear') {
// First get current emails for the response
const getResponse = await client.get<AttioPersonResponse>(
`/objects/people/records/${record_id}`
);
const previousEmails =
getResponse.data.values.email_addresses
?.map((e) => e.email_address)
.filter(Boolean) || [];
await client.put<AttioPersonResponse>(
`/objects/people/records/${record_id}`,
{
data: {
values: {
email_addresses: [],
},
},
}
);
return createSuccessResponse({
record_id,
operation: 'clear',
previous_emails: previousEmails,
current_emails: [],
message: 'All email addresses cleared successfully',
});
}
// For all other operations, we need the email_addresses parameter
const validEmails = (email_addresses || [])
.filter((e) => e && e.trim().length > 0)
.map((e) => e.trim().toLowerCase());
if (validEmails.length === 0) {
throw new ValidationError(
'At least one valid email address is required',
'email_addresses'
);
}
// Handle set operation (replace all emails)
if (operation === 'set') {
// First get current emails for the response
const getResponse = await client.get<AttioPersonResponse>(
`/objects/people/records/${record_id}`
);
const previousEmails =
getResponse.data.values.email_addresses
?.map((e) => e.email_address)
.filter(Boolean) || [];
const response = await client.put<AttioPersonResponse>(
`/objects/people/records/${record_id}`,
{
data: {
values: {
email_addresses: validEmails.map((email) => ({
email_address: email,
})),
},
},
}
);
const currentEmails =
response.data.values.email_addresses
?.map((e) => e.email_address)
.filter(Boolean) || [];
return createSuccessResponse({
record_id,
operation: 'set',
emails_set: validEmails,
previous_emails: previousEmails,
current_emails: currentEmails,
message: `Email addresses set to: ${validEmails.join(', ')}`,
});
}
// For add and remove operations, we need to get current emails first
const getResponse = await client.get<AttioPersonResponse>(
`/objects/people/records/${record_id}`
);
const currentEmails =
getResponse.data.values.email_addresses
?.map((e) => e.email_address)
.filter(Boolean) || [];
// Handle add operation (append to existing emails)
if (operation === 'add') {
// Filter out emails that already exist (case-insensitive)
const currentEmailsLower = currentEmails.map((e) => e.toLowerCase());
const emailsToAdd = validEmails.filter(
(email) => !currentEmailsLower.includes(email.toLowerCase())
);
if (emailsToAdd.length === 0) {
return createSuccessResponse({
record_id,
operation: 'add',
emails_requested: validEmails,
emails_added: [],
current_emails: currentEmails,
message: 'All requested email addresses already exist',
});
}
// PATCH to append new emails
const response = await client.patch<AttioPersonResponse>(
`/objects/people/records/${record_id}`,
{
data: {
values: {
email_addresses: emailsToAdd.map((email) => ({
email_address: email,
})),
},
},
}
);
const updatedEmails =
response.data.values.email_addresses
?.map((e) => e.email_address)
.filter(Boolean) || [];
return createSuccessResponse({
record_id,
operation: 'add',
emails_requested: validEmails,
emails_added: emailsToAdd,
previous_emails: currentEmails,
current_emails: updatedEmails,
message: `Added ${emailsToAdd.length} email(s): ${emailsToAdd.join(', ')}`,
});
}
// Handle remove operation (delete specific emails)
if (operation === 'remove') {
// Filter to emails that actually exist (case-insensitive)
const currentEmailsLower = currentEmails.map((e) => e.toLowerCase());
const emailsToRemove = validEmails.filter((email) =>
currentEmailsLower.includes(email.toLowerCase())
);
if (emailsToRemove.length === 0) {
return createSuccessResponse({
record_id,
operation: 'remove',
emails_requested: validEmails,
emails_removed: [],
current_emails: currentEmails,
message: 'None of the requested email addresses exist',
});
}
// Calculate remaining emails (case-insensitive comparison)
const emailsToRemoveLower = emailsToRemove.map((e) => e.toLowerCase());
const remainingEmails = currentEmails.filter(
(email) => !emailsToRemoveLower.includes(email.toLowerCase())
);
// PUT to replace with remaining emails
const response = await client.put<AttioPersonResponse>(
`/objects/people/records/${record_id}`,
{
data: {
values: {
email_addresses: remainingEmails.map((email) => ({
email_address: email,
})),
},
},
}
);
const updatedEmails =
response.data.values.email_addresses
?.map((e) => e.email_address)
.filter(Boolean) || [];
return createSuccessResponse({
record_id,
operation: 'remove',
emails_requested: validEmails,
emails_removed: emailsToRemove,
previous_emails: currentEmails,
current_emails: updatedEmails,
message: `Removed ${emailsToRemove.length} email(s): ${emailsToRemove.join(', ')}`,
});
}
// Should never reach here
throw new ValidationError('Invalid operation', 'operation');
} catch (error) {
return handleToolError(error, 'manage_person_emails');
}
}