# Directory Structure
```
├── .gitignore
├── .prettierrc
├── api-extended-json.json
├── COMMAND.md
├── eslint.config.mjs
├── example.png
├── examples
│ ├── client-example.ts
│ └── large-file-example.ts
├── jest.config.js
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── index.ts
│ ├── jsonUtils.test.ts
│ ├── jsonUtils.ts
│ ├── server.ts
│ └── types.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
```
1 | {
2 | "semi": true,
3 | "trailingComma": "all",
4 | "singleQuote": true,
5 | "printWidth": 100,
6 | "tabWidth": 2
7 | }
8 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | dist/
3 | large-example.json
4 | .DS_Store
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 | .env
10 | coverage/
11 | .vscode/
12 | .idea/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # JSON Query MCP
2 |
3 | A Model Context Protocol (MCP) server for querying large JSON files.
4 | This server provides tools for working with large JSON data that can be used by LLM models implementing the [Model Context Protocol](https://modelcontextprotocol.io).
5 |
6 | ## Features
7 |
8 | - Query JSON files using JSONPath expressions
9 | - Search for keys similar to a query string
10 | - Search for values similar to a query string
11 |
12 | ## Example
13 |
14 | Here is an example of the Cursor Agent using the tool to read a a very large (>1M character) JSON Swagger
15 | definition, and extracting a small portion to write a typescript interface.
16 |
17 | 
18 |
19 | ## Usage
20 |
21 | npx json-query-mcp
22 |
23 | ## Installation in Cursor
24 |
25 | Add the following to your cursor mcp json
26 | (on macOS this is `/Users/$USER/.cursor/mcp.json`)
27 |
28 | ```mcp.json
29 | {
30 | "mcpServers": {
31 | ... other mcp servers
32 | "json-query": {
33 | "command": "npx",
34 | "args": [<local path to this repo>],
35 | },
36 | }
37 | }
38 | ```
39 |
40 | ## Development
41 |
42 | ```bash
43 | # Run in development mode
44 | npm run dev
45 |
46 | # Run tests
47 | npm test
48 |
49 | # Format code
50 | npm run format
51 |
52 | # Lint code
53 | npm run lint
54 |
55 | # Fix lints
56 | npm run fix
57 | ```
58 |
59 | ## License
60 |
61 | MIT
62 |
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: ['**/*.test.ts'],
5 | };
6 |
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface JsonPathResult {
2 | path: string;
3 | value: unknown;
4 | }
5 |
6 | export interface SearchResult {
7 | path: string;
8 | similarity: number;
9 | value?: unknown;
10 | }
11 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "nodenext",
5 | "moduleResolution": "nodenext",
6 | "lib": ["ES2022"],
7 | "outDir": "./dist",
8 | "rootDir": "./src",
9 | "strict": true,
10 | "esModuleInterop": true,
11 | "skipLibCheck": true,
12 | "resolveJsonModule": true,
13 | "forceConsistentCasingInFileNames": true
14 | },
15 | "include": ["src/**/*", "eslint.config.mjs"],
16 | "exclude": ["node_modules", "jest.config.js"]
17 | }
18 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4 | import { program } from 'commander';
5 | import { createServerWithTools } from './server.js';
6 |
7 | import packageJSON from '../package.json';
8 |
9 | function setupExitWatchdog(server: McpServer) {
10 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
11 | process.stdin.on('close', async () => {
12 | setTimeout(() => process.exit(0), 15000);
13 | await server.close();
14 | process.exit(0);
15 | });
16 | }
17 |
18 | program
19 | .version('Version ' + packageJSON.version)
20 | .name(packageJSON.name)
21 | .action(async () => {
22 | const server = createServerWithTools({
23 | name: 'json-query',
24 | version: packageJSON.version,
25 | });
26 | setupExitWatchdog(server);
27 |
28 | const transport = new StdioServerTransport();
29 | await server.connect(transport);
30 |
31 | console.error('MCP server started');
32 | });
33 | program.parse(process.argv);
34 |
```
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
```
1 | import eslint from '@eslint/js';
2 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
3 | import tseslint from 'typescript-eslint';
4 |
5 | export default tseslint.config(
6 | eslint.configs.recommended,
7 | tseslint.configs.strictTypeChecked,
8 | tseslint.configs.stylisticTypeChecked,
9 | eslintPluginPrettierRecommended,
10 | {
11 | ignores: [
12 | '**/*.json',
13 | '**/dist/',
14 | '**/package.json',
15 | '**/package-lock.json',
16 | '**/examples/**',
17 | 'node_modules/**',
18 | 'jest.config.js',
19 | 'eslint.config.mjs',
20 | ],
21 | },
22 | {
23 | languageOptions: {
24 | parserOptions: {
25 | projectService: true,
26 | tsconfigRootDir: import.meta.dirname,
27 | },
28 | },
29 | },
30 | {
31 | files: ['**/*.js', '**/*.jsx', '**/*.json'],
32 | extends: [tseslint.configs.disableTypeChecked],
33 | },
34 | {
35 | files: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'],
36 | rules: {
37 | '@typescript-eslint/no-unused-expressions': 'off',
38 | '@typescript-eslint/no-empty-function': 'off',
39 | },
40 | },
41 | {
42 | rules: {
43 | 'prettier/prettier': 'error',
44 | curly: ['error', 'multi-line'],
45 | },
46 | },
47 | );
48 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "json-query-mcp",
3 | "version": "1.0.0",
4 | "description": "MCP server for querying large JSON files",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "build": "tsc",
8 | "start": "node dist/index.js",
9 | "dev": "ts-node src/index.ts",
10 | "lint": "eslint . --ext .ts",
11 | "fix": "eslint . --ext .ts --fix",
12 | "format": "prettier --write \"src/**/*.ts\"",
13 | "test": "jest",
14 | "prestart": "npm run build",
15 | "preinstall": "npm run build"
16 | },
17 | "keywords": [
18 | "mcp",
19 | "json",
20 | "jsonpath",
21 | "search"
22 | ],
23 | "author": "Michael Graczyk",
24 | "license": "MIT",
25 | "dependencies": {
26 | "@modelcontextprotocol/sdk": "^1.10.0",
27 | "commander": "^13.1.0",
28 | "jsonpath-plus": "^10.3.0",
29 | "string-similarity": "^4.0.4",
30 | "zod": "^3.24.3"
31 | },
32 | "devDependencies": {
33 | "@eslint/js": "^9.17.0",
34 | "@types/jest": "^29.5.14",
35 | "@types/node": "^22.14.1",
36 | "@types/string-similarity": "^4.0.2",
37 | "eslint": "^9.24.0",
38 | "eslint-config-prettier": "^10.1.2",
39 | "eslint-plugin-prettier": "^5.2.6",
40 | "jest": "^29.7.0",
41 | "prettier": "^3.5.3",
42 | "ts-jest": "^29.3.2",
43 | "ts-node": "^10.9.2",
44 | "typescript": "^5.8.3",
45 | "typescript-eslint": "^8.30.1"
46 | },
47 | "bin": "dist/index.js"
48 | }
49 |
```
--------------------------------------------------------------------------------
/COMMAND.md:
--------------------------------------------------------------------------------
```markdown
1 | Finishing implementing this MCP server (https://modelcontextprotocol.io/llms-full.txt) that does the following.
2 | The server implements "json query" tools. This will be used to provide content from a very large json file to a model.
3 | The MCP server should provide tools that do the following:
4 | 1. Query by JSONPath. Given a JSONPath, extract all the path evaluation against the provided json file
5 | 2. Search keys by string. Given a string, search for any keys that are close to that string. Returns a jsonpath to N matching keys sorted in relevance order (N=5 by default)
6 | 3. Search values. Given a value, search for any values that are close to that string. Returns JSONPaths of N matching values in relevance order (N=5 by default)
7 |
8 | Write it in typescript using npm and node.
9 | Please follow all common best practices and conventions.
10 | Don't do anything clever or strange.
11 | Document the code and make sure package.json is production ready.
12 | Use eslint and prettier for formatting with default but strict configurations.
13 |
14 | You still need to implement the tools and connect them to the server.
15 | You should use the types from "@modelcontextprotocol/sdk/types.js" wherever possible.
16 | Read the (https://modelcontextprotocol.io/llms-full.txt) to understand what is required.
17 | Do not modify the readme or do anything else.
18 |
```
--------------------------------------------------------------------------------
/src/jsonUtils.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import path from 'path';
2 | import { JsonUtils } from './jsonUtils';
3 |
4 | const exampleJsonPath = path.resolve(__dirname, '../example.json');
5 |
6 | describe('JsonUtils', () => {
7 | describe('queryByJsonPath', () => {
8 | it('should return matching results for a valid path', async () => {
9 | const results = await JsonUtils.queryByJsonPath('$.store.book[*].title', exampleJsonPath);
10 |
11 | expect(results).toHaveLength(3);
12 | expect(results.map((r) => r.value)).toEqual([
13 | 'Moby Dick',
14 | 'The Great Gatsby',
15 | 'A Brief History of Time',
16 | ]);
17 | });
18 | });
19 |
20 | describe('searchKeys', () => {
21 | it('should find keys similar to the query', async () => {
22 | const results = await JsonUtils.searchKeys('author', exampleJsonPath);
23 |
24 | expect(results.length).toBeGreaterThan(0);
25 |
26 | const authorMatch = results.find((r) => r.path.includes('author'));
27 | expect(authorMatch).toBeDefined();
28 | expect(authorMatch?.similarity).toBeGreaterThan(0.5);
29 | });
30 | });
31 |
32 | describe('searchValues', () => {
33 | it('should find values similar to the query', async () => {
34 | const results = await JsonUtils.searchValues('Fitzgerald', exampleJsonPath);
35 |
36 | expect(results.length).toBeGreaterThan(0);
37 |
38 | const fitzgeraldMatch = results.find(
39 | (r) => typeof r.value === 'string' && r.value.includes('Fitzgerald'),
40 | );
41 | expect(fitzgeraldMatch).toBeDefined();
42 | expect(fitzgeraldMatch?.similarity).toBeGreaterThan(0.5);
43 | });
44 | });
45 | });
46 |
```
--------------------------------------------------------------------------------
/examples/client-example.ts:
--------------------------------------------------------------------------------
```typescript
1 | import path from 'path';
2 | import { fileURLToPath } from 'url';
3 | import fs from 'fs';
4 |
5 | // For ES Modules
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = path.dirname(__filename);
8 |
9 | async function main(): Promise<void> {
10 | const exampleJsonPath = path.resolve(__dirname, '../example.json');
11 |
12 | // Example MCP client request for queryByJsonPath
13 | const request = {
14 | version: '0.1',
15 | tool_calls: [
16 | {
17 | name: 'queryByJsonPath',
18 | parameters: {
19 | path: '$.store.book[*].title',
20 | jsonFile: exampleJsonPath,
21 | },
22 | },
23 | {
24 | name: 'searchKeys',
25 | parameters: {
26 | query: 'author',
27 | jsonFile: exampleJsonPath,
28 | limit: 3,
29 | },
30 | },
31 | {
32 | name: 'searchValues',
33 | parameters: {
34 | query: 'Fitzgerald',
35 | jsonFile: exampleJsonPath,
36 | limit: 3,
37 | },
38 | },
39 | ],
40 | };
41 |
42 | try {
43 | const response = await fetch('http://localhost:3000/v1/tools', {
44 | method: 'POST',
45 | headers: {
46 | 'Content-Type': 'application/json',
47 | },
48 | body: JSON.stringify(request),
49 | });
50 |
51 | if (!response.ok) {
52 | throw new Error(`HTTP error! status: ${response.status}`);
53 | }
54 |
55 | const data = await response.json();
56 | console.log('MCP Response:');
57 | console.log(JSON.stringify(data, null, 2));
58 | } catch (error) {
59 | console.error('Error calling MCP server:', error);
60 | }
61 | }
62 |
63 | main().catch(console.error);
64 |
65 | // Example output:
66 | //
67 | // MCP Response:
68 | // {
69 | // "version": "0.1",
70 | // "results": [
71 | // [
72 | // {
73 | // "path": "$.store.book[0].title",
74 | // "value": "Moby Dick"
75 | // },
76 | // {
77 | // "path": "$.store.book[1].title",
78 | // "value": "The Great Gatsby"
79 | // },
80 | // {
81 | // "path": "$.store.book[2].title",
82 | // "value": "A Brief History of Time"
83 | // }
84 | // ],
85 | // [
86 | // {
87 | // "path": "$.store.book[0].author",
88 | // "similarity": 0.7272727272727273
89 | // },
90 | // {
91 | // "path": "$.store.book[1].author",
92 | // "similarity": 0.7272727272727273
93 | // },
94 | // {
95 | // "path": "$.store.book[2].author",
96 | // "similarity": 0.7272727272727273
97 | // }
98 | // ],
99 | // [
100 | // {
101 | // "path": "$.store.book[1].author",
102 | // "similarity": 0.5882352941176471,
103 | // "value": "F. Scott Fitzgerald"
104 | // }
105 | // ]
106 | // ]
107 | // }
108 |
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3 |
4 | import { z } from 'zod';
5 | import path from 'path';
6 |
7 | import { JsonUtils } from './jsonUtils.js';
8 |
9 | interface Options {
10 | name: string;
11 | version: string;
12 | }
13 |
14 | const PATH_ARG_DESCRIPTION = "Absolute path to the JSON file.";
15 |
16 | const getErrorResponse = (error: unknown): CallToolResult => {
17 | const errorMessage = error instanceof Error ? error.message : String(error);
18 | return {
19 | content: [
20 | {
21 | type: 'text',
22 | text: `Error: ${errorMessage}`,
23 | },
24 | ],
25 | isError: true,
26 | };
27 | };
28 |
29 | export function createServerWithTools(options: Options): McpServer {
30 | const { name, version } = options;
31 |
32 | const server = new McpServer({ name, version });
33 |
34 | // Tool 1: Query by JSONPath
35 | server.tool(
36 | 'json_query_jsonpath',
37 | 'Query a JSON file using JSONPath. Use to get values precisely from large JSON files.',
38 | {
39 | file_path: z.string().describe(PATH_ARG_DESCRIPTION),
40 | jsonpath: z.string().min(1).describe('JSONPath expression to evaluate'),
41 | },
42 | async ({ file_path, jsonpath }) => {
43 | try {
44 | const resolvedPath = path.resolve(file_path);
45 |
46 | const results = await JsonUtils.queryByJsonPath(jsonpath, resolvedPath);
47 |
48 | return {
49 | content: [
50 | {
51 | type: 'text',
52 | text: JSON.stringify(results, null, 2),
53 | },
54 | ],
55 | };
56 | } catch (error) {
57 | return getErrorResponse(error);
58 | }
59 | },
60 | );
61 |
62 | // Tool 2: Search keys
63 | server.tool(
64 | 'json_query_search_keys',
65 | 'Search for keys in a JSON file. Use when you do not know the path to a key in a large JSON file, but have some idea what the key is.',
66 | {
67 | file_path: z.string().describe(PATH_ARG_DESCRIPTION),
68 | query: z.string().min(1).describe('Search term for finding matching keys'),
69 | limit: z
70 | .number()
71 | .int()
72 | .min(1)
73 | .max(100)
74 | .optional()
75 | .default(5)
76 | .describe('Maximum number of results to return (default: 5)'),
77 | },
78 | async ({ file_path, query, limit }) => {
79 | try {
80 | const resolvedPath = path.resolve(file_path);
81 |
82 | const results = await JsonUtils.searchKeys(query, resolvedPath, limit);
83 |
84 | return {
85 | content: [
86 | {
87 | type: 'text',
88 | text: JSON.stringify(results, null, 2),
89 | },
90 | ],
91 | };
92 | } catch (error) {
93 | return getErrorResponse(error);
94 | }
95 | },
96 | );
97 |
98 | // Tool 3: Search values
99 | server.tool(
100 | 'json_query_search_values',
101 | 'Search for values in a JSON file. Use when you do not know the path to a value in a large JSON file, but have some idea what the value is.',
102 | {
103 | file_path: z.string().describe(PATH_ARG_DESCRIPTION),
104 | query: z.string().min(1).describe('Search term for finding matching values'),
105 | limit: z
106 | .number()
107 | .int()
108 | .min(1)
109 | .max(100)
110 | .optional()
111 | .default(5)
112 | .describe('Maximum number of results to return (default: 5)'),
113 | },
114 | async ({ file_path, query, limit }) => {
115 | try {
116 | const resolvedPath = path.resolve(file_path);
117 |
118 | const results = await JsonUtils.searchValues(query, resolvedPath, limit);
119 |
120 | return {
121 | content: [
122 | {
123 | type: 'text',
124 | text: JSON.stringify(results, null, 2),
125 | },
126 | ],
127 | };
128 | } catch (error) {
129 | return getErrorResponse(error);
130 | }
131 | },
132 | );
133 |
134 | return server;
135 | }
136 |
```
--------------------------------------------------------------------------------
/examples/large-file-example.ts:
--------------------------------------------------------------------------------
```typescript
1 | import fs from 'fs';
2 | import path from 'path';
3 |
4 | // This example demonstrates creating and querying a larger JSON file
5 |
6 | async function generateLargeJson(
7 | filePath: string,
8 | itemCount = 1000,
9 | ): Promise<void> {
10 | const data = {
11 | items: Array.from({ length: itemCount }, (_, i) => ({
12 | id: `item-${i}`,
13 | name: `Product ${i}`,
14 | description: `This is a description for product ${i}`,
15 | price: Math.round(Math.random() * 10000) / 100,
16 | categories: [
17 | `category-${Math.floor(Math.random() * 10)}`,
18 | `category-${Math.floor(Math.random() * 10)}`,
19 | ],
20 | metadata: {
21 | created: new Date().toISOString(),
22 | status: ['active', 'inactive', 'archived'][
23 | Math.floor(Math.random() * 3)
24 | ],
25 | tags: Array.from(
26 | { length: Math.floor(Math.random() * 5) + 1 },
27 | () => `tag-${Math.floor(Math.random() * 20)}`,
28 | ),
29 | },
30 | })),
31 | stats: {
32 | totalCount: itemCount,
33 | activeTags: Array.from({ length: 20 }, (_, i) => `tag-${i}`),
34 | priceRanges: {
35 | budget: { min: 0, max: 49.99 },
36 | standard: { min: 50, max: 99.99 },
37 | premium: { min: 100, max: Infinity },
38 | },
39 | },
40 | };
41 |
42 | await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2));
43 | console.log(`Generated large JSON file (${itemCount} items) at: ${filePath}`);
44 | }
45 |
46 | async function queryMcpServer(jsonFilePath: string): Promise<void> {
47 | const queryExamples = [
48 | {
49 | type: 'queryByJsonPath',
50 | title: 'Get products with price > 90',
51 | parameters: {
52 | path: '$.items[?(@.price > 90)]',
53 | jsonFile: jsonFilePath,
54 | },
55 | },
56 | {
57 | type: 'queryByJsonPath',
58 | title: 'Get all active products',
59 | parameters: {
60 | path: '$.items[?(@.metadata.status == "active")]',
61 | jsonFile: jsonFilePath,
62 | },
63 | },
64 | {
65 | type: 'searchKeys',
66 | title: 'Search for keys related to "tag"',
67 | parameters: {
68 | query: 'tag',
69 | jsonFile: jsonFilePath,
70 | limit: 3,
71 | },
72 | },
73 | {
74 | type: 'searchValues',
75 | title: 'Search for values containing "Product 5"',
76 | parameters: {
77 | query: 'Product 5',
78 | jsonFile: jsonFilePath,
79 | limit: 3,
80 | },
81 | },
82 | ];
83 |
84 | for (const example of queryExamples) {
85 | console.log(`\nRunning: ${example.title}`);
86 |
87 | try {
88 | const response = await fetch('http://localhost:3000/v1/tools', {
89 | method: 'POST',
90 | headers: {
91 | 'Content-Type': 'application/json',
92 | },
93 | body: JSON.stringify({
94 | version: '0.1',
95 | tool_calls: [
96 | {
97 | name: example.type,
98 | parameters: example.parameters,
99 | },
100 | ],
101 | }),
102 | });
103 |
104 | if (!response.ok) {
105 | throw new Error(`HTTP error! status: ${response.status}`);
106 | }
107 |
108 | const data = await response.json();
109 | console.log('Result:');
110 |
111 | // Format the output to avoid overwhelming console
112 | if (example.type === 'queryByJsonPath') {
113 | console.log(`Found ${data.results[0].length} matches`);
114 | console.log('First 3 matches:');
115 | console.log(JSON.stringify(data.results[0].slice(0, 3), null, 2));
116 | } else {
117 | console.log(JSON.stringify(data.results[0], null, 2));
118 | }
119 | } catch (error) {
120 | console.error(`Error executing ${example.title}:`, error);
121 | }
122 | }
123 | }
124 |
125 | async function main(): Promise<void> {
126 | const largeJsonPath = path.resolve(__dirname, '../large-example.json');
127 |
128 | // Generate a large JSON file for testing
129 | await generateLargeJson(largeJsonPath, 1000);
130 |
131 | // Run various queries against the large file
132 | await queryMcpServer(largeJsonPath);
133 | }
134 |
135 | main().catch(console.error);
136 |
```
--------------------------------------------------------------------------------
/src/jsonUtils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import fs from 'fs/promises';
2 | import { JSONPath } from 'jsonpath-plus';
3 | import stringSimilarity from 'string-similarity';
4 | import { JsonPathResult, SearchResult } from './types.js';
5 |
6 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class
7 | export class JsonUtils {
8 | private static async readJsonFile(filePath: string): Promise<unknown> {
9 | try {
10 | const content = await fs.readFile(filePath, 'utf-8');
11 | return JSON.parse(content);
12 | } catch (error) {
13 | if (error instanceof Error) {
14 | throw new Error(`Failed to read or parse JSON file: ${error}`);
15 | } else {
16 | throw new Error('Failed to read or parse JSON file');
17 | }
18 | }
19 | }
20 |
21 | static async queryByJsonPath(path: string, jsonFile: string): Promise<JsonPathResult[]> {
22 | const data = await this.readJsonFile(jsonFile);
23 |
24 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
25 | const results = JSONPath({
26 | path,
27 | json: data as object,
28 | resultType: 'all',
29 | }) as { path: string; value: unknown }[];
30 |
31 | return results.map((result) => ({
32 | path: result.path,
33 | value: result.value,
34 | }));
35 | }
36 |
37 | static async searchKeys(query: string, jsonFile: string, limit = 5): Promise<SearchResult[]> {
38 | const data = await this.readJsonFile(jsonFile);
39 | const keyPaths: { path: string; key: string }[] = [];
40 |
41 | const collectKeys = (obj: unknown, path = '$'): void => {
42 | if (obj && typeof obj === 'object') {
43 | if (Array.isArray(obj)) {
44 | obj.forEach((item, index) => {
45 | collectKeys(item, `${path}[${index.toString()}]`);
46 | });
47 | } else {
48 | Object.entries(obj).forEach(([key, value]) => {
49 | const newPath = path === '$' ? `$.${key}` : `${path}.${key}`;
50 | keyPaths.push({ path: newPath, key });
51 | collectKeys(value, newPath);
52 | });
53 | }
54 | }
55 | };
56 |
57 | collectKeys(data);
58 |
59 | const matches = keyPaths.map((item) => ({
60 | path: item.path,
61 | similarity: stringSimilarity.compareTwoStrings(query.toLowerCase(), item.key.toLowerCase()),
62 | }));
63 |
64 | return matches.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
65 | }
66 |
67 | static async searchValues(query: string, jsonFile: string, limit = 5): Promise<SearchResult[]> {
68 | const data = await this.readJsonFile(jsonFile);
69 | const valuePaths: { path: string; value: unknown }[] = [];
70 |
71 | const collectValues = (obj: unknown, path = '$'): void => {
72 | if (obj && typeof obj === 'object') {
73 | if (Array.isArray(obj)) {
74 | obj.forEach((item, index) => {
75 | const newPath = `${path}[${index.toString()}]`;
76 | if (typeof item === 'string' || typeof item === 'number') {
77 | valuePaths.push({ path: newPath, value: item });
78 | }
79 | collectValues(item, newPath);
80 | });
81 | } else {
82 | Object.entries(obj).forEach(([key, value]) => {
83 | const newPath = path === '$' ? `$.${key}` : `${path}.${key}`;
84 | if (typeof value === 'string' || typeof value === 'number') {
85 | valuePaths.push({ path: newPath, value });
86 | }
87 | collectValues(value, newPath);
88 | });
89 | }
90 | }
91 | };
92 |
93 | collectValues(data);
94 |
95 | const stringQuery = String(query).toLowerCase();
96 | const matches = valuePaths
97 | .filter((item) => typeof item.value === 'string' || typeof item.value === 'number')
98 | .map((item) => ({
99 | path: item.path,
100 | similarity: stringSimilarity.compareTwoStrings(
101 | stringQuery,
102 | String(item.value).toLowerCase(),
103 | ),
104 | value: item.value,
105 | }));
106 |
107 | return matches.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
108 | }
109 | }
110 |
```