-
-
Notifications
You must be signed in to change notification settings - Fork 208
Description
PLIP (Plone Improvement Proposal)
Responsible Persons
Proposer: Jens W. Klein, @jensens
Seconder:
Abstract
Add llms.txt and llms-full.txt views to plone.restapi that auto-generate AI-friendly documentation of all registered REST API services at runtime. The source of truth are structured docstrings on existing service classes, metadata already present in the ZCA registry (permissions, context interfaces, HTTP methods), and the existing auto-generated request/response examples from the test suite.
No ZCML changes, no new dependencies, no hand-maintained docs to keep in sync.
This follows the llms.txt standard — a lightweight, widely adopted convention for making websites and APIs discoverable by large language models.
The goal: any AI agent — Claude Code, Cursor, ChatGPT, LangChain, CrewAI, custom pipelines — can fetch /llms.txt from a Plone site and immediately understand what the REST API offers, how to authenticate, and how Plone's content model works.
Motivation
AI agents need to understand what an API can do before they can use it. Right now there's no machine-friendly way to discover Plone's REST API capabilities at runtime.
plone.restapi has 119 registered service endpoints, JWT auth, and a comprehensive feature set. The infrastructure is all there — it just doesn't tell AI tools about itself.
This matters beyond MCP. There's a separate PLIP for an MCP server, but an MCP server only serves MCP-capable clients (Claude Desktop, ChatGPT Desktop). An llms.txt serves all AI use cases — MCP servers, OpenAI plugins, coding agents, LangChain pipelines, or a developer pasting context into any LLM. In fact, an MCP server could auto-generate its tool definitions from llms.txt, making both PLIPs complementary.
What makes this different from our docs llms.txt?
I'm saying we already have those files from the Sphinx extension. How is your proposal different? I don't get it. The filenames are identical. (@stevepiercy in a chat)
He is talking about
What the docs site provides:
- TOC of Plone's documentation: installation-, admin-, developer-, contributing-, much more
- Static: generic and same for every Plone site in the world
- Helps an AI understand what Plone is and how to develop with it
What I propose — https://mysite.com/llms.txt:
- Runtime-generated description of this specific site's REST API: actual endpoints available, permissions needed, installed add-ons with their own restapi endpoints, how to interact with this site's content
- Includes site-specific context (what this site is about, provided by the site admin)
- Dynamic, site-specific: different for every Plone installation
- Helps an AI operate this specific Plone site: CRUD content, manage workflows, query/search data
They're totally different things. The docs llms.txt is like a textbook about Plone. The site llms.txt is like the control panel manual for a specific machine.
Concrete comparison
The existing 6.docs.plone.org/llms.txt (Sphinx-generated, static):
# Plone Documentation
> This is the community-maintained documentation for the Plone content management system.
## Docs
- [Plone 6 Documentation](https://6.docs.plone.org/_sources/index.md)
- [Get started](https://6.docs.plone.org/_sources/install/index.md)
- [Admin guide](https://6.docs.plone.org/_sources/admin-guide/index.md)
- [Developer guide](https://6.docs.plone.org/_sources/developer-guide/index.md)
- [REST API Endpoints](https://6.docs.plone.org/_sources/plone.restapi/docs/source/endpoints/index.md)
...This is a table of contents linking to documentation pages. Useful for learning about Plone, but tells an agent nothing about a specific site.
The proposed mysite.com/llms.txt (runtime-generated, site-specific):
# Plone REST API — kleinundpartner.at
> Website of Klein & Partner, an IT consultancy in Innsbruck, Austria.
> Content is in German and English. The site provides information about
> services, team members, and blog posts about Python/Plone development.
>
> RESTful hypermedia API. All endpoints accept and return application/json.
> Authenticate via JWT token (@login) or HTTP Basic Auth.
> See llms-full.txt for the full reference including Plone's traversal
> model, parameters, and request/response examples.
## Content (any content object)
- [@content GET](llms-full.txt#content-get) (zope2.View): Retrieve a serialized content object
- [@content PATCH](llms-full.txt#content-patch) (cmf.ModifyPortalContent): Update a content object
- [@content DELETE](llms-full.txt#content-delete) (cmf.DeleteObjects): Delete a content object
## Content (folderish only)
- [@contents GET](llms-full.txt#contents) (zope2.View): List folder contents with batching
- [POST](llms-full.txt#content-post) (cmf.AddPortalContent): Create a new content object
## Workflow (any content object)
- [@workflow GET](llms-full.txt#workflow-get) (zope2.View): Get current state and transitions
- [@workflow POST](llms-full.txt#workflow-post) (cmf.ModifyPortalContent): Execute a transition
## Users & Permissions (site root)
- [@users GET](llms-full.txt#users) (cmf.ManagePortal): List users
- [@groups GET](llms-full.txt#groups) (cmf.ManagePortal): List groups
- [@sharing GET](llms-full.txt#sharing) (zope2.View): Get local role assignments
## Any Context
- [@actions GET](llms-full.txt#actions) (zope2.View): Get available actions
- [@breadcrumbs GET](llms-full.txt#breadcrumbs) (zope2.View): Get breadcrumb navigation
## EasyForm (any easyform object)
- [@easyform-data GET](llms-full.txt#easyform-data) (cmf.ModifyPortalContent): Get form submissionsThe last section only appears if collective.easyform is installed. A different site with different add-ons gets different output. The site description at the top gives the agent immediate context about what this site is about.
Why llms.txt and Not OpenAPI
Plone uses traversal, not routing. Content lives in a tree, and the URL is the path through that tree. REST API endpoints are appended to the content path (/about/team/@workflow). Which endpoints are available depends on the type of object at that path — a Folder supports @contents and POST, a Document does not.
This is fundamentally at odds with OpenAPI's model, which assumes a fixed set of URL paths like /api/users/{id}. There's no way in the OpenAPI spec to express "this endpoint is available when the object at this path provides IFolderish."
An llms.txt can explain this in prose — and LLMs excel at reading prose and reasoning about it. An AI agent that reads "this works on folders, not documents" understands it immediately. OpenAPI tooling would choke on traversal paths.
So llms.txt isn't "OpenAPI but simpler" — it's a fundamentally better fit for Plone because it can teach the agent the concepts, not just list endpoints.
Assumptions
- plone.restapi remains the primary REST interface
- Service classes are registered via the
plone:serviceZCML directive (no change to this) - Add-ons use the same
plone:servicemechanism and are auto-included viaz3c.autoinclude
Proposal & Implementation
1. Site Description via Registry
A plone.registry record (e.g. plone.llms_site_description) where a site admin can provide a short markdown description of the site. The llms.txt view includes this as the blockquote summary at the top.
This is the one piece that can't be auto-generated — what the site is about, what it does, who the audience is. A simple text field in a control panel, editable by the site admin or with AI assistance.
If left empty, the view falls back to a generic description ("Plone CMS REST API").
2. Structured Docstring Convention
Define a simple convention for docstrings on plone.restapi.services.Service subclasses:
class BreadcrumbsGet(Service):
"""Get breadcrumbs for the current context.
Category: Navigation
Examples: breadcrumbs
"""Full version with all optional sections:
class ActionsGet(Service):
"""Get available actions for the current context.
Category: Workflow
Examples: actions
Returns a list of available workflow transitions and object
actions for the current content object.
Parameters:
expand (str): Comma-separated list of elements to expand
"""What goes in the docstring (things the ZCA registry doesn't know):
- First line: summary description
Category:— grouping for the llms.txt outputExamples:— reference totests/http-examples/{name}.req/.respfiles- Free-form description
Parameters:— query/body parameters
What does NOT go in the docstring (already in the ZCA registry):
- HTTP method
- Endpoint name
- Required permission
- Context interface (
for_)
3. Graceful Degradation for Add-ons (and Undocumented Services)
Not every service will have a full docstring — especially third-party add-ons. The views handle this gracefully by using what's always available from the ZCA registry. The levels of participation are:
- Does nothing — endpoint shows up with name, method, permission, context, and factory class name. Already useful for discovery.
- Adds a one-line docstring — now there's a human-readable description. Covers most needs.
- Adds
Category:and description — proper grouping and full context in the output. - Adds
Examples:and ships.req/.respfiles — the full experience with inlined request/response examples.
Each level is incrementally better, none are required. An endpoint with no docstring at all still produces useful output:
### @my-addon-endpoint GET
Context: Any content object (Interface)
Permission: zope2.View
Package: my.addon
No description available. Refer to the add-on documentation.An agent knows the endpoint exists, what method to use, what permission is needed, and on what context it works. That's often enough to try a GET and inspect the response.
4. Utilizing Existing Auto-Generated Examples
plone.restapi already has a sophisticated documentation infrastructure: test_documentation.py makes real API calls against a test instance and captures request/response pairs via save_request_and_response_for_docs(). These are stored as .req/.resp files in tests/http-examples/ — hundreds of them, always in sync with the actual API behavior. They are shipped with the package.
The llms-full.txt view reads these files at runtime and inlines them. The Examples: docstring field provides the mapping from service class to example filename.
Example output in llms-full.txt:
### @breadcrumbs GET
Context: Any content object
Permission: zope2.View
Get breadcrumbs (the path from the site root to the current object).
Request:
```http
GET /plone/front-page/@breadcrumbs HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
```
Response:
```http
HTTP/1.1 200 OK
Content-Type: application/json
{
"items": [
{"title": "Welcome to Plone", "@id": "http://localhost:55001/plone/front-page"}
]
}
```Add-ons that want the same experience can adopt plone.restapi's documentation pattern: a test_documentation.py using save_request_and_response_for_docs() and shipping the generated files. This is optional and should be documented clearly so add-on authors can opt in easily.
5. Service Registry for Introspection
plone.rest currently registers services as ZCA adapters but doesn't maintain an enumerable list. Add a simple module-level registry (populated during ZCML processing) so the views can iterate all registered services without hacking around ZCA internals.
6. llms.txt View
A browser view (not an @-prefixed JSON service) on the site root, returning text/markdown. Reads the site description from the registry, introspects all registered services, reads docstrings and ZCA metadata, groups by category. See the concrete example in the comparison section above.
7. llms-full.txt View
A browser view returning text/markdown with:
a) The site description from the registry record.
b) An explanation of Plone's traversal model:
## How Plone URLs Work
Plone uses traversal, not routing. Content lives in a tree, and the URL
is the path through that tree. Any content object can be addressed by its
path, e.g. `/about/team/john`.
REST API endpoints are appended to the content path with an `@` prefix:
`/about/team/john/@workflow`. Which endpoints are available depends on
the type of object at that path:
- A Folder at `/about` supports `@contents` (listing children) and
`POST` (creating new content inside it).
- A Document at `/about/history` does not.
- All content supports `@workflow`, `@actions`, `@sharing`, etc.
- Some endpoints like `@users` and `@groups` only work on the site root.
To find out what's at a path, GET it. The response includes the content
type and available operations.c) Detailed per-endpoint documentation with context, permission, description, parameters — from docstrings and ZCA metadata.
d) Real request/response examples — inlined from the existing tests/http-examples/*.req and *.resp files where available.
Deliverables
- Docstring convention documented in plone.restapi contributing guide
- Documentation for add-on authors on how to opt into richer llms.txt output
- Registry record and control panel field for site description
- Service registry in
plone.restfor enumeration of registered services llms.txtview in plone.restapi (summary)llms-full.txtview in plone.restapi (full reference with traversal explanation and inlined examples)- Docstrings backfilled on the ~20 most important existing services
- Tests
MVP
- Docstring convention defined and documented
- Registry record for site description (
plone.llms_site_description) - Service registry in plone.rest for introspection
-
llms.txtview returning site description, categorized summary with permissions and context -
llms-full.txtview with site description, traversal explanation, endpoint details, and inlined request/response examples - Docstrings on the 20 most important services (content CRUD, search, workflow, users, types, auth)
Later
- Docstring coverage for all 119 existing services
- Prompt recipes section: common multi-step workflows ("create a page, add blocks, publish")
- Per-content-type schema documentation
- Volto block type documentation with examples
- Documentation for add-on authors on how to ship examples for their endpoints
- Auto-detect multilingual setup: if
plone.app.multilingualis installed, include available languages and language root structure (/de,/en,/fr) in the output - Add-on documentation snippets via adapter: add-ons can register an
ILLMSDocumentationadapter that returns a markdown snippet explaining their specific concepts, injected intollms-full.txt
Risks
Minimal. This adds two read-only views, a docstring convention, and a registry record. No changes to existing behavior, no new dependencies, no new security surface. The views themselves should be accessible without special permissions (zope2.View) so agents can discover the API — they still need proper authentication to actually use the endpoints.
Note on AI-Assisted Implementation
I plan to implement this PLIP with AI assistance (Claude Code). If there's an ongoing discussion or policy about accepting AI-assisted code in Plone core, I'd like to know upfront whether this affects the acceptance of the PLIP.
Participants
- Jens W. Klein, @jensens
Metadata
Metadata
Assignees
Labels
Type
Projects
Status