@cloudgrid-io/mcp 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/tools.js +63 -42
  3. package/src/web.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "MCP server for CloudGrid. Two editions: a local stdio server (full toolset) and a hosted web server (light, CLI-free toolset) over MCP Streamable HTTP.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/tools.js CHANGED
@@ -11,8 +11,8 @@
11
11
  import { execFile } from "node:child_process";
12
12
  import { promisify } from "node:util";
13
13
  import { readFile } from "node:fs/promises";
14
- import { readFileSync } from "node:fs";
15
- import { basename } from "node:path";
14
+ import { readFileSync, existsSync, statSync } from "node:fs";
15
+ import { basename, resolve } from "node:path";
16
16
  import { z } from "zod";
17
17
  import { newLoginCode, buildLoginUrl, pollStatusOnce, decodeJwt } from "./auth.js";
18
18
 
@@ -59,11 +59,29 @@ function okResult({ text, structured, meta }) {
59
59
  }
60
60
 
61
61
  // ── CLI wrapping (local edition only) ──────────────────────────────────────────
62
- async function runCloudgrid(args) {
62
+
63
+ // Resolve and validate a caller-supplied working directory. Returns the resolved
64
+ // absolute path, or process.cwd() when omitted.
65
+ function resolveCwd(cwd) {
66
+ if (cwd === undefined || cwd === null || cwd === "") return undefined; // let execFile default
67
+ const abs = resolve(cwd);
68
+ if (!existsSync(abs)) {
69
+ throw new Error(`Directory does not exist: ${abs}`);
70
+ }
71
+ if (!statSync(abs).isDirectory()) {
72
+ throw new Error(`Not a directory: ${abs}`);
73
+ }
74
+ return abs;
75
+ }
76
+
77
+ async function runCloudgrid(args, opts = {}) {
78
+ const cwd = resolveCwd(opts.cwd);
63
79
  try {
64
80
  const { stdout, stderr } = await execFileAsync("cloudgrid", args, {
65
81
  maxBuffer: 16 * 1024 * 1024,
66
82
  timeout: 10 * 60 * 1000,
83
+ stdio: ["ignore", "pipe", "pipe"],
84
+ ...(cwd ? { cwd } : {}),
67
85
  });
68
86
  return (stdout || stderr || "").trim() || "Done.";
69
87
  } catch (err) {
@@ -80,10 +98,16 @@ async function runCloudgrid(args) {
80
98
  }
81
99
  }
82
100
 
83
- function cliTool(buildArgs) {
101
+ function cliTool(buildArgs, { cwdParam = false } = {}) {
84
102
  return async (input) => {
85
103
  try {
86
- return ok(await runCloudgrid(buildArgs(input || {})));
104
+ const params = input || {};
105
+ const opts = {};
106
+ if (cwdParam) {
107
+ // Accept cwd, directory, or dir as the working-directory override.
108
+ opts.cwd = params.cwd ?? params.directory ?? params.dir;
109
+ }
110
+ return ok(await runCloudgrid(buildArgs(params), opts));
87
111
  } catch (err) {
88
112
  return fail(err.message);
89
113
  }
@@ -572,36 +596,28 @@ export function registerTools(server, ctx) {
572
596
  structured: { needs_sign_in: true, login_url: url },
573
597
  });
574
598
  }
575
- // Org disambiguation with per-session pick gate: the first drop
576
- // always shows the picker when >1 org (even if the model guessed an
577
- // org). The flag ctx.state.awaitingOrgPick gates: only after the
578
- // picker was shown and the re-call supplies a valid slug do we honor
579
- // it. This prevents the model from silently guessing and skipping
580
- // the user's choice.
599
+ // Stateless org disambiguation no dependency on prior-call state
600
+ // so it works even when the client reconnects on every tool call
601
+ // (ChatGPT Apps SDK behaviour).
581
602
  {
582
603
  const orgs = await fetchUserOrgs(token);
583
- if (orgs.length > 1) {
584
- const suppliedOrg = input?.org;
585
- const validOrg = suppliedOrg && orgs.some((o) => o.slug === suppliedOrg);
586
- if (ctx.state.awaitingOrgPick && validOrg) {
587
- // The picker was shown previously and the user (or widget)
588
- // picked a valid org honor it, clear the flag, and publish.
589
- ctx.state.awaitingOrgPick = false;
590
- input = { ...(input || {}), org: suppliedOrg };
591
- } else {
592
- // First drop (any model-guessed org is ignored) or the
593
- // re-call supplied an invalid slug — show the picker.
594
- ctx.state.awaitingOrgPick = true;
595
- const lines = ["Which org should this be published to?"];
596
- for (const o of orgs) lines.push(` ${o.slug} ${o.name} (${o.role})`);
597
- lines.push("Pass the org slug in the org parameter to publish.");
598
- return okResult({
599
- text: lines.join("\n"),
600
- structured: { needs_org: true, orgs },
601
- meta: { "openai/outputTemplate": ORG_PICKER_URI },
602
- });
603
- }
604
+ const suppliedOrg = input?.org;
605
+ const validOrg = suppliedOrg && orgs.some((o) => o.slug === suppliedOrg);
606
+ if (validOrg) {
607
+ // Supplied org matches a real org slug — publish to it.
608
+ input = { ...(input || {}), org: suppliedOrg };
609
+ } else if (orgs.length > 1) {
610
+ // No valid org supplied and multiple orgs — ask once.
611
+ const lines = ["Which org should this be published to?"];
612
+ for (const o of orgs) lines.push(` ${o.slug} ${o.name} (${o.role})`);
613
+ lines.push("Pass the org slug in the org parameter to publish.");
614
+ return okResult({
615
+ text: lines.join("\n"),
616
+ structured: { needs_org: true, orgs },
617
+ meta: { "openai/outputTemplate": ORG_PICKER_URI },
618
+ });
604
619
  } else if (orgs.length === 1) {
620
+ // Single org — publish to it silently.
605
621
  input = { ...(input || {}), org: orgs[0].slug };
606
622
  }
607
623
  }
@@ -782,6 +798,7 @@ export function registerTools(server, ctx) {
782
798
  description: z.string().optional().describe("Initial one-line description."),
783
799
  dir: z.string().optional().describe("Target directory. Defaults to ./<name>."),
784
800
  org: z.string().optional().describe("Override the active org for this init."),
801
+ cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
785
802
  },
786
803
  { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
787
804
  cliTool(({ kind, name, type, description, dir, org }) => {
@@ -791,7 +808,7 @@ export function registerTools(server, ctx) {
791
808
  if (dir) args.push("--dir", dir);
792
809
  if (org) args.push("--org", org);
793
810
  return args;
794
- }),
811
+ }, { cwdParam: true }),
795
812
  );
796
813
 
797
814
  server.tool(
@@ -801,6 +818,7 @@ export function registerTools(server, ctx) {
801
818
  target: z.string().optional().describe("Path or URL. Omit to deploy the entity linked to the current directory."),
802
819
  org: z.string().optional().describe("Pick or override the org."),
803
820
  no_deploy: z.boolean().optional().describe("Register the entity but do not build or deploy."),
821
+ cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
804
822
  },
805
823
  { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
806
824
  cliTool(({ target, org, no_deploy }) => {
@@ -808,9 +826,9 @@ export function registerTools(server, ctx) {
808
826
  if (target) args.push(target);
809
827
  if (org) args.push("--org", org);
810
828
  if (no_deploy) args.push("--no-deploy");
811
- args.push("--no-clipboard", "--no-notify");
829
+ args.push("--auto", "--no-clipboard", "--no-notify");
812
830
  return args;
813
- }),
831
+ }, { cwdParam: true }),
814
832
  );
815
833
 
816
834
  server.tool(
@@ -966,7 +984,7 @@ export function registerTools(server, ctx) {
966
984
  confirm: z.literal(true).describe("Must be true to proceed."),
967
985
  },
968
986
  { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
969
- cliTool(({ name }) => ["unplug", name]),
987
+ cliTool(({ name }) => ["unplug", name, "--skip-confirm"]),
970
988
  );
971
989
 
972
990
  server.tool(
@@ -977,7 +995,7 @@ export function registerTools(server, ctx) {
977
995
  confirm: z.literal(true).describe("Must be true to proceed."),
978
996
  },
979
997
  { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
980
- cliTool(({ name }) => ["delete", name]),
998
+ cliTool(({ name }) => ["delete", name, "--yes"]),
981
999
  );
982
1000
 
983
1001
  server.tool(
@@ -1015,19 +1033,20 @@ export function registerTools(server, ctx) {
1015
1033
  name: z.string().describe("Entity slug."),
1016
1034
  key: z.string().optional().describe("Variable name. Required for get and set."),
1017
1035
  value: z.string().optional().describe("Variable value. Required for set."),
1036
+ cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
1018
1037
  },
1019
1038
  { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
1020
1039
  cliTool(({ action, name, key, value }) => {
1021
1040
  if (action === "set") {
1022
1041
  if (!key || value === undefined) throw new Error("key and value are required for set");
1023
- return ["env", "set", name, key, value];
1042
+ return ["env", "set", name, `${key}=${value}`];
1024
1043
  }
1025
1044
  if (action === "get") {
1026
1045
  if (!key) throw new Error("key is required for get");
1027
- return ["env", "get", name, key];
1046
+ return ["env", "get", key, name];
1028
1047
  }
1029
1048
  return ["env", "list", name];
1030
- }),
1049
+ }, { cwdParam: true }),
1031
1050
  );
1032
1051
 
1033
1052
  server.tool(
@@ -1038,6 +1057,7 @@ export function registerTools(server, ctx) {
1038
1057
  name: z.string().describe("Entity slug."),
1039
1058
  key: z.string().optional().describe("Secret name. Required for set."),
1040
1059
  value: z.string().optional().describe("Secret value. Required for set."),
1060
+ cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
1041
1061
  },
1042
1062
  { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
1043
1063
  cliTool(({ action, name, key, value }) => {
@@ -1046,7 +1066,7 @@ export function registerTools(server, ctx) {
1046
1066
  return ["secrets", "set", name, key, value];
1047
1067
  }
1048
1068
  return ["secrets", "list", name];
1049
- }),
1069
+ }, { cwdParam: true }),
1050
1070
  );
1051
1071
 
1052
1072
  server.tool(
@@ -1055,6 +1075,7 @@ export function registerTools(server, ctx) {
1055
1075
  {
1056
1076
  template: z.string().optional().describe("Template name."),
1057
1077
  dir: z.string().optional().describe("Target directory."),
1078
+ cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
1058
1079
  },
1059
1080
  { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
1060
1081
  cliTool(({ template, dir }) => {
@@ -1062,7 +1083,7 @@ export function registerTools(server, ctx) {
1062
1083
  if (template) args.push(template);
1063
1084
  if (dir) args.push("--dir", dir);
1064
1085
  return args;
1065
- }),
1086
+ }, { cwdParam: true }),
1066
1087
  );
1067
1088
 
1068
1089
  server.tool(
package/src/web.js CHANGED
@@ -55,7 +55,7 @@ function makeWebContext(sessionId) {
55
55
  let sessionToken = null;
56
56
  return {
57
57
  edition: "web",
58
- state: { pendingLoginCode: null, lastAnonClaim: null, lastDrop: null, anonCookie: null, awaitingOrgPick: false },
58
+ state: { pendingLoginCode: null, lastAnonClaim: null, lastDrop: null, anonCookie: null },
59
59
  canOpenBrowser: false,
60
60
  // Transport OAuth wins; the in-tool login flow is the fallback.
61
61
  getToken: async () => sessionAuth[sessionId] ?? sessionToken,