Skip to content

Commit 1665657

Browse files
committed
feat: MCP support for json5/yaml/toml/xml/html
1 parent 03e75ea commit 1665657

File tree

4 files changed

+202
-26
lines changed

4 files changed

+202
-26
lines changed

packages/diff-mcp/package-lock.json

Lines changed: 97 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/diff-mcp/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
},
1212
"scripts": {
1313
"build": "tsc",
14+
"start": "node build/index.js",
1415
"type-check": "tsc --noEmit",
1516
"lint": "biome check --error-on-warnings .",
1617
"test": "vitest --coverage",
@@ -23,12 +24,17 @@
2324
},
2425
"keywords": ["mcp", "compare", "diff", "json", "jsondiffpatch"],
2526
"dependencies": {
26-
"jsondiffpatch": "^0.7.3",
2727
"@dmsnell/diff-match-patch": "^1.1.0",
2828
"@modelcontextprotocol/sdk": "^1.8.0",
29+
"fast-xml-parser": "^5.0.9",
30+
"js-yaml": "^4.1.0",
31+
"json5": "^2.2.3",
32+
"jsondiffpatch": "^0.7.2",
33+
"smol-toml": "^1.3.1",
2934
"zod": "^3.24.2"
3035
},
3136
"devDependencies": {
37+
"@types/js-yaml": "^4.0.9",
3238
"@vitest/coverage-v8": "^3.0.9",
3339
"tslib": "^2.6.2",
3440
"typescript": "^5.8.2",

packages/diff-mcp/src/server.ts

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import * as consoleFormatter from "jsondiffpatch/formatters/console";
33
import * as jsonpatchFormatter from "jsondiffpatch/formatters/jsonpatch";
44
import { create } from "jsondiffpatch/with-text-diffs";
55

6+
import { XMLParser } from "fast-xml-parser";
7+
import yaml from "js-yaml";
8+
import json5 from "json5";
9+
import { parse as tomlParse } from "smol-toml";
610
import { z } from "zod";
711

812
export const createMcpServer = () => {
@@ -21,26 +25,26 @@ export const createMcpServer = () => {
2125
"compare text or data and get a readable diff",
2226
{
2327
state: z.object({
24-
left: z
25-
.string()
26-
.or(z.record(z.string(), z.unknown()))
27-
.or(z.array(z.unknown()))
28-
.describe("The left side of the diff."),
29-
right: z
30-
.string()
31-
.or(z.record(z.string(), z.unknown()))
32-
.or(z.array(z.unknown()))
33-
.describe(
34-
"The right side of the diff (to compare with the left side).",
35-
),
28+
left: inputDataSchema.describe("The left side of the diff."),
29+
leftFormat: formatSchema
30+
.optional()
31+
.describe("format of left side of the diff"),
32+
right: inputDataSchema.describe(
33+
"The right side of the diff (to compare with the left side).",
34+
),
35+
rightFormat: formatSchema
36+
.optional()
37+
.describe("format of right side of the diff"),
3638
outputFormat: z
3739
.enum(["text", "json", "jsonpatch"])
40+
.default("text")
3841
.describe(
3942
"The output format. " +
40-
"text: human readable text diff, " +
43+
"text: (default) human readable text diff, " +
4144
"json: a compact json diff (jsondiffpatch delta format), " +
4245
"jsonpatch: json patch diff (RFC 6902)",
43-
),
46+
)
47+
.optional(),
4448
}),
4549
},
4650
({ state }) => {
@@ -56,23 +60,37 @@ export const createMcpServer = () => {
5660
},
5761
});
5862

59-
const delta = jsondiffpatch.diff(state.left, state.right);
60-
63+
const left = parseData(state.left, state.leftFormat);
64+
const right = parseData(state.right, state.rightFormat);
65+
const delta = jsondiffpatch.diff(left, right);
6166
const output =
6267
state.outputFormat === "json"
6368
? delta
6469
: state.outputFormat === "jsonpatch"
6570
? jsonpatchFormatter.format(delta)
66-
: consoleFormatter.format(delta);
71+
: consoleFormatter.format(delta, left);
72+
73+
const legend =
74+
state.outputFormat === "text"
75+
? `\n\nlegend:
76+
- lines starting with "+" indicate new property or item array
77+
- lines starting with "-" indicate removed property or item array
78+
- "value => newvalue" indicate property value changed
79+
- "x: ~> y indicate array item moved from index x to y
80+
- text diffs are lines that start "line,char" numbers, and have a line below
81+
with "+" under added chars, and "-" under removed chars.
82+
- you can use this exact representations when showing differences to the user
83+
\n`
84+
: "";
6785

6886
return {
6987
content: [
7088
{
7189
type: "text",
7290
text:
73-
typeof output === "string"
91+
(typeof output === "string"
7492
? output
75-
: JSON.stringify(output, null, 2),
93+
: JSON.stringify(output, null, 2)) + legend,
7694
},
7795
],
7896
};
@@ -93,3 +111,63 @@ export const createMcpServer = () => {
93111

94112
return server;
95113
};
114+
115+
const inputDataSchema = z
116+
.string()
117+
.or(z.record(z.string(), z.unknown()))
118+
.or(z.array(z.unknown()));
119+
120+
const formatSchema = z
121+
.enum(["text", "json", "json5", "yaml", "toml", "xml", "html"])
122+
.default("json5");
123+
124+
const parseData = (
125+
data: z.infer<typeof inputDataSchema>,
126+
format: z.infer<typeof formatSchema> | undefined,
127+
) => {
128+
if (typeof data !== "string") {
129+
// already parsed
130+
return data;
131+
}
132+
if (!format || format === "text") {
133+
return data;
134+
}
135+
136+
if (format === "json") {
137+
try {
138+
return JSON.parse(data);
139+
} catch {
140+
// if json is invalid, try json5
141+
return json5.parse(data);
142+
}
143+
}
144+
if (format === "json5") {
145+
return json5.parse(data);
146+
}
147+
if (format === "yaml") {
148+
return yaml.load(data);
149+
}
150+
if (format === "xml") {
151+
const parser = new XMLParser({
152+
ignoreAttributes: false,
153+
preserveOrder: true,
154+
});
155+
return parser.parse(data);
156+
}
157+
if (format === "html") {
158+
const parser = new XMLParser({
159+
ignoreAttributes: false,
160+
preserveOrder: true,
161+
unpairedTags: ["hr", "br", "link", "meta"],
162+
stopNodes: ["*.pre", "*.script"],
163+
processEntities: true,
164+
htmlEntities: true,
165+
});
166+
return parser.parse(data);
167+
}
168+
if (format === "toml") {
169+
return tomlParse(data);
170+
}
171+
format satisfies never;
172+
throw new Error(`unsupported format: ${format}`);
173+
};

packages/diff-mcp/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"esModuleInterop": true,
1111
"forceConsistentCasingInFileNames": true,
1212
"strict": true,
13-
"skipLibCheck": false,
13+
"skipLibCheck": true,
1414
"noFallthroughCasesInSwitch": true,
1515
"noImplicitReturns": true,
1616
"noUncheckedIndexedAccess": true,

0 commit comments

Comments
 (0)