# Directory Structure ``` ├── .gitignore ├── dist │ └── index.js ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ ├── index.ts │ └── types │ └── modules.d.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules 2 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # GIS Data Conversion MCP 2 | 3 | [](https://smithery.ai/server/@ronantakizawa/gis-dataconvertersion-mcp) 4 | 5 | <a href="https://glama.ai/mcp/servers/@ronantakizawa/gis-dataconvertersion-mcp"> 6 | <img width="380" height="200" src="https://glama.ai/mcp/servers/@ronantakizawa/gis-dataconvertersion-mcp/badge" /> 7 | </a> 8 | 9 |  10 | 11 | The GIS Data Conversion MCP is an MCP (Model Context Protocol) server that gives LLMs access to geographic data conversion tools. 12 | 13 | This server uses various GIS libraries to allow LLMs to convert between different geographic data formats, coordinate systems, and spatial references. 14 | 15 | ## Features 16 | 17 | - **Reverse Geocoding** - Convert coordinates to location information 18 | - **WKT/GeoJSON Conversion** - Convert between Well-Known Text and GeoJSON formats 19 | - **CSV/GeoJSON Conversion** - Transform tabular data with coordinates to GeoJSON and vice versa 20 | - **TopoJSON/GeoJSON Conversion** - Convert between GeoJSON and TopoJSON (topology-preserving format) 21 | - **KML/GeoJSON Conversion** - Transform KML files to GeoJSON format 22 | 23 | ## Demo 24 | ### Reverse Geocoding 25 | https://github.com/user-attachments/assets/e21b10c3-bb67-4322-9742-efa8c7d8b332 26 | 27 | ### TopoJSON to GeoJSON 28 | https://github.com/user-attachments/assets/a5d56051-8aed-48bb-8de1-820df8d34fe3 29 | 30 | ## Installation 31 | To use this server with Claude Desktop, you need to configure it in the MCP settings: 32 | 33 | **For macOS:** 34 | Edit the file at `'~/Library/Application Support/Claude/claude_desktop_config.json'` 35 | 36 | ``` 37 | { 38 | "mcpServers": { 39 | "gis-dataconversion-mcp": { 40 | "command": "npx", 41 | "args": [ 42 | "-y", 43 | "a11y-mcp-server" 44 | ] 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | **For Windows:** 51 | Edit the file at `%APPDATA%\Claude\settings\claude_mcp_settings.json` 52 | 53 | **For Linux:** 54 | Edit the file at `~/.config/Claude/settings/claude_mcp_settings.json` 55 | Replace `/path/to/axe-mcp-server/build/index.js` with the actual path to your compiled server file. 56 | 57 | 58 | ## Available Tools 59 | 60 | ### wkt_to_geojson 61 | Converts Well-Known Text (WKT) to GeoJSON format. 62 | 63 | ### geojson_to_wkt 64 | Converts GeoJSON to Well-Known Text (WKT) format. 65 | 66 | ### csv_to_geojson 67 | Converts CSV with geographic data to GeoJSON. 68 | 69 | **Parameters:** 70 | 71 | - `csv` (required): CSV string to convert 72 | - `latfield` (required): Field name for latitude 73 | - `lonfield` (required): Field name for longitude 74 | - `delimiter` (optional): CSV delimiter (default is comma) 75 | 76 | ### geojson_to_csv 77 | Converts GeoJSON to CSV format. 78 | 79 | ### geojson_to_topojson 80 | Converts GeoJSON to TopoJSON format (more compact with shared boundaries). 81 | 82 | **Parameters:** 83 | 84 | - `geojson` (required): GeoJSON object to convert 85 | - `objectName` (optional): Name of the TopoJSON object to create (default: "data") 86 | - `quantization` (optional): Quantization parameter for simplification (default: 1e4, 0 to disable) 87 | 88 | ### topojson_to_geojson 89 | Converts TopoJSON to GeoJSON format. 90 | 91 | **Parameters:** 92 | 93 | - `geojson` (required): GeoJSON object to convert 94 | - `objectName` (optional): Name of the TopoJSON object to create (default: "data") 95 | 96 | ### kml_to_geojson 97 | Converts KML to GeoJSON format. 98 | 99 | ### geojson_to_kml 100 | Converts GeoJSON to KML format. 101 | 102 | ### coordinates_to_location 103 | Converts latitude/longitude coordinates to location name using reverse geocoding. 104 | 105 | 106 | ## Dependencies 107 | 108 | - @modelcontextprotocol/sdk 109 | - wellknown 110 | - csv2geojson 111 | - topojson-client 112 | - topojson-server 113 | - @tmcw/togeojson 114 | - xmldom 115 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Use Node 15 (specifically a stable minor version) 2 | FROM node:15.14.0 3 | 4 | # Create app directory 5 | WORKDIR /app 6 | 7 | # Copy only package files first for faster caching 8 | COPY package*.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install --ignore-scripts 12 | 13 | # Copy source files (including tsconfig.json etc) 14 | COPY . . 15 | 16 | # Build TypeScript 17 | RUN npm run build 18 | 19 | # Default command 20 | ENTRYPOINT ["node", "dist/index.js"] 21 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/deployments 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: [] 9 | properties: {} 10 | commandFunction: 11 | # A function that produces the CLI command to start the MCP on stdio. 12 | |- 13 | config => ({ command: 'node', args: ['dist/index.js'] }) 14 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "typeRoots": ["./node_modules/@types", "./src/types"], 14 | "noImplicitAny": false 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["node_modules"] 18 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "gis-dataconversion-mcp", 3 | "version": "1.0.3", 4 | "description": "An MCP server for converting GIS data in different formats", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "module": "dist/index.js", 8 | "bin": { 9 | "gis-mcp-server": "dist/index.js" 10 | }, 11 | "engines": { 12 | "node": ">=15.0.0" 13 | }, 14 | "access": "public", 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "build": "tsc && shx chmod +x dist/*.js", 20 | "prepare": "npm run build", 21 | "start": "node dist/index.js" 22 | }, 23 | "keywords": [ 24 | "gis", 25 | "geojson", 26 | "wkt", 27 | "csv", 28 | "topojson", 29 | "kml", 30 | "mcp", 31 | "claude", 32 | "ai" 33 | ], 34 | "author": "Ronan Takizawa", 35 | "license": "MIT", 36 | "dependencies": { 37 | "@modelcontextprotocol/sdk": "^0.6.0", 38 | "@tmcw/togeojson": "^5.6.2", 39 | "csv2geojson": "^5.1.1", 40 | "shpjs": "^3.6.3", 41 | "tokml": "^0.4.0", 42 | "topojson-client": "^3.1.0", 43 | "topojson-server": "^3.0.1", 44 | "wellknown": "^0.5.0", 45 | "xmldom": "^0.6.0" 46 | }, 47 | "devDependencies": { 48 | "@types/node": "^15.14.9", 49 | "@types/topojson-client": "^3.1.1", 50 | "@types/topojson-server": "^3.0.1", 51 | "shx": "^0.3.3", 52 | "typescript": "^4.5.5" 53 | }, 54 | "repository": { 55 | "type": "git", 56 | "url": "git+https://github.com/ronantakizawa/gis-dataconversion-mcp.git" 57 | }, 58 | "bugs": { 59 | "url": "https://github.com/ronantakizawa/gis-dataconvertersion-mcp/issues" 60 | }, 61 | "homepage": "https://github.com/ronantakizawa/gis-dataconversion-mcp#readme" 62 | } 63 | ``` -------------------------------------------------------------------------------- /src/types/modules.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Type definitions for missing modules 2 | 3 | declare module 'wellknown' { 4 | export function parse(wkt: string): GeoJSON.Geometry | GeoJSON.Feature | null; 5 | export function stringify(geojson: GeoJSON.Geometry | GeoJSON.Feature): string; 6 | 7 | const _default: { 8 | parse: typeof parse; 9 | stringify: typeof stringify; 10 | }; 11 | export default _default; 12 | } 13 | 14 | declare module 'csv2geojson' { 15 | export interface Csv2GeoJSONOptions { 16 | latfield: string; 17 | lonfield: string; 18 | delimiter?: string; 19 | } 20 | 21 | export interface GeoJSONResult { 22 | type: string; 23 | features: Array<{ 24 | type: string; 25 | properties: Record<string, any>; 26 | geometry: { 27 | type: string; 28 | coordinates: number[]; 29 | }; 30 | }>; 31 | } 32 | 33 | export function csv2geojson( 34 | csvString: string, 35 | options: Csv2GeoJSONOptions, 36 | callback: (err: Error | null, data: GeoJSONResult | null) => void 37 | ): void; 38 | 39 | const _default: { 40 | csv2geojson: typeof csv2geojson; 41 | }; 42 | export default _default; 43 | } 44 | 45 | declare module 'quadkeytools' { 46 | export interface BoundingBox { 47 | north: number; 48 | south: number; 49 | east: number; 50 | west: number; 51 | } 52 | 53 | export interface Location { 54 | lat: number; 55 | lng: number; 56 | } 57 | 58 | export function locationToQuadkey(location: Location, detail: number): string; 59 | export function bbox(quadkey: string): BoundingBox; 60 | export function children(quadkey: string): string[]; 61 | export function inside(location: Location, quadkey: string): boolean; 62 | export function sibling(quadkey: string, direction: 'left' | 'right' | 'up' | 'down'): string; 63 | 64 | const _default: { 65 | locationToQuadkey: typeof locationToQuadkey; 66 | bbox: typeof bbox; 67 | children: typeof children; 68 | inside: typeof inside; 69 | sibling: typeof sibling; 70 | }; 71 | export default _default; 72 | } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { 5 | CallToolRequestSchema, 6 | ErrorCode, 7 | ListToolsRequestSchema, 8 | McpError, 9 | } from '@modelcontextprotocol/sdk/types.js'; 10 | 11 | // Import GIS conversion libraries 12 | import wellknown from 'wellknown'; 13 | import csv2geojson from 'csv2geojson'; 14 | 15 | // Import TopoJSON libraries 16 | import * as topojsonClient from 'topojson-client'; 17 | import * as topojsonServer from 'topojson-server'; 18 | 19 | // Import KML/KMZ conversion libraries 20 | import { kml as kmlToGeoJSON } from '@tmcw/togeojson'; 21 | import tokml from 'tokml'; 22 | import { DOMParser } from 'xmldom'; 23 | 24 | // Import https for making requests (Node.js built-in) 25 | import * as https from 'https'; 26 | 27 | // Define the tool response type to match what the MCP SDK expects 28 | type ToolResponse = { 29 | content: Array<{ 30 | type: 'text'; 31 | text: string; 32 | }>; 33 | }; 34 | 35 | class GisFormatServer { 36 | private server: Server; 37 | 38 | constructor() { 39 | console.error('[Setup] Initializing GIS Format Conversion MCP server...'); 40 | 41 | this.server = new Server( 42 | { 43 | name: 'gis-format-conversion-server', 44 | version: '0.1.0', 45 | }, 46 | { 47 | capabilities: { 48 | tools: {}, 49 | }, 50 | } 51 | ); 52 | 53 | this.setupToolHandlers(); 54 | 55 | this.server.onerror = (error) => console.error('[Error]', error); 56 | process.on('SIGINT', async () => { 57 | await this.server.close(); 58 | process.exit(0); 59 | }); 60 | } 61 | 62 | // Define a consistent return type for all tool methods 63 | private formatToolResponse(text: string): ToolResponse { 64 | return { 65 | content: [ 66 | { 67 | type: 'text', 68 | text 69 | }, 70 | ], 71 | }; 72 | } 73 | 74 | // Helper function to calculate centroid of polygon 75 | private getCentroid(points: number[][]): number[] { 76 | const n = points.length; 77 | let sumX = 0; 78 | let sumY = 0; 79 | 80 | for (let i = 0; i < n; i++) { 81 | sumX += points[i][0]; 82 | sumY += points[i][1]; 83 | } 84 | 85 | return [sumX / n, sumY / n]; 86 | } 87 | 88 | private setupToolHandlers() { 89 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 90 | tools: [ 91 | { 92 | name: 'wkt_to_geojson', 93 | description: 'Convert Well-Known Text (WKT) to GeoJSON format', 94 | inputSchema: { 95 | type: 'object', 96 | properties: { 97 | wkt: { 98 | type: 'string', 99 | description: 'Well-Known Text (WKT) string to convert', 100 | }, 101 | }, 102 | required: ['wkt'], 103 | }, 104 | }, 105 | { 106 | name: 'geojson_to_wkt', 107 | description: 'Convert GeoJSON to Well-Known Text (WKT) format', 108 | inputSchema: { 109 | type: 'object', 110 | properties: { 111 | geojson: { 112 | type: 'object', 113 | description: 'GeoJSON object to convert', 114 | }, 115 | }, 116 | required: ['geojson'], 117 | }, 118 | }, 119 | { 120 | name: 'csv_to_geojson', 121 | description: 'Convert CSV with geographic data to GeoJSON', 122 | inputSchema: { 123 | type: 'object', 124 | properties: { 125 | csv: { 126 | type: 'string', 127 | description: 'CSV string to convert', 128 | }, 129 | latfield: { 130 | type: 'string', 131 | description: 'Field name for latitude', 132 | }, 133 | lonfield: { 134 | type: 'string', 135 | description: 'Field name for longitude', 136 | }, 137 | delimiter: { 138 | type: 'string', 139 | description: 'CSV delimiter (default is comma)', 140 | default: ',', 141 | }, 142 | }, 143 | required: ['csv', 'latfield', 'lonfield'], 144 | }, 145 | }, 146 | { 147 | name: 'geojson_to_csv', 148 | description: 'Convert GeoJSON to CSV format', 149 | inputSchema: { 150 | type: 'object', 151 | properties: { 152 | geojson: { 153 | type: 'object', 154 | description: 'GeoJSON object to convert', 155 | }, 156 | includeAllProperties: { 157 | type: 'boolean', 158 | description: 'Include all feature properties in the CSV', 159 | default: true, 160 | }, 161 | }, 162 | required: ['geojson'], 163 | }, 164 | }, 165 | { 166 | name: 'geojson_to_topojson', 167 | description: 'Convert GeoJSON to TopoJSON format (more compact with shared boundaries)', 168 | inputSchema: { 169 | type: 'object', 170 | properties: { 171 | geojson: { 172 | type: 'object', 173 | description: 'GeoJSON object to convert', 174 | }, 175 | objectName: { 176 | type: 'string', 177 | description: 'Name of the TopoJSON object to create', 178 | default: 'data', 179 | }, 180 | quantization: { 181 | type: 'number', 182 | description: 'Quantization parameter for simplification (0 to disable)', 183 | default: 1e4, 184 | }, 185 | }, 186 | required: ['geojson'], 187 | }, 188 | }, 189 | { 190 | name: 'topojson_to_geojson', 191 | description: 'Convert TopoJSON to GeoJSON format', 192 | inputSchema: { 193 | type: 'object', 194 | properties: { 195 | topojson: { 196 | type: 'object', 197 | description: 'TopoJSON object to convert', 198 | }, 199 | objectName: { 200 | type: 'string', 201 | description: 'Name of the TopoJSON object to convert (if not provided, first object is used)', 202 | }, 203 | }, 204 | required: ['topojson'], 205 | }, 206 | }, 207 | { 208 | name: 'kml_to_geojson', 209 | description: 'Convert KML to GeoJSON format', 210 | inputSchema: { 211 | type: 'object', 212 | properties: { 213 | kml: { 214 | type: 'string', 215 | description: 'KML content to convert', 216 | }, 217 | }, 218 | required: ['kml'], 219 | }, 220 | }, 221 | { 222 | name: 'geojson_to_kml', 223 | description: 'Convert GeoJSON to KML format', 224 | inputSchema: { 225 | type: 'object', 226 | properties: { 227 | geojson: { 228 | type: 'object', 229 | description: 'GeoJSON object to convert', 230 | }, 231 | documentName: { 232 | type: 'string', 233 | description: 'Name for the KML document', 234 | default: 'GeoJSON Conversion', 235 | }, 236 | documentDescription: { 237 | type: 'string', 238 | description: 'Description for the KML document', 239 | default: 'Converted from GeoJSON by GIS Format Conversion MCP', 240 | }, 241 | nameProperty: { 242 | type: 'string', 243 | description: 'Property name in GeoJSON to use as KML name', 244 | default: 'name', 245 | }, 246 | descriptionProperty: { 247 | type: 'string', 248 | description: 'Property name in GeoJSON to use as KML description', 249 | default: 'description', 250 | } 251 | }, 252 | required: ['geojson'], 253 | }, 254 | }, 255 | { 256 | name: 'coordinates_to_location', 257 | description: 'Convert latitude/longitude coordinates to location name using reverse geocoding', 258 | inputSchema: { 259 | type: 'object', 260 | properties: { 261 | latitude: { 262 | type: 'number', 263 | description: 'Latitude coordinate', 264 | }, 265 | longitude: { 266 | type: 'number', 267 | description: 'Longitude coordinate', 268 | } 269 | }, 270 | required: ['latitude', 'longitude'], 271 | }, 272 | } 273 | ], 274 | })); 275 | 276 | // Using the 'as any' type assertion to bypass the TypeScript error 277 | this.server.setRequestHandler(CallToolRequestSchema, (async (request: any) => { 278 | try { 279 | switch (request.params.name) { 280 | case 'wkt_to_geojson': 281 | return await this.wktToGeoJSON(request.params.arguments); 282 | case 'geojson_to_wkt': 283 | return await this.geoJSONToWKT(request.params.arguments); 284 | case 'csv_to_geojson': 285 | return await this.csvToGeoJSON(request.params.arguments); 286 | case 'geojson_to_csv': 287 | return await this.geojsonToCSV(request.params.arguments); 288 | case 'geojson_to_topojson': 289 | return await this.geojsonToTopoJSON(request.params.arguments); 290 | case 'topojson_to_geojson': 291 | return await this.topojsonToGeoJSON(request.params.arguments); 292 | case 'kml_to_geojson': 293 | return await this.kmlToGeoJSON(request.params.arguments); 294 | case 'geojson_to_kml': 295 | return await this.geojsonToKML(request.params.arguments); 296 | case 'coordinates_to_location': 297 | return await this.coordinatesToLocation(request.params.arguments); 298 | default: 299 | throw new McpError( 300 | ErrorCode.MethodNotFound, 301 | `Unknown tool: ${request.params.name}` 302 | ); 303 | } 304 | } catch (error: unknown) { 305 | if (error instanceof Error) { 306 | console.error('[Error] Failed to process request:', error); 307 | throw new McpError( 308 | ErrorCode.InternalError, 309 | `Failed to process request: ${error.message}` 310 | ); 311 | } 312 | throw error; 313 | } 314 | }) as any); 315 | } 316 | 317 | async wktToGeoJSON(args: any): Promise<ToolResponse> { 318 | const { wkt } = args; 319 | 320 | if (!wkt) { 321 | throw new McpError( 322 | ErrorCode.InvalidParams, 323 | 'Missing required parameter: wkt' 324 | ); 325 | } 326 | 327 | try { 328 | console.error(`[Converting] WKT to GeoJSON: "${wkt.substring(0, 50)}${wkt.length > 50 ? '...' : ''}"`); 329 | 330 | const geojson = wellknown.parse(wkt); 331 | 332 | if (!geojson) { 333 | throw new Error('Failed to parse WKT string'); 334 | } 335 | 336 | return this.formatToolResponse(JSON.stringify(geojson, null, 2)); 337 | } catch (error) { 338 | console.error('[Error] WKT to GeoJSON conversion failed:', error); 339 | throw new McpError( 340 | ErrorCode.InternalError, 341 | `WKT to GeoJSON conversion failed: ${error instanceof Error ? error.message : String(error)}` 342 | ); 343 | } 344 | } 345 | 346 | async geoJSONToWKT(args: any): Promise<ToolResponse> { 347 | const { geojson } = args; 348 | 349 | if (!geojson) { 350 | throw new McpError( 351 | ErrorCode.InvalidParams, 352 | 'Missing required parameter: geojson' 353 | ); 354 | } 355 | 356 | try { 357 | console.error(`[Converting] GeoJSON to WKT: ${JSON.stringify(geojson).substring(0, 50)}...`); 358 | 359 | const wkt = wellknown.stringify(geojson); 360 | 361 | if (!wkt) { 362 | throw new Error('Failed to convert GeoJSON to WKT'); 363 | } 364 | 365 | return this.formatToolResponse(wkt); 366 | } catch (error) { 367 | console.error('[Error] GeoJSON to WKT conversion failed:', error); 368 | throw new McpError( 369 | ErrorCode.InternalError, 370 | `GeoJSON to WKT conversion failed: ${error instanceof Error ? error.message : String(error)}` 371 | ); 372 | } 373 | } 374 | 375 | async csvToGeoJSON(args: any): Promise<ToolResponse> { 376 | const { csv, latfield, lonfield, delimiter = ',' } = args; 377 | 378 | if (!csv || !latfield || !lonfield) { 379 | throw new McpError( 380 | ErrorCode.InvalidParams, 381 | 'Missing required parameters: csv, latfield, lonfield' 382 | ); 383 | } 384 | 385 | return new Promise<ToolResponse>((resolve, reject) => { 386 | try { 387 | console.error(`[Converting] CSV to GeoJSON using lat field ${latfield} and lon field ${lonfield}`); 388 | 389 | csv2geojson.csv2geojson(csv, { 390 | latfield, 391 | lonfield, 392 | delimiter 393 | }, (err: Error | null, data: any) => { 394 | if (err) { 395 | console.error('[Error] CSV to GeoJSON conversion failed:', err); 396 | reject(new McpError( 397 | ErrorCode.InternalError, 398 | `CSV to GeoJSON conversion failed: ${err.message}` 399 | )); 400 | return; 401 | } 402 | 403 | resolve(this.formatToolResponse(JSON.stringify(data, null, 2))); 404 | }); 405 | } catch (error) { 406 | console.error('[Error] CSV to GeoJSON conversion failed:', error); 407 | reject(new McpError( 408 | ErrorCode.InternalError, 409 | `CSV to GeoJSON conversion failed: ${error instanceof Error ? error.message : String(error)}` 410 | )); 411 | } 412 | }); 413 | } 414 | 415 | async geojsonToCSV(args: any): Promise<ToolResponse> { 416 | const { geojson, includeAllProperties = true } = args; 417 | 418 | if (!geojson || !geojson.features) { 419 | throw new McpError( 420 | ErrorCode.InvalidParams, 421 | 'Invalid GeoJSON: missing features array' 422 | ); 423 | } 424 | 425 | try { 426 | console.error('[Converting] GeoJSON to CSV'); 427 | 428 | // Extract all unique property keys 429 | const properties = new Set<string>(); 430 | geojson.features.forEach((feature: any) => { 431 | if (feature.properties) { 432 | Object.keys(feature.properties).forEach(key => properties.add(key)); 433 | } 434 | }); 435 | 436 | // Always include geometry columns 437 | const headers = ['latitude', 'longitude', ...Array.from(properties)]; 438 | 439 | // Generate CSV rows 440 | let csvRows = [headers.join(',')]; 441 | 442 | geojson.features.forEach((feature: any) => { 443 | // Extract coordinates (handling different geometry types) 444 | let lat: number | string = ''; 445 | let lon: number | string = ''; 446 | 447 | if (feature.geometry.type === 'Point') { 448 | [lon, lat] = feature.geometry.coordinates; 449 | } else if (feature.geometry.type === 'Polygon') { 450 | const centroid = this.getCentroid(feature.geometry.coordinates[0]); 451 | lon = centroid[0]; 452 | lat = centroid[1]; 453 | } else if (feature.geometry.type === 'LineString' || feature.geometry.type === 'MultiPoint') { 454 | // Use first coordinate for these types 455 | [lon, lat] = feature.geometry.coordinates[0]; 456 | } else if (feature.geometry.type === 'MultiPolygon') { 457 | // Use the centroid of the first polygon 458 | const centroid = this.getCentroid(feature.geometry.coordinates[0][0]); 459 | lon = centroid[0]; 460 | lat = centroid[1]; 461 | } else if (feature.geometry.type === 'MultiLineString') { 462 | // Use the first point of the first linestring 463 | [lon, lat] = feature.geometry.coordinates[0][0]; 464 | } else if (feature.geometry.type === 'GeometryCollection') { 465 | // Use the first geometry 466 | if (feature.geometry.geometries && feature.geometry.geometries.length > 0) { 467 | const firstGeom = feature.geometry.geometries[0]; 468 | if (firstGeom.type === 'Point') { 469 | [lon, lat] = firstGeom.coordinates; 470 | } else if (firstGeom.type === 'Polygon') { 471 | const centroid = this.getCentroid(firstGeom.coordinates[0]); 472 | lon = centroid[0]; 473 | lat = centroid[1]; 474 | } 475 | } 476 | } 477 | 478 | // Convert coordinates to strings for CSV 479 | const latStr = String(lat); 480 | const lonStr = String(lon); 481 | 482 | // Build row with all properties 483 | const row = [latStr, lonStr]; 484 | properties.forEach(prop => { 485 | const value = feature.properties && feature.properties[prop] !== undefined ? 486 | feature.properties[prop] : ''; 487 | // Make sure strings with commas are properly quoted 488 | row.push(typeof value === 'string' ? `"${value.replace(/"/g, '""')}"` : value); 489 | }); 490 | 491 | csvRows.push(row.join(',')); 492 | }); 493 | 494 | return this.formatToolResponse(csvRows.join('\n')); 495 | } catch (error) { 496 | console.error('[Error] GeoJSON to CSV conversion failed:', error); 497 | throw new McpError( 498 | ErrorCode.InternalError, 499 | `GeoJSON to CSV conversion failed: ${error instanceof Error ? error.message : String(error)}` 500 | ); 501 | } 502 | } 503 | 504 | async geojsonToTopoJSON(args: any): Promise<ToolResponse> { 505 | const { geojson, objectName = 'data', quantization = 1e4 } = args; 506 | 507 | if (!geojson) { 508 | throw new McpError( 509 | ErrorCode.InvalidParams, 510 | 'Missing required parameter: geojson' 511 | ); 512 | } 513 | 514 | try { 515 | console.error('[Converting] GeoJSON to TopoJSON'); 516 | 517 | // Create a topology object from the GeoJSON 518 | const objectsMap: Record<string, any> = {}; 519 | objectsMap[objectName] = geojson; 520 | 521 | // Generate the topology 522 | const topology = topojsonServer.topology(objectsMap); 523 | 524 | // Apply quantization if specified 525 | let result = topology; 526 | if (quantization > 0) { 527 | // Use type assertion to work around TypeScript type incompatibility 528 | result = topojsonClient.quantize(topology as any, quantization); 529 | } 530 | 531 | return this.formatToolResponse(JSON.stringify(result, null, 2)); 532 | } catch (error) { 533 | console.error('[Error] GeoJSON to TopoJSON conversion failed:', error); 534 | throw new McpError( 535 | ErrorCode.InternalError, 536 | `GeoJSON to TopoJSON conversion failed: ${error instanceof Error ? error.message : String(error)}` 537 | ); 538 | } 539 | } 540 | 541 | async topojsonToGeoJSON(args: any): Promise<ToolResponse> { 542 | const { topojson, objectName } = args; 543 | 544 | if (!topojson) { 545 | throw new McpError( 546 | ErrorCode.InvalidParams, 547 | 'Missing required parameter: topojson' 548 | ); 549 | } 550 | 551 | try { 552 | console.error('[Converting] TopoJSON to GeoJSON'); 553 | 554 | // Determine which object to convert 555 | let objName = objectName; 556 | 557 | // If no object name provided, use the first object in the topology 558 | if (!objName && topojson.objects) { 559 | objName = Object.keys(topojson.objects)[0]; 560 | } 561 | 562 | if (!objName || !topojson.objects || !topojson.objects[objName]) { 563 | throw new Error('No valid object found in TopoJSON'); 564 | } 565 | 566 | // Convert TopoJSON to GeoJSON 567 | const geojson = topojsonClient.feature(topojson, topojson.objects[objName]); 568 | 569 | return this.formatToolResponse(JSON.stringify(geojson, null, 2)); 570 | } catch (error) { 571 | console.error('[Error] TopoJSON to GeoJSON conversion failed:', error); 572 | throw new McpError( 573 | ErrorCode.InternalError, 574 | `TopoJSON to GeoJSON conversion failed: ${error instanceof Error ? error.message : String(error)}` 575 | ); 576 | } 577 | } 578 | 579 | async kmlToGeoJSON(args: any): Promise<ToolResponse> { 580 | const { kml } = args; 581 | 582 | if (!kml) { 583 | throw new McpError( 584 | ErrorCode.InvalidParams, 585 | 'Missing required parameter: kml' 586 | ); 587 | } 588 | 589 | try { 590 | console.error('[Converting] KML to GeoJSON'); 591 | 592 | // Parse KML string to XML DOM 593 | const parser = new DOMParser(); 594 | const kmlDoc = parser.parseFromString(kml, 'text/xml'); 595 | 596 | // Convert KML to GeoJSON 597 | const geojson = kmlToGeoJSON(kmlDoc); 598 | 599 | return this.formatToolResponse(JSON.stringify(geojson, null, 2)); 600 | } catch (error) { 601 | console.error('[Error] KML to GeoJSON conversion failed:', error); 602 | throw new McpError( 603 | ErrorCode.InternalError, 604 | `KML to GeoJSON conversion failed: ${error instanceof Error ? error.message : String(error)}` 605 | ); 606 | } 607 | } 608 | 609 | async geojsonToKML(args: any): Promise<ToolResponse> { 610 | const { 611 | geojson, 612 | documentName = 'GeoJSON Conversion', 613 | documentDescription = 'Converted from GeoJSON by GIS Format Conversion MCP', 614 | nameProperty = 'name', 615 | descriptionProperty = 'description' 616 | } = args; 617 | 618 | if (!geojson) { 619 | throw new McpError( 620 | ErrorCode.InvalidParams, 621 | 'Missing required parameter: geojson' 622 | ); 623 | } 624 | 625 | try { 626 | console.error('[Converting] GeoJSON to KML'); 627 | 628 | // Set up options for tokml 629 | const options = { 630 | documentName: documentName, 631 | documentDescription: documentDescription, 632 | name: nameProperty, 633 | description: descriptionProperty 634 | }; 635 | 636 | // Convert GeoJSON to KML using tokml 637 | const kml = tokml(geojson, options); 638 | 639 | return this.formatToolResponse(kml); 640 | } catch (error) { 641 | console.error('[Error] GeoJSON to KML conversion failed:', error); 642 | throw new McpError( 643 | ErrorCode.InternalError, 644 | `GeoJSON to KML conversion failed: ${error instanceof Error ? error.message : String(error)}` 645 | ); 646 | } 647 | } 648 | 649 | async coordinatesToLocation(args: any): Promise<ToolResponse> { 650 | const { latitude, longitude } = args; 651 | 652 | if (latitude === undefined || longitude === undefined) { 653 | throw new McpError( 654 | ErrorCode.InvalidParams, 655 | 'Missing required parameters: latitude, longitude' 656 | ); 657 | } 658 | 659 | try { 660 | console.error(`[Converting] Coordinates (${latitude}, ${longitude}) to location name`); 661 | 662 | // Using Nominatim OSM service (free, but has usage limitations) 663 | const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}`; 664 | 665 | return new Promise<ToolResponse>((resolve, reject) => { 666 | // Use the imported https module directly 667 | const req = https.request(url, { 668 | method: 'GET', 669 | headers: { 670 | 'User-Agent': 'GisFormatMcpServer/1.0' 671 | } 672 | }, (res) => { 673 | if (res.statusCode !== 200) { 674 | reject(new Error(`Geocoding service returned ${res.statusCode}: ${res.statusMessage}`)); 675 | return; 676 | } 677 | 678 | let data = ''; 679 | res.on('data', (chunk) => { 680 | data += chunk; 681 | }); 682 | 683 | res.on('end', () => { 684 | try { 685 | const parsedData = JSON.parse(data); 686 | 687 | // Always return detailed format 688 | const result = { 689 | displayName: parsedData.display_name, 690 | address: parsedData.address, 691 | type: parsedData.type, 692 | osmId: parsedData.osm_id, 693 | osmType: parsedData.osm_type, 694 | category: parsedData.category 695 | }; 696 | 697 | resolve(this.formatToolResponse(JSON.stringify(result, null, 2))); 698 | } catch (error) { 699 | reject(new Error(`Failed to parse geocoding response: ${error instanceof Error ? error.message : String(error)}`)); 700 | } 701 | }); 702 | }); 703 | 704 | req.on('error', (error) => { 705 | reject(new Error(`Geocoding request failed: ${error.message}`)); 706 | }); 707 | 708 | req.end(); 709 | }); 710 | } catch (error) { 711 | console.error('[Error] Coordinates to location conversion failed:', error); 712 | throw new McpError( 713 | ErrorCode.InternalError, 714 | `Coordinates to location conversion failed: ${error instanceof Error ? error.message : String(error)}` 715 | ); 716 | } 717 | } 718 | 719 | async run() { 720 | const transport = new StdioServerTransport(); 721 | await this.server.connect(transport); 722 | console.error('GIS Format Conversion MCP server running on stdio'); 723 | } 724 | } 725 | 726 | const server = new GisFormatServer(); 727 | server.run().catch(console.error); ```