@avocadostudio-ai/mcp-server 0.2.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/LICENSE +201 -0
- package/README.md +128 -0
- package/dist/capabilities.d.ts +63 -0
- package/dist/capabilities.js +116 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +17 -0
- package/dist/http-auth.d.ts +14 -0
- package/dist/http-auth.js +26 -0
- package/dist/http.d.ts +13 -0
- package/dist/http.js +132 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +32 -0
- package/dist/orchestrator-client.d.ts +93 -0
- package/dist/orchestrator-client.js +64 -0
- package/dist/tools/_helpers.d.ts +33 -0
- package/dist/tools/_helpers.js +58 -0
- package/dist/tools/blocks.d.ts +4 -0
- package/dist/tools/blocks.js +256 -0
- package/dist/tools/chat.d.ts +3 -0
- package/dist/tools/chat.js +52 -0
- package/dist/tools/discovery.d.ts +4 -0
- package/dist/tools/discovery.js +105 -0
- package/dist/tools/history.d.ts +3 -0
- package/dist/tools/history.js +37 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +28 -0
- package/dist/tools/media.d.ts +3 -0
- package/dist/tools/media.js +84 -0
- package/dist/tools/pages.d.ts +4 -0
- package/dist/tools/pages.js +131 -0
- package/dist/tools/preview.d.ts +3 -0
- package/dist/tools/preview.js +32 -0
- package/dist/tools/publishing.d.ts +3 -0
- package/dist/tools/publishing.js +67 -0
- package/dist/tools/sessions.d.ts +8 -0
- package/dist/tools/sessions.js +39 -0
- package/dist/tools/sites.d.ts +3 -0
- package/dist/tools/sites.js +90 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +16 -0
- package/package.json +68 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { jsonResult, errorResult } from "./_helpers.js";
|
|
3
|
+
const pageSlug = z
|
|
4
|
+
.string()
|
|
5
|
+
.min(1)
|
|
6
|
+
.describe("Page slug, starts with '/' (e.g. '/', '/pricing'). This is the page's identity in the draft, " +
|
|
7
|
+
"NOT necessarily the URL it is served at — see `path` on avocado-list-pages before navigating to one.");
|
|
8
|
+
export function registerPageTools(server, client, gate) {
|
|
9
|
+
server.tool("avocado-get-page", "Fetch a single page's full draft document (blocks + meta) by slug.", { slug: pageSlug }, async ({ slug }) => {
|
|
10
|
+
try {
|
|
11
|
+
return jsonResult(await client.getPage(slug));
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
return errorResult(err);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
server.tool("avocado-list-pages", "List every page in the current draft (home page first). Returns both `slugs: string[]` and a richer `pages: [{ slug, path?, title, updatedAt, blockCount }]` summary — use `pages` to plan multi-page work without needing a follow-up avocado-get-page per slug. Note: `title` is the page's internal/display title (the one shown in the editor nav and settable via `avocado-rename-page`). It is distinct from the SEO `<title>` tag (`meta.title`) which is set via `avocado-update-page-meta` and only visible through `avocado-get-page`. IMPORTANT: `slug` identifies the page, and is only the site's URL when `path` is absent. A site with locale prefixes, a basePath, or its own routing reports the real URL in `path` — open `path` when it is present, never the slug, or you will land on a 404. ALSO CHECK `source` before trusting the list: `draft`/`adapter` is the site's own content; `demo` means these are Avocado's bundled demo pages and belong to no real site (usually a missing `siteId`); `unknown-site` means no site by that id is known here, so the empty list is not a report that the site has no pages; `empty` means the site is known and genuinely has none. `note` explains the case in words.", {}, async () => {
|
|
18
|
+
try {
|
|
19
|
+
return jsonResult(await client.listSlugs());
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
return errorResult(err);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const _gated_0 = server.tool("avocado-create-page", "Create a new page. The page must include a unique slug, title, and an initial blocks array (can be empty).", {
|
|
26
|
+
slug: pageSlug,
|
|
27
|
+
title: z.string().min(1),
|
|
28
|
+
blocks: z.array(z.object({
|
|
29
|
+
id: z.string().min(1),
|
|
30
|
+
type: z.string().min(1),
|
|
31
|
+
props: z.record(z.string(), z.unknown()),
|
|
32
|
+
})).default([]),
|
|
33
|
+
meta: z.object({
|
|
34
|
+
title: z.string().optional(),
|
|
35
|
+
description: z.string().optional(),
|
|
36
|
+
ogImage: z.string().optional(),
|
|
37
|
+
}).optional(),
|
|
38
|
+
}, async ({ slug, title, blocks, meta }) => {
|
|
39
|
+
const refusal = gate?.refuse("createPages");
|
|
40
|
+
if (refusal)
|
|
41
|
+
return errorResult(refusal);
|
|
42
|
+
try {
|
|
43
|
+
return jsonResult(await client.applyOps([
|
|
44
|
+
{
|
|
45
|
+
op: "create_page",
|
|
46
|
+
page: {
|
|
47
|
+
id: `p_${slug.replace(/[^a-zA-Z0-9]+/g, "_")}_${Date.now()}`,
|
|
48
|
+
slug,
|
|
49
|
+
title,
|
|
50
|
+
updatedAt: new Date().toISOString(),
|
|
51
|
+
blocks,
|
|
52
|
+
meta,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
]));
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return errorResult(err);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
gate?.track("createPages", _gated_0);
|
|
62
|
+
server.tool("avocado-rename-page", "Rename a page — change its slug and/or its display/internal title. Provide either `newSlug`, `newTitle`, or both. A pure title change leaves the URL and all internal links untouched; a slug change rewrites internal links to the old slug automatically. Note: this tool changes the page's display title (shown in the editor and in `avocado-list-pages`), which is separate from SEO `meta.title` — use `avocado-update-page-meta` for that.", {
|
|
63
|
+
slug: pageSlug,
|
|
64
|
+
newSlug: z.string().min(1).describe("New slug (starts with '/'). Omit for a title-only rename.").optional(),
|
|
65
|
+
newTitle: z.string().min(1).describe("New display/internal title. Omit for a slug-only rename.").optional(),
|
|
66
|
+
}, async ({ slug, newSlug, newTitle }) => {
|
|
67
|
+
if (!newSlug && !newTitle)
|
|
68
|
+
return errorResult(new Error("Provide newSlug or newTitle."));
|
|
69
|
+
try {
|
|
70
|
+
return jsonResult(await client.applyOps([
|
|
71
|
+
{ op: "rename_page", pageSlug: slug, newPageSlug: newSlug, newTitle },
|
|
72
|
+
]));
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return errorResult(err);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const _gated_1 = server.tool("avocado-duplicate-page", "Duplicate a page, optionally under a new slug/title and inserted after a specific page in the nav order. When `newTitle` is passed, `meta.title` is synced to the new title too (so SEO doesn't show the source page's title). Response includes `duplicatedPages: [{ slug, blockIdMap: { [oldId]: newId } }]` — use the map to target copied blocks directly without a follow-up avocado-get-page.", {
|
|
79
|
+
slug: pageSlug,
|
|
80
|
+
newSlug: z.string().min(1).optional(),
|
|
81
|
+
newTitle: z.string().min(1).optional(),
|
|
82
|
+
afterSlug: z.string().min(1).optional().describe("Slug of the page the duplicate should appear after in nav order."),
|
|
83
|
+
}, async ({ slug, newSlug, newTitle, afterSlug }) => {
|
|
84
|
+
const refusal = gate?.refuse("createPages");
|
|
85
|
+
if (refusal)
|
|
86
|
+
return errorResult(refusal);
|
|
87
|
+
try {
|
|
88
|
+
return jsonResult(await client.applyOps([
|
|
89
|
+
{ op: "duplicate_page", pageSlug: slug, newPageSlug: newSlug, newTitle, afterPageSlug: afterSlug },
|
|
90
|
+
]));
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return errorResult(err);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
gate?.track("createPages", _gated_1);
|
|
97
|
+
const _gated_2 = server.tool("avocado-remove-page", "Delete a page from the draft. Undo-able from the editor history.", { slug: pageSlug }, async ({ slug }) => {
|
|
98
|
+
const refusal = gate?.refuse("deletePages");
|
|
99
|
+
if (refusal)
|
|
100
|
+
return errorResult(refusal);
|
|
101
|
+
try {
|
|
102
|
+
return jsonResult(await client.applyOps([{ op: "remove_page", pageSlug: slug }]));
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
return errorResult(err);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
gate?.track("deletePages", _gated_2);
|
|
109
|
+
server.tool("avocado-update-page-meta", "Patch a page's SEO metadata (title, description, ogImage). Only provided keys are updated.", {
|
|
110
|
+
slug: pageSlug,
|
|
111
|
+
title: z.string().optional(),
|
|
112
|
+
description: z.string().optional(),
|
|
113
|
+
ogImage: z.string().optional(),
|
|
114
|
+
}, async ({ slug, title, description, ogImage }) => {
|
|
115
|
+
const patch = {};
|
|
116
|
+
if (title !== undefined)
|
|
117
|
+
patch.title = title;
|
|
118
|
+
if (description !== undefined)
|
|
119
|
+
patch.description = description;
|
|
120
|
+
if (ogImage !== undefined)
|
|
121
|
+
patch.ogImage = ogImage;
|
|
122
|
+
if (Object.keys(patch).length === 0)
|
|
123
|
+
return errorResult(new Error("Provide at least one of title, description, ogImage."));
|
|
124
|
+
try {
|
|
125
|
+
return jsonResult(await client.applyOps([{ op: "update_page_meta", pageSlug: slug, patch }]));
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
return errorResult(err);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { errorResult } from "./_helpers.js";
|
|
3
|
+
export function registerPreviewTools(server, client) {
|
|
4
|
+
server.tool("avocado-screenshot-page", "Take a full-page screenshot of a page and return it as an inline image. Defaults to the draft (what the editor sees), so in-progress edits are visible before publish. Pass `published: true` to screenshot the live public route instead. Site must have a previewUrl registered (via avocado-register-site).", {
|
|
5
|
+
slug: z.string().optional().describe("Page slug to screenshot, e.g. '/' or '/pricing'. Defaults to the home page."),
|
|
6
|
+
published: z.boolean().optional().describe("If true, screenshot the published public route instead of the draft. Defaults to false (draft)."),
|
|
7
|
+
previewUrl: z.string().url().optional().describe("Override the registered previewUrl (useful for ad-hoc captures)."),
|
|
8
|
+
draftPath: z.string().optional().describe("Override the site's draft-preview route for this capture. A prefix the slug is appended to ('/avocado') or a template naming where it goes ('/preview/{slug}/draft'). Defaults to the site's registered draftPath, then '/preview-draft'. If a draft screenshot comes back as the site's 404 page, this is why."),
|
|
9
|
+
}, async ({ slug, published, previewUrl, draftPath }) => {
|
|
10
|
+
try {
|
|
11
|
+
const res = await client.request("POST", "/preview/screenshot", {
|
|
12
|
+
body: client.scopedBody({ slug, published, previewUrl, draftPath }),
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: `Screenshot of ${res.url} (${res.mode} mode, ${res.width}×${res.height})`,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: "image",
|
|
22
|
+
data: res.base64,
|
|
23
|
+
mimeType: res.mimeType,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return errorResult(err);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { jsonResult, errorResult } from "./_helpers.js";
|
|
3
|
+
export function registerPublishingTools(server, client) {
|
|
4
|
+
server.tool("avocado-compute-publish-diff", "Compute the diff between the current draft and the last published snapshot. Shows what would change if you publish now.", {
|
|
5
|
+
siteOrigin: z.string().url().optional().describe("Override the published-content source (defaults to the configured site origin)."),
|
|
6
|
+
}, async ({ siteOrigin }) => {
|
|
7
|
+
try {
|
|
8
|
+
return jsonResult(await client.request("GET", "/publish/diff", {
|
|
9
|
+
query: client.scoped({ siteOrigin }),
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
return errorResult(err);
|
|
14
|
+
}
|
|
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.", {
|
|
17
|
+
siteOrigin: z.string().url().optional(),
|
|
18
|
+
}, async ({ siteOrigin }) => {
|
|
19
|
+
if (!client.config.publishToken) {
|
|
20
|
+
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
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return jsonResult(await client.request("POST", "/publish", {
|
|
24
|
+
body: client.scopedBody({ siteOrigin }),
|
|
25
|
+
bearer: client.config.publishToken,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return errorResult(err);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
server.tool("avocado-get-publish-status", [
|
|
33
|
+
"Fetch the current publish tracker for this site (target type, state, last deployment, URLs).",
|
|
34
|
+
"Returns `{ status: 'idle' }` when nothing has been published by the running orchestrator process — that is the normal state before a first publish and after any restart, NOT a sign that publishing is broken. Use avocado-compute-publish-diff to see what a publish would change.",
|
|
35
|
+
].join("\n"), {}, async () => {
|
|
36
|
+
try {
|
|
37
|
+
return jsonResult(await client.request("GET", "/publish/status", { query: client.scoped() }));
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return errorResult(err);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
server.tool("avocado-list-snapshots", "List published snapshots (prior publish commits) available to restore from. Defaults to the latest 30.", {
|
|
44
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
45
|
+
}, async ({ limit }) => {
|
|
46
|
+
try {
|
|
47
|
+
return jsonResult(await client.request("GET", "/restore/snapshots", {
|
|
48
|
+
query: { siteId: client.config.siteId, limit },
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return errorResult(err);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
server.tool("avocado-restore-snapshot", "Rewind the draft to a previously-published snapshot, identified by its git commit sha (from avocado-list-snapshots).", {
|
|
56
|
+
commit: z.string().regex(/^[a-f0-9]{7,40}$/i).describe("Git commit sha (7–40 hex chars) of the published snapshot."),
|
|
57
|
+
}, async ({ commit }) => {
|
|
58
|
+
try {
|
|
59
|
+
return jsonResult(await client.request("POST", "/restore/snapshot", {
|
|
60
|
+
body: client.scopedBody({ commit }),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return errorResult(err);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { OrchestratorClient } from "../orchestrator-client.ts";
|
|
3
|
+
import type { CapabilityGate } from "../capabilities.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Session introspection tools — answer "which draft am I editing?" and "what
|
|
6
|
+
* other sessions exist on this orchestrator?" without dropping to raw SQLite.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerSessionTools(server: McpServer, client: OrchestratorClient, gate?: CapabilityGate): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsonResult, errorResult } from "./_helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* Session introspection tools — answer "which draft am I editing?" and "what
|
|
4
|
+
* other sessions exist on this orchestrator?" without dropping to raw SQLite.
|
|
5
|
+
*/
|
|
6
|
+
export function registerSessionTools(server, client, gate) {
|
|
7
|
+
server.tool("avocado-whoami", "Return this MCP install's bound session + a summary of its state (siteId, draft page count, version, last mutation). Call it first: it is how you confirm you are about to edit the draft you think you are. The `source` field says whose content is behind the session — `draft`/`adapter` is the site's own, `demo` is Avocado's bundled demo content (no real site), and `unknown-site` means no site by that id is known here, so everything you do afterwards would apply to a session that matches nothing. `note` explains it in words, and `knownSiteIds` lists what the id could have meant.", {}, async () => {
|
|
8
|
+
try {
|
|
9
|
+
const bound = {
|
|
10
|
+
session: client.config.session,
|
|
11
|
+
siteId: client.config.siteId,
|
|
12
|
+
orchestratorUrl: client.config.orchestratorUrl,
|
|
13
|
+
};
|
|
14
|
+
const summary = await client.whoami();
|
|
15
|
+
/*
|
|
16
|
+
* Refresh capabilities here rather than only at startup: whoami is
|
|
17
|
+
* what an agent calls first, and a site that came up after this
|
|
18
|
+
* process did should not be answered from a stale "unknown".
|
|
19
|
+
* Non-blocking by construction — the gate's probe has its own short
|
|
20
|
+
* timeout and falls back to permissive.
|
|
21
|
+
*/
|
|
22
|
+
const capabilities = gate ? await gate.refresh() : undefined;
|
|
23
|
+
// Prefer the server-reported orchestratorUrl (strips trailing slashes, reflects the actual host),
|
|
24
|
+
// but surface the locally configured session/siteId so mismatches are debuggable.
|
|
25
|
+
return jsonResult({ ...summary, bound, ...(capabilities ? { capabilities } : {}) });
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
return errorResult(err);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
server.tool("avocado-list-sessions", "List every session this orchestrator has state for, sorted by most recently mutated. Each row: { sessionKey, session, siteId, version, draftPageCount, lastMutatedAt }. Use to discover other drafts (e.g. {siteId}::{session} style keys) without shelling into SQLite.", {}, async () => {
|
|
32
|
+
try {
|
|
33
|
+
return jsonResult(await client.listSessions());
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return errorResult(err);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { jsonResult, errorResult } from "./_helpers.js";
|
|
3
|
+
export function registerSiteTools(server, client) {
|
|
4
|
+
server.tool("avocado-register-site", "Register or update the site entry for this install (preview URL, display name, port, purpose, tone). Required once before the editor can find the site.", {
|
|
5
|
+
name: z.string().min(1).optional().describe("Display name shown in the editor's site switcher."),
|
|
6
|
+
previewUrl: z.string().url().optional().describe("URL where the Next.js site is running (e.g. http://localhost:3000)."),
|
|
7
|
+
draftPath: z.string().optional().describe("The route this site renders orchestrator drafts on — a prefix the page slug is appended to ('/avocado') or a template naming where it goes ('/preview/{slug}/draft'). Defaults to '/preview-draft'. Set this for a site that wired Avocado into its own app, or draft screenshots photograph its 404 page."),
|
|
8
|
+
port: z.number().int().positive().optional(),
|
|
9
|
+
purpose: z.string().optional().describe("One-liner describing what the site is about. Used as AI context."),
|
|
10
|
+
secret: z.string().optional().describe("DRAFT_MODE_SECRET value so the orchestrator can fetch draft content."),
|
|
11
|
+
}, async (args) => {
|
|
12
|
+
try {
|
|
13
|
+
return jsonResult(await client.request("POST", "/sites/register", {
|
|
14
|
+
body: client.scopedBody(args),
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
return errorResult(err);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
server.tool("avocado-list-sites", "List every site registered under the current session.", {}, async () => {
|
|
22
|
+
try {
|
|
23
|
+
return jsonResult(await client.request("GET", "/sites", { query: { session: client.config.session } }));
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return errorResult(err);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
server.tool("avocado-get-site-config", "Fetch the current site's config (name, logo, tone, purpose, nav labels/groups, theme overrides).", {}, async () => {
|
|
30
|
+
try {
|
|
31
|
+
return jsonResult(await client.getSiteConfig());
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return errorResult(err);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
server.tool("avocado-update-site-config", "Patch the site's config (name, logo, navLabels, navGroups). Only provided keys are updated; others remain untouched. Undo-able via history.", {
|
|
38
|
+
name: z.string().optional(),
|
|
39
|
+
logo: z.string().optional().describe("URL or path to the site logo."),
|
|
40
|
+
navLabels: z.record(z.string(), z.string()).optional().describe("Slug → custom nav label (e.g. { '/pricing': 'Plans & Pricing' })."),
|
|
41
|
+
navGroups: z.record(z.string(), z.array(z.string())).optional().describe("Parent label → child slugs (e.g. { 'Products': ['/bananas', '/cherries'] })."),
|
|
42
|
+
}, async ({ name, logo, navLabels, navGroups }) => {
|
|
43
|
+
const patch = {};
|
|
44
|
+
if (name !== undefined)
|
|
45
|
+
patch.name = name;
|
|
46
|
+
if (logo !== undefined)
|
|
47
|
+
patch.logo = logo;
|
|
48
|
+
if (navLabels !== undefined)
|
|
49
|
+
patch.navLabels = navLabels;
|
|
50
|
+
if (navGroups !== undefined)
|
|
51
|
+
patch.navGroups = navGroups;
|
|
52
|
+
if (Object.keys(patch).length === 0) {
|
|
53
|
+
return errorResult(new Error("Provide at least one of name, logo, navLabels, navGroups."));
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return jsonResult(await client.applyOps([{ op: "update_site_config", patch }]));
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return errorResult(err);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
server.tool("avocado-update-theme", "Change the site-wide visual theme — colors, fonts, and corner radius applied to every page and block. Uses semantic tokens (not raw CSS variables). Merge-patch: only provided tokens change; pass an empty string to reset a token to the theme default. Undo-able via history.", {
|
|
63
|
+
brandColor: z.string().optional().describe("Primary brand color (CSS color). Sets --brand, --brand-hover, --link."),
|
|
64
|
+
accentColor: z.string().optional().describe("Secondary accent color. Sets --accent, --accent-hover."),
|
|
65
|
+
backgroundColor: z.string().optional().describe("Page background color. Sets --bg-0, --bg-000."),
|
|
66
|
+
surfaceColor: z.string().optional().describe("Card/panel surface color. Sets --surface, --card-bg, --bg-100."),
|
|
67
|
+
headingColor: z.string().optional().describe("Heading text color. Sets --heading, --text-100."),
|
|
68
|
+
textColor: z.string().optional().describe("Body text color. Sets --body, --text-200."),
|
|
69
|
+
mutedTextColor: z.string().optional().describe("Muted/secondary text color. Sets --body-secondary, --text-300, --caption."),
|
|
70
|
+
headingFont: z.string().optional().describe("Heading font stack (e.g. 'Georgia, serif'). Sets --font-heading."),
|
|
71
|
+
bodyFont: z.string().optional().describe("Body font stack. Sets --font-body."),
|
|
72
|
+
radius: z.string().optional().describe("Corner radius across buttons/cards/inputs (e.g. '12px'). Sets --radius and friends."),
|
|
73
|
+
cssVars: z.record(z.string(), z.string()).optional().describe("Raw --var → value escape hatch, merged after the semantic tokens (raw wins)."),
|
|
74
|
+
}, async ({ cssVars, ...tokens }) => {
|
|
75
|
+
const patch = {};
|
|
76
|
+
for (const [k, v] of Object.entries(tokens)) {
|
|
77
|
+
if (v !== undefined)
|
|
78
|
+
patch[k] = v;
|
|
79
|
+
}
|
|
80
|
+
if (Object.keys(patch).length === 0 && (!cssVars || Object.keys(cssVars).length === 0)) {
|
|
81
|
+
return errorResult(new Error("Provide at least one theme token or a cssVars entry."));
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return jsonResult(await client.applyOps([{ op: "update_theme", patch, ...(cssVars ? { cssVars } : {}) }]));
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return errorResult(err);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SERVER_VERSION: string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
/*
|
|
3
|
+
* The version the server reports in `initialize`.
|
|
4
|
+
*
|
|
5
|
+
* Read from the manifest rather than written here. It was the literal "0.1.0"
|
|
6
|
+
* in two files, and it stayed "0.1.0" through the package being renamed,
|
|
7
|
+
* republished and moved — so the first client to ask this server what it was
|
|
8
|
+
* would have been told the wrong number. A hardcoded version is only ever
|
|
9
|
+
* correct on the day someone remembers it.
|
|
10
|
+
*
|
|
11
|
+
* `../package.json` resolves to the package root from both `src/` in dev and
|
|
12
|
+
* `dist/` in a published install, and `exports` declares that subpath so the
|
|
13
|
+
* read works from a registry consumer too.
|
|
14
|
+
*/
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
export const SERVER_VERSION = require("../package.json").version;
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@avocadostudio-ai/mcp-server",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Avocado Studio MCP server — exposes page/block/discovery tools over the Model Context Protocol.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"avocado-mcp": "./dist/index.js",
|
|
9
|
+
"avocado-mcp-http": "./dist/http.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"avocado",
|
|
18
|
+
"avocado-studio",
|
|
19
|
+
"mcp",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"cms",
|
|
22
|
+
"ai",
|
|
23
|
+
"claude"
|
|
24
|
+
],
|
|
25
|
+
"homepage": "https://github.com/avocadostudio-ai/avocado/tree/main/packages/mcp-server#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/avocadostudio-ai/avocado/issues"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/avocadostudio-ai/avocado.git",
|
|
32
|
+
"directory": "packages/mcp-server"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"registry": "https://registry.npmjs.org",
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
|
+
"zod": "^4.3.6",
|
|
44
|
+
"@avocadostudio-ai/shared": "0.2.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.13.10",
|
|
48
|
+
"tsx": "^4.19.3",
|
|
49
|
+
"typescript": "^5.7.3"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc -p tsconfig.build.json && chmod +x dist/index.js dist/http.js",
|
|
53
|
+
"dev": "tsx watch src/index.ts",
|
|
54
|
+
"dev:http": "tsx watch src/http.ts",
|
|
55
|
+
"start": "tsx src/index.ts",
|
|
56
|
+
"start:http": "tsx src/http.ts",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"test": "NODE_ENV=test node ../../scripts/run-tests.mjs"
|
|
59
|
+
},
|
|
60
|
+
"exports": {
|
|
61
|
+
".": {
|
|
62
|
+
"types": "./dist/index.d.ts",
|
|
63
|
+
"import": "./dist/index.js",
|
|
64
|
+
"default": "./dist/index.js"
|
|
65
|
+
},
|
|
66
|
+
"./package.json": "./package.json"
|
|
67
|
+
}
|
|
68
|
+
}
|