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 +6 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/activity.d.ts +13 -0
- package/dist/tools/activity.d.ts.map +1 -0
- package/dist/tools/activity.js +547 -0
- package/dist/tools/activity.js.map +1 -0
- package/dist/tools/documents.d.ts +11 -0
- package/dist/tools/documents.d.ts.map +1 -0
- package/dist/tools/documents.js +453 -0
- package/dist/tools/documents.js.map +1 -0
- package/dist/tools/kanban.d.ts.map +1 -1
- package/dist/tools/kanban.js +221 -48
- package/dist/tools/kanban.js.map +1 -1
- package/dist/tools/uploads.d.ts +9 -0
- package/dist/tools/uploads.d.ts.map +1 -0
- package/dist/tools/uploads.js +319 -0
- package/dist/tools/uploads.js.map +1 -0
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA
|
|
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
|
|
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
|