mcp-xml
Version:
MCP server for XML file operations - read, analyze, search, find, relate, and write XML files
744 lines • 31.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const fast_xml_parser_1 = require("fast-xml-parser");
const glob_1 = require("glob");
const fs_extra_1 = __importDefault(require("fs-extra"));
const path_1 = __importDefault(require("path"));
class XMLMCPServer {
constructor() {
this.server = new index_js_1.Server({
name: 'mcp-xml',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
this.parser = new fast_xml_parser_1.XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_',
allowBooleanAttributes: true,
parseTagValue: false,
trimValues: true,
});
this.builder = new fast_xml_parser_1.XMLBuilder({
ignoreAttributes: false,
attributeNamePrefix: '@_',
format: true,
indentBy: ' ',
});
this.workingDirectory = process.cwd();
this.setupToolHandlers();
}
setupToolHandlers() {
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
tools: [
{
name: 'find_xml_files',
description: 'Find XML files in the current directory and subdirectories',
inputSchema: {
type: 'object',
properties: {
directory: {
type: 'string',
description: 'Directory to search (defaults to current directory)',
},
pattern: {
type: 'string',
description: 'Glob pattern for XML files (defaults to **/*.xml)',
},
},
},
},
{
name: 'read_xml_file',
description: 'Read and parse an XML file',
inputSchema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'Path to the XML file to read',
},
},
required: ['filePath'],
},
},
{
name: 'analyze_xml_file',
description: 'Analyze an XML file structure and content',
inputSchema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'Path to the XML file to analyze',
},
},
required: ['filePath'],
},
},
{
name: 'search_xml_content',
description: 'Search for specific content within XML files',
inputSchema: {
type: 'object',
properties: {
searchTerm: {
type: 'string',
description: 'Term to search for in XML content',
},
directory: {
type: 'string',
description: 'Directory to search (defaults to current directory)',
},
elementName: {
type: 'string',
description: 'Specific XML element name to search within',
},
},
required: ['searchTerm'],
},
},
{
name: 'write_xml_file',
description: 'Write data to an XML file',
inputSchema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'Path where to write the XML file',
},
data: {
type: 'object',
description: 'JavaScript object to convert to XML',
},
xmlDeclaration: {
type: 'boolean',
description: 'Include XML declaration (defaults to true)',
},
},
required: ['filePath', 'data'],
},
},
{
name: 'transform_xml',
description: 'Transform XML content using a transformation function',
inputSchema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'Path to the XML file to transform',
},
outputPath: {
type: 'string',
description: 'Path where to save the transformed XML',
},
operation: {
type: 'string',
enum: ['filter_elements', 'rename_elements', 'add_attributes', 'remove_attributes'],
description: 'Type of transformation to perform',
},
parameters: {
type: 'object',
description: 'Parameters for the transformation operation',
},
},
required: ['filePath', 'outputPath', 'operation'],
},
},
{
name: 'relate_xml_files',
description: 'Find relationships between XML files based on content or structure',
inputSchema: {
type: 'object',
properties: {
directory: {
type: 'string',
description: 'Directory to analyze (defaults to current directory)',
},
relationType: {
type: 'string',
enum: ['common_elements', 'shared_attributes', 'similar_structure', 'reference_links'],
description: 'Type of relationship to analyze',
},
},
required: ['relationType'],
},
},
{
name: 'validate_xml',
description: 'Validate XML file structure and well-formedness',
inputSchema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'Path to the XML file to validate',
},
},
required: ['filePath'],
},
},
],
}));
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'find_xml_files':
return await this.findXMLFiles(args);
case 'read_xml_file':
return await this.readXMLFile(args);
case 'analyze_xml_file':
return await this.analyzeXMLFile(args);
case 'search_xml_content':
return await this.searchXMLContent(args);
case 'write_xml_file':
return await this.writeXMLFile(args);
case 'transform_xml':
return await this.transformXML(args);
case 'relate_xml_files':
return await this.relateXMLFiles(args);
case 'validate_xml':
return await this.validateXML(args);
default:
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
}
catch (error) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
async findXMLFiles(args) {
const directory = args.directory || this.workingDirectory;
const pattern = args.pattern || '**/*.xml';
const searchPattern = path_1.default.join(directory, pattern);
const files = await (0, glob_1.glob)(searchPattern, {
ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**']
});
return {
content: [
{
type: 'text',
text: `Found ${files.length} XML files:\n${files.map(f => `- ${path_1.default.relative(this.workingDirectory, f)}`).join('\n')}`,
},
],
};
}
async readXMLFile(args) {
const filePath = path_1.default.resolve(this.workingDirectory, args.filePath);
if (!await fs_extra_1.default.pathExists(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const content = await fs_extra_1.default.readFile(filePath, 'utf8');
const parsed = this.parser.parse(content);
return {
content: [
{
type: 'text',
text: `XML Content from ${args.filePath}:\n\nRaw XML:\n${content}\n\nParsed Structure:\n${JSON.stringify(parsed, null, 2)}`,
},
],
};
}
async analyzeXMLFile(args) {
const filePath = path_1.default.resolve(this.workingDirectory, args.filePath);
if (!await fs_extra_1.default.pathExists(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const content = await fs_extra_1.default.readFile(filePath, 'utf8');
const parsed = this.parser.parse(content);
const analysis = this.analyzeXMLStructure(parsed, content);
return {
content: [
{
type: 'text',
text: `XML Analysis for ${args.filePath}:\n\n` +
`Element Count: ${analysis.elementCount}\n` +
`Attribute Count: ${analysis.attributeCount}\n` +
`Max Depth: ${analysis.depth}\n` +
`Has Namespaces: ${analysis.hasNamespaces}\n` +
`Elements Found: ${analysis.elements.join(', ')}\n` +
`Attributes Found: ${analysis.attributes.join(', ')}\n` +
`Namespaces: ${analysis.namespaces.join(', ')}\n`,
},
],
};
}
analyzeXMLStructure(obj, rawContent) {
const elements = new Set();
const attributes = new Set();
const namespaces = new Set();
let maxDepth = 0;
const traverse = (node, depth = 0) => {
maxDepth = Math.max(maxDepth, depth);
if (typeof node === 'object' && node !== null) {
Object.keys(node).forEach(key => {
if (key.startsWith('@_')) {
attributes.add(key.substring(2));
}
else if (key.includes(':')) {
namespaces.add(key.split(':')[0]);
elements.add(key);
}
else if (key !== '?xml') {
elements.add(key);
}
if (Array.isArray(node[key])) {
node[key].forEach((item) => traverse(item, depth + 1));
}
else if (typeof node[key] === 'object') {
traverse(node[key], depth + 1);
}
});
}
};
traverse(obj);
return {
elementCount: elements.size,
elements: Array.from(elements),
attributeCount: attributes.size,
attributes: Array.from(attributes),
depth: maxDepth,
hasNamespaces: namespaces.size > 0,
namespaces: Array.from(namespaces),
};
}
async searchXMLContent(args) {
const directory = args.directory || this.workingDirectory;
const searchTerm = args.searchTerm;
const elementName = args.elementName;
const xmlFiles = await (0, glob_1.glob)(path_1.default.join(directory, '**/*.xml'), {
ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**']
});
const results = [];
for (const file of xmlFiles) {
try {
const content = await fs_extra_1.default.readFile(file, 'utf8');
const matches = [];
if (elementName) {
const parsed = this.parser.parse(content);
const elementMatches = this.findInElement(parsed, elementName, searchTerm);
matches.push(...elementMatches);
}
else {
if (content.includes(searchTerm)) {
const lines = content.split('\n');
lines.forEach((line, index) => {
if (line.includes(searchTerm)) {
matches.push(`Line ${index + 1}: ${line.trim()}`);
}
});
}
}
if (matches.length > 0) {
results.push({
file: path_1.default.relative(this.workingDirectory, file),
matches,
});
}
}
catch (error) {
// Skip files that can't be read or parsed
}
}
return {
content: [
{
type: 'text',
text: `Search Results for "${searchTerm}"${elementName ? ` in element "${elementName}"` : ''}:\n\n` +
results.map(result => `${result.file}:\n${result.matches.map(match => ` - ${match}`).join('\n')}`).join('\n\n') || 'No matches found.',
},
],
};
}
findInElement(obj, elementName, searchTerm) {
const matches = [];
const search = (node, path = '') => {
if (typeof node === 'object' && node !== null) {
Object.keys(node).forEach(key => {
const currentPath = path ? `${path}.${key}` : key;
if (key === elementName || key.endsWith(`:${elementName}`)) {
const value = typeof node[key] === 'string' ? node[key] : JSON.stringify(node[key]);
if (value.includes(searchTerm)) {
matches.push(`Found in ${currentPath}: ${value}`);
}
}
if (Array.isArray(node[key])) {
node[key].forEach((item, index) => {
search(item, `${currentPath}[${index}]`);
});
}
else if (typeof node[key] === 'object') {
search(node[key], currentPath);
}
});
}
};
search(obj);
return matches;
}
async writeXMLFile(args) {
const filePath = path_1.default.resolve(this.workingDirectory, args.filePath);
const data = args.data;
const includeDeclaration = args.xmlDeclaration !== false;
await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
let xmlContent = this.builder.build(data);
if (includeDeclaration && !xmlContent.startsWith('<?xml')) {
xmlContent = '<?xml version="1.0" encoding="UTF-8"?>\n' + xmlContent;
}
await fs_extra_1.default.writeFile(filePath, xmlContent, 'utf8');
return {
content: [
{
type: 'text',
text: `Successfully wrote XML file to ${args.filePath}\n\nContent:\n${xmlContent}`,
},
],
};
}
async transformXML(args) {
const filePath = path_1.default.resolve(this.workingDirectory, args.filePath);
const outputPath = path_1.default.resolve(this.workingDirectory, args.outputPath);
const operation = args.operation;
const parameters = args.parameters || {};
if (!await fs_extra_1.default.pathExists(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const content = await fs_extra_1.default.readFile(filePath, 'utf8');
const parsed = this.parser.parse(content);
let transformed;
switch (operation) {
case 'filter_elements':
transformed = this.filterElements(parsed, parameters.elements || []);
break;
case 'rename_elements':
transformed = this.renameElements(parsed, parameters.mapping || {});
break;
case 'add_attributes':
transformed = this.addAttributes(parsed, parameters.element, parameters.attributes || {});
break;
case 'remove_attributes':
transformed = this.removeAttributes(parsed, parameters.element, parameters.attributes || []);
break;
default:
throw new Error(`Unknown transformation operation: ${operation}`);
}
await fs_extra_1.default.ensureDir(path_1.default.dirname(outputPath));
const xmlContent = '<?xml version="1.0" encoding="UTF-8"?>\n' + this.builder.build(transformed);
await fs_extra_1.default.writeFile(outputPath, xmlContent, 'utf8');
return {
content: [
{
type: 'text',
text: `Successfully transformed XML from ${args.filePath} to ${args.outputPath}\n\nTransformation: ${operation}\nParameters: ${JSON.stringify(parameters, null, 2)}`,
},
],
};
}
filterElements(obj, elementsToKeep) {
if (typeof obj !== 'object' || obj === null)
return obj;
const filtered = {};
Object.keys(obj).forEach(key => {
if (elementsToKeep.includes(key) || key.startsWith('@_') || key === '?xml') {
if (Array.isArray(obj[key])) {
filtered[key] = obj[key].map((item) => this.filterElements(item, elementsToKeep));
}
else if (typeof obj[key] === 'object') {
filtered[key] = this.filterElements(obj[key], elementsToKeep);
}
else {
filtered[key] = obj[key];
}
}
});
return filtered;
}
renameElements(obj, mapping) {
if (typeof obj !== 'object' || obj === null)
return obj;
const renamed = {};
Object.keys(obj).forEach(key => {
const newKey = mapping[key] || key;
if (Array.isArray(obj[key])) {
renamed[newKey] = obj[key].map((item) => this.renameElements(item, mapping));
}
else if (typeof obj[key] === 'object') {
renamed[newKey] = this.renameElements(obj[key], mapping);
}
else {
renamed[newKey] = obj[key];
}
});
return renamed;
}
addAttributes(obj, elementName, attributes) {
if (typeof obj !== 'object' || obj === null)
return obj;
const modified = { ...obj };
Object.keys(modified).forEach(key => {
if (key === elementName) {
Object.keys(attributes).forEach(attr => {
modified[`@_${attr}`] = attributes[attr];
});
}
if (Array.isArray(modified[key])) {
modified[key] = modified[key].map((item) => this.addAttributes(item, elementName, attributes));
}
else if (typeof modified[key] === 'object') {
modified[key] = this.addAttributes(modified[key], elementName, attributes);
}
});
return modified;
}
removeAttributes(obj, elementName, attributes) {
if (typeof obj !== 'object' || obj === null)
return obj;
const modified = { ...obj };
Object.keys(modified).forEach(key => {
if (key === elementName) {
attributes.forEach(attr => {
delete modified[`@_${attr}`];
});
}
if (Array.isArray(modified[key])) {
modified[key] = modified[key].map((item) => this.removeAttributes(item, elementName, attributes));
}
else if (typeof modified[key] === 'object') {
modified[key] = this.removeAttributes(modified[key], elementName, attributes);
}
});
return modified;
}
async relateXMLFiles(args) {
const directory = args.directory || this.workingDirectory;
const relationType = args.relationType;
const xmlFiles = await (0, glob_1.glob)(path_1.default.join(directory, '**/*.xml'), {
ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**']
});
const relationships = [];
for (let i = 0; i < xmlFiles.length; i++) {
for (let j = i + 1; j < xmlFiles.length; j++) {
try {
const content1 = await fs_extra_1.default.readFile(xmlFiles[i], 'utf8');
const content2 = await fs_extra_1.default.readFile(xmlFiles[j], 'utf8');
const parsed1 = this.parser.parse(content1);
const parsed2 = this.parser.parse(content2);
const relationship = this.findRelationship(parsed1, parsed2, relationType);
if (relationship) {
relationships.push({
files: [
path_1.default.relative(this.workingDirectory, xmlFiles[i]),
path_1.default.relative(this.workingDirectory, xmlFiles[j])
],
relationship: relationType,
details: relationship,
});
}
}
catch (error) {
// Skip files that can't be processed
}
}
}
return {
content: [
{
type: 'text',
text: `XML File Relationships (${relationType}):\n\n` +
relationships.map(rel => `${rel.files[0]} ↔ ${rel.files[1]}\n ${rel.details}`).join('\n\n') || 'No relationships found.',
},
],
};
}
findRelationship(obj1, obj2, type) {
switch (type) {
case 'common_elements':
const elements1 = this.getElementNames(obj1);
const elements2 = this.getElementNames(obj2);
const common = elements1.filter(e => elements2.includes(e));
return common.length > 0 ? `Common elements: ${common.join(', ')}` : null;
case 'shared_attributes':
const attrs1 = this.getAttributeNames(obj1);
const attrs2 = this.getAttributeNames(obj2);
const sharedAttrs = attrs1.filter(a => attrs2.includes(a));
return sharedAttrs.length > 0 ? `Shared attributes: ${sharedAttrs.join(', ')}` : null;
case 'similar_structure':
const structure1 = this.getStructureSignature(obj1);
const structure2 = this.getStructureSignature(obj2);
const similarity = this.calculateSimilarity(structure1, structure2);
return similarity > 0.7 ? `Structural similarity: ${Math.round(similarity * 100)}%` : null;
case 'reference_links':
const refs = this.findCrossReferences(obj1, obj2);
return refs.length > 0 ? `Cross-references found: ${refs.join(', ')}` : null;
default:
return null;
}
}
getElementNames(obj) {
const names = new Set();
const traverse = (node) => {
if (typeof node === 'object' && node !== null) {
Object.keys(node).forEach(key => {
if (!key.startsWith('@_') && key !== '?xml') {
names.add(key);
}
if (Array.isArray(node[key])) {
node[key].forEach(traverse);
}
else if (typeof node[key] === 'object') {
traverse(node[key]);
}
});
}
};
traverse(obj);
return Array.from(names);
}
getAttributeNames(obj) {
const names = new Set();
const traverse = (node) => {
if (typeof node === 'object' && node !== null) {
Object.keys(node).forEach(key => {
if (key.startsWith('@_')) {
names.add(key.substring(2));
}
if (Array.isArray(node[key])) {
node[key].forEach(traverse);
}
else if (typeof node[key] === 'object') {
traverse(node[key]);
}
});
}
};
traverse(obj);
return Array.from(names);
}
getStructureSignature(obj) {
const signature = [];
const traverse = (node, path = '') => {
if (typeof node === 'object' && node !== null) {
Object.keys(node).forEach(key => {
if (!key.startsWith('@_') && key !== '?xml') {
const currentPath = path ? `${path}.${key}` : key;
signature.push(currentPath);
if (Array.isArray(node[key])) {
node[key].forEach((item, index) => {
traverse(item, `${currentPath}[${index}]`);
});
}
else if (typeof node[key] === 'object') {
traverse(node[key], currentPath);
}
}
});
}
};
traverse(obj);
return signature.sort().join('|');
}
calculateSimilarity(str1, str2) {
const set1 = new Set(str1.split('|'));
const set2 = new Set(str2.split('|'));
const intersection = new Set([...set1].filter(x => set2.has(x)));
const union = new Set([...set1, ...set2]);
return intersection.size / union.size;
}
findCrossReferences(obj1, obj2) {
const refs = [];
const values1 = this.getAllTextValues(obj1);
const values2 = this.getAllTextValues(obj2);
values1.forEach(val1 => {
values2.forEach(val2 => {
if (val1 === val2 && val1.length > 3) {
refs.push(val1);
}
});
});
return [...new Set(refs)];
}
getAllTextValues(obj) {
const values = [];
const traverse = (node) => {
if (typeof node === 'string') {
values.push(node);
}
else if (typeof node === 'object' && node !== null) {
Object.keys(node).forEach(key => {
if (Array.isArray(node[key])) {
node[key].forEach(traverse);
}
else {
traverse(node[key]);
}
});
}
};
traverse(obj);
return values;
}
async validateXML(args) {
const filePath = path_1.default.resolve(this.workingDirectory, args.filePath);
if (!await fs_extra_1.default.pathExists(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const content = await fs_extra_1.default.readFile(filePath, 'utf8');
const issues = [];
let isValid = true;
try {
const parsed = this.parser.parse(content);
// Basic validation checks
if (!content.trim().startsWith('<')) {
issues.push('XML does not start with opening tag');
isValid = false;
}
if (!content.trim().endsWith('>')) {
issues.push('XML does not end with closing tag');
isValid = false;
}
// Check for balanced tags (basic check)
const openTags = content.match(/<[^/][^>]*>/g) || [];
const closeTags = content.match(/<\/[^>]*>/g) || [];
const selfClosingTags = content.match(/<[^>]*\/>/g) || [];
if (openTags.length !== closeTags.length + selfClosingTags.length) {
issues.push('Mismatched opening and closing tags detected');
isValid = false;
}
}
catch (error) {
issues.push(`Parser error: ${error instanceof Error ? error.message : String(error)}`);
isValid = false;
}
return {
content: [
{
type: 'text',
text: `XML Validation for ${args.filePath}:\n\n` +
`Status: ${isValid ? '✅ Valid' : '❌ Invalid'}\n` +
(issues.length > 0 ? `Issues found:\n${issues.map(issue => `- ${issue}`).join('\n')}` : 'No issues found.'),
},
],
};
}
async run() {
const transport = new stdio_js_1.StdioServerTransport();
await this.server.connect(transport);
console.error('MCP XML Server running on stdio');
}
}
const server = new XMLMCPServer();
server.run().catch(console.error);
//# sourceMappingURL=index.js.map