json-explorer-mcp 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +42 -4
- package/dist/tools/navigation.d.ts +18 -0
- package/dist/tools/navigation.js +45 -3
- package/dist/tools/structure.d.ts +2 -0
- package/dist/tools/structure.js +2 -2
- package/dist/utils/path-helpers.js +23 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,11 +3,11 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { jsonInspect, jsonSchema, jsonValidate } from "./tools/structure.js";
|
|
6
|
-
import { jsonKeys, jsonGet, jsonSet } from "./tools/navigation.js";
|
|
6
|
+
import { jsonKeys, jsonGet, jsonSet, jsonFormat } from "./tools/navigation.js";
|
|
7
7
|
import { jsonSearch, jsonSample, jsonStats } from "./tools/query.js";
|
|
8
8
|
const server = new McpServer({
|
|
9
9
|
name: "json-explorer",
|
|
10
|
-
version: "1.0.
|
|
10
|
+
version: "1.0.6",
|
|
11
11
|
});
|
|
12
12
|
// Tool: json_inspect - Get file overview
|
|
13
13
|
server.tool("json_inspect", "Get an overview of a JSON file including size, structure type, and a depth-limited preview. Use this first to understand what you're working with.", {
|
|
@@ -103,13 +103,49 @@ server.tool("json_set", "Set a value at a specific JSONPath. Supports schema val
|
|
|
103
103
|
inferSchema: z.boolean().optional().describe("If true, infers schema from existing value at path and validates against it. Defaults to false."),
|
|
104
104
|
dryRun: z.boolean().optional().describe("If true, validates and returns result without writing to file. Defaults to false."),
|
|
105
105
|
outputFile: z.string().optional().describe("Output file path. If not provided, writes to the source file."),
|
|
106
|
-
|
|
106
|
+
strict: z.boolean().optional().describe("Enable AJV strict mode. Set to false for schemas with custom keywords. Defaults to true."),
|
|
107
|
+
}, { readOnlyHint: false }, async ({ file, path, value, schema, inferSchema, dryRun, outputFile, strict }) => {
|
|
107
108
|
try {
|
|
108
109
|
const result = await jsonSet(file, path, value, {
|
|
109
110
|
schema,
|
|
110
111
|
inferSchema,
|
|
111
112
|
dryRun,
|
|
112
113
|
outputFile,
|
|
114
|
+
strict,
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: JSON.stringify(result, null, 2),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
return {
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: "text",
|
|
130
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
isError: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
// Tool: json_format - Reformat JSON file
|
|
138
|
+
server.tool("json_format", "Reformat a JSON file with configurable indentation and optional key sorting. Useful for normalizing JSON formatting or preparing files for version control.", {
|
|
139
|
+
file: z.string().describe("Absolute path to the JSON file to format"),
|
|
140
|
+
indent: z.number().optional().describe("Number of spaces for indentation. Use 0 for compact single-line output. Defaults to 2."),
|
|
141
|
+
sortKeys: z.boolean().optional().describe("Sort object keys alphabetically (recursive). Defaults to false."),
|
|
142
|
+
outputFile: z.string().optional().describe("Output file path. If not provided, overwrites the source file."),
|
|
143
|
+
}, { readOnlyHint: false }, async ({ file, indent, sortKeys, outputFile }) => {
|
|
144
|
+
try {
|
|
145
|
+
const result = await jsonFormat(file, {
|
|
146
|
+
indent,
|
|
147
|
+
sortKeys,
|
|
148
|
+
outputFile,
|
|
113
149
|
});
|
|
114
150
|
return {
|
|
115
151
|
content: [
|
|
@@ -171,7 +207,8 @@ server.tool("json_validate", "Validate JSON data against a JSON Schema. Schema c
|
|
|
171
207
|
schemas: z.record(z.union([z.string(), z.object({}).passthrough()])).optional().describe("Pre-register schemas by URI. Map of URI to schema file path or inline schema object."),
|
|
172
208
|
schemaDir: z.string().optional().describe("Directory containing schema files. All .json files will be loaded and registered by their $id."),
|
|
173
209
|
schemaId: z.string().optional().describe("When using schemaDir, the $id of the schema to use as entrypoint."),
|
|
174
|
-
|
|
210
|
+
strict: z.boolean().optional().describe("Enable AJV strict mode. Set to false for schemas with custom keywords. Defaults to true."),
|
|
211
|
+
}, { readOnlyHint: true }, async ({ file, schema, path, errorLimit, resolveLocalRefs, resolveNetworkRefs, schemas, schemaDir, schemaId, strict }) => {
|
|
175
212
|
try {
|
|
176
213
|
const result = await jsonValidate(file, schema ?? {}, path, {
|
|
177
214
|
errorLimit,
|
|
@@ -180,6 +217,7 @@ server.tool("json_validate", "Validate JSON data against a JSON Schema. Schema c
|
|
|
180
217
|
schemas,
|
|
181
218
|
schemaDir,
|
|
182
219
|
schemaId,
|
|
220
|
+
strict,
|
|
183
221
|
});
|
|
184
222
|
return {
|
|
185
223
|
content: [
|
|
@@ -28,6 +28,8 @@ export interface SetOptions {
|
|
|
28
28
|
dryRun?: boolean;
|
|
29
29
|
/** Output file path. If not provided, writes to the source file. */
|
|
30
30
|
outputFile?: string;
|
|
31
|
+
/** Enable AJV strict mode. Set to false for schemas with custom keywords. Defaults to true. */
|
|
32
|
+
strict?: boolean;
|
|
31
33
|
}
|
|
32
34
|
export interface SetResult {
|
|
33
35
|
success: boolean;
|
|
@@ -42,3 +44,19 @@ export interface SetResult {
|
|
|
42
44
|
dryRun: boolean;
|
|
43
45
|
}
|
|
44
46
|
export declare function jsonSet(filePath: string, path: string, value: unknown, options?: SetOptions): Promise<SetResult>;
|
|
47
|
+
export interface FormatOptions {
|
|
48
|
+
/** Number of spaces for indentation. Use 0 for compact output. Defaults to 2. */
|
|
49
|
+
indent?: number;
|
|
50
|
+
/** Sort object keys alphabetically. Defaults to false. */
|
|
51
|
+
sortKeys?: boolean;
|
|
52
|
+
/** Output file path. If not provided, writes to the source file. */
|
|
53
|
+
outputFile?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface FormatResult {
|
|
56
|
+
success: boolean;
|
|
57
|
+
inputFile: string;
|
|
58
|
+
outputFile: string;
|
|
59
|
+
originalSize: number;
|
|
60
|
+
formattedSize: number;
|
|
61
|
+
}
|
|
62
|
+
export declare function jsonFormat(filePath: string, options?: FormatOptions): Promise<FormatResult>;
|
package/dist/tools/navigation.js
CHANGED
|
@@ -131,7 +131,7 @@ function inferSimpleSchema(value) {
|
|
|
131
131
|
return { type: typeof value };
|
|
132
132
|
}
|
|
133
133
|
export async function jsonSet(filePath, path, value, options = {}) {
|
|
134
|
-
const { schema, inferSchema: shouldInferSchema = false, dryRun = false, outputFile, } = options;
|
|
134
|
+
const { schema, inferSchema: shouldInferSchema = false, dryRun = false, outputFile, strict = true, } = options;
|
|
135
135
|
// Load the JSON file
|
|
136
136
|
const data = await loadJson(filePath);
|
|
137
137
|
// Deep clone to avoid mutating cached data
|
|
@@ -156,7 +156,7 @@ export async function jsonSet(filePath, path, value, options = {}) {
|
|
|
156
156
|
// Validate the new value against schema
|
|
157
157
|
if (schemaToValidate) {
|
|
158
158
|
const { Ajv, addFormats } = await getAjv();
|
|
159
|
-
const ajv = new Ajv({ allErrors: true });
|
|
159
|
+
const ajv = new Ajv({ allErrors: true, strict });
|
|
160
160
|
addFormats(ajv);
|
|
161
161
|
const validate = ajv.compile(schemaToValidate);
|
|
162
162
|
const valid = validate(value);
|
|
@@ -189,7 +189,7 @@ export async function jsonSet(filePath, path, value, options = {}) {
|
|
|
189
189
|
}
|
|
190
190
|
// Write to file
|
|
191
191
|
const targetFile = outputFile || filePath;
|
|
192
|
-
const jsonOutput = JSON.stringify(modifiedData, null, 2);
|
|
192
|
+
const jsonOutput = JSON.stringify(modifiedData, null, 2) + "\n";
|
|
193
193
|
await writeFile(targetFile, jsonOutput, "utf-8");
|
|
194
194
|
return {
|
|
195
195
|
success: true,
|
|
@@ -200,3 +200,45 @@ export async function jsonSet(filePath, path, value, options = {}) {
|
|
|
200
200
|
dryRun: false,
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
|
+
// Recursively sort object keys
|
|
204
|
+
function sortObjectKeys(obj) {
|
|
205
|
+
if (obj === null || typeof obj !== "object") {
|
|
206
|
+
return obj;
|
|
207
|
+
}
|
|
208
|
+
if (Array.isArray(obj)) {
|
|
209
|
+
return obj.map(sortObjectKeys);
|
|
210
|
+
}
|
|
211
|
+
const sorted = {};
|
|
212
|
+
const keys = Object.keys(obj).sort();
|
|
213
|
+
for (const key of keys) {
|
|
214
|
+
sorted[key] = sortObjectKeys(obj[key]);
|
|
215
|
+
}
|
|
216
|
+
return sorted;
|
|
217
|
+
}
|
|
218
|
+
export async function jsonFormat(filePath, options = {}) {
|
|
219
|
+
const { indent = 2, sortKeys = false, outputFile, } = options;
|
|
220
|
+
// Load the JSON file
|
|
221
|
+
const data = await loadJson(filePath);
|
|
222
|
+
// Get original size
|
|
223
|
+
const originalContent = JSON.stringify(data);
|
|
224
|
+
const originalSize = originalContent.length;
|
|
225
|
+
// Sort keys if requested
|
|
226
|
+
const dataToFormat = sortKeys ? sortObjectKeys(data) : data;
|
|
227
|
+
// Format the JSON
|
|
228
|
+
const formattedContent = indent === 0
|
|
229
|
+
? JSON.stringify(dataToFormat)
|
|
230
|
+
: JSON.stringify(dataToFormat, null, indent);
|
|
231
|
+
// Add trailing newline
|
|
232
|
+
const outputContent = formattedContent + "\n";
|
|
233
|
+
const formattedSize = formattedContent.length;
|
|
234
|
+
// Write to file
|
|
235
|
+
const targetFile = outputFile || filePath;
|
|
236
|
+
await writeFile(targetFile, outputContent, "utf-8");
|
|
237
|
+
return {
|
|
238
|
+
success: true,
|
|
239
|
+
inputFile: filePath,
|
|
240
|
+
outputFile: targetFile,
|
|
241
|
+
originalSize,
|
|
242
|
+
formattedSize,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -41,6 +41,8 @@ export interface ValidateOptions {
|
|
|
41
41
|
schemaDir?: string;
|
|
42
42
|
/** When using schemaDir, specify the $id of the schema to use as entrypoint (instead of schema param). */
|
|
43
43
|
schemaId?: string;
|
|
44
|
+
/** Disable AJV strict mode for schemas with custom keywords. Defaults to true. */
|
|
45
|
+
strict?: boolean;
|
|
44
46
|
}
|
|
45
47
|
export declare function jsonValidate(filePath: string, schema: object | string, path?: string, options?: ValidateOptions | number): Promise<ValidateResult>;
|
|
46
48
|
export {};
|
package/dist/tools/structure.js
CHANGED
|
@@ -267,7 +267,7 @@ export async function jsonValidate(filePath, schema, path, options = {}) {
|
|
|
267
267
|
: options;
|
|
268
268
|
// Environment variable can completely disable network refs for security
|
|
269
269
|
const networkRefsDisabled = process.env.JSON_EXPLORER_NO_NETWORK === "1";
|
|
270
|
-
const { errorLimit = 10, resolveLocalRefs = true, resolveNetworkRefs = false, schemas = {}, schemaDir, schemaId, } = opts;
|
|
270
|
+
const { errorLimit = 10, resolveLocalRefs = true, resolveNetworkRefs = false, schemas = {}, schemaDir, schemaId, strict = true, } = opts;
|
|
271
271
|
const effectiveResolveNetworkRefs = resolveNetworkRefs && !networkRefsDisabled;
|
|
272
272
|
const data = await loadJson(filePath);
|
|
273
273
|
const targetData = path ? getValueAtPath(data, path) : data;
|
|
@@ -301,7 +301,7 @@ export async function jsonValidate(filePath, schema, path, options = {}) {
|
|
|
301
301
|
const { Ajv, addFormats } = await getAjv();
|
|
302
302
|
// Configure ajv options
|
|
303
303
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
304
|
-
const ajvOptions = { allErrors: true };
|
|
304
|
+
const ajvOptions = { allErrors: true, strict };
|
|
305
305
|
// Set up network schema loading if enabled
|
|
306
306
|
if (effectiveResolveNetworkRefs) {
|
|
307
307
|
ajvOptions.loadSchema = async (uri) => {
|
|
@@ -81,10 +81,20 @@ function parsePathSegments(path) {
|
|
|
81
81
|
const segments = [];
|
|
82
82
|
// Remove leading $
|
|
83
83
|
const pathWithoutRoot = normalized.slice(1);
|
|
84
|
-
// Match either .key, [index], or ["key"]
|
|
85
|
-
|
|
84
|
+
// Match either .key (including $-prefixed keys like $id), [index], or ["key"]
|
|
85
|
+
// Keys can start with letter, underscore, or $ and contain alphanumeric, underscore, or $
|
|
86
|
+
const regex = /\.([a-zA-Z_$][a-zA-Z0-9_$]*)|\.?(\[(\d+)\])|\.?\["([^"]+)"\]|\.?\['([^']+)'\]/g;
|
|
86
87
|
let match;
|
|
88
|
+
let lastIndex = 0;
|
|
87
89
|
while ((match = regex.exec(pathWithoutRoot)) !== null) {
|
|
90
|
+
// Check for gaps in parsing (unmatched content)
|
|
91
|
+
if (match.index > lastIndex) {
|
|
92
|
+
const unmatched = pathWithoutRoot.slice(lastIndex, match.index);
|
|
93
|
+
if (unmatched && unmatched !== ".") {
|
|
94
|
+
throw new Error(`Invalid path segment: could not parse "${unmatched}" in path "${path}"`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
lastIndex = regex.lastIndex;
|
|
88
98
|
if (match[1] !== undefined) {
|
|
89
99
|
// .key format
|
|
90
100
|
segments.push(match[1]);
|
|
@@ -102,6 +112,17 @@ function parsePathSegments(path) {
|
|
|
102
112
|
segments.push(match[5]);
|
|
103
113
|
}
|
|
104
114
|
}
|
|
115
|
+
// Check for trailing unmatched content
|
|
116
|
+
if (lastIndex < pathWithoutRoot.length) {
|
|
117
|
+
const unmatched = pathWithoutRoot.slice(lastIndex);
|
|
118
|
+
if (unmatched && unmatched !== ".") {
|
|
119
|
+
throw new Error(`Invalid path segment: could not parse "${unmatched}" in path "${path}"`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Safety check: if path has content but no segments were parsed, something is wrong
|
|
123
|
+
if (pathWithoutRoot.length > 0 && segments.length === 0) {
|
|
124
|
+
throw new Error(`Failed to parse path "${path}": no valid segments found`);
|
|
125
|
+
}
|
|
105
126
|
return segments;
|
|
106
127
|
}
|
|
107
128
|
/**
|