// Google Contacts MCP Tools (People API)
// 10 tools for managing contacts and contact groups
import { z } from 'zod';
import type { ContactsService } from '../services/contacts.js';
// ─────────────────────────────────────────────────────────────────────────────
// TOOL DEFINITIONS
// ─────────────────────────────────────────────────────────────────────────────
export const contactsTools = [
// ─────────────────────────────────────────────────────────────────────────
// CONTACTS
// ─────────────────────────────────────────────────────────────────────────
{
name: 'contacts_listContacts',
description: 'List all contacts for the authenticated user, sorted by last modified.',
inputSchema: {
type: 'object' as const,
properties: {
pageSize: {
type: 'number',
description: 'Maximum contacts to return (default: 100, max: 1000)'
},
pageToken: {
type: 'string',
description: 'Pagination token'
}
},
required: []
}
},
{
name: 'contacts_getContact',
description: 'Get details of a specific contact by resource name.',
inputSchema: {
type: 'object' as const,
properties: {
resourceName: {
type: 'string',
description: 'Contact resource name (e.g., "people/c123456789")'
}
},
required: ['resourceName']
}
},
{
name: 'contacts_searchContacts',
description: 'Search contacts by name, email, phone, or other fields.',
inputSchema: {
type: 'object' as const,
properties: {
query: {
type: 'string',
description: 'Search query (name, email, phone, etc.)'
},
pageSize: {
type: 'number',
description: 'Maximum results (default: 30)'
}
},
required: ['query']
}
},
{
name: 'contacts_createContact',
description: 'Create a new contact.',
inputSchema: {
type: 'object' as const,
properties: {
givenName: {
type: 'string',
description: 'First name (required)'
},
familyName: {
type: 'string',
description: 'Last name'
},
middleName: {
type: 'string',
description: 'Middle name'
},
email: {
type: 'string',
description: 'Email address'
},
emailType: {
type: 'string',
description: 'Email type: work, home, other'
},
phone: {
type: 'string',
description: 'Phone number'
},
phoneType: {
type: 'string',
description: 'Phone type: mobile, work, home, other'
},
organization: {
type: 'string',
description: 'Company/organization name'
},
jobTitle: {
type: 'string',
description: 'Job title'
},
streetAddress: {
type: 'string',
description: 'Street address'
},
city: {
type: 'string',
description: 'City'
},
region: {
type: 'string',
description: 'State/region'
},
postalCode: {
type: 'string',
description: 'Postal/ZIP code'
},
country: {
type: 'string',
description: 'Country'
},
notes: {
type: 'string',
description: 'Notes about the contact'
}
},
required: ['givenName']
}
},
{
name: 'contacts_updateContact',
description: 'Update an existing contact. Only provided fields will be updated.',
inputSchema: {
type: 'object' as const,
properties: {
resourceName: {
type: 'string',
description: 'Contact resource name'
},
givenName: {
type: 'string',
description: 'First name'
},
familyName: {
type: 'string',
description: 'Last name'
},
email: {
type: 'string',
description: 'Email address'
},
phone: {
type: 'string',
description: 'Phone number'
},
organization: {
type: 'string',
description: 'Company name'
},
jobTitle: {
type: 'string',
description: 'Job title'
},
notes: {
type: 'string',
description: 'Notes'
}
},
required: ['resourceName']
}
},
{
name: 'contacts_deleteContact',
description: 'Delete a contact.',
inputSchema: {
type: 'object' as const,
properties: {
resourceName: {
type: 'string',
description: 'Contact resource name to delete'
}
},
required: ['resourceName']
}
},
// ─────────────────────────────────────────────────────────────────────────
// CONTACT GROUPS
// ─────────────────────────────────────────────────────────────────────────
{
name: 'contacts_listGroups',
description: 'List contact groups (labels) including system groups.',
inputSchema: {
type: 'object' as const,
properties: {
pageSize: {
type: 'number',
description: 'Maximum groups to return'
},
pageToken: {
type: 'string',
description: 'Pagination token'
}
},
required: []
}
},
{
name: 'contacts_createGroup',
description: 'Create a new contact group (label).',
inputSchema: {
type: 'object' as const,
properties: {
name: {
type: 'string',
description: 'Group name'
}
},
required: ['name']
}
},
{
name: 'contacts_updateGroup',
description: 'Rename a contact group.',
inputSchema: {
type: 'object' as const,
properties: {
resourceName: {
type: 'string',
description: 'Group resource name'
},
name: {
type: 'string',
description: 'New group name'
}
},
required: ['resourceName', 'name']
}
},
{
name: 'contacts_deleteGroup',
description: 'Delete a contact group.',
inputSchema: {
type: 'object' as const,
properties: {
resourceName: {
type: 'string',
description: 'Group resource name to delete'
},
deleteContacts: {
type: 'boolean',
description: 'Also delete contacts in group (default: false)'
}
},
required: ['resourceName']
}
},
];
// ─────────────────────────────────────────────────────────────────────────────
// ZOD SCHEMAS
// ─────────────────────────────────────────────────────────────────────────────
const ListContactsSchema = z.object({
pageSize: z.number().min(1).max(1000).optional(),
pageToken: z.string().optional(),
});
const GetContactSchema = z.object({
resourceName: z.string().min(1),
});
const SearchContactsSchema = z.object({
query: z.string().min(1),
pageSize: z.number().min(1).max(30).optional(),
});
const CreateContactSchema = z.object({
givenName: z.string().min(1),
familyName: z.string().optional(),
middleName: z.string().optional(),
email: z.string().email().optional(),
emailType: z.string().optional(),
phone: z.string().optional(),
phoneType: z.string().optional(),
organization: z.string().optional(),
jobTitle: z.string().optional(),
streetAddress: z.string().optional(),
city: z.string().optional(),
region: z.string().optional(),
postalCode: z.string().optional(),
country: z.string().optional(),
notes: z.string().optional(),
});
const UpdateContactSchema = z.object({
resourceName: z.string().min(1),
givenName: z.string().optional(),
familyName: z.string().optional(),
middleName: z.string().optional(),
email: z.string().email().optional(),
emailType: z.string().optional(),
phone: z.string().optional(),
phoneType: z.string().optional(),
organization: z.string().optional(),
jobTitle: z.string().optional(),
notes: z.string().optional(),
});
const DeleteContactSchema = z.object({
resourceName: z.string().min(1),
});
const ListGroupsSchema = z.object({
pageSize: z.number().min(1).max(100).optional(),
pageToken: z.string().optional(),
});
const CreateGroupSchema = z.object({
name: z.string().min(1),
});
const UpdateGroupSchema = z.object({
resourceName: z.string().min(1),
name: z.string().min(1),
});
const DeleteGroupSchema = z.object({
resourceName: z.string().min(1),
deleteContacts: z.boolean().optional(),
});
// ─────────────────────────────────────────────────────────────────────────────
// HANDLERS
// ─────────────────────────────────────────────────────────────────────────────
export function createContactsHandlers(contactsService: ContactsService) {
return {
contacts_listContacts: async (args: unknown) => {
const { pageSize, pageToken } = ListContactsSchema.parse(args);
const result = await contactsService.listContacts(pageSize, pageToken);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
contacts: result.contacts.map(c => ({
resourceName: c.resourceName,
name: c.names?.[0]?.displayName || `${c.names?.[0]?.givenName || ''} ${c.names?.[0]?.familyName || ''}`.trim(),
email: c.emailAddresses?.[0]?.value,
phone: c.phoneNumbers?.[0]?.value,
organization: c.organizations?.[0]?.name,
jobTitle: c.organizations?.[0]?.title,
})),
totalCount: result.contacts.length,
totalItems: result.totalItems,
nextPageToken: result.nextPageToken
}, null, 2)
}]
};
},
contacts_getContact: async (args: unknown) => {
const { resourceName } = GetContactSchema.parse(args);
const contact = await contactsService.getContact(resourceName);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
contact
}, null, 2)
}]
};
},
contacts_searchContacts: async (args: unknown) => {
const params = SearchContactsSchema.parse(args);
const result = await contactsService.searchContacts(params);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
contacts: result.contacts.map(c => ({
resourceName: c.resourceName,
name: c.names?.[0]?.displayName || `${c.names?.[0]?.givenName || ''} ${c.names?.[0]?.familyName || ''}`.trim(),
email: c.emailAddresses?.[0]?.value,
phone: c.phoneNumbers?.[0]?.value,
organization: c.organizations?.[0]?.name,
})),
totalCount: result.contacts.length
}, null, 2)
}]
};
},
contacts_createContact: async (args: unknown) => {
const params = CreateContactSchema.parse(args);
const contact = await contactsService.createContact(params);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
message: 'Contact created successfully',
contact: {
resourceName: contact.resourceName,
name: contact.names?.[0]?.displayName,
email: contact.emailAddresses?.[0]?.value,
phone: contact.phoneNumbers?.[0]?.value,
}
}, null, 2)
}]
};
},
contacts_updateContact: async (args: unknown) => {
const params = UpdateContactSchema.parse(args);
const contact = await contactsService.updateContact(params);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
message: 'Contact updated successfully',
contact: {
resourceName: contact.resourceName,
name: contact.names?.[0]?.displayName,
email: contact.emailAddresses?.[0]?.value,
phone: contact.phoneNumbers?.[0]?.value,
}
}, null, 2)
}]
};
},
contacts_deleteContact: async (args: unknown) => {
const { resourceName } = DeleteContactSchema.parse(args);
await contactsService.deleteContact(resourceName);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
message: 'Contact deleted successfully'
}, null, 2)
}]
};
},
contacts_listGroups: async (args: unknown) => {
const { pageSize, pageToken } = ListGroupsSchema.parse(args);
const result = await contactsService.listContactGroups(pageSize, pageToken);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
groups: result.groups.map(g => ({
resourceName: g.resourceName,
name: g.name || g.formattedName,
memberCount: g.memberCount,
groupType: g.groupType,
})),
totalCount: result.groups.length,
nextPageToken: result.nextPageToken
}, null, 2)
}]
};
},
contacts_createGroup: async (args: unknown) => {
const { name } = CreateGroupSchema.parse(args);
const group = await contactsService.createContactGroup(name);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
message: 'Contact group created successfully',
group
}, null, 2)
}]
};
},
contacts_updateGroup: async (args: unknown) => {
const { resourceName, name } = UpdateGroupSchema.parse(args);
const group = await contactsService.updateContactGroup(resourceName, name);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
message: 'Contact group updated successfully',
group
}, null, 2)
}]
};
},
contacts_deleteGroup: async (args: unknown) => {
const { resourceName, deleteContacts } = DeleteGroupSchema.parse(args);
await contactsService.deleteContactGroup(resourceName, deleteContacts);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
message: 'Contact group deleted successfully'
}, null, 2)
}]
};
},
};
}