Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -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"]
}
]
}
11 changes: 11 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"]
}
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 18 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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
Expand All @@ -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 }}

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ credentials.json
.mcpregistry_*
.mcp.json
output/
artifacts/
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<p align="center"><b>English</b> · <a href="./README.zh-TW.md">繁體中文</a></p>

Expand Down Expand Up @@ -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` |
Expand Down Expand Up @@ -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:
Expand Down
14 changes: 13 additions & 1 deletion README.zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。

<p align="center"><a href="./README.md">English</a> · <b>繁體中文</b></p>

Expand Down Expand Up @@ -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` |
Expand Down Expand Up @@ -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 名稱:
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"README.md",
"plugin.json",
".codex-plugin",
".claude-plugin",
"skills",
"examples"
],
Expand All @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
46 changes: 46 additions & 0 deletions scripts/package-claude-plugin.mjs
Original file line number Diff line number Diff line change
@@ -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}`);
28 changes: 28 additions & 0 deletions tests/pluginPackage.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ interface Marketplace {
}>;
}

interface ClaudeMarketplace {
name: string;
owner: { name: string };
plugins: Array<{
name: string;
source: Record<string, string>;
}>;
}

function readJson<T>(path: string): T {
return JSON.parse(readFileSync(new URL(`../${path}`, import.meta.url), "utf8")) as T;
}
Expand All @@ -31,16 +40,35 @@ test("plugin package is Skill-only and versioned with the npm package", () => {
const packageJson = readJson<PackageManifest>("package.json");
const portableManifest = readJson<PluginManifest>("plugin.json");
const codexManifest = readJson<PluginManifest>(".codex-plugin/plugin.json");
const claudeManifest = readJson<PluginManifest>(".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<ClaudeMarketplace>(".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", () => {
Expand Down
Loading