# 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);
```