@nimbus-dev/connectors 0.2.3 → 0.2.4

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 (35) hide show
  1. package/connectors/apple/src/server.ts +5 -1
  2. package/connectors/aws/src/server.ts +5 -1
  3. package/connectors/azure/src/server.ts +5 -1
  4. package/connectors/bitbucket/src/server.ts +5 -1
  5. package/connectors/circleci/src/server.ts +5 -1
  6. package/connectors/confluence/src/server.ts +5 -1
  7. package/connectors/fastmail/src/server.ts +5 -1
  8. package/connectors/gcp/src/server.ts +5 -1
  9. package/connectors/github/src/server.ts +2 -0
  10. package/connectors/github-actions/src/server.ts +5 -1
  11. package/connectors/gitlab/src/server.ts +5 -1
  12. package/connectors/gmail/src/server.ts +5 -1
  13. package/connectors/google-drive/src/server.ts +5 -1
  14. package/connectors/iac/src/server.ts +5 -1
  15. package/connectors/iac/src/tools.ts +77 -20
  16. package/connectors/imap/src/server.ts +5 -1
  17. package/connectors/jenkins/src/server.ts +5 -1
  18. package/connectors/jira/src/server.ts +5 -1
  19. package/connectors/kubernetes/src/server.ts +5 -1
  20. package/connectors/linear/src/server.ts +5 -1
  21. package/connectors/notion/src/server.ts +5 -1
  22. package/connectors/obsidian/src/server.ts +5 -1
  23. package/connectors/onedrive/src/server.ts +5 -1
  24. package/connectors/outlook/src/server.ts +5 -1
  25. package/connectors/pagerduty/src/server.ts +5 -1
  26. package/connectors/protonmail/src/server.ts +5 -1
  27. package/connectors/slack/src/server.ts +5 -1
  28. package/connectors/teams/src/server.ts +5 -1
  29. package/package.json +1 -1
  30. package/shared/cli-json-kit.ts +1 -13
  31. package/shared/consent-kit.ts +39 -17
  32. package/shared/nimbus-spawn.ts +31 -1
  33. package/shared/run-read-only-mcp-connector.ts +7 -1
  34. package/shared/safe-cli-arg.ts +0 -20
  35. package/shared/windows-batch-args.ts +9 -8
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
15
16
  import { requireProcessEnv } from "../../../shared/mcp-tool-kit.ts";
16
17
  import {
17
18
  createAppleCalDavClient,
@@ -39,7 +40,10 @@ try {
39
40
  // non-fatal; listEvents/list tools will re-discover on demand
40
41
  }
41
42
 
42
- const server = new McpServer({ name: "nimbus-apple", version: "0.1.0" });
43
+ const server = new McpServer(
44
+ { name: "nimbus-apple", version: "0.1.0" },
45
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
46
+ );
43
47
 
44
48
  registerAppleTools(server, {
45
49
  client: createAppleImapClient(email, appPw),
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerAwsTools } from "./tools.ts";
4
5
 
5
- const mcp = new McpServer({ name: "nimbus-aws", version: "0.1.0" });
6
+ const mcp = new McpServer(
7
+ { name: "nimbus-aws", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerAwsTools(mcp);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerAzureTools } from "./tools.ts";
4
5
 
5
- const mcp = new McpServer({ name: "nimbus-azure", version: "0.1.0" });
6
+ const mcp = new McpServer(
7
+ { name: "nimbus-azure", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerAzureTools(mcp);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerBitbucketTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-bitbucket", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-bitbucket", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerBitbucketTools(server);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerCircleciTools } from "./tools.ts";
4
5
 
5
- const mcp = new McpServer({ name: "nimbus-circleci", version: "0.1.0" });
6
+ const mcp = new McpServer(
7
+ { name: "nimbus-circleci", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerCircleciTools(mcp);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerConfluenceTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-confluence", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-confluence", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerConfluenceTools(server);
8
12
 
@@ -1,9 +1,13 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { createFetchJmapClient } from "./jmap-client.ts";
4
5
  import { registerFastmailTools } from "./tools.ts";
5
6
 
6
- const server = new McpServer({ name: "nimbus-fastmail", version: "0.1.0" });
7
+ const server = new McpServer(
8
+ { name: "nimbus-fastmail", version: "0.1.0" },
9
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
10
+ );
7
11
  registerFastmailTools(server, createFetchJmapClient());
8
12
 
9
13
  await server.connect(new StdioServerTransport());
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerGcpTools } from "./tools.ts";
4
5
 
5
- const mcp = new McpServer({ name: "nimbus-gcp", version: "0.1.0" });
6
+ const mcp = new McpServer(
7
+ { name: "nimbus-gcp", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerGcpTools(mcp);
8
12
 
@@ -1,10 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerGithubTools } from "./tools.ts";
4
5
 
5
6
  const server = new McpServer(
6
7
  { name: "nimbus-github", version: "0.1.0" },
7
8
  {
9
+ capabilities: CONSENT_SERVER_CAPABILITIES,
8
10
  // Machine-readable security tiering, so a client can surface it rather than relying on a
9
11
  // human having read the NOTICE file.
10
12
  instructions:
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerGithubActionsTools } from "./tools.ts";
4
5
 
5
- const mcp = new McpServer({ name: "nimbus-github-actions", version: "0.1.0" });
6
+ const mcp = new McpServer(
7
+ { name: "nimbus-github-actions", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerGithubActionsTools(mcp);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerGitlabTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-gitlab", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-gitlab", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerGitlabTools(server);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerGmailTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-gmail", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-gmail", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerGmailTools(server);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerGoogleDriveTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-google-drive", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-google-drive", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerGoogleDriveTools(server);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerIacTools } from "./tools.ts";
4
5
 
5
- const mcp = new McpServer({ name: "nimbus-iac", version: "0.1.0" });
6
+ const mcp = new McpServer(
7
+ { name: "nimbus-iac", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerIacTools(mcp);
8
12
 
@@ -1,5 +1,8 @@
1
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { z } from "zod";
2
- import { awsCliArg, awsCliDocument, cliArg } from "../../../shared/cli-json-kit.ts";
5
+ import { awsCliArg, cliArg } from "../../../shared/cli-json-kit.ts";
3
6
  import type { ConsentServer } from "../../../shared/consent-kit.ts";
4
7
  import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
5
8
  import {
@@ -9,6 +12,49 @@ import {
9
12
  } from "../../../shared/mcp-tool-kit.ts";
10
13
  import { runCliOkThrowing } from "../../../shared/run-cli-json.ts";
11
14
 
15
+ /**
16
+ * The most a template may be when it is deployed without an S3 bucket, which this tool does not
17
+ * take: `aws cloudformation deploy` refuses a larger one itself, and only after the prompt.
18
+ */
19
+ const TEMPLATE_MAX_BYTES = 51_200;
20
+
21
+ /**
22
+ * A CloudFormation template. It reaches the AWS CLI as a file, never as an argument (see
23
+ * {@link withTemplateFile}), so none of the argument rules apply to it: a YAML template may well
24
+ * start with `---`. Its size is refused here, in the schema, because no consent can lift it: a
25
+ * larger template would be put to the human, spend a unit of write budget, and then fail. The
26
+ * character bound is the one a client is shown; no string over it can be within the byte bound.
27
+ */
28
+ const templateDocument = z
29
+ .string()
30
+ .min(1)
31
+ .max(TEMPLATE_MAX_BYTES)
32
+ .refine((t) => new TextEncoder().encode(t).length <= TEMPLATE_MAX_BYTES, {
33
+ message: `must be at most ${String(TEMPLATE_MAX_BYTES)} bytes as UTF-8, the most aws cloudformation deploy takes without an S3 bucket`,
34
+ });
35
+
36
+ /**
37
+ * Run `body` with `template` written to a file of its own, removed afterwards however `body` ends.
38
+ *
39
+ * `aws cloudformation deploy` takes a template only as `--template-file`, and passing one as an
40
+ * argument would not work anyway: a template can run past the 32,767 characters a Windows command
41
+ * line holds, and an `aws` that is a batch file, as a pip-installed v1 is, has cmd.exe parse every
42
+ * quote in it.
43
+ */
44
+ async function withTemplateFile<T>(
45
+ template: string,
46
+ body: (path: string) => Promise<T>,
47
+ ): Promise<T> {
48
+ const dir = await mkdtemp(join(tmpdir(), "nimbus-cfn-"));
49
+ try {
50
+ const path = join(dir, "template");
51
+ await writeFile(path, template, { encoding: "utf8", mode: 0o600, flag: "wx" });
52
+ return await body(path);
53
+ } finally {
54
+ await rm(dir, { recursive: true, force: true });
55
+ }
56
+ }
57
+
12
58
  /** Tool names exposed by this connector — for contract/introspection tests. */
13
59
  export const IAC_TOOL_NAMES = [
14
60
  "iac_terraform_plan",
@@ -42,9 +88,12 @@ export function registerIacTools(
42
88
  "Run terraform plan in a directory.",
43
89
  z.object({ workingDirectory: cliArg }),
44
90
  async (p) => {
45
- await runCliOkThrowing(["terraform", "-chdir", p.workingDirectory, "plan", "-input=false"], {
46
- ...processEnv,
47
- });
91
+ await runCliOkThrowing(
92
+ ["terraform", `-chdir=${p.workingDirectory}`, "plan", "-input=false"],
93
+ {
94
+ ...processEnv,
95
+ },
96
+ );
48
97
  return jsonResult({ ok: true });
49
98
  },
50
99
  );
@@ -60,7 +109,7 @@ export function registerIacTools(
60
109
  z.object({ workingDirectory: cliArg }),
61
110
  async (p) => {
62
111
  await runCliOkThrowing(
63
- ["terraform", "-chdir", p.workingDirectory, "apply", "-auto-approve", "-input=false"],
112
+ ["terraform", `-chdir=${p.workingDirectory}`, "apply", "-auto-approve", "-input=false"],
64
113
  { ...processEnv },
65
114
  );
66
115
  return jsonResult({ ok: true });
@@ -82,7 +131,7 @@ export function registerIacTools(
82
131
  z.object({ workingDirectory: cliArg }),
83
132
  async (p) => {
84
133
  await runCliOkThrowing(
85
- ["terraform", "-chdir", p.workingDirectory, "destroy", "-auto-approve", "-input=false"],
134
+ ["terraform", `-chdir=${p.workingDirectory}`, "destroy", "-auto-approve", "-input=false"],
86
135
  { ...processEnv },
87
136
  );
88
137
  return jsonResult({ ok: true });
@@ -99,22 +148,30 @@ export function registerIacTools(
99
148
  "Deploy a CloudFormation stack via AWS CLI.",
100
149
  z.object({
101
150
  stackName: awsCliArg,
102
- templateBody: awsCliDocument,
151
+ templateBody: templateDocument,
103
152
  }),
104
153
  async (p) => {
105
- await runCliOkThrowing(
106
- [
107
- "aws",
108
- "cloudformation",
109
- "deploy",
110
- "--stack-name",
111
- p.stackName,
112
- "--template-body",
113
- p.templateBody,
114
- "--capabilities",
115
- "CAPABILITY_IAM",
116
- ],
117
- { ...processEnv },
154
+ await withTemplateFile(p.templateBody, (templateFile) =>
155
+ runCliOkThrowing(
156
+ [
157
+ "aws",
158
+ "cloudformation",
159
+ "deploy",
160
+ "--stack-name",
161
+ p.stackName,
162
+ "--template-file",
163
+ templateFile,
164
+ "--capabilities",
165
+ "CAPABILITY_IAM",
166
+ // A deploy of a template that changes nothing has done what was asked, not failed.
167
+ "--no-fail-on-empty-changeset",
168
+ ],
169
+ // The file is UTF-8. Left to itself the CLI reads it in the locale's encoding, and on a
170
+ // Windows machine using cp1252 a template holding "Á" fails to decode while one holding
171
+ // "é" deploys as "é". AWS CLI v2 reads AWS_CLI_FILE_ENCODING and ignores PYTHONUTF8; v1,
172
+ // plain Python, reads PYTHONUTF8 and ignores the other. Both are set.
173
+ { ...processEnv, AWS_CLI_FILE_ENCODING: "UTF-8", PYTHONUTF8: "1" },
174
+ ),
118
175
  );
119
176
  return jsonResult({ ok: true });
120
177
  },
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
13
14
  import { envInt } from "../../../shared/imap-tool-kit.ts";
14
15
  import { createImapFlowClient, createNodemailerMailer } from "../../../shared/imapflow-adapter.ts";
15
16
  import { requireProcessEnv } from "../../../shared/mcp-tool-kit.ts";
@@ -22,7 +23,10 @@ const SMTPS_PORT = 465;
22
23
 
23
24
  const smtpPort = envInt("IMAP_SMTP_PORT", DEFAULT_SMTP_PORT);
24
25
 
25
- const server = new McpServer({ name: "nimbus-imap", version: "0.1.0" });
26
+ const server = new McpServer(
27
+ { name: "nimbus-imap", version: "0.1.0" },
28
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
29
+ );
26
30
 
27
31
  registerImapTools(
28
32
  server,
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerJenkinsTools } from "./tools.ts";
4
5
 
5
- const mcp = new McpServer({ name: "nimbus-jenkins", version: "0.1.0" });
6
+ const mcp = new McpServer(
7
+ { name: "nimbus-jenkins", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerJenkinsTools(mcp);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerJiraTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-jira", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-jira", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerJiraTools(server);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerKubernetesTools } from "./tools.ts";
4
5
 
5
- const mcp = new McpServer({ name: "nimbus-kubernetes", version: "0.1.0" });
6
+ const mcp = new McpServer(
7
+ { name: "nimbus-kubernetes", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerKubernetesTools(mcp);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerLinearTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-linear", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-linear", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerLinearTools(server);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerNotionTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-notion", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-notion", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerNotionTools(server);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerObsidianTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-obsidian", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-obsidian", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerObsidianTools(server);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerOnedriveTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-onedrive", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-onedrive", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerOnedriveTools(server);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerOutlookTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-outlook", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-outlook", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerOutlookTools(server);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerPagerdutyTools } from "./tools.ts";
4
5
 
5
- const mcp = new McpServer({ name: "nimbus-pagerduty", version: "0.1.0" });
6
+ const mcp = new McpServer(
7
+ { name: "nimbus-pagerduty", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerPagerdutyTools(mcp);
8
12
 
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
12
13
  import { optionalEnv } from "../../../shared/env-json-api.ts";
13
14
  import { envInt } from "../../../shared/imap-tool-kit.ts";
14
15
  import { createImapFlowClient, createNodemailerMailer } from "../../../shared/imapflow-adapter.ts";
@@ -28,7 +29,10 @@ const DEFAULT_SMTP_PORT = 1025;
28
29
  */
29
30
  const BRIDGE_TLS = { secure: false, rejectUnauthorized: false } as const;
30
31
 
31
- const server = new McpServer({ name: "nimbus-protonmail", version: "0.1.0" });
32
+ const server = new McpServer(
33
+ { name: "nimbus-protonmail", version: "0.1.0" },
34
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
35
+ );
32
36
 
33
37
  registerProtonmailTools(
34
38
  server,
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerSlackTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-slack", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-slack", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerSlackTools(server);
8
12
 
@@ -1,8 +1,12 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "../../../shared/consent-kit.ts";
3
4
  import { registerTeamsTools } from "./tools.ts";
4
5
 
5
- const server = new McpServer({ name: "nimbus-teams", version: "0.1.0" });
6
+ const server = new McpServer(
7
+ { name: "nimbus-teams", version: "0.1.0" },
8
+ { capabilities: CONSENT_SERVER_CAPABILITIES },
9
+ );
6
10
 
7
11
  registerTeamsTools(server);
8
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbus-dev/connectors",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -29,12 +29,7 @@
29
29
 
30
30
  import { z } from "zod";
31
31
  import { optionalEnv } from "./env-json-api.ts";
32
- import {
33
- awsCliArgProblem,
34
- awsCliDocumentProblem,
35
- azCliArgProblem,
36
- cliArgProblem,
37
- } from "./safe-cli-arg.ts";
32
+ import { awsCliArgProblem, azCliArgProblem, cliArgProblem } from "./safe-cli-arg.ts";
38
33
 
39
34
  /** Body-snippet length in the thrown error. The value every connector used. */
40
35
  export const DEFAULT_STDERR_SNIPPET = 400;
@@ -68,13 +63,6 @@ export const cliArg = cliArgSchema(cliArgProblem);
68
63
  */
69
64
  export const awsCliArg = cliArgSchema(awsCliArgProblem);
70
65
 
71
- /**
72
- * A DOCUMENT passed to the `aws` CLI as one argument — a CloudFormation template body. Unlike
73
- * {@link awsCliArg} it may span lines and run past 1024 characters; it still may not start with `-`
74
- * or with a prefix that makes the CLI read it from somewhere else.
75
- */
76
- export const awsCliDocument = cliArgSchema(awsCliDocumentProblem);
77
-
78
66
  /**
79
67
  * A value passed to the `az` CLI as an argument: {@link cliArg}, and not one `az` would replace by
80
68
  * a file's contents — a leading `@`, or `=@` anywhere.
@@ -59,6 +59,19 @@ export type ConsentServer = {
59
59
  sendLoggingMessage(params: { level: "info" | "warning"; data: unknown }): Promise<void>;
60
60
  };
61
61
 
62
+ /**
63
+ * What a connector with write tools constructs its `McpServer` with:
64
+ * `new McpServer(info, { capabilities: CONSENT_SERVER_CAPABILITIES })`.
65
+ *
66
+ * The kit tells the client about every gated outcome with `notifications/message`, and the SDK
67
+ * sends that only for a server that declared the MCP `logging` capability. No connector did, so
68
+ * each notification was dropped without a word. It must be declared at construction: that is the
69
+ * only moment the SDK installs its `logging/setLevel` handler, and a server that declared the
70
+ * capability later would refuse a client setting the level. `scripts/connector-boot.test.ts`
71
+ * fails a connector whose manifest declares a write and whose booted server does not declare it.
72
+ */
73
+ export const CONSENT_SERVER_CAPABILITIES = { logging: {} };
74
+
62
75
  export type WriteToolConfig<T> = {
63
76
  /** Action type this tool performs, e.g. "repo.branch.delete". Machine-readable, not prose. */
64
77
  readonly mutates: string;
@@ -185,23 +198,32 @@ export function createWriteToolRegistrar(
185
198
  outcome: AuditOutcome,
186
199
  detail: Record<string, unknown>,
187
200
  ): Promise<void> {
188
- // Meant as a client-visible channel, but it reaches no client today: the SDK sends
189
- // `notifications/message` only for a server that declared the MCP `logging` capability, and
190
- // no connector declares it. The durable log below is the only record.
191
- await server.sendLoggingMessage({
192
- level: outcome === "executed" ? "info" : "warning",
193
- data: { connector: cfg.connector, tool, outcome },
194
- });
195
- // Durable channel: only when the operator configured a path. An append that fails rejects, and
196
- // every record before the mutation is awaited, so a write whose entry did not land never runs.
197
- if (auditLog !== undefined && auditLog !== "") {
198
- await appendAuditEntry(auditLog, {
199
- ts: new Date().toISOString(),
200
- connector: cfg.connector,
201
- tool,
202
- outcome,
203
- detail,
204
- });
201
+ try {
202
+ // Durable channel first: only when the operator configured a path. An append that fails
203
+ // rejects, and every record before the mutation is awaited, so a write whose entry did not
204
+ // land never runs.
205
+ if (auditLog !== undefined && auditLog !== "") {
206
+ await appendAuditEntry(auditLog, {
207
+ ts: new Date().toISOString(),
208
+ connector: cfg.connector,
209
+ tool,
210
+ outcome,
211
+ detail,
212
+ });
213
+ }
214
+ } finally {
215
+ // Then the client-visible channel, told of every record attempted, one whose append failed
216
+ // included. It reaches the client because the connector declared the MCP `logging`
217
+ // capability (CONSENT_SERVER_CAPABILITIES), and a client may filter it by level. Best
218
+ // effort, and after the durable log: a client that has gone away mid-write must not keep the
219
+ // `executed` or `failed` record of a write that ran out of the log, and since the capability
220
+ // made this channel live, a dead pipe is where a send now fails.
221
+ await server
222
+ .sendLoggingMessage({
223
+ level: outcome === "executed" ? "info" : "warning",
224
+ data: { connector: cfg.connector, tool, outcome },
225
+ })
226
+ .catch(() => undefined);
205
227
  }
206
228
  }
207
229
 
@@ -16,6 +16,36 @@ import { batchArgumentRefusal } from "./windows-batch-args.ts";
16
16
 
17
17
  export type SpawnResult = { code: number; stdout: string; stderr: string };
18
18
 
19
+ /**
20
+ * The child's environment: `base` with `overrides` laid over it.
21
+ *
22
+ * On Windows a variable's name is case-insensitive, so an override replaces the base's variable
23
+ * whatever its spelling, instead of sitting beside it as a second one that the child may or may
24
+ * not see in its place. And the search path is spelled `PATH`, the only spelling Bun resolves a
25
+ * bare command through. A process started from PowerShell, cmd or Explorer has `Path`, and with it
26
+ * a bare `az` or `gcloud`, which are the batch files `az.cmd` and `gcloud.cmd` there, did not start
27
+ * at all: Bun 1.3.14 answered ENOENT, while an `.exe` such as `aws.exe` still started.
28
+ *
29
+ * Elsewhere names are case-sensitive, and the two are merged as written.
30
+ */
31
+ export function childEnvironment(
32
+ base: Record<string, string | undefined>,
33
+ overrides: Record<string, string | undefined>,
34
+ platform: NodeJS.Platform = process.platform,
35
+ ): Record<string, string | undefined> {
36
+ if (platform !== "win32") {
37
+ return { ...base, ...overrides };
38
+ }
39
+ const byName = new Map<string, readonly [string, string | undefined]>();
40
+ for (const source of [base, overrides]) {
41
+ for (const [key, value] of Object.entries(source)) {
42
+ const name = key.toUpperCase();
43
+ byName.set(name, [name === "PATH" ? "PATH" : key, value]);
44
+ }
45
+ }
46
+ return Object.fromEntries(byName.values());
47
+ }
48
+
19
49
  /**
20
50
  * The child's environment for `env`, and why `command` must not be spawned with it, if it must
21
51
  * not. Nothing is spawned for a refused command: it resolves as a failure whose stderr says so.
@@ -24,7 +54,7 @@ function prepareSpawn(
24
54
  command: readonly string[],
25
55
  env: Record<string, string | undefined>,
26
56
  ): { childEnv: Record<string, string | undefined>; refusal: SpawnResult | undefined } {
27
- const childEnv = { ...process.env, ...env };
57
+ const childEnv = childEnvironment(process.env, env);
28
58
  const refusal = batchArgumentRefusal(command, childEnv);
29
59
  return {
30
60
  childEnv,
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CONSENT_SERVER_CAPABILITIES } from "./consent-kit.ts";
3
4
  import {
4
5
  createRegisterSimpleTool,
5
6
  createZodToolRegistrar,
@@ -41,7 +42,12 @@ export function buildReadOnlyMcpConnector(
41
42
  register: (reg: ZodToolRegistrar, server: unknown) => void,
42
43
  options?: BuildReadOnlyMcpConnectorOptions,
43
44
  ): unknown {
44
- const make = options?.createServer ?? ((info) => new McpServer(info));
45
+ // Every server built here declares the MCP `logging` capability, since the consent kit's
46
+ // notifications reach a client only through it. A truly read-only connector sends no log
47
+ // message, so declaring it costs that connector nothing.
48
+ const make =
49
+ options?.createServer ??
50
+ ((info) => new McpServer(info, { capabilities: CONSENT_SERVER_CAPABILITIES }));
45
51
  const mcp = make({ name: serverName, version: "0.1.0" });
46
52
  const reg = createZodToolRegistrar(createRegisterSimpleTool(mcp));
47
53
  register(reg, mcp);
@@ -124,26 +124,6 @@ export function awsCliArgProblem(value: unknown): string | undefined {
124
124
  return cliArgProblem(value) ?? awsPrefixProblem(value) ?? awsShorthandProblem(value);
125
125
  }
126
126
 
127
- /**
128
- * Why `value` must not be passed to the AWS CLI as a DOCUMENT — a CloudFormation template body —
129
- * or `undefined` when it may.
130
- *
131
- * A document legitimately spans lines and runs past the 1024 characters an argument is held to,
132
- * so of the argument rules only the two about where the CLI looks remain: a leading `-`, which the
133
- * CLI would parse as an option, and a loading prefix, which would make it read the "document" from
134
- * a file. `@=` is not among them: shorthand syntax applies to structure parameters, and a template
135
- * body is a string.
136
- */
137
- export function awsCliDocumentProblem(value: unknown): string | undefined {
138
- if (typeof value !== "string" || value.length === 0) {
139
- return "must be a non-empty string";
140
- }
141
- if (value.startsWith("-")) {
142
- return 'must not start with "-" (argv flag smuggling is not allowed)';
143
- }
144
- return awsPrefixProblem(value);
145
- }
146
-
147
127
  // ---------------------------------------------------------------------------
148
128
  // az
149
129
  // ---------------------------------------------------------------------------
@@ -28,9 +28,9 @@
28
28
  * AWS resource name, an Azure app, cluster or node pool name, or a subscription id. An Azure
29
29
  * resource group may hold `(` and `)`, but never a space, so the runtime never quotes it and
30
30
  * `az.cmd` already failed on it as above. What did work and is now refused: an Azure subscription
31
- * passed by a display name holding one of these characters and a space — its id still works — and
32
- * a CloudFormation template body, whenever the `aws` that runs is a batch file: a pip-installed v1
33
- * found on `PATH` before any `aws.exe`.
31
+ * passed by a display name holding one of these characters and a space — its id still works. A
32
+ * CloudFormation template, full of quotes, was refused too whenever the `aws` that runs is a batch
33
+ * file, until it began to reach `aws` as a file instead of an argument.
34
34
  *
35
35
  * Which file runs is decided the way the runtime decides it — see {@link mayRunAsBatchFile} — so
36
36
  * an `aws.exe` found first is never refused, whatever batch file of that name comes later.
@@ -183,11 +183,12 @@ function firstHolderIsBatch(bases: readonly string[], exists: (path: string) =>
183
183
  * and libuv starts only a `.com` or an `.exe`: never a batch file.
184
184
  *
185
185
  * It says yes where Bun could change without notice, and nowhere else: a PATH variable counts in
186
- * any spelling, not only `PATH` — today Bun reads no other, and a bare `az` with only `Path` set
187
- * does not start at all — and each is searched both as written and with whitespace and quotes
188
- * stripped from its entries; a name written with a `/` is looked up in its directory as one with
189
- * a `\` is; and the first place holding the name answers yes if it holds a `.cmd` or `.bat` by
190
- * that name, even beside an `.exe` Bun would try first.
186
+ * any spelling, not only `PATH` — Bun reads no other, though `nimbus-spawn.ts` respells the
187
+ * variable `PATH` before every spawn, so a bare `az` under `Path` starts too — and each is searched
188
+ * both as written and with whitespace and quotes stripped from its entries; a name written with a
189
+ * `/` is looked up in its directory as one with a `\` is; and the first place holding the name
190
+ * answers yes if it holds a `.cmd` or `.bat` by that name, even beside an `.exe` Bun would try
191
+ * first.
191
192
  */
192
193
  export function mayRunAsBatchFile(
193
194
  bin: string,