Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
room_idNoConversation room ID. Omit to list all conversations.
limitNoMax messages/conversations. Default: 20
unread_onlyNoOnly unread conversations. Default: false

Implementation Reference

  • 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();
      }
    }
  • 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));
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden. It accurately describes behavior: listing conversations vs. full history, and mentions unread status and last message preview. It does not contradict any annotations (none exist).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences, front-loaded with the main purpose. Every sentence adds value: first sentence states what it does, second explains the two modes, third gives usage guidance. No wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read tool with 3 parameters and no output schema, the description is complete. It explains the two modes, parameter behavior, and provides context for when to use. No gaps are apparent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so all parameters are described in the schema. The description adds value by explaining the behavioral implications of the room_id parameter (two modes). This goes beyond the schema's description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Read Upwork messages' and distinguishes two modes: without room_id to list conversations with unread status and last message preview, and with room_id to read full history. This specificity differentiates it from siblings like send_message or get_proposals.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit usage context: 'Use this to check for client responses, new invitations, and ongoing contract discussions.' While it doesn't list when not to use or alternatives, the context is clear and helpful.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zcrossoverz/upwork-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server