Skip to main content
Glama
JsonEditorMCPServerTestable.ts8.98 kB
import { promises as fs } from 'fs'; import path from 'path'; // Testable version of JsonEditorMCPServer that exposes private methods // This version doesn't import the MCP SDK to avoid ESM issues in Jest export class JsonEditorMCPServerTestable { public validateAbsolutePath(filePath: string): void { if (!path.isAbsolute(filePath)) { throw new Error(`Path must be absolute: ${filePath}`); } } // Expose private methods for testing public deepMergeDuplicates(obj: any, visited = new WeakSet()): any { if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) { return obj; } // Check for special object types (Date, RegExp, etc.) if (obj instanceof Date || obj instanceof RegExp || obj instanceof Function) { return obj; } // Check for circular references if (visited.has(obj)) { return obj; // Return the object as-is to avoid infinite recursion } visited.add(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, visited); } else { // Last value wins for primitives or incompatible types result[key] = value; } } else { seenKeys.add(key); result[key] = this.deepMergeDuplicates(value, visited); } } return result; } public deepMerge(target: any, source: any, visited = new WeakSet()): any { if (source === null || typeof source !== 'object' || Array.isArray(source)) { return source; } if (target === null || typeof target !== 'object' || Array.isArray(target)) { return source; } // Check for circular references if (visited.has(target) || visited.has(source)) { return source; // Return source to avoid infinite recursion } visited.add(target); visited.add(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, visited); } else { result[key] = value; } } return result; } public 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; } public 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; } public 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]; } public async readJsonFile(filePath: string): Promise<any> { try { const content = await fs.readFile(filePath, 'utf-8'); return JSON.parse(content); } catch (error) { if ((error as any).code === 'ENOENT') { return {}; } throw new Error(`Failed to read JSON file: ${error instanceof Error ? error.message : String(error)}`); } } public async mergeDuplicateKeys(filePath: string): Promise<void> { this.validateAbsolutePath(filePath); const jsonData = await this.readJsonFile(filePath); const mergedData = this.deepMergeDuplicates(jsonData); await this.writeJsonFile(filePath, mergedData); } public 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'); } public async readMultipleJsonValues(filePaths: string[], path: string): Promise<Record<string, any>> { 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 results; } 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'); } } public async writeMultipleJsonValues(filePaths: string[], path: string, value: any): Promise<Record<string, string>> { for (const filePath of filePaths) { this.validateAbsolutePath(filePath); } const results: Record<string, string> = {}; 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; } } } for (const filePath of filePaths) { 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); results[filePath] = 'Successfully wrote'; } catch (error) { results[filePath] = `Error: ${error instanceof Error ? error.message : String(error)}`; } } return results; } public async deleteMultipleJsonValues(filePaths: string[], path: string): Promise<Record<string, string>> { 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 results; } }

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