Skip to main content
Glama
contacts.ts22.5 kB
import { z } from 'zod'; import type { ContactProfileInput, CreateAddressPayload, CreateContactFieldPayload, UpdateAddressPayload, UpdateContactFieldPayload } from '../../client/MonicaClient.js'; import type { ToolRegistrationContext } from '../context.js'; import { buildContactSummary, normalizeAddress, normalizeContactDetail, normalizeContactField } from '../../utils/formatters.js'; import { resolveContactFieldTypeId, resolveCountryId, resolveGenderId } from '../../utils/resolvers.js'; const birthdateSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('exact'), day: z.number().int().min(1).max(31), month: z.number().int().min(1).max(12), year: z.number().int().min(1900).max(9999) }), z.object({ type: z.literal('age'), age: z.number().int().min(0).max(150) }), z.object({ type: z.literal('unknown') }) ]); const deceasedDateSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('exact'), day: z.number().int().min(1).max(31), month: z.number().int().min(1).max(12), year: z.number().int().min(1900).max(9999) }), z.object({ type: z.literal('age'), age: z.number().int().min(0).max(150) }), z.object({ type: z.literal('unknown') }) ]); const contactProfileSchema = z.object({ firstName: z.string().min(1).max(50), lastName: z.string().max(100).optional().nullable(), nickname: z.string().max(100).optional().nullable(), description: z.string().max(2000).optional().nullable(), genderId: z.number().int().positive().optional(), genderName: z.string().min(1).max(50).optional(), isPartial: z.boolean().optional(), isDeceased: z.boolean().optional(), birthdate: birthdateSchema.optional(), deceasedDate: deceasedDateSchema.optional(), remindOnDeceasedDate: z.boolean().optional() }); type ContactProfileForm = z.infer<typeof contactProfileSchema> & { genderId?: number }; const contactFieldPayloadSchema = z .object({ contactId: z.number().int().positive(), contactFieldTypeId: z.number().int().positive().optional(), contactFieldTypeName: z.string().min(1).max(255).optional(), data: z.string().min(1).max(255) }) .superRefine((data, ctx) => { if (typeof data.contactFieldTypeId !== 'number' && !data.contactFieldTypeName) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide contactFieldTypeId or contactFieldTypeName.' }); } }); type ContactFieldPayloadForm = z.infer<typeof contactFieldPayloadSchema>; const addressPayloadSchema = z.object({ contactId: z.number().int().positive(), name: z.string().min(1).max(255), street: z.string().max(255).optional().nullable(), city: z.string().max(255).optional().nullable(), province: z.string().max(255).optional().nullable(), postalCode: z.string().max(255).optional().nullable(), countryId: z.string().max(3).optional().nullable(), countryIso: z.string().max(3).optional().nullable(), countryName: z.string().max(255).optional().nullable() }); type AddressPayloadForm = z.infer<typeof addressPayloadSchema>; const contactToolInputShape = { section: z.enum(['summary', 'profile', 'field', 'address']), action: z.enum(['create', 'update', 'delete', 'list', 'get']).optional(), contactId: z.number().int().positive().optional(), contactFieldId: z.number().int().positive().optional(), addressId: z.number().int().positive().optional(), includeContactFields: z.boolean().optional(), limit: z.number().int().min(1).max(100).optional(), page: z.number().int().min(1).optional(), profile: contactProfileSchema.optional(), fieldPayload: contactFieldPayloadSchema.optional(), addressPayload: addressPayloadSchema.optional() } as const; const contactToolInputSchema = z.object(contactToolInputShape).superRefine((data, ctx) => { switch (data.section) { case 'summary': if (typeof data.contactId !== 'number') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide contactId when retrieving a summary.' }); } break; case 'profile': { if (!data.action || !['create', 'update', 'delete'].includes(data.action)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Use action create, update, or delete for profile operations.' }); break; } if (data.action === 'create' && !data.profile) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide profile details when creating a contact.' }); } if (data.action === 'update') { if (typeof data.contactId !== 'number') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide contactId when updating a contact.' }); } if (!data.profile) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide profile details when updating a contact.' }); } } if (data.action === 'delete' && typeof data.contactId !== 'number') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide contactId when deleting a contact.' }); } break; } case 'field': { if (!data.action || !['list', 'get', 'create', 'update', 'delete'].includes(data.action)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Use action list/get/create/update/delete for contact fields.' }); break; } if (data.action === 'list' && typeof data.contactId !== 'number') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide contactId when listing contact fields.' }); } if (data.action === 'get' && typeof data.contactFieldId !== 'number') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide contactFieldId when retrieving a contact field.' }); } if (['create', 'update'].includes(data.action) && !data.fieldPayload) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide contact field details for this action.' }); } if (['update', 'delete'].includes(data.action) && typeof data.contactFieldId !== 'number') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide contactFieldId when updating or deleting a contact field.' }); } break; } case 'address': { if (!data.action || !['list', 'get', 'create', 'update', 'delete'].includes(data.action)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Use action list/get/create/update/delete for addresses.' }); break; } if (data.action === 'list' && typeof data.contactId !== 'number') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide contactId when listing addresses.' }); } if (data.action === 'get' && typeof data.addressId !== 'number') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide addressId when retrieving an address.' }); } if (['create', 'update'].includes(data.action) && !data.addressPayload) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide address details for this action.' }); } if (['update', 'delete'].includes(data.action) && typeof data.addressId !== 'number') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Provide addressId when updating or deleting an address.' }); } break; } default: break; } }); type ContactToolInput = z.infer<typeof contactToolInputSchema>; /** * Shared handler for contact operations - used by both the main tool and wrapper tools */ export async function handleContactOperation( input: ContactToolInput, context: ToolRegistrationContext ): Promise<any> { const { client, logger } = context; switch (input.section) { case 'summary': { const response = await client.getContact(input.contactId!, input.includeContactFields); const contact = normalizeContactDetail(response.data); const summary = buildContactSummary(response.data); return { content: [ { type: 'text' as const, text: summary } ], structuredContent: { section: input.section, contact } }; } case 'profile': { if (input.action === 'delete') { await client.deleteContact(input.contactId!); logger.info({ contactId: input.contactId }, 'Deleted Monica contact'); return { content: [ { type: 'text' as const, text: `Deleted contact ID ${input.contactId}.` } ], structuredContent: { section: input.section, action: input.action, contactId: input.contactId } }; } const profile = input.profile!; const contactId = input.contactId; const genderId = await resolveGenderId(client, profile.genderId, profile.genderName); const payload = toContactProfileInput({ ...profile, genderId }); if (input.action === 'create') { const response = await client.createContact(payload); const contact = normalizeContactDetail(response.data); logger.info({ contactId: contact.id }, 'Created Monica contact'); return { content: [ { type: 'text' as const, text: `Created contact ${contact.name || `#${contact.id}`} (ID ${contact.id}).` } ], structuredContent: { section: input.section, action: input.action, contact } }; } const response = await client.updateContact(contactId!, payload); const contact = normalizeContactDetail(response.data); logger.info({ contactId }, 'Updated Monica contact'); return { content: [ { type: 'text' as const, text: `Updated contact ${contact.name || `#${contact.id}`} (ID ${contact.id}).` } ], structuredContent: { section: input.section, action: input.action, contactId, contact } }; } case 'field': { switch (input.action) { case 'list': { const response = await client.listContactFields({ contactId: input.contactId!, limit: input.limit, page: input.page }); const fields = response.data.map(normalizeContactField); return { content: [ { type: 'text' as const, text: fields.length ? `Fetched ${fields.length} contact field${fields.length === 1 ? '' : 's'} for contact ${input.contactId}.` : `No contact fields found for contact ${input.contactId}.` } ], structuredContent: { section: input.section, action: input.action, contactId: input.contactId, fields, pagination: { currentPage: response.meta.current_page, lastPage: response.meta.last_page, perPage: response.meta.per_page, total: response.meta.total } } }; } case 'get': { const response = await client.getContactField(input.contactFieldId!); const field = normalizeContactField(response.data); return { content: [ { type: 'text' as const, text: `Retrieved contact field ${field.type.name} (ID ${field.id}).` } ], structuredContent: { section: input.section, action: input.action, field } }; } case 'create': { const payload = input.fieldPayload!; const contactFieldTypeId = await resolveContactFieldTypeId(client, { contactFieldTypeId: payload.contactFieldTypeId, contactFieldTypeName: payload.contactFieldTypeName }); const result = await client.createContactField( toContactFieldPayloadInput({ ...payload, contactFieldTypeId }) ); const field = normalizeContactField(result.data); logger.info({ contactFieldId: field.id, contactId: field.contactId }, 'Created Monica contact field'); return { content: [ { type: 'text' as const, text: `Created contact field ${field.type.name} (ID ${field.id}) for contact ${field.contactId}.` } ], structuredContent: { section: input.section, action: input.action, field } }; } case 'update': { const payload = input.fieldPayload!; const contactFieldTypeId = await resolveContactFieldTypeId(client, { contactFieldTypeId: payload.contactFieldTypeId, contactFieldTypeName: payload.contactFieldTypeName }); const result = await client.updateContactField( input.contactFieldId!, toContactFieldPayloadInput({ ...payload, contactFieldTypeId }) ); const field = normalizeContactField(result.data); logger.info({ contactFieldId: input.contactFieldId }, 'Updated Monica contact field'); return { content: [ { type: 'text' as const, text: `Updated contact field ${field.type.name} (ID ${field.id}).` } ], structuredContent: { section: input.section, action: input.action, contactFieldId: input.contactFieldId, field } }; } case 'delete': { const result = await client.deleteContactField(input.contactFieldId!); logger.info({ contactFieldId: input.contactFieldId }, 'Deleted Monica contact field'); return { content: [ { type: 'text' as const, text: `Deleted contact field ID ${input.contactFieldId}.` } ], structuredContent: { section: input.section, action: input.action, contactFieldId: input.contactFieldId, result } }; } default: return unreachable(input.action as never); } } case 'address': { switch (input.action) { case 'list': { const response = await client.listAddresses({ contactId: input.contactId!, limit: input.limit, page: input.page }); const addresses = response.data.map(normalizeAddress); const summary = addresses.length ? `Fetched ${addresses.length} address${addresses.length === 1 ? '' : 'es'} for contact ${input.contactId}.` : `No addresses found for contact ${input.contactId}.`; return { content: [ { type: 'text' as const, text: summary } ], structuredContent: { section: input.section, action: input.action, contactId: input.contactId, addresses, pagination: { currentPage: response.meta.current_page, lastPage: response.meta.last_page, perPage: response.meta.per_page, total: response.meta.total } } }; } case 'get': { const response = await client.getAddress(input.addressId!); const address = normalizeAddress(response.data); return { content: [ { type: 'text' as const, text: `Retrieved address "${address.name}" (ID ${address.id}).` } ], structuredContent: { section: input.section, action: input.action, address } }; } case 'create': { const payload = input.addressPayload!; const countryId = await resolveCountryId(client, { countryId: payload.countryId, countryIso: payload.countryIso, countryName: payload.countryName }); const result = await client.createAddress( toAddressPayloadInput({ ...payload, countryId }) ); const address = normalizeAddress(result.data); logger.info({ addressId: address.id, contactId: address.contact.id }, 'Created Monica address'); return { content: [ { type: 'text' as const, text: `Created address "${address.name}" (ID ${address.id}) for contact ${address.contact.id}.` } ], structuredContent: { section: input.section, action: input.action, address } }; } case 'update': { const payload = input.addressPayload!; const countryId = await resolveCountryId(client, { countryId: payload.countryId, countryIso: payload.countryIso, countryName: payload.countryName }); const result = await client.updateAddress( input.addressId!, toAddressPayloadInput({ ...payload, countryId }) ); const address = normalizeAddress(result.data); logger.info({ addressId: input.addressId }, 'Updated Monica address'); return { content: [ { type: 'text' as const, text: `Updated address "${address.name}" (ID ${address.id}).` } ], structuredContent: { section: input.section, action: input.action, addressId: input.addressId, address } }; } case 'delete': { const result = await client.deleteAddress(input.addressId!); logger.info({ addressId: input.addressId }, 'Deleted Monica address'); return { content: [ { type: 'text' as const, text: `Deleted address ID ${input.addressId}.` } ], structuredContent: { section: input.section, action: input.action, addressId: input.addressId, result } }; } default: return unreachable(input.action as never); } } default: return unreachable(input.section as never); } } export function registerContactTools(context: ToolRegistrationContext): void { const { server } = context; server.registerTool( 'monica_manage_contact', { title: 'Manage Monica contact', description: 'Advanced contact management tool with section-based operations. Set section to "summary" to retrieve full contact details, "profile" for contact creation/updates, "field" for managing contact fields (email/phone/etc), or "address" for address management. For simpler operations, prefer the dedicated wrapper tools: monica_manage_contact_profile, monica_manage_contact_field, or monica_manage_contact_address.', inputSchema: contactToolInputShape }, async (rawInput) => { const input = contactToolInputSchema.parse(rawInput); return handleContactOperation(input, context); } ); } function toContactProfileInput(profile: ContactProfileForm & { genderId?: number }): ContactProfileInput { return { firstName: profile.firstName, lastName: profile.lastName ?? null, nickname: profile.nickname ?? null, description: profile.description ?? null, genderId: profile.genderId, isPartial: profile.isPartial, isDeceased: profile.isDeceased, birthdate: profile.birthdate as ContactProfileInput['birthdate'], deceasedDate: profile.deceasedDate as ContactProfileInput['deceasedDate'], remindOnDeceasedDate: profile.remindOnDeceasedDate }; } function toContactFieldPayloadInput( payload: ContactFieldPayloadForm & { contactFieldTypeId: number } ): CreateContactFieldPayload & UpdateContactFieldPayload { return { contactId: payload.contactId, contactFieldTypeId: payload.contactFieldTypeId, data: payload.data }; } function toAddressPayloadInput( payload: AddressPayloadForm & { countryId: string | null } ): CreateAddressPayload & UpdateAddressPayload { return { contactId: payload.contactId, name: payload.name, street: payload.street ?? null, city: payload.city ?? null, province: payload.province ?? null, postalCode: payload.postalCode ?? null, countryId: payload.countryId }; } function unreachable(value: never): never { throw new Error(`Unhandled case: ${JSON.stringify(value)}`); }

Implementation Reference

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/Jacob-Stokes/monica-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server