@nerdo/json-emitter-mcp
v0.4.0
Published
MCP server that converts YAML to validated JSON — a safer emission surface for LLMs generating structured JSON payloads.
Downloads
29
Readme
json-emitter
An MCP server with one tool: emit_json(yaml, jsonSchema?, options?). It converts a YAML 1.2 payload to JSON, optionally validating against a JSON Schema. The response body is the JSON. Failures raise an MCP error with a message naming the phase and location. A returned response is always a valid JSON payload.
Why
LLMs emitting JSON directly frequently drop escape-within-prose-strings at length — a stray " inside a long "text" value silently breaks the whole document (e.g. SyntaxError at position 12021). YAML block scalars (|, >) eliminate the context switch: inside |, quotes/colons/pipes/asterisks are just prose.
The tool returns the JSON as the response body rather than wrapping it in an envelope, and raises errors rather than returning them as data. Callers work with the JSON directly; whatever processing they do with it afterwards is their call.
Install & run
Local checkout (no publish):
bun install
bun run src/main.tsOr build and run the bundle:
bun run build
bun dist/main.jsBy default it listens on stdio. To listen on Streamable HTTP:
JSON_EMITTER_TRANSPORT=http JSON_EMITTER_HTTP_PORT=3000 bun src/main.ts
# or
bun src/main.ts --transport=http --port=3000
# then POST JSON-RPC to http://127.0.0.1:3000/mcpUsing it from a Claude Code MCP config
From npm (once published):
{
"mcpServers": {
"json-emitter": {
"command": "npx",
"args": ["-y", "@nerdo/json-emitter-mcp"]
}
}
}From a local checkout:
{
"mcpServers": {
"json-emitter": {
"command": "bun",
"args": ["run", "/absolute/path/to/json-emitter-mcp/src/main.ts"]
}
}
}The tool
emit_json({
yaml: string,
jsonSchema?: object,
options?: { pretty?: boolean }
})Success
The tool returns with content[0].text set to the JSON — literally, with no envelope:
{"text":"TI-13196 has been \"explore\" status...","count":3}Default output is compact. Pass options: {pretty: true} for 2-space indentation.
Failure
The tool raises an MCP error. MCP clients surface it as an exception from callTool(...); LLMs see an error in the tool result. The error message names the phase and location.
Parse failure (malformed YAML):
YAML parse error at line 1, column 19 (offset 18):
Missing closing "quote at line 1, column 19:
1 | foo: "unterminated
1 | ^Schema-compile failure (the user-supplied jsonSchema itself is invalid):
JSON Schema is invalid and could not be compiled: schema is invalid: data/type must be equal to one of the allowed valuesValidation failure (YAML parses, but the data doesn't match the schema):
JSON Schema validation failed with 1 issue(s):
/text: must NOT have more than 3000 characters (keyword: maxLength, params: {"limit":3000})For common parse errors (duplicate keys, multi-document streams, tab indentation, inconsistent indentation), the message appends a one-line Hint: pointing at the fix:
YAML parse error at line 2, column 1 (offset 5):
Map keys must be unique at line 2, column 1:
1 | a: 1
2 | a: 2
2 | ^
Hint: Remove the duplicate key — YAML 1.2 mappings must have unique keys. If you need multiple values, use a sequence.Resources
The server also exposes one MCP resource for clients that list them:
| URI | MIME | Purpose |
|-----|------|---------|
| json-emitter://docs/yaml-authoring-guide | text/markdown | Quoting rules, block-scalar usage, what the tool rejects vs. silently converts, and why a jsonSchema catches indentation-driven structural bugs. |
Read it via resources/read in any MCP client, or via the Inspector.
Example
Input YAML:
text: |
TI-13196 has been "explore" status for a full sprint — Monday worth a check-in.
Commits like "fix: thing" and times like 10:30am pass through intact.
count: 3Optional JSON Schema:
{
"type": "object",
"required": ["text", "count"],
"properties": {
"text": { "type": "string", "maxLength": 3000 },
"count": { "type": "integer" }
}
}Tool response content (success):
{"text":"TI-13196 has been \"explore\" status for a full sprint — Monday worth a check-in.\nCommits like \"fix: thing\" and times like 10:30am pass through intact.\n","count":3}That's the response body — already valid JSON, already compact, the contract's output.
Authoring YAML for this tool
- Put long or multi-line text under a
|block scalar. Inside|, quotes/colons/pipes/asterisks are just prose. - Quote strings that look like booleans, numbers, dates, or null (
yes,on,12,2024-01-01). YAML 1.2 Core Schema is used — the Norway problem is off (nostays"no", notfalse), but ambiguous-looking plain scalars are still safer quoted. - Quote all-digit identifiers (git SHAs, phone numbers, IDs past 2⁵³) and scientific-notation-shaped strings (
"1e2") to avoid precision loss and silent numeric coercion. - Pass the target
jsonSchemawhenever one exists. Without it, only parse errors can be caught — the schema is also the one mechanical defense against indentation-driven structural bugs.
For a longer reference — including what the tool rejects outright and the inherently non-catchable residual — see the json-emitter://docs/yaml-authoring-guide MCP resource.
Libraries
@modelcontextprotocol/sdk— MCP server + stdio/Streamable HTTP transportsyaml— YAML 1.2 Core Schema, strict mode, pretty errorsajv+ajv-formats— JSON Schema 2020-12 validationarktype— input boundary validationulid— error instance IDs
Dev loop
bun run validate # test → typecheck → test → build
bun run test # just tests
bun run typecheck # just tsc --noEmit
bun run lint # biome lint
bun run format # biome format --writeRun the Inspector against a local copy:
bunx @modelcontextprotocol/inspector bun src/main.ts
# or CLI mode:
bunx @modelcontextprotocol/inspector --cli bun src/main.ts --method tools/list
bunx @modelcontextprotocol/inspector --cli bun src/main.ts --method tools/call --tool-name emit_json --tool-arg yaml='foo: bar'License
MIT.
