@@ -3,6 +3,10 @@ import * as consoleFormatter from "jsondiffpatch/formatters/console";
33import * as jsonpatchFormatter from "jsondiffpatch/formatters/jsonpatch" ;
44import { 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" ;
610import { z } from "zod" ;
711
812export 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+ } ;
0 commit comments