diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..efaabfd --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "cablate-maps", + "description": "Google Maps tools and workflows for AI agents", + "owner": { "name": "CabLate", "url": "https://github.com/cablate" }, + "plugins": [ + { + "name": "mcp-google-map", + "displayName": "Google Maps CLI", + "description": "Place research, travel planning, and local SEO without an MCP connection.", + "source": { + "source": "npm", + "package": "@cablate/mcp-google-map", + "version": "latest", + "registry": "https://registry.npmjs.org" + }, + "category": "Productivity", + "tags": ["google-maps", "travel", "local-seo"] + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..6d784f1 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "mcp-google-map", + "displayName": "Google Maps CLI", + "version": "0.0.68", + "description": "Google Maps place research, travel planning, and local SEO through three focused Skills and a standalone CLI.", + "author": { "name": "CabLate", "url": "https://github.com/cablate" }, + "homepage": "https://github.com/cablate/mcp-google-map", + "repository": "https://github.com/cablate/mcp-google-map", + "license": "MIT", + "keywords": ["google-maps", "places", "routing", "travel-planning", "local-seo"] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 055cff9..152e015 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,25 @@ jobs: - run: npm run format:check - run: npm run test:unit + - name: Package Claude plugin + run: npm run package:claude-plugin + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Validate Claude plugin + run: | + npx -y @anthropic-ai/claude-code@2.1.283 plugin validate artifacts/claude-plugin/mcp-google-map --strict + npx -y @anthropic-ai/claude-code@2.1.283 plugin validate . --strict + + - uses: actions/upload-artifact@v4 + with: + name: claude-plugin + path: | + artifacts/claude-plugin/*.zip + artifacts/claude-plugin/*.sha256 + - name: Smoke Test run: npm test env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 554d635..13f37dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,6 +70,20 @@ jobs: VERSION=$(node -p "require('./package.json').version") jq --arg v "$VERSION" '.version = $v' plugin.json > plugin.tmp && mv plugin.tmp plugin.json jq --arg v "$VERSION" '.version = $v' .codex-plugin/plugin.json > plugin.tmp && mv plugin.tmp .codex-plugin/plugin.json + jq --arg v "$VERSION" '.version = $v' .claude-plugin/plugin.json > plugin.tmp && mv plugin.tmp .claude-plugin/plugin.json + + - name: Package Claude plugin + run: npm run package:claude-plugin + + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Validate Claude plugin + run: | + npx -y @anthropic-ai/claude-code@2.1.283 plugin validate artifacts/claude-plugin/mcp-google-map --strict + npx -y @anthropic-ai/claude-code@2.1.283 plugin validate . --strict - name: Update CHANGELOG run: | @@ -165,7 +179,7 @@ jobs: - name: Commit version bump & tag run: | VERSION=$(node -p "require('./package.json').version") - git add package.json package-lock.json CHANGELOG.md server.json plugin.json .codex-plugin/plugin.json + git add package.json package-lock.json CHANGELOG.md server.json plugin.json .codex-plugin/plugin.json .claude-plugin/plugin.json git commit -m "chore: release v${VERSION}" git tag "v${VERSION}" git pull --rebase @@ -181,7 +195,9 @@ jobs: else NOTES="Initial release" fi - gh release create "v${VERSION}" --title "v${VERSION}" --notes "$NOTES" + gh release create "v${VERSION}" --title "v${VERSION}" --notes "$NOTES" \ + artifacts/claude-plugin/*.zip \ + artifacts/claude-plugin/*.sha256 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 371d6f8..c0e0c19 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ credentials.json .mcpregistry_* .mcp.json output/ +artifacts/ diff --git a/README.md b/README.md index 749788f..2a861cc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ `@cablate/mcp-google-map` turns Google Maps Platform into 18 read-only agent tools and three focused Skills. An agent can find real places, verify routes, compare options, build practical itineraries, or audit local search visibility—through MCP or a standalone CLI. -You choose the integration model: Codex Plugin when you want Skills without MCP setup, stdio for desktop MCP clients, or Streamable HTTP for shared and remote deployments. +You choose the integration model: Codex or Claude Code Plugin when you want Skills without MCP setup, stdio for desktop MCP clients, or Streamable HTTP for shared and remote deployments.

English · 繁體中文

@@ -47,6 +47,7 @@ These are data and planning tools, not guarantees of safety, accessibility, open | Use | Best for | What runs | |---|---|---| | **Codex Plugin** | Asking Codex map, travel, or local SEO questions without MCP configuration | A matching Skill loads on demand and calls the standalone CLI | +| **Claude Code Plugin** | Installing the same three Skills from a Claude marketplace | Skills are namespaced under `mcp-google-map` and call the standalone CLI | | **Standalone CLI** | Scripts, automation, and other Skill-compatible agents | One stateless command returns JSON | | **MCP stdio** | Claude Desktop, Cursor, VS Code, and other local MCP clients | The client starts a local MCP process | | **Streamable HTTP** | Multi-session, containerized, LAN, or remote access | A self-hosted server exposes `/mcp` | @@ -74,6 +75,17 @@ Try asking: Codex selects `google-maps` for general location research, `google-maps-travel-planning` for itineraries, or `google-maps-local-seo` for business visibility analysis. The plugin does not register or start an MCP server. See the [no-MCP walkthrough](./examples/agent-skill-demo.md) for a reproducible example. +## Install in Claude Code—no MCP required + +```bash +claude plugin marketplace add cablate/mcp-google-map +claude plugin install mcp-google-map@cablate-maps +``` + +Start a new Claude Code session, set `GOOGLE_MAPS_API_KEY` in its environment, and run the same `doctor` check shown above. Claude exposes the installed Skills with the plugin namespace, for example `/mcp-google-map:google-maps-travel-planning`. + +Each GitHub release also includes a versioned `mcp-google-map-claude-plugin-v*.zip` and SHA-256 checksum for direct distribution. The release workflow builds this archive from the same `skills/` source and validates it with Claude Code's official strict validator before publishing. + ## Use the standalone CLI Every MCP tool also has a short CLI name: diff --git a/README.zh-TW.md b/README.zh-TW.md index d0f23d4..bcadf4a 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -4,7 +4,7 @@ `@cablate/mcp-google-map` 將 Google Maps Platform 整理成 18 個唯讀代理工具與三個用途明確的 Skills。代理可以搜尋真實地點、驗證路線、比較選項、建立可執行的旅行計畫,或分析在地搜尋能見度;既可透過 MCP,也可直接使用獨立 CLI。 -整合方式由你決定:不想設定 MCP 時安裝 Codex Plugin;桌面 MCP client 使用 stdio;共享或遠端部署則使用 Streamable HTTP。 +整合方式由你決定:不想設定 MCP 時安裝 Codex 或 Claude Code Plugin;桌面 MCP client 使用 stdio;共享或遠端部署則使用 Streamable HTTP。

English · 繁體中文

@@ -47,6 +47,7 @@ | 使用方式 | 適合情境 | 實際執行內容 | |---|---|---| | **Codex Plugin** | 不設定 MCP,直接讓 Codex 回答地圖、旅行或 Local SEO 問題 | 符合請求的 Skill 按需載入並呼叫獨立 CLI | +| **Claude Code Plugin** | 從 Claude marketplace 安裝相同的三個 Skills | Skills 以 `mcp-google-map` namespace 載入並呼叫獨立 CLI | | **獨立 CLI** | 腳本、自動化與其他支援 Skill 的代理 | 單次無狀態命令回傳 JSON | | **MCP stdio** | Claude Desktop、Cursor、VS Code 與其他本機 MCP clients | Client 啟動本機 MCP 程序 | | **Streamable HTTP** | 多 session、容器、區域網路或遠端存取 | 自架 server 暴露 `/mcp` | @@ -74,6 +75,17 @@ npx -y @cablate/mcp-google-map doctor Codex 會依請求選擇一般地圖研究、旅行規劃或 Local SEO Skill。Plugin 不會註冊或啟動 MCP server。可參考[非 MCP 完整示範](./examples/agent-skill-demo.md)重現整個流程。 +## 安裝到 Claude Code,不需要 MCP + +```bash +claude plugin marketplace add cablate/mcp-google-map +claude plugin install mcp-google-map@cablate-maps +``` + +開啟新的 Claude Code session,在執行環境設定 `GOOGLE_MAPS_API_KEY`,再執行前述 `doctor` 檢查。Claude 會以 plugin namespace 顯示 Skills,例如 `/mcp-google-map:google-maps-travel-planning`。 + +每個 GitHub release 也會附上版本化的 `mcp-google-map-claude-plugin-v*.zip` 與 SHA-256 checksum,供直接散布。Release workflow 從同一份 `skills/` 來源產生壓縮檔,並在發布前使用 Claude Code 官方 strict validator 驗證。 + ## 使用獨立 CLI 每一個 MCP 工具都有簡短的 CLI 名稱: diff --git a/package-lock.json b/package-lock.json index f86632c..56458bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@typescript-eslint/parser": "^8.24.0", "eslint": "^9.20.1", "eslint-config-prettier": "^10.0.1", + "fflate": "^0.8.3", "jest": "^29.7.0", "prettier": "^3.5.0", "ts-jest": "^29.2.5", @@ -4560,6 +4561,13 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -11618,6 +11626,12 @@ "web-streams-polyfill": "^3.0.3" } }, + "fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true + }, "file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", diff --git a/package.json b/package.json index b587f2d..f35d5d1 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "README.md", "plugin.json", ".codex-plugin", + ".claude-plugin", "skills", "examples" ], @@ -27,6 +28,7 @@ "lint": "eslint .", "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"", + "package:claude-plugin": "node scripts/package-claude-plugin.mjs", "prepublishOnly": "npm run build" }, "engines": { @@ -81,6 +83,7 @@ "@typescript-eslint/parser": "^8.24.0", "eslint": "^9.20.1", "eslint-config-prettier": "^10.0.1", + "fflate": "^0.8.3", "jest": "^29.7.0", "prettier": "^3.5.0", "ts-jest": "^29.2.5", diff --git a/scripts/package-claude-plugin.mjs b/scripts/package-claude-plugin.mjs new file mode 100644 index 0000000..425c076 --- /dev/null +++ b/scripts/package-claude-plugin.mjs @@ -0,0 +1,46 @@ +import { Buffer } from "node:buffer"; +import { log } from "node:console"; +import { createHash } from "node:crypto"; +import { cpSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { basename, join, relative } from "node:path"; +import { fileURLToPath, URL } from "node:url"; +import { zipSync } from "fflate"; + +const rootPath = fileURLToPath(new URL("../", import.meta.url)); +const packageJson = JSON.parse(readFileSync(join(rootPath, "package.json"), "utf8")); +const artifacts = join(rootPath, "artifacts", "claude-plugin"); +const pluginName = "mcp-google-map"; +const staging = join(artifacts, pluginName); +const archiveName = `${pluginName}-claude-plugin-v${packageJson.version}.zip`; +const archivePath = join(artifacts, archiveName); + +rmSync(artifacts, { recursive: true, force: true }); +mkdirSync(join(staging, ".claude-plugin"), { recursive: true }); +cpSync(join(rootPath, ".claude-plugin", "plugin.json"), join(staging, ".claude-plugin", "plugin.json")); +cpSync(join(rootPath, "skills"), join(staging, "skills"), { recursive: true }); +for (const file of ["README.md", "README.zh-TW.md", "LICENSE"]) cpSync(join(rootPath, file), join(staging, file)); + +const manifest = JSON.parse(readFileSync(join(staging, ".claude-plugin", "plugin.json"), "utf8")); +if (manifest.name !== pluginName || manifest.version !== packageJson.version) { + throw new Error("Claude plugin name/version must match the package release."); +} + +function collect(directory, prefix = pluginName) { + const entries = {}; + for (const name of readdirSync(directory)) { + const path = join(directory, name); + const archiveKey = `${prefix}/${name}`.replaceAll("\\", "/"); + if (statSync(path).isDirectory()) Object.assign(entries, collect(path, archiveKey)); + else entries[archiveKey] = new Uint8Array(readFileSync(path)); + } + return entries; +} + +const archive = Buffer.from(zipSync(collect(staging), { level: 9 })); +writeFileSync(archivePath, archive); +const digest = createHash("sha256").update(archive).digest("hex"); +writeFileSync(`${archivePath}.sha256`, `${digest} ${basename(archivePath)}\n`); + +log(`plugin=${relative(rootPath, staging)}`); +log(`archive=${relative(rootPath, archivePath)}`); +log(`sha256=${digest}`); diff --git a/tests/pluginPackage.unit.test.ts b/tests/pluginPackage.unit.test.ts index 3ba8636..e9f701f 100644 --- a/tests/pluginPackage.unit.test.ts +++ b/tests/pluginPackage.unit.test.ts @@ -23,6 +23,15 @@ interface Marketplace { }>; } +interface ClaudeMarketplace { + name: string; + owner: { name: string }; + plugins: Array<{ + name: string; + source: Record; + }>; +} + function readJson(path: string): T { return JSON.parse(readFileSync(new URL(`../${path}`, import.meta.url), "utf8")) as T; } @@ -31,16 +40,35 @@ test("plugin package is Skill-only and versioned with the npm package", () => { const packageJson = readJson("package.json"); const portableManifest = readJson("plugin.json"); const codexManifest = readJson(".codex-plugin/plugin.json"); + const claudeManifest = readJson(".claude-plugin/plugin.json"); assert.equal(portableManifest.name, "mcp-google-map"); assert.equal(codexManifest.name, portableManifest.name); + assert.equal(claudeManifest.name, portableManifest.name); assert.equal(portableManifest.version, packageJson.version); assert.equal(codexManifest.version, packageJson.version); + assert.equal(claudeManifest.version, packageJson.version); assert.equal(codexManifest.skills, "./skills/"); assert.equal("mcpServers" in portableManifest, false); assert.equal("mcpServers" in codexManifest, false); assert.ok(packageJson.files.includes("plugin.json")); assert.ok(packageJson.files.includes(".codex-plugin")); + assert.ok(packageJson.files.includes(".claude-plugin")); +}); + +test("Claude marketplace installs the published npm plugin", () => { + const marketplace = readJson(".claude-plugin/marketplace.json"); + const [plugin] = marketplace.plugins; + + assert.equal(marketplace.name, "cablate-maps"); + assert.equal(marketplace.owner.name, "CabLate"); + assert.equal(plugin.name, "mcp-google-map"); + assert.deepEqual(plugin.source, { + source: "npm", + package: "@cablate/mcp-google-map", + version: "latest", + registry: "https://registry.npmjs.org", + }); }); test("plugin exposes three focused Skills with shared diagnostics", () => {