New:Socket for Asana Is Now Available.Learn more
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.6.0
to
0.7.0
+16
-1
dist/da-admin/client.d.ts

@@ -10,3 +10,3 @@ /**

*/
import type { DaClientOptions, DaSourceEntry, DaSourceContent, DaVersion, DaOperationResponse, DaDocument, DaExportResult, DaPushResult } from './types.js';
import type { DaClientOptions, DaSourceEntry, DaSourceContent, DaVersion, DaOperationResponse, DaDocument, DaExportResult, DaPushResult, DaPushPreview, DaUndo } from './types.js';
/** Friendly message shown when a DA operation is attempted without a token. */

@@ -73,3 +73,18 @@ export declare const NEEDS_DA_TOKEN_MESSAGE = "No DA token configured. Set EDS_DA_TOKEN to a Document Authoring API token to use the DA content tools \u2014 see the README.";

concurrency?: number;
withUndo?: boolean;
}): Promise<DaPushResult>;
/**
* Preview a push without writing: classify each document as create / update /
* unchanged (with line-change counts for updates). Read-only, so always safe.
*/
previewPush(documents: DaDocument[]): Promise<DaPushPreview>;
/**
* Undo a push: re-write the prior content of updated docs and delete the docs
* the push created. Takes the `undo` object returned by a `withUndo` push.
*/
rollback(undo: DaUndo): Promise<DaPushResult>;
/** Get a document's source, or null if it does not exist (404). */
private getSourceOrNull;
/** The site-relative document path a write would target (with .html applied). */
private docPath;
/** Run `fn` over `items` with at most `concurrency` in flight at once. */

@@ -76,0 +91,0 @@ private mapWithConcurrency;

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

}
/** Count added/removed lines between two versions (multiset line difference). */
function lineChanges(oldContent, newContent) {
const tally = (text) => {
const m = new Map();
for (const line of text.split('\n'))
m.set(line, (m.get(line) ?? 0) + 1);
return m;
};
const oldT = tally(oldContent);
const newT = tally(newContent);
let added = 0;
let removed = 0;
for (const [line, n] of newT)
added += Math.max(0, n - (oldT.get(line) ?? 0));
for (const [line, n] of oldT)
removed += Math.max(0, n - (newT.get(line) ?? 0));
return { added, removed };
}
export class DaClient {

@@ -255,6 +273,29 @@ token;

const failed = [];
const restore = [];
const remove = [];
await this.mapWithConcurrency(documents, async (doc) => {
try {
// Read prior state first (needed for the undo entry and to skip no-op
// writes), but only RECORD the undo entry AFTER the write succeeds.
// Recording before the write would let a failed write leave a phantom
// undo entry — e.g. a `remove` for a doc that was never created, which
// rollback would then delete: the safety feature causing data loss.
let prior = null;
if (options.withUndo) {
prior = await this.getSourceOrNull(doc.path);
// Already in the desired state: don't write a spurious version, and
// there's nothing to undo. Mirrors previewPush's `unchanged`.
if (prior && prior.content === doc.content) {
succeeded.push(doc.path);
return;
}
}
await this.putSource(doc.path, doc.content, doc.contentType);
succeeded.push(doc.path);
if (options.withUndo) {
if (prior)
restore.push(prior);
else
remove.push(this.docPath(doc.path));
}
}

@@ -265,4 +306,64 @@ catch (error) {

}, concurrency);
return { succeeded, failed };
const result = { succeeded, failed };
if (options.withUndo)
result.undo = { restore, remove };
return result;
}
/**
* Preview a push without writing: classify each document as create / update /
* unchanged (with line-change counts for updates). Read-only, so always safe.
*/
async previewPush(documents) {
const plan = [];
await this.mapWithConcurrency(documents, async (doc) => {
const prior = await this.getSourceOrNull(doc.path);
if (prior === null) {
plan.push({ path: this.docPath(doc.path), action: 'create' });
}
else if (prior.content === doc.content) {
plan.push({ path: prior.path, action: 'unchanged' });
}
else {
plan.push({ path: prior.path, action: 'update', changes: lineChanges(prior.content, doc.content) });
}
}, 6);
const summary = { create: 0, update: 0, unchanged: 0 };
for (const e of plan)
summary[e.action] += 1;
return { plan, summary };
}
/**
* Undo a push: re-write the prior content of updated docs and delete the docs
* the push created. Takes the `undo` object returned by a `withUndo` push.
*/
async rollback(undo) {
const restored = await this.pushDocuments(undo.restore);
const removed = [];
const failed = [...restored.failed];
await this.mapWithConcurrency(undo.remove, async (path) => {
try {
await this.deleteSource(path);
removed.push(this.docPath(path));
}
catch (error) {
failed.push({ path, error: error instanceof Error ? error.message : String(error) });
}
}, 6);
return { succeeded: [...restored.succeeded, ...removed], failed };
}
/** Get a document's source, or null if it does not exist (404). */
async getSourceOrNull(path) {
try {
return await this.getSource(path);
}
catch (error) {
if (error instanceof EdsApiError && error.status === 404)
return null;
throw error;
}
}
/** The site-relative document path a write would target (with .html applied). */
docPath(path) {
return `/${this.normalizeDocPath(path)}`;
}
/** Run `fn` over `items` with at most `concurrency` in flight at once. */

@@ -269,0 +370,0 @@ async mapWithConcurrency(items, fn, concurrency) {

@@ -94,2 +94,38 @@ /**

}>;
/**
* Present when the push was made with `withUndo`. The reverse operation:
* `restore` re-writes the prior content of updated docs, `remove` deletes
* the docs this push newly created. Pass it to `eds_da_rollback` to undo.
*/
undo?: DaUndo;
}
/** One document's status in a dry-run push preview. */
export interface DaPushPlanEntry {
/** Site-relative document path. */
path: string;
/** What the push would do to it. */
action: 'create' | 'update' | 'unchanged';
/** For updates: line-level change counts (added/removed). */
changes?: {
added: number;
removed: number;
};
}
/** Result of a dry-run push preview (no writes performed). */
export interface DaPushPreview {
/** Per-document plan. */
plan: DaPushPlanEntry[];
/** Roll-up counts. */
summary: {
create: number;
update: number;
unchanged: number;
};
}
/** The reverse of a push, used to undo it. */
export interface DaUndo {
/** Prior content to re-write (undoes updates). */
restore: DaDocument[];
/** Paths the push created, to delete (undoes creates). */
remove: string[];
}

@@ -75,2 +75,4 @@ /**

}>;
dryRun?: boolean;
withUndo?: boolean;
}): Promise<{

@@ -82,2 +84,17 @@ content: {

}>;
export declare function handleDaRollback(client: DaClient, args: {
undo: {
restore: Array<{
path: string;
content: string;
contentType?: string;
}>;
remove: string[];
};
}): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
export declare function handleDaGetVersions(client: DaClient, args: {

@@ -84,0 +101,0 @@ path: string;

@@ -126,3 +126,19 @@ /**

try {
const result = await client.pushDocuments(args.documents);
// Dry-run: preview what would change, write nothing.
if (args.dryRun) {
const preview = await client.previewPush(args.documents);
const s = preview.summary;
const lines = [
`Dry run — nothing was written. ${s.create} create, ${s.update} update, ${s.unchanged} unchanged.`,
'',
];
for (const e of preview.plan) {
const detail = e.action === 'update' && e.changes
? ` (+${e.changes.added}/-${e.changes.removed} lines)`
: '';
lines.push(` ${e.action.toUpperCase().padEnd(9)} ${e.path}${detail}`);
}
return textResult(lines.join('\n'));
}
const result = await client.pushDocuments(args.documents, { withUndo: args.withUndo });
const lines = [

@@ -141,2 +157,5 @@ `Pushed ${result.succeeded.length} document${result.succeeded.length === 1 ? '' : 's'}; ${result.failed.length} failed.`,

}
if (result.undo) {
lines.push('', 'To undo this push, call eds_da_rollback with this exact object:', JSON.stringify({ undo: result.undo }));
}
return textResult(lines.join('\n'));

@@ -148,2 +167,19 @@ }

}
export async function handleDaRollback(client, args) {
try {
const result = await client.rollback(args.undo);
const lines = [
`Rolled back: ${result.succeeded.length} restored/removed; ${result.failed.length} failed.`,
];
if (result.failed.length > 0) {
lines.push('', 'Failed:');
for (const f of result.failed)
lines.push(` ✗ ${f.path} — ${f.error}`);
}
return textResult(lines.join('\n'));
}
catch (error) {
return errorResult(error);
}
}
export async function handleDaGetVersions(client, args) {

@@ -150,0 +186,0 @@ try {

+1
-1
/**
* MCP server factory for the EDS MCP server.
*
* Creates a {@link McpServer} instance with all 30 tools registered.
* Creates a {@link McpServer} instance with all 31 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 30 tools registered.
* Creates a {@link McpServer} instance with all 31 tools registered.
* Tool naming follows the `eds_{verb}_{noun}` convention used by Adobe's

@@ -270,3 +270,3 @@ * first-party MCP servers.

}, async (args) => daHandlers.handleDaExport(daClient, args));
server.tool('eds_da_push', 'Bulk-push many edited Document Authoring documents back in one call. The "push" half of the bulk workflow — write a whole batch of {path, content} at once. Returns per-document succeeded/failed. Requires EDS_DA_TOKEN.', {
server.tool('eds_da_push', 'Bulk-push many edited Document Authoring documents back in one call. Set dryRun to PREVIEW what would change (create/update/unchanged) without writing anything — safest to run this first. Set withUndo to make the write reversible (returns an undo object for eds_da_rollback). Requires EDS_DA_TOKEN.', {
documents: z

@@ -281,4 +281,26 @@ .array(z.object({

.describe('The documents to write back'),
dryRun: z
.boolean()
.optional()
.describe('Preview the changes without writing anything (recommended first pass)'),
withUndo: z
.boolean()
.optional()
.describe('Capture prior state so the push can be reverted with eds_da_rollback'),
}, async (args) => daHandlers.handleDaPush(daClient, args));
server.tool('eds_da_rollback', 'Undo a previous eds_da_push. Pass the exact `undo` object returned by a push that used withUndo — it restores overwritten documents and deletes newly-created ones. Requires EDS_DA_TOKEN.', {
undo: z
.object({
restore: z
.array(z.object({
path: daSourcePath.describe('Prior document path'),
content: z.string(),
contentType: z.string().optional(),
}))
.describe('Prior document contents to re-write'),
remove: z.array(daSourcePath).describe('Paths the push created, to delete'),
})
.describe('The undo object returned by a withUndo push'),
}, async (args) => daHandlers.handleDaRollback(daClient, args));
return server;
}
{
"name": "@focusgts/eds-mcp-server",
"version": "0.6.0",
"version": "0.7.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">

**30 tools. No extra dependencies beyond the MCP SDK. Works with any EDS site.**
**31 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/>30 tools"]
A["AI agent<br/>(Claude Code · Cursor · Copilot)"] -- MCP / stdio --> B["eds-mcp-server<br/>31 tools"]
B --> C["Admin API<br/>admin.hlx.page"]

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

## 🛠️ The 30 tools
## 🛠️ The 31 tools

@@ -144,5 +144,6 @@ ### Edge Delivery Services — publish, content, analytics

**Bulk ("clone")**
**Bulk ("clone") + safe writes**
- `eds_da_export`
- `eds_da_push`
- `eds_da_rollback`

@@ -154,2 +155,4 @@ </td></tr>

>
> **Safe by default.** `eds_da_push` takes `dryRun: true` to **preview** exactly what a bulk edit would do (create / update / unchanged, with line-diff counts) without writing a thing, and `withUndo: true` to make the write **reversible** — it returns an `undo` object you hand to `eds_da_rollback` to restore prior content and remove any docs the push created. Preview before writing, undo after: the difference between an impressive demo and something you'd point at a production site.
>
> `EDS_DA_TOKEN` is an Adobe IMS access token for Document Authoring — grab it from an authenticated [da.live](https://da.live) session (the IMS `access_token`). Document paths assume `.html` when no extension is given (`index` → `index.html`).

@@ -156,0 +159,0 @@