@nimbus-dev/connectors 0.1.0
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/LICENSE +662 -0
- package/NOTICE +40 -0
- package/README.md +111 -0
- package/connectors/airflow/nimbus.extension.json +16 -0
- package/connectors/airflow/src/search-filter.ts +38 -0
- package/connectors/airflow/src/server.ts +91 -0
- package/connectors/apple/nimbus.extension.json +40 -0
- package/connectors/apple/src/apple-mail-core.ts +43 -0
- package/connectors/apple/src/caldav-core.ts +116 -0
- package/connectors/apple/src/calendar-tools.ts +278 -0
- package/connectors/apple/src/server.ts +541 -0
- package/connectors/apple/src/tools.ts +138 -0
- package/connectors/argocd/nimbus.extension.json +16 -0
- package/connectors/argocd/src/search-filter.ts +24 -0
- package/connectors/argocd/src/server.ts +158 -0
- package/connectors/athena/nimbus.extension.json +15 -0
- package/connectors/athena/src/server.ts +6 -0
- package/connectors/athena/src/tools.ts +181 -0
- package/connectors/aws/nimbus.extension.json +21 -0
- package/connectors/aws/src/server.ts +191 -0
- package/connectors/azure/nimbus.extension.json +15 -0
- package/connectors/azure/src/server.ts +139 -0
- package/connectors/bigeye/nimbus.extension.json +16 -0
- package/connectors/bigeye/src/search-filter.ts +22 -0
- package/connectors/bigeye/src/server.ts +161 -0
- package/connectors/bigquery/nimbus.extension.json +15 -0
- package/connectors/bigquery/src/server.ts +6 -0
- package/connectors/bigquery/src/tools.ts +175 -0
- package/connectors/bitbucket/nimbus.extension.json +15 -0
- package/connectors/bitbucket/src/server.ts +252 -0
- package/connectors/bitrise/nimbus.extension.json +15 -0
- package/connectors/bitrise/src/search-filter.ts +11 -0
- package/connectors/bitrise/src/server.ts +83 -0
- package/connectors/canva/nimbus.extension.json +15 -0
- package/connectors/canva/src/search-filter.ts +23 -0
- package/connectors/canva/src/server.ts +77 -0
- package/connectors/circleci/nimbus.extension.json +15 -0
- package/connectors/circleci/src/server.ts +184 -0
- package/connectors/cloud-logging/nimbus.extension.json +15 -0
- package/connectors/cloud-logging/src/server.ts +6 -0
- package/connectors/cloud-logging/src/tools.ts +115 -0
- package/connectors/cloudwatch/nimbus.extension.json +15 -0
- package/connectors/cloudwatch/src/server.ts +6 -0
- package/connectors/cloudwatch/src/tools.ts +144 -0
- package/connectors/codemagic/nimbus.extension.json +15 -0
- package/connectors/codemagic/src/search-filter.ts +11 -0
- package/connectors/codemagic/src/server.ts +74 -0
- package/connectors/confluence/nimbus.extension.json +15 -0
- package/connectors/confluence/src/kb-append.ts +25 -0
- package/connectors/confluence/src/server.ts +309 -0
- package/connectors/dagster/nimbus.extension.json +16 -0
- package/connectors/dagster/src/search-filter.ts +53 -0
- package/connectors/dagster/src/server.ts +218 -0
- package/connectors/databricks/nimbus.extension.json +16 -0
- package/connectors/databricks/src/search-filter.ts +28 -0
- package/connectors/databricks/src/server.ts +81 -0
- package/connectors/datadog/nimbus.extension.json +15 -0
- package/connectors/datadog/src/server.ts +56 -0
- package/connectors/dataprofile/nimbus.extension.json +16 -0
- package/connectors/dataprofile/src/profile.ts +298 -0
- package/connectors/dataprofile/src/server.ts +6 -0
- package/connectors/dataprofile/src/tools.ts +69 -0
- package/connectors/dbt/nimbus.extension.json +16 -0
- package/connectors/dbt/src/search-filter.ts +28 -0
- package/connectors/dbt/src/server.ts +96 -0
- package/connectors/dependencytrack/nimbus.extension.json +16 -0
- package/connectors/dependencytrack/src/search-filter.ts +24 -0
- package/connectors/dependencytrack/src/server.ts +80 -0
- package/connectors/discord/nimbus.extension.json +15 -0
- package/connectors/discord/src/server.ts +93 -0
- package/connectors/elasticsearch/nimbus.extension.json +16 -0
- package/connectors/elasticsearch/src/server.ts +6 -0
- package/connectors/elasticsearch/src/tools.ts +110 -0
- package/connectors/fastmail/nimbus.extension.json +16 -0
- package/connectors/fastmail/src/jmap-core.ts +86 -0
- package/connectors/fastmail/src/server.ts +194 -0
- package/connectors/fastmail/src/tools.ts +143 -0
- package/connectors/figma/nimbus.extension.json +15 -0
- package/connectors/figma/src/search-filter.ts +24 -0
- package/connectors/figma/src/server.ts +117 -0
- package/connectors/firebase/nimbus.extension.json +15 -0
- package/connectors/firebase/src/search-filter.ts +30 -0
- package/connectors/firebase/src/server.ts +94 -0
- package/connectors/firebase/src/token.ts +41 -0
- package/connectors/flagsmith/nimbus.extension.json +15 -0
- package/connectors/flagsmith/src/search-filter.ts +29 -0
- package/connectors/flagsmith/src/server.ts +102 -0
- package/connectors/flux/nimbus.extension.json +16 -0
- package/connectors/flux/src/search-filter.ts +42 -0
- package/connectors/flux/src/server.ts +182 -0
- package/connectors/gcp/nimbus.extension.json +15 -0
- package/connectors/gcp/src/server.ts +140 -0
- package/connectors/github/nimbus.extension.json +15 -0
- package/connectors/github/src/server.ts +347 -0
- package/connectors/github-actions/nimbus.extension.json +15 -0
- package/connectors/github-actions/src/server.ts +213 -0
- package/connectors/gitlab/nimbus.extension.json +15 -0
- package/connectors/gitlab/src/server.ts +392 -0
- package/connectors/gmail/nimbus.extension.json +15 -0
- package/connectors/gmail/src/server.ts +277 -0
- package/connectors/google-drive/nimbus.extension.json +15 -0
- package/connectors/google-drive/src/drive-query.ts +3 -0
- package/connectors/google-drive/src/server.ts +531 -0
- package/connectors/google-meet/nimbus.extension.json +15 -0
- package/connectors/google-meet/src/server.ts +89 -0
- package/connectors/google-photos/nimbus.extension.json +15 -0
- package/connectors/google-photos/src/server.ts +139 -0
- package/connectors/grafana/nimbus.extension.json +15 -0
- package/connectors/grafana/src/server.ts +61 -0
- package/connectors/great-expectations/nimbus.extension.json +16 -0
- package/connectors/great-expectations/src/gx-parse.ts +292 -0
- package/connectors/great-expectations/src/server.ts +6 -0
- package/connectors/great-expectations/src/tools.ts +91 -0
- package/connectors/greenhouse/nimbus.extension.json +15 -0
- package/connectors/greenhouse/src/search-filter.ts +65 -0
- package/connectors/greenhouse/src/server.ts +67 -0
- package/connectors/hubspot/nimbus.extension.json +15 -0
- package/connectors/hubspot/src/search-filter.ts +28 -0
- package/connectors/hubspot/src/server.ts +81 -0
- package/connectors/iac/nimbus.extension.json +14 -0
- package/connectors/iac/src/server.ts +145 -0
- package/connectors/imap/nimbus.extension.json +16 -0
- package/connectors/imap/src/imap-core.ts +110 -0
- package/connectors/imap/src/server.ts +232 -0
- package/connectors/imap/src/tools.ts +40 -0
- package/connectors/intercom/nimbus.extension.json +15 -0
- package/connectors/intercom/src/search-filter.ts +46 -0
- package/connectors/intercom/src/server.ts +65 -0
- package/connectors/jenkins/nimbus.extension.json +15 -0
- package/connectors/jenkins/src/jenkins-api.ts +156 -0
- package/connectors/jenkins/src/server.ts +223 -0
- package/connectors/jira/nimbus.extension.json +15 -0
- package/connectors/jira/src/server.ts +309 -0
- package/connectors/kubernetes/nimbus.extension.json +15 -0
- package/connectors/kubernetes/src/server.ts +163 -0
- package/connectors/launchdarkly/nimbus.extension.json +15 -0
- package/connectors/launchdarkly/src/search-filter.ts +31 -0
- package/connectors/launchdarkly/src/server.ts +79 -0
- package/connectors/lever/nimbus.extension.json +15 -0
- package/connectors/lever/src/search-filter.ts +35 -0
- package/connectors/lever/src/server.ts +64 -0
- package/connectors/linear/nimbus.extension.json +15 -0
- package/connectors/linear/src/server.ts +467 -0
- package/connectors/localdb/nimbus.extension.json +16 -0
- package/connectors/localdb/src/server.ts +6 -0
- package/connectors/localdb/src/sql-scan.ts +158 -0
- package/connectors/localdb/src/tools.ts +67 -0
- package/connectors/looker/nimbus.extension.json +16 -0
- package/connectors/looker/src/search-filter.ts +22 -0
- package/connectors/looker/src/server.ts +254 -0
- package/connectors/mendeley/nimbus.extension.json +15 -0
- package/connectors/mendeley/src/search-filter.ts +68 -0
- package/connectors/mendeley/src/server.ts +59 -0
- package/connectors/mercury/nimbus.extension.json +15 -0
- package/connectors/mercury/src/search-filter.ts +11 -0
- package/connectors/mercury/src/server.ts +61 -0
- package/connectors/metabase/nimbus.extension.json +16 -0
- package/connectors/metabase/src/search-filter.ts +22 -0
- package/connectors/metabase/src/server.ts +83 -0
- package/connectors/miro/nimbus.extension.json +15 -0
- package/connectors/miro/src/search-filter.ts +24 -0
- package/connectors/miro/src/server.ts +76 -0
- package/connectors/mlflow/nimbus.extension.json +16 -0
- package/connectors/mlflow/src/search-filter.ts +38 -0
- package/connectors/mlflow/src/server.ts +167 -0
- package/connectors/monte-carlo/nimbus.extension.json +16 -0
- package/connectors/monte-carlo/src/search-filter.ts +11 -0
- package/connectors/monte-carlo/src/server.ts +241 -0
- package/connectors/netlify/nimbus.extension.json +15 -0
- package/connectors/netlify/src/search-filter.ts +37 -0
- package/connectors/netlify/src/server.ts +64 -0
- package/connectors/newrelic/nimbus.extension.json +15 -0
- package/connectors/newrelic/src/server.ts +48 -0
- package/connectors/notion/nimbus.extension.json +15 -0
- package/connectors/notion/src/kb-append.ts +23 -0
- package/connectors/notion/src/server.ts +343 -0
- package/connectors/obsidian/nimbus.extension.json +19 -0
- package/connectors/obsidian/src/server.ts +433 -0
- package/connectors/onedrive/nimbus.extension.json +15 -0
- package/connectors/onedrive/src/server.ts +256 -0
- package/connectors/outlook/nimbus.extension.json +15 -0
- package/connectors/outlook/src/server.ts +358 -0
- package/connectors/outlook/src/tool-scope-policy.ts +53 -0
- package/connectors/pagerduty/nimbus.extension.json +15 -0
- package/connectors/pagerduty/src/server.ts +140 -0
- package/connectors/pipedrive/nimbus.extension.json +15 -0
- package/connectors/pipedrive/src/search-filter.ts +11 -0
- package/connectors/pipedrive/src/server.ts +62 -0
- package/connectors/powerbi/nimbus.extension.json +16 -0
- package/connectors/powerbi/src/search-filter.ts +22 -0
- package/connectors/powerbi/src/server.ts +249 -0
- package/connectors/prefect/nimbus.extension.json +16 -0
- package/connectors/prefect/src/search-filter.ts +40 -0
- package/connectors/prefect/src/server.ts +94 -0
- package/connectors/protonmail/nimbus.extension.json +16 -0
- package/connectors/protonmail/src/mail-core.ts +96 -0
- package/connectors/protonmail/src/server.ts +231 -0
- package/connectors/protonmail/src/tools.ts +45 -0
- package/connectors/raindrop/nimbus.extension.json +15 -0
- package/connectors/raindrop/src/search-filter.ts +20 -0
- package/connectors/raindrop/src/server.ts +107 -0
- package/connectors/ramp/nimbus.extension.json +15 -0
- package/connectors/ramp/src/search-filter.ts +37 -0
- package/connectors/ramp/src/server.ts +109 -0
- package/connectors/readwise/nimbus.extension.json +15 -0
- package/connectors/readwise/src/search-filter.ts +64 -0
- package/connectors/readwise/src/server.ts +92 -0
- package/connectors/sagemaker/nimbus.extension.json +15 -0
- package/connectors/sagemaker/src/server.ts +6 -0
- package/connectors/sagemaker/src/tools.ts +121 -0
- package/connectors/salesforce/nimbus.extension.json +15 -0
- package/connectors/salesforce/src/search-filter.ts +23 -0
- package/connectors/salesforce/src/server.ts +94 -0
- package/connectors/semgrep/nimbus.extension.json +15 -0
- package/connectors/semgrep/src/search-filter.ts +31 -0
- package/connectors/semgrep/src/server.ts +108 -0
- package/connectors/sentry/nimbus.extension.json +15 -0
- package/connectors/sentry/src/server.ts +66 -0
- package/connectors/slack/nimbus.extension.json +15 -0
- package/connectors/slack/src/server.ts +360 -0
- package/connectors/snowflake/nimbus.extension.json +16 -0
- package/connectors/snowflake/src/search-filter.ts +26 -0
- package/connectors/snowflake/src/server.ts +236 -0
- package/connectors/snyk/nimbus.extension.json +15 -0
- package/connectors/snyk/src/search-filter.ts +28 -0
- package/connectors/snyk/src/server.ts +144 -0
- package/connectors/sonarqube/nimbus.extension.json +15 -0
- package/connectors/sonarqube/src/search-filter.ts +28 -0
- package/connectors/sonarqube/src/server.ts +115 -0
- package/connectors/stackoverflow/nimbus.extension.json +15 -0
- package/connectors/stackoverflow/src/search-filter.ts +52 -0
- package/connectors/stackoverflow/src/server.ts +81 -0
- package/connectors/storybook/nimbus.extension.json +16 -0
- package/connectors/storybook/src/server.ts +6 -0
- package/connectors/storybook/src/storybook-parse.ts +67 -0
- package/connectors/storybook/src/tools.ts +68 -0
- package/connectors/stripe/nimbus.extension.json +15 -0
- package/connectors/stripe/src/search-filter.ts +19 -0
- package/connectors/stripe/src/server.ts +65 -0
- package/connectors/superset/nimbus.extension.json +16 -0
- package/connectors/superset/src/search-filter.ts +22 -0
- package/connectors/superset/src/server.ts +119 -0
- package/connectors/tableau/nimbus.extension.json +16 -0
- package/connectors/tableau/src/search-filter.ts +22 -0
- package/connectors/tableau/src/server.ts +248 -0
- package/connectors/teams/nimbus.extension.json +20 -0
- package/connectors/teams/src/bot-send.ts +72 -0
- package/connectors/teams/src/server.ts +257 -0
- package/connectors/testflight/nimbus.extension.json +15 -0
- package/connectors/testflight/src/jwt.ts +27 -0
- package/connectors/testflight/src/search-filter.ts +30 -0
- package/connectors/testflight/src/server.ts +74 -0
- package/connectors/vercel/nimbus.extension.json +15 -0
- package/connectors/vercel/src/search-filter.ts +34 -0
- package/connectors/vercel/src/server.ts +79 -0
- package/connectors/vertex-ai/nimbus.extension.json +15 -0
- package/connectors/vertex-ai/src/server.ts +6 -0
- package/connectors/vertex-ai/src/tools.ts +145 -0
- package/connectors/wiz/nimbus.extension.json +15 -0
- package/connectors/wiz/src/search-filter.ts +50 -0
- package/connectors/wiz/src/server.ts +192 -0
- package/connectors/workday/nimbus.extension.json +19 -0
- package/connectors/workday/src/search-filter.ts +5 -0
- package/connectors/workday/src/server.ts +78 -0
- package/connectors/workday/src/tools.ts +1 -0
- package/connectors/zendesk/nimbus.extension.json +16 -0
- package/connectors/zendesk/src/search-filter.ts +11 -0
- package/connectors/zendesk/src/server.ts +77 -0
- package/connectors/zoom/nimbus.extension.json +15 -0
- package/connectors/zoom/src/encode-meeting-path.ts +5 -0
- package/connectors/zoom/src/recordings-window.ts +34 -0
- package/connectors/zoom/src/search-filter.ts +42 -0
- package/connectors/zoom/src/server.ts +98 -0
- package/connectors/zotero/nimbus.extension.json +15 -0
- package/connectors/zotero/src/search-filter.ts +81 -0
- package/connectors/zotero/src/server.ts +82 -0
- package/package.json +152 -0
- package/shared/atlassian-json-fetch.ts +42 -0
- package/shared/audit-chain.ts +133 -0
- package/shared/connector-mode.ts +60 -0
- package/shared/consent-kit.ts +323 -0
- package/shared/fetch-bearer-json.ts +18 -0
- package/shared/header-safe.ts +28 -0
- package/shared/imap-bodystructure.ts +113 -0
- package/shared/imap-mail-core.ts +57 -0
- package/shared/imap-tool-kit.ts +293 -0
- package/shared/join-api-path.ts +18 -0
- package/shared/kb-markdown.ts +142 -0
- package/shared/mcp-search-tool.ts +37 -0
- package/shared/mcp-tool-kit.ts +35 -0
- package/shared/nimbus-spawn.ts +128 -0
- package/shared/rest-tool-kit.ts +18 -0
- package/shared/run-cli-json.ts +55 -0
- package/shared/run-read-only-mcp-connector.ts +59 -0
- package/shared/safe-cli-arg.ts +55 -0
- package/shared/search-filter.ts +140 -0
- package/shared/strip-trailing-slashes.ts +7 -0
- package/shared/write-scope.ts +58 -0
- package/standalone/src/bin.ts +19 -0
- package/standalone/src/launcher.ts +186 -0
- package/standalone/tsconfig.json +8 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import type { WriteToolRegistrar } from "./consent-kit.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Shared tool-layer helpers for the IMAP/JMAP email connectors (imap, protonmail).
|
|
4
|
+
* Extracted to eliminate byte-identical duplication in tools.ts across those two
|
|
5
|
+
* connectors. FastMail (JMAP) uses a different view shape (id vs uid, no mailbox
|
|
6
|
+
* field) and different arg schemas — its view transformer stays local.
|
|
7
|
+
*
|
|
8
|
+
* Scope: Zod arg schemas, the shared message view transformer, and the shared
|
|
9
|
+
* `envInt` + `previewFromParts` server-layer helpers. Class bodies and
|
|
10
|
+
* connector-specific envelope/meta mappers are deliberately NOT extracted here
|
|
11
|
+
* (they implement different local interfaces and are out of scope per the dedup
|
|
12
|
+
* brief).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
import { headerLine } from "./header-safe.ts";
|
|
18
|
+
import { capPreview, clampLimit } from "./imap-mail-core.ts";
|
|
19
|
+
import { createRegisterSimpleTool, type McpListResult, mcpJsonResult } from "./mcp-tool-kit.ts";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Shared Zod arg schemas (byte-identical across imap + protonmail tools.ts)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const listArgs = z.object({
|
|
26
|
+
mailbox: z.string().min(1).optional(),
|
|
27
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const getArgs = z.object({
|
|
31
|
+
uid: z.number().int().min(1),
|
|
32
|
+
mailbox: z.string().min(1).optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const searchArgs = z.object({
|
|
36
|
+
query: z.string().min(1).max(500),
|
|
37
|
+
mailbox: z.string().min(1).optional(),
|
|
38
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const sendArgs = z.object({
|
|
42
|
+
to: headerLine({ min: 1 }),
|
|
43
|
+
subject: headerLine({ min: 1, max: 998 }),
|
|
44
|
+
body: z.string().max(1_000_000),
|
|
45
|
+
cc: headerLine().optional(),
|
|
46
|
+
bcc: headerLine().optional(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Shared Zod schemas for the IMAP/protonmail tool arg payloads. */
|
|
50
|
+
export const emailToolSchemas = {
|
|
51
|
+
listArgs,
|
|
52
|
+
getArgs,
|
|
53
|
+
searchArgs,
|
|
54
|
+
sendArgs,
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Shared view transformer (imap + protonmail tools.ts — viewMessage)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The minimal shape that `viewEmailMessage` reads from a message meta object.
|
|
63
|
+
* imap's `ImapMessageMeta` and protonmail's `MailMessageMeta` both satisfy this.
|
|
64
|
+
*/
|
|
65
|
+
export interface EmailMessageMeta {
|
|
66
|
+
readonly uid: number;
|
|
67
|
+
readonly mailbox: string;
|
|
68
|
+
readonly uidValidity: string | null;
|
|
69
|
+
readonly envelope: {
|
|
70
|
+
readonly date?: Date | string | null;
|
|
71
|
+
readonly subject?: string | null;
|
|
72
|
+
readonly messageId?: string | null;
|
|
73
|
+
readonly from?: readonly { readonly name?: string; readonly address?: string }[];
|
|
74
|
+
readonly to?: readonly { readonly name?: string; readonly address?: string }[];
|
|
75
|
+
readonly cc?: readonly { readonly name?: string; readonly address?: string }[];
|
|
76
|
+
};
|
|
77
|
+
readonly attachments: readonly {
|
|
78
|
+
readonly filename: string | null;
|
|
79
|
+
readonly sizeBytes: number | null;
|
|
80
|
+
readonly mimeType: string | null;
|
|
81
|
+
}[];
|
|
82
|
+
readonly preview: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Reduce an {@link EmailMessageMeta} to a JSON-safe view. Returns HEADERS,
|
|
87
|
+
* attachment METADATA (filename/size/mimetype), and the capped preview — never
|
|
88
|
+
* attachment bytes or a full body. Addresses are formatted by the caller-supplied
|
|
89
|
+
* `formatAddr` so each connector can use its own local `formatAddress` helper.
|
|
90
|
+
*/
|
|
91
|
+
export function viewEmailMessage(
|
|
92
|
+
m: EmailMessageMeta,
|
|
93
|
+
formatAddr: (a: { readonly name?: string; readonly address?: string }) => string,
|
|
94
|
+
): Record<string, unknown> {
|
|
95
|
+
const env = m.envelope;
|
|
96
|
+
return {
|
|
97
|
+
uid: m.uid,
|
|
98
|
+
mailbox: m.mailbox,
|
|
99
|
+
uidValidity: m.uidValidity,
|
|
100
|
+
messageId: env.messageId ?? null,
|
|
101
|
+
subject: env.subject ?? null,
|
|
102
|
+
date: env.date instanceof Date ? env.date.toISOString() : (env.date ?? null),
|
|
103
|
+
from: (env.from ?? []).map(formatAddr),
|
|
104
|
+
to: (env.to ?? []).map(formatAddr),
|
|
105
|
+
cc: (env.cc ?? []).map(formatAddr),
|
|
106
|
+
attachments: m.attachments.map((a) => ({
|
|
107
|
+
filename: a.filename,
|
|
108
|
+
sizeBytes: a.sizeBytes,
|
|
109
|
+
mimeType: a.mimeType,
|
|
110
|
+
})),
|
|
111
|
+
preview: m.preview,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Shared server-layer helpers (server.ts — envInt, previewFromParts)
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Parse an environment variable as an integer in the range [1, 65535]. Returns
|
|
121
|
+
* `fallback` if the variable is absent, empty, non-numeric, or out of range.
|
|
122
|
+
* Byte-identical to the `envInt` helper in imap/server.ts and protonmail/server.ts.
|
|
123
|
+
*/
|
|
124
|
+
export function envInt(name: string, fallback: number): number {
|
|
125
|
+
const raw = process.env[name]?.trim();
|
|
126
|
+
if (raw === undefined || raw === "") {
|
|
127
|
+
return fallback;
|
|
128
|
+
}
|
|
129
|
+
const n = Number(raw);
|
|
130
|
+
return Number.isFinite(n) && n > 0 && n <= 65535 ? Math.trunc(n) : fallback;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract and cap a plain-text body preview from the map of IMAP body parts
|
|
135
|
+
* fetched by imapflow. Looks up part keys `partKey → "1" → "TEXT"` in order.
|
|
136
|
+
* Returns an empty string when nothing is found. Byte-equivalent to the
|
|
137
|
+
* `previewFromParts` helpers in imap/server.ts and protonmail/server.ts.
|
|
138
|
+
*/
|
|
139
|
+
export function previewFromParts(parts: Map<string, Buffer> | undefined, partKey: string): string {
|
|
140
|
+
if (parts === undefined) {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
const buf = parts.get(partKey) ?? parts.get("1") ?? parts.get("TEXT");
|
|
144
|
+
if (buf === undefined) {
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
147
|
+
return capPreview(buf.toString("utf8"));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Shared tool-registration factory (imap + protonmail tools.ts — the 4 blocks)
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
/** Read surface the email tool factory depends on (imap's ImapClient / protonmail's MailClient both satisfy it). */
|
|
155
|
+
export interface EmailReadClient {
|
|
156
|
+
list(options: { mailbox?: string; limit?: number }): Promise<EmailMessageMeta[]>;
|
|
157
|
+
get(uid: number, mailbox?: string): Promise<EmailMessageMeta | null>;
|
|
158
|
+
search(options: { query: string; mailbox?: string; limit?: number }): Promise<EmailMessageMeta[]>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Send surface the email tool factory depends on (imap's/protonmail's SmtpMailer satisfy it). */
|
|
162
|
+
export interface EmailSendMailer {
|
|
163
|
+
send(input: { to: string; subject: string; body: string; cc?: string; bcc?: string }): Promise<{
|
|
164
|
+
messageId: string | null;
|
|
165
|
+
accepted: readonly string[];
|
|
166
|
+
rejected: readonly string[];
|
|
167
|
+
}>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The four tool descriptions, supplied verbatim by each connector. */
|
|
171
|
+
export interface EmailToolDescriptions {
|
|
172
|
+
readonly list: string;
|
|
173
|
+
readonly get: string;
|
|
174
|
+
readonly search: string;
|
|
175
|
+
readonly send: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Register the 4 email tools (`<prefix>_list|_get|_search|_mail_send`) onto an
|
|
180
|
+
* MCP server. The IMAP client + SMTP mailer are injected so tests exercise the
|
|
181
|
+
* tool surface without opening sockets. Byte-equivalent to the hand-written
|
|
182
|
+
* `registerImapTools` / `registerProtonmailTools` bodies it replaces — the tool
|
|
183
|
+
* names (via `toolPrefix`) and descriptions are the only per-connector inputs.
|
|
184
|
+
*/
|
|
185
|
+
export function registerEmailConnectorTools(opts: {
|
|
186
|
+
server: { tool: (...args: never) => unknown };
|
|
187
|
+
toolPrefix: string;
|
|
188
|
+
descriptions: EmailToolDescriptions;
|
|
189
|
+
client: EmailReadClient;
|
|
190
|
+
mailer: EmailSendMailer;
|
|
191
|
+
formatAddr: (a: { readonly name?: string; readonly address?: string }) => string;
|
|
192
|
+
/**
|
|
193
|
+
* The connector's write registrar, for `<prefix>_mail_send`.
|
|
194
|
+
*
|
|
195
|
+
* Passed IN rather than built here on purpose: `standaloneEligibility` reads a connector's own
|
|
196
|
+
* `server.ts`/`tools.ts` to decide whether its mutations are gated, and a registrar constructed
|
|
197
|
+
* inside this shared module would be invisible there — the connector would be refused despite
|
|
198
|
+
* being hardened. Requiring it also means a new email connector cannot forget it.
|
|
199
|
+
*/
|
|
200
|
+
registerWriteTool: WriteToolRegistrar;
|
|
201
|
+
}): void {
|
|
202
|
+
const { server, toolPrefix, descriptions, client, mailer, formatAddr, registerWriteTool } = opts;
|
|
203
|
+
const registerSimpleTool = createRegisterSimpleTool(server);
|
|
204
|
+
const { listArgs, getArgs, searchArgs, sendArgs } = emailToolSchemas;
|
|
205
|
+
|
|
206
|
+
registerSimpleTool(
|
|
207
|
+
`${toolPrefix}_list`,
|
|
208
|
+
descriptions.list,
|
|
209
|
+
listArgs.shape,
|
|
210
|
+
async (args: unknown): Promise<McpListResult> => {
|
|
211
|
+
const parsed = listArgs.safeParse(args);
|
|
212
|
+
if (!parsed.success) {
|
|
213
|
+
throw new Error(parsed.error.message);
|
|
214
|
+
}
|
|
215
|
+
const o: { mailbox?: string; limit?: number } = {};
|
|
216
|
+
if (parsed.data.mailbox !== undefined) {
|
|
217
|
+
o.mailbox = parsed.data.mailbox;
|
|
218
|
+
}
|
|
219
|
+
o.limit = clampLimit(parsed.data.limit);
|
|
220
|
+
const items = await client.list(o);
|
|
221
|
+
return mcpJsonResult({ items: items.map((m) => viewEmailMessage(m, formatAddr)) });
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
registerSimpleTool(
|
|
226
|
+
`${toolPrefix}_get`,
|
|
227
|
+
descriptions.get,
|
|
228
|
+
getArgs.shape,
|
|
229
|
+
async (args: unknown): Promise<McpListResult> => {
|
|
230
|
+
const parsed = getArgs.safeParse(args);
|
|
231
|
+
if (!parsed.success) {
|
|
232
|
+
throw new Error(parsed.error.message);
|
|
233
|
+
}
|
|
234
|
+
const m = await client.get(parsed.data.uid, parsed.data.mailbox);
|
|
235
|
+
return mcpJsonResult(m === null ? { item: null } : { item: viewEmailMessage(m, formatAddr) });
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
registerSimpleTool(
|
|
240
|
+
`${toolPrefix}_search`,
|
|
241
|
+
descriptions.search,
|
|
242
|
+
searchArgs.shape,
|
|
243
|
+
async (args: unknown): Promise<McpListResult> => {
|
|
244
|
+
const parsed = searchArgs.safeParse(args);
|
|
245
|
+
if (!parsed.success) {
|
|
246
|
+
throw new Error(parsed.error.message);
|
|
247
|
+
}
|
|
248
|
+
const o: { query: string; mailbox?: string; limit?: number } = {
|
|
249
|
+
query: parsed.data.query,
|
|
250
|
+
limit: clampLimit(parsed.data.limit),
|
|
251
|
+
};
|
|
252
|
+
if (parsed.data.mailbox !== undefined) {
|
|
253
|
+
o.mailbox = parsed.data.mailbox;
|
|
254
|
+
}
|
|
255
|
+
const items = await client.search(o);
|
|
256
|
+
return mcpJsonResult({ matches: items.map((m) => viewEmailMessage(m, formatAddr)) });
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
registerWriteTool(
|
|
261
|
+
`${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
|
+
},
|
|
270
|
+
descriptions.send,
|
|
271
|
+
sendArgs,
|
|
272
|
+
async (parsedData): Promise<McpListResult> => {
|
|
273
|
+
const parsed = { success: true as const, data: parsedData };
|
|
274
|
+
const input: { to: string; subject: string; body: string; cc?: string; bcc?: string } = {
|
|
275
|
+
to: parsed.data.to,
|
|
276
|
+
subject: parsed.data.subject,
|
|
277
|
+
body: parsed.data.body,
|
|
278
|
+
};
|
|
279
|
+
if (parsed.data.cc !== undefined && parsed.data.cc !== "") {
|
|
280
|
+
input.cc = parsed.data.cc;
|
|
281
|
+
}
|
|
282
|
+
if (parsed.data.bcc !== undefined && parsed.data.bcc !== "") {
|
|
283
|
+
input.bcc = parsed.data.bcc;
|
|
284
|
+
}
|
|
285
|
+
const res = await mailer.send(input);
|
|
286
|
+
return mcpJsonResult({
|
|
287
|
+
messageId: res.messageId,
|
|
288
|
+
accepted: res.accepted,
|
|
289
|
+
rejected: res.rejected,
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Join `path` onto `baseUrl`, passing an already-absolute URL straight through
|
|
3
|
+
* (pagination cursors commonly arrive absolute).
|
|
4
|
+
*
|
|
5
|
+
* The absolute test is the full `http://` / `https://` scheme, not a bare
|
|
6
|
+
* `http` prefix: a RELATIVE path that merely starts with those four letters —
|
|
7
|
+
* `httpbin/status`, or an `http-headers` endpoint — was returned unjoined,
|
|
8
|
+
* silently dropping the base URL and issuing the request against a relative
|
|
9
|
+
* path. Narrow enough that it would have surfaced in production rather than in
|
|
10
|
+
* review.
|
|
11
|
+
*/
|
|
12
|
+
export function joinApiPath(baseUrl: string, path: string): string {
|
|
13
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
14
|
+
return path;
|
|
15
|
+
}
|
|
16
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
17
|
+
return `${baseUrl}${suffix}`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, robust converter from the SIMPLE markdown the tribal answer-synthesizer emits
|
|
3
|
+
* (plain paragraphs + `-` bulleted lists; no headers/tables/code/HTML) into Notion blocks and
|
|
4
|
+
* Confluence storage HTML, plus a Sources section built from citations. The body parser is a
|
|
5
|
+
* line-walker that NEVER throws on unexpected markup: any non-empty, non-bullet line becomes a
|
|
6
|
+
* paragraph (so a stray `#` or ``` line degrades to literal paragraph text, not an error).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface KbCitation {
|
|
10
|
+
itemId: string;
|
|
11
|
+
channelId: string;
|
|
12
|
+
url: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type KbLine = { kind: "bullet" | "paragraph"; text: string };
|
|
16
|
+
|
|
17
|
+
/** Classify each non-blank line as a bullet (`- ` prefix) or a paragraph (everything else). */
|
|
18
|
+
export function parseSimpleMarkdown(md: string): KbLine[] {
|
|
19
|
+
const out: KbLine[] = [];
|
|
20
|
+
for (const raw of md.split("\n")) {
|
|
21
|
+
const line = raw.trim();
|
|
22
|
+
if (line === "") continue;
|
|
23
|
+
const bullet = /^[-*]\s+(\S.*)?$/.exec(line);
|
|
24
|
+
if (bullet === null) {
|
|
25
|
+
out.push({ kind: "paragraph", text: line });
|
|
26
|
+
} else {
|
|
27
|
+
out.push({ kind: "bullet", text: bullet[1] ?? "" });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function escapeHtml(s: string): string {
|
|
34
|
+
return s
|
|
35
|
+
.replaceAll("&", "&")
|
|
36
|
+
.replaceAll("<", "<")
|
|
37
|
+
.replaceAll(">", ">")
|
|
38
|
+
.replaceAll('"', """)
|
|
39
|
+
.replaceAll("'", "'");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function citationLabel(c: KbCitation): string {
|
|
43
|
+
return `${c.channelId} · ${c.itemId}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function notionRichText(content: string, url?: string | null): Record<string, unknown>[] {
|
|
47
|
+
const text: Record<string, unknown> = { content: content.slice(0, 2000) };
|
|
48
|
+
if (url !== undefined && url !== null && url !== "") text["link"] = { url };
|
|
49
|
+
return [{ type: "text", text }];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Body → Notion blocks, then a "Sources:" paragraph + one bulleted link per citation. */
|
|
53
|
+
export function markdownToNotionBlocks(
|
|
54
|
+
md: string,
|
|
55
|
+
citations: readonly KbCitation[],
|
|
56
|
+
): Record<string, unknown>[] {
|
|
57
|
+
const blocks: Record<string, unknown>[] = [];
|
|
58
|
+
for (const line of parseSimpleMarkdown(md)) {
|
|
59
|
+
if (line.kind === "bullet") {
|
|
60
|
+
blocks.push({
|
|
61
|
+
object: "block",
|
|
62
|
+
type: "bulleted_list_item",
|
|
63
|
+
bulleted_list_item: { rich_text: notionRichText(line.text) },
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
blocks.push({
|
|
67
|
+
object: "block",
|
|
68
|
+
type: "paragraph",
|
|
69
|
+
paragraph: { rich_text: notionRichText(line.text) },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (citations.length > 0) {
|
|
74
|
+
blocks.push({
|
|
75
|
+
object: "block",
|
|
76
|
+
type: "paragraph",
|
|
77
|
+
paragraph: { rich_text: notionRichText("Sources:") },
|
|
78
|
+
});
|
|
79
|
+
for (const c of citations) {
|
|
80
|
+
blocks.push({
|
|
81
|
+
object: "block",
|
|
82
|
+
type: "bulleted_list_item",
|
|
83
|
+
bulleted_list_item: { rich_text: notionRichText(citationLabel(c), c.url) },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return blocks;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Body → Confluence storage HTML, then a Sources `<p>` + `<ul>` of citation links. */
|
|
91
|
+
export function markdownToConfluenceStorage(md: string, citations: readonly KbCitation[]): string {
|
|
92
|
+
const parts: string[] = [];
|
|
93
|
+
let bulletBuffer: string[] = [];
|
|
94
|
+
const flushBullets = (): void => {
|
|
95
|
+
if (bulletBuffer.length === 0) return;
|
|
96
|
+
const lis = bulletBuffer.map((b) => `<li>${b}</li>`).join("");
|
|
97
|
+
parts.push(`<ul>${lis}</ul>`);
|
|
98
|
+
bulletBuffer = [];
|
|
99
|
+
};
|
|
100
|
+
for (const line of parseSimpleMarkdown(md)) {
|
|
101
|
+
if (line.kind === "bullet") {
|
|
102
|
+
bulletBuffer.push(escapeHtml(line.text));
|
|
103
|
+
} else {
|
|
104
|
+
flushBullets();
|
|
105
|
+
parts.push(`<p>${escapeHtml(line.text)}</p>`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
flushBullets();
|
|
109
|
+
if (citations.length > 0) {
|
|
110
|
+
parts.push("<p>Sources:</p>");
|
|
111
|
+
const items = citations.map((c) => {
|
|
112
|
+
const label = escapeHtml(citationLabel(c));
|
|
113
|
+
return c.url !== null && c.url !== ""
|
|
114
|
+
? `<li><a href="${escapeHtml(c.url)}">${label}</a></li>`
|
|
115
|
+
: `<li>${label}</li>`;
|
|
116
|
+
});
|
|
117
|
+
parts.push(`<ul>${items.join("")}</ul>`);
|
|
118
|
+
}
|
|
119
|
+
return parts.join("");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Parse + validate a citations JSON string into KbCitation[]; tolerant, never throws on shape. */
|
|
123
|
+
export function parseCitationsJson(raw: string): KbCitation[] {
|
|
124
|
+
let parsed: unknown;
|
|
125
|
+
try {
|
|
126
|
+
parsed = JSON.parse(raw) as unknown;
|
|
127
|
+
} catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(parsed)) return [];
|
|
131
|
+
const out: KbCitation[] = [];
|
|
132
|
+
for (const v of parsed) {
|
|
133
|
+
if (v === null || typeof v !== "object") continue;
|
|
134
|
+
const rec = v as Record<string, unknown>;
|
|
135
|
+
const itemId = rec["itemId"];
|
|
136
|
+
const channelId = rec["channelId"];
|
|
137
|
+
const url = rec["url"];
|
|
138
|
+
if (typeof itemId !== "string" || typeof channelId !== "string") continue;
|
|
139
|
+
out.push({ itemId, channelId, url: typeof url === "string" ? url : null });
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type McpListResult, mcpJsonResult, type ZodObjectSchema } from "./mcp-tool-kit.ts";
|
|
3
|
+
import type { SearchMatchOptions } from "./search-filter.ts";
|
|
4
|
+
|
|
5
|
+
/** A `makeQueryFilter(...)` result — the shape every connector search filter has. */
|
|
6
|
+
export type SearchFilter = (
|
|
7
|
+
rows: readonly unknown[],
|
|
8
|
+
opts: SearchMatchOptions,
|
|
9
|
+
) => readonly unknown[];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build the shared connector search input schema. `query` is always a non-empty
|
|
13
|
+
* string; `maxLimit` is the per-connector cap (varies: 100/200/500/50/1000/2000)
|
|
14
|
+
* and MUST be passed verbatim from each call site so behavior is unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export function searchToolInputSchema(maxLimit = 100): ZodObjectSchema<SearchMatchOptions> {
|
|
17
|
+
return z.object({
|
|
18
|
+
query: z.string().min(1),
|
|
19
|
+
limit: z.number().int().min(1).max(maxLimit).optional(),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the `{ matches }` envelope: filter the rows when they are an array, else
|
|
25
|
+
* return an empty match set. Verbatim equivalent of the per-connector tail
|
|
26
|
+
* `const matches = Array.isArray(X) ? filter(X, { query, limit }) : []; return jsonResult({ matches })`.
|
|
27
|
+
* `rows` stays `unknown` because external payloads are untyped at the boundary
|
|
28
|
+
* (Non-Negotiable #7).
|
|
29
|
+
*/
|
|
30
|
+
export function matchesResult(
|
|
31
|
+
rows: unknown,
|
|
32
|
+
filter: SearchFilter,
|
|
33
|
+
opts: SearchMatchOptions,
|
|
34
|
+
): McpListResult {
|
|
35
|
+
const matches = Array.isArray(rows) ? filter(rows, opts) : [];
|
|
36
|
+
return mcpJsonResult({ matches });
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-tool-kit — re-export of the MCP connector helpers now owned by
|
|
3
|
+
* `@nimbus-dev/sdk/connector-kit`.
|
|
4
|
+
*
|
|
5
|
+
* These helpers originated here and were upstreamed to the SDK in 1.11.0. The
|
|
6
|
+
* SDK is now the single owner; this module stays as the connector-facing import
|
|
7
|
+
* path so the ~99 connectors keep their existing relative imports.
|
|
8
|
+
*
|
|
9
|
+
* Named re-exports, deliberately not `export *`: a star would also re-export the
|
|
10
|
+
* sibling kits' symbols (fetch-bearer-json, rest-tool-kit) through this path,
|
|
11
|
+
* widening the surface each connector sees and making a two-module import
|
|
12
|
+
* ambiguous. Keep this list matched to the symbols this file used to define.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
HttpJsonBodyResponse,
|
|
17
|
+
HttpTextResponse,
|
|
18
|
+
McpListResult,
|
|
19
|
+
RegisterSimpleToolFn,
|
|
20
|
+
ZodObjectSchema,
|
|
21
|
+
} from "@nimbus-dev/sdk/connector-kit";
|
|
22
|
+
export {
|
|
23
|
+
createRegisterSimpleTool,
|
|
24
|
+
createZodToolRegistrar,
|
|
25
|
+
encodeBasicAuthHeader,
|
|
26
|
+
fetchWithTimeout,
|
|
27
|
+
mcpJsonResult,
|
|
28
|
+
mcpJsonResultFromTextIfOk,
|
|
29
|
+
mcpJsonResultIfOk,
|
|
30
|
+
parseJsonTextIfOk,
|
|
31
|
+
putOptionalBoolean,
|
|
32
|
+
putOptionalNonEmptyString,
|
|
33
|
+
registerZodTool,
|
|
34
|
+
requireProcessEnv,
|
|
35
|
+
} from "@nimbus-dev/sdk/connector-kit";
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export type SpawnResult = { code: number; stdout: string; stderr: string };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Node implementation. Exported so it can be tested directly: the suite runs under Bun, so
|
|
7
|
+
* `nimbusSpawn` would otherwise never exercise this branch and it would ship unverified.
|
|
8
|
+
*
|
|
9
|
+
* Two Node APIs are deliberately NOT used, and the reasons are not obvious:
|
|
10
|
+
*
|
|
11
|
+
* - `spawnSync` blocks the event loop. In a stdio MCP server that must keep answering JSON-RPC —
|
|
12
|
+
* including an in-flight `elicitation/create` round-trip — a synchronous spawn deadlocks the
|
|
13
|
+
* consent gate against itself.
|
|
14
|
+
* - `execFile` caps captured output at a 1 MB `maxBuffer` by default and errors past it. The
|
|
15
|
+
* `Bun.spawn` path reads stdout uncapped, and `aws logs` / `gcloud logging` JSON routinely
|
|
16
|
+
* exceeds 1 MB, so `execFile` would be a silent-truncation regression dressed up as a
|
|
17
|
+
* portability fix.
|
|
18
|
+
*
|
|
19
|
+
* Output is accumulated as raw `Buffer`s and decoded ONCE at the end. Decoding each chunk with
|
|
20
|
+
* `chunk.toString("utf8")` corrupts any multi-byte character that straddles a chunk boundary, and
|
|
21
|
+
* chunk boundaries are a function of pipe timing — so it fails intermittently, on non-ASCII data,
|
|
22
|
+
* in production.
|
|
23
|
+
*/
|
|
24
|
+
export function spawnViaNode(
|
|
25
|
+
command: readonly string[],
|
|
26
|
+
env: Record<string, string | undefined>,
|
|
27
|
+
): Promise<SpawnResult> {
|
|
28
|
+
const [bin, ...args] = command;
|
|
29
|
+
if (bin === undefined) {
|
|
30
|
+
return Promise.resolve({ code: 1, stdout: "", stderr: "empty command" });
|
|
31
|
+
}
|
|
32
|
+
return new Promise((resolveP) => {
|
|
33
|
+
const child = spawn(bin, args, { env: { ...process.env, ...env } });
|
|
34
|
+
const outChunks: Buffer[] = [];
|
|
35
|
+
const errChunks: Buffer[] = [];
|
|
36
|
+
child.stdout.on("data", (c: Buffer) => {
|
|
37
|
+
outChunks.push(c);
|
|
38
|
+
});
|
|
39
|
+
child.stderr.on("data", (c: Buffer) => {
|
|
40
|
+
errChunks.push(c);
|
|
41
|
+
});
|
|
42
|
+
const decode = (): { stdout: string; stderr: string } => ({
|
|
43
|
+
stdout: Buffer.concat(outChunks).toString("utf8"),
|
|
44
|
+
stderr: Buffer.concat(errChunks).toString("utf8"),
|
|
45
|
+
});
|
|
46
|
+
child.on("error", (e: Error) => {
|
|
47
|
+
const d = decode();
|
|
48
|
+
resolveP({ code: 1, stdout: d.stdout, stderr: `${d.stderr}${e.message}` });
|
|
49
|
+
});
|
|
50
|
+
child.on("close", (code: number | null) => {
|
|
51
|
+
resolveP({ code: code ?? 1, ...decode() });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Bun implementation. Draining the whole stream decodes multi-byte characters correctly. */
|
|
57
|
+
export async function spawnViaBun(
|
|
58
|
+
command: readonly string[],
|
|
59
|
+
env: Record<string, string | undefined>,
|
|
60
|
+
): Promise<SpawnResult> {
|
|
61
|
+
if (command.length === 0) {
|
|
62
|
+
return { code: 1, stdout: "", stderr: "empty command" };
|
|
63
|
+
}
|
|
64
|
+
// `Bun.spawn` THROWS synchronously when the binary does not exist, where `child_process` emits
|
|
65
|
+
// an "error" event instead. Both branches must honour the same contract — resolve with a
|
|
66
|
+
// non-zero code, never reject — or a caller's behaviour would depend on the runtime.
|
|
67
|
+
try {
|
|
68
|
+
const proc = Bun.spawn([...command], {
|
|
69
|
+
env: { ...process.env, ...env },
|
|
70
|
+
stdout: "pipe",
|
|
71
|
+
stderr: "pipe",
|
|
72
|
+
});
|
|
73
|
+
const code = await proc.exited;
|
|
74
|
+
const stdout = await new Response(proc.stdout).text();
|
|
75
|
+
const stderr = await new Response(proc.stderr).text();
|
|
76
|
+
return { code, stdout, stderr };
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return { code: 1, stdout: "", stderr: e instanceof Error ? e.message : String(e) };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether `Bun.spawn` is usable on the given global.
|
|
84
|
+
*
|
|
85
|
+
* Takes the global as a PARAMETER rather than reading `globalThis` directly, because Bun defines
|
|
86
|
+
* its global as non-writable AND non-configurable — it cannot be stubbed or redefined, so the
|
|
87
|
+
* "absent" side is otherwise unreachable from a suite that runs under Bun. That side is the one
|
|
88
|
+
* the standalone npx artifact always takes, so it must be exercisable.
|
|
89
|
+
*/
|
|
90
|
+
export function detectBunSpawn(g: unknown = globalThis): boolean {
|
|
91
|
+
const bun = (g as { Bun?: { spawn?: unknown } }).Bun;
|
|
92
|
+
return typeof bun?.spawn === "function";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The shape both implementations share, and what `selectSpawnImpl` hands back. */
|
|
96
|
+
export type SpawnImpl = (
|
|
97
|
+
command: readonly string[],
|
|
98
|
+
env: Record<string, string | undefined>,
|
|
99
|
+
) => Promise<SpawnResult>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Which implementation a given global calls for.
|
|
103
|
+
*
|
|
104
|
+
* Returning the FUNCTION rather than taking a `useBun` flag is deliberate: a boolean selector
|
|
105
|
+
* argument makes one entry point mean two things, and the two implementations are already exported
|
|
106
|
+
* as the two things it selected between. Injecting the global — for the same
|
|
107
|
+
* non-writable/non-configurable reason `detectBunSpawn` takes one — is what keeps the Node side
|
|
108
|
+
* reachable from a suite that runs under Bun.
|
|
109
|
+
*/
|
|
110
|
+
export function selectSpawnImpl(g: unknown = globalThis): SpawnImpl {
|
|
111
|
+
return detectBunSpawn(g) ? spawnViaBun : spawnViaNode;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Spawn a CLI and collect its output, on Bun or Node.
|
|
116
|
+
*
|
|
117
|
+
* Detection happens at CALL time, not module load, so a test that swaps `Bun.spawn` is still
|
|
118
|
+
* honoured. That is not incidental: `cloudwatch/test/tools.test.ts` stubs `Bun.spawn` globally, and
|
|
119
|
+
* routing unconditionally through Node made it spawn a real `aws` and hang the suite.
|
|
120
|
+
*
|
|
121
|
+
* Never rejects: a spawn failure resolves with a non-zero code, matching the previous behaviour.
|
|
122
|
+
*/
|
|
123
|
+
export function nimbusSpawn(
|
|
124
|
+
command: readonly string[],
|
|
125
|
+
env: Record<string, string | undefined>,
|
|
126
|
+
): Promise<SpawnResult> {
|
|
127
|
+
return selectSpawnImpl()(command, env);
|
|
128
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rest-tool-kit — re-export of the shared REST fetch wrapper for Bearer-auth
|
|
3
|
+
* connectors, now owned by `@nimbus-dev/sdk/connector-kit`.
|
|
4
|
+
*
|
|
5
|
+
* These helpers originated here and were upstreamed to the SDK in 1.11.0. The
|
|
6
|
+
* SDK is now the single owner; this module stays as the connector-facing import
|
|
7
|
+
* path so the ~99 connectors keep their existing relative imports.
|
|
8
|
+
*
|
|
9
|
+
* Named re-exports, deliberately not `export *` — see the note in
|
|
10
|
+
* `mcp-tool-kit.ts`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
RestFetcherConfig,
|
|
15
|
+
RestFetchResult,
|
|
16
|
+
RestToolRegistrar,
|
|
17
|
+
} from "@nimbus-dev/sdk/connector-kit";
|
|
18
|
+
export { makeRestFetcher, makeRestToolRegistrar } from "@nimbus-dev/sdk/connector-kit";
|