@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.
- package/README.md +19 -7
- package/connectors/airflow/src/search-filter.ts +2 -15
- package/connectors/airflow/src/server.ts +2 -87
- package/connectors/airflow/src/tools.ts +74 -0
- package/connectors/apple/src/adapters.ts +326 -0
- package/connectors/apple/src/apple-mail-core.ts +4 -8
- package/connectors/apple/src/calendar-tools.ts +28 -8
- package/connectors/apple/src/server.ts +23 -512
- package/connectors/apple/src/tools.ts +18 -44
- package/connectors/argocd/src/server.ts +25 -38
- package/connectors/athena/src/tools.ts +26 -43
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +199 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +134 -0
- package/connectors/bigeye/src/search-filter.ts +4 -15
- package/connectors/bigeye/src/server.ts +38 -38
- package/connectors/bigquery/src/tools.ts +1 -26
- package/connectors/bitbucket/src/server.ts +2 -244
- package/connectors/bitbucket/src/tools.ts +241 -0
- package/connectors/bitrise/src/server.ts +2 -79
- package/connectors/bitrise/src/tools.ts +76 -0
- package/connectors/canva/src/search-filter.ts +2 -11
- package/connectors/canva/src/server.ts +2 -73
- package/connectors/canva/src/tools.ts +66 -0
- package/connectors/circleci/src/server.ts +2 -176
- package/connectors/circleci/src/tools.ts +193 -0
- package/connectors/cloud-logging/src/tools.ts +19 -35
- package/connectors/cloudwatch/src/tools.ts +20 -38
- package/connectors/codemagic/src/server.ts +2 -70
- package/connectors/codemagic/src/tools.ts +71 -0
- package/connectors/confluence/src/server.ts +2 -301
- package/connectors/confluence/src/tools.ts +320 -0
- package/connectors/dagster/src/server.ts +2 -214
- package/connectors/dagster/src/tools.ts +187 -0
- package/connectors/databricks/src/search-filter.ts +7 -7
- package/connectors/databricks/src/server.ts +2 -77
- package/connectors/databricks/src/tools.ts +66 -0
- package/connectors/datadog/src/server.ts +2 -48
- package/connectors/datadog/src/tools.ts +48 -0
- package/connectors/dataprofile/src/profile.ts +16 -46
- package/connectors/dbt/src/server.ts +2 -92
- package/connectors/dbt/src/tools.ts +80 -0
- package/connectors/dependencytrack/src/server.ts +2 -76
- package/connectors/dependencytrack/src/tools.ts +65 -0
- package/connectors/discord/src/server.ts +2 -85
- package/connectors/discord/src/tools.ts +82 -0
- package/connectors/elasticsearch/src/tools.ts +7 -37
- package/connectors/fastmail/src/jmap-client.ts +202 -0
- package/connectors/fastmail/src/jmap-core.ts +8 -21
- package/connectors/fastmail/src/server.ts +2 -187
- package/connectors/fastmail/src/tools.ts +4 -23
- package/connectors/figma/src/search-filter.ts +2 -11
- package/connectors/figma/src/server.ts +2 -113
- package/connectors/figma/src/tools.ts +99 -0
- package/connectors/firebase/src/server.ts +2 -90
- package/connectors/firebase/src/token.ts +2 -5
- package/connectors/firebase/src/tools.ts +106 -0
- package/connectors/flagsmith/src/server.ts +2 -98
- package/connectors/flagsmith/src/tools.ts +95 -0
- package/connectors/flux/src/server.ts +52 -53
- package/connectors/gcp/src/server.ts +2 -132
- package/connectors/gcp/src/tools.ts +123 -0
- package/connectors/github/src/server.ts +2 -328
- package/connectors/github/src/tools.ts +311 -0
- package/connectors/github-actions/src/server.ts +2 -205
- package/connectors/github-actions/src/tools.ts +209 -0
- package/connectors/gitlab/src/server.ts +2 -384
- package/connectors/gitlab/src/tools.ts +392 -0
- package/connectors/gmail/src/server.ts +2 -270
- package/connectors/gmail/src/tools.ts +209 -0
- package/connectors/google-drive/src/server.ts +2 -524
- package/connectors/google-drive/src/tools.ts +529 -0
- package/connectors/google-meet/src/server.ts +2 -82
- package/connectors/google-meet/src/tools.ts +87 -0
- package/connectors/google-photos/src/server.ts +2 -132
- package/connectors/google-photos/src/tools.ts +138 -0
- package/connectors/grafana/src/server.ts +2 -53
- package/connectors/grafana/src/tools.ts +53 -0
- package/connectors/great-expectations/src/gx-parse.ts +14 -45
- package/connectors/greenhouse/src/server.ts +2 -63
- package/connectors/greenhouse/src/tools.ts +68 -0
- package/connectors/hubspot/src/server.ts +2 -77
- package/connectors/hubspot/src/tools.ts +70 -0
- package/connectors/iac/src/server.ts +2 -137
- package/connectors/iac/src/tools.ts +153 -0
- package/connectors/imap/src/imap-core.ts +29 -82
- package/connectors/imap/src/server.ts +33 -220
- package/connectors/intercom/src/server.ts +2 -61
- package/connectors/intercom/src/tools.ts +58 -0
- package/connectors/jenkins/src/jenkins-api.ts +2 -6
- package/connectors/jenkins/src/server.ts +2 -215
- package/connectors/jenkins/src/tools.ts +240 -0
- package/connectors/jira/src/server.ts +2 -301
- package/connectors/jira/src/tools.ts +325 -0
- package/connectors/kubernetes/src/server.ts +2 -155
- package/connectors/kubernetes/src/tools.ts +160 -0
- package/connectors/launchdarkly/src/server.ts +2 -75
- package/connectors/launchdarkly/src/tools.ts +72 -0
- package/connectors/lever/src/search-filter.ts +4 -12
- package/connectors/lever/src/server.ts +2 -60
- package/connectors/lever/src/tools.ts +61 -0
- package/connectors/linear/src/server.ts +2 -459
- package/connectors/linear/src/tools.ts +482 -0
- package/connectors/localdb/src/sql-scan.ts +13 -41
- package/connectors/localdb/src/tools.ts +1 -1
- package/connectors/looker/src/search-filter.ts +2 -15
- package/connectors/looker/src/server.ts +35 -70
- package/connectors/mendeley/src/server.ts +2 -55
- package/connectors/mendeley/src/tools.ts +54 -0
- package/connectors/mercury/src/server.ts +2 -57
- package/connectors/mercury/src/tools.ts +50 -0
- package/connectors/metabase/src/search-filter.ts +4 -15
- package/connectors/metabase/src/server.ts +2 -79
- package/connectors/metabase/src/tools.ts +64 -0
- package/connectors/miro/src/server.ts +2 -72
- package/connectors/miro/src/tools.ts +65 -0
- package/connectors/mlflow/src/search-filter.ts +3 -7
- package/connectors/mlflow/src/server.ts +20 -40
- package/connectors/monte-carlo/src/server.ts +50 -57
- package/connectors/netlify/src/search-filter.ts +6 -14
- package/connectors/netlify/src/server.ts +2 -60
- package/connectors/netlify/src/tools.ts +53 -0
- package/connectors/newrelic/src/server.ts +2 -40
- package/connectors/newrelic/src/tools.ts +37 -0
- package/connectors/notion/src/server.ts +2 -335
- package/connectors/notion/src/tools.ts +344 -0
- package/connectors/obsidian/src/server.ts +2 -425
- package/connectors/obsidian/src/tools.ts +466 -0
- package/connectors/onedrive/src/server.ts +2 -249
- package/connectors/onedrive/src/tools.ts +253 -0
- package/connectors/outlook/src/server.ts +2 -351
- package/connectors/outlook/src/tools.ts +357 -0
- package/connectors/pagerduty/src/server.ts +2 -132
- package/connectors/pagerduty/src/tools.ts +126 -0
- package/connectors/pipedrive/src/server.ts +2 -58
- package/connectors/pipedrive/src/tools.ts +63 -0
- package/connectors/powerbi/src/search-filter.ts +2 -15
- package/connectors/powerbi/src/server.ts +43 -68
- package/connectors/prefect/src/search-filter.ts +3 -16
- package/connectors/prefect/src/server.ts +2 -90
- package/connectors/prefect/src/tools.ts +83 -0
- package/connectors/protonmail/src/mail-core.ts +16 -75
- package/connectors/protonmail/src/server.ts +34 -214
- package/connectors/raindrop/src/server.ts +2 -103
- package/connectors/raindrop/src/tools.ts +103 -0
- package/connectors/ramp/src/server.ts +2 -105
- package/connectors/ramp/src/tools.ts +98 -0
- package/connectors/readwise/src/server.ts +2 -88
- package/connectors/readwise/src/tools.ts +81 -0
- package/connectors/sagemaker/src/tools.ts +21 -40
- package/connectors/salesforce/src/search-filter.ts +4 -11
- package/connectors/salesforce/src/server.ts +2 -90
- package/connectors/salesforce/src/tools.ts +75 -0
- package/connectors/semgrep/src/search-filter.ts +3 -10
- package/connectors/semgrep/src/server.ts +2 -104
- package/connectors/semgrep/src/tools.ts +92 -0
- package/connectors/sentry/src/server.ts +2 -58
- package/connectors/sentry/src/tools.ts +53 -0
- package/connectors/slack/src/server.ts +2 -352
- package/connectors/slack/src/tools.ts +376 -0
- package/connectors/snowflake/src/search-filter.ts +4 -19
- package/connectors/snowflake/src/server.ts +6 -25
- package/connectors/snyk/src/server.ts +2 -140
- package/connectors/snyk/src/tools.ts +136 -0
- package/connectors/sonarqube/src/server.ts +2 -111
- package/connectors/sonarqube/src/tools.ts +98 -0
- package/connectors/stackoverflow/src/server.ts +2 -77
- package/connectors/stackoverflow/src/tools.ts +66 -0
- package/connectors/storybook/src/storybook-parse.ts +3 -6
- package/connectors/stripe/src/server.ts +2 -61
- package/connectors/stripe/src/tools.ts +54 -0
- package/connectors/superset/src/search-filter.ts +4 -15
- package/connectors/superset/src/server.ts +2 -115
- package/connectors/superset/src/tools.ts +98 -0
- package/connectors/tableau/src/search-filter.ts +2 -15
- package/connectors/tableau/src/server.ts +13 -65
- package/connectors/teams/src/server.ts +2 -249
- package/connectors/teams/src/tools.ts +283 -0
- package/connectors/testflight/src/server.ts +2 -70
- package/connectors/testflight/src/tools.ts +79 -0
- package/connectors/vercel/src/search-filter.ts +2 -10
- package/connectors/vercel/src/server.ts +2 -75
- package/connectors/vercel/src/tools.ts +73 -0
- package/connectors/vertex-ai/src/tools.ts +23 -33
- package/connectors/wiz/src/search-filter.ts +6 -30
- package/connectors/wiz/src/server.ts +2 -188
- package/connectors/wiz/src/tools.ts +168 -0
- package/connectors/zendesk/src/server.ts +2 -73
- package/connectors/zendesk/src/tools.ts +60 -0
- package/connectors/zoom/src/search-filter.ts +1 -7
- package/connectors/zoom/src/server.ts +2 -94
- package/connectors/zoom/src/tools.ts +93 -0
- package/connectors/zotero/src/server.ts +2 -78
- package/connectors/zotero/src/tools.ts +67 -0
- package/package.json +14 -12
- package/shared/access-token-cache.ts +123 -0
- package/shared/atlassian-json-fetch.ts +6 -16
- package/shared/audit-chain.ts +601 -34
- package/shared/cli-json-kit.ts +191 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- package/shared/consent-kit.ts +81 -26
- package/shared/cursor-list-tool.ts +29 -0
- package/shared/env-json-api.ts +209 -0
- package/shared/fetch-json-text.ts +27 -0
- package/shared/github-rest.ts +62 -0
- package/shared/graphql-json.ts +60 -0
- package/shared/imap-mail-core.ts +91 -0
- package/shared/imap-tool-kit.ts +57 -23
- package/shared/imapflow-adapter.ts +417 -0
- package/shared/nimbus-spawn.ts +84 -27
- package/shared/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/safe-cli-arg.ts +139 -17
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- package/shared/windows-batch-args.ts +273 -0
- package/shared/write-budget.ts +100 -0
- 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
|
+
}
|
package/shared/imap-mail-core.ts
CHANGED
|
@@ -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
|
+
}
|
package/shared/imap-tool-kit.ts
CHANGED
|
@@ -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 (
|
|
273
|
-
const
|
|
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,
|