Skip to main content
Glama

read_p2p_messages

Read direct message chat history using user access token, with options for sorting, time range, and auto-expanding merged messages.

Instructions

[User UAT] Read P2P (direct message) chat history using user_access_token. Works for chats the bot cannot access. Returns newest messages first by default. Auto-expands merge_forward messages into their child messages by default — disable with expand_merge_forward=false. Requires OAuth setup.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
chat_idYesChat ID (numeric from create_p2p_chat, or oc_xxx from list_user_chats). Both formats work.
page_sizeNoMessages to fetch (default 20, max 50)
start_timeNoStart timestamp in seconds (optional)
end_timeNoEnd timestamp in seconds (optional)
sort_typeNoSort order (default: ByCreateTimeDesc = newest first)
expand_merge_forwardNoAuto-expand merge_forward placeholders into their child messages (default true). Children carry parentMessageId; use that id (not the child id) with download_message_resource (kind=image or file).

Implementation Reference

  • The async handler for the read_p2p_messages tool. It takes args (chat_id, page_size, start_time, end_time, sort_type, expand_merge_forward) and a ctx object. It resolves the chat_id (numeric, oc_xxx, or a user/group name via search), then calls official.readMessagesAsUser() to fetch P2P message history using the User Access Token (UAT). Returns the messages as JSON.
    async read_p2p_messages(args, ctx) {
      const official = ctx.getOfficialClient();
      let chatId = args.chat_id;
      let uc = null;
      let ucError = null;
      try { uc = await ctx.getUserClient(); } catch (e) { ucError = e; }
      // If chat_id is not numeric or oc_, try to resolve as user name → P2P chat
      if (!/^\d+$/.test(chatId) && !chatId.startsWith('oc_')) {
        if (uc) {
          const results = await uc.search(chatId);
          const user = results.find(r => r.type === 'user');
          if (user) {
            const pChatId = await uc.createChat(String(user.id));
            if (pChatId) chatId = String(pChatId);
            else return text(`Found user "${user.title}" but failed to create P2P chat.`);
          } else {
            // Maybe it's a group name
            const group = results.find(r => r.type === 'group');
            if (group) chatId = String(group.id);
            else return text(`Cannot resolve "${args.chat_id}" to a chat. Use search_contacts to find the ID first.`);
          }
        } else {
          const hint = ucError ? `Cookie auth failed: ${ucError.message}. Fix LARK_COOKIE first, or p` : 'P';
          return text(`"${args.chat_id}" is not a valid chat ID. ${hint}rovide a numeric ID or oc_xxx format. Use search_contacts + create_p2p_chat to get the ID.`);
        }
      }
      return json(await official.readMessagesAsUser(chatId, {
        pageSize: args.page_size, startTime: args.start_time, endTime: args.end_time,
        sortType: args.sort_type,
        expandMergeForward: args.expand_merge_forward !== false,
      }, uc));
    },
  • The inputSchema for the read_p2p_messages tool, defining its name, description, and input parameters: chat_id (required, string), page_size (number), start_time (string), end_time (string), sort_type (enum: ByCreateTimeDesc/ByCreateTimeAsc), expand_merge_forward (boolean).
    {
      name: 'read_p2p_messages',
      description: '[User UAT] Read P2P (direct message) chat history using user_access_token. Works for chats the bot cannot access. Returns newest messages first by default. Auto-expands merge_forward messages into their child messages by default — disable with expand_merge_forward=false. Requires OAuth setup.',
      inputSchema: {
        type: 'object',
        properties: {
          chat_id: { type: 'string', description: 'Chat ID (numeric from create_p2p_chat, or oc_xxx from list_user_chats). Both formats work.' },
          page_size: { type: 'number', description: 'Messages to fetch (default 20, max 50)' },
          start_time: { type: 'string', description: 'Start timestamp in seconds (optional)' },
          end_time: { type: 'string', description: 'End timestamp in seconds (optional)' },
          sort_type: { type: 'string', enum: ['ByCreateTimeDesc', 'ByCreateTimeAsc'], description: 'Sort order (default: ByCreateTimeDesc = newest first)' },
          expand_merge_forward: { type: 'boolean', description: 'Auto-expand merge_forward placeholders into their child messages (default true). Children carry parentMessageId; use that id (not the child id) with download_message_resource (kind=image or file).' },
        },
        required: ['chat_id'],
      },
    },
  • src/server.js:37-57 (registration)
    Registration of tool modules: src/tools/im-read.js is required at line 46. Its schemas are flattened into TOOLS (line 56) and handlers into HANDLERS (line 57) for MCP dispatch.
    const TOOL_MODULES = [
      require('./tools/bitable'),
      require('./tools/calendar'),
      require('./tools/contacts'),
      require('./tools/diagnostics'),
      require('./tools/docs'),
      require('./tools/drive'),
      require('./tools/events'),
      require('./tools/groups'),
      require('./tools/im-read'),
      require('./tools/messaging-bot'),
      require('./tools/messaging-user'),
      require('./tools/okr'),
      require('./tools/profile'),
      require('./tools/tasks'),
      require('./tools/uploads'),
      require('./tools/wiki'),
    ];
    
    const TOOLS = TOOL_MODULES.flatMap((m) => m.schemas);
    const HANDLERS = Object.fromEntries(TOOL_MODULES.flatMap((m) => Object.entries(m.handlers)));
  • src/server.js:391-412 (registration)
    The CallTool request handler that dispatches tool calls by name. It looks up the handler from HANDLERS[name] — for 'read_p2p_messages' it finds the handler in src/tools/im-read.js.
    server.setRequestHandler(CallToolRequestSchema, async (request) => {
      // Cross-process active-profile sync (v1.3.9 A.2): if another MCP process
      // wrote a new `active` field to credentials.json, pick it up before dispatch.
      _syncActiveProfileFromDisk();
      const { name, arguments: args } = request.params;
      const handler = HANDLERS[name];
      if (!handler) {
        return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
      }
      // Strip via_profile from args before passing to the handler — it's a
      // routing-layer concern, not a tool argument. Keep a copy for routing.
      const cleanArgs = (args && typeof args === 'object') ? { ...args } : {};
      delete cleanArgs.via_profile;
    
      try {
        return await profileRouter.withProfileRouting(buildCtx(), name, args || {}, async () => {
          return handler(cleanArgs, buildCtx());
        });
      } catch (err) {
        return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
      }
    });
  • The readMessagesAsUser() method on LarkOfficialClient that performs the actual Feishu API call (im/v1/messages) using a User Access Token. Called by the read_p2p_messages handler to fetch P2P message history.
    async readMessagesAsUser(chatId, { pageSize = 20, startTime, endTime, pageToken, sortType = 'ByCreateTimeDesc', expandMergeForward = true } = {}, userClient) {
      // Feishu API requires end_time >= start_time; auto-set end_time to now if missing
      if (startTime && !endTime) {
        endTime = String(Math.floor(Date.now() / 1000));
      }
      const params = new URLSearchParams({
        container_id_type: 'chat', container_id: chatId, page_size: String(pageSize),
        sort_type: sortType,
      });
      if (startTime) params.set('start_time', startTime);
      if (endTime) params.set('end_time', endTime);
      if (pageToken) params.set('page_token', pageToken);
      const data = await this._withUAT(async (uat) => {
        const res = await fetchWithTimeout(`https://open.feishu.cn/open-apis/im/v1/messages?${params}`, {
          headers: { 'Authorization': `Bearer ${uat}` },
        });
        return res.json();
      });
      if (data.code !== 0) throw new Error(`readMessagesAsUser failed (${data.code}): ${data.msg}`);
      const items = (data.data.items || []).map(m => this._formatMessage(m));
      await this._populateSenderNames(items, userClient);
      if (expandMergeForward) await this._expandMergeForwardItems(items, userClient, { preferUAT: true });
      return { items, hasMore: data.data.has_more, pageToken: data.data.page_token };
    },
Behavior4/5

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

Discloses key behaviors: uses user_access_token, returns newest first by default, auto-expands merge_forward messages with disable option, and requires OAuth. No annotation provided, so description carries this burden well.

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?

Concise, front-loaded with purpose, and every sentence adds useful information. No filler.

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

Completeness4/5

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

Covers purpose, usage context, parameter details, and behavioral quirks. Lacks return format description but is acceptable given no output schema.

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%, baseline 3. Description adds value beyond schema: explains chat_id formats and expand_merge_forward behavior with details on parentMessageId usage.

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?

Clearly states it reads P2P chat history with user token. Mentions it works for chats the bot cannot access, distinguishing it from bot-based message reading tools.

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?

Implies use when bot cannot access chat and requires OAuth setup. Does not explicitly mention when not to use or compare to siblings like read_messages.

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/EthanQC/feishu-user-plugin'

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