# 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 | ```