@nimbus-dev/connectors 0.2.0 → 0.2.2
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 +18 -4
- 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 +15 -34
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +197 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +133 -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 +14 -27
- package/connectors/cloudwatch/src/tools.ts +15 -34
- 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 +152 -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 +159 -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 +19 -38
- 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 +15 -12
- package/shared/access-token-cache.ts +123 -0
- package/shared/atlassian-json-fetch.ts +6 -16
- package/shared/audit-chain.ts +13 -6
- package/shared/cli-json-kit.ts +150 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- 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/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- package/standalone/src/launcher.ts +19 -7
|
@@ -1,232 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IMAP/SMTP connector entry point.
|
|
3
|
+
*
|
|
4
|
+
* Everything below the config is in `shared/imapflow-adapter.ts`: the imapflow
|
|
5
|
+
* client and nodemailer mailer used to be written out in full here and in
|
|
6
|
+
* protonmail's entry point, which made them the most duplicated pair in the repo
|
|
7
|
+
* and left both files unreachable from a test. This file is now only the
|
|
8
|
+
* env → config mapping the adapter cannot know.
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
type BodyStructureNode,
|
|
7
|
-
extractAttachments,
|
|
8
|
-
findTextPlainPart,
|
|
9
|
-
} from "../../../shared/imap-bodystructure.ts";
|
|
10
|
-
import { envInt, previewFromParts } from "../../../shared/imap-tool-kit.ts";
|
|
13
|
+
import { envInt } from "../../../shared/imap-tool-kit.ts";
|
|
14
|
+
import { createImapFlowClient, createNodemailerMailer } from "../../../shared/imapflow-adapter.ts";
|
|
11
15
|
import { requireProcessEnv } from "../../../shared/mcp-tool-kit.ts";
|
|
12
|
-
import {
|
|
13
|
-
clampLimit,
|
|
14
|
-
type ImapAddress,
|
|
15
|
-
type ImapClient,
|
|
16
|
-
type ImapEnvelope,
|
|
17
|
-
type ImapListOptions,
|
|
18
|
-
type ImapMessageMeta,
|
|
19
|
-
type ImapSearchOptions,
|
|
20
|
-
PREVIEW_FETCH_BYTES,
|
|
21
|
-
type SendMailInput,
|
|
22
|
-
type SendMailResult,
|
|
23
|
-
type SmtpMailer,
|
|
24
|
-
} from "./imap-core.ts";
|
|
25
16
|
import { registerImapTools } from "./tools.ts";
|
|
26
17
|
|
|
27
18
|
const DEFAULT_IMAP_PORT = 993;
|
|
28
19
|
const DEFAULT_SMTP_PORT = 465;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Build an {@link ImapAddress} from an imapflow envelope address, omitting
|
|
33
|
-
* `name`/`address` when absent (the interface uses `exactOptionalPropertyTypes`,
|
|
34
|
-
* so an explicit `undefined` is not assignable).
|
|
35
|
-
*/
|
|
36
|
-
function toImapAddress(a: { name?: string; address?: string }): ImapAddress {
|
|
37
|
-
const out: { name?: string; address?: string } = {};
|
|
38
|
-
if (a.name !== undefined) {
|
|
39
|
-
out.name = a.name;
|
|
40
|
-
}
|
|
41
|
-
if (a.address !== undefined) {
|
|
42
|
-
out.address = a.address;
|
|
43
|
-
}
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function envelopeFromImap(env: FetchMessageObject["envelope"]): ImapEnvelope {
|
|
48
|
-
if (env === undefined) {
|
|
49
|
-
return {};
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
date: env.date ?? null,
|
|
53
|
-
subject: env.subject ?? null,
|
|
54
|
-
messageId: env.messageId ?? null,
|
|
55
|
-
from: (env.from ?? []).map(toImapAddress),
|
|
56
|
-
to: (env.to ?? []).map(toImapAddress),
|
|
57
|
-
cc: (env.cc ?? []).map(toImapAddress),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function toMessageMeta(
|
|
62
|
-
msg: FetchMessageObject,
|
|
63
|
-
mailbox: string,
|
|
64
|
-
uidValidity: string | null,
|
|
65
|
-
): ImapMessageMeta {
|
|
66
|
-
const structure = (msg.bodyStructure ?? null) as BodyStructureNode | null;
|
|
67
|
-
const partKey = findTextPlainPart(structure);
|
|
68
|
-
return {
|
|
69
|
-
uid: msg.uid,
|
|
70
|
-
mailbox,
|
|
71
|
-
uidValidity,
|
|
72
|
-
envelope: envelopeFromImap(msg.envelope),
|
|
73
|
-
attachments: extractAttachments(structure),
|
|
74
|
-
preview: previewFromParts(msg.bodyParts, partKey),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Fetch query that requests ONLY headers + structure + a truncated text body
|
|
80
|
-
* part. There is no `source` / full-body request here by construction.
|
|
81
|
-
*/
|
|
82
|
-
function previewFetchQuery(): FetchQueryObject {
|
|
83
|
-
return {
|
|
84
|
-
uid: true,
|
|
85
|
-
envelope: true,
|
|
86
|
-
bodyStructure: true,
|
|
87
|
-
internalDate: true,
|
|
88
|
-
// Two candidate text part keys, each capped to PREVIEW_FETCH_BYTES bytes.
|
|
89
|
-
bodyParts: [
|
|
90
|
-
{ key: "1", start: 0, maxLength: PREVIEW_FETCH_BYTES },
|
|
91
|
-
{ key: "TEXT", start: 0, maxLength: PREVIEW_FETCH_BYTES },
|
|
92
|
-
],
|
|
93
|
-
};
|
|
94
|
-
}
|
|
20
|
+
/** The port on which SMTP uses implicit TLS; anything else is STARTTLS. */
|
|
21
|
+
const SMTPS_PORT = 465;
|
|
95
22
|
|
|
96
|
-
|
|
97
|
-
class ImapFlowClient implements ImapClient {
|
|
98
|
-
private readonly config: {
|
|
99
|
-
host: string;
|
|
100
|
-
port: number;
|
|
101
|
-
user: string;
|
|
102
|
-
pass: string;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
constructor() {
|
|
106
|
-
this.config = {
|
|
107
|
-
host: requireProcessEnv("IMAP_HOST"),
|
|
108
|
-
port: envInt("IMAP_PORT", DEFAULT_IMAP_PORT),
|
|
109
|
-
user: requireProcessEnv("IMAP_USERNAME"),
|
|
110
|
-
pass: requireProcessEnv("IMAP_PASSWORD"),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
private newClient(): ImapFlow {
|
|
115
|
-
return new ImapFlow({
|
|
116
|
-
host: this.config.host,
|
|
117
|
-
port: this.config.port,
|
|
118
|
-
secure: true,
|
|
119
|
-
auth: { user: this.config.user, pass: this.config.pass },
|
|
120
|
-
logger: false,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
private async withMailbox<T>(
|
|
125
|
-
mailbox: string,
|
|
126
|
-
fn: (client: ImapFlow, uidValidity: string | null) => Promise<T>,
|
|
127
|
-
): Promise<T> {
|
|
128
|
-
const client = this.newClient();
|
|
129
|
-
await client.connect();
|
|
130
|
-
const lock = await client.getMailboxLock(mailbox);
|
|
131
|
-
try {
|
|
132
|
-
const mb = client.mailbox;
|
|
133
|
-
const uidValidity = mb === false ? null : String(mb.uidValidity);
|
|
134
|
-
return await fn(client, uidValidity);
|
|
135
|
-
} finally {
|
|
136
|
-
lock.release();
|
|
137
|
-
await client.logout();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async list(options: ImapListOptions): Promise<ImapMessageMeta[]> {
|
|
142
|
-
const mailbox = options.mailbox ?? DEFAULT_MAILBOX;
|
|
143
|
-
const limit = clampLimit(options.limit);
|
|
144
|
-
return this.withMailbox(mailbox, async (client, uidValidity) => {
|
|
145
|
-
const out: ImapMessageMeta[] = [];
|
|
146
|
-
const status = await client.status(mailbox, { messages: true });
|
|
147
|
-
const total = status.messages ?? 0;
|
|
148
|
-
if (total === 0) {
|
|
149
|
-
return out;
|
|
150
|
-
}
|
|
151
|
-
const start = Math.max(1, total - limit + 1);
|
|
152
|
-
const range = `${start}:${total}`;
|
|
153
|
-
for await (const msg of client.fetch(range, previewFetchQuery())) {
|
|
154
|
-
out.push(toMessageMeta(msg, mailbox, uidValidity));
|
|
155
|
-
}
|
|
156
|
-
// Most-recent first.
|
|
157
|
-
out.sort((a, b) => b.uid - a.uid);
|
|
158
|
-
return out;
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async get(uid: number, mailbox?: string): Promise<ImapMessageMeta | null> {
|
|
163
|
-
const box = mailbox ?? DEFAULT_MAILBOX;
|
|
164
|
-
return this.withMailbox(box, async (client, uidValidity) => {
|
|
165
|
-
const msg = await client.fetchOne(String(uid), previewFetchQuery(), { uid: true });
|
|
166
|
-
return msg === false ? null : toMessageMeta(msg, box, uidValidity);
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async search(options: ImapSearchOptions): Promise<ImapMessageMeta[]> {
|
|
171
|
-
const mailbox = options.mailbox ?? DEFAULT_MAILBOX;
|
|
172
|
-
const limit = clampLimit(options.limit);
|
|
173
|
-
const q = options.query;
|
|
174
|
-
return this.withMailbox(mailbox, async (client, uidValidity) => {
|
|
175
|
-
const uids = await client.search(
|
|
176
|
-
{ or: [{ subject: q }, { from: q }, { to: q }] },
|
|
177
|
-
{ uid: true },
|
|
178
|
-
);
|
|
179
|
-
if (uids === false || uids.length === 0) {
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
const picked = uids.slice(-limit).sort((a, b) => b - a);
|
|
183
|
-
const out: ImapMessageMeta[] = [];
|
|
184
|
-
for await (const msg of client.fetch(picked, previewFetchQuery(), { uid: true })) {
|
|
185
|
-
out.push(toMessageMeta(msg, mailbox, uidValidity));
|
|
186
|
-
}
|
|
187
|
-
out.sort((a, b) => b.uid - a.uid);
|
|
188
|
-
return out;
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Real SMTP mailer over nodemailer. */
|
|
194
|
-
class NodemailerMailer implements SmtpMailer {
|
|
195
|
-
private readonly transport: Transporter;
|
|
196
|
-
private readonly from: string;
|
|
197
|
-
|
|
198
|
-
constructor() {
|
|
199
|
-
const host = requireProcessEnv("IMAP_SMTP_HOST");
|
|
200
|
-
const port = envInt("IMAP_SMTP_PORT", DEFAULT_SMTP_PORT);
|
|
201
|
-
const user = requireProcessEnv("IMAP_SMTP_USERNAME");
|
|
202
|
-
const pass = requireProcessEnv("IMAP_SMTP_PASSWORD");
|
|
203
|
-
this.from = user;
|
|
204
|
-
this.transport = createTransport({
|
|
205
|
-
host,
|
|
206
|
-
port,
|
|
207
|
-
secure: port === 465,
|
|
208
|
-
auth: { user, pass },
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async send(input: SendMailInput): Promise<SendMailResult> {
|
|
213
|
-
const info = await this.transport.sendMail({
|
|
214
|
-
from: this.from,
|
|
215
|
-
to: input.to,
|
|
216
|
-
subject: input.subject,
|
|
217
|
-
text: input.body,
|
|
218
|
-
...(input.cc === undefined ? {} : { cc: input.cc }),
|
|
219
|
-
...(input.bcc === undefined ? {} : { bcc: input.bcc }),
|
|
220
|
-
});
|
|
221
|
-
return {
|
|
222
|
-
messageId: info.messageId ?? null,
|
|
223
|
-
accepted: (info.accepted ?? []).map(String),
|
|
224
|
-
rejected: (info.rejected ?? []).map(String),
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
}
|
|
23
|
+
const smtpPort = envInt("IMAP_SMTP_PORT", DEFAULT_SMTP_PORT);
|
|
228
24
|
|
|
229
25
|
const server = new McpServer({ name: "nimbus-imap", version: "0.1.0" });
|
|
230
|
-
|
|
26
|
+
|
|
27
|
+
registerImapTools(
|
|
28
|
+
server,
|
|
29
|
+
createImapFlowClient({
|
|
30
|
+
host: requireProcessEnv("IMAP_HOST"),
|
|
31
|
+
port: envInt("IMAP_PORT", DEFAULT_IMAP_PORT),
|
|
32
|
+
user: requireProcessEnv("IMAP_USERNAME"),
|
|
33
|
+
pass: requireProcessEnv("IMAP_PASSWORD"),
|
|
34
|
+
secure: true,
|
|
35
|
+
}),
|
|
36
|
+
createNodemailerMailer({
|
|
37
|
+
host: requireProcessEnv("IMAP_SMTP_HOST"),
|
|
38
|
+
port: smtpPort,
|
|
39
|
+
user: requireProcessEnv("IMAP_SMTP_USERNAME"),
|
|
40
|
+
pass: requireProcessEnv("IMAP_SMTP_PASSWORD"),
|
|
41
|
+
secure: smtpPort === SMTPS_PORT,
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
231
44
|
|
|
232
45
|
await server.connect(new StdioServerTransport());
|
|
@@ -1,65 +1,6 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { matchesResult, searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
|
|
3
|
-
import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
|
|
4
1
|
import { runReadOnlyMcpConnector } from "../../../shared/run-read-only-mcp-connector.ts";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const BASE = "https://api.intercom.io";
|
|
8
|
-
|
|
9
|
-
function apiToken(): string {
|
|
10
|
-
const t = process.env["INTERCOM_TOKEN"]?.trim();
|
|
11
|
-
if (t === undefined || t === "") {
|
|
12
|
-
throw new Error("INTERCOM_TOKEN is not set");
|
|
13
|
-
}
|
|
14
|
-
return t;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function authHeader(): Record<string, string> {
|
|
18
|
-
return {
|
|
19
|
-
Authorization: `Bearer ${apiToken()}`,
|
|
20
|
-
"Intercom-Version": "2.11",
|
|
21
|
-
Accept: "application/json",
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function intercomGet(path: string): Promise<unknown> {
|
|
26
|
-
const res = await fetch(`${BASE}${path}`, { headers: authHeader() });
|
|
27
|
-
const text = await res.text();
|
|
28
|
-
if (!res.ok) {
|
|
29
|
-
throw new Error(`Intercom ${String(res.status)}: ${text.slice(0, 400)}`);
|
|
30
|
-
}
|
|
31
|
-
return JSON.parse(text) as unknown;
|
|
32
|
-
}
|
|
2
|
+
import { registerIntercomTools } from "./tools.ts";
|
|
33
3
|
|
|
34
4
|
await runReadOnlyMcpConnector("nimbus-intercom", (reg) => {
|
|
35
|
-
reg
|
|
36
|
-
"intercom_list",
|
|
37
|
-
'List the user\'s Intercom conversations (`GET /conversations?per_page=150`). Returns the `{ type: "conversation.list", conversations: [...], pages, total_count }` envelope — `conversations` holds the conversation objects.',
|
|
38
|
-
z.object({}),
|
|
39
|
-
async () => {
|
|
40
|
-
return jsonResult(await intercomGet(`/conversations?per_page=150`));
|
|
41
|
-
},
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
reg(
|
|
45
|
-
"intercom_get",
|
|
46
|
-
"Fetch one Intercom conversation by its id (`GET /conversations/{id}` — note the PLURAL `conversations` in the get-by-id path). Returns the conversation object directly. Throws when no match is found.",
|
|
47
|
-
z.object({
|
|
48
|
-
id: z.string().min(1),
|
|
49
|
-
}),
|
|
50
|
-
async (p) => {
|
|
51
|
-
return jsonResult(await intercomGet(`/conversations/${encodeURIComponent(p.id)}`));
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
reg(
|
|
56
|
-
"intercom_search",
|
|
57
|
-
"Substring search across the user's Intercom conversations (first page only). Matches the query against the conversation subject, the source message body, the state, the source author name + email, and the tags (case-insensitive). Returns a `{ matches: [...] }` envelope.",
|
|
58
|
-
searchToolInputSchema(100),
|
|
59
|
-
async (p) => {
|
|
60
|
-
const root = await intercomGet(`/conversations?per_page=150`);
|
|
61
|
-
const conversations = (root as { conversations?: unknown[] } | null)?.conversations;
|
|
62
|
-
return matchesResult(conversations, filterIntercomConversations, p);
|
|
63
|
-
},
|
|
64
|
-
);
|
|
5
|
+
registerIntercomTools(reg);
|
|
65
6
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createJsonGetter, requiredEnv } from "../../../shared/env-json-api.ts";
|
|
3
|
+
import { matchesResult, searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
|
|
4
|
+
import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
|
|
5
|
+
import type { ZodToolRegistrar } from "../../../shared/run-read-only-mcp-connector.ts";
|
|
6
|
+
import { filterIntercomConversations } from "./search-filter.ts";
|
|
7
|
+
|
|
8
|
+
const BASE = "https://api.intercom.io";
|
|
9
|
+
|
|
10
|
+
function authHeader(): Record<string, string> {
|
|
11
|
+
return {
|
|
12
|
+
Authorization: `Bearer ${requiredEnv("INTERCOM_TOKEN")}`,
|
|
13
|
+
"Intercom-Version": "2.11",
|
|
14
|
+
Accept: "application/json",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const intercomGet = createJsonGetter({
|
|
19
|
+
base: BASE,
|
|
20
|
+
label: "Intercom",
|
|
21
|
+
headers: authHeader,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/** Tool names exposed by this connector — for contract/introspection tests. */
|
|
25
|
+
export const INTERCOM_TOOL_NAMES = ["intercom_get", "intercom_list", "intercom_search"] as const;
|
|
26
|
+
|
|
27
|
+
export function registerIntercomTools(reg: ZodToolRegistrar): void {
|
|
28
|
+
reg(
|
|
29
|
+
"intercom_list",
|
|
30
|
+
'List the user\'s Intercom conversations (`GET /conversations?per_page=150`). Returns the `{ type: "conversation.list", conversations: [...], pages, total_count }` envelope — `conversations` holds the conversation objects.',
|
|
31
|
+
z.object({}),
|
|
32
|
+
async () => {
|
|
33
|
+
return jsonResult(await intercomGet(`/conversations?per_page=150`));
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
reg(
|
|
38
|
+
"intercom_get",
|
|
39
|
+
"Fetch one Intercom conversation by its id (`GET /conversations/{id}` — note the PLURAL `conversations` in the get-by-id path). Returns the conversation object directly. Throws when no match is found.",
|
|
40
|
+
z.object({
|
|
41
|
+
id: z.string().min(1),
|
|
42
|
+
}),
|
|
43
|
+
async (p) => {
|
|
44
|
+
return jsonResult(await intercomGet(`/conversations/${encodeURIComponent(p.id)}`));
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
reg(
|
|
49
|
+
"intercom_search",
|
|
50
|
+
"Substring search across the user's Intercom conversations (first page only). Matches the query against the conversation subject, the source message body, the state, the source author name + email, and the tags (case-insensitive). Returns a `{ matches: [...] }` envelope.",
|
|
51
|
+
searchToolInputSchema(100),
|
|
52
|
+
async (p) => {
|
|
53
|
+
const root = await intercomGet(`/conversations?per_page=150`);
|
|
54
|
+
const conversations = (root as { conversations?: unknown[] } | null)?.conversations;
|
|
55
|
+
return matchesResult(conversations, filterIntercomConversations, p);
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
+
import { requiredBaseUrl } from "../../../shared/env-json-api.ts";
|
|
1
2
|
import { encodeBasicAuthHeader } from "../../../shared/mcp-tool-kit.ts";
|
|
2
|
-
import { stripTrailingSlashes } from "../../../shared/strip-trailing-slashes.ts";
|
|
3
3
|
|
|
4
4
|
export function jenkinsBaseUrl(): string {
|
|
5
|
-
|
|
6
|
-
if (raw === "") {
|
|
7
|
-
throw new Error("JENKINS_BASE_URL is not set");
|
|
8
|
-
}
|
|
9
|
-
return stripTrailingSlashes(raw);
|
|
5
|
+
return requiredBaseUrl("JENKINS_BASE_URL");
|
|
10
6
|
}
|
|
11
7
|
|
|
12
8
|
export function jenkinsAuthHeader(): string {
|
|
@@ -1,223 +1,10 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
createRegisterSimpleTool,
|
|
7
|
-
createZodToolRegistrar,
|
|
8
|
-
mcpJsonResult as jsonResult,
|
|
9
|
-
} from "../../../shared/mcp-tool-kit.ts";
|
|
10
|
-
import {
|
|
11
|
-
getJenkinsCrumb,
|
|
12
|
-
jenkinsAuthHeader,
|
|
13
|
-
jenkinsBaseUrl,
|
|
14
|
-
jenkinsFetchJson,
|
|
15
|
-
jenkinsPost,
|
|
16
|
-
jobApiRoot,
|
|
17
|
-
} from "./jenkins-api.ts";
|
|
3
|
+
import { registerJenkinsTools } from "./tools.ts";
|
|
18
4
|
|
|
19
5
|
const mcp = new McpServer({ name: "nimbus-jenkins", version: "0.1.0" });
|
|
20
6
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const reg = createZodToolRegistrar(createRegisterSimpleTool(mcp));
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Every MUTATING jenkins tool goes through here. Outside the gateway this adds the
|
|
27
|
-
* consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
|
|
28
|
-
* the gateway it is a pass-through, because executor.ts (I2) is the gate there.
|
|
29
|
-
*/
|
|
30
|
-
const registerWriteTool = createWriteToolRegistrar(mcp, {
|
|
31
|
-
connector: "jenkins",
|
|
32
|
-
scopeEnv: "NIMBUS_MCP_JENKINS_WRITE_SCOPE",
|
|
33
|
-
scopeKinds: ["job"],
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const JOBS_TREE =
|
|
37
|
-
"jobs[name,fullname,url,jobs[name,fullname,url,jobs[name,fullname,url,jobs[name,fullname,url]]]]";
|
|
38
|
-
|
|
39
|
-
type JobNode = {
|
|
40
|
-
name?: string;
|
|
41
|
-
fullName?: string;
|
|
42
|
-
url?: string;
|
|
43
|
-
jobs?: JobNode[];
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
function jenkinsJobNodeDisplayName(n: JobNode): string {
|
|
47
|
-
if (typeof n.fullName === "string" && n.fullName !== "") {
|
|
48
|
-
return n.fullName;
|
|
49
|
-
}
|
|
50
|
-
if (typeof n.name === "string") {
|
|
51
|
-
return n.name;
|
|
52
|
-
}
|
|
53
|
-
return "";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function flattenJobs(
|
|
57
|
-
nodes: JobNode[] | undefined,
|
|
58
|
-
out: { fullName: string; url?: string }[],
|
|
59
|
-
): void {
|
|
60
|
-
if (nodes === undefined) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
for (const n of nodes) {
|
|
64
|
-
const fn = jenkinsJobNodeDisplayName(n);
|
|
65
|
-
if (fn !== "") {
|
|
66
|
-
if (typeof n.url === "string") {
|
|
67
|
-
out.push({ fullName: fn, url: n.url });
|
|
68
|
-
} else {
|
|
69
|
-
out.push({ fullName: fn });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
flattenJobs(n.jobs, out);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
reg(
|
|
77
|
-
"jenkins_job_list",
|
|
78
|
-
"List Jenkins jobs (nested folders, limited depth).",
|
|
79
|
-
z.object({}),
|
|
80
|
-
async () => {
|
|
81
|
-
const base = jenkinsBaseUrl();
|
|
82
|
-
const url = `${base}/api/json?tree=${encodeURIComponent(JOBS_TREE)}`;
|
|
83
|
-
const root = await jenkinsGetJsonExpectOk(url);
|
|
84
|
-
if (root === null || typeof root !== "object" || Array.isArray(root)) {
|
|
85
|
-
throw new Error("Jenkins: invalid jobs response");
|
|
86
|
-
}
|
|
87
|
-
const jobsRaw = (root as Record<string, unknown>)["jobs"];
|
|
88
|
-
const list: { fullName: string; url?: string }[] = [];
|
|
89
|
-
flattenJobs(Array.isArray(jobsRaw) ? (jobsRaw as JobNode[]) : undefined, list);
|
|
90
|
-
return jsonResult({ jobs: list });
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
async function jenkinsGetJsonExpectOk(url: string): Promise<unknown> {
|
|
95
|
-
const auth = jenkinsAuthHeader();
|
|
96
|
-
const res = await jenkinsFetchJson(url, { method: "GET", authHeader: auth });
|
|
97
|
-
if (!res.ok) {
|
|
98
|
-
throw new Error(`Jenkins ${String(res.status)}: ${res.text.slice(0, 400)}`);
|
|
99
|
-
}
|
|
100
|
-
return res.json;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const jobNameSchema = z.object({
|
|
104
|
-
jobName: z.string().min(1).describe("Job full name (e.g. folder/sub/job)"),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
reg("jenkins_job_get", "Get Jenkins job metadata by full name.", jobNameSchema, async (parsed) => {
|
|
108
|
-
const base = jenkinsBaseUrl();
|
|
109
|
-
const root = jobApiRoot(base, parsed.jobName);
|
|
110
|
-
const url = `${root}/api/json`;
|
|
111
|
-
return jsonResult(await jenkinsGetJsonExpectOk(url));
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
reg(
|
|
115
|
-
"jenkins_build_list",
|
|
116
|
-
"List recent builds for a job.",
|
|
117
|
-
z.object({
|
|
118
|
-
jobName: z.string().min(1),
|
|
119
|
-
limit: z.number().int().min(1).max(50).optional(),
|
|
120
|
-
}),
|
|
121
|
-
async (parsed) => {
|
|
122
|
-
const base = jenkinsBaseUrl();
|
|
123
|
-
const lim = parsed.limit ?? 20;
|
|
124
|
-
const tree = encodeURIComponent(
|
|
125
|
-
`builds[number,url,result,duration,timestamp,building]{0,${String(lim)}}`,
|
|
126
|
-
);
|
|
127
|
-
const url = `${jobApiRoot(base, parsed.jobName)}/api/json?tree=${tree}`;
|
|
128
|
-
return jsonResult(await jenkinsGetJsonExpectOk(url));
|
|
129
|
-
},
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
reg(
|
|
133
|
-
"jenkins_build_get",
|
|
134
|
-
"Get a single build by job name and build number.",
|
|
135
|
-
z.object({
|
|
136
|
-
jobName: z.string().min(1),
|
|
137
|
-
buildNumber: z.number().int().min(1),
|
|
138
|
-
}),
|
|
139
|
-
async (parsed) => {
|
|
140
|
-
const base = jenkinsBaseUrl();
|
|
141
|
-
const url = `${jobApiRoot(base, parsed.jobName)}/${String(parsed.buildNumber)}/api/json`;
|
|
142
|
-
return jsonResult(await jenkinsGetJsonExpectOk(url));
|
|
143
|
-
},
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
registerWriteTool(
|
|
147
|
-
"jenkins_build_trigger",
|
|
148
|
-
{
|
|
149
|
-
mutates: "jenkins.build.trigger",
|
|
150
|
-
recoverable: true,
|
|
151
|
-
scopeTargetOf: (p) => ({ kind: "job", value: p.jobName }),
|
|
152
|
-
},
|
|
153
|
-
"Trigger a new build for a job.",
|
|
154
|
-
jobNameSchema,
|
|
155
|
-
async (parsed) => {
|
|
156
|
-
const base = jenkinsBaseUrl();
|
|
157
|
-
const auth = jenkinsAuthHeader();
|
|
158
|
-
const crumb = await getJenkinsCrumb(base, auth);
|
|
159
|
-
const url = `${jobApiRoot(base, parsed.jobName)}/build`;
|
|
160
|
-
const res = await jenkinsPost(url, auth, crumb);
|
|
161
|
-
if (!res.ok) {
|
|
162
|
-
throw new Error(`Jenkins trigger ${String(res.status)}: ${res.text.slice(0, 400)}`);
|
|
163
|
-
}
|
|
164
|
-
return jsonResult({ ok: true, jobName: parsed.jobName });
|
|
165
|
-
},
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
registerWriteTool(
|
|
169
|
-
"jenkins_build_abort",
|
|
170
|
-
{
|
|
171
|
-
mutates: "jenkins.build.abort",
|
|
172
|
-
recoverable: true,
|
|
173
|
-
scopeTargetOf: (p) => ({ kind: "job", value: p.jobName }),
|
|
174
|
-
},
|
|
175
|
-
"Abort/stop a running build.",
|
|
176
|
-
z.object({
|
|
177
|
-
jobName: z.string().min(1),
|
|
178
|
-
buildNumber: z.number().int().min(1),
|
|
179
|
-
}),
|
|
180
|
-
async (parsed) => {
|
|
181
|
-
const base = jenkinsBaseUrl();
|
|
182
|
-
const auth = jenkinsAuthHeader();
|
|
183
|
-
const crumb = await getJenkinsCrumb(base, auth);
|
|
184
|
-
const url = `${jobApiRoot(base, parsed.jobName)}/${String(parsed.buildNumber)}/stop`;
|
|
185
|
-
const res = await jenkinsPost(url, auth, crumb);
|
|
186
|
-
if (!res.ok) {
|
|
187
|
-
throw new Error(`Jenkins abort ${String(res.status)}: ${res.text.slice(0, 400)}`);
|
|
188
|
-
}
|
|
189
|
-
return jsonResult({ ok: true, jobName: parsed.jobName, buildNumber: parsed.buildNumber });
|
|
190
|
-
},
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
reg(
|
|
194
|
-
"jenkins_build_log_tail",
|
|
195
|
-
"Fetch console text for a build (full log; tail client-side).",
|
|
196
|
-
z.object({
|
|
197
|
-
jobName: z.string().min(1),
|
|
198
|
-
buildNumber: z.number().int().min(1),
|
|
199
|
-
maxLines: z.number().int().min(1).max(5000).optional(),
|
|
200
|
-
}),
|
|
201
|
-
async (parsed) => {
|
|
202
|
-
const base = jenkinsBaseUrl();
|
|
203
|
-
const auth = jenkinsAuthHeader();
|
|
204
|
-
const url = `${jobApiRoot(base, parsed.jobName)}/${String(parsed.buildNumber)}/consoleText`;
|
|
205
|
-
const res = await fetch(url, { headers: { Authorization: auth } });
|
|
206
|
-
const text = await res.text();
|
|
207
|
-
if (!res.ok) {
|
|
208
|
-
throw new Error(`Jenkins log ${String(res.status)}: ${text.slice(0, 400)}`);
|
|
209
|
-
}
|
|
210
|
-
const maxLines = parsed.maxLines ?? 200;
|
|
211
|
-
const lines = text.split(/\r?\n/);
|
|
212
|
-
const tail = lines.slice(Math.max(0, lines.length - maxLines)).join("\n");
|
|
213
|
-
return jsonResult({
|
|
214
|
-
jobName: parsed.jobName,
|
|
215
|
-
buildNumber: parsed.buildNumber,
|
|
216
|
-
lineCount: lines.length,
|
|
217
|
-
tail,
|
|
218
|
-
});
|
|
219
|
-
},
|
|
220
|
-
);
|
|
7
|
+
registerJenkinsTools(mcp);
|
|
221
8
|
|
|
222
9
|
const transport = new StdioServerTransport();
|
|
223
10
|
await mcp.connect(transport);
|