send-message
Send messages to Zulip channels or direct users using stream names or email addresses. Specify topic for channel messages and format content with Zulip Markdown.
Instructions
š¬ SEND MESSAGE: Send a message to a Zulip stream (channel) or direct message to users. IMPORTANT: For streams use exact names from 'get-subscribed-streams'. For DMs use actual email addresses from 'search-users' tool (NOT display names). Always include 'topic' for stream messages.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| type | Yes | 'stream' for channel messages, 'direct' for private messages | |
| to | Yes | For streams: channel name (e.g., 'general'). For direct: comma-separated user emails (e.g., 'user@example.com' or 'user1@example.com,user2@example.com') | |
| content | Yes | Message content using Zulip Markdown syntax. Support mentions (@**Name**), code blocks, links, etc. | |
| topic | No | Topic name for stream messages (required for streams, max length varies by server) |
Implementation Reference
- src/server.ts:404-428 (handler)MCP handler function for 'send-message' tool: validates inputs (requires topic for streams, validates emails for direct), constructs params, calls ZulipClient.sendMessage, returns success/error in MCP format.server.tool( "send-message", "š¬ SEND MESSAGE: Send a message to a Zulip stream (channel) or direct message to users. IMPORTANT: For streams use exact names from 'get-subscribed-streams'. For DMs use actual email addresses from 'search-users' tool (NOT display names). Always include 'topic' for stream messages.", SendMessageSchema.shape, async ({ type, to, content, topic }) => { try { if (type === 'stream' && !topic) { return createErrorResponse('Topic is required for stream messages. Think of it as a subject line for your message.'); } if (type === 'direct') { const validation = validateEmailList(to); if (!validation.isValid) { return createErrorResponse(`Invalid email format for direct message recipients: ${to}. Use 'search-users' tool to find correct email addresses. Don't use display names.`); } } const messageParams = { type, to, content, ...(topic && { topic }) }; const result = await zulipClient.sendMessage(messageParams); return createSuccessResponse(`Message sent successfully! Message ID: ${result.id}`); } catch (error) { return createErrorResponse(`Error sending message: ${error instanceof Error ? error.message : 'Unknown error'}`); } } );
- src/types.ts:113-119 (schema)Zod schema defining the input parameters for the send-message tool: type (stream/direct), to (stream name or emails), content, optional topic.export const SendMessageSchema = z.object({ type: z.enum(["stream", "direct"]).describe("'stream' for channel messages, 'direct' for private messages"), to: z.string().describe("For streams: channel name (e.g., 'general'). For direct: comma-separated user emails (e.g., 'user@example.com' or 'user1@example.com,user2@example.com')"), content: z.string().describe("Message content using Zulip Markdown syntax. Support mentions (@**Name**), code blocks, links, etc."), topic: z.string().optional().describe("Topic name for stream messages (required for streams, max length varies by server)") });
- src/server.ts:404-428 (registration)Registers the 'send-message' tool with the MCP server using server.tool(), providing name, description, schema, and handler function.server.tool( "send-message", "š¬ SEND MESSAGE: Send a message to a Zulip stream (channel) or direct message to users. IMPORTANT: For streams use exact names from 'get-subscribed-streams'. For DMs use actual email addresses from 'search-users' tool (NOT display names). Always include 'topic' for stream messages.", SendMessageSchema.shape, async ({ type, to, content, topic }) => { try { if (type === 'stream' && !topic) { return createErrorResponse('Topic is required for stream messages. Think of it as a subject line for your message.'); } if (type === 'direct') { const validation = validateEmailList(to); if (!validation.isValid) { return createErrorResponse(`Invalid email format for direct message recipients: ${to}. Use 'search-users' tool to find correct email addresses. Don't use display names.`); } } const messageParams = { type, to, content, ...(topic && { topic }) }; const result = await zulipClient.sendMessage(messageParams); return createSuccessResponse(`Message sent successfully! Message ID: ${result.id}`); } catch (error) { return createErrorResponse(`Error sending message: ${error instanceof Error ? error.message : 'Unknown error'}`); } } );
- src/zulip/client.ts:76-155 (helper)ZulipClient.sendMessage helper: constructs API payload for /messages endpoint, handles direct (array of emails) vs stream (name + topic), tries JSON then form-encoded request to Zulip API.async sendMessage(params: { type: 'stream' | 'direct'; to: string; content: string; topic?: string; }): Promise<{ id: number }> { if (process.env.NODE_ENV === 'development') { debugLog('š Debug - sendMessage called with:', JSON.stringify(params, null, 2)); } // Use the type directly - newer API supports "direct" const payload: any = { type: params.type, content: params.content }; // Handle recipients based on message type if (params.type === 'direct') { // For direct messages, handle both single and multiple recipients const recipients = params.to.includes(',') ? params.to.split(',').map(email => email.trim()) : [params.to.trim()]; // Try both formats to see which works payload.to = recipients; // Array format first debugLog('š Debug - Direct message recipients:', recipients); } else { // For stream messages, 'to' is the stream name payload.to = params.to; if (params.topic) { payload.topic = params.topic; } } debugLog('š Debug - Final payload:', JSON.stringify(payload, null, 2)); try { // Try JSON first (modern API) const response = await this.client.post('/messages', payload); debugLog('ā Debug - Message sent successfully:', response.data); return response.data; } catch (jsonError) { debugLog('ā ļø Debug - JSON request failed, trying form-encoded...'); if (jsonError instanceof Error) { debugLog('Error:', (jsonError as any).response?.data || jsonError.message); } // Fallback to form-encoded with different recipient format const formPayload = { ...payload }; if (params.type === 'direct') { // Try JSON string format for recipients const recipients = params.to.includes(',') ? params.to.split(',').map(email => email.trim()) : [params.to.trim()]; formPayload.to = JSON.stringify(recipients); } debugLog('š Debug - Form payload:', JSON.stringify(formPayload, null, 2)); const response = await this.client.post('/messages', formPayload, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, transformRequest: [(data) => { const params = new URLSearchParams(); for (const key in data) { if (data[key] !== undefined) { params.append(key, String(data[key])); } } const formString = params.toString(); debugLog('š Debug - Form-encoded string:', formString); return formString; }] }); debugLog('ā Debug - Form-encoded message sent successfully:', response.data); return response.data; } }