get_messages
Retrieve Upwork message conversations. List all conversations with unread status and last preview, or read full history of a specific room to monitor client responses, invitations, and contract discussions.
Instructions
Read Upwork messages.
Without room_id: lists all conversations with unread status and last message preview
With room_id: reads the full message history of that conversation
Use this to check for client responses, new invitations, and ongoing contract discussions.
Tool call template
This tool declares no required parameters. Add optional parameters from the input schema as needed.
{
"id": 1,
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"arguments": {},
"name": "get_messages"
}
}Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| room_id | No | Conversation room ID. Omit to list all conversations. | |
| limit | No | Max messages/conversations. Default: 20 | |
| unread_only | No | Only unread conversations. Default: false |
Implementation Reference
- src/tools/get-messages.ts:47-185 (handler)Main handler function that implements the 'get_messages' tool. If a room_id is provided, it navigates to the specific conversation room URL and scrapes messages from the DOM. Otherwise, it navigates to the messages/rooms listing and scrapes conversation previews. Uses Playwright page.evaluate to extract data from Upwork's DOM selectors.
export async function getMessages(input: GetMessagesInput): Promise<GetMessagesResult> { const page = await ensureLoggedIn(); try { if (input.room_id) { // Read specific conversation const url = `https://www.upwork.com/messages/rooms/${input.room_id}`; console.error('[getMessages] Reading conversation:', url); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); await humanDelay(2000, 4000); await page.waitForSelector('[data-test="message-list"], .message-list, .messages', { timeout: 15000, }).catch(() => console.error('[getMessages] Message list selector not found')); await humanDelay(1000, 2000); const result = await page.evaluate( ({ limit, room_id }: { limit: number; room_id: string }): GetMessagesResult => { const participantEl = document.querySelector( '[data-test="room-participant-name"], .room-header-name, h1' ); const participant_name = participantEl?.textContent?.trim() ?? ''; const messageEls = document.querySelectorAll( '[data-test="message-item"], .message-item, .chat-message' ); const messages: Message[] = []; Array.from(messageEls) .slice(-limit) .forEach((el, i) => { const isMine = el.classList.contains('outgoing') || el.classList.contains('sent') || el.querySelector('[data-test="my-message"]') !== null; messages.push({ id: el.getAttribute('data-id') ?? `msg_${i}`, sender: el .querySelector('[data-test="sender-name"], .sender-name') ?.textContent?.trim() ?? (isMine ? 'Me' : participant_name), content: el .querySelector('[data-test="message-text"], .message-text, p') ?.textContent?.trim() ?? '', sent_at: el.querySelector('time')?.getAttribute('datetime') ?? el.querySelector('time')?.textContent?.trim() ?? '', is_mine: isMine, }); }); return { messages, room_id, participant_name }; }, { limit: input.limit, room_id: input.room_id } ); console.error(`[getMessages] Got ${result.messages?.length ?? 0} messages`); return result; } else { // List all conversations console.error('[getMessages] Listing conversations...'); await page.goto('https://www.upwork.com/messages/rooms', { waitUntil: 'domcontentloaded', timeout: 30000, }); await humanDelay(2000, 4000); await page.waitForSelector( '[data-test="room-list"], .room-list, .conversation-list, aside', { timeout: 15000 } ).catch(() => console.error('[getMessages] Room list selector not found')); await humanDelay(1000, 2000); const result = await page.evaluate( ({ limit, unreadOnly }: { limit: number; unreadOnly: boolean }): GetMessagesResult => { const roomEls = document.querySelectorAll( '[data-test="room-item"], .room-item, .conversation-item' ); const conversations: Conversation[] = []; roomEls.forEach((el, i) => { if (i >= limit) return; const unread = el.classList.contains('unread') || el.querySelector('.unread-badge, [data-test="unread"]') !== null; if (unreadOnly && !unread) return; const linkEl = el.querySelector('a[href*="/messages/rooms/"]'); const href = linkEl?.getAttribute('href') ?? ''; const roomIdMatch = href.match(/rooms\/([^/\s?]+)/); const room_id = roomIdMatch?.[1] ?? `room_${i}`; const room_url = href.startsWith('http') ? href : `https://www.upwork.com${href}`; conversations.push({ room_id, room_url, participant_name: el .querySelector( '[data-test="participant-name"], .participant-name, .room-name, strong' ) ?.textContent?.trim() ?? '', participant_type: 'client', last_message_preview: el .querySelector('[data-test="last-message"], .last-message, p') ?.textContent?.trim() .slice(0, 100) ?? '', last_message_at: el.querySelector('time')?.textContent?.trim() ?? '', unread, job_title: el .querySelector('[data-test="job-title"], .job-title') ?.textContent?.trim() ?? '', }); }); return { conversations }; }, { limit: input.limit, unreadOnly: input.unread_only } ); console.error(`[getMessages] Found ${result.conversations?.length ?? 0} conversations`); return result; } } finally { await page.close(); } } - src/tools/get-messages.ts:4-45 (schema)Zod schema (GetMessagesSchema) defining the input shape: optional room_id, optional limit (default 20), optional unread_only (default false). Also defines TypeScript interfaces for Conversation, Message, and GetMessagesResult.
export const GetMessagesSchema = z.object({ room_id: z .string() .optional() .describe( 'Specific conversation room ID to read. If omitted, returns list of all conversations.' ), limit: z.coerce.number().optional().default(20).describe('Max messages or conversations to return'), unread_only: z .coerce.boolean() .optional() .default(false) .describe('Only return conversations with unread messages'), }); export type GetMessagesInput = z.infer<typeof GetMessagesSchema>; export interface Conversation { room_id: string; participant_name: string; participant_type: string; // 'client' | 'agency' | etc last_message_preview: string; last_message_at: string; unread: boolean; job_title: string; room_url: string; } export interface Message { id: string; sender: string; content: string; sent_at: string; is_mine: boolean; } export interface GetMessagesResult { conversations?: Conversation[]; messages?: Message[]; room_id?: string; participant_name?: string; } - src/index.ts:148-166 (registration)Tool registration in the MCP server (index.ts) — defines name 'get_messages', description, and JSON Schema inputSchema for the tool list returned by ListToolsRequestSchema.
{ name: 'get_messages', description: `Read Upwork messages. - Without room_id: lists all conversations with unread status and last message preview - With room_id: reads the full message history of that conversation Use this to check for client responses, new invitations, and ongoing contract discussions.`, inputSchema: { type: 'object', properties: { room_id: { type: 'string', description: 'Conversation room ID. Omit to list all conversations.', }, limit: { type: ['number', 'string'], description: 'Max messages/conversations. Default: 20' }, unread_only: { type: 'boolean', description: 'Only unread conversations. Default: false' }, }, }, }, - src/index.ts:313-317 (registration)CallToolRequestSchema handler dispatch in index.ts — parses args with GetMessagesSchema and delegates to the getMessages function.
case 'get_messages': { const input = GetMessagesSchema.parse(args); result = await getMessages(input); break; } - src/worker.ts:32-32 (registration)Worker process handler dispatch for 'get_messages' — parses args with GetMessagesSchema and calls getMessages. Same pattern as index.ts but in the separate worker HTTP server process.
case 'get_messages': return getMessages(GetMessagesSchema.parse(args));