@happyvertical/smrt-cli 0.55.15 → 0.55.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/agents/app-commands.md +10 -0
- package/agents/kitchen.md +24 -4
- package/dist/{AuditLogCollection-WDHL-_AF-Cp5NKpKo.js → AuditLogCollection-WDHL-_AF-02D9mDot.js} +1 -1
- package/dist/{OidcIdentity-BUblgQLs-CqMp2Smf.js → OidcIdentity-BUblgQLs-hzB0lrxA.js} +2 -2
- package/dist/{OidcIdentityCollection-ClOyaHeF-l1ifhCWu.js → OidcIdentityCollection-ClOyaHeF-D7aAvcYW.js} +1 -1
- package/dist/{ProfileAssetCollection-CRMcjLJ--Bh5AcTQv.js → ProfileAssetCollection-CRMcjLJ--CRQsOHTZ.js} +3 -3
- package/dist/{ProfileCollection-C4INKh7Y-DH_e1QdB.js → ProfileCollection-C4INKh7Y-DUcvwlG4.js} +17 -17
- package/dist/{ProfileLinkCollection-DFfZRsgv-BGLiIASL.js → ProfileLinkCollection-DFfZRsgv-4ulMzaJO.js} +2 -2
- package/dist/{ProfileMetadataCollection-BIYKy4Y3-DSVsCaCz.js → ProfileMetadataCollection-BIYKy4Y3-BGVdmb-k.js} +1 -1
- package/dist/{ProfileMetafieldCollection-Buz7gzag-cxq_JG8K.js → ProfileMetafieldCollection-Buz7gzag-Cuuzb-Yt.js} +1 -1
- package/dist/{ProfileRelationshipCollection-C4g_QLI4-DCxIpptB.js → ProfileRelationshipCollection-C4g_QLI4-DHnLSOex.js} +1 -1
- package/dist/{ProfileTypeCollection-B7kwi1mU-BzT8PP3l.js → ProfileTypeCollection-B7kwi1mU-tffgStmE.js} +2 -2
- package/dist/app/cli.d.ts.map +1 -1
- package/dist/app/operations.d.ts.map +1 -1
- package/dist/app.js +2 -2
- package/dist/{cli-OZsH5cdL.js → cli-FDpY0gCt.js} +51 -5
- package/dist/commands/kitchen/mcp/bridge.d.ts +3 -0
- package/dist/commands/kitchen/mcp/bridge.d.ts.map +1 -0
- package/dist/commands/kitchen/mcp/preview.d.ts +8 -0
- package/dist/commands/kitchen/mcp/preview.d.ts.map +1 -0
- package/dist/commands/kitchen/mcp/server.d.ts.map +1 -1
- package/dist/commands/kitchen/mcp/tools.d.ts +21 -2
- package/dist/commands/kitchen/mcp/tools.d.ts.map +1 -1
- package/dist/commands/kitchen/mcp/ui.d.ts +40 -0
- package/dist/commands/kitchen/mcp/ui.d.ts.map +1 -0
- package/dist/commands/kitchen.d.ts.map +1 -1
- package/dist/{commands-Cut_g1l0.js → commands-Cve0qfxc.js} +30 -4
- package/dist/{dist-DoeiKxBG.js → dist-BQ5ZrH9U.js} +16 -16
- package/dist/{dist-CSWROA_j.js → dist-BQjejZ0-.js} +3 -3
- package/dist/{dist-Daxp0QSv.js → dist-BtlfVNPV.js} +4 -4
- package/dist/{dist-DuLsatO8.js → dist-C_pNF2sM.js} +2 -2
- package/dist/dist-DHRli5Sc.js +21 -0
- package/dist/index.js +12 -12
- package/dist/{oidcProvisioningPrimitives-DNP9EZlU-DNut7Q6q.js → oidcProvisioningPrimitives-DNP9EZlU-B5DmitVf.js} +3 -3
- package/dist/{portability-CES1YC2m.js → portability-BbltyqQ-.js} +1 -1
- package/dist/{resolveIdentity-CkyqvctR-BBpXD0_v.js → resolveIdentity-CkyqvctR-BDafA2yA.js} +14 -14
- package/dist/{server-Dp096tZj.js → server-C69K3w-t.js} +682 -39
- package/package.json +10 -10
- package/scripts/verify-package-types-exports.js +6 -1
- package/dist/dist-Du1UPfme.js +0 -21
|
@@ -1,12 +1,107 @@
|
|
|
1
1
|
import { c as SMRT_SCOPE, l as loadRegistryManifest, r as cliVersion, s as validateCookbookText, t as createCookbookApplier, u as resolveRecipeIndex } from "./apply-BNDIdixP.js";
|
|
2
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { validateCookbook } from "@happyvertical/smrt-core/cookbook";
|
|
7
|
+
import { createServer } from "node:http";
|
|
7
8
|
import { Server } from "@modelcontextprotocol/server";
|
|
8
9
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
9
10
|
import { buildBatchTool, checkSchema, createCookbookEngine } from "@happyvertical/smrt-core/cookbook/engine";
|
|
11
|
+
//#region src/commands/kitchen/mcp/preview.ts
|
|
12
|
+
/** Loopback-only bridge for the live SvelteKit preview during local development. */
|
|
13
|
+
async function startKitchenPreview(session, port, origin) {
|
|
14
|
+
if (!/^http:\/\/127\.0\.0\.1:\d+$/.test(origin)) throw new Error("Preview origin must be a 127.0.0.1 HTTP origin.");
|
|
15
|
+
const server = createServer(async (request, response) => {
|
|
16
|
+
response.setHeader("Cache-Control", "no-store");
|
|
17
|
+
response.setHeader("Vary", "Origin");
|
|
18
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
19
|
+
if (request.headers.origin !== origin) {
|
|
20
|
+
response.writeHead(403).end(JSON.stringify({
|
|
21
|
+
ok: false,
|
|
22
|
+
error: "Origin refused."
|
|
23
|
+
}));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
response.setHeader("Access-Control-Allow-Origin", origin);
|
|
27
|
+
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
28
|
+
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
29
|
+
if (request.method === "OPTIONS") {
|
|
30
|
+
response.writeHead(204).end();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const path = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
|
|
34
|
+
const match = /^\/plans\/(plan-[a-f0-9-]{8,36})(\/commands)?$/.exec(path);
|
|
35
|
+
if (!match || !session.previewPlans.has(match[1]) || !(request.method === "GET" && !match[2] || request.method === "POST" && match[2])) {
|
|
36
|
+
response.writeHead(404).end(JSON.stringify({
|
|
37
|
+
ok: false,
|
|
38
|
+
error: "Not found."
|
|
39
|
+
}));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
let result;
|
|
44
|
+
if (request.method === "GET") result = await session.call("get_plan", { planId: match[1] });
|
|
45
|
+
else {
|
|
46
|
+
if (!/^application\/json(?:\s*;|$)/i.test(request.headers["content-type"] ?? "")) {
|
|
47
|
+
response.writeHead(415).end(JSON.stringify({
|
|
48
|
+
ok: false,
|
|
49
|
+
error: "JSON required."
|
|
50
|
+
}));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const chunks = [];
|
|
54
|
+
let bytes = 0;
|
|
55
|
+
for await (const chunk of request) {
|
|
56
|
+
bytes += chunk.length;
|
|
57
|
+
if (bytes > 2097152) {
|
|
58
|
+
response.writeHead(413).end(JSON.stringify({
|
|
59
|
+
ok: false,
|
|
60
|
+
error: "Request too large."
|
|
61
|
+
}));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
chunks.push(chunk);
|
|
65
|
+
}
|
|
66
|
+
const input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
67
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
68
|
+
response.writeHead(400).end(JSON.stringify({
|
|
69
|
+
ok: false,
|
|
70
|
+
error: {
|
|
71
|
+
code: "invalid_input",
|
|
72
|
+
message: "A JSON object is required."
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
result = await session.call("run_commands", {
|
|
78
|
+
...input,
|
|
79
|
+
planId: match[1]
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
response.writeHead(200).end(JSON.stringify(result.data));
|
|
83
|
+
} catch {
|
|
84
|
+
response.writeHead(400).end(JSON.stringify({
|
|
85
|
+
ok: false,
|
|
86
|
+
error: {
|
|
87
|
+
code: "invalid_input",
|
|
88
|
+
message: "Invalid JSON request."
|
|
89
|
+
}
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
await new Promise((resolve, reject) => {
|
|
94
|
+
server.once("error", reject);
|
|
95
|
+
server.listen(port, "127.0.0.1", resolve);
|
|
96
|
+
});
|
|
97
|
+
const address = server.address();
|
|
98
|
+
if (!address || typeof address === "string") throw new Error("Preview listener has no port.");
|
|
99
|
+
return {
|
|
100
|
+
server,
|
|
101
|
+
url: `http://127.0.0.1:${address.port}`
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
10
105
|
//#region src/commands/kitchen/mcp/catalog.ts
|
|
11
106
|
/**
|
|
12
107
|
* The engine catalog (#3752) built from real recipe manifests: the recipes
|
|
@@ -199,6 +294,19 @@ var snapshotSchema = {
|
|
|
199
294
|
required: ["revision"],
|
|
200
295
|
additionalProperties: true
|
|
201
296
|
};
|
|
297
|
+
/** A document and snapshot captured together from the current engine state. */
|
|
298
|
+
var stateSchema = {
|
|
299
|
+
type: "object",
|
|
300
|
+
description: "Current authoritative state; paired atomically, including on replay.",
|
|
301
|
+
properties: {
|
|
302
|
+
snapshot: snapshotSchema,
|
|
303
|
+
cookbook: {
|
|
304
|
+
type: "object",
|
|
305
|
+
additionalProperties: true
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
required: ["snapshot", "cookbook"]
|
|
309
|
+
};
|
|
202
310
|
/** An output schema: `ok` always, `error` when it is false, plus the extras. */
|
|
203
311
|
var output = (properties, required = []) => ({
|
|
204
312
|
$schema: SCHEMA,
|
|
@@ -315,7 +423,7 @@ var KITCHEN_TOOLS = [
|
|
|
315
423
|
{
|
|
316
424
|
name: "start_plan",
|
|
317
425
|
title: "Start a plan",
|
|
318
|
-
description: "Start a new plan for an app, empty or from a ready-made cookbook (see list_cookbooks). Returns the plan id every other tool takes.
|
|
426
|
+
description: "Start a new plan for an app, empty or from a ready-made cookbook (see list_cookbooks). Returns the plan id every other tool takes.",
|
|
319
427
|
inputSchema: input({
|
|
320
428
|
cookbook: {
|
|
321
429
|
type: "string",
|
|
@@ -332,7 +440,8 @@ var KITCHEN_TOOLS = [
|
|
|
332
440
|
}),
|
|
333
441
|
outputSchema: output({
|
|
334
442
|
planId: { type: "string" },
|
|
335
|
-
snapshot: snapshotSchema
|
|
443
|
+
snapshot: snapshotSchema,
|
|
444
|
+
state: stateSchema
|
|
336
445
|
}, []),
|
|
337
446
|
annotations: annotations("Start a plan", {})
|
|
338
447
|
},
|
|
@@ -360,7 +469,8 @@ var KITCHEN_TOOLS = [
|
|
|
360
469
|
type: "array",
|
|
361
470
|
items: { type: "object" }
|
|
362
471
|
},
|
|
363
|
-
snapshot: snapshotSchema
|
|
472
|
+
snapshot: snapshotSchema,
|
|
473
|
+
state: stateSchema
|
|
364
474
|
}),
|
|
365
475
|
annotations: annotations("Change the plan", { idempotentHint: true })
|
|
366
476
|
},
|
|
@@ -371,10 +481,22 @@ var KITCHEN_TOOLS = [
|
|
|
371
481
|
inputSchema: input({ planId }, ["planId"]),
|
|
372
482
|
outputSchema: output({
|
|
373
483
|
planId: { type: "string" },
|
|
374
|
-
snapshot: snapshotSchema
|
|
484
|
+
snapshot: snapshotSchema,
|
|
485
|
+
state: stateSchema
|
|
375
486
|
}),
|
|
376
487
|
annotations: annotations("Get the plan", READ_ONLY)
|
|
377
488
|
},
|
|
489
|
+
{
|
|
490
|
+
name: "open_preview",
|
|
491
|
+
title: "Open the live app preview",
|
|
492
|
+
description: "Return a local browser URL for this plan when the Kitchen preview bridge is enabled. The app shell and chat then use the same plan.",
|
|
493
|
+
inputSchema: input({ planId }, ["planId"]),
|
|
494
|
+
outputSchema: output({
|
|
495
|
+
planId: { type: "string" },
|
|
496
|
+
url: { type: "string" }
|
|
497
|
+
}),
|
|
498
|
+
annotations: annotations("Open the live app preview", READ_ONLY)
|
|
499
|
+
},
|
|
378
500
|
{
|
|
379
501
|
name: "validate_plan",
|
|
380
502
|
title: "Validate the plan",
|
|
@@ -526,6 +648,10 @@ var KitchenSession = class KitchenSession {
|
|
|
526
648
|
options;
|
|
527
649
|
loaded;
|
|
528
650
|
plans = /* @__PURE__ */ new Map();
|
|
651
|
+
updates = /* @__PURE__ */ new Map();
|
|
652
|
+
previewUrl;
|
|
653
|
+
/** Plans `open_preview` handed to the browser; the preview bridge serves only these. */
|
|
654
|
+
previewPlans = /* @__PURE__ */ new Set();
|
|
529
655
|
constructor(options) {
|
|
530
656
|
this.options = options;
|
|
531
657
|
}
|
|
@@ -540,8 +666,93 @@ var KitchenSession = class KitchenSession {
|
|
|
540
666
|
}
|
|
541
667
|
return this.loaded;
|
|
542
668
|
}
|
|
543
|
-
|
|
544
|
-
|
|
669
|
+
planPath(id) {
|
|
670
|
+
if (!/^plan-[a-f0-9-]{8,36}$/.test(id)) throw new Error("Invalid plan id.");
|
|
671
|
+
if (!this.options.plansDir) throw new Error("Plans directory is not configured.");
|
|
672
|
+
return join(this.options.plansDir, `${id}.json`);
|
|
673
|
+
}
|
|
674
|
+
savePlan(id, engine) {
|
|
675
|
+
if (!this.options.plansDir) return;
|
|
676
|
+
mkdirSync(this.options.plansDir, {
|
|
677
|
+
recursive: true,
|
|
678
|
+
mode: 448
|
|
679
|
+
});
|
|
680
|
+
const target = this.planPath(id);
|
|
681
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
682
|
+
const serialized = `${JSON.stringify(engine.state())}\n`;
|
|
683
|
+
try {
|
|
684
|
+
writeFileSync(temporary, serialized, { mode: 384 });
|
|
685
|
+
renameSync(temporary, target);
|
|
686
|
+
this.plans.set(id, {
|
|
687
|
+
engine,
|
|
688
|
+
serialized
|
|
689
|
+
});
|
|
690
|
+
} finally {
|
|
691
|
+
if (existsSync(temporary)) rmSync(temporary);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/** Serialize the read/check/write transaction across Kitchen processes. */
|
|
695
|
+
async lockPlan(id) {
|
|
696
|
+
if (!this.options.plansDir) return () => {};
|
|
697
|
+
mkdirSync(this.options.plansDir, {
|
|
698
|
+
recursive: true,
|
|
699
|
+
mode: 448
|
|
700
|
+
});
|
|
701
|
+
const path = `${this.planPath(id)}.lock`;
|
|
702
|
+
const token = randomUUID();
|
|
703
|
+
const deadline = Date.now() + 3e4;
|
|
704
|
+
for (;;) {
|
|
705
|
+
try {
|
|
706
|
+
const fd = openSync(path, "wx", 384);
|
|
707
|
+
try {
|
|
708
|
+
writeFileSync(fd, JSON.stringify({
|
|
709
|
+
pid: process.pid,
|
|
710
|
+
token
|
|
711
|
+
}));
|
|
712
|
+
} catch {
|
|
713
|
+
rmSync(path, { force: true });
|
|
714
|
+
return fail("unavailable", "The plan cannot be locked on this device.");
|
|
715
|
+
} finally {
|
|
716
|
+
closeSync(fd);
|
|
717
|
+
}
|
|
718
|
+
return () => {
|
|
719
|
+
try {
|
|
720
|
+
if (JSON.parse(readFileSync(path, "utf8")).token === token) unlinkSync(path);
|
|
721
|
+
} catch {}
|
|
722
|
+
};
|
|
723
|
+
} catch (cause) {
|
|
724
|
+
if (cause.code !== "EEXIST") return fail("unavailable", "The plan cannot be locked on this device.");
|
|
725
|
+
}
|
|
726
|
+
if (Date.now() >= deadline) return fail("conflict", "This plan is busy in another Kitchen session. Retry the same command; if it stays busy, stop Kitchen and remove its stale plan lock.");
|
|
727
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
async plan(id) {
|
|
731
|
+
if (typeof id === "string" && this.options.plansDir) {
|
|
732
|
+
if (!/^plan-[a-f0-9-]{8,36}$/.test(id)) return fail("not_found", `No plan ${JSON.stringify(id)}.`, { path: "planId" });
|
|
733
|
+
let serialized;
|
|
734
|
+
try {
|
|
735
|
+
serialized = readFileSync(this.planPath(id), "utf8");
|
|
736
|
+
} catch (error) {
|
|
737
|
+
if (error.code === "ENOENT") return fail("not_found", `No plan ${JSON.stringify(id)}.`, { path: "planId" });
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
740
|
+
const cached = this.plans.get(id);
|
|
741
|
+
if (cached?.serialized === serialized) return cached;
|
|
742
|
+
const saved = JSON.parse(serialized);
|
|
743
|
+
const { catalog } = await this.catalog();
|
|
744
|
+
const plan = {
|
|
745
|
+
engine: createCookbookEngine({
|
|
746
|
+
catalog,
|
|
747
|
+
state: saved,
|
|
748
|
+
undoPrefix: `undo-${randomUUID()}`
|
|
749
|
+
}),
|
|
750
|
+
serialized
|
|
751
|
+
};
|
|
752
|
+
this.plans.set(id, plan);
|
|
753
|
+
return plan;
|
|
754
|
+
}
|
|
755
|
+
return (typeof id === "string" ? this.plans.get(id) : void 0) ?? fail("not_found", `No plan ${JSON.stringify(id)} in this server; call start_plan.`, { path: "planId" });
|
|
545
756
|
}
|
|
546
757
|
/** Validate arguments against a tool's published schema. */
|
|
547
758
|
static check(tool, args) {
|
|
@@ -576,18 +787,38 @@ var KitchenSession = class KitchenSession {
|
|
|
576
787
|
case "start_plan": return this.startPlan(args);
|
|
577
788
|
case "run_commands": return this.runCommands(args);
|
|
578
789
|
case "get_plan": {
|
|
579
|
-
const plan = this.plan(args.planId);
|
|
790
|
+
const plan = await this.plan(args.planId);
|
|
580
791
|
if (!("engine" in plan)) return plan;
|
|
581
792
|
const snapshot = plan.engine.snapshot();
|
|
582
793
|
return {
|
|
583
794
|
data: {
|
|
584
795
|
ok: true,
|
|
585
796
|
planId: args.planId,
|
|
586
|
-
snapshot
|
|
797
|
+
snapshot,
|
|
798
|
+
state: {
|
|
799
|
+
snapshot,
|
|
800
|
+
cookbook: plan.engine.cookbook()
|
|
801
|
+
}
|
|
587
802
|
},
|
|
588
803
|
text: describeSnapshot(snapshot)
|
|
589
804
|
};
|
|
590
805
|
}
|
|
806
|
+
case "open_preview": {
|
|
807
|
+
const plan = await this.plan(args.planId);
|
|
808
|
+
if (!("engine" in plan)) return plan;
|
|
809
|
+
if (!this.previewUrl || !this.options.previewOrigin) return fail("unavailable", "The local preview bridge is not enabled.");
|
|
810
|
+
const port = new URL(this.previewUrl).port;
|
|
811
|
+
this.previewPlans.add(args.planId);
|
|
812
|
+
const url = `${this.options.previewOrigin}/?kitchenPlan=${encodeURIComponent(args.planId)}&kitchenPort=${port}`;
|
|
813
|
+
return {
|
|
814
|
+
data: {
|
|
815
|
+
ok: true,
|
|
816
|
+
planId: args.planId,
|
|
817
|
+
url
|
|
818
|
+
},
|
|
819
|
+
text: `Open the live app shell: ${url}`
|
|
820
|
+
};
|
|
821
|
+
}
|
|
591
822
|
case "validate_plan": return this.validatePlan(args);
|
|
592
823
|
case "export_plan": return this.exportPlan(args);
|
|
593
824
|
case "apply_plan": return this.applyPlan(args);
|
|
@@ -636,7 +867,10 @@ var KitchenSession = class KitchenSession {
|
|
|
636
867
|
}
|
|
637
868
|
async startPlan(args) {
|
|
638
869
|
const { catalog } = await this.catalog();
|
|
639
|
-
const engine = createCookbookEngine({
|
|
870
|
+
const engine = createCookbookEngine({
|
|
871
|
+
catalog,
|
|
872
|
+
undoPrefix: `undo-${randomUUID()}`
|
|
873
|
+
});
|
|
640
874
|
const steps = [];
|
|
641
875
|
if (typeof args.cookbook === "string") steps.push({
|
|
642
876
|
name: "apply_cookbook",
|
|
@@ -650,27 +884,62 @@ var KitchenSession = class KitchenSession {
|
|
|
650
884
|
const result = engine.run(step);
|
|
651
885
|
if (!result.ok) return failFrom(result.error);
|
|
652
886
|
}
|
|
653
|
-
const id = `plan-${randomUUID()
|
|
654
|
-
this.
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
887
|
+
const id = `plan-${randomUUID()}`;
|
|
888
|
+
if (this.options.plansDir) try {
|
|
889
|
+
this.savePlan(id, engine);
|
|
890
|
+
} catch {
|
|
891
|
+
return fail("unavailable", "The plan could not be saved on this device.");
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
this.plans.set(id, { engine });
|
|
895
|
+
while (this.plans.size > 20) {
|
|
896
|
+
const oldest = this.plans.keys().next().value;
|
|
897
|
+
if (oldest === void 0) break;
|
|
898
|
+
this.plans.delete(oldest);
|
|
899
|
+
}
|
|
659
900
|
}
|
|
660
901
|
const snapshot = engine.snapshot();
|
|
661
902
|
return {
|
|
662
903
|
data: {
|
|
663
904
|
ok: true,
|
|
664
905
|
planId: id,
|
|
665
|
-
snapshot
|
|
906
|
+
snapshot,
|
|
907
|
+
state: {
|
|
908
|
+
snapshot,
|
|
909
|
+
cookbook: engine.cookbook()
|
|
910
|
+
}
|
|
666
911
|
},
|
|
667
912
|
text: `Started ${id}.\n${describeSnapshot(snapshot)}`
|
|
668
913
|
};
|
|
669
914
|
}
|
|
670
|
-
runCommands(args) {
|
|
671
|
-
const
|
|
915
|
+
async runCommands(args) {
|
|
916
|
+
const id = args.planId;
|
|
917
|
+
const previous = this.updates.get(id) ?? Promise.resolve();
|
|
918
|
+
let release;
|
|
919
|
+
const gate = new Promise((resolve) => {
|
|
920
|
+
release = resolve;
|
|
921
|
+
});
|
|
922
|
+
const current = previous.then(() => gate);
|
|
923
|
+
this.updates.set(id, current);
|
|
924
|
+
await previous;
|
|
925
|
+
try {
|
|
926
|
+
const lock = await this.lockPlan(id);
|
|
927
|
+
if (typeof lock !== "function") return lock;
|
|
928
|
+
try {
|
|
929
|
+
return await this.runCommandsUnlocked(args);
|
|
930
|
+
} finally {
|
|
931
|
+
lock();
|
|
932
|
+
}
|
|
933
|
+
} finally {
|
|
934
|
+
release();
|
|
935
|
+
if (this.updates.get(id) === current) this.updates.delete(id);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
async runCommandsUnlocked(args) {
|
|
939
|
+
const plan = await this.plan(args.planId);
|
|
672
940
|
if (!("engine" in plan)) return plan;
|
|
673
941
|
const { planId: _planId, ...batch } = args;
|
|
942
|
+
if (!Array.isArray(batch.commands)) return fail("invalid_input", "commands must be an array.", { path: "commands" });
|
|
674
943
|
const commands = batch.commands;
|
|
675
944
|
const lone = commands.length === 1 && commands[0]?.name === "undo" ? plan.engine.run({
|
|
676
945
|
...commands[0],
|
|
@@ -678,7 +947,26 @@ var KitchenSession = class KitchenSession {
|
|
|
678
947
|
...batch.expectedRevision !== void 0 ? { expectedRevision: batch.expectedRevision } : {}
|
|
679
948
|
}) : null;
|
|
680
949
|
if (lone) {
|
|
681
|
-
if (!lone.ok)
|
|
950
|
+
if (!lone.ok) {
|
|
951
|
+
const failed = failFrom(lone.error);
|
|
952
|
+
return {
|
|
953
|
+
...failed,
|
|
954
|
+
data: {
|
|
955
|
+
...failed.data,
|
|
956
|
+
planId: args.planId,
|
|
957
|
+
state: {
|
|
958
|
+
snapshot: plan.engine.snapshot(),
|
|
959
|
+
cookbook: plan.engine.cookbook()
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
if (!lone.replayed) try {
|
|
965
|
+
this.savePlan(args.planId, plan.engine);
|
|
966
|
+
} catch {
|
|
967
|
+
this.plans.delete(args.planId);
|
|
968
|
+
return fail("unavailable", "The plan could not be saved on this device.");
|
|
969
|
+
}
|
|
682
970
|
return {
|
|
683
971
|
data: {
|
|
684
972
|
ok: true,
|
|
@@ -686,7 +974,11 @@ var KitchenSession = class KitchenSession {
|
|
|
686
974
|
...lone.replayed ? { replayed: true } : {},
|
|
687
975
|
receipt: lone.receipt,
|
|
688
976
|
results: [lone.receipt],
|
|
689
|
-
snapshot: lone.snapshot
|
|
977
|
+
snapshot: lone.snapshot,
|
|
978
|
+
state: {
|
|
979
|
+
snapshot: plan.engine.snapshot(),
|
|
980
|
+
cookbook: plan.engine.cookbook()
|
|
981
|
+
}
|
|
690
982
|
},
|
|
691
983
|
text: `${lone.receipt.summary}\n${describeSnapshot(lone.snapshot)}`
|
|
692
984
|
};
|
|
@@ -699,11 +991,21 @@ var KitchenSession = class KitchenSession {
|
|
|
699
991
|
ok: false,
|
|
700
992
|
planId: args.planId,
|
|
701
993
|
error: result.error,
|
|
702
|
-
snapshot: current
|
|
994
|
+
snapshot: current,
|
|
995
|
+
state: {
|
|
996
|
+
snapshot: current,
|
|
997
|
+
cookbook: plan.engine.cookbook()
|
|
998
|
+
}
|
|
703
999
|
},
|
|
704
1000
|
text: `Error ${result.error.code}: ${result.error.message}\nNothing was changed (revision ${current.revision}).`
|
|
705
1001
|
};
|
|
706
1002
|
}
|
|
1003
|
+
if (!result.replayed) try {
|
|
1004
|
+
this.savePlan(args.planId, plan.engine);
|
|
1005
|
+
} catch {
|
|
1006
|
+
this.plans.delete(args.planId);
|
|
1007
|
+
return fail("unavailable", "The plan could not be saved on this device.");
|
|
1008
|
+
}
|
|
707
1009
|
return {
|
|
708
1010
|
data: {
|
|
709
1011
|
ok: true,
|
|
@@ -711,13 +1013,17 @@ var KitchenSession = class KitchenSession {
|
|
|
711
1013
|
...result.replayed ? { replayed: true } : {},
|
|
712
1014
|
receipt: result.receipt,
|
|
713
1015
|
results: result.results.map((r) => r.receipt),
|
|
714
|
-
snapshot: result.snapshot
|
|
1016
|
+
snapshot: result.snapshot,
|
|
1017
|
+
state: {
|
|
1018
|
+
snapshot: plan.engine.snapshot(),
|
|
1019
|
+
cookbook: plan.engine.cookbook()
|
|
1020
|
+
}
|
|
715
1021
|
},
|
|
716
1022
|
text: `${result.replayed ? "Already applied (same id): " : ""}${result.results.map((r) => r.receipt.summary).join("\n")}\n${describeSnapshot(result.snapshot)}`
|
|
717
1023
|
};
|
|
718
1024
|
}
|
|
719
1025
|
async validatePlan(args) {
|
|
720
|
-
const plan = this.plan(args.planId);
|
|
1026
|
+
const plan = await this.plan(args.planId);
|
|
721
1027
|
if (!("engine" in plan)) return plan;
|
|
722
1028
|
const { index } = await this.catalog();
|
|
723
1029
|
const checked = plan.engine.run({ name: "validate" });
|
|
@@ -741,8 +1047,8 @@ var KitchenSession = class KitchenSession {
|
|
|
741
1047
|
text: valid ? `Valid. Packages needed: ${report.packages.join(", ") || "none"}.` : `Not valid:\n${firstLines(errors.length ? errors : diagnostics.map((d) => `${d.path}: ${d.message}`), 12)}`
|
|
742
1048
|
};
|
|
743
1049
|
}
|
|
744
|
-
exportPlan(args) {
|
|
745
|
-
const plan = this.plan(args.planId);
|
|
1050
|
+
async exportPlan(args) {
|
|
1051
|
+
const plan = await this.plan(args.planId);
|
|
746
1052
|
if (!("engine" in plan)) return plan;
|
|
747
1053
|
const result = plan.engine.run({
|
|
748
1054
|
name: "export_cookbook",
|
|
@@ -762,7 +1068,7 @@ var KitchenSession = class KitchenSession {
|
|
|
762
1068
|
};
|
|
763
1069
|
}
|
|
764
1070
|
async applyPlan(args) {
|
|
765
|
-
const plan = this.plan(args.planId);
|
|
1071
|
+
const plan = await this.plan(args.planId);
|
|
766
1072
|
if (!("engine" in plan)) return plan;
|
|
767
1073
|
const dir = args.dir;
|
|
768
1074
|
if (!isAbsolute(dir)) return fail("invalid_input", "dir must be an absolute path.", { path: "dir" });
|
|
@@ -835,6 +1141,217 @@ var KitchenSession = class KitchenSession {
|
|
|
835
1141
|
}
|
|
836
1142
|
};
|
|
837
1143
|
//#endregion
|
|
1144
|
+
//#region src/commands/kitchen/mcp/bridge.ts
|
|
1145
|
+
/** Minimal MCP Apps bridge, injected before the self-contained Planner bundle. */
|
|
1146
|
+
var KITCHEN_BRIDGE = `<script>
|
|
1147
|
+
(() => {
|
|
1148
|
+
const pending = new Map();
|
|
1149
|
+
const listeners = new Set();
|
|
1150
|
+
let sequence = 0;
|
|
1151
|
+
let latest;
|
|
1152
|
+
let hostOrigin = '*';
|
|
1153
|
+
let initialized = false;
|
|
1154
|
+
function send(message) { window.parent.postMessage({ jsonrpc: '2.0', ...message }, hostOrigin); }
|
|
1155
|
+
function request(method, params) {
|
|
1156
|
+
return new Promise((resolve, reject) => {
|
|
1157
|
+
const id = 'kitchen-' + ++sequence;
|
|
1158
|
+
const timer = setTimeout(() => {
|
|
1159
|
+
pending.delete(id);
|
|
1160
|
+
reject(new Error('The host did not answer. Refresh the plan before retrying.'));
|
|
1161
|
+
}, 30000);
|
|
1162
|
+
pending.set(id, { resolve, reject, timer });
|
|
1163
|
+
send({ id, method, params });
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
// The Planner is an app shell that scrolls inside itself, so it asks for a
|
|
1167
|
+
// stable frame instead of its content height: the host's fixed height wins,
|
|
1168
|
+
// else its maxHeight, else a default. Fullscreen belongs to the host.
|
|
1169
|
+
const DEFAULT_HEIGHT = 720;
|
|
1170
|
+
let dimensions = {};
|
|
1171
|
+
let displayMode = 'inline';
|
|
1172
|
+
let reported;
|
|
1173
|
+
function appHeight() {
|
|
1174
|
+
if (displayMode === 'fullscreen' || typeof dimensions.height === 'number') return undefined;
|
|
1175
|
+
const max = dimensions.maxHeight;
|
|
1176
|
+
return typeof max === 'number' && max > 0 ? Math.round(max) : DEFAULT_HEIGHT;
|
|
1177
|
+
}
|
|
1178
|
+
function reportSize() {
|
|
1179
|
+
const height = appHeight();
|
|
1180
|
+
if (height === undefined || height === reported) return;
|
|
1181
|
+
reported = height;
|
|
1182
|
+
send({ method: 'ui/notifications/size-changed', params: { height } });
|
|
1183
|
+
}
|
|
1184
|
+
function applyHostContext(context) {
|
|
1185
|
+
if (!context || typeof context !== 'object') return;
|
|
1186
|
+
const next = context.containerDimensions;
|
|
1187
|
+
if (next && typeof next === 'object') dimensions = next;
|
|
1188
|
+
if (typeof context.displayMode === 'string') displayMode = context.displayMode;
|
|
1189
|
+
}
|
|
1190
|
+
function publish(data) {
|
|
1191
|
+
if (!data || typeof data.planId !== 'string' || !data.state) return;
|
|
1192
|
+
latest = data;
|
|
1193
|
+
for (const listener of listeners) listener(data);
|
|
1194
|
+
}
|
|
1195
|
+
window.addEventListener('message', (event) => {
|
|
1196
|
+
if (event.source !== window.parent) return;
|
|
1197
|
+
if (hostOrigin !== '*' && event.origin !== hostOrigin) return;
|
|
1198
|
+
const message = event.data;
|
|
1199
|
+
if (!message || message.jsonrpc !== '2.0') return;
|
|
1200
|
+
if (message.id !== undefined && pending.has(message.id)) {
|
|
1201
|
+
if (event.origin && event.origin !== 'null') hostOrigin = event.origin;
|
|
1202
|
+
const item = pending.get(message.id);
|
|
1203
|
+
clearTimeout(item.timer);
|
|
1204
|
+
pending.delete(message.id);
|
|
1205
|
+
if (message.error) item.reject(new Error(message.error.message || 'Host request failed'));
|
|
1206
|
+
else item.resolve(message.result);
|
|
1207
|
+
} else if (message.method === 'ui/notifications/host-context-changed') {
|
|
1208
|
+
applyHostContext(message.params);
|
|
1209
|
+
if (initialized) reportSize();
|
|
1210
|
+
} else if (message.method === 'ui/notifications/tool-result') {
|
|
1211
|
+
publish(message.params?.structuredContent);
|
|
1212
|
+
} else if (message.method === 'ping' && message.id !== undefined) {
|
|
1213
|
+
send({ id: message.id, result: {} });
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
const ready = request('ui/initialize', {
|
|
1217
|
+
appInfo: { name: 'smrt-kitchen-planner', version: '1.0.0' },
|
|
1218
|
+
appCapabilities: { availableDisplayModes: ['inline', 'fullscreen'] },
|
|
1219
|
+
protocolVersion: '2026-01-26',
|
|
1220
|
+
}).then((result) => {
|
|
1221
|
+
if (result?.protocolVersion !== '2026-01-26') throw new Error('Unsupported MCP Apps protocol');
|
|
1222
|
+
send({ method: 'ui/notifications/initialized', params: {} });
|
|
1223
|
+
applyHostContext(result.hostContext);
|
|
1224
|
+
initialized = true;
|
|
1225
|
+
reportSize();
|
|
1226
|
+
return result;
|
|
1227
|
+
});
|
|
1228
|
+
ready.catch(() => {});
|
|
1229
|
+
window.smrtKitchen = {
|
|
1230
|
+
async callTool(name, args) {
|
|
1231
|
+
if (!['get_plan', 'run_commands', 'export_plan', 'validate_plan', 'planner_ui_update'].includes(name)) {
|
|
1232
|
+
throw new Error('This tool is not available from the Planner UI');
|
|
1233
|
+
}
|
|
1234
|
+
await ready;
|
|
1235
|
+
const result = await request('tools/call', { name, arguments: args });
|
|
1236
|
+
if (!result?.structuredContent) throw new Error('The host returned no structured data');
|
|
1237
|
+
return result.structuredContent;
|
|
1238
|
+
},
|
|
1239
|
+
subscribe(listener) {
|
|
1240
|
+
listeners.add(listener);
|
|
1241
|
+
if (latest) listener(latest);
|
|
1242
|
+
return () => listeners.delete(listener);
|
|
1243
|
+
},
|
|
1244
|
+
};
|
|
1245
|
+
ready.then(async () => {
|
|
1246
|
+
const resumePlanId = window.__smrtKitchenReloadPlanId;
|
|
1247
|
+
if (resumePlanId) {
|
|
1248
|
+
try {
|
|
1249
|
+
const result = await request('tools/call', {
|
|
1250
|
+
name: 'get_plan', arguments: { planId: resumePlanId },
|
|
1251
|
+
});
|
|
1252
|
+
publish(result?.structuredContent);
|
|
1253
|
+
} catch { /* A later render result can still supply the plan. */ }
|
|
1254
|
+
}
|
|
1255
|
+
let version = window.__smrtKitchenUiVersion;
|
|
1256
|
+
if (!version) return;
|
|
1257
|
+
let checking = false;
|
|
1258
|
+
let stopped = false;
|
|
1259
|
+
let reloadNotice;
|
|
1260
|
+
function showReloadNotice() {
|
|
1261
|
+
if (reloadNotice) return;
|
|
1262
|
+
reloadNotice = document.createElement('p');
|
|
1263
|
+
reloadNotice.setAttribute('role', 'alert');
|
|
1264
|
+
reloadNotice.textContent = 'The new Planner UI could not reload. Reopen this app tab to use it.';
|
|
1265
|
+
document.body.prepend(reloadNotice);
|
|
1266
|
+
}
|
|
1267
|
+
const check = async () => {
|
|
1268
|
+
if (checking || stopped || document.visibilityState === 'hidden') return;
|
|
1269
|
+
checking = true;
|
|
1270
|
+
let attemptedVersion = false;
|
|
1271
|
+
try {
|
|
1272
|
+
const result = await request('tools/call', {
|
|
1273
|
+
name: 'planner_ui_update',
|
|
1274
|
+
arguments: { version, planId: latest?.planId || resumePlanId || '' },
|
|
1275
|
+
});
|
|
1276
|
+
const update = result?.structuredContent;
|
|
1277
|
+
if (update?.changed && typeof update.html === 'string' && typeof update.version === 'string') {
|
|
1278
|
+
// A rejected artifact must not be resent every poll. A later build
|
|
1279
|
+
// has a different digest and can still replace it.
|
|
1280
|
+
version = update.version;
|
|
1281
|
+
attemptedVersion = true;
|
|
1282
|
+
const next = new DOMParser().parseFromString(update.html, 'text/html');
|
|
1283
|
+
const nextStyle = next.getElementById('smrt-planner-styles');
|
|
1284
|
+
const nextEntry = next.getElementById('smrt-planner-entry');
|
|
1285
|
+
const currentStyle = document.getElementById('smrt-planner-styles');
|
|
1286
|
+
const currentEntry = document.getElementById('smrt-planner-entry');
|
|
1287
|
+
if (!nextStyle || !nextEntry || !currentStyle || !currentEntry || !window.smrtKitchenUnmount) {
|
|
1288
|
+
throw new Error('The Planner bundle cannot be reloaded in this tab');
|
|
1289
|
+
}
|
|
1290
|
+
await window.smrtKitchenUnmount();
|
|
1291
|
+
const style = document.createElement('style');
|
|
1292
|
+
style.id = 'smrt-planner-styles';
|
|
1293
|
+
style.textContent = nextStyle.textContent;
|
|
1294
|
+
currentStyle.replaceWith(style);
|
|
1295
|
+
const entry = document.createElement('script');
|
|
1296
|
+
entry.id = 'smrt-planner-entry';
|
|
1297
|
+
entry.textContent = nextEntry.textContent;
|
|
1298
|
+
currentEntry.replaceWith(entry);
|
|
1299
|
+
reloadNotice?.remove();
|
|
1300
|
+
reloadNotice = undefined;
|
|
1301
|
+
}
|
|
1302
|
+
} catch {
|
|
1303
|
+
if (attemptedVersion) showReloadNotice();
|
|
1304
|
+
// A build may be in progress; check again for a newer digest.
|
|
1305
|
+
}
|
|
1306
|
+
checking = false;
|
|
1307
|
+
};
|
|
1308
|
+
const timer = setInterval(check, 1500);
|
|
1309
|
+
window.addEventListener('pagehide', () => {
|
|
1310
|
+
stopped = true;
|
|
1311
|
+
clearInterval(timer);
|
|
1312
|
+
}, { once: true });
|
|
1313
|
+
}).catch(() => {});
|
|
1314
|
+
})();
|
|
1315
|
+
<\/script>`;
|
|
1316
|
+
//#endregion
|
|
1317
|
+
//#region src/commands/kitchen/mcp/ui.ts
|
|
1318
|
+
/** Opt-in desktop capability probe. This is not the Planner UI. */
|
|
1319
|
+
var KITCHEN_UI_URI = "ui://smrt-kitchen/capability-v1.html";
|
|
1320
|
+
var KITCHEN_UI_CONTENT = {
|
|
1321
|
+
uri: KITCHEN_UI_URI,
|
|
1322
|
+
name: "SMRT Kitchen desktop capability check",
|
|
1323
|
+
mimeType: "text/html;profile=mcp-app",
|
|
1324
|
+
text: `<!doctype html>
|
|
1325
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1326
|
+
${KITCHEN_BRIDGE}
|
|
1327
|
+
<title>SMRT Kitchen capability check</title></head>
|
|
1328
|
+
<body><main><h1>SMRT Kitchen</h1>
|
|
1329
|
+
<p>Local MCP App resource rendered.</p>
|
|
1330
|
+
<p>This capability check does not edit your plan. Continue using the Kitchen tools in chat.</p>
|
|
1331
|
+
</main></body></html>`,
|
|
1332
|
+
_meta: { ui: {
|
|
1333
|
+
prefersBorder: true,
|
|
1334
|
+
csp: {
|
|
1335
|
+
connectDomains: [],
|
|
1336
|
+
resourceDomains: []
|
|
1337
|
+
}
|
|
1338
|
+
} }
|
|
1339
|
+
};
|
|
1340
|
+
/** Load only an operator-selected local artifact; tool arguments never choose files. */
|
|
1341
|
+
function loadPlannerUi(file, resumePlanId) {
|
|
1342
|
+
const html = readFileSync(file, "utf8");
|
|
1343
|
+
if (!/<head(?:\s[^>]*)?>/i.test(html)) throw new Error("Planner UI must be a complete self-contained HTML document");
|
|
1344
|
+
const digest = createHash("sha256").update(html).digest("hex").slice(0, 16);
|
|
1345
|
+
const bootstrap = `<script>window.__smrtKitchenUiVersion=${JSON.stringify(digest)};window.__smrtKitchenReloadPlanId=${JSON.stringify(resumePlanId ?? "").replace(/</g, "\\u003c")};<\/script>`;
|
|
1346
|
+
const text = html.replace(/<head(?:\s[^>]*)?>/i, (head) => head + bootstrap + KITCHEN_BRIDGE);
|
|
1347
|
+
return {
|
|
1348
|
+
...KITCHEN_UI_CONTENT,
|
|
1349
|
+
uri: `ui://smrt-kitchen/planner-${digest}.html`,
|
|
1350
|
+
name: "SMRT Planner",
|
|
1351
|
+
text
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
//#endregion
|
|
838
1355
|
//#region src/commands/kitchen/mcp/server.ts
|
|
839
1356
|
/**
|
|
840
1357
|
* `smrt kitchen mcp` (#3752): the Kitchen tools served over stdio. stdout
|
|
@@ -849,24 +1366,143 @@ var CATALOG_CACHE_HINT = {
|
|
|
849
1366
|
/** Build the MCP server; one {@link KitchenSession} (plans, catalog) per server. */
|
|
850
1367
|
function createKitchenMcpServer(options) {
|
|
851
1368
|
const session = new KitchenSession(options);
|
|
1369
|
+
let plannerUi;
|
|
1370
|
+
if (options.plannerUi) try {
|
|
1371
|
+
plannerUi = loadPlannerUi(options.plannerUi);
|
|
1372
|
+
} catch {
|
|
1373
|
+
console.error(`[${SERVER_NAME}] Planner UI is unavailable; planning tools remain active.`);
|
|
1374
|
+
}
|
|
1375
|
+
const resources = [...options.uiSmoke ? [KITCHEN_UI_CONTENT] : [], ...plannerUi ? [plannerUi] : []];
|
|
1376
|
+
const getPlanTool = KITCHEN_TOOLS.find((tool) => tool.name === "get_plan");
|
|
1377
|
+
if (!getPlanTool) throw new Error("Kitchen get_plan tool is missing");
|
|
852
1378
|
const server = new Server({
|
|
853
1379
|
name: SERVER_NAME,
|
|
854
1380
|
version: cliVersion()
|
|
855
1381
|
}, {
|
|
856
|
-
capabilities: {
|
|
1382
|
+
capabilities: {
|
|
1383
|
+
tools: {},
|
|
1384
|
+
...resources.length ? { resources: {} } : {}
|
|
1385
|
+
},
|
|
857
1386
|
cacheHints: { "tools/list": CATALOG_CACHE_HINT }
|
|
858
1387
|
});
|
|
859
|
-
server.setRequestHandler("tools/list", async () => ({ tools:
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1388
|
+
server.setRequestHandler("tools/list", async () => ({ tools: [
|
|
1389
|
+
...KITCHEN_TOOLS.map((tool) => ({
|
|
1390
|
+
name: tool.name,
|
|
1391
|
+
title: tool.title,
|
|
1392
|
+
description: tool.description,
|
|
1393
|
+
inputSchema: tool.inputSchema,
|
|
1394
|
+
outputSchema: tool.outputSchema,
|
|
1395
|
+
annotations: tool.annotations
|
|
1396
|
+
})),
|
|
1397
|
+
...plannerUi ? [{
|
|
1398
|
+
name: "planner_ui_update",
|
|
1399
|
+
title: "Check Planner UI build",
|
|
1400
|
+
description: "Check whether the local Planner UI artifact has changed.",
|
|
1401
|
+
inputSchema: {
|
|
1402
|
+
type: "object",
|
|
1403
|
+
properties: {
|
|
1404
|
+
version: { type: "string" },
|
|
1405
|
+
planId: { type: "string" }
|
|
1406
|
+
},
|
|
1407
|
+
required: ["version"],
|
|
1408
|
+
additionalProperties: false
|
|
1409
|
+
},
|
|
1410
|
+
annotations: {
|
|
1411
|
+
readOnlyHint: true,
|
|
1412
|
+
destructiveHint: false,
|
|
1413
|
+
idempotentHint: true,
|
|
1414
|
+
openWorldHint: false
|
|
1415
|
+
},
|
|
1416
|
+
_meta: { ui: { visibility: ["app"] } }
|
|
1417
|
+
}, {
|
|
1418
|
+
...getPlanTool,
|
|
1419
|
+
inputSchema: getPlanTool.inputSchema,
|
|
1420
|
+
outputSchema: getPlanTool.outputSchema,
|
|
1421
|
+
name: "render_plan",
|
|
1422
|
+
title: "Open the Planner",
|
|
1423
|
+
description: "Open the interactive Planner for an existing plan. Call start_plan first, then pass its planId. Data tools keep working without the UI.",
|
|
1424
|
+
_meta: {
|
|
1425
|
+
ui: { resourceUri: plannerUi.uri },
|
|
1426
|
+
"openai/ui": { entrypoints: [{ type: "thread" }] }
|
|
1427
|
+
}
|
|
1428
|
+
}] : [],
|
|
1429
|
+
...options.uiSmoke ? [{
|
|
1430
|
+
name: "kitchen_ui_smoke",
|
|
1431
|
+
title: "Check local Kitchen UI",
|
|
1432
|
+
description: "Render a static local MCP Apps capability check. Does not edit or display the Planner.",
|
|
1433
|
+
inputSchema: {
|
|
1434
|
+
type: "object",
|
|
1435
|
+
properties: {},
|
|
1436
|
+
additionalProperties: false
|
|
1437
|
+
},
|
|
1438
|
+
annotations: {
|
|
1439
|
+
readOnlyHint: true,
|
|
1440
|
+
destructiveHint: false,
|
|
1441
|
+
idempotentHint: true,
|
|
1442
|
+
openWorldHint: false
|
|
1443
|
+
},
|
|
1444
|
+
_meta: { ui: { resourceUri: KITCHEN_UI_URI } }
|
|
1445
|
+
}] : []
|
|
1446
|
+
] }));
|
|
1447
|
+
if (resources.length) {
|
|
1448
|
+
server.setRequestHandler("resources/list", async () => ({ resources: resources.map(({ uri, name, mimeType }) => ({
|
|
1449
|
+
uri,
|
|
1450
|
+
name,
|
|
1451
|
+
mimeType
|
|
1452
|
+
})) }));
|
|
1453
|
+
server.setRequestHandler("resources/read", async (request) => {
|
|
1454
|
+
const resource = resources.find((item) => item.uri === request.params.uri);
|
|
1455
|
+
if (!resource) throw new Error(`Unknown Kitchen resource: ${request.params.uri}`);
|
|
1456
|
+
return { contents: [resource] };
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
867
1459
|
server.setRequestHandler("tools/call", async (request) => {
|
|
868
1460
|
const { name, arguments: args } = request.params;
|
|
869
|
-
|
|
1461
|
+
if (plannerUi && options.plannerUi && name === "planner_ui_update") {
|
|
1462
|
+
const version = typeof args?.version === "string" ? args.version : "";
|
|
1463
|
+
const planId = typeof args?.planId === "string" ? args.planId : void 0;
|
|
1464
|
+
let current;
|
|
1465
|
+
try {
|
|
1466
|
+
current = loadPlannerUi(options.plannerUi, planId);
|
|
1467
|
+
} catch {
|
|
1468
|
+
return {
|
|
1469
|
+
content: [{
|
|
1470
|
+
type: "text",
|
|
1471
|
+
text: "Planner UI build is not ready."
|
|
1472
|
+
}],
|
|
1473
|
+
structuredContent: {
|
|
1474
|
+
changed: false,
|
|
1475
|
+
version,
|
|
1476
|
+
error: {
|
|
1477
|
+
code: "unavailable",
|
|
1478
|
+
message: "Planner UI build is not ready."
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
const currentVersion = current.uri.slice(26, -5);
|
|
1484
|
+
const changed = currentVersion !== version;
|
|
1485
|
+
const data = changed ? {
|
|
1486
|
+
changed,
|
|
1487
|
+
version: currentVersion,
|
|
1488
|
+
html: current.text
|
|
1489
|
+
} : {
|
|
1490
|
+
changed,
|
|
1491
|
+
version: currentVersion
|
|
1492
|
+
};
|
|
1493
|
+
return {
|
|
1494
|
+
content: [{
|
|
1495
|
+
type: "text",
|
|
1496
|
+
text: changed ? "Planner UI updated." : "Planner UI unchanged."
|
|
1497
|
+
}],
|
|
1498
|
+
structuredContent: data
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
if (options.uiSmoke && name === "kitchen_ui_smoke") return { content: [{
|
|
1502
|
+
type: "text",
|
|
1503
|
+
text: "Local MCP Apps capability check. Confirm whether the host displays the SMRT Kitchen card."
|
|
1504
|
+
}] };
|
|
1505
|
+
const result = await session.call(plannerUi && name === "render_plan" ? "get_plan" : name, args ?? {});
|
|
870
1506
|
return {
|
|
871
1507
|
content: [{
|
|
872
1508
|
type: "text",
|
|
@@ -883,10 +1519,17 @@ function createKitchenMcpServer(options) {
|
|
|
883
1519
|
}
|
|
884
1520
|
/** Serve on this process's stdio until the host closes it or the process is stopped. */
|
|
885
1521
|
async function runKitchenMcp(options) {
|
|
886
|
-
const
|
|
1522
|
+
const { server, session } = createKitchenMcpServer(options);
|
|
1523
|
+
const preview = options.previewPort !== void 0 ? await startKitchenPreview(session, options.previewPort, options.previewOrigin ?? "") : void 0;
|
|
1524
|
+
session.previewUrl = preview?.url;
|
|
1525
|
+
if (preview) console.error(`[${SERVER_NAME}] Live preview bridge: ${preview.url}`);
|
|
1526
|
+
const handle = serveStdio(() => server, { onerror: (error) => console.error(`[${SERVER_NAME}] ${error.message}`) });
|
|
887
1527
|
await new Promise((resolve) => {
|
|
888
1528
|
const done = () => {
|
|
889
|
-
handle.close().finally(
|
|
1529
|
+
handle.close().finally(() => {
|
|
1530
|
+
preview?.server.close();
|
|
1531
|
+
resolve();
|
|
1532
|
+
});
|
|
890
1533
|
};
|
|
891
1534
|
process.once("SIGINT", done);
|
|
892
1535
|
process.once("SIGTERM", done);
|