import "../../../base64-CmWvODNW.mjs";
import "../../../types-CuW0zWDA.mjs";
import { t as apiError } from "../../../error-CkAseSqZ.mjs";
import { Dn as contentBylineInputSchema, Tn as bylineUpdateBody, un as contentSeoInput, yn as bylineCreateBody } from "../../../redirects-DXEXBAII.mjs";
import "../../../byline-fields-2Lp5Hbgf.mjs";
import "../../../status-CqJQX1QN.mjs";
import "../../../api/schemas/index.mjs";
import { i as hasScope } from "../../../api-tokens-DsvvCtdh.mjs";
import { n as convertDataForWrite, t as convertDataForRead } from "../../../portable-text-cCa0cGV6.mjs";
import { z } from "zod";
import { Role, canActOnOwn, hasPermission } from "@emdash-cms/auth";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

//#region src/mcp/server.ts
const COLLECTION_SLUG_PATTERN = /^[a-z][a-z0-9_]*$/;
/** http(s) scheme matcher used by `settings_update` URL validation. */
const HTTP_SCHEME_PATTERN = /^https?:\/\//i;
const settingsMediaReferenceSchema = z.object({
	mediaId: z.string().describe("Media item ID (use media_create or media_list)"),
	alt: z.string().optional().describe("Alt text for the media reference")
});
const settingsSocialSchema = z.object({
	twitter: z.string().optional(),
	github: z.string().optional(),
	facebook: z.string().optional(),
	instagram: z.string().optional(),
	linkedin: z.string().optional(),
	youtube: z.string().optional()
});
const settingsSeoSchema = z.object({
	titleSeparator: z.string().max(10).optional().describe("Separator between page title and site title (e.g. ' | ')"),
	defaultOgImage: settingsMediaReferenceSchema.optional().describe("Default Open Graph image when content has none"),
	robotsTxt: z.string().max(5e3).optional().describe("Custom robots.txt body. Leave unset for the EmDash default."),
	googleVerification: z.string().max(100).optional().describe("Google Search Console verification token"),
	bingVerification: z.string().max(100).optional().describe("Bing Webmaster Tools verification token")
});
/**
* Return a successful tool response with the data as pretty-printed JSON.
*/
function respondData(data) {
	return { content: [{
		type: "text",
		text: JSON.stringify(data, null, 2)
	}] };
}
/**
* Return a structured error tool response.
*
* The error code is emitted both in the human-readable message (as a stable
* `[CODE]` prefix that callers can match on) and in `_meta.code` so MCP-aware
* clients can read it programmatically once the SDK supports forwarding meta.
*/
function respondError(code, message, details) {
	const text = `[${code}] ${message}`;
	const meta = { code };
	if (details !== void 0) meta.details = details;
	return {
		content: [{
			type: "text",
			text
		}],
		isError: true,
		_meta: meta
	};
}
/**
* Auth/permission errors thrown from `requireScope` / `requireRole` /
* `requireOwnership` / `requireDraftAccess`. Carries a stable string `code`
* field so `respondHandlerError` can surface it through `_meta.code` and
* the message prefix.
*
* Distinct from `McpError` (which the SDK catches at JSON-RPC level — the
* code there is numeric, not a stable EmDash error code).
*/
var EmDashAuthError = class extends Error {
	name = "EmDashAuthError";
	constructor(message, code) {
		super(message);
		this.code = code;
	}
};
/**
* Map an unknown thrown error to a structured error envelope.
*
* Recognises (in priority order):
*  - `EmDashAuthError` — `code` is a stable EmDash auth code
*    (`UNAUTHORIZED`, `INSUFFICIENT_SCOPE`, `INSUFFICIENT_PERMISSIONS`).
*  - `Error` objects with an `apiError: { code, details? }` annotation
*    (handlers throw these for NOT_FOUND / CONFLICT inside transactions;
*    see `api/handlers/content.ts:538`).
*  - `SchemaError` (and any error with a string `code` field) — the code
*    is forwarded verbatim. `details` is forwarded too if present.
*  - Plain `Error` instances — message preserved, code falls back to
*    `fallbackCode` (or `INTERNAL_ERROR`).
*  - Strings — used directly as the message.
*  - Anything else — coerced via `String()`.
*
* The original message is always preserved so tests and humans can see the
* specific failure cause. Numeric `code` values (e.g. on `McpError`) are
* ignored — the field is reserved for stable string codes.
*/
function respondHandlerError(error, fallbackCode = "INTERNAL_ERROR") {
	let code = fallbackCode;
	let message;
	let details;
	if (error instanceof EmDashAuthError) {
		message = error.message || fallbackCode;
		code = error.code;
	} else if (error instanceof Error) {
		message = error.message || fallbackCode;
		const apiError = error.apiError;
		if (apiError && typeof apiError.code === "string" && apiError.code) {
			code = apiError.code;
			if (apiError.details && typeof apiError.details === "object") details = apiError.details;
		} else {
			const rawCode = error.code;
			if (typeof rawCode === "string" && rawCode) code = rawCode;
			const rawDetails = error.details;
			if (rawDetails && typeof rawDetails === "object") details = rawDetails;
		}
	} else if (typeof error === "string") message = error;
	else message = String(error);
	return respondError(code, message, details);
}
/**
* Unwrap an ApiResult<T> into MCP tool result format.
*
* On success returns the data as JSON. On failure propagates the structured
* `{ code, message, details }` from the handler so the caller sees both a
* machine-readable code (in `_meta.code` and as a `[CODE]` message prefix)
* and the original human-readable message.
*/
function unwrap(result) {
	if (result.success && result.data !== void 0) return respondData(result.data);
	const err = result.error && typeof result.error === "object" ? result.error : void 0;
	if (!err) return respondError("INTERNAL_ERROR", "Unknown error");
	return respondError(typeof err.code === "string" && err.code ? err.code : "INTERNAL_ERROR", typeof err.message === "string" && err.message ? err.message : "Unknown error", err.details && typeof err.details === "object" ? err.details : void 0);
}
/**
* Return a JSON text block (success path for tools that don't go through
* the ApiResult-returning handler layer, e.g. schema/menu/taxonomy).
*/
function jsonResult(data) {
	return respondData(data);
}
function isPublished(t) {
	return typeof t === "object" && t !== null && "status" in t && t.status === "published";
}
function getExtra(extra) {
	const payload = extra.authInfo?.extra;
	if (!payload?.emdash) throw new Error("EmDash not available — server misconfigured");
	return payload;
}
function getEmDash(extra) {
	return getExtra(extra).emdash;
}
async function getCollectionFields(ec, collection) {
	try {
		const { SchemaRegistry } = await import("../../../schema-Blhoxp3P.mjs").then((n) => n.t);
		const col = await new SchemaRegistry(ec.db).getCollectionWithFields(collection);
		return col ? col.fields : null;
	} catch {
		return null;
	}
}
/**
* Convert markdown strings to Portable Text for `portableText` fields on write.
* Non-string values pass through, so callers may still send Portable Text. If
* the schema can't be loaded, data is returned unchanged for the handler to
* validate.
*/
async function convertWriteData(ec, collection, data) {
	const fields = await getCollectionFields(ec, collection);
	if (!fields) return data;
	return convertDataForWrite(data, fields);
}
/**
* Convert `portableText` field values on each item's `data` to markdown,
* mutating in place. Inverse of {@link convertWriteData}, gated on the read
* tools' `markdown` argument.
*/
async function applyReadMarkdown(ec, collection, items) {
	const fields = await getCollectionFields(ec, collection);
	if (!fields) return;
	for (const item of items) if (item.data && typeof item.data === "object") item.data = convertDataForRead(item.data, fields, false);
}
async function invalidateBylines() {
	const { invalidateBylineCache } = await import("../../../bylines-Woq87129.mjs").then((n) => n.t);
	invalidateBylineCache();
}
/**
* Enforce a scope requirement on the current request.
*
* When tokenScopes is undefined (session auth), all operations are allowed
* since session users have full access based on their role. When scopes are
* present (token auth), the required scope must be included.
*/
function requireScope(extra, scope) {
	const payload = getExtra(extra);
	if (payload.tokenScopes && !hasScope(payload.tokenScopes, scope)) throw new EmDashAuthError(`Insufficient scope: requires ${scope}`, "INSUFFICIENT_SCOPE");
}
/**
* Defense-in-depth: enforce a minimum RBAC role on the current request.
*
* This is checked in addition to scope requirements. Even if a token has
* the right scopes (e.g. due to a bug in scope clamping), the user's
* actual role must still meet the minimum.
*/
function requireRole(extra, minRole) {
	if (getExtra(extra).userRole < minRole) throw new EmDashAuthError("Insufficient permissions for this operation", "INSUFFICIENT_PERMISSIONS");
}
/**
* Whether the current user may read non-published content (drafts, scheduled,
* trashed, revisions, compare). SUBSCRIBER may hold content:read for
* member-only published content but must not see drafts.
*/
function canReadDrafts(extra) {
	return hasPermission({ role: getExtra(extra).userRole }, "content:read_drafts");
}
/**
* Throw if the current user cannot read non-published content. Used by
* editor-only views (revisions, compare, trash, preview-url).
*/
function requireDraftAccess(extra) {
	if (!canReadDrafts(extra)) throw new EmDashAuthError("Insufficient permissions for this operation", "INSUFFICIENT_PERMISSIONS");
}
/**
* Enforce ownership-based permission checks, mirroring the REST API's
* requireOwnerPerm() pattern.
*
* If the user is the owner, checks ownPermission. Otherwise checks
* anyPermission (which requires EDITOR+ role).
*/
function requireOwnership(extra, ownerId, ownPermission, anyPermission) {
	const payload = getExtra(extra);
	if (!canActOnOwn({
		id: payload.userId,
		role: payload.userRole
	}, ownerId, ownPermission, anyPermission)) throw new EmDashAuthError("Insufficient permissions for this operation", "INSUFFICIENT_PERMISSIONS");
}
/**
* Extract the author ID from a content handler response.
*
* Content handlers return `{ item: { id, authorId, ... }, _rev? }`.
* This helper navigates that shape safely. Returns "" when authorId is
* missing or non-string (e.g. seed-imported content with no author);
* `canActOnOwn` then decides based on the caller's permissions —
* an actor with `*:edit_any` succeeds, an actor with only `*:edit_own`
* is denied with a clean permission error.
*/
function extractContentAuthorId(data) {
	if (!data || typeof data !== "object") return "";
	const obj = data;
	const item = obj.item && typeof obj.item === "object" ? obj.item : obj;
	return typeof item?.authorId === "string" ? item.authorId : "";
}
/**
* Extract the resolved ID from a content handler response.
* Handles slug -> ID resolution performed by the handler.
*/
function extractContentId(data) {
	if (!data || typeof data !== "object") return void 0;
	const obj = data;
	const item = obj.item && typeof obj.item === "object" ? obj.item : obj;
	return typeof item?.id === "string" ? item.id : void 0;
}
function createMcpServer() {
	const server = new McpServer({
		name: "emdash",
		version: "0.1.0"
	}, { capabilities: { logging: {} } });
	const originalRegisterTool = server.registerTool.bind(server);
	server.registerTool = ((name, config, callback) => {
		const wrapped = async (...callbackArgs) => {
			try {
				return await callback(...callbackArgs);
			} catch (error) {
				return respondHandlerError(error, "INTERNAL_ERROR");
			}
		};
		return originalRegisterTool(name, config, wrapped);
	});
	server.registerTool("content_list", {
		title: "List Content",
		description: "List content items in a collection with optional filtering and pagination. Returns items sorted by the specified field. Use the nextCursor value from the response to fetch the next page. Status can be 'draft', 'published', or 'scheduled'. If no status is given, all non-trashed items are returned.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug (e.g. 'posts', 'pages')"),
			status: z.enum([
				"draft",
				"published",
				"scheduled"
			]).optional().describe("Filter by content status"),
			limit: z.number().int().min(1).max(100).optional().describe("Max items to return (default 50, max 100)"),
			cursor: z.string().min(1).max(2048).optional().describe("Pagination cursor from a previous response"),
			orderBy: z.string().optional().describe("Field to sort by (e.g. 'created_at', 'updated_at')"),
			order: z.enum(["asc", "desc"]).optional().describe("Sort direction (default 'desc')"),
			locale: z.string().optional().describe("Filter by locale (e.g. 'en', 'fr'). Only relevant when i18n is enabled."),
			markdown: z.boolean().optional().describe("Return rich text (portableText) fields as Markdown strings instead of Portable Text arrays (default false).")
		}),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const ec = getEmDash(extra);
		const status = canReadDrafts(extra) ? args.status : "published";
		const result = await ec.handleContentList(args.collection, {
			status,
			limit: args.limit,
			cursor: args.cursor,
			orderBy: args.orderBy,
			order: args.order,
			locale: args.locale
		});
		if (result.success && args.markdown) {
			const payload = result.data;
			if (payload && typeof payload === "object" && "items" in payload && Array.isArray(payload.items)) await applyReadMarkdown(ec, args.collection, payload.items);
		}
		return unwrap(result);
	});
	server.registerTool("content_get", {
		title: "Get Content",
		description: "Get a single content item by its ID or slug. Returns the full content data including all field values, metadata, and a _rev token for optimistic concurrency (pass _rev back when updating to detect conflicts).",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug (e.g. 'posts', 'pages')"),
			id: z.string().describe("Content item ID (ULID) or slug"),
			locale: z.string().optional().describe("Locale to scope slug lookup (e.g. 'fr'). Only affects slug resolution; IDs are globally unique."),
			markdown: z.boolean().optional().describe("Return rich text (portableText) fields as Markdown strings instead of Portable Text arrays (default false).")
		}),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const ec = getEmDash(extra);
		const result = await ec.handleContentGet(args.collection, args.id, args.locale);
		if (result.success && !canReadDrafts(extra)) {
			const data = result.data && typeof result.data === "object" ? result.data : void 0;
			const item = data?.item && typeof data.item === "object" ? data.item : void 0;
			if ((typeof item?.status === "string" ? item.status : null) !== "published") return unwrap({
				success: false,
				error: {
					code: "NOT_FOUND",
					message: `Content item not found: ${args.id}`
				}
			});
		}
		if (result.success && args.markdown && result.data?.item) await applyReadMarkdown(ec, args.collection, [result.data.item]);
		return unwrap(result);
	});
	server.registerTool("content_create", {
		title: "Create Content",
		description: "Create a new content item in a collection. The 'data' object should contain field values matching the collection's schema (use schema_get_collection to check). For rich text (portableText) fields, pass a Markdown string — converted to Portable Text automatically; prefer this. Pass a Portable Text JSON array only for complex content Markdown can't express (custom blocks, embeds). A slug is auto-generated if not provided. Items are created as 'draft' by default — use content_publish to make them live.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug (e.g. 'posts', 'pages')"),
			data: z.record(z.string(), z.unknown()).describe("Field values as key-value pairs matching the collection schema"),
			slug: z.string().optional().describe("URL slug (auto-generated from title if omitted)"),
			status: z.enum(["draft", "published"]).optional().describe("Initial status (default 'draft'). Requires publish permission."),
			locale: z.string().optional().describe("Locale for this content (e.g. 'fr'). Defaults to default locale."),
			translationOf: z.string().optional().describe("ID of the content item this is a translation of. Links items in the same translation group."),
			bylines: z.array(contentBylineInputSchema).optional().describe("Bylines to credit. Each entry references an existing byline by id (see byline_list / byline_create) with an optional roleLabel. The first entry becomes the primary byline.")
		}),
		annotations: { destructiveHint: false }
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.CONTRIBUTOR);
		const { emdash, userId } = getExtra(extra);
		if (args.translationOf) {
			const source = await emdash.handleContentGet(args.collection, args.translationOf);
			if (!source.success) return unwrap(source);
			requireOwnership(extra, extractContentAuthorId(source.data), "content:edit_own", "content:edit_any");
		}
		const data = await convertWriteData(emdash, args.collection, args.data);
		if (args.status === "published") {
			if (!hasPermission({
				id: userId,
				role: getExtra(extra).userRole
			}, "content:publish_own")) throw new EmDashAuthError("Insufficient permissions: publishing requires content:publish_own", "INSUFFICIENT_PERMISSIONS");
			const result = await emdash.handleContentCreate(args.collection, {
				data,
				slug: args.slug,
				authorId: userId,
				locale: args.locale,
				translationOf: args.translationOf,
				bylines: args.bylines
			});
			if (!result.success) return unwrap(result);
			const itemId = extractContentId(result.data);
			if (itemId) return unwrap(await emdash.handleContentPublish(args.collection, itemId));
			return unwrap(result);
		}
		return unwrap(await emdash.handleContentCreate(args.collection, {
			data,
			slug: args.slug,
			authorId: userId,
			locale: args.locale,
			translationOf: args.translationOf,
			bylines: args.bylines
		}));
	});
	server.registerTool("content_update", {
		title: "Update Content",
		description: "Update an existing content item. Only include fields you want to change in the 'data' object — unspecified fields are left unchanged. Rich text (portableText) fields accept a Markdown string (recommended, converted automatically); use a Portable Text JSON array only for complex content Markdown can't express (custom blocks, embeds). Pass the _rev token from content_get to enable optimistic concurrency checking (the update fails if the item was modified since you read it). `seo` and `bylines` are persisted alongside the field updates in a single transaction. `publishedAt` requires the content:publish_any permission and is useful for migrations or correcting historical dates.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug"),
			locale: z.string().optional().describe("Locale to scope slug lookup (e.g. 'fr'). Only affects slug resolution; IDs are globally unique."),
			data: z.record(z.string(), z.unknown()).optional().describe("Field values to update (only include changed fields)"),
			slug: z.string().optional().describe("New URL slug"),
			status: z.enum(["draft", "published"]).optional().describe("New status. Setting to 'published' requires publish permission. Setting to 'draft' unpublishes the item and also requires publish permission."),
			seo: contentSeoInput.optional().describe("Per-content SEO metadata. Only valid for collections with SEO enabled (see schema_get_collection.hasSeo). Fields not included are left unchanged; pass null to clear."),
			bylines: z.array(contentBylineInputSchema).optional().describe("Replace the byline list for this item. The first entry becomes the primary byline. Pass an empty array to clear all bylines."),
			publishedAt: z.iso.datetime({
				offset: true,
				message: "must be an ISO 8601 datetime"
			}).nullish().describe("Override the publication timestamp (ISO 8601). Requires content:publish_any permission. Pass null to clear. Useful for content migrations."),
			_rev: z.string().optional().describe("Revision token from content_get for conflict detection")
		})
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.AUTHOR);
		const { emdash, userId, userRole } = getExtra(extra);
		const existing = await emdash.handleContentGet(args.collection, args.id, args.locale);
		if (!existing.success) return unwrap(existing);
		const ownerId = extractContentAuthorId(existing.data);
		requireOwnership(extra, ownerId, "content:edit_own", "content:edit_any");
		const data = args.data ? await convertWriteData(emdash, args.collection, args.data) : args.data;
		if (args.publishedAt !== void 0) {
			if (!hasPermission({
				id: userId,
				role: userRole
			}, "content:publish_any")) throw new EmDashAuthError("Setting publishedAt requires content:publish_any permission", "INSUFFICIENT_PERMISSIONS");
		}
		const resolvedId = extractContentId(existing.data) ?? args.id;
		if (args.status === "published") {
			requireOwnership(extra, ownerId, "content:publish_own", "content:publish_any");
			if (args.data || args.slug || args.seo !== void 0 || args.bylines !== void 0 || args.publishedAt !== void 0) {
				const updateResult = await emdash.handleContentUpdate(args.collection, resolvedId, {
					data,
					slug: args.slug,
					authorId: userId,
					locale: args.locale,
					seo: args.seo,
					bylines: args.bylines,
					publishedAt: args.publishedAt,
					_rev: args._rev
				});
				if (!updateResult.success) return unwrap(updateResult);
			}
			return unwrap(await emdash.handleContentPublish(args.collection, resolvedId));
		}
		if (args.status === "draft") {
			requireOwnership(extra, ownerId, "content:publish_own", "content:publish_any");
			if (args.data || args.slug || args.seo !== void 0 || args.bylines !== void 0 || args.publishedAt !== void 0) {
				const updateResult = await emdash.handleContentUpdate(args.collection, resolvedId, {
					data,
					slug: args.slug,
					authorId: userId,
					locale: args.locale,
					seo: args.seo,
					bylines: args.bylines,
					publishedAt: args.publishedAt,
					_rev: args._rev
				});
				if (!updateResult.success) return unwrap(updateResult);
			}
			return unwrap(await emdash.handleContentUnpublish(args.collection, resolvedId));
		}
		return unwrap(await emdash.handleContentUpdate(args.collection, resolvedId, {
			data,
			slug: args.slug,
			authorId: userId,
			locale: args.locale,
			seo: args.seo,
			bylines: args.bylines,
			publishedAt: args.publishedAt,
			_rev: args._rev
		}));
	});
	server.registerTool("content_delete", {
		title: "Delete Content (Trash)",
		description: "Soft-delete a content item by moving it to the trash. The item can be restored later with content_restore, or permanently deleted with content_permanent_delete.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug")
		}),
		annotations: { destructiveHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.AUTHOR);
		const ec = getEmDash(extra);
		const existing = await ec.handleContentGet(args.collection, args.id);
		if (!existing.success) return unwrap(existing);
		requireOwnership(extra, extractContentAuthorId(existing.data), "content:delete_own", "content:delete_any");
		const resolvedId = extractContentId(existing.data) ?? args.id;
		return unwrap(await ec.handleContentDelete(args.collection, resolvedId));
	});
	server.registerTool("content_restore", {
		title: "Restore Content",
		description: "Restore a soft-deleted content item from the trash back to its previous state.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug")
		})
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.AUTHOR);
		const ec = getEmDash(extra);
		const existing = await ec.handleContentGetIncludingTrashed(args.collection, args.id);
		if (!existing.success) return unwrap(existing);
		requireOwnership(extra, extractContentAuthorId(existing.data), "content:edit_own", "content:edit_any");
		const resolvedId = extractContentId(existing.data) ?? args.id;
		return unwrap(await ec.handleContentRestore(args.collection, resolvedId));
	});
	server.registerTool("content_permanent_delete", {
		title: "Permanently Delete Content",
		description: "Permanently and irreversibly delete a trashed content item. The item must be in the trash first (use content_delete). This cannot be undone.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug")
		}),
		annotations: { destructiveHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.ADMIN);
		return unwrap(await getEmDash(extra).handleContentPermanentDelete(args.collection, args.id));
	});
	server.registerTool("content_publish", {
		title: "Publish Content",
		description: "Publish a content item, making it live on the site. Creates a published revision from the current draft. Further edits create a new draft without affecting the live version until re-published. Pass `publishedAt` to backdate (e.g. when migrating content from another CMS) — this requires the content:publish_any permission. Without `publishedAt`, the existing `published_at` is preserved on re-publish (idempotent) and falls back to the current time on first publish.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug"),
			publishedAt: z.iso.datetime({
				offset: true,
				message: "must be an ISO 8601 datetime"
			}).optional().describe("Override publication timestamp (ISO 8601). Requires content:publish_any permission. Useful when importing content with original publish dates.")
		})
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.AUTHOR);
		const { emdash, userId, userRole } = getExtra(extra);
		const existing = await emdash.handleContentGet(args.collection, args.id);
		if (!existing.success) return unwrap(existing);
		requireOwnership(extra, extractContentAuthorId(existing.data), "content:publish_own", "content:publish_any");
		if (args.publishedAt !== void 0) {
			if (!hasPermission({
				id: userId,
				role: userRole
			}, "content:publish_any")) throw new EmDashAuthError("Setting publishedAt requires content:publish_any permission", "INSUFFICIENT_PERMISSIONS");
		}
		const resolvedId = extractContentId(existing.data) ?? args.id;
		return unwrap(await emdash.handleContentPublish(args.collection, resolvedId, { publishedAt: args.publishedAt }));
	});
	server.registerTool("content_unpublish", {
		title: "Unpublish Content",
		description: "Unpublish a content item, reverting it to draft status. It will no longer be visible on the live site but its content is preserved.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug")
		})
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.AUTHOR);
		const ec = getEmDash(extra);
		const existing = await ec.handleContentGet(args.collection, args.id);
		if (!existing.success) return unwrap(existing);
		requireOwnership(extra, extractContentAuthorId(existing.data), "content:publish_own", "content:publish_any");
		const resolvedId = extractContentId(existing.data) ?? args.id;
		return unwrap(await ec.handleContentUnpublish(args.collection, resolvedId));
	});
	server.registerTool("content_schedule", {
		title: "Schedule Content",
		description: "Schedule a content item for future publication. It will be automatically published at the specified date/time. The scheduledAt value must be an ISO 8601 datetime string in the future (e.g. '2025-06-01T09:00:00Z').",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug"),
			scheduledAt: z.string().describe("ISO 8601 datetime for publication (e.g. '2025-06-01T09:00:00Z')")
		})
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.AUTHOR);
		const ec = getEmDash(extra);
		const existing = await ec.handleContentGet(args.collection, args.id);
		if (!existing.success) return unwrap(existing);
		requireOwnership(extra, extractContentAuthorId(existing.data), "content:publish_own", "content:publish_any");
		const resolvedId = extractContentId(existing.data) ?? args.id;
		return unwrap(await ec.handleContentSchedule(args.collection, resolvedId, args.scheduledAt));
	});
	server.registerTool("content_unschedule", {
		title: "Cancel Scheduled Publication",
		description: "Cancel a previously scheduled publication. The item remains in its current status (typically 'draft' or 'scheduled'); only the scheduledAt timestamp is cleared. Idempotent — calling on an item that isn't scheduled is a no-op.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug")
		})
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.AUTHOR);
		const ec = getEmDash(extra);
		const existing = await ec.handleContentGet(args.collection, args.id);
		if (!existing.success) return unwrap(existing);
		requireOwnership(extra, extractContentAuthorId(existing.data), "content:publish_own", "content:publish_any");
		const resolvedId = extractContentId(existing.data) ?? args.id;
		return unwrap(await ec.handleContentUnschedule(args.collection, resolvedId));
	});
	server.registerTool("content_compare", {
		title: "Compare Live vs Draft",
		description: "Compare the published (live) version of a content item with its current draft. Returns both versions and a flag indicating whether there are changes. Useful for reviewing unpublished edits before publishing.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug")
		}),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		requireDraftAccess(extra);
		return unwrap(await getEmDash(extra).handleContentCompare(args.collection, args.id));
	});
	server.registerTool("content_discard_draft", {
		title: "Discard Draft",
		description: "Discard the current draft changes and revert to the last published version. Only works on items that have been published at least once.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug")
		}),
		annotations: { destructiveHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.AUTHOR);
		const ec = getEmDash(extra);
		const existing = await ec.handleContentGet(args.collection, args.id);
		if (!existing.success) return unwrap(existing);
		requireOwnership(extra, extractContentAuthorId(existing.data), "content:edit_own", "content:edit_any");
		const resolvedId = extractContentId(existing.data) ?? args.id;
		return unwrap(await ec.handleContentDiscardDraft(args.collection, resolvedId));
	});
	server.registerTool("content_list_trashed", {
		title: "List Trashed Content",
		description: "List soft-deleted content items in a collection's trash. These items can be restored with content_restore or permanently deleted with content_permanent_delete.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			limit: z.number().int().min(1).max(100).optional().describe("Max items (default 50)"),
			cursor: z.string().min(1).max(2048).optional().describe("Pagination cursor")
		}),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		requireDraftAccess(extra);
		return unwrap(await getEmDash(extra).handleContentListTrashed(args.collection, {
			limit: args.limit,
			cursor: args.cursor
		}));
	});
	server.registerTool("content_duplicate", {
		title: "Duplicate Content",
		description: "Create a copy of an existing content item. The duplicate is created as a draft with '(Copy)' appended to the title and an auto-generated slug.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug to duplicate")
		})
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.CONTRIBUTOR);
		return unwrap(await getEmDash(extra).handleContentDuplicate(args.collection, args.id));
	});
	server.registerTool("content_translations", {
		title: "Get Content Translations",
		description: "Get all locale variants of a content item. Returns the translation group and a summary of each locale version (id, locale, slug, status). Only relevant when i18n is enabled on the site.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug")
		}),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const result = await getEmDash(extra).handleContentTranslations(args.collection, args.id);
		if (result.success && !canReadDrafts(extra)) {
			const data = result.data && typeof result.data === "object" ? result.data : void 0;
			const filtered = (Array.isArray(data?.translations) ? data.translations : []).filter(isPublished);
			return unwrap({
				success: true,
				data: {
					...data,
					translations: filtered
				}
			});
		}
		return unwrap(result);
	});
	server.registerTool("byline_list", {
		title: "List Bylines",
		description: "List bylines (author/contributor credits) with optional filtering and pagination. Bylines are standalone records referenced by content items; use the returned id with content_create/content_update or byline_get. Use the nextCursor value from the response to fetch the next page.",
		inputSchema: z.object({
			search: z.string().optional().describe("Filter by display name or slug substring"),
			isGuest: z.boolean().optional().describe("Filter by guest (true) or linked-user (false)"),
			userId: z.string().optional().describe("Filter to the byline linked to a CMS user ID"),
			locale: z.string().optional().describe("Filter by locale (omit for all)"),
			limit: z.number().int().min(1).max(100).optional().describe("Max items (default 50)"),
			cursor: z.string().min(1).max(2048).optional().describe("Pagination cursor")
		}),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const ec = getEmDash(extra);
		try {
			const { BylineRepository } = await import("../../../byline-Bm8ZgUP9.mjs").then((n) => n.n);
			return jsonResult(await new BylineRepository(ec.db).findMany({
				search: args.search,
				isGuest: args.isGuest,
				userId: args.userId,
				locale: args.locale,
				limit: args.limit,
				cursor: args.cursor
			}));
		} catch (error) {
			return respondHandlerError(error, "BYLINE_LIST_ERROR");
		}
	});
	server.registerTool("byline_get", {
		title: "Get Byline",
		description: "Get a single byline by its ID, including bio, avatar, website, linked user, and any custom fields.",
		inputSchema: z.object({ id: z.string().describe("Byline ID") }),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const ec = getEmDash(extra);
		try {
			const { BylineRepository } = await import("../../../byline-Bm8ZgUP9.mjs").then((n) => n.n);
			const byline = await new BylineRepository(ec.db).findById(args.id);
			if (!byline) return respondError("NOT_FOUND", `Byline '${args.id}' not found`);
			return jsonResult(byline);
		} catch (error) {
			return respondHandlerError(error, "BYLINE_GET_ERROR");
		}
	});
	server.registerTool("byline_create", {
		title: "Create Byline",
		description: "Create a new byline (author/contributor credit). The slug must be unique and contain only lowercase letters, digits, and hyphens. Link the byline to a CMS user via userId, or leave it as a standalone guest credit. The returned id can then be passed to content_create/content_update bylines.",
		inputSchema: z.object({ ...bylineCreateBody.shape }),
		annotations: { destructiveHint: false }
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { handleBylineCreate } = await import("../../../bylines-6tm9mgQb.mjs").then((n) => n.t);
			const result = await handleBylineCreate(ec.db, args);
			if (result.success) await invalidateBylines();
			return unwrap(result);
		} catch (error) {
			return respondHandlerError(error, "BYLINE_CREATE_ERROR");
		}
	});
	server.registerTool("byline_update", {
		title: "Update Byline",
		description: "Update an existing byline. Any field can be omitted to leave it unchanged. Renaming the slug must not collide with another byline.",
		inputSchema: z.object({
			id: z.string().describe("Byline ID to update"),
			...bylineUpdateBody.shape
		})
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { handleBylineUpdate } = await import("../../../bylines-6tm9mgQb.mjs").then((n) => n.t);
			const { id, ...input } = args;
			const result = await handleBylineUpdate(ec.db, id, input);
			if (result.success) await invalidateBylines();
			return unwrap(result);
		} catch (error) {
			return respondHandlerError(error, "BYLINE_UPDATE_ERROR");
		}
	});
	server.registerTool("byline_delete", {
		title: "Delete Byline",
		description: "Permanently delete a byline. Any content crediting this byline loses the association, and it is cleared as a primary byline where set.",
		inputSchema: z.object({ id: z.string().describe("Byline ID to delete") }),
		annotations: { destructiveHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { BylineRepository } = await import("../../../byline-Bm8ZgUP9.mjs").then((n) => n.n);
			if (!await new BylineRepository(ec.db).delete(args.id)) return respondError("NOT_FOUND", `Byline '${args.id}' not found`);
			await invalidateBylines();
			return jsonResult({ deleted: args.id });
		} catch (error) {
			return respondHandlerError(error, "BYLINE_DELETE_ERROR");
		}
	});
	server.registerTool("byline_translations", {
		title: "List Byline Translations",
		description: "Return every locale variant of a byline, identified via its shared translation_group.",
		inputSchema: z.object({ id: z.string().describe("Byline id (or translation_group)") }),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const ec = getEmDash(extra);
		try {
			const { handleBylineTranslations } = await import("../../../bylines-6tm9mgQb.mjs").then((n) => n.t);
			return unwrap(await handleBylineTranslations(ec.db, args.id));
		} catch (error) {
			return respondHandlerError(error, "BYLINE_TRANSLATIONS_ERROR");
		}
	});
	server.registerTool("schema_list_collections", {
		title: "List Collections",
		description: "List all content collections defined in the CMS. Each collection represents a content type (e.g. posts, pages, products) with its own schema and database table. Returns slug, label, supported features, and timestamps.",
		inputSchema: z.object({}),
		annotations: { readOnlyHint: true }
	}, async (_args, extra) => {
		requireScope(extra, "schema:read");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { SchemaRegistry } = await import("../../../schema-Blhoxp3P.mjs").then((n) => n.t);
			return jsonResult({ items: await new SchemaRegistry(ec.db).listCollections() });
		} catch (error) {
			return respondHandlerError(error, "SCHEMA_LIST_ERROR");
		}
	});
	server.registerTool("schema_get_collection", {
		title: "Get Collection Schema",
		description: "Get detailed info about a collection including all field definitions. Fields describe the data model: name, type (string, text, number, boolean, datetime, portableText, image, reference, json, select, multiSelect, slug), constraints, and validation rules. Use this to understand what data content_create and content_update expect.",
		inputSchema: z.object({ slug: z.string().describe("Collection slug (e.g. 'posts'). Use schema_list_collections to see available slugs.") }),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "schema:read");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { SchemaRegistry } = await import("../../../schema-Blhoxp3P.mjs").then((n) => n.t);
			const collection = await new SchemaRegistry(ec.db).getCollectionWithFields(args.slug);
			if (!collection) return respondError("NOT_FOUND", `Collection '${args.slug}' not found`);
			return jsonResult(collection);
		} catch (error) {
			return respondHandlerError(error, "SCHEMA_GET_ERROR");
		}
	});
	server.registerTool("schema_create_collection", {
		title: "Create Collection",
		description: "Create a new content collection (content type). This creates a database table and schema definition. The slug must be lowercase alphanumeric with underscores, starting with a letter. Supports: 'drafts' (draft/publish workflow), 'revisions' (version history), 'preview' (live preview), 'scheduling' (timed publish), 'search' (full-text indexing).",
		inputSchema: z.object({
			slug: z.string().regex(COLLECTION_SLUG_PATTERN).describe("Unique identifier (lowercase letters, numbers, underscores)"),
			label: z.string().describe("Display name (plural, e.g. 'Blog Posts')"),
			labelSingular: z.string().optional().describe("Singular display name (e.g. 'Blog Post')"),
			description: z.string().optional().describe("Description of this collection"),
			icon: z.string().optional().describe("Icon name for the admin UI"),
			supports: z.array(z.enum([
				"drafts",
				"revisions",
				"preview",
				"scheduling",
				"search"
			])).optional().describe("Features to enable (default: ['drafts', 'revisions'])")
		})
	}, async (args, extra) => {
		requireScope(extra, "schema:write");
		requireRole(extra, Role.ADMIN);
		const ec = getEmDash(extra);
		try {
			const { SchemaRegistry } = await import("../../../schema-Blhoxp3P.mjs").then((n) => n.t);
			const collection = await new SchemaRegistry(ec.db).createCollection({
				slug: args.slug,
				label: args.label,
				labelSingular: args.labelSingular,
				description: args.description,
				icon: args.icon,
				supports: args.supports
			});
			ec.invalidateUrlPatternCache();
			return jsonResult(collection);
		} catch (error) {
			return respondHandlerError(error, "SCHEMA_CREATE_ERROR");
		}
	});
	server.registerTool("schema_delete_collection", {
		title: "Delete Collection",
		description: "Delete a collection and its database table. This is irreversible and deletes all content in the collection. Use with extreme caution.",
		inputSchema: z.object({
			slug: z.string().describe("Collection slug to delete"),
			force: z.boolean().optional().describe("Force deletion even if the collection has content (default false)")
		}),
		annotations: { destructiveHint: true }
	}, async (args, extra) => {
		requireScope(extra, "schema:write");
		requireRole(extra, Role.ADMIN);
		const ec = getEmDash(extra);
		try {
			const { SchemaRegistry } = await import("../../../schema-Blhoxp3P.mjs").then((n) => n.t);
			await new SchemaRegistry(ec.db).deleteCollection(args.slug, { force: args.force });
			ec.invalidateUrlPatternCache();
			return jsonResult({ deleted: args.slug });
		} catch (error) {
			return respondHandlerError(error, "SCHEMA_DELETE_ERROR");
		}
	});
	server.registerTool("schema_create_field", {
		title: "Add Field to Collection",
		description: "Add a new field to a collection's schema. This adds a column to the database table. Field types: string (short text), text (long text), number (decimal), integer, boolean, datetime, select (single choice), multiSelect (multiple), portableText (rich text), image, file, reference (link to another collection), json, slug (URL-safe id). For select/multiSelect, provide choices in validation.options array.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug to add the field to"),
			slug: z.string().regex(COLLECTION_SLUG_PATTERN).describe("Field identifier (lowercase letters, numbers, underscores)"),
			label: z.string().describe("Display name for the field"),
			type: z.enum([
				"string",
				"text",
				"number",
				"integer",
				"boolean",
				"datetime",
				"select",
				"multiSelect",
				"portableText",
				"image",
				"file",
				"reference",
				"json",
				"slug"
			]).describe("Data type for this field"),
			required: z.boolean().optional().describe("Whether the field is required (default false)"),
			unique: z.boolean().optional().describe("Whether values must be unique (default false)"),
			defaultValue: z.unknown().optional().describe("Default value for new items"),
			validation: z.object({
				min: z.number().optional(),
				max: z.number().optional(),
				minLength: z.number().optional(),
				maxLength: z.number().optional(),
				pattern: z.string().optional(),
				options: z.array(z.string()).optional().describe("Allowed values for select/multiSelect")
			}).optional().describe("Validation constraints"),
			options: z.object({
				collection: z.string().optional().describe("Target collection slug for reference fields"),
				rows: z.number().optional().describe("Number of rows for textarea")
			}).passthrough().optional().describe("Widget configuration"),
			searchable: z.boolean().optional().describe("Include in full-text search index (default false)"),
			translatable: z.boolean().optional().describe("Whether this field is translatable (default true). Non-translatable fields are synced across all locales in a translation group.")
		})
	}, async (args, extra) => {
		requireScope(extra, "schema:write");
		requireRole(extra, Role.ADMIN);
		const ec = getEmDash(extra);
		try {
			const { SchemaRegistry } = await import("../../../schema-Blhoxp3P.mjs").then((n) => n.t);
			return jsonResult(await new SchemaRegistry(ec.db).createField(args.collection, {
				slug: args.slug,
				label: args.label,
				type: args.type,
				required: args.required,
				unique: args.unique,
				defaultValue: args.defaultValue,
				validation: args.validation,
				options: args.options,
				searchable: args.searchable,
				translatable: args.translatable
			}));
		} catch (error) {
			return respondHandlerError(error, "FIELD_CREATE_ERROR");
		}
	});
	server.registerTool("schema_delete_field", {
		title: "Remove Field from Collection",
		description: "Remove a field from a collection. This drops the column from the database table and deletes all data in that field. Irreversible.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			fieldSlug: z.string().describe("Field slug to remove")
		}),
		annotations: { destructiveHint: true }
	}, async (args, extra) => {
		requireScope(extra, "schema:write");
		requireRole(extra, Role.ADMIN);
		const ec = getEmDash(extra);
		try {
			const { SchemaRegistry } = await import("../../../schema-Blhoxp3P.mjs").then((n) => n.t);
			await new SchemaRegistry(ec.db).deleteField(args.collection, args.fieldSlug);
			return jsonResult({
				deleted: args.fieldSlug,
				collection: args.collection
			});
		} catch (error) {
			return respondHandlerError(error, "FIELD_DELETE_ERROR");
		}
	});
	server.registerTool("media_list", {
		title: "List Media",
		description: "List uploaded media files (images, documents, etc.) with optional MIME type filtering and pagination. Returns file metadata including filename, URL, dimensions, and alt text.",
		inputSchema: z.object({
			mimeType: z.string().optional().describe("Filter by MIME type prefix (e.g. 'image/', 'application/pdf')"),
			limit: z.number().int().min(1).max(100).optional().describe("Max items (default 50)"),
			cursor: z.string().min(1).max(2048).optional().describe("Pagination cursor")
		}),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "media:read");
		return unwrap(await getEmDash(extra).handleMediaList({
			mimeType: args.mimeType,
			limit: args.limit,
			cursor: args.cursor
		}));
	});
	server.registerTool("media_create", {
		title: "Register Uploaded Media",
		description: "Register a media file that has already been uploaded to storage. The caller is responsible for placing the file at `storageKey` (typically using a signed upload URL obtained from the admin UI or a separate API). This tool persists the metadata record so the file is discoverable via media_list / media_get and can be referenced by content. For binary uploads the MCP transport is not appropriate — use the signed-upload flow instead.",
		inputSchema: z.object({
			filename: z.string().describe("Original filename (e.g. 'logo.png')"),
			mimeType: z.string().describe("MIME type (e.g. 'image/png')"),
			storageKey: z.string().describe("Storage path/key the file was uploaded to"),
			size: z.number().int().nonnegative().optional().describe("File size in bytes"),
			width: z.number().int().positive().optional().describe("Image width in pixels"),
			height: z.number().int().positive().optional().describe("Image height in pixels"),
			contentHash: z.string().optional().describe("Hash of the file contents (for dedupe)"),
			blurhash: z.string().optional().describe("Blurhash for image placeholders"),
			dominantColor: z.string().optional().describe("Hex color string for the image's dominant color")
		})
	}, async (args, extra) => {
		requireScope(extra, "media:write");
		requireRole(extra, Role.AUTHOR);
		const { emdash, userId } = getExtra(extra);
		return unwrap(await emdash.handleMediaCreate({
			filename: args.filename,
			mimeType: args.mimeType,
			storageKey: args.storageKey,
			size: args.size,
			width: args.width,
			height: args.height,
			contentHash: args.contentHash,
			blurhash: args.blurhash,
			dominantColor: args.dominantColor,
			authorId: userId
		}));
	});
	server.registerTool("media_get", {
		title: "Get Media Item",
		description: "Get details of a single media file by its ID. Returns metadata including filename, MIME type, size, dimensions, alt text, and URL.",
		inputSchema: z.object({ id: z.string().describe("Media item ID") }),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "media:read");
		return unwrap(await getEmDash(extra).handleMediaGet(args.id));
	});
	server.registerTool("media_update", {
		title: "Update Media Metadata",
		description: "Update the metadata of an uploaded media file. You can change the alt text, caption, and dimensions. The file itself cannot be changed.",
		inputSchema: z.object({
			id: z.string().describe("Media item ID"),
			alt: z.string().optional().describe("Alt text for accessibility"),
			caption: z.string().optional().describe("Caption text"),
			width: z.number().int().optional().describe("Image width in pixels"),
			height: z.number().int().optional().describe("Image height in pixels")
		})
	}, async (args, extra) => {
		requireScope(extra, "media:write");
		requireRole(extra, Role.AUTHOR);
		const ec = getEmDash(extra);
		const existing = await ec.handleMediaGet(args.id);
		if (!existing.success) return unwrap(existing);
		const media = existing.data?.item;
		requireOwnership(extra, typeof media?.authorId === "string" ? media.authorId : "", "media:edit_own", "media:edit_any");
		return unwrap(await ec.handleMediaUpdate(args.id, {
			alt: args.alt,
			caption: args.caption,
			width: args.width,
			height: args.height
		}));
	});
	server.registerTool("media_delete", {
		title: "Delete Media",
		description: "Permanently delete an uploaded media file. Removes the database record and the file from storage. Content referencing this media will have broken references. Cannot be undone.",
		inputSchema: z.object({ id: z.string().describe("Media item ID") }),
		annotations: { destructiveHint: true }
	}, async (args, extra) => {
		requireScope(extra, "media:write");
		requireRole(extra, Role.AUTHOR);
		const ec = getEmDash(extra);
		const existing = await ec.handleMediaGet(args.id);
		if (!existing.success) return unwrap(existing);
		const media = existing.data?.item;
		requireOwnership(extra, typeof media?.authorId === "string" ? media.authorId : "", "media:delete_own", "media:delete_any");
		return unwrap(await ec.handleMediaDelete(args.id));
	});
	server.registerTool("search", {
		title: "Search Content",
		description: "Full-text search across content collections. Searches indexed fields for matching content. Collections must have 'search' in their supports list and fields must be marked as searchable. Returns collection, item ID, title, excerpt, and relevance score.",
		inputSchema: z.object({
			query: z.string().describe("Search query text"),
			collections: z.array(z.string()).optional().describe("Limit search to specific collection slugs (all if omitted)"),
			locale: z.string().optional().describe("Filter results by locale (omit to search all locales)"),
			limit: z.number().int().min(1).max(50).optional().describe("Max results (default 20)")
		}),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const ec = getEmDash(extra);
		try {
			const { searchWithDb } = await import("../../../search-DZN2HZ-V.mjs").then((n) => n.t);
			return jsonResult(await searchWithDb(ec.db, args.query, {
				collections: args.collections,
				locale: args.locale,
				limit: args.limit
			}));
		} catch (error) {
			return respondHandlerError(error, "SEARCH_ERROR");
		}
	});
	server.registerTool("taxonomy_list", {
		title: "List Taxonomies",
		description: "List all taxonomy definitions (e.g. categories, tags). Taxonomies are classification systems applied to content. Each has a name, label, and can be hierarchical (categories) or flat (tags). Optionally filter by locale.",
		inputSchema: z.object({ locale: z.string().optional().describe("Filter by locale (omit for all)") }),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const ec = getEmDash(extra);
		try {
			const { handleTaxonomyList } = await import("../../../taxonomies-BGBd16Eu.mjs").then((n) => n.l);
			return unwrap(await handleTaxonomyList(ec.db, { locale: args.locale }));
		} catch (error) {
			return respondHandlerError(error, "TAXONOMY_LIST_ERROR");
		}
	});
	server.registerTool("taxonomy_list_terms", {
		title: "List Taxonomy Terms",
		description: "List terms in a taxonomy with pagination. Terms are individual entries (e.g. specific categories or tags). Hierarchical taxonomies can have parent-child relationships.",
		inputSchema: z.object({
			taxonomy: z.string().describe("Taxonomy name (e.g. 'categories', 'tags')"),
			limit: z.number().int().min(1).max(100).optional().describe("Max items (default 50)"),
			cursor: z.string().min(1).max(2048).optional().describe("Pagination cursor"),
			locale: z.string().optional().describe("Filter by locale (omit for all)")
		}),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const ec = getEmDash(extra);
		try {
			const { handleTaxonomyList } = await import("../../../taxonomies-BGBd16Eu.mjs").then((n) => n.l);
			const listResult = await handleTaxonomyList(ec.db, { locale: args.locale });
			if (!listResult.success) return unwrap(listResult);
			if (!listResult.data.taxonomies.find((t) => t.name === args.taxonomy)) return respondError("NOT_FOUND", `Taxonomy '${args.taxonomy}' not found`);
			const { TaxonomyRepository } = await import("../../../taxonomy-BxGxZZdu.mjs").then((n) => n.n);
			const { decodeCursor, encodeCursor, InvalidCursorError } = await import("../../../types-CuW0zWDA.mjs").then((n) => n.o);
			const repo = new TaxonomyRepository(ec.db);
			const limit = Math.min(args.limit ?? 50, 100);
			const terms = await repo.findByName(args.taxonomy, { locale: args.locale });
			let startIdx = 0;
			if (args.cursor) {
				let decoded;
				try {
					decoded = decodeCursor(args.cursor);
				} catch (error) {
					if (error instanceof InvalidCursorError) return respondError("INVALID_CURSOR", error.message);
					throw error;
				}
				startIdx = terms.findIndex((t) => t.label > decoded.orderValue || t.label === decoded.orderValue && t.id > decoded.id);
				if (startIdx < 0) startIdx = terms.length;
			}
			const page = terms.slice(startIdx, startIdx + limit);
			const hasMore = startIdx + limit < terms.length;
			const last = page.at(-1);
			const nextCursor = hasMore && last ? encodeCursor(last.label, last.id) : void 0;
			return jsonResult({
				items: page.map((t) => ({
					id: t.id,
					name: t.name,
					slug: t.slug,
					label: t.label,
					parentId: t.parentId,
					description: typeof t.data?.description === "string" ? t.data.description : void 0,
					locale: t.locale,
					translationGroup: t.translationGroup
				})),
				nextCursor
			});
		} catch (error) {
			return respondHandlerError(error, "TAXONOMY_LIST_TERMS_ERROR");
		}
	});
	server.registerTool("taxonomy_create_term", {
		title: "Create Taxonomy Term",
		description: "Create a new term in a taxonomy. For hierarchical taxonomies like categories, you can specify a parentId to create a child term. The parent must exist and belong to the same taxonomy. The parent's ancestor chain must not exceed 100 levels — attempts to attach a new term beneath a chain of 100+ existing ancestors are rejected.",
		inputSchema: z.object({
			taxonomy: z.string().describe("Taxonomy name (e.g. 'categories', 'tags')"),
			slug: z.string().describe("URL-safe identifier for the term"),
			label: z.string().describe("Display name"),
			parentId: z.string().optional().describe("Parent term ID for hierarchical taxonomies"),
			description: z.string().optional().describe("Description of the term"),
			locale: z.string().optional().describe("Locale for the new term (e.g. 'es')"),
			translationOf: z.string().optional().describe("Term id to join as a translation (same translation_group)")
		})
	}, async (args, extra) => {
		requireScope(extra, "taxonomies:manage");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { handleTermCreate } = await import("../../../taxonomies-BGBd16Eu.mjs").then((n) => n.l);
			return unwrap(await handleTermCreate(ec.db, args.taxonomy, {
				slug: args.slug,
				label: args.label,
				parentId: args.parentId,
				description: args.description,
				locale: args.locale,
				translationOf: args.translationOf
			}));
		} catch (error) {
			return respondHandlerError(error, "TAXONOMY_TERM_CREATE_ERROR");
		}
	});
	server.registerTool("taxonomy_update_term", {
		title: "Update Taxonomy Term",
		description: "Update an existing term in a taxonomy. Any field can be omitted to leave it unchanged. Renaming a term's slug must not collide with another term in the same taxonomy. Set parentId to null to detach from a parent. The new parent must exist, belong to the same taxonomy, and not introduce a cycle (a term cannot be its own ancestor). The new parent's ancestor chain must not exceed 100 levels — reparenting under a chain of 100+ ancestors is rejected.",
		inputSchema: z.object({
			taxonomy: z.string().describe("Taxonomy name (e.g. 'categories', 'tags')"),
			termSlug: z.string().describe("Current slug of the term to update"),
			slug: z.string().optional().describe("New slug (must be unique in the taxonomy)"),
			label: z.string().optional().describe("New display name"),
			parentId: z.string().nullable().optional().describe("New parent term ID; null to detach"),
			description: z.string().optional().describe("New description")
		})
	}, async (args, extra) => {
		requireScope(extra, "taxonomies:manage");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { handleTermUpdate } = await import("../../../taxonomies-BGBd16Eu.mjs").then((n) => n.l);
			return unwrap(await handleTermUpdate(ec.db, args.taxonomy, args.termSlug, {
				slug: args.slug,
				label: args.label,
				parentId: args.parentId,
				description: args.description
			}));
		} catch (error) {
			return respondHandlerError(error, "TAXONOMY_TERM_UPDATE_ERROR");
		}
	});
	server.registerTool("taxonomy_delete_term", {
		title: "Delete Taxonomy Term",
		description: "Permanently delete a term from a taxonomy. Any content tagged with this term loses the association. Cannot delete a term that has children — delete children first.",
		inputSchema: z.object({
			taxonomy: z.string().describe("Taxonomy name"),
			termSlug: z.string().describe("Slug of the term to delete")
		}),
		annotations: { destructiveHint: true }
	}, async (args, extra) => {
		requireScope(extra, "taxonomies:manage");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { handleTermDelete } = await import("../../../taxonomies-BGBd16Eu.mjs").then((n) => n.l);
			return unwrap(await handleTermDelete(ec.db, args.taxonomy, args.termSlug));
		} catch (error) {
			return respondHandlerError(error, "TAXONOMY_TERM_DELETE_ERROR");
		}
	});
	server.registerTool("taxonomy_term_translations", {
		title: "List Term Translations",
		description: "Return every locale variant of a taxonomy term, identified via its shared translation_group.",
		inputSchema: z.object({ id: z.string().describe("Term id (or translation_group)") }),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const ec = getEmDash(extra);
		try {
			const { handleTermTranslations } = await import("../../../taxonomies-BGBd16Eu.mjs").then((n) => n.l);
			return unwrap(await handleTermTranslations(ec.db, args.id));
		} catch (error) {
			return respondHandlerError(error, "TERM_TRANSLATIONS_ERROR");
		}
	});
	server.registerTool("menu_list", {
		title: "List Menus",
		description: "List navigation menus. Menus are per-locale: filter by `locale` to get just one locale's worth, or omit to list every row (one per locale per menu name).",
		inputSchema: z.object({ locale: z.string().optional().describe("Filter by locale (omit for all)") }),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const ec = getEmDash(extra);
		try {
			const { handleMenuList } = await import("../../../menus-Cfivc2Ad.mjs").then((n) => n.d);
			return unwrap(await handleMenuList(ec.db, { locale: args.locale }));
		} catch (error) {
			return respondHandlerError(error, "MENU_LIST_ERROR");
		}
	});
	server.registerTool("menu_get", {
		title: "Get Menu with Items",
		description: "Get a menu by name, including its items. When multiple locales exist, pass `locale` to pick the right one.",
		inputSchema: z.object({
			name: z.string().describe("Menu name (e.g. 'main', 'footer')"),
			locale: z.string().optional().describe("Locale to resolve the menu for")
		}),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const ec = getEmDash(extra);
		try {
			const { handleMenuGet } = await import("../../../menus-Cfivc2Ad.mjs").then((n) => n.d);
			return unwrap(await handleMenuGet(ec.db, args.name, { locale: args.locale }));
		} catch (error) {
			return respondHandlerError(error, "MENU_GET_ERROR");
		}
	});
	server.registerTool("menu_translations", {
		title: "List Menu Translations",
		description: "Return every locale variant of a menu, identified via the shared translation_group.",
		inputSchema: z.object({ id: z.string().describe("Menu id (or translation_group)") }),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		const ec = getEmDash(extra);
		try {
			const { handleMenuTranslations } = await import("../../../menus-Cfivc2Ad.mjs").then((n) => n.d);
			return unwrap(await handleMenuTranslations(ec.db, args.id));
		} catch (error) {
			return respondHandlerError(error, "MENU_TRANSLATIONS_ERROR");
		}
	});
	server.registerTool("menu_create", {
		title: "Create Menu",
		description: "Create a new navigation menu. The `name` is the stable identifier used by site templates (e.g. 'main', 'footer'); `label` is the human-readable name shown in the admin. Menus are per-locale, so pass `locale` when the same menu name exists in multiple translations. Add items afterwards with menu_set_items. If `translationOf` is set, `locale` must also be set.",
		inputSchema: z.object({
			name: z.string().regex(COLLECTION_SLUG_PATTERN).describe("Stable identifier (lowercase letters, numbers, underscores)"),
			label: z.string().describe("Display name for the admin"),
			locale: z.string().optional().describe("Locale for this menu (e.g. 'fr-fr')"),
			translationOf: z.string().optional().describe("Existing menu id to create this locale variant from")
		})
	}, async (args, extra) => {
		requireScope(extra, "menus:manage");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { handleMenuCreate } = await import("../../../menus-Cfivc2Ad.mjs").then((n) => n.d);
			return unwrap(await handleMenuCreate(ec.db, {
				name: args.name,
				label: args.label,
				locale: args.locale,
				translationOf: args.translationOf
			}));
		} catch (error) {
			return respondHandlerError(error, "MENU_CREATE_ERROR");
		}
	});
	server.registerTool("menu_update", {
		title: "Update Menu",
		description: "Update a menu's label. The `name` (stable identifier) cannot be changed. On multi-locale installs, pass `locale` so the correct translation is updated.",
		inputSchema: z.object({
			name: z.string().describe("Menu name to update"),
			label: z.string().describe("New display label"),
			locale: z.string().optional().describe("Locale of the menu to update")
		})
	}, async (args, extra) => {
		requireScope(extra, "menus:manage");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { handleMenuUpdate } = await import("../../../menus-Cfivc2Ad.mjs").then((n) => n.d);
			return unwrap(await handleMenuUpdate(ec.db, args.name, {
				label: args.label,
				locale: args.locale
			}));
		} catch (error) {
			return respondHandlerError(error, "MENU_UPDATE_ERROR");
		}
	});
	server.registerTool("menu_delete", {
		title: "Delete Menu",
		description: "Delete a menu. Items are also removed. Cannot be undone. On multi-locale installs, pass `locale` so only the intended translation is removed.",
		inputSchema: z.object({
			name: z.string().describe("Menu name to delete"),
			locale: z.string().optional().describe("Locale of the menu to delete")
		}),
		annotations: { destructiveHint: true }
	}, async (args, extra) => {
		requireScope(extra, "menus:manage");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { handleMenuDelete } = await import("../../../menus-Cfivc2Ad.mjs").then((n) => n.d);
			return unwrap(await handleMenuDelete(ec.db, args.name, { locale: args.locale }));
		} catch (error) {
			return respondHandlerError(error, "MENU_DELETE_ERROR");
		}
	});
	server.registerTool("menu_set_items", {
		title: "Set Menu Items",
		description: "Replace the entire item list of a menu in one call. This is atomic: the existing items are deleted and the new list is inserted in the order provided. Use this rather than per-item add/remove tools so the resulting order and parent links are unambiguous. On multi-locale installs, pass `locale` so only the intended translation is rewritten.",
		inputSchema: z.object({
			name: z.string().describe("Menu name to update"),
			locale: z.string().optional().describe("Locale of the menu to rewrite"),
			items: z.array(z.object({
				label: z.string().describe("Item display text"),
				type: z.enum([
					"custom",
					"page",
					"post",
					"taxonomy",
					"collection"
				]).describe("Item kind"),
				customUrl: z.string().optional().describe("URL for type='custom' items (ignored otherwise)"),
				referenceCollection: z.string().optional().describe("Target collection slug for content references"),
				referenceId: z.string().optional().describe("Target content/term ID for references"),
				titleAttr: z.string().optional().describe("HTML title attribute"),
				target: z.string().optional().describe("HTML target attribute, e.g. '_blank'"),
				cssClasses: z.string().optional().describe("Space-separated CSS classes"),
				parentIndex: z.number().int().nonnegative().optional().describe("Array index of the parent item (must be earlier in the list). Omit for top-level items.")
			})).describe("Ordered list of menu items")
		})
	}, async (args, extra) => {
		requireScope(extra, "menus:manage");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { handleMenuSetItems } = await import("../../../menus-Cfivc2Ad.mjs").then((n) => n.d);
			return unwrap(await handleMenuSetItems(ec.db, args.name, args.items, { locale: args.locale }));
		} catch (error) {
			return respondHandlerError(error, "MENU_SET_ITEMS_ERROR");
		}
	});
	server.registerTool("revision_list", {
		title: "List Revisions",
		description: "List revision history for a content item. Revisions are snapshots created on publish or update. Returns newest-first. Requires the collection to support 'revisions'.",
		inputSchema: z.object({
			collection: z.string().describe("Collection slug"),
			id: z.string().describe("Content item ID or slug"),
			limit: z.number().int().min(1).max(50).optional().describe("Max revisions (default 20)")
		}),
		annotations: { readOnlyHint: true }
	}, async (args, extra) => {
		requireScope(extra, "content:read");
		requireDraftAccess(extra);
		return unwrap(await getEmDash(extra).handleRevisionList(args.collection, args.id, { limit: args.limit }));
	});
	server.registerTool("revision_restore", {
		title: "Restore Revision",
		description: "Restore a content item to a previous revision. Replaces the current draft with the specified revision's data. Not automatically published — use content_publish afterward if needed.",
		inputSchema: z.object({ revisionId: z.string().describe("Revision ID to restore") })
	}, async (args, extra) => {
		requireScope(extra, "content:write");
		requireRole(extra, Role.AUTHOR);
		const { emdash, userId } = getExtra(extra);
		const revision = await emdash.handleRevisionGet(args.revisionId);
		if (!revision.success) return unwrap(revision);
		const revItem = revision.data?.item;
		if (!revItem?.collection || !revItem?.entryId) return respondError("VALIDATION_ERROR", "Revision is missing collection or entry reference");
		const existing = await emdash.handleContentGet(revItem.collection, revItem.entryId);
		if (!existing.success) return unwrap(existing);
		requireOwnership(extra, extractContentAuthorId(existing.data), "content:edit_own", "content:edit_any");
		return unwrap(await emdash.handleRevisionRestore(args.revisionId, userId));
	});
	server.registerTool("settings_get", {
		title: "Get Site Settings",
		description: "Get all site-wide settings (title, tagline, logo, favicon, URL, date/time formatting, social links, SEO defaults). Media references (logo, favicon, defaultOgImage) include resolved URLs. Unset values are omitted from the response.",
		inputSchema: z.object({}),
		annotations: { readOnlyHint: true }
	}, async (_args, extra) => {
		requireScope(extra, "settings:read");
		requireRole(extra, Role.EDITOR);
		const ec = getEmDash(extra);
		try {
			const { handleSettingsGet } = await import("../../../settings-rZyHEcNT.mjs").then((n) => n.r);
			return unwrap(await handleSettingsGet(ec.db, ec.storage));
		} catch (error) {
			return respondHandlerError(error, "SETTINGS_READ_ERROR");
		}
	});
	server.registerTool("settings_update", {
		title: "Update Site Settings",
		description: "Update one or more site-wide settings. This is a partial update: only the fields provided are changed; omitted fields are left as-is. Returns the full settings object after the update. To set a media reference (logo, favicon, seo.defaultOgImage), pass an object with `mediaId` (and optional `alt`) — the media item must already exist (use media_create first).",
		inputSchema: z.object({
			title: z.string().optional().describe("Site title"),
			tagline: z.string().optional().describe("Site tagline / short description"),
			logo: settingsMediaReferenceSchema.optional().describe("Logo media reference ({ mediaId, alt? })"),
			favicon: settingsMediaReferenceSchema.optional().describe("Favicon media reference ({ mediaId, alt? })"),
			url: z.union([z.string().url().refine((u) => HTTP_SCHEME_PATTERN.test(u), "URL must use http or https"), z.literal("")]).optional().describe("Canonical site URL (http or https). Empty string clears it."),
			postsPerPage: z.number().int().min(1).max(100).optional().describe("Default page size for content listings"),
			dateFormat: z.string().optional().describe("Date format token string"),
			timezone: z.string().optional().describe("IANA timezone identifier"),
			social: settingsSocialSchema.optional().describe("Social handles / URLs"),
			seo: settingsSeoSchema.optional().describe("Site-wide SEO defaults")
		})
	}, async (args, extra) => {
		requireScope(extra, "settings:manage");
		requireRole(extra, Role.ADMIN);
		const ec = getEmDash(extra);
		try {
			const { handleSettingsUpdate } = await import("../../../settings-rZyHEcNT.mjs").then((n) => n.r);
			return unwrap(await handleSettingsUpdate(ec.db, ec.storage, args));
		} catch (error) {
			return respondHandlerError(error, "SETTINGS_UPDATE_ERROR");
		}
	});
	return server;
}

//#endregion
//#region src/astro/routes/api/mcp.ts
/**
* MCP Streamable HTTP endpoint
*
* Exposes an MCP server at /_emdash/api/mcp using the Streamable HTTP
* transport (Web Standard variant). The server runs stateless — each
* request creates a fresh transport, so no session tracking is needed.
* Authentication is handled by the existing EmDash auth middleware.
*
* POST /_emdash/api/mcp — JSON-RPC tool calls
* GET  /_emdash/api/mcp — SSE stream (not used in stateless mode)
* DELETE /_emdash/api/mcp — Session close (not used in stateless mode)
*/
const prerender = false;
const POST = async ({ request, locals }) => {
	const { emdash, user } = locals;
	if (!emdash) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
	if (!user) return apiError("UNAUTHORIZED", "Authentication required", 401);
	const server = createMcpServer();
	try {
		const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
		await server.connect(transport);
		return await transport.handleRequest(request, { authInfo: {
			token: "",
			clientId: "emdash-admin",
			scopes: [],
			extra: {
				emdash,
				userId: user.id,
				userRole: user.role,
				tokenScopes: locals.tokenScopes
			}
		} });
	} catch (error) {
		console.error("[MCP]", error);
		await server.close().catch(() => {});
		return new Response(JSON.stringify({
			jsonrpc: "2.0",
			error: {
				code: -32603,
				message: "Internal server error"
			},
			id: null
		}), {
			status: 500,
			headers: {
				"Content-Type": "application/json",
				"Cache-Control": "private, no-store"
			}
		});
	}
};
/**
* GET — SSE stream. Not used in stateless mode.
*/
const GET = async () => {
	return new Response(JSON.stringify({
		jsonrpc: "2.0",
		error: {
			code: -32e3,
			message: "Method not allowed. This is a stateless MCP endpoint — use POST."
		},
		id: null
	}), {
		status: 405,
		headers: {
			"Content-Type": "application/json",
			"Cache-Control": "private, no-store"
		}
	});
};
/**
* DELETE — Session close. Not used in stateless mode.
*/
const DELETE = async () => {
	return new Response(JSON.stringify({
		jsonrpc: "2.0",
		error: {
			code: -32e3,
			message: "Method not allowed. This is a stateless MCP endpoint."
		},
		id: null
	}), {
		status: 405,
		headers: {
			"Content-Type": "application/json",
			"Cache-Control": "private, no-store"
		}
	});
};

//#endregion
export { DELETE, GET, POST, prerender };
//# sourceMappingURL=mcp.mjs.map