UNPKG

@periskope/whatsapp-mcp

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,267 lines (1,242 loc) 72.1 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, chat_type, label, start_time, end_time) { const response = await fetch(`${CHATS_ENDPOINT}?offset=${offset}&limit=${limit}${chat_type ? `&chat_type=${chat_type}` : ''}${label ? `&label=${label}` : ''}${start_time ? `&start_time=${start_time}` : ''}${end_time ? `&end_time=${end_time}` : ''}`, { 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, start_time, end_time) { const formattedChatId = formatPhone(chatId, 'server'); const response = await fetch(`${CHATS_ENDPOINT}/${formattedChatId}/messages?offset=${offset}&limit=${limit}${start_time ? `&start_time=${start_time}` : ''}${end_time ? `&end_time=${end_time}` : ''}`, { 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(); } async searchChat(searchQuery) { const response = await fetch(`${CHATS_ENDPOINT}/search`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ search_query: searchQuery, }), }); 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(); } async searchMessage(searchQuery) { const response = await fetch(`${MESSAGES_ENDPOINT}/search`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ search_query: searchQuery, }), }); return await response.json(); } // TICKETS // async createTicket(subject, chat_id, quoted_message_id, labels, priority, due_date, status, assignee) { const response = await fetch(`${TICKETS_ENDPOINT}/create`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ subject, chat_id, quoted_message_id, labels, priority, due_date, status, assignee }), }); return await response.json(); } 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 createContact(contactId, contactName, isInternal, labels) { const response = await fetch(`${CONTACTS_ENDPOINT}/create`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ contactId, contactName, isInternal, labels }), }); return await response.json(); } 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(); } async searchContact(searchQuery) { const response = await fetch(`${CONTACTS_ENDPOINT}/search`, { method: 'POST', headers: defaultHeaders, body: JSON.stringify({ search_query: searchQuery, }), }); 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' }, chat_type: { type: 'string', description: 'Chat type, e.g. "user", "group"', optional: true }, label: { type: 'string', description: 'Label name of label id to filter chats by, e.g. "support" or "label-kbvlbnvesomvgqpt"', optional: true }, start_time: { type: 'string', description: 'Start time in ISO format e.g. "2025-04-25 or 2025-04-25T00:00:00Z"', optional: true }, end_time: { type: 'string', description: 'End time in ISO format e.g. "2025-04-25 or 2025-04-25T00:00:00Z"', optional: true }, }, required: ['offset', 'limit'], }, }; const searchChatTool = { name: 'periskope_search_chat', description: 'Searches for a chat by name', inputSchema: { type: 'object', properties: { search_query: { type: 'string', description: 'Search query' }, }, required: ['search_query'], }, }; 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' }, start_time: { type: 'string', description: 'Start time in ISO format e.g. "2025-04-25 or 2025-04-25T00:00:00Z"', optional: true }, end_time: { type: 'string', description: 'End time in ISO format e.g. "2025-04-25 or 2025-04-25T00:00:00Z"', optional: true }, }, 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'], }, }; const searchMessageTool = { name: 'periskope_search_message', description: 'Searches for a message by its content', inputSchema: { type: 'object', properties: { search_query: { type: 'string', description: 'Search query' }, }, required: ['search_query'], }, }; // TICKETS // const createTicketTool = { name: 'periskope_create_ticket', description: 'Creates a new ticket', inputSchema: { type: 'object', properties: { subject: { type: 'string', description: 'Subject of the ticket' }, chat_id: { type: 'string', description: 'Chat ID in format 919826000000@c.us' }, quoted_message_id: { type: 'string', description: 'ID of message to which ticket is attached to' }, labels: { type: 'string', description: 'Comma-separated labels' }, priority: { type: 'string', description: 'Priority of the ticket from "0" to "4"', optional: true }, due_date: { type: 'string', description: 'Due date in ISO format e.g. "2025-04-25 or 2025-04-25T00:00:00Z"', optional: true }, status: { type: 'string', description: 'Status of the ticket only "open"/ "closed"/ "inprogress"', optional: true }, assignee: { type: 'string', description: 'Email of assignee', optional: true }, }, required: ['subject', 'chat_id', 'quoted_message_id'], }, }; 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 createContactTool = { name: 'periskope_create_contact', description: 'Creates a new WhatsApp contact', inputSchema: { type: 'object', properties: { contact_id: { type: 'string', description: 'Contact ID in format 919826000000@c.us' }, contact_name: { type: 'string', description: 'Name of the contact' }, is_internal: { type: 'boolean', description: 'Whether the contact is internal member of the team or an external customer' }, labels: { type: 'string', description: 'Comma-separated labels' }, }, required: ['contact_id', 'contact_name'], }, }; 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'], }, }; const searchContactTool = { name: 'periskope_search_contact', description: 'Searches for a contact by its name', inputSchema: { type: 'object', properties: { search_query: { type: 'string', description: 'Search query' }, }, required: ['search_query'], }, }; // 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: - Search and filter: Search for chats, messages, contacts - 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 in markdown - IMPORTANT: Always send message as me (i.e. the person's phone number you are using to send the message) - periskope_forward_message: - Get message_id from existing messages - Can forward to multiple chats simultaneously - 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_search_chat: - Search for chats by name - Use when you need to find out chat_id when the user knows the name of the chat - Use to search for chats semantically - periskope_list_chats: - Get all chats - Use when you need to find out chat_id or chat_name of a lot of chats - Use when user doesn't know the name or number of the chat - Use when you need to filter chats by chat_type, label, created at (start_time, end_time) - periskope_get_ticket/periskope_update_ticket: - Use for customer support workflow - Status values depend on your organization's workflow - Priority helps triage issues - periskope_create_ticket: - Use when you need to create a new ticket - Create with priority, due_date, status, assignee if mentioned else just provide chat_id, subject, quoted_message_id - Priority can be low (1), medium (2), high (3), urgent (4) - Status can be open, closed, inprogress only - 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, inprogress only - Priority can be low (1), medium (2), high (3), urgent (4) - 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_search_message: - Search for messages by content - Use when you need to look for messages by their content or a tone of voice - Use to search for messages semantically using search terms that might be present inside the message - periskope_create_contact: - Use when you need to create a new contact - Create with is_internal, labels if mentioned else just provide contact_id, contact_name - 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), chat_type: z.string().optional(), label: z.string().optional(), start_time: z.string().optional(), end_time: z.string().optional(), }); 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(), }); const SearchChatArgsSchema = z.object({ search_query: z.string().describe('Search query'), }); // 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), start_time: z.string().optional(), end_time: z.string().optional(), }); const GetMessageByIdArgsSchema = z.object({ message_id: z.string().describe('ID of message to get'), }); const SearchMessageArgsSchema = z.object({ search_query: z.string().describe('Search query'), }); // TICKETS // const CreateTicketArgsSchema = z.object({ subject: z.string().describe('Subject of the ticket'), chat_id: z.string().describe('Chat ID in format 919826000000@c.us'), quoted_message_id: z.string().describe('ID of message to which ticket is attached to'), labels: z.string().describe('Comma-separated labels').optional(), priority: z.string().describe('Priority of the ticket from "0" to "4"').optional(), due_date: z.string().describe('Due date in ISO format e.g. "2025-04-25 or 2025-04-25T00:00:00Z"').optional(), status: z.string().describe('Status of the ticket only "open"/ "closed"/ "inprogress"').optional(), assignee: z.string().email().optional(), }); 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 CreateContactArgsSchema = z.object({ contact_id: z.string().describe('Contact ID in format 919826000000@c.us'), contact_name: z.string().describe('Name of the contact'), is_internal: z.boolean().describe('Whether the contact is internal member of the team or an external customer').optional(), labels: z.string().describe('Comma-separated labels').optional(), }); 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'), }); const SearchContactArgsSchema = z.object({ search_query: z.string().describe('Search query'), }); // 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, searchChatTool, // MESSAGES sendMessageTool, listMessagesInAChatTool, getMessageByIdTool, reactToMessageTool, searchMessageTool, // TICKETS getTicketTool, updateTicketTool, listTicketsTool, forwardMessageTool, editMessageTool, deleteMessageTool, // CONTACTS listContactsTool, updateContactLabelsTool, updateContactTool, getContactByIdTool, searchContactTool, // 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