@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.
- package/AGENTS.md +6 -6
- package/CHANGELOG.md +229 -0
- package/README.md +23 -24
- package/bin/connecta.mjs +1 -5
- package/dist/activity.d.ts +5 -4
- package/dist/activity.js +1 -10
- package/dist/artifacts/connector.js +1 -1
- package/dist/artifacts/json.js +10 -0
- package/dist/artifacts/operations.js +1 -1
- package/dist/artifacts/validate.js +1 -2
- package/dist/auth/downstream-oauth.js +147 -28
- package/dist/auth/static-oauth.js +504 -0
- package/dist/catalog-service.js +1 -1
- 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/execute.js +92 -368
- package/dist/executors/quickjs.js +1 -1
- package/dist/exempt-writes.js +162 -0
- package/dist/index.d.ts +13 -32
- package/dist/index.js +14 -47
- package/dist/invocation.js +6 -19
- package/dist/meta-tools.js +1 -2
- package/dist/oauth-partition.js +17 -0
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/model.d.ts +3 -3
- package/dist/operator-ui/view.js +6 -4
- 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/mcp.js +5 -21
- package/dist/routes/oauth.js +29 -3
- package/dist/routes/shared.d.ts +1 -6
- package/dist/server.js +0 -4
- package/dist/skills.js +8 -23
- package/dist/tool-safety.d.ts +1 -1
- package/dist/tool-safety.js +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +15 -17
- package/documentation/auth.md +120 -6
- package/documentation/code-mode.md +91 -194
- package/documentation/meta-tools.md +38 -41
- package/ethos.md +8 -9
- package/examples/worker/README.md +10 -14
- package/examples/worker/src/d1-activity-row.ts +4 -1
- package/examples/worker/src/index.ts +1 -9
- package/package.json +26 -2
- package/templates/node/AGENTS.md +4 -3
- package/templates/node/README.md +3 -4
- package/templates/node/package.json +1 -1
- package/dist/resumable.js +0 -1323
- 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
|
|
10
|
+
* program call it without asking (#566). Still approval-required
|
|
11
11
|
* everywhere else.
|
|
12
|
-
* - `needs_approval` — everything else: a program
|
|
13
|
-
* `
|
|
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.
|
package/dist/operator-ui/view.js
CHANGED
|
@@ -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
|
|
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:
|
|
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: "
|
|
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;
|