basecamp-mcp 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -129,10 +129,13 @@ npm run dev
129
129
  - `basecamp_list_kanban_columns` - List all columns in a kanban board
130
130
  - `basecamp_list_kanban_cards` - List cards in a column with steps and assignees
131
131
  - `basecamp_get_kanban_card` - Get complete details of a specific card
132
- - `basecamp_create_kanban_card` - Create new card with title and optional content
133
- - `basecamp_update_kanban_card` - Update card with advanced content editing (supports full replacement, append, prepend, search/replace, plus title, due date, assignees, notifications)
132
+ - `basecamp_create_kanban_card` - Create new card with title, content, and optional checklist steps
133
+ - `basecamp_update_kanban_card` - Update card with advanced content editing (supports full replacement, append, prepend, search/replace, plus title, due date, assignees, notifications, and complete step array management)
134
134
  - `basecamp_move_kanban_card` - Move a card to a different column and/or position
135
- - `basecamp_create_kanban_step` - Add checklist step to a card
135
+
136
+ #### Activity
137
+ - `basecamp_list_recordings` - Browse recent activity globally or across specific projects, with filtering by type, date range, person, and text search. All filters support multiple values for OR-matching (e.g., multiple project IDs, person IDs, types, or search terms)
138
+ - `basecamp_list_campfire_messages` - Browse chat messages from Campfires with filtering by campfire, person, text content, and date range. All filters support multiple values for OR-matching
136
139
 
137
140
  ## Development
138
141
 
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  * - Comments (universal - work on any resource)
10
10
  * - People
11
11
  * - Kanban (cards, columns, steps)
12
+ * - Activity (recordings browsing)
12
13
  *
13
14
  * Environment variables required:
14
15
  * - BASECAMP_CLIENT_ID
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;GAgBG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;GAiBG"}
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * - Comments (universal - work on any resource)
10
10
  * - People
11
11
  * - Kanban (cards, columns, steps)
12
+ * - Activity (recordings browsing)
12
13
  *
13
14
  * Environment variables required:
14
15
  * - BASECAMP_CLIENT_ID
@@ -18,13 +19,16 @@
18
19
  */
19
20
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
20
21
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
+ import { registerActivityTools } from "./tools/activity.js";
21
23
  import { registerCommentTools } from "./tools/comments.js";
24
+ import { registerDocumentTools } from "./tools/documents.js";
22
25
  import { registerKanbanTools } from "./tools/kanban.js";
23
26
  import { registerMessageTools } from "./tools/messages.js";
24
27
  import { registerPeopleTools } from "./tools/people.js";
25
28
  // Import tool registration functions
26
29
  import { registerProjectTools } from "./tools/projects.js";
27
30
  import { registerTodoTools } from "./tools/todos.js";
31
+ import { registerUploadTools } from "./tools/uploads.js";
28
32
  /**
29
33
  * Main server initialization and startup
30
34
  */
@@ -55,6 +59,9 @@ async function main() {
55
59
  registerCommentTools(server);
56
60
  registerPeopleTools(server);
57
61
  registerKanbanTools(server);
62
+ registerActivityTools(server);
63
+ registerDocumentTools(server);
64
+ registerUploadTools(server);
58
65
  console.error("Tools registered successfully");
59
66
  // Create stdio transport
60
67
  const transport = new StdioServerTransport();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,qCAAqC;AACrC,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD;;GAEG;AACH,KAAK,UAAU,IAAI;IACjB,0CAA0C;IAC1C,MAAM,eAAe,GAAG;QACtB,oBAAoB;QACpB,wBAAwB;QACxB,wBAAwB;QACxB,qBAAqB;KACtB,CAAC;IAEF,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3E,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CACX,kDAAkD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACvE,CAAC;QACF,OAAO,CAAC,KAAK,CACX,+EAA+E,CAChF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,6BAA6B;IAC7B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,+BAA+B;IAC/B,OAAO,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;IACtC,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,OAAO,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAE/C,yBAAyB;IACzB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAE7C,8BAA8B;IAC9B,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,OAAO,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACtD,OAAO,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;AACpD,CAAC;AAED,iBAAiB;AACjB,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,KAAK,CAAC,CAAC;IAClE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,qCAAqC;AACrC,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD;;GAEG;AACH,KAAK,UAAU,IAAI;IACjB,0CAA0C;IAC1C,MAAM,eAAe,GAAG;QACtB,oBAAoB;QACpB,wBAAwB;QACxB,wBAAwB;QACxB,qBAAqB;KACtB,CAAC;IAEF,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3E,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CACX,kDAAkD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACvE,CAAC;QACF,OAAO,CAAC,KAAK,CACX,+EAA+E,CAChF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,6BAA6B;IAC7B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,+BAA+B;IAC/B,OAAO,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;IACtC,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,OAAO,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAE/C,yBAAyB;IACzB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAE7C,8BAA8B;IAC9B,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,OAAO,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACtD,OAAO,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;AACpD,CAAC;AAED,iBAAiB;AACjB,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,KAAK,CAAC,CAAC;IAClE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Activity browsing tools for Basecamp MCP server
3
+ *
4
+ * Uses the Basecamp recordings API to provide activity browsing:
5
+ * listing recent changes across projects with filtering by type,
6
+ * date range, person, project, and text search.
7
+ */
8
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ /**
10
+ * Register all activity-related tools with the MCP server
11
+ */
12
+ export declare function registerActivityTools(server: McpServer): void;
13
+ //# sourceMappingURL=activity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/tools/activity.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAwHzE;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAqiB7D"}
@@ -0,0 +1,547 @@
1
+ /**
2
+ * Activity browsing tools for Basecamp MCP server
3
+ *
4
+ * Uses the Basecamp recordings API to provide activity browsing:
5
+ * listing recent changes across projects with filtering by type,
6
+ * date range, person, project, and text search.
7
+ */
8
+ import { asyncPagedIterator } from "basecamp-client";
9
+ import { z } from "zod";
10
+ import { CHARACTER_LIMIT, DEFAULT_LIMIT } from "../constants.js";
11
+ import { BasecampIdSchema } from "../schemas/common.js";
12
+ import { initializeBasecampClient } from "../utils/auth.js";
13
+ import { handleBasecampError } from "../utils/errorHandlers.js";
14
+ /** Default recording types to fetch when no type filter is specified */
15
+ const DEFAULT_RECORDING_TYPES = [
16
+ "Todo",
17
+ "Message",
18
+ "Document",
19
+ "Comment",
20
+ "Upload",
21
+ "Kanban::Card",
22
+ ];
23
+ /** Map of friendly type aliases to canonical Basecamp API type names */
24
+ const TYPE_ALIASES = {
25
+ todo: "Todo",
26
+ todos: "Todo",
27
+ message: "Message",
28
+ messages: "Message",
29
+ msg: "Message",
30
+ document: "Document",
31
+ doc: "Document",
32
+ docs: "Document",
33
+ comment: "Comment",
34
+ comments: "Comment",
35
+ upload: "Upload",
36
+ uploads: "Upload",
37
+ file: "Upload",
38
+ files: "Upload",
39
+ todolist: "Todolist",
40
+ question: "Question::Answer",
41
+ answer: "Question::Answer",
42
+ event: "Schedule::Entry",
43
+ schedule: "Schedule::Entry",
44
+ vault: "Vault",
45
+ card: "Kanban::Card",
46
+ cards: "Kanban::Card",
47
+ kanban: "Kanban::Card",
48
+ step: "Kanban::Step",
49
+ steps: "Kanban::Step",
50
+ };
51
+ /**
52
+ * Parse a "since" value into a Date object.
53
+ *
54
+ * Supports:
55
+ * - Relative durations: "24h", "7d", "2w"
56
+ * - Keywords: "today", "yesterday"
57
+ * - ISO 8601 dates: "2024-01-15", "2024-01-15T10:00:00Z"
58
+ */
59
+ function parseSince(value) {
60
+ const now = new Date();
61
+ const trimmed = value.trim();
62
+ const lower = trimmed.toLowerCase();
63
+ // Relative durations: Nh, Nd, Nw
64
+ const hourMatch = lower.match(/^(\d+)h$/);
65
+ if (hourMatch) {
66
+ return new Date(now.getTime() - Number.parseInt(hourMatch[1], 10) * 3600000);
67
+ }
68
+ const dayMatch = lower.match(/^(\d+)d$/);
69
+ if (dayMatch) {
70
+ return new Date(now.getTime() - Number.parseInt(dayMatch[1], 10) * 86400000);
71
+ }
72
+ const weekMatch = lower.match(/^(\d+)w$/);
73
+ if (weekMatch) {
74
+ return new Date(now.getTime() - Number.parseInt(weekMatch[1], 10) * 7 * 86400000);
75
+ }
76
+ // Keywords
77
+ if (lower === "today") {
78
+ const d = new Date(now);
79
+ d.setHours(0, 0, 0, 0);
80
+ return d;
81
+ }
82
+ if (lower === "yesterday") {
83
+ const d = new Date(now);
84
+ d.setDate(d.getDate() - 1);
85
+ d.setHours(0, 0, 0, 0);
86
+ return d;
87
+ }
88
+ // ISO 8601 / date string
89
+ const parsed = new Date(trimmed);
90
+ if (Number.isNaN(parsed.getTime())) {
91
+ throw new Error(`Invalid since value: "${value}". ` +
92
+ 'Use ISO 8601 (e.g., "2024-01-15"), relative duration (e.g., "24h", "7d", "2w"), ' +
93
+ 'or keyword ("today", "yesterday").');
94
+ }
95
+ return parsed;
96
+ }
97
+ /**
98
+ * Resolve an array of type strings (possibly using aliases)
99
+ * to deduplicated canonical Basecamp API type names.
100
+ */
101
+ function resolveTypes(types) {
102
+ const resolved = new Set();
103
+ for (const t of types) {
104
+ const lower = t.trim().toLowerCase();
105
+ resolved.add(TYPE_ALIASES[lower] || t.trim());
106
+ }
107
+ return [...resolved];
108
+ }
109
+ /**
110
+ * Register all activity-related tools with the MCP server
111
+ */
112
+ export function registerActivityTools(server) {
113
+ server.registerTool("basecamp_list_recordings", {
114
+ title: "List Basecamp Activity (Recordings)",
115
+ description: `Browse recent activity across Basecamp by listing recordings. Recordings represent all content in Basecamp: todos, messages, documents, comments, uploads, and more.
116
+
117
+ Use this tool to:
118
+ - See what's been happening across all projects or specific projects
119
+ - Find recent activity by one or more people
120
+ - Review changes since a specific date or time period
121
+ - Filter activity by content type (todos, messages, documents, etc.)
122
+ - Search activity by title text
123
+
124
+ All filters support multiple values for OR-matching.
125
+
126
+ Examples:
127
+ - "What happened in the last 24 hours?" → since: "24h"
128
+ - "Show recent todos in project 12345" → project_ids: [12345], type: ["todo"]
129
+ - "What did Alice and Bob do this week?" → person_ids: [111, 222], since: "7d"
130
+ - "Find messages mentioning launch across projects 1 and 2" → project_ids: [1, 2], type: ["message"], query: ["launch"]
131
+ - "Find items about design or UX" → query: ["design", "UX"]
132
+ - "List all messages across projects" → type: ["message"]`,
133
+ inputSchema: {
134
+ project_ids: z
135
+ .array(BasecampIdSchema)
136
+ .optional()
137
+ .describe("Filter to specific projects (bucket IDs). Supports multiple IDs for OR-matching. Omit to browse across all projects."),
138
+ type: z
139
+ .array(z.string())
140
+ .optional()
141
+ .describe('Recording type filter. Options: "todo", "message", "document", "comment", "upload", ' +
142
+ '"todolist", "question", "schedule", "vault". ' +
143
+ "Supports multiple values for OR-matching. " +
144
+ "Omit to fetch all common types (todo, message, document, comment, upload, card)."),
145
+ since: z
146
+ .string()
147
+ .optional()
148
+ .describe('Show activity since this time. Accepts ISO 8601 dates (e.g., "2024-01-15"), ' +
149
+ 'relative durations ("24h", "7d", "2w"), or keywords ("today", "yesterday").'),
150
+ person_ids: z
151
+ .array(BasecampIdSchema)
152
+ .optional()
153
+ .describe("Filter by creator person IDs. Supports multiple IDs for OR-matching. Use basecamp_list_people to find person IDs."),
154
+ query: z
155
+ .array(z.string())
156
+ .optional()
157
+ .describe("Case-insensitive text search against recording titles. Supports multiple terms for OR-matching."),
158
+ sort: z
159
+ .enum(["created_at", "updated_at"])
160
+ .optional()
161
+ .describe('Sort field: "created_at" (default) or "updated_at".'),
162
+ direction: z
163
+ .enum(["desc", "asc"])
164
+ .optional()
165
+ .describe('Sort direction: "desc" (default, newest first) or "asc" (oldest first).'),
166
+ status: z
167
+ .enum(["active", "archived", "trashed"])
168
+ .optional()
169
+ .describe('Recording status filter: "active" (default), "archived", or "trashed".'),
170
+ limit: z
171
+ .number()
172
+ .min(1)
173
+ .max(100)
174
+ .optional()
175
+ .describe(`Maximum number of recordings to return (default: ${DEFAULT_LIMIT}, max: 100).`),
176
+ },
177
+ annotations: {
178
+ readOnlyHint: true,
179
+ destructiveHint: false,
180
+ idempotentHint: true,
181
+ openWorldHint: true,
182
+ },
183
+ }, async (params) => {
184
+ try {
185
+ const client = await initializeBasecampClient();
186
+ // Determine which types to fetch
187
+ const types = params.type
188
+ ? resolveTypes(params.type)
189
+ : DEFAULT_RECORDING_TYPES;
190
+ // Build shared query params
191
+ const baseQuery = {};
192
+ if (params.project_ids && params.project_ids.length > 0) {
193
+ baseQuery.bucket = params.project_ids.join(",");
194
+ }
195
+ if (params.sort) {
196
+ baseQuery.sort = params.sort;
197
+ }
198
+ if (params.direction) {
199
+ baseQuery.direction = params.direction;
200
+ }
201
+ if (params.status) {
202
+ baseQuery.status = params.status;
203
+ }
204
+ // Parse since date upfront if provided
205
+ const sinceDate = params.since ? parseSince(params.since) : null;
206
+ const sortField = params.sort || "created_at";
207
+ // Fetch recordings for each type in parallel, with early termination if since is set
208
+ const fetchPromises = types.map(async (type) => {
209
+ const items = [];
210
+ for await (const item of asyncPagedIterator({
211
+ fetchPage: client.recordings.list,
212
+ request: {
213
+ query: { type, ...baseQuery },
214
+ },
215
+ })) {
216
+ // If filtering by date and results are sorted desc (default),
217
+ // stop once we hit records older than the cutoff
218
+ if (sinceDate && params.direction !== "asc") {
219
+ const itemDate = new Date(sortField === "updated_at" ? item.updated_at : item.created_at);
220
+ if (itemDate < sinceDate) {
221
+ break;
222
+ }
223
+ }
224
+ items.push(item);
225
+ }
226
+ return items;
227
+ });
228
+ const results = await Promise.all(fetchPromises);
229
+ let filtered = results.flat();
230
+ // If direction is asc, we couldn't do early termination, so filter now
231
+ if (sinceDate && params.direction === "asc") {
232
+ filtered = filtered.filter((r) => {
233
+ const date = new Date(sortField === "updated_at" ? r.updated_at : r.created_at);
234
+ return date >= sinceDate;
235
+ });
236
+ }
237
+ // Filter by person IDs (OR-match)
238
+ if (params.person_ids && params.person_ids.length > 0) {
239
+ const personIdSet = new Set(params.person_ids);
240
+ filtered = filtered.filter((r) => r.creator && personIdSet.has(r.creator.id));
241
+ }
242
+ // Filter by text search (case-insensitive substring match on title, OR-matching)
243
+ if (params.query && params.query.length > 0) {
244
+ const lowerQueries = params.query.map((q) => q.toLowerCase());
245
+ filtered = filtered.filter((r) => {
246
+ const lowerTitle = r.title.toLowerCase();
247
+ return lowerQueries.some((q) => lowerTitle.includes(q));
248
+ });
249
+ }
250
+ // Sort merged results
251
+ const sortDir = params.direction || "desc";
252
+ filtered.sort((a, b) => {
253
+ const dateA = new Date(sortField === "updated_at" ? a.updated_at : a.created_at).getTime();
254
+ const dateB = new Date(sortField === "updated_at" ? b.updated_at : b.created_at).getTime();
255
+ return sortDir === "desc" ? dateB - dateA : dateA - dateB;
256
+ });
257
+ // Apply limit
258
+ const limit = params.limit || DEFAULT_LIMIT;
259
+ const total = filtered.length;
260
+ filtered = filtered.slice(0, limit);
261
+ // Serialize response
262
+ const serialized = filtered.map((r) => ({
263
+ id: r.id,
264
+ type: r.type,
265
+ title: r.title,
266
+ status: r.status,
267
+ created_at: r.created_at,
268
+ updated_at: r.updated_at,
269
+ url: r.app_url,
270
+ creator: r.creator
271
+ ? {
272
+ id: r.creator.id,
273
+ name: r.creator.name,
274
+ email: r.creator.email_address,
275
+ }
276
+ : null,
277
+ project: r.bucket
278
+ ? {
279
+ id: r.bucket.id,
280
+ name: r.bucket.name,
281
+ }
282
+ : null,
283
+ ...(r.parent
284
+ ? {
285
+ parent: {
286
+ id: r.parent.id,
287
+ title: r.parent.title,
288
+ type: r.parent.type,
289
+ },
290
+ }
291
+ : {}),
292
+ }));
293
+ const result = {
294
+ recordings: serialized,
295
+ total_fetched: total,
296
+ returned: serialized.length,
297
+ };
298
+ if (total > limit) {
299
+ result.truncated = true;
300
+ result.truncation_message = `Showing ${limit} of ${total} recordings. Increase limit or narrow filters to see more.`;
301
+ }
302
+ let jsonStr = JSON.stringify(result, null, 2);
303
+ // Handle response size limit
304
+ if (jsonStr.length > CHARACTER_LIMIT) {
305
+ const reducedLimit = Math.floor(serialized.length / 2);
306
+ const reduced = serialized.slice(0, reducedLimit);
307
+ jsonStr = JSON.stringify({
308
+ recordings: reduced,
309
+ total_fetched: total,
310
+ returned: reduced.length,
311
+ truncated: true,
312
+ truncation_message: `Response truncated to ${reduced.length} recordings due to size limits. Use more specific filters or a smaller limit.`,
313
+ }, null, 2);
314
+ }
315
+ return {
316
+ content: [{ type: "text", text: jsonStr }],
317
+ };
318
+ }
319
+ catch (error) {
320
+ return {
321
+ content: [{ type: "text", text: handleBasecampError(error) }],
322
+ };
323
+ }
324
+ });
325
+ server.registerTool("basecamp_list_campfire_messages", {
326
+ title: "List Campfire Messages",
327
+ description: `Browse chat messages from Basecamp Campfires. Campfires are real-time chat rooms within projects.
328
+
329
+ Use this tool to:
330
+ - See recent chat activity across all campfires or specific ones
331
+ - Find messages from specific people
332
+ - Search message content for keywords
333
+ - Review chat history since a specific date or time period
334
+
335
+ All filters support multiple values for OR-matching.
336
+
337
+ Examples:
338
+ - "What's been discussed in chat today?" → since: "today"
339
+ - "Show messages from Alice and Bob" → person_ids: [111, 222]
340
+ - "Find chat messages mentioning deploy or release" → query: ["deploy", "release"]
341
+ - "Recent messages in campfire 12345" → campfire_ids: [12345]`,
342
+ inputSchema: {
343
+ campfire_ids: z
344
+ .array(BasecampIdSchema)
345
+ .optional()
346
+ .describe("Filter to specific campfires by ID. Supports multiple IDs for OR-matching. Omit to browse all campfires."),
347
+ person_ids: z
348
+ .array(BasecampIdSchema)
349
+ .optional()
350
+ .describe("Filter by sender person IDs. Supports multiple IDs for OR-matching. Use basecamp_list_people to find person IDs."),
351
+ query: z
352
+ .array(z.string())
353
+ .optional()
354
+ .describe("Case-insensitive text search against message content. Supports multiple terms for OR-matching."),
355
+ since: z
356
+ .string()
357
+ .optional()
358
+ .describe('Show messages since this time. Accepts ISO 8601 dates (e.g., "2024-01-15"), ' +
359
+ 'relative durations ("24h", "7d", "2w"), or keywords ("today", "yesterday").'),
360
+ limit: z
361
+ .number()
362
+ .min(1)
363
+ .max(100)
364
+ .optional()
365
+ .describe(`Maximum number of messages to return (default: ${DEFAULT_LIMIT}, max: 100).`),
366
+ },
367
+ annotations: {
368
+ readOnlyHint: true,
369
+ destructiveHint: false,
370
+ idempotentHint: true,
371
+ openWorldHint: true,
372
+ },
373
+ }, async (params) => {
374
+ try {
375
+ const client = await initializeBasecampClient();
376
+ // Parse since date upfront if provided
377
+ const sinceDate = params.since ? parseSince(params.since) : null;
378
+ // Determine which campfires to fetch from
379
+ let campfiresToFetch = [];
380
+ if (params.campfire_ids && params.campfire_ids.length > 0) {
381
+ // Fetch all campfires to get bucket IDs for the requested campfire IDs
382
+ const allCampfires = [];
383
+ for await (const campfire of asyncPagedIterator({
384
+ fetchPage: client.campfires.list,
385
+ request: {},
386
+ })) {
387
+ allCampfires.push(campfire);
388
+ }
389
+ const campfireIdSet = new Set(params.campfire_ids);
390
+ campfiresToFetch = allCampfires
391
+ .filter((c) => campfireIdSet.has(c.id))
392
+ .map((c) => ({
393
+ bucketId: c.bucket.id,
394
+ campfireId: c.id,
395
+ campfireName: c.title,
396
+ projectName: c.bucket.name,
397
+ }));
398
+ if (campfiresToFetch.length === 0) {
399
+ return {
400
+ content: [
401
+ {
402
+ type: "text",
403
+ text: JSON.stringify({
404
+ error: "No campfires found matching the provided IDs.",
405
+ provided_ids: params.campfire_ids,
406
+ }, null, 2),
407
+ },
408
+ ],
409
+ };
410
+ }
411
+ }
412
+ else {
413
+ // Fetch all campfires, filtering by updated_at if since is provided
414
+ for await (const campfire of asyncPagedIterator({
415
+ fetchPage: client.campfires.list,
416
+ request: {},
417
+ })) {
418
+ // Skip campfires that haven't been updated since the filter date
419
+ if (sinceDate) {
420
+ const campfireUpdatedAt = new Date(campfire.updated_at);
421
+ if (campfireUpdatedAt < sinceDate) {
422
+ continue;
423
+ }
424
+ }
425
+ campfiresToFetch.push({
426
+ bucketId: campfire.bucket.id,
427
+ campfireId: campfire.id,
428
+ campfireName: campfire.title,
429
+ projectName: campfire.bucket.name,
430
+ });
431
+ }
432
+ if (campfiresToFetch.length === 0) {
433
+ return {
434
+ content: [
435
+ {
436
+ type: "text",
437
+ text: JSON.stringify({ messages: [], total_fetched: 0, returned: 0 }, null, 2),
438
+ },
439
+ ],
440
+ };
441
+ }
442
+ }
443
+ // Fetch lines from each campfire in parallel
444
+ const fetchPromises = campfiresToFetch.map(async ({ bucketId, campfireId, campfireName, projectName }) => {
445
+ const lines = [];
446
+ for await (const line of asyncPagedIterator({
447
+ fetchPage: client.campfires.listLines,
448
+ request: {
449
+ params: { bucketId, campfireId },
450
+ },
451
+ })) {
452
+ // API returns newest first - early termination if since is set
453
+ if (sinceDate) {
454
+ const lineDate = new Date(line.created_at);
455
+ if (lineDate < sinceDate) {
456
+ break;
457
+ }
458
+ }
459
+ lines.push({
460
+ ...line,
461
+ campfire_name: campfireName,
462
+ project_name: projectName,
463
+ });
464
+ }
465
+ return lines;
466
+ });
467
+ const results = await Promise.all(fetchPromises);
468
+ let allLines = results.flat();
469
+ // Filter by person IDs (OR-match)
470
+ if (params.person_ids && params.person_ids.length > 0) {
471
+ const personIdSet = new Set(params.person_ids);
472
+ allLines = allLines.filter((line) => line.creator && personIdSet.has(line.creator.id));
473
+ }
474
+ // Filter by text search (case-insensitive substring match on content, OR-matching)
475
+ if (params.query && params.query.length > 0) {
476
+ const lowerQueries = params.query.map((q) => q.toLowerCase());
477
+ allLines = allLines.filter((line) => {
478
+ const lowerContent = line.content.toLowerCase();
479
+ return lowerQueries.some((q) => lowerContent.includes(q));
480
+ });
481
+ }
482
+ // Sort all messages by created_at desc (newest first)
483
+ allLines.sort((a, b) => {
484
+ const dateA = new Date(a.created_at).getTime();
485
+ const dateB = new Date(b.created_at).getTime();
486
+ return dateB - dateA;
487
+ });
488
+ // Apply limit
489
+ const limit = params.limit || DEFAULT_LIMIT;
490
+ const total = allLines.length;
491
+ allLines = allLines.slice(0, limit);
492
+ // Serialize response
493
+ const serialized = allLines.map((line) => ({
494
+ id: line.id,
495
+ content: line.content,
496
+ created_at: line.created_at,
497
+ url: line.app_url,
498
+ creator: line.creator
499
+ ? {
500
+ id: line.creator.id,
501
+ name: line.creator.name,
502
+ email: line.creator.email_address,
503
+ }
504
+ : null,
505
+ campfire: {
506
+ id: line.parent?.id,
507
+ name: line.campfire_name,
508
+ },
509
+ project: {
510
+ id: line.bucket.id,
511
+ name: line.project_name,
512
+ },
513
+ }));
514
+ const result = {
515
+ messages: serialized,
516
+ total_fetched: total,
517
+ returned: serialized.length,
518
+ };
519
+ if (total > limit) {
520
+ result.truncated = true;
521
+ result.truncation_message = `Showing ${limit} of ${total} messages. Increase limit or narrow filters to see more.`;
522
+ }
523
+ let jsonStr = JSON.stringify(result, null, 2);
524
+ // Handle response size limit
525
+ if (jsonStr.length > CHARACTER_LIMIT) {
526
+ const reducedLimit = Math.floor(serialized.length / 2);
527
+ const reduced = serialized.slice(0, reducedLimit);
528
+ jsonStr = JSON.stringify({
529
+ messages: reduced,
530
+ total_fetched: total,
531
+ returned: reduced.length,
532
+ truncated: true,
533
+ truncation_message: `Response truncated to ${reduced.length} messages due to size limits. Use more specific filters or a smaller limit.`,
534
+ }, null, 2);
535
+ }
536
+ return {
537
+ content: [{ type: "text", text: jsonStr }],
538
+ };
539
+ }
540
+ catch (error) {
541
+ return {
542
+ content: [{ type: "text", text: handleBasecampError(error) }],
543
+ };
544
+ }
545
+ });
546
+ }
547
+ //# sourceMappingURL=activity.js.map