json-render-mcp
Skill30 starsChanged 9 days agoMITView on GitHub
Builds interactive json-render panels with PMX Canvas native tools or standalone MCP Apps. Use for stateful forms, repeated content, and AX actions in PMX Canvas, or MCP-hosted UIs in Claude, ChatGPT, Cursor, and VS Code.
- Installs packages
In this repository20 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- control-session-orchestratorSkill
- data-analysisSkill
- doc-coauthoringSkill
- frontend-designSkill
- json-render-codegenSkill
- json-render-coreSkill
- json-render-inkSkill
- json-render-reactSkill
- json-render-shadcnSkill
- playwright-cliSkill
- pmx-canvas-orchestrationSkill
- pmx-canvasSkill
- pmx-canvas-testingSkill
- published-consumer-e2eSkill
- tufte-vizSkill
- web-artifacts-builderSkill
- web-design-guidelinesSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 1 is distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 17
Read canvas://schema (or use canvas_render { action: "describe-schema" }) for the installed component catalog.
- Very specific: line 22
Create it with canvas_render { action: "add-json-render", title, spec }.
- Very specific: line 30
Put initial values in spec-level state, alongside root and elements.
- Very specific: line 32
Use JSON Pointer paths: value: { "$bindState": "/form/title" } for Input, Textarea, or Select.
- Distinctive: line 65
Use an explicit validation step and state-based gating.
- Very specific: line 129
Use this path only when building a separate MCP Apps server rather than a native PMX panel.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
@json-render/mcp
MCP Apps integration that serves json-render UIs as interactive MCP Apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
In pmx-canvas, prefer the native canvas_render { action: "add-json-render" } and
canvas_render { action: "add-graph" } composite actions first. They store validated specs
directly in canvas node state and render them through the local pmx-canvas viewer route without
needing a separate sidecar server.
PMX Canvas workflow (json-render 0.21)
- Read
canvas://schema(or usecanvas_render { action: "describe-schema" }) for the installed component catalog. PMX includes shadcn components and its own charts; do not assume every component or action from an upstream example is registered. - Build a complete
{ root, elements, state }spec. Validate withcanvas_render { action: "validate", type: "json-render", spec }. - Create it with
canvas_render { action: "add-json-render", title, spec }. For incremental generation, useaction: "stream-json-render", reuse the returnednodeIdfor subsequent patches, and setdone: trueon the final call. - Inspect the panel and exercise its inputs. Schema acceptance does not prove that an action reached the server or that a form edit was persisted.
State, forms, and repeated content
- Put initial values in spec-level
state, alongsiderootandelements. PMX seeds its viewer from this object; do not putinitialStateon a component. - Use JSON Pointer paths:
value: { "$bindState": "/form/title" }forInput,Textarea, orSelect;checked: { "$bindState": "/form/urgent" }forCheckboxorSwitch. Supply the catalog's requiredlabelandnameprops.$statereads a value only;$bindStatealso writes edits.props.statePathis not input binding. repeat,watch,visible,on, andslotsare element fields, not props.repeat: { "statePath": "/projects", "key": "id" }repeats the container's children. Within those children, a nested container can userepeat: { "statePath": { "$item": "tasks" }, "key": "id" }to iterate the current project's tasks. Do not use"/tasks"(a root path) for that nested array.$itemand$indexrefer to the innermost repeat; usechecked: { "$bindItem": "done" }to edit that task'sdonefield.- Card body content uses
children: ["bodyId"]. Named regions useslots: { "header": ["headingId"], "footer": ["actionsId"] }. Every referenced ID must exist inelements; do not put body content inslots.default. A header slot replaces the Card's title and description props; without it, those props work as before. - Watchers skip mount:
watch: { "/form/title": { "action": "setState", "params": { "statePath": "/dirty", "value": true } } }runs after the value changes, not to initialize/dirty. Seed initial and derived defaults explicitly. Avoid watchers that write back to their own watched paths and create loops.
Actions and persistence boundaries
- Bind buttons with
on.press, for example an action namedax.work.createorax.steer, with its AX payload inparams. PMX forwards only registered handlers; invented action names cannot execute arbitrary tools, shell commands, or requests. The server still validates the payload and the node's AX capabilities. - AX handlers are fire-and-forget postMessages. Their
onSuccesscallbacks mean local dispatch completed, not server acknowledgment. Do not clear a draft or show “Saved” on that basis. Confirm via reflected AX state under reserved/ax(where applicable), or query the server's AX state/timeline. Do not use/axfor drafts. - Built-in
validateFormwrites{ valid, errors }to/formValidation(orparams.statePath) and returns without runningonSuccess. Do not chain submission through that callback, or assume a following action in an array is validation-gated. Use an explicit validation step and state-based gating; server-side validation is still required for any submitted data. - Form edits,
setState, and$bindItemmutate viewer-local state, not the stored spec. Iframe reloads, including spec/stream updates, reset it from the latest spec's initial state. Finish streaming before collecting input; persist important changes through an explicit supported server interaction. Stored canvas nodes survive reloads, but unsent local drafts do not.
Example: editable task draft
Pass this object to canvas_render. Editing either field sets a local dirty flag;
the button requests a real AX work item without pretending the request was saved.
{
"action": "add-json-render",
"title": "Task draft",
"spec": {
"root": "card",
"state": { "form": { "title": "Review release notes", "detail": "Check the upgrade caveats." }, "dirty": false },
"elements": {
"card": {
"type": "Card",
"props": { "title": "Task draft" },
"children": ["fields"],
"slots": { "footer": ["request"] }
},
"fields": {
"type": "Stack",
"props": { "direction": "vertical", "gap": "md" },
"children": ["title", "detail"],
"watch": {
"/form/title": { "action": "setState", "params": { "statePath": "/dirty", "value": true } },
"/form/detail": { "action": "setState", "params": { "statePath": "/dirty", "value": true } }
}
},
"title": {
"type": "Input",
"props": { "label": "Title", "name": "title", "value": { "$bindState": "/form/title" } }
},
"detail": {
"type": "Textarea",
"props": { "label": "Details", "name": "detail", "value": { "$bindState": "/form/detail" } }
},
"request": {
"type": "Button",
"props": { "label": "Request task", "variant": "primary" },
"on": {
"press": {
"action": "ax.work.create",
"params": { "title": { "$state": "/form/title" }, "detail": { "$state": "/form/detail" } }
}
}
}
}
}
}The draft remains visible after dispatch. Verify that the work item appears in AX state before reporting success. Repeated clicks can create duplicate work items.
Standalone MCP App quick start
Use this path only when building a separate MCP Apps server rather than a native
PMX panel. PMX's canvas_render path needs none of the server, bundler, or client
configuration below.
Server (Node.js)
import { createMcpApp } from "@json-render/mcp";
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "node:fs";
const catalog = defineCatalog(schema, {
components: { ...shadcnComponentDefinitions },
actions: {},
});
const server = createMcpApp({
name: "My App",
version: "1.0.0",
catalog,
html: fs.readFileSync("dist/index.html", "utf-8"),
});
await server.connect(new StdioServerTransport());Client (React, inside iframe)
import { useJsonRenderApp } from "@json-render/mcp/app";
import { JSONUIProvider, Renderer } from "@json-render/react";
function McpAppView({ registry }) {
const { spec, loading, error } = useJsonRenderApp();
if (error) return <div>Error: {error.message}</div>;
if (!spec) return <div>Waiting...</div>;
return (
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<Renderer spec={spec} registry={registry} loading={loading} />
</JSONUIProvider>
);
}Architecture
createMcpApp()creates anMcpServerthat registers arender-uitool and aui://HTML resource- The tool description includes the catalog prompt so the LLM knows how to generate valid specs
- The HTML resource is a Vite-bundled single-file React app with json-render renderers
- Inside the iframe,
useJsonRenderApp()connects to the host viapostMessageand renders specs
Server API
createMcpApp(options)- main entry, creates a full MCP serverregisterJsonRenderTool(server, options)- register a json-render tool on an existing serverregisterJsonRenderResource(server, options)- register the UI resource
Client API (@json-render/mcp/app)
useJsonRenderApp(options?)- React hook, returns{ spec, loading, connected, error, callServerTool }buildAppHtml(options)- generate HTML from bundled JS/CSS
Building the iframe HTML
Bundle the React app into a single self-contained HTML file using Vite + vite-plugin-singlefile:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";
export default defineConfig({
plugins: [react(), viteSingleFile()],
build: { outDir: "dist" },
});Client Configuration
Cursor (.cursor/mcp.json)
{
"mcpServers": {
"my-app": {
"command": "npx",
"args": ["tsx", "server.ts", "--stdio"]
}
}
}Claude Desktop
{
"mcpServers": {
"my-app": {
"command": "npx",
"args": ["tsx", "/path/to/server.ts", "--stdio"]
}
}
}Dependencies
# Server
npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk
# Client (iframe)
npm install @json-render/react @json-render/shadcn react react-dom
# Build tools
npm install -D vite @vitejs/plugin-react vite-plugin-singlefileDoes it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

