@cloudgrid-io/mcp 0.3.4 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
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,6 +11,7 @@
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";
14
15
  import { basename } from "node:path";
15
16
  import { z } from "zod";
16
17
  import { newLoginCode, buildLoginUrl, pollStatusOnce, decodeJwt } from "./auth.js";
@@ -24,6 +25,17 @@ export const API_BASE = (process.env.CLOUDGRID_API_URL || "https://api.cloudgrid
24
25
 
25
26
  const ANON_HTML_MAX_BYTES = 2_000_000;
26
27
  const CONSOLE_URL = "https://console.cloudgrid.io/";
28
+
29
+ // ── Widget resources (ChatGPT Apps SDK, web edition only) ────────────────────
30
+ const LIVE_RESULT_URI = "ui://cloudgrid/live-result.html";
31
+ const ORG_PICKER_URI = "ui://cloudgrid/org-picker.html";
32
+ const LIVE_RESULT_HTML = readFileSync(new URL("./widgets/live-result.html", import.meta.url), "utf-8");
33
+ const ORG_PICKER_HTML = readFileSync(new URL("./widgets/org-picker.html", import.meta.url), "utf-8");
34
+ const WIDGET_CSP = {
35
+ connectDomains: ["https://*.cloudgrid.io"],
36
+ resourceDomains: ["https://*.cloudgrid.io"],
37
+ };
38
+
27
39
  const VISIBILITY_LABELS = {
28
40
  private: "Only you",
29
41
  org: "Your org",
@@ -38,8 +50,12 @@ function ok(text) {
38
50
  function fail(text) {
39
51
  return { content: [{ type: "text", text }], isError: true };
40
52
  }
41
- function okResult({ text, structured }) {
42
- return { content: [{ type: "text", text }], structuredContent: structured };
53
+ function okResult({ text, structured, meta }) {
54
+ return {
55
+ content: [{ type: "text", text }],
56
+ structuredContent: structured,
57
+ ...(meta ? { _meta: meta } : {}),
58
+ };
43
59
  }
44
60
 
45
61
  // ── CLI wrapping (local edition only) ──────────────────────────────────────────
@@ -440,6 +456,33 @@ async function runVisibility(ctx, { target, visibility, org }) {
440
456
  // Registers the tools onto `server`. ctx.edition decides whether the CLI-wrapping
441
457
  // tools are included (they need a local machine).
442
458
  export function registerTools(server, ctx) {
459
+ // ── Widget resources (web edition, ChatGPT Apps SDK) ──────────────────────
460
+ if (ctx.edition === "web") {
461
+ server.registerResource("cloudgrid-live-result", LIVE_RESULT_URI, {
462
+ description: "Live result card after a CloudGrid drop — shows URL, grid link, and visibility controls.",
463
+ mimeType: "text/html;profile=mcp-app",
464
+ }, async () => ({
465
+ contents: [{
466
+ uri: LIVE_RESULT_URI,
467
+ mimeType: "text/html;profile=mcp-app",
468
+ text: LIVE_RESULT_HTML,
469
+ _meta: { ui: { csp: WIDGET_CSP } },
470
+ }],
471
+ }));
472
+
473
+ server.registerResource("cloudgrid-org-picker", ORG_PICKER_URI, {
474
+ description: "Org picker card — lets the user choose which organization to publish into.",
475
+ mimeType: "text/html;profile=mcp-app",
476
+ }, async () => ({
477
+ contents: [{
478
+ uri: ORG_PICKER_URI,
479
+ mimeType: "text/html;profile=mcp-app",
480
+ text: ORG_PICKER_HTML,
481
+ _meta: { ui: { csp: WIDGET_CSP } },
482
+ }],
483
+ }));
484
+ }
485
+
443
486
  // ── Direct-API tools (both editions) ──────────────────────────────────────
444
487
 
445
488
  // Drop — both editions.
@@ -479,6 +522,12 @@ export function registerTools(server, ctx) {
479
522
  login_url: z.string().optional().describe("Sign-in URL when authentication is needed."),
480
523
  },
481
524
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
525
+ ...(ctx.edition === "web" ? {
526
+ _meta: {
527
+ ui: { resourceUri: LIVE_RESULT_URI, csp: WIDGET_CSP },
528
+ "openai/outputTemplate": LIVE_RESULT_URI,
529
+ },
530
+ } : {}),
482
531
  },
483
532
  async (input) => {
484
533
  try {
@@ -507,6 +556,7 @@ export function registerTools(server, ctx) {
507
556
  return okResult({
508
557
  text: lines.join("\n"),
509
558
  structured: { needs_org: true, orgs },
559
+ meta: { "openai/outputTemplate": ORG_PICKER_URI },
510
560
  });
511
561
  }
512
562
  if (orgs.length === 1) {
@@ -0,0 +1,157 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ *{margin:0;padding:0;box-sizing:border-box}
8
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#1a1a2e;color:#e0e0e0;padding:16px}
9
+ .card{background:#16213e;border-radius:12px;padding:20px;border:1px solid rgba(138,107,255,.2)}
10
+ .title{font-size:14px;font-weight:600;color:#fff;margin-bottom:12px}
11
+ .url{font-size:13px;color:#4dd0ff;word-break:break-all;margin-bottom:16px}
12
+ .buttons{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}
13
+ .btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;font-size:13px;font-weight:500;border:none;cursor:pointer;transition:opacity .15s}
14
+ .btn:hover{opacity:.85}
15
+ .btn-primary{background:linear-gradient(135deg,#8a6bff,#4dd0ff);color:#fff}
16
+ .btn-secondary{background:rgba(138,107,255,.15);color:#a78bfa;border:1px solid rgba(138,107,255,.3)}
17
+ .vis-section{margin-top:12px;border-top:1px solid rgba(255,255,255,.08);padding-top:12px}
18
+ .vis-label{font-size:12px;color:#9ca3af;margin-bottom:8px}
19
+ .vis-pills{display:flex;gap:6px;flex-wrap:wrap}
20
+ .vis-pill{padding:6px 12px;border-radius:6px;font-size:12px;border:1px solid rgba(138,107,255,.3);background:transparent;color:#d1d5db;cursor:pointer;transition:all .15s}
21
+ .vis-pill:hover{border-color:#8a6bff;color:#fff}
22
+ .vis-pill.active{background:rgba(138,107,255,.25);border-color:#8a6bff;color:#fff}
23
+ .vis-pill.updating{opacity:.6;pointer-events:none}
24
+ .status{font-size:12px;color:#9ca3af;margin-top:8px}
25
+ .sign-in{text-align:center;padding:20px 0}
26
+ .sign-in p{margin-bottom:12px;color:#9ca3af;font-size:13px}
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <div id="root"></div>
31
+ <script>
32
+ (function () {
33
+ var root = document.getElementById("root");
34
+
35
+ function esc(s) {
36
+ var d = document.createElement("div");
37
+ d.textContent = s;
38
+ return d.innerHTML;
39
+ }
40
+
41
+ function render(data) {
42
+ if (!data) { root.innerHTML = ""; return; }
43
+
44
+ if (data.needs_sign_in) {
45
+ root.innerHTML =
46
+ '<div class="card sign-in">' +
47
+ '<p>Sign in to publish to your org</p>' +
48
+ '<button class="btn btn-primary" id="sign-in-btn">Sign in with Google</button>' +
49
+ '</div>';
50
+ var signInBtn = document.getElementById("sign-in-btn");
51
+ if (signInBtn) {
52
+ signInBtn.addEventListener("click", function () {
53
+ if (data.login_url && window.openai && window.openai.openExternal) {
54
+ window.openai.openExternal({ href: data.login_url, redirectUrl: false });
55
+ }
56
+ });
57
+ }
58
+ return;
59
+ }
60
+
61
+ if (!data.url) {
62
+ root.innerHTML = '<div class="card"><div class="title">Done</div></div>';
63
+ return;
64
+ }
65
+
66
+ var label =
67
+ data.status === "created" ? "Published" :
68
+ data.status === "updated" ? "Updated" :
69
+ data.status === "unchanged" ? "Already live" : "Live";
70
+
71
+ var html =
72
+ '<div class="card">' +
73
+ '<div class="title">' + esc(label) + '</div>' +
74
+ '<div class="url">' + esc(data.url) + '</div>' +
75
+ '<div class="buttons">' +
76
+ '<button class="btn btn-primary" id="open-btn">Open</button>';
77
+
78
+ if (data.console_url) {
79
+ html += '<button class="btn btn-secondary" id="grid-btn">Open in your grid</button>';
80
+ }
81
+ html += '</div>';
82
+
83
+ if (data.visibility_options && data.current_visibility) {
84
+ html +=
85
+ '<div class="vis-section">' +
86
+ '<div class="vis-label">Visible to</div>' +
87
+ '<div class="vis-pills">';
88
+ for (var i = 0; i < data.visibility_options.length; i++) {
89
+ var opt = data.visibility_options[i];
90
+ var cls = opt.value === data.current_visibility ? " active" : "";
91
+ html += '<button class="vis-pill' + cls + '" data-vis="' + esc(opt.value) + '">' + esc(opt.label) + '</button>';
92
+ }
93
+ html += '</div><div class="status" id="vis-status"></div></div>';
94
+ }
95
+
96
+ html += '</div>';
97
+ root.innerHTML = html;
98
+
99
+ var openBtn = document.getElementById("open-btn");
100
+ if (openBtn) {
101
+ openBtn.addEventListener("click", function () {
102
+ if (window.openai && window.openai.openExternal) {
103
+ window.openai.openExternal({ href: data.url, redirectUrl: false });
104
+ }
105
+ });
106
+ }
107
+ var gridBtn = document.getElementById("grid-btn");
108
+ if (gridBtn) {
109
+ gridBtn.addEventListener("click", function () {
110
+ if (window.openai && window.openai.openExternal) {
111
+ window.openai.openExternal({ href: data.console_url, redirectUrl: false });
112
+ }
113
+ });
114
+ }
115
+
116
+ var pills = document.querySelectorAll(".vis-pill");
117
+ for (var j = 0; j < pills.length; j++) {
118
+ (function (pill) {
119
+ pill.addEventListener("click", function () {
120
+ var vis = pill.getAttribute("data-vis");
121
+ if (vis === data.current_visibility) return;
122
+ var all = document.querySelectorAll(".vis-pill");
123
+ for (var k = 0; k < all.length; k++) all[k].classList.add("updating");
124
+ var statusEl = document.getElementById("vis-status");
125
+ if (statusEl) statusEl.textContent = "Updating\u2026";
126
+ if (!window.openai || !window.openai.callTool) return;
127
+ window.openai.callTool("cloudgrid_visibility", { visibility: vis }).then(function (res) {
128
+ var newVis = res && res.structuredContent && res.structuredContent.visibility;
129
+ if (newVis) {
130
+ data.current_visibility = newVis;
131
+ for (var m = 0; m < all.length; m++) {
132
+ all[m].classList.remove("updating", "active");
133
+ if (all[m].getAttribute("data-vis") === newVis) all[m].classList.add("active");
134
+ }
135
+ if (statusEl) statusEl.textContent = "";
136
+ }
137
+ }).catch(function () {
138
+ for (var m = 0; m < all.length; m++) all[m].classList.remove("updating");
139
+ if (statusEl) statusEl.textContent = "Failed to update visibility";
140
+ });
141
+ });
142
+ })(pills[j]);
143
+ }
144
+ }
145
+
146
+ var data = window.openai && window.openai.toolOutput;
147
+ if (data) render(data);
148
+ window.addEventListener("message", function (e) {
149
+ if (e.data && e.data.method === "ui/notifications/tool-result" &&
150
+ e.data.params && e.data.params.structuredContent) {
151
+ render(e.data.params.structuredContent);
152
+ }
153
+ });
154
+ })();
155
+ </script>
156
+ </body>
157
+ </html>
@@ -0,0 +1,94 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ *{margin:0;padding:0;box-sizing:border-box}
8
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#1a1a2e;color:#e0e0e0;padding:16px}
9
+ .card{background:#16213e;border-radius:12px;padding:20px;border:1px solid rgba(138,107,255,.2)}
10
+ .title{font-size:14px;font-weight:600;color:#fff;margin-bottom:4px}
11
+ .subtitle{font-size:12px;color:#9ca3af;margin-bottom:16px}
12
+ .orgs{display:flex;flex-direction:column;gap:8px}
13
+ .org-btn{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-radius:8px;border:1px solid rgba(138,107,255,.3);background:transparent;color:#e0e0e0;cursor:pointer;transition:all .15s;font-size:13px;text-align:left}
14
+ .org-btn:hover{border-color:#8a6bff;background:rgba(138,107,255,.1)}
15
+ .org-btn.loading{opacity:.6;pointer-events:none}
16
+ .org-name{font-weight:500;color:#fff}
17
+ .org-role{font-size:11px;color:#9ca3af}
18
+ .status{font-size:12px;color:#9ca3af;margin-top:12px}
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <div id="root"></div>
23
+ <script>
24
+ (function () {
25
+ var root = document.getElementById("root");
26
+
27
+ function esc(s) {
28
+ var d = document.createElement("div");
29
+ d.textContent = s;
30
+ return d.innerHTML;
31
+ }
32
+
33
+ function render(data) {
34
+ if (!data || !data.orgs || data.orgs.length === 0) {
35
+ root.innerHTML = '<div class="card"><div class="title">No organizations found</div></div>';
36
+ return;
37
+ }
38
+
39
+ var html =
40
+ '<div class="card">' +
41
+ '<div class="title">Choose an organization</div>' +
42
+ '<div class="subtitle">Which org should this be published to?</div>' +
43
+ '<div class="orgs">';
44
+ for (var i = 0; i < data.orgs.length; i++) {
45
+ var org = data.orgs[i];
46
+ html +=
47
+ '<button class="org-btn" data-slug="' + esc(org.slug) + '">' +
48
+ '<span class="org-name">' + esc(org.name) + '</span>' +
49
+ '<span class="org-role">' + esc(org.role) + '</span>' +
50
+ '</button>';
51
+ }
52
+ html += '</div><div class="status" id="status"></div></div>';
53
+ root.innerHTML = html;
54
+
55
+ var buttons = document.querySelectorAll(".org-btn");
56
+ for (var j = 0; j < buttons.length; j++) {
57
+ (function (btn) {
58
+ btn.addEventListener("click", function () {
59
+ var slug = btn.getAttribute("data-slug");
60
+ var all = document.querySelectorAll(".org-btn");
61
+ for (var k = 0; k < all.length; k++) all[k].classList.add("loading");
62
+ var statusEl = document.getElementById("status");
63
+ if (statusEl) statusEl.textContent = "Publishing\u2026";
64
+ if (!window.openai || !window.openai.callTool) return;
65
+ var input = (window.openai && window.openai.toolInput) || {};
66
+ var args = {};
67
+ if (input.html) args.html = input.html;
68
+ if (input.path) args.path = input.path;
69
+ if (input.filename) args.filename = input.filename;
70
+ if (input.fresh !== undefined) args.fresh = input.fresh;
71
+ args.org = slug;
72
+ window.openai.callTool("cloudgrid_drop", args).then(function () {
73
+ // The host will render the new result with the live-result card.
74
+ }).catch(function () {
75
+ for (var m = 0; m < all.length; m++) all[m].classList.remove("loading");
76
+ if (statusEl) statusEl.textContent = "Failed to publish";
77
+ });
78
+ });
79
+ })(buttons[j]);
80
+ }
81
+ }
82
+
83
+ var data = window.openai && window.openai.toolOutput;
84
+ if (data) render(data);
85
+ window.addEventListener("message", function (e) {
86
+ if (e.data && e.data.method === "ui/notifications/tool-result" &&
87
+ e.data.params && e.data.params.structuredContent) {
88
+ render(e.data.params.structuredContent);
89
+ }
90
+ });
91
+ })();
92
+ </script>
93
+ </body>
94
+ </html>