UNPKG

mcp-json

Version:

MCP server for JSON file operations - read, analyze, search, find, relate, validate, and write JSON files

979 lines 40.4 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const ajv_1 = __importDefault(require("ajv")); const ajv_formats_1 = __importDefault(require("ajv-formats")); const JSONPath = __importStar(require("jsonpath")); const glob_1 = require("glob"); const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const lodash_1 = __importDefault(require("lodash")); class JSONMCPServer { constructor() { this.server = new index_js_1.Server({ name: 'mcp-json', version: '1.0.0', }, { capabilities: { tools: {}, }, }); this.ajv = new ajv_1.default({ allErrors: true, verbose: true }); (0, ajv_formats_1.default)(this.ajv); this.workingDirectory = process.cwd(); this.setupToolHandlers(); } setupToolHandlers() { this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: [ { name: 'find_json_files', description: 'Find JSON files in the current directory and subdirectories', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'Directory to search (defaults to current directory)', }, pattern: { type: 'string', description: 'Glob pattern for JSON files (defaults to **/*.json)', }, }, }, }, { name: 'read_json_file', description: 'Read and parse a JSON file', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the JSON file to read', }, }, required: ['filePath'], }, }, { name: 'analyze_json_file', description: 'Analyze a JSON file structure and content', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the JSON file to analyze', }, }, required: ['filePath'], }, }, { name: 'search_json_content', description: 'Search for specific content within JSON files', inputSchema: { type: 'object', properties: { searchTerm: { type: 'string', description: 'Term to search for in JSON content', }, directory: { type: 'string', description: 'Directory to search (defaults to current directory)', }, jsonPath: { type: 'string', description: 'JSONPath expression to search within specific paths', }, }, required: ['searchTerm'], }, }, { name: 'write_json_file', description: 'Write data to a JSON file', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path where to write the JSON file', }, data: { type: 'object', description: 'JavaScript object to convert to JSON', }, indent: { type: 'number', description: 'Number of spaces for indentation (defaults to 2)', }, }, required: ['filePath', 'data'], }, }, { name: 'transform_json', description: 'Transform JSON content using various operations', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the JSON file to transform', }, outputPath: { type: 'string', description: 'Path where to save the transformed JSON', }, operation: { type: 'string', enum: ['filter_keys', 'rename_keys', 'add_properties', 'remove_properties', 'flatten', 'unflatten'], description: 'Type of transformation to perform', }, parameters: { type: 'object', description: 'Parameters for the transformation operation', }, }, required: ['filePath', 'outputPath', 'operation'], }, }, { name: 'relate_json_files', description: 'Find relationships between JSON files based on content or structure', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'Directory to analyze (defaults to current directory)', }, relationType: { type: 'string', enum: ['common_keys', 'shared_values', 'similar_structure', 'reference_links'], description: 'Type of relationship to analyze', }, }, required: ['relationType'], }, }, { name: 'validate_json', description: 'Validate JSON file structure and content against a schema', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the JSON file to validate', }, schema: { type: 'object', description: 'JSON Schema to validate against (optional)', }, }, required: ['filePath'], }, }, { name: 'query_json', description: 'Query JSON files using JSONPath expressions', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the JSON file to query', }, jsonPath: { type: 'string', description: 'JSONPath expression to execute', }, }, required: ['filePath', 'jsonPath'], }, }, { name: 'merge_json_files', description: 'Merge multiple JSON files into one', inputSchema: { type: 'object', properties: { filePaths: { type: 'array', items: { type: 'string' }, description: 'Array of JSON file paths to merge', }, outputPath: { type: 'string', description: 'Path where to save the merged JSON', }, strategy: { type: 'string', enum: ['merge', 'concat', 'deep_merge'], description: 'Merge strategy to use', }, }, required: ['filePaths', 'outputPath', 'strategy'], }, }, ], })); this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'find_json_files': return await this.findJSONFiles(args); case 'read_json_file': return await this.readJSONFile(args); case 'analyze_json_file': return await this.analyzeJSONFile(args); case 'search_json_content': return await this.searchJSONContent(args); case 'write_json_file': return await this.writeJSONFile(args); case 'transform_json': return await this.transformJSON(args); case 'relate_json_files': return await this.relateJSONFiles(args); case 'validate_json': return await this.validateJSON(args); case 'query_json': return await this.queryJSON(args); case 'merge_json_files': return await this.mergeJSONFiles(args); default: throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { if (error instanceof types_js_1.McpError) { throw error; } throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Error executing ${name}: ${error}`); } }); } async findJSONFiles(args) { const directory = args.directory || this.workingDirectory; const pattern = args.pattern || '**/*.json'; try { const fullPattern = path_1.default.resolve(directory, pattern); const files = await (0, glob_1.glob)(fullPattern); return { content: [ { type: 'text', text: JSON.stringify({ directory, pattern, found: files.length, files: files.map(f => path_1.default.relative(this.workingDirectory, f)) }, null, 2) } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to find JSON files: ${error}`); } } async readJSONFile(args) { const { filePath } = args; try { const fullPath = path_1.default.resolve(this.workingDirectory, filePath); const content = await fs_extra_1.default.readFile(fullPath, 'utf8'); const parsedContent = JSON.parse(content); return { content: [ { type: 'text', text: JSON.stringify({ filePath, content: parsedContent, size: content.length, lines: content.split('\n').length }, null, 2) } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to read JSON file: ${error}`); } } async analyzeJSONFile(args) { const { filePath } = args; try { const fullPath = path_1.default.resolve(this.workingDirectory, filePath); const content = await fs_extra_1.default.readFile(fullPath, 'utf8'); const parsedContent = JSON.parse(content); const analysis = this.analyzeJSONStructure(parsedContent, content); return { content: [ { type: 'text', text: JSON.stringify({ filePath, analysis, summary: { totalKeys: analysis.keyCount, maxDepth: analysis.depth, fileSize: analysis.size, structure: analysis.valueTypes } }, null, 2) } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to analyze JSON file: ${error}`); } } analyzeJSONStructure(obj, rawContent) { const analysis = { keyCount: 0, keys: [], valueTypes: {}, depth: 0, arrayCount: 0, objectCount: 0, paths: [], size: rawContent.length }; const traverse = (node, path = '$', depth = 0) => { analysis.depth = Math.max(analysis.depth, depth); analysis.paths.push(path); if (Array.isArray(node)) { analysis.arrayCount++; analysis.valueTypes['array'] = (analysis.valueTypes['array'] || 0) + 1; node.forEach((item, index) => { traverse(item, `${path}[${index}]`, depth + 1); }); } else if (node !== null && typeof node === 'object') { analysis.objectCount++; analysis.valueTypes['object'] = (analysis.valueTypes['object'] || 0) + 1; Object.keys(node).forEach(key => { analysis.keyCount++; analysis.keys.push(key); const newPath = path === '$' ? `$.${key}` : `${path}.${key}`; traverse(node[key], newPath, depth + 1); }); } else { const type = typeof node; analysis.valueTypes[type] = (analysis.valueTypes[type] || 0) + 1; } }; traverse(obj); analysis.keys = [...new Set(analysis.keys)]; // Remove duplicates return analysis; } async searchJSONContent(args) { const { searchTerm, directory, jsonPath } = args; const searchDir = directory || this.workingDirectory; try { const files = await (0, glob_1.glob)(path_1.default.resolve(searchDir, '**/*.json')); const results = []; for (const file of files) { const content = await fs_extra_1.default.readFile(file, 'utf8'); const parsedContent = JSON.parse(content); let matches = []; if (jsonPath) { // Search within specific JSONPath try { const pathResults = JSONPath.query(parsedContent, jsonPath); matches = this.searchInValue(pathResults, searchTerm); } catch (error) { // Invalid JSONPath, skip } } else { // Search in entire JSON matches = this.searchInValue(parsedContent, searchTerm); } if (matches.length > 0) { results.push({ file: path_1.default.relative(this.workingDirectory, file), matches: matches.length, matchedPaths: matches }); } } return { content: [ { type: 'text', text: JSON.stringify({ searchTerm, directory: searchDir, jsonPath, totalFiles: files.length, matchedFiles: results.length, results }, null, 2) } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to search JSON content: ${error}`); } } searchInValue(obj, searchTerm, path = '$') { const matches = []; const search = (node, currentPath) => { if (typeof node === 'string' && node.toLowerCase().includes(searchTerm.toLowerCase())) { matches.push(currentPath); } else if (Array.isArray(node)) { node.forEach((item, index) => { search(item, `${currentPath}[${index}]`); }); } else if (node !== null && typeof node === 'object') { Object.keys(node).forEach(key => { const newPath = currentPath === '$' ? `$.${key}` : `${currentPath}.${key}`; if (key.toLowerCase().includes(searchTerm.toLowerCase())) { matches.push(newPath); } search(node[key], newPath); }); } }; search(obj, path); return matches; } async writeJSONFile(args) { const { filePath, data, indent = 2 } = args; try { const fullPath = path_1.default.resolve(this.workingDirectory, filePath); await fs_extra_1.default.ensureDir(path_1.default.dirname(fullPath)); const jsonString = JSON.stringify(data, null, indent); await fs_extra_1.default.writeFile(fullPath, jsonString, 'utf8'); return { content: [ { type: 'text', text: JSON.stringify({ filePath, status: 'success', size: jsonString.length, lines: jsonString.split('\n').length }, null, 2) } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to write JSON file: ${error}`); } } async transformJSON(args) { const { filePath, outputPath, operation, parameters = {} } = args; try { const fullPath = path_1.default.resolve(this.workingDirectory, filePath); const content = await fs_extra_1.default.readFile(fullPath, 'utf8'); const parsedContent = JSON.parse(content); let transformedData; switch (operation) { case 'filter_keys': transformedData = this.filterKeys(parsedContent, parameters.keysToKeep || []); break; case 'rename_keys': transformedData = this.renameKeys(parsedContent, parameters.keyMapping || {}); break; case 'add_properties': transformedData = this.addProperties(parsedContent, parameters.properties || {}); break; case 'remove_properties': transformedData = this.removeProperties(parsedContent, parameters.keysToRemove || []); break; case 'flatten': transformedData = this.flattenObject(parsedContent, parameters.separator || '.'); break; case 'unflatten': transformedData = this.unflattenObject(parsedContent, parameters.separator || '.'); break; default: throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Unknown operation: ${operation}`); } const outputFullPath = path_1.default.resolve(this.workingDirectory, outputPath); await fs_extra_1.default.ensureDir(path_1.default.dirname(outputFullPath)); await fs_extra_1.default.writeFile(outputFullPath, JSON.stringify(transformedData, null, 2), 'utf8'); return { content: [ { type: 'text', text: JSON.stringify({ inputFile: filePath, outputFile: outputPath, operation, parameters, status: 'success' }, null, 2) } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to transform JSON: ${error}`); } } filterKeys(obj, keysToKeep) { if (Array.isArray(obj)) { return obj.map(item => this.filterKeys(item, keysToKeep)); } else if (obj !== null && typeof obj === 'object') { const filtered = {}; keysToKeep.forEach(key => { if (key in obj) { filtered[key] = this.filterKeys(obj[key], keysToKeep); } }); return filtered; } return obj; } renameKeys(obj, keyMapping) { if (Array.isArray(obj)) { return obj.map(item => this.renameKeys(item, keyMapping)); } else if (obj !== null && typeof obj === 'object') { const renamed = {}; Object.keys(obj).forEach(key => { const newKey = keyMapping[key] || key; renamed[newKey] = this.renameKeys(obj[key], keyMapping); }); return renamed; } return obj; } addProperties(obj, properties) { if (Array.isArray(obj)) { return obj.map(item => this.addProperties(item, properties)); } else if (obj !== null && typeof obj === 'object') { return { ...obj, ...properties }; } return obj; } removeProperties(obj, keysToRemove) { if (Array.isArray(obj)) { return obj.map(item => this.removeProperties(item, keysToRemove)); } else if (obj !== null && typeof obj === 'object') { const filtered = {}; Object.keys(obj).forEach(key => { if (!keysToRemove.includes(key)) { filtered[key] = this.removeProperties(obj[key], keysToRemove); } }); return filtered; } return obj; } flattenObject(obj, separator) { const flatten = (current, prefix = '') => { const flattened = {}; Object.keys(current).forEach(key => { const newKey = prefix ? `${prefix}${separator}${key}` : key; if (current[key] !== null && typeof current[key] === 'object' && !Array.isArray(current[key])) { Object.assign(flattened, flatten(current[key], newKey)); } else { flattened[newKey] = current[key]; } }); return flattened; }; return flatten(obj); } unflattenObject(obj, separator) { const result = {}; Object.keys(obj).forEach(key => { const keys = key.split(separator); let current = result; keys.forEach((k, index) => { if (index === keys.length - 1) { current[k] = obj[key]; } else { if (!(k in current)) { current[k] = {}; } current = current[k]; } }); }); return result; } async relateJSONFiles(args) { const { directory, relationType } = args; const searchDir = directory || this.workingDirectory; try { const files = await (0, glob_1.glob)(path_1.default.resolve(searchDir, '**/*.json')); const relationships = []; for (let i = 0; i < files.length; i++) { for (let j = i + 1; j < files.length; j++) { const content1 = await fs_extra_1.default.readFile(files[i], 'utf8'); const content2 = await fs_extra_1.default.readFile(files[j], 'utf8'); const parsed1 = JSON.parse(content1); const parsed2 = JSON.parse(content2); const relationship = this.findRelationship(parsed1, parsed2, relationType); if (relationship) { relationships.push({ file1: path_1.default.relative(this.workingDirectory, files[i]), file2: path_1.default.relative(this.workingDirectory, files[j]), relationship: relationType, similarity: relationship.similarity, details: relationship.details }); } } } return { content: [ { type: 'text', text: JSON.stringify({ directory: searchDir, relationType, totalFiles: files.length, relationships: relationships.length, results: relationships }, null, 2) } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to relate JSON files: ${error}`); } } findRelationship(obj1, obj2, type) { switch (type) { case 'common_keys': return this.findCommonKeys(obj1, obj2); case 'shared_values': return this.findSharedValues(obj1, obj2); case 'similar_structure': return this.findSimilarStructure(obj1, obj2); case 'reference_links': return this.findReferenceLinks(obj1, obj2); default: return null; } } findCommonKeys(obj1, obj2) { const keys1 = this.getAllKeys(obj1); const keys2 = this.getAllKeys(obj2); const commonKeys = keys1.filter(key => keys2.includes(key)); if (commonKeys.length === 0) return null; const similarity = commonKeys.length / Math.max(keys1.length, keys2.length); return { similarity, details: `Common keys: ${commonKeys.join(', ')}` }; } findSharedValues(obj1, obj2) { const values1 = this.getAllValues(obj1); const values2 = this.getAllValues(obj2); const sharedValues = values1.filter(value => values2.includes(value)); if (sharedValues.length === 0) return null; const similarity = sharedValues.length / Math.max(values1.length, values2.length); return { similarity, details: `Shared values: ${sharedValues.slice(0, 5).join(', ')}${sharedValues.length > 5 ? '...' : ''}` }; } findSimilarStructure(obj1, obj2) { const structure1 = this.getStructure(obj1); const structure2 = this.getStructure(obj2); const similarity = this.calculateSimilarity(structure1, structure2); if (similarity < 0.3) return null; return { similarity, details: `Structure similarity: ${Math.round(similarity * 100)}%` }; } findReferenceLinks(obj1, obj2) { const references = this.findCrossReferences(obj1, obj2); if (references.length === 0) return null; return { similarity: 1.0, details: `Cross-references found: ${references.join(', ')}` }; } getAllKeys(obj) { const keys = []; const traverse = (node) => { if (Array.isArray(node)) { node.forEach(item => traverse(item)); } else if (node !== null && typeof node === 'object') { Object.keys(node).forEach(key => { keys.push(key); traverse(node[key]); }); } }; traverse(obj); return [...new Set(keys)]; // Remove duplicates } getAllValues(obj) { const values = []; const traverse = (node) => { if (Array.isArray(node)) { node.forEach(item => traverse(item)); } else if (node !== null && typeof node === 'object') { Object.values(node).forEach(value => traverse(value)); } else if (typeof node === 'string') { values.push(node); } }; traverse(obj); return values; } getStructure(obj) { const traverse = (node) => { if (Array.isArray(node)) { return `[${node.map(item => traverse(item)).join(',')}]`; } else if (node !== null && typeof node === 'object') { const keys = Object.keys(node).sort(); return `{${keys.map(key => `${key}:${traverse(node[key])}`).join(',')}}`; } else { return typeof node; } }; return traverse(obj); } calculateSimilarity(str1, str2) { const longer = str1.length > str2.length ? str1 : str2; const shorter = str1.length > str2.length ? str2 : str1; if (longer.length === 0) return 1.0; const editDistance = this.levenshteinDistance(longer, shorter); return (longer.length - editDistance) / longer.length; } levenshteinDistance(str1, str2) { const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null)); for (let i = 0; i <= str1.length; i++) matrix[0][i] = i; for (let j = 0; j <= str2.length; j++) matrix[j][0] = j; for (let j = 1; j <= str2.length; j++) { for (let i = 1; i <= str1.length; i++) { const substitutionCost = str1[i - 1] === str2[j - 1] ? 0 : 1; matrix[j][i] = Math.min(matrix[j][i - 1] + 1, // deletion matrix[j - 1][i] + 1, // insertion matrix[j - 1][i - 1] + substitutionCost // substitution ); } } return matrix[str2.length][str1.length]; } findCrossReferences(obj1, obj2) { const values1 = this.getAllValues(obj1); const values2 = this.getAllValues(obj2); const references = []; values1.forEach(value => { if (values2.includes(value) && value.length > 3) { references.push(value); } }); return [...new Set(references)]; } async validateJSON(args) { const { filePath, schema } = args; try { const fullPath = path_1.default.resolve(this.workingDirectory, filePath); const content = await fs_extra_1.default.readFile(fullPath, 'utf8'); // First, validate JSON syntax let parsedContent; try { parsedContent = JSON.parse(content); } catch (parseError) { return { content: [ { type: 'text', text: JSON.stringify({ filePath, valid: false, syntaxError: parseError instanceof Error ? parseError.message : String(parseError), type: 'syntax' }, null, 2) } ] }; } // If schema is provided, validate against schema if (schema) { const validate = this.ajv.compile(schema); const valid = validate(parsedContent); return { content: [ { type: 'text', text: JSON.stringify({ filePath, valid, errors: validate.errors || [], type: 'schema' }, null, 2) } ] }; } // Basic validation passed return { content: [ { type: 'text', text: JSON.stringify({ filePath, valid: true, type: 'syntax' }, null, 2) } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to validate JSON: ${error}`); } } async queryJSON(args) { const { filePath, jsonPath } = args; try { const fullPath = path_1.default.resolve(this.workingDirectory, filePath); const content = await fs_extra_1.default.readFile(fullPath, 'utf8'); const parsedContent = JSON.parse(content); const results = JSONPath.query(parsedContent, jsonPath); return { content: [ { type: 'text', text: JSON.stringify({ filePath, jsonPath, results, count: results.length }, null, 2) } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to query JSON: ${error}`); } } async mergeJSONFiles(args) { const { filePaths, outputPath, strategy } = args; try { const contents = await Promise.all(filePaths.map(async (filePath) => { const fullPath = path_1.default.resolve(this.workingDirectory, filePath); const content = await fs_extra_1.default.readFile(fullPath, 'utf8'); return JSON.parse(content); })); let mergedData; switch (strategy) { case 'merge': mergedData = Object.assign({}, ...contents); break; case 'concat': mergedData = contents.flat(); break; case 'deep_merge': mergedData = lodash_1.default.merge({}, ...contents); break; default: throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Unknown merge strategy: ${strategy}`); } const outputFullPath = path_1.default.resolve(this.workingDirectory, outputPath); await fs_extra_1.default.ensureDir(path_1.default.dirname(outputFullPath)); await fs_extra_1.default.writeFile(outputFullPath, JSON.stringify(mergedData, null, 2), 'utf8'); return { content: [ { type: 'text', text: JSON.stringify({ inputFiles: filePaths, outputFile: outputPath, strategy, status: 'success' }, null, 2) } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to merge JSON files: ${error}`); } } async run() { const transport = new stdio_js_1.StdioServerTransport(); await this.server.connect(transport); console.error('MCP JSON Server running on stdio'); } } const server = new JSONMCPServer(); server.run().catch(console.error); //# sourceMappingURL=index.js.map