Skip to main content
Glama
index.ts13.6 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, CallToolResult, } from '@modelcontextprotocol/sdk/types.js'; import { promises as fs } from 'fs'; import path from 'path'; class JsonEditorMCPServer { private server: Server; constructor() { this.server = new Server( { name: 'json-editor-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); } private validateAbsolutePath(filePath: string): void { if (!path.isAbsolute(filePath)) { throw new Error(`Path must be absolute: ${filePath}`); } } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'merge_duplicate_keys', description: 'Deep merge duplicate keys in a JSON file. Last value wins for primitives, objects merge recursively.', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the JSON file', }, }, required: ['filePath'], }, }, { name: 'read_multiple_json_values', description: 'Read values from multiple JSON files at a specified path using dot notation. Returns a map of file paths to values.', inputSchema: { type: 'object', properties: { filePaths: { type: 'array', items: { type: 'string' }, description: 'Array of paths to JSON files', }, path: { type: 'string', description: 'Dot notation path to the value (e.g., "common.welcome")', }, }, required: ['filePaths', 'path'], }, }, { name: 'write_json_values', description: 'Write a value to a JSON file at a specified path using dot notation. Creates missing paths automatically.', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Absolute path to the JSON file', }, path: { type: 'string', description: 'Dot notation path to the value (e.g., "common.welcome")', }, value: { description: 'Value to write (any JSON-serializable type)', }, }, required: ['filePath', 'path', 'value'], }, }, { name: 'delete_multiple_json_values', description: 'Delete a value at a specified path from multiple JSON files using dot notation. Returns a map of file paths to deletion results.', inputSchema: { type: 'object', properties: { filePaths: { type: 'array', items: { type: 'string' }, description: 'Array of paths to JSON files', }, path: { type: 'string', description: 'Dot notation path to the value to delete (e.g., "common.welcome")', }, }, required: ['filePaths', 'path'], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (!args) { throw new Error('No arguments provided'); } try { switch (name) { case 'merge_duplicate_keys': return await this.mergeDuplicateKeys(args.filePath as string); case 'read_multiple_json_values': return await this.readMultipleJsonValues(args.filePaths as string[], args.path as string); case 'write_json_values': return await this.writeJsonValues(args.filePath as string, args.path as string, args.value); case 'delete_multiple_json_values': return await this.deleteMultipleJsonValues(args.filePaths as string[], args.path as string); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); } private parsePythonDict(value: string): any { try { let jsonString = value.trim(); if (!jsonString.startsWith('{') || !jsonString.endsWith('}')) { throw new Error('Not a Python dict'); } jsonString = jsonString .replace(/'/g, '"') .replace(/True/g, 'true') .replace(/False/g, 'false') .replace(/None/g, 'null'); return JSON.parse(jsonString); } catch { throw new Error('Failed to parse Python dict'); } } private async writeJsonValues(filePath: string, path: string, value: any): Promise<CallToolResult> { this.validateAbsolutePath(filePath); let processedValue = value; if (typeof value === 'string') { try { const parsed = JSON.parse(value); if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) { processedValue = parsed; } else { processedValue = parsed; } } catch { try { processedValue = this.parsePythonDict(value); } catch { processedValue = value; } } } try { let jsonData = await this.readJsonFile(filePath); if (processedValue !== null && typeof processedValue === 'object' && !Array.isArray(processedValue)) { const entries = Object.entries(processedValue); if (entries.length === 0) { this.setValueAtPath(jsonData, path, processedValue); } else { for (const [key, val] of entries) { const nestedPath = path ? `${path}.${key}` : key; this.setValueAtPath(jsonData, nestedPath, val); } } } else { this.setValueAtPath(jsonData, path, processedValue); } await this.writeJsonFile(filePath, jsonData); return { content: [ { type: 'text', text: `Successfully wrote to ${filePath}`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } private async mergeDuplicateKeys(filePath: string): Promise<CallToolResult> { this.validateAbsolutePath(filePath); const jsonData = await this.readJsonFile(filePath); const mergedData = this.deepMergeDuplicates(jsonData); await this.writeJsonFile(filePath, mergedData); return { content: [ { type: 'text', text: `Successfully merged duplicate keys in ${filePath}`, }, ], }; } private async readMultipleJsonValues(filePaths: string[], path: string): Promise<CallToolResult> { for (const filePath of filePaths) { this.validateAbsolutePath(filePath); } const results: Record<string, any> = {}; for (const filePath of filePaths) { try { const jsonData = await this.readJsonFile(filePath); const value = this.getValueAtPath(jsonData, path); results[filePath] = value; } catch (error) { results[filePath] = `Error: ${error instanceof Error ? error.message : String(error)}`; } } return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } private async deleteMultipleJsonValues(filePaths: string[], path: string): Promise<CallToolResult> { for (const filePath of filePaths) { this.validateAbsolutePath(filePath); } const results: Record<string, string> = {}; for (const filePath of filePaths) { try { const jsonData = await this.readJsonFile(filePath); this.deleteValueAtPath(jsonData, path); await this.writeJsonFile(filePath, jsonData); results[filePath] = 'Successfully deleted'; } catch (error) { results[filePath] = `Error: ${error instanceof Error ? error.message : String(error)}`; } } return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } private async readJsonFile(filePath: string): Promise<any> { try { const content = await fs.readFile(filePath, 'utf-8'); return JSON.parse(content); } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { return {}; } throw new Error(`Failed to read JSON file: ${error instanceof Error ? error.message : String(error)}`); } } private async writeJsonFile(filePath: string, data: any): Promise<void> { const content = JSON.stringify(data, null, 2); await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, content, 'utf-8'); } private getValueAtPath(obj: any, path: string): any { const keys = path.split('.'); let current = obj; for (const key of keys) { if (current === null || current === undefined || typeof current !== 'object') { throw new Error(`Path ${path} not found: ${key} is not an object`); } if (!(key in current)) { throw new Error(`Path ${path} not found: ${key} does not exist`); } current = current[key]; } return current; } private setValueAtPath(obj: any, path: string, value: any): void { const keys = path.split('.'); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) { current[key] = {}; } current = current[key]; } current[keys[keys.length - 1]] = value; } private deleteValueAtPath(obj: any, path: string): void { const keys = path.split('.'); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (current === null || current === undefined || typeof current !== 'object') { throw new Error(`Path ${path} not found: ${key} is not an object`); } if (!(key in current)) { throw new Error(`Path ${path} not found: ${key} does not exist`); } current = current[key]; } const lastKey = keys[keys.length - 1]; if (current === null || current === undefined || typeof current !== 'object') { throw new Error(`Path ${path} not found: cannot delete from non-object`); } if (!(lastKey in current)) { throw new Error(`Path ${path} not found: ${lastKey} does not exist`); } delete current[lastKey]; } private deepMergeDuplicates(obj: any): any { if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) { return obj; } const result: any = {}; const seenKeys = new Set<string>(); for (const [key, value] of Object.entries(obj)) { if (seenKeys.has(key)) { // Merge with existing value const existingValue = result[key]; if (typeof existingValue === 'object' && typeof value === 'object' && existingValue !== null && value !== null && !Array.isArray(existingValue) && !Array.isArray(value)) { result[key] = this.deepMerge(existingValue, value); } else { // Last value wins for primitives or incompatible types result[key] = this.deepMergeDuplicates(value); } } else { seenKeys.add(key); result[key] = this.deepMergeDuplicates(value); } } return result; } private deepMerge(target: any, source: any): any { if (source === null || typeof source !== 'object' || Array.isArray(source)) { return source; } if (target === null || typeof target !== 'object' || Array.isArray(target)) { return source; } const result = { ...target }; for (const [key, value] of Object.entries(source)) { if (key in result && typeof result[key] === 'object' && typeof value === 'object' && result[key] !== null && value !== null && !Array.isArray(result[key]) && !Array.isArray(value)) { result[key] = this.deepMerge(result[key], value); } else { result[key] = value; } } return result; } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('JSON Editor MCP server running on stdio'); } } // Start the server const server = new JsonEditorMCPServer(); server.run().catch(console.error);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/peternagy1332/json-editor-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server