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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ }