json-explorer-mcp 1.0.2 → 1.0.3

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/README.md CHANGED
@@ -110,6 +110,80 @@ Retrieve the value at a specific path. Large values are automatically truncated.
110
110
  json_get(file: "/path/to/data.json", path: "$.users[0]")
111
111
  ```
112
112
 
113
+ ### json_set
114
+
115
+ Set a value at a specific JSONPath with optional schema validation.
116
+
117
+ ```typescript
118
+ json_set(
119
+ file: "/path/to/data.json",
120
+ path: "$.users[0].name",
121
+ value: "New Name"
122
+ )
123
+ ```
124
+
125
+ With schema validation (validates before writing):
126
+
127
+ ```typescript
128
+ json_set(
129
+ file: "/path/to/data.json",
130
+ path: "$.users[0]",
131
+ value: { id: 1, name: "Alice", email: "alice@example.com" },
132
+ schema: "/path/to/user-schema.json"
133
+ )
134
+ ```
135
+
136
+ With inferred schema (infers type from existing value):
137
+
138
+ ```typescript
139
+ json_set(
140
+ file: "/path/to/data.json",
141
+ path: "$.config.maxRetries",
142
+ value: 5,
143
+ inferSchema: true // Will reject if new value type doesn't match existing
144
+ )
145
+ ```
146
+
147
+ With dry run (validates without writing):
148
+
149
+ ```typescript
150
+ json_set(
151
+ file: "/path/to/data.json",
152
+ path: "$.config.enabled",
153
+ value: true,
154
+ dryRun: true, // Returns result without modifying file
155
+ outputFile: "/path/to/output.json" // Or write to different file
156
+ )
157
+ ```
158
+
159
+ Returns:
160
+
161
+ ```json
162
+ {
163
+ "success": true,
164
+ "path": "$.config.enabled",
165
+ "previousValue": false,
166
+ "newValue": true,
167
+ "dryRun": false,
168
+ "outputFile": "/path/to/data.json"
169
+ }
170
+ ```
171
+
172
+ On validation failure:
173
+
174
+ ```json
175
+ {
176
+ "success": false,
177
+ "path": "$.config.maxRetries",
178
+ "previousValue": 3,
179
+ "newValue": "not a number",
180
+ "validationErrors": [
181
+ { "path": "$", "message": "must be integer" }
182
+ ],
183
+ "dryRun": true
184
+ }
185
+ ```
186
+
113
187
  ### json_schema
114
188
 
115
189
  Infer the JSON schema/structure at a path. For arrays, samples items to determine the item schema.
@@ -144,6 +218,7 @@ Validate JSON data against a JSON Schema. Schema can be provided inline or as a
144
218
  **Features:**
145
219
  - Automatic resolution of local file `$ref` references
146
220
  - Optional network `$ref` resolution (disabled by default)
221
+ - Schema registry for pre-registering schemas by URI (map remote refs to local files)
147
222
  - Validates that referenced files are actual JSON Schemas
148
223
  - Error limiting (default 10) to avoid huge error lists
149
224
 
@@ -177,6 +252,33 @@ json_validate(
177
252
  )
178
253
  ```
179
254
 
255
+ With pre-registered schemas (for complex `$ref` scenarios):
256
+
257
+ ```typescript
258
+ json_validate(
259
+ file: "/path/to/data.json",
260
+ schema: "/path/to/schema.json",
261
+ schemas: {
262
+ // Map URIs to local files or inline schemas
263
+ "https://example.com/schemas/user.json": "/local/path/to/user-schema.json",
264
+ "https://example.com/schemas/address.json": { type: "object", properties: { ... } }
265
+ }
266
+ )
267
+ ```
268
+
269
+ With schema directory (loads all schemas by their `$id`):
270
+
271
+ ```typescript
272
+ json_validate(
273
+ file: "/path/to/data.json",
274
+ schema: {}, // Not needed when using schemaDir+schemaId
275
+ schemaDir: "/path/to/schemas/", // Directory with .json schema files
276
+ schemaId: "main-schema" // $id of the entrypoint schema
277
+ )
278
+ ```
279
+
280
+ This is useful when you have a directory of interconnected schemas that reference each other by `$id`. All `.json` files in the directory are loaded and registered by their `$id` (or filename if no `$id` is present).
281
+
180
282
  Returns:
181
283
 
182
284
  ```json
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 } from "./tools/navigation.js";
6
+ import { jsonKeys, jsonGet, jsonSet } 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.0",
10
+ version: "1.0.3",
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.", {
@@ -94,6 +94,44 @@ server.tool("json_get", "Retrieve the value at a specific path. Large values are
94
94
  };
95
95
  }
96
96
  });
97
+ // Tool: json_set - Set value at path with optional schema validation
98
+ server.tool("json_set", "Set a value at a specific JSONPath. Supports schema validation (inline, file, or inferred from existing data) and dry-run mode. Can write to the source file or a different output file.", {
99
+ file: z.string().describe("Absolute path to the JSON file to modify"),
100
+ path: z.string().describe("JSONPath where to set the value (e.g., '$.users[0].name', 'config.enabled')"),
101
+ value: z.unknown().describe("The value to set at the path"),
102
+ schema: z.union([z.string(), z.object({}).passthrough()]).optional().describe("JSON Schema to validate value against (inline object or path to schema file)"),
103
+ inferSchema: z.boolean().optional().describe("If true, infers schema from existing value at path and validates against it. Defaults to false."),
104
+ dryRun: z.boolean().optional().describe("If true, validates and returns result without writing to file. Defaults to false."),
105
+ outputFile: z.string().optional().describe("Output file path. If not provided, writes to the source file."),
106
+ }, { readOnlyHint: false }, async ({ file, path, value, schema, inferSchema, dryRun, outputFile }) => {
107
+ try {
108
+ const result = await jsonSet(file, path, value, {
109
+ schema,
110
+ inferSchema,
111
+ dryRun,
112
+ outputFile,
113
+ });
114
+ return {
115
+ content: [
116
+ {
117
+ type: "text",
118
+ text: JSON.stringify(result, null, 2),
119
+ },
120
+ ],
121
+ };
122
+ }
123
+ catch (error) {
124
+ return {
125
+ content: [
126
+ {
127
+ type: "text",
128
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
129
+ },
130
+ ],
131
+ isError: true,
132
+ };
133
+ }
134
+ });
97
135
  // Tool: json_schema - Infer structure
98
136
  server.tool("json_schema", "Infer the JSON schema/structure at a path. For arrays, samples items to determine the item schema.", {
99
137
  file: z.string().describe("Absolute path to the JSON file"),
@@ -123,19 +161,25 @@ server.tool("json_schema", "Infer the JSON schema/structure at a path. For array
123
161
  }
124
162
  });
125
163
  // Tool: json_validate - Validate against JSON Schema
126
- server.tool("json_validate", "Validate JSON data against a JSON Schema. Schema can be provided inline or as a path to a schema file. Automatically resolves local file $refs. Use resolveNetworkRefs to also fetch remote schemas.", {
164
+ server.tool("json_validate", "Validate JSON data against a JSON Schema. Schema can be provided inline, as a file path, or loaded from a directory by $id. Automatically resolves local file $refs. Use schemas to pre-register schemas by URI, or schemaDir+schemaId to load all schemas from a directory.", {
127
165
  file: z.string().describe("Absolute path to the JSON file to validate"),
128
- schema: z.union([z.string(), z.object({}).passthrough()]).describe("JSON Schema object or path to schema file"),
166
+ schema: z.union([z.string(), z.object({}).passthrough()]).optional().describe("JSON Schema object or path to schema file. Optional if using schemaDir+schemaId."),
129
167
  path: z.string().optional().describe("JSONPath to validate. Defaults to root."),
130
168
  errorLimit: z.number().optional().describe("Maximum number of errors to return. Defaults to 10."),
131
169
  resolveLocalRefs: z.boolean().optional().describe("Resolve $ref to local files. Defaults to true."),
132
170
  resolveNetworkRefs: z.boolean().optional().describe("Resolve $ref to HTTP URLs. Defaults to false for security."),
133
- }, { readOnlyHint: true }, async ({ file, schema, path, errorLimit, resolveLocalRefs, resolveNetworkRefs }) => {
171
+ 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
+ schemaDir: z.string().optional().describe("Directory containing schema files. All .json files will be loaded and registered by their $id."),
173
+ schemaId: z.string().optional().describe("When using schemaDir, the $id of the schema to use as entrypoint."),
174
+ }, { readOnlyHint: true }, async ({ file, schema, path, errorLimit, resolveLocalRefs, resolveNetworkRefs, schemas, schemaDir, schemaId }) => {
134
175
  try {
135
- const result = await jsonValidate(file, schema, path, {
176
+ const result = await jsonValidate(file, schema ?? {}, path, {
136
177
  errorLimit,
137
178
  resolveLocalRefs,
138
179
  resolveNetworkRefs,
180
+ schemas,
181
+ schemaDir,
182
+ schemaId,
139
183
  });
140
184
  return {
141
185
  content: [
@@ -220,7 +264,7 @@ server.tool("json_sample", "Get sample items from an array. Supports first, last
220
264
  }
221
265
  });
222
266
  // Tool: json_stats - Aggregate statistics for arrays
223
- server.tool("json_stats", "Get aggregate statistics for array fields. If no path provided, discovers all arrays in the file. With a path, returns counts, min/max/avg for numbers, value distributions for strings.", {
267
+ server.tool("json_stats", "Get aggregate statistics for arrays. Supports arrays of objects (field-level stats), strings (length distribution), numbers (min/max/avg/median/stdDev/percentiles), and mixed types. If no path provided, discovers and analyzes all arrays in the file.", {
224
268
  file: z.string().describe("Absolute path to the JSON file"),
225
269
  path: z.string().optional().describe("JSONPath to the array. If omitted, discovers and lists all arrays in the file."),
226
270
  fields: z.array(z.string()).optional().describe("Specific fields to analyze. If not provided, analyzes all top-level fields."),
@@ -19,3 +19,26 @@ export interface GetResult {
19
19
  originalSize?: number;
20
20
  }
21
21
  export declare function jsonGet(filePath: string, path: string, maxSize?: number): Promise<GetResult>;
22
+ export interface SetOptions {
23
+ /** JSON Schema to validate against (inline object or file path) */
24
+ schema?: object | string;
25
+ /** Infer schema from existing value at path. Defaults to false. */
26
+ inferSchema?: boolean;
27
+ /** If true, only validate without writing. Returns the modified JSON. */
28
+ dryRun?: boolean;
29
+ /** Output file path. If not provided, writes to the source file. */
30
+ outputFile?: string;
31
+ }
32
+ export interface SetResult {
33
+ success: boolean;
34
+ path: string;
35
+ previousValue?: unknown;
36
+ newValue: unknown;
37
+ validationErrors?: Array<{
38
+ path: string;
39
+ message: string;
40
+ }>;
41
+ outputFile?: string;
42
+ dryRun: boolean;
43
+ }
44
+ export declare function jsonSet(filePath: string, path: string, value: unknown, options?: SetOptions): Promise<SetResult>;
@@ -1,5 +1,6 @@
1
1
  import { loadJson, getValueType, truncateValue, getValuePreview } from "../utils/json-parser.js";
2
- import { getValueAtPath, getKeysAtPath, buildPathTo } from "../utils/path-helpers.js";
2
+ import { getValueAtPath, getKeysAtPath, buildPathTo, setValueAtPath } from "../utils/path-helpers.js";
3
+ import { writeFile } from "fs/promises";
3
4
  export async function jsonKeys(filePath, path, limit = 50) {
4
5
  const data = await loadJson(filePath);
5
6
  const targetPath = path || "$";
@@ -95,3 +96,107 @@ export async function jsonGet(filePath, path, maxSize = 5000) {
95
96
  ...(truncated && { originalSize }),
96
97
  };
97
98
  }
99
+ // Dynamic import for ajv (ESM/CJS compat)
100
+ async function getAjv() {
101
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
+ const ajvModule = await import("ajv");
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ const formatsModule = await import("ajv-formats");
105
+ const Ajv = ajvModule.default || ajvModule;
106
+ const addFormats = formatsModule.default || formatsModule;
107
+ return { Ajv, addFormats };
108
+ }
109
+ // Infer a simple JSON Schema from a value
110
+ function inferSimpleSchema(value) {
111
+ if (value === null) {
112
+ return { type: "null" };
113
+ }
114
+ if (Array.isArray(value)) {
115
+ if (value.length === 0) {
116
+ return { type: "array" };
117
+ }
118
+ // Sample first item for items schema
119
+ return { type: "array", items: inferSimpleSchema(value[0]) };
120
+ }
121
+ if (typeof value === "object") {
122
+ const properties = {};
123
+ for (const [key, val] of Object.entries(value)) {
124
+ properties[key] = inferSimpleSchema(val);
125
+ }
126
+ return { type: "object", properties };
127
+ }
128
+ if (typeof value === "number") {
129
+ return Number.isInteger(value) ? { type: "integer" } : { type: "number" };
130
+ }
131
+ return { type: typeof value };
132
+ }
133
+ export async function jsonSet(filePath, path, value, options = {}) {
134
+ const { schema, inferSchema: shouldInferSchema = false, dryRun = false, outputFile, } = options;
135
+ // Load the JSON file
136
+ const data = await loadJson(filePath);
137
+ // Deep clone to avoid mutating cached data
138
+ const clonedData = JSON.parse(JSON.stringify(data));
139
+ // Get previous value at path (may be undefined if path doesn't exist yet)
140
+ const previousValue = getValueAtPath(clonedData, path);
141
+ // Determine schema for validation
142
+ let schemaToValidate;
143
+ if (schema) {
144
+ if (typeof schema === "string") {
145
+ // Load schema from file
146
+ schemaToValidate = (await loadJson(schema));
147
+ }
148
+ else {
149
+ schemaToValidate = schema;
150
+ }
151
+ }
152
+ else if (shouldInferSchema && previousValue !== undefined) {
153
+ // Infer schema from existing value
154
+ schemaToValidate = inferSimpleSchema(previousValue);
155
+ }
156
+ // Validate the new value against schema
157
+ if (schemaToValidate) {
158
+ const { Ajv, addFormats } = await getAjv();
159
+ const ajv = new Ajv({ allErrors: true });
160
+ addFormats(ajv);
161
+ const validate = ajv.compile(schemaToValidate);
162
+ const valid = validate(value);
163
+ if (!valid) {
164
+ const errors = (validate.errors || []).map((err) => ({
165
+ path: err.instancePath || "$",
166
+ message: err.message || "Unknown error",
167
+ }));
168
+ return {
169
+ success: false,
170
+ path,
171
+ previousValue,
172
+ newValue: value,
173
+ validationErrors: errors,
174
+ dryRun: true, // Force dryRun on validation failure
175
+ };
176
+ }
177
+ }
178
+ // Set the value at path
179
+ const modifiedData = setValueAtPath(clonedData, path, value);
180
+ // If dry run, return the modified data without writing
181
+ if (dryRun) {
182
+ return {
183
+ success: true,
184
+ path,
185
+ previousValue,
186
+ newValue: value,
187
+ dryRun: true,
188
+ };
189
+ }
190
+ // Write to file
191
+ const targetFile = outputFile || filePath;
192
+ const jsonOutput = JSON.stringify(modifiedData, null, 2);
193
+ await writeFile(targetFile, jsonOutput, "utf-8");
194
+ return {
195
+ success: true,
196
+ path,
197
+ previousValue,
198
+ newValue: value,
199
+ outputFile: targetFile,
200
+ dryRun: false,
201
+ };
202
+ }
@@ -35,6 +35,12 @@ export interface ValidateOptions {
35
35
  errorLimit?: number;
36
36
  resolveLocalRefs?: boolean;
37
37
  resolveNetworkRefs?: boolean;
38
+ /** Pre-register schemas by URI. Value can be inline schema object or path to schema file. */
39
+ schemas?: Record<string, object | string>;
40
+ /** Directory containing schema files. All .json files will be loaded and registered by their $id. */
41
+ schemaDir?: string;
42
+ /** When using schemaDir, specify the $id of the schema to use as entrypoint (instead of schema param). */
43
+ schemaId?: string;
38
44
  }
39
45
  export declare function jsonValidate(filePath: string, schema: object | string, path?: string, options?: ValidateOptions | number): Promise<ValidateResult>;
40
46
  export {};
@@ -1,6 +1,7 @@
1
1
  import { loadJson, getFileInfo, formatBytes, getValueType } from "../utils/json-parser.js";
2
2
  import { getDepthPreview, getValueAtPath } from "../utils/path-helpers.js";
3
- import { dirname, resolve, isAbsolute } from "path";
3
+ import { dirname, resolve, isAbsolute, join } from "path";
4
+ import { readdir } from "fs/promises";
4
5
  // Dynamic import for ajv (ESM/CJS compat)
5
6
  async function getAjv() {
6
7
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -213,6 +214,34 @@ function isValidJsonSchema(obj) {
213
214
  const isEmpty = Object.keys(schema).length === 0;
214
215
  return hasSchemaKeyword || isEmpty;
215
216
  }
217
+ // Load all JSON schema files from a directory, returning map of $id -> schema
218
+ async function loadSchemasFromDir(dir) {
219
+ const schemas = new Map();
220
+ const files = await readdir(dir);
221
+ const jsonFiles = files.filter(f => f.endsWith(".json"));
222
+ for (const file of jsonFiles) {
223
+ const filePath = join(dir, file);
224
+ try {
225
+ const schema = await loadJson(filePath);
226
+ if (!isValidJsonSchema(schema)) {
227
+ continue; // Skip non-schema JSON files
228
+ }
229
+ const schemaRecord = schema;
230
+ const id = schemaRecord.$id;
231
+ if (id) {
232
+ schemas.set(id, { schema, file: filePath });
233
+ }
234
+ else {
235
+ // Use filename as fallback id
236
+ schemas.set(file, { schema, file: filePath });
237
+ }
238
+ }
239
+ catch {
240
+ // Skip files that can't be loaded
241
+ }
242
+ }
243
+ return schemas;
244
+ }
216
245
  // Fetch a schema from a URL
217
246
  async function fetchNetworkSchema(url) {
218
247
  const response = await fetch(url);
@@ -238,24 +267,36 @@ export async function jsonValidate(filePath, schema, path, options = {}) {
238
267
  : options;
239
268
  // Environment variable can completely disable network refs for security
240
269
  const networkRefsDisabled = process.env.JSON_EXPLORER_NO_NETWORK === "1";
241
- const { errorLimit = 10, resolveLocalRefs = true, resolveNetworkRefs = false, } = opts;
270
+ const { errorLimit = 10, resolveLocalRefs = true, resolveNetworkRefs = false, schemas = {}, schemaDir, schemaId, } = opts;
242
271
  const effectiveResolveNetworkRefs = resolveNetworkRefs && !networkRefsDisabled;
243
272
  const data = await loadJson(filePath);
244
273
  const targetData = path ? getValueAtPath(data, path) : data;
245
274
  if (targetData === undefined) {
246
275
  throw new Error(`Path not found: ${path}`);
247
276
  }
248
- // Load schema from file if it's a string path
277
+ // Load schema - from schemaDir+schemaId, file path, or inline
249
278
  let schemaObj;
250
- let schemaDir;
251
- if (typeof schema === "string") {
279
+ let schemaBaseDir;
280
+ let dirSchemas;
281
+ if (schemaDir && schemaId) {
282
+ // Load all schemas from directory and use schemaId as entrypoint
283
+ dirSchemas = await loadSchemasFromDir(schemaDir);
284
+ const entry = dirSchemas.get(schemaId);
285
+ if (!entry) {
286
+ const availableIds = Array.from(dirSchemas.keys()).join(", ");
287
+ throw new Error(`Schema with $id "${schemaId}" not found in ${schemaDir}. Available: ${availableIds}`);
288
+ }
289
+ schemaObj = entry.schema;
290
+ schemaBaseDir = schemaDir;
291
+ }
292
+ else if (typeof schema === "string") {
252
293
  schemaObj = (await loadJson(schema));
253
- schemaDir = dirname(resolve(schema));
294
+ schemaBaseDir = dirname(resolve(schema));
254
295
  }
255
296
  else {
256
297
  schemaObj = schema;
257
298
  // For inline schemas, use current working directory
258
- schemaDir = process.cwd();
299
+ schemaBaseDir = process.cwd();
259
300
  }
260
301
  const { Ajv, addFormats } = await getAjv();
261
302
  // Configure ajv options
@@ -272,10 +313,41 @@ export async function jsonValidate(filePath, schema, path, options = {}) {
272
313
  }
273
314
  const ajv = new Ajv(ajvOptions);
274
315
  addFormats(ajv);
275
- // Pre-load local refs if enabled
316
+ // Pre-register user-provided schemas
276
317
  const resolvedRefs = [];
277
- if (resolveLocalRefs) {
278
- const localRefs = await loadLocalRefs(schemaObj, schemaDir);
318
+ for (const [uri, schemaValue] of Object.entries(schemas)) {
319
+ let schemaToAdd;
320
+ if (typeof schemaValue === "string") {
321
+ // Load from file
322
+ const loaded = await loadJson(schemaValue);
323
+ if (!isValidJsonSchema(loaded)) {
324
+ throw new Error(`Schema file ${schemaValue} is not a valid JSON Schema`);
325
+ }
326
+ schemaToAdd = loaded;
327
+ resolvedRefs.push(schemaValue);
328
+ }
329
+ else {
330
+ // Inline schema object
331
+ if (!isValidJsonSchema(schemaValue)) {
332
+ throw new Error(`Inline schema for "${uri}" is not a valid JSON Schema`);
333
+ }
334
+ schemaToAdd = schemaValue;
335
+ }
336
+ ajv.addSchema(schemaToAdd, uri);
337
+ }
338
+ // Register all schemas from schemaDir (if provided)
339
+ if (dirSchemas) {
340
+ for (const [id, { schema: dirSchema, file }] of dirSchemas) {
341
+ // Don't re-register the entrypoint schema
342
+ if (id !== schemaId) {
343
+ ajv.addSchema(dirSchema, id);
344
+ resolvedRefs.push(file);
345
+ }
346
+ }
347
+ }
348
+ // Pre-load local refs if enabled (skip if using schemaDir since all schemas are already loaded)
349
+ if (resolveLocalRefs && !dirSchemas) {
350
+ const localRefs = await loadLocalRefs(schemaObj, schemaBaseDir);
279
351
  for (const [refPath, refSchema] of localRefs) {
280
352
  // If schema has $id, ajv will use that for resolution
281
353
  // Otherwise we add it by its relative filename
@@ -3,4 +3,10 @@ export declare function normalizeToJsonPath(path: string): string;
3
3
  export declare function getKeysAtPath(data: unknown, path: string): string[];
4
4
  export declare function buildPathTo(basePath: string, key: string): string;
5
5
  export declare function findPaths(data: unknown, predicate: (value: unknown, path: string) => boolean, currentPath?: string, maxResults?: number): string[];
6
+ /**
7
+ * Set a value at a specific JSONPath in the data.
8
+ * Mutates the data in place and returns the modified data.
9
+ * Throws if the path doesn't exist (parent must exist).
10
+ */
11
+ export declare function setValueAtPath(data: unknown, path: string, value: unknown): unknown;
6
12
  export declare function getDepthPreview(data: unknown, maxDepth?: number, currentDepth?: number): unknown;
@@ -70,6 +70,94 @@ export function findPaths(data, predicate, currentPath = "$", maxResults = 100)
70
70
  traverse(data, currentPath);
71
71
  return results;
72
72
  }
73
+ /**
74
+ * Parse a JSONPath into segments for traversal.
75
+ * Returns array of keys/indices to traverse.
76
+ */
77
+ function parsePathSegments(path) {
78
+ const normalized = normalizeToJsonPath(path);
79
+ if (normalized === "$")
80
+ return [];
81
+ const segments = [];
82
+ // Remove leading $
83
+ const pathWithoutRoot = normalized.slice(1);
84
+ // Match either .key, [index], or ["key"]
85
+ const regex = /\.([a-zA-Z_][a-zA-Z0-9_]*)|\.?(\[(\d+)\])|\.?\["([^"]+)"\]|\.?\['([^']+)'\]/g;
86
+ let match;
87
+ while ((match = regex.exec(pathWithoutRoot)) !== null) {
88
+ if (match[1] !== undefined) {
89
+ // .key format
90
+ segments.push(match[1]);
91
+ }
92
+ else if (match[3] !== undefined) {
93
+ // [index] format
94
+ segments.push(parseInt(match[3], 10));
95
+ }
96
+ else if (match[4] !== undefined) {
97
+ // ["key"] format
98
+ segments.push(match[4]);
99
+ }
100
+ else if (match[5] !== undefined) {
101
+ // ['key'] format
102
+ segments.push(match[5]);
103
+ }
104
+ }
105
+ return segments;
106
+ }
107
+ /**
108
+ * Set a value at a specific JSONPath in the data.
109
+ * Mutates the data in place and returns the modified data.
110
+ * Throws if the path doesn't exist (parent must exist).
111
+ */
112
+ export function setValueAtPath(data, path, value) {
113
+ const segments = parsePathSegments(path);
114
+ if (segments.length === 0) {
115
+ // Setting root - just return the new value
116
+ return value;
117
+ }
118
+ // Traverse to parent
119
+ let current = data;
120
+ for (let i = 0; i < segments.length - 1; i++) {
121
+ const segment = segments[i];
122
+ if (current === null || typeof current !== "object") {
123
+ throw new Error(`Cannot traverse path: parent at segment "${segment}" is not an object or array`);
124
+ }
125
+ if (Array.isArray(current)) {
126
+ if (typeof segment !== "number") {
127
+ throw new Error(`Cannot use string key "${segment}" on array`);
128
+ }
129
+ if (segment < 0 || segment >= current.length) {
130
+ throw new Error(`Array index ${segment} out of bounds (length: ${current.length})`);
131
+ }
132
+ current = current[segment];
133
+ }
134
+ else {
135
+ const key = String(segment);
136
+ if (!(key in current)) {
137
+ throw new Error(`Key "${key}" not found in object`);
138
+ }
139
+ current = current[key];
140
+ }
141
+ }
142
+ // Set the value at the final segment
143
+ const finalSegment = segments[segments.length - 1];
144
+ if (current === null || typeof current !== "object") {
145
+ throw new Error(`Cannot set value: parent is not an object or array`);
146
+ }
147
+ if (Array.isArray(current)) {
148
+ if (typeof finalSegment !== "number") {
149
+ throw new Error(`Cannot use string key "${finalSegment}" on array`);
150
+ }
151
+ if (finalSegment < 0 || finalSegment >= current.length) {
152
+ throw new Error(`Array index ${finalSegment} out of bounds (length: ${current.length})`);
153
+ }
154
+ current[finalSegment] = value;
155
+ }
156
+ else {
157
+ current[String(finalSegment)] = value;
158
+ }
159
+ return data;
160
+ }
73
161
  export function getDepthPreview(data, maxDepth = 2, currentDepth = 0) {
74
162
  if (currentDepth >= maxDepth) {
75
163
  if (data === null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-explorer-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "MCP server for efficiently exploring large JSON files",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",