XPath MCP Server
by JayArrowz
Verified
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import xpath from "xpath";
import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
import { z } from 'zod';
import puppeteer from 'puppeteer';
const parser = new DOMParser();
const XPathArgumentsSchema = z.object({
xml: z.string().describe("The XML content to query"),
query: z.string().describe("The XPath query to execute"),
mimeType: z.string()
.describe("The MIME type (e.g. text/xml, application/xml, text/html, application/xhtml+xml)")
.default("text/html")
});
const XPathWithUrlArgumentsSchema = z.object({
url: z.string().url().describe("The URL to fetch XML/HTML content from"),
query: z.string().describe("The XPath query to execute"),
mimeType: z.string()
.describe("The MIME type (e.g. text/xml, application/xml, text/html, application/xhtml+xml)")
.default("text/html")
});
// Create server instance
const server = new Server(
{
name: "mcp_xpath",
version: "0.6.2"
},
{
capabilities: {
tools: {}
}
}
);
function resultToString(result: string | number | boolean | Node | Node[] | null): string {
if (result === null) {
return "null";
} else if (Array.isArray(result)) {
return result.map(resultToString).join("\n");
} else if (typeof result === 'object' && result.nodeType !== undefined) {
// Handle DOM nodes
if (result.nodeType === 1) { // Element node
const serializer = new XMLSerializer();
return serializer.serializeToString(result);
} else if (result.nodeType === 2) { // Attribute node
return `${result.nodeName}="${result.nodeValue}"`;
} else if (result.nodeType === 3) { // Text node
return result.nodeValue || "";
} else {
// Default fallback for other node types
try {
const serializer = new XMLSerializer();
return serializer.serializeToString(result);
} catch (e) {
return String(result);
}
}
} else {
return String(result);
}
}
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "xpath",
description: "Select query XML content using XPath",
inputSchema: {
type: "object",
properties: {
xml: {
type: "string",
description: "The XML content to query",
},
query: {
type: "string",
description: "The XPath query to execute",
},
mimeType: {
type: "string",
description: "The MIME type (e.g. text/xml, application/xml, text/html, application/xhtml+xml)",
default: "text/html"
}
},
required: ["xml", "query"],
},
},
{
name: "xpathwithurl",
description: "Fetch content from a URL and select query it using XPath",
inputSchema: {
type: "object",
properties: {
url: {
type: "string",
description: "The URL to fetch XML/HTML content from",
},
query: {
type: "string",
description: "The XPath query to execute",
},
mimeType: {
type: "string",
description: "The MIME type (e.g. text/xml, application/xml, text/html, application/xhtml+xml)",
default: "text/html"
}
},
required: ["url", "query"],
},
}
],
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === "xpath") {
const { xml, query, mimeType } = XPathArgumentsSchema.parse(args);
try {
// Parse XML
const firstOpeningTag = xml.indexOf("<");
const lastClosingTag = xml.lastIndexOf(">");
const sanitizedXml = xml.substring(firstOpeningTag, lastClosingTag + 1);
const parsedXml = parser.parseFromString(sanitizedXml, mimeType);
// Check for parsing errors
const errors = xpath.select('//parsererror', parsedXml);
if (Array.isArray(errors) && errors.length > 0) {
return {
content: [{ type: "text", text: "XML parsing error: " + resultToString(errors[0]) }]
};
}
const result = xpath.select(query, parsedXml);
// If result is an empty array, provide more information
if (Array.isArray(result) && result.length === 0) {
return {
content: [{ type: "text", text: "No nodes matched the query." }]
};
}
return {
content: [{ type: "text", text: resultToString(result) }]
};
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error processing XPath query: ${errorMessage}` }]
};
}
} else if (name === "xpathwithurl") {
const { url, query, mimeType } = XPathWithUrlArgumentsSchema.parse(args);
// Launch puppeteer browser
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
try {
// Navigate to the URL and wait until network is idle
await page.goto(url, { waitUntil: 'networkidle0' });
// Get the rendered HTML
const xml = await page.content();
// Parse XML
const parsedXml = parser.parseFromString(xml, mimeType);
// Check for parsing errors
const errors = xpath.select('//parsererror', parsedXml);
if (Array.isArray(errors) && errors.length > 0) {
return {
content: [{ type: "text", text: "XML parsing error: " + resultToString(errors[0]) }]
};
}
const result = xpath.select(query, parsedXml);
// If result is an empty array, provide more information
if (Array.isArray(result) && result.length === 0) {
return {
content: [{ type: "text", text: "No nodes matched the query." }]
};
}
return {
content: [{ type: "text", text: resultToString(result) }]
};
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error processing XPath query: ${errorMessage}` }]
};
} finally {
// Make sure to close the browser
await browser.close();
}
} else {
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Invalid arguments: ${error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(", ")}`
);
}
throw error;
}
});
// Start the server
async function main() {
try {
console.error("Starting XPATH MCP Server...");
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("XPATH MCP Server 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);
});