@nimbus-dev/connectors 0.2.1 → 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.
Files changed (215) hide show
  1. package/README.md +14 -4
  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 +15 -34
  12. package/connectors/aws/src/server.ts +2 -183
  13. package/connectors/aws/src/tools.ts +197 -0
  14. package/connectors/azure/src/server.ts +2 -131
  15. package/connectors/azure/src/tools.ts +133 -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 +14 -27
  29. package/connectors/cloudwatch/src/tools.ts +15 -34
  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 +152 -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 +159 -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 +19 -38
  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 +13 -6
  200. package/shared/cli-json-kit.ts +150 -0
  201. package/shared/collection-tool-kit.ts +143 -0
  202. package/shared/connector-mode.ts +3 -2
  203. package/shared/cursor-list-tool.ts +29 -0
  204. package/shared/env-json-api.ts +209 -0
  205. package/shared/fetch-json-text.ts +27 -0
  206. package/shared/github-rest.ts +62 -0
  207. package/shared/graphql-json.ts +60 -0
  208. package/shared/imap-mail-core.ts +91 -0
  209. package/shared/imap-tool-kit.ts +57 -23
  210. package/shared/imapflow-adapter.ts +417 -0
  211. package/shared/rest-tool-kit.ts +77 -7
  212. package/shared/run-cli-json.ts +38 -16
  213. package/shared/search-filter.ts +58 -18
  214. package/shared/walk-files.ts +118 -0
  215. 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 { type FetchMessageObject, type FetchQueryObject, ImapFlow } from "imapflow";
4
- import { createTransport, type Transporter } from "nodemailer";
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
- const DEFAULT_MAILBOX = "INBOX";
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
- /** Real IMAP client over imapflow. Per-call connect/lock/logout keeps it stateless. */
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
- registerImapTools(server, new ImapFlowClient(), new NodemailerMailer());
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 { filterIntercomConversations } from "./search-filter.ts";
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
- const raw = process.env["JENKINS_BASE_URL"]?.trim() ?? "";
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 { z } from "zod";
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
- import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
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);