Skip to main content
Glama

JSON MCP Boilerplate

by ricleedo
index.ts14.6 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { existsSync, readFileSync } from "fs"; import { resolve } from "path"; import { z } from "zod"; // Create the JSON MCP server const server = new McpServer({ name: "json-tools", version: "1.0.0", }); // Utility functions function safeParseJSON(content: string, filePath: string): any { try { return JSON.parse(content); } catch (error: any) { throw new Error(`Invalid JSON in file ${filePath}: ${error.message}`); } } function readJSONFile(filePath: string): any { const absolutePath = resolve(filePath); if (!existsSync(absolutePath)) { throw new Error(`File not found: ${absolutePath}`); } const content = readFileSync(absolutePath, "utf8"); return safeParseJSON(content, absolutePath); } function getValueByPath(obj: any, path: string): any { return path.split(".").reduce((current, key) => { if (current === null || current === undefined) return undefined; if (Array.isArray(current) && !isNaN(Number(key))) { return current[Number(key)]; } return current[key]; }, obj); } function analyzeJSONStructure( obj: any, maxDepth: number = 3, currentDepth: number = 0, maxKeys?: number ): any { if (currentDepth > maxDepth) return "[...depth limit reached...]"; if (obj === null) return null; if (typeof obj !== "object") return typeof obj; if (Array.isArray(obj)) { if (obj.length === 0) return []; const sample = obj .slice(0, 3) .map((item) => analyzeJSONStructure(item, maxDepth, currentDepth + 1, maxKeys) ); return obj.length > 3 ? [...sample, `[...${obj.length - 3} more items]`] : sample; } const result: any = {}; const keys = Object.keys(obj); const keyLimit = maxKeys ?? keys.length; // Show all keys by default const sampleKeys = keys.slice(0, keyLimit); for (const key of sampleKeys) { result[key] = analyzeJSONStructure( obj[key], maxDepth, currentDepth + 1, maxKeys ); } if (keys.length > keyLimit) { result[`[...${keys.length - keyLimit} more keys]`] = "..."; } return result; } function filterObject(obj: any, condition: string): any { try { // For arrays: item, index are available // For objects: value, key, index are available const conditionFn = new Function( "item", "key", "index", "value", `return ${condition}` ); if (Array.isArray(obj)) { return obj.filter((item, index) => conditionFn(item, undefined, index, item) ); } else if (typeof obj === "object" && obj !== null) { const result: any = {}; Object.entries(obj).forEach(([key, value], index) => { if (conditionFn(value, key, index, value)) { result[key] = value; } }); return result; } return obj; } catch (error: any) { throw new Error(`Invalid filter condition: ${error.message}`); } } function truncateForOutput(obj: any, maxOutputLength: number = 25000): any { // Check if truncation is needed by estimating output size const estimatedSize = JSON.stringify(obj).length; if (estimatedSize <= maxOutputLength) { return obj; } function truncateValue(value: any): any { if (typeof value === "string" && value.length > 200) { const truncated = value.slice(0, 200); const remaining = value.length - 200; return `${truncated}...${remaining} more characters`; } if (Array.isArray(value)) { if (value.length <= 1) { return value.map((item) => truncateValue(item)); } const firstItem = truncateValue(value[0]); const remaining = value.length - 1; // Using a special marker that will be replaced later return [firstItem, `...${remaining} more items`]; } if (typeof value === "object" && value !== null) { const keys = Object.keys(value); if (keys.length <= 200) { const result: any = {}; for (const key of keys) { result[key] = truncateValue(value[key]); } return result; } const result: any = {}; const firstKeys = keys.slice(0, 200); for (const key of firstKeys) { result[key] = truncateValue(value[key]); } const remaining = keys.length - 200; // Using a special marker that will be replaced later result[`...${remaining} more properties`] = "..."; return result; } return value; } return truncateValue(obj); } // Tool 1: JSON Read - Read and analyze JSON files with flexible output server.tool( "json_read", "Read and analyze JSON. Always use this tool to explore JSON structure, understand data schema, or get high-level overviews of large JSON. Use this for initial data exploration or when you need to understand the shape and types of data before extracting specific values.", { file_path: z.string().describe("Path to the JSON file"), path: z.string().optional().describe("Dot notation to specific location"), max_depth: z.number().optional().describe("Limit traversal depth"), max_keys: z .number() .optional() .describe( "Maximum number of keys to show per object (default: show all keys)" ), sample_arrays: z .number() .optional() .describe("Show only first N array items"), keys_only: z.boolean().optional().describe("Return only the key structure"), include_types: z.boolean().optional().describe("Add type information"), include_stats: z .boolean() .optional() .describe("Add file size and structure statistics"), }, async ({ file_path, path, max_depth, max_keys, sample_arrays, keys_only, include_types, include_stats, }) => { try { const data = readJSONFile(file_path); const target = path ? getValueByPath(data, path) : data; let result: any; if (keys_only) { result = analyzeJSONStructure(target, max_depth || 3, 0, max_keys); } else if (sample_arrays !== undefined) { result = JSON.parse( JSON.stringify(target, (key, value) => { if (Array.isArray(value) && sample_arrays) { return value.slice(0, sample_arrays); } return value; }) ); } else { result = target; } // Build stats markdown section if requested let statsMarkdown = ""; if (include_stats) { const fileContent = readFileSync(resolve(file_path), "utf8"); const fileSize = (fileContent.length / 1024).toFixed(2); const nodeCount = JSON.stringify(data).length; statsMarkdown = "## File Statistics\n\n"; statsMarkdown += `- **File Size**: ${fileSize} KB\n`; statsMarkdown += `- **Total Nodes**: ${nodeCount.toLocaleString()}\n`; statsMarkdown += `- **Root Type**: ${ Array.isArray(data) ? "array" : typeof data }\n`; if (Array.isArray(target)) { statsMarkdown += `- **Array Length**: ${target.length}\n`; const elementTypes = [...new Set(target.map((item) => typeof item))]; statsMarkdown += `- **Element Types**: ${elementTypes.join(", ")}\n`; } else if (typeof target === "object" && target !== null) { const keys = Object.keys(target); statsMarkdown += `- **Key Count**: ${keys.length}\n`; if (keys.length > 0) { const topKeys = keys.slice(0, 10); statsMarkdown += `- **Top Keys**: ${topKeys.join(", ")}`; if (keys.length > 10) { statsMarkdown += ` (and ${keys.length - 10} more)`; } statsMarkdown += "\n"; } } statsMarkdown += "\n## Data\n\n"; } // Build type info markdown if requested let typeInfo = ""; if (include_types && !include_stats) { typeInfo = `**Type**: ${typeof target}`; if (Array.isArray(target)) { typeInfo = `**Type**: array (length: ${target.length})`; } typeInfo += "\n\n"; } const truncatedOutput = truncateForOutput(result); let outputText = JSON.stringify(truncatedOutput, null, 2); // Replace quoted truncation messages with unquoted text for markdown-like output outputText = outputText.replace( /"\.\.\.(\d+) more items"/g, "...$1 more items" ); outputText = outputText.replace( /"\.\.\.(\d+) more properties": "\.\.\.?"/g, "...$1 more properties" ); return { content: [ { type: "text", text: statsMarkdown + typeInfo + outputText }, ], }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message}` }], }; } } ); // Tool 2: JSON Extract - Extract specific data using various methods server.tool( "json_extract", "Extract specific data using paths, filters, patterns, or slices from JSON files. Always use this tool when you need to retrieve particular values, filter arrays/objects by conditions, search for patterns, or slice data. Ideal for targeted data extraction, data transformation, and focused analysis of specific JSON elements.", { file_path: z.string().describe("Path to the JSON file"), path: z.string().optional().describe("Dot notation path to target"), filter: z .string() .optional() .describe("JS condition to filter results (e.g., 'item.age > 18')"), pattern: z.string().optional().describe("Regex pattern to search for"), search_type: z .enum(["key", "value", "both"]) .optional() .describe("What to search when using pattern"), start: z.number().optional().describe("Array slice start index"), end: z.number().optional().describe("Array slice end index"), keys: z .array(z.string()) .optional() .describe("Specific object keys to extract"), default_value: z.any().optional().describe("Fallback if path not found"), }, async ({ file_path, path, filter, pattern, search_type, start, end, keys, default_value, }) => { try { const data = readJSONFile(file_path); let target = path ? getValueByPath(data, path) : data; // If path was specified but not found, return default value if (path && target === undefined) { const output = default_value !== undefined ? default_value : null; return { content: [ { type: "text", text: JSON.stringify( { result: output, message: `Path not found: ${path}, returning default value`, }, null, 2 ), }, ], }; } // Apply filter if specified if (filter) { target = filterObject(target, filter); } // Apply pattern search if specified if (pattern) { const results: any[] = []; const flags = "gi"; // Case-insensitive by default const regex = new RegExp(pattern, flags); const searchFor = search_type || "both"; function searchObject(obj: any, currentPath: string = ""): void { if (Array.isArray(obj)) { obj.forEach((item, index) => { searchObject(item, `${currentPath}[${index}]`); }); } else if (typeof obj === "object" && obj !== null) { Object.entries(obj).forEach(([key, value]) => { const newPath = currentPath ? `${currentPath}.${key}` : key; if ( (searchFor === "key" || searchFor === "both") && regex.test(key) ) { results.push({ type: "key", path: newPath, key, value }); } if (searchFor === "value" || searchFor === "both") { if (value === null && pattern === "null") { results.push({ type: "value", path: newPath, key, value }); } else if (typeof value === "string" && regex.test(value)) { results.push({ type: "value", path: newPath, key, value }); } else if ( typeof value === "number" && regex.test(value.toString()) ) { results.push({ type: "value", path: newPath, key, value }); } else if ( typeof value === "boolean" && regex.test(value.toString()) ) { results.push({ type: "value", path: newPath, key, value }); } } searchObject(value, newPath); }); } } searchObject(target); target = results; } // Apply slicing if specified if ((start !== undefined || end !== undefined) && Array.isArray(target)) { const sliceStart = start || 0; const sliceEnd = end || target.length; target = target.slice(sliceStart, sliceEnd); } // Extract specific keys if specified if ( keys && typeof target === "object" && target !== null && !Array.isArray(target) ) { const extracted: any = {}; keys.forEach((key) => { if (key in target) { extracted[key] = target[key]; } }); target = extracted; } const truncatedTarget = truncateForOutput(target); let outputText = JSON.stringify(truncatedTarget, null, 2); // Replace quoted truncation messages with unquoted text for markdown-like output outputText = outputText.replace( /"\.\.\.(\d+) more items"/g, "...$1 more items" ); outputText = outputText.replace( /"\.\.\.(\d+) more properties": "\.\.\.?"/g, "...$1 more properties" ); return { content: [{ type: "text", text: outputText }], }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message}` }], }; } } ); // Start the server async function main() { try { const transport = new StdioServerTransport(); await server.connect(transport); console.error("JSON Tools MCP Server running..."); } catch (error) { console.error("Error starting server:", error); process.exit(1); } } main().catch(console.error);

Implementation Reference

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/ricleedo/JSON-MCP-Boilerplate'

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