From 24b8c5cfafbd9710da03eb89d219b4c4246f5950 Mon Sep 17 00:00:00 2001 From: DaxServer Date: Mon, 6 Oct 2025 18:15:47 +0200 Subject: [PATCH 1/3] feat: add mcpb --- .github/workflows/lint.yml | 4 +- .github/workflows/release-mcpb.yml | 46 +++++ .../{release.yml => release-npm.yml} | 8 +- .github/workflows/test.yml | 4 +- .gitignore | 3 + .mcpbignore | 11 ++ INSTALLATION.md | 134 ++++++++++++++ MCPB_README.md | 172 ++++++++++++++++++ bun.lock | 10 +- eslint.config.js | 14 -- eslint.config.ts | 30 +++ manifest.json | 57 ++++++ package.json | 5 +- prettier.config.ts | 11 ++ src/clients/__tests__/nominatimClient.test.ts | 6 +- src/clients/nominatimClient.ts | 7 +- src/index.ts | 4 +- src/tools/__tests__/geocode.test.ts | 35 ++-- src/tools/__tests__/prepareResponse.test.ts | 6 +- src/tools/__tests__/reverseGeocode.test.ts | 33 ++-- src/tools/geocode.ts | 7 +- src/tools/reverseGeocode.ts | 2 +- src/types/commonTypes.ts | 4 +- src/types/geocodeTypes.ts | 2 +- src/types/reverseGeocodeTypes.ts | 2 +- 25 files changed, 519 insertions(+), 98 deletions(-) create mode 100644 .github/workflows/release-mcpb.yml rename .github/workflows/{release.yml => release-npm.yml} (83%) create mode 100644 .mcpbignore create mode 100644 INSTALLATION.md create mode 100644 MCPB_README.md delete mode 100644 eslint.config.js create mode 100644 eslint.config.ts create mode 100644 manifest.json diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fc8f4d0..1b70b4a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,10 +9,8 @@ permissions: jobs: lint: runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Bun uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/release-mcpb.yml b/.github/workflows/release-mcpb.yml new file mode 100644 index 0000000..0f811a2 --- /dev/null +++ b/.github/workflows/release-mcpb.yml @@ -0,0 +1,46 @@ +name: Release MCPB Package + +on: + release: + types: + - published + +permissions: + contents: write + id-token: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.2.23 + + - name: Set package version + env: + VERSION: ${{ github.ref_name }} + run: | + echo $(jq --arg v "${{ env.VERSION }}" '(.version) = $v' package.json) > package.json + echo $(jq --arg v "${{ env.VERSION }}" '(.version) = $v' manifest.json) > manifest.json + + - name: Install Dependencies + run: bun install --frozen-lockfile + + - name: Build + run: bun run build + + - name: Pack MCPB + run: bunx @anthropic-ai/mcpb pack + + - name: Clean MCPB + run: bunx @anthropic-ai/mcpb clean mcp.mcpb + + - name: Upload MCPB to GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: mcp.mcpb + fail_on_unmatched_files: true diff --git a/.github/workflows/release.yml b/.github/workflows/release-npm.yml similarity index 83% rename from .github/workflows/release.yml rename to .github/workflows/release-npm.yml index a863271..c29b1b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release-npm.yml @@ -1,4 +1,4 @@ -name: Release +name: Release NPM Package on: release: @@ -11,13 +11,10 @@ permissions: jobs: release: - name: Release - runs-on: ubuntu-latest - steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Bun uses: oven-sh/setup-bun@v2 @@ -29,6 +26,7 @@ jobs: VERSION: ${{ github.ref_name }} run: | echo $(jq --arg v "${{ env.VERSION }}" '(.version) = $v' package.json) > package.json + echo $(jq --arg v "${{ env.VERSION }}" '(.version) = $v' manifest.json) > manifest.json - name: Install Dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7156507..7409219 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,10 +9,8 @@ permissions: jobs: test: runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Bun uses: oven-sh/setup-bun@v2 diff --git a/.gitignore b/.gitignore index d0a86d3..66f91e7 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Trae .trae + +# MCPB +*.mcpb diff --git a/.mcpbignore b/.mcpbignore new file mode 100644 index 0000000..744dd62 --- /dev/null +++ b/.mcpbignore @@ -0,0 +1,11 @@ +*.config.* +*.md +**/__tests__/* + +.git* +.trae + +src + +glama.json +tsconfig.json diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 0000000..f9bf96b --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,134 @@ +# Geocoding MCP Desktop Extension - Installation Guide + +## Overview +This guide will help you install and configure the Geocoding MCP Desktop Extension (DXT) for Claude Desktop. + +## Prerequisites +- Claude Desktop application installed +- macOS, Windows, or Linux operating system +- Internet connection for geocoding API calls + +## Installation Steps + +### Method 1: Manual Installation (Recommended) + +1. **Locate the DXT file** + - Find the `mcp.dxt` file in your project directory + - File size: ~44MB + +2. **Install via Claude Desktop** + - Open Claude Desktop + - Go to Settings → Extensions + - Click "Install Extension" + - Select the `mcp.dxt` file + - Follow the installation prompts + +3. **Verify Installation** + - Restart Claude Desktop + - Check that "Geocoding MCP" appears in your extensions list + - The extension should show as "Active" + +### Method 2: Command Line Installation (Advanced) + +```bash +# If you have the DXT CLI installed +dxt install mcp.dxt + +# Or using bunx +bunx @anthropic-ai/dxt install mcp.dxt +``` + +## Configuration + +No additional configuration is required. The extension uses the free Nominatim API service. + +## Usage + +Once installed, you can use the following tools in Claude Desktop: + +### Geocode Tool +Convert addresses to coordinates: +``` +Find the coordinates for "Times Square, New York" +``` + +### Reverse Geocode Tool +Convert coordinates to addresses: +``` +What address is at coordinates 40.7580, -73.9855? +``` + +## Available Parameters + +### Geocode Parameters +- `query` (required): Address or place name to search +- `format`: Output format (json, xml, geojson, etc.) +- `addressdetails`: Include detailed address components (0 or 1) +- `countrycodes`: Limit search to specific countries +- `limit`: Maximum number of results +- `extratags`: Include additional tags +- `namedetails`: Include name details +- `polygon_*`: Include geometry data + +### Reverse Geocode Parameters +- `lat` (required): Latitude (-90 to 90) +- `lon` (required): Longitude (-180 to 180) +- `zoom`: Detail level (0-18, higher = more detailed) +- `format`: Output format +- `addressdetails`: Include detailed address components +- `extratags`: Include additional tags +- `namedetails`: Include name details + +## Troubleshooting + +### Extension Not Loading +1. Check that the DXT file is not corrupted +2. Restart Claude Desktop +3. Verify system compatibility +4. Check Claude Desktop logs for errors + +### API Errors +1. Verify internet connection +2. Check if Nominatim service is accessible +3. Ensure query parameters are valid +4. Try simpler queries if complex ones fail + +### Performance Issues +1. Reduce the number of results requested +2. Avoid requesting polygon data unless needed +3. Use appropriate zoom levels for reverse geocoding + +## Data Source & License + +- **Data Source**: OpenStreetMap via Nominatim API +- **License**: Data © OpenStreetMap contributors, ODbL 1.0 +- **Usage Policy**: Please respect Nominatim's usage policy +- **Rate Limits**: Be mindful of API rate limits for heavy usage + +## Support + +For issues or questions: +1. Check the troubleshooting section above +2. Review the DXT_README.md for detailed documentation +3. Verify your query format and parameters +4. Check Claude Desktop extension logs + +## Uninstallation + +To remove the extension: +1. Open Claude Desktop Settings +2. Go to Extensions +3. Find "Geocoding MCP" in the list +4. Click "Uninstall" or "Remove" +5. Restart Claude Desktop + +## Version Information + +- **Extension Version**: 1.0.0 +- **DXT Version**: 0.1 +- **MCP SDK Version**: Latest +- **Node.js Compatibility**: 18.x, 20.x, 22.x + +--- + +**Note**: This extension runs locally and does not store or transmit personal data beyond the geocoding queries sent to the Nominatim API. \ No newline at end of file diff --git a/MCPB_README.md b/MCPB_README.md new file mode 100644 index 0000000..cc4d403 --- /dev/null +++ b/MCPB_README.md @@ -0,0 +1,172 @@ +# Geocoding MCP Desktop Extension (MCPB) + +A Desktop Extension (MCPB) for geocoding and reverse geocoding using the OpenStreetMap Nominatim API. This extension provides MCP (Model Context Protocol) tools for converting addresses to coordinates and coordinates to addresses. + +## Features + +- **Geocoding**: Convert addresses or location descriptions to coordinates +- **Reverse Geocoding**: Convert coordinates (latitude/longitude) to addresses +- **Multiple Output Formats**: Support for JSON, GeoJSON, and GeocodeJSON formats +- **Detailed Address Information**: Optional address breakdowns, extra tags, and name details +- **Polygon Geometries**: Support for GeoJSON, KML, SVG, and WKT polygon outputs +- **Flexible Filtering**: Filter by country codes, layers (address, POI, railway, natural, manmade), and feature types + +## Installation + +### Prerequisites + +- Claude Desktop (version 1.0.0 or higher) +- Node.js runtime (version 16.0.0 or higher) +- Compatible with macOS, Windows, and Linux + +### Install the Extension + +1. Download the `mcp.mcpb` file from this repository +2. Double-click the `.mcpb` file to open it with Claude Desktop +3. Follow the installation prompts in Claude Desktop +4. The extension will be automatically installed and configured + +### Manual Installation + +If you prefer to install manually: + +1. Open Claude Desktop +2. Go to Settings → Extensions +3. Click "Install Extension" +4. Select the `mcp.mcpb` file +5. Confirm the installation + +## Usage + +Once installed, the extension provides two main tools: + +### 1. Geocode Tool + +Convert an address or location description to coordinates. + +**Example Usage:** +``` +Geocode "1600 Amphitheatre Parkway, Mountain View, CA" +``` + +**Parameters:** +- `query` (required): Free-form string to search for +- `format`: Response format (xml, json, jsonv2, geojson, geocodejson) +- `addressdetails`: Include address breakdown (0 or 1) +- `extratags`: Include extra information (0 or 1) +- `namedetails`: Include full list of names (0 or 1) +- `countrycodes`: Filter by country codes (ISO 3166-1alpha2) +- `layer`: Filter by themes (address, poi, railway, natural, manmade) +- `featureType`: Fine-grained address selection (state, country, city, settlement) +- `polygon_*`: Add geometry in various formats + +### 2. Reverse Geocode Tool + +Convert coordinates to an address. + +**Example Usage:** +``` +Reverse geocode coordinates 37.4224764, -122.0842499 +``` + +**Parameters:** +- `lat` (required): Latitude in WGS84 projection +- `lon` (required): Longitude in WGS84 projection +- `zoom`: Level of detail (3=country, 18=buildings) +- `format`: Response format (xml, json, jsonv2, geojson, geocodejson) +- `addressdetails`: Include address breakdown (0 or 1) +- `extratags`: Include extra information (0 or 1) +- `namedetails`: Include full list of names (0 or 1) +- `layer`: Filter by themes (address, poi, railway, natural, manmade) +- `polygon_*`: Add geometry in various formats + +## Zoom Levels for Reverse Geocoding + +- **3**: Country +- **5**: State +- **8**: County +- **10**: City +- **12**: Town/Borough +- **13**: Village/Suburb +- **14**: Neighbourhood +- **15**: Any settlement +- **16**: Major streets +- **17**: Major and minor streets +- **18**: Buildings + +## Output Formats + +- **json**: Standard JSON format +- **jsonv2**: Enhanced JSON format (default) +- **geojson**: GeoJSON format +- **geocodejson**: GeocodeJSON format +- **xml**: XML format + +## Data Source and License + +This extension uses the OpenStreetMap Nominatim API: +- **Data**: © OpenStreetMap contributors +- **License**: ODbL 1.0 +- **Copyright**: http://osm.org/copyright + +## Development + +### Building from Source + +1. Clone the repository +2. Install dependencies: `bun install` +3. Build the project: `bun run build` +4. Create the MCPB package: `bunx @anthropic-ai/mcpb pack` + +### Project Structure + +``` +├── manifest.json # MCPB extension manifest +├── dist/ # Compiled JavaScript files +│ ├── index.js # Main entry point +│ ├── tools/ # MCP tool implementations +│ ├── clients/ # API client code +│ └── types/ # TypeScript type definitions +├── src/ # Source TypeScript files +└── mcp.mcpb # Generated MCPB extension file +``` + +### Testing + +Run tests with: +```bash +bun test +``` + +Run type checking: +```bash +bun tsc +``` + +## Troubleshooting + +### Common Issues + +1. **Extension not loading**: Ensure Claude Desktop version is 1.0.0 or higher +2. **Node.js errors**: Verify Node.js version 16.0.0 or higher is installed +3. **API errors**: Check internet connection and Nominatim service availability + +### Support + +For issues and support: +- Check the [OpenStreetMap Nominatim documentation](https://nominatim.org/release-docs/latest/api/) +- Review the [MCP SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk) +- File issues in the project repository + +## Contributing + +Contributions are welcome! Please: +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests for new functionality +5. Submit a pull request + +## License + +MIT License - see LICENSE file for details. diff --git a/bun.lock b/bun.lock index 9392ea3..6b7f44b 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,9 @@ "@eslint/js": "^9.37.0", "@types/bun": "1.2.23", "eslint": "^9.37.0", + "eslint-config-prettier": "^10.1.8", "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.3.0", "tsc-alias": "^1.8.16", "typescript-eslint": "^8.45.0", }, @@ -183,6 +185,8 @@ "eslint": ["eslint@9.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.4.0", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.37.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig=="], + "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], @@ -369,6 +373,8 @@ "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.3.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0 || 3" }, "optionalPeers": ["vue-tsc"] }, "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], @@ -423,7 +429,7 @@ "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], @@ -479,6 +485,8 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 6ab3cad..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-check - -import eslint from '@eslint/js' -import tseslint from 'typescript-eslint' - -export default tseslint.config({ - extends: [eslint.configs.recommended, tseslint.configs.recommended], - rules: { - semi: ['error', 'never'], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-vars': 'off', - 'comma-dangle': ['error', 'always-multiline'], - }, -}) diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..c943c0d --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,30 @@ +import pluginJs from '@eslint/js' +import prettierConfig from 'eslint-config-prettier' +import { defineConfig } from 'eslint/config' +import globals from 'globals' +import tseslint from 'typescript-eslint' + +export default defineConfig( + { + ignores: ['dist', 'node_modules', '**/*.d.ts'], + }, + { + languageOptions: { + globals: globals.browser, + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + prettierConfig, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-unused-vars': 'off', + }, + }, +) diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..e380e93 --- /dev/null +++ b/manifest.json @@ -0,0 +1,57 @@ +{ + "manifest_version": "0.2", + "name": "geocoding-mcp", + "display_name": "Geocoding MCP Extension", + "version": "dev", + "description": "A geocoding and reverse geocoding MCP extension using OpenStreetMap Nominatim API", + "long_description": "This extension provides geocoding and reverse geocoding capabilities through the OpenStreetMap Nominatim API. It allows you to convert addresses to coordinates (geocoding) and coordinates to addresses (reverse geocoding). The extension supports various output formats including JSON, GeoJSON, and includes detailed address breakdowns, extra tags, and polygon geometries.", + "author": { + "name": "Srihari Thalla", + "email": "daxserver@icloud.com" + }, + "keywords": [ + "geocoding", + "reverse-geocoding", + "nominatim", + "openstreetmap", + "osm", + "geojson", + "geocodejson", + "mapping", + "location", + "coordinates" + ], + "license": "MIT", + "server": { + "type": "node", + "entry_point": "dist/index.js", + "mcp_config": { + "command": "node", + "args": [ + "${__dirname}/dist/index.js" + ], + "env": {} + } + }, + "tools": [ + { + "name": "geocode", + "description": "Convert an address or location description to coordinates using OpenStreetMap Nominatim API" + }, + { + "name": "reverse_geocode", + "description": "Convert coordinates (latitude/longitude) to an address using OpenStreetMap Nominatim API" + } + ], + "compatibility": { + "claude_desktop": ">=0.11.0", + "platforms": [ + "darwin", + "win32", + "linux" + ], + "runtimes": { + "node": ">=18.0.0" + } + } +} diff --git a/package.json b/package.json index 3aee145..f52d9e7 100644 --- a/package.json +++ b/package.json @@ -34,16 +34,19 @@ "scripts": { "build": "tsc && tsc-alias", "dev": "tsc --watch", - "format": "prettier --write \"**/*.ts\"", + "format": "prettier --cache --write \"**/*.ts\"", "inspect": "bunx @modelcontextprotocol/inspector node dist/index.js", "lint": "eslint src", + "mcpb": "bunx @anthropic-ai/mcpb", "prepublishOnly": "bun run build && chmod 755 dist/index.js" }, "devDependencies": { "@eslint/js": "^9.37.0", "@types/bun": "1.2.23", "eslint": "^9.37.0", + "eslint-config-prettier": "^10.1.8", "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.3.0", "tsc-alias": "^1.8.16", "typescript-eslint": "^8.45.0" }, diff --git a/prettier.config.ts b/prettier.config.ts index f53a7ab..1ba2e50 100644 --- a/prettier.config.ts +++ b/prettier.config.ts @@ -4,8 +4,19 @@ const config: Config = { $schema: 'https://json.schemastore.org/prettierrc', semi: false, singleQuote: true, + tabWidth: 2, + useTabs: false, + printWidth: 100, + trailingComma: 'all', + bracketSpacing: true, + arrowParens: 'always', + bracketSameLine: false, singleAttributePerLine: true, parser: 'typescript', + vueIndentScriptAndStyle: false, + htmlWhitespaceSensitivity: 'ignore', + endOfLine: 'lf', + plugins: ['prettier-plugin-organize-imports'], overrides: [ { files: ['*.d.ts'], diff --git a/src/clients/__tests__/nominatimClient.test.ts b/src/clients/__tests__/nominatimClient.test.ts index 59dd93a..5f54e3d 100644 --- a/src/clients/__tests__/nominatimClient.test.ts +++ b/src/clients/__tests__/nominatimClient.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, test } from 'bun:test' +import { describe, expect, it, test } from 'bun:test' import packageJson from '../../../package.json' with { type: 'json' } // Note: Due to difficulties in reliably mocking 'axios' with bun:test in this environment, @@ -36,9 +36,7 @@ describe('nominatimClient', () => { const expectedUserAgent = `GeocodingMCP github.com/geocoding-ai/mcp ${packageJson.version}` // This test primarily verifies the construction of the USER_AGENT string itself. // The actual header being set by axios relies on axios's internal mechanisms. - expect(expectedUserAgent).toContain( - 'GeocodingMCP github.com/geocoding-ai/mcp', - ) + expect(expectedUserAgent).toContain('GeocodingMCP github.com/geocoding-ai/mcp') expect(expectedUserAgent).toContain(packageJson.version) }) }) diff --git a/src/clients/nominatimClient.ts b/src/clients/nominatimClient.ts index 2e89397..1efffe1 100644 --- a/src/clients/nominatimClient.ts +++ b/src/clients/nominatimClient.ts @@ -1,6 +1,6 @@ -import axios from 'axios' -import type { ReverseGeocodeParams } from '@/types/reverseGeocodeTypes.js' import type { GeocodeParams } from '@/types/geocodeTypes.js' +import type { ReverseGeocodeParams } from '@/types/reverseGeocodeTypes.js' +import axios from 'axios' import packageJson from '../../package.json' with { type: 'json' } const USER_AGENT = `GeocodingMCP github.com/geocoding-ai/mcp ${packageJson.version}` @@ -12,8 +12,7 @@ const nominatimClient = axios.create({ }, }) -const condenseOutput = (result: any[]) => - result.map(({ licence, ...item }) => item) +const condenseOutput = (result: any[]) => result.map(({ licence, ...item }) => item) export const geocodeAddress = async (params: GeocodeParams) => { const response = await nominatimClient.get('search', { params }) diff --git a/src/index.ts b/src/index.ts index a597245..43e7af6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ #!/usr/bin/env node -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { registerGeocodeTool } from '@/tools/geocode.js' import { registerReverseGeocodeTool } from '@/tools/reverseGeocode.js' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import packageJson from '../package.json' with { type: 'json' } const server = new McpServer({ diff --git a/src/tools/__tests__/geocode.test.ts b/src/tools/__tests__/geocode.test.ts index 1d9db1a..6aec333 100644 --- a/src/tools/__tests__/geocode.test.ts +++ b/src/tools/__tests__/geocode.test.ts @@ -1,8 +1,8 @@ // src/tools/geocode.test.ts -import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test' -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { registerGeocodeTool } from '@/tools/geocode.js' -import * as nominatimClient from '@/clients/nominatimClient.js' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test' +import * as nominatimClient from '../../clients/nominatimClient.js' // Use the actual implementation from prepareResponse import { handleGeocodeResult } from '@/tools/prepareResponse.js' import type { GeocodeParams } from '@/types/geocodeTypes.js' @@ -26,20 +26,16 @@ mock.module('@modelcontextprotocol/sdk/server/mcp.js', () => ({ })) // Mock only nominatimClient -mock.module('@/clients/nominatimClient.js', () => ({ +mock.module('../../clients/nominatimClient.js', () => ({ geocodeAddress: mock(async (params: GeocodeParams) => { // Default successful mock implementation, can be overridden in tests - return [ - { place_id: 123, display_name: `Mocked result for ${params.query}` }, - ] + return [{ place_id: 123, display_name: `Mocked result for ${params.query}` }] }), })) describe('registerGeocodeTool', () => { let serverInstance: McpServer - let toolHandler: - | ((params: GeocodeParams) => Promise) - | undefined + let toolHandler: ((params: GeocodeParams) => Promise) | undefined beforeEach(() => { serverInstance = new McpServer({ name: 'test-server', version: '1.0' }) @@ -54,8 +50,7 @@ describe('registerGeocodeTool', () => { it("should register a tool named 'geocode'", () => { expect(serverInstance.tool).toHaveBeenCalled() - const mockCalls = (serverInstance.tool as ReturnType).mock - .calls + const mockCalls = (serverInstance.tool as ReturnType).mock.calls expect(mockCalls[0]?.[0]).toBe('geocode') expect(typeof mockCalls[0]?.[1]).toBe('string') // Description expect(mockCalls[0]?.[2]).toBeDefined() // Schema @@ -73,9 +68,7 @@ describe('registerGeocodeTool', () => { // Expected result from the actual handleGeocodeResult const expectedCallToolResult = handleGeocodeResult(mockGeocodeApiResult) - const geocodeAddressSpy = nominatimClient.geocodeAddress as ReturnType< - typeof mock - > + const geocodeAddressSpy = nominatimClient.geocodeAddress as ReturnType geocodeAddressSpy.mockResolvedValue(mockGeocodeApiResult) const result = await toolHandler(params) @@ -93,13 +86,9 @@ describe('registerGeocodeTool', () => { addressdetails: 1, countrycodes: 'fr', } - const geocodeAddressSpy = nominatimClient.geocodeAddress as ReturnType< - typeof mock - > + const geocodeAddressSpy = nominatimClient.geocodeAddress as ReturnType // Provide a default resolution for this spy instance for this test - geocodeAddressSpy.mockResolvedValue([ - { place_id: 456, display_name: 'Paris Result' }, - ]) + geocodeAddressSpy.mockResolvedValue([{ place_id: 456, display_name: 'Paris Result' }]) await toolHandler(params) expect(geocodeAddressSpy).toHaveBeenCalledWith(params) @@ -111,9 +100,7 @@ describe('registerGeocodeTool', () => { const params: GeocodeParams = { query: 'trigger-api-error' } const errorMessage = 'Nominatim API error during test' // Use a distinct message - const geocodeAddressSpy = nominatimClient.geocodeAddress as ReturnType< - typeof mock - > + const geocodeAddressSpy = nominatimClient.geocodeAddress as ReturnType // Configure the mock to reject with a specific error when called geocodeAddressSpy.mockImplementation(async () => { throw new Error(errorMessage) diff --git a/src/tools/__tests__/prepareResponse.test.ts b/src/tools/__tests__/prepareResponse.test.ts index 6eed476..7f1c19b 100644 --- a/src/tools/__tests__/prepareResponse.test.ts +++ b/src/tools/__tests__/prepareResponse.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect } from 'bun:test' import { handleGeocodeResult } from '@/tools/prepareResponse.js' import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js' +import { describe, expect, it } from 'bun:test' describe('handleGeocodeResult', () => { it('should return a message if the result is null', () => { @@ -43,9 +43,7 @@ describe('handleGeocodeResult', () => { }) it('should return the stringified result if it is a valid array', () => { - const result = [ - { place_id: 1, lat: '10.0', lon: '20.0', display_name: 'Test Location' }, - ] + const result = [{ place_id: 1, lat: '10.0', lon: '20.0', display_name: 'Test Location' }] const expected: CallToolResult = { content: [ { diff --git a/src/tools/__tests__/reverseGeocode.test.ts b/src/tools/__tests__/reverseGeocode.test.ts index ed15f62..c52148a 100644 --- a/src/tools/__tests__/reverseGeocode.test.ts +++ b/src/tools/__tests__/reverseGeocode.test.ts @@ -1,11 +1,11 @@ // src/tools/reverseGeocode.test.ts -import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test' -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' -import { registerReverseGeocodeTool } from '@/tools/reverseGeocode.js' -import * as nominatimClient from '@/clients/nominatimClient.js' import { handleGeocodeResult } from '@/tools/prepareResponse.js' // Using actual implementation +import { registerReverseGeocodeTool } from '@/tools/reverseGeocode.js' import type { ReverseGeocodeParams } from '@/types/reverseGeocodeTypes.js' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js' +import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test' +import * as nominatimClient from '../../clients/nominatimClient.js' // Mock McpServer to capture the handler mock.module('@modelcontextprotocol/sdk/server/mcp.js', () => ({ @@ -24,7 +24,7 @@ mock.module('@modelcontextprotocol/sdk/server/mcp.js', () => ({ })) // Mock only nominatimClient -mock.module('@/clients/nominatimClient.js', () => ({ +mock.module('../../clients/nominatimClient.js', () => ({ // Also mock geocodeAddress to avoid interference if it's imported by other modules under test geocodeAddress: mock(async (params: any) => [ { place_id: 999, display_name: `Geocode mock for ${params.query}` }, @@ -40,9 +40,7 @@ mock.module('@/clients/nominatimClient.js', () => ({ describe('registerReverseGeocodeTool', () => { let serverInstance: McpServer - let toolHandler: - | ((params: ReverseGeocodeParams) => Promise) - | undefined + let toolHandler: ((params: ReverseGeocodeParams) => Promise) | undefined beforeEach(() => { serverInstance = new McpServer({ name: 'test-server', version: '1.0' }) @@ -56,8 +54,7 @@ describe('registerReverseGeocodeTool', () => { it("should register a tool named 'reverse_geocode'", () => { expect(serverInstance.tool).toHaveBeenCalled() - const mockCalls = (serverInstance.tool as ReturnType).mock - .calls + const mockCalls = (serverInstance.tool as ReturnType).mock.calls expect(mockCalls[0]?.[0]).toBe('reverse_geocode') expect(typeof mockCalls[0]?.[1]).toBe('string') // Description expect(mockCalls[0]?.[2]).toBeDefined() // Schema @@ -73,13 +70,9 @@ describe('registerReverseGeocodeTool', () => { display_name: 'New York, NY', } - const expectedCallToolResult = handleGeocodeResult( - mockReverseGeocodeApiResult, - ) + const expectedCallToolResult = handleGeocodeResult(mockReverseGeocodeApiResult) - const reverseGeocodeSpy = nominatimClient.reverseGeocode as ReturnType< - typeof mock - > + const reverseGeocodeSpy = nominatimClient.reverseGeocode as ReturnType reverseGeocodeSpy.mockResolvedValue(mockReverseGeocodeApiResult) const result = await toolHandler(params) @@ -97,9 +90,7 @@ describe('registerReverseGeocodeTool', () => { zoom: 10, format: 'jsonv2', } - const reverseGeocodeSpy = nominatimClient.reverseGeocode as ReturnType< - typeof mock - > + const reverseGeocodeSpy = nominatimClient.reverseGeocode as ReturnType reverseGeocodeSpy.mockResolvedValue({ place_id: 789, display_name: 'Paris Result', @@ -115,9 +106,7 @@ describe('registerReverseGeocodeTool', () => { const params: ReverseGeocodeParams = { lat: '0', lon: '0' } // Params to trigger error const errorMessage = 'Nominatim API error for reverse geocode' - const reverseGeocodeSpy = nominatimClient.reverseGeocode as ReturnType< - typeof mock - > + const reverseGeocodeSpy = nominatimClient.reverseGeocode as ReturnType reverseGeocodeSpy.mockImplementation(async () => { throw new Error(errorMessage) }) diff --git a/src/tools/geocode.ts b/src/tools/geocode.ts index 2d0ec5d..cef6adf 100644 --- a/src/tools/geocode.ts +++ b/src/tools/geocode.ts @@ -1,10 +1,7 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { geocodeAddress } from '@/clients/nominatimClient.js' import { handleGeocodeResult } from '@/tools/prepareResponse.js' -import { - GeocodeParamsSchema, - type GeocodeParams, -} from '@/types/geocodeTypes.js' +import { GeocodeParamsSchema, type GeocodeParams } from '@/types/geocodeTypes.js' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' export const registerGeocodeTool = (server: McpServer) => { server.tool( diff --git a/src/tools/reverseGeocode.ts b/src/tools/reverseGeocode.ts index fe00403..ff3be28 100644 --- a/src/tools/reverseGeocode.ts +++ b/src/tools/reverseGeocode.ts @@ -1,10 +1,10 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { reverseGeocode } from '@/clients/nominatimClient.js' import { handleGeocodeResult } from '@/tools/prepareResponse.js' import { ReverseGeocodeParamsSchema, type ReverseGeocodeParams, } from '@/types/reverseGeocodeTypes.js' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' export const registerReverseGeocodeTool = (server: McpServer) => { server.tool( diff --git a/src/types/commonTypes.ts b/src/types/commonTypes.ts index ce008e3..61bae1b 100644 --- a/src/types/commonTypes.ts +++ b/src/types/commonTypes.ts @@ -1,9 +1,7 @@ import { z } from 'zod' export const CommonNominatimParamsSchema: z.ZodRawShape = { - format: z - .enum(['xml', 'json', 'jsonv2', 'geojson', 'geocodejson']) - .default('jsonv2'), + format: z.enum(['xml', 'json', 'jsonv2', 'geojson', 'geocodejson']).default('jsonv2'), addressdetails: z .union([z.literal(0), z.literal(1)]) .optional() diff --git a/src/types/geocodeTypes.ts b/src/types/geocodeTypes.ts index a19978c..5814050 100644 --- a/src/types/geocodeTypes.ts +++ b/src/types/geocodeTypes.ts @@ -1,5 +1,5 @@ -import { z } from 'zod' import { CommonNominatimParamsSchema } from '@/types/commonTypes.js' +import { z } from 'zod' export const GeocodeParamsSchema: z.ZodRawShape = { q: z.string().min(1), diff --git a/src/types/reverseGeocodeTypes.ts b/src/types/reverseGeocodeTypes.ts index 86f24e5..6c44425 100644 --- a/src/types/reverseGeocodeTypes.ts +++ b/src/types/reverseGeocodeTypes.ts @@ -1,5 +1,5 @@ -import { z } from 'zod' import { CommonNominatimParamsSchema } from '@/types/commonTypes.js' +import { z } from 'zod' export const ReverseGeocodeParamsSchema: z.ZodRawShape = { lat: z.number().min(-90).max(90), From 35b31b570d4dadb91eb89faea773ea850c8f9737 Mon Sep 17 00:00:00 2001 From: DaxServer Date: Mon, 6 Oct 2025 18:17:15 +0200 Subject: [PATCH 2/3] chore: add jiti --- bun.lock | 3 +++ package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/bun.lock b/bun.lock index 6b7f44b..c773293 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "@types/bun": "1.2.23", "eslint": "^9.37.0", "eslint-config-prettier": "^10.1.8", + "jiti": "^2.6.1", "prettier": "^3.6.2", "prettier-plugin-organize-imports": "^4.3.0", "tsc-alias": "^1.8.16", @@ -295,6 +296,8 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], diff --git a/package.json b/package.json index f52d9e7..72f543d 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@types/bun": "1.2.23", "eslint": "^9.37.0", "eslint-config-prettier": "^10.1.8", + "jiti": "^2.6.1", "prettier": "^3.6.2", "prettier-plugin-organize-imports": "^4.3.0", "tsc-alias": "^1.8.16", From c7f217705a5e0665c4212cd8fc3eb2c21e1644e6 Mon Sep 17 00:00:00 2001 From: DaxServer Date: Tue, 7 Oct 2025 10:53:30 +0200 Subject: [PATCH 3/3] chore: apply code review suggestions --- INSTALLATION.md | 24 ++++++++++++------------ MCPB_README.md | 6 +++--- eslint.config.ts | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index f9bf96b..71dacf7 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -1,7 +1,7 @@ # Geocoding MCP Desktop Extension - Installation Guide ## Overview -This guide will help you install and configure the Geocoding MCP Desktop Extension (DXT) for Claude Desktop. +This guide will help you install and configure the Geocoding MCP Desktop Extension (MCPB) for Claude Desktop. ## Prerequisites - Claude Desktop application installed @@ -12,15 +12,15 @@ This guide will help you install and configure the Geocoding MCP Desktop Extensi ### Method 1: Manual Installation (Recommended) -1. **Locate the DXT file** - - Find the `mcp.dxt` file in your project directory +1. **Locate the MCPB file** + - Find the `mcp.mcpb` file in your project directory - File size: ~44MB 2. **Install via Claude Desktop** - Open Claude Desktop - Go to Settings → Extensions - Click "Install Extension" - - Select the `mcp.dxt` file + - Select the `mcp.mcpb` file - Follow the installation prompts 3. **Verify Installation** @@ -31,11 +31,11 @@ This guide will help you install and configure the Geocoding MCP Desktop Extensi ### Method 2: Command Line Installation (Advanced) ```bash -# If you have the DXT CLI installed -dxt install mcp.dxt +# If you have the MCPB CLI installed +mcpb install mcp.mcpb # Or using bunx -bunx @anthropic-ai/dxt install mcp.dxt +bunx @anthropic-ai/mcpb install mcp.mcpb ``` ## Configuration @@ -48,13 +48,13 @@ Once installed, you can use the following tools in Claude Desktop: ### Geocode Tool Convert addresses to coordinates: -``` +```text Find the coordinates for "Times Square, New York" ``` ### Reverse Geocode Tool Convert coordinates to addresses: -``` +```text What address is at coordinates 40.7580, -73.9855? ``` @@ -124,10 +124,10 @@ To remove the extension: ## Version Information -- **Extension Version**: 1.0.0 -- **DXT Version**: 0.1 +- **Extension Version**: 1.x +- **MCPB Version**: 0.2 - **MCP SDK Version**: Latest -- **Node.js Compatibility**: 18.x, 20.x, 22.x +- **Node.js Compatibility**: 18.x and above --- diff --git a/MCPB_README.md b/MCPB_README.md index cc4d403..d5fcb9f 100644 --- a/MCPB_README.md +++ b/MCPB_README.md @@ -16,7 +16,7 @@ A Desktop Extension (MCPB) for geocoding and reverse geocoding using the OpenStr ### Prerequisites - Claude Desktop (version 1.0.0 or higher) -- Node.js runtime (version 16.0.0 or higher) +- Node.js runtime (version 18.0.0 or higher) - Compatible with macOS, Windows, and Linux ### Install the Extension @@ -45,7 +45,7 @@ Once installed, the extension provides two main tools: Convert an address or location description to coordinates. **Example Usage:** -``` +```text Geocode "1600 Amphitheatre Parkway, Mountain View, CA" ``` @@ -65,7 +65,7 @@ Geocode "1600 Amphitheatre Parkway, Mountain View, CA" Convert coordinates to an address. **Example Usage:** -``` +```text Reverse geocode coordinates 37.4224764, -122.0842499 ``` diff --git a/eslint.config.ts b/eslint.config.ts index c943c0d..82ee7eb 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -10,7 +10,7 @@ export default defineConfig( }, { languageOptions: { - globals: globals.browser, + globals: globals.node, parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname,