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.
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));