emdash 0.0.0-a → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -1
- package/dist/adapters-BLMa4JGD.d.mts +106 -0
- package/dist/adapters-BLMa4JGD.d.mts.map +1 -0
- package/dist/apply-Bjfq_b4-.mjs +1293 -0
- package/dist/apply-Bjfq_b4-.mjs.map +1 -0
- package/dist/astro/index.d.mts +51 -0
- package/dist/astro/index.d.mts.map +1 -0
- package/dist/astro/index.mjs +1333 -0
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/middleware/auth.d.mts +31 -0
- package/dist/astro/middleware/auth.d.mts.map +1 -0
- package/dist/astro/middleware/auth.mjs +654 -0
- package/dist/astro/middleware/auth.mjs.map +1 -0
- package/dist/astro/middleware/redirect.d.mts +22 -0
- package/dist/astro/middleware/redirect.d.mts.map +1 -0
- package/dist/astro/middleware/redirect.mjs +63 -0
- package/dist/astro/middleware/redirect.mjs.map +1 -0
- package/dist/astro/middleware/request-context.d.mts +18 -0
- package/dist/astro/middleware/request-context.d.mts.map +1 -0
- package/dist/astro/middleware/request-context.mjs +1310 -0
- package/dist/astro/middleware/request-context.mjs.map +1 -0
- package/dist/astro/middleware/setup.d.mts +20 -0
- package/dist/astro/middleware/setup.d.mts.map +1 -0
- package/dist/astro/middleware/setup.mjs +47 -0
- package/dist/astro/middleware/setup.mjs.map +1 -0
- package/dist/astro/middleware.d.mts +13 -0
- package/dist/astro/middleware.d.mts.map +1 -0
- package/dist/astro/middleware.mjs +1613 -0
- package/dist/astro/middleware.mjs.map +1 -0
- package/dist/astro/types.d.mts +250 -0
- package/dist/astro/types.d.mts.map +1 -0
- package/dist/astro/types.mjs +1 -0
- package/dist/base64-MBPo9ozB.mjs +59 -0
- package/dist/base64-MBPo9ozB.mjs.map +1 -0
- package/dist/byline-CL847F26.mjs +213 -0
- package/dist/byline-CL847F26.mjs.map +1 -0
- package/dist/bylines-C2a-2TGt.mjs +136 -0
- package/dist/bylines-C2a-2TGt.mjs.map +1 -0
- package/dist/chunk-ClPoSABd.mjs +21 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +3909 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/client/cf-access.d.mts +60 -0
- package/dist/client/cf-access.d.mts.map +1 -0
- package/dist/client/cf-access.mjs +179 -0
- package/dist/client/cf-access.mjs.map +1 -0
- package/dist/client/index.d.mts +398 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +346 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/config-CKE8p9xM.mjs +55 -0
- package/dist/config-CKE8p9xM.mjs.map +1 -0
- package/dist/connection-B4zVnQIa.mjs +40 -0
- package/dist/connection-B4zVnQIa.mjs.map +1 -0
- package/dist/content-D6C2WsZC.mjs +824 -0
- package/dist/content-D6C2WsZC.mjs.map +1 -0
- package/dist/db/index.d.mts +4 -0
- package/dist/db/index.mjs +62 -0
- package/dist/db/index.mjs.map +1 -0
- package/dist/db/libsql.d.mts +11 -0
- package/dist/db/libsql.d.mts.map +1 -0
- package/dist/db/libsql.mjs +17 -0
- package/dist/db/libsql.mjs.map +1 -0
- package/dist/db/postgres.d.mts +11 -0
- package/dist/db/postgres.d.mts.map +1 -0
- package/dist/db/postgres.mjs +30 -0
- package/dist/db/postgres.mjs.map +1 -0
- package/dist/db/sqlite.d.mts +11 -0
- package/dist/db/sqlite.d.mts.map +1 -0
- package/dist/db/sqlite.mjs +16 -0
- package/dist/db/sqlite.mjs.map +1 -0
- package/dist/default-Cyi4aAxu.mjs +81 -0
- package/dist/default-Cyi4aAxu.mjs.map +1 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs +90 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs.map +1 -0
- package/dist/error-Cxz0tQeO.mjs +27 -0
- package/dist/error-Cxz0tQeO.mjs.map +1 -0
- package/dist/index-C1xF3OGh.d.mts +4527 -0
- package/dist/index-C1xF3OGh.d.mts.map +1 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +30 -0
- package/dist/load-yOOlckBj.mjs +28 -0
- package/dist/load-yOOlckBj.mjs.map +1 -0
- package/dist/loader-fz8Q_3EO.mjs +447 -0
- package/dist/loader-fz8Q_3EO.mjs.map +1 -0
- package/dist/manifest-schema-Dcl0R6nM.mjs +184 -0
- package/dist/manifest-schema-Dcl0R6nM.mjs.map +1 -0
- package/dist/media/index.d.mts +26 -0
- package/dist/media/index.d.mts.map +1 -0
- package/dist/media/index.mjs +55 -0
- package/dist/media/index.mjs.map +1 -0
- package/dist/media/local-runtime.d.mts +39 -0
- package/dist/media/local-runtime.d.mts.map +1 -0
- package/dist/media/local-runtime.mjs +133 -0
- package/dist/media/local-runtime.mjs.map +1 -0
- package/dist/media-DqHVh136.mjs +200 -0
- package/dist/media-DqHVh136.mjs.map +1 -0
- package/dist/mode-C2EzN1uE.mjs +23 -0
- package/dist/mode-C2EzN1uE.mjs.map +1 -0
- package/dist/page/index.d.mts +140 -0
- package/dist/page/index.d.mts.map +1 -0
- package/dist/page/index.mjs +416 -0
- package/dist/page/index.mjs.map +1 -0
- package/dist/placeholder-CmGAmqeO.d.mts +276 -0
- package/dist/placeholder-CmGAmqeO.d.mts.map +1 -0
- package/dist/placeholder-SmpOx-_v.mjs +243 -0
- package/dist/placeholder-SmpOx-_v.mjs.map +1 -0
- package/dist/plugin-utils.d.mts +58 -0
- package/dist/plugin-utils.d.mts.map +1 -0
- package/dist/plugin-utils.mjs +78 -0
- package/dist/plugin-utils.mjs.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +22 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.mjs +113 -0
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -0
- package/dist/query-CS_iSj34.mjs +460 -0
- package/dist/query-CS_iSj34.mjs.map +1 -0
- package/dist/redirect-DIfIni3r.mjs +329 -0
- package/dist/redirect-DIfIni3r.mjs.map +1 -0
- package/dist/registry-D_w5HW4G.mjs +863 -0
- package/dist/registry-D_w5HW4G.mjs.map +1 -0
- package/dist/request-context.d.mts +49 -0
- package/dist/request-context.d.mts.map +1 -0
- package/dist/request-context.mjs +43 -0
- package/dist/request-context.mjs.map +1 -0
- package/dist/runner-B-u2F2b6.mjs +1412 -0
- package/dist/runner-B-u2F2b6.mjs.map +1 -0
- package/dist/runner-EAtf0ZIe.d.mts +27 -0
- package/dist/runner-EAtf0ZIe.d.mts.map +1 -0
- package/dist/runtime.d.mts +26 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +42 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/search-DG603UrT.mjs +9211 -0
- package/dist/search-DG603UrT.mjs.map +1 -0
- package/dist/seed/index.d.mts +3 -0
- package/dist/seed/index.mjs +15 -0
- package/dist/seo/index.d.mts +70 -0
- package/dist/seo/index.d.mts.map +1 -0
- package/dist/seo/index.mjs +70 -0
- package/dist/seo/index.mjs.map +1 -0
- package/dist/storage/local.d.mts +39 -0
- package/dist/storage/local.d.mts.map +1 -0
- package/dist/storage/local.mjs +166 -0
- package/dist/storage/local.mjs.map +1 -0
- package/dist/storage/s3.d.mts +32 -0
- package/dist/storage/s3.d.mts.map +1 -0
- package/dist/storage/s3.mjs +175 -0
- package/dist/storage/s3.mjs.map +1 -0
- package/dist/tokens-DpgrkrXK.mjs +171 -0
- package/dist/tokens-DpgrkrXK.mjs.map +1 -0
- package/dist/transport-BFGblqwG.d.mts +42 -0
- package/dist/transport-BFGblqwG.d.mts.map +1 -0
- package/dist/transport-yxiQsi8I.mjs +418 -0
- package/dist/transport-yxiQsi8I.mjs.map +1 -0
- package/dist/types-BRuPJGdV.d.mts +102 -0
- package/dist/types-BRuPJGdV.d.mts.map +1 -0
- package/dist/types-C4-fAxN3.d.mts +182 -0
- package/dist/types-C4-fAxN3.d.mts.map +1 -0
- package/dist/types-CMMN0pNg.mjs +31 -0
- package/dist/types-CMMN0pNg.mjs.map +1 -0
- package/dist/types-CUBbjgmP.mjs +16 -0
- package/dist/types-CUBbjgmP.mjs.map +1 -0
- package/dist/types-DRjfYOEv.d.mts +426 -0
- package/dist/types-DRjfYOEv.d.mts.map +1 -0
- package/dist/types-DY5zk5HN.mjs +73 -0
- package/dist/types-DY5zk5HN.mjs.map +1 -0
- package/dist/types-DaNLHo_T.d.mts +184 -0
- package/dist/types-DaNLHo_T.d.mts.map +1 -0
- package/dist/types-DvhsUmSJ.d.mts +1111 -0
- package/dist/types-DvhsUmSJ.d.mts.map +1 -0
- package/dist/validate-CpBtVMsD.d.mts +378 -0
- package/dist/validate-CpBtVMsD.d.mts.map +1 -0
- package/dist/validate-CqRJb_xU.mjs +97 -0
- package/dist/validate-CqRJb_xU.mjs.map +1 -0
- package/dist/validate-O7PWmlnq.mjs +328 -0
- package/dist/validate-O7PWmlnq.mjs.map +1 -0
- package/locals.d.ts +46 -0
- package/package.json +233 -19
- package/src/api/authorize.ts +63 -0
- package/src/api/csrf.ts +48 -0
- package/src/api/error.ts +99 -0
- package/src/api/errors.ts +445 -0
- package/src/api/escape.ts +9 -0
- package/src/api/handlers/api-tokens.ts +240 -0
- package/src/api/handlers/comments.ts +314 -0
- package/src/api/handlers/content.ts +1315 -0
- package/src/api/handlers/dashboard.ts +205 -0
- package/src/api/handlers/device-flow.ts +687 -0
- package/src/api/handlers/index.ts +163 -0
- package/src/api/handlers/manifest.ts +158 -0
- package/src/api/handlers/marketplace.ts +930 -0
- package/src/api/handlers/media.ts +207 -0
- package/src/api/handlers/menus.ts +493 -0
- package/src/api/handlers/oauth-authorization.ts +429 -0
- package/src/api/handlers/oauth-clients.ts +353 -0
- package/src/api/handlers/oauth-user-lookup.ts +39 -0
- package/src/api/handlers/plugins.ts +254 -0
- package/src/api/handlers/redirects.ts +360 -0
- package/src/api/handlers/revision.ts +145 -0
- package/src/api/handlers/schema.ts +534 -0
- package/src/api/handlers/sections.ts +289 -0
- package/src/api/handlers/seo.ts +115 -0
- package/src/api/handlers/settings.ts +49 -0
- package/src/api/handlers/snapshot.ts +350 -0
- package/src/api/handlers/taxonomies.ts +523 -0
- package/src/api/index.ts +6 -0
- package/src/api/openapi/document.ts +2368 -0
- package/src/api/openapi/index.ts +1 -0
- package/src/api/parse.ts +139 -0
- package/src/api/redirect.ts +14 -0
- package/src/api/rev.ts +67 -0
- package/src/api/schemas/auth.ts +112 -0
- package/src/api/schemas/bylines.ts +85 -0
- package/src/api/schemas/comments.ts +117 -0
- package/src/api/schemas/common.ts +89 -0
- package/src/api/schemas/content.ts +191 -0
- package/src/api/schemas/import.ts +52 -0
- package/src/api/schemas/index.ts +17 -0
- package/src/api/schemas/media.ts +116 -0
- package/src/api/schemas/menus.ts +111 -0
- package/src/api/schemas/redirects.ts +155 -0
- package/src/api/schemas/schema.ts +203 -0
- package/src/api/schemas/search.ts +63 -0
- package/src/api/schemas/sections.ts +67 -0
- package/src/api/schemas/settings.ts +63 -0
- package/src/api/schemas/setup.ts +37 -0
- package/src/api/schemas/taxonomies.ts +113 -0
- package/src/api/schemas/users.ts +96 -0
- package/src/api/schemas/widgets.ts +80 -0
- package/src/api/site-url.ts +25 -0
- package/src/api/types.ts +82 -0
- package/src/astro/index.ts +27 -0
- package/src/astro/integration/index.ts +303 -0
- package/src/astro/integration/routes.ts +834 -0
- package/src/astro/integration/runtime.ts +338 -0
- package/src/astro/integration/virtual-modules.ts +469 -0
- package/src/astro/integration/vite-config.ts +328 -0
- package/src/astro/middleware/auth.ts +743 -0
- package/src/astro/middleware/redirect.ts +89 -0
- package/src/astro/middleware/request-context.ts +129 -0
- package/src/astro/middleware/setup.ts +89 -0
- package/src/astro/middleware.ts +398 -0
- package/src/astro/routes/PluginRegistry.tsx +15 -0
- package/src/astro/routes/admin.astro +81 -0
- package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
- package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
- package/src/astro/routes/api/admin/api-tokens/[id].ts +40 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
- package/src/astro/routes/api/admin/bylines/index.ts +72 -0
- package/src/astro/routes/api/admin/comments/[id]/status.ts +120 -0
- package/src/astro/routes/api/admin/comments/[id].ts +64 -0
- package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
- package/src/astro/routes/api/admin/comments/counts.ts +30 -0
- package/src/astro/routes/api/admin/comments/index.ts +46 -0
- package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +91 -0
- package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
- package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -0
- package/src/astro/routes/api/admin/plugins/index.ts +32 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +61 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +62 -0
- package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +61 -0
- package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
- package/src/astro/routes/api/admin/users/[id]/disable.ts +69 -0
- package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
- package/src/astro/routes/api/admin/users/[id]/index.ts +146 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
- package/src/astro/routes/api/admin/users/index.ts +66 -0
- package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
- package/src/astro/routes/api/auth/invite/accept.ts +52 -0
- package/src/astro/routes/api/auth/invite/complete.ts +84 -0
- package/src/astro/routes/api/auth/invite/index.ts +99 -0
- package/src/astro/routes/api/auth/logout.ts +40 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +89 -0
- package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
- package/src/astro/routes/api/auth/me.ts +60 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +219 -0
- package/src/astro/routes/api/auth/oauth/[provider].ts +119 -0
- package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
- package/src/astro/routes/api/auth/passkey/index.ts +54 -0
- package/src/astro/routes/api/auth/passkey/options.ts +82 -0
- package/src/astro/routes/api/auth/passkey/register/options.ts +86 -0
- package/src/astro/routes/api/auth/passkey/register/verify.ts +117 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +66 -0
- package/src/astro/routes/api/auth/signup/complete.ts +85 -0
- package/src/astro/routes/api/auth/signup/request.ts +77 -0
- package/src/astro/routes/api/auth/signup/verify.ts +53 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +312 -0
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +105 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
- package/src/astro/routes/api/content/[collection]/index.ts +59 -0
- package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
- package/src/astro/routes/api/dashboard.ts +32 -0
- package/src/astro/routes/api/dev/emails.ts +36 -0
- package/src/astro/routes/api/import/probe.ts +47 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +510 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +283 -0
- package/src/astro/routes/api/import/wordpress/media.ts +338 -0
- package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
- package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +347 -0
- package/src/astro/routes/api/manifest.ts +62 -0
- package/src/astro/routes/api/mcp.ts +124 -0
- package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
- package/src/astro/routes/api/media/[id].ts +145 -0
- package/src/astro/routes/api/media/file/[key].ts +79 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +86 -0
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
- package/src/astro/routes/api/media/providers/index.ts +30 -0
- package/src/astro/routes/api/media/upload-url.ts +137 -0
- package/src/astro/routes/api/media.ts +190 -0
- package/src/astro/routes/api/menus/[name]/items.ts +87 -0
- package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
- package/src/astro/routes/api/menus/[name].ts +65 -0
- package/src/astro/routes/api/menus/index.ts +47 -0
- package/src/astro/routes/api/oauth/authorize.ts +412 -0
- package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
- package/src/astro/routes/api/oauth/device/code.ts +51 -0
- package/src/astro/routes/api/oauth/device/token.ts +69 -0
- package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
- package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
- package/src/astro/routes/api/oauth/token.ts +184 -0
- package/src/astro/routes/api/openapi.json.ts +32 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -0
- package/src/astro/routes/api/redirects/404s/index.ts +72 -0
- package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
- package/src/astro/routes/api/redirects/[id].ts +84 -0
- package/src/astro/routes/api/redirects/index.ts +52 -0
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
- package/src/astro/routes/api/revisions/[revisionId]/restore.ts +62 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +76 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
- package/src/astro/routes/api/schema/collections/index.ts +47 -0
- package/src/astro/routes/api/schema/index.ts +109 -0
- package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
- package/src/astro/routes/api/schema/orphans/index.ts +26 -0
- package/src/astro/routes/api/search/enable.ts +64 -0
- package/src/astro/routes/api/search/index.ts +55 -0
- package/src/astro/routes/api/search/rebuild.ts +72 -0
- package/src/astro/routes/api/search/stats.ts +35 -0
- package/src/astro/routes/api/search/suggest.ts +53 -0
- package/src/astro/routes/api/sections/[slug].ts +84 -0
- package/src/astro/routes/api/sections/index.ts +52 -0
- package/src/astro/routes/api/settings/email.ts +150 -0
- package/src/astro/routes/api/settings.ts +67 -0
- package/src/astro/routes/api/setup/admin-verify.ts +100 -0
- package/src/astro/routes/api/setup/admin.ts +94 -0
- package/src/astro/routes/api/setup/dev-bypass.ts +199 -0
- package/src/astro/routes/api/setup/dev-reset.ts +40 -0
- package/src/astro/routes/api/setup/index.ts +126 -0
- package/src/astro/routes/api/setup/status.ts +122 -0
- package/src/astro/routes/api/snapshot.ts +75 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +95 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
- package/src/astro/routes/api/taxonomies/index.ts +59 -0
- package/src/astro/routes/api/themes/preview.ts +77 -0
- package/src/astro/routes/api/typegen.ts +114 -0
- package/src/astro/routes/api/well-known/auth.ts +68 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +44 -0
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +37 -0
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +72 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
- package/src/astro/routes/api/widget-areas/[name].ts +87 -0
- package/src/astro/routes/api/widget-areas/index.ts +99 -0
- package/src/astro/routes/api/widget-components.ts +22 -0
- package/src/astro/routes/robots.txt.ts +77 -0
- package/src/astro/routes/sitemap.xml.ts +97 -0
- package/src/astro/storage/adapters.ts +74 -0
- package/src/astro/storage/index.ts +19 -0
- package/src/astro/storage/types.ts +60 -0
- package/src/astro/types.ts +346 -0
- package/src/auth/api-tokens.ts +25 -0
- package/src/auth/challenge-store.ts +80 -0
- package/src/auth/mode.ts +96 -0
- package/src/auth/oauth-state-store.ts +96 -0
- package/src/auth/passkey-config.ts +27 -0
- package/src/auth/rate-limit.ts +158 -0
- package/src/auth/scopes.ts +33 -0
- package/src/auth/types.ts +104 -0
- package/src/aws-sdk.d.ts +100 -0
- package/src/bylines/index.ts +237 -0
- package/src/cleanup.ts +153 -0
- package/src/cli/client-factory.ts +100 -0
- package/src/cli/commands/auth.ts +46 -0
- package/src/cli/commands/bundle-utils.ts +247 -0
- package/src/cli/commands/bundle.ts +609 -0
- package/src/cli/commands/content.ts +442 -0
- package/src/cli/commands/dev.ts +191 -0
- package/src/cli/commands/doctor.ts +211 -0
- package/src/cli/commands/export-seed.ts +630 -0
- package/src/cli/commands/import/wordpress.ts +1056 -0
- package/src/cli/commands/init.ts +192 -0
- package/src/cli/commands/login.ts +547 -0
- package/src/cli/commands/media.ts +165 -0
- package/src/cli/commands/menu.ts +67 -0
- package/src/cli/commands/plugin-init.ts +291 -0
- package/src/cli/commands/plugin-validate.ts +31 -0
- package/src/cli/commands/plugin.ts +33 -0
- package/src/cli/commands/publish.ts +699 -0
- package/src/cli/commands/schema.ts +233 -0
- package/src/cli/commands/search-cmd.ts +54 -0
- package/src/cli/commands/seed.ts +288 -0
- package/src/cli/commands/taxonomy.ts +128 -0
- package/src/cli/commands/types.ts +68 -0
- package/src/cli/credentials.ts +236 -0
- package/src/cli/index.ts +70 -0
- package/src/cli/output.ts +75 -0
- package/src/cli/wxr/parser.ts +969 -0
- package/src/client/cf-access.ts +193 -0
- package/src/client/index.ts +854 -0
- package/src/client/portable-text.ts +413 -0
- package/src/client/transport.ts +200 -0
- package/src/comments/moderator.ts +46 -0
- package/src/comments/notifications.ts +144 -0
- package/src/comments/query.ts +105 -0
- package/src/comments/service.ts +213 -0
- package/src/components/Break.astro +45 -0
- package/src/components/Button.astro +71 -0
- package/src/components/Buttons.astro +49 -0
- package/src/components/Code.astro +59 -0
- package/src/components/Columns.astro +59 -0
- package/src/components/CommentForm.astro +315 -0
- package/src/components/Comments.astro +232 -0
- package/src/components/Cover.astro +128 -0
- package/src/components/EmDashBodyEnd.astro +32 -0
- package/src/components/EmDashBodyStart.astro +32 -0
- package/src/components/EmDashHead.astro +53 -0
- package/src/components/EmDashImage.astro +178 -0
- package/src/components/EmDashMedia.astro +167 -0
- package/src/components/Embed.astro +128 -0
- package/src/components/File.astro +122 -0
- package/src/components/Gallery.astro +93 -0
- package/src/components/HtmlBlock.astro +33 -0
- package/src/components/Image.astro +178 -0
- package/src/components/InlineEditor.astro +27 -0
- package/src/components/InlinePortableTextEditor.tsx +1905 -0
- package/src/components/LiveSearch.astro +614 -0
- package/src/components/PortableText.astro +51 -0
- package/src/components/Pullquote.astro +51 -0
- package/src/components/Table.astro +108 -0
- package/src/components/WidgetArea.astro +22 -0
- package/src/components/WidgetRenderer.astro +72 -0
- package/src/components/index.ts +116 -0
- package/src/components/marks/Link.astro +31 -0
- package/src/components/marks/StrikeThrough.astro +7 -0
- package/src/components/marks/Subscript.astro +7 -0
- package/src/components/marks/Superscript.astro +7 -0
- package/src/components/marks/Underline.astro +7 -0
- package/src/components/widgets/Archives.astro +65 -0
- package/src/components/widgets/Categories.astro +35 -0
- package/src/components/widgets/RecentPosts.astro +51 -0
- package/src/components/widgets/Search.astro +18 -0
- package/src/components/widgets/Tags.astro +38 -0
- package/src/content/converters/index.ts +9 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +385 -0
- package/src/content/converters/prosemirror-to-portable-text.ts +413 -0
- package/src/content/converters/types.ts +120 -0
- package/src/content/index.ts +5 -0
- package/src/database/connection.ts +67 -0
- package/src/database/dialect-helpers.ts +138 -0
- package/src/database/index.ts +5 -0
- package/src/database/migrations/001_initial.ts +136 -0
- package/src/database/migrations/002_media_status.ts +26 -0
- package/src/database/migrations/003_schema_registry.ts +79 -0
- package/src/database/migrations/004_plugins.ts +62 -0
- package/src/database/migrations/005_menus.ts +67 -0
- package/src/database/migrations/006_taxonomy_defs.ts +51 -0
- package/src/database/migrations/007_widgets.ts +42 -0
- package/src/database/migrations/008_auth.ts +194 -0
- package/src/database/migrations/009_user_disabled.ts +27 -0
- package/src/database/migrations/011_sections.ts +65 -0
- package/src/database/migrations/012_search.ts +25 -0
- package/src/database/migrations/013_scheduled_publishing.ts +51 -0
- package/src/database/migrations/014_draft_revisions.ts +72 -0
- package/src/database/migrations/015_indexes.ts +82 -0
- package/src/database/migrations/016_api_tokens.ts +89 -0
- package/src/database/migrations/017_authorization_codes.ts +45 -0
- package/src/database/migrations/018_seo.ts +56 -0
- package/src/database/migrations/019_i18n.ts +618 -0
- package/src/database/migrations/020_collection_url_pattern.ts +23 -0
- package/src/database/migrations/021_remove_section_categories.ts +43 -0
- package/src/database/migrations/022_marketplace_plugin_state.ts +46 -0
- package/src/database/migrations/023_plugin_metadata.ts +33 -0
- package/src/database/migrations/024_media_placeholders.ts +32 -0
- package/src/database/migrations/025_oauth_clients.ts +28 -0
- package/src/database/migrations/026_cron_tasks.ts +49 -0
- package/src/database/migrations/027_comments.ts +87 -0
- package/src/database/migrations/028_drop_author_url.ts +9 -0
- package/src/database/migrations/029_redirects.ts +67 -0
- package/src/database/migrations/030_widen_scheduled_index.ts +48 -0
- package/src/database/migrations/031_bylines.ts +90 -0
- package/src/database/migrations/032_rate_limits.ts +42 -0
- package/src/database/migrations/runner.ts +170 -0
- package/src/database/repositories/audit.ts +294 -0
- package/src/database/repositories/byline.ts +387 -0
- package/src/database/repositories/comment.ts +458 -0
- package/src/database/repositories/content.ts +1144 -0
- package/src/database/repositories/index.ts +30 -0
- package/src/database/repositories/media.ts +347 -0
- package/src/database/repositories/options.ts +150 -0
- package/src/database/repositories/plugin-storage.ts +373 -0
- package/src/database/repositories/redirect.ts +480 -0
- package/src/database/repositories/revision.ts +200 -0
- package/src/database/repositories/seo.ts +176 -0
- package/src/database/repositories/taxonomy.ts +294 -0
- package/src/database/repositories/types.ts +132 -0
- package/src/database/repositories/user.ts +258 -0
- package/src/database/transaction.ts +54 -0
- package/src/database/types.ts +501 -0
- package/src/database/validate.ts +138 -0
- package/src/db/adapters.ts +125 -0
- package/src/db/index.ts +37 -0
- package/src/db/libsql.ts +23 -0
- package/src/db/postgres.ts +30 -0
- package/src/db/sqlite.ts +27 -0
- package/src/emdash-runtime.ts +2096 -0
- package/src/fields/boolean.ts +34 -0
- package/src/fields/datetime.ts +44 -0
- package/src/fields/file.ts +41 -0
- package/src/fields/image.ts +34 -0
- package/src/fields/index.ts +42 -0
- package/src/fields/integer.ts +50 -0
- package/src/fields/json.ts +37 -0
- package/src/fields/multiselect.ts +48 -0
- package/src/fields/number.ts +52 -0
- package/src/fields/portable-text.ts +33 -0
- package/src/fields/reference.ts +29 -0
- package/src/fields/richtext.ts +31 -0
- package/src/fields/select.ts +46 -0
- package/src/fields/slug.ts +38 -0
- package/src/fields/text.ts +55 -0
- package/src/fields/textarea.ts +52 -0
- package/src/fields/types.ts +64 -0
- package/src/i18n/config.ts +68 -0
- package/src/import/index.ts +90 -0
- package/src/import/menus.ts +436 -0
- package/src/import/registry.ts +111 -0
- package/src/import/sections.ts +103 -0
- package/src/import/settings.ts +281 -0
- package/src/import/sources/wordpress-plugin.ts +641 -0
- package/src/import/sources/wordpress-rest.ts +191 -0
- package/src/import/sources/wxr.ts +330 -0
- package/src/import/ssrf.ts +260 -0
- package/src/import/types.ts +418 -0
- package/src/import/utils.ts +412 -0
- package/src/index.ts +481 -0
- package/src/loader.ts +770 -0
- package/src/mcp/server.ts +1463 -0
- package/src/media/index.ts +32 -0
- package/src/media/local-runtime.ts +213 -0
- package/src/media/local.ts +46 -0
- package/src/media/normalize.ts +190 -0
- package/src/media/placeholder.ts +150 -0
- package/src/media/provider-loader.ts +78 -0
- package/src/media/types.ts +279 -0
- package/src/menus/index.ts +324 -0
- package/src/menus/types.ts +112 -0
- package/src/page/context.ts +93 -0
- package/src/page/fragments.ts +89 -0
- package/src/page/index.ts +58 -0
- package/src/page/jsonld.ts +94 -0
- package/src/page/metadata.ts +185 -0
- package/src/page/seo-contributions.ts +136 -0
- package/src/plugin-utils.ts +80 -0
- package/src/plugins/adapt-sandbox-entry.ts +207 -0
- package/src/plugins/context.ts +833 -0
- package/src/plugins/cron.ts +361 -0
- package/src/plugins/define-plugin.ts +259 -0
- package/src/plugins/email-console.ts +73 -0
- package/src/plugins/email.ts +209 -0
- package/src/plugins/hooks.ts +1273 -0
- package/src/plugins/index.ts +193 -0
- package/src/plugins/manager.ts +595 -0
- package/src/plugins/manifest-schema.ts +230 -0
- package/src/plugins/marketplace.ts +460 -0
- package/src/plugins/request-meta.ts +139 -0
- package/src/plugins/routes.ts +302 -0
- package/src/plugins/sandbox/index.ts +18 -0
- package/src/plugins/sandbox/noop.ts +76 -0
- package/src/plugins/sandbox/types.ts +173 -0
- package/src/plugins/scheduler/node.ts +122 -0
- package/src/plugins/scheduler/piggyback.ts +71 -0
- package/src/plugins/scheduler/types.ts +27 -0
- package/src/plugins/state.ts +208 -0
- package/src/plugins/storage-indexes.ts +326 -0
- package/src/plugins/storage-query.ts +240 -0
- package/src/plugins/types.ts +1284 -0
- package/src/preview/helpers.ts +27 -0
- package/src/preview/index.ts +40 -0
- package/src/preview/tokens.ts +279 -0
- package/src/preview/urls.ts +118 -0
- package/src/query.ts +674 -0
- package/src/redirects/patterns.ts +224 -0
- package/src/request-context.ts +67 -0
- package/src/runtime.ts +21 -0
- package/src/schema/index.ts +29 -0
- package/src/schema/query.ts +44 -0
- package/src/schema/registry.ts +965 -0
- package/src/schema/types.ts +276 -0
- package/src/schema/zod-generator.ts +413 -0
- package/src/search/fts-manager.ts +452 -0
- package/src/search/index.ts +26 -0
- package/src/search/query.ts +396 -0
- package/src/search/text-extraction.ts +162 -0
- package/src/search/types.ts +114 -0
- package/src/sections/index.ts +226 -0
- package/src/sections/types.ts +86 -0
- package/src/seed/apply.ts +1141 -0
- package/src/seed/default.ts +86 -0
- package/src/seed/index.ts +28 -0
- package/src/seed/load.ts +35 -0
- package/src/seed/types.ts +341 -0
- package/src/seed/validate.ts +642 -0
- package/src/seo/index.ts +179 -0
- package/src/settings/index.ts +203 -0
- package/src/settings/types.ts +58 -0
- package/src/storage/index.ts +28 -0
- package/src/storage/local.ts +253 -0
- package/src/storage/s3.ts +271 -0
- package/src/storage/types.ts +204 -0
- package/src/taxonomies/index.ts +309 -0
- package/src/taxonomies/types.ts +61 -0
- package/src/ui.ts +75 -0
- package/src/utils/base64.ts +73 -0
- package/src/utils/hash.ts +36 -0
- package/src/utils/sanitize.ts +20 -0
- package/src/utils/slugify.ts +29 -0
- package/src/utils/url.ts +48 -0
- package/src/virtual-modules.d.ts +111 -0
- package/src/visual-editing/editable.ts +108 -0
- package/src/visual-editing/toolbar.ts +1229 -0
- package/src/widgets/components.ts +105 -0
- package/src/widgets/index.ts +131 -0
- package/src/widgets/types.ts +81 -0
|
@@ -0,0 +1,1463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmDash MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Exposes content, schema, media, search, taxonomy, and menu operations
|
|
5
|
+
* as MCP tools over the Streamable HTTP transport.
|
|
6
|
+
*
|
|
7
|
+
* Tools use the EmDashHandlers interface (same as locals.emdash) so
|
|
8
|
+
* they work with the pre-bound handlers that the middleware provides.
|
|
9
|
+
* The handlers instance is passed per-request via authInfo on the transport.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import type { Permission, RoleLevel } from "@emdash-cms/auth";
|
|
15
|
+
import { canActOnOwn, Role } from "@emdash-cms/auth";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
import type { EmDashHandlers } from "../astro/types.js";
|
|
19
|
+
import { hasScope } from "../auth/api-tokens.js";
|
|
20
|
+
|
|
21
|
+
const COLLECTION_SLUG_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
type HandlerResult = { success: boolean; data?: unknown; error?: unknown };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Unwrap an ApiResult<T> into MCP tool result format.
|
|
31
|
+
* On success, returns the data as pretty-printed JSON text content.
|
|
32
|
+
* On failure, returns the error message with isError flag.
|
|
33
|
+
*/
|
|
34
|
+
function unwrap(result: HandlerResult): {
|
|
35
|
+
content: Array<{ type: "text"; text: string }>;
|
|
36
|
+
isError?: true;
|
|
37
|
+
} {
|
|
38
|
+
if (result.success && result.data !== undefined) {
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const errMsg =
|
|
44
|
+
result.error && typeof result.error === "object" && "message" in result.error
|
|
45
|
+
? String((result.error as Record<string, unknown>).message)
|
|
46
|
+
: "Unknown error";
|
|
47
|
+
return { content: [{ type: "text", text: errMsg }], isError: true };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Return a JSON text block.
|
|
52
|
+
*/
|
|
53
|
+
function jsonResult(data: unknown): {
|
|
54
|
+
content: Array<{ type: "text"; text: string }>;
|
|
55
|
+
} {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Return an error text block.
|
|
63
|
+
*/
|
|
64
|
+
function errorResult(error: unknown): {
|
|
65
|
+
content: Array<{ type: "text"; text: string }>;
|
|
66
|
+
isError: true;
|
|
67
|
+
} {
|
|
68
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
69
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Context extraction
|
|
74
|
+
//
|
|
75
|
+
// The route handler passes emdash + userId in authInfo.extra.
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
interface EmDashExtra {
|
|
79
|
+
emdash: EmDashHandlers;
|
|
80
|
+
userId: string;
|
|
81
|
+
/** The authenticated user's RBAC role level. */
|
|
82
|
+
userRole: RoleLevel;
|
|
83
|
+
/** Token scopes — undefined for session auth (all access allowed). */
|
|
84
|
+
tokenScopes?: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getExtra(extra: { authInfo?: { extra?: Record<string, unknown> } }): EmDashExtra {
|
|
88
|
+
const payload = extra.authInfo?.extra as EmDashExtra | undefined;
|
|
89
|
+
if (!payload?.emdash) {
|
|
90
|
+
throw new Error("EmDash not available — server misconfigured");
|
|
91
|
+
}
|
|
92
|
+
return payload;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getEmDash(extra: { authInfo?: { extra?: Record<string, unknown> } }): EmDashHandlers {
|
|
96
|
+
return getExtra(extra).emdash;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Enforce a scope requirement on the current request.
|
|
101
|
+
*
|
|
102
|
+
* When tokenScopes is undefined (session auth), all operations are allowed
|
|
103
|
+
* since session users have full access based on their role. When scopes are
|
|
104
|
+
* present (token auth), the required scope must be included.
|
|
105
|
+
*/
|
|
106
|
+
function requireScope(
|
|
107
|
+
extra: { authInfo?: { extra?: Record<string, unknown> } },
|
|
108
|
+
scope: string,
|
|
109
|
+
): void {
|
|
110
|
+
const payload = getExtra(extra);
|
|
111
|
+
if (payload.tokenScopes && !hasScope(payload.tokenScopes, scope)) {
|
|
112
|
+
throw new McpError(ErrorCode.InvalidRequest, `Insufficient scope: requires ${scope}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Defense-in-depth: enforce a minimum RBAC role on the current request.
|
|
118
|
+
*
|
|
119
|
+
* This is checked in addition to scope requirements. Even if a token has
|
|
120
|
+
* the right scopes (e.g. due to a bug in scope clamping), the user's
|
|
121
|
+
* actual role must still meet the minimum.
|
|
122
|
+
*/
|
|
123
|
+
function requireRole(
|
|
124
|
+
extra: { authInfo?: { extra?: Record<string, unknown> } },
|
|
125
|
+
minRole: RoleLevel,
|
|
126
|
+
): void {
|
|
127
|
+
const payload = getExtra(extra);
|
|
128
|
+
if (payload.userRole < minRole) {
|
|
129
|
+
throw new McpError(ErrorCode.InvalidRequest, "Insufficient permissions for this operation");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Enforce ownership-based permission checks, mirroring the REST API's
|
|
135
|
+
* requireOwnerPerm() pattern.
|
|
136
|
+
*
|
|
137
|
+
* If the user is the owner, checks ownPermission. Otherwise checks
|
|
138
|
+
* anyPermission (which requires EDITOR+ role).
|
|
139
|
+
*/
|
|
140
|
+
function requireOwnership(
|
|
141
|
+
extra: { authInfo?: { extra?: Record<string, unknown> } },
|
|
142
|
+
ownerId: string,
|
|
143
|
+
ownPermission: Permission,
|
|
144
|
+
anyPermission: Permission,
|
|
145
|
+
): void {
|
|
146
|
+
const payload = getExtra(extra);
|
|
147
|
+
const user = { id: payload.userId, role: payload.userRole };
|
|
148
|
+
if (!canActOnOwn(user, ownerId, ownPermission, anyPermission)) {
|
|
149
|
+
throw new McpError(ErrorCode.InvalidRequest, "Insufficient permissions for this operation");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Extract the author ID from a content handler response.
|
|
155
|
+
*
|
|
156
|
+
* Content handlers return `{ item: { id, authorId, ... }, _rev? }`.
|
|
157
|
+
* This helper navigates that shape safely.
|
|
158
|
+
*/
|
|
159
|
+
function extractContentAuthorId(data: unknown): string {
|
|
160
|
+
if (!data || typeof data !== "object") {
|
|
161
|
+
throw new McpError(
|
|
162
|
+
ErrorCode.InternalError,
|
|
163
|
+
"Cannot determine content ownership: no data returned",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const obj = data as Record<string, unknown>;
|
|
167
|
+
const item =
|
|
168
|
+
obj.item && typeof obj.item === "object" ? (obj.item as Record<string, unknown>) : obj;
|
|
169
|
+
const authorId = typeof item?.authorId === "string" ? item.authorId : "";
|
|
170
|
+
if (!authorId) {
|
|
171
|
+
throw new McpError(
|
|
172
|
+
ErrorCode.InternalError,
|
|
173
|
+
"Cannot determine content ownership: content has no authorId",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return authorId;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extract the resolved ID from a content handler response.
|
|
181
|
+
* Handles slug -> ID resolution performed by the handler.
|
|
182
|
+
*/
|
|
183
|
+
function extractContentId(data: unknown): string | undefined {
|
|
184
|
+
if (!data || typeof data !== "object") return undefined;
|
|
185
|
+
const obj = data as Record<string, unknown>;
|
|
186
|
+
const item =
|
|
187
|
+
obj.item && typeof obj.item === "object" ? (obj.item as Record<string, unknown>) : obj;
|
|
188
|
+
return typeof item?.id === "string" ? item.id : undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Server factory
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
export function createMcpServer(): McpServer {
|
|
196
|
+
const server = new McpServer(
|
|
197
|
+
{ name: "emdash", version: "0.1.0" },
|
|
198
|
+
{ capabilities: { logging: {} } },
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// =====================================================================
|
|
202
|
+
// Content tools
|
|
203
|
+
// =====================================================================
|
|
204
|
+
|
|
205
|
+
server.registerTool(
|
|
206
|
+
"content_list",
|
|
207
|
+
{
|
|
208
|
+
title: "List Content",
|
|
209
|
+
description:
|
|
210
|
+
"List content items in a collection with optional filtering and pagination. " +
|
|
211
|
+
"Returns items sorted by the specified field. Use the nextCursor value from " +
|
|
212
|
+
"the response to fetch the next page. Status can be 'draft', 'published', " +
|
|
213
|
+
"or 'scheduled'. If no status is given, all non-trashed items are returned.",
|
|
214
|
+
inputSchema: z.object({
|
|
215
|
+
collection: z.string().describe("Collection slug (e.g. 'posts', 'pages')"),
|
|
216
|
+
status: z
|
|
217
|
+
.enum(["draft", "published", "scheduled"])
|
|
218
|
+
.optional()
|
|
219
|
+
.describe("Filter by content status"),
|
|
220
|
+
limit: z
|
|
221
|
+
.number()
|
|
222
|
+
.int()
|
|
223
|
+
.min(1)
|
|
224
|
+
.max(100)
|
|
225
|
+
.optional()
|
|
226
|
+
.describe("Max items to return (default 50, max 100)"),
|
|
227
|
+
cursor: z.string().optional().describe("Pagination cursor from a previous response"),
|
|
228
|
+
orderBy: z
|
|
229
|
+
.string()
|
|
230
|
+
.optional()
|
|
231
|
+
.describe("Field to sort by (e.g. 'created_at', 'updated_at')"),
|
|
232
|
+
order: z.enum(["asc", "desc"]).optional().describe("Sort direction (default 'desc')"),
|
|
233
|
+
locale: z
|
|
234
|
+
.string()
|
|
235
|
+
.optional()
|
|
236
|
+
.describe("Filter by locale (e.g. 'en', 'fr'). Only relevant when i18n is enabled."),
|
|
237
|
+
}),
|
|
238
|
+
annotations: { readOnlyHint: true },
|
|
239
|
+
},
|
|
240
|
+
async (args, extra) => {
|
|
241
|
+
requireScope(extra, "content:read");
|
|
242
|
+
const ec = getEmDash(extra);
|
|
243
|
+
return unwrap(
|
|
244
|
+
await ec.handleContentList(args.collection, {
|
|
245
|
+
status: args.status,
|
|
246
|
+
limit: args.limit,
|
|
247
|
+
cursor: args.cursor,
|
|
248
|
+
orderBy: args.orderBy,
|
|
249
|
+
order: args.order,
|
|
250
|
+
locale: args.locale,
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
server.registerTool(
|
|
257
|
+
"content_get",
|
|
258
|
+
{
|
|
259
|
+
title: "Get Content",
|
|
260
|
+
description:
|
|
261
|
+
"Get a single content item by its ID or slug. Returns the full content data " +
|
|
262
|
+
"including all field values, metadata, and a _rev token for optimistic " +
|
|
263
|
+
"concurrency (pass _rev back when updating to detect conflicts).",
|
|
264
|
+
inputSchema: z.object({
|
|
265
|
+
collection: z.string().describe("Collection slug (e.g. 'posts', 'pages')"),
|
|
266
|
+
id: z.string().describe("Content item ID (ULID) or slug"),
|
|
267
|
+
locale: z
|
|
268
|
+
.string()
|
|
269
|
+
.optional()
|
|
270
|
+
.describe(
|
|
271
|
+
"Locale to scope slug lookup (e.g. 'fr'). Only affects slug resolution; IDs are globally unique.",
|
|
272
|
+
),
|
|
273
|
+
}),
|
|
274
|
+
annotations: { readOnlyHint: true },
|
|
275
|
+
},
|
|
276
|
+
async (args, extra) => {
|
|
277
|
+
requireScope(extra, "content:read");
|
|
278
|
+
const ec = getEmDash(extra);
|
|
279
|
+
return unwrap(await ec.handleContentGet(args.collection, args.id, args.locale));
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
server.registerTool(
|
|
284
|
+
"content_create",
|
|
285
|
+
{
|
|
286
|
+
title: "Create Content",
|
|
287
|
+
description:
|
|
288
|
+
"Create a new content item in a collection. The 'data' object should " +
|
|
289
|
+
"contain field values matching the collection's schema (use " +
|
|
290
|
+
"schema_get_collection to check). Rich text fields accept Portable Text " +
|
|
291
|
+
"JSON arrays. A slug is auto-generated if not provided. Items are created " +
|
|
292
|
+
"as 'draft' by default — use content_publish to make them live.",
|
|
293
|
+
inputSchema: z.object({
|
|
294
|
+
collection: z.string().describe("Collection slug (e.g. 'posts', 'pages')"),
|
|
295
|
+
data: z
|
|
296
|
+
.record(z.string(), z.unknown())
|
|
297
|
+
.describe("Field values as key-value pairs matching the collection schema"),
|
|
298
|
+
slug: z.string().optional().describe("URL slug (auto-generated from title if omitted)"),
|
|
299
|
+
status: z
|
|
300
|
+
.enum(["draft", "published"])
|
|
301
|
+
.optional()
|
|
302
|
+
.describe("Initial status (default 'draft')"),
|
|
303
|
+
locale: z
|
|
304
|
+
.string()
|
|
305
|
+
.optional()
|
|
306
|
+
.describe("Locale for this content (e.g. 'fr'). Defaults to default locale."),
|
|
307
|
+
translationOf: z
|
|
308
|
+
.string()
|
|
309
|
+
.optional()
|
|
310
|
+
.describe(
|
|
311
|
+
"ID of the content item this is a translation of. Links items in the same translation group.",
|
|
312
|
+
),
|
|
313
|
+
}),
|
|
314
|
+
annotations: { destructiveHint: false },
|
|
315
|
+
},
|
|
316
|
+
async (args, extra) => {
|
|
317
|
+
requireScope(extra, "content:write");
|
|
318
|
+
requireRole(extra, Role.CONTRIBUTOR);
|
|
319
|
+
const { emdash, userId } = getExtra(extra);
|
|
320
|
+
return unwrap(
|
|
321
|
+
await emdash.handleContentCreate(args.collection, {
|
|
322
|
+
data: args.data,
|
|
323
|
+
slug: args.slug,
|
|
324
|
+
status: args.status,
|
|
325
|
+
authorId: userId,
|
|
326
|
+
locale: args.locale,
|
|
327
|
+
translationOf: args.translationOf,
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
server.registerTool(
|
|
334
|
+
"content_update",
|
|
335
|
+
{
|
|
336
|
+
title: "Update Content",
|
|
337
|
+
description:
|
|
338
|
+
"Update an existing content item. Only include fields you want to change " +
|
|
339
|
+
"in the 'data' object — unspecified fields are left unchanged. Pass the " +
|
|
340
|
+
"_rev token from content_get to enable optimistic concurrency checking " +
|
|
341
|
+
"(the update fails if the item was modified since you read it).",
|
|
342
|
+
inputSchema: z.object({
|
|
343
|
+
collection: z.string().describe("Collection slug"),
|
|
344
|
+
id: z.string().describe("Content item ID or slug"),
|
|
345
|
+
data: z
|
|
346
|
+
.record(z.string(), z.unknown())
|
|
347
|
+
.optional()
|
|
348
|
+
.describe("Field values to update (only include changed fields)"),
|
|
349
|
+
slug: z.string().optional().describe("New URL slug"),
|
|
350
|
+
status: z.enum(["draft", "published"]).optional().describe("New status"),
|
|
351
|
+
_rev: z
|
|
352
|
+
.string()
|
|
353
|
+
.optional()
|
|
354
|
+
.describe("Revision token from content_get for conflict detection"),
|
|
355
|
+
}),
|
|
356
|
+
},
|
|
357
|
+
async (args, extra) => {
|
|
358
|
+
requireScope(extra, "content:write");
|
|
359
|
+
requireRole(extra, Role.AUTHOR);
|
|
360
|
+
const { emdash, userId } = getExtra(extra);
|
|
361
|
+
|
|
362
|
+
// Fetch item to check ownership
|
|
363
|
+
const existing = await emdash.handleContentGet(args.collection, args.id);
|
|
364
|
+
if (!existing.success) {
|
|
365
|
+
return unwrap(existing);
|
|
366
|
+
}
|
|
367
|
+
requireOwnership(
|
|
368
|
+
extra,
|
|
369
|
+
extractContentAuthorId(existing.data),
|
|
370
|
+
"content:edit_own",
|
|
371
|
+
"content:edit_any",
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const resolvedId = extractContentId(existing.data) ?? args.id;
|
|
375
|
+
return unwrap(
|
|
376
|
+
await emdash.handleContentUpdate(args.collection, resolvedId, {
|
|
377
|
+
data: args.data,
|
|
378
|
+
slug: args.slug,
|
|
379
|
+
status: args.status,
|
|
380
|
+
authorId: userId,
|
|
381
|
+
_rev: args._rev,
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
server.registerTool(
|
|
388
|
+
"content_delete",
|
|
389
|
+
{
|
|
390
|
+
title: "Delete Content (Trash)",
|
|
391
|
+
description:
|
|
392
|
+
"Soft-delete a content item by moving it to the trash. The item can be " +
|
|
393
|
+
"restored later with content_restore, or permanently deleted with " +
|
|
394
|
+
"content_permanent_delete.",
|
|
395
|
+
inputSchema: z.object({
|
|
396
|
+
collection: z.string().describe("Collection slug"),
|
|
397
|
+
id: z.string().describe("Content item ID or slug"),
|
|
398
|
+
}),
|
|
399
|
+
annotations: { destructiveHint: true },
|
|
400
|
+
},
|
|
401
|
+
async (args, extra) => {
|
|
402
|
+
requireScope(extra, "content:write");
|
|
403
|
+
requireRole(extra, Role.AUTHOR);
|
|
404
|
+
const ec = getEmDash(extra);
|
|
405
|
+
|
|
406
|
+
// Fetch item to check ownership
|
|
407
|
+
const existing = await ec.handleContentGet(args.collection, args.id);
|
|
408
|
+
if (!existing.success) {
|
|
409
|
+
return unwrap(existing);
|
|
410
|
+
}
|
|
411
|
+
requireOwnership(
|
|
412
|
+
extra,
|
|
413
|
+
extractContentAuthorId(existing.data),
|
|
414
|
+
"content:delete_own",
|
|
415
|
+
"content:delete_any",
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const resolvedId = extractContentId(existing.data) ?? args.id;
|
|
419
|
+
return unwrap(await ec.handleContentDelete(args.collection, resolvedId));
|
|
420
|
+
},
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
server.registerTool(
|
|
424
|
+
"content_restore",
|
|
425
|
+
{
|
|
426
|
+
title: "Restore Content",
|
|
427
|
+
description: "Restore a soft-deleted content item from the trash back to its previous state.",
|
|
428
|
+
inputSchema: z.object({
|
|
429
|
+
collection: z.string().describe("Collection slug"),
|
|
430
|
+
id: z.string().describe("Content item ID or slug"),
|
|
431
|
+
}),
|
|
432
|
+
},
|
|
433
|
+
async (args, extra) => {
|
|
434
|
+
requireScope(extra, "content:write");
|
|
435
|
+
requireRole(extra, Role.AUTHOR);
|
|
436
|
+
const ec = getEmDash(extra);
|
|
437
|
+
|
|
438
|
+
// Fetch trashed item to check ownership
|
|
439
|
+
const existing = await ec.handleContentGetIncludingTrashed(args.collection, args.id);
|
|
440
|
+
if (!existing.success) {
|
|
441
|
+
return unwrap(existing);
|
|
442
|
+
}
|
|
443
|
+
requireOwnership(
|
|
444
|
+
extra,
|
|
445
|
+
extractContentAuthorId(existing.data),
|
|
446
|
+
"content:edit_own",
|
|
447
|
+
"content:edit_any",
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
const resolvedId = extractContentId(existing.data) ?? args.id;
|
|
451
|
+
return unwrap(await ec.handleContentRestore(args.collection, resolvedId));
|
|
452
|
+
},
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
server.registerTool(
|
|
456
|
+
"content_permanent_delete",
|
|
457
|
+
{
|
|
458
|
+
title: "Permanently Delete Content",
|
|
459
|
+
description:
|
|
460
|
+
"Permanently and irreversibly delete a trashed content item. The item " +
|
|
461
|
+
"must be in the trash first (use content_delete). This cannot be undone.",
|
|
462
|
+
inputSchema: z.object({
|
|
463
|
+
collection: z.string().describe("Collection slug"),
|
|
464
|
+
id: z.string().describe("Content item ID or slug"),
|
|
465
|
+
}),
|
|
466
|
+
annotations: { destructiveHint: true },
|
|
467
|
+
},
|
|
468
|
+
async (args, extra) => {
|
|
469
|
+
requireScope(extra, "content:write");
|
|
470
|
+
requireRole(extra, Role.ADMIN);
|
|
471
|
+
const ec = getEmDash(extra);
|
|
472
|
+
return unwrap(await ec.handleContentPermanentDelete(args.collection, args.id));
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
server.registerTool(
|
|
477
|
+
"content_publish",
|
|
478
|
+
{
|
|
479
|
+
title: "Publish Content",
|
|
480
|
+
description:
|
|
481
|
+
"Publish a content item, making it live on the site. Creates a published " +
|
|
482
|
+
"revision from the current draft. Further edits create a new draft without " +
|
|
483
|
+
"affecting the live version until re-published.",
|
|
484
|
+
inputSchema: z.object({
|
|
485
|
+
collection: z.string().describe("Collection slug"),
|
|
486
|
+
id: z.string().describe("Content item ID or slug"),
|
|
487
|
+
}),
|
|
488
|
+
},
|
|
489
|
+
async (args, extra) => {
|
|
490
|
+
requireScope(extra, "content:write");
|
|
491
|
+
requireRole(extra, Role.AUTHOR);
|
|
492
|
+
const ec = getEmDash(extra);
|
|
493
|
+
|
|
494
|
+
// Fetch item to check ownership
|
|
495
|
+
const existing = await ec.handleContentGet(args.collection, args.id);
|
|
496
|
+
if (!existing.success) {
|
|
497
|
+
return unwrap(existing);
|
|
498
|
+
}
|
|
499
|
+
requireOwnership(
|
|
500
|
+
extra,
|
|
501
|
+
extractContentAuthorId(existing.data),
|
|
502
|
+
"content:publish_own",
|
|
503
|
+
"content:publish_any",
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
const resolvedId = extractContentId(existing.data) ?? args.id;
|
|
507
|
+
return unwrap(await ec.handleContentPublish(args.collection, resolvedId));
|
|
508
|
+
},
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
server.registerTool(
|
|
512
|
+
"content_unpublish",
|
|
513
|
+
{
|
|
514
|
+
title: "Unpublish Content",
|
|
515
|
+
description:
|
|
516
|
+
"Unpublish a content item, reverting it to draft status. It will no " +
|
|
517
|
+
"longer be visible on the live site but its content is preserved.",
|
|
518
|
+
inputSchema: z.object({
|
|
519
|
+
collection: z.string().describe("Collection slug"),
|
|
520
|
+
id: z.string().describe("Content item ID or slug"),
|
|
521
|
+
}),
|
|
522
|
+
},
|
|
523
|
+
async (args, extra) => {
|
|
524
|
+
requireScope(extra, "content:write");
|
|
525
|
+
requireRole(extra, Role.AUTHOR);
|
|
526
|
+
const ec = getEmDash(extra);
|
|
527
|
+
|
|
528
|
+
// Fetch item to check ownership
|
|
529
|
+
const existing = await ec.handleContentGet(args.collection, args.id);
|
|
530
|
+
if (!existing.success) {
|
|
531
|
+
return unwrap(existing);
|
|
532
|
+
}
|
|
533
|
+
requireOwnership(
|
|
534
|
+
extra,
|
|
535
|
+
extractContentAuthorId(existing.data),
|
|
536
|
+
"content:publish_own",
|
|
537
|
+
"content:publish_any",
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
const resolvedId = extractContentId(existing.data) ?? args.id;
|
|
541
|
+
return unwrap(await ec.handleContentUnpublish(args.collection, resolvedId));
|
|
542
|
+
},
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
server.registerTool(
|
|
546
|
+
"content_schedule",
|
|
547
|
+
{
|
|
548
|
+
title: "Schedule Content",
|
|
549
|
+
description:
|
|
550
|
+
"Schedule a content item for future publication. It will be automatically " +
|
|
551
|
+
"published at the specified date/time. The scheduledAt value must be an " +
|
|
552
|
+
"ISO 8601 datetime string in the future (e.g. '2025-06-01T09:00:00Z').",
|
|
553
|
+
inputSchema: z.object({
|
|
554
|
+
collection: z.string().describe("Collection slug"),
|
|
555
|
+
id: z.string().describe("Content item ID or slug"),
|
|
556
|
+
scheduledAt: z
|
|
557
|
+
.string()
|
|
558
|
+
.describe("ISO 8601 datetime for publication (e.g. '2025-06-01T09:00:00Z')"),
|
|
559
|
+
}),
|
|
560
|
+
},
|
|
561
|
+
async (args, extra) => {
|
|
562
|
+
requireScope(extra, "content:write");
|
|
563
|
+
requireRole(extra, Role.AUTHOR);
|
|
564
|
+
const ec = getEmDash(extra);
|
|
565
|
+
|
|
566
|
+
// Fetch item to check ownership
|
|
567
|
+
const existing = await ec.handleContentGet(args.collection, args.id);
|
|
568
|
+
if (!existing.success) {
|
|
569
|
+
return unwrap(existing);
|
|
570
|
+
}
|
|
571
|
+
requireOwnership(
|
|
572
|
+
extra,
|
|
573
|
+
extractContentAuthorId(existing.data),
|
|
574
|
+
"content:publish_own",
|
|
575
|
+
"content:publish_any",
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const resolvedId = extractContentId(existing.data) ?? args.id;
|
|
579
|
+
return unwrap(await ec.handleContentSchedule(args.collection, resolvedId, args.scheduledAt));
|
|
580
|
+
},
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
server.registerTool(
|
|
584
|
+
"content_compare",
|
|
585
|
+
{
|
|
586
|
+
title: "Compare Live vs Draft",
|
|
587
|
+
description:
|
|
588
|
+
"Compare the published (live) version of a content item with its current " +
|
|
589
|
+
"draft. Returns both versions and a flag indicating whether there are " +
|
|
590
|
+
"changes. Useful for reviewing unpublished edits before publishing.",
|
|
591
|
+
inputSchema: z.object({
|
|
592
|
+
collection: z.string().describe("Collection slug"),
|
|
593
|
+
id: z.string().describe("Content item ID or slug"),
|
|
594
|
+
}),
|
|
595
|
+
annotations: { readOnlyHint: true },
|
|
596
|
+
},
|
|
597
|
+
async (args, extra) => {
|
|
598
|
+
requireScope(extra, "content:read");
|
|
599
|
+
const ec = getEmDash(extra);
|
|
600
|
+
return unwrap(await ec.handleContentCompare(args.collection, args.id));
|
|
601
|
+
},
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
server.registerTool(
|
|
605
|
+
"content_discard_draft",
|
|
606
|
+
{
|
|
607
|
+
title: "Discard Draft",
|
|
608
|
+
description:
|
|
609
|
+
"Discard the current draft changes and revert to the last published " +
|
|
610
|
+
"version. Only works on items that have been published at least once.",
|
|
611
|
+
inputSchema: z.object({
|
|
612
|
+
collection: z.string().describe("Collection slug"),
|
|
613
|
+
id: z.string().describe("Content item ID or slug"),
|
|
614
|
+
}),
|
|
615
|
+
annotations: { destructiveHint: true },
|
|
616
|
+
},
|
|
617
|
+
async (args, extra) => {
|
|
618
|
+
requireScope(extra, "content:write");
|
|
619
|
+
requireRole(extra, Role.AUTHOR);
|
|
620
|
+
const ec = getEmDash(extra);
|
|
621
|
+
|
|
622
|
+
// Fetch item to check ownership
|
|
623
|
+
const existing = await ec.handleContentGet(args.collection, args.id);
|
|
624
|
+
if (!existing.success) {
|
|
625
|
+
return unwrap(existing);
|
|
626
|
+
}
|
|
627
|
+
requireOwnership(
|
|
628
|
+
extra,
|
|
629
|
+
extractContentAuthorId(existing.data),
|
|
630
|
+
"content:edit_own",
|
|
631
|
+
"content:edit_any",
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
const resolvedId = extractContentId(existing.data) ?? args.id;
|
|
635
|
+
return unwrap(await ec.handleContentDiscardDraft(args.collection, resolvedId));
|
|
636
|
+
},
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
server.registerTool(
|
|
640
|
+
"content_list_trashed",
|
|
641
|
+
{
|
|
642
|
+
title: "List Trashed Content",
|
|
643
|
+
description:
|
|
644
|
+
"List soft-deleted content items in a collection's trash. These items " +
|
|
645
|
+
"can be restored with content_restore or permanently deleted with " +
|
|
646
|
+
"content_permanent_delete.",
|
|
647
|
+
inputSchema: z.object({
|
|
648
|
+
collection: z.string().describe("Collection slug"),
|
|
649
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max items (default 50)"),
|
|
650
|
+
cursor: z.string().optional().describe("Pagination cursor"),
|
|
651
|
+
}),
|
|
652
|
+
annotations: { readOnlyHint: true },
|
|
653
|
+
},
|
|
654
|
+
async (args, extra) => {
|
|
655
|
+
requireScope(extra, "content:read");
|
|
656
|
+
const ec = getEmDash(extra);
|
|
657
|
+
return unwrap(
|
|
658
|
+
await ec.handleContentListTrashed(args.collection, {
|
|
659
|
+
limit: args.limit,
|
|
660
|
+
cursor: args.cursor,
|
|
661
|
+
}),
|
|
662
|
+
);
|
|
663
|
+
},
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
server.registerTool(
|
|
667
|
+
"content_duplicate",
|
|
668
|
+
{
|
|
669
|
+
title: "Duplicate Content",
|
|
670
|
+
description:
|
|
671
|
+
"Create a copy of an existing content item. The duplicate is created " +
|
|
672
|
+
"as a draft with '(Copy)' appended to the title and an auto-generated slug.",
|
|
673
|
+
inputSchema: z.object({
|
|
674
|
+
collection: z.string().describe("Collection slug"),
|
|
675
|
+
id: z.string().describe("Content item ID or slug to duplicate"),
|
|
676
|
+
}),
|
|
677
|
+
},
|
|
678
|
+
async (args, extra) => {
|
|
679
|
+
requireScope(extra, "content:write");
|
|
680
|
+
requireRole(extra, Role.CONTRIBUTOR);
|
|
681
|
+
const ec = getEmDash(extra);
|
|
682
|
+
return unwrap(await ec.handleContentDuplicate(args.collection, args.id));
|
|
683
|
+
},
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
server.registerTool(
|
|
687
|
+
"content_translations",
|
|
688
|
+
{
|
|
689
|
+
title: "Get Content Translations",
|
|
690
|
+
description:
|
|
691
|
+
"Get all locale variants of a content item. Returns the translation group " +
|
|
692
|
+
"and a summary of each locale version (id, locale, slug, status). Only " +
|
|
693
|
+
"relevant when i18n is enabled on the site.",
|
|
694
|
+
inputSchema: z.object({
|
|
695
|
+
collection: z.string().describe("Collection slug"),
|
|
696
|
+
id: z.string().describe("Content item ID or slug"),
|
|
697
|
+
}),
|
|
698
|
+
annotations: { readOnlyHint: true },
|
|
699
|
+
},
|
|
700
|
+
async (args, extra) => {
|
|
701
|
+
requireScope(extra, "content:read");
|
|
702
|
+
const ec = getEmDash(extra);
|
|
703
|
+
return unwrap(await ec.handleContentTranslations(args.collection, args.id));
|
|
704
|
+
},
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
// =====================================================================
|
|
708
|
+
// Schema tools
|
|
709
|
+
// =====================================================================
|
|
710
|
+
|
|
711
|
+
server.registerTool(
|
|
712
|
+
"schema_list_collections",
|
|
713
|
+
{
|
|
714
|
+
title: "List Collections",
|
|
715
|
+
description:
|
|
716
|
+
"List all content collections defined in the CMS. Each collection " +
|
|
717
|
+
"represents a content type (e.g. posts, pages, products) with its own " +
|
|
718
|
+
"schema and database table. Returns slug, label, supported features, " +
|
|
719
|
+
"and timestamps.",
|
|
720
|
+
inputSchema: z.object({}),
|
|
721
|
+
annotations: { readOnlyHint: true },
|
|
722
|
+
},
|
|
723
|
+
async (_args, extra) => {
|
|
724
|
+
requireScope(extra, "schema:read");
|
|
725
|
+
requireRole(extra, Role.EDITOR);
|
|
726
|
+
const ec = getEmDash(extra);
|
|
727
|
+
try {
|
|
728
|
+
const { SchemaRegistry } = await import("../schema/index.js");
|
|
729
|
+
const registry = new SchemaRegistry(ec.db);
|
|
730
|
+
const items = await registry.listCollections();
|
|
731
|
+
return jsonResult({ items });
|
|
732
|
+
} catch (error) {
|
|
733
|
+
return errorResult(error);
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
server.registerTool(
|
|
739
|
+
"schema_get_collection",
|
|
740
|
+
{
|
|
741
|
+
title: "Get Collection Schema",
|
|
742
|
+
description:
|
|
743
|
+
"Get detailed info about a collection including all field definitions. " +
|
|
744
|
+
"Fields describe the data model: name, type (string, text, number, " +
|
|
745
|
+
"boolean, datetime, portableText, image, reference, json, select, " +
|
|
746
|
+
"multiSelect, slug), constraints, and validation rules. Use this to " +
|
|
747
|
+
"understand what data content_create and content_update expect.",
|
|
748
|
+
inputSchema: z.object({
|
|
749
|
+
slug: z
|
|
750
|
+
.string()
|
|
751
|
+
.describe(
|
|
752
|
+
"Collection slug (e.g. 'posts'). Use schema_list_collections to see available slugs.",
|
|
753
|
+
),
|
|
754
|
+
}),
|
|
755
|
+
annotations: { readOnlyHint: true },
|
|
756
|
+
},
|
|
757
|
+
async (args, extra) => {
|
|
758
|
+
requireScope(extra, "schema:read");
|
|
759
|
+
requireRole(extra, Role.EDITOR);
|
|
760
|
+
const ec = getEmDash(extra);
|
|
761
|
+
try {
|
|
762
|
+
const { SchemaRegistry } = await import("../schema/index.js");
|
|
763
|
+
const registry = new SchemaRegistry(ec.db);
|
|
764
|
+
const collection = await registry.getCollectionWithFields(args.slug);
|
|
765
|
+
if (!collection) {
|
|
766
|
+
return errorResult(`Collection '${args.slug}' not found`);
|
|
767
|
+
}
|
|
768
|
+
return jsonResult(collection);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
return errorResult(error);
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
server.registerTool(
|
|
776
|
+
"schema_create_collection",
|
|
777
|
+
{
|
|
778
|
+
title: "Create Collection",
|
|
779
|
+
description:
|
|
780
|
+
"Create a new content collection (content type). This creates a database " +
|
|
781
|
+
"table and schema definition. The slug must be lowercase alphanumeric " +
|
|
782
|
+
"with underscores, starting with a letter. Supports: 'drafts' (draft/" +
|
|
783
|
+
"publish workflow), 'revisions' (version history), 'preview' (live " +
|
|
784
|
+
"preview), 'scheduling' (timed publish), 'search' (full-text indexing).",
|
|
785
|
+
inputSchema: z.object({
|
|
786
|
+
slug: z
|
|
787
|
+
.string()
|
|
788
|
+
.regex(COLLECTION_SLUG_PATTERN)
|
|
789
|
+
.describe("Unique identifier (lowercase letters, numbers, underscores)"),
|
|
790
|
+
label: z.string().describe("Display name (plural, e.g. 'Blog Posts')"),
|
|
791
|
+
labelSingular: z.string().optional().describe("Singular display name (e.g. 'Blog Post')"),
|
|
792
|
+
description: z.string().optional().describe("Description of this collection"),
|
|
793
|
+
icon: z.string().optional().describe("Icon name for the admin UI"),
|
|
794
|
+
supports: z
|
|
795
|
+
.array(z.enum(["drafts", "revisions", "preview", "scheduling", "search"]))
|
|
796
|
+
.optional()
|
|
797
|
+
.describe("Features to enable (default: ['drafts', 'revisions'])"),
|
|
798
|
+
}),
|
|
799
|
+
},
|
|
800
|
+
async (args, extra) => {
|
|
801
|
+
requireScope(extra, "schema:write");
|
|
802
|
+
requireRole(extra, Role.ADMIN);
|
|
803
|
+
const ec = getEmDash(extra);
|
|
804
|
+
try {
|
|
805
|
+
const { SchemaRegistry } = await import("../schema/index.js");
|
|
806
|
+
const registry = new SchemaRegistry(ec.db);
|
|
807
|
+
const collection = await registry.createCollection({
|
|
808
|
+
slug: args.slug,
|
|
809
|
+
label: args.label,
|
|
810
|
+
labelSingular: args.labelSingular,
|
|
811
|
+
description: args.description,
|
|
812
|
+
icon: args.icon,
|
|
813
|
+
supports: args.supports,
|
|
814
|
+
});
|
|
815
|
+
ec.invalidateManifest();
|
|
816
|
+
return jsonResult(collection);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
return errorResult(error);
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
server.registerTool(
|
|
824
|
+
"schema_delete_collection",
|
|
825
|
+
{
|
|
826
|
+
title: "Delete Collection",
|
|
827
|
+
description:
|
|
828
|
+
"Delete a collection and its database table. This is irreversible and " +
|
|
829
|
+
"deletes all content in the collection. Use with extreme caution.",
|
|
830
|
+
inputSchema: z.object({
|
|
831
|
+
slug: z.string().describe("Collection slug to delete"),
|
|
832
|
+
force: z
|
|
833
|
+
.boolean()
|
|
834
|
+
.optional()
|
|
835
|
+
.describe("Force deletion even if the collection has content (default false)"),
|
|
836
|
+
}),
|
|
837
|
+
annotations: { destructiveHint: true },
|
|
838
|
+
},
|
|
839
|
+
async (args, extra) => {
|
|
840
|
+
requireScope(extra, "schema:write");
|
|
841
|
+
requireRole(extra, Role.ADMIN);
|
|
842
|
+
const ec = getEmDash(extra);
|
|
843
|
+
try {
|
|
844
|
+
const { SchemaRegistry } = await import("../schema/index.js");
|
|
845
|
+
const registry = new SchemaRegistry(ec.db);
|
|
846
|
+
await registry.deleteCollection(args.slug, { force: args.force });
|
|
847
|
+
ec.invalidateManifest();
|
|
848
|
+
return jsonResult({ deleted: args.slug });
|
|
849
|
+
} catch (error) {
|
|
850
|
+
return errorResult(error);
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
server.registerTool(
|
|
856
|
+
"schema_create_field",
|
|
857
|
+
{
|
|
858
|
+
title: "Add Field to Collection",
|
|
859
|
+
description:
|
|
860
|
+
"Add a new field to a collection's schema. This adds a column to the " +
|
|
861
|
+
"database table. Field types: string (short text), text (long text), " +
|
|
862
|
+
"number (decimal), integer, boolean, datetime, select (single choice), " +
|
|
863
|
+
"multiSelect (multiple), portableText (rich text), image, file, " +
|
|
864
|
+
"reference (link to another collection), json, slug (URL-safe id). " +
|
|
865
|
+
"For select/multiSelect, provide choices in validation.options array.",
|
|
866
|
+
inputSchema: z.object({
|
|
867
|
+
collection: z.string().describe("Collection slug to add the field to"),
|
|
868
|
+
slug: z
|
|
869
|
+
.string()
|
|
870
|
+
.regex(COLLECTION_SLUG_PATTERN)
|
|
871
|
+
.describe("Field identifier (lowercase letters, numbers, underscores)"),
|
|
872
|
+
label: z.string().describe("Display name for the field"),
|
|
873
|
+
type: z
|
|
874
|
+
.enum([
|
|
875
|
+
"string",
|
|
876
|
+
"text",
|
|
877
|
+
"number",
|
|
878
|
+
"integer",
|
|
879
|
+
"boolean",
|
|
880
|
+
"datetime",
|
|
881
|
+
"select",
|
|
882
|
+
"multiSelect",
|
|
883
|
+
"portableText",
|
|
884
|
+
"image",
|
|
885
|
+
"file",
|
|
886
|
+
"reference",
|
|
887
|
+
"json",
|
|
888
|
+
"slug",
|
|
889
|
+
])
|
|
890
|
+
.describe("Data type for this field"),
|
|
891
|
+
required: z.boolean().optional().describe("Whether the field is required (default false)"),
|
|
892
|
+
unique: z.boolean().optional().describe("Whether values must be unique (default false)"),
|
|
893
|
+
defaultValue: z.unknown().optional().describe("Default value for new items"),
|
|
894
|
+
validation: z
|
|
895
|
+
.object({
|
|
896
|
+
min: z.number().optional(),
|
|
897
|
+
max: z.number().optional(),
|
|
898
|
+
minLength: z.number().optional(),
|
|
899
|
+
maxLength: z.number().optional(),
|
|
900
|
+
pattern: z.string().optional(),
|
|
901
|
+
options: z
|
|
902
|
+
.array(z.string())
|
|
903
|
+
.optional()
|
|
904
|
+
.describe("Allowed values for select/multiSelect"),
|
|
905
|
+
})
|
|
906
|
+
.optional()
|
|
907
|
+
.describe("Validation constraints"),
|
|
908
|
+
options: z
|
|
909
|
+
.object({
|
|
910
|
+
collection: z
|
|
911
|
+
.string()
|
|
912
|
+
.optional()
|
|
913
|
+
.describe("Target collection slug for reference fields"),
|
|
914
|
+
rows: z.number().optional().describe("Number of rows for textarea"),
|
|
915
|
+
})
|
|
916
|
+
.passthrough()
|
|
917
|
+
.optional()
|
|
918
|
+
.describe("Widget configuration"),
|
|
919
|
+
searchable: z
|
|
920
|
+
.boolean()
|
|
921
|
+
.optional()
|
|
922
|
+
.describe("Include in full-text search index (default false)"),
|
|
923
|
+
translatable: z
|
|
924
|
+
.boolean()
|
|
925
|
+
.optional()
|
|
926
|
+
.describe(
|
|
927
|
+
"Whether this field is translatable (default true). " +
|
|
928
|
+
"Non-translatable fields are synced across all locales in a translation group.",
|
|
929
|
+
),
|
|
930
|
+
}),
|
|
931
|
+
},
|
|
932
|
+
async (args, extra) => {
|
|
933
|
+
requireScope(extra, "schema:write");
|
|
934
|
+
requireRole(extra, Role.ADMIN);
|
|
935
|
+
const ec = getEmDash(extra);
|
|
936
|
+
try {
|
|
937
|
+
const { SchemaRegistry } = await import("../schema/index.js");
|
|
938
|
+
const registry = new SchemaRegistry(ec.db);
|
|
939
|
+
const field = await registry.createField(args.collection, {
|
|
940
|
+
slug: args.slug,
|
|
941
|
+
label: args.label,
|
|
942
|
+
type: args.type,
|
|
943
|
+
required: args.required,
|
|
944
|
+
unique: args.unique,
|
|
945
|
+
defaultValue: args.defaultValue,
|
|
946
|
+
validation: args.validation,
|
|
947
|
+
options: args.options,
|
|
948
|
+
searchable: args.searchable,
|
|
949
|
+
translatable: args.translatable,
|
|
950
|
+
});
|
|
951
|
+
ec.invalidateManifest();
|
|
952
|
+
return jsonResult(field);
|
|
953
|
+
} catch (error) {
|
|
954
|
+
return errorResult(error);
|
|
955
|
+
}
|
|
956
|
+
},
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
server.registerTool(
|
|
960
|
+
"schema_delete_field",
|
|
961
|
+
{
|
|
962
|
+
title: "Remove Field from Collection",
|
|
963
|
+
description:
|
|
964
|
+
"Remove a field from a collection. This drops the column from the " +
|
|
965
|
+
"database table and deletes all data in that field. Irreversible.",
|
|
966
|
+
inputSchema: z.object({
|
|
967
|
+
collection: z.string().describe("Collection slug"),
|
|
968
|
+
fieldSlug: z.string().describe("Field slug to remove"),
|
|
969
|
+
}),
|
|
970
|
+
annotations: { destructiveHint: true },
|
|
971
|
+
},
|
|
972
|
+
async (args, extra) => {
|
|
973
|
+
requireScope(extra, "schema:write");
|
|
974
|
+
requireRole(extra, Role.ADMIN);
|
|
975
|
+
const ec = getEmDash(extra);
|
|
976
|
+
try {
|
|
977
|
+
const { SchemaRegistry } = await import("../schema/index.js");
|
|
978
|
+
const registry = new SchemaRegistry(ec.db);
|
|
979
|
+
await registry.deleteField(args.collection, args.fieldSlug);
|
|
980
|
+
ec.invalidateManifest();
|
|
981
|
+
return jsonResult({ deleted: args.fieldSlug, collection: args.collection });
|
|
982
|
+
} catch (error) {
|
|
983
|
+
return errorResult(error);
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
// =====================================================================
|
|
989
|
+
// Media tools
|
|
990
|
+
// =====================================================================
|
|
991
|
+
|
|
992
|
+
server.registerTool(
|
|
993
|
+
"media_list",
|
|
994
|
+
{
|
|
995
|
+
title: "List Media",
|
|
996
|
+
description:
|
|
997
|
+
"List uploaded media files (images, documents, etc.) with optional MIME " +
|
|
998
|
+
"type filtering and pagination. Returns file metadata including filename, " +
|
|
999
|
+
"URL, dimensions, and alt text.",
|
|
1000
|
+
inputSchema: z.object({
|
|
1001
|
+
mimeType: z
|
|
1002
|
+
.string()
|
|
1003
|
+
.optional()
|
|
1004
|
+
.describe("Filter by MIME type prefix (e.g. 'image/', 'application/pdf')"),
|
|
1005
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max items (default 50)"),
|
|
1006
|
+
cursor: z.string().optional().describe("Pagination cursor"),
|
|
1007
|
+
}),
|
|
1008
|
+
annotations: { readOnlyHint: true },
|
|
1009
|
+
},
|
|
1010
|
+
async (args, extra) => {
|
|
1011
|
+
requireScope(extra, "media:read");
|
|
1012
|
+
const ec = getEmDash(extra);
|
|
1013
|
+
return unwrap(
|
|
1014
|
+
await ec.handleMediaList({
|
|
1015
|
+
mimeType: args.mimeType,
|
|
1016
|
+
limit: args.limit,
|
|
1017
|
+
cursor: args.cursor,
|
|
1018
|
+
}),
|
|
1019
|
+
);
|
|
1020
|
+
},
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
server.registerTool(
|
|
1024
|
+
"media_get",
|
|
1025
|
+
{
|
|
1026
|
+
title: "Get Media Item",
|
|
1027
|
+
description:
|
|
1028
|
+
"Get details of a single media file by its ID. Returns metadata " +
|
|
1029
|
+
"including filename, MIME type, size, dimensions, alt text, and URL.",
|
|
1030
|
+
inputSchema: z.object({
|
|
1031
|
+
id: z.string().describe("Media item ID"),
|
|
1032
|
+
}),
|
|
1033
|
+
annotations: { readOnlyHint: true },
|
|
1034
|
+
},
|
|
1035
|
+
async (args, extra) => {
|
|
1036
|
+
requireScope(extra, "media:read");
|
|
1037
|
+
const ec = getEmDash(extra);
|
|
1038
|
+
return unwrap(await ec.handleMediaGet(args.id));
|
|
1039
|
+
},
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
server.registerTool(
|
|
1043
|
+
"media_update",
|
|
1044
|
+
{
|
|
1045
|
+
title: "Update Media Metadata",
|
|
1046
|
+
description:
|
|
1047
|
+
"Update the metadata of an uploaded media file. You can change the " +
|
|
1048
|
+
"alt text, caption, and dimensions. The file itself cannot be changed.",
|
|
1049
|
+
inputSchema: z.object({
|
|
1050
|
+
id: z.string().describe("Media item ID"),
|
|
1051
|
+
alt: z.string().optional().describe("Alt text for accessibility"),
|
|
1052
|
+
caption: z.string().optional().describe("Caption text"),
|
|
1053
|
+
width: z.number().int().optional().describe("Image width in pixels"),
|
|
1054
|
+
height: z.number().int().optional().describe("Image height in pixels"),
|
|
1055
|
+
}),
|
|
1056
|
+
},
|
|
1057
|
+
async (args, extra) => {
|
|
1058
|
+
requireScope(extra, "media:write");
|
|
1059
|
+
requireRole(extra, Role.AUTHOR);
|
|
1060
|
+
const ec = getEmDash(extra);
|
|
1061
|
+
|
|
1062
|
+
// Fetch media item for ownership check
|
|
1063
|
+
const existing = await ec.handleMediaGet(args.id);
|
|
1064
|
+
if (!existing.success) {
|
|
1065
|
+
return unwrap(existing);
|
|
1066
|
+
}
|
|
1067
|
+
const media = (existing.data as Record<string, unknown> | undefined)?.item as
|
|
1068
|
+
| Record<string, unknown>
|
|
1069
|
+
| undefined;
|
|
1070
|
+
const authorId = typeof media?.authorId === "string" ? media.authorId : "";
|
|
1071
|
+
requireOwnership(extra, authorId, "media:edit_own", "media:edit_any");
|
|
1072
|
+
|
|
1073
|
+
return unwrap(
|
|
1074
|
+
await ec.handleMediaUpdate(args.id, {
|
|
1075
|
+
alt: args.alt,
|
|
1076
|
+
caption: args.caption,
|
|
1077
|
+
width: args.width,
|
|
1078
|
+
height: args.height,
|
|
1079
|
+
}),
|
|
1080
|
+
);
|
|
1081
|
+
},
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
server.registerTool(
|
|
1085
|
+
"media_delete",
|
|
1086
|
+
{
|
|
1087
|
+
title: "Delete Media",
|
|
1088
|
+
description:
|
|
1089
|
+
"Permanently delete an uploaded media file. Removes the database record " +
|
|
1090
|
+
"and the file from storage. Content referencing this media will have " +
|
|
1091
|
+
"broken references. Cannot be undone.",
|
|
1092
|
+
inputSchema: z.object({
|
|
1093
|
+
id: z.string().describe("Media item ID"),
|
|
1094
|
+
}),
|
|
1095
|
+
annotations: { destructiveHint: true },
|
|
1096
|
+
},
|
|
1097
|
+
async (args, extra) => {
|
|
1098
|
+
requireScope(extra, "media:write");
|
|
1099
|
+
requireRole(extra, Role.AUTHOR);
|
|
1100
|
+
const ec = getEmDash(extra);
|
|
1101
|
+
|
|
1102
|
+
// Fetch media item for ownership check
|
|
1103
|
+
const existing = await ec.handleMediaGet(args.id);
|
|
1104
|
+
if (!existing.success) {
|
|
1105
|
+
return unwrap(existing);
|
|
1106
|
+
}
|
|
1107
|
+
const media = (existing.data as Record<string, unknown> | undefined)?.item as
|
|
1108
|
+
| Record<string, unknown>
|
|
1109
|
+
| undefined;
|
|
1110
|
+
const authorId = typeof media?.authorId === "string" ? media.authorId : "";
|
|
1111
|
+
requireOwnership(extra, authorId, "media:delete_own", "media:delete_any");
|
|
1112
|
+
|
|
1113
|
+
return unwrap(await ec.handleMediaDelete(args.id));
|
|
1114
|
+
},
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
// =====================================================================
|
|
1118
|
+
// Search tool
|
|
1119
|
+
// =====================================================================
|
|
1120
|
+
|
|
1121
|
+
server.registerTool(
|
|
1122
|
+
"search",
|
|
1123
|
+
{
|
|
1124
|
+
title: "Search Content",
|
|
1125
|
+
description:
|
|
1126
|
+
"Full-text search across content collections. Searches indexed fields " +
|
|
1127
|
+
"for matching content. Collections must have 'search' in their supports " +
|
|
1128
|
+
"list and fields must be marked as searchable. Returns collection, item " +
|
|
1129
|
+
"ID, title, excerpt, and relevance score.",
|
|
1130
|
+
inputSchema: z.object({
|
|
1131
|
+
query: z.string().describe("Search query text"),
|
|
1132
|
+
collections: z
|
|
1133
|
+
.array(z.string())
|
|
1134
|
+
.optional()
|
|
1135
|
+
.describe("Limit search to specific collection slugs (all if omitted)"),
|
|
1136
|
+
locale: z
|
|
1137
|
+
.string()
|
|
1138
|
+
.optional()
|
|
1139
|
+
.describe("Filter results by locale (omit to search all locales)"),
|
|
1140
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max results (default 20)"),
|
|
1141
|
+
}),
|
|
1142
|
+
annotations: { readOnlyHint: true },
|
|
1143
|
+
},
|
|
1144
|
+
async (args, extra) => {
|
|
1145
|
+
requireScope(extra, "content:read");
|
|
1146
|
+
const ec = getEmDash(extra);
|
|
1147
|
+
try {
|
|
1148
|
+
const { searchWithDb } = await import("../search/index.js");
|
|
1149
|
+
const results = await searchWithDb(ec.db, args.query, {
|
|
1150
|
+
collections: args.collections,
|
|
1151
|
+
locale: args.locale,
|
|
1152
|
+
limit: args.limit,
|
|
1153
|
+
});
|
|
1154
|
+
return jsonResult(results);
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
return errorResult(error);
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
// =====================================================================
|
|
1162
|
+
// Taxonomy tools
|
|
1163
|
+
// =====================================================================
|
|
1164
|
+
|
|
1165
|
+
server.registerTool(
|
|
1166
|
+
"taxonomy_list",
|
|
1167
|
+
{
|
|
1168
|
+
title: "List Taxonomies",
|
|
1169
|
+
description:
|
|
1170
|
+
"List all taxonomy definitions (e.g. categories, tags). Taxonomies are " +
|
|
1171
|
+
"classification systems applied to content. Each has a name, label, and " +
|
|
1172
|
+
"can be hierarchical (categories) or flat (tags).",
|
|
1173
|
+
inputSchema: z.object({}),
|
|
1174
|
+
annotations: { readOnlyHint: true },
|
|
1175
|
+
},
|
|
1176
|
+
async (_args, extra) => {
|
|
1177
|
+
requireScope(extra, "content:read");
|
|
1178
|
+
const ec = getEmDash(extra);
|
|
1179
|
+
try {
|
|
1180
|
+
const rows = (await ec.db
|
|
1181
|
+
.selectFrom("_emdash_taxonomy_defs" as never)
|
|
1182
|
+
.selectAll()
|
|
1183
|
+
.execute()) as Array<{
|
|
1184
|
+
id: string;
|
|
1185
|
+
name: string;
|
|
1186
|
+
label: string;
|
|
1187
|
+
label_singular: string | null;
|
|
1188
|
+
hierarchical: number;
|
|
1189
|
+
collections: string | null;
|
|
1190
|
+
}>;
|
|
1191
|
+
const taxonomies = rows.map((row) => ({
|
|
1192
|
+
id: row.id,
|
|
1193
|
+
name: row.name,
|
|
1194
|
+
label: row.label,
|
|
1195
|
+
labelSingular: row.label_singular ?? undefined,
|
|
1196
|
+
hierarchical: row.hierarchical === 1,
|
|
1197
|
+
collections: row.collections ? JSON.parse(row.collections) : [],
|
|
1198
|
+
}));
|
|
1199
|
+
return jsonResult(taxonomies);
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
return errorResult(error);
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
server.registerTool(
|
|
1207
|
+
"taxonomy_list_terms",
|
|
1208
|
+
{
|
|
1209
|
+
title: "List Taxonomy Terms",
|
|
1210
|
+
description:
|
|
1211
|
+
"List terms in a taxonomy with pagination. Terms are individual entries " +
|
|
1212
|
+
"(e.g. specific categories or tags). Hierarchical taxonomies can have " +
|
|
1213
|
+
"parent-child relationships.",
|
|
1214
|
+
inputSchema: z.object({
|
|
1215
|
+
taxonomy: z.string().describe("Taxonomy name (e.g. 'categories', 'tags')"),
|
|
1216
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max items (default 50)"),
|
|
1217
|
+
cursor: z.string().optional().describe("Pagination cursor"),
|
|
1218
|
+
}),
|
|
1219
|
+
annotations: { readOnlyHint: true },
|
|
1220
|
+
},
|
|
1221
|
+
async (args, extra) => {
|
|
1222
|
+
requireScope(extra, "content:read");
|
|
1223
|
+
const ec = getEmDash(extra);
|
|
1224
|
+
try {
|
|
1225
|
+
const taxonomy = (await ec.db
|
|
1226
|
+
.selectFrom("_emdash_taxonomy_defs" as never)
|
|
1227
|
+
.select("id" as never)
|
|
1228
|
+
.where("name" as never, "=", args.taxonomy as never)
|
|
1229
|
+
.executeTakeFirst()) as { id: string } | undefined;
|
|
1230
|
+
|
|
1231
|
+
if (!taxonomy) return errorResult(`Taxonomy '${args.taxonomy}' not found`);
|
|
1232
|
+
|
|
1233
|
+
const limit = Math.min(args.limit ?? 50, 100);
|
|
1234
|
+
let query = ec.db
|
|
1235
|
+
.selectFrom("_emdash_taxonomy_terms" as never)
|
|
1236
|
+
.selectAll()
|
|
1237
|
+
.where("taxonomy_id" as never, "=", taxonomy.id as never)
|
|
1238
|
+
.orderBy("label" as never, "asc")
|
|
1239
|
+
.limit(limit + 1);
|
|
1240
|
+
|
|
1241
|
+
if (args.cursor) {
|
|
1242
|
+
query = query.where("id" as never, ">" as never, args.cursor as never);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const rows = (await query.execute()) as Array<{ id: string }>;
|
|
1246
|
+
const hasMore = rows.length > limit;
|
|
1247
|
+
const items = hasMore ? rows.slice(0, limit) : rows;
|
|
1248
|
+
const nextCursor = hasMore ? items.at(-1)?.id : undefined;
|
|
1249
|
+
|
|
1250
|
+
return jsonResult({ items, nextCursor });
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
return errorResult(error);
|
|
1253
|
+
}
|
|
1254
|
+
},
|
|
1255
|
+
);
|
|
1256
|
+
|
|
1257
|
+
server.registerTool(
|
|
1258
|
+
"taxonomy_create_term",
|
|
1259
|
+
{
|
|
1260
|
+
title: "Create Taxonomy Term",
|
|
1261
|
+
description:
|
|
1262
|
+
"Create a new term in a taxonomy. For hierarchical taxonomies like " +
|
|
1263
|
+
"categories, you can specify a parentId to create a child term.",
|
|
1264
|
+
inputSchema: z.object({
|
|
1265
|
+
taxonomy: z.string().describe("Taxonomy name (e.g. 'categories', 'tags')"),
|
|
1266
|
+
slug: z.string().describe("URL-safe identifier for the term"),
|
|
1267
|
+
label: z.string().describe("Display name"),
|
|
1268
|
+
parentId: z.string().optional().describe("Parent term ID for hierarchical taxonomies"),
|
|
1269
|
+
description: z.string().optional().describe("Description of the term"),
|
|
1270
|
+
}),
|
|
1271
|
+
},
|
|
1272
|
+
async (args, extra) => {
|
|
1273
|
+
requireScope(extra, "content:write");
|
|
1274
|
+
requireRole(extra, Role.EDITOR);
|
|
1275
|
+
const ec = getEmDash(extra);
|
|
1276
|
+
try {
|
|
1277
|
+
const { ulid } = await import("ulidx");
|
|
1278
|
+
|
|
1279
|
+
const taxonomy = (await ec.db
|
|
1280
|
+
.selectFrom("_emdash_taxonomy_defs" as never)
|
|
1281
|
+
.select("id" as never)
|
|
1282
|
+
.where("name" as never, "=", args.taxonomy as never)
|
|
1283
|
+
.executeTakeFirst()) as { id: string } | undefined;
|
|
1284
|
+
|
|
1285
|
+
if (!taxonomy) return errorResult(`Taxonomy '${args.taxonomy}' not found`);
|
|
1286
|
+
|
|
1287
|
+
const id = ulid();
|
|
1288
|
+
await ec.db
|
|
1289
|
+
.insertInto("_emdash_taxonomy_terms" as never)
|
|
1290
|
+
.values({
|
|
1291
|
+
id,
|
|
1292
|
+
taxonomy_id: taxonomy.id,
|
|
1293
|
+
slug: args.slug,
|
|
1294
|
+
label: args.label,
|
|
1295
|
+
parent_id: args.parentId ?? null,
|
|
1296
|
+
description: args.description ?? null,
|
|
1297
|
+
} as never)
|
|
1298
|
+
.execute();
|
|
1299
|
+
|
|
1300
|
+
const term = await ec.db
|
|
1301
|
+
.selectFrom("_emdash_taxonomy_terms" as never)
|
|
1302
|
+
.selectAll()
|
|
1303
|
+
.where("id" as never, "=", id as never)
|
|
1304
|
+
.executeTakeFirstOrThrow();
|
|
1305
|
+
|
|
1306
|
+
return jsonResult(term);
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
return errorResult(error);
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
// =====================================================================
|
|
1314
|
+
// Menu tools
|
|
1315
|
+
// =====================================================================
|
|
1316
|
+
|
|
1317
|
+
server.registerTool(
|
|
1318
|
+
"menu_list",
|
|
1319
|
+
{
|
|
1320
|
+
title: "List Menus",
|
|
1321
|
+
description:
|
|
1322
|
+
"List all navigation menus defined in the CMS. Menus are named " +
|
|
1323
|
+
"navigation structures (e.g. 'main', 'footer') containing ordered " +
|
|
1324
|
+
"items with labels, URLs, and optional nesting.",
|
|
1325
|
+
inputSchema: z.object({}),
|
|
1326
|
+
annotations: { readOnlyHint: true },
|
|
1327
|
+
},
|
|
1328
|
+
async (_args, extra) => {
|
|
1329
|
+
requireScope(extra, "content:read");
|
|
1330
|
+
const ec = getEmDash(extra);
|
|
1331
|
+
try {
|
|
1332
|
+
const menus = await ec.db
|
|
1333
|
+
.selectFrom("_emdash_menus" as never)
|
|
1334
|
+
.select([
|
|
1335
|
+
"id" as never,
|
|
1336
|
+
"name" as never,
|
|
1337
|
+
"label" as never,
|
|
1338
|
+
"created_at" as never,
|
|
1339
|
+
"updated_at" as never,
|
|
1340
|
+
])
|
|
1341
|
+
.orderBy("name" as never, "asc")
|
|
1342
|
+
.execute();
|
|
1343
|
+
return jsonResult(menus);
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
return errorResult(error);
|
|
1346
|
+
}
|
|
1347
|
+
},
|
|
1348
|
+
);
|
|
1349
|
+
|
|
1350
|
+
server.registerTool(
|
|
1351
|
+
"menu_get",
|
|
1352
|
+
{
|
|
1353
|
+
title: "Get Menu with Items",
|
|
1354
|
+
description:
|
|
1355
|
+
"Get a menu by name including all its items in order. Items have a " +
|
|
1356
|
+
"label, URL, type (custom/content/collection), and optional parent " +
|
|
1357
|
+
"for nesting.",
|
|
1358
|
+
inputSchema: z.object({
|
|
1359
|
+
name: z.string().describe("Menu name (e.g. 'main', 'footer')"),
|
|
1360
|
+
}),
|
|
1361
|
+
annotations: { readOnlyHint: true },
|
|
1362
|
+
},
|
|
1363
|
+
async (args, extra) => {
|
|
1364
|
+
requireScope(extra, "content:read");
|
|
1365
|
+
const ec = getEmDash(extra);
|
|
1366
|
+
try {
|
|
1367
|
+
const menu = (await ec.db
|
|
1368
|
+
.selectFrom("_emdash_menus" as never)
|
|
1369
|
+
.selectAll()
|
|
1370
|
+
.where("name" as never, "=", args.name as never)
|
|
1371
|
+
.executeTakeFirst()) as { id: string } | undefined;
|
|
1372
|
+
|
|
1373
|
+
if (!menu) return errorResult(`Menu '${args.name}' not found`);
|
|
1374
|
+
|
|
1375
|
+
const items = await ec.db
|
|
1376
|
+
.selectFrom("_emdash_menu_items" as never)
|
|
1377
|
+
.selectAll()
|
|
1378
|
+
.where("menu_id" as never, "=", menu.id as never)
|
|
1379
|
+
.orderBy("sort_order" as never, "asc")
|
|
1380
|
+
.execute();
|
|
1381
|
+
|
|
1382
|
+
return jsonResult({ ...menu, items });
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
return errorResult(error);
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
);
|
|
1388
|
+
|
|
1389
|
+
// =====================================================================
|
|
1390
|
+
// Revision tools
|
|
1391
|
+
// =====================================================================
|
|
1392
|
+
|
|
1393
|
+
server.registerTool(
|
|
1394
|
+
"revision_list",
|
|
1395
|
+
{
|
|
1396
|
+
title: "List Revisions",
|
|
1397
|
+
description:
|
|
1398
|
+
"List revision history for a content item. Revisions are snapshots " +
|
|
1399
|
+
"created on publish or update. Returns newest-first. Requires the " +
|
|
1400
|
+
"collection to support 'revisions'.",
|
|
1401
|
+
inputSchema: z.object({
|
|
1402
|
+
collection: z.string().describe("Collection slug"),
|
|
1403
|
+
id: z.string().describe("Content item ID or slug"),
|
|
1404
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max revisions (default 20)"),
|
|
1405
|
+
}),
|
|
1406
|
+
annotations: { readOnlyHint: true },
|
|
1407
|
+
},
|
|
1408
|
+
async (args, extra) => {
|
|
1409
|
+
requireScope(extra, "content:read");
|
|
1410
|
+
const ec = getEmDash(extra);
|
|
1411
|
+
return unwrap(
|
|
1412
|
+
await ec.handleRevisionList(args.collection, args.id, {
|
|
1413
|
+
limit: args.limit,
|
|
1414
|
+
}),
|
|
1415
|
+
);
|
|
1416
|
+
},
|
|
1417
|
+
);
|
|
1418
|
+
|
|
1419
|
+
server.registerTool(
|
|
1420
|
+
"revision_restore",
|
|
1421
|
+
{
|
|
1422
|
+
title: "Restore Revision",
|
|
1423
|
+
description:
|
|
1424
|
+
"Restore a content item to a previous revision. Replaces the current " +
|
|
1425
|
+
"draft with the specified revision's data. Not automatically published — " +
|
|
1426
|
+
"use content_publish afterward if needed.",
|
|
1427
|
+
inputSchema: z.object({
|
|
1428
|
+
revisionId: z.string().describe("Revision ID to restore"),
|
|
1429
|
+
}),
|
|
1430
|
+
},
|
|
1431
|
+
async (args, extra) => {
|
|
1432
|
+
requireScope(extra, "content:write");
|
|
1433
|
+
requireRole(extra, Role.AUTHOR);
|
|
1434
|
+
const { emdash, userId } = getExtra(extra);
|
|
1435
|
+
|
|
1436
|
+
// Fetch the revision to discover the parent content entry
|
|
1437
|
+
const revision = await emdash.handleRevisionGet(args.revisionId);
|
|
1438
|
+
if (!revision.success) {
|
|
1439
|
+
return unwrap(revision);
|
|
1440
|
+
}
|
|
1441
|
+
const revItem = revision.data?.item;
|
|
1442
|
+
if (!revItem?.collection || !revItem?.entryId) {
|
|
1443
|
+
return errorResult("Revision is missing collection or entry reference");
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// Fetch the content entry to check ownership
|
|
1447
|
+
const existing = await emdash.handleContentGet(revItem.collection, revItem.entryId);
|
|
1448
|
+
if (!existing.success) {
|
|
1449
|
+
return unwrap(existing);
|
|
1450
|
+
}
|
|
1451
|
+
requireOwnership(
|
|
1452
|
+
extra,
|
|
1453
|
+
extractContentAuthorId(existing.data),
|
|
1454
|
+
"content:edit_own",
|
|
1455
|
+
"content:edit_any",
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
return unwrap(await emdash.handleRevisionRestore(args.revisionId, userId));
|
|
1459
|
+
},
|
|
1460
|
+
);
|
|
1461
|
+
|
|
1462
|
+
return server;
|
|
1463
|
+
}
|