Skip to main content
Glama
communications.ts21.5 kB
import { z } from 'zod'; import type { CreateConversationPayload, UpdateConversationPayload, CreateConversationMessagePayload, UpdateConversationMessagePayload, CreateCallPayload, UpdateCallPayload } from '../../client/MonicaClient.js'; import type { ToolRegistrationContext } from '../context.js'; import { normalizeConversation, normalizeCall } from '../../utils/formatters.js'; import { resolveContactFieldTypeId } from '../../utils/resolvers.js'; const conversationPayloadSchema = z.object({ happenedAt: z .string() .regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/u, 'happenedAt must use YYYY-MM-DD format.') .optional(), contactFieldTypeId: z.number().int().positive().optional(), contactFieldTypeName: z.string().min(1).max(255).optional(), contactId: z.number().int().positive().optional() }); type ConversationPayloadForm = z.infer<typeof conversationPayloadSchema>; const conversationMessagePayloadSchema = z.object({ writtenAt: z .string() .regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/u, 'writtenAt must use YYYY-MM-DD format.') .optional(), writtenByMe: z.boolean().optional(), content: z.string().min(1).max(1_000_000).optional(), contactId: z.number().int().positive().optional() }); type ConversationMessagePayloadForm = z.infer<typeof conversationMessagePayloadSchema>; const callPayloadSchema = z.object({ calledAt: z .string() .regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/u, 'calledAt must use YYYY-MM-DD format.') .optional(), contactId: z.number().int().positive().optional(), content: z.string().max(1_000_000).optional().nullable() }); type CallPayloadForm = z.infer<typeof callPayloadSchema>; export function registerCommunicationTools(context: ToolRegistrationContext): void { const { server, client, logger } = context; server.registerTool( 'monica_manage_conversation', { title: 'Manage Monica conversations', description: 'List, inspect, create, update, delete, or manage messages inside Monica conversations. Provide either contactFieldTypeId or contactFieldTypeName for the channel.', inputSchema: { action: z.enum([ 'list', 'get', 'create', 'update', 'delete', 'addMessage', 'updateMessage', 'deleteMessage' ]), conversationId: z.number().int().positive().optional(), messageId: 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: conversationPayloadSchema.optional(), messagePayload: conversationMessagePayloadSchema.optional() } }, async ({ action, conversationId, messageId, contactId, limit, page, payload, messagePayload }) => { switch (action) { case 'list': { const response = await client.listConversations({ contactId, limit, page }); const conversations = response.data.map(normalizeConversation); const scope = contactId ? `contact ${contactId}` : 'your account'; const textSummary = conversations.length ? `Found ${conversations.length} conversation${conversations.length === 1 ? '' : 's'} for ${scope}.` : `No conversations found for ${scope}.`; return { content: [ { type: 'text' as const, text: textSummary } ], structuredContent: { action, contactId, conversations, pagination: { currentPage: response.meta.current_page, lastPage: response.meta.last_page, perPage: response.meta.per_page, total: response.meta.total } } }; } case 'get': { if (!conversationId) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide conversationId when retrieving a conversation.' } ] }; } const response = await client.getConversation(conversationId); const conversation = normalizeConversation(response.data); const channel = conversation.channel.name; const contactName = conversation.contact?.name || `Contact ${conversation.contactId}`; return { content: [ { type: 'text' as const, text: `Conversation ${conversationId} via ${channel} with ${contactName}. ${conversation.messages.length} message(s).` } ], structuredContent: { action, conversationId, conversation } }; } case 'create': { if (!payload) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide happenedAt, contactFieldTypeId or contactFieldTypeName, and contactId when creating a conversation.' } ] }; } let input: CreateConversationPayload; try { input = await toConversationCreatePayload(client, payload); } catch (error) { return { isError: true as const, content: [ { type: 'text' as const, text: (error as Error).message } ] }; } const response = await client.createConversation(input); const conversation = normalizeConversation(response.data); logger.info({ conversationId: conversation.id, contactId: conversation.contactId }, 'Created Monica conversation'); return { content: [ { type: 'text' as const, text: `Created conversation ${conversation.id} for contact ${conversation.contactId}.` } ], structuredContent: { action, conversation } }; } case 'update': { if (!conversationId) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide conversationId when updating a conversation.' } ] }; } if (!payload) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide happenedAt and channel info when updating a conversation.' } ] }; } let input: UpdateConversationPayload; try { input = toConversationUpdatePayload(payload); } catch (error) { return { isError: true as const, content: [ { type: 'text' as const, text: (error as Error).message } ] }; } const response = await client.updateConversation(conversationId, input); const conversation = normalizeConversation(response.data); logger.info({ conversationId }, 'Updated Monica conversation'); return { content: [ { type: 'text' as const, text: `Updated conversation ${conversationId}.` } ], structuredContent: { action, conversationId, conversation } }; } case 'delete': { if (!conversationId) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide conversationId when deleting a conversation.' } ] }; } const result = await client.deleteConversation(conversationId); logger.info({ conversationId }, 'Deleted Monica conversation'); return { content: [ { type: 'text' as const, text: `Deleted conversation ID ${conversationId}.` } ], structuredContent: { action, conversationId, result } }; } case 'addMessage': { if (!conversationId) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide conversationId when adding a message.' } ] }; } if (!messagePayload) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide message details when adding to a conversation.' } ] }; } let input: CreateConversationMessagePayload; try { input = toConversationMessageCreatePayload(messagePayload); } catch (error) { return { isError: true as const, content: [ { type: 'text' as const, text: (error as Error).message } ] }; } const response = await client.addConversationMessage(conversationId, input); const conversation = normalizeConversation(response.data); logger.info({ conversationId, contactId: input.contactId }, 'Added Monica conversation message'); return { content: [ { type: 'text' as const, text: `Added message to conversation ${conversationId}.` } ], structuredContent: { action, conversationId, conversation } }; } case 'updateMessage': { if (!conversationId || !messageId) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide conversationId and messageId when updating a message.' } ] }; } if (!messagePayload) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide message details when updating a conversation message.' } ] }; } let input: UpdateConversationMessagePayload; try { input = toConversationMessageUpdatePayload(messagePayload); } catch (error) { return { isError: true as const, content: [ { type: 'text' as const, text: (error as Error).message } ] }; } const response = await client.updateConversationMessage(conversationId, messageId, input); const conversation = normalizeConversation(response.data); logger.info({ conversationId, messageId }, 'Updated Monica conversation message'); return { content: [ { type: 'text' as const, text: `Updated message ${messageId} in conversation ${conversationId}.` } ], structuredContent: { action, conversationId, messageId, conversation } }; } case 'deleteMessage': { if (!conversationId || !messageId) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide conversationId and messageId when deleting a conversation message.' } ] }; } const result = await client.deleteConversationMessage(conversationId, messageId); logger.info({ conversationId, messageId }, 'Deleted Monica conversation message'); return { content: [ { type: 'text' as const, text: `Deleted message ${messageId} from conversation ${conversationId}.` } ], structuredContent: { action, conversationId, messageId, result } }; } default: return { isError: true as const, content: [ { type: 'text' as const, text: `Unsupported action: ${action}.` } ] }; } } ); server.registerTool( 'monica_manage_call', { title: 'Log Monica calls', description: 'List, inspect, create, update, or delete logged phone calls. Use this to capture quick notes about conversations with contacts.', inputSchema: { action: z.enum(['list', 'get', 'create', 'update', 'delete']), callId: 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: callPayloadSchema.optional() } }, async ({ action, callId, contactId, limit, page, payload }) => { switch (action) { case 'list': { const response = await client.listCalls({ contactId, limit, page }); const calls = response.data.map(normalizeCall); const scope = contactId ? `contact ${contactId}` : 'your account'; const textSummary = calls.length ? `Found ${calls.length} call${calls.length === 1 ? '' : 's'} for ${scope}.` : `No calls found for ${scope}.`; return { content: [ { type: 'text' as const, text: textSummary } ], structuredContent: { action, contactId, calls, pagination: { currentPage: response.meta.current_page, lastPage: response.meta.last_page, perPage: response.meta.per_page, total: response.meta.total } } }; } case 'get': { if (!callId) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide callId when retrieving a call.' } ] }; } const response = await client.getCall(callId); const call = normalizeCall(response.data); const contactName = call.contact?.name || `Contact ${call.contactId}`; return { content: [ { type: 'text' as const, text: `Call ${call.id} with ${contactName} on ${call.calledAt ?? 'unknown date'}.` } ], structuredContent: { action, callId, call } }; } case 'create': { if (!payload) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide calledAt, contactId, and optional content when logging a call.' } ] }; } let input: CreateCallPayload; try { input = toCallCreatePayload(payload); } catch (error) { return { isError: true as const, content: [ { type: 'text' as const, text: (error as Error).message } ] }; } const response = await client.createCall(input); const call = normalizeCall(response.data); logger.info({ callId: call.id, contactId: call.contactId }, 'Logged Monica call'); return { content: [ { type: 'text' as const, text: `Logged call ${call.id} for contact ${call.contactId}.` } ], structuredContent: { action, call } }; } case 'update': { if (!callId) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide callId when updating a call.' } ] }; } if (!payload) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide call details when updating a call.' } ] }; } const patch = toCallUpdatePayload(payload); if (patch.calledAt === undefined && patch.contactId === undefined && patch.content === undefined) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Include at least one field (calledAt, contactId, or content) to update the call.' } ] }; } const response = await client.updateCall(callId, patch); const call = normalizeCall(response.data); logger.info({ callId, contactId: call.contactId }, 'Updated Monica call'); return { content: [ { type: 'text' as const, text: `Updated call ${callId}.` } ], structuredContent: { action, callId, call } }; } case 'delete': { if (!callId) { return { isError: true as const, content: [ { type: 'text' as const, text: 'Provide callId when deleting a call.' } ] }; } const result = await client.deleteCall(callId); logger.info({ callId }, 'Deleted Monica call'); return { content: [ { type: 'text' as const, text: `Deleted call ID ${callId}.` } ], structuredContent: { action, callId, result } }; } default: return { isError: true as const, content: [ { type: 'text' as const, text: `Unsupported action: ${action}.` } ] }; } } ); } async function toConversationCreatePayload( client: ToolRegistrationContext['client'], payload: ConversationPayloadForm ): Promise<CreateConversationPayload> { if (!payload.happenedAt) { throw new Error('Provide happenedAt when creating a conversation.'); } if (typeof payload.contactId !== 'number') { throw new Error('Provide contactId when creating a conversation.'); } const contactFieldTypeId = await resolveContactFieldTypeId(client, { contactFieldTypeId: payload.contactFieldTypeId, contactFieldTypeName: payload.contactFieldTypeName }); return { happenedAt: payload.happenedAt, contactFieldTypeId, contactId: payload.contactId }; } function toConversationUpdatePayload(payload: ConversationPayloadForm): UpdateConversationPayload { if (!payload.happenedAt) { throw new Error('Provide happenedAt when updating a conversation.'); } return { happenedAt: payload.happenedAt }; } function toConversationMessageCreatePayload( payload: ConversationMessagePayloadForm ): CreateConversationMessagePayload { if ( typeof payload.contactId !== 'number' || !payload.writtenAt || typeof payload.writtenByMe !== 'boolean' || !payload.content ) { throw new Error('Provide contactId, writtenAt, writtenByMe, and content when adding a conversation message.'); } return { contactId: payload.contactId, writtenAt: payload.writtenAt, writtenByMe: payload.writtenByMe, content: payload.content }; } function toConversationMessageUpdatePayload( payload: ConversationMessagePayloadForm ): UpdateConversationMessagePayload { if ( typeof payload.contactId !== 'number' || !payload.writtenAt || typeof payload.writtenByMe !== 'boolean' || !payload.content ) { throw new Error('Provide contactId, writtenAt, writtenByMe, and content when updating a conversation message.'); } return { contactId: payload.contactId, writtenAt: payload.writtenAt, writtenByMe: payload.writtenByMe, content: payload.content }; } function toCallCreatePayload(payload: CallPayloadForm): CreateCallPayload { if (!payload.calledAt || typeof payload.contactId !== 'number') { throw new Error('Provide calledAt and contactId when creating a call.'); } return { contactId: payload.contactId, calledAt: payload.calledAt, content: payload.content ?? null }; } function toCallUpdatePayload(payload: CallPayloadForm): UpdateCallPayload { const result: UpdateCallPayload = {}; if (payload.contactId !== undefined) { result.contactId = payload.contactId; } if (payload.calledAt !== undefined) { result.calledAt = payload.calledAt; } if (payload.content !== undefined) { result.content = payload.content ?? null; } return result; }

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