@cloudgrid-io/mcp 0.18.0 → 0.19.1
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 +1 -1
- package/src/tools.js +139 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "MCP server for CloudGrid. Three editions: a local stdio server (full toolset), a hosted web server (light, CLI-free toolset), and a public docs server (search_cloudgrid_documentation) over MCP Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/tools.js
CHANGED
|
@@ -100,6 +100,10 @@ Operating rules:
|
|
|
100
100
|
12. To choose what to build: match the request against the workflow when: triggers and the capability-map (grid_fetch({kind:"doc", name:"capability-map"})). Pick the template whose needs: matches what the app requires (persistence → database; scheduled → cron; etc.). Classify the ARTIFACT to pick the deploy: ONE self-contained HTML page (a single file — CSS+JS inline, images/fonts as data: URIs; that is the normal hosted output) → an inspiration — instant, ANY edition, deploy via grid_plug with the inline html param. Only genuinely SEPARATE files/folders (a real assets/ dir, separate .css/.js files, multiple pages, a SPA build) — OR anything needing needs: (data/server/LLM/cron) → a runtime app — grid_plug on a linked folder with a cloudgrid.yaml, local edition only, async build.
|
|
101
101
|
13. Before writing a cloudgrid.yaml, fetch the reference: grid_fetch({kind:"doc", name:"cloudgrid-yaml"}) — it has the full schema and the needs: vocabulary. Declare infrastructure with needs: (the deployer injects from it): needs: { database: true } → Mongo (DATABASE_MONGODB_URL); needs: { cache: true } → Redis; scheduled work → a service of type: cron (Python or Node). Use needs: OR requires:, never both — declaring both is rejected.
|
|
102
102
|
14. Databases — CloudGrid supports both, so never tell the user to self-host. Managed: needs: { database: true } provisions Mongo and injects DATABASE_MONGODB_URL. Bring-your-own (they already run Postgres / MySQL / MongoDB / Supabase / Neon / PlanetScale / etc.): needs: { database: { tier: external, secret: MY_DB } } plus grid secrets set MY_DB=<connection-string> — the connection string lives in env SECRETS, never committed. If asked "what databases does CloudGrid support?": all of them — the managed CloudGrid database out of the box, or bring your own via keys — ask which they want.
|
|
103
|
+
15. Editing an existing thing from just its URL (a fresh chat, no prior context — e.g. "change the background to green here <url>"): call grid_source(url) first. It resolves the entity_id and returns the current HTML plus its kind, single_html, capabilities, and replug_handle — read those to pick the branch:
|
|
104
|
+
- Single-HTML inspiration you can re-plug (single_html: true and capabilities.replug: true): edit the returned HTML and call grid_plug with target_entity_id (or grid+slug — the replug_handle) to update the SAME URL in place. This works on every edition, including hosted.
|
|
105
|
+
- Multi-file app or agent (kind is app or agent, or single_html: false): do NOT try to edit it as one inline HTML file. Tell the user it is a multi-file <kind>, give them the entity_id and the source (source_download_url), and explain that rebuilding it needs the local edition (Claude Desktop/Code) or the CLI — the hosted server cannot rebuild a multi-file app.
|
|
106
|
+
- Not yours (capabilities.replug: false, reason not_owner): do NOT attempt a re-plug. Offer to fork it into the user's own grid with grid_fork and edit the copy instead.
|
|
103
107
|
|
|
104
108
|
Deploy is via grid_plug on every edition: for a single HTML page pass it inline as the html param (works on the hosted MCP too); for a multi-file app write the files and pass a folder path (local MCP / CLI). A single HTML page deploys synchronously as an inspiration, so you get a URL right away.`;
|
|
105
109
|
|
|
@@ -1520,6 +1524,7 @@ export async function runPlug(ctx, input, deps = {}) {
|
|
|
1520
1524
|
cloudgrid_yaml,
|
|
1521
1525
|
target_entity_id,
|
|
1522
1526
|
grid,
|
|
1527
|
+
slug,
|
|
1523
1528
|
hints,
|
|
1524
1529
|
anon,
|
|
1525
1530
|
owner_token,
|
|
@@ -1575,7 +1580,19 @@ export async function runPlug(ctx, input, deps = {}) {
|
|
|
1575
1580
|
);
|
|
1576
1581
|
}
|
|
1577
1582
|
|
|
1578
|
-
|
|
1583
|
+
// grid+slug re-plug handle (the pickup contract's `replug_handle`): when no
|
|
1584
|
+
// explicit target_entity_id was given but a grid+slug pair is, resolve it to
|
|
1585
|
+
// an existing entity_id and re-plug that in place. A slug that does NOT resolve
|
|
1586
|
+
// to an existing entity → targetEntityId stays empty → this is a CREATE (no
|
|
1587
|
+
// false-positive re-plug). target_entity_id remains the primary/documented
|
|
1588
|
+
// handle. Best-effort resolve (pickup contract); never fetches the public URL.
|
|
1589
|
+
let targetEntityId = target_entity_id;
|
|
1590
|
+
if ((typeof targetEntityId !== "string" || targetEntityId.length === 0) && grid && slug) {
|
|
1591
|
+
const resolved = await resolveEntityViaPickup(ctx, { target: slug, grid });
|
|
1592
|
+
if (resolved?.entity_id) targetEntityId = resolved.entity_id;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
const isEdit = typeof targetEntityId === "string" && targetEntityId.length > 0;
|
|
1579
1596
|
|
|
1580
1597
|
// An inspiration edit content-versions the FIRST uploaded artifact — when a
|
|
1581
1598
|
// multi-file folder rides a re-plug, put the primary entry first so the edit
|
|
@@ -1595,10 +1612,10 @@ export async function runPlug(ctx, input, deps = {}) {
|
|
|
1595
1612
|
if (isEdit && !ownerToken) {
|
|
1596
1613
|
// Recover the owner token from session state when re-plugging the drop this
|
|
1597
1614
|
// session made anonymously.
|
|
1598
|
-
if (ctx.state.lastDrop?.entity_id ===
|
|
1615
|
+
if (ctx.state.lastDrop?.entity_id === targetEntityId && ctx.state.lastDrop.owner_token) {
|
|
1599
1616
|
ownerToken = ctx.state.lastDrop.owner_token;
|
|
1600
1617
|
} else if (
|
|
1601
|
-
ctx.state.lastAnonClaim?.entity_id ===
|
|
1618
|
+
ctx.state.lastAnonClaim?.entity_id === targetEntityId &&
|
|
1602
1619
|
ctx.state.lastAnonClaim.token
|
|
1603
1620
|
) {
|
|
1604
1621
|
ownerToken = ctx.state.lastAnonClaim.token;
|
|
@@ -1686,7 +1703,7 @@ export async function runPlug(ctx, input, deps = {}) {
|
|
|
1686
1703
|
form.append("artifact", new Blob([a.buffer], { type: a.type || "application/octet-stream" }), a.path);
|
|
1687
1704
|
}
|
|
1688
1705
|
if (isEdit) {
|
|
1689
|
-
form.append("target_entity_id",
|
|
1706
|
+
form.append("target_entity_id", targetEntityId);
|
|
1690
1707
|
if (useAnonWire) {
|
|
1691
1708
|
form.append("owner_token", ownerToken);
|
|
1692
1709
|
} else {
|
|
@@ -2038,8 +2055,10 @@ function isCloudgridHost(hostname) {
|
|
|
2038
2055
|
}
|
|
2039
2056
|
|
|
2040
2057
|
// Shape an HTML string into the grid_source result (shared by the API-read
|
|
2041
|
-
// and the public-fetch paths). Caps the body at SOURCE_MAX_BYTES.
|
|
2042
|
-
|
|
2058
|
+
// and the public-fetch paths). Caps the body at SOURCE_MAX_BYTES. `extra` carries
|
|
2059
|
+
// optional edition metadata resolved from the pickup contract (kind, single_html,
|
|
2060
|
+
// capabilities, replug_handle, source_download_url) — merged into structured.
|
|
2061
|
+
function shapeSourceResult(sourceUrl, entityId, htmlStr, extra = {}) {
|
|
2043
2062
|
const buf = Buffer.from(htmlStr, "utf-8");
|
|
2044
2063
|
const totalBytes = buf.length;
|
|
2045
2064
|
const truncated = totalBytes > SOURCE_MAX_BYTES;
|
|
@@ -2055,7 +2074,7 @@ function shapeSourceResult(sourceUrl, entityId, htmlStr) {
|
|
|
2055
2074
|
lines.push("", html);
|
|
2056
2075
|
return {
|
|
2057
2076
|
text: lines.join("\n"),
|
|
2058
|
-
structured: { url: sourceUrl, entity_id: entityId ?? null, bytes: totalBytes, truncated, html },
|
|
2077
|
+
structured: { url: sourceUrl, entity_id: entityId ?? null, bytes: totalBytes, truncated, html, ...extra },
|
|
2059
2078
|
};
|
|
2060
2079
|
}
|
|
2061
2080
|
|
|
@@ -2082,8 +2101,17 @@ async function readInspirationSourceViaApi(ctx, { entityId, slug, grid }) {
|
|
|
2082
2101
|
{ method: "GET", headers, signal: AbortSignal.timeout(15_000) },
|
|
2083
2102
|
);
|
|
2084
2103
|
if (!res.ok) continue;
|
|
2085
|
-
|
|
2086
|
-
|
|
2104
|
+
// The route (GET /v2/inspirations/:seg/source) serves the RAW HTML bytes
|
|
2105
|
+
// as text/html — NOT a JSON { html } envelope. Read the body as text.
|
|
2106
|
+
// (Tolerate a JSON { html } shape too, in case a variant ever returns it.)
|
|
2107
|
+
const ct = (res.headers.get("content-type") || "").toLowerCase();
|
|
2108
|
+
if (ct.includes("application/json")) {
|
|
2109
|
+
const data = await res.json().catch(() => null);
|
|
2110
|
+
if (data && typeof data.html === "string") return data.html;
|
|
2111
|
+
continue;
|
|
2112
|
+
}
|
|
2113
|
+
const html = await res.text().catch(() => null);
|
|
2114
|
+
if (typeof html === "string" && html.length > 0) return html;
|
|
2087
2115
|
} catch {
|
|
2088
2116
|
// try the next attempt, then the public-fetch fallback
|
|
2089
2117
|
}
|
|
@@ -2091,6 +2119,50 @@ async function readInspirationSourceViaApi(ctx, { entityId, slug, grid }) {
|
|
|
2091
2119
|
return null;
|
|
2092
2120
|
}
|
|
2093
2121
|
|
|
2122
|
+
// Resolve a public URL / grid+slug to a REAL entity_id (+ edition metadata) via
|
|
2123
|
+
// the deployed pickup contract (POST /api/v2/entities/:target/pickup — the same
|
|
2124
|
+
// endpoint runClaim uses). Used by runSource when a bare URL arrives with no
|
|
2125
|
+
// session entity_id (a fresh chat): the contract returns
|
|
2126
|
+
// { entity_id, slug, grid, kind, single_html, capabilities, replug_handle,
|
|
2127
|
+
// source_download_url, ... }
|
|
2128
|
+
// so the agent can re-plug the SAME entity in place. This is a metadata resolve
|
|
2129
|
+
// (no claim_token in the body → no ownership transfer). Best-effort: returns null
|
|
2130
|
+
// on any failure so the caller falls back to today's behavior — and NEVER fetches
|
|
2131
|
+
// the public *.cloudgrid.io URL (the hosted pod cannot egress to it).
|
|
2132
|
+
async function resolveEntityViaPickup(ctx, { target, url, grid }) {
|
|
2133
|
+
const pathSeg = target || url;
|
|
2134
|
+
if (!pathSeg) return null;
|
|
2135
|
+
let token = null;
|
|
2136
|
+
try { token = await ctx.getToken(); } catch { /* anonymous is fine */ }
|
|
2137
|
+
const headers = { "Content-Type": "application/json" };
|
|
2138
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
2139
|
+
// Grid-native header + X-CloudGrid-Org alias (same slug) during the soak.
|
|
2140
|
+
if (grid) {
|
|
2141
|
+
headers["X-CloudGrid-Grid"] = grid;
|
|
2142
|
+
headers["X-CloudGrid-Org"] = grid;
|
|
2143
|
+
}
|
|
2144
|
+
if (ctx.state.anonCookie) headers["Cookie"] = ctx.state.anonCookie;
|
|
2145
|
+
try {
|
|
2146
|
+
const res = await fetch(
|
|
2147
|
+
`${API_BASE}/api/v2/entities/${encodeURIComponent(pathSeg)}/pickup`,
|
|
2148
|
+
{
|
|
2149
|
+
method: "POST",
|
|
2150
|
+
headers,
|
|
2151
|
+
// Send the URL as a resolution key too (the contract accepts id, slug, or
|
|
2152
|
+
// a {url} body). NO claim_token → resolve only, never a claim.
|
|
2153
|
+
body: JSON.stringify(url ? { url } : {}),
|
|
2154
|
+
signal: AbortSignal.timeout(15_000),
|
|
2155
|
+
},
|
|
2156
|
+
);
|
|
2157
|
+
if (!res.ok) return null;
|
|
2158
|
+
const data = await res.json().catch(() => null);
|
|
2159
|
+
if (data && typeof data.entity_id === "string" && data.entity_id.length > 0) return data;
|
|
2160
|
+
return null;
|
|
2161
|
+
} catch {
|
|
2162
|
+
return null;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2094
2166
|
// Fetch a drop's/inspiration's current deployed HTML inline as text so an agent
|
|
2095
2167
|
// that lost the content can edit it and re-plug in place. Resolves the fetch URL
|
|
2096
2168
|
// (explicit url → session lastDrop → composed grid+slug), then reads the HTML
|
|
@@ -2129,19 +2201,40 @@ export async function runSource(ctx, { entity_id, url, grid, slug } = {}) {
|
|
|
2129
2201
|
}
|
|
2130
2202
|
|
|
2131
2203
|
const resolvedUrl = parsed.toString();
|
|
2132
|
-
|
|
2204
|
+
let eid = entity_id ?? (last && (!entity_id || last.entity_id === entity_id) ? last.entity_id : null);
|
|
2133
2205
|
|
|
2134
|
-
//
|
|
2135
|
-
// Read the HTML from the API server-side rather than fetching the public URL.
|
|
2136
|
-
// The hosted MCP pod can reach the API but NOT the public *.cloudgrid.io
|
|
2137
|
-
// ingress ("fetch failed") — this is the fix for hosted edit-in-place. Derive
|
|
2138
|
-
// the grid/slug hint from the inspiration path URL (`grid.cloudgrid.io/slug`).
|
|
2206
|
+
// Derive the grid/slug hint from the inspiration path URL (`grid.cloudgrid.io/slug`).
|
|
2139
2207
|
const host = parsed.hostname;
|
|
2140
2208
|
const gridHint = grid ?? (host.endsWith(".cloudgrid.io") && !host.includes("--") && host.split(".").length === 3
|
|
2141
2209
|
? host.split(".")[0] : null);
|
|
2142
2210
|
const slugHint = slug ?? (parsed.pathname.replace(/^\/+/, "").split("/")[0] || null);
|
|
2211
|
+
|
|
2212
|
+
// ── URL → entity_id (fresh chat, no session) via the pickup contract ──────
|
|
2213
|
+
// When a bare URL/slug arrived with no known entity_id, resolve a REAL
|
|
2214
|
+
// entity_id (+ edition metadata) so the agent can re-plug in place. Best-
|
|
2215
|
+
// effort: a failure falls back to today's behavior (never regress the read),
|
|
2216
|
+
// and we NEVER fetch the public URL for resolution.
|
|
2217
|
+
let extra = {};
|
|
2218
|
+
if (!eid) {
|
|
2219
|
+
const pickup = await resolveEntityViaPickup(ctx, { target: slugHint, url: resolvedUrl, grid: gridHint });
|
|
2220
|
+
if (pickup?.entity_id) {
|
|
2221
|
+
eid = pickup.entity_id;
|
|
2222
|
+
extra = {
|
|
2223
|
+
...(pickup.kind ? { kind: pickup.kind } : {}),
|
|
2224
|
+
...(typeof pickup.single_html === "boolean" ? { single_html: pickup.single_html } : {}),
|
|
2225
|
+
...(pickup.capabilities ? { capabilities: pickup.capabilities } : {}),
|
|
2226
|
+
...(pickup.replug_handle ? { replug_handle: pickup.replug_handle } : {}),
|
|
2227
|
+
...(pickup.source_download_url ? { source_download_url: pickup.source_download_url } : {}),
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// ── API-first (reachable) ────────────────────────────────────────────────
|
|
2233
|
+
// Read the HTML from the API server-side rather than fetching the public URL.
|
|
2234
|
+
// The hosted MCP pod can reach the API but NOT the public *.cloudgrid.io
|
|
2235
|
+
// ingress ("fetch failed") — this is the fix for hosted edit-in-place.
|
|
2143
2236
|
const apiHtml = await readInspirationSourceViaApi(ctx, { entityId: eid, slug: slugHint, grid: gridHint });
|
|
2144
|
-
if (apiHtml != null) return shapeSourceResult(resolvedUrl, eid, apiHtml);
|
|
2237
|
+
if (apiHtml != null) return shapeSourceResult(resolvedUrl, eid, apiHtml, extra);
|
|
2145
2238
|
|
|
2146
2239
|
// ── Fallback: direct public fetch ────────────────────────────────────────
|
|
2147
2240
|
// Local edition has normal egress; last resort on hosted (e.g. a runtime app
|
|
@@ -2177,7 +2270,7 @@ export async function runSource(ctx, { entity_id, url, grid, slug } = {}) {
|
|
|
2177
2270
|
}
|
|
2178
2271
|
|
|
2179
2272
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
2180
|
-
return shapeSourceResult(resolvedUrl, eid, buf.toString("utf-8"));
|
|
2273
|
+
return shapeSourceResult(resolvedUrl, eid, buf.toString("utf-8"), extra);
|
|
2181
2274
|
}
|
|
2182
2275
|
|
|
2183
2276
|
// ── Registration ───────────────────────────────────────────────────────────────
|
|
@@ -2299,6 +2392,12 @@ export function registerTools(server, ctx) {
|
|
|
2299
2392
|
"never moves grids, but pass its home grid here when it differs from your active grid (the API " +
|
|
2300
2393
|
"checks your membership in the entity's grid). Anonymous → always the Guest grid.",
|
|
2301
2394
|
),
|
|
2395
|
+
slug: z.string().optional().describe(
|
|
2396
|
+
"Alternative RE-PLUG handle: paired with `grid`, resolves to an existing entity (the pickup " +
|
|
2397
|
+
"contract's replug_handle) and updates it in place — for a client that holds only grid+slug, not the " +
|
|
2398
|
+
"raw entity_id. target_entity_id takes precedence. A grid+slug that does NOT resolve to an existing " +
|
|
2399
|
+
"entity is treated as a CREATE (never a false-positive re-plug).",
|
|
2400
|
+
),
|
|
2302
2401
|
hints: z.object({
|
|
2303
2402
|
kind: z.enum(["inspiration", "app", "agent"]).optional().describe("Force the detected kind; omit to let the server auto-detect."),
|
|
2304
2403
|
yaml: z.string().optional().describe("An inline cloudgrid.yaml override used as a detection hint."),
|
|
@@ -2326,7 +2425,8 @@ export function registerTools(server, ctx) {
|
|
|
2326
2425
|
? "or `artifact_files` — a multi-file app inline. "
|
|
2327
2426
|
: "`path` — a local folder/file (a multi-file app); or `artifact_files` — inline files. ") +
|
|
2328
2427
|
"No target_entity_id → CREATE a new entity (inspiration/app/agent, auto-detected or hinted); " +
|
|
2329
|
-
"with target_entity_id
|
|
2428
|
+
"with target_entity_id (or grid+slug — the replug_handle, when you hold only those) → RE-PLUG: " +
|
|
2429
|
+
"update the SAME entity in place — same entity_id, same URL, same " +
|
|
2330
2430
|
"deploy history, expiry reset. The returned entity_id + url are the durable re-plug handle; persist " +
|
|
2331
2431
|
"them (plus owner_token for anonymous pages) to update the entity in later sessions. " +
|
|
2332
2432
|
"Note: in-place re-plug currently supports inspirations (HTML/static pages); to rebuild a deployed " +
|
|
@@ -2476,10 +2576,14 @@ export function registerTools(server, ctx) {
|
|
|
2476
2576
|
description:
|
|
2477
2577
|
"Retrieve the CURRENT deployed HTML of an inspiration/drop inline as text, so you can edit it and " +
|
|
2478
2578
|
"re-plug the SAME URL when you no longer have its source in context (e.g. the user asks to 'change the " +
|
|
2479
|
-
"color' of a
|
|
2480
|
-
"url (or grid+slug).
|
|
2481
|
-
"
|
|
2482
|
-
"
|
|
2579
|
+
"color' of a page — even in a fresh chat with only its URL). Defaults to this session's last drop; " +
|
|
2580
|
+
"otherwise pass the public url (or grid+slug). Given just a URL with no session, it resolves the " +
|
|
2581
|
+
"entity_id via the pickup contract and also returns the entity's kind, single_html, capabilities " +
|
|
2582
|
+
"(replug/fork), and replug_handle — read those to decide whether to edit in place (single-HTML + " +
|
|
2583
|
+
"capabilities.replug), fall back for a multi-file app/agent (use source_download_url + the local " +
|
|
2584
|
+
"edition/CLI), or offer a fork when it isn't yours. For multi-file or runtime (app/agent) source " +
|
|
2585
|
+
"bundles, use grid_download (signed tarball URLs). Reads the HTML from the API server-side; " +
|
|
2586
|
+
"read-only, creates nothing.",
|
|
2483
2587
|
inputSchema: {
|
|
2484
2588
|
entity_id: z.string().optional().describe("The drop's durable id. Defaults to this session's last drop."),
|
|
2485
2589
|
url: z.string().optional().describe("The public URL of the drop (e.g. https://<grid>.cloudgrid.io/<slug>). Defaults to this session's last drop URL."),
|
|
@@ -2492,6 +2596,19 @@ export function registerTools(server, ctx) {
|
|
|
2492
2596
|
bytes: z.number().describe("Total size of the live document in bytes."),
|
|
2493
2597
|
truncated: z.boolean().describe("True if the body exceeded 1.5MB and was cut — the drop may be multi-file."),
|
|
2494
2598
|
html: z.string().describe("The current deployed HTML (truncated to 1.5MB when oversized)."),
|
|
2599
|
+
kind: z.string().optional().describe("Resolved via the pickup contract: inspiration | app | agent."),
|
|
2600
|
+
single_html: z.boolean().optional().describe("Resolved via the pickup contract: true when this is a single editable HTML document (edit-in-place); false → multi-file."),
|
|
2601
|
+
capabilities: z.object({
|
|
2602
|
+
replug: z.boolean().describe("Whether the caller may re-plug (write) this entity in place."),
|
|
2603
|
+
fork: z.boolean().optional().describe("Whether the caller may fork it."),
|
|
2604
|
+
reason: z.string().optional().describe("Why an action is unavailable, e.g. not_owner."),
|
|
2605
|
+
}).optional().describe("Resolved via the pickup contract: what the caller may do with this entity."),
|
|
2606
|
+
replug_handle: z.object({
|
|
2607
|
+
target_entity_id: z.string().optional().describe("Pass as grid_plug's target_entity_id to re-plug in place."),
|
|
2608
|
+
grid: z.string().optional().describe("The entity's home grid slug."),
|
|
2609
|
+
slug: z.string().optional().describe("The entity's grid-scoped slug."),
|
|
2610
|
+
}).optional().describe("Resolved via the pickup contract: the durable re-plug handle."),
|
|
2611
|
+
source_download_url: z.string().optional().describe("Resolved via the pickup contract: the source-download route (used for the multi-file fallback message)."),
|
|
2495
2612
|
},
|
|
2496
2613
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
2497
2614
|
},
|