@cloudgrid-io/mcp 0.21.15 → 0.21.17
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 +2 -1
- package/src/oauth.js +107 -8
- package/src/tools/constants.js +28 -17
- package/src/tools/deploy.js +10 -8
- package/src/tools/register.js +23 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.17",
|
|
4
4
|
"description": "MCP access to CloudGrid — the live runtime environment where the user's apps run WITH the infrastructure they need out of the box: managed MongoDB, Redis (cache/queues/pub-sub), a pgvector store, persistent disk, and built-in AI with no API keys, in any language or stack, no Docker or Kubernetes to manage. Brainstorm, build, plug, and improve apps, games, dashboards, and tools; instant single-page publishes and full runtime-app deploys with a live URL — for developers and non-technical users. Three editions: local stdio (full toolset), hosted web (light, CLI-free), and a public docs-search server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"test:web-restart-recovery": "node --test test/web-restart-recovery.test.mjs",
|
|
75
75
|
"test:oauth-client-persistence": "node --test test/oauth-client-persistence.test.mjs",
|
|
76
76
|
"test:oauth-token-exchange": "node --test test/oauth-token-exchange.test.mjs",
|
|
77
|
+
"test:oauth-resource-indicators": "node --test test/oauth-resource-indicators.test.mjs",
|
|
77
78
|
"test:oauth-observability": "node --test test/oauth-flow-observability.test.mjs",
|
|
78
79
|
"test:web-capabilities-log": "node --test test/web-capabilities-log.test.mjs",
|
|
79
80
|
"test:mcp-transport-observability": "node --test test/mcp-transport-observability.test.mjs",
|
package/src/oauth.js
CHANGED
|
@@ -21,8 +21,8 @@ const CODE_TTL_MS = 5 * 60 * 1000; // authorize sessions + auth codes live 5 min
|
|
|
21
21
|
// are 5-minute, retry-on-failure sessions: losing them to a restart just means
|
|
22
22
|
// the user clicks "sign in" again, never that they must re-add the connector.
|
|
23
23
|
// Client REGISTRATIONS are NOT here — they are stateless (see below, #3060).
|
|
24
|
-
const authSessions = new Map(); // sid -> { cgCode, client_id, redirect_uri, state, code_challenge, created }
|
|
25
|
-
const authCodes = new Map(); // code -> { jwt, client_id, redirect_uri, code_challenge, created }
|
|
24
|
+
const authSessions = new Map(); // sid -> { cgCode, client_id, redirect_uri, state, code_challenge, resource, created }
|
|
25
|
+
const authCodes = new Map(); // code -> { jwt, client_id, redirect_uri, code_challenge, resource, created }
|
|
26
26
|
|
|
27
27
|
function sweep(map) {
|
|
28
28
|
const now = Date.now();
|
|
@@ -58,6 +58,38 @@ function hostOf(uri) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// --- RFC 8707 Resource Indicators (#329) --------------------------------------
|
|
62
|
+
// The MCP 2025-06-18 authorization spec has clients send `resource` — the
|
|
63
|
+
// canonical URI of the MCP server the token is for — on BOTH the authorize and
|
|
64
|
+
// token requests. We accept it, validate it against this server's own resource
|
|
65
|
+
// identifier (the `${base}/mcp` value /.well-known/oauth-protected-resource
|
|
66
|
+
// returns), and bind it to the code. A mismatch is `invalid_target` (RFC 8707
|
|
67
|
+
// §2). A MISSING resource stays acceptable: Claude web / Claude Code work today
|
|
68
|
+
// and may not send it, and the spec is explicit that clients "MUST send this
|
|
69
|
+
// parameter regardless of whether authorization servers support it" — i.e. the
|
|
70
|
+
// server may still honour requests without it.
|
|
71
|
+
//
|
|
72
|
+
// Comparison is on the CANONICAL form, not a raw string, so a cosmetically
|
|
73
|
+
// different but equivalent URI (uppercase scheme/host, a trailing slash, a
|
|
74
|
+
// default port) is not falsely rejected. Per the spec: lowercase scheme and
|
|
75
|
+
// host (URL parsing does this), no fragment, and the trailing-slash-free path.
|
|
76
|
+
// The QUERY STRING is also dropped by construction (we rebuild from protocol +
|
|
77
|
+
// host + path only), so `…/mcp?x=1` compares EQUAL to `…/mcp`. That is the
|
|
78
|
+
// permissive direction and fine here — our resource identifier carries no
|
|
79
|
+
// query — but it is stated because nobody would predict it from the code.
|
|
80
|
+
// Returns null for anything that is not an absolute URI — that then fails the
|
|
81
|
+
// equality check as a mismatch.
|
|
82
|
+
function normalizeResource(uri) {
|
|
83
|
+
try {
|
|
84
|
+
const u = new URL(String(uri));
|
|
85
|
+
if (!u.host) return null;
|
|
86
|
+
const path = u.pathname.replace(/\/+$/, ""); // …/mcp/ ≡ …/mcp
|
|
87
|
+
return `${u.protocol}//${u.host}${path}`; // protocol + host are already lowercased & default-port-stripped
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
61
93
|
// --- Stateless client registration (#3060) -----------------------------------
|
|
62
94
|
// A `client_id` is a signed token that CARRIES its own redirect-URI set, not a
|
|
63
95
|
// random handle into a process-local Map. Registration signs the (sorted,
|
|
@@ -198,6 +230,10 @@ function interstitialHtml(sid, loginUrl) {
|
|
|
198
230
|
export function mountOAuth(app, publicBase, opts = {}) {
|
|
199
231
|
if (!opts.requireAuth) return;
|
|
200
232
|
const base = publicBase.replace(/\/+$/, "");
|
|
233
|
+
// This server's resource identifier — the exact value the protected-resource
|
|
234
|
+
// metadata returns and the canonical `resource` a client must send (#329).
|
|
235
|
+
const resourceId = `${base}/mcp`;
|
|
236
|
+
const expectedResource = normalizeResource(resourceId);
|
|
201
237
|
|
|
202
238
|
// Resolve the client-registration signing secret(s) once at mount. Log which
|
|
203
239
|
// mode is active — durable vs. ephemeral — so a production deploy can be
|
|
@@ -236,6 +272,14 @@ export function mountOAuth(app, publicBase, opts = {}) {
|
|
|
236
272
|
code_challenge_methods_supported: ["S256"],
|
|
237
273
|
token_endpoint_auth_methods_supported: ["none"],
|
|
238
274
|
scopes_supported: ["cloudgrid"],
|
|
275
|
+
// RFC 8707 Resource Indicators support (#329). RFC 8707 defines NO
|
|
276
|
+
// authorization-server metadata field of its own, and neither does the MCP
|
|
277
|
+
// spec; the only IANA-registered resource-related AS-metadata field is
|
|
278
|
+
// `protected_resources` (RFC 9728 §4) — a JSON array of the resource
|
|
279
|
+
// identifiers this AS issues tokens for. Advertising ours here lets a client
|
|
280
|
+
// discover the canonical `resource` value to send, and signals that we honour
|
|
281
|
+
// the parameter rather than ignoring it.
|
|
282
|
+
protected_resources: [resourceId],
|
|
239
283
|
};
|
|
240
284
|
|
|
241
285
|
app.options(/^\/(\.well-known|oauth)\/.*/, (_req, res) => {
|
|
@@ -302,16 +346,19 @@ export function mountOAuth(app, publicBase, opts = {}) {
|
|
|
302
346
|
// Authorization endpoint — render the bridge interstitial.
|
|
303
347
|
app.get("/oauth/authorize", (req, res) => {
|
|
304
348
|
sweep(authSessions);
|
|
305
|
-
const { client_id, redirect_uri, state, code_challenge, code_challenge_method, response_type } = req.query;
|
|
306
|
-
// Observability (#345): the request SHAPE the client actually sent,
|
|
307
|
-
// before any check so a rejected request is still visible. Presence,
|
|
308
|
-
// values — nothing here is reusable in a log aggregator.
|
|
349
|
+
const { client_id, redirect_uri, state, code_challenge, code_challenge_method, response_type, resource } = req.query;
|
|
350
|
+
// Observability (#345/#329): the request SHAPE the client actually sent,
|
|
351
|
+
// logged before any check so a rejected request is still visible. Presence,
|
|
352
|
+
// not values — nothing here is reusable in a log aggregator. The resource
|
|
353
|
+
// HOST is included: it is not a secret, and its presence/absence is the exact
|
|
354
|
+
// signal that distinguishes ChatGPT (sends it) from Claude (does not).
|
|
309
355
|
console.error(
|
|
310
356
|
`[oauth] authorize request: client_id=${clientIdPrefix(client_id)} ` +
|
|
311
357
|
`response_type=${response_type ? String(response_type) : "absent"} ` +
|
|
312
358
|
`state=${state ? "present" : "absent"} ` +
|
|
313
359
|
`pkce=${code_challenge ? (code_challenge_method ? String(code_challenge_method) : "present(no-method)") : "absent"} ` +
|
|
314
|
-
`redirect_host=${hostOf(redirect_uri)}
|
|
360
|
+
`redirect_host=${hostOf(redirect_uri)} ` +
|
|
361
|
+
`resource_host=${hostOf(resource)}`,
|
|
315
362
|
);
|
|
316
363
|
const client = verifyClientId(client_id, clientSecrets.secrets);
|
|
317
364
|
if (!client || !client.redirect_uris.includes(String(redirect_uri))) {
|
|
@@ -322,6 +369,39 @@ export function mountOAuth(app, publicBase, opts = {}) {
|
|
|
322
369
|
res.status(400).send("This server requires response_type=code with PKCE (S256).");
|
|
323
370
|
return;
|
|
324
371
|
}
|
|
372
|
+
// RFC 8707 (#329): if a `resource` is present it must be THIS server's
|
|
373
|
+
// resource identifier. Compared on the canonical form so an equivalent URI
|
|
374
|
+
// (case, trailing slash, default port) is accepted. A mismatch is
|
|
375
|
+
// invalid_target; a MISSING resource stays acceptable (Claude compatibility).
|
|
376
|
+
// The bound value is the canonical string, so the token endpoint can compare
|
|
377
|
+
// apples to apples later.
|
|
378
|
+
let boundResource = "";
|
|
379
|
+
if (resource !== undefined && String(resource) !== "") {
|
|
380
|
+
const requested = normalizeResource(resource);
|
|
381
|
+
if (!requested || requested !== expectedResource) {
|
|
382
|
+
// RFC 6749 §4.1.2.1: the redirect_uri is already verified as registered
|
|
383
|
+
// (the render-path 400 above), so an authorization error is returned by
|
|
384
|
+
// REDIRECTING to it with error= and state=, NOT by rendering an opaque
|
|
385
|
+
// body the client cannot read — RFC 8707 §2 defines invalid_target as
|
|
386
|
+
// exactly such an OAuth error code. This is the whole point of the PR:
|
|
387
|
+
// a spec fix that makes a silent failure legible must not add a new
|
|
388
|
+
// illegible one (an opaque 400 where ChatGPT expects
|
|
389
|
+
// error=invalid_target&state=… would just be another #329-shaped
|
|
390
|
+
// symptom). The two 400s above STAY renders: an unverified client or
|
|
391
|
+
// redirect_uri must NEVER be redirected to, so they are not precedent.
|
|
392
|
+
console.error(
|
|
393
|
+
`[oauth] authorize refused (redirect): invalid_target resource_host=${hostOf(resource)} expected_host=${hostOf(resourceId)}`,
|
|
394
|
+
);
|
|
395
|
+
const sep = String(redirect_uri).includes("?") ? "&" : "?";
|
|
396
|
+
let location = `${redirect_uri}${sep}error=invalid_target&error_description=${encodeURIComponent(
|
|
397
|
+
"The requested resource does not identify this server.",
|
|
398
|
+
)}`;
|
|
399
|
+
if (state) location += `&state=${encodeURIComponent(String(state))}`;
|
|
400
|
+
res.redirect(302, location);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
boundResource = requested;
|
|
404
|
+
}
|
|
325
405
|
const sid = randomUUID();
|
|
326
406
|
const cgCode = newLoginCode();
|
|
327
407
|
authSessions.set(sid, {
|
|
@@ -330,6 +410,7 @@ export function mountOAuth(app, publicBase, opts = {}) {
|
|
|
330
410
|
redirect_uri: String(redirect_uri),
|
|
331
411
|
state: state ? String(state) : "",
|
|
332
412
|
code_challenge: String(code_challenge),
|
|
413
|
+
resource: boundResource,
|
|
333
414
|
created: Date.now(),
|
|
334
415
|
});
|
|
335
416
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
@@ -358,6 +439,7 @@ export function mountOAuth(app, publicBase, opts = {}) {
|
|
|
358
439
|
client_id: sess.client_id,
|
|
359
440
|
redirect_uri: sess.redirect_uri,
|
|
360
441
|
code_challenge: sess.code_challenge,
|
|
442
|
+
resource: sess.resource, // RFC 8707 binding carried from the authorize session (#329)
|
|
361
443
|
created: Date.now(),
|
|
362
444
|
});
|
|
363
445
|
authSessions.delete(String(req.query.sid));
|
|
@@ -420,7 +502,7 @@ export function mountOAuth(app, publicBase, opts = {}) {
|
|
|
420
502
|
app.post("/oauth/token", (req, res) => {
|
|
421
503
|
corsOk(res);
|
|
422
504
|
sweep(authCodes);
|
|
423
|
-
const { grant_type, code, code_verifier, redirect_uri, client_id } = req.body ?? {};
|
|
505
|
+
const { grant_type, code, code_verifier, redirect_uri, client_id, resource } = req.body ?? {};
|
|
424
506
|
if (grant_type !== "authorization_code") {
|
|
425
507
|
denyToken(res, "unsupported_grant_type", "grant_type must be authorization_code", `(got ${JSON.stringify(grant_type ?? null)})`);
|
|
426
508
|
return;
|
|
@@ -448,6 +530,23 @@ export function mountOAuth(app, publicBase, opts = {}) {
|
|
|
448
530
|
denyToken(res, "invalid_grant", "redirect_uri does not match the authorize request", `(sent ${JSON.stringify(redirect_uri ?? null)}, expected ${JSON.stringify(rec.redirect_uri)})`);
|
|
449
531
|
return;
|
|
450
532
|
}
|
|
533
|
+
// RFC 8707 (#329): if the client repeats `resource` at the token endpoint it
|
|
534
|
+
// must be the one bound at authorize. Mismatch → invalid_target, refused via
|
|
535
|
+
// denyToken so it is logged. A token request with NO resource stays
|
|
536
|
+
// acceptable (Claude compatibility), even when one was bound. Only the HOSTS
|
|
537
|
+
// are logged — the resource URI is not a secret but nothing bearer-shaped is.
|
|
538
|
+
if (resource !== undefined && String(resource) !== "") {
|
|
539
|
+
const requested = normalizeResource(resource);
|
|
540
|
+
if (!requested || requested !== rec.resource) {
|
|
541
|
+
denyToken(
|
|
542
|
+
res,
|
|
543
|
+
"invalid_target",
|
|
544
|
+
"resource does not match the authorize request",
|
|
545
|
+
`(resource_host ${hostOf(resource)} vs bound ${hostOf(rec.resource)})`,
|
|
546
|
+
);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
451
550
|
const challenge = b64url(createHash("sha256").update(String(code_verifier ?? "")).digest());
|
|
452
551
|
if (challenge !== rec.code_challenge) {
|
|
453
552
|
denyToken(res, "invalid_grant", code_verifier === undefined ? "code_verifier is required (PKCE)" : "PKCE verification failed");
|
package/src/tools/constants.js
CHANGED
|
@@ -64,25 +64,36 @@ export const VISIBILITY_LABELS = {
|
|
|
64
64
|
// the widget HTML is verified to render. The resources stay registered either
|
|
65
65
|
// way (harmless when no tool references them via outputTemplate).
|
|
66
66
|
//
|
|
67
|
-
// 2026-08-
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// OpenAI's
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
67
|
+
// 2026-08-31 (#308, corrected): the widgets targeted a RETIRED MIME. At DevDay
|
|
68
|
+
// (Oct 2025) ChatGPT's Apps SDK used `text/html+skybridge`, and #309 switched us
|
|
69
|
+
// to it. But the Apps SDK merged with MCP Apps into SEP-1865, ratified
|
|
70
|
+
// 2026-01-26, and OpenAI's CURRENT docs (developers.openai.com/apps-sdk
|
|
71
|
+
// build/custom-ux, deploy/troubleshooting) now specify `text/html;profile=mcp-app`
|
|
72
|
+
// — the same MIME Claude reads. skybridge is not mentioned anywhere in the
|
|
73
|
+
// current docs. So ChatGPT opened the iframe on our skybridge resource, could not
|
|
74
|
+
// recognise it, and never populated it — the empty frame over the text.
|
|
75
|
+
//
|
|
76
|
+
// The clincher: our own Claude sign-in card (#303) RENDERS, and it declares
|
|
77
|
+
// `text/html;profile=mcp-app`. The MIME that renders is the standard one; the one
|
|
78
|
+
// that does not is skybridge. The two hosts converged; the old separation was the
|
|
79
|
+
// defect. CHATGPT_WIDGET_MIME below now equals RESOURCE_MIME_TYPE by value. The
|
|
80
|
+
// widget bridge (window.openai.toolOutput / openExternal / callTool /
|
|
81
|
+
// ui/notifications/tool-result) is already current — verified against
|
|
82
|
+
// developers.openai.com/apps-sdk/reference — so only the MIME changes.
|
|
83
|
+
//
|
|
84
|
+
// The flag stays where the platform sets it; this only makes the resource the
|
|
85
|
+
// type ChatGPT now expects. Done is a card seen rendering in real ChatGPT.
|
|
80
86
|
export const APPS_WIDGETS_ENABLED = process.env.MCP_APPS_WIDGETS === "1";
|
|
81
87
|
// The mimeType ChatGPT's Apps-SDK renderer requires for a ui:// widget resource.
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
88
|
+
// Since SEP-1865 (2026-01-26) this EQUALS RESOURCE_MIME_TYPE (`text/html;profile=
|
|
89
|
+
// mcp-app`): both hosts now render the standard MCP-Apps MIME. Both hosts also
|
|
90
|
+
// read the SAME binding key, `_meta.ui.resourceUri`; ChatGPT honors
|
|
91
|
+
// `openai/outputTemplate` only as a compatibility alias (OpenAI's own
|
|
92
|
+
// deploy/troubleshooting page). So our two mechanisms stay separate not because
|
|
93
|
+
// the hosts key off different MIMEs or different keys, but because WE bind them
|
|
94
|
+
// on different keys: the ChatGPT widget via `openai/outputTemplate` (the alias,
|
|
95
|
+
// which Claude ignores), the Claude card via `_meta.ui.resourceUri`.
|
|
96
|
+
export const CHATGPT_WIDGET_MIME = "text/html;profile=mcp-app";
|
|
86
97
|
export const LIVE_RESULT_URI = "ui://cloudgrid/live-result.html";
|
|
87
98
|
// URI/resource-name/filename stay `org-picker` — that's the stable contract the
|
|
88
99
|
// web card is registered under; only the JS identifier moves toward grid.
|
package/src/tools/deploy.js
CHANGED
|
@@ -2382,15 +2382,16 @@ export async function runPlug(ctx, input, deps = {}) {
|
|
|
2382
2382
|
// /grids/<slug> form 404s; /home?grid= does not. Falls back to the root when
|
|
2383
2383
|
// the slug is unknown.
|
|
2384
2384
|
//
|
|
2385
|
-
// NOT on the building branch
|
|
2386
|
-
//
|
|
2387
|
-
//
|
|
2388
|
-
//
|
|
2389
|
-
//
|
|
2385
|
+
// NOT on the building branch. This line invites the user to "see and manage all
|
|
2386
|
+
// their apps in their grid" — an invitation that presumes the app is live. A
|
|
2387
|
+
// still-building app is not, and it sits two lines under "Do NOT tell the user it
|
|
2388
|
+
// is live", so relaying a manage-your-apps prompt there contradicts the honesty
|
|
2389
|
+
// gate. For an async runtime build the console line arrives once liveness is
|
|
2390
|
+
// confirmed, in runCheckDeploy's success branch.
|
|
2390
2391
|
if (data.entity_id && !isBuilding) {
|
|
2391
2392
|
const consoleUrl = consoleGridUrl(data.grid ?? grid ?? null);
|
|
2392
2393
|
structured.console_url = consoleUrl;
|
|
2393
|
-
lines.push(`
|
|
2394
|
+
lines.push(`Give the user this link so they can see and manage all their apps in their grid: ${consoleUrl}`);
|
|
2394
2395
|
}
|
|
2395
2396
|
|
|
2396
2397
|
// Visibility is the user's choice — never set silently. On a NEW deploy,
|
|
@@ -2881,9 +2882,10 @@ export async function runCheckDeploy(ctx, { poll_url, grid } = {}) {
|
|
|
2881
2882
|
return {
|
|
2882
2883
|
// The "now live" moment for an async runtime build lands here, not in
|
|
2883
2884
|
// runPlug (which returned "building"). Point the user at their grid too
|
|
2884
|
-
// (#321) — this is where liveness is confirmed, so
|
|
2885
|
+
// (#321) — this is where liveness is confirmed, so the manage-your-apps
|
|
2886
|
+
// invitation is honest to make.
|
|
2885
2887
|
text: `Live${url ? `: ${url}` : ""} — the build finished. Give the user the URL.\n` +
|
|
2886
|
-
`
|
|
2888
|
+
`Give the user this link so they can see and manage all their apps in their grid: ${consoleGridUrl(gridSlug)}`,
|
|
2887
2889
|
structured: { status: "success", live: true, ...(url ? { url } : {}), console_url: consoleGridUrl(gridSlug) },
|
|
2888
2890
|
};
|
|
2889
2891
|
}
|
package/src/tools/register.js
CHANGED
|
@@ -133,9 +133,10 @@ export function registerTools(server, ctx) {
|
|
|
133
133
|
|
|
134
134
|
// ── Widget resources (web edition, ChatGPT Apps SDK) ──────────────────────
|
|
135
135
|
if (ctx.edition === "web") {
|
|
136
|
-
// ChatGPT Apps-SDK widgets. mimeType
|
|
137
|
-
// (
|
|
138
|
-
//
|
|
136
|
+
// ChatGPT Apps-SDK widgets. mimeType is CHATGPT_WIDGET_MIME, which since
|
|
137
|
+
// SEP-1865 (2026-01-26) IS `text/html;profile=mcp-app` — the standard MCP-Apps
|
|
138
|
+
// type. The old `text/html+skybridge` was retired and was the black frame
|
|
139
|
+
// (#308); the hosts stay separated by binding, not MIME. widgetAccessible
|
|
139
140
|
// lets the widget call tools back over window.openai.callTool (the visibility
|
|
140
141
|
// pills call grid_visibility). These make no direct network requests, so no
|
|
141
142
|
// openai/widgetCSP is declared — the default deny is correct.
|
|
@@ -538,11 +539,12 @@ export function registerTools(server, ctx) {
|
|
|
538
539
|
// live at a public URL in place. Versions + grid rollback exist, but the
|
|
539
540
|
// honest MCP annotation for overwrite-live-state is destructive.
|
|
540
541
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
542
|
+
// Bound via openai/outputTemplate, which ChatGPT honors as a compatibility
|
|
543
|
+
// alias for `_meta.ui.resourceUri` and which Claude ignores — so this
|
|
544
|
+
// widget renders in ChatGPT only. Deliberately NOT `_meta.ui.resourceUri`:
|
|
545
|
+
// since SEP-1865 both hosts read THAT key, so binding there would render the
|
|
546
|
+
// widget in Claude too. The two mechanisms stay independent because they
|
|
547
|
+
// bind on different keys, not different MIMEs (#308/#303).
|
|
546
548
|
...(ctx.edition === "web" && APPS_WIDGETS_ENABLED ? {
|
|
547
549
|
_meta: { "openai/outputTemplate": LIVE_RESULT_URI },
|
|
548
550
|
} : {}),
|
|
@@ -717,10 +719,11 @@ export function registerTools(server, ctx) {
|
|
|
717
719
|
login_url: z.string().describe("URL to open in a browser to complete sign-in."),
|
|
718
720
|
},
|
|
719
721
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
720
|
-
// Bind the SEP-1865 sign-in card on the web edition (Claude web)
|
|
721
|
-
//
|
|
722
|
-
// and
|
|
723
|
-
//
|
|
722
|
+
// Bind the SEP-1865 sign-in card on the web edition (Claude web) via the
|
|
723
|
+
// standard `_meta.ui.resourceUri` key. It carries NO openai/outputTemplate
|
|
724
|
+
// and is not gated by MCP_APPS_WIDGETS, so it is the Claude sign-in card;
|
|
725
|
+
// a client without the UI extension ignores _meta.ui and gets the
|
|
726
|
+
// text-first result below. NOT gated behind MCP_APPS_WIDGETS — a
|
|
724
727
|
// second, independent mechanism (see constants.js). The result shape is
|
|
725
728
|
// unchanged: content[0] stays the sign-in URL text so the link is never
|
|
726
729
|
// unreachable if the card does not render.
|
|
@@ -865,7 +868,7 @@ export function registerTools(server, ctx) {
|
|
|
865
868
|
status: z.string().describe("The build status: success | failed | building | queued | unknown."),
|
|
866
869
|
live: z.boolean().describe("True only when the build finished successfully and the URL serves."),
|
|
867
870
|
url: z.string().optional().describe("The live URL, when known."),
|
|
868
|
-
console_url: z.string().optional().describe("On success:
|
|
871
|
+
console_url: z.string().optional().describe("On success: a link to see and manage all the apps in the grid, in the console."),
|
|
869
872
|
error: z.string().optional().describe("User-language failure reason, when the build failed."),
|
|
870
873
|
build_log_tail: z.string().optional().describe("Sanitized tail of the Cloud Build log (the real error), when the build failed."),
|
|
871
874
|
suggested_fix: z.string().optional().describe("A suggested fix for the failure, when the platform provides one."),
|
|
@@ -1194,6 +1197,13 @@ export function registerTools(server, ctx) {
|
|
|
1194
1197
|
error: z.boolean().optional().describe("True when the grid listing failed (network/auth error). The text carries a human-readable message. Do NOT treat this as needs_grid_create."),
|
|
1195
1198
|
},
|
|
1196
1199
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1200
|
+
// ChatGPT Apps-SDK card, same binding as grid_plug: grid_hello returns a
|
|
1201
|
+
// runPlug result, so it deserves the same live-result widget. Without this a
|
|
1202
|
+
// grid_hello success renders no card even when the MIME is correct (#308).
|
|
1203
|
+
// openai/outputTemplate is ChatGPT-only; Claude ignores it, so #303 is safe.
|
|
1204
|
+
...(ctx.edition === "web" && APPS_WIDGETS_ENABLED ? {
|
|
1205
|
+
_meta: { "openai/outputTemplate": LIVE_RESULT_URI },
|
|
1206
|
+
} : {}),
|
|
1197
1207
|
},
|
|
1198
1208
|
async (input) => {
|
|
1199
1209
|
try {
|