json-mcp-server
Version:
Fast, easy and reliable JSON MCP Server.
222 lines (221 loc) • 8.06 kB
JavaScript
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);
});