UNPKG

@periskope/whatsapp-mcp-staging

Version:

The Periskope WhatsApp MCP (Model Context Protocol) tool provides an interface to interact with Periskope's WhatsApp API services through Claude, GPT, and other AI assistants that support the Model Context Protocol.

1,248 lines (1,226 loc) 60.2 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { CHATS_ENDPOINT, CONTACTS_ENDPOINT, CREATE_CHAT_ENDPOINT, MESSAGES_ENDPOINT, SEND_MESSAGE_ENDPOINT, TICKETS_ENDPOINT, } from './_utils/constants.js'; import { formatPhone } from './_utils/utils.js'; // Environment variables const PERISKOPE_API_KEY = process.env.PERISKOPE_API_KEY; const PERISKOPE_PHONE_ID = process.env.PERISKOPE_PHONE_ID; if (!PERISKOPE_API_KEY) { console.error('PERISKOPE_API_KEY is not set'); process.exit(1); } if (!PERISKOPE_PHONE_ID) { console.error('PERISKOPE_PHONE_ID is not set'); process.exit(1); } const defaultHeaders = { Authorization: `Bearer ${PERISKOPE_API_KEY}`, 'Content-Type': 'application/json', 'x-phone': PERISKOPE_PHONE_ID, }; // Client class to handle API calls class PeriskopeClient { // ----------------------------------------------------------------------------------------------// // TOOL FUNCTIONS // // ----------------------------------------------------------------------------------------------// // CHATS // async getChatById(chatId) { if (!chatId) { throw new Error('Invalid arguments: Chat ID is required'); } const formattedChatId = formatPhone(chatId, 'server'); const response = await fetch(`${CHATS_ENDPOINT}/${formattedChatId}`, { method: 'GET', headers: defaultHeaders, }); return await response.json(); } async listChats(offset = 0, limit = 20) { const response = await fetch(`${CHATS_ENDPOINT}?offset=${offset}&limit=${limit}`, { method: 'GET', headers: defaultHeaders, }); const data = await response.json(); return data.chats.map((chat) => ({ uri: `periskope-chat:///${chat.chat_id}`, mimeType: 'application/json', name: chat.name || chat.chat_id, description: `WhatsApp chat: ${chat.name || chat.chat_id}`, metadata: { chat_id: chat.chat_id, type: chat.type, }, })); } async createChat(groupName, participants, options) { const formattedParticipants = participants.map((phone) => formatPhone(phone, 'server')); const response = await fetch(CREATE_CHAT_ENDPOINT, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ group_name: groupName, participants: formattedParticipants, options, }), }); return await response.json(); } async listMessagesInAChat(chatId, offset = 0, limit = 20) { const formattedChatId = formatPhone(chatId, 'server'); const response = await fetch(`${CHATS_ENDPOINT}/${formattedChatId}/messages?offset=${offset}&limit=${limit}`, { method: 'GET', headers: defaultHeaders, }); return await response.json(); } async updateChatLabels(chatIds, labels) { const formattedChatIds = chatIds.map((chatId) => formatPhone(chatId, 'server')); const response = await fetch(`${CHATS_ENDPOINT}/labels`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ chat_ids: formattedChatIds, labels: labels, }), }); return await response.json(); } async updateChat(chatId, labels, assigned_to, custom_properties) { const formattedChatId = formatPhone(chatId, 'server'); const response = await fetch(`${CHATS_ENDPOINT}/${formattedChatId}`, { method: 'PATCH', headers: defaultHeaders, body: JSON.stringify({ labels: labels, assigned_to: assigned_to, custom_properties: custom_properties, }), }); return await response.json(); } async updateChatSettings(chatId, description, image, messagesAdminsOnly, infoAdminsOnly, addMembersAdminsOnly, name) { const formattedChatId = formatPhone(chatId, 'server'); const response = await fetch(`${CHATS_ENDPOINT}/${formattedChatId}/settings`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ description: description, image: image, messagesAdminsOnly: messagesAdminsOnly, infoAdminsOnly: infoAdminsOnly, addMembersAdminsOnly: addMembersAdminsOnly, name: name, }), }); return await response.json(); } // MESSAGES // async sendWhatsappMessage(phone, message) { if (!phone || !message) { throw new Error('Invalid arguments: Phone and message are required'); } const formattedPhone = formatPhone(phone, 'server'); const response = await fetch(SEND_MESSAGE_ENDPOINT, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ chat_id: formattedPhone, message: message, message_type: 'chat', }), }); return await response.json(); } async forwardMessage(messageId, forwardChatIds) { const formattedChatIds = forwardChatIds.map((chatId) => formatPhone(chatId, 'server')); const response = await fetch(`${MESSAGES_ENDPOINT}/${messageId}/forward`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ forward_chat_ids: formattedChatIds, }), }); return await response.json(); } async editMessage(messageId, editedBody) { const response = await fetch(`${MESSAGES_ENDPOINT}/${messageId}`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ edited_body: editedBody }), }); return await response.json(); } async deleteMessage(messageId) { const response = await fetch(`${MESSAGES_ENDPOINT}/${messageId}/delete`, { method: 'POST', headers: defaultHeaders, }); return await response.json(); } async reactToMessage(messageId, reaction) { const response = await fetch(`${MESSAGES_ENDPOINT}/${messageId}/react`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ reaction: reaction }), }); return await response.json(); } async getMessageById(messageId) { const response = await fetch(`${MESSAGES_ENDPOINT}/${messageId}`, { method: 'GET', headers: defaultHeaders, }); return await response.json(); } // TICKETS // async getTicketById(ticketId) { const response = await fetch(`${TICKETS_ENDPOINT}/${ticketId}`, { method: 'GET', headers: defaultHeaders, }); return await response.json(); } async listTickets(offset = 0, limit = 10) { const response = await fetch(`${TICKETS_ENDPOINT}?offset=${offset}&limit=${limit}`, { method: 'GET', headers: defaultHeaders, }); const data = await response.json(); return data.tickets.map((ticket) => ({ uri: `periskope-ticket:///${ticket.ticket_id}`, mimeType: 'application/json', name: ticket.subject, description: `Ticket: ${ticket.subject}`, metadata: { ticket_id: ticket.ticket_id, status: ticket.status, priority: ticket.priority, }, })); } async updateTicket(ticketId, assignee, status, priority, subject, labels) { const response = await fetch(`${TICKETS_ENDPOINT}/${ticketId}`, { method: 'PATCH', headers: defaultHeaders, body: JSON.stringify({ assignee: assignee, status: status, priority: priority, subject: subject, labels: labels, }), }); return await response.json(); } // CONTACTS // async getContactById(contactId) { const formattedContactId = formatPhone(contactId, 'server'); const response = await fetch(`${CONTACTS_ENDPOINT}/${formattedContactId}`, { method: 'GET', headers: defaultHeaders, }); return await response.json(); } async listContacts(offset = 0, limit = 100) { const response = await fetch(`${CONTACTS_ENDPOINT}?offset=${offset}&limit=${limit}`, { method: 'GET', headers: defaultHeaders, }); const data = await response.json(); return data.contacts.map((contact) => ({ uri: `periskope-contact:///${contact.contact_id}`, mimeType: 'application/json', name: contact.contact_name || contact.contact_id, description: `WhatsApp contact: ${contact.contact_name || contact.contact_id}`, metadata: { contact_id: contact.contact_id, labels: contact.labels, }, })); } async updateContactLabels(contactIds, labels) { const response = await fetch(`${CONTACTS_ENDPOINT}/labels`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ contact_ids: contactIds, labels: labels, }), }); return await response.json(); } async updateContact(contactId, isInternal, contactName, labels) { const formattedContactId = formatPhone(contactId, 'server'); const response = await fetch(`${CONTACTS_ENDPOINT}/${formattedContactId}`, { method: 'PATCH', headers: defaultHeaders, body: JSON.stringify({ is_internal: isInternal, contact_name: contactName, labels: labels, }), }); return await response.json(); } // CHAT ACTIONS // async addParticipantsToGroup(chatId, participants) { const formattedChatId = formatPhone(chatId, 'server'); const response = await fetch(`${CHATS_ENDPOINT}/${formattedChatId}/add`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ participants: participants, }), }); return await response.json(); } async promoteParticipantsToAdmins(chatId, participants) { const formattedChatId = formatPhone(chatId, 'server'); const response = await fetch(`${CHATS_ENDPOINT}/${formattedChatId}/promote`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ participants: participants, }), }); return await response.json(); } async demoteParticipantsFromAdmins(chatId, participants) { const formattedChatId = formatPhone(chatId, 'server'); const response = await fetch(`${CHATS_ENDPOINT}/${formattedChatId}/demote`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ participants: participants, }), }); return await response.json(); } async removeParticipantsFromGroup(chatId, participants) { const formattedChatId = formatPhone(chatId, 'server'); const response = await fetch(`${CHATS_ENDPOINT}/${formattedChatId}/remove`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ participants: participants, }), }); return await response.json(); } } // ----------------------------------------------------------------------------------------------// // TOOL DEFINITIONS // // ----------------------------------------------------------------------------------------------// // CHATS // const getChatTool = { name: 'periskope_get_chat', description: 'Gets details about a specific WhatsApp chat', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID in format 919826000000@c.us', }, }, required: ['chat_id'], }, }; const listChatsTool = { name: 'periskope_list_chats', description: 'Lists all WhatsApp chats', inputSchema: { type: 'object', properties: { offset: { type: 'number', description: 'Offset' }, limit: { type: 'number', description: 'Limit' }, }, required: ['offset', 'limit'], }, }; const createChatTool = { name: 'periskope_create_chat', description: 'Creates a new WhatsApp group chat', inputSchema: { type: 'object', properties: { group_name: { type: 'string', description: 'Name for the group' }, participants: { type: 'array', items: { type: 'string' }, description: 'List of phone numbers in format 919826000000@c.us', }, options: { type: 'object', properties: { addMembersAdminsOnly: { type: 'boolean' }, description: { type: 'string' }, image: { type: 'string' }, infoAdminsOnly: { type: 'boolean' }, messagesAdminsOnly: { type: 'boolean' }, }, }, }, required: ['group_name', 'participants'], }, }; const listMessagesInAChatTool = { name: 'periskope_list_messages_in_a_chat', description: 'Lists all messages in a specific WhatsApp chat', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID in format 919826000000@c.us' }, offset: { type: 'number', description: 'Offset' }, limit: { type: 'number', description: 'Limit' }, }, required: ['chat_id', 'offset', 'limit'], }, }; const updateChatLabelsTool = { name: 'periskope_update_chat_labels', description: 'Updates the labels of a WhatsApp chat', inputSchema: { type: 'object', properties: { chat_ids: { type: 'array', description: 'Array of chat IDs' }, labels: { type: 'string', description: 'Comma-separated labels' }, }, required: ['chat_ids', 'labels'], }, }; const updateChatTool = { name: 'periskope_update_chat', description: 'Updates a WhatsApp chat properties on periskope', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID in format 919826000000@c.us' }, labels: { type: 'string', description: 'Comma-separated labels', optional: true }, assigned_to: { type: 'string', description: 'Email of the assignee', optional: true }, custom_properties: { type: 'object', description: 'Custom properties', optional: true }, }, required: ['chat_id'], }, }; const updateChatSettingsTool = { name: 'periskope_update_chat_settings', description: 'Updates the settings of a WhatsApp chat', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID in format 919826000000@c.us' }, description: { type: 'string', description: 'Description of the chat', optional: true }, image: { type: 'string', description: 'Image of the chat in base64 format or a live url', optional: true }, messagesAdminsOnly: { type: 'boolean', description: 'Whether messages are only visible to admins', optional: true }, infoAdminsOnly: { type: 'boolean', description: 'Whether info is only visible to admins', optional: true }, addMembersAdminsOnly: { type: 'boolean', description: 'Whether adding members is only visible to admins', optional: true }, name: { type: 'string', description: 'Name of the chat', optional: true }, }, required: ['chat_id'], }, }; // MESSAGES // const sendMessageTool = { name: 'periskope_send_message', description: 'Sends a WhatsApp message to a specified number', inputSchema: { type: 'object', properties: { phone: { type: 'string', description: 'Phone number in format 919826000000@c.us', }, message: { type: 'string', description: 'Message text to send' }, }, required: ['phone', 'message'], }, }; const forwardMessageTool = { name: 'periskope_forward_message', description: 'Forwards a message to other chats', inputSchema: { type: 'object', properties: { message_id: { type: 'string', description: 'ID of message to forward' }, forward_chat_ids: { type: 'array', items: { type: 'string' }, description: 'List of chat IDs to forward to', }, }, required: ['message_id', 'forward_chat_ids'], }, }; const deleteMessageTool = { name: 'periskope_delete_message', description: 'Deletes a message from a chat', inputSchema: { type: 'object', properties: { message_id: { type: 'string', description: 'ID of message to delete' }, }, required: ['message_id'], }, }; const editMessageTool = { name: 'periskope_edit_message', description: 'Edits a message in a chat', inputSchema: { type: 'object', properties: { message_id: { type: 'string', description: 'ID of message to edit' }, edited_body: { type: 'string', description: 'New message text' }, }, required: ['message_id', 'edited_body'], }, }; const reactToMessageTool = { name: 'periskope_react_to_message', description: 'Reacts to a message', inputSchema: { type: 'object', properties: { message_id: { type: 'string', description: 'ID of message to react to' }, reaction: { type: 'string', description: 'Reaction to send' }, }, required: ['message_id', 'reaction'], }, }; const getMessageByIdTool = { name: 'periskope_get_message_by_id', description: 'Gets a message by its ID', inputSchema: { type: 'object', properties: { message_id: { type: 'string', description: 'ID of message to get' }, }, required: ['message_id'], }, }; // TICKETS // const listTicketsTool = { name: 'periskope_get_all_tickets', description: 'Gets all tickets', inputSchema: { type: 'object', properties: { offset: { type: 'number', description: 'Offset' }, limit: { type: 'number', description: 'Limit' }, }, required: ['offset', 'limit'], }, }; const getTicketTool = { name: 'periskope_get_ticket', description: 'Gets details about a specific ticket', inputSchema: { type: 'object', properties: { ticket_id: { type: 'string', description: 'Ticket ID' }, }, required: ['ticket_id'], }, }; const updateTicketTool = { name: 'periskope_update_ticket', description: 'Updates an existing ticket', inputSchema: { type: 'object', properties: { ticket_id: { type: 'string', description: 'Ticket ID' }, assignee: { type: 'string', description: 'Email of assignee' }, due_date: { type: 'string', description: 'Due date (ISO format)' }, labels: { type: 'string', description: 'Comma-separated labels' }, priority: { type: 'string', description: 'Priority level' }, status: { type: 'string', description: 'Ticket status' }, subject: { type: 'string', description: 'Ticket subject' }, }, required: ['ticket_id'], }, }; // CONTACTS // const updateContactLabelsTool = { name: 'periskope_update_contact_labels', description: 'Updates the labels of a WhatsApp contact', inputSchema: { type: 'object', properties: { contact_ids: { type: 'array', description: "An array of contact IDs e.g. ['919826000000@c.us', '919826000001@c.us']", }, labels: { type: 'string', description: 'Comma-separated labels' }, }, required: ['contact_ids', 'labels'], }, }; const listContactsTool = { name: 'periskope_list_contacts', description: 'Lists all WhatsApp contacts', inputSchema: { type: 'object', properties: { offset: { type: 'number', description: 'Offset' }, limit: { type: 'number', description: 'Limit' }, }, required: ['offset', 'limit'], }, }; const updateContactTool = { name: 'periskope_update_contact', description: 'Updates a WhatsApp contact', inputSchema: { type: 'object', properties: { contact_id: { type: 'string', description: 'Contact ID in format 919826000000@c.us' }, is_internal: { type: 'boolean', description: 'Whether the contact is internal member of the team or an external customer' }, contact_name: { type: 'string', description: 'Name of the contact' }, labels: { type: 'string', description: 'Comma-separated labels' }, }, required: ['contact_id'], }, }; const getContactByIdTool = { name: 'periskope_get_contact_by_id', description: 'Gets a contact by its ID', inputSchema: { type: 'object', properties: { contact_id: { type: 'string', description: 'Contact ID in format 919826000000@c.us' }, }, required: ['contact_id'], }, }; // CHAT ACTIONS // const addParticipantsToGroupTool = { name: 'periskope_add_participants_to_group', description: 'Adds participants to a WhatsApp group', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID in format 919826000000@c.us', }, participants: { type: 'array', description: 'An array of participant IDs', }, }, required: ['chat_id', 'participants'], }, }; const removeParticipantsFromGroupTool = { name: 'periskope_remove_participants_from_group', description: 'Removes participants from a WhatsApp group', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID in format 919826000000@c.us', }, participants: { type: 'array', description: 'An array of participant IDs', }, }, required: ['chat_id', 'participants'], }, }; const promoteParticipantsToAdminsTool = { name: 'periskope_promote_participants_to_admins', description: 'Promotes participants to admins in a WhatsApp group', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID in format 919826000000@c.us', }, participants: { type: 'array', description: 'An array of participant IDs', }, }, required: ['chat_id', 'participants'], }, }; const demoteParticipantsFromAdminsTool = { name: 'periskope_demote_participants_from_admins', description: 'Demotes participants from admins in a WhatsApp group', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID in format 919826000000@c.us', }, participants: { type: 'array', description: 'An array of participant IDs', }, }, required: ['chat_id', 'participants'], }, }; // Resource templates const resourceTemplates = [ { uriTemplate: 'periskope-chat:///{chatId}', name: 'WhatsApp Chat', description: 'A WhatsApp chat with its messages and details', parameters: { chatId: { type: 'string', description: 'The unique identifier of the WhatsApp chat', }, }, examples: ['periskope-chat:///919826000000@c.us'], }, { uriTemplate: 'periskope-ticket:///{ticketId}', name: 'Support Ticket', description: 'A support ticket with its details and associated chat', parameters: { ticketId: { type: 'string', description: 'The unique identifier of the ticket', }, }, examples: ['periskope-ticket:///ticket-123456'], }, { uriTemplate: 'periskope-contact:///{contactId}', name: 'WhatsApp Contact', description: 'Details about a WhatsApp contact', parameters: { contactId: { type: 'string', description: 'The unique identifier of the contact', }, }, examples: ['periskope-contact:///919826000000@c.us'], }, { uriTemplate: 'periskope-chats', name: 'All Chats', description: 'All WhatsApp chats', parameters: { offset: { type: 'number', description: 'Offset' }, limit: { type: 'number', description: 'Limit' }, }, examples: ['periskope-chats'], }, { uriTemplate: 'periskope-tickets', name: 'All Tickets', description: 'All tickets created on Periskope', parameters: { offset: { type: 'number', description: 'Offset' }, limit: { type: 'number', description: 'Limit' }, }, examples: ['periskope-tickets'], }, { uriTemplate: 'periskope-contacts', name: 'All Contacts', description: 'All WhatsApp contacts', parameters: { offset: { type: 'number', description: 'Offset' }, limit: { type: 'number', description: 'Limit' }, }, examples: ['periskope-contacts'], }, ]; // Server prompt const serverPrompt = { name: 'periskope-server-prompt', description: 'Instructions for using the Periskope MCP server effectively', instructions: `This server provides access to Periskope, a WhatsApp communication platform. Use it to manage chats, send messages, and handle support tickets. Key capabilities: - Communication: Send WhatsApp messages to individuals or groups - Chat management: Create groups, forward messages - Contact management: View and search contacts - Ticket handling: Create, view and update support tickets Tool Usage: - periskope_send_message: - Phone numbers should be in international format (e.g., "919826000000@c.us") - Message can include text formatting - periskope_create_chat: - Requires at least 2 participant phone numbers - group_name is required and should be brief but descriptive - Optional settings can control admin permissions - periskope_forward_message: - Get message_id from existing messages - Can forward to multiple chats simultaneously - periskope_get_ticket/periskope_update_ticket: - Use for customer support workflow - Status values depend on your organization's workflow - Priority helps triage issues - periskope_get_all_tickets: - Get all tickets - Use when you need to find out ticket_id or subject of a lot of tickets - Status can be open, closed, pending, etc. - Priority can be low (1), medium (2), high (3), urgent (4) - periskope_list_chats: - Get all chats - Use when you need to find out chat_id or chat_name of a lot of chats - Status can be active, inactive, pending, etc. - periskope_update_chat_settings: - Update the settings of a WhatsApp chat - Use when you need to update the description, image, messagesAdminsOnly, infoAdminsOnly, addMembersAdminsOnly, name of a chat - periskope_update_chat_labels: - Update the labels of a WhatsApp chat - Use when you need to update the labels of a chat - periskope_update_chat: - Update the settings of a WhatsApp chat - Use when you need to update the description, image, messagesAdminsOnly, infoAdminsOnly, addMembersAdminsOnly, name of a chat - periskope_update_contact_labels: - Update the labels of a WhatsApp contact - Use when you need to update the labels of a contact - periskope_update_contact: - Update the settings of a WhatsApp contact - Use when you need to update the is_internal, contact_name, labels of a contact - periskope_add_participants_to_group: - Add participants to a WhatsApp group - Use when you need to add participants to a group - periskope_remove_participants_from_group: - Remove participants from a WhatsApp group - Use when you need to remove participants from a group - periskope_promote_participants_to_admins: - Promote participants to admins in a WhatsApp group - Use when you need to promote participants to admins in a group - periskope_demote_participants_from_admins: - Demote participants from admins in a WhatsApp group - Use when you need to demote participants from admins in a group Best practices: - When sending messages: - Keep messages concise and focused - Use clear, friendly language - Double-check phone numbers for accuracy - When creating group chats: - Use descriptive names that identify the purpose - Only add relevant participants - Consider privacy implications Resource patterns: - periskope-chat:///{chatId} - For accessing specific chats - periskope-ticket:///{ticketId} - For viewing ticket details - periskope-contact:///{contactId} - For contact information - periskope-chats - For listing all chats - periskope-tickets - For listing all tickets - periskope-contacts - For listing all contacts The server uses the authenticated API key's permissions for all operations.`, }; // ----------------------------------------------------------------------------------------------// // ZOD SCHEMAS // // ----------------------------------------------------------------------------------------------// // CHATS // const GetChatArgsSchema = z.object({ chat_id: z.string().describe('Chat ID in format 919826000000@c.us'), }); const CreateChatArgsSchema = z.object({ group_name: z.string().describe('Name for the group'), participants: z.array(z.string()).describe('List of phone numbers'), options: z .object({ addMembersAdminsOnly: z.boolean().optional().default(false), description: z.string().optional(), image: z.string().optional(), infoAdminsOnly: z.boolean().optional().default(false), messagesAdminsOnly: z.boolean().optional().default(false), }) .optional(), }); const ListChatsArgsSchema = z.object({ offset: z.number().default(0), limit: z.number().default(10), }); const UpdateChatLabelsArgsSchema = z.object({ chat_ids: z.array(z.string()).describe('An array of chat IDs'), labels: z.string().describe('Comma-separated labels'), }); const UpdateChatArgsSchema = z.object({ chat_id: z.string().describe('Chat ID in format 919826000000@c.us'), labels: z.string().describe('Comma-separated labels').optional(), assigned_to: z.string().email().optional(), custom_properties: z.object({}).optional(), }); const UpdateChatSettingsArgsSchema = z.object({ chat_id: z.string().describe('Chat ID in format 919826000000@c.us'), description: z.string().optional(), image: z.string().optional(), messagesAdminsOnly: z.boolean().optional(), infoAdminsOnly: z.boolean().optional(), addMembersAdminsOnly: z.boolean().optional(), name: z.string().optional(), }); // MESSAGES // const SendMessageArgsSchema = z.object({ phone: z.string().describe('Phone number in format 919826000000@c.us'), message: z.string().describe('Message text to send'), }); const EditMessageArgsSchema = z.object({ message_id: z.string().describe('ID of message to edit'), edited_body: z.string().describe('New message text'), }); const ForwardMessageArgsSchema = z.object({ message_id: z.string().describe('ID of message to forward'), forward_chat_ids: z .array(z.string()) .describe('List of chat IDs to forward to'), }); const DeleteMessageArgsSchema = z.object({ message_id: z.string().describe('ID of message to delete'), }); const ReactToMessageArgsSchema = z.object({ message_id: z.string().describe('ID of message to react to'), reaction: z.string().describe('Reaction to add'), }); const ListMessagesInAChatArgsSchema = z.object({ chat_id: z.string().describe('Chat ID in format 919826000000@c.us'), offset: z.number().default(0), limit: z.number().default(10), }); const GetMessageByIdArgsSchema = z.object({ message_id: z.string().describe('ID of message to get'), }); // TICKETS // const GetTicketArgsSchema = z.object({ ticket_id: z.string().describe('Ticket ID'), }); const UpdateTicketArgsSchema = z.object({ ticket_id: z.string().describe('Ticket ID'), assignee: z.string().email().optional(), due_date: z.string().optional(), labels: z.string().optional(), priority: z.string().optional(), status: z.string().optional(), subject: z.string().optional(), }); const ListTicketsArgsSchema = z.object({ offset: z.number().default(0), limit: z.number().default(10), }); // CONTACTS // const UpdateContactLabelsArgsSchema = z.object({ contact_ids: z.array(z.string()).describe('An array of contact IDs'), labels: z.string().describe('Comma-separated labels'), }); const ListContactsArgsSchema = z.object({ offset: z.number().default(0), limit: z.number().default(10), }); const UpdateContactArgsSchema = z.object({ contact_id: z.string().describe('Contact ID in format 919826000000@c.us'), is_internal: z.boolean().describe('Whether the contact is internal member of the team or an external customer'), contact_name: z.string().describe('Name of the contact'), labels: z.string().describe('Comma-separated labels'), }); const GetContactByIdArgsSchema = z.object({ contact_id: z.string().describe('Contact ID in format 919826000000@c.us'), }); // CHAT ACTIONS // const RemoveParticipantsFromGroupArgsSchema = z.object({ chat_id: z.string().describe('Chat ID in format 919826000000@c.us'), participants: z.array(z.string()).describe('An array of participant IDs'), }); const PromoteParticipantsToAdminsArgsSchema = z.object({ chat_id: z.string().describe('Chat ID in format 919826000000@c.us'), participants: z.array(z.string()).describe('An array of participant IDs'), }); const AddParticipantsToGroupArgsSchema = z.object({ chat_id: z.string().describe('Chat ID in format 919826000000@c.us'), participants: z.array(z.string()).describe('An array of participant IDs'), }); const DemoteParticipantsFromAdminsArgsSchema = z.object({ chat_id: z.string().describe('Chat ID in format 919826000000@c.us'), participants: z.array(z.string()).describe('An array of participant IDs'), }); async function main() { try { console.error('Starting Periskope MCP Server...'); const periskopeClient = new PeriskopeClient(); const server = new Server({ name: 'periskope-mcp-server', version: '1.0.0', }, { capabilities: { prompts: { default: serverPrompt, }, resources: { templates: true, read: true, }, tools: {}, }, }); // Resource handlers server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ ...(await periskopeClient.listChats()), ...(await periskopeClient.listTickets()), ...(await periskopeClient.listContacts()), ], })); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = new URL(request.params.uri); const path = uri.pathname.replace(/^\//, ''); if (uri.protocol === 'periskope-chat:') { const chat = await periskopeClient.getChatById(path); return { contents: [ { uri: request.params.uri, mimeType: 'application/json', text: JSON.stringify(chat, null, 2), }, ], }; } if (uri.protocol === 'periskope-ticket:') { const ticket = await periskopeClient.getTicketById(path); return { contents: [ { uri: request.params.uri, mimeType: 'application/json', text: JSON.stringify(ticket, null, 2), }, ], }; } if (uri.protocol === 'periskope-contact:') { const contact = await periskopeClient.getContactById(path); return { contents: [ { uri: request.params.uri, mimeType: 'application/json', text: JSON.stringify(contact, null, 2), }, ], }; } throw new Error(`Unsupported resource URI: ${request.params.uri}`); }); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ // CHATS getChatTool, listChatsTool, createChatTool, updateChatLabelsTool, updateChatTool, updateChatSettingsTool, // MESSAGES sendMessageTool, listMessagesInAChatTool, getMessageByIdTool, reactToMessageTool, // TICKETS getTicketTool, updateTicketTool, listTicketsTool, forwardMessageTool, editMessageTool, deleteMessageTool, // CONTACTS listContactsTool, updateContactLabelsTool, updateContactTool, getContactByIdTool, // CHAT ACTIONS addParticipantsToGroupTool, removeParticipantsFromGroupTool, promoteParticipantsToAdminsTool, demoteParticipantsFromAdminsTool, ], })); server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { return { resourceTemplates: resourceTemplates, }; }); server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [serverPrompt], }; }); server.setRequestHandler(GetPromptRequestSchema, async (request) => { if (request.params.name === serverPrompt.name) { return { prompt: serverPrompt, }; } throw new Error(`Prompt not found: ${request.params.name}`); }); // Tool handlers server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; if (!args) { console.error('Missing arguments'); throw new Error('Missing arguments'); } switch (name) { // CHATS // case 'periskope_list_chats': { const validatedArgs = ListChatsArgsSchema.parse(args); const chats = await periskopeClient.listChats(validatedArgs.offset, validatedArgs.limit); return { content: [ { type: 'text', text: `Chats fetched successfully: ${JSON.stringify(chats)}`, }, ], }; } case 'periskope_get_chat': { const validatedArgs = GetChatArgsSchema.parse(args); const chat = await periskopeClient.getChatById(validatedArgs.chat_id); return { content: [ { type: 'text', text: `Chat fetched successfully: ${JSON.stringify(chat)}`, }, ], }; } case 'periskope_create_chat': { const validatedArgs = CreateChatArgsSchema.parse(args); const result = await periskopeClient.createChat(validatedArgs.group_name, validatedArgs.participants, validatedArgs.options); return { content: [ { type: 'text', text: `Chat created successfully: ${JSON.stringify(result)}`, }, ], }; } case 'periskope_update_chat_labels': { const validatedArgs = UpdateChatLabelsArgsSchema.parse(args); const result = await periskopeClient.updateChatLabels(validatedArgs.chat_ids, validatedArgs.labels); return { content: [ { type: 'text', text: `Chat labels updated successfully: ${JSON.stringify(result)}`, }, ], }; } case 'periskope_update_chat': { const validatedArgs = UpdateChatArgsSchema.parse(args); const result = await periskopeClient.updateChat(validatedArgs.chat_id, validatedArgs.labels, validatedArgs.assigned_to, validatedArgs.custom_properties); return { content: [ { type: 'text', text: `Chat updated successfully: ${JSON.stringify(result)}`, }, ], }; } case 'periskope_update_chat_settings': { const validatedArgs = UpdateChatSettingsArgsSchema.parse(args); const result = await periskopeClient.updateChatSettings(validatedArgs.chat_id, validatedArgs.description, validatedArgs.image, validatedArgs.messagesAdminsOnly, validatedArgs.infoAdminsOnly, validatedArgs.addMembersAdminsOnly, validatedArgs.name); return { content: [ { type: 'text', text: `Chat settings updated successfully: ${JSON.stringify(result)}`, }, ], }; } // MESSAGES // case 'periskope_get_message_by_id': { const validatedArgs = GetMessageByIdArgsSchema.parse(args); const message = await periskopeClient.getMessageById(validatedArgs.message_id); return { content: [ { type: 'text', text: `Message fetched successfully: ${JSON.stringify(message)}`, }, ], }; } case 'periskope_list_messages_in_a_chat': { const validatedArgs = ListMessagesInAChatArgsSchema.parse(args); const messages = await periskopeClient.listMessagesInAChat(validatedArgs.chat_id, validatedArgs.offset, validatedArgs.limit); return { content: [ { type: 'text', text: `Messages fetched successfully: ${JSON.stringify(messages)}`, }, ], }; } case 'periskope_forward_message': { const validatedArgs = ForwardMessageArgsSchema.parse(args); const result = await periskopeClient.forwardMessage(validatedArgs.message_id, validatedArgs.forward_chat_ids); return { content: [ { type: 'text', text: `Message forwarded successfully: ${JSON.stringify(result)}`, }, ], }; } case 'periskope_send_message': { const validatedArgs = SendMessageArgsSchema.parse(args); const result = await periskopeClient.sendWhatsappMessage(validatedArgs.phone, validatedArgs.message); return { content: [ { type: 'text', text: `Message sent successfully: ${JSON.stringify(result)}`, }, ], }; } case 'periskope_edit_message': { const validatedArgs = EditMessageArgsSchema.parse(args); const result = await periskopeClient.editMessage(validatedArgs.message_id, validatedArgs.edited_body); return { content: [ { type: 'text', text: `Message edited successfully: ${JSON.stringify(result)}`, }, ], }; } case 'periskope_delete_message': { const validatedArgs = DeleteMessageArgsSchema.parse(args); const result = await periskopeClient.deleteMessage(validatedArgs.message_id); return { content: [ { type: 'text', text: `Message deleted successfully: ${JSON.stringify(result)}`, }, ], }; } case 'periskope_react_to_message': { const validatedArgs = ReactToMessageArgsSchema.parse(args); const result = await periskopeClient.reactToMessage(validatedArgs.message_id, validatedArgs.reaction); return { content: [ { type: 'text', text: `Message reacted to successfully: ${JSON.stringify(result)}`, },