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
| Name | Required | Description | Default |
|---|---|---|---|
| chat_id | Yes | Chat ID (numeric from create_p2p_chat, or oc_xxx from list_user_chats). Both formats work. | |
| page_size | No | Messages to fetch (default 20, max 50) | |
| start_time | No | Start timestamp in seconds (optional) | |
| end_time | No | End timestamp in seconds (optional) | |
| sort_type | No | Sort order (default: ByCreateTimeDesc = newest first) | |
| expand_merge_forward | No | 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). |
Implementation Reference
- src/tools/im-read.js:186-217 (handler)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)); }, - src/tools/im-read.js:108-123 (schema)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 }; } }); - src/clients/official/im.js:29-52 (helper)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 }; },