UNPKG

json-mcp-server

Version:
222 lines (221 loc) 8.06 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema, } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { readFileSync } from 'fs'; import { zodToJsonSchema } from 'zod-to-json-schema'; import fs from 'fs'; import _path from 'path'; import os from 'os'; const ToolInputSchema = ToolSchema.shape.inputSchema; const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')); const SERVER_VERSION = packageJson.version; // Server setup const server = new Server({ name: 'json-mcp-server', version: SERVER_VERSION, }, { capabilities: { tools: {}, }, }); // Schema definitions const PathArgSchema = z.object({ path: z.string(), }); const SplitArgSchema = z.object({ path: z.string(), numObjects: z.number().int().positive(), }); // Function implementation const expandHome = (filepath) => { if (filepath.startsWith('~/') || filepath === '~') { return _path.join(os.homedir(), filepath.slice(1)); } return filepath; }; const validatePath = async (requestedPath) => { const expandedPath = expandHome(requestedPath); const absolute = _path.isAbsolute(expandedPath) ? _path.resolve(expandedPath) : _path.resolve(process.cwd(), expandedPath); try { const realPath = await fs.promises.realpath(absolute); return realPath; } catch (error) { const parentDir = _path.dirname(absolute); try { return absolute; } catch { throw new Error(`Parent directory does not exist: ${parentDir}`); } } }; const getParentDir = async (requestedPath) => { const expandedPath = expandHome(requestedPath); const absolute = _path.isAbsolute(expandedPath) ? _path.resolve(expandedPath) : _path.resolve(process.cwd(), expandedPath); try { const parentDir = _path.dirname(absolute); return parentDir; } catch (error) { throw new Error('Parent directory does not exist'); } }; const getAllFileNames = async (dir, extension) => { try { const filenames = await fs.promises.readdir(dir); return filenames.filter((fn) => fn.endsWith(extension)); } catch (error) { throw new Error(`No such folder ${dir}`); } }; const getAllJsonNames = async (dir) => getAllFileNames(dir, '.json'); const getMultipleJsonFilePath = async (fileDir) => { const filePaths = []; const fileNames = await getAllJsonNames(fileDir); for (let index = 0; index < fileNames.length; index++) { filePaths.push(`${fileDir}/${fileNames[index]}`); } return filePaths; }; const readFileToJson = async (fileName) => { try { const content = await fs.promises.readFile(fileName, 'utf-8'); return JSON.parse(content); } catch (error) { throw new Error(`Error reading or parsing file ${fileName}: ${error}`); } }; const parseMultipleJson = async (fileDir) => { const multipleJson = []; const fileNames = await getMultipleJsonFilePath(fileDir); for (let index = 0; index < fileNames.length; index++) { multipleJson.push(readFileToJson(fileNames[index])); } return multipleJson; }; const loadFlatJsons = async (path) => { const promises = await parseMultipleJson(path); if (promises.length === 0) throw new Error(`No files in ${path}`); const jsons = await Promise.all(promises); return jsons.flat(); }; // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'split', description: 'Split a JSON file into a specified number of objects', inputSchema: zodToJsonSchema(SplitArgSchema), }, { name: 'merge', description: 'Merge JSON files into a one JSON file', inputSchema: zodToJsonSchema(PathArgSchema), }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; switch (name) { case 'split': { const parsed = SplitArgSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for split: ${parsed.error}`); } const { path, numObjects } = parsed.data; const validPath = await validatePath(path); const content = await fs.promises.readFile(validPath, 'utf-8'); const folder = await getParentDir(path); const jsonData = JSON.parse(content); if (numObjects >= jsonData.length / 2) { throw new Error(`The number of objects (${numObjects}) per file cannot be half or more of the total (${jsonData.length}) JSON data length.`); } const totalParts = Math.ceil(jsonData.length / numObjects); await Promise.all(Array.from({ length: totalParts }, async (_, i) => { const partArray = jsonData .slice(i * numObjects, (i + 1) * numObjects) ?.flat(); const partFileName = _path.join(folder, `part${i + 1}.json`); try { await fs.promises.writeFile(partFileName, JSON.stringify(partArray, null, 2)); console.log(`Successfully wrote ${partFileName}`); } catch (err) { console.error(`Error writing file ${partFileName}:`, err); } })); return { content: [ { type: 'text', text: `Successfully splitted to ${parsed.data.path}`, }, ], }; } case 'merge': { const parsed = PathArgSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for merge: ${parsed.error}`); } const { path } = parsed.data; const validPath = await validatePath(path); const content = await loadFlatJsons(validPath); const fileName = _path.join(validPath, 'merged.json'); try { await fs.promises.writeFile(fileName, JSON.stringify(content, null, 2)); console.log(`Successfully wrote ${fileName}`); } catch (err) { console.error(`Error writing file ${fileName}:`, err); } return { content: [ { type: 'text', text: `Successfully merged to ${fileName}`, }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true, }; } }); // Start server export async function main() { try { const transport = new StdioServerTransport(); await server.connect(transport); console.error(`JSON MCP Server ${SERVER_VERSION} running on stdio`); } catch (error) { console.error('Error during startup:', error); process.exit(1); } } main().catch((error) => { console.error('Fatal error in main():', error); process.exit(1); });