@cloudgrid-io/mcp 0.20.27 → 0.20.28
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 +4 -2
- package/src/index.js +19 -0
- package/src/staleness.js +71 -0
- package/src/tools/deploy.js +123 -2
- package/src/tools/register.js +52 -2
- package/src/tools.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.28",
|
|
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": {
|
|
@@ -51,7 +51,9 @@
|
|
|
51
51
|
"build:mcpb": "node scripts/build-mcpb.mjs",
|
|
52
52
|
"test:widgets": "node test/apps-widgets.test.mjs",
|
|
53
53
|
"test:fork": "node test/fork.test.mjs",
|
|
54
|
-
"test:set-sharing": "node test/set-sharing.test.mjs"
|
|
54
|
+
"test:set-sharing": "node test/set-sharing.test.mjs",
|
|
55
|
+
"test:check-deploy": "node test/check-deploy.test.mjs",
|
|
56
|
+
"test:staleness": "node test/staleness.test.mjs"
|
|
55
57
|
},
|
|
56
58
|
"dependencies": {
|
|
57
59
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
package/src/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { randomUUID } from "node:crypto";
|
|
|
19
19
|
import { createSessionLogger } from "./session-logger.js";
|
|
20
20
|
import { createSink } from "./log-sink.js";
|
|
21
21
|
import { INSTRUCTIONS_LOCAL } from "./playbook.js";
|
|
22
|
+
import { checkForNewerVersion } from "./staleness.js";
|
|
22
23
|
|
|
23
24
|
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url)));
|
|
24
25
|
|
|
@@ -32,6 +33,24 @@ const ctx = {
|
|
|
32
33
|
savedLocationNote: () => `Credentials saved to ${credentialsPath()}.`,
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
// Boot-time staleness self-check (local edition only — hosted auto-updates on
|
|
37
|
+
// cutover). Fire-and-forget with a short timeout: offline or slow → stays null.
|
|
38
|
+
// Surfaced in grid_start's live context so the model relays the reinstall note
|
|
39
|
+
// in-session; also logged to stderr for the Desktop MCP log.
|
|
40
|
+
ctx.staleness = null;
|
|
41
|
+
checkForNewerVersion(version)
|
|
42
|
+
.then((info) => {
|
|
43
|
+
if (info?.behind) {
|
|
44
|
+
ctx.staleness = info;
|
|
45
|
+
console.error(
|
|
46
|
+
`cloudgrid-mcp: this MCP is v${info.current}; latest is v${info.latest}. ` +
|
|
47
|
+
`The .mcpb Desktop extension never auto-updates — reinstall the latest from ` +
|
|
48
|
+
`https://github.com/cloudgrid-io/mcp/releases/latest`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.catch(() => {});
|
|
53
|
+
|
|
35
54
|
// QA session log (dark by default: no CLOUDGRID_QA_SLACK_WEBHOOK → null → no-op).
|
|
36
55
|
// The stdio process IS the session. user_request is forwarded by Claude Code via
|
|
37
56
|
// CLOUDGRID_USER_REQUEST when present.
|
package/src/staleness.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Boot-time staleness self-check for the LOCAL edition.
|
|
2
|
+
//
|
|
3
|
+
// The .mcpb Desktop extension is a frozen snapshot: it never auto-updates, so
|
|
4
|
+
// an installed bundle silently rots (a pre-0.20.17 bundle crashed on boot for a
|
|
5
|
+
// week; stale installs pinned old CLIs that the API rejects). The hosted
|
|
6
|
+
// edition auto-updates on cutover and never needs this.
|
|
7
|
+
//
|
|
8
|
+
// The check is strictly best-effort: one fetch to the npm registry with a short
|
|
9
|
+
// timeout, fire-and-forget at boot. Offline, slow, or erroring → null, silently.
|
|
10
|
+
// The result is surfaced in TWO places: stderr (Desktop MCP logs) and the
|
|
11
|
+
// grid_start live context, where the model can relay it to the user in-session
|
|
12
|
+
// — turning silent rot into a one-line user action.
|
|
13
|
+
|
|
14
|
+
const REGISTRY_LATEST_URL = "https://registry.npmjs.org/@cloudgrid-io/mcp/latest";
|
|
15
|
+
const CHECK_TIMEOUT_MS = 2500;
|
|
16
|
+
|
|
17
|
+
// Compare two semver strings (no prerelease handling — releases are plain
|
|
18
|
+
// X.Y.Z). Returns -1 / 0 / 1 for a < b / a == b / a > b; null if unparsable.
|
|
19
|
+
export function compareSemver(a, b) {
|
|
20
|
+
const pa = String(a ?? "").split(".").map((n) => Number.parseInt(n, 10));
|
|
21
|
+
const pb = String(b ?? "").split(".").map((n) => Number.parseInt(n, 10));
|
|
22
|
+
if (pa.length !== 3 || pb.length !== 3 || pa.some(Number.isNaN) || pb.some(Number.isNaN)) return null;
|
|
23
|
+
for (let i = 0; i < 3; i++) {
|
|
24
|
+
if (pa[i] < pb[i]) return -1;
|
|
25
|
+
if (pa[i] > pb[i]) return 1;
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One best-effort registry check. Resolves { current, latest, behind } when the
|
|
32
|
+
* registry answered and `current` is older than `latest`; resolves null in every
|
|
33
|
+
* other case (up to date, ahead, offline, timeout, bad payload). Never throws.
|
|
34
|
+
*/
|
|
35
|
+
export async function checkForNewerVersion(current, { fetchImpl = fetch, timeoutMs = CHECK_TIMEOUT_MS } = {}) {
|
|
36
|
+
try {
|
|
37
|
+
const ac = new AbortController();
|
|
38
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
39
|
+
let res;
|
|
40
|
+
try {
|
|
41
|
+
res = await fetchImpl(REGISTRY_LATEST_URL, { signal: ac.signal });
|
|
42
|
+
} finally {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
if (!res?.ok) return null;
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
const latest = data?.version;
|
|
48
|
+
if (typeof latest !== "string") return null;
|
|
49
|
+
if (compareSemver(current, latest) === -1) {
|
|
50
|
+
return { current, latest, behind: true };
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The one-line, model-relayable note grid_start appends when the local MCP is
|
|
60
|
+
* stale. Kept short and §23-plain; names the .mcpb non-auto-update property so
|
|
61
|
+
* the model explains WHY a reinstall is needed.
|
|
62
|
+
*/
|
|
63
|
+
export function stalenessNote(staleness) {
|
|
64
|
+
if (!staleness?.behind) return null;
|
|
65
|
+
return (
|
|
66
|
+
`Note: this local CloudGrid MCP is v${staleness.current}; the latest is v${staleness.latest}. ` +
|
|
67
|
+
`If it is installed as the Claude Desktop extension (.mcpb), it never auto-updates — ` +
|
|
68
|
+
`tell the user to reinstall the latest cloudgrid.mcpb from https://github.com/cloudgrid-io/mcp/releases/latest ` +
|
|
69
|
+
`(npx installs pick up the latest on their next cold start).`
|
|
70
|
+
);
|
|
71
|
+
}
|
package/src/tools/deploy.js
CHANGED
|
@@ -1401,6 +1401,10 @@ export async function runPlug(ctx, input, deps = {}) {
|
|
|
1401
1401
|
// Kind lets grid_visibility route to the right visibility surface
|
|
1402
1402
|
// (inspirations vs runtime entities) without a re-detect round-trip.
|
|
1403
1403
|
kind: data.detection?.kind ?? data.kind ?? null,
|
|
1404
|
+
// poll_url lets grid_check_deploy default to this session's build with
|
|
1405
|
+
// no arguments ("is it live yet?" needs no ids).
|
|
1406
|
+
poll_url: data.poll_url ?? null,
|
|
1407
|
+
grid: data.grid ?? null,
|
|
1404
1408
|
owner_token: useAnonWire ? (freshOwnerToken ?? ownerToken ?? null) : null,
|
|
1405
1409
|
};
|
|
1406
1410
|
}
|
|
@@ -1414,6 +1418,32 @@ export async function runPlug(ctx, input, deps = {}) {
|
|
|
1414
1418
|
ctx.state.lastAnonClaim = null;
|
|
1415
1419
|
}
|
|
1416
1420
|
|
|
1421
|
+
// Confirm-before-claiming-live: a runtime build is async (status "building" +
|
|
1422
|
+
// poll_url). Rather than returning immediately — which trained models to
|
|
1423
|
+
// promise URLs that then 502'd — poll the deploy trace server-side for a short
|
|
1424
|
+
// budget. Fast builds come back "live" in this same tool call; a build that
|
|
1425
|
+
// outlives the budget returns "building" with explicit do-not-claim-live copy;
|
|
1426
|
+
// a failed build surfaces the platform's user-language error instead of a URL.
|
|
1427
|
+
if ((data.status === "building" || data.poll_url) && data.poll_url && !useAnonWire) {
|
|
1428
|
+
const verdict = await pollDeployTrace(ctx, {
|
|
1429
|
+
pollUrl: data.poll_url,
|
|
1430
|
+
grid: data.grid,
|
|
1431
|
+
budgetMs: ctx.deployPollBudgetMs ?? DEPLOY_POLL_BUDGET_MS,
|
|
1432
|
+
intervalMs: ctx.deployPollIntervalMs ?? DEPLOY_POLL_INTERVAL_MS,
|
|
1433
|
+
});
|
|
1434
|
+
if (verdict.status === "success") {
|
|
1435
|
+
data.status = "live";
|
|
1436
|
+
data.poll_url = undefined;
|
|
1437
|
+
} else if (verdict.status === "failed") {
|
|
1438
|
+
const msg = verdict.error || "The build failed.";
|
|
1439
|
+
throw new Error(
|
|
1440
|
+
`Deploy failed (trace ${data.trace_id ?? "n/a"}): ${msg} The URL is NOT live — do not give it to the user as working.`,
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
// Anything else (still building / poll unreachable): fall through to the
|
|
1444
|
+
// building wording below — the do-not-claim-live path.
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1417
1447
|
const structured = {
|
|
1418
1448
|
...(data.entity_id ? { entity_id: data.entity_id } : {}),
|
|
1419
1449
|
...(data.slug ? { slug: data.slug } : {}),
|
|
@@ -1456,10 +1486,16 @@ export async function runPlug(ctx, input, deps = {}) {
|
|
|
1456
1486
|
? `Building (async): ${url} — the update is deploying, not live yet.`
|
|
1457
1487
|
: `Building (async): ${url} — the deploy is in progress, not live yet.`,
|
|
1458
1488
|
);
|
|
1489
|
+
// Point at a tool that exists on THIS edition. grid_status is CLI-wrapping,
|
|
1490
|
+
// local-only — telling a hosted (ChatGPT/claude.ai) model to "run
|
|
1491
|
+
// grid_status" dead-ends (observed live: a hosted session had no status
|
|
1492
|
+
// tool and blind-polled the public URL into a 502). grid_check_deploy is
|
|
1493
|
+
// direct-API and registered on both editions.
|
|
1494
|
+
const checkHint = ctx.edition === "web" ? "grid_check_deploy" : "grid_check_deploy (or grid_status)";
|
|
1459
1495
|
lines.push(
|
|
1460
1496
|
data.poll_url
|
|
1461
|
-
? `
|
|
1462
|
-
:
|
|
1497
|
+
? `Call ${checkHint} until status is "success" (trace ${data.trace_id ?? "n/a"}). Do NOT tell the user it is live until then.`
|
|
1498
|
+
: `Call ${checkHint} until it is ready. Do NOT tell the user it is live until then.`,
|
|
1463
1499
|
);
|
|
1464
1500
|
} else if (isEdit) {
|
|
1465
1501
|
lines.push(`Updated in place: ${url}`);
|
|
@@ -1724,6 +1760,91 @@ export async function runVisibility(ctx, { target, visibility, kind, org }) {
|
|
|
1724
1760
|
throw new Error(`Visibility change failed (HTTP ${last?.res?.status}): ${msg}${hint ? ` ${hint}` : ""}`);
|
|
1725
1761
|
}
|
|
1726
1762
|
|
|
1763
|
+
// ── Deploy-trace polling (confirm-before-claiming-live) ─────────────────────
|
|
1764
|
+
// A runtime build is async: POST /api/v2/plug answers 202 { status: "building",
|
|
1765
|
+
// poll_url: "/deploys/<trace_id>" } and GET {API}/deploys/<trace_id> (org-authed)
|
|
1766
|
+
// reports { status, error } until a terminal "success" | "failed". Budget below
|
|
1767
|
+
// is deliberately under typical host tool timeouts; slower builds fall back to
|
|
1768
|
+
// the explicit do-not-claim-live wording + grid_check_deploy.
|
|
1769
|
+
const DEPLOY_POLL_BUDGET_MS = 45_000;
|
|
1770
|
+
const DEPLOY_POLL_INTERVAL_MS = 3_000;
|
|
1771
|
+
const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1772
|
+
|
|
1773
|
+
// GET one deploy-trace snapshot. Returns { status, error } — status is the
|
|
1774
|
+
// server's word ("queued"/"building"/"success"/"failed"), error is the
|
|
1775
|
+
// user-language message when failed. Throws only on a non-OK HTTP response.
|
|
1776
|
+
async function fetchDeployTrace(ctx, { pollUrl, grid }) {
|
|
1777
|
+
const token = await ctx.getToken();
|
|
1778
|
+
const headers = {};
|
|
1779
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1780
|
+
const orgSlug = grid || (await ctx.getActiveGrid());
|
|
1781
|
+
if (orgSlug) {
|
|
1782
|
+
headers["X-CloudGrid-Grid"] = orgSlug;
|
|
1783
|
+
headers["X-CloudGrid-Org"] = orgSlug;
|
|
1784
|
+
}
|
|
1785
|
+
const path = pollUrl.startsWith("http") ? pollUrl : `${API_BASE}${pollUrl.startsWith("/") ? "" : "/"}${pollUrl}`;
|
|
1786
|
+
const res = await fetch(path, { headers });
|
|
1787
|
+
const raw = await res.text();
|
|
1788
|
+
let data = null;
|
|
1789
|
+
try { data = JSON.parse(raw); } catch { /* handled below */ }
|
|
1790
|
+
if (!res.ok) {
|
|
1791
|
+
throw new Error(`Deploy status check failed (HTTP ${res.status}): ${data?.error?.message || data?.error || raw || res.status}`);
|
|
1792
|
+
}
|
|
1793
|
+
return {
|
|
1794
|
+
status: data?.status ?? "unknown",
|
|
1795
|
+
error: data?.error?.message_user || data?.error?.message || null,
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// Poll until terminal or the budget runs out. Never throws — an unreachable
|
|
1800
|
+
// poll endpoint degrades to { status: "unknown" } and the caller keeps the
|
|
1801
|
+
// do-not-claim-live wording (the safe direction).
|
|
1802
|
+
export async function pollDeployTrace(ctx, { pollUrl, grid, budgetMs = DEPLOY_POLL_BUDGET_MS, intervalMs = DEPLOY_POLL_INTERVAL_MS }) {
|
|
1803
|
+
const start = Date.now();
|
|
1804
|
+
let last = { status: "unknown", error: null };
|
|
1805
|
+
for (;;) {
|
|
1806
|
+
try {
|
|
1807
|
+
last = await fetchDeployTrace(ctx, { pollUrl, grid });
|
|
1808
|
+
} catch {
|
|
1809
|
+
// transient/unauthorized/network — keep last, retry within budget
|
|
1810
|
+
}
|
|
1811
|
+
if (last.status === "success" || last.status === "failed") return last;
|
|
1812
|
+
if (Date.now() - start + intervalMs > budgetMs) return last;
|
|
1813
|
+
await sleepMs(intervalMs);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// grid_check_deploy — one authed status check for an async build. Direct API,
|
|
1818
|
+
// both editions: this is the status verb hosted (ChatGPT/claude.ai) sessions
|
|
1819
|
+
// were missing — they had no way to confirm a runtime build came live and
|
|
1820
|
+
// blind-polled the public URL into 502s.
|
|
1821
|
+
export async function runCheckDeploy(ctx, { poll_url, grid } = {}) {
|
|
1822
|
+
const pollUrl = poll_url || ctx.state.lastDrop?.poll_url;
|
|
1823
|
+
if (!pollUrl) {
|
|
1824
|
+
throw new Error(
|
|
1825
|
+
"No build to check. Pass poll_url from a grid_deploy result, or deploy something first in this session. (Instant inspiration deploys are live on return and have no poll_url.)",
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
const verdict = await fetchDeployTrace(ctx, { pollUrl, grid: grid || ctx.state.lastDrop?.grid });
|
|
1829
|
+
const url = ctx.state.lastDrop?.url;
|
|
1830
|
+
if (verdict.status === "success") {
|
|
1831
|
+
return {
|
|
1832
|
+
text: `Live${url ? `: ${url}` : ""} — the build finished. Give the user the URL.`,
|
|
1833
|
+
structured: { status: "success", live: true, ...(url ? { url } : {}) },
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
if (verdict.status === "failed") {
|
|
1837
|
+
return {
|
|
1838
|
+
text: `The build FAILED: ${verdict.error || "no reason reported"}. The URL is not live — do not give it to the user as working. Fix the app (or re-deploy) and try again.`,
|
|
1839
|
+
structured: { status: "failed", live: false, ...(verdict.error ? { error: verdict.error } : {}) },
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
return {
|
|
1843
|
+
text: `Still ${verdict.status === "unknown" ? "building (status unavailable)" : verdict.status} — NOT live yet. Do not tell the user it is live. Wait ~15s and call grid_check_deploy again.`,
|
|
1844
|
+
structured: { status: verdict.status, live: false },
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1727
1848
|
// Max HTML we'll return inline from runSource. Past this, we return the first
|
|
1728
1849
|
// slice with truncated:true — a re-plug needs the complete document, so a
|
|
1729
1850
|
// truncated body is a signal the drop is likely multi-file rather than a single
|
package/src/tools/register.js
CHANGED
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
runDownload,
|
|
27
27
|
runVisibility,
|
|
28
28
|
runSource,
|
|
29
|
+
runCheckDeploy,
|
|
29
30
|
} from "./deploy.js";
|
|
31
|
+
import { stalenessNote } from "../staleness.js";
|
|
30
32
|
|
|
31
33
|
// Build the argv for grid_create_project → `grid new` (CLI >= 0.15.14; `init`
|
|
32
34
|
// is a deprecated alias we no longer author). Modern shape: `new <slug>` with
|
|
@@ -643,6 +645,36 @@ export function registerTools(server, ctx) {
|
|
|
643
645
|
},
|
|
644
646
|
);
|
|
645
647
|
|
|
648
|
+
// Deploy-status check — both editions. The status verb hosted sessions were
|
|
649
|
+
// missing: grid_status wraps the CLI (local-only), so a ChatGPT/claude.ai
|
|
650
|
+
// session had NO way to confirm an async runtime build came live and
|
|
651
|
+
// blind-polled the public URL into 502s. Direct API, no CLI.
|
|
652
|
+
reg(
|
|
653
|
+
"grid_check_deploy",
|
|
654
|
+
{
|
|
655
|
+
description:
|
|
656
|
+
"Check whether an async runtime-app build has finished and the app is live. Call this after grid_deploy returns status \"building\" — repeat every ~15s until it reports success or failed, and do NOT tell the user the app is live until it does. Defaults to this session's last deploy; pass poll_url from a grid_deploy result to check another. Requires sign-in (builds are owned). Calls the API directly — works on every edition, including hosted.",
|
|
657
|
+
inputSchema: {
|
|
658
|
+
poll_url: z.string().optional().describe("The poll_url from a grid_deploy result. Defaults to this session's last deploy."),
|
|
659
|
+
grid: z.string().optional().describe("Grid of the entity. Defaults to the deploy's grid, then the active grid."),
|
|
660
|
+
},
|
|
661
|
+
outputSchema: {
|
|
662
|
+
status: z.string().describe("The build status: success | failed | building | queued | unknown."),
|
|
663
|
+
live: z.boolean().describe("True only when the build finished successfully and the URL serves."),
|
|
664
|
+
url: z.string().optional().describe("The live URL, when known."),
|
|
665
|
+
error: z.string().optional().describe("User-language failure reason, when the build failed."),
|
|
666
|
+
},
|
|
667
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
668
|
+
},
|
|
669
|
+
async (input) => {
|
|
670
|
+
try {
|
|
671
|
+
return okResult(await runCheckDeploy(ctx, input || {}));
|
|
672
|
+
} catch (err) {
|
|
673
|
+
return fail(err.message);
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
);
|
|
677
|
+
|
|
646
678
|
// Org listing — both editions.
|
|
647
679
|
reg(
|
|
648
680
|
"grid_list_grids",
|
|
@@ -724,6 +756,13 @@ export function registerTools(server, ctx) {
|
|
|
724
756
|
.object({
|
|
725
757
|
active_grid: z.string().nullable().describe("The user's active grid/org slug, or null."),
|
|
726
758
|
signed_in: z.boolean().describe("Whether the current session is signed in."),
|
|
759
|
+
update_available: z
|
|
760
|
+
.object({
|
|
761
|
+
current: z.string().describe("This MCP's version."),
|
|
762
|
+
latest: z.string().describe("Latest published version."),
|
|
763
|
+
})
|
|
764
|
+
.optional()
|
|
765
|
+
.describe("Present when this local MCP is behind the latest release — relay the reinstall note to the user."),
|
|
727
766
|
})
|
|
728
767
|
.optional()
|
|
729
768
|
.describe("Live context from the current session."),
|
|
@@ -744,17 +783,28 @@ export function registerTools(server, ctx) {
|
|
|
744
783
|
} catch {
|
|
745
784
|
activeGrid = null;
|
|
746
785
|
}
|
|
786
|
+
// Staleness (local edition): populated fire-and-forget at boot. The
|
|
787
|
+
// .mcpb never auto-updates, so this is where silent rot becomes a
|
|
788
|
+
// one-line user action — the model relays the note in-session.
|
|
789
|
+
const stale = stalenessNote(ctx.staleness);
|
|
747
790
|
const structured = {
|
|
748
791
|
playbook: PLAYBOOK,
|
|
749
792
|
workflows,
|
|
750
|
-
context: {
|
|
793
|
+
context: {
|
|
794
|
+
active_grid: activeGrid,
|
|
795
|
+
signed_in: signedIn,
|
|
796
|
+
...(ctx.staleness?.behind
|
|
797
|
+
? { update_available: { current: ctx.staleness.current, latest: ctx.staleness.latest } }
|
|
798
|
+
: {}),
|
|
799
|
+
},
|
|
751
800
|
};
|
|
752
801
|
const wfLines = workflows.length
|
|
753
802
|
? workflows.map((w) => ` - ${w.name}: ${w.when || w.summary}`).join("\n")
|
|
754
803
|
: " (none available)";
|
|
755
804
|
const text =
|
|
756
805
|
`${PLAYBOOK}\n\nAvailable workflows:\n${wfLines}\n\n` +
|
|
757
|
-
`Next: match the intent to a workflow and call grid_get_template({kind:"workflow", name})
|
|
806
|
+
`Next: match the intent to a workflow and call grid_get_template({kind:"workflow", name}).` +
|
|
807
|
+
(stale ? `\n\n${stale}` : "");
|
|
758
808
|
return okResult({ text, structured });
|
|
759
809
|
},
|
|
760
810
|
);
|
package/src/tools.js
CHANGED