# 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: -------------------------------------------------------------------------------- ``` { "semi": true, "trailingComma": "all", "singleQuote": true, "printWidth": 100, "tabWidth": 2 } ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` node_modules/ dist/ large-example.json .DS_Store *.log npm-debug.log* yarn-debug.log* yarn-error.log* .env coverage/ .vscode/ .idea/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # JSON Query MCP A Model Context Protocol (MCP) server for querying large JSON files. 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). ## Features - Query JSON files using JSONPath expressions - Search for keys similar to a query string - Search for values similar to a query string ## Example Here is an example of the Cursor Agent using the tool to read a a very large (>1M character) JSON Swagger definition, and extracting a small portion to write a typescript interface.  ## Usage npx json-query-mcp ## Installation in Cursor Add the following to your cursor mcp json (on macOS this is `/Users/$USER/.cursor/mcp.json`) ```mcp.json { "mcpServers": { ... other mcp servers "json-query": { "command": "npx", "args": [<local path to this repo>], }, } } ``` ## Development ```bash # Run in development mode npm run dev # Run tests npm test # Format code npm run format # Lint code npm run lint # Fix lints npm run fix ``` ## License MIT ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- ```javascript module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['**/*.test.ts'], }; ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript export interface JsonPathResult { path: string; value: unknown; } export interface SearchResult { path: string; similarity: number; value?: unknown; } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "nodenext", "moduleResolution": "nodenext", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "resolveJsonModule": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*", "eslint.config.mjs"], "exclude": ["node_modules", "jest.config.js"] } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { program } from 'commander'; import { createServerWithTools } from './server.js'; import packageJSON from '../package.json'; function setupExitWatchdog(server: McpServer) { // eslint-disable-next-line @typescript-eslint/no-misused-promises process.stdin.on('close', async () => { setTimeout(() => process.exit(0), 15000); await server.close(); process.exit(0); }); } program .version('Version ' + packageJSON.version) .name(packageJSON.name) .action(async () => { const server = createServerWithTools({ name: 'json-query', version: packageJSON.version, }); setupExitWatchdog(server); const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP server started'); }); program.parse(process.argv); ``` -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- ``` import eslint from '@eslint/js'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import tseslint from 'typescript-eslint'; export default tseslint.config( eslint.configs.recommended, tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked, eslintPluginPrettierRecommended, { ignores: [ '**/*.json', '**/dist/', '**/package.json', '**/package-lock.json', '**/examples/**', 'node_modules/**', 'jest.config.js', 'eslint.config.mjs', ], }, { languageOptions: { parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname, }, }, }, { files: ['**/*.js', '**/*.jsx', '**/*.json'], extends: [tseslint.configs.disableTypeChecked], }, { files: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'], rules: { '@typescript-eslint/no-unused-expressions': 'off', '@typescript-eslint/no-empty-function': 'off', }, }, { rules: { 'prettier/prettier': 'error', curly: ['error', 'multi-line'], }, }, ); ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "json-query-mcp", "version": "1.0.0", "description": "MCP server for querying large JSON files", "main": "dist/index.js", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "ts-node src/index.ts", "lint": "eslint . --ext .ts", "fix": "eslint . --ext .ts --fix", "format": "prettier --write \"src/**/*.ts\"", "test": "jest", "prestart": "npm run build", "preinstall": "npm run build" }, "keywords": [ "mcp", "json", "jsonpath", "search" ], "author": "Michael Graczyk", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.10.0", "commander": "^13.1.0", "jsonpath-plus": "^10.3.0", "string-similarity": "^4.0.4", "zod": "^3.24.3" }, "devDependencies": { "@eslint/js": "^9.17.0", "@types/jest": "^29.5.14", "@types/node": "^22.14.1", "@types/string-similarity": "^4.0.2", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.2", "eslint-plugin-prettier": "^5.2.6", "jest": "^29.7.0", "prettier": "^3.5.3", "ts-jest": "^29.3.2", "ts-node": "^10.9.2", "typescript": "^5.8.3", "typescript-eslint": "^8.30.1" }, "bin": "dist/index.js" } ``` -------------------------------------------------------------------------------- /COMMAND.md: -------------------------------------------------------------------------------- ```markdown Finishing implementing this MCP server (https://modelcontextprotocol.io/llms-full.txt) that does the following. The server implements "json query" tools. This will be used to provide content from a very large json file to a model. The MCP server should provide tools that do the following: 1. Query by JSONPath. Given a JSONPath, extract all the path evaluation against the provided json file 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) 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) Write it in typescript using npm and node. Please follow all common best practices and conventions. Don't do anything clever or strange. Document the code and make sure package.json is production ready. Use eslint and prettier for formatting with default but strict configurations. You still need to implement the tools and connect them to the server. You should use the types from "@modelcontextprotocol/sdk/types.js" wherever possible. Read the (https://modelcontextprotocol.io/llms-full.txt) to understand what is required. Do not modify the readme or do anything else. ``` -------------------------------------------------------------------------------- /src/jsonUtils.test.ts: -------------------------------------------------------------------------------- ```typescript import path from 'path'; import { JsonUtils } from './jsonUtils'; const exampleJsonPath = path.resolve(__dirname, '../example.json'); describe('JsonUtils', () => { describe('queryByJsonPath', () => { it('should return matching results for a valid path', async () => { const results = await JsonUtils.queryByJsonPath('$.store.book[*].title', exampleJsonPath); expect(results).toHaveLength(3); expect(results.map((r) => r.value)).toEqual([ 'Moby Dick', 'The Great Gatsby', 'A Brief History of Time', ]); }); }); describe('searchKeys', () => { it('should find keys similar to the query', async () => { const results = await JsonUtils.searchKeys('author', exampleJsonPath); expect(results.length).toBeGreaterThan(0); const authorMatch = results.find((r) => r.path.includes('author')); expect(authorMatch).toBeDefined(); expect(authorMatch?.similarity).toBeGreaterThan(0.5); }); }); describe('searchValues', () => { it('should find values similar to the query', async () => { const results = await JsonUtils.searchValues('Fitzgerald', exampleJsonPath); expect(results.length).toBeGreaterThan(0); const fitzgeraldMatch = results.find( (r) => typeof r.value === 'string' && r.value.includes('Fitzgerald'), ); expect(fitzgeraldMatch).toBeDefined(); expect(fitzgeraldMatch?.similarity).toBeGreaterThan(0.5); }); }); }); ``` -------------------------------------------------------------------------------- /examples/client-example.ts: -------------------------------------------------------------------------------- ```typescript import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; // For ES Modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); async function main(): Promise<void> { const exampleJsonPath = path.resolve(__dirname, '../example.json'); // Example MCP client request for queryByJsonPath const request = { version: '0.1', tool_calls: [ { name: 'queryByJsonPath', parameters: { path: '$.store.book[*].title', jsonFile: exampleJsonPath, }, }, { name: 'searchKeys', parameters: { query: 'author', jsonFile: exampleJsonPath, limit: 3, }, }, { name: 'searchValues', parameters: { query: 'Fitzgerald', jsonFile: exampleJsonPath, limit: 3, }, }, ], }; try { const response = await fetch('http://localhost:3000/v1/tools', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(request), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); console.log('MCP Response:'); console.log(JSON.stringify(data, null, 2)); } catch (error) { console.error('Error calling MCP server:', error); } } main().catch(console.error); // Example output: // // MCP Response: // { // "version": "0.1", // "results": [ // [ // { // "path": "$.store.book[0].title", // "value": "Moby Dick" // }, // { // "path": "$.store.book[1].title", // "value": "The Great Gatsby" // }, // { // "path": "$.store.book[2].title", // "value": "A Brief History of Time" // } // ], // [ // { // "path": "$.store.book[0].author", // "similarity": 0.7272727272727273 // }, // { // "path": "$.store.book[1].author", // "similarity": 0.7272727272727273 // }, // { // "path": "$.store.book[2].author", // "similarity": 0.7272727272727273 // } // ], // [ // { // "path": "$.store.book[1].author", // "similarity": 0.5882352941176471, // "value": "F. Scott Fitzgerald" // } // ] // ] // } ``` -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import path from 'path'; import { JsonUtils } from './jsonUtils.js'; interface Options { name: string; version: string; } const PATH_ARG_DESCRIPTION = "Absolute path to the JSON file."; const getErrorResponse = (error: unknown): CallToolResult => { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Error: ${errorMessage}`, }, ], isError: true, }; }; export function createServerWithTools(options: Options): McpServer { const { name, version } = options; const server = new McpServer({ name, version }); // Tool 1: Query by JSONPath server.tool( 'json_query_jsonpath', 'Query a JSON file using JSONPath. Use to get values precisely from large JSON files.', { file_path: z.string().describe(PATH_ARG_DESCRIPTION), jsonpath: z.string().min(1).describe('JSONPath expression to evaluate'), }, async ({ file_path, jsonpath }) => { try { const resolvedPath = path.resolve(file_path); const results = await JsonUtils.queryByJsonPath(jsonpath, resolvedPath); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } catch (error) { return getErrorResponse(error); } }, ); // Tool 2: Search keys server.tool( 'json_query_search_keys', '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.', { file_path: z.string().describe(PATH_ARG_DESCRIPTION), query: z.string().min(1).describe('Search term for finding matching keys'), limit: z .number() .int() .min(1) .max(100) .optional() .default(5) .describe('Maximum number of results to return (default: 5)'), }, async ({ file_path, query, limit }) => { try { const resolvedPath = path.resolve(file_path); const results = await JsonUtils.searchKeys(query, resolvedPath, limit); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } catch (error) { return getErrorResponse(error); } }, ); // Tool 3: Search values server.tool( 'json_query_search_values', '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.', { file_path: z.string().describe(PATH_ARG_DESCRIPTION), query: z.string().min(1).describe('Search term for finding matching values'), limit: z .number() .int() .min(1) .max(100) .optional() .default(5) .describe('Maximum number of results to return (default: 5)'), }, async ({ file_path, query, limit }) => { try { const resolvedPath = path.resolve(file_path); const results = await JsonUtils.searchValues(query, resolvedPath, limit); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } catch (error) { return getErrorResponse(error); } }, ); return server; } ``` -------------------------------------------------------------------------------- /examples/large-file-example.ts: -------------------------------------------------------------------------------- ```typescript import fs from 'fs'; import path from 'path'; // This example demonstrates creating and querying a larger JSON file async function generateLargeJson( filePath: string, itemCount = 1000, ): Promise<void> { const data = { items: Array.from({ length: itemCount }, (_, i) => ({ id: `item-${i}`, name: `Product ${i}`, description: `This is a description for product ${i}`, price: Math.round(Math.random() * 10000) / 100, categories: [ `category-${Math.floor(Math.random() * 10)}`, `category-${Math.floor(Math.random() * 10)}`, ], metadata: { created: new Date().toISOString(), status: ['active', 'inactive', 'archived'][ Math.floor(Math.random() * 3) ], tags: Array.from( { length: Math.floor(Math.random() * 5) + 1 }, () => `tag-${Math.floor(Math.random() * 20)}`, ), }, })), stats: { totalCount: itemCount, activeTags: Array.from({ length: 20 }, (_, i) => `tag-${i}`), priceRanges: { budget: { min: 0, max: 49.99 }, standard: { min: 50, max: 99.99 }, premium: { min: 100, max: Infinity }, }, }, }; await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2)); console.log(`Generated large JSON file (${itemCount} items) at: ${filePath}`); } async function queryMcpServer(jsonFilePath: string): Promise<void> { const queryExamples = [ { type: 'queryByJsonPath', title: 'Get products with price > 90', parameters: { path: '$.items[?(@.price > 90)]', jsonFile: jsonFilePath, }, }, { type: 'queryByJsonPath', title: 'Get all active products', parameters: { path: '$.items[?(@.metadata.status == "active")]', jsonFile: jsonFilePath, }, }, { type: 'searchKeys', title: 'Search for keys related to "tag"', parameters: { query: 'tag', jsonFile: jsonFilePath, limit: 3, }, }, { type: 'searchValues', title: 'Search for values containing "Product 5"', parameters: { query: 'Product 5', jsonFile: jsonFilePath, limit: 3, }, }, ]; for (const example of queryExamples) { console.log(`\nRunning: ${example.title}`); try { const response = await fetch('http://localhost:3000/v1/tools', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ version: '0.1', tool_calls: [ { name: example.type, parameters: example.parameters, }, ], }), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); console.log('Result:'); // Format the output to avoid overwhelming console if (example.type === 'queryByJsonPath') { console.log(`Found ${data.results[0].length} matches`); console.log('First 3 matches:'); console.log(JSON.stringify(data.results[0].slice(0, 3), null, 2)); } else { console.log(JSON.stringify(data.results[0], null, 2)); } } catch (error) { console.error(`Error executing ${example.title}:`, error); } } } async function main(): Promise<void> { const largeJsonPath = path.resolve(__dirname, '../large-example.json'); // Generate a large JSON file for testing await generateLargeJson(largeJsonPath, 1000); // Run various queries against the large file await queryMcpServer(largeJsonPath); } main().catch(console.error); ``` -------------------------------------------------------------------------------- /src/jsonUtils.ts: -------------------------------------------------------------------------------- ```typescript import fs from 'fs/promises'; import { JSONPath } from 'jsonpath-plus'; import stringSimilarity from 'string-similarity'; import { JsonPathResult, SearchResult } from './types.js'; // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class JsonUtils { private static async readJsonFile(filePath: string): Promise<unknown> { try { const content = await fs.readFile(filePath, 'utf-8'); return JSON.parse(content); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to read or parse JSON file: ${error}`); } else { throw new Error('Failed to read or parse JSON file'); } } } static async queryByJsonPath(path: string, jsonFile: string): Promise<JsonPathResult[]> { const data = await this.readJsonFile(jsonFile); // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const results = JSONPath({ path, json: data as object, resultType: 'all', }) as { path: string; value: unknown }[]; return results.map((result) => ({ path: result.path, value: result.value, })); } static async searchKeys(query: string, jsonFile: string, limit = 5): Promise<SearchResult[]> { const data = await this.readJsonFile(jsonFile); const keyPaths: { path: string; key: string }[] = []; const collectKeys = (obj: unknown, path = '$'): void => { if (obj && typeof obj === 'object') { if (Array.isArray(obj)) { obj.forEach((item, index) => { collectKeys(item, `${path}[${index.toString()}]`); }); } else { Object.entries(obj).forEach(([key, value]) => { const newPath = path === '$' ? `$.${key}` : `${path}.${key}`; keyPaths.push({ path: newPath, key }); collectKeys(value, newPath); }); } } }; collectKeys(data); const matches = keyPaths.map((item) => ({ path: item.path, similarity: stringSimilarity.compareTwoStrings(query.toLowerCase(), item.key.toLowerCase()), })); return matches.sort((a, b) => b.similarity - a.similarity).slice(0, limit); } static async searchValues(query: string, jsonFile: string, limit = 5): Promise<SearchResult[]> { const data = await this.readJsonFile(jsonFile); const valuePaths: { path: string; value: unknown }[] = []; const collectValues = (obj: unknown, path = '$'): void => { if (obj && typeof obj === 'object') { if (Array.isArray(obj)) { obj.forEach((item, index) => { const newPath = `${path}[${index.toString()}]`; if (typeof item === 'string' || typeof item === 'number') { valuePaths.push({ path: newPath, value: item }); } collectValues(item, newPath); }); } else { Object.entries(obj).forEach(([key, value]) => { const newPath = path === '$' ? `$.${key}` : `${path}.${key}`; if (typeof value === 'string' || typeof value === 'number') { valuePaths.push({ path: newPath, value }); } collectValues(value, newPath); }); } } }; collectValues(data); const stringQuery = String(query).toLowerCase(); const matches = valuePaths .filter((item) => typeof item.value === 'string' || typeof item.value === 'number') .map((item) => ({ path: item.path, similarity: stringSimilarity.compareTwoStrings( stringQuery, String(item.value).toLowerCase(), ), value: item.value, })); return matches.sort((a, b) => b.similarity - a.similarity).slice(0, limit); } } ```