#
tokens: 5766/50000 15/15 files
lines: off (toggle) GitHub
raw markdown copy
# 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.

![Example](./example.png)

## 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);
  }
}

```