Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"dependencies": {
"@basehub/mutation-api-helpers": "2.1.2",
"@modelcontextprotocol/sdk": "^1.11.4",
"basehub": "9.0.9",
"basehub": "^9.0.16",
"ts-loader": "^9.4.2",
"typescript": "^5.0.4",
"xmcp": "latest",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

119 changes: 119 additions & 0 deletions src/tools/update-blocks-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { basehub } from "basehub";
import { z } from "zod";
import type { InferSchema } from "xmcp";

export const schema = {
updates: z
.array(
z.object({
component: z
.string()
.describe("Component type (e.g. 'hero', 'features')"),
field: z
.string()
.describe("Field to update (e.g. 'title', 'subtitle')"),
value: z.string().describe("New value for the field"),
})
)
.min(1, "You must provide at least one field to update."),
autoCommit: z.string().optional().describe("Optional commit message"),
};

export const metadata = {
name: "update_blocks_content",
description: `
Before using this MCP, make sure you've read the \`basehub.d.ts\` file in your codebase to use the correct component names.

This MCP updates multiple BaseHub component fields in a single batch.

Example input:
[
{ "component": "<component_name>", "field": "title", "value": "New title!" },
{ "component": "<component_name>", "field": "subtitle", "value": "New subtitle!" }
]
`,
annotations: {
title: "Update Blocks Content",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
};

export default async function updateBlocksContent({
updates,
autoCommit,
}: InferSchema<typeof schema>) {
try {
const updatesByComponent = updates.reduce<
Record<string, Array<{ field: string; value: string }>>
>((acc, { component, field, value }) => {
if (!acc[component]) {
acc[component] = [];
}
acc[component].push({ field, value });
return acc;
}, {});

const query = {
_componentInstances: Object.keys(updatesByComponent).reduce(
(acc, component) => {
acc[component] = { items: { _id: true } };
return acc;
},
{} as Record<string, { items: { _id: boolean } }>
),
};

const response = (await basehub().query(query)) as {
_componentInstances?: Record<string, { items?: Array<{ _id?: string }> }>;
};

if (!response._componentInstances) {
throw new Error("Failed to fetch component instances");
}

const transactionData = Object.entries(updatesByComponent).map(
([component, fields]) => {
const instance = response._componentInstances?.[component]?.items?.[0];
if (!instance?._id) {
throw new Error(`No instance found for component "${component}"`);
}

const updateData = fields.reduce<
Record<string, { type: "text"; value: unknown }>
>((acc, { field, value }) => {
acc[field] = { type: "text", value };
return acc;
}, {});

return {
id: instance._id,
type: "update" as const,
value: updateData,
};
}
);

const result = await basehub().mutation({
transaction: {
__args: {
data: transactionData,
// TODO: research auto commit AI criteria (mainly used for dev env)
// ...(autoCommit ? { autoCommit } : {}),
},
message: true,
status: true,
duration: true,
},
});

return {
content: [{ type: "text", text: JSON.stringify(result) }],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "An unknown error occurred";
throw new Error(`update blocks content failed: ${errorMessage}`);
}
}