@focusgts/eds-mcp-server
Advanced tools
| /** | ||
| * Metadata fix (ADR-011) — add or correct a page's `<head>` metadata by editing | ||
| * its DA source, so the EDS pipeline emits the right `<meta>` tags. | ||
| * | ||
| * EDS reads per-page metadata from a **Metadata block** in the authored document. | ||
| * Grounded against Adobe's own pipeline (`helix-html-pipeline` | ||
| * `src/steps/extract-metadata.js`): it does `select('div.metadata', document)`, | ||
| * reads each row as `[name, value] = row.children`, and for the image field pulls | ||
| * the `src` from an `<img>` inside the value cell. So the DA source shape is: | ||
| * | ||
| * <div class="metadata"> | ||
| * <div><div>Title</div><div>value</div></div> | ||
| * <div><div>Description</div><div>value</div></div> | ||
| * <div><div>Image</div><div><img src="…"></div></div> | ||
| * </div> | ||
| * | ||
| * The writer edits that block with a depth-aware scanner (no DOM dependency), | ||
| * and is careful never to corrupt the document: | ||
| * - it **preserves untouched rows verbatim** (so an existing Image/`<img>` row or | ||
| * an authored link in a value is never flattened away when another field is | ||
| * changed); | ||
| * - it matches the metadata block even when it carries variant classes | ||
| * (`class="metadata foo"`), any attribute order, or single quotes — so it never | ||
| * appends a duplicate block that the pipeline would ignore; | ||
| * - metadata keys round-trip reversibly (`Image Alt` ⇄ `image-alt`), so repeated | ||
| * fixes are idempotent and never accumulate duplicate rows. | ||
| */ | ||
| /** Metadata fields a fix can set. Keys are matched case-insensitively. */ | ||
| export interface MetadataFields { | ||
| title?: string; | ||
| description?: string; | ||
| image?: string; | ||
| 'image-alt'?: string; | ||
| [key: string]: string | undefined; | ||
| } | ||
| /** One field the fix changed. */ | ||
| export interface MetadataChange { | ||
| field: string; | ||
| from: string | null; | ||
| to: string; | ||
| } | ||
| /** Result of applying metadata to a page's DA source. */ | ||
| export interface ApplyMetadataResult { | ||
| html: string; | ||
| changes: MetadataChange[]; | ||
| } | ||
| /** | ||
| * Find a `<div>` whose `class` attribute contains `className` as a | ||
| * whitespace-delimited token — in any attribute position, either quote style, | ||
| * and tolerating variant classes (`class="metadata foo"`). Depth-aware. | ||
| */ | ||
| export declare function findDivBlock(html: string, className: string): { | ||
| start: number; | ||
| end: number; | ||
| inner: string; | ||
| } | null; | ||
| /** Parse a metadata block's rows into a key → value map (test/inspection helper). */ | ||
| export declare function parseMetadataRows(inner: string): Map<string, string>; | ||
| /** Build a fresh `<div class="metadata">` block from a key → value map. */ | ||
| export declare function buildMetadataBlock(fields: Map<string, string>): string; | ||
| /** | ||
| * Apply metadata `fields` to a page's DA source HTML. | ||
| * | ||
| * Merges into an existing metadata block — rebuilding only the rows that change, | ||
| * preserving every other row **verbatim** (including `<img>` and authored markup) | ||
| * — or inserts a new block before `</main>` (or `</body>`). Returns the new HTML | ||
| * and the fields that actually changed; an empty change list means the page was | ||
| * already correct and `html` is returned unchanged. | ||
| */ | ||
| export declare function applyMetadata(html: string, fields: MetadataFields): ApplyMetadataResult; |
| /** | ||
| * Metadata fix (ADR-011) — add or correct a page's `<head>` metadata by editing | ||
| * its DA source, so the EDS pipeline emits the right `<meta>` tags. | ||
| * | ||
| * EDS reads per-page metadata from a **Metadata block** in the authored document. | ||
| * Grounded against Adobe's own pipeline (`helix-html-pipeline` | ||
| * `src/steps/extract-metadata.js`): it does `select('div.metadata', document)`, | ||
| * reads each row as `[name, value] = row.children`, and for the image field pulls | ||
| * the `src` from an `<img>` inside the value cell. So the DA source shape is: | ||
| * | ||
| * <div class="metadata"> | ||
| * <div><div>Title</div><div>value</div></div> | ||
| * <div><div>Description</div><div>value</div></div> | ||
| * <div><div>Image</div><div><img src="…"></div></div> | ||
| * </div> | ||
| * | ||
| * The writer edits that block with a depth-aware scanner (no DOM dependency), | ||
| * and is careful never to corrupt the document: | ||
| * - it **preserves untouched rows verbatim** (so an existing Image/`<img>` row or | ||
| * an authored link in a value is never flattened away when another field is | ||
| * changed); | ||
| * - it matches the metadata block even when it carries variant classes | ||
| * (`class="metadata foo"`), any attribute order, or single quotes — so it never | ||
| * appends a duplicate block that the pipeline would ignore; | ||
| * - metadata keys round-trip reversibly (`Image Alt` ⇄ `image-alt`), so repeated | ||
| * fixes are idempotent and never accumulate duplicate rows. | ||
| */ | ||
| const LABELS = { | ||
| title: 'Title', | ||
| description: 'Description', | ||
| image: 'Image', | ||
| 'image-alt': 'Image Alt', | ||
| url: 'URL', | ||
| }; | ||
| /** Canonical key ⇄ display label. Reversible: `Image Alt` ⇄ `image-alt`. */ | ||
| function normalizeKey(raw) { | ||
| return raw.trim().toLowerCase().replace(/\s+/g, '-'); | ||
| } | ||
| function labelFor(key) { | ||
| return LABELS[key] ?? key.replace(/(^|-)([a-z])/g, (_, s, c) => (s === '-' ? ' ' : '') + c.toUpperCase()); | ||
| } | ||
| function escapeHtml(s) { | ||
| return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | ||
| } | ||
| /** Decode entities in a single left-to-right pass (so `&lt;` → `<`, not `<`). */ | ||
| function decodeEntities(s) { | ||
| const map = { amp: '&', lt: '<', gt: '>', quot: '"', '#x27': "'", '#39': "'" }; | ||
| return s.replace(/&(amp|lt|gt|quot|#x27|#39);/g, (_, e) => map[e]); | ||
| } | ||
| function stripTags(s) { | ||
| return decodeEntities(s.replace(/<[^>]*>/g, '')).trim(); | ||
| } | ||
| /** | ||
| * The comparable value of a metadata value cell: an image's `src` when the cell | ||
| * holds an `<img>` (matching the pipeline), otherwise its plain text. | ||
| */ | ||
| function cellValue(inner) { | ||
| const img = /<img\b[^>]*\bsrc\s*=\s*("([^"]*)"|'([^']*)')/i.exec(inner); | ||
| if (img) | ||
| return img[2] ?? img[3] ?? ''; | ||
| return stripTags(inner); | ||
| } | ||
| /** From `innerStart`, find the matching `</div>` (depth-aware, ignores `<div/>`). */ | ||
| function scanDivClose(html, innerStart) { | ||
| const tag = /<div\b[^>]*>|<\/div\s*>/gi; | ||
| tag.lastIndex = innerStart; | ||
| let depth = 1; | ||
| let m; | ||
| while ((m = tag.exec(html)) !== null) { | ||
| if (m[0][1] === '/') { | ||
| depth--; | ||
| if (depth === 0) | ||
| return { innerEnd: m.index, end: tag.lastIndex }; | ||
| } | ||
| else if (!m[0].endsWith('/>')) { | ||
| depth++; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Find a `<div>` whose `class` attribute contains `className` as a | ||
| * whitespace-delimited token — in any attribute position, either quote style, | ||
| * and tolerating variant classes (`class="metadata foo"`). Depth-aware. | ||
| */ | ||
| export function findDivBlock(html, className) { | ||
| const divRe = /<div\b([^>]*)>/gi; | ||
| let m; | ||
| while ((m = divRe.exec(html)) !== null) { | ||
| if (m[0].endsWith('/>')) | ||
| continue; | ||
| const classAttr = /\bclass\s*=\s*("([^"]*)"|'([^']*)')/i.exec(m[1]); | ||
| if (!classAttr) | ||
| continue; | ||
| const classes = (classAttr[2] ?? classAttr[3] ?? '').split(/\s+/); | ||
| if (!classes.includes(className)) | ||
| continue; | ||
| const innerStart = m.index + m[0].length; | ||
| const close = scanDivClose(html, innerStart); | ||
| if (!close) | ||
| return null; | ||
| return { start: m.index, end: close.end, inner: html.slice(innerStart, close.innerEnd) }; | ||
| } | ||
| return null; | ||
| } | ||
| /** Each top-level `<div>` block's outer + inner HTML (depth-aware). */ | ||
| function topLevelDivBlocks(html) { | ||
| const out = []; | ||
| let i = 0; | ||
| while (i < html.length) { | ||
| const open = /<div\b[^>]*>/i.exec(html.slice(i)); | ||
| if (!open) | ||
| break; | ||
| const openAbs = i + open.index; | ||
| if (open[0].endsWith('/>')) { | ||
| i = openAbs + open[0].length; | ||
| continue; | ||
| } | ||
| const innerStart = openAbs + open[0].length; | ||
| const close = scanDivClose(html, innerStart); | ||
| if (!close) | ||
| break; | ||
| out.push({ outer: html.slice(openAbs, close.end), inner: html.slice(innerStart, close.innerEnd) }); | ||
| i = close.end; | ||
| } | ||
| return out; | ||
| } | ||
| function parseRows(blockInner) { | ||
| const rows = []; | ||
| for (const row of topLevelDivBlocks(blockInner)) { | ||
| const cells = topLevelDivBlocks(row.inner); | ||
| if (cells.length >= 2) { | ||
| rows.push({ key: normalizeKey(stripTags(cells[0].inner)), value: cellValue(cells[1].inner), outer: row.outer }); | ||
| } | ||
| else { | ||
| rows.push({ key: '', value: '', outer: row.outer }); // malformed — preserve verbatim | ||
| } | ||
| } | ||
| return rows; | ||
| } | ||
| /** Parse a metadata block's rows into a key → value map (test/inspection helper). */ | ||
| export function parseMetadataRows(inner) { | ||
| const map = new Map(); | ||
| for (const r of parseRows(inner)) | ||
| if (r.key) | ||
| map.set(r.key, r.value); | ||
| return map; | ||
| } | ||
| function valueCell(key, value) { | ||
| // The pipeline resolves the image field from an <img> in the cell. | ||
| if (key === 'image') | ||
| return `<img src="${escapeHtml(value)}">`; | ||
| return `<p>${escapeHtml(value)}</p>`; | ||
| } | ||
| function buildRow(key, value, indent = ' ') { | ||
| return (`${indent}<div>\n` + | ||
| `${indent} <div><p>${escapeHtml(labelFor(key))}</p></div>\n` + | ||
| `${indent} <div>${valueCell(key, value)}</div>\n` + | ||
| `${indent}</div>`); | ||
| } | ||
| function assembleBlock(rowHtml) { | ||
| return `<div class="metadata">\n${rowHtml.join('\n')}\n </div>`; | ||
| } | ||
| /** Build a fresh `<div class="metadata">` block from a key → value map. */ | ||
| export function buildMetadataBlock(fields) { | ||
| const rows = [...fields.entries()].filter(([, v]) => v !== undefined && v !== '').map(([k, v]) => buildRow(k, v)); | ||
| return assembleBlock(rows); | ||
| } | ||
| /** | ||
| * Apply metadata `fields` to a page's DA source HTML. | ||
| * | ||
| * Merges into an existing metadata block — rebuilding only the rows that change, | ||
| * preserving every other row **verbatim** (including `<img>` and authored markup) | ||
| * — or inserts a new block before `</main>` (or `</body>`). Returns the new HTML | ||
| * and the fields that actually changed; an empty change list means the page was | ||
| * already correct and `html` is returned unchanged. | ||
| */ | ||
| export function applyMetadata(html, fields) { | ||
| const block = findDivBlock(html, 'metadata'); | ||
| const rows = block ? parseRows(block.inner) : []; | ||
| const byKey = new Map(rows.filter((r) => r.key).map((r) => [r.key, r])); | ||
| const changes = []; | ||
| const newValueByKey = new Map(); | ||
| for (const [rawKey, value] of Object.entries(fields)) { | ||
| if (value === undefined) | ||
| continue; | ||
| const key = normalizeKey(rawKey); | ||
| const from = byKey.get(key)?.value ?? null; | ||
| if (from !== value) { | ||
| changes.push({ field: key, from, to: value }); | ||
| newValueByKey.set(key, value); | ||
| } | ||
| } | ||
| if (changes.length === 0) | ||
| return { html, changes: [] }; | ||
| // Assemble: rebuild only changed rows; keep every other row byte-for-byte. | ||
| const assembled = []; | ||
| const used = new Set(); | ||
| for (const r of rows) { | ||
| if (r.key && newValueByKey.has(r.key)) { | ||
| assembled.push(buildRow(r.key, newValueByKey.get(r.key))); | ||
| } | ||
| else { | ||
| assembled.push(r.outer.trim()); | ||
| } | ||
| if (r.key) | ||
| used.add(r.key); | ||
| } | ||
| for (const [key, value] of newValueByKey) { | ||
| if (!used.has(key)) | ||
| assembled.push(buildRow(key, value)); | ||
| } | ||
| const newBlock = assembleBlock(assembled); | ||
| if (block) { | ||
| return { html: html.slice(0, block.start) + newBlock + html.slice(block.end), changes }; | ||
| } | ||
| const mainClose = html.search(/<\/main>/i); | ||
| if (mainClose >= 0) | ||
| return { html: `${html.slice(0, mainClose)}${newBlock}\n${html.slice(mainClose)}`, changes }; | ||
| const bodyClose = html.search(/<\/body>/i); | ||
| if (bodyClose >= 0) | ||
| return { html: `${html.slice(0, bodyClose)}${newBlock}\n${html.slice(bodyClose)}`, changes }; | ||
| return { html: `${html}\n${newBlock}`, changes }; | ||
| } |
| /** | ||
| * MCP tool handlers for the safe-fix layer (ADR-011). | ||
| * | ||
| * `eds_fix_metadata` repairs a page's `<head>` metadata by editing its DA source | ||
| * through the ADR-009 safe-writes path (preview + undo), and can optionally | ||
| * preview+publish so the change goes live. The agent supplies the content; this | ||
| * handler writes it correctly and reversibly. | ||
| */ | ||
| import type { EdsClient } from '../eds-admin/client.js'; | ||
| import type { DaClient } from '../da-admin/client.js'; | ||
| import { type MetadataFields } from '../fix/metadata.js'; | ||
| export declare function handleFixMetadata(daClient: DaClient, edsClient: EdsClient, args: { | ||
| path: string; | ||
| metadata: MetadataFields; | ||
| dryRun?: boolean; | ||
| withUndo?: boolean; | ||
| publish?: boolean; | ||
| }): Promise<{ | ||
| content: { | ||
| type: "text"; | ||
| text: string; | ||
| }[]; | ||
| }>; |
| /** | ||
| * MCP tool handlers for the safe-fix layer (ADR-011). | ||
| * | ||
| * `eds_fix_metadata` repairs a page's `<head>` metadata by editing its DA source | ||
| * through the ADR-009 safe-writes path (preview + undo), and can optionally | ||
| * preview+publish so the change goes live. The agent supplies the content; this | ||
| * handler writes it correctly and reversibly. | ||
| */ | ||
| import { formatError } from '../utils/errors.js'; | ||
| import { applyMetadata } from '../fix/metadata.js'; | ||
| function textResult(text) { | ||
| return { content: [{ type: 'text', text }] }; | ||
| } | ||
| function errorResult(error) { | ||
| return { | ||
| content: [{ type: 'text', text: `Error: ${formatError(error)}` }], | ||
| isError: true, | ||
| }; | ||
| } | ||
| export async function handleFixMetadata(daClient, edsClient, args) { | ||
| try { | ||
| const source = await daClient.getSource(args.path); | ||
| const { html, changes } = applyMetadata(source.content, args.metadata); | ||
| if (changes.length === 0) { | ||
| return textResult(`No metadata changes needed for ${source.path} — already correct.`); | ||
| } | ||
| // Dry run: show the before/after, write nothing. | ||
| if (args.dryRun) { | ||
| const lines = [ | ||
| `Dry run — nothing written. ${changes.length} metadata field(s) would change on ${source.path}:`, | ||
| '', | ||
| ]; | ||
| for (const c of changes) { | ||
| lines.push(` ${c.field}: ${c.from === null ? '(none)' : `"${c.from}"`} → "${c.to}"`); | ||
| } | ||
| return textResult(lines.join('\n')); | ||
| } | ||
| // Write through the safe-writes path. | ||
| const result = await daClient.pushDocuments([{ path: source.path, content: html, contentType: source.contentType }], { withUndo: args.withUndo }); | ||
| if (result.failed.length > 0) { | ||
| return errorResult(new Error(`Failed to write ${source.path}: ${result.failed[0].error}`)); | ||
| } | ||
| const lines = [`Updated metadata on ${source.path}: ${changes.map((c) => c.field).join(', ')}.`]; | ||
| // Optionally make it live — a DA write alone is not visible until republished. | ||
| if (args.publish) { | ||
| try { | ||
| await edsClient.previewAndPublish(args.path); | ||
| lines.push('Previewed + published — the change is live.'); | ||
| } | ||
| catch (e) { | ||
| lines.push(`(Written to DA, but publish failed: ${formatError(e)} — run eds_preview_and_publish manually.)`); | ||
| } | ||
| } | ||
| else { | ||
| lines.push('(Written to DA. Pass publish:true, or run eds_preview_and_publish, to make it live.)'); | ||
| } | ||
| if (result.undo) { | ||
| lines.push('', 'To undo this change, call eds_da_rollback with:', JSON.stringify({ undo: result.undo })); | ||
| } | ||
| return textResult(lines.join('\n')); | ||
| } | ||
| catch (error) { | ||
| return errorResult(error); | ||
| } | ||
| } |
@@ -18,2 +18,4 @@ /** | ||
| ref?: string; | ||
| /** How long to wait for the browser callback, in ms. Defaults to 120000. */ | ||
| timeoutMs?: number; | ||
| } | ||
@@ -20,0 +22,0 @@ /** Result of a successful login. */ |
@@ -80,2 +80,3 @@ /** | ||
| const ref = options.ref ?? 'main'; | ||
| const timeoutMs = options.timeoutMs ?? LOGIN_TIMEOUT_MS; | ||
| const state = generateNonce(); | ||
@@ -200,10 +201,10 @@ return new Promise((resolve, reject) => { | ||
| openBrowser(loginUrl); | ||
| process.stderr.write(`\nWaiting for sign-in to complete (timeout ${LOGIN_TIMEOUT_MS / 1000}s)...\n` + | ||
| process.stderr.write(`\nWaiting for sign-in to complete (timeout ${timeoutMs / 1000}s)...\n` + | ||
| 'Use Google Chrome or Firefox to sign in (Safari blocks the local callback). ' + | ||
| "If it doesn't complete, set EDS_API_KEY instead — see the README.\n"); | ||
| timeout = setTimeout(() => { | ||
| settle(() => reject(new Error(`Login timed out after ${LOGIN_TIMEOUT_MS / 1000}s — no callback was received. ` + | ||
| settle(() => reject(new Error(`Login timed out after ${timeoutMs / 1000}s — no callback was received. ` + | ||
| 'Safari is not supported (it blocks the local callback) — use Google Chrome or Firefox. ' + | ||
| 'Alternatively, set EDS_API_KEY instead of signing in — see the README.'))); | ||
| }, LOGIN_TIMEOUT_MS); | ||
| }, timeoutMs); | ||
| timeout.unref(); | ||
@@ -210,0 +211,0 @@ }); |
| /** | ||
| * MCP server factory for the EDS MCP server. | ||
| * | ||
| * Creates a {@link McpServer} instance with all 33 tools registered. | ||
| * Creates a {@link McpServer} instance with all 34 tools registered. | ||
| * Tool naming follows the `eds_{verb}_{noun}` convention used by Adobe's | ||
@@ -6,0 +6,0 @@ * first-party MCP servers. |
+38
-1
| /** | ||
| * MCP server factory for the EDS MCP server. | ||
| * | ||
| * Creates a {@link McpServer} instance with all 33 tools registered. | ||
| * Creates a {@link McpServer} instance with all 34 tools registered. | ||
| * Tool naming follows the `eds_{verb}_{noun}` convention used by Adobe's | ||
@@ -19,2 +19,3 @@ * first-party MCP servers. | ||
| import * as auditHandlers from './audit-handlers.js'; | ||
| import * as fixHandlers from './fix-handlers.js'; | ||
| import { ALL_DIMENSIONS } from '../audit/types.js'; | ||
@@ -340,3 +341,39 @@ const require = createRequire(import.meta.url); | ||
| }, async (args) => auditHandlers.handleAuditSite(client, args)); | ||
| // ------------------------------------------------------------------------- | ||
| // Safe fixes (ADR-011) — repair audit findings through the safe-writes layer | ||
| // ------------------------------------------------------------------------- | ||
| server.tool('eds_fix_metadata', 'Fix a page\'s SEO/social metadata (title, description, Open Graph image) by editing its Document Authoring source. Adds or updates the page\'s Metadata block idempotently, through the safe-writes path (dry-run + undo). The AGENT supplies the values (e.g. write a good meta description); this tool writes them correctly and reversibly. Requires EDS_DA_TOKEN. Set publish:true to preview+publish so the change goes live.', { | ||
| path: edsPath.describe('Site-relative page path to fix (e.g. /blog/post)'), | ||
| metadata: z | ||
| .object({ | ||
| title: z.string().optional().describe('Page title (aim for 30–60 characters)'), | ||
| description: z.string().optional().describe('Meta description (aim for 120–160 characters)'), | ||
| image: z.string().optional().describe('Open Graph / social share image URL'), | ||
| imageAlt: z.string().optional().describe('Alt text for the social image'), | ||
| }) | ||
| .describe('Metadata fields to set (only the ones provided are changed)'), | ||
| dryRun: z | ||
| .boolean() | ||
| .optional() | ||
| .describe('Preview the before/after without writing (recommended first pass)'), | ||
| withUndo: z | ||
| .boolean() | ||
| .optional() | ||
| .describe('Make the write reversible — returns an undo object for eds_da_rollback'), | ||
| publish: z | ||
| .boolean() | ||
| .optional() | ||
| .describe('Preview + publish the page after writing so the change goes live'), | ||
| }, async (args) => { | ||
| const { imageAlt, ...rest } = args.metadata; | ||
| const metadata = { ...rest, ...(imageAlt !== undefined ? { 'image-alt': imageAlt } : {}) }; | ||
| return fixHandlers.handleFixMetadata(daClient, client, { | ||
| path: args.path, | ||
| metadata, | ||
| dryRun: args.dryRun, | ||
| withUndo: args.withUndo, | ||
| publish: args.publish, | ||
| }); | ||
| }); | ||
| return server; | ||
| } |
+1
-1
| { | ||
| "name": "@focusgts/eds-mcp-server", | ||
| "version": "0.8.0", | ||
| "version": "0.9.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", |
+10
-4
@@ -13,3 +13,3 @@ <div align="center"> | ||
| **33 tools. No extra dependencies beyond the MCP SDK. Works with any EDS site.** | ||
| **34 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/>33 tools"] | ||
| A["AI agent<br/>(Claude Code · Cursor · Copilot)"] -- MCP / stdio --> B["eds-mcp-server<br/>34 tools"] | ||
| B --> C["Admin API<br/>admin.hlx.page"] | ||
@@ -80,3 +80,3 @@ B --> D["Content API<br/>*.aem.live"] | ||
| ## 🛠️ The 33 tools | ||
| ## 🛠️ The 34 tools | ||
@@ -163,4 +163,10 @@ ### Edge Delivery Services — publish, content, analytics | ||
| > **It tells you what's wrong.** `eds_audit_site` sweeps the whole site (or a subtree) and returns a **prioritized** list of issues across **SEO** (missing titles/descriptions, no H1, blocked from indexing), **accessibility** (images without alt text, missing landmarks, unlabeled form inputs), **freshness** (pages not updated in over a year), **sitemap coverage**, and — with a `domain` — **performance** (Core Web Vitals) and **404s** from Adobe's own real-user data. `eds_audit_page` does the same for one page. Read-only and safe to run anytime. Pair it with the `eds_da_*` write tools (dry-run + undo) to fix what it finds. | ||
| > **It tells you what's wrong.** `eds_audit_site` sweeps the whole site (or a subtree) and returns a **prioritized** list of issues across **SEO** (missing titles/descriptions, no H1, blocked from indexing), **accessibility** (images without alt text, missing landmarks, unlabeled form inputs), **freshness** (pages not updated in over a year), **sitemap coverage**, and — with a `domain` — **performance** (Core Web Vitals) and **404s** from Adobe's own real-user data. `eds_audit_page` does the same for one page. Read-only and safe to run anytime. | ||
| ### Safe fixes — repair what the audit finds | ||
| - `eds_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.** | ||
| --- | ||
@@ -167,0 +173,0 @@ |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
249569
8.09%45
9.76%5570
8.22%284
2.16%