@avocadostudio-ai/mcp-server 0.3.3 → 0.5.0

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/README.md CHANGED
@@ -14,7 +14,7 @@ Each install is scoped to **one site**: `(session, siteId)` are bound at launch
14
14
  | Sites | `avocado-register-site`, `avocado-list-sites`, `avocado-get-site-config`, `avocado-update-site-config` |
15
15
  | Media | `avocado-upload-image`, `avocado-generate-image`, `avocado-search-unsplash`, `avocado-transcribe-audio`, `avocado-interpret-image` |
16
16
  | Publishing | `avocado-compute-publish-diff`, `avocado-publish-content`, `avocado-get-publish-status`, `avocado-list-snapshots`, `avocado-restore-snapshot` |
17
- | History | `avocado-undo-edit`, `avocado-redo-edit`, `avocado-restore-version` |
17
+ | History | `avocado-undo-edit`, `avocado-redo-edit`, `avocado-restore-version`, `avocado-discard-changes` |
18
18
  | Planner | `avocado-chat-plan`, `avocado-preview-plan`, `avocado-approve-pending-plan`, `avocado-discard-pending-plan` |
19
19
  | Preview | `avocado-screenshot-page` — returns a full-page JPEG inline (visual feedback channel for chat-only hosts like Claude Desktop) |
20
20
 
@@ -34,4 +34,23 @@ export function registerHistoryTools(server, client) {
34
34
  return errorResult(err);
35
35
  }
36
36
  });
37
+ server.tool("avocado-discard-changes", [
38
+ "Throw away changes from the history log. Each affected page is rolled back to the state it held immediately before the earliest version selected on it.",
39
+ "A page has one timeline, so discarding a change also discards the later changes to that same page — the response lists them under `discarded[].alsoDiscarded`. Pages you did not select keep their edits.",
40
+ "Versions with nothing recorded before them come back under `skipped` rather than being guessed at. The discard itself is undoable.",
41
+ ].join("\n"), {
42
+ versions: z
43
+ .array(z.number().int().positive())
44
+ .min(1)
45
+ .describe("Version numbers from the history log to discard."),
46
+ }, async ({ versions }) => {
47
+ try {
48
+ return jsonResult(await client.request("POST", "/history/discard", {
49
+ body: client.scopedBody({ versions }),
50
+ }));
51
+ }
52
+ catch (err) {
53
+ return errorResult(err);
54
+ }
55
+ });
37
56
  }
@@ -8,6 +8,7 @@ import { registerHistoryTools } from "./history.js";
8
8
  import { registerChatTools } from "./chat.js";
9
9
  import { registerPreviewTools } from "./preview.js";
10
10
  import { registerSessionTools } from "./sessions.js";
11
+ import { registerQaTools } from "./qa.js";
11
12
  /**
12
13
  * `gate` is optional so a caller that has no capability answer yet — every
13
14
  * caller, at startup — still registers the full tool set. The gate hides and
@@ -25,4 +26,5 @@ export function registerAllTools(server, client, gate) {
25
26
  registerHistoryTools(server, client);
26
27
  registerChatTools(server, client);
27
28
  registerPreviewTools(server, client);
29
+ registerQaTools(server, client);
28
30
  }
@@ -13,15 +13,24 @@ export function registerPublishingTools(server, client) {
13
13
  return errorResult(err);
14
14
  }
15
15
  });
16
- server.tool("avocado-publish-content", "Publish the current draft to the live site. Requires AVOCADO_PUBLISH_TOKEN env var (the orchestrator's DRAFT_MODE_SECRET) to be set on the MCP server process.", {
16
+ server.tool("avocado-publish-content", [
17
+ "Publish the current draft to the live site. Requires AVOCADO_PUBLISH_TOKEN env var (the orchestrator's DRAFT_MODE_SECRET) to be set on the MCP server process.",
18
+ "Pass `slugs` to publish only those pages — every other page keeps exactly what it has live. Omit it to publish the whole draft.",
19
+ ].join("\n"), {
17
20
  siteOrigin: z.string().url().optional(),
18
- }, async ({ siteOrigin }) => {
21
+ slugs: z.array(z.string()).optional().describe("Publish only these page slugs, leaving every other page as it is on the live site. Omit for a full publish."),
22
+ includeSiteConfig: z.boolean().optional().describe("Whether the site header/nav ships with this publish. Defaults to true."),
23
+ }, async ({ siteOrigin, slugs, includeSiteConfig }) => {
19
24
  if (!client.config.publishToken) {
20
25
  return errorResult(new Error("AVOCADO_PUBLISH_TOKEN is not set on the MCP server. Re-install the connector with --env AVOCADO_PUBLISH_TOKEN=<DRAFT_MODE_SECRET>."));
21
26
  }
22
27
  try {
23
28
  return jsonResult(await client.request("POST", "/publish", {
24
- body: client.scopedBody({ siteOrigin }),
29
+ body: client.scopedBody({
30
+ siteOrigin,
31
+ ...(slugs && slugs.length > 0 ? { slugs } : {}),
32
+ ...(includeSiteConfig === false ? { includeSiteConfig: false } : {}),
33
+ }),
25
34
  bearer: client.config.publishToken,
26
35
  }));
27
36
  }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { OrchestratorClient } from "../orchestrator-client.ts";
3
+ export declare function registerQaTools(server: McpServer, client: OrchestratorClient): void;
@@ -0,0 +1,115 @@
1
+ import { z } from "zod";
2
+ import { getAllBlockMeta, panelCoverage, formatPanelCoverage } from "@avocadostudio-ai/shared";
3
+ /*
4
+ * The step that asks whether the editing surface an integration just produced is
5
+ * any good.
6
+ *
7
+ * An agent can finish wiring a site — routes mounted, manifest served, markers
8
+ * emitted, build clean — and hand back a property panel whose list rows read
9
+ * `Item 4`, whose labels come from somebody else's block, and whose polymorphic
10
+ * branches never narrow. Every one of those was visible in the data the whole
11
+ * time; nothing was asking. The build cannot see it, the type checker cannot see
12
+ * it, and `editableCoverage` is about the preview.
13
+ *
14
+ * So this is the QA move: after integrating, before claiming success, ask the
15
+ * site what it will actually look like to edit, and either fix what comes back
16
+ * or report it verbatim.
17
+ *
18
+ * It reads the manifest from the ORCHESTRATOR for the same reason discovery
19
+ * does — that is the process which runs the site's `registerBlocks()` — and
20
+ * compares it against the site's real pages. No browser, no screenshot, no model
21
+ * call.
22
+ */
23
+ /** The editor's own registry, which is what a colliding type name collides with. */
24
+ function editorBuiltins() {
25
+ return getAllBlockMeta();
26
+ }
27
+ export function registerQaTools(server, client) {
28
+ server.tool("avocado-check-editing-surface", "QA the property panel this site produces: which list rows a person can tell apart, which polymorphic branches actually narrow, which props are in the content but described by nothing, and where block type names collide with Avocado's built-ins. Call this after integrating a site or changing its block schemas, and before reporting the integration as done — a site can build cleanly, serve a valid manifest and still be unintelligible to edit.", {
29
+ slugs: z
30
+ .array(z.string())
31
+ .optional()
32
+ .describe("Pages to examine. Defaults to every slug the site has."),
33
+ includeBuiltinCollisions: z
34
+ .boolean()
35
+ .optional()
36
+ .describe("Report block type names that also exist in the editor's built-in registry with a different shape. Default true; this is usually the root cause of several other findings.")
37
+ }, async ({ slugs, includeBuiltinCollisions = true }) => {
38
+ let manifest;
39
+ try {
40
+ manifest = await client.request("GET", "/blocks/manifest");
41
+ if (!manifest || !Array.isArray(manifest.blocks))
42
+ throw new Error("no blocks array");
43
+ }
44
+ catch (err) {
45
+ /*
46
+ * Deliberately an error rather than a fallback to this process's
47
+ * registry. Discovery can hedge — a labelled guess still helps an agent
48
+ * plan. A QA check that silently measured the wrong site's blocks would
49
+ * report an all-clear on a panel nobody has looked at, which is worse
50
+ * than not running.
51
+ */
52
+ return {
53
+ content: [{
54
+ type: "text",
55
+ text: `Cannot QA the editing surface: the site's block manifest is unreadable at ` +
56
+ `${client.config.orchestratorUrl}/blocks/manifest (${err instanceof Error ? err.message : String(err)}). ` +
57
+ `This check has to read the manifest from the process that renders the site, not from this one.`
58
+ }],
59
+ isError: true
60
+ };
61
+ }
62
+ let targetSlugs = slugs;
63
+ if (!targetSlugs || targetSlugs.length === 0) {
64
+ try {
65
+ const index = await client.request("GET", "/draft/slugs", {
66
+ query: { session: client.config.session, siteId: client.config.siteId }
67
+ });
68
+ targetSlugs = index.slugs ?? [];
69
+ }
70
+ catch {
71
+ targetSlugs = [];
72
+ }
73
+ }
74
+ const pages = [];
75
+ const unreadable = [];
76
+ for (const slug of targetSlugs) {
77
+ try {
78
+ pages.push(await client.getPage(slug));
79
+ }
80
+ catch {
81
+ unreadable.push(slug);
82
+ }
83
+ }
84
+ const report = panelCoverage(manifest, pages, {
85
+ ...(includeBuiltinCollisions ? { builtinTypes: editorBuiltins() } : {})
86
+ });
87
+ /*
88
+ * Both shapes on purpose. The formatted report is what a person reads when
89
+ * the agent quotes it; the structured findings are what the agent filters,
90
+ * counts and decides on. Handing back only prose makes an agent re-parse
91
+ * its own tool output, and only JSON makes its final message unreadable.
92
+ */
93
+ return {
94
+ content: [{
95
+ type: "text",
96
+ text: JSON.stringify({
97
+ pagesExamined: pages.length,
98
+ ...(unreadable.length > 0 ? { unreadableSlugs: unreadable } : {}),
99
+ rowsExamined: report.rowsExamined,
100
+ rowsLabelled: report.rowsLabelled,
101
+ findingCount: report.findings.length,
102
+ findings: report.findings,
103
+ unknownBlockTypes: report.unknownBlockTypes,
104
+ report: formatPanelCoverage(report),
105
+ guidance: report.findings.length === 0
106
+ ? "No editing-surface findings. The panel can label every list row it was shown."
107
+ : "Fix these before reporting the integration as done, or list the ones you are leaving and why. " +
108
+ "`colliding_type` first: where a site re-registers a built-in name with its own shape, anything " +
109
+ "the manifest does not declare explicitly is taken from the built-in, which usually explains " +
110
+ "several of the findings under it."
111
+ }, null, 2)
112
+ }]
113
+ };
114
+ });
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avocadostudio-ai/mcp-server",
3
- "version": "0.3.3",
3
+ "version": "0.5.0",
4
4
  "description": "Avocado Studio MCP server — exposes page/block/discovery tools over the Model Context Protocol.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "dependencies": {
42
42
  "@modelcontextprotocol/sdk": "^1.29.0",
43
43
  "zod": "^4.3.6",
44
- "@avocadostudio-ai/shared": "^0.3.3"
44
+ "@avocadostudio-ai/shared": "^0.5.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^22.13.10",