Skip to main content
Glama

list_messages

Retrieve and display message board posts from a Basecamp project with pagination options and customizable content formats.

Instructions

Resolves the project's message board from the dock, then lists messages with pagination. Optionally include full content.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_idYes
pageNo
limitNo
formatNo
include_bodyNo
renderNo

Implementation Reference

  • The main execution logic for the 'list_messages' tool. Resolves project message board, fetches paginated messages, optionally includes rendered bodies, and formats output as table (default), JSON, or Markdown.
    async ({ project_id, page, limit, format, include_body, render }) => { const fmt = format ?? "table"; const board = await getMessageBoardFromDock(project_id); if (!board) { return { content: [ { type: "text", text: "No message board found in the project's dock.", }, ], }; } const { data, headers } = await bcRequestWithHeaders<any[]>( "GET", `/buckets/${project_id}/message_boards/${board.id}/messages.json`, undefined, page ? { page } : undefined ); let messages = Array.isArray(data) ? data : []; if (limit) messages = messages.slice(0, limit); // If we want bodies, fetch each full message let bodies: Record<number, string> = {}; if (include_body) { const fulls = await Promise.all( messages.map((m) => bcRequest<any>( "GET", `/buckets/${project_id}/messages/${m.id}.json` ).then((msg) => ({ id: m.id, html: msg.content || msg.body || "" })) ) ); for (const { id, html } of fulls) { bodies[id] = renderBody(html, render ?? "markdown"); } } const rows = messages.map((m) => ({ id: m.id, subject: m.subject ?? m.title ?? "", status: m.status, created_at: m.created_at, updated_at: m.updated_at, comments_count: m.comments_count ?? m.comments?.count ?? 0, url: m.url || m.app_url || m.web_url || m.html_url || "", body: include_body ? bodies[m.id] : undefined, })); const nextPage = parseNextPage(headers.get("Link")); if (fmt === "json") { return { content: [ { type: "text", text: JSON.stringify( { page: page ?? 1, nextPage, count: rows.length, messages: rows, }, null, 2 ), }, ], }; } if (fmt === "markdown") { // Full readable output with bodies const md = [ `# Messages (page ${page ?? 1}${ nextPage ? ` → next ${nextPage}` : "" })`, "", ...rows.map((r) => { const header = `## ${r.subject || "(no subject)"} _(ID: ${r.id})_`; const meta = `- Comments: ${r.comments_count ?? 0}\n` + `- Created: ${(r.created_at ?? "").slice(0, 10)} | Updated: ${( r.updated_at ?? "" ).slice(0, 10)}\n` + (r.url ? `- Link: ${r.url}\n` : ""); const body = include_body && r.body ? `\n${r.body}\n` : ""; return [header, meta, body].join("\n"); }), ].join("\n"); return { content: [{ type: "text", text: md }] }; } // Default table (no full bodies to avoid huge output) const trunc = (s: string, n: number) => s && s.length > n ? s.slice(0, n - 1) + "…" : s || ""; const header = [ "ID".padEnd(12), "SUBJECT".padEnd(40), "CMTS".padEnd(4), "CREATED".padEnd(10), "UPDATED".padEnd(10), "URL", ].join(" "); const lines = [ `Page: ${page ?? 1}${nextPage ? ` | Next page: ${nextPage}` : ""}`, "", header, "-".repeat(120), ...rows.map((r) => [ String(r.id).padEnd(12), trunc(r.subject, 40).padEnd(40), String(r.comments_count ?? 0).padEnd(4), (r.created_at ?? "").slice(0, 10).padEnd(10), (r.updated_at ?? "").slice(0, 10).padEnd(10), trunc(r.url ?? "", 40), ].join(" ") ), include_body ? '\n(Use format: "markdown" to see full message content.)' : "", ].join("\n"); return { content: [{ type: "text", text: lines }] }; }
  • Zod input schema defining parameters: project_id (required), optional page, limit, format (table/json/markdown), include_body, render (markdown/html/text).
    inputSchema: { project_id: z.number().int(), page: z.number().int().positive().optional(), limit: z.number().int().positive().max(50).optional(), // keep modest for rate limits format: z.enum(["table", "json", "markdown"]).optional(), // default: table include_body: z.boolean().optional(), // include full message content render: z.enum(["markdown", "html", "text"]).optional(), // how to render body (default markdown) },
  • Tool registration via server.registerTool in registerMessageTools function, including name, metadata, schema, and handler reference. This function is called from the main MCP server setup.
    server.registerTool( "list_messages", { title: "List messages", description: "Resolves the project's message board from the dock, then lists messages with pagination. Optionally include full content.", inputSchema: { project_id: z.number().int(), page: z.number().int().positive().optional(), limit: z.number().int().positive().max(50).optional(), // keep modest for rate limits format: z.enum(["table", "json", "markdown"]).optional(), // default: table include_body: z.boolean().optional(), // include full message content render: z.enum(["markdown", "html", "text"]).optional(), // how to render body (default markdown) }, }, async ({ project_id, page, limit, format, include_body, render }) => { const fmt = format ?? "table"; const board = await getMessageBoardFromDock(project_id); if (!board) { return { content: [ { type: "text", text: "No message board found in the project's dock.", }, ], }; } const { data, headers } = await bcRequestWithHeaders<any[]>( "GET", `/buckets/${project_id}/message_boards/${board.id}/messages.json`, undefined, page ? { page } : undefined ); let messages = Array.isArray(data) ? data : []; if (limit) messages = messages.slice(0, limit); // If we want bodies, fetch each full message let bodies: Record<number, string> = {}; if (include_body) { const fulls = await Promise.all( messages.map((m) => bcRequest<any>( "GET", `/buckets/${project_id}/messages/${m.id}.json` ).then((msg) => ({ id: m.id, html: msg.content || msg.body || "" })) ) ); for (const { id, html } of fulls) { bodies[id] = renderBody(html, render ?? "markdown"); } } const rows = messages.map((m) => ({ id: m.id, subject: m.subject ?? m.title ?? "", status: m.status, created_at: m.created_at, updated_at: m.updated_at, comments_count: m.comments_count ?? m.comments?.count ?? 0, url: m.url || m.app_url || m.web_url || m.html_url || "", body: include_body ? bodies[m.id] : undefined, })); const nextPage = parseNextPage(headers.get("Link")); if (fmt === "json") { return { content: [ { type: "text", text: JSON.stringify( { page: page ?? 1, nextPage, count: rows.length, messages: rows, }, null, 2 ), }, ], }; } if (fmt === "markdown") { // Full readable output with bodies const md = [ `# Messages (page ${page ?? 1}${ nextPage ? ` → next ${nextPage}` : "" })`, "", ...rows.map((r) => { const header = `## ${r.subject || "(no subject)"} _(ID: ${r.id})_`; const meta = `- Comments: ${r.comments_count ?? 0}\n` + `- Created: ${(r.created_at ?? "").slice(0, 10)} | Updated: ${( r.updated_at ?? "" ).slice(0, 10)}\n` + (r.url ? `- Link: ${r.url}\n` : ""); const body = include_body && r.body ? `\n${r.body}\n` : ""; return [header, meta, body].join("\n"); }), ].join("\n"); return { content: [{ type: "text", text: md }] }; } // Default table (no full bodies to avoid huge output) const trunc = (s: string, n: number) => s && s.length > n ? s.slice(0, n - 1) + "…" : s || ""; const header = [ "ID".padEnd(12), "SUBJECT".padEnd(40), "CMTS".padEnd(4), "CREATED".padEnd(10), "UPDATED".padEnd(10), "URL", ].join(" "); const lines = [ `Page: ${page ?? 1}${nextPage ? ` | Next page: ${nextPage}` : ""}`, "", header, "-".repeat(120), ...rows.map((r) => [ String(r.id).padEnd(12), trunc(r.subject, 40).padEnd(40), String(r.comments_count ?? 0).padEnd(4), (r.created_at ?? "").slice(0, 10).padEnd(10), (r.updated_at ?? "").slice(0, 10).padEnd(10), trunc(r.url ?? "", 40), ].join(" ") ), include_body ? '\n(Use format: "markdown" to see full message content.)' : "", ].join("\n"); return { content: [{ type: "text", text: lines }] }; } );
  • Helper function to resolve the message board ID and URL from the project's dock array.
    async function getMessageBoardFromDock( project_id: number ): Promise<{ id: number; url: string } | null> { const project = await bcRequest<any>("GET", `/projects/${project_id}.json`); const dock = Array.isArray(project?.dock) ? project.dock : []; const board = dock.find((d: any) => { const n = (d?.name || d?.app_name || "").toLowerCase(); return ( n === "message_board" || n === "message-board" || n === "messageboard" ); }); if (!board) return null; const href: string = board.url || board.href || board.api_url || board.app_url || ""; const m = href.match(/\/message_boards\/(\d+)/); return m ? { id: Number(m[1]), url: href } : null; }
  • Helper function to convert HTML message bodies to Markdown (using Turndown), HTML, or plain text.
    function renderBody( html: string, render: "markdown" | "html" | "text" = "markdown" ): string { const markdown = td.turndown(html || ""); if (render === "markdown") return markdown; if (render === "html") return html || ""; // crude Markdown → plain-text return markdown .replace(/```[\s\S]*?```/g, "") // drop code blocks .replace(/`([^`]+)`/g, "$1") // inline code .replace(/!\[([^\]]*)]\([^)]*\)/g, "$1") // images .replace(/\[([^\]]+)]\([^)]*\)/g, "$1") // links .replace(/^[#>\-\*\d\.\s]+/gm, "") // headings/quotes/lists .replace(/[*_~`]/g, "") // emphasis .trim(); }

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/craigashields/basecamp-mcp'

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