@zackbart/connecta 0.27.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.
Files changed (73) hide show
  1. package/AGENTS.md +6 -6
  2. package/CHANGELOG.md +229 -0
  3. package/README.md +23 -24
  4. package/bin/connecta.mjs +1 -5
  5. package/dist/activity.d.ts +5 -4
  6. package/dist/activity.js +1 -10
  7. package/dist/artifacts/connector.js +1 -1
  8. package/dist/artifacts/json.js +10 -0
  9. package/dist/artifacts/operations.js +1 -1
  10. package/dist/artifacts/validate.js +1 -2
  11. package/dist/auth/downstream-oauth.js +147 -28
  12. package/dist/auth/static-oauth.js +504 -0
  13. package/dist/catalog-service.js +1 -1
  14. package/dist/connectors/api-connector.d.ts +168 -0
  15. package/dist/connectors/api-connector.js +127 -0
  16. package/dist/connectors/api.d.ts +8 -71
  17. package/dist/connectors/api.js +10 -99
  18. package/dist/connectors/guarded-fetch.js +37 -3
  19. package/dist/connectors/remote-mcp.js +7 -32
  20. package/dist/execute.js +92 -368
  21. package/dist/executors/quickjs.js +1 -1
  22. package/dist/exempt-writes.js +162 -0
  23. package/dist/index.d.ts +13 -32
  24. package/dist/index.js +14 -47
  25. package/dist/invocation.js +6 -19
  26. package/dist/meta-tools.js +1 -2
  27. package/dist/oauth-partition.js +17 -0
  28. package/dist/operator-ui/generated.js +1 -1
  29. package/dist/operator-ui/model.d.ts +3 -3
  30. package/dist/operator-ui/view.js +6 -4
  31. package/dist/providers/basecamp.d.ts +65 -0
  32. package/dist/providers/basecamp.js +445 -0
  33. package/dist/providers/breeze.d.ts +34 -0
  34. package/dist/providers/breeze.js +1402 -0
  35. package/dist/providers/ccb.d.ts +103 -0
  36. package/dist/providers/ccb.js +1570 -0
  37. package/dist/providers/cloudflare.js +1 -1
  38. package/dist/providers/linear.js +1 -1
  39. package/dist/providers/mixpanel.js +1 -1
  40. package/dist/providers/notion.js +1 -1
  41. package/dist/providers/overflow.d.ts +60 -0
  42. package/dist/providers/overflow.js +1572 -0
  43. package/dist/providers/planning-center.d.ts +59 -0
  44. package/dist/providers/planning-center.js +2039 -0
  45. package/dist/providers/revenuecat.js +1 -1
  46. package/dist/providers/stripe.js +1 -1
  47. package/dist/providers/tithely.d.ts +49 -0
  48. package/dist/providers/tithely.js +1034 -0
  49. package/dist/providers/vercel.js +1 -1
  50. package/dist/routes/mcp.js +5 -21
  51. package/dist/routes/oauth.js +29 -3
  52. package/dist/routes/shared.d.ts +1 -6
  53. package/dist/server.js +0 -4
  54. package/dist/skills.js +8 -23
  55. package/dist/tool-safety.d.ts +1 -1
  56. package/dist/tool-safety.js +1 -1
  57. package/dist/types.d.ts +2 -2
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/documentation/architecture.md +15 -17
  61. package/documentation/auth.md +120 -6
  62. package/documentation/code-mode.md +91 -194
  63. package/documentation/meta-tools.md +38 -41
  64. package/ethos.md +8 -9
  65. package/examples/worker/README.md +10 -14
  66. package/examples/worker/src/d1-activity-row.ts +4 -1
  67. package/examples/worker/src/index.ts +1 -9
  68. package/package.json +26 -2
  69. package/templates/node/AGENTS.md +4 -3
  70. package/templates/node/README.md +3 -4
  71. package/templates/node/package.json +1 -1
  72. package/dist/resumable.js +0 -1323
  73. package/dist/run-journal.js +0 -347
@@ -7,10 +7,10 @@ import type { CatalogAccessObservation, CatalogDriftReport } from "../types.js";
7
7
  *
8
8
  * - `runs_in_programs` — explicitly read-only, so `execute_code` may call it.
9
9
  * - `exempt` — not read-only, but the deployment's `execute.approval` lets a
10
- * program call it without pausing (#566). Still approval-required
10
+ * program call it without asking (#566). Still approval-required
11
11
  * everywhere else.
12
- * - `needs_approval` — everything else: a program pauses for
13
- * `resume_execution`, and a direct call crosses `call_destructive_tool`.
12
+ * - `needs_approval` — everything else: a program refuses it, and it crosses
13
+ * `call_destructive_tool`, where the host asks.
14
14
  *
15
15
  * The badge renders from a table keyed by this union, so each value is one
16
16
  * entry, not a branch.
@@ -413,12 +413,12 @@ export const TOOL_SAFETY_BADGE = {
413
413
  exempt: {
414
414
  label: "exempt from approval",
415
415
  tone: "neutral",
416
- title: "Not read-only, but this deployment's config lets programs call it without pausing. Each call still counts against the write budget and appears in activity; call_tool still refuses it.",
416
+ title: "Not read-only, but this deployment's config lets programs call it without asking. Each call still counts against the write budget and appears in activity; call_tool still refuses it.",
417
417
  },
418
418
  needs_approval: {
419
419
  label: "asks for approval",
420
420
  tone: "warn",
421
- title: "Not explicitly read-only: a program pauses for resume_execution, and a direct call crosses call_destructive_tool — either way the host asks first.",
421
+ title: "Not explicitly read-only: programs refuse it, and it runs through call_destructive_tool, where the host asks first.",
422
422
  },
423
423
  };
424
424
  /**
@@ -608,7 +608,8 @@ export function activitySummary(events) {
608
608
  return `${events.length} loaded call${events.length === 1 ? "" : "s"} · ${tools} tool${tools === 1 ? "" : "s"}`;
609
609
  }
610
610
  // A pause and an approval are neither success nor failure: each gets its own
611
- // class, and the stylesheet paints neither as an error.
611
+ // class, and the stylesheet paints neither as an error. Nothing has emitted
612
+ // either since issue #672 removed program pauses, but older rows carry them.
612
613
  const ACTIVITY_OUTCOMES = [
613
614
  "success",
614
615
  "error",
@@ -626,7 +627,7 @@ const OUTCOME_BADGE = {
626
627
  error: { label: "Failed", tone: "danger" },
627
628
  timeout: { label: "Timed out", tone: "danger" },
628
629
  cancelled: { label: "Cancelled", tone: "neutral" },
629
- paused: { label: "Waiting for approval", tone: "warn" },
630
+ paused: { label: "Paused for approval", tone: "warn" },
630
631
  approved: { label: "Approved", tone: "ok" },
631
632
  };
632
633
  export function activityOutcomeBadge(outcome) {
@@ -636,6 +637,7 @@ const SOURCE_LABELS = {
636
637
  execute_code: "In a program",
637
638
  call_tool: "Direct call",
638
639
  call_destructive_tool: "Direct call, approved by the host",
640
+ // History only: resume_execution left with program pauses (#672).
639
641
  resume_execution: "Resumed program",
640
642
  batch_call: "Batch call",
641
643
  };
@@ -0,0 +1,65 @@
1
+ import type { Connector, ConnectorCallAdmissionPolicy } from "../types.js";
2
+ /**
3
+ * Basecamp's one hosted MCP endpoint, streamable HTTP.
4
+ *
5
+ * Live but undocumented: as of 2026-10-05 37signals has announced no MCP
6
+ * server, and basecamp.com/agents (where the endpoint's own 401 body points)
7
+ * describes the CLI and SDKs without naming it. What the server does publish is
8
+ * its OAuth discovery — protected-resource metadata naming
9
+ * `https://app.basecamp.com` as the authorization server — and that is what
10
+ * this provider is written against, beside a captured live `tools/list`.
11
+ */
12
+ export declare const BASECAMP_MCP_ENDPOINT = "https://mcp.basecamp.com/mcp";
13
+ export interface BasecampOptions {
14
+ /** Human-readable display name; defaults to "Basecamp". */
15
+ title?: string;
16
+ /**
17
+ * Downstream auth ownership. Defaults to one shared deployment grant. With
18
+ * `"personal"` each principal authorizes separately and may pick a
19
+ * different Basecamp account at consent, which the guide says.
20
+ */
21
+ authScope?: "shared" | "personal";
22
+ /** Which Basecamp account this is and what decisions it answers. */
23
+ purpose: string;
24
+ /**
25
+ * Required. The HTTPS URL of a Client ID Metadata Document this deployment
26
+ * controls: a public JSON document whose `client_id` is its own URL, whose
27
+ * `redirect_uris` include `<publicUrl>/oauth/callback/<connector id>`, with
28
+ * `token_endpoint_auth_method: "none"` and the `authorization_code` and
29
+ * `refresh_token` grants. Basecamp advertises dynamic client registration
30
+ * but restricts it for HTTPS redirect URIs, which every deployed connecta
31
+ * has; its authorization server accepts a metadata-document client id
32
+ * instead (`client_id_metadata_document_supported: true`). Connecta does not
33
+ * serve one of its own yet, so the deployment hosts it.
34
+ */
35
+ clientMetadataUrl: string;
36
+ /** Account-specific conventions appended to the maintained provider guide. */
37
+ instructions?: string;
38
+ /** Connector-specific inline result limit; omit to inherit the deployment. */
39
+ maxResultBytes?: number;
40
+ /**
41
+ * Optional per-runtime policy. There is no default: Basecamp's API documents
42
+ * several dynamically adjusted limits (GET versus POST, per second, hour,
43
+ * and day) and quotes only "currently 50 requests per 10 second period per
44
+ * IP address" for scale — metered at the IP that calls the API, which is
45
+ * Basecamp's own MCP server rather than this runtime. A per-runtime counter
46
+ * approximates none of that, so the operator picks (P12).
47
+ */
48
+ callAdmission?: ConnectorCallAdmissionPolicy;
49
+ }
50
+ /**
51
+ * One release-reviewed manifest, used both to classify a live tool and as the
52
+ * baseline the runtime drift check compares against, so the annotation a
53
+ * caller gets and the verdict a check reads can never disagree (P13). Names
54
+ * and verdicts only — no schemas are vendored; the live `tools/list` response
55
+ * stays authoritative.
56
+ *
57
+ * Basecamp publishes no tool inventory, so this list has no public document
58
+ * to be checked against before a release. Catalog drift is visible at runtime
59
+ * only — as unclassified-tool and unserved-name counts on connector status and
60
+ * `/health` — and the credential-free provider check reads the OAuth
61
+ * discovery metadata the server does publish instead.
62
+ */
63
+ export declare const BASECAMP_VETTED_CATALOG: import("../catalog-drift.js").VettedCatalog;
64
+ /** A maintained Basecamp hosted-MCP connection. */
65
+ export declare function basecamp(id: string, options: BasecampOptions): Connector;
@@ -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;