MCP server
The Model Context Protocol (MCP) is an open standard for connecting AI assistants to programs that offer them tools: the assistant starts the program, asks which tools it has, and calls them. xoong-mcp is that program for xoong. It gives an assistant the spec, the real operation names of your API, the checker and an explanation of every error, so the screens it writes pass xoong check instead of guessing attributes.
What the assistant gets
Six tools make a loop. The assistant reads the spec, looks up the operations, writes the screen, checks it and fixes what the message says, before it shows you any code.
get_spec: readllms.txt, the whole public API in one short document.list_operations: the operation names of your API, so it never invents one.- Write the screen.
check: runxoong checkon the HTML (or the script) it wrote. Each finding has a line, a column, a message and the fix.explain_errororexplain_syntaxwhen the message is not enough: what a code means, what an element or attribute does and which values it takes.search_docsfor anything else in the documentation.
Back to step 3 until check says No findings. The server also sends this advice itself, in the instructions of its initialize reply.
Install and register
The server reads .xoong/operations.json, so run xoong sync in the project first. Then install the package, which has one program, xoong-mcp, and register it with your assistant as a server that runs over standard input and output (stdio).
npm install --save-dev @xoong/mcp@next
npx xoong sync openapi.json
# for you, in this project
claude mcp add xoong -- npx xoong-mcp
# for everyone who clones the project (writes .mcp.json)
claude mcp add --scope project xoong -- npx xoong-mcp
claude mcp list{
"mcpServers": {
"xoong": {
"command": "npx",
"args": ["-y", "@xoong/mcp@next", "--operations", "/path/to/my-app/.xoong/operations.json"]
}
}
}{
"mcpServers": {
"xoong": { "command": "npx", "args": ["xoong-mcp"] }
}
}{
"mcpServers": {
"xoong": {
"command": "npx",
"args": ["xoong-mcp", "--operations", ".xoong/operations.json", "--docs", "docs"]
}
}
}Claude Desktop keeps its servers in claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\); it does not know your project folder, so the path of operations.json is absolute and npx -y fetches the package. Cursor reads .cursor/mcp.json in the project, or ~/.cursor/mcp.json for every project. Other clients use the same mcpServers shape; look for the place in their documentation.
To see that it answers without an assistant, send it one message:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | npx xoong-mcpWhere it finds its files
The server looks for the table of operations and for the project's docs in the folder it is started from. Three options change that; none is required.
| Option | Default | What it does |
|---|---|---|
--operations <file> | .xoong/operations.json | The table that xoong sync wrote. It is read again on every call. |
--llms <file> | the spec packed with the package | A spec to give get_spec instead of the xoong one. If the packed one is missing, llms.txt in the folder is the last resort. |
--docs <folder> | a docs folder in the working folder, if there is one | The project's own documentation. search_docs searches it first, then the xoong documentation. |
If a file is missing, the tool that needs it answers with an error that says what to do (run `xoong sync <openapi>` first or pass --operations <file>), and the other tools keep working.
The tools
This table is made from the definitions in the server's source, so it is what tools/list returns.
| Tool | What it does | Input |
|---|---|---|
get_spec | Returns llms.txt, the whole public API in one short document. Read it before writing xoong code. | none |
list_operations | The operation names to use in xo-click, xo-submit and <xo-view get>, each with its method and path, query parameters and request fields (a * marks a required one). Without a filter it lists all. The table is read again on every call, so a new xoong sync needs no restart. | filter (string): Only operations whose id or path contains this text. |
check | Runs xoong check on the text it is given and returns the findings (code, line, column, message, fix), or No findings. Give html or script: one of the two is needed. It takes text, not a file name, so it checks what the assistant is about to write. | html (string): The HTML source of a screen.script (string): JavaScript or TypeScript source instead of html: checks html`...` templates and client.load/query/mutate/stream calls. |
explain_error | What a code means and how to fix it: the XO2xx codes of xoong check and the XO1xx codes of xoong doctor and xoong sync. Capitals do not matter. An unknown code gets the list of known ones. | code (string, required) |
explain_syntax | What an element or attribute does, which attributes an element has and which values they take. Without a name it lists every element and attribute. A name that does not exist gets suggestions, so an assistant asks before it invents an attribute. | name (string): An element (xo-table or <xo-table>), an attribute (xo-then), or an attribute of an element ("xo-table page-size"). |
search_docs | A case-insensitive search of the .md files, line by line, up to 40 lines, with file names and line numbers. It searches the project's own docs folder first, then the xoong documentation (marked xoong/ when both are searched). | query (string, required) |
Errors of the tools are not protocol errors: the reply has isError: true and a text the assistant can act on, such as the closest name for an unknown attribute. explain_error knows two families of codes: XO2xx from xoong check and XO1xx from xoong doctor and xoong sync. They are all on the Error codes page, which is made from the same table.
A session, recorded
This is a real exchange. When the page is generated, the site starts xoong-mcp, sends it the messages below over standard input (JSON-RPC 2.0, one message per line) and prints what comes back, on the site's own demo API. The first message is shown as it goes over the wire; the others are shortened to the tool and its arguments, and a tool's reply is the text of its content. An assistant does all of this for you; what you see is its answer.
> {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"assistant","version":"1"}}}
< {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "xoong",
"version": "0.0.54"
},
"instructions": "Call get_spec first. Write xoong screens, then call check on the HTML and fix every error from its message. Never invent an xo-* attribute or an operation: explain_syntax says what an element or attribute does, list_operations gives the real operation names, explain_error explains a code."
}
> tools/list
< get_spec, list_operations, check, explain_error, explain_syntax, search_docs (each with its description and input schema)
> tools/call list_operations {"filter":"delete"}
< [
{
"id": "deletePerson",
"call": "DELETE /api/people/{id}"
}
]
> tools/call check {"html":"<xo-view get=\"GET /api/people\">\n <button xo-click=\"deletePersn\">Delete</button>\n</xo-view>"}
< [
{
"level": "error",
"code": "XO201",
"file": "screen.html",
"line": 2,
"column": 11,
"message": "unknown operation \"deletePersn\" (closest: \"deletePerson\")",
"fix": "Use \"deletePerson\""
}
]
> tools/call explain_error {"code":"XO201"}
< XO201: An operation name in xo-click, xo-submit, <xo-view get> or <xo-form op> does not exist in the synced API. Use the closest name the message gives, or run `xoong sync` if the API changed.
> tools/call explain_syntax {"name":"xo-dialog size"}
< <xo-dialog size>: `sm`, `wide` or `full` (the default is 40rem).
Values: sm, wide, full.
> tools/call check {"html":"<xo-view get=\"GET /api/people\">\n <button xo-click=\"deletePerson\">Delete</button>\n</xo-view>"}
< No findings.
The assistant sent a misspelled operation, deletePersn. check answered with the code, the place and the closest name; explain_error said what XO201 means; the second check came back clean.
What it does not do yet
- No
scaffoldtool. The assistant writes the screens; a tool that generates a view, form or admin screen for an operation is planned. - No resources and no prompts. It declares the
toolscapability only. The spec and the docs are reached through the toolsget_specandsearch_docs. - It reads, and does not run or write. It never runs
sync,doctor,buildordev, and never writes a file. Runxoong syncyourself when the API changes. checktakes text, not file names. To check files, runxoong check.- Standard input and output only, no HTTP transport. It speaks protocol version 2024-11-05 and answers
initialize,ping,tools/listandtools/call; it is written without an SDK, and its only dependency is@xoong/cli.
The spec and the docs come with the package
The npm tarball may hold src/ only, so before the package is packed a script (prepack.mjs) copies llms.txt and the docs/*.md files into src/; the copies are generated, not committed, and removed again afterwards. That is why get_spec and search_docs work in a project that installed @xoong/mcp from npm. In the xoong repository the server reads the files at the root. A test packs a copy of the package into a node_modules layout and asks it for the spec from an empty project.
Without MCP
An assistant that cannot run a server can still work from two things. llms.txt, at the root of the repository, is what get_spec returns: attach it or paste it. And npx xoong check --format json gives it the same findings that the check tool does; paste them back. Without list_operations, give it the operation names from .xoong/operations.json. A test keeps llms.txt honest: it stays within a token budget, names only commands that exist and documents every attribute the code defines.
See also: Command line · Editor support · Error codes · How it is built