index.js•26.6 kB
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
// Import GIS conversion libraries
import wellknown from 'wellknown';
import csv2geojson from 'csv2geojson';
// Import TopoJSON libraries
import * as topojsonClient from 'topojson-client';
import * as topojsonServer from 'topojson-server';
// Import KML/KMZ conversion libraries
import { kml as kmlToGeoJSON } from '@tmcw/togeojson';
import tokml from 'tokml';
import { DOMParser } from 'xmldom';
// Import https for making requests (Node.js built-in)
import * as https from 'https';
class GisFormatServer {
constructor() {
console.error('[Setup] Initializing GIS Format Conversion MCP server...');
this.server = new Server({
name: 'gis-format-conversion-server',
version: '0.1.0',
}, {
capabilities: {
tools: {},
},
});
this.setupToolHandlers();
this.server.onerror = (error) => console.error('[Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
// Define a consistent return type for all tool methods
formatToolResponse(text) {
return {
content: [
{
type: 'text',
text
},
],
};
}
// Helper function to calculate centroid of polygon
getCentroid(points) {
const n = points.length;
let sumX = 0;
let sumY = 0;
for (let i = 0; i < n; i++) {
sumX += points[i][0];
sumY += points[i][1];
}
return [sumX / n, sumY / n];
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'wkt_to_geojson',
description: 'Convert Well-Known Text (WKT) to GeoJSON format',
inputSchema: {
type: 'object',
properties: {
wkt: {
type: 'string',
description: 'Well-Known Text (WKT) string to convert',
},
},
required: ['wkt'],
},
},
{
name: 'geojson_to_wkt',
description: 'Convert GeoJSON to Well-Known Text (WKT) format',
inputSchema: {
type: 'object',
properties: {
geojson: {
type: 'object',
description: 'GeoJSON object to convert',
},
},
required: ['geojson'],
},
},
{
name: 'csv_to_geojson',
description: 'Convert CSV with geographic data to GeoJSON',
inputSchema: {
type: 'object',
properties: {
csv: {
type: 'string',
description: 'CSV string to convert',
},
latfield: {
type: 'string',
description: 'Field name for latitude',
},
lonfield: {
type: 'string',
description: 'Field name for longitude',
},
delimiter: {
type: 'string',
description: 'CSV delimiter (default is comma)',
default: ',',
},
},
required: ['csv', 'latfield', 'lonfield'],
},
},
{
name: 'geojson_to_csv',
description: 'Convert GeoJSON to CSV format',
inputSchema: {
type: 'object',
properties: {
geojson: {
type: 'object',
description: 'GeoJSON object to convert',
},
includeAllProperties: {
type: 'boolean',
description: 'Include all feature properties in the CSV',
default: true,
},
},
required: ['geojson'],
},
},
{
name: 'geojson_to_topojson',
description: 'Convert GeoJSON to TopoJSON format (more compact with shared boundaries)',
inputSchema: {
type: 'object',
properties: {
geojson: {
type: 'object',
description: 'GeoJSON object to convert',
},
objectName: {
type: 'string',
description: 'Name of the TopoJSON object to create',
default: 'data',
},
quantization: {
type: 'number',
description: 'Quantization parameter for simplification (0 to disable)',
default: 1e4,
},
},
required: ['geojson'],
},
},
{
name: 'topojson_to_geojson',
description: 'Convert TopoJSON to GeoJSON format',
inputSchema: {
type: 'object',
properties: {
topojson: {
type: 'object',
description: 'TopoJSON object to convert',
},
objectName: {
type: 'string',
description: 'Name of the TopoJSON object to convert (if not provided, first object is used)',
},
},
required: ['topojson'],
},
},
{
name: 'kml_to_geojson',
description: 'Convert KML to GeoJSON format',
inputSchema: {
type: 'object',
properties: {
kml: {
type: 'string',
description: 'KML content to convert',
},
},
required: ['kml'],
},
},
{
name: 'geojson_to_kml',
description: 'Convert GeoJSON to KML format',
inputSchema: {
type: 'object',
properties: {
geojson: {
type: 'object',
description: 'GeoJSON object to convert',
},
documentName: {
type: 'string',
description: 'Name for the KML document',
default: 'GeoJSON Conversion',
},
documentDescription: {
type: 'string',
description: 'Description for the KML document',
default: 'Converted from GeoJSON by GIS Format Conversion MCP',
},
nameProperty: {
type: 'string',
description: 'Property name in GeoJSON to use as KML name',
default: 'name',
},
descriptionProperty: {
type: 'string',
description: 'Property name in GeoJSON to use as KML description',
default: 'description',
}
},
required: ['geojson'],
},
},
{
name: 'coordinates_to_location',
description: 'Convert latitude/longitude coordinates to location name using reverse geocoding',
inputSchema: {
type: 'object',
properties: {
latitude: {
type: 'number',
description: 'Latitude coordinate',
},
longitude: {
type: 'number',
description: 'Longitude coordinate',
}
},
required: ['latitude', 'longitude'],
},
}
],
}));
// Using the 'as any' type assertion to bypass the TypeScript error
this.server.setRequestHandler(CallToolRequestSchema, (async (request) => {
try {
switch (request.params.name) {
case 'wkt_to_geojson':
return await this.wktToGeoJSON(request.params.arguments);
case 'geojson_to_wkt':
return await this.geoJSONToWKT(request.params.arguments);
case 'csv_to_geojson':
return await this.csvToGeoJSON(request.params.arguments);
case 'geojson_to_csv':
return await this.geojsonToCSV(request.params.arguments);
case 'geojson_to_topojson':
return await this.geojsonToTopoJSON(request.params.arguments);
case 'topojson_to_geojson':
return await this.topojsonToGeoJSON(request.params.arguments);
case 'kml_to_geojson':
return await this.kmlToGeoJSON(request.params.arguments);
case 'geojson_to_kml':
return await this.geojsonToKML(request.params.arguments);
case 'coordinates_to_location':
return await this.coordinatesToLocation(request.params.arguments);
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
}
catch (error) {
if (error instanceof Error) {
console.error('[Error] Failed to process request:', error);
throw new McpError(ErrorCode.InternalError, `Failed to process request: ${error.message}`);
}
throw error;
}
}));
}
async wktToGeoJSON(args) {
const { wkt } = args;
if (!wkt) {
throw new McpError(ErrorCode.InvalidParams, 'Missing required parameter: wkt');
}
try {
console.error(`[Converting] WKT to GeoJSON: "${wkt.substring(0, 50)}${wkt.length > 50 ? '...' : ''}"`);
const geojson = wellknown.parse(wkt);
if (!geojson) {
throw new Error('Failed to parse WKT string');
}
return this.formatToolResponse(JSON.stringify(geojson, null, 2));
}
catch (error) {
console.error('[Error] WKT to GeoJSON conversion failed:', error);
throw new McpError(ErrorCode.InternalError, `WKT to GeoJSON conversion failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async geoJSONToWKT(args) {
const { geojson } = args;
if (!geojson) {
throw new McpError(ErrorCode.InvalidParams, 'Missing required parameter: geojson');
}
try {
console.error(`[Converting] GeoJSON to WKT: ${JSON.stringify(geojson).substring(0, 50)}...`);
const wkt = wellknown.stringify(geojson);
if (!wkt) {
throw new Error('Failed to convert GeoJSON to WKT');
}
return this.formatToolResponse(wkt);
}
catch (error) {
console.error('[Error] GeoJSON to WKT conversion failed:', error);
throw new McpError(ErrorCode.InternalError, `GeoJSON to WKT conversion failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async csvToGeoJSON(args) {
const { csv, latfield, lonfield, delimiter = ',' } = args;
if (!csv || !latfield || !lonfield) {
throw new McpError(ErrorCode.InvalidParams, 'Missing required parameters: csv, latfield, lonfield');
}
return new Promise((resolve, reject) => {
try {
console.error(`[Converting] CSV to GeoJSON using lat field ${latfield} and lon field ${lonfield}`);
csv2geojson.csv2geojson(csv, {
latfield,
lonfield,
delimiter
}, (err, data) => {
if (err) {
console.error('[Error] CSV to GeoJSON conversion failed:', err);
reject(new McpError(ErrorCode.InternalError, `CSV to GeoJSON conversion failed: ${err.message}`));
return;
}
resolve(this.formatToolResponse(JSON.stringify(data, null, 2)));
});
}
catch (error) {
console.error('[Error] CSV to GeoJSON conversion failed:', error);
reject(new McpError(ErrorCode.InternalError, `CSV to GeoJSON conversion failed: ${error instanceof Error ? error.message : String(error)}`));
}
});
}
async geojsonToCSV(args) {
const { geojson, includeAllProperties = true } = args;
if (!geojson || !geojson.features) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid GeoJSON: missing features array');
}
try {
console.error('[Converting] GeoJSON to CSV');
// Extract all unique property keys
const properties = new Set();
geojson.features.forEach((feature) => {
if (feature.properties) {
Object.keys(feature.properties).forEach(key => properties.add(key));
}
});
// Always include geometry columns
const headers = ['latitude', 'longitude', ...Array.from(properties)];
// Generate CSV rows
let csvRows = [headers.join(',')];
geojson.features.forEach((feature) => {
// Extract coordinates (handling different geometry types)
let lat = '';
let lon = '';
if (feature.geometry.type === 'Point') {
[lon, lat] = feature.geometry.coordinates;
}
else if (feature.geometry.type === 'Polygon') {
const centroid = this.getCentroid(feature.geometry.coordinates[0]);
lon = centroid[0];
lat = centroid[1];
}
else if (feature.geometry.type === 'LineString' || feature.geometry.type === 'MultiPoint') {
// Use first coordinate for these types
[lon, lat] = feature.geometry.coordinates[0];
}
else if (feature.geometry.type === 'MultiPolygon') {
// Use the centroid of the first polygon
const centroid = this.getCentroid(feature.geometry.coordinates[0][0]);
lon = centroid[0];
lat = centroid[1];
}
else if (feature.geometry.type === 'MultiLineString') {
// Use the first point of the first linestring
[lon, lat] = feature.geometry.coordinates[0][0];
}
else if (feature.geometry.type === 'GeometryCollection') {
// Use the first geometry
if (feature.geometry.geometries && feature.geometry.geometries.length > 0) {
const firstGeom = feature.geometry.geometries[0];
if (firstGeom.type === 'Point') {
[lon, lat] = firstGeom.coordinates;
}
else if (firstGeom.type === 'Polygon') {
const centroid = this.getCentroid(firstGeom.coordinates[0]);
lon = centroid[0];
lat = centroid[1];
}
}
}
// Convert coordinates to strings for CSV
const latStr = String(lat);
const lonStr = String(lon);
// Build row with all properties
const row = [latStr, lonStr];
properties.forEach(prop => {
const value = feature.properties && feature.properties[prop] !== undefined ?
feature.properties[prop] : '';
// Make sure strings with commas are properly quoted
row.push(typeof value === 'string' ? `"${value.replace(/"/g, '""')}"` : value);
});
csvRows.push(row.join(','));
});
return this.formatToolResponse(csvRows.join('\n'));
}
catch (error) {
console.error('[Error] GeoJSON to CSV conversion failed:', error);
throw new McpError(ErrorCode.InternalError, `GeoJSON to CSV conversion failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async geojsonToTopoJSON(args) {
const { geojson, objectName = 'data', quantization = 1e4 } = args;
if (!geojson) {
throw new McpError(ErrorCode.InvalidParams, 'Missing required parameter: geojson');
}
try {
console.error('[Converting] GeoJSON to TopoJSON');
// Create a topology object from the GeoJSON
const objectsMap = {};
objectsMap[objectName] = geojson;
// Generate the topology
const topology = topojsonServer.topology(objectsMap);
// Apply quantization if specified
let result = topology;
if (quantization > 0) {
// Use type assertion to work around TypeScript type incompatibility
result = topojsonClient.quantize(topology, quantization);
}
return this.formatToolResponse(JSON.stringify(result, null, 2));
}
catch (error) {
console.error('[Error] GeoJSON to TopoJSON conversion failed:', error);
throw new McpError(ErrorCode.InternalError, `GeoJSON to TopoJSON conversion failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async topojsonToGeoJSON(args) {
const { topojson, objectName } = args;
if (!topojson) {
throw new McpError(ErrorCode.InvalidParams, 'Missing required parameter: topojson');
}
try {
console.error('[Converting] TopoJSON to GeoJSON');
// Determine which object to convert
let objName = objectName;
// If no object name provided, use the first object in the topology
if (!objName && topojson.objects) {
objName = Object.keys(topojson.objects)[0];
}
if (!objName || !topojson.objects || !topojson.objects[objName]) {
throw new Error('No valid object found in TopoJSON');
}
// Convert TopoJSON to GeoJSON
const geojson = topojsonClient.feature(topojson, topojson.objects[objName]);
return this.formatToolResponse(JSON.stringify(geojson, null, 2));
}
catch (error) {
console.error('[Error] TopoJSON to GeoJSON conversion failed:', error);
throw new McpError(ErrorCode.InternalError, `TopoJSON to GeoJSON conversion failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async kmlToGeoJSON(args) {
const { kml } = args;
if (!kml) {
throw new McpError(ErrorCode.InvalidParams, 'Missing required parameter: kml');
}
try {
console.error('[Converting] KML to GeoJSON');
// Parse KML string to XML DOM
const parser = new DOMParser();
const kmlDoc = parser.parseFromString(kml, 'text/xml');
// Convert KML to GeoJSON
const geojson = kmlToGeoJSON(kmlDoc);
return this.formatToolResponse(JSON.stringify(geojson, null, 2));
}
catch (error) {
console.error('[Error] KML to GeoJSON conversion failed:', error);
throw new McpError(ErrorCode.InternalError, `KML to GeoJSON conversion failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async geojsonToKML(args) {
const { geojson, documentName = 'GeoJSON Conversion', documentDescription = 'Converted from GeoJSON by GIS Format Conversion MCP', nameProperty = 'name', descriptionProperty = 'description' } = args;
if (!geojson) {
throw new McpError(ErrorCode.InvalidParams, 'Missing required parameter: geojson');
}
try {
console.error('[Converting] GeoJSON to KML');
// Set up options for tokml
const options = {
documentName: documentName,
documentDescription: documentDescription,
name: nameProperty,
description: descriptionProperty
};
// Convert GeoJSON to KML using tokml
const kml = tokml(geojson, options);
return this.formatToolResponse(kml);
}
catch (error) {
console.error('[Error] GeoJSON to KML conversion failed:', error);
throw new McpError(ErrorCode.InternalError, `GeoJSON to KML conversion failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async coordinatesToLocation(args) {
const { latitude, longitude } = args;
if (latitude === undefined || longitude === undefined) {
throw new McpError(ErrorCode.InvalidParams, 'Missing required parameters: latitude, longitude');
}
try {
console.error(`[Converting] Coordinates (${latitude}, ${longitude}) to location name`);
// Using Nominatim OSM service (free, but has usage limitations)
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}`;
return new Promise((resolve, reject) => {
// Use the imported https module directly
const req = https.request(url, {
method: 'GET',
headers: {
'User-Agent': 'GisFormatMcpServer/1.0'
}
}, (res) => {
if (res.statusCode !== 200) {
reject(new Error(`Geocoding service returned ${res.statusCode}: ${res.statusMessage}`));
return;
}
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const parsedData = JSON.parse(data);
// Always return detailed format
const result = {
displayName: parsedData.display_name,
address: parsedData.address,
type: parsedData.type,
osmId: parsedData.osm_id,
osmType: parsedData.osm_type,
category: parsedData.category
};
resolve(this.formatToolResponse(JSON.stringify(result, null, 2)));
}
catch (error) {
reject(new Error(`Failed to parse geocoding response: ${error instanceof Error ? error.message : String(error)}`));
}
});
});
req.on('error', (error) => {
reject(new Error(`Geocoding request failed: ${error.message}`));
});
req.end();
});
}
catch (error) {
console.error('[Error] Coordinates to location conversion failed:', error);
throw new McpError(ErrorCode.InternalError, `Coordinates to location conversion failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('GIS Format Conversion MCP server running on stdio');
}
}
const server = new GisFormatServer();
server.run().catch(console.error);