import { z } from 'zod';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { Logger } from 'pino';
import type {
ContactProfileInput,
CreateActivityPayload,
CreateNotePayload,
CreateAddressPayload,
CreateActivityTypePayload,
CreateActivityTypeCategoryPayload,
CreateTaskPayload,
CreateRelationshipPayload,
CreateRelationshipTypePayload,
CreateRelationshipTypeGroupPayload,
CreateReminderPayload,
CreateGroupPayload,
CreateContactFieldPayload,
CreateContactFieldTypePayload,
CreateTagPayload,
MonicaClient,
UpdateActivityPayload,
UpdateAddressPayload,
UpdateActivityTypePayload,
UpdateActivityTypeCategoryPayload,
UpdateRelationshipPayload,
UpdateRelationshipTypePayload,
UpdateRelationshipTypeGroupPayload,
UpdateReminderPayload,
UpdateGroupPayload,
UpdateTaskPayload,
UpdateNotePayload,
UpdateContactFieldPayload,
UpdateContactFieldTypePayload,
UpdateTagPayload
} from '../client/MonicaClient.js';
import {
buildContactSummary,
normalizeContactDetail,
normalizeContactSummary,
normalizeNote,
normalizeTask,
normalizeActivity,
normalizeAddress,
normalizeActivityType,
normalizeActivityTypeCategory,
normalizeCountry,
normalizeGender,
normalizeRelationship,
normalizeRelationshipType,
normalizeRelationshipTypeGroup,
normalizeReminder,
normalizeGroup,
normalizeContactField,
normalizeContactFieldType,
normalizeTag
} from '../utils/formatters.js';
interface RegisterToolsOptions {
server: McpServer;
client: MonicaClient;
logger: Logger;
}
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(),
isPartial: z.boolean().optional(),
isDeceased: z.boolean().optional(),
birthdate: birthdateSchema.optional(),
deceasedDate: deceasedDateSchema.optional(),
remindOnDeceasedDate: z.boolean().optional()
});
type ContactProfileForm = z.infer<typeof contactProfileSchema>;
const activityPayloadSchema = z.object({
activityTypeId: z.number().int().positive(),
summary: z.string().min(1).max(255),
description: z.string().max(1_000_000).optional().nullable(),
happenedAt: z
.string()
.regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/u, 'happenedAt must be in YYYY-MM-DD format.'),
contactIds: z.array(z.number().int().positive()).min(1, 'Provide at least one contact ID.'),
emotionIds: z.array(z.number().int().positive()).optional()
});
type ActivityPayloadForm = z.infer<typeof activityPayloadSchema>;
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()
});
type AddressPayloadForm = z.infer<typeof addressPayloadSchema>;
const activityTypePayloadSchema = z.object({
name: z.string().min(1).max(255),
categoryId: z.number().int().positive(),
locationType: z.string().max(255).optional().nullable()
});
type ActivityTypePayloadForm = z.infer<typeof activityTypePayloadSchema>;
const activityTypeCategoryPayloadSchema = z.object({
name: z.string().min(1).max(255)
});
type ActivityTypeCategoryPayloadForm = z.infer<typeof activityTypeCategoryPayloadSchema>;
const taskPayloadSchema = z.object({
title: z.string().min(1).max(255).optional(),
description: z.string().max(1_000_000).optional().nullable(),
status: z.enum(['open', 'completed']).optional(),
completedAt: z
.string()
.regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/u, 'completedAt must use YYYY-MM-DD format.')
.optional()
.nullable(),
contactId: z.number().int().positive().optional()
});
type TaskPayloadForm = z.infer<typeof taskPayloadSchema>;
const notePayloadSchema = z.object({
body: z.string().max(1_000_000).optional(),
contactId: z.number().int().positive().optional(),
isFavorited: z.boolean().optional()
});
type NotePayloadForm = z.infer<typeof notePayloadSchema>;
const relationshipPayloadSchema = z.object({
contactIsId: z.number().int().positive().optional(),
ofContactId: z.number().int().positive().optional(),
relationshipTypeId: z.number().int().positive().optional()
});
type RelationshipPayloadForm = z.infer<typeof relationshipPayloadSchema>;
const groupPayloadSchema = z.object({
name: z.string().min(1).max(255)
});
type GroupPayloadForm = z.infer<typeof groupPayloadSchema>;
const reminderPayloadSchema = z.object({
title: z.string().min(1).max(100_000).optional(),
description: z.string().max(1_000_000).optional().nullable(),
nextExpectedDate: z
.string()
.regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/u, 'nextExpectedDate must use YYYY-MM-DD format.')
.optional(),
frequencyType: z.enum(['one_time', 'day', 'week', 'month', 'year']).optional(),
frequencyNumber: z.number().int().min(1).optional(),
contactId: z.number().int().positive().optional()
});
type ReminderPayloadForm = z.infer<typeof reminderPayloadSchema>;
const contactFieldPayloadSchema = z.object({
contactId: z.number().int().positive(),
contactFieldTypeId: z.number().int().positive(),
data: z.string().min(1).max(255)
});
type ContactFieldPayloadForm = z.infer<typeof contactFieldPayloadSchema>;
const contactFieldTypePayloadSchema = z.object({
name: z.string().min(1).max(255),
fontawesomeIcon: z.string().max(255).optional().nullable(),
protocol: z.string().max(255).optional().nullable(),
delible: z.boolean().optional(),
kind: z.string().max(255).optional().nullable()
});
type ContactFieldTypePayloadForm = z.infer<typeof contactFieldTypePayloadSchema>;
const relationshipTypePayloadSchema = z.object({
name: z.string().min(1).max(255),
reverseName: z.string().min(1).max(255),
relationshipTypeGroupId: z.number().int().positive().optional().nullable(),
delible: z.boolean().optional()
});
type RelationshipTypePayloadForm = z.infer<typeof relationshipTypePayloadSchema>;
const relationshipTypeGroupPayloadSchema = z.object({
name: z.string().min(1).max(255),
delible: z.boolean().optional()
});
type RelationshipTypeGroupPayloadForm = z.infer<typeof relationshipTypeGroupPayloadSchema>;
const tagPayloadSchema = z.object({
name: z.string().min(1).max(255)
});
type TagPayloadForm = z.infer<typeof tagPayloadSchema>;
export function registerTools({ server, client, logger }: RegisterToolsOptions): void {
server.registerTool(
'monica_search_contacts',
{
title: 'Search Monica contacts',
description: 'Search Monica CRM contacts by name, nickname, or email. Returns contact IDs and basic info. Use the returned ID with other tools to get details or make updates.',
inputSchema: {
query: z.string().min(2, 'Provide at least 2 characters to search.'),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
includePartial: z.boolean().optional()
}
},
async ({ query, limit, page, includePartial }) => {
const results = await client.searchContacts({ query, limit, page, includePartial });
const contacts = results.data.map(normalizeContactSummary);
let text: string;
if (contacts.length === 0) {
text = `No Monica contacts matched "${query}".`;
} else {
const contactList = contacts.map(contact => {
const emails = contact.emails.length ? ` (${contact.emails.join(', ')})` : '';
const phones = contact.phones.length ? ` [${contact.phones.join(', ')}]` : '';
const nickname = contact.nickname ? ` "${contact.nickname}"` : '';
return `• ID: ${contact.id} - ${contact.name}${nickname}${emails}${phones}`;
}).join('\n');
text = `Found ${contacts.length} contact(s) matching "${query}":\n\n${contactList}`;
}
return {
content: [
{
type: 'text' as const,
text
}
],
structuredContent: {
contacts,
pagination: {
currentPage: results.meta.current_page,
lastPage: results.meta.last_page,
perPage: results.meta.per_page,
total: results.meta.total
}
}
};
}
);
server.registerTool(
'monica_get_contact_summary',
{
title: 'Get Monica contact detail',
description: 'Retrieve full profile details for a specific contact ID. Use this after searching to see complete contact information before making updates. DEPENDENCY: Requires contactId - use monica_search_contacts to find existing contacts or monica_manage_contact with action="create" to create a new contact first.',
inputSchema: {
contactId: z.number().int().positive(),
includeContactFields: z.boolean().optional()
}
},
async ({ contactId, includeContactFields }) => {
const response = await client.getContact(contactId, includeContactFields);
const contact = response.data;
const summary = buildContactSummary(contact);
return {
content: [
{
type: 'text' as const,
text: summary
}
],
structuredContent: {
contact: normalizeContactDetail(contact)
}
};
}
);
server.registerTool(
'monica_create_note',
{
title: 'Create Monica note',
description: 'Attach a note to a Monica contact.',
inputSchema: {
contactId: z.number().int().positive(),
body: z.string().min(1, 'Provide note content.'),
isFavorited: z.boolean().optional()
}
},
async ({ contactId, body, isFavorited }) => {
const result = await client.createNote({ contactId, body, isFavorited });
const note = normalizeNote(result.data);
logger.info({ contactId, noteId: note.id }, 'Created Monica note');
return {
content: [
{
type: 'text' as const,
text: `Note ${note.id} created for contact ${contactId}.`
}
],
structuredContent: {
note
}
};
}
);
server.registerTool(
'monica_manage_activity',
{
title: 'Manage Monica activity',
description: 'Manage activities (meetings, calls, events) in Monica CRM. Use this to track interactions and shared experiences with contacts.',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
activityId: z.number().int().positive().optional(),
contactId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: activityPayloadSchema.optional()
}
},
async ({ action, activityId, contactId, limit, page, payload }) => {
if (action === 'list') {
const response = await client.listActivities({ contactId, limit, page });
const activities = response.data.map(normalizeActivity);
const scope = contactId ? `contact ${contactId}` : 'your account';
const summary = activities.length
? `Fetched ${activities.length} activit${activities.length === 1 ? 'y' : 'ies'} for ${scope}.`
: `No activities found for ${scope} matching the criteria.`;
return {
content: [
{
type: 'text' as const,
text: summary
}
],
structuredContent: {
action,
contactId,
activities,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
if (action === 'get') {
if (!activityId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide activityId when retrieving an activity.'
}
]
};
}
const response = await client.getActivity(activityId);
const activity = normalizeActivity(response.data);
return {
content: [
{
type: 'text' as const,
text: `Retrieved activity ${activity.summary || `#${activity.id}`} (ID ${activity.id}).`
}
],
structuredContent: {
action,
activity
}
};
}
if (action === 'create') {
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide an activity payload when creating an activity.'
}
]
};
}
const result = await client.createActivity(toActivityPayloadInput(payload));
const activity = normalizeActivity(result.data);
logger.info({ activityId: activity.id }, 'Created Monica activity');
return {
content: [
{
type: 'text' as const,
text: `Created activity ${activity.summary || `#${activity.id}`} (ID ${activity.id}).`
}
],
structuredContent: {
action,
activity
}
};
}
if (action === 'update') {
if (!activityId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide activityId when updating an activity.'
}
]
};
}
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide an activity payload when updating an activity.'
}
]
};
}
const result = await client.updateActivity(activityId, toActivityPayloadInput(payload));
const activity = normalizeActivity(result.data);
logger.info({ activityId }, 'Updated Monica activity');
return {
content: [
{
type: 'text' as const,
text: `Updated activity ${activity.summary || `#${activity.id}`} (ID ${activity.id}).`
}
],
structuredContent: {
action,
activityId,
activity
}
};
}
if (!activityId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide activityId when deleting an activity.'
}
]
};
}
const result = await client.deleteActivity(activityId);
logger.info({ activityId }, 'Deleted Monica activity');
return {
content: [
{
type: 'text' as const,
text: `Deleted activity ID ${activityId}.`
}
],
structuredContent: {
action,
activityId,
result
}
};
}
);
server.registerTool(
'monica_manage_address',
{
title: 'Manage Monica address',
description: 'Manage physical addresses for contacts. Use this to add home, work, or other addresses to contact profiles. DEPENDENCY: For create/update actions, requires contactId - use monica_search_contacts to find existing contacts or monica_manage_contact with action="create" to create a new contact first.',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
addressId: z.number().int().positive().optional(),
contactId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: addressPayloadSchema.optional()
}
},
async ({ action, addressId, contactId, limit, page, payload }) => {
if (action === 'list') {
if (!contactId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide contactId when listing addresses.'
}
]
};
}
const response = await client.listAddresses({ contactId, limit, page });
const addresses = response.data.map(normalizeAddress);
const summary = addresses.length
? `Fetched ${addresses.length} address(es) for contact ${contactId}.`
: `No addresses found for contact ${contactId}.`;
return {
content: [
{
type: 'text' as const,
text: summary
}
],
structuredContent: {
action,
contactId,
addresses,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
if (action === 'get') {
if (!addressId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide addressId when retrieving an address.'
}
]
};
}
const response = await client.getAddress(addressId);
const address = normalizeAddress(response.data);
return {
content: [
{
type: 'text' as const,
text: `Retrieved address ${address.name} (ID ${address.id}).`
}
],
structuredContent: {
action,
address
}
};
}
if (action === 'create') {
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide an address payload when creating an address.'
}
]
};
}
const result = await client.createAddress(toAddressPayloadInput(payload));
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: {
action,
address
}
};
}
if (action === 'update') {
if (!addressId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide addressId when updating an address.'
}
]
};
}
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide an address payload when updating an address.'
}
]
};
}
const result = await client.updateAddress(addressId, toAddressPayloadInput(payload));
const address = normalizeAddress(result.data);
logger.info({ addressId, contactId: address.contact.id }, 'Updated Monica address');
return {
content: [
{
type: 'text' as const,
text: `Updated address ${address.name} (ID ${address.id}).`
}
],
structuredContent: {
action,
addressId,
address
}
};
}
if (!addressId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide addressId when deleting an address.'
}
]
};
}
const result = await client.deleteAddress(addressId);
logger.info({ addressId }, 'Deleted Monica address');
return {
content: [
{
type: 'text' as const,
text: `Deleted address ID ${addressId}.`
}
],
structuredContent: {
action,
addressId,
result
}
};
}
);
server.registerTool(
'monica_lookup_country',
{
title: 'Lookup Monica countries',
description:
'Retrieve the supported country catalog (name, ISO code, Monica ID). Use this before creating or updating addresses so you can map human-friendly names to the correct country identifier.',
inputSchema: {
limit: z.number().int().min(1).max(250).optional(),
page: z.number().int().min(1).optional(),
iso: z
.string()
.regex(/^[A-Za-z]{2,3}$/u, 'Provide a 2- or 3-letter ISO country code (e.g., US, FRA).')
.optional(),
id: z.string().min(2).max(5).optional(),
search: z
.string()
.min(2, 'Provide at least 2 characters to search by country name or code.')
.optional()
}
},
async ({ limit, page, iso, id, search }) => {
const normalizedIso = iso?.toLowerCase();
const normalizedId = id?.toLowerCase();
const normalizedSearch = search?.toLowerCase();
const filtersActive = Boolean(normalizedIso || normalizedId || normalizedSearch);
const shouldFetchAll = !page && filtersActive;
const perPage = Math.min(limit ?? 100, 100);
const matchesFilter = (country: ReturnType<typeof normalizeCountry>) => {
if (normalizedIso && country.iso.toLowerCase() !== normalizedIso) {
return false;
}
if (normalizedId && country.id.toLowerCase() !== normalizedId) {
return false;
}
if (normalizedSearch) {
const haystack = `${country.name} ${country.iso} ${country.id}`.toLowerCase();
if (!haystack.includes(normalizedSearch)) {
return false;
}
}
return true;
};
const filterPhrases: string[] = [];
if (iso) {
filterPhrases.push(`ISO ${iso.toUpperCase()}`);
}
if (id) {
filterPhrases.push(`ID ${id}`);
}
if (search) {
filterPhrases.push(`name/code containing "${search}"`);
}
if (!shouldFetchAll) {
const targetPage = page ?? 1;
const response = await client.listCountries(perPage, targetPage);
const countries = response.data.map(normalizeCountry);
const filteredCountries = filtersActive ? countries.filter(matchesFilter) : countries;
const matchLabel = filteredCountries.length === 1 ? 'country' : 'countries';
let text: string;
if (filtersActive) {
const description = filterPhrases.join(' and ');
text = filteredCountries.length
? `Found ${filteredCountries.length} ${matchLabel} matching ${description}.`
: `No countries matched ${description}.`;
} else {
const scope = page ? `on page ${targetPage}` : 'from Monica';
text = filteredCountries.length
? `Fetched ${filteredCountries.length} ${matchLabel} ${scope}.`
: `No countries ${page ? `found on page ${targetPage}.` : 'returned.'}`;
}
logger.info(
{
limit: perPage,
page: targetPage,
iso: normalizedIso,
id: normalizedId,
search: normalizedSearch,
filtersActive,
matched: filteredCountries.length
},
'Listed Monica countries'
);
return {
content: [
{
type: 'text' as const,
text
}
],
structuredContent: {
action: filtersActive ? 'filter' : 'list',
filters: {
id: id ?? undefined,
iso: iso ? iso.toUpperCase() : undefined,
search: search ?? undefined
},
countries: filteredCountries,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
const collected: ReturnType<typeof normalizeCountry>[] = [];
let currentPage = 1;
let lastMeta: {
current_page: number;
last_page: number;
per_page: number;
total: number;
} | null = null;
let pagesFetched = 0;
while (true) {
const response = await client.listCountries(perPage, currentPage);
pagesFetched += 1;
lastMeta = response.meta;
const countries = response.data.map(normalizeCountry);
const filteredCountries = countries.filter(matchesFilter);
collected.push(...filteredCountries);
const foundIso = normalizedIso
? collected.some(country => country.iso.toLowerCase() === normalizedIso)
: false;
const foundId = normalizedId
? collected.some(country => country.id.toLowerCase() === normalizedId)
: false;
if (foundIso || foundId) {
break;
}
if (response.meta.current_page >= response.meta.last_page) {
break;
}
currentPage += 1;
}
const description = filterPhrases.join(' and ');
const matchLabel = collected.length === 1 ? 'country' : 'countries';
const text = collected.length
? `Scanned ${pagesFetched} page${pagesFetched === 1 ? '' : 's'} and found ${collected.length} ${matchLabel} matching ${description}.`
: `Scanned ${pagesFetched} page${pagesFetched === 1 ? '' : 's'} but no countries matched ${description}.`;
logger.info(
{
limit: perPage,
iso: normalizedIso,
id: normalizedId,
search: normalizedSearch,
pagesFetched,
matched: collected.length
},
'Scanned Monica countries'
);
return {
content: [
{
type: 'text' as const,
text
}
],
structuredContent: {
action: 'filter',
filters: {
id: id ?? undefined,
iso: iso ? iso.toUpperCase() : undefined,
search: search ?? undefined
},
countries: collected,
pagination: lastMeta
? {
currentPage: lastMeta.current_page,
lastPage: lastMeta.last_page,
perPage: lastMeta.per_page,
total: lastMeta.total
}
: undefined,
pagesFetched
}
};
}
);
server.registerTool(
'monica_lookup_gender',
{
title: 'Lookup Monica genders',
description:
'Fetch the gender catalog so you can supply the correct genderId when creating or updating contacts. Monica requires a genderId for every contact profile.',
inputSchema: {
limit: z.number().int().min(1).max(250).optional(),
page: z.number().int().min(1).optional(),
genderId: z.number().int().positive().optional(),
search: z
.string()
.min(2, 'Provide at least 2 characters to search by gender name.')
.optional()
}
},
async ({ limit, page, genderId, search }) => {
const normalizedSearch = search?.toLowerCase();
const filtersActive = typeof genderId === 'number' || Boolean(normalizedSearch);
const shouldFetchAll = !page && filtersActive;
const perPage = Math.min(limit ?? 100, 100);
const matchesFilter = (gender: ReturnType<typeof normalizeGender>) => {
if (typeof genderId === 'number' && gender.id !== genderId) {
return false;
}
if (normalizedSearch && !gender.name.toLowerCase().includes(normalizedSearch)) {
return false;
}
return true;
};
const filterPhrases: string[] = [];
if (typeof genderId === 'number') {
filterPhrases.push(`ID ${genderId}`);
}
if (search) {
filterPhrases.push(`name containing "${search}"`);
}
if (!shouldFetchAll) {
const targetPage = page ?? 1;
const response = await client.listGenders(perPage, targetPage);
const genders = response.data.map(normalizeGender);
const filteredGenders = filtersActive ? genders.filter(matchesFilter) : genders;
const matchLabel = filteredGenders.length === 1 ? 'gender' : 'genders';
let text: string;
if (filtersActive) {
const description = filterPhrases.join(' and ');
text = filteredGenders.length
? `Found ${filteredGenders.length} ${matchLabel} matching ${description}.`
: `No genders matched ${description}.`;
} else {
const scope = page ? `on page ${targetPage}` : 'from Monica';
text = filteredGenders.length
? `Fetched ${filteredGenders.length} ${matchLabel} ${scope}.`
: `No genders ${page ? `found on page ${targetPage}.` : 'returned.'}`;
}
logger.info(
{
limit: perPage,
page: targetPage,
genderId,
search: normalizedSearch,
filtersActive,
matched: filteredGenders.length
},
'Listed Monica genders'
);
return {
content: [
{
type: 'text' as const,
text
}
],
structuredContent: {
action: filtersActive ? 'filter' : 'list',
filters: {
genderId: genderId ?? undefined,
search: search ?? undefined
},
genders: filteredGenders,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
const collected: ReturnType<typeof normalizeGender>[] = [];
let currentPage = 1;
let lastMeta: {
current_page: number;
last_page: number;
per_page: number;
total: number;
} | null = null;
let pagesFetched = 0;
while (true) {
const response = await client.listGenders(perPage, currentPage);
pagesFetched += 1;
lastMeta = response.meta;
const genders = response.data.map(normalizeGender);
const filteredGenders = genders.filter(matchesFilter);
collected.push(...filteredGenders);
const foundId = typeof genderId === 'number'
? collected.some((gender) => gender.id === genderId)
: false;
if (foundId) {
break;
}
if (response.meta.current_page >= response.meta.last_page) {
break;
}
currentPage += 1;
}
const description = filterPhrases.join(' and ');
const matchLabel = collected.length === 1 ? 'gender' : 'genders';
const text = collected.length
? `Scanned ${pagesFetched} page${pagesFetched === 1 ? '' : 's'} and found ${collected.length} ${matchLabel} matching ${description}.`
: `Scanned ${pagesFetched} page${pagesFetched === 1 ? '' : 's'} but no genders matched ${description}.`;
logger.info(
{
limit: perPage,
genderId,
search: normalizedSearch,
pagesFetched,
matched: collected.length
},
'Scanned Monica genders'
);
return {
content: [
{
type: 'text' as const,
text
}
],
structuredContent: {
action: 'filter',
filters: {
genderId: genderId ?? undefined,
search: search ?? undefined
},
genders: collected,
pagination: lastMeta
? {
currentPage: lastMeta.current_page,
lastPage: lastMeta.last_page,
perPage: lastMeta.per_page,
total: lastMeta.total
}
: undefined,
pagesFetched
}
};
}
);
server.registerTool(
'monica_manage_relationship',
{
title: 'Manage Monica relationships',
description:
'List, inspect, create, update, or delete relationships between contacts. Use this to confirm existing connections or link two contacts once you know the correct relationshipTypeId.',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
relationshipId: z.number().int().positive().optional(),
contactId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: relationshipPayloadSchema.optional()
}
},
async ({ action, relationshipId, contactId, limit, page, payload }) => {
if (action === 'list') {
if (!contactId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide contactId when listing relationships.'
}
]
};
}
const response = await client.listRelationships({ contactId, limit, page });
const relationships = response.data.map(normalizeRelationship);
let text: string;
if (relationships.length === 0) {
text = `No relationships found for contact ${contactId}.`;
} else {
const lines = relationships.map((relationship) => {
const primary = relationship.contact?.name || `Contact ${relationship.contactId}`;
const related = relationship.relatedContact?.name || `Contact ${relationship.relatedContactId}`;
return `• ${primary} is ${relationship.relationshipType.name} of ${related}`;
});
text = `Found ${relationships.length} relationship${relationships.length === 1 ? '' : 's'} for contact ${contactId}:\n\n${lines.join('\n')}`;
}
logger.info(
{
contactId,
limit,
page,
matched: relationships.length
},
'Listed Monica relationships'
);
return {
content: [
{
type: 'text' as const,
text
}
],
structuredContent: {
action,
contactId,
relationships,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
if (action === 'get') {
if (!relationshipId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide relationshipId when retrieving a relationship.'
}
]
};
}
const response = await client.getRelationship(relationshipId);
const relationship = normalizeRelationship(response.data);
const primary = relationship.contact?.name || `Contact ${relationship.contactId}`;
const related = relationship.relatedContact?.name || `Contact ${relationship.relatedContactId}`;
return {
content: [
{
type: 'text' as const,
text: `Retrieved relationship ${relationship.relationshipType.name} between ${primary} and ${related}.`
}
],
structuredContent: {
action,
relationshipId,
relationship
}
};
}
if (action === 'create') {
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a relationship payload when creating a relationship (contactIsId, ofContactId, relationshipTypeId).'
}
]
};
}
const createPayload = toRelationshipCreatePayload(payload);
const response = await client.createRelationship(createPayload);
const relationship = normalizeRelationship(response.data);
logger.info(
{
relationshipId: relationship.id,
contactId: relationship.contactId,
relatedContactId: relationship.relatedContactId,
relationshipTypeId: relationship.relationshipType.id
},
'Created Monica relationship'
);
const primary = relationship.contact?.name || `Contact ${relationship.contactId}`;
const related = relationship.relatedContact?.name || `Contact ${relationship.relatedContactId}`;
return {
content: [
{
type: 'text' as const,
text: `Linked ${primary} as ${relationship.relationshipType.name} of ${related}.`
}
],
structuredContent: {
action,
relationship
}
};
}
if (action === 'update') {
if (!relationshipId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide relationshipId when updating a relationship.'
}
]
};
}
if (!payload || typeof payload.relationshipTypeId !== 'number') {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide relationshipTypeId in the payload when updating a relationship.'
}
]
};
}
const updated = await client.updateRelationship(
relationshipId,
toRelationshipUpdatePayload(payload)
);
const relationship = normalizeRelationship(updated.data);
logger.info(
{
relationshipId,
relationshipTypeId: relationship.relationshipType.id
},
'Updated Monica relationship'
);
const primary = relationship.contact?.name || `Contact ${relationship.contactId}`;
const related = relationship.relatedContact?.name || `Contact ${relationship.relatedContactId}`;
return {
content: [
{
type: 'text' as const,
text: `Updated relationship ${relationshipId}: ${primary} is now marked as ${relationship.relationshipType.name} of ${related}.`
}
],
structuredContent: {
action,
relationshipId,
relationship
}
};
}
if (!relationshipId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide relationshipId when deleting a relationship.'
}
]
};
}
const result = await client.deleteRelationship(relationshipId);
logger.info({ relationshipId }, 'Deleted Monica relationship');
return {
content: [
{
type: 'text' as const,
text: `Deleted relationship ID ${relationshipId}.`
}
],
structuredContent: {
action,
relationshipId,
result
}
};
}
);
server.registerTool(
'monica_manage_activity_type',
{
title: 'Manage activity types',
description: 'List, inspect, create, update, or delete Monica activity types.',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
activityTypeId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: activityTypePayloadSchema.optional()
}
},
async ({ action, activityTypeId, limit, page, payload }) => {
if (action === 'list') {
const response = await client.listActivityTypes({ limit, page });
const activityTypes = response.data.map(normalizeActivityType);
return {
content: [
{
type: 'text' as const,
text: activityTypes.length
? `Fetched ${activityTypes.length} activity type${activityTypes.length === 1 ? '' : 's'}.`
: 'No activity types found.'
}
],
structuredContent: {
action,
activityTypes,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
if (action === 'get') {
if (!activityTypeId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide activityTypeId when retrieving an activity type.'
}
]
};
}
const response = await client.getActivityType(activityTypeId);
const activityType = normalizeActivityType(response.data);
return {
content: [
{
type: 'text' as const,
text: `Retrieved activity type ${activityType.name} (ID ${activityType.id}).`
}
],
structuredContent: {
action,
activityType
}
};
}
if (action === 'create') {
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide an activity type payload when creating an activity type.'
}
]
};
}
const result = await client.createActivityType(toActivityTypePayloadInput(payload));
const activityType = normalizeActivityType(result.data);
logger.info({ activityTypeId: activityType.id }, 'Created Monica activity type');
return {
content: [
{
type: 'text' as const,
text: `Created activity type ${activityType.name} (ID ${activityType.id}).`
}
],
structuredContent: {
action,
activityType
}
};
}
if (action === 'update') {
if (!activityTypeId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide activityTypeId when updating an activity type.'
}
]
};
}
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide an activity type payload when updating an activity type.'
}
]
};
}
const result = await client.updateActivityType(activityTypeId, toActivityTypePayloadInput(payload));
const activityType = normalizeActivityType(result.data);
logger.info({ activityTypeId }, 'Updated Monica activity type');
return {
content: [
{
type: 'text' as const,
text: `Updated activity type ${activityType.name} (ID ${activityType.id}).`
}
],
structuredContent: {
action,
activityTypeId,
activityType
}
};
}
if (!activityTypeId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide activityTypeId when deleting an activity type.'
}
]
};
}
const result = await client.deleteActivityType(activityTypeId);
logger.info({ activityTypeId }, 'Deleted Monica activity type');
return {
content: [
{
type: 'text' as const,
text: `Deleted activity type ID ${activityTypeId}.`
}
],
structuredContent: {
action,
activityTypeId,
result
}
};
}
);
server.registerTool(
'monica_manage_activity_type_category',
{
title: 'Manage activity type categories',
description: 'List, inspect, create, update, or delete activity type categories.',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
activityTypeCategoryId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: activityTypeCategoryPayloadSchema.optional()
}
},
async ({ action, activityTypeCategoryId, limit, page, payload }) => {
if (action === 'list') {
const response = await client.listActivityTypeCategories({ limit, page });
const categories = response.data.map(normalizeActivityTypeCategory);
return {
content: [
{
type: 'text' as const,
text: categories.length
? `Fetched ${categories.length} activity type categor${categories.length === 1 ? 'y' : 'ies'}.`
: 'No activity type categories found.'
}
],
structuredContent: {
action,
categories,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
if (action === 'get') {
if (!activityTypeCategoryId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide activityTypeCategoryId when retrieving a category.'
}
]
};
}
const response = await client.getActivityTypeCategory(activityTypeCategoryId);
const category = normalizeActivityTypeCategory(response.data);
return {
content: [
{
type: 'text' as const,
text: `Retrieved activity type category ${category.name} (ID ${category.id}).`
}
],
structuredContent: {
action,
category
}
};
}
if (action === 'create') {
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a category payload when creating an activity type category.'
}
]
};
}
const result = await client.createActivityTypeCategory(toActivityTypeCategoryPayloadInput(payload));
const category = normalizeActivityTypeCategory(result.data);
logger.info({ activityTypeCategoryId: category.id }, 'Created Monica activity type category');
return {
content: [
{
type: 'text' as const,
text: `Created activity type category ${category.name} (ID ${category.id}).`
}
],
structuredContent: {
action,
category
}
};
}
if (action === 'update') {
if (!activityTypeCategoryId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide activityTypeCategoryId when updating a category.'
}
]
};
}
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a category payload when updating an activity type category.'
}
]
};
}
const result = await client.updateActivityTypeCategory(
activityTypeCategoryId,
toActivityTypeCategoryPayloadInput(payload)
);
const category = normalizeActivityTypeCategory(result.data);
logger.info({ activityTypeCategoryId }, 'Updated Monica activity type category');
return {
content: [
{
type: 'text' as const,
text: `Updated activity type category ${category.name} (ID ${category.id}).`
}
],
structuredContent: {
action,
activityTypeCategoryId,
category
}
};
}
if (!activityTypeCategoryId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide activityTypeCategoryId when deleting a category.'
}
]
};
}
const result = await client.deleteActivityTypeCategory(activityTypeCategoryId);
logger.info({ activityTypeCategoryId }, 'Deleted Monica activity type category');
return {
content: [
{
type: 'text' as const,
text: `Deleted activity type category ID ${activityTypeCategoryId}.`
}
],
structuredContent: {
action,
activityTypeCategoryId,
result
}
};
}
);
server.registerTool(
'monica_manage_task',
{
title: 'Manage Monica tasks',
description:
'List, inspect, create, update, or delete tasks. Use this to add follow-ups, mark them complete, or target them to another contact.',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
taskId: z.number().int().positive().optional(),
contactId: z.number().int().positive().optional(),
status: z.enum(['open', 'completed', 'all']).optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: taskPayloadSchema.optional()
}
},
async ({ action, taskId, contactId, status, limit, page, payload }) => {
if (action === 'list') {
const response = await client.listTasks({ contactId, status, limit, page });
const tasks = response.data.map(normalizeTask);
const scope = contactId ? `contact ${contactId}` : 'your account';
const summary = tasks.length
? `Fetched ${tasks.length} task${tasks.length === 1 ? '' : 's'} for ${scope}.`
: `No tasks found for ${scope} matching the criteria.`;
return {
content: [
{
type: 'text' as const,
text: summary
}
],
structuredContent: {
action,
contactId,
status,
tasks,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
if (action === 'get') {
if (!taskId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide taskId when retrieving a task.'
}
]
};
}
const response = await client.getTask(taskId);
const task = normalizeTask(response.data);
return {
content: [
{
type: 'text' as const,
text: `Retrieved task ${task.title} (ID ${task.id}).`
}
],
structuredContent: {
action,
task
}
};
}
if (action === 'create') {
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide task details (title, contactId, etc.) when creating a task.'
}
]
};
}
if (!payload.title || !payload.contactId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Task creation requires both title and contactId.'
}
]
};
}
const result = await client.createTask(toTaskCreatePayload(payload));
const task = normalizeTask(result.data);
logger.info({ taskId: task.id, contactId: task.contactId }, 'Created Monica task');
return {
content: [
{
type: 'text' as const,
text: `Created task ${task.title} (ID ${task.id}).`
}
],
structuredContent: {
action,
task
}
};
}
if (action === 'update') {
if (!taskId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide taskId when updating a task.'
}
]
};
}
if (!payload || Object.keys(payload).length === 0) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide at least one field (title, status, description, etc.) when updating a task.'
}
]
};
}
const result = await client.updateTask(taskId, toTaskUpdatePayload(payload));
const task = normalizeTask(result.data);
logger.info({ taskId }, 'Updated Monica task');
return {
content: [
{
type: 'text' as const,
text: `Updated task ${task.title} (ID ${task.id}).`
}
],
structuredContent: {
action,
taskId,
task
}
};
}
if (!taskId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide taskId when deleting a task.'
}
]
};
}
const result = await client.deleteTask(taskId);
logger.info({ taskId }, 'Deleted Monica task');
return {
content: [
{
type: 'text' as const,
text: `Deleted task ID ${taskId}.`
}
],
structuredContent: {
action,
taskId,
result
}
};
}
);
server.registerTool(
'monica_manage_note',
{
title: 'Manage Monica notes',
description:
'List, inspect, create, update, or delete notes attached to a contact. Use this to capture or revise journal snippets.',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
noteId: z.number().int().positive().optional(),
contactId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: notePayloadSchema.optional()
}
},
async ({ action, noteId, contactId, limit, page, payload }) => {
if (action === 'list') {
if (!contactId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide contactId when listing notes.'
}
]
};
}
const response = await client.fetchContactNotes(contactId, limit, page);
const notes = response.data.map(normalizeNote);
const summary = notes.length
? `Fetched ${notes.length} note${notes.length === 1 ? '' : 's'} for contact ${contactId}.`
: `No notes found for contact ${contactId}.`;
return {
content: [
{
type: 'text' as const,
text: summary
}
],
structuredContent: {
action,
contactId,
notes,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
if (action === 'get') {
if (!noteId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide noteId when retrieving a note.'
}
]
};
}
const response = await client.getNote(noteId);
const note = normalizeNote(response.data);
return {
content: [
{
type: 'text' as const,
text: `Retrieved note ${note.id} for contact ${note.contactId}.`
}
],
structuredContent: {
action,
note
}
};
}
if (action === 'create') {
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide note content and contactId when creating a note.'
}
]
};
}
if (!payload.body || !payload.contactId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Note creation requires both body and contactId.'
}
]
};
}
const result = await client.createNote(toNoteCreatePayload(payload));
const note = normalizeNote(result.data);
logger.info({ noteId: note.id, contactId: note.contactId }, 'Created Monica note');
return {
content: [
{
type: 'text' as const,
text: `Created note ${note.id} for contact ${note.contactId}.`
}
],
structuredContent: {
action,
note
}
};
}
if (action === 'update') {
if (!noteId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide noteId when updating a note.'
}
]
};
}
if (!payload || Object.keys(payload).length === 0) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide note fields (body, contactId, isFavorited) when updating a note.'
}
]
};
}
const result = await client.updateNote(noteId, toNoteUpdatePayload(payload));
const note = normalizeNote(result.data);
logger.info({ noteId }, 'Updated Monica note');
return {
content: [
{
type: 'text' as const,
text: `Updated note ${note.id} for contact ${note.contactId}.`
}
],
structuredContent: {
action,
noteId,
note
}
};
}
if (!noteId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide noteId when deleting a note.'
}
]
};
}
const result = await client.deleteNote(noteId);
logger.info({ noteId }, 'Deleted Monica note');
return {
content: [
{
type: 'text' as const,
text: `Deleted note ID ${noteId}.`
}
],
structuredContent: {
action,
noteId,
result
}
};
}
);
server.registerTool(
'monica_manage_contact_field_type',
{
title: 'Manage contact field types',
description: 'ADVANCED: Manage the types/categories of contact fields (like "Mobile Phone", "Work Email"). Most users should use monica_manage_contact_field directly instead. Use this only to create new field types or see available options.',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
contactFieldTypeId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: contactFieldTypePayloadSchema.optional()
}
},
async ({ action, contactFieldTypeId, limit, page, payload }) => {
if (action === 'list') {
const response = await client.listContactFieldTypes({ limit, page });
const types = response.data.map(normalizeContactFieldType);
let text: string;
if (types.length === 0) {
text = 'No contact field types found.';
} else {
const typeList = types.map(type => {
const icon = type.icon ? ` ${type.icon}` : '';
return `• ID: ${type.id} - ${type.name}${icon}`;
}).join('\n');
text = `Found ${types.length} contact field type${types.length === 1 ? '' : 's'}:\n\n${typeList}`;
}
return {
content: [
{
type: 'text' as const,
text
}
],
structuredContent: {
action,
types,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
if (action === 'get') {
if (!contactFieldTypeId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide contactFieldTypeId when retrieving a contact field type.'
}
]
};
}
const response = await client.getContactFieldType(contactFieldTypeId);
const type = normalizeContactFieldType(response.data);
return {
content: [
{
type: 'text' as const,
text: `Retrieved contact field type ${type.name} (ID ${type.id}).`
}
],
structuredContent: {
action,
contactFieldType: type
}
};
}
if (action === 'create') {
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a contact field type payload when creating a new type.'
}
]
};
}
const result = await client.createContactFieldType(toContactFieldTypePayloadInput(payload));
const type = normalizeContactFieldType(result.data);
logger.info({ contactFieldTypeId: type.id }, 'Created Monica contact field type');
return {
content: [
{
type: 'text' as const,
text: `Created contact field type ${type.name} (ID ${type.id}).`
}
],
structuredContent: {
action,
contactFieldType: type
}
};
}
if (action === 'update') {
if (!contactFieldTypeId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide contactFieldTypeId when updating a contact field type.'
}
]
};
}
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a contact field type payload when updating a contact field type.'
}
]
};
}
const result = await client.updateContactFieldType(contactFieldTypeId, toContactFieldTypePayloadInput(payload));
const type = normalizeContactFieldType(result.data);
logger.info({ contactFieldTypeId }, 'Updated Monica contact field type');
return {
content: [
{
type: 'text' as const,
text: `Updated contact field type ${type.name} (ID ${type.id}).`
}
],
structuredContent: {
action,
contactFieldTypeId,
contactFieldType: type
}
};
}
if (!contactFieldTypeId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide contactFieldTypeId when deleting a contact field type.'
}
]
};
}
const result = await client.deleteContactFieldType(contactFieldTypeId);
logger.info({ contactFieldTypeId }, 'Deleted Monica contact field type');
return {
content: [
{
type: 'text' as const,
text: `Deleted contact field type ID ${contactFieldTypeId}.`
}
],
structuredContent: {
action,
contactFieldTypeId,
result
}
};
}
);
server.registerTool(
'monica_manage_contact_field',
{
title: 'Manage contact fields',
description: 'PREFERRED FOR PHONES/EMAILS: Add, update, or remove phone numbers, emails, and other contact information. Also handles custom fields like social media handles. Use this instead of basic contact management for communication details. DEPENDENCY: For create action, requires contactId (use monica_manage_contact with action="create") and contactFieldTypeId (use monica_manage_contact_field_type with action="list" or "create").',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
contactFieldId: z.number().int().positive().optional(),
contactId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: contactFieldPayloadSchema.optional()
}
},
async ({ action, contactFieldId, contactId, limit, page, payload }) => {
if (action === 'list') {
if (!contactId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide contactId when listing contact fields.'
}
]
};
}
const response = await client.listContactFields({ contactId, limit, 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 ${contactId}.`
: `No contact fields found for contact ${contactId}.`
}
],
structuredContent: {
action,
contactId,
fields,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
if (action === 'get') {
if (!contactFieldId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide contactFieldId when retrieving a contact field.'
}
]
};
}
const response = await client.getContactField(contactFieldId);
const field = normalizeContactField(response.data);
return {
content: [
{
type: 'text' as const,
text: `Retrieved contact field ${field.type.name} (ID ${field.id}).`
}
],
structuredContent: {
action,
field
}
};
}
if (action === 'create') {
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a contact field payload when creating a contact field.'
}
]
};
}
const result = await client.createContactField(toContactFieldPayloadInput(payload));
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: {
action,
field
}
};
}
if (action === 'update') {
if (!contactFieldId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide contactFieldId when updating a contact field.'
}
]
};
}
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a contact field payload when updating a contact field.'
}
]
};
}
const result = await client.updateContactField(contactFieldId, toContactFieldPayloadInput(payload));
const field = normalizeContactField(result.data);
logger.info({ contactFieldId }, 'Updated Monica contact field');
return {
content: [
{
type: 'text' as const,
text: `Updated contact field ${field.type.name} (ID ${field.id}).`
}
],
structuredContent: {
action,
contactFieldId,
field
}
};
}
if (!contactFieldId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide contactFieldId when deleting a contact field.'
}
]
};
}
const result = await client.deleteContactField(contactFieldId);
logger.info({ contactFieldId }, 'Deleted Monica contact field');
return {
content: [
{
type: 'text' as const,
text: `Deleted contact field ID ${contactFieldId}.`
}
],
structuredContent: {
action,
contactFieldId,
result
}
};
}
);
server.registerTool(
'monica_manage_contact',
{
title: 'Manage Monica contact',
description: 'Create, update, or delete contacts in Monica CRM. Use this for basic profile info (name, gender, description, birthdate). For phone numbers and emails, use contact field management instead.',
inputSchema: {
action: z.enum(['create', 'update', 'delete']),
contactId: z.number().int().positive().optional(),
profile: contactProfileSchema.optional()
}
},
async ({ action, contactId, profile }) => {
if (action === 'create') {
if (!profile) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a contact profile when creating a contact.'
}
]
};
}
const payload = toContactProfileInput(profile);
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: {
action,
contact
}
};
}
if (action === 'update') {
if (!contactId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide contactId when updating a contact.'
}
]
};
}
if (!profile) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a contact profile when updating a contact.'
}
]
};
}
const payload = toContactProfileInput(profile);
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: {
action,
contactId,
contact
}
};
}
if (!contactId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide contactId when deleting a contact.'
}
]
};
}
const result = await client.deleteContact(contactId);
logger.info({ contactId }, 'Deleted Monica contact');
return {
content: [
{
type: 'text' as const,
text: `Deleted contact ID ${contactId}.`
}
],
structuredContent: {
action,
contactId,
result
}
};
}
);
server.registerTool(
'monica_list_tasks',
{
title: 'List Monica tasks',
description: 'List tasks across Monica CRM or for a specific contact.',
inputSchema: {
contactId: z.number().int().positive().optional(),
status: z.enum(['open', 'completed', 'all']).optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional()
}
},
async ({ contactId, status, limit, page }) => {
const tasksResponse = await client.listTasks({ contactId, status, limit, page });
const tasks = tasksResponse.data.map(normalizeTask);
const scope = contactId ? `contact ${contactId}` : 'your account';
const summary = tasks.length
? `Fetched ${tasks.length} task(s) for ${scope}.`
: `No tasks found for ${scope} matching the criteria.`;
return {
content: [
{
type: 'text' as const,
text: summary
}
],
structuredContent: {
tasks,
pagination: {
currentPage: tasksResponse.meta.current_page,
lastPage: tasksResponse.meta.last_page,
perPage: tasksResponse.meta.per_page,
total: tasksResponse.meta.total
}
}
};
}
);
server.registerTool(
'monica_manage_relationship_type',
{
title: 'Manage relationship types',
description: 'List, inspect, create, update, or delete relationship types. Use this to manage the catalog of relationship types (like "Father", "Friend", "Colleague") that can be used when creating relationships between contacts.',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
relationshipTypeId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: relationshipTypePayloadSchema.optional()
}
},
async ({ action, relationshipTypeId, limit, page, payload }) => {
if (action === 'list') {
const response = await client.listRelationshipTypes({ limit, page });
const types = response.data.map(normalizeRelationshipType);
let text: string;
if (types.length === 0) {
text = 'No relationship types found.';
} else {
const typeList = types.map(type => {
const group = type.groupId ? ` (Group: ${type.groupId})` : '';
const deletable = type.isDeletable ? ' [Deletable]' : ' [Protected]';
return `• ID: ${type.id} - ${type.name} ↔ ${type.reverseName}${group}${deletable}`;
}).join('\n');
text = `Found ${types.length} relationship type${types.length === 1 ? '' : 's'}:\n\n${typeList}`;
}
return {
content: [
{
type: 'text' as const,
text
}
],
structuredContent: {
action,
types,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
if (action === 'get') {
if (!relationshipTypeId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide relationshipTypeId when retrieving a relationship type.'
}
]
};
}
const response = await client.getRelationshipType(relationshipTypeId);
const type = normalizeRelationshipType(response.data);
return {
content: [
{
type: 'text' as const,
text: `Retrieved relationship type "${type.name}" ↔ "${type.reverseName}" (ID: ${type.id}).`
}
],
structuredContent: {
action,
relationshipTypeId,
type
}
};
}
if (action === 'create') {
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide relationship type details (name, reverseName) when creating a relationship type.'
}
]
};
}
if (!payload.name || !payload.reverseName) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Both name and reverseName are required to create a relationship type.'
}
]
};
}
const createPayload = toRelationshipTypePayloadInput(payload);
const response = await client.createRelationshipType(createPayload);
const type = normalizeRelationshipType(response.data);
logger.info(
{
relationshipTypeId: type.id,
name: type.name,
reverseName: type.reverseName
},
'Created Monica relationship type'
);
return {
content: [
{
type: 'text' as const,
text: `Created relationship type "${type.name}" ↔ "${type.reverseName}" (ID: ${type.id}).`
}
],
structuredContent: {
action,
type
}
};
}
if (action === 'update') {
if (!relationshipTypeId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide relationshipTypeId when updating a relationship type.'
}
]
};
}
if (!payload || Object.keys(payload).length === 0) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide at least one field (name, reverseName, etc.) when updating a relationship type.'
}
]
};
}
const updatePayload = toRelationshipTypePayloadInput(payload);
const response = await client.updateRelationshipType(relationshipTypeId, updatePayload);
const type = normalizeRelationshipType(response.data);
logger.info({ relationshipTypeId }, 'Updated Monica relationship type');
return {
content: [
{
type: 'text' as const,
text: `Updated relationship type "${type.name}" ↔ "${type.reverseName}" (ID: ${type.id}).`
}
],
structuredContent: {
action,
relationshipTypeId,
type
}
};
}
if (!relationshipTypeId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide relationshipTypeId when deleting a relationship type.'
}
]
};
}
const result = await client.deleteRelationshipType(relationshipTypeId);
logger.info({ relationshipTypeId }, 'Deleted Monica relationship type');
return {
content: [
{
type: 'text' as const,
text: `Deleted relationship type ID ${relationshipTypeId}.`
}
],
structuredContent: {
action,
relationshipTypeId,
result
}
};
}
);
server.registerTool(
'monica_manage_group',
{
title: 'Manage Monica groups',
description:
'List, inspect, create, update, or delete contact groups. Use this to organize contacts into named collections (e.g., "Family", "Travel buddies").',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
groupId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: groupPayloadSchema.optional()
}
},
async ({ action, groupId, limit, page, payload }) => {
switch (action) {
case 'list': {
const response = await client.listGroups({ limit, page });
const groups = response.data.map(normalizeGroup);
const summaryLines = groups.map((group) => {
const contactLabel = group.contactCount === 1 ? 'contact' : 'contacts';
return `• ID ${group.id}: ${group.name} (${group.contactCount} ${contactLabel})`;
});
const text = groups.length
? `Found ${groups.length} group${groups.length === 1 ? '' : 's'}:\n\n${summaryLines.join('\n')}`
: 'No groups found.';
return {
content: [
{
type: 'text' as const,
text
}
],
structuredContent: {
action,
groups,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
case 'get': {
if (!groupId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide groupId when retrieving a group.'
}
]
};
}
const response = await client.getGroup(groupId);
const group = normalizeGroup(response.data);
const contactNames = group.contacts.map((contact) => contact.name || `Contact ${contact.id}`);
const contactsSummary = contactNames.length
? `Members: ${contactNames.join(', ')}`
: 'Members: none yet.';
return {
content: [
{
type: 'text' as const,
text: `Group ${group.name} (ID ${group.id}). ${contactsSummary}`
}
],
structuredContent: {
action,
groupId,
group
}
};
}
case 'create': {
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a group payload when creating a group (name).'
}
]
};
}
const response = await client.createGroup(toGroupCreatePayload(payload));
const group = normalizeGroup(response.data);
logger.info({ groupId: group.id }, 'Created Monica group');
return {
content: [
{
type: 'text' as const,
text: `Created group ${group.name} (ID ${group.id}).`
}
],
structuredContent: {
action,
group
}
};
}
case 'update': {
if (!groupId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide groupId when updating a group.'
}
]
};
}
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a group payload when updating a group (name).'
}
]
};
}
const response = await client.updateGroup(groupId, toGroupUpdatePayload(payload));
const group = normalizeGroup(response.data);
logger.info({ groupId }, 'Updated Monica group');
return {
content: [
{
type: 'text' as const,
text: `Updated group ${group.name} (ID ${group.id}).`
}
],
structuredContent: {
action,
groupId,
group
}
};
}
case 'delete': {
if (!groupId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide groupId when deleting a group.'
}
]
};
}
const result = await client.deleteGroup(groupId);
logger.info({ groupId }, 'Deleted Monica group');
return {
content: [
{
type: 'text' as const,
text: `Deleted group ID ${groupId}.`
}
],
structuredContent: {
action,
groupId,
result
}
};
}
default:
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: `Unsupported action: ${action}.`
}
]
};
}
}
);
server.registerTool(
'monica_manage_reminder',
{
title: 'Manage Monica reminders',
description:
'List, inspect, create, update, or delete reminders. Use this to schedule follow-ups or stay-in-touch nudges for specific contacts.',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
reminderId: z.number().int().positive().optional(),
contactId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: reminderPayloadSchema.optional()
}
},
async ({ action, reminderId, contactId, limit, page, payload }) => {
switch (action) {
case 'list': {
const response = await client.listReminders({ contactId, limit, page });
const reminders = response.data.map(normalizeReminder);
const scope = contactId ? `contact ${contactId}` : 'your account';
const text = reminders.length
? `Found ${reminders.length} reminder${reminders.length === 1 ? '' : 's'} for ${scope}.`
: `No reminders found for ${scope}.`;
return {
content: [
{
type: 'text' as const,
text
}
],
structuredContent: {
action,
contactId,
reminders,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
case 'get': {
if (!reminderId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide reminderId when retrieving a reminder.'
}
]
};
}
const response = await client.getReminder(reminderId);
const reminder = normalizeReminder(response.data);
const contactName = reminder.contact?.name || `Contact ${reminder.contactId}`;
return {
content: [
{
type: 'text' as const,
text: `Reminder "${reminder.title}" for ${contactName}. Next due ${reminder.nextExpectedDate ?? 'unknown'}.`
}
],
structuredContent: {
action,
reminderId,
reminder
}
};
}
case 'create': {
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a reminder payload when creating a reminder.'
}
]
};
}
let createPayload: CreateReminderPayload;
try {
createPayload = toReminderCreatePayload(payload);
} catch (error) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: (error as Error).message
}
]
};
}
const response = await client.createReminder(createPayload);
const reminder = normalizeReminder(response.data);
logger.info({ reminderId: reminder.id, contactId: reminder.contactId }, 'Created Monica reminder');
return {
content: [
{
type: 'text' as const,
text: `Created reminder "${reminder.title}" for contact ${reminder.contactId}.`
}
],
structuredContent: {
action,
reminder
}
};
}
case 'update': {
if (!reminderId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide reminderId when updating a reminder.'
}
]
};
}
if (!payload) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide a reminder payload when updating a reminder.'
}
]
};
}
const patch = toReminderUpdatePayload(payload);
if (
patch.title === undefined &&
patch.description === undefined &&
patch.nextExpectedDate === undefined &&
patch.frequencyType === undefined &&
patch.frequencyNumber === undefined &&
patch.contactId === undefined
) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Include at least one field to update the reminder.'
}
]
};
}
const response = await client.updateReminder(reminderId, patch);
const reminder = normalizeReminder(response.data);
logger.info({ reminderId, contactId: reminder.contactId }, 'Updated Monica reminder');
return {
content: [
{
type: 'text' as const,
text: `Updated reminder ${reminderId} ("${reminder.title}").`
}
],
structuredContent: {
action,
reminderId,
reminder
}
};
}
case 'delete': {
if (!reminderId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide reminderId when deleting a reminder.'
}
]
};
}
const result = await client.deleteReminder(reminderId);
logger.info({ reminderId }, 'Deleted Monica reminder');
return {
content: [
{
type: 'text' as const,
text: `Deleted reminder ID ${reminderId}.`
}
],
structuredContent: {
action,
reminderId,
result
}
};
}
default:
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: `Unsupported action: ${action}.`
}
]
};
}
}
);
server.registerTool(
'monica_manage_relationship_type_group',
{
title: 'Browse relationship type groups',
description: 'List and inspect relationship type groups. These are system-defined categories (like "love", "family", "friend", "work") used to organize relationship types. Note: This endpoint is read-only.',
inputSchema: {
action: z.enum(['list', 'get']),
relationshipTypeGroupId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional()
}
},
async ({ action, relationshipTypeGroupId, limit, page }) => {
if (action === 'list') {
const response = await client.listRelationshipTypeGroups({ limit, page });
const groups = response.data.map(normalizeRelationshipTypeGroup);
let text: string;
if (groups.length === 0) {
text = 'No relationship type groups found.';
} else {
const groupList = groups.map(group => {
const deletable = group.isDeletable ? ' [Deletable]' : ' [Protected]';
return `• ID: ${group.id} - ${group.name}${deletable}`;
}).join('\n');
text = `Found ${groups.length} relationship type group${groups.length === 1 ? '' : 's'}:\n\n${groupList}`;
}
return {
content: [
{
type: 'text' as const,
text
}
],
structuredContent: {
action,
groups,
pagination: {
currentPage: response.meta.current_page,
lastPage: response.meta.last_page,
perPage: response.meta.per_page,
total: response.meta.total
}
}
};
}
if (action === 'get') {
if (!relationshipTypeGroupId) {
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: 'Provide relationshipTypeGroupId when retrieving a relationship type group.'
}
]
};
}
const response = await client.getRelationshipTypeGroup(relationshipTypeGroupId);
const group = normalizeRelationshipTypeGroup(response.data);
return {
content: [
{
type: 'text' as const,
text: `Retrieved relationship type group "${group.name}" (ID: ${group.id}).`
}
],
structuredContent: {
action,
relationshipTypeGroupId,
group
}
};
}
return {
isError: true as const,
content: [
{
type: 'text' as const,
text: `Action "${action}" is not supported. Relationship type groups are read-only. Supported actions: list, get.`
}
]
};
}
);
server.registerTool(
'monica_manage_tag',
{
title: 'Manage Monica tags',
description: 'List, inspect, create, update, or delete tags. Tags allow you to group and categorize contacts.',
inputSchema: {
action: z.enum(['list', 'get', 'create', 'update', 'delete']),
tagId: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(100).optional(),
page: z.number().int().min(1).optional(),
payload: tagPayloadSchema.optional()
}
},
async ({ action, tagId, payload, limit, page }) => {
switch (action) {
case 'list': {
const result = await client.listTags(limit, page);
const tags = result.data.map(normalizeTag);
return {
content: [
{
type: 'text' as const,
text: `Found ${result.meta.total} tags:\n${tags.map(tag => `• ${tag.name} (ID: ${tag.id})`).join('\n')}`
}
]
};
}
case 'get': {
if (!tagId) {
throw new Error('tagId is required for get action');
}
const result = await client.getTag(tagId);
const tag = normalizeTag(result.data);
return {
content: [
{
type: 'text' as const,
text: `Tag Details:\n• Name: ${tag.name}\n• Slug: ${tag.nameSlug}\n• Created: ${tag.createdAt}\n• Updated: ${tag.updatedAt}`
}
]
};
}
case 'create': {
if (!payload) {
throw new Error('payload is required for create action');
}
const input = toTagPayloadInput(payload);
const result = await client.createTag(input);
const tag = normalizeTag(result.data);
return {
content: [
{
type: 'text' as const,
text: `Created tag "${tag.name}" (ID: ${tag.id})`
}
]
};
}
case 'update': {
if (!tagId) {
throw new Error('tagId is required for update action');
}
if (!payload) {
throw new Error('payload is required for update action');
}
const input = toTagPayloadInput(payload);
const result = await client.updateTag(tagId, input);
const tag = normalizeTag(result.data);
return {
content: [
{
type: 'text' as const,
text: `Updated tag "${tag.name}" (ID: ${tag.id})`
}
]
};
}
case 'delete': {
if (!tagId) {
throw new Error('tagId is required for delete action');
}
await client.deleteTag(tagId);
return {
content: [
{
type: 'text' as const,
text: `Deleted tag with ID ${tagId}`
}
]
};
}
default:
throw new Error(`Unknown action: ${action}`);
}
}
);
server.registerTool(
'monica_health_check',
{
title: 'Test Monica connectivity',
description: 'Verify that the configured Monica credentials work.'
},
async () => {
await client.healthCheck();
return {
content: [
{
type: 'text' as const,
text: 'Successfully connected to Monica CRM API.'
}
]
};
}
);
}
function toContactProfileInput(profile: ContactProfileForm): 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 toActivityPayloadInput(payload: ActivityPayloadForm): CreateActivityPayload {
const input: CreateActivityPayload = {
activityTypeId: payload.activityTypeId,
summary: payload.summary,
description: payload.description ?? null,
happenedAt: payload.happenedAt,
contactIds: payload.contactIds,
emotionIds: payload.emotionIds && payload.emotionIds.length ? payload.emotionIds : undefined
};
return input;
}
function toAddressPayloadInput(payload: AddressPayloadForm): CreateAddressPayload {
const base: CreateAddressPayload = {
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 ?? null
};
return base;
}
function toContactFieldPayloadInput(payload: ContactFieldPayloadForm): CreateContactFieldPayload {
return {
contactId: payload.contactId,
contactFieldTypeId: payload.contactFieldTypeId,
data: payload.data
};
}
function toContactFieldTypePayloadInput(payload: ContactFieldTypePayloadForm): CreateContactFieldTypePayload {
return {
name: payload.name,
fontawesomeIcon: payload.fontawesomeIcon ?? null,
protocol: payload.protocol ?? null,
delible: payload.delible,
kind: payload.kind ?? null
};
}
function toActivityTypePayloadInput(payload: ActivityTypePayloadForm): CreateActivityTypePayload {
return {
name: payload.name,
categoryId: payload.categoryId,
locationType: payload.locationType ?? null
};
}
function toActivityTypeCategoryPayloadInput(
payload: ActivityTypeCategoryPayloadForm
): CreateActivityTypeCategoryPayload {
return {
name: payload.name
};
}
function toTaskCreatePayload(payload: TaskPayloadForm): CreateTaskPayload {
return {
title: payload.title!,
description: payload.description ?? null,
status: payload.status ?? 'open',
completedAt: payload.completedAt ?? null,
contactId: payload.contactId!
};
}
function toTaskUpdatePayload(payload: TaskPayloadForm): UpdateTaskPayload {
return {
title: payload.title ?? undefined,
description: payload.description ?? undefined,
status: payload.status ?? undefined,
completedAt: payload.completedAt ?? undefined,
contactId: payload.contactId ?? undefined
};
}
function toNoteCreatePayload(payload: NotePayloadForm): CreateNotePayload {
return {
contactId: payload.contactId!,
body: payload.body!,
isFavorited: payload.isFavorited
};
}
function toNoteUpdatePayload(payload: NotePayloadForm): UpdateNotePayload {
return {
contactId: payload.contactId ?? undefined,
body: payload.body ?? undefined,
isFavorited: payload.isFavorited ?? undefined
};
}
function toRelationshipCreatePayload(payload: RelationshipPayloadForm): CreateRelationshipPayload {
if (
typeof payload.contactIsId !== 'number' ||
typeof payload.ofContactId !== 'number' ||
typeof payload.relationshipTypeId !== 'number'
) {
throw new Error('contactIsId, ofContactId, and relationshipTypeId are required to create a relationship.');
}
return {
contactIsId: payload.contactIsId,
ofContactId: payload.ofContactId,
relationshipTypeId: payload.relationshipTypeId
};
}
function toRelationshipUpdatePayload(payload: RelationshipPayloadForm): UpdateRelationshipPayload {
if (typeof payload.relationshipTypeId !== 'number') {
throw new Error('relationshipTypeId is required to update a relationship.');
}
return {
relationshipTypeId: payload.relationshipTypeId
};
}
function toGroupCreatePayload(payload: GroupPayloadForm): CreateGroupPayload {
return {
name: payload.name
};
}
function toGroupUpdatePayload(payload: GroupPayloadForm): UpdateGroupPayload {
return {
name: payload.name
};
}
function toReminderCreatePayload(payload: ReminderPayloadForm): CreateReminderPayload {
if (
!payload.title ||
!payload.nextExpectedDate ||
!payload.frequencyType ||
typeof payload.contactId !== 'number'
) {
throw new Error('Provide title, nextExpectedDate, frequencyType, and contactId when creating a reminder.');
}
return {
title: payload.title,
description: payload.description ?? null,
nextExpectedDate: payload.nextExpectedDate,
frequencyType: payload.frequencyType,
frequencyNumber: payload.frequencyNumber ?? null,
contactId: payload.contactId
};
}
function toReminderUpdatePayload(payload: ReminderPayloadForm): UpdateReminderPayload {
const result: UpdateReminderPayload = {};
if (payload.title !== undefined) {
result.title = payload.title;
}
if (payload.description !== undefined) {
result.description = payload.description;
}
if (payload.nextExpectedDate !== undefined) {
result.nextExpectedDate = payload.nextExpectedDate;
}
if (payload.frequencyType !== undefined) {
result.frequencyType = payload.frequencyType;
}
if (payload.frequencyNumber !== undefined) {
result.frequencyNumber = payload.frequencyNumber;
}
if (payload.contactId !== undefined) {
result.contactId = payload.contactId;
}
return result;
}
function toRelationshipTypePayloadInput(payload: RelationshipTypePayloadForm): CreateRelationshipTypePayload {
return {
name: payload.name,
reverseName: payload.reverseName,
relationshipTypeGroupId: payload.relationshipTypeGroupId ?? null,
delible: payload.delible
};
}
function toRelationshipTypeGroupPayloadInput(payload: RelationshipTypeGroupPayloadForm): CreateRelationshipTypeGroupPayload {
return {
name: payload.name,
delible: payload.delible
};
}
function toTagPayloadInput(payload: TagPayloadForm): CreateTagPayload {
return {
name: payload.name
};
}