MCP server
this site, not the packageThis site answers to agents as well as to people. One read-only endpoint, six tools and four resources, over the same bytes the pages render.
readOnlyHint, so a client that honours the hint calls them without prompting.
There will never be a seventh tool that writes — that is what makes calling
these unprompted safe, and it is enforced by a test rather than by intention.
Add it
One URL. Streamable HTTP, no key, no install.
Any MCP client
The endpoint is https://simpleui.dennisrahmen.dev/mcp. It is stateless, so a client may reconnect
or fan out across processes without leaving sessions behind on the server.
{
"mcpServers": {
"dr-simple-ui": {
"type": "http",
"url": "https://simpleui.dennisrahmen.dev/mcp"
}
}
}
Claude Code
Or add it to a project's .mcp.json with the block above.
claude mcp add --transport http dr-simple-ui https://simpleui.dennisrahmen.dev/mcp
The six tools
Six verbs, no overlap. They are shaped around one workflow — an agent writing a page in a consuming app, knowing roughly what it wants and needing the exact markup plus enough semantics to pick the right variant.
| Tool | What it answers |
|---|---|
search |
“What is there for a sortable table with status badges?” Ranks examples, classes, tokens and pages together. Returns references, never markup — a search that returned markup would spend the context window on the first call. |
get_example |
The markup for up to five examples, byte-for-byte what this site renders. Valid
in a .razor page and an .html file alike.
|
describe_class |
What a class actually does: the declarations from the shipped stylesheet, its cascade layer, its modifiers, and which examples use it. Also the way to check a class your app already styles before upgrading. |
get_page |
Every example on one page — one call instead of five. With no id, the page list. |
get_tokens |
The tokens as an ordered array of blocks, media condition included, for writing
brand.css.
|
get_integration_guide |
The host page and its load order, the branding recipe, the JavaScript and C# surface, or the rules a consuming app follows. |
The whole loop
search → get_example → describe_class. Nothing else
is needed to write a page.
search { "query": "filter bar above a sortable table", "installedVersion": "0.2.0" }
→ hits: [ { kind: "page", ref: "/toolbar", title: "Toolbar", score: 78 },
{ kind: "example", ref: "Toolbar/FilterBar", classes: ["toolbar", "toolbar-field", …] },
{ kind: "example", ref: "Table/SortableTable", classes: ["table", "table-sortable", …] },
{ kind: "class", ref: ".table-sticky", blurb: "In layer dr.paint. Used by 2 example(s)." } ]
get_example { "ids": ["Toolbar/FilterBar", "Table/SortableTable"] }
→ the markup, byte-for-byte what this site renders, plus every class it uses
and the release each one first shipped in.
describe_class { "names": ["table-sortable", "col-num"] }
→ the declarations the shipped stylesheet holds for each, its cascade layer,
its modifiers, and which examples use it.
The four resources
Whole artefacts, addressable by URI. A resource is attached by the person, not pulled by the model — which is the right party to decide whether 200 KB of stylesheet belongs in a context window.
| URI | Type | What it is |
|---|---|---|
drsimpleui://stylesheet |
text/css |
The whole shipped stylesheet, exactly as the package delivers it. |
drsimpleui://tokens |
application/json |
The token export, as an ordered array of blocks. |
drsimpleui://version |
application/json |
What this catalogue was built from, and the latest released version. |
drsimpleui://docs/{name} |
text/markdown |
One documentation file: getting-started, architecture,
CLAUDE.consuming-app, releasing.
|
get_tokens and get_integration_guide
duplicate two of these on purpose: the tool is the guaranteed path, the resource the ergonomic
one.
Say which version you have
This site is built from main and can be ahead of any release. Every tool takes
installedVersion.
meta block names anything your version does not have —
“3 of these are not in 0.2.0: .popover, .search-panel,
--brand-glow. Upgrade DR.Simple_UI, or use something else.” The results still
come back: filtering them out silently would hide that an upgrade is the fix, which is
usually the right answer. Every class and token also carries its own since.
Limits
| Limit | Value | Why |
|---|---|---|
| Concurrent requests | 16, plus 8 queued | The actual control. Not partitioned, so there is nothing to spoof around. |
| Per caller | 60 burst, 30/min |
Fairness, not security: behind a proxy whose addresses we do not control,
per-IP limiting is not a security measure. A token bucket because real agent
traffic is bursty — six get_example calls, then nothing.
|
| Request body | 256 KB | JSON-RPC calls here are a few hundred bytes. |
| Per call | 25 / 5 / 10 |
Hits from search, ids per get_example, names per
describe_class. Markup is capped at 8 KB per example and cut
at a tag boundary, so a truncated response still parses.
|
/mcp alone, never globally. A global limiter
would also count this site's own SignalR upgrades and every static asset, so one person
browsing would trip a ceiling sized for tool calls. A rejected call answers
429 with Retry-After: 60.
Then give the agent the rules
The server answers questions. It does not stop an agent inventing a class name or wrapping a table in a component.
docs/CLAUDE.consuming-app.md (opens in a new tab) into your app's own
CLAUDE.md, or fetch it through
get_integration_guide { "section": "rules" }. It is the short list of things
that go wrong: inventing token names, restyling the frame, and reaching for a component
where a class exists.
describe_class
Up to ten class names per call. The declarations come from the stylesheet the running app
serves, so they are what ships rather than a transcription of it, and
usedByExamples points at the pages that show the class in context.
Pass installedVersion and meta.warning names everything in the
answer that your version does not have. That is the whole reason since exists:
this site is built from main and can be ahead of the package you installed.
describe_class { "names": ["health-badge", "stat-target"], "installedVersion": "0.4.0" }
{
"meta": {
"branch": "main",
"commit": "9ac438c",
"builtUtc": "2026-08-03T11:20:44Z",
"latestRelease": "0.4.0",
"installedVersion": "0.4.0",
"warning": "2 of these are not in 0.4.0: .health-badge, .stat-target. Upgrade DR.Simple_UI, or use something else."
},
"classes": [
{
"name": ".health-badge",
"layer": "dr.paint",
"declarations": ["display: inline-flex", "align-items: center", "gap: var(--space-3)", "…"],
"modifiers": [".health-badge--live", ".health-badge--streaming", ".health-badge--down"],
"usedByExamples": ["/status#a-connection-s-state"],
"since": "unreleased"
},
{
"name": ".stat-target",
"layer": "dr.paint",
"declarations": ["font-size: var(--text-2)", "color: var(--muted)"],
"modifiers": [],
"usedByExamples": ["/stat#target-link-and-unavailable"],
"since": "unreleased"
}
],
"notFound": []
}
# `declarations` is read from the stylesheet the running app serves, so it is
# what ships rather than a transcription. `since` comes from the git tags via
# build/class-history.sh, and `meta.warning` names everything in the answer
# that the version you passed does not have — so an agent can tell "copy this"
# from "upgrade first" without asking. At most ten names per call.