Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
147 commits
Select commit Hold shift + click to select a range
f86fb60
docs(mcp-server): document binary-response envelope in execute tool
fmontes Jun 19, 2026
eb2ac73
docs(a11y-agent): add phased session plan + S0 spike findings
fmontes Jun 19, 2026
0f552d5
feat(dotcms-agents): scaffold Nx Node app (Hono) for agent capabilities
fmontes Jun 19, 2026
a373801
feat(dotcms-agents): lock a11y-fix contract (zod schemas + tests)
fmontes Jun 19, 2026
89fcf4b
feat(dotcms-agents): path-allowlist adapter wrapper (safety boundary …
fmontes Jun 19, 2026
c4db65e
feat(dotcms-agents): the a11y-fix loop (deterministic skeleton, §5)
fmontes Jun 19, 2026
44f4e70
feat(dotcms-agents): HTTP layer — POST /a11y/fix + GET /a11y/active-run
fmontes Jun 19, 2026
f247900
fix(dotcms-agents): drop refuse-if-dirty; reach theme CSS/JS sources
fmontes Jun 19, 2026
3f4e1f8
perf(dotcms-agents): default to Sonnet + cache triage file context
fmontes Jun 19, 2026
1c3fb9b
feat(dotcms-agents): optional LLM usage sink for cost measurement
fmontes Jun 19, 2026
fd93b91
perf(dotcms-agents): don't send JavaScript to the agent
fmontes Jun 19, 2026
a12f4d9
feat(page): return ALL theme files in /_render-sources, not a type wh…
fmontes Jun 19, 2026
b5e2272
docs(a11y-agent): add S1.5 CSS attribution (deterministic, spike-vali…
fmontes Jun 19, 2026
b3abb0d
feat(dotcms-agents): add OpenRouter as a selectable LLM provider
fmontes Jun 19, 2026
51b5930
feat(dotcms-agents): default to OpenRouter + moonshotai/kimi-k2.7-code
fmontes Jun 19, 2026
8c250e7
fix(dotcms-agents): only send Anthropic cacheControl on the Anthropic…
fmontes Jun 19, 2026
2702547
docs(a11y-agent): lock lazy per-violation source reading (S1.5)
fmontes Jun 19, 2026
71114d7
docs(a11y-agent): close stylesheet-discovery decision; full lazy chai…
fmontes Jun 19, 2026
31106ef
feat(a11y-studio): add Accessibility Studio portlet (S3 MVP)
fmontes Jun 19, 2026
fa1de8b
feat(dotcms-agents): css-attribution + css-source-map modules (S1.5 f…
fmontes Jun 19, 2026
199c064
feat(dotcms-agents): wire lazy CSS-attribution path into the loop (S1.5)
fmontes Jun 19, 2026
8fd63f5
feat(dotcms-agents): batch CSS re-scans + cap LLM output (S1.5 valida…
fmontes Jun 19, 2026
7f7bf25
fix(dotcms-agents): exclude state-pseudo rules from CSS attribution
fmontes Jun 19, 2026
0d83931
feat(dotcms-agents): adapt to raw-axe scanner response (normalize in …
fmontes Jun 19, 2026
aa07699
feat(dotcms-agents): deterministic contrast fix from axe data (no LLM)
fmontes Jun 19, 2026
6f321b6
fix(dotcms-agents): attribute contrast by axe's flagged color, not sp…
fmontes Jun 19, 2026
89b3796
feat(a11y-studio): render preview iframe via /dot-page proxy in EDIT_…
fmontes Jun 19, 2026
bc08ac5
feat(a11y-studio): consume raw axe-core scanner response in a11y report
fmontes Jun 19, 2026
50bc6ca
fix(dotcms-agents): try both sides of the contrast pair (white-text f…
fmontes Jun 19, 2026
13a45bc
fix(dotcms-agents): default to GLM-5.2 + retry transient empty LLM ou…
fmontes Jun 19, 2026
215e2fe
feat(dotcms-agents): hybrid loop — deterministic PASS 1 + agentic res…
fmontes Jun 20, 2026
0aa4911
perf(dotcms-agents): filter EDIT_MODE chrome + tighten PASS 2 prompt
fmontes Jun 20, 2026
0dab40a
fix(dotcms-agents): only filter editor chrome, treat data-dot as attr…
fmontes Jun 20, 2026
c060a85
refactor(dotcms-agents): drop Output.object — tool-calling only, no f…
fmontes Jun 20, 2026
b60e8e1
refactor(dotcms-agents): /simplify cleanup — dedup helpers, cache, de…
fmontes Jun 20, 2026
83ca684
feat(a11y-studio): run a real axe scan and feed the score widget
fmontes Jun 20, 2026
d7465a2
feat(a11y-studio): draw violation markers inside the preview iframe
fmontes Jun 20, 2026
35c2b18
chore(a11y-studio): DEV-ONLY rewrite scan URL :4200 → :8080
fmontes Jun 20, 2026
bf0122d
fix(dotcms-agents): host-scope the stylesheet fetch (non-default sites)
fmontes Jun 20, 2026
8c66b7a
revert(dotcms-agents): don't re-append host_id in fetchStylesheet
fmontes Jun 20, 2026
0198e49
feat(dotcms-agents): honor scanner renderReliable — abort on broken r…
fmontes Jun 20, 2026
4bb91c3
feat(dotcms-agents): scan in PREVIEW_MODE (working content, no editor…
fmontes Jun 20, 2026
c6d8d9a
feat(a11y-studio): live agent stream, preview modes, and dev/prod fixes
fmontes Jun 20, 2026
79e62ad
refactor(dotcms-agents): restructure src into api/domain/dotcms/fix/s…
fmontes Jun 21, 2026
595ecc8
refactor(dotcms-agents): depend on a DotcmsGateway port, not the conc…
fmontes Jun 21, 2026
7e5f1c5
refactor(dotcms-agents): split run-fix into orchestrator + CSS engine…
fmontes Jun 21, 2026
e491199
refactor(dotcms-agents): extract request-parser and research prompt
fmontes Jun 21, 2026
b0d33f7
feat(agent-contracts): scaffold @dotcms/agent-contracts shared lib
fmontes Jun 21, 2026
685ff9b
refactor(dotcms-agents): move a11y contract into @dotcms/agent-contracts
fmontes Jun 21, 2026
ed3e173
refactor(dot-accessibility-studio): consume @dotcms/agent-contracts (…
fmontes Jun 21, 2026
8f8a4ed
fix(dotcms-agents): eliminate implicit any + enable noImplicitAny
fmontes Jun 21, 2026
56aa313
refactor(agents-contract): rename lib folder/project dotcms-agents-co…
fmontes Jun 21, 2026
70b5adc
refactor(ai-agents): rename app dotcms-agents → ai-agents (+ URL path)
fmontes Jun 21, 2026
d531560
feat(ai-agents): autoload repo .env so `nx serve ai-agents` just works
fmontes Jun 21, 2026
119355a
feat(ai-agents): add Stop endpoint (cooperative run cancellation)
fmontes Jun 21, 2026
5b98fa8
feat(agent-contracts): report changedFiles on every run (done or stop…
fmontes Jun 22, 2026
264c235
feat(a11y-studio): rebuild the left panel to the new design (5 states)
fmontes Jun 22, 2026
32bcd7a
feat(a11y-studio): surface needs-review (incomplete) count in the sco…
fmontes Jun 22, 2026
ed6143c
fix(a11y-studio): make the re-scan button work (allow runScan from sc…
fmontes Jun 22, 2026
7fc6588
fix(a11y-studio): phase-aware marker overlay (not just LIVE-only)
fmontes Jun 22, 2026
dca4b46
feat(a11y-studio): explain axe 'incomplete' with a Needs-your-review …
fmontes Jun 22, 2026
3431cdf
fix(a11y-studio): don't load the page picker until the current site i…
fmontes Jun 22, 2026
eafa4ae
refactor(a11y-agent): migrate from @dotcms/agentic-tools to @dotcms/ai
fmontes Jun 25, 2026
330bc9d
fix(@dotcms/ai): wire package.json exports to compiled dist outputs
fmontes Jun 25, 2026
d3579a3
fix(a11y-agent): point dev proxy and server default port to localhost…
fmontes Jun 25, 2026
4429323
fix(dev-proxy): rewrite /ai-agents/a11y to /agent/a11y for combined s…
fmontes Jun 25, 2026
f3174a2
refactor: remove apps/ai-agents — agent code now lives in the scanner…
fmontes Jun 25, 2026
fb7d978
feat(a11y-agent): add Java proxy resource for streaming agent SSE (S4)
fmontes Jun 26, 2026
3a3bf92
chore: remove unused agents-contract library
fmontes Jun 26, 2026
5fe3888
fix(a11y-studio): inline agent contract types after removing agents-c…
fmontes Jun 26, 2026
289c9d8
refactor(a11y-studio): simplify picker and run components
fmontes Jun 26, 2026
33706d0
docs(rest): add typed @Schema descriptions to write-endpoint request …
fmontes Jun 26, 2026
5c3b042
fix(rest): turn write-endpoint 500s into clean 400s
fmontes Jun 26, 2026
b0a7506
feat(mcp-server): add page_create tool
fmontes Jun 27, 2026
365918f
fix(mcp-server): upload empty files as-is instead of skipping
fmontes Jun 28, 2026
cd9cab9
feat(mcp-server): raise execute tool sandbox timeout default to 45s
fmontes Jul 17, 2026
e5e2c6a
refactor(ai-ui): extract agent-agnostic message/streaming kernel
fmontes Jul 17, 2026
73208be
feat(mcp-server): keep request/response schemas in generated spec wit…
fmontes Jul 17, 2026
d42f1fc
fix(mcp-server): harden authoring tools against recurring failures
fmontes Jul 18, 2026
1656901
docs(rest): fix OpenAPI annotations for common authoring failures
fmontes Jul 18, 2026
3f9c4bf
test: migrate branch specs to @openng/spectator after upstream rename
fmontes Jul 20, 2026
1e1943c
test(page): update PageResourceTest for new host_id param on render/l…
fmontes Jul 20, 2026
93afadd
fix(sdk-ai): don't break build when OpenAPI spec source is unreachable
github-actions[bot] Jul 20, 2026
611c311
Point OpenAPI spec generation at the new headless environment
fmontes Jul 20, 2026
4423656
Reorder imports alphabetically in ai-evals tools.ts
fmontes Jul 20, 2026
802f894
build(ai-sdk): generate spec from local openapi.yaml only
fmontes Jul 20, 2026
b16f903
docs(openapi): document ContainerUUID.identifier accepted formats
fmontes Jul 20, 2026
2061827
docs(openapi): clarify empty system-actions list is not a blocker
fmontes Jul 20, 2026
f99c20a
feat(contenttype): accept short base-type names for the clazz discrim…
fmontes Jul 20, 2026
a714776
feat(vtl): surface structured Velocity errors and warnings from /api/…
fmontes Jul 20, 2026
82638ec
feat(mcp-server): add page_place_content tool
fmontes Jul 20, 2026
9ae1420
fix(mcp-server): import DotCMSRuntime type directly, not via ReturnType
fmontes Jul 21, 2026
1fd9321
fix(mcp-server): import DotCMSRuntime type directly in remaining tools
fmontes Jul 21, 2026
02d17b9
remove plan
fmontes Jul 21, 2026
93ba456
fix launch
fmontes Jul 21, 2026
bbdad3b
refactor(mcp-server): rename place-content lib to page-place-content
fmontes Jul 21, 2026
682f584
refactor(mcp-server): prefix PlaceContent models with Page; fix stray…
fmontes Jul 21, 2026
52bdbbb
feat(mcp-server): add page_verify render-verification tool
fmontes Jul 21, 2026
f93cdbc
fix(mcp-server): page_create root page on non-default site (Host.getI…
fmontes Jul 21, 2026
4c48b07
fix(mcp-server): upload_assets include-glob matches nothing (brace/gl…
fmontes Jul 22, 2026
9cfa5bd
feat(a11y-agent): agents shell + run-id stop, stream polish, done-rep…
fmontes Jul 23, 2026
02ddb5b
feat(rest): accept short field-type clazz names + OpenAPI authoring f…
fmontes Jul 23, 2026
027773b
feat(a11y-agent): heartbeat-driven "working" bubble in the activity log
fmontes Jul 23, 2026
f25f579
feat(a11y-agent): live fixing feedback — thinking indicator, live sco…
fmontes Jul 30, 2026
87173c5
feat(a11y-agent): working-vs-live source file diff view
fmontes Jul 30, 2026
f524dc7
feat(a11y-agent): show containing folder path in diff file list
fmontes Jul 30, 2026
43961e5
feat(a11y-agent): diff file list shows only name + line counts
fmontes Jul 30, 2026
e3b5b0f
fix(a11y-agent): move agent routes from /agent/a11y to /agents/a11y
fmontes Aug 2, 2026
bf050ab
refactor(a11y-agent): show file diff in a PrimeNG drawer, not a route
fmontes Aug 3, 2026
e1b51d5
fix(a11y-agent): keep diff drawer host out of the run screen grid
fmontes Aug 3, 2026
e117304
fix(a11y-agent): use position:fixed, not display:contents, for diff host
fmontes Aug 3, 2026
71ff98b
fix(a11y-agent): show View file changes on any completed run
fmontes Aug 3, 2026
b2d0617
fix(a11y-agent): gate the whole p-drawer on open() to kill lingering …
fmontes Aug 3, 2026
a098405
fix(a11y-agent): append diff drawer to body so its mask sits below co…
fmontes Aug 3, 2026
b2de03a
feat(a11y-agent): widen diff drawer to 80vw for code comparison
fmontes Aug 3, 2026
261e796
feat(a11y-agent): inline code diff with Preview/Code tabs, replacing …
fmontes Aug 3, 2026
8080d70
feat(a11y-agent): Code tab always enabled, not just after a run
fmontes Aug 3, 2026
1b40e40
feat(a11y-agent): enforce code review before publishing
fmontes Aug 3, 2026
0fecbf9
feat(a11y-agent): send hostId at top level, relay agent errors verbatim
fmontes Aug 3, 2026
9315126
feat(a11y-agent): changed-files accordion in side panel, diff in prev…
fmontes Aug 3, 2026
c655426
feat(a11y-agent): split side panel into scanner + files accordion panels
fmontes Aug 3, 2026
e72b789
refactor(a11y-agent): use PrimeNG accordion, move discard beside publish
fmontes Aug 3, 2026
f0de57e
feat(a11y-agent): publish/discard whenever files changed, not just af…
fmontes Aug 3, 2026
a0d0a48
refactor(a11y-agent): memoize axe derivations, drop template method c…
fmontes Aug 3, 2026
cd54ebd
refactor(a11y-agent): slim the studio store's public surface
fmontes Aug 4, 2026
2899213
refactor(a11y-agent): split the studio store into picker + run stores
fmontes Aug 4, 2026
445c2a7
fix(a11y-agent): wrap long needs-review text instead of truncating
fmontes Aug 4, 2026
5d956a6
fix(a11y-agent): reload the run screen when the site selector changes
fmontes Aug 4, 2026
a052f2d
style(a11y-agent): align side-panel accordion with PrimeNG defaults
fmontes Aug 4, 2026
15c1439
refactor(a11y-agent): move skip-CSS to the fix step, default the pane…
fmontes Aug 4, 2026
1789765
fix(a11y-agent): scroll the accordion CONTENT, cap the files panel at…
fmontes Aug 4, 2026
8ed65ba
fix(a11y-agent): add a divider between the scanner and files panels
fmontes Aug 4, 2026
ec8925a
refactor(a11y-agent): default the remaining side-panel fonts
fmontes Aug 4, 2026
d92395f
fix(a11y-agent): stop the page scrolling when the files panel is coll…
fmontes Aug 4, 2026
e4d93de
Fix format
fmontes Aug 4, 2026
2c5b061
refactor(a11y-agent): extract diff-viewer template to html file
fmontes Aug 4, 2026
46715b5
delete plans
fmontes Aug 4, 2026
e639ba0
Merge branch 'main' of https://github.com/dotCMS/core into fmontes/do…
fmontes Aug 5, 2026
9996d84
Merge branch 'fmontes/dotcms-openapi-authoring-fixes' of https://gith…
fmontes Aug 5, 2026
ca7917d
fix(a11y-agent): treat reported rows as pass-1 handoff, derive counts…
fmontes Aug 5, 2026
88a6d60
docs(architecture): seed primitives.yaml for the AI agent platform
fmontes Aug 5, 2026
a2c6bfc
Merge branch 'main' of https://github.com/dotCMS/core into fmontes/do…
fmontes Aug 5, 2026
043af7b
Fix format
fmontes Aug 6, 2026
ed0702a
refactor(a11y-agent): address PR review feedback
fmontes Aug 6, 2026
b5f031c
docs(a11y-agent): document the /dot-page proxy as a backend gap
fmontes Aug 6, 2026
e02235f
Fix format
fmontes Aug 6, 2026
b142e43
fix(a11y-agent): prefix upstream Page Scanner calls with /v1
fmontes Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ dist-docs/

#env
/core-web/**/.env
.env
.env.*
!.env.example

/**/**/*.css.map

Expand Down Expand Up @@ -218,6 +221,9 @@ dist/
*.egg
.venv/

# S0 spike throwaway
/core-web/scratch/

# Spec-Kit working artifacts — process-only, kept local (see .specify/CUSTOMIZATIONS.md).
# spec.md (and data-model.md / contracts/ when they carry verified contracts) stay tracked.
specs/*/plan.md
Expand Down
5 changes: 3 additions & 2 deletions core-web/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.env
/libs/agentic-tools/src/generated

vite.config.*.timestamp*
vitest.config.*.timestamp*
vitest.config.*.timestamp*

.angular
15 changes: 4 additions & 11 deletions core-web/apps/ai-evals/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,13 @@ import { tool } from 'ai';
import { z } from 'zod/v4';

import { createApiAdapter } from '@dotcms/ai/adapter';
import { createExecutor } from '@dotcms/ai/sandbox';
import { createExecutor, formatSandboxResult } from '@dotcms/ai/sandbox';
import { getSpec } from '@dotcms/ai/spec';

function sandboxResult(
result: Awaited<ReturnType<ReturnType<typeof createExecutor>['execute']>>
): string {
if (!result.success) return `Error: ${result.error?.name}: ${result.error?.message}`;
return typeof result.value === 'string' ? result.value : JSON.stringify(result.value, null, 2);
}

export function makeTools(dotcmsUrl: string, authToken: string) {
// nosemgrep: detect-vercelai -- internal LLM eval harness (ai-evals), not shipped runtime code; Vercel AI SDK usage is intentional
const searchTool = tool({
description: `Explore the dotCMS REST API spec. Write JavaScript with the \`spec\` global (spec.paths keyed by path string). Return the data you need.`,
description: `Explore the dotCMS REST API spec. Write JavaScript with the \`spec\` global (\`spec.paths\` + \`spec.components.schemas\`, \`$ref\`-based). Schemas in requestBody/responses are usually \`$ref\`s — call \`resolveRef(schemaOrName, depth)\` to expand them. Return the data you need.`,
inputSchema: z.object({
code: z
.string()
Expand All @@ -31,7 +24,7 @@ export function makeTools(dotcmsUrl: string, authToken: string) {
variables: { spec },
sandbox: { timeout: 10000 }
});
return sandboxResult(result);
return formatSandboxResult(result);
}
});

Expand All @@ -54,7 +47,7 @@ export function makeTools(dotcmsUrl: string, authToken: string) {
sandbox: { timeout: 15000 },
adapters: ['api']
});
return sandboxResult(result);
return formatSandboxResult(result);
}
});

Expand Down
45 changes: 45 additions & 0 deletions core-web/apps/dotcms-ui/proxy-dev.conf.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,51 @@ export default [
changeOrigin: true,
logLevel: 'debug'
},
// 2. Embedded dotCMS page proxy (a11y portlet iframe).
//
// Lets the portlet iframe load live/edit-mode pages same-origin in dev.
// Use src="/dot-page/index?mode=EDIT_MODE" — the prefix is stripped so it
// hits the dotCMS page renderer (e.g. /index) on the BE. The sentinel prefix
// avoids colliding with the dev server's own Angular routes.
//
// DEV-ONLY WORKAROUND FOR A MISSING BACKEND CAPABILITY — do not delete this
// rule on its own; it is load-bearing (see below).
// --------------------------------------------------------------------------
// Why it exists: the Accessibility Studio's side-by-side frames are not
// passive previews. The run screen reaches INTO each iframe's contentWindow to
// inject the axe violation-marker overlay and to sync scroll between the two
// frames (see A11yMarkerService + DotA11yRunComponent.frameWindow). That is
// same-origin-only by the browser's security model — cross-origin frames throw
// on contentWindow access, so the markers silently never render.
//
// In prod there is no problem: the portlet is served FROM the dotCMS origin, so
// the page is already same-origin and the iframe needs no prefix at all. This
// rule exists purely because `nx serve` puts the app on a different origin than
// the backend, and it papers over that split in the dev server instead of in
// the platform.
//
// What the real fix is (BACKEND): dotCMS should expose a first-class,
// same-origin endpoint for rendering a page for inspection — i.e. a supported
// resource under /api that returns the page render, so the Studio (and any
// future agent that needs to inspect a rendered page) can frame it directly
// with no origin games and no dev-server rewrite. Today no such endpoint
// exists, which is the actual gap.
//
// Until that lands this rule must stay, and it must stay in sync with
// DotA11yRunComponent.previewPathPrefix, which emits the `/dot-page` sentinel
// under isDevMode(). Removing one without the other 404s the preview frames in
// local dev. Both should be deleted together once the backend endpoint exists.
{
context: ['/dot-page'],
target,
secure: false,
changeOrigin: true,
logLevel: 'debug',
followRedirects: false,
pathRewrite: {
'^/dot-page': ''
}
},
Comment thread
fmontes marked this conversation as resolved.
// 2. Main API Proxy
{
context: [
Expand Down
6 changes: 6 additions & 0 deletions core-web/apps/dotcms-ui/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ const PORTLETS_ANGULAR: Route[] = [
(m) => m.dotPublishingQueueRoutes
)
},
{
path: 'agents',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only top-level portlet route in this array without canActivate: [MenuGuardService] - the adjacent users entry has both canActivate and canActivateChild. Is the omission intentional, or should the agents portlet be menu-permission gated like its siblings?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing. Will be in the next commit

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — the omission is real, and it's portlet authorization that's bypassed, not authentication. MenuGuardService enforces that the portlet is in the user's layout and granted to their role, and every sibling lazy route here carries it. Without it, removing Accessibility Studio from a role's layout hides the menu entry but doesn't block direct URL entry to /agents/a11y.

We're merging without it for now — tracking it as follow-up rather than adding it in this PR. Flagging for anyone reading this later: the guard is necessary but not sufficient on its own, since the backend half (A11yAgentResource) needs its own permission check regardless of what the route does.

data: { reuseRoute: false },
loadChildren: () =>
import('@dotcms/portlets/dot-agents/portlet').then((m) => m.dotAgentsRoutes)
},
Comment thread
zJaaal marked this conversation as resolved.
{
path: 'users',
canActivate: [MenuGuardService],
Expand Down
4 changes: 2 additions & 2 deletions core-web/apps/mcp-server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ DOTCMS_URL=
# API token for authentication
AUTH_TOKEN=

# Sandbox execution timeout in milliseconds (default: 15000)
SANDBOX_TIMEOUT=15000
# Sandbox execution timeout in milliseconds (default: 45000)
SANDBOX_TIMEOUT=45000
8 changes: 4 additions & 4 deletions core-web/apps/mcp-server/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Configure the MCP server via the `env` block in your MCP client config:
|---|---|---|
| `DOTCMS_URL` | Yes | Base URL of the dotCMS instance |
| `AUTH_TOKEN` | Yes | JWT Bearer token (generate in dotCMS → User Tools → API Tokens) |
| `SANDBOX_TIMEOUT` | No | Sandbox execution timeout in ms (default: `15000`) |
| `SANDBOX_TIMEOUT` | No | Sandbox execution timeout in ms (default: `45000`) |

## Architecture Overview

Expand All @@ -60,7 +60,7 @@ This is a **Model Context Protocol (MCP) server** for dotCMS, built with [xmcp](
**Entry Point**: xmcp generates the entry point at build time (`dist/stdio.js`).

**Build Pipeline**:
1. `generate-spec` — fetches the OpenAPI spec from a dotCMS instance, processes it into `src/generated/spec.json` (dereferences $refs, filters to relevant endpoints)
1. `generate-spec` — fetches the OpenAPI spec from a dotCMS instance, processes it into `src/generated/spec.json` (filters to relevant endpoints, keeps request/response `$ref`s, prunes `components.schemas` to just the schemas those endpoints reference)
2. `xmcp build` — bundles everything with rspack into `dist/`

**Tool Layer** (`src/tools/`):
Expand All @@ -86,7 +86,7 @@ This is a **Model Context Protocol (MCP) server** for dotCMS, built with [xmcp](
- Main thread executes the actual HTTP call with injected auth
- Result is posted back to the sandbox

**Build-time Spec Processing**: `scripts/generate-spec.ts` fetches the OpenAPI spec from a URL (or reads a local file), dereferences it, filters to allowed endpoint prefixes, strips response schemas, and handles circular references. The developer must provide the spec URL or file path when running `generate-spec`.
**Build-time Spec Processing**: `scripts/generate-spec.ts` (thin CLI) + `scripts/spec-transform.ts` (pure, testable logic) fetch the OpenAPI spec from a URL (or read a local file), filter to allowed endpoint prefixes, replace only Jersey-autogenerated multipart bodies with a placeholder (curated multipart schemas are kept), keep request/response `$ref`s as-is, and prune `components.schemas` to just the schemas transitively referenced by the kept paths. Keeping `$ref`s (rather than dereferencing) dedupes shared schemas and is naturally acyclic. The developer must provide the spec URL or file path when running `generate-spec`.

### Type System

Expand All @@ -99,7 +99,7 @@ All interfaces are in `src/lib/types.ts`:

### Search Tool
**Purpose**: Explore the dotCMS REST API specification
**Sandbox globals**: `spec` (the dereferenced OpenAPI spec object)
**Sandbox globals**: `spec` (the filtered OpenAPI spec: `$ref`-based `paths` + `components.schemas`), plus helpers `resolveRef`, `pick`, `table`, `count`, `sum`, `first`. Use `resolveRef(schemaOrName, depth)` to expand `$ref`s at a bounded depth.
**Read-only**: Yes — no side effects

### Execute Tool
Expand Down
13 changes: 8 additions & 5 deletions core-web/apps/mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Before setting up the MCP server, you need these environment variables to connec
| ------------------ | -------- | ---------------------------------- | ------- |
| `DOTCMS_URL` | ✅ | Your dotCMS instance URL | `https://demo.dotcms.com` |
| `AUTH_TOKEN` | ✅ | API authentication token (created in [setup step](#create-a-dotcms-api-token)) | `your-api-token-here` |
| `SANDBOX_TIMEOUT` | ❌ | Sandbox execution timeout in ms (default: 15000) | `15000` |
| `SANDBOX_TIMEOUT` | ❌ | Sandbox execution timeout in ms (default: 45000) | `45000` |
| `DEBUG` | ❌ | When set to any truthy value, emits diagnostic logs to stderr (e.g. context cache load events) | `1` |


Expand Down Expand Up @@ -208,7 +208,7 @@ The dotCMS MCP Server provides tools that enable comprehensive content managemen

**Purpose**: Explore the dotCMS REST API specification using JavaScript code that runs in an isolated sandbox.

The `spec` global contains the full dereferenced OpenAPI spec with `paths` object.
The `spec` global contains the filtered OpenAPI spec — `$ref`-based, with a `paths` object and a `components.schemas` map. Request/response `.schema` values are usually `$ref`s (e.g. `{ $ref: '#/components/schemas/PageView' }`); call `resolveRef(schemaOrName, depth)` to expand them at a bounded depth. Output is hard-capped (~25k chars), so return only what you need.

```javascript
// List all available endpoint paths
Expand All @@ -218,6 +218,9 @@ return Object.keys(spec.paths)
return Object.entries(spec.paths)
.filter(([path]) => path.includes('contenttype'))
.map(([path, methods]) => ({ path, methods: Object.keys(methods) }))

// Resolve a request-body schema one level deep
return resolveRef(spec.paths['/api/v1/contenttype'].post.requestBody.content['application/json'].schema, 1)
```

### Execute
Expand Down Expand Up @@ -459,7 +462,7 @@ libs/sdk/ai/ # Portable runtime primitives
│ │ ├── bun-worker.ts # Bun Web Worker sandbox
│ │ └── node-worker.ts # Node.js worker_threads sandbox
│ └── generated/
│ └── spec.json # Committed processed OpenAPI spec
│ └── spec.json # Build-generated, git-ignored (lives in libs/sdk/ai)
└── project.json # Nx project configuration
```

Expand All @@ -479,7 +482,7 @@ libs/sdk/ai/ # Portable runtime primitives
- Adapter pattern bridges sandbox ↔ main thread for API calls

**Build-time Spec Processing**: The OpenAPI spec is pre-processed at build time:
- `generate-spec` target dereferences `$ref` pointers and filters to relevant endpoints
- `generate-spec` target filters to relevant endpoints, keeps request/response `$ref`s, and prunes `components.schemas` to just the referenced schemas
- Output is a compact JSON embedded in the bundle
- Reduces runtime overhead and MCP response size

Expand All @@ -501,7 +504,7 @@ pnpm nx test mcp-server
# Run tests in watch mode
pnpm nx test mcp-server --watch

# Refresh the OpenAPI spec (run when dotCMS API changes, then commit spec.json)
# Refresh the OpenAPI spec (git-ignored; regenerated by build/serve/test via dependsOn)
# Defaults to https://demo.dotcms.com/api/openapi.json
pnpm nx run sdk-ai:generate-spec
```
Expand Down
108 changes: 108 additions & 0 deletions core-web/apps/mcp-server/src/lib/assets-transfer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { includeMatcher, splitIncludePatterns } from './assets-transfer';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This spec covers splitIncludePatterns and includeMatcher only, but the production diff for assets-transfer.ts is +214/-35 and reworks real transfer behavior that has no coverage here: the 0-byte skip being removed (files now upload as-is), the new retry-with-'\n' fallback, the new final re-check pass in verifyLive that fixes a false-negative, and the new totalSeen / zero-match warning branch. Neither uploadAssets nor downloadAssets is invoked with a fake runtime.

Would it be worth adding a few tests around those paths, given they're the behavior changes the rework is for?


describe('splitIncludePatterns', () => {
it('splits comma-separated patterns and trims them', () => {
expect(splitIncludePatterns('*.vtl, *.scss')).toEqual(['*.vtl', '*.scss']);
});

it('does NOT split on a comma inside a brace group', () => {
expect(splitIncludePatterns('*.{png,webp,jpg}')).toEqual(['*.{png,webp,jpg}']);
});

it('splits around a brace group but keeps the group intact', () => {
expect(splitIncludePatterns('*.{png,jpg},*.vtl')).toEqual(['*.{png,jpg}', '*.vtl']);
});

it('drops empty entries and returns [] for undefined', () => {
expect(splitIncludePatterns('*.png,,')).toEqual(['*.png']);
expect(splitIncludePatterns(undefined)).toEqual([]);
});
});

describe('includeMatcher', () => {
it('matches everything when no include is given', () => {
const m = includeMatcher();
expect(m('a.png')).toBe(true);
expect(m('deep/nested/a.vtl')).toBe(true);
});

// The three repro cases from the bug report — files live directly in the source dir.
describe('bug report repro (top-level files)', () => {
it('brace expansion matches top-level files (was: 0 matched)', () => {
const m = includeMatcher('*.{png,webp,jpg}');
expect(m('amazon-logo.png')).toBe(true);
expect(m('book1.webp')).toBe(true);
expect(m('cover.jpg')).toBe(true);
expect(m('notes.txt')).toBe(false);
});

it('** globstar matches a top-level file too (was: 0 matched)', () => {
const m = includeMatcher('**/*.png');
expect(m('amazon-logo.png')).toBe(true); // no subdirectory — must still match
expect(m('img/hero.png')).toBe(true);
expect(m('a/b/c/deep.png')).toBe(true);
expect(m('a/b/c/deep.webp')).toBe(false);
});

it('plain top-level glob still works', () => {
const m = includeMatcher('*.png');
expect(m('amazon-logo.png')).toBe(true);
expect(m('book1.webp')).toBe(false);
});
});

describe('single-star does not cross directories', () => {
it('"*.png" (no slash) matches a basename anywhere in the tree', () => {
const m = includeMatcher('*.png');
expect(m('a.png')).toBe(true);
expect(m('deep/dir/a.png')).toBe(true); // basename match, unanchored
});

it('an anchored "img/*.png" only matches that one directory level', () => {
const m = includeMatcher('img/*.png');
expect(m('img/a.png')).toBe(true);
expect(m('img/sub/a.png')).toBe(false); // * does not cross /
expect(m('other/a.png')).toBe(false);
});
});

describe('** globstar depth', () => {
it('"img/**/*.png" matches zero or more intermediate dirs', () => {
const m = includeMatcher('img/**/*.png');
expect(m('img/a.png')).toBe(true); // zero intermediate dirs
expect(m('img/sub/a.png')).toBe(true);
expect(m('img/a/b/c.png')).toBe(true);
expect(m('other/a.png')).toBe(false);
});
});

describe('? single char', () => {
it('matches exactly one non-slash char', () => {
const m = includeMatcher('file?.txt');
expect(m('file1.txt')).toBe(true);
expect(m('fileA.txt')).toBe(true);
expect(m('file.txt')).toBe(false);
expect(m('file12.txt')).toBe(false);
});
});

describe('literals are escaped', () => {
it('a dot in the pattern is literal, not "any char"', () => {
const m = includeMatcher('*.png');
expect(m('axpng')).toBe(false); // the "." must be a real dot
expect(m('a.png')).toBe(true);
});

it('multiple patterns OR together', () => {
const m = includeMatcher('*.vtl,*.scss');
expect(m('theme.vtl')).toBe(true);
expect(m('styles.scss')).toBe(true);
expect(m('image.png')).toBe(false);
});
});

it('is case-insensitive', () => {
const m = includeMatcher('*.PNG');
expect(m('photo.png')).toBe(true);
});
});
Loading
Loading