marekskopal / typo3-mcp-server
MCP server for TYPO3 CMS administration
Package info
github.com/marekskopal/typo3-mcp-server
Type:typo3-cms-extension
pkg:composer/marekskopal/typo3-mcp-server
Requires
- php: >=8.3
- ext-curl: *
- mcp/sdk: ^0.8
- typo3/cms-backend: ^13.4.35 || ^14.3.7
- typo3/cms-core: ^13.4.35 || ^14.3.7
- typo3/cms-extbase: ^13.4.35 || ^14.3.7
- typo3/cms-fluid: ^13.4.35 || ^14.3.7
- typo3/cms-frontend: ^13.4.35 || ^14.3.7
Requires (Dev)
- dg/bypass-finals: ^1.9
- phpstan/extension-installer: ^1.3
- phpstan/phpstan: ^2.0
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^12.0
- shipmonk/phpstan-rules: ^4.0
- slevomat/coding-standard: ^8.14
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v1.4.0
- 1.3.1
- v1.3.0
- v1.2.0
- v1.1.0
- v1.0.0
- v0.12.4
- v0.12.3
- v0.12.2
- v0.12.1
- v0.12.0
- v0.11.0
- v0.10.0
- v0.9.3
- v0.9.2
- v0.9.1
- v0.9.0
- v0.8.0
- v0.7.1
- v0.7.0
- v0.6.0
- v0.5.0
- v0.4.0
- v0.3.0
- v0.2.0
- v0.1.0
- dev-feat/typoscript-tools
- dev-feat/converge-tool-return-shapes
- dev-fix/honour-non-exclude-fields
- dev-fix/backend-module-redirect-uri-validation
- dev-fix/workspace-tools-read-gate
- dev-fix/system-info-admin-only-fields
- dev-fix/schema-resource-table-gate
- dev-fix/upload-from-url-permission-order
- dev-fix/audit-log-payload-keys
- dev-release/v1.0.0-prep
- dev-backup/session-fix-and-tests
This package is auto-updated.
Last update: 2026-09-19 19:37:17 UTC
README
TYPO3 CMS extension that implements an MCP (Model Context Protocol) server for TYPO3 administration. It exposes 60+ tools for managing pages, content elements, files, backend users, and custom extension records via the MCP protocol, allowing AI assistants to interact with your TYPO3 instance.
This extension is designed primarily for direct, fully autonomous AI operation — changes take effect immediately, with no approval queue between the AI and the live site. The goal is to let AI agents build, update, and maintain TYPO3 sites end-to-end without human intervention.
Workspace support is also available when typo3/cms-workspaces is installed: the AI can switch into a workspace, make draft changes, and use the workspace tools to publish or discard them. This is a secondary mode for cases where review-before-publish is required — direct mode remains the primary use case.
Example Prompts
These are the kinds of tasks an AI agent can accomplish autonomously through this MCP server:
- "Create a new 'Services' page under the homepage with three subpages: Web Development, Consulting, and Support. Add introductory text content to each."
- "Translate all pages and content elements under page 12 to German and French."
- "Upload the product images from these URLs and attach them to the corresponding news records."
- "Reorganize the page tree: move all blog posts from 2023 under a new '2023 Archive' page."
- "Create a contact form page with a header, text element explaining our office hours, and an address content element."
- "Review all pages under 'Products' and update their SEO meta descriptions based on their content."
- "Set up the site structure for a new microsite: landing page, about, pricing with three tiers, FAQ, and contact — add placeholder content to each."
- "Find all hidden pages in the site and list them with their paths so I can decide which to publish or delete."
- "Add a news record for today's product launch, upload the press release PDF, and link it as a file reference."
Requirements
- PHP 8.3+
- TYPO3 v13.4.35+ or v14.3.7+ (these are the first releases past TYPO3-CORE-SA-2026-022)
Installation
composer require marekskopal/typo3-mcp-server
After installation:
- Activate the extension in TYPO3 backend or via CLI:
vendor/bin/typo3 extension:setup
- Run database migrations to create the required OAuth and session tables:
vendor/bin/typo3 database:updateschema
Re-run this command after every extension update — new tables (e.g.tx_msmcpserver_mcp_sessionintroduced for persistent sessions) won't be created automatically.
Setup
The MCP server supports two transports:
- HTTP transport — for remote AI clients connecting over the network (requires OAuth)
- stdio transport — for local AI tools running on the same machine (no OAuth needed)
stdio Transport (Recommended for Local Use)
For AI tools running on the same server as TYPO3 (Claude Desktop, Cursor, Windsurf, VS Code, etc.), use the stdio transport. No OAuth setup is required — the server runs as a backend user directly:
vendor/bin/typo3 mcp:server
Use --user to specify which backend user to run as (defaults to admin):
vendor/bin/typo3 mcp:server --user editor
HTTP Transport
For remote AI clients, the MCP server is available at /mcp on your TYPO3 instance. It uses the Streamable HTTP transport (MCP protocol version 2025-03-26 or later, negotiated with the client).
HTTP transport requires OAuth 2.1 authentication. See the Authentication section below.
The base path is configurable via the mcpBasePath extension setting if /mcp is already in use by another handler — see Base Path.
AI Client Configuration
Clients cache the tool list. An MCP client fetches the tools and their descriptions once, when it connects. After upgrading the extension, enabling a table in the Extension Tables module or changing
EXTCONF, restart the client (or disconnect and reconnect the server) so it sees the new tools and field lists — until then it works from the old descriptions and may, for example, not offer a field thattable_schemaalready reports.
Claude Desktop
Add to your Claude Desktop config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
stdio (local):
{
"mcpServers": {
"typo3": {
"command": "php",
"args": ["vendor/bin/typo3", "mcp:server", "--user", "admin"],
"cwd": "/path/to/your/typo3/project"
}
}
}
HTTP (remote):
{
"mcpServers": {
"typo3": {
"url": "https://your-typo3-site.com/mcp"
}
}
}
OAuth authentication is handled automatically — Claude Desktop will open a browser window for the authorization flow.
Claude Code (CLI)
stdio (local):
claude mcp add typo3 -- php vendor/bin/typo3 mcp:server
Or add to your project's .mcp.json:
{
"mcpServers": {
"typo3": {
"command": "php",
"args": ["vendor/bin/typo3", "mcp:server"]
}
}
}
HTTP (remote):
claude mcp add --transport http typo3 https://your-typo3-site.com/mcp
Or in .mcp.json:
{
"mcpServers": {
"typo3": {
"url": "https://your-typo3-site.com/mcp"
}
}
}
Cursor
Add to .cursor/mcp.json in your project root:
stdio (local):
{
"mcpServers": {
"typo3": {
"command": "php",
"args": ["vendor/bin/typo3", "mcp:server"],
"cwd": "/path/to/your/typo3/project"
}
}
}
HTTP (remote):
{
"mcpServers": {
"typo3": {
"url": "https://your-typo3-site.com/mcp"
}
}
}
Windsurf
Add to ~/.codeium/windsurf/mcp_config.json:
stdio (local):
{
"mcpServers": {
"typo3": {
"command": "php",
"args": ["vendor/bin/typo3", "mcp:server"],
"cwd": "/path/to/your/typo3/project"
}
}
}
HTTP (remote):
{
"mcpServers": {
"typo3": {
"url": "https://your-typo3-site.com/mcp"
}
}
}
VS Code (Copilot)
Add to your VS Code settings (.vscode/settings.json):
stdio (local):
{
"mcp": {
"servers": {
"typo3": {
"command": "php",
"args": ["vendor/bin/typo3", "mcp:server"],
"cwd": "/path/to/your/typo3/project"
}
}
}
}
HTTP (remote):
{
"mcp": {
"servers": {
"typo3": {
"url": "https://your-typo3-site.com/mcp"
}
}
}
}
Other MCP Clients
Any MCP-compatible client can connect via either transport:
- stdio: Run
php vendor/bin/typo3 mcp:server— communicates via stdin/stdout, no auth needed - HTTP: Connect to
https://your-typo3-site.com/mcp— requires OAuth 2.1 with PKCE. Server metadata is available at/.well-known/oauth-authorization-server/mcp(RFC 8414 path-insert) for auto-discovery.
Authentication
HTTP transport uses OAuth 2.1 with PKCE (S256). Each token is linked to a backend user — all operations respect that user's TYPO3 permissions.
The stdio transport does not require OAuth — the user is specified via the --user flag.
OAuth Features
- Authorization Code flow with PKCE — standard OAuth 2.1 for MCP clients
- Authentication via the real TYPO3 backend login — the
/mcp/oauth/authorizeendpoint redirects unauthenticated users to/typo3/loginand only renders a single-click consent screen onceBE_USERis established; MFA,starttime/endtime, per-user lockout, andsys_logfailed-login entries come from the standard backend pipeline. Both of TYPO3's MFA gates apply to the consent screen: a session that still owes its MFA challenge, or a user whom therequireMfapolicy obliges to set MFA up first, is sent back through the backend login instead of being offered the Authorize button - Dynamic Client Registration (RFC 7591) — clients can self-register. Because anyone can then register a client under any name, the consent screen always names the host the authorization will be sent to and marks a self-registered client as not verified by an administrator; installations that provision every client in the backend module can switch the endpoint off with
dynamicClientRegistrationEnabled = 0(see Client Registration) - Token Revocation (RFC 7009) — revoke access and refresh tokens
- Protected Resource Metadata (RFC 9728) — auto-discovery of auth requirements
OAuth Endpoints
| Endpoint | Description |
|---|---|
/.well-known/oauth-authorization-server/mcp |
Authorization server metadata (RFC 8414) |
/.well-known/oauth-protected-resource/mcp |
Protected resource metadata (RFC 9728) |
/mcp/oauth/authorize |
Authorization endpoint |
/mcp/oauth/token |
Token endpoint |
/mcp/oauth/revoke |
Token revocation endpoint |
/mcp/oauth/register |
Dynamic client registration |
Base Path
The MCP endpoint and all OAuth sub-paths share a single configurable prefix. Override it via Settings > Extension Configuration > ms_mcp_server:
| Setting | Default | Description |
|---|---|---|
mcpBasePath |
/mcp |
Base URL path for the MCP endpoint. Must start with /. May contain directories (e.g. /typo3-mcp, /api/mcp). |
Per RFC 8414 §3.1 and RFC 9728 §3, the .well-known/* document for an issuer/resource https://host/<path> lives at https://host/.well-known/<doc>/<path> — the well-known string is inserted between the host and the issuer's path component, not appended. Spec-compliant MCP clients (e.g. Claude Code 2.1+) use that form.
Example with mcpBasePath = /typo3-mcp:
| Endpoint | Path |
|---|---|
| MCP server | /typo3-mcp |
| Authorization | /typo3-mcp/oauth/authorize |
| Token | /typo3-mcp/oauth/token |
| Registration | /typo3-mcp/oauth/register |
| Revocation | /typo3-mcp/oauth/revoke |
| Authorization server metadata | /.well-known/oauth-authorization-server/typo3-mcp |
| Protected resource metadata | /.well-known/oauth-protected-resource/typo3-mcp |
And with mcpBasePath = /api/mcp:
| Endpoint | Path |
|---|---|
| MCP server | /api/mcp |
| Authorization server metadata | /.well-known/oauth-authorization-server/api/mcp |
| Protected resource metadata | /.well-known/oauth-protected-resource/api/mcp |
Client Registration
| Setting | Default | Description |
|---|---|---|
dynamicClientRegistrationEnabled |
1 |
Offer unauthenticated RFC 7591 registration at /mcp/oauth/register. With 0 the endpoint answers 403 and the RFC 8414 metadata no longer advertises a registration_endpoint, so clients ask for a client_id created in the backend module instead |
Whatever the setting, the consent screen tells the user where the authorization code will go (the redirect URI's host, or the whole URI for a private-use scheme such as com.example.app:/oauth) and, for a client that registered itself, that no administrator has verified it. Client names supplied at registration are stripped of control and format characters (bidi overrides, zero-width joiners), whitespace-collapsed and capped at 255 characters before they are stored, so a name cannot disguise the client on that screen.
Token Lifetimes
Configurable via Settings > Extension Configuration > ms_mcp_server:
| Setting | Default | Description |
|---|---|---|
accessTokenLifetime |
3600 (1 hour) | Access token lifetime in seconds |
refreshTokenLifetime |
2592000 (30 days) | Refresh token lifetime in seconds (sliding: each rotation issues a fresh token with this lifetime) |
refreshTokenMaxLifetime |
7776000 (90 days) | Absolute cap on how long a refresh-token chain stays valid regardless of rotation; after this the user must re-authenticate |
codeLifetime |
60 (1 minute) | Authorization code lifetime in seconds |
Sessions
| Setting | Default | Description |
|---|---|---|
sessionLifetime |
86400 (1 day) | MCP session idle lifetime in seconds (sliding TTL — any activity extends it); idle sessions are removed by mcp:cleanup |
Audit Logging
Every tool, resource and prompt invocation can be written to sys_log with the user, timing, arguments (redacted to size-capped scalars) and the affected table/record.
What is recorded from the arguments. Scalars — uid, pid, table name, a plain-text search term — are kept, capped at 100 characters each. An argument holding a JSON object, which is how every write tool takes its fields payload and the search tools take search, is reduced to its field names ({header, bodytext, hidden}): the trail answers which fields were touched without copying record content, and potentially personal data, into a table every administrator can read. Arrays and objects passed directly are omitted entirely.
| Setting | Default | Description |
|---|---|---|
auditLogLevel |
mutations |
How much reaches sys_log — see below |
| Value | What is written |
|---|---|
all |
Every invocation, successful reads included |
mutations |
Writes, plus every failure — including failed reads |
errors |
Failures only |
off |
Nothing |
The default skips successful reads, because they are the unbounded part: an agent session doing a few thousand pages_list / record_search / content_get calls otherwise writes a few thousand rows, each an INSERT in the hot path of the tool call, into a table shared with TYPO3 core's own logging that mcp:cleanup does not prune. Set auditLogLevel = all to keep the previous behaviour.
Classification is fail-closed: the read shapes are enumerated and anything unrecognised counts as a write, so a tool added under an unfamiliar name lands in the trail rather than quietly falling out of it.
Rate Limiting
OAuth endpoints are protected by IP-based rate limiting with configurable per-endpoint limits:
| Setting | Default | Description |
|---|---|---|
rateLimitEnabled |
1 |
Enable/disable rate limiting |
rateLimitAuthorize |
5 / 300s |
Authorize POST (consent submission — auth code minting) |
rateLimitAuthorizeGet |
20 / 300s |
Authorize GET (consent form display) |
rateLimitToken |
20 / 300s |
Token exchange/refresh |
rateLimitRegister |
10 / 3600s |
Client registration |
rateLimitRevoke |
20 / 300s |
Token revocation |
Each endpoint's window length is configurable too, via the matching rateLimit*Window setting (rateLimitAuthorizeWindow, rateLimitAuthorizeGetWindow, rateLimitTokenWindow, rateLimitRegisterWindow, rateLimitRevokeWindow; values in seconds).
Returns 429 Too Many Requests with Retry-After header when exceeded.
Backend Module
The System > MCP Server backend module provides:
- Register and manage OAuth clients
- Edit client settings (name, redirect URIs, linked backend user). Redirect URIs are held to the same rules as self-registration: HTTPS, an
httploopback address, or a reverse-domain private-use scheme, with no fragment - Restrict a client to one backend user. With a backend user selected, the consent screen refuses every other signed-in account (
403, with a link back to the application carryingerror=access_denied), the code exchange refuses a code minted for another account, and a refresh for a grant that no longer matches is refused and revokes its whole token family — so re-assigning a client cuts off the account it used to serve. "Any user" (the default) keeps the client open to every backend user. - View active tokens per client with status (active/refreshable/expired)
- Revoke individual tokens
- Discover extension tables — scan installed extensions, enable/disable for MCP tool generation, customize label/prefix
Tools Reference
How a tool answers. Every tool returns a JSON object describing what it did. A tool never encodes a failure into a successful result: if the tool could not do what was asked — a malformed fields or search payload, an unknown table or field, a permission the user does not hold, a write whose target does not exist — the call fails with an MCP tool error carrying the reason, which a client sees as an error rather than as data. What a tool did do comes back as data, and that includes a read that matched nothing: an empty records list, or, for the *_get tools, {"found": false, "table": …, "uid": …, "message": …}. The rule is worth knowing because the distinction is the one an agent acts on — an error means "ask differently", a result means "this is the answer".
Write errors. A write that TYPO3's DataHandler refuses — no permission, an invalid value — is reported with DataHandler's own message (TYPO3 DataHandler reported errors while updating pages:68: …). If a DataHandler hook throws (an extension reacting to the change, such as EXT:redirects on a slug change), the tool reports the exception class and code and that the change may already have been applied, since hooks run after the row is written; read the record back to verify. The raw exception message goes to the TYPO3 log only, because it can embed SQL or file paths.
Pages
| Tool | Description |
|---|---|
pages_list |
List child pages with pagination. Supports language filtering and field selection. |
pages_get |
Get a single page with all readable fields. |
pages_create |
Create a new page. Pass fields as JSON, set language with sysLanguageUid. |
pages_update |
Update page fields. Pass a JSON object with field names and new values. |
pages_delete |
Delete a page by UID. Supports dryRun. |
pages_copy |
Copy a page. Set includeSubpages: true to copy the entire subtree. |
pages_move |
Move a page in the tree. Subpages move with the page. |
pages_tree |
Get the page tree hierarchy with configurable depth (1-10, default 3). |
pages_search |
Search pages by title (plain text) or advanced conditions (JSON). Supports sorting. |
Target positioning (for pages_copy, pages_move): Provide exactly one of targetPid (place as first child of that parent page) or afterUid (place as a sibling after that page, under the same parent).
Content Elements
| Tool | Description |
|---|---|
content_list |
List content elements on a page with pagination and language filtering. |
content_get |
Get a single content element with all readable fields. |
content_create |
Create a content element. Pass fields as JSON, set language with sysLanguageUid. |
content_update |
Update content element fields. |
content_delete |
Delete a content element by UID. Supports dryRun. |
content_move |
Move a content element to a new position. |
content_copy |
Copy a content element to a new position. |
content_search |
Search content by header (plain text) or advanced conditions (JSON). Supports language filtering and sorting. |
Target positioning (for content_move, content_copy): Provide exactly one of targetPid (place at the top of that page) or afterUid (place after that content element, on the same page and column as the sibling).
File Management
| Tool | Description |
|---|---|
file_storage_list |
List accessible file storages and the file mounts that bound them. |
file_list |
List files and directories with pagination. |
file_search |
Search files by name pattern and/or extension across storage. |
file_get_info |
Get file metadata: UID, name, size, MIME type, public URL. |
file_upload |
Upload a file from text content or base64-encoded binary data. |
file_upload_from_url |
Download a file from URL and store it (max 100 MB). The target storage and folder are resolved and authorized before anything is fetched. |
file_copy |
Copy a file to a directory. |
file_delete |
Delete a file by identifier. |
file_move |
Move a file to a different directory. |
file_rename |
Rename a file. |
directory_create |
Create a new directory. |
directory_delete |
Delete a directory. Set recursive: true for non-empty directories. |
directory_move |
Move a directory to a different parent. |
directory_rename |
Rename a directory. |
All file tools accept an optional storageUid parameter (default: 1 for fileadmin).
File mounts. A non-admin user is confined to their configured file mounts, exactly as in the
backend: paths outside a mount are rejected, and read-only mounts reject writes. Since a mounted
user's storage root is typically outside their mounts, file_list on / returns the mount folders
themselves, and file_storage_list reports the valid roots up front — so an AI client can find out
where it may work instead of guessing paths relative to the storage root. Administrators keep
unrestricted access to every storage.
File References
| Tool | Description |
|---|---|
file_reference_add |
Attach files to a record's image/media field. Pass sys_file UIDs from upload results. |
file_reference_list |
List file references for a record field. Returns reference UIDs and metadata. |
file_reference_remove |
Remove file references by UID. Detaches files but does not delete them. |
Schema and Search
| Tool | Description |
|---|---|
table_schema |
Get TCA field definitions for any table. Use before creating/updating records to discover valid fields and options. |
record_search |
Search records in any table with field conditions, operators, and sorting. |
record_count |
Count records in any table without fetching them. Supports pid and search condition filtering. |
The search parameter is either a plain-text term — LIKE-matched against the table's label field (TCA ctrl.label, usually title; a table without one rejects plain text and answers with the JSON shape to use instead) — or a JSON object with field names as keys. Each field's value takes one of these shapes:
| Shape | Example | Meaning |
|---|---|---|
| plain string or number | {"title": "hello"} |
LIKE '%hello%' |
| operator-keyed object | {"TSconfig": {"like": "tx_news"}} |
the named operator |
| long form | {"uid": {"op": "gt", "value": "10"}} |
the named operator |
| list | {"uid": [1, 2]} |
IN (1, 2) |
true / false |
{"hidden": true} |
= 1 / = 0 |
null |
{"l10n_source": null} |
IS NULL |
Search operators: eq, neq, like (default), gt, gte, lt, lte, in (comma-separated string or list), null, notNull.
A condition of any other shape — {"TSconfig": {"contains": "x"}}, an unknown operator, a nested object as the operand — is rejected with an error that names the field and the accepted shapes, rather than being dropped. A condition on a field the user cannot read is dropped and reported in ignoredFields. Both tools accept exactly the same search.
Many-to-many fields: table_schema marks a select or group column that relates through an MM table with "relation": "mm" and "mm": "<mm table>", alongside foreignTable (select) or allowed (group) and any minitems / maxitems. record_search returns such fields as lists of related UIDs, but they cannot be used as search conditions — the physical column only stores the relation count, so a condition on one is rejected with an error rather than matched against that count. See Many-to-many relation fields for the read/write format.
Search examples:
// Plain-text term on the label field "Brio" // Simple LIKE search on a named field {"title": "hello"} // Operator-keyed shorthand: pages whose TSconfig mentions tx_news {"TSconfig": {"like": "tx_news"}} // Long form, several conditions {"uid": {"op": "gt", "value": "10"}, "title": {"op": "eq", "value": "Home"}} // Combined with sorting orderBy: "title", orderDirection: "DESC"
Translation
| Tool | Description |
|---|---|
site_languages |
List available languages for a site. Pass any page ID belonging to the site. |
record_translate |
Create a translation of a record. Source must be in default language. Uses TYPO3 connected mode. |
Batch Operations
| Tool | Description |
|---|---|
record_delete_batch |
Delete multiple records by comma-separated UIDs. Single atomic DataHandler operation. |
record_update_batch |
Update same fields on multiple records (e.g., {"hidden":1} on UIDs 1,2,3). |
record_move_batch |
Move multiple records to a target position. |
All batch tools work on any TCA table.
Dry-run mode. The destructive tools take dryRun: true, which returns exactly what would change — the affected UIDs, the ones skipped as non-existent, and for updates the fields that would be written and the ones that would be ignored — without touching the database. The response carries "dryRun": true, and single-record deletes additionally report "deleted": false, so a preview cannot be mistaken for a completed change.
Available on record_delete_batch, record_update_batch, record_move_batch, pages_delete, content_delete, and every generated table tool's <prefix>_delete, <prefix>_delete_batch, <prefix>_update_batch and <prefix>_move_batch. Worth a first pass whenever the UID list came from interpreting an instruction rather than from an explicit list: the gap between "delete the old news items" and what that actually resolves to is where the risk lives.
Cache
| Tool | Description |
|---|---|
cache_clear |
Flush caches. Scopes: pages (default), all, or page (single page by pageId). |
Backend Users & Groups
Restricted to admin backend users only. Sensitive be_users columns (password, mfa) are never returned; soft-deleted records are always excluded.
| Tool | Description |
|---|---|
backend_user_list |
List be_users. Optional search (LIKE on username), activeOnly, adminOnly filters; paginated. |
backend_user_get |
Get a single backend user by uid with extended fields (groups, mounts, language, TSconfig, etc.). |
backend_group_list |
List be_groups with optional title search; paginated. |
backend_group_get |
Get a single backend group by uid with permission and mount details. |
Permissions
Tools for inspecting the authenticated backend user's effective permissions.
| Tool | Description |
|---|---|
permission_check_table |
Check whether the current user can read (select) and/or write (modify) a specific table. |
permission_check_page |
Check what the current user can do on a page: show, edit, delete, create subpages, edit content. |
permission_check_summary |
Summary of the current user's permissions: admin status, allowed tables for read/write, languages, file permissions, web/file mounts. |
Admin-only tables. A table TCA marks ctrl.adminOnly (sys_template among them) is withheld from a non-admin on every read path — record_search, record_count, table_schema, the schema resources and the dedicated tools alike — regardless of their tables_select grant. This matches core, which hides such a table from the list module and refuses a non-admin's write in DataHandler.
Exclude fields. A column TCA marks exclude (pages.TSconfig, tt_content.pi_flexform, most starttime / endtime / fe_group, usually hidden) is returned and accepted only for a user who holds the matching non_exclude_fields grant — the same rule that decides whether the backend's list module renders the column. Administrators hold every grant, so nothing is hidden from them. A field withheld this way is reported in ignoredFields when a write names it, and never appears in table_schema.
Redirects
Registered only when typo3/cms-redirects is installed. Operates on the sys_redirect table through DataHandler.
| Tool | Description |
|---|---|
redirect_list |
List redirects with pagination; filter by sourceHost, sourcePath, target (LIKE) and disabled. |
redirect_get |
Get a single redirect record by uid. |
redirect_create |
Create a redirect. Required: sourceHost, sourcePath, target; optional pid (default 0), targetStatuscode and extra fields JSON. |
redirect_update |
Update an existing redirect. Pass fields as a JSON object. |
redirect_delete |
Delete a redirect by uid. |
Scheduler
Registered only when typo3/cms-scheduler is installed. Operates on the tx_scheduler_task table.
| Tool | Description |
|---|---|
scheduler_list |
List scheduler tasks with pagination; filter by tasktype (LIKE), taskGroup, disable. |
scheduler_get |
Get a single scheduler task by uid. |
scheduler_update |
Update a task. Writable fields: disable, description, task_group. |
scheduler_delete |
Delete a scheduler task by uid. |
TypoScript
Operates on the sys_template table plus a read-only view of the compiled result. sys_template is
marked adminOnly in TCA, so these tools — like the backend's own list module — answer only for an
administrator; a non-admin is refused whatever their tables_select grant says.
| Tool | Description |
|---|---|
typoscript_list |
List TypoScript template records with pagination; filter by title (LIKE), root, hidden and pid. The constants / config source is not included. |
typoscript_get |
Get a single template record by uid, including the full constants and config source. |
typoscript_create |
Create a template. Required: pid, title; optional root, clear, constants, config and extra fields JSON. |
typoscript_update |
Update a template. Pass fields as a JSON object — constants and config are ordinary writable fields. |
typoscript_delete |
Delete a template by uid. |
typoscript_rootline |
Which TypoScript sources apply to a page and in what order: the rootline, the site and its sets, and every applying sys_template record with its root / clear flags, basedOn chain and static includes. |
typoscript_active |
The effective, compiled TypoScript for a page as a flat map of dotted paths. |
clear is a bitmask deciding what inherited TypoScript a template discards: 0 nothing, 1
constants, 2 setup, 3 both. basedOn is a comma-separated list of sys_template uids and
include_static_file a comma-separated list of EXT:<key>/<path> identifiers; neither is an MM
relation, so both are read and written as the plain strings TYPO3 stores.
typoscript_active is the equivalent of the backend's Active TypoScript view: constants are
already substituted and conditions resolved, so it is what the frontend would actually use.
{ "pageId": 12, "path": "lib.contentElement", "setup": {
"lib.contentElement": "FLUIDTEMPLATE",
"lib.contentElement.templateName": "Default"
} }
Pass type as setup (the default), constants or both, and path to return one object path and
its children. Output is capped at 2000 entries per section; when the result comes back with
"truncated": true, narrow it with path. Note that since TYPO3 v13.1 a site and its sets provide
TypoScript before any sys_template record does, which typoscript_rootline reports. Conditions are
evaluated without an HTTP request, so a condition reading the request takes its default branch.
Workspaces
Registered only when typo3/cms-workspaces is installed. Direct (live-mode) operation is the primary use case for this extension — these tools enable a secondary draft/publish workflow when review is required. After workspace_switch, all subsequent reads and writes (pages_*, content_*, etc.) operate on the chosen workspace.
| Tool | Description |
|---|---|
workspace_list |
List workspaces accessible to the current backend user (including the implicit live workspace, uid 0). |
workspace_get |
Get workspace metadata by uid: title, custom stages flag, current user access level. |
workspace_switch |
Switch the active workspace. Persists to be_users.workspace_id. Use uid 0 to return to live. |
workspace_changes_list |
List records modified in the current workspace, grouped by table, with t3ver_state and stage. Limited to tables the user may read. |
workspace_publish |
Publish a workspace version to live (swap). The version must live in the user's current workspace and in a table they may read — otherwise it reports "not found"; use workspace_switch first. |
workspace_discard |
Discard a workspace version, dropping unpublished changes. |
workspace_stage_set |
Move a workspace version to a different stage (-10 ready to publish, -20 ready to review, 0 editing, or a custom stage uid). |
Pagination in a workspace. A workspace overlay runs in PHP after the query, dropping records that are hidden in the current workspace (a page deleted in the workspace leaves a DELETE_PLACEHOLDER row behind). A SQL COUNT cannot be overlaid, so outside the live workspace the listing and search tools return hasMore in place of total, and paginate over the overlaid result set — page with offset until hasMore is false. record_count counts overlaid records too, and marks its answer "exact": false if the result set was too large to overlay in full.
Dynamic Extension Tools
Additional CRUD tools are registered automatically for tables configured via EXTCONF or enabled through the Extension Tables backend module (auto-discovery). No extension table is exposed by default — enabling one is always an explicit administrator decision.
Each registered table generates 9 tools. For tx_news_domain_model_news registered under the prefix news:
| Tool | Description |
|---|---|
news_list |
List news records by page ID |
news_get |
Get a single news record |
news_create |
Create a new news record |
news_update |
Update news record fields |
news_delete |
Delete a news record |
news_move |
Move a news record |
news_delete_batch |
Delete multiple news records by comma-separated UIDs |
news_update_batch |
Update the same fields on multiple news records |
news_move_batch |
Move multiple news records to a target position |
See Adding Support for Other Extensions to register your own tables.
Many-to-many relation fields
A select or group column with an MM table (for example tx_msdarts_domain_model_team.groups, a selectCheckBox relating teams to groups through tx_msdarts_team_group_mm) is a first-class field of every generated tool and of the generic record tools. Its physical column only stores the relation count, so the tools never return or accept it raw:
- Read (
<prefix>_get,<prefix>_listwithselectFields,record_search): the field is a list of related UIDs in MM sorting order, e.g."groups": [20, 21]. Resolution goes through TYPO3'sRelationHandler, soMM_opposite_field,MM_match_fieldsand the workspace overlay behave as in the backend. Agroupfield that allows several tables returnstable_uidstrings ("tt_content_12") instead, since a bare integer would be ambiguous. Only fields that were actually selected are resolved: the default list fields never include an MM field, so a plain<prefix>_liststays a single query — opt in withselectFields. - Write (
<prefix>_create,<prefix>_update,<prefix>_update_batch,record_update_batch): pass a JSON array of UIDs ("groups": [20, 21]) or the comma-separated string DataHandler takes ("groups": "20,21"). An empty array or string clears the relation. A non-integer entry is rejected with an error naming the field before anything is written. For agroupfield allowing several tables use thetable_uidform; bare integers are accepted when exactly one table is allowed. DataHandler writes the MM rows itself. - Search: MM fields are not usable as
record_search/record_countconditions and are rejected with an error (see Schema and Search).
The create / update tool descriptions mark these fields as groups (uid list). inline relations, file fields (file / sys_file_reference, see File References) and category fields are unaffected, as are select / group fields without an MM table, which keep their raw column value.
Resources Reference
Resources provide read-only context about the TYPO3 instance. AI clients can read these to understand the environment before taking actions. The schema resources honour the backend user's tables_select grant, exactly as the table_schema tool does.
| Resource | URI | Description |
|---|---|---|
| System Info | typo3://system/info |
TYPO3 version, PHP version, OS; application context and project path for administrators only (null otherwise) |
| Site Configuration | typo3://sites |
All sites with root pages, base URLs, and languages |
| TCA Tables | typo3://schema/tables |
Database tables with labels, limited to those the user may read (tables_select) |
| Backend User | typo3://user/me |
Current user's UID, username, admin status, groups |
| Table Schema | typo3://schema/tables/{tableName} |
Full field schema for a specific table; refused for a table outside the user's tables_select grant |
| Backend Layout | typo3://pages/{pageId}/backend-layout |
Page's backend layout with column positions and grid structure |
Prompts Reference
Prompts provide guided multi-step workflows that instruct the AI through complex tasks.
| Prompt | Parameters | Description |
|---|---|---|
translate_page_content |
pageId, targetLanguageId (0 = all) |
Translate a page and all its content elements to one or all available languages. |
audit_page_seo |
pageId |
Audit SEO metadata, check for missing titles/descriptions/alt text, report findings. |
summarize_page |
pageId |
Generate a content inventory with all elements, translations, and statistics. |
check_translation_status |
pageId, depth (default 3) |
Scan page subtree, report missing translations per language with coverage percentages. |
audit_content_structure |
pageId, depth (default 3) |
Find content in non-existent backend layout columns (orphaned after layout changes). |
migrate_content |
sourcePageId, targetPageId |
Move all content between pages with layout compatibility check. |
Adding Support for Other Extensions
Option 1: Auto-discovery (no code changes)
Go to System > MCP Server > Manage Extension Tables, click Discover Extension Tables, then enable the tables you want. The extension scans TCA for installed extension tables and lets you toggle them on/off with customizable labels and prefixes.
Option 2: Code configuration
Register custom tables in your extension's ext_localconf.php:
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['ms_mcp_server']['tables']['tx_news_domain_model_news'] = [ 'label' => 'News', 'prefix' => 'news', ]; $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['ms_mcp_server']['tables']['tx_blog_domain_model_post'] = [ 'label' => 'Blog Post', 'prefix' => 'blog_post', ];
Unlike auto-discovery, EXTCONF entries are treated as trusted operator configuration and are not run through the table/prefix/label validation the discovery module applies — so only register tables you control.
This automatically creates 9 tools (blog_post_list, blog_post_get, blog_post_create, blog_post_update, blog_post_delete, blog_post_move, plus the batch variants blog_post_delete_batch, blog_post_update_batch, blog_post_move_batch) with fields resolved from TCA. The resolved read and writable fields include many-to-many relation fields; the default list fields (uid, pid, label, disabled) do not.
Optional overrides:
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['ms_mcp_server']['tables']['tx_blog_domain_model_post'] = [ 'label' => 'Blog Post', 'prefix' => 'blog_post', 'listFields' => ['uid', 'pid', 'title', 'datetime'], // Fields shown in list results 'readFields' => ['title', 'datetime', 'bodytext'], // Fields returned by get 'writableFields' => ['title', 'datetime', 'bodytext'], // Fields accepted by create/update ];
Maintenance
The mcp:cleanup command purges expired OAuth authorization codes / access tokens / refresh tokens, dynamically registered OAuth clients that never obtained (or no longer have) an authorization after 30 days, idle MCP sessions past sessionLifetime, and stale rate-limit window rows. Clients created manually in the backend module are never purged:
vendor/bin/typo3 mcp:cleanup
Run it daily. Two ways:
Via TYPO3 Scheduler (recommended)
- System > Scheduler > Scheduled tasks > Add task.
- Class: Execute console commands (
TYPO3\CMS\Scheduler\Task\ExecuteSchedulableCommandTask). - Frequency: pick an interval (
86400seconds for daily) or a cron expression (0 3 * * *for 03:00 every day). - Schedulable Command: select
mcp:cleanupfrom the dropdown. - Save. Use Run task once to verify it works.
The command is auto-discovered via the console.command tag — no extra registration needed.
Via cron
0 3 * * * cd /path/to/typo3-project && vendor/bin/typo3 mcp:cleanup >/dev/null 2>&1
Architecture
HTTP request → McpServerMiddleware (Bearer auth)
→ ErrorHandlingContainer (wraps tools with error handling)
→ McpServerFactory (auto-discovers tools via DI tags)
→ MCP SDK Server (StreamableHttpTransport)
→ Tool execution → JSON response
Tools, resources, and prompts are auto-discovered via DI container tags — no manual registration needed. Adding a new tool is as simple as creating a class with a #[McpTool] attribute.
Error handling is centralized in ErrorHandlingProxy. Tool classes contain only business logic — no try/catch boilerplate, no logger injection.
Every tool returns a typed result object from Classes/Tool/Result/, never a hand-rolled JSON string, and signals failure by throwing ToolCallException — see How a tool answers.
Audit logging records every tool and resource invocation to TYPO3's sys_log table — visible in the backend log module with user ID, tool name, execution time, and outcome.
Development
composer install # Static analysis (PHPStan level max) vendor/bin/phpstan analyse # Code style (Slevomat Coding Standard) vendor/bin/phpcs vendor/bin/phpcbf # Tests vendor/bin/phpunit
A plain composer install resolves to the highest supported TYPO3, so the commands above only ever
see v14. CI runs PHPStan and PHPUnit against both supported branches; to reproduce the v13 leg
locally, pin the branch first and restore afterwards:
composer require --no-update typo3/cms-core:^13.4.35 && composer update vendor/bin/phpstan analyse && vendor/bin/phpunit git checkout composer.json && composer update
License
GPL-2.0-or-later