json-explorer-mcp 1.0.0
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 +206 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +252 -0
- package/dist/tools/navigation.d.ts +21 -0
- package/dist/tools/navigation.js +97 -0
- package/dist/tools/query.d.ts +54 -0
- package/dist/tools/query.js +249 -0
- package/dist/tools/structure.d.ts +33 -0
- package/dist/tools/structure.js +148 -0
- package/dist/utils/json-parser.d.ts +10 -0
- package/dist/utils/json-parser.js +86 -0
- package/dist/utils/path-helpers.d.ts +6 -0
- package/dist/utils/path-helpers.js +98 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# JSON Explorer MCP Server
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server for efficiently exploring large JSON files without loading entire contents into context. Designed to help AI assistants navigate complex JSON data structures while minimizing token usage.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Lazy exploration** - Only loads and returns what you need
|
|
8
|
+
- **Smart truncation** - Large values are automatically summarized
|
|
9
|
+
- **Caching** - Parsed JSON is cached with file modification checks
|
|
10
|
+
- **Schema inference** - Understand structure without reading all data
|
|
11
|
+
- **Aggregate statistics** - Get counts, distributions, and numeric stats
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install
|
|
17
|
+
npm run build
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### With Claude Desktop
|
|
23
|
+
|
|
24
|
+
Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"json-explorer": {
|
|
30
|
+
"command": "node",
|
|
31
|
+
"args": ["/path/to/json-explorer-mcp/dist/index.js"]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### With Claude Code
|
|
38
|
+
|
|
39
|
+
Add to `.mcp.json` in your project:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"json-explorer": {
|
|
45
|
+
"command": "node",
|
|
46
|
+
"args": ["dist/index.js"]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Tools
|
|
53
|
+
|
|
54
|
+
### json_inspect
|
|
55
|
+
|
|
56
|
+
Get an overview of a JSON file including size, structure type, and a depth-limited preview.
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
json_inspect(file: "/path/to/data.json")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"file": "/path/to/data.json",
|
|
66
|
+
"size": "13.1 MB",
|
|
67
|
+
"type": "object",
|
|
68
|
+
"preview": {
|
|
69
|
+
"users": "[... 1000 items]",
|
|
70
|
+
"config": "{... 5 keys}"
|
|
71
|
+
},
|
|
72
|
+
"topLevelInfo": "Object with 2 keys: users, config"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### json_keys
|
|
77
|
+
|
|
78
|
+
List all keys (for objects) or indices (for arrays) at a given path with type info and previews.
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
json_keys(file: "/path/to/data.json", path: "$.users")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"path": "$.users",
|
|
88
|
+
"type": "array",
|
|
89
|
+
"keys": [
|
|
90
|
+
{ "key": "[0]", "type": "object", "preview": "{\"id\": 1, \"name\": \"Alice\", ...}", "path": "$.users[0]" },
|
|
91
|
+
{ "key": "[1]", "type": "object", "preview": "{\"id\": 2, \"name\": \"Bob\", ...}", "path": "$.users[1]" }
|
|
92
|
+
],
|
|
93
|
+
"totalCount": 1000
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### json_get
|
|
98
|
+
|
|
99
|
+
Retrieve the value at a specific path. Large values are automatically truncated.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
json_get(file: "/path/to/data.json", path: "$.users[0]")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### json_schema
|
|
106
|
+
|
|
107
|
+
Infer the JSON schema/structure at a path. For arrays, samples items to determine the item schema.
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
json_schema(file: "/path/to/data.json", path: "$.users")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"path": "$.users",
|
|
117
|
+
"schema": {
|
|
118
|
+
"type": "array",
|
|
119
|
+
"items": {
|
|
120
|
+
"type": "object",
|
|
121
|
+
"properties": {
|
|
122
|
+
"id": { "type": "number" },
|
|
123
|
+
"name": { "type": "string" },
|
|
124
|
+
"email": { "type": "string" }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### json_search
|
|
132
|
+
|
|
133
|
+
Search for keys or values matching a pattern (regex supported).
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
json_search(file: "/path/to/data.json", query: "email", searchType: "key")
|
|
137
|
+
json_search(file: "/path/to/data.json", query: "@example.com", searchType: "value")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### json_sample
|
|
141
|
+
|
|
142
|
+
Get sample items from an array. Supports first, last, random, or range-based sampling.
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
json_sample(file: "/path/to/data.json", path: "$.users", count: 5, mode: "random")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### json_stats
|
|
149
|
+
|
|
150
|
+
Get aggregate statistics for array fields. If no path provided, discovers and analyzes all arrays of objects in the file.
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
json_stats(file: "/path/to/data.json")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"arrays": [
|
|
160
|
+
{ "path": "$.users", "length": 1000, "itemType": "object", "fields": ["id", "name", "status"], "fieldCount": 10 }
|
|
161
|
+
],
|
|
162
|
+
"stats": [
|
|
163
|
+
{
|
|
164
|
+
"path": "$.users",
|
|
165
|
+
"arrayLength": 1000,
|
|
166
|
+
"fields": [
|
|
167
|
+
{ "field": "id", "type": "number", "count": 1000, "nullCount": 0, "min": 1, "max": 1000, "avg": 500.5 },
|
|
168
|
+
{ "field": "status", "type": "string", "count": 1000, "nullCount": 0, "uniqueCount": 3, "distribution": { "active": 800, "inactive": 150, "pending": 50 } }
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
With a specific path:
|
|
176
|
+
```
|
|
177
|
+
json_stats(file: "/path/to/data.json", path: "$.users", fields: ["status", "created_at"])
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## JSONPath Syntax
|
|
181
|
+
|
|
182
|
+
All path parameters support JSONPath syntax:
|
|
183
|
+
|
|
184
|
+
- `$` - Root object
|
|
185
|
+
- `$.foo` - Property access
|
|
186
|
+
- `$.foo.bar` - Nested property
|
|
187
|
+
- `$.users[0]` - Array index
|
|
188
|
+
- `$.users[*]` - All array items (in search results)
|
|
189
|
+
- `$["special-key"]` - Bracket notation for special characters
|
|
190
|
+
|
|
191
|
+
## Development
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Install dependencies
|
|
195
|
+
npm install
|
|
196
|
+
|
|
197
|
+
# Build
|
|
198
|
+
npm run build
|
|
199
|
+
|
|
200
|
+
# Run in development mode
|
|
201
|
+
npm run dev
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { jsonInspect, jsonSchema, jsonValidate } from "./tools/structure.js";
|
|
6
|
+
import { jsonKeys, jsonGet } from "./tools/navigation.js";
|
|
7
|
+
import { jsonSearch, jsonSample, jsonStats } from "./tools/query.js";
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "json-explorer",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
});
|
|
12
|
+
// Tool: json_inspect - Get file overview
|
|
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.", {
|
|
14
|
+
file: z.string().describe("Absolute path to the JSON file"),
|
|
15
|
+
}, { readOnlyHint: true }, async ({ file }) => {
|
|
16
|
+
try {
|
|
17
|
+
const result = await jsonInspect(file);
|
|
18
|
+
return {
|
|
19
|
+
content: [
|
|
20
|
+
{
|
|
21
|
+
type: "text",
|
|
22
|
+
text: JSON.stringify(result, null, 2),
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
isError: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// Tool: json_keys - List keys at a path
|
|
40
|
+
server.tool("json_keys", "List all keys (for objects) or indices (for arrays) at a given path. Returns type and preview for each key.", {
|
|
41
|
+
file: z.string().describe("Absolute path to the JSON file"),
|
|
42
|
+
path: z.string().optional().describe("JSONPath to the target location (e.g., '$.users', 'data.items'). Defaults to root."),
|
|
43
|
+
limit: z.number().optional().describe("Maximum number of keys to return. Defaults to 50."),
|
|
44
|
+
}, { readOnlyHint: true }, async ({ file, path, limit }) => {
|
|
45
|
+
try {
|
|
46
|
+
const result = await jsonKeys(file, path, limit);
|
|
47
|
+
return {
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: "text",
|
|
51
|
+
text: JSON.stringify(result, null, 2),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// Tool: json_get - Get value at a path
|
|
69
|
+
server.tool("json_get", "Retrieve the value at a specific path. Large values are automatically truncated with size info.", {
|
|
70
|
+
file: z.string().describe("Absolute path to the JSON file"),
|
|
71
|
+
path: z.string().describe("JSONPath to the value (e.g., '$.users[0]', 'data.config.settings')"),
|
|
72
|
+
maxSize: z.number().optional().describe("Maximum response size in characters before truncation. Defaults to 5000."),
|
|
73
|
+
}, { readOnlyHint: true }, async ({ file, path, maxSize }) => {
|
|
74
|
+
try {
|
|
75
|
+
const result = await jsonGet(file, path, maxSize);
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: JSON.stringify(result, null, 2),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: "text",
|
|
90
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// Tool: json_schema - Infer structure
|
|
98
|
+
server.tool("json_schema", "Infer the JSON schema/structure at a path. For arrays, samples items to determine the item schema.", {
|
|
99
|
+
file: z.string().describe("Absolute path to the JSON file"),
|
|
100
|
+
path: z.string().optional().describe("JSONPath to analyze. Defaults to root."),
|
|
101
|
+
}, { readOnlyHint: true }, async ({ file, path }) => {
|
|
102
|
+
try {
|
|
103
|
+
const result = await jsonSchema(file, path);
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: JSON.stringify(result, null, 2),
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return {
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
isError: true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// 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.", {
|
|
127
|
+
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"),
|
|
129
|
+
path: z.string().optional().describe("JSONPath to validate. Defaults to root."),
|
|
130
|
+
}, { readOnlyHint: true }, async ({ file, schema, path }) => {
|
|
131
|
+
try {
|
|
132
|
+
const result = await jsonValidate(file, schema, path);
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "text",
|
|
137
|
+
text: JSON.stringify(result, null, 2),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
isError: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
// Tool: json_search - Search for keys or values
|
|
155
|
+
server.tool("json_search", "Search for keys or values matching a pattern (regex). Returns matching paths with previews.", {
|
|
156
|
+
file: z.string().describe("Absolute path to the JSON file"),
|
|
157
|
+
query: z.string().describe("Search pattern (regex supported)"),
|
|
158
|
+
searchType: z.enum(["key", "value"]).optional().describe("Search in keys or values. Defaults to 'key'."),
|
|
159
|
+
maxResults: z.number().optional().describe("Maximum results to return. Defaults to 50."),
|
|
160
|
+
}, { readOnlyHint: true }, async ({ file, query, searchType, maxResults }) => {
|
|
161
|
+
try {
|
|
162
|
+
const result = await jsonSearch(file, query, searchType, maxResults);
|
|
163
|
+
return {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: "text",
|
|
167
|
+
text: JSON.stringify(result, null, 2),
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
return {
|
|
174
|
+
content: [
|
|
175
|
+
{
|
|
176
|
+
type: "text",
|
|
177
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
isError: true,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// Tool: json_sample - Sample array items
|
|
185
|
+
server.tool("json_sample", "Get sample items from an array. Supports first, last, random, or range-based sampling.", {
|
|
186
|
+
file: z.string().describe("Absolute path to the JSON file"),
|
|
187
|
+
path: z.string().describe("JSONPath to the array"),
|
|
188
|
+
count: z.number().optional().describe("Number of items to sample. Defaults to 5."),
|
|
189
|
+
mode: z.enum(["first", "last", "random", "range"]).optional().describe("Sampling mode. Defaults to 'first'."),
|
|
190
|
+
rangeStart: z.number().optional().describe("Starting index for 'range' mode."),
|
|
191
|
+
}, { readOnlyHint: true }, async ({ file, path, count, mode, rangeStart }) => {
|
|
192
|
+
try {
|
|
193
|
+
const result = await jsonSample(file, path, count, mode, rangeStart);
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: JSON.stringify(result, null, 2),
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
return {
|
|
205
|
+
content: [
|
|
206
|
+
{
|
|
207
|
+
type: "text",
|
|
208
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
isError: true,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
// Tool: json_stats - Aggregate statistics for arrays
|
|
216
|
+
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.", {
|
|
217
|
+
file: z.string().describe("Absolute path to the JSON file"),
|
|
218
|
+
path: z.string().optional().describe("JSONPath to the array. If omitted, discovers and lists all arrays in the file."),
|
|
219
|
+
fields: z.array(z.string()).optional().describe("Specific fields to analyze. If not provided, analyzes all top-level fields."),
|
|
220
|
+
}, { readOnlyHint: true }, async ({ file, path, fields }) => {
|
|
221
|
+
try {
|
|
222
|
+
const result = await jsonStats(file, path, fields);
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
{
|
|
226
|
+
type: "text",
|
|
227
|
+
text: JSON.stringify(result, null, 2),
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
isError: true,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
// Start the server
|
|
245
|
+
async function main() {
|
|
246
|
+
const transport = new StdioServerTransport();
|
|
247
|
+
await server.connect(transport);
|
|
248
|
+
}
|
|
249
|
+
main().catch((error) => {
|
|
250
|
+
console.error("Server error:", error);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface KeyInfo {
|
|
2
|
+
key: string;
|
|
3
|
+
type: string;
|
|
4
|
+
preview: string;
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
export interface KeysResult {
|
|
8
|
+
path: string;
|
|
9
|
+
type: string;
|
|
10
|
+
keys: KeyInfo[];
|
|
11
|
+
totalCount: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function jsonKeys(filePath: string, path?: string, limit?: number): Promise<KeysResult>;
|
|
14
|
+
export interface GetResult {
|
|
15
|
+
path: string;
|
|
16
|
+
type: string;
|
|
17
|
+
value: unknown;
|
|
18
|
+
truncated: boolean;
|
|
19
|
+
originalSize?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function jsonGet(filePath: string, path: string, maxSize?: number): Promise<GetResult>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { loadJson, getValueType, truncateValue, getValuePreview } from "../utils/json-parser.js";
|
|
2
|
+
import { getValueAtPath, getKeysAtPath, buildPathTo } from "../utils/path-helpers.js";
|
|
3
|
+
export async function jsonKeys(filePath, path, limit = 50) {
|
|
4
|
+
const data = await loadJson(filePath);
|
|
5
|
+
const targetPath = path || "$";
|
|
6
|
+
const value = getValueAtPath(data, targetPath);
|
|
7
|
+
if (value === undefined) {
|
|
8
|
+
throw new Error(`Path not found: ${targetPath}`);
|
|
9
|
+
}
|
|
10
|
+
const type = getValueType(value);
|
|
11
|
+
if (type !== "object" && type !== "array") {
|
|
12
|
+
throw new Error(`Cannot list keys of ${type}. Path "${targetPath}" is not an object or array.`);
|
|
13
|
+
}
|
|
14
|
+
const allKeys = getKeysAtPath(data, targetPath);
|
|
15
|
+
const limitedKeys = allKeys.slice(0, limit);
|
|
16
|
+
const keys = limitedKeys.map((key) => {
|
|
17
|
+
const childPath = buildPathTo(targetPath, key);
|
|
18
|
+
const childValue = getValueAtPath(data, childPath);
|
|
19
|
+
return {
|
|
20
|
+
key,
|
|
21
|
+
type: getValueType(childValue),
|
|
22
|
+
preview: getValuePreview(childValue, 2),
|
|
23
|
+
path: childPath,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
path: targetPath,
|
|
28
|
+
type,
|
|
29
|
+
keys,
|
|
30
|
+
totalCount: allKeys.length,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export async function jsonGet(filePath, path, maxSize = 5000) {
|
|
34
|
+
const data = await loadJson(filePath);
|
|
35
|
+
const value = getValueAtPath(data, path);
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
throw new Error(`Path not found: ${path}`);
|
|
38
|
+
}
|
|
39
|
+
const type = getValueType(value);
|
|
40
|
+
const stringified = JSON.stringify(value);
|
|
41
|
+
const originalSize = stringified.length;
|
|
42
|
+
let resultValue;
|
|
43
|
+
let truncated = false;
|
|
44
|
+
if (originalSize > maxSize) {
|
|
45
|
+
truncated = true;
|
|
46
|
+
// For objects/arrays, show structure with truncated content
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
const itemsToShow = Math.min(10, value.length);
|
|
49
|
+
resultValue = {
|
|
50
|
+
_truncated: true,
|
|
51
|
+
_totalItems: value.length,
|
|
52
|
+
_showing: itemsToShow,
|
|
53
|
+
items: value.slice(0, itemsToShow).map((item) => {
|
|
54
|
+
const itemStr = JSON.stringify(item);
|
|
55
|
+
if (itemStr.length > 500) {
|
|
56
|
+
return JSON.parse(truncateValue(item, 500).replace(/\.\.\. \(\d+ chars total\)$/, ""));
|
|
57
|
+
}
|
|
58
|
+
return item;
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
else if (type === "object" && value !== null) {
|
|
63
|
+
const keys = Object.keys(value);
|
|
64
|
+
const keysToShow = keys.slice(0, 20);
|
|
65
|
+
const truncatedObj = {
|
|
66
|
+
_truncated: true,
|
|
67
|
+
_totalKeys: keys.length,
|
|
68
|
+
_showing: keysToShow.length,
|
|
69
|
+
};
|
|
70
|
+
for (const key of keysToShow) {
|
|
71
|
+
const val = value[key];
|
|
72
|
+
const valStr = JSON.stringify(val);
|
|
73
|
+
if (valStr.length > 200) {
|
|
74
|
+
truncatedObj[key] = getValuePreview(val, 2);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
truncatedObj[key] = val;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
resultValue = truncatedObj;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Large string or other primitive
|
|
84
|
+
resultValue = truncateValue(value, maxSize);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
resultValue = value;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
path,
|
|
92
|
+
type,
|
|
93
|
+
value: resultValue,
|
|
94
|
+
truncated,
|
|
95
|
+
...(truncated && { originalSize }),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface SearchMatch {
|
|
2
|
+
path: string;
|
|
3
|
+
key?: string;
|
|
4
|
+
value?: unknown;
|
|
5
|
+
preview: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SearchResult {
|
|
8
|
+
query: string;
|
|
9
|
+
searchType: "key" | "value";
|
|
10
|
+
matches: SearchMatch[];
|
|
11
|
+
totalMatches: number;
|
|
12
|
+
truncated: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function jsonSearch(filePath: string, query: string, searchType?: "key" | "value", maxResults?: number): Promise<SearchResult>;
|
|
15
|
+
export interface SampleResult {
|
|
16
|
+
path: string;
|
|
17
|
+
arrayLength: number;
|
|
18
|
+
sampleType: "first" | "last" | "random" | "range";
|
|
19
|
+
items: Array<{
|
|
20
|
+
index: number;
|
|
21
|
+
value: unknown;
|
|
22
|
+
type: string;
|
|
23
|
+
}>;
|
|
24
|
+
hasMore: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function jsonSample(filePath: string, path: string, count?: number, mode?: "first" | "last" | "random" | "range", rangeStart?: number): Promise<SampleResult>;
|
|
27
|
+
export interface FieldStats {
|
|
28
|
+
field: string;
|
|
29
|
+
type: string;
|
|
30
|
+
count: number;
|
|
31
|
+
nullCount: number;
|
|
32
|
+
uniqueValues?: unknown[];
|
|
33
|
+
uniqueCount?: number;
|
|
34
|
+
min?: number;
|
|
35
|
+
max?: number;
|
|
36
|
+
avg?: number;
|
|
37
|
+
distribution?: Record<string, number>;
|
|
38
|
+
}
|
|
39
|
+
export interface ArrayInfo {
|
|
40
|
+
path: string;
|
|
41
|
+
length: number;
|
|
42
|
+
itemType: string;
|
|
43
|
+
fields?: string[];
|
|
44
|
+
fieldCount?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface StatsResult {
|
|
47
|
+
path: string;
|
|
48
|
+
arrayLength: number;
|
|
49
|
+
fields: FieldStats[];
|
|
50
|
+
}
|
|
51
|
+
export interface MultiStatsResult {
|
|
52
|
+
stats: StatsResult[];
|
|
53
|
+
}
|
|
54
|
+
export declare function jsonStats(filePath: string, path?: string, fields?: string[]): Promise<StatsResult | MultiStatsResult>;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { loadJson, getValuePreview, getValueType } from "../utils/json-parser.js";
|
|
2
|
+
import { findPaths, getValueAtPath } from "../utils/path-helpers.js";
|
|
3
|
+
export async function jsonSearch(filePath, query, searchType = "key", maxResults = 50) {
|
|
4
|
+
const data = await loadJson(filePath);
|
|
5
|
+
const regex = new RegExp(query, "i");
|
|
6
|
+
let matchingPaths;
|
|
7
|
+
if (searchType === "key") {
|
|
8
|
+
// Search for keys matching the pattern
|
|
9
|
+
matchingPaths = findPaths(data, (_, path) => {
|
|
10
|
+
// Extract the last key from the path
|
|
11
|
+
const lastDot = path.lastIndexOf(".");
|
|
12
|
+
const lastBracket = path.lastIndexOf("[");
|
|
13
|
+
const lastSep = Math.max(lastDot, lastBracket);
|
|
14
|
+
if (lastSep === -1)
|
|
15
|
+
return false;
|
|
16
|
+
let key;
|
|
17
|
+
if (lastBracket > lastDot) {
|
|
18
|
+
// It's an array index or bracket notation
|
|
19
|
+
const match = path.slice(lastBracket).match(/\["?([^\]"]+)"?\]/);
|
|
20
|
+
key = match ? match[1] : "";
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
key = path.slice(lastDot + 1);
|
|
24
|
+
}
|
|
25
|
+
return regex.test(key);
|
|
26
|
+
}, "$", maxResults + 1);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// Search for values matching the pattern
|
|
30
|
+
matchingPaths = findPaths(data, (value) => {
|
|
31
|
+
if (typeof value === "string") {
|
|
32
|
+
return regex.test(value);
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
35
|
+
return regex.test(String(value));
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}, "$", maxResults + 1);
|
|
39
|
+
}
|
|
40
|
+
const truncated = matchingPaths.length > maxResults;
|
|
41
|
+
const limitedPaths = matchingPaths.slice(0, maxResults);
|
|
42
|
+
const matches = limitedPaths.map((path) => {
|
|
43
|
+
const value = getValueAtPath(data, path);
|
|
44
|
+
const lastDot = path.lastIndexOf(".");
|
|
45
|
+
const lastBracket = path.lastIndexOf("[");
|
|
46
|
+
const lastSep = Math.max(lastDot, lastBracket);
|
|
47
|
+
let key;
|
|
48
|
+
if (lastSep !== -1) {
|
|
49
|
+
if (lastBracket > lastDot) {
|
|
50
|
+
const match = path.slice(lastBracket).match(/\["?([^\]"]+)"?\]/);
|
|
51
|
+
key = match ? match[1] : undefined;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
key = path.slice(lastDot + 1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
path,
|
|
59
|
+
key,
|
|
60
|
+
value: searchType === "value" ? value : undefined,
|
|
61
|
+
preview: getValuePreview(value, 2),
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
query,
|
|
66
|
+
searchType,
|
|
67
|
+
matches,
|
|
68
|
+
totalMatches: matchingPaths.length,
|
|
69
|
+
truncated,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export async function jsonSample(filePath, path, count = 5, mode = "first", rangeStart) {
|
|
73
|
+
const data = await loadJson(filePath);
|
|
74
|
+
const value = getValueAtPath(data, path);
|
|
75
|
+
if (!Array.isArray(value)) {
|
|
76
|
+
throw new Error(`Path "${path}" is not an array. Got: ${getValueType(value)}`);
|
|
77
|
+
}
|
|
78
|
+
const arrayLength = value.length;
|
|
79
|
+
let indices;
|
|
80
|
+
switch (mode) {
|
|
81
|
+
case "first":
|
|
82
|
+
indices = Array.from({ length: Math.min(count, arrayLength) }, (_, i) => i);
|
|
83
|
+
break;
|
|
84
|
+
case "last":
|
|
85
|
+
const start = Math.max(0, arrayLength - count);
|
|
86
|
+
indices = Array.from({ length: Math.min(count, arrayLength) }, (_, i) => start + i);
|
|
87
|
+
break;
|
|
88
|
+
case "random":
|
|
89
|
+
const available = Array.from({ length: arrayLength }, (_, i) => i);
|
|
90
|
+
indices = [];
|
|
91
|
+
for (let i = 0; i < Math.min(count, arrayLength); i++) {
|
|
92
|
+
const randomIndex = Math.floor(Math.random() * available.length);
|
|
93
|
+
indices.push(available.splice(randomIndex, 1)[0]);
|
|
94
|
+
}
|
|
95
|
+
indices.sort((a, b) => a - b);
|
|
96
|
+
break;
|
|
97
|
+
case "range":
|
|
98
|
+
const rangeStartIdx = rangeStart ?? 0;
|
|
99
|
+
indices = Array.from({ length: Math.min(count, arrayLength - rangeStartIdx) }, (_, i) => rangeStartIdx + i);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
const items = indices.map((index) => {
|
|
103
|
+
const itemValue = value[index];
|
|
104
|
+
const itemStr = JSON.stringify(itemValue);
|
|
105
|
+
const isTruncated = itemStr.length > 1000;
|
|
106
|
+
return {
|
|
107
|
+
index,
|
|
108
|
+
value: isTruncated ? getValuePreview(itemValue, 3) : itemValue,
|
|
109
|
+
type: getValueType(itemValue),
|
|
110
|
+
...(isTruncated && { truncated: true, originalSize: itemStr.length }),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
path,
|
|
115
|
+
arrayLength,
|
|
116
|
+
sampleType: mode,
|
|
117
|
+
items,
|
|
118
|
+
hasMore: arrayLength > count,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function findArraysOfObjects(data, currentPath = "$", maxDepth = 5, depth = 0) {
|
|
122
|
+
const results = [];
|
|
123
|
+
if (depth > maxDepth)
|
|
124
|
+
return results;
|
|
125
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
126
|
+
const firstItem = data[0];
|
|
127
|
+
// Only include arrays of objects (not primitives or nested arrays)
|
|
128
|
+
if (typeof firstItem === "object" && firstItem !== null && !Array.isArray(firstItem)) {
|
|
129
|
+
const fields = Object.keys(firstItem);
|
|
130
|
+
results.push({
|
|
131
|
+
path: currentPath,
|
|
132
|
+
length: data.length,
|
|
133
|
+
itemType: "object",
|
|
134
|
+
fields: fields.slice(0, 10),
|
|
135
|
+
fieldCount: fields.length,
|
|
136
|
+
});
|
|
137
|
+
// Check for nested arrays within the object items
|
|
138
|
+
for (const key of fields) {
|
|
139
|
+
const childValue = firstItem[key];
|
|
140
|
+
if (Array.isArray(childValue) && childValue.length > 0) {
|
|
141
|
+
const nestedFirst = childValue[0];
|
|
142
|
+
if (typeof nestedFirst === "object" && nestedFirst !== null && !Array.isArray(nestedFirst)) {
|
|
143
|
+
const nestedFields = Object.keys(nestedFirst);
|
|
144
|
+
results.push({
|
|
145
|
+
path: `${currentPath}[*].${key}`,
|
|
146
|
+
length: childValue.length,
|
|
147
|
+
itemType: "object",
|
|
148
|
+
fields: nestedFields.slice(0, 10),
|
|
149
|
+
fieldCount: nestedFields.length,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (typeof data === "object" && data !== null) {
|
|
157
|
+
for (const key of Object.keys(data)) {
|
|
158
|
+
const childPath = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)
|
|
159
|
+
? `${currentPath}.${key}`
|
|
160
|
+
: `${currentPath}["${key}"]`;
|
|
161
|
+
results.push(...findArraysOfObjects(data[key], childPath, maxDepth, depth + 1));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return results;
|
|
165
|
+
}
|
|
166
|
+
export async function jsonStats(filePath, path, fields) {
|
|
167
|
+
const data = await loadJson(filePath);
|
|
168
|
+
// If no path provided, compute stats for all arrays of objects
|
|
169
|
+
if (!path) {
|
|
170
|
+
const arrays = findArraysOfObjects(data);
|
|
171
|
+
const allStats = [];
|
|
172
|
+
for (const arr of arrays) {
|
|
173
|
+
try {
|
|
174
|
+
const value = getValueAtPath(data, arr.path);
|
|
175
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
176
|
+
const stats = computeArrayStats(value, arr.path, fields);
|
|
177
|
+
allStats.push(stats);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Skip arrays we can't access (e.g., wildcard paths)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { stats: allStats };
|
|
185
|
+
}
|
|
186
|
+
const value = getValueAtPath(data, path);
|
|
187
|
+
if (!Array.isArray(value)) {
|
|
188
|
+
throw new Error(`Path "${path}" is not an array. Got: ${getValueType(value)}`);
|
|
189
|
+
}
|
|
190
|
+
return computeArrayStats(value, path, fields);
|
|
191
|
+
}
|
|
192
|
+
function computeArrayStats(value, path, fields) {
|
|
193
|
+
if (value.length === 0) {
|
|
194
|
+
return { path, arrayLength: 0, fields: [] };
|
|
195
|
+
}
|
|
196
|
+
// Determine fields to analyze
|
|
197
|
+
const firstItem = value[0];
|
|
198
|
+
const fieldsToAnalyze = fields ?? (typeof firstItem === "object" && firstItem !== null
|
|
199
|
+
? Object.keys(firstItem)
|
|
200
|
+
: []);
|
|
201
|
+
const fieldStats = fieldsToAnalyze.map((field) => {
|
|
202
|
+
const values = value.map((item) => {
|
|
203
|
+
if (typeof item === "object" && item !== null) {
|
|
204
|
+
return item[field];
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
});
|
|
208
|
+
const nonNullValues = values.filter((v) => v !== null && v !== undefined);
|
|
209
|
+
const nullCount = values.length - nonNullValues.length;
|
|
210
|
+
// Determine predominant type
|
|
211
|
+
const types = new Set(nonNullValues.map((v) => getValueType(v)));
|
|
212
|
+
const type = types.size === 1 ? [...types][0] : [...types].join(" | ");
|
|
213
|
+
const stats = {
|
|
214
|
+
field,
|
|
215
|
+
type,
|
|
216
|
+
count: nonNullValues.length,
|
|
217
|
+
nullCount,
|
|
218
|
+
};
|
|
219
|
+
// Numeric stats
|
|
220
|
+
const numericValues = nonNullValues.filter((v) => typeof v === "number");
|
|
221
|
+
if (numericValues.length > 0) {
|
|
222
|
+
stats.min = Math.min(...numericValues);
|
|
223
|
+
stats.max = Math.max(...numericValues);
|
|
224
|
+
stats.avg = numericValues.reduce((a, b) => a + b, 0) / numericValues.length;
|
|
225
|
+
}
|
|
226
|
+
// String/categorical stats
|
|
227
|
+
const stringValues = nonNullValues.filter((v) => typeof v === "string" || typeof v === "boolean");
|
|
228
|
+
if (stringValues.length > 0) {
|
|
229
|
+
const distribution = {};
|
|
230
|
+
for (const v of stringValues) {
|
|
231
|
+
const key = String(v);
|
|
232
|
+
distribution[key] = (distribution[key] || 0) + 1;
|
|
233
|
+
}
|
|
234
|
+
const uniqueKeys = Object.keys(distribution);
|
|
235
|
+
stats.uniqueCount = uniqueKeys.length;
|
|
236
|
+
// Only show distribution if there are <= 20 unique values
|
|
237
|
+
if (uniqueKeys.length <= 20) {
|
|
238
|
+
stats.distribution = distribution;
|
|
239
|
+
stats.uniqueValues = uniqueKeys;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return stats;
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
path,
|
|
246
|
+
arrayLength: value.length,
|
|
247
|
+
fields: fieldStats,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface InspectResult {
|
|
2
|
+
file: string;
|
|
3
|
+
size: string;
|
|
4
|
+
type: string;
|
|
5
|
+
preview: unknown;
|
|
6
|
+
topLevelInfo: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function jsonInspect(filePath: string): Promise<InspectResult>;
|
|
9
|
+
interface SchemaNode {
|
|
10
|
+
type: string;
|
|
11
|
+
properties?: Record<string, SchemaNode>;
|
|
12
|
+
items?: SchemaNode;
|
|
13
|
+
nullable?: boolean;
|
|
14
|
+
enum?: unknown[];
|
|
15
|
+
}
|
|
16
|
+
export interface SchemaResult {
|
|
17
|
+
path: string;
|
|
18
|
+
schema: SchemaNode;
|
|
19
|
+
}
|
|
20
|
+
export declare function jsonSchema(filePath: string, path?: string): Promise<SchemaResult>;
|
|
21
|
+
export interface ValidationError {
|
|
22
|
+
path: string;
|
|
23
|
+
message: string;
|
|
24
|
+
keyword: string;
|
|
25
|
+
params: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface ValidateResult {
|
|
28
|
+
valid: boolean;
|
|
29
|
+
errors: ValidationError[];
|
|
30
|
+
errorCount: number;
|
|
31
|
+
}
|
|
32
|
+
export declare function jsonValidate(filePath: string, schema: object | string, path?: string): Promise<ValidateResult>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { loadJson, getFileInfo, formatBytes, getValueType } from "../utils/json-parser.js";
|
|
2
|
+
import { getDepthPreview, getValueAtPath } from "../utils/path-helpers.js";
|
|
3
|
+
// Dynamic import for ajv (ESM/CJS compat)
|
|
4
|
+
async function getAjv() {
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
const ajvModule = await import("ajv");
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
const formatsModule = await import("ajv-formats");
|
|
9
|
+
const Ajv = ajvModule.default || ajvModule;
|
|
10
|
+
const addFormats = formatsModule.default || formatsModule;
|
|
11
|
+
return { Ajv, addFormats };
|
|
12
|
+
}
|
|
13
|
+
export async function jsonInspect(filePath) {
|
|
14
|
+
const info = await getFileInfo(filePath);
|
|
15
|
+
if (!info.exists) {
|
|
16
|
+
throw new Error(`File not found: ${filePath}`);
|
|
17
|
+
}
|
|
18
|
+
const data = await loadJson(filePath);
|
|
19
|
+
const type = getValueType(data);
|
|
20
|
+
let topLevelInfo;
|
|
21
|
+
if (Array.isArray(data)) {
|
|
22
|
+
topLevelInfo = `Array with ${data.length} items`;
|
|
23
|
+
}
|
|
24
|
+
else if (type === "object" && data !== null) {
|
|
25
|
+
const keys = Object.keys(data);
|
|
26
|
+
topLevelInfo = `Object with ${keys.length} keys: ${keys.slice(0, 10).join(", ")}${keys.length > 10 ? ` ... +${keys.length - 10} more` : ""}`;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
topLevelInfo = `Primitive value: ${type}`;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
file: filePath,
|
|
33
|
+
size: formatBytes(info.size),
|
|
34
|
+
type,
|
|
35
|
+
preview: getDepthPreview(data, 2),
|
|
36
|
+
topLevelInfo,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function inferSchema(value, sampleSize = 5) {
|
|
40
|
+
if (value === null) {
|
|
41
|
+
return { type: "null" };
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
if (value.length === 0) {
|
|
45
|
+
return { type: "array", items: { type: "unknown" } };
|
|
46
|
+
}
|
|
47
|
+
// Sample items to infer item schema
|
|
48
|
+
const sampled = value.slice(0, sampleSize);
|
|
49
|
+
const itemSchemas = sampled.map((item) => inferSchema(item, sampleSize));
|
|
50
|
+
// Merge item schemas
|
|
51
|
+
const mergedItemSchema = mergeSchemas(itemSchemas);
|
|
52
|
+
return { type: "array", items: mergedItemSchema };
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === "object") {
|
|
55
|
+
const properties = {};
|
|
56
|
+
for (const [key, val] of Object.entries(value)) {
|
|
57
|
+
properties[key] = inferSchema(val, sampleSize);
|
|
58
|
+
}
|
|
59
|
+
return { type: "object", properties };
|
|
60
|
+
}
|
|
61
|
+
return { type: typeof value };
|
|
62
|
+
}
|
|
63
|
+
function mergeSchemas(schemas) {
|
|
64
|
+
if (schemas.length === 0) {
|
|
65
|
+
return { type: "unknown" };
|
|
66
|
+
}
|
|
67
|
+
if (schemas.length === 1) {
|
|
68
|
+
return schemas[0];
|
|
69
|
+
}
|
|
70
|
+
// Check if all schemas are the same type
|
|
71
|
+
const types = new Set(schemas.map((s) => s.type));
|
|
72
|
+
if (types.size === 1) {
|
|
73
|
+
const type = schemas[0].type;
|
|
74
|
+
if (type === "object") {
|
|
75
|
+
// Merge object properties
|
|
76
|
+
const allKeys = new Set();
|
|
77
|
+
for (const schema of schemas) {
|
|
78
|
+
if (schema.properties) {
|
|
79
|
+
for (const key of Object.keys(schema.properties)) {
|
|
80
|
+
allKeys.add(key);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const properties = {};
|
|
85
|
+
for (const key of allKeys) {
|
|
86
|
+
const keySchemas = schemas
|
|
87
|
+
.filter((s) => s.properties && s.properties[key])
|
|
88
|
+
.map((s) => s.properties[key]);
|
|
89
|
+
properties[key] = keySchemas.length > 0 ? mergeSchemas(keySchemas) : { type: "unknown" };
|
|
90
|
+
}
|
|
91
|
+
return { type: "object", properties };
|
|
92
|
+
}
|
|
93
|
+
if (type === "array") {
|
|
94
|
+
const itemSchemas = schemas
|
|
95
|
+
.filter((s) => s.items)
|
|
96
|
+
.map((s) => s.items);
|
|
97
|
+
return {
|
|
98
|
+
type: "array",
|
|
99
|
+
items: itemSchemas.length > 0 ? mergeSchemas(itemSchemas) : { type: "unknown" },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return schemas[0];
|
|
103
|
+
}
|
|
104
|
+
// Mixed types - return union description
|
|
105
|
+
return { type: Array.from(types).join(" | ") };
|
|
106
|
+
}
|
|
107
|
+
export async function jsonSchema(filePath, path) {
|
|
108
|
+
const data = await loadJson(filePath);
|
|
109
|
+
const targetData = path ? getValueAtPath(data, path) : data;
|
|
110
|
+
if (targetData === undefined) {
|
|
111
|
+
throw new Error(`Path not found: ${path}`);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
path: path || "$",
|
|
115
|
+
schema: inferSchema(targetData),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export async function jsonValidate(filePath, schema, path) {
|
|
119
|
+
const data = await loadJson(filePath);
|
|
120
|
+
const targetData = path ? getValueAtPath(data, path) : data;
|
|
121
|
+
if (targetData === undefined) {
|
|
122
|
+
throw new Error(`Path not found: ${path}`);
|
|
123
|
+
}
|
|
124
|
+
// Load schema from file if it's a string path
|
|
125
|
+
let schemaObj;
|
|
126
|
+
if (typeof schema === "string") {
|
|
127
|
+
schemaObj = (await loadJson(schema));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
schemaObj = schema;
|
|
131
|
+
}
|
|
132
|
+
const { Ajv, addFormats } = await getAjv();
|
|
133
|
+
const ajv = new Ajv({ allErrors: true });
|
|
134
|
+
addFormats(ajv);
|
|
135
|
+
const validate = ajv.compile(schemaObj);
|
|
136
|
+
const valid = validate(targetData);
|
|
137
|
+
const errors = (validate.errors || []).map((err) => ({
|
|
138
|
+
path: err.instancePath || "$",
|
|
139
|
+
message: err.message || "Unknown error",
|
|
140
|
+
keyword: err.keyword,
|
|
141
|
+
params: err.params,
|
|
142
|
+
}));
|
|
143
|
+
return {
|
|
144
|
+
valid: valid === true,
|
|
145
|
+
errors,
|
|
146
|
+
errorCount: errors.length,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function getFileInfo(filePath: string): Promise<{
|
|
2
|
+
size: number;
|
|
3
|
+
mtime: number;
|
|
4
|
+
exists: boolean;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function loadJson(filePath: string): Promise<unknown>;
|
|
7
|
+
export declare function getValueType(value: unknown): string;
|
|
8
|
+
export declare function truncateValue(value: unknown, maxLength?: number): string;
|
|
9
|
+
export declare function getValuePreview(value: unknown, maxItems?: number): string;
|
|
10
|
+
export declare function formatBytes(bytes: number): string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFile, stat } from "fs/promises";
|
|
2
|
+
const cache = new Map();
|
|
3
|
+
const MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100MB total cache limit
|
|
4
|
+
let currentCacheSize = 0;
|
|
5
|
+
export async function getFileInfo(filePath) {
|
|
6
|
+
try {
|
|
7
|
+
const stats = await stat(filePath);
|
|
8
|
+
return {
|
|
9
|
+
size: stats.size,
|
|
10
|
+
mtime: stats.mtimeMs,
|
|
11
|
+
exists: true,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return { size: 0, mtime: 0, exists: false };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function loadJson(filePath) {
|
|
19
|
+
const info = await getFileInfo(filePath);
|
|
20
|
+
if (!info.exists) {
|
|
21
|
+
throw new Error(`File not found: ${filePath}`);
|
|
22
|
+
}
|
|
23
|
+
// Check cache
|
|
24
|
+
const cached = cache.get(filePath);
|
|
25
|
+
if (cached && cached.mtime === info.mtime) {
|
|
26
|
+
return cached.data;
|
|
27
|
+
}
|
|
28
|
+
// Read and parse file
|
|
29
|
+
const content = await readFile(filePath, "utf-8");
|
|
30
|
+
const data = JSON.parse(content);
|
|
31
|
+
// Update cache (with size management)
|
|
32
|
+
if (info.size < MAX_CACHE_SIZE / 2) {
|
|
33
|
+
// Only cache files smaller than half the max cache size
|
|
34
|
+
while (currentCacheSize + info.size > MAX_CACHE_SIZE && cache.size > 0) {
|
|
35
|
+
// Evict oldest entry
|
|
36
|
+
const firstKey = cache.keys().next().value;
|
|
37
|
+
if (firstKey) {
|
|
38
|
+
const entry = cache.get(firstKey);
|
|
39
|
+
if (entry) {
|
|
40
|
+
currentCacheSize -= entry.size;
|
|
41
|
+
}
|
|
42
|
+
cache.delete(firstKey);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
cache.set(filePath, { data, mtime: info.mtime, size: info.size });
|
|
46
|
+
currentCacheSize += info.size;
|
|
47
|
+
}
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
export function getValueType(value) {
|
|
51
|
+
if (value === null)
|
|
52
|
+
return "null";
|
|
53
|
+
if (Array.isArray(value))
|
|
54
|
+
return "array";
|
|
55
|
+
return typeof value;
|
|
56
|
+
}
|
|
57
|
+
export function truncateValue(value, maxLength = 500) {
|
|
58
|
+
const str = JSON.stringify(value);
|
|
59
|
+
if (str.length <= maxLength)
|
|
60
|
+
return str;
|
|
61
|
+
return str.slice(0, maxLength) + `... (${str.length} chars total)`;
|
|
62
|
+
}
|
|
63
|
+
export function getValuePreview(value, maxItems = 3) {
|
|
64
|
+
if (value === null)
|
|
65
|
+
return "null";
|
|
66
|
+
if (typeof value !== "object")
|
|
67
|
+
return truncateValue(value, 100);
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
const preview = value.slice(0, maxItems).map((v) => truncateValue(v, 50));
|
|
70
|
+
const more = value.length > maxItems ? `, ... +${value.length - maxItems} more` : "";
|
|
71
|
+
return `[${preview.join(", ")}${more}]`;
|
|
72
|
+
}
|
|
73
|
+
const keys = Object.keys(value);
|
|
74
|
+
const preview = keys.slice(0, maxItems).map((k) => `"${k}": ${truncateValue(value[k], 30)}`);
|
|
75
|
+
const more = keys.length > maxItems ? `, ... +${keys.length - maxItems} more` : "";
|
|
76
|
+
return `{${preview.join(", ")}${more}}`;
|
|
77
|
+
}
|
|
78
|
+
export function formatBytes(bytes) {
|
|
79
|
+
if (bytes < 1024)
|
|
80
|
+
return `${bytes} B`;
|
|
81
|
+
if (bytes < 1024 * 1024)
|
|
82
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
83
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
84
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
85
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
86
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function getValueAtPath(data: unknown, path: string): unknown;
|
|
2
|
+
export declare function normalizeToJsonPath(path: string): string;
|
|
3
|
+
export declare function getKeysAtPath(data: unknown, path: string): string[];
|
|
4
|
+
export declare function buildPathTo(basePath: string, key: string): string;
|
|
5
|
+
export declare function findPaths(data: unknown, predicate: (value: unknown, path: string) => boolean, currentPath?: string, maxResults?: number): string[];
|
|
6
|
+
export declare function getDepthPreview(data: unknown, maxDepth?: number, currentDepth?: number): unknown;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { JSONPath } from "jsonpath-plus";
|
|
2
|
+
export function getValueAtPath(data, path) {
|
|
3
|
+
if (!path || path === "$" || path === ".") {
|
|
4
|
+
return data;
|
|
5
|
+
}
|
|
6
|
+
// Normalize path to JSONPath format
|
|
7
|
+
const normalizedPath = normalizeToJsonPath(path);
|
|
8
|
+
const results = JSONPath({ path: normalizedPath, json: data, wrap: false });
|
|
9
|
+
return results;
|
|
10
|
+
}
|
|
11
|
+
export function normalizeToJsonPath(path) {
|
|
12
|
+
// Already a JSONPath expression
|
|
13
|
+
if (path.startsWith("$")) {
|
|
14
|
+
return path;
|
|
15
|
+
}
|
|
16
|
+
// Dot notation like "foo.bar.baz" or "foo[0].bar"
|
|
17
|
+
if (path.startsWith(".")) {
|
|
18
|
+
return "$" + path;
|
|
19
|
+
}
|
|
20
|
+
// Simple path without leading $ or .
|
|
21
|
+
return "$." + path;
|
|
22
|
+
}
|
|
23
|
+
export function getKeysAtPath(data, path) {
|
|
24
|
+
const value = getValueAtPath(data, path);
|
|
25
|
+
if (value === null || typeof value !== "object") {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
return value.map((_, i) => `[${i}]`);
|
|
30
|
+
}
|
|
31
|
+
return Object.keys(value);
|
|
32
|
+
}
|
|
33
|
+
export function buildPathTo(basePath, key) {
|
|
34
|
+
const normalizedBase = basePath === "$" || basePath === "." || !basePath ? "$" : normalizeToJsonPath(basePath);
|
|
35
|
+
if (key.startsWith("[")) {
|
|
36
|
+
return normalizedBase + key;
|
|
37
|
+
}
|
|
38
|
+
// Handle keys that need bracket notation (spaces, dots, special chars)
|
|
39
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
|
|
40
|
+
return normalizedBase + "." + key;
|
|
41
|
+
}
|
|
42
|
+
return normalizedBase + `["${key}"]`;
|
|
43
|
+
}
|
|
44
|
+
export function findPaths(data, predicate, currentPath = "$", maxResults = 100) {
|
|
45
|
+
const results = [];
|
|
46
|
+
function traverse(value, path) {
|
|
47
|
+
if (results.length >= maxResults)
|
|
48
|
+
return;
|
|
49
|
+
if (predicate(value, path)) {
|
|
50
|
+
results.push(path);
|
|
51
|
+
}
|
|
52
|
+
if (value !== null && typeof value === "object") {
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
for (let i = 0; i < value.length && results.length < maxResults; i++) {
|
|
55
|
+
traverse(value[i], `${path}[${i}]`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
for (const key of Object.keys(value)) {
|
|
60
|
+
if (results.length >= maxResults)
|
|
61
|
+
break;
|
|
62
|
+
const childPath = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)
|
|
63
|
+
? `${path}.${key}`
|
|
64
|
+
: `${path}["${key}"]`;
|
|
65
|
+
traverse(value[key], childPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
traverse(data, currentPath);
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
export function getDepthPreview(data, maxDepth = 2, currentDepth = 0) {
|
|
74
|
+
if (currentDepth >= maxDepth) {
|
|
75
|
+
if (data === null)
|
|
76
|
+
return null;
|
|
77
|
+
if (typeof data !== "object")
|
|
78
|
+
return data;
|
|
79
|
+
if (Array.isArray(data))
|
|
80
|
+
return `[... ${data.length} items]`;
|
|
81
|
+
return `{... ${Object.keys(data).length} keys}`;
|
|
82
|
+
}
|
|
83
|
+
if (data === null || typeof data !== "object") {
|
|
84
|
+
return data;
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(data)) {
|
|
87
|
+
return data.slice(0, 5).map((item) => getDepthPreview(item, maxDepth, currentDepth + 1));
|
|
88
|
+
}
|
|
89
|
+
const result = {};
|
|
90
|
+
const keys = Object.keys(data);
|
|
91
|
+
for (const key of keys.slice(0, 10)) {
|
|
92
|
+
result[key] = getDepthPreview(data[key], maxDepth, currentDepth + 1);
|
|
93
|
+
}
|
|
94
|
+
if (keys.length > 10) {
|
|
95
|
+
result["..."] = `+${keys.length - 10} more keys`;
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "json-explorer-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for efficiently exploring large JSON files",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"json-explorer-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"prepublishOnly": "npm run build && npm test"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"json",
|
|
25
|
+
"explorer",
|
|
26
|
+
"model-context-protocol"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
31
|
+
"ajv": "^8.17.1",
|
|
32
|
+
"ajv-formats": "^3.0.1",
|
|
33
|
+
"jsonpath-plus": "^10.0.0",
|
|
34
|
+
"zod": "^3.23.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"tsx": "^4.0.0",
|
|
39
|
+
"typescript": "^5.0.0",
|
|
40
|
+
"vitest": "^4.0.17"
|
|
41
|
+
}
|
|
42
|
+
}
|