@zackbart/connecta 0.28.0 → 0.28.1
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/CHANGELOG.md +173 -0
- package/README.md +8 -5
- package/dist/artifacts/connector.js +1 -1
- package/dist/auth/downstream-oauth.js +147 -28
- package/dist/auth/static-oauth.js +504 -0
- package/dist/connectors/api-connector.d.ts +168 -0
- package/dist/connectors/api-connector.js +127 -0
- package/dist/connectors/api.d.ts +8 -71
- package/dist/connectors/api.js +10 -99
- package/dist/connectors/guarded-fetch.js +37 -3
- package/dist/connectors/remote-mcp.js +7 -32
- package/dist/index.d.ts +1 -1
- package/dist/oauth-partition.js +17 -0
- package/dist/providers/basecamp.d.ts +65 -0
- package/dist/providers/basecamp.js +445 -0
- package/dist/providers/breeze.d.ts +34 -0
- package/dist/providers/breeze.js +1402 -0
- package/dist/providers/ccb.d.ts +103 -0
- package/dist/providers/ccb.js +1570 -0
- package/dist/providers/cloudflare.js +1 -1
- package/dist/providers/linear.js +1 -1
- package/dist/providers/mixpanel.js +1 -1
- package/dist/providers/notion.js +1 -1
- package/dist/providers/overflow.d.ts +60 -0
- package/dist/providers/overflow.js +1572 -0
- package/dist/providers/planning-center.d.ts +59 -0
- package/dist/providers/planning-center.js +2039 -0
- package/dist/providers/revenuecat.js +1 -1
- package/dist/providers/stripe.js +1 -1
- package/dist/providers/tithely.d.ts +49 -0
- package/dist/providers/tithely.js +1034 -0
- package/dist/providers/vercel.js +1 -1
- package/dist/routes/oauth.js +29 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +5 -3
- package/documentation/auth.md +120 -6
- package/ethos.md +3 -3
- package/package.json +25 -1
- package/templates/node/package.json +1 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { remoteMcp } from "../connectors/remote-mcp.js";
|
|
2
|
+
import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
|
|
3
|
+
import { defined } from "../connectors/api-connector.js";
|
|
4
|
+
/**
|
|
5
|
+
* Basecamp's one hosted MCP endpoint, streamable HTTP.
|
|
6
|
+
*
|
|
7
|
+
* Live but undocumented: as of 2026-10-05 37signals has announced no MCP
|
|
8
|
+
* server, and basecamp.com/agents (where the endpoint's own 401 body points)
|
|
9
|
+
* describes the CLI and SDKs without naming it. What the server does publish is
|
|
10
|
+
* its OAuth discovery — protected-resource metadata naming
|
|
11
|
+
* `https://app.basecamp.com` as the authorization server — and that is what
|
|
12
|
+
* this provider is written against, beside a captured live `tools/list`.
|
|
13
|
+
*/
|
|
14
|
+
export const BASECAMP_MCP_ENDPOINT = "https://mcp.basecamp.com/mcp";
|
|
15
|
+
/**
|
|
16
|
+
* Requested only when Basecamp stops advertising scopes of its own.
|
|
17
|
+
*
|
|
18
|
+
* The MCP client picks the scope from the `WWW-Authenticate` challenge first
|
|
19
|
+
* (`mcp full` today), then the protected resource's `scopes_supported`, and
|
|
20
|
+
* only then this value — appending `offline_access` itself whenever the
|
|
21
|
+
* authorization server lists it. So for Basecamp as it serves today, this is a
|
|
22
|
+
* fallback that matches what the challenge already asks for, not a lever.
|
|
23
|
+
*/
|
|
24
|
+
const FALLBACK_SCOPE = "full mcp offline_access";
|
|
25
|
+
/**
|
|
26
|
+
* Reviewed reads, listed by name (P5), from the live `tools/list` captured
|
|
27
|
+
* 2026-10-05 (227 tools, every one explicitly annotated by the server). Each
|
|
28
|
+
* read below carries the server's own `readOnlyHint: true`; the
|
|
29
|
+
* classification agrees rather than argues. A superset by design: tools gated
|
|
30
|
+
* by account type or feature cost nothing when absent, while an unlisted new
|
|
31
|
+
* one fails closed.
|
|
32
|
+
*/
|
|
33
|
+
const READ_ONLY_TOOLS = new Set([
|
|
34
|
+
// Orientation and identity
|
|
35
|
+
"get_basecamp_guide",
|
|
36
|
+
"get_me",
|
|
37
|
+
"get_account",
|
|
38
|
+
"get_my_profile",
|
|
39
|
+
"get_my_preferences",
|
|
40
|
+
"get_by_url",
|
|
41
|
+
"search",
|
|
42
|
+
"search_mentions",
|
|
43
|
+
// People
|
|
44
|
+
"get_person",
|
|
45
|
+
"list_people",
|
|
46
|
+
"list_pingable_people",
|
|
47
|
+
"list_assignable_people",
|
|
48
|
+
"list_project_people",
|
|
49
|
+
"get_out_of_office",
|
|
50
|
+
// Projects, docks, and templates
|
|
51
|
+
"list_projects",
|
|
52
|
+
"get_project",
|
|
53
|
+
"get_project_tools",
|
|
54
|
+
"get_dock_tool",
|
|
55
|
+
"get_project_construction",
|
|
56
|
+
"list_templates",
|
|
57
|
+
"get_template",
|
|
58
|
+
"list_folders",
|
|
59
|
+
"get_folder",
|
|
60
|
+
// My work and notifications. Reading the Hey! menu changes nothing; the
|
|
61
|
+
// write that does is `mark_as_read`, below.
|
|
62
|
+
"get_my_assignments",
|
|
63
|
+
"get_my_completed_assignments",
|
|
64
|
+
"get_my_due_assignments",
|
|
65
|
+
"get_my_day",
|
|
66
|
+
"get_catchup",
|
|
67
|
+
"get_my_notifications",
|
|
68
|
+
"get_bubble_ups",
|
|
69
|
+
"get_my_note",
|
|
70
|
+
"list_my_bookmarks",
|
|
71
|
+
"list_my_drafts",
|
|
72
|
+
"get_bookmark",
|
|
73
|
+
"get_question_reminders",
|
|
74
|
+
// Answers the same question as the reads above and renders it in a panel
|
|
75
|
+
// for clients that draw one; connecta relays the data and draws nothing.
|
|
76
|
+
"show_my_work",
|
|
77
|
+
// Activity and feeds. Both feed cursors are held by the caller and passed
|
|
78
|
+
// back, so polling advances nothing on Basecamp's side.
|
|
79
|
+
"get_progress_report",
|
|
80
|
+
"get_project_timeline",
|
|
81
|
+
"get_person_progress",
|
|
82
|
+
"list_feed_events",
|
|
83
|
+
"get_event_inbox",
|
|
84
|
+
"list_everything_feed",
|
|
85
|
+
"list_everything_todos",
|
|
86
|
+
"list_everything_cards",
|
|
87
|
+
"summarize_recording",
|
|
88
|
+
// Mints a short-lived, single-connection WebSocket ticket for the event
|
|
89
|
+
// feed's live lane, and the server files it read-only. Reviewed rather than
|
|
90
|
+
// copied: it writes no Basecamp record, and what the ticket unlocks is a
|
|
91
|
+
// read of the same feed `list_feed_events` already serves on this path.
|
|
92
|
+
// Filing it destructive would be the only lever that overrides the server
|
|
93
|
+
// (an additive verdict cannot), and it would buy a standing annotation
|
|
94
|
+
// conflict on every refresh for a tool connecta's one-request calls cannot
|
|
95
|
+
// use anyway — so the guide tells agents not to call it instead.
|
|
96
|
+
"create_stream_ticket",
|
|
97
|
+
// To-dos
|
|
98
|
+
"get_todoset",
|
|
99
|
+
"list_todolists",
|
|
100
|
+
"get_todolist",
|
|
101
|
+
"list_todolist_groups",
|
|
102
|
+
"get_todolist_group",
|
|
103
|
+
"list_todos",
|
|
104
|
+
"get_todo",
|
|
105
|
+
"get_assigned_todos",
|
|
106
|
+
"get_overdue_todos",
|
|
107
|
+
"get_hill_chart",
|
|
108
|
+
// Messages
|
|
109
|
+
"get_message_board",
|
|
110
|
+
"list_messages",
|
|
111
|
+
"get_message",
|
|
112
|
+
"list_message_types",
|
|
113
|
+
"get_message_type",
|
|
114
|
+
// Comments and boosts
|
|
115
|
+
"list_comments",
|
|
116
|
+
"get_comment",
|
|
117
|
+
"list_recording_boosts",
|
|
118
|
+
"list_event_boosts",
|
|
119
|
+
"get_boost",
|
|
120
|
+
// Card tables
|
|
121
|
+
"get_card_table",
|
|
122
|
+
"get_card_column",
|
|
123
|
+
"list_cards",
|
|
124
|
+
"get_card",
|
|
125
|
+
"get_card_step",
|
|
126
|
+
// Chat
|
|
127
|
+
"get_campfire",
|
|
128
|
+
"list_campfire_lines",
|
|
129
|
+
"get_campfire_line",
|
|
130
|
+
"list_campfire_uploads",
|
|
131
|
+
// Schedules
|
|
132
|
+
"get_schedule",
|
|
133
|
+
"list_schedule_entries",
|
|
134
|
+
"get_schedule_entry",
|
|
135
|
+
"get_schedule_entry_occurrence",
|
|
136
|
+
"get_upcoming_schedule",
|
|
137
|
+
"get_calendar",
|
|
138
|
+
"list_lineup_markers",
|
|
139
|
+
// Docs and files
|
|
140
|
+
"get_vault",
|
|
141
|
+
"list_vaults",
|
|
142
|
+
"list_documents",
|
|
143
|
+
"get_document",
|
|
144
|
+
"list_uploads",
|
|
145
|
+
"get_upload",
|
|
146
|
+
"list_upload_versions",
|
|
147
|
+
"get_cloud_file",
|
|
148
|
+
"get_google_document",
|
|
149
|
+
// Check-ins
|
|
150
|
+
"get_questionnaire",
|
|
151
|
+
"list_questions",
|
|
152
|
+
"get_question",
|
|
153
|
+
"list_question_answerers",
|
|
154
|
+
"list_answers",
|
|
155
|
+
"get_answer",
|
|
156
|
+
"get_answers_by_person",
|
|
157
|
+
// Timesheets
|
|
158
|
+
"get_timesheet_report",
|
|
159
|
+
"get_project_timesheet",
|
|
160
|
+
"get_recording_timesheet",
|
|
161
|
+
"get_timesheet_entry",
|
|
162
|
+
// Gauges
|
|
163
|
+
"list_gauges",
|
|
164
|
+
"list_gauge_needles",
|
|
165
|
+
"get_gauge_needle",
|
|
166
|
+
]);
|
|
167
|
+
/**
|
|
168
|
+
* Reviewed writes with their destructive verdict. Most follow the server's
|
|
169
|
+
* own explicit annotation, which is also what the verb says: `create_*` brings
|
|
170
|
+
* a record into being, and `update_*`, `trash_*`, `delete_*`, `destroy_*`,
|
|
171
|
+
* moves, repositions, and toggles act on one that exists. The four rows that
|
|
172
|
+
* part from the server carry their reason.
|
|
173
|
+
*
|
|
174
|
+
* Trash and permanent deletion are both destructive, because both take a
|
|
175
|
+
* record out of everyone's view; the difference — `trash_*` is restorable for
|
|
176
|
+
* 25 days, `delete_*`/`destroy_*` and `remove_account_logo` are not — is the
|
|
177
|
+
* guide's to say, not a third verdict's.
|
|
178
|
+
*/
|
|
179
|
+
const WRITE_TOOLS = new Map([
|
|
180
|
+
// Account and profile
|
|
181
|
+
["update_account_name", "destructive"],
|
|
182
|
+
["remove_account_logo", "destructive"],
|
|
183
|
+
["update_my_profile", "destructive"],
|
|
184
|
+
["update_my_preferences", "destructive"],
|
|
185
|
+
// "The first update creates the note", but every later one replaces it.
|
|
186
|
+
["update_my_note", "destructive"],
|
|
187
|
+
["enable_out_of_office", "destructive"],
|
|
188
|
+
["disable_out_of_office", "destructive"],
|
|
189
|
+
// Projects, docks, and templates
|
|
190
|
+
["create_project", "additive"],
|
|
191
|
+
["create_project_from_template", "additive"],
|
|
192
|
+
["update_project", "destructive"],
|
|
193
|
+
// Grants and revokes access, and can create people — that is, invite an
|
|
194
|
+
// outside address into the account (the server marks it open-world).
|
|
195
|
+
["update_project_access", "destructive"],
|
|
196
|
+
["trash_project", "destructive"],
|
|
197
|
+
["create_template", "additive"],
|
|
198
|
+
["update_template", "destructive"],
|
|
199
|
+
["delete_template", "destructive"],
|
|
200
|
+
["enable_dock_tool", "destructive"],
|
|
201
|
+
["disable_dock_tool", "destructive"],
|
|
202
|
+
["update_dock_tool", "destructive"],
|
|
203
|
+
["reposition_dock_tool", "destructive"],
|
|
204
|
+
["trash_dock_tool", "destructive"],
|
|
205
|
+
// Destructive where the server says additive: "optionally filing projects
|
|
206
|
+
// into it" moves projects that already sit on the person's home screen, so
|
|
207
|
+
// the call can rearrange existing state as well as create a folder.
|
|
208
|
+
["create_folder", "destructive"],
|
|
209
|
+
["update_folder", "destructive"],
|
|
210
|
+
["delete_folder", "destructive"],
|
|
211
|
+
// My work. Prioritizing adds an assignment to the person's own list and
|
|
212
|
+
// removes nothing — RevenueCat's attach/detach argument — so it is additive
|
|
213
|
+
// where the server says destructive; deprioritizing and reordering are the
|
|
214
|
+
// halves that move existing state. The server's explicit `destructiveHint`
|
|
215
|
+
// still stands at runtime: the classification fills silence and does not
|
|
216
|
+
// soften an annotation, so this verdict is the release's record, not an
|
|
217
|
+
// overrule.
|
|
218
|
+
["prioritize_assignment", "additive"],
|
|
219
|
+
["deprioritize_assignment", "destructive"],
|
|
220
|
+
["reorder_up_next", "destructive"],
|
|
221
|
+
// Flips the read state of existing notifications, which is how a person
|
|
222
|
+
// knows what they have not seen yet — and no tool flips it back.
|
|
223
|
+
["mark_as_read", "destructive"],
|
|
224
|
+
["create_bookmark", "additive"],
|
|
225
|
+
["delete_bookmark", "destructive"],
|
|
226
|
+
// To-dos
|
|
227
|
+
["create_todolist", "additive"],
|
|
228
|
+
["update_todolist", "destructive"],
|
|
229
|
+
["reposition_todolist", "destructive"],
|
|
230
|
+
["trash_todolist", "destructive"],
|
|
231
|
+
["create_todolist_group", "additive"],
|
|
232
|
+
["update_todolist_group", "destructive"],
|
|
233
|
+
["reposition_todolist_group", "destructive"],
|
|
234
|
+
["trash_todolist_group", "destructive"],
|
|
235
|
+
["create_todo", "additive"],
|
|
236
|
+
["create_todoset_todo", "additive"],
|
|
237
|
+
["update_todo", "destructive"],
|
|
238
|
+
["complete_todo", "destructive"],
|
|
239
|
+
["uncomplete_todo", "destructive"],
|
|
240
|
+
["reposition_todo", "destructive"],
|
|
241
|
+
["trash_todo", "destructive"],
|
|
242
|
+
["update_hill_chart_settings", "destructive"],
|
|
243
|
+
// Messages
|
|
244
|
+
["create_message", "additive"],
|
|
245
|
+
["update_message", "destructive"],
|
|
246
|
+
["pin_message", "destructive"],
|
|
247
|
+
["unpin_message", "destructive"],
|
|
248
|
+
["archive_message", "destructive"],
|
|
249
|
+
["unarchive_message", "destructive"],
|
|
250
|
+
["trash_message", "destructive"],
|
|
251
|
+
["create_message_type", "additive"],
|
|
252
|
+
["update_message_type", "destructive"],
|
|
253
|
+
["delete_message_type", "destructive"],
|
|
254
|
+
// Comments and boosts
|
|
255
|
+
["create_comment", "additive"],
|
|
256
|
+
["update_comment", "destructive"],
|
|
257
|
+
["trash_comment", "destructive"],
|
|
258
|
+
["create_recording_boost", "additive"],
|
|
259
|
+
["create_event_boost", "additive"],
|
|
260
|
+
["delete_boost", "destructive"],
|
|
261
|
+
// Card tables
|
|
262
|
+
["create_card_column", "additive"],
|
|
263
|
+
["update_card_column", "destructive"],
|
|
264
|
+
["move_card_column", "destructive"],
|
|
265
|
+
["set_card_column_color", "destructive"],
|
|
266
|
+
["enable_card_column_on_hold", "destructive"],
|
|
267
|
+
["disable_card_column_on_hold", "destructive"],
|
|
268
|
+
// Watching a column adds the person to its subscribers and removes nothing:
|
|
269
|
+
// additive for the same reason as `prioritize_assignment`, with the server's
|
|
270
|
+
// explicit `destructiveHint` still the one a caller sees. Two endpoints, one
|
|
271
|
+
// effect, so both names get the same verdict; the unwatching halves stay
|
|
272
|
+
// destructive.
|
|
273
|
+
["watch_column", "additive"],
|
|
274
|
+
["subscribe_to_card_column", "additive"],
|
|
275
|
+
["unwatch_column", "destructive"],
|
|
276
|
+
["unsubscribe_from_card_column", "destructive"],
|
|
277
|
+
["create_wormhole", "additive"],
|
|
278
|
+
["update_wormhole", "destructive"],
|
|
279
|
+
["delete_wormhole", "destructive"],
|
|
280
|
+
["create_card", "additive"],
|
|
281
|
+
["update_card", "destructive"],
|
|
282
|
+
["move_card", "destructive"],
|
|
283
|
+
["trash_card", "destructive"],
|
|
284
|
+
["create_card_step", "additive"],
|
|
285
|
+
["update_card_step", "destructive"],
|
|
286
|
+
["complete_card_step", "destructive"],
|
|
287
|
+
["uncomplete_card_step", "destructive"],
|
|
288
|
+
["reposition_card_step", "destructive"],
|
|
289
|
+
["delete_card_step", "destructive"],
|
|
290
|
+
// Chat
|
|
291
|
+
["create_campfire_line", "additive"],
|
|
292
|
+
["create_campfire_upload", "additive"],
|
|
293
|
+
["delete_campfire_line", "destructive"],
|
|
294
|
+
// Schedules
|
|
295
|
+
["create_schedule_entry", "additive"],
|
|
296
|
+
["update_schedule_entry", "destructive"],
|
|
297
|
+
["trash_schedule_entry", "destructive"],
|
|
298
|
+
["update_schedule_settings", "destructive"],
|
|
299
|
+
["update_calendar", "destructive"],
|
|
300
|
+
["create_lineup_marker", "additive"],
|
|
301
|
+
["update_lineup_marker", "destructive"],
|
|
302
|
+
["delete_lineup_marker", "destructive"],
|
|
303
|
+
// Docs and files. `create_attachment` uploads bytes and returns the
|
|
304
|
+
// `attachable_sgid` that `create_upload` files into a vault.
|
|
305
|
+
["create_vault", "additive"],
|
|
306
|
+
["update_vault", "destructive"],
|
|
307
|
+
["create_document", "additive"],
|
|
308
|
+
["update_document", "destructive"],
|
|
309
|
+
["trash_document", "destructive"],
|
|
310
|
+
["create_attachment", "additive"],
|
|
311
|
+
["create_upload", "additive"],
|
|
312
|
+
["update_upload", "destructive"],
|
|
313
|
+
["trash_upload", "destructive"],
|
|
314
|
+
["create_cloud_file", "additive"],
|
|
315
|
+
["update_cloud_file", "destructive"],
|
|
316
|
+
["create_google_document", "additive"],
|
|
317
|
+
["update_google_document", "destructive"],
|
|
318
|
+
// Check-ins
|
|
319
|
+
["create_question", "additive"],
|
|
320
|
+
["update_question", "destructive"],
|
|
321
|
+
["pause_question", "destructive"],
|
|
322
|
+
["resume_question", "destructive"],
|
|
323
|
+
["update_question_notification_settings", "destructive"],
|
|
324
|
+
["create_answer", "additive"],
|
|
325
|
+
["update_answer", "destructive"],
|
|
326
|
+
// Timesheets
|
|
327
|
+
["create_timesheet_entry", "additive"],
|
|
328
|
+
["update_timesheet_entry", "destructive"],
|
|
329
|
+
["destroy_timesheet_entry", "destructive"],
|
|
330
|
+
// Gauges
|
|
331
|
+
["toggle_gauge", "destructive"],
|
|
332
|
+
["create_gauge_needle", "additive"],
|
|
333
|
+
["update_gauge_needle", "destructive"],
|
|
334
|
+
["destroy_gauge_needle", "destructive"],
|
|
335
|
+
]);
|
|
336
|
+
/**
|
|
337
|
+
* One release-reviewed manifest, used both to classify a live tool and as the
|
|
338
|
+
* baseline the runtime drift check compares against, so the annotation a
|
|
339
|
+
* caller gets and the verdict a check reads can never disagree (P13). Names
|
|
340
|
+
* and verdicts only — no schemas are vendored; the live `tools/list` response
|
|
341
|
+
* stays authoritative.
|
|
342
|
+
*
|
|
343
|
+
* Basecamp publishes no tool inventory, so this list has no public document
|
|
344
|
+
* to be checked against before a release. Catalog drift is visible at runtime
|
|
345
|
+
* only — as unclassified-tool and unserved-name counts on connector status and
|
|
346
|
+
* `/health` — and the credential-free provider check reads the OAuth
|
|
347
|
+
* discovery metadata the server does publish instead.
|
|
348
|
+
*/
|
|
349
|
+
export const BASECAMP_VETTED_CATALOG = vettedCatalog({
|
|
350
|
+
reads: READ_ONLY_TOOLS,
|
|
351
|
+
writes: WRITE_TOOLS,
|
|
352
|
+
});
|
|
353
|
+
/** The catalog's summary bound; a longer declared value throws (`src/registry.ts`). */
|
|
354
|
+
const SUMMARY_BUDGET = 120;
|
|
355
|
+
/**
|
|
356
|
+
* Fit a purpose-bearing summary inside the catalog's bound. Basecamp's routing
|
|
357
|
+
* fact is the account a grant reached, which only the operator's purpose can
|
|
358
|
+
* name, so the summary carries the purpose and clipping is this function's
|
|
359
|
+
* job rather than the operator's.
|
|
360
|
+
*/
|
|
361
|
+
function boundedSummary(prefix, purpose) {
|
|
362
|
+
const full = `${prefix}${purpose}`;
|
|
363
|
+
if (full.length <= SUMMARY_BUDGET)
|
|
364
|
+
return full;
|
|
365
|
+
return `${full.slice(0, SUMMARY_BUDGET - 1).trimEnd()}…`;
|
|
366
|
+
}
|
|
367
|
+
function usageGuide(purpose, authScope, instructions) {
|
|
368
|
+
const accountInstructions = instructions?.trim();
|
|
369
|
+
// Leads the guide because discovery summarizes a connector by its first
|
|
370
|
+
// content line, and which account a call lands in is the one thing that
|
|
371
|
+
// decides routing between two Basecamp connections (P3).
|
|
372
|
+
const grant = authScope === "personal"
|
|
373
|
+
? "Each person authorizes this connector separately and picks the account on Basecamp's consent screen, so two people can reach different accounts through it."
|
|
374
|
+
: "One shared authorization, made on Basecamp's consent screen, decides that account for everyone who uses this connector.";
|
|
375
|
+
return `# Basecamp usage
|
|
376
|
+
|
|
377
|
+
One account per authorization: ${purpose}. Basecamp's hosted server acts in the single account the grant was made for, and no tool takes an account id. ${grant} Another account is another connector, never an argument.
|
|
378
|
+
|
|
379
|
+
- Confirm the account before relying on it: \`get_me\` returns the signed-in person and a label naming the connected account. A record id from another account's URL answers "not found" here — read that as wrong connector, not missing data.
|
|
380
|
+
- Basecamp's own reference notes are a read away: \`get_basecamp_guide\` with \`topic\` \`concepts\`, \`finding-things\`, \`my-work\`, \`posting-and-mentions\`, or \`deleting-and-trash\`. Read the topic before an unfamiliar sequence instead of guessing one.
|
|
381
|
+
- Resolve ids before acting; never guess one. A Basecamp URL carries the account, project, and record ids, and \`get_by_url\` reads any link into those ids, the record, and where a reply goes. \`search\` finds records by text; \`list_projects\` then \`get_project\` returns a project's dock, whose tool ids (to-do set, message board, schedule, vault, card table, chat) the create tools take. People are ids from \`list_people\`, \`list_project_people\`, or \`list_pingable_people\`; mention someone by passing ids as \`create_comment\`'s \`mentions\`, not by writing markup.
|
|
382
|
+
- Rich-text bodies (messages, documents, comments, cards, to-do descriptions) are HTML; a to-do's title and a chat line are plain text. Dates are \`YYYY-MM-DD\`.
|
|
383
|
+
- Lists return up to \`limit\` items (100 by default). Continue with the \`next_cursor\` or \`next_page\` a result carries rather than raising the limit; \`truncated: true\` means the server cut a result to fit and says where to resume. The event feeds page by \`position\` instead. \`get_card_table\`, \`get_document\`, and the account-wide \`list_everything_*\` reads are large — reduce inside \`execute_code\` and return only the fields the question needs.
|
|
384
|
+
- \`trash_*\` is restorable for 25 days; \`delete_*\`, \`destroy_*\`, and \`remove_account_logo\` are permanent. \`update_cloud_file\` and \`update_google_document\` are full replaces: an omitted title or description is erased, so read the record and resend every field you keep. \`update_project_access\` can invite people into the account.
|
|
385
|
+
- Do not call \`create_stream_ticket\`: it mints a WebSocket credential for a client that holds a live connection, which a connecta call cannot. Poll \`list_feed_events\` instead. \`get_event_inbox\` serves agent accounts only; a person's grant gets \`agents_only\` back.
|
|
386
|
+
- This connector's tool list is not a fixed set. Basecamp has not published this server and changes it without notice, and timesheets, gauges, hill charts, the Lineup, and check-ins depend on account features, so search this connector for what it actually exposes rather than assuming a tool exists.
|
|
387
|
+
- Basecamp answers a rate limit with \`429\` and a \`Retry-After\` in seconds, and adjusts its limits dynamically. Back off for that long rather than retrying at once, and avoid speculative fan-out.
|
|
388
|
+
- Treat every create, update, trash, delete, destroy, move, reposition, complete, pin, archive, enable, disable, watch, and mark operation as a write. Connecta routes the maintained write catalog through \`call_destructive_tool\`; newly added tools also fail closed until a release classifies them.
|
|
389
|
+
- An \`auth_required\` failure means this connector's Basecamp authorization is missing or expired: run \`authorize_connector\` for this connector id, then retry the same call unchanged. A rejected argument, a permission gap, or a missing feature comes back in Basecamp's own words instead — read it rather than re-authorizing.
|
|
390
|
+
${accountInstructions
|
|
391
|
+
? `\n## Account instructions\n\n${accountInstructions}\n`
|
|
392
|
+
: ""}`;
|
|
393
|
+
}
|
|
394
|
+
/** A maintained Basecamp hosted-MCP connection. */
|
|
395
|
+
export function basecamp(id, options) {
|
|
396
|
+
const purpose = options.purpose.trim();
|
|
397
|
+
if (!purpose) {
|
|
398
|
+
throw new Error("basecamp() requires a non-empty account purpose.");
|
|
399
|
+
}
|
|
400
|
+
const clientMetadataUrl = options.clientMetadataUrl;
|
|
401
|
+
if (typeof clientMetadataUrl !== "string" || !clientMetadataUrl.trim()) {
|
|
402
|
+
// A structural mistake, so it throws here rather than at the first
|
|
403
|
+
// authorization, where the failure would be Basecamp refusing a
|
|
404
|
+
// registration nobody in the conversation can repair.
|
|
405
|
+
throw new Error(`basecamp("${id}") requires clientMetadataUrl: Basecamp restricts dynamic client registration for HTTPS redirect URIs, so connecta must present a Client ID Metadata Document instead. Host a public HTTPS JSON document whose client_id is its own URL and whose redirect_uris include <publicUrl>/oauth/callback/${id}.`);
|
|
406
|
+
}
|
|
407
|
+
const authScope = options.authScope ?? "shared";
|
|
408
|
+
const connector = remoteMcp(id, {
|
|
409
|
+
url: BASECAMP_MCP_ENDPOINT,
|
|
410
|
+
...(options.authScope ? { authScope: options.authScope } : {}),
|
|
411
|
+
title: options.title ?? "Basecamp",
|
|
412
|
+
description: `Basecamp projects, to-dos, messages, cards, schedules, and files (one account per authorization) — ${purpose}`,
|
|
413
|
+
// OAuth only (P9's headless alternative knowingly missed). Basecamp issues
|
|
414
|
+
// personal access tokens at app.basecamp.com/my/access_tokens, but nothing
|
|
415
|
+
// public says this endpoint accepts one, and a PAT carries no refresh
|
|
416
|
+
// token; shipping a slot for a framing nobody has verified is the
|
|
417
|
+
// borrowed-convention mistake P9 names. The access mode P4 asks for is
|
|
418
|
+
// missing for the same kind of reason: Basecamp advertises a `read` scope
|
|
419
|
+
// beside `full`, but the MCP client requests the scope the server's own
|
|
420
|
+
// challenge names (`mcp full`) ahead of anything configured here, and what
|
|
421
|
+
// `read` permits on this server is unverified — a read-only option would
|
|
422
|
+
// be a label, not a guarantee. The classification fails closed either way.
|
|
423
|
+
auth: {
|
|
424
|
+
type: "oauth",
|
|
425
|
+
clientMetadataUrl,
|
|
426
|
+
scope: FALLBACK_SCOPE,
|
|
427
|
+
},
|
|
428
|
+
requireHttps: true,
|
|
429
|
+
usageGuide: {
|
|
430
|
+
content: usageGuide(purpose, authScope, options.instructions),
|
|
431
|
+
// Explicit rather than derived, and purpose-bearing: the derived summary
|
|
432
|
+
// would cut the account sentence mid-clause, and two Basecamp
|
|
433
|
+
// connections differ only by the account each grant reached (P3).
|
|
434
|
+
summary: boundedSummary("One account per grant; get_me names it: ", purpose),
|
|
435
|
+
// Not `required`. Basecamp's own schemas and `get_basecamp_guide` cover
|
|
436
|
+
// each call; this guide carries account routing and the id chain, worth
|
|
437
|
+
// reading before a run rather than before every call.
|
|
438
|
+
},
|
|
439
|
+
...defined({
|
|
440
|
+
callAdmission: options.callAdmission,
|
|
441
|
+
maxResultBytes: options.maxResultBytes,
|
|
442
|
+
}),
|
|
443
|
+
});
|
|
444
|
+
return withVettedCatalog(connector, BASECAMP_VETTED_CATALOG);
|
|
445
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Connector, ConnectorCallAdmissionPolicy } from "../types.js";
|
|
2
|
+
/** Every church's API lives at `https://<subdomain>.breezechms.com/api`. */
|
|
3
|
+
export declare const BREEZE_HOST_SUFFIX = ".breezechms.com";
|
|
4
|
+
export interface BreezeOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The church's Breeze subdomain — `gracechurch` for
|
|
7
|
+
* `gracechurch.breezechms.com`. Required, validated as one hostname label at
|
|
8
|
+
* construction, and the only host input: no argument can steer a request
|
|
9
|
+
* (or the API key) anywhere else.
|
|
10
|
+
*/
|
|
11
|
+
subdomain: string;
|
|
12
|
+
/** Which church this connection serves, and for whom. */
|
|
13
|
+
purpose: string;
|
|
14
|
+
/** Human-readable display name; defaults to "Breeze ChMS (<subdomain>)". */
|
|
15
|
+
title?: string;
|
|
16
|
+
/** Church-specific conventions appended to the maintained provider guide. */
|
|
17
|
+
instructions?: string;
|
|
18
|
+
/** Downstream auth ownership. Defaults to one shared deployment grant. */
|
|
19
|
+
authScope?: "shared" | "personal";
|
|
20
|
+
/**
|
|
21
|
+
* Optional per-runtime downstream call-admission policy. None is declared by
|
|
22
|
+
* default: Breeze documents no rate limit, and the only number in
|
|
23
|
+
* circulation (~20 requests per minute) is a third party's. An operator who
|
|
24
|
+
* wants to stay under it can supply, for example,
|
|
25
|
+
* `{ rules: [{ maxConcurrency: 2, budget: { kind: "rolling-window", maxCalls: 20, windowMs: 60_000 } }] }`.
|
|
26
|
+
*/
|
|
27
|
+
callAdmission?: ConnectorCallAdmissionPolicy;
|
|
28
|
+
/** Connector-specific inline result limit; omit to inherit the deployment. */
|
|
29
|
+
maxResultBytes?: number;
|
|
30
|
+
/** Default `list_people` page size. Defaults to 100; connecta's cap is 1,000. */
|
|
31
|
+
defaultPageSize?: number;
|
|
32
|
+
}
|
|
33
|
+
/** A maintained Breeze ChMS connection for one church's subdomain. */
|
|
34
|
+
export declare function breeze(id: string, options: BreezeOptions): Connector;
|