@nimbus-dev/connectors 0.2.1 → 0.2.3

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 (220) hide show
  1. package/README.md +19 -7
  2. package/connectors/airflow/src/search-filter.ts +2 -15
  3. package/connectors/airflow/src/server.ts +2 -87
  4. package/connectors/airflow/src/tools.ts +74 -0
  5. package/connectors/apple/src/adapters.ts +326 -0
  6. package/connectors/apple/src/apple-mail-core.ts +4 -8
  7. package/connectors/apple/src/calendar-tools.ts +28 -8
  8. package/connectors/apple/src/server.ts +23 -512
  9. package/connectors/apple/src/tools.ts +18 -44
  10. package/connectors/argocd/src/server.ts +25 -38
  11. package/connectors/athena/src/tools.ts +26 -43
  12. package/connectors/aws/src/server.ts +2 -183
  13. package/connectors/aws/src/tools.ts +199 -0
  14. package/connectors/azure/src/server.ts +2 -131
  15. package/connectors/azure/src/tools.ts +134 -0
  16. package/connectors/bigeye/src/search-filter.ts +4 -15
  17. package/connectors/bigeye/src/server.ts +38 -38
  18. package/connectors/bigquery/src/tools.ts +1 -26
  19. package/connectors/bitbucket/src/server.ts +2 -244
  20. package/connectors/bitbucket/src/tools.ts +241 -0
  21. package/connectors/bitrise/src/server.ts +2 -79
  22. package/connectors/bitrise/src/tools.ts +76 -0
  23. package/connectors/canva/src/search-filter.ts +2 -11
  24. package/connectors/canva/src/server.ts +2 -73
  25. package/connectors/canva/src/tools.ts +66 -0
  26. package/connectors/circleci/src/server.ts +2 -176
  27. package/connectors/circleci/src/tools.ts +193 -0
  28. package/connectors/cloud-logging/src/tools.ts +19 -35
  29. package/connectors/cloudwatch/src/tools.ts +20 -38
  30. package/connectors/codemagic/src/server.ts +2 -70
  31. package/connectors/codemagic/src/tools.ts +71 -0
  32. package/connectors/confluence/src/server.ts +2 -301
  33. package/connectors/confluence/src/tools.ts +320 -0
  34. package/connectors/dagster/src/server.ts +2 -214
  35. package/connectors/dagster/src/tools.ts +187 -0
  36. package/connectors/databricks/src/search-filter.ts +7 -7
  37. package/connectors/databricks/src/server.ts +2 -77
  38. package/connectors/databricks/src/tools.ts +66 -0
  39. package/connectors/datadog/src/server.ts +2 -48
  40. package/connectors/datadog/src/tools.ts +48 -0
  41. package/connectors/dataprofile/src/profile.ts +16 -46
  42. package/connectors/dbt/src/server.ts +2 -92
  43. package/connectors/dbt/src/tools.ts +80 -0
  44. package/connectors/dependencytrack/src/server.ts +2 -76
  45. package/connectors/dependencytrack/src/tools.ts +65 -0
  46. package/connectors/discord/src/server.ts +2 -85
  47. package/connectors/discord/src/tools.ts +82 -0
  48. package/connectors/elasticsearch/src/tools.ts +7 -37
  49. package/connectors/fastmail/src/jmap-client.ts +202 -0
  50. package/connectors/fastmail/src/jmap-core.ts +8 -21
  51. package/connectors/fastmail/src/server.ts +2 -187
  52. package/connectors/fastmail/src/tools.ts +4 -23
  53. package/connectors/figma/src/search-filter.ts +2 -11
  54. package/connectors/figma/src/server.ts +2 -113
  55. package/connectors/figma/src/tools.ts +99 -0
  56. package/connectors/firebase/src/server.ts +2 -90
  57. package/connectors/firebase/src/token.ts +2 -5
  58. package/connectors/firebase/src/tools.ts +106 -0
  59. package/connectors/flagsmith/src/server.ts +2 -98
  60. package/connectors/flagsmith/src/tools.ts +95 -0
  61. package/connectors/flux/src/server.ts +52 -53
  62. package/connectors/gcp/src/server.ts +2 -132
  63. package/connectors/gcp/src/tools.ts +123 -0
  64. package/connectors/github/src/server.ts +2 -328
  65. package/connectors/github/src/tools.ts +311 -0
  66. package/connectors/github-actions/src/server.ts +2 -205
  67. package/connectors/github-actions/src/tools.ts +209 -0
  68. package/connectors/gitlab/src/server.ts +2 -384
  69. package/connectors/gitlab/src/tools.ts +392 -0
  70. package/connectors/gmail/src/server.ts +2 -270
  71. package/connectors/gmail/src/tools.ts +209 -0
  72. package/connectors/google-drive/src/server.ts +2 -524
  73. package/connectors/google-drive/src/tools.ts +529 -0
  74. package/connectors/google-meet/src/server.ts +2 -82
  75. package/connectors/google-meet/src/tools.ts +87 -0
  76. package/connectors/google-photos/src/server.ts +2 -132
  77. package/connectors/google-photos/src/tools.ts +138 -0
  78. package/connectors/grafana/src/server.ts +2 -53
  79. package/connectors/grafana/src/tools.ts +53 -0
  80. package/connectors/great-expectations/src/gx-parse.ts +14 -45
  81. package/connectors/greenhouse/src/server.ts +2 -63
  82. package/connectors/greenhouse/src/tools.ts +68 -0
  83. package/connectors/hubspot/src/server.ts +2 -77
  84. package/connectors/hubspot/src/tools.ts +70 -0
  85. package/connectors/iac/src/server.ts +2 -137
  86. package/connectors/iac/src/tools.ts +153 -0
  87. package/connectors/imap/src/imap-core.ts +29 -82
  88. package/connectors/imap/src/server.ts +33 -220
  89. package/connectors/intercom/src/server.ts +2 -61
  90. package/connectors/intercom/src/tools.ts +58 -0
  91. package/connectors/jenkins/src/jenkins-api.ts +2 -6
  92. package/connectors/jenkins/src/server.ts +2 -215
  93. package/connectors/jenkins/src/tools.ts +240 -0
  94. package/connectors/jira/src/server.ts +2 -301
  95. package/connectors/jira/src/tools.ts +325 -0
  96. package/connectors/kubernetes/src/server.ts +2 -155
  97. package/connectors/kubernetes/src/tools.ts +160 -0
  98. package/connectors/launchdarkly/src/server.ts +2 -75
  99. package/connectors/launchdarkly/src/tools.ts +72 -0
  100. package/connectors/lever/src/search-filter.ts +4 -12
  101. package/connectors/lever/src/server.ts +2 -60
  102. package/connectors/lever/src/tools.ts +61 -0
  103. package/connectors/linear/src/server.ts +2 -459
  104. package/connectors/linear/src/tools.ts +482 -0
  105. package/connectors/localdb/src/sql-scan.ts +13 -41
  106. package/connectors/localdb/src/tools.ts +1 -1
  107. package/connectors/looker/src/search-filter.ts +2 -15
  108. package/connectors/looker/src/server.ts +35 -70
  109. package/connectors/mendeley/src/server.ts +2 -55
  110. package/connectors/mendeley/src/tools.ts +54 -0
  111. package/connectors/mercury/src/server.ts +2 -57
  112. package/connectors/mercury/src/tools.ts +50 -0
  113. package/connectors/metabase/src/search-filter.ts +4 -15
  114. package/connectors/metabase/src/server.ts +2 -79
  115. package/connectors/metabase/src/tools.ts +64 -0
  116. package/connectors/miro/src/server.ts +2 -72
  117. package/connectors/miro/src/tools.ts +65 -0
  118. package/connectors/mlflow/src/search-filter.ts +3 -7
  119. package/connectors/mlflow/src/server.ts +20 -40
  120. package/connectors/monte-carlo/src/server.ts +50 -57
  121. package/connectors/netlify/src/search-filter.ts +6 -14
  122. package/connectors/netlify/src/server.ts +2 -60
  123. package/connectors/netlify/src/tools.ts +53 -0
  124. package/connectors/newrelic/src/server.ts +2 -40
  125. package/connectors/newrelic/src/tools.ts +37 -0
  126. package/connectors/notion/src/server.ts +2 -335
  127. package/connectors/notion/src/tools.ts +344 -0
  128. package/connectors/obsidian/src/server.ts +2 -425
  129. package/connectors/obsidian/src/tools.ts +466 -0
  130. package/connectors/onedrive/src/server.ts +2 -249
  131. package/connectors/onedrive/src/tools.ts +253 -0
  132. package/connectors/outlook/src/server.ts +2 -351
  133. package/connectors/outlook/src/tools.ts +357 -0
  134. package/connectors/pagerduty/src/server.ts +2 -132
  135. package/connectors/pagerduty/src/tools.ts +126 -0
  136. package/connectors/pipedrive/src/server.ts +2 -58
  137. package/connectors/pipedrive/src/tools.ts +63 -0
  138. package/connectors/powerbi/src/search-filter.ts +2 -15
  139. package/connectors/powerbi/src/server.ts +43 -68
  140. package/connectors/prefect/src/search-filter.ts +3 -16
  141. package/connectors/prefect/src/server.ts +2 -90
  142. package/connectors/prefect/src/tools.ts +83 -0
  143. package/connectors/protonmail/src/mail-core.ts +16 -75
  144. package/connectors/protonmail/src/server.ts +34 -214
  145. package/connectors/raindrop/src/server.ts +2 -103
  146. package/connectors/raindrop/src/tools.ts +103 -0
  147. package/connectors/ramp/src/server.ts +2 -105
  148. package/connectors/ramp/src/tools.ts +98 -0
  149. package/connectors/readwise/src/server.ts +2 -88
  150. package/connectors/readwise/src/tools.ts +81 -0
  151. package/connectors/sagemaker/src/tools.ts +21 -40
  152. package/connectors/salesforce/src/search-filter.ts +4 -11
  153. package/connectors/salesforce/src/server.ts +2 -90
  154. package/connectors/salesforce/src/tools.ts +75 -0
  155. package/connectors/semgrep/src/search-filter.ts +3 -10
  156. package/connectors/semgrep/src/server.ts +2 -104
  157. package/connectors/semgrep/src/tools.ts +92 -0
  158. package/connectors/sentry/src/server.ts +2 -58
  159. package/connectors/sentry/src/tools.ts +53 -0
  160. package/connectors/slack/src/server.ts +2 -352
  161. package/connectors/slack/src/tools.ts +376 -0
  162. package/connectors/snowflake/src/search-filter.ts +4 -19
  163. package/connectors/snowflake/src/server.ts +6 -25
  164. package/connectors/snyk/src/server.ts +2 -140
  165. package/connectors/snyk/src/tools.ts +136 -0
  166. package/connectors/sonarqube/src/server.ts +2 -111
  167. package/connectors/sonarqube/src/tools.ts +98 -0
  168. package/connectors/stackoverflow/src/server.ts +2 -77
  169. package/connectors/stackoverflow/src/tools.ts +66 -0
  170. package/connectors/storybook/src/storybook-parse.ts +3 -6
  171. package/connectors/stripe/src/server.ts +2 -61
  172. package/connectors/stripe/src/tools.ts +54 -0
  173. package/connectors/superset/src/search-filter.ts +4 -15
  174. package/connectors/superset/src/server.ts +2 -115
  175. package/connectors/superset/src/tools.ts +98 -0
  176. package/connectors/tableau/src/search-filter.ts +2 -15
  177. package/connectors/tableau/src/server.ts +13 -65
  178. package/connectors/teams/src/server.ts +2 -249
  179. package/connectors/teams/src/tools.ts +283 -0
  180. package/connectors/testflight/src/server.ts +2 -70
  181. package/connectors/testflight/src/tools.ts +79 -0
  182. package/connectors/vercel/src/search-filter.ts +2 -10
  183. package/connectors/vercel/src/server.ts +2 -75
  184. package/connectors/vercel/src/tools.ts +73 -0
  185. package/connectors/vertex-ai/src/tools.ts +23 -33
  186. package/connectors/wiz/src/search-filter.ts +6 -30
  187. package/connectors/wiz/src/server.ts +2 -188
  188. package/connectors/wiz/src/tools.ts +168 -0
  189. package/connectors/zendesk/src/server.ts +2 -73
  190. package/connectors/zendesk/src/tools.ts +60 -0
  191. package/connectors/zoom/src/search-filter.ts +1 -7
  192. package/connectors/zoom/src/server.ts +2 -94
  193. package/connectors/zoom/src/tools.ts +93 -0
  194. package/connectors/zotero/src/server.ts +2 -78
  195. package/connectors/zotero/src/tools.ts +67 -0
  196. package/package.json +14 -12
  197. package/shared/access-token-cache.ts +123 -0
  198. package/shared/atlassian-json-fetch.ts +6 -16
  199. package/shared/audit-chain.ts +601 -34
  200. package/shared/cli-json-kit.ts +191 -0
  201. package/shared/collection-tool-kit.ts +143 -0
  202. package/shared/connector-mode.ts +3 -2
  203. package/shared/consent-kit.ts +81 -26
  204. package/shared/cursor-list-tool.ts +29 -0
  205. package/shared/env-json-api.ts +209 -0
  206. package/shared/fetch-json-text.ts +27 -0
  207. package/shared/github-rest.ts +62 -0
  208. package/shared/graphql-json.ts +60 -0
  209. package/shared/imap-mail-core.ts +91 -0
  210. package/shared/imap-tool-kit.ts +57 -23
  211. package/shared/imapflow-adapter.ts +417 -0
  212. package/shared/nimbus-spawn.ts +84 -27
  213. package/shared/rest-tool-kit.ts +77 -7
  214. package/shared/run-cli-json.ts +38 -16
  215. package/shared/safe-cli-arg.ts +139 -17
  216. package/shared/search-filter.ts +58 -18
  217. package/shared/walk-files.ts +118 -0
  218. package/shared/windows-batch-args.ts +273 -0
  219. package/shared/write-budget.ts +100 -0
  220. package/standalone/src/launcher.ts +19 -7
@@ -0,0 +1,209 @@
1
+ /**
2
+ * env-json-api — the read-only JSON GET client that 31 connectors had each
3
+ * written out by hand.
4
+ *
5
+ * The shape below appeared, byte for byte, in every one of them:
6
+ *
7
+ * ```ts
8
+ * function apiToken(): string {
9
+ * const t = process.env["X_TOKEN"]?.trim();
10
+ * if (t === undefined || t === "") throw new Error("X_TOKEN is not set");
11
+ * return t;
12
+ * }
13
+ * function authHeader(): Record<string, string> {
14
+ * return { Authorization: `Bearer ${apiToken()}`, Accept: "application/json" };
15
+ * }
16
+ * async function xGet(path: string): Promise<unknown> {
17
+ * const res = await fetch(`${BASE}${path}`, { headers: authHeader() });
18
+ * const text = await res.text();
19
+ * if (!res.ok) throw new Error(`X ${String(res.status)}: ${text.slice(0, 400)}`);
20
+ * return JSON.parse(text) as unknown;
21
+ * }
22
+ * ```
23
+ *
24
+ * Only four things ever varied: the base URL, the service label in the error,
25
+ * the auth header, and (never, in practice) the 400-character snippet cap. So
26
+ * those are the inputs and the rest is here once.
27
+ *
28
+ * This is deliberately NOT `rest-tool-kit`'s `makeRestFetcher`. That one returns
29
+ * `{ ok, status, json, text }` for callers that branch on status; these 31
30
+ * connectors all throw on a non-2xx and return parsed JSON, and rewriting their
31
+ * tool bodies to the other contract would have been a behaviour change rather
32
+ * than a deduplication. Both shapes are legitimate; this module owns the
33
+ * throwing one.
34
+ *
35
+ * Environment is read at CALL time, not at module load. That is not incidental:
36
+ * the connectors behaved that way (their `apiToken()` ran per request), tests
37
+ * depend on being able to set the variable after import, and a connector must
38
+ * fail with "X_TOKEN is not set" on the tool call rather than at startup.
39
+ */
40
+
41
+ import { stripTrailingSlashes } from "./strip-trailing-slashes.ts";
42
+
43
+ /** Body-snippet length in the thrown error. The value every connector used. */
44
+ export const DEFAULT_SNIPPET_MAX = 400;
45
+
46
+ /**
47
+ * `process.env[name]`, trimmed. Throws `"<name> is not set"` when the variable
48
+ * is absent, empty, or whitespace only.
49
+ *
50
+ * Distinct from `requireProcessEnv` in `mcp-tool-kit`, which does not trim: a
51
+ * variable set to a single space passes that check and then produces an
52
+ * `Authorization: Bearer ` header and a puzzling 401. Every one of the 31
53
+ * connectors trimmed, so trimming is the behaviour being preserved here.
54
+ */
55
+ export function requiredEnv(name: string): string {
56
+ const raw = process.env[name]?.trim();
57
+ if (raw === undefined || raw === "") {
58
+ throw new Error(`${name} is not set`);
59
+ }
60
+ return raw;
61
+ }
62
+
63
+ /** `process.env[name]`, trimmed, or `fallback` when absent or empty. */
64
+ export function optionalEnv(name: string, fallback: string): string {
65
+ const raw = process.env[name]?.trim();
66
+ return raw === undefined || raw === "" ? fallback : raw;
67
+ }
68
+
69
+ /**
70
+ * A required base URL from the environment, with trailing slashes stripped so
71
+ * `${base}${path}` cannot produce a double slash.
72
+ */
73
+ export function requiredBaseUrl(name: string): string {
74
+ return stripTrailingSlashes(requiredEnv(name));
75
+ }
76
+
77
+ /** An optional base URL from the environment, trailing slashes stripped. */
78
+ export function optionalBaseUrl(name: string, fallback: string): string {
79
+ return stripTrailingSlashes(optionalEnv(name, fallback));
80
+ }
81
+
82
+ /** Builds the request headers. Called per request, so it reads env at call time. */
83
+ export type HeaderFactory = () => Record<string, string>;
84
+
85
+ /**
86
+ * `{ [header]: "<scheme> <token>", Accept: "application/json", ...extra }` where
87
+ * `token` is the trimmed value of `env`.
88
+ *
89
+ * `scheme` is the RFC 7235 auth scheme — `"Bearer"` for most, but the tree also
90
+ * contains `"Token"` (Readwise, dbt, Flagsmith) and lowercase `"token"` (Snyk),
91
+ * which are what those APIs actually accept and so are not normalised. Pass
92
+ * `scheme: ""` for the APIs that want the bare credential with no scheme
93
+ * (Bitrise, LaunchDarkly), and `header` for the ones that use their own header
94
+ * instead of `Authorization` (Codemagic, Dependency-Track, Metabase, Zotero).
95
+ */
96
+ export function envAuthHeaders(cfg: {
97
+ readonly env: string;
98
+ readonly scheme?: string;
99
+ readonly header?: string;
100
+ readonly extra?: Record<string, string>;
101
+ }): HeaderFactory {
102
+ const header = cfg.header ?? "Authorization";
103
+ const scheme = cfg.scheme ?? "Bearer";
104
+ return (): Record<string, string> => {
105
+ const token = requiredEnv(cfg.env);
106
+ return {
107
+ [header]: scheme === "" ? token : `${scheme} ${token}`,
108
+ Accept: "application/json",
109
+ ...cfg.extra,
110
+ };
111
+ };
112
+ }
113
+
114
+ /**
115
+ * The fetch the getter and the poster use. Defaults to the global `fetch`.
116
+ *
117
+ * A seam because the connectors that reach a self-hosted control plane (argocd,
118
+ * flux, mlflow) use `fetchWithTimeout` instead: a server that stops answering
119
+ * should fail the tool call, not hang it. A timeout was the whole reason they
120
+ * had carried hand-written copies of this getter.
121
+ */
122
+ export type JsonFetch = (url: string, init: RequestInit) => Promise<Response>;
123
+
124
+ export interface JsonApiConfig {
125
+ /**
126
+ * Base URL the path is appended to. A function when the base itself comes
127
+ * from the environment, so it is resolved per request rather than at import.
128
+ */
129
+ readonly base: string | (() => string);
130
+ /** Service name that prefixes a failure, e.g. `"Stripe 404: ..."`. */
131
+ readonly label: string;
132
+ readonly headers: HeaderFactory;
133
+ /** Body-snippet length in the error. Defaults to {@link DEFAULT_SNIPPET_MAX}. */
134
+ readonly snippetMax?: number;
135
+ /** Overrides the global `fetch` — see {@link JsonFetch}. */
136
+ readonly fetch?: JsonFetch;
137
+ }
138
+
139
+ /**
140
+ * One request against `config`, shared by the getter and the poster: resolve the
141
+ * base, send what `init` builds from the request headers, and return the body
142
+ * text — throwing `"<errorPrefix> <status>: <body snippet>"` on any non-2xx.
143
+ *
144
+ * The base is resolved BEFORE the headers are built, so a connector missing both
145
+ * its URL and its credential names the URL first, as every hand-written copy did.
146
+ */
147
+ function sendFor(
148
+ config: JsonApiConfig,
149
+ ): (
150
+ path: string,
151
+ init: (headers: Record<string, string>) => RequestInit,
152
+ errorPrefix: string,
153
+ ) => Promise<string> {
154
+ const { base, headers } = config;
155
+ const snippetMax = config.snippetMax ?? DEFAULT_SNIPPET_MAX;
156
+ const doFetch = config.fetch ?? ((url, init) => fetch(url, init));
157
+ return async (path, init, errorPrefix) => {
158
+ const root = typeof base === "string" ? base : base();
159
+ const res = await doFetch(`${root}${path}`, init(headers()));
160
+ const text = await res.text();
161
+ if (!res.ok) {
162
+ throw new Error(`${errorPrefix} ${String(res.status)}: ${text.slice(0, snippetMax)}`);
163
+ }
164
+ return text;
165
+ };
166
+ }
167
+
168
+ /**
169
+ * A `(path) => Promise<unknown>` GET client for `config`. Resolves to the parsed
170
+ * JSON body, and throws `"<label> <status>: <body snippet>"` on any non-2xx.
171
+ *
172
+ * `path` is appended to the base as given — callers build it, and must
173
+ * `encodeURIComponent` any user-supplied segment, exactly as before.
174
+ */
175
+ export function createJsonGetter(config: JsonApiConfig): (path: string) => Promise<unknown> {
176
+ const { label } = config;
177
+ const send = sendFor(config);
178
+ return async (path: string): Promise<unknown> =>
179
+ JSON.parse(await send(path, (headers) => ({ headers }), label)) as unknown;
180
+ }
181
+
182
+ /**
183
+ * The mutating twin of {@link createJsonGetter}: a `(path, body) => Promise<unknown>`
184
+ * POST client that sends `body` as JSON (adding `Content-Type: application/json` to
185
+ * the request headers) and resolves to the parsed response — `{}` when the response
186
+ * body is empty, as a mutation's often is.
187
+ *
188
+ * Throws `"<label> <path> <status>: <body snippet>"` on any non-2xx: unlike the
189
+ * getter's, the message names the path, so a failed mutation says which endpoint
190
+ * refused it. The argocd and mlflow write tools each carried a copy of this.
191
+ */
192
+ export function createJsonPoster(
193
+ config: JsonApiConfig,
194
+ ): (path: string, body: unknown) => Promise<unknown> {
195
+ const { label } = config;
196
+ const send = sendFor(config);
197
+ return async (path: string, body: unknown): Promise<unknown> => {
198
+ const text = await send(
199
+ path,
200
+ (headers) => ({
201
+ method: "POST",
202
+ headers: { ...headers, "Content-Type": "application/json" },
203
+ body: JSON.stringify(body),
204
+ }),
205
+ `${label} ${path}`,
206
+ );
207
+ return text === "" ? {} : (JSON.parse(text) as unknown);
208
+ };
209
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * fetch-json-text — send a JSON API request and hand back the raw response text.
3
+ *
4
+ * `atlassian-json-fetch.ts` (jira, confluence) and notion built this request the same way around
5
+ * different credentials: their own headers first, `Content-Type: application/json` only when the
6
+ * request has a body, and any header the caller passes in `init` overriding all of them. A non-2xx
7
+ * is reported rather than thrown, because these APIs put the useful diagnostic in the error body.
8
+ */
9
+ export async function fetchJsonText(
10
+ url: string,
11
+ baseHeaders: Readonly<Record<string, string>>,
12
+ init?: RequestInit,
13
+ ): Promise<{ ok: boolean; status: number; text: string }> {
14
+ const headers: Record<string, string> = { ...baseHeaders };
15
+ if (init?.body !== undefined) {
16
+ headers["Content-Type"] = "application/json";
17
+ }
18
+ const res = await fetch(url, {
19
+ ...init,
20
+ headers: {
21
+ ...headers,
22
+ ...(init?.headers as Record<string, string> | undefined),
23
+ },
24
+ });
25
+ const text = await res.text();
26
+ return { ok: res.ok, status: res.status, text };
27
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * github-rest — the GitHub REST client the `github` and `github-actions`
3
+ * connectors share.
4
+ *
5
+ * Both talk to the same API with the same token and had each written out its
6
+ * base URL, its default headers, the fetch built from them and the
7
+ * `/repos/<owner>/<repo>` path. The pinned `X-GitHub-Api-Version` lived in three
8
+ * places, so a bump had to find all three.
9
+ */
10
+
11
+ import { makeRestFetcher, type RestFetchResult } from "./rest-tool-kit.ts";
12
+
13
+ export const GH_API = "https://api.github.com";
14
+
15
+ /** The media type GitHub documents for its REST API. */
16
+ export const GH_ACCEPT = "application/vnd.github+json";
17
+
18
+ /** The REST API version both connectors are written against. */
19
+ export const GH_API_VERSION = "2022-11-28";
20
+
21
+ /** Sent with every request, beside the Bearer token. */
22
+ export const GH_HEADERS: Record<string, string> = {
23
+ Accept: GH_ACCEPT,
24
+ "X-GitHub-Api-Version": GH_API_VERSION,
25
+ };
26
+
27
+ /** A Bearer-authenticated JSON request against {@link GH_API} (a relative path or a same-origin URL). */
28
+ export function ghFetch(token: string, path: string, init?: RequestInit): Promise<RestFetchResult> {
29
+ return makeRestFetcher({ apiBase: GH_API, token, defaultHeaders: GH_HEADERS })(path, init);
30
+ }
31
+
32
+ /** `/repos/<owner>/<repo>`, both segments URI-encoded. */
33
+ export function ghRepoPath(owner: string, repo: string): string {
34
+ return `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
35
+ }
36
+
37
+ /** The paging arguments every GitHub list tool accepts. */
38
+ export interface GhPaging {
39
+ readonly perPage?: number | undefined;
40
+ readonly page?: number | undefined;
41
+ }
42
+
43
+ /**
44
+ * GitHub's paging parameters, in the order every list tool sends them: `per_page` (30 when not
45
+ * given — GitHub's own default), then `page` only when one was given.
46
+ */
47
+ export function setGhPaging(params: URLSearchParams, paging: GhPaging): void {
48
+ params.set("per_page", String(paging.perPage ?? 30));
49
+ if (paging.page !== undefined) {
50
+ params.set("page", String(paging.page));
51
+ }
52
+ }
53
+
54
+ /**
55
+ * `path` plus the query string `setQuery` builds, as the relative path {@link ghFetch} takes. The
56
+ * parameters keep the order `setQuery` sets them in.
57
+ */
58
+ export function ghQueryPath(path: string, setQuery: (params: URLSearchParams) => void): string {
59
+ const u = new URL(`${GH_API}${path}`);
60
+ setQuery(u.searchParams);
61
+ return `${u.pathname}${u.search}`;
62
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * graphql-json — one GraphQL-over-HTTP request, for the connectors whose API is a
3
+ * GraphQL endpoint read as plain JSON (dagster, wiz).
4
+ *
5
+ * Both had written out the same request and the same reading of the response
6
+ * envelope: a present `errors` member fails the call even when `data` came back
7
+ * too, and an absent `data` member fails it as well. That reading is a rule about
8
+ * what a partial GraphQL answer means, and it belongs in one place.
9
+ *
10
+ * Linear and Monte Carlo read the envelope differently (an `errors` ARRAY that
11
+ * must be non-empty, messages joined, a result union) and keep their own clients.
12
+ */
13
+
14
+ /** Body-snippet length in the thrown errors. The value both connectors used. */
15
+ const SNIPPET_MAX = 400;
16
+
17
+ export interface GraphqlRequest {
18
+ /** The GraphQL endpoint. */
19
+ readonly url: string;
20
+ /** Service name that prefixes every failure, e.g. `"Wiz 401: ..."`. */
21
+ readonly label: string;
22
+ /** Auth headers; `Content-Type` and `Accept` (both `application/json`) are added after them. */
23
+ readonly headers: Readonly<Record<string, string>>;
24
+ readonly query: string;
25
+ /** Omitted from the request body entirely when undefined. */
26
+ readonly variables?: Record<string, unknown>;
27
+ }
28
+
29
+ /**
30
+ * POST `query` (and `variables`) to `url` and resolve to the response's `data`.
31
+ *
32
+ * Throws `"<label> <status>: <body snippet>"` on a non-2xx,
33
+ * `"<label> GraphQL error: <errors snippet>"` when the response carries `errors`,
34
+ * and `"<label> GraphQL: response missing \`data\` field"` when it carries no `data`.
35
+ */
36
+ export async function postGraphql<T>(request: GraphqlRequest): Promise<T> {
37
+ const res = await fetch(request.url, {
38
+ method: "POST",
39
+ headers: {
40
+ ...request.headers,
41
+ "Content-Type": "application/json",
42
+ Accept: "application/json",
43
+ },
44
+ body: JSON.stringify({ query: request.query, variables: request.variables }),
45
+ });
46
+ const text = await res.text();
47
+ if (!res.ok) {
48
+ throw new Error(`${request.label} ${String(res.status)}: ${text.slice(0, SNIPPET_MAX)}`);
49
+ }
50
+ const parsed = JSON.parse(text) as { data?: T; errors?: unknown };
51
+ if (parsed.errors !== undefined) {
52
+ throw new Error(
53
+ `${request.label} GraphQL error: ${JSON.stringify(parsed.errors).slice(0, SNIPPET_MAX)}`,
54
+ );
55
+ }
56
+ if (parsed.data === undefined) {
57
+ throw new Error(`${request.label} GraphQL: response missing \`data\` field`);
58
+ }
59
+ return parsed.data;
60
+ }
@@ -55,3 +55,94 @@ export function formatAddress(a: MailAddress): string {
55
55
  }
56
56
  return addr;
57
57
  }
58
+
59
+ /**
60
+ * Message ENVELOPE — RFC 2822 header fields only.
61
+ *
62
+ * These five declarations were written out identically in `imap/src/imap-core.ts`
63
+ * and `protonmail/src/mail-core.ts`, differing only in the `Imap`/`Mail` name
64
+ * prefix. Both connectors index the same thing through the same protocol, so
65
+ * there was never a second shape here — only a second copy. Each core now
66
+ * aliases these, keeping its own names for its own callers.
67
+ */
68
+ export interface MailEnvelope {
69
+ readonly date?: Date | string | null;
70
+ readonly subject?: string | null;
71
+ readonly messageId?: string | null;
72
+ readonly from?: readonly MailAddress[];
73
+ readonly to?: readonly MailAddress[];
74
+ readonly cc?: readonly MailAddress[];
75
+ }
76
+
77
+ /**
78
+ * Attachment METADATA only — filename, size, mimetype. NEVER the bytes.
79
+ * Derived from BODYSTRUCTURE.
80
+ */
81
+ export interface MailAttachmentMeta {
82
+ readonly filename: string | null;
83
+ readonly sizeBytes: number | null;
84
+ readonly mimeType: string | null;
85
+ }
86
+
87
+ /**
88
+ * A single mailbox message reduced to header + attachment-metadata + a capped
89
+ * text preview. This is the ONLY shape these connectors ever materialize; it
90
+ * has no field that could carry attachment content or a full body.
91
+ */
92
+ export interface MailMessageMeta {
93
+ readonly uid: number;
94
+ readonly mailbox: string;
95
+ readonly uidValidity: string | null;
96
+ readonly envelope: MailEnvelope;
97
+ readonly attachments: readonly MailAttachmentMeta[];
98
+ /** Plain-text body preview, already capped to {@link PREVIEW_MAX_CHARS}. */
99
+ readonly preview: string;
100
+ }
101
+
102
+ /** Listing options. */
103
+ export interface MailListOptions {
104
+ readonly mailbox?: string;
105
+ readonly limit?: number;
106
+ }
107
+
108
+ /** Search options — searches header/envelope fields, never body-content scans. */
109
+ export interface MailSearchOptions {
110
+ readonly query: string;
111
+ readonly mailbox?: string;
112
+ readonly limit?: number;
113
+ }
114
+
115
+ /**
116
+ * Minimal IMAP client surface these connectors depend on. Implemented for real
117
+ * by `shared/imapflow-adapter.ts` and by a fake in tests. Deliberately exposes
118
+ * ONLY header/attachment-metadata + capped-preview reads.
119
+ */
120
+ export interface MailClient {
121
+ /** List the most recent messages (header + attachment metadata + preview). */
122
+ list(options: MailListOptions): Promise<MailMessageMeta[]>;
123
+ /** Fetch one message's header + attachment metadata + preview by uid. */
124
+ get(uid: number, mailbox?: string): Promise<MailMessageMeta | null>;
125
+ /** Search by subject/from/to substring; returns matching message metas. */
126
+ search(options: MailSearchOptions): Promise<MailMessageMeta[]>;
127
+ }
128
+
129
+ /** Outgoing message for the SMTP send tool. */
130
+ export interface SendMailInput {
131
+ readonly to: string;
132
+ readonly subject: string;
133
+ readonly body: string;
134
+ readonly cc?: string;
135
+ readonly bcc?: string;
136
+ }
137
+
138
+ /** Result of a send (message id assigned by the SMTP server, if any). */
139
+ export interface SendMailResult {
140
+ readonly messageId: string | null;
141
+ readonly accepted: readonly string[];
142
+ readonly rejected: readonly string[];
143
+ }
144
+
145
+ /** Minimal SMTP mailer surface; implemented for real over nodemailer. */
146
+ export interface SmtpMailer {
147
+ send(input: SendMailInput): Promise<SendMailResult>;
148
+ }
@@ -1,4 +1,4 @@
1
- import type { WriteToolRegistrar } from "./consent-kit.ts";
1
+ import type { WriteToolConfig, WriteToolRegistrar } from "./consent-kit.ts";
2
2
  /**
3
3
  * Shared tool-layer helpers for the IMAP/JMAP email connectors (imap, protonmail).
4
4
  * Extracted to eliminate byte-identical duplication in tools.ts across those two
@@ -10,6 +10,10 @@ import type { WriteToolRegistrar } from "./consent-kit.ts";
10
10
  * connector-specific envelope/meta mappers are deliberately NOT extracted here
11
11
  * (they implement different local interfaces and are out of scope per the dedup
12
12
  * brief).
13
+ *
14
+ * The mail-SEND pieces — `sendArgs`, {@link outgoingMail} and {@link mailSendConsent} —
15
+ * serve every email connector that sends, whatever its transport: imap, protonmail and
16
+ * apple through the factory below, and fastmail (JMAP), gmail and outlook directly.
13
17
  */
14
18
 
15
19
  import { z } from "zod";
@@ -54,6 +58,55 @@ export const emailToolSchemas = {
54
58
  sendArgs,
55
59
  } as const;
56
60
 
61
+ // ---------------------------------------------------------------------------
62
+ // Shared mail-send helpers (every connector that sends or drafts a new message)
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /** What a send or draft tool hands its transport. */
66
+ export interface OutgoingMail {
67
+ readonly to: string;
68
+ readonly subject: string;
69
+ readonly body: string;
70
+ readonly cc?: string;
71
+ readonly bcc?: string;
72
+ }
73
+
74
+ /**
75
+ * The outgoing message for validated `sendArgs`-shaped input: an empty `cc` or `bcc` is
76
+ * left out rather than handed on as an empty header.
77
+ */
78
+ export function outgoingMail(args: {
79
+ readonly to: string;
80
+ readonly subject: string;
81
+ readonly body: string;
82
+ readonly cc?: string | undefined;
83
+ readonly bcc?: string | undefined;
84
+ }): OutgoingMail {
85
+ return {
86
+ to: args.to,
87
+ subject: args.subject,
88
+ body: args.body,
89
+ ...(args.cc !== undefined && args.cc !== "" ? { cc: args.cc } : {}),
90
+ ...(args.bcc !== undefined && args.bcc !== "" ? { bcc: args.bcc } : {}),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * The consent declaration every mail-send tool shares, given its action type (`mutates`).
96
+ * A sent mail cannot be recalled and nothing remains to query, so the recipient and
97
+ * subject ARE the pre-state; the recipient is what the write scope is checked against.
98
+ */
99
+ export function mailSendConsent<T extends { readonly to: string; readonly subject: string }>(
100
+ mutates: string,
101
+ ): WriteToolConfig<T> {
102
+ return {
103
+ mutates,
104
+ recoverable: false,
105
+ capturePreState: (p) => Promise.resolve({ to: p.to, subject: p.subject }),
106
+ scopeTargetOf: (p) => ({ kind: "recipient", value: p.to }),
107
+ };
108
+ }
109
+
57
110
  // ---------------------------------------------------------------------------
58
111
  // Shared view transformer (imap + protonmail tools.ts — viewMessage)
59
112
  // ---------------------------------------------------------------------------
@@ -259,30 +312,11 @@ export function registerEmailConnectorTools(opts: {
259
312
 
260
313
  registerWriteTool(
261
314
  `${toolPrefix}_mail_send`,
262
- {
263
- mutates: `${toolPrefix}.mail.send`,
264
- // A sent mail cannot be recalled and nothing remains to query, so the recipient and
265
- // subject ARE the pre-state.
266
- recoverable: false,
267
- capturePreState: (p) => Promise.resolve({ to: p.to, subject: p.subject }),
268
- scopeTargetOf: (p) => ({ kind: "recipient", value: p.to }),
269
- },
315
+ mailSendConsent(`${toolPrefix}.mail.send`),
270
316
  descriptions.send,
271
317
  sendArgs,
272
- async (parsedData): Promise<McpListResult> => {
273
- const parsed = { success: true as const, data: parsedData };
274
- const input: { to: string; subject: string; body: string; cc?: string; bcc?: string } = {
275
- to: parsed.data.to,
276
- subject: parsed.data.subject,
277
- body: parsed.data.body,
278
- };
279
- if (parsed.data.cc !== undefined && parsed.data.cc !== "") {
280
- input.cc = parsed.data.cc;
281
- }
282
- if (parsed.data.bcc !== undefined && parsed.data.bcc !== "") {
283
- input.bcc = parsed.data.bcc;
284
- }
285
- const res = await mailer.send(input);
318
+ async (args): Promise<McpListResult> => {
319
+ const res = await mailer.send(outgoingMail(args));
286
320
  return mcpJsonResult({
287
321
  messageId: res.messageId,
288
322
  accepted: res.accepted,