Sign In

@focusgts/eds-mcp-server

Package Overview
Dependencies
Maintainers
1
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@focusgts/eds-mcp-server - npm Package Compare versions

Comparing version
0.9.0
to
0.10.0
+13
-0
dist/mcp/fix-handlers.d.ts

@@ -24,1 +24,14 @@ /**

}>;
export declare function handleBulkFixMetadata(daClient: DaClient, edsClient: EdsClient, args: {
pages: Array<{
path: string;
metadata: MetadataFields;
}>;
dryRun?: boolean;
publish?: boolean;
}): Promise<{
content: {
type: "text";
text: string;
}[];
}>;

@@ -20,2 +20,14 @@ /**

}
/** Run `fn` over `items` with at most `concurrency` in flight at once. */
async function mapWithConcurrency(items, fn, concurrency) {
let next = 0;
const worker = async () => {
while (next < items.length) {
const i = next++;
await fn(items[i]);
}
};
const size = Math.max(1, Math.min(concurrency, items.length));
await Promise.all(Array.from({ length: size }, () => worker()));
}
export async function handleFixMetadata(daClient, edsClient, args) {

@@ -67,1 +79,120 @@ try {

}
export async function handleBulkFixMetadata(daClient, edsClient, args) {
try {
// Dedupe input by DA-normalized path (case-sensitive; strip a leading "/"
// and a trailing ".html"), MERGING metadata so two entries for the same page
// combine rather than racing each other on write (which would produce a
// nondeterministic result and a broken undo).
const deduped = new Map();
for (const p of args.pages) {
const key = p.path.replace(/^\/+/, '').replace(/\.html$/i, '');
const existing = deduped.get(key);
if (existing)
existing.metadata = { ...existing.metadata, ...p.metadata };
else
deduped.set(key, { path: p.path, metadata: { ...p.metadata } });
}
const pages = [...deduped.values()];
// 1. Read + transform each page (bounded concurrency). Collect only the
// pages that actually change; record read failures without aborting.
const plans = [];
const readFailed = [];
await mapWithConcurrency(pages, async (p) => {
try {
const source = await daClient.getSource(p.path);
const { html, changes } = applyMetadata(source.content, p.metadata);
if (changes.length > 0) {
plans.push({ path: p.path, sourcePath: source.path, contentType: source.contentType, html, fields: changes.map((c) => c.field) });
}
}
catch (e) {
readFailed.push({ path: p.path, error: e instanceof Error ? e.message : String(e) });
}
}, 6);
const unchanged = pages.length - plans.length - readFailed.length;
// 2. Dry run: full plan, no writes.
if (args.dryRun) {
const lines = [
`Dry run — nothing written. ${plans.length} page(s) would change, ${unchanged} already correct, ${readFailed.length} unreadable.`,
];
if (plans.length > 0) {
lines.push('', 'Would change:');
for (const pl of plans)
lines.push(` ${pl.sourcePath}: ${pl.fields.join(', ')}`);
}
if (readFailed.length > 0) {
lines.push('', 'Could not read:');
for (const f of readFailed)
lines.push(` ✗ ${f.path} — ${f.error}`);
}
return textResult(lines.join('\n'));
}
if (plans.length === 0) {
// Lead with the failure when nothing could be read (e.g. bad token) so it
// doesn't read as a success.
if (readFailed.length > 0) {
const lines = [
`Nothing written — ${readFailed.length} page(s) could not be read; ${unchanged} already correct.`,
'',
'Could not read:',
];
for (const f of readFailed)
lines.push(` ✗ ${f.path} — ${f.error}`);
return textResult(lines.join('\n'));
}
return textResult(`No changes needed — ${unchanged} page(s) already correct.`);
}
// 3. Push ALL changed pages in ONE batch → a single aggregated undo.
const result = await daClient.pushDocuments(plans.map((pl) => ({ path: pl.sourcePath, content: pl.html, contentType: pl.contentType })), { withUndo: true });
const lines = [
`Fixed ${result.succeeded.length} page(s) in one batch; ${result.failed.length} write failure(s); ${unchanged} already correct; ${readFailed.length} unreadable.`,
];
// 4. Optionally publish the pages that were written.
if (args.publish && result.succeeded.length > 0) {
const written = new Set(result.succeeded);
const toPublish = plans.filter((pl) => written.has(pl.sourcePath)).map((pl) => pl.path);
let published = 0;
const pubFailed = [];
await mapWithConcurrency(toPublish, async (path) => {
try {
await edsClient.previewAndPublish(path);
published++;
}
catch {
pubFailed.push(path);
}
}, 6);
lines.push(`Published ${published}/${toPublish.length} page(s) live${pubFailed.length ? ` (${pubFailed.length} publish failure(s))` : ''}.`);
}
else if (result.succeeded.length > 0) {
lines.push('(Written to DA. Pass publish:true to make the batch live.)');
}
if (readFailed.length > 0) {
lines.push('', 'Could not read:');
for (const f of readFailed)
lines.push(` ✗ ${f.path} — ${f.error}`);
}
if (result.failed.length > 0) {
lines.push('', 'Write failures:');
for (const f of result.failed)
lines.push(` ✗ ${f.path} — ${f.error}`);
}
// Return the aggregated undo — but never inline a giant blob. A large batch's
// undo carries every page's full prior HTML; past a size cap, inlining it is
// unusable, so advise smaller batches (DA versioning is the per-page fallback).
if (result.undo && (result.undo.restore.length > 0 || result.undo.remove.length > 0)) {
const undoJson = JSON.stringify({ undo: result.undo });
const UNDO_INLINE_CAP = 200_000;
if (undoJson.length <= UNDO_INLINE_CAP) {
lines.push('', 'To undo this ENTIRE batch in one call, use eds_da_rollback with:', undoJson);
}
else {
lines.push('', `(This batch's undo is ${Math.round(undoJson.length / 1024)} KB — too large to return inline. For a single returnable undo, run smaller batches; DA also versions every page, so any page can be reverted from its version history.)`);
}
}
return textResult(lines.join('\n'));
}
catch (error) {
return errorResult(error);
}
}
+1
-1
/**
* MCP server factory for the EDS MCP server.
*
* Creates a {@link McpServer} instance with all 34 tools registered.
* Creates a {@link McpServer} instance with all 35 tools registered.
* Tool naming follows the `eds_{verb}_{noun}` convention used by Adobe's

@@ -6,0 +6,0 @@ * first-party MCP servers.

/**
* MCP server factory for the EDS MCP server.
*
* Creates a {@link McpServer} instance with all 34 tools registered.
* Creates a {@link McpServer} instance with all 35 tools registered.
* Tool naming follows the `eds_{verb}_{noun}` convention used by Adobe's

@@ -376,3 +376,28 @@ * first-party MCP servers.

});
server.tool('eds_bulk_fix_metadata', 'Fix SEO/social metadata across MANY pages in one reversible operation. Takes a list of { path, metadata } (the agent supplies each page\'s values after auditing). Writes all changed pages in a single batch and returns ONE undo object that reverts the entire batch via eds_da_rollback. dryRun previews the whole plan; publish:true previews+publishes the batch live. Requires EDS_DA_TOKEN. Pair with eds_audit_site to fix a site\'s findings at once.', {
pages: z
.array(z.object({
path: edsPath.describe('Site-relative page path'),
metadata: z
.object({
title: z.string().optional(),
description: z.string().optional(),
image: z.string().optional(),
imageAlt: z.string().optional(),
})
.describe('Metadata fields to set on this page (only provided ones change)'),
}))
.min(1)
.max(500)
.describe('The pages to fix, each with its own metadata values'),
dryRun: z.boolean().optional().describe('Preview the whole plan without writing (recommended first pass)'),
publish: z.boolean().optional().describe('Preview + publish the changed pages so the batch goes live'),
}, async (args) => {
const pages = args.pages.map((p) => {
const { imageAlt, ...rest } = p.metadata;
return { path: p.path, metadata: { ...rest, ...(imageAlt !== undefined ? { 'image-alt': imageAlt } : {}) } };
});
return fixHandlers.handleBulkFixMetadata(daClient, client, { pages, dryRun: args.dryRun, publish: args.publish });
});
return server;
}
{
"name": "@focusgts/eds-mcp-server",
"version": "0.9.0",
"version": "0.10.0",
"mcpName": "io.github.focusgts/eds-mcp-server",

@@ -5,0 +5,0 @@ "description": "MCP server for Adobe Edge Delivery Services — preview, publish, metrics, and content operations",

@@ -13,3 +13,3 @@ <div align="center">

**34 tools. No extra dependencies beyond the MCP SDK. Works with any EDS site.**
**35 tools. No extra dependencies beyond the MCP SDK. Works with any EDS site.**
The first MCP server purpose-built for Edge Delivery Services.

@@ -44,3 +44,3 @@

flowchart LR
A["AI agent<br/>(Claude Code · Cursor · Copilot)"] -- MCP / stdio --> B["eds-mcp-server<br/>34 tools"]
A["AI agent<br/>(Claude Code · Cursor · Copilot)"] -- MCP / stdio --> B["eds-mcp-server<br/>35 tools"]
B --> C["Admin API<br/>admin.hlx.page"]

@@ -80,3 +80,3 @@ B --> D["Content API<br/>*.aem.live"]

## 🛠️ The 34 tools
## 🛠️ The 35 tools

@@ -168,4 +168,7 @@ ### Edge Delivery Services — publish, content, analytics

- `eds_fix_metadata`
- `eds_bulk_fix_metadata`
> **It fixes what it finds — reversibly.** `eds_fix_metadata` repairs a page's title, meta description and Open Graph image by editing its Document Authoring source, routed through the same **dry-run + undo** path as the write tools. The agent supplies the content (e.g. writes a fitting description); the tool writes it *correctly and idempotently* (merges into the page's Metadata block, never duplicates it). Pass `publish: true` to preview + publish so the change goes live. The full loop: **audit → fix → publish → re-audit to zero.**
> **It fixes what it finds — reversibly.** `eds_fix_metadata` repairs a page's title, meta description and Open Graph image by editing its Document Authoring source, routed through the same **dry-run + undo** path as the write tools. The agent supplies the content (e.g. writes a fitting description); the tool writes it *correctly and idempotently* (merges into the page's Metadata block, never duplicates it). Pass `publish: true` to preview + publish so the change goes live.
>
> **`eds_bulk_fix_metadata`** does it across a **whole site in one reversible operation** — pass a list of `{ path, metadata }`, and it writes every changed page in a single batch that returns **one** undo reverting all of it. The full loop: **`eds_audit_site` → fix the batch → publish → re-audit to zero** — with a single undo if anything looks off.

@@ -172,0 +175,0 @@ ---