@cloudgrid-io/mcp 0.21.5 → 0.21.7

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.
@@ -0,0 +1,131 @@
1
+ /**
2
+ * @file grid_login MCP App — the sign-in card Claude web renders for grid_login.
3
+ *
4
+ * This module is the UI half of the MCP Apps (SEP-1865) mechanism. It talks to
5
+ * the host over postMessage JSON-RPC via the official `App` class — NOT
6
+ * `window.openai`, which does not exist in Claude's host (that global is the
7
+ * ChatGPT Apps-SDK bridge the old org-picker widget targets).
8
+ *
9
+ * It is bundled into a single self-contained src/widgets/grid-login.html by
10
+ * scripts/build-login-widget.mjs (esbuild inlines this file and the whole
11
+ * @modelcontextprotocol/ext-apps App class). The iframe CSP is deny-by-default,
12
+ * so nothing here may reference a remote script, stylesheet, or font.
13
+ *
14
+ * Security invariant (issue #302): the card NEVER receives, displays, or stores
15
+ * a token. Its only job is to open the sign-in URL in the browser; the browser
16
+ * performs the auth. The most we surface after sign-in is the account email,
17
+ * which grid_login_status returns and which is not a credential.
18
+ */
19
+ import {
20
+ App,
21
+ applyDocumentTheme,
22
+ applyHostFonts,
23
+ applyHostStyleVariables,
24
+ } from "@modelcontextprotocol/ext-apps";
25
+
26
+ const els = {
27
+ root: document.getElementById("card"),
28
+ status: document.getElementById("status"),
29
+ signin: document.getElementById("signin"),
30
+ check: document.getElementById("check"),
31
+ fallback: document.getElementById("fallback"),
32
+ fallbackUrl: document.getElementById("fallback-url"),
33
+ };
34
+
35
+ // The sign-in URL, lifted from the tool result. Never a token — just the URL
36
+ // the browser opens. Held only to wire the button.
37
+ let loginUrl = null;
38
+
39
+ // Pull the sign-in URL out of a grid_login tool result. structuredContent is
40
+ // the contract (grid_login sets { login_url }); the text content is the
41
+ // text-first fallback that ALSO carries the URL, so we read it if structured is
42
+ // missing. This mirrors the server: the URL is never hidden behind the card.
43
+ function readLoginUrl(result) {
44
+ const fromStructured = result?.structuredContent?.login_url;
45
+ if (typeof fromStructured === "string" && fromStructured) return fromStructured;
46
+ const text = Array.isArray(result?.content)
47
+ ? result.content.filter((c) => c?.type === "text").map((c) => c.text).join("\n")
48
+ : "";
49
+ const match = text.match(/https?:\/\/\S+/);
50
+ return match ? match[0] : null;
51
+ }
52
+
53
+ function showSignInReady() {
54
+ if (loginUrl) {
55
+ els.fallbackUrl.textContent = loginUrl;
56
+ els.fallbackUrl.setAttribute("href", loginUrl);
57
+ els.fallback.hidden = false;
58
+ }
59
+ els.signin.disabled = !loginUrl;
60
+ els.status.textContent = loginUrl
61
+ ? "Sign in to publish to your grid."
62
+ : "Preparing the sign-in link.";
63
+ }
64
+
65
+ function showAwaitingReturn() {
66
+ els.status.textContent =
67
+ "Finish signing in on the page that opened, then come back and select Check sign-in.";
68
+ els.check.hidden = false;
69
+ }
70
+
71
+ const app = new App({ name: "CloudGrid sign-in", version: "1.0.0" });
72
+
73
+ app.onerror = (err) => console.error("[grid-login card]", err);
74
+
75
+ app.onhostcontextchanged = (ctx) => {
76
+ if (ctx?.theme) applyDocumentTheme(ctx.theme);
77
+ if (ctx?.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
78
+ if (ctx?.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts);
79
+ };
80
+
81
+ // The grid_login result arrives here after connect. It carries the URL, never
82
+ // a token.
83
+ app.ontoolresult = (result) => {
84
+ loginUrl = readLoginUrl(result);
85
+ showSignInReady();
86
+ };
87
+
88
+ els.signin.addEventListener("click", async () => {
89
+ if (!loginUrl) return;
90
+ try {
91
+ await app.openLink({ url: loginUrl });
92
+ showAwaitingReturn();
93
+ } catch (err) {
94
+ console.error("[grid-login card] openLink failed", err);
95
+ // The link is still on-screen (fallback block); the user can open it
96
+ // manually. Never dead-end.
97
+ showAwaitingReturn();
98
+ }
99
+ });
100
+
101
+ // Check sign-in: ask the server to finish the flow (grid_login_status). It
102
+ // returns { status, email } and NEVER a token, so the card stays token-free.
103
+ els.check.addEventListener("click", async () => {
104
+ els.check.disabled = true;
105
+ els.status.textContent = "Checking sign-in.";
106
+ try {
107
+ const result = await app.callServerTool({ name: "grid_login_status", arguments: {} });
108
+ const status = result?.structuredContent?.status;
109
+ const email = result?.structuredContent?.email;
110
+ if (status === "authenticated") {
111
+ els.status.textContent = email ? `Signed in as ${email}.` : "Signed in.";
112
+ els.signin.hidden = true;
113
+ els.check.hidden = true;
114
+ } else {
115
+ els.status.textContent =
116
+ "Still waiting for sign-in. Finish on the page that opened, then check again.";
117
+ els.check.disabled = false;
118
+ }
119
+ } catch (err) {
120
+ console.error("[grid-login card] status check failed", err);
121
+ els.status.textContent =
122
+ "Could not check sign-in from here. Complete it in the browser and I will continue.";
123
+ els.check.disabled = false;
124
+ }
125
+ });
126
+
127
+ showSignInReady();
128
+ app.connect().then(() => {
129
+ const ctx = app.getHostContext();
130
+ if (ctx) app.onhostcontextchanged(ctx);
131
+ });