diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index b8c8b8b..d360544 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "mcp-google-map", "version": "0.0.64", - "description": "Use Google Maps-backed place search, routing, geocoding, weather, air quality, and geographic analysis through a standalone CLI.", + "description": "Use focused Skills for Google Maps place research, travel planning, and local SEO through a standalone CLI.", "author": { "name": "CabLate", "url": "https://github.com/cablate" @@ -20,8 +20,8 @@ "skills": "./skills/", "interface": { "displayName": "Google Maps CLI", - "shortDescription": "Geospatial research and planning through a standalone CLI.", - "longDescription": "Guide an AI agent to use the @cablate/mcp-google-map exec CLI for place discovery, routing, geocoding, travel planning, local SEO, weather, air quality, and other geographic workflows. No MCP connection is required.", + "shortDescription": "Focused maps, travel planning, and local SEO workflows through a standalone CLI.", + "longDescription": "Three focused Skills guide an AI agent to use the @cablate/mcp-google-map CLI for general geospatial research, evidence-backed travel planning, and local SEO analysis. No MCP connection is required.", "developerName": "CabLate", "category": "Productivity", "capabilities": [ @@ -30,8 +30,8 @@ "websiteURL": "https://github.com/cablate/mcp-google-map", "defaultPrompt": [ "Find and compare places for my trip.", - "Plan an efficient route between these stops.", - "Research what is near this location." + "Plan a practical multi-day itinerary with checked routes.", + "Analyze this business's local Google Maps visibility." ] } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 923f18a..055cff9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,6 @@ jobs: node-version: 18 - run: npm ci - - run: python3 scripts/package-skill.py --check + - run: python3 scripts/validate-skills.py - run: npm run build - run: npm run test:unit diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c022f6..2075944 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Split the Skill-only plugin into focused general maps, travel planning, and local SEO Skills with shared setup and attribution guidance. +- Add a non-billable `doctor` readiness check and opt-in `doctor --live` checks for Geocoding, Places API (New), and Routes API. + ## [0.0.48] - feat: batch keyword scanning for local rank tracker (#68) diff --git a/CLAUDE.md b/CLAUDE.md index 7c68dc9..29eda2c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,8 +16,8 @@ | 3 | `src/cli.ts` | EXEC_TOOLS 列表 + switch case | | 4 | `tests/smoke.test.ts` | expectedTools 陣列 + tool count assertions | | 5 | `README.md` | tool 數量(header、vs Grounding Lite 表、Server Info、exec mode)+ Available Tools 表格 + Project Structure | -| 6 | `skills/google-maps/SKILL.md` | Tool Map 表格 | -| 7 | `skills/google-maps/references/tools-api.md` | 參數文件 + chaining patterns | +| 6 | `skills/*/SKILL.md` | 對應使用者目標、工具選擇與 Skill 邊界 | +| 7 | `skills/google-maps/references/tools-api.md` | 參數文件 + chaining patterns;專用流程同步更新其 Skill reference | | 8 | `server.json` | description(如有提到 tool 數量) | | 9 | `package.json` | description(如有提到 tool 數量) | diff --git a/README.md b/README.md index cab6e61..c4dbb72 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ - **18 tools** — 14 atomic + 4 composite (explore-area, plan-route, compare-places, local-rank-tracker) - **3 modes** — stdio, StreamableHTTP, standalone exec CLI -- **Agent Skill** — built-in skill definition teaches AI how to chain geo tools ([`skills/google-maps/`](./skills/google-maps/)) +- **3 Agent Skills** — focused workflows for general maps, travel planning, and local SEO ([`skills/`](./skills/)) ### vs Google Grounding Lite @@ -89,7 +89,7 @@ All tools are annotated with `readOnlyHint: true` and `destructiveHint: false` ## Installation -### Codex Plugin (Agent Skill, no MCP required) +### Codex Plugin (3 Agent Skills, no MCP required) Install the CabLate marketplace, then install the Skill-only plugin: @@ -98,7 +98,13 @@ codex plugin marketplace add cablate/mcp-google-map --ref main codex plugin add mcp-google-map@cablate ``` -Set `GOOGLE_MAPS_API_KEY` in the environment available to Codex, make sure Node.js 18+ and `npx` are installed, then start a new conversation. The plugin teaches the agent to run `npx -y @cablate/mcp-google-map exec ...` directly. It does not register or start an MCP server; the MCP setup below remains an independent option. +Set `GOOGLE_MAPS_API_KEY` in the environment available to Codex, make sure Node.js 18+ and `npx` are installed, then start a new conversation. The plugin discovers three focused Skills and teaches the agent to run `npx -y @cablate/mcp-google-map exec ...` directly. It does not register or start an MCP server; the MCP setup below remains an independent option. + +At runtime, Codex sees the name and description of each Skill and loads the full instructions only when the request matches: + +- `google-maps` — place search, geocoding, routes, neighborhood and environmental facts +- `google-maps-travel-planning` — day trips and multi-day itineraries +- `google-maps-local-seo` — Google Business Profile visibility and geographic rank analysis ### Method 1: stdio (Recommended for most clients) @@ -169,17 +175,17 @@ npx @cablate/mcp-google-map exec geocode '{"address":"Tokyo Tower"}' npx @cablate/mcp-google-map exec search-places '{"query":"ramen in Tokyo"}' ``` -All 18 tools available: `geocode`, `reverse-geocode`, `search-nearby`, `search-places`, `place-details`, `directions`, `distance-matrix`, `elevation`, `timezone`, `weather`, `air-quality`, `static-map`, `batch-geocode-tool`, `search-along-route`, `explore-area`, `plan-route`, `compare-places`, `local-rank-tracker`. See [`skills/google-maps/`](./skills/google-maps/) for the agent skill definition and full parameter docs. +All 18 tools available: `geocode`, `reverse-geocode`, `search-nearby`, `search-places`, `place-details`, `directions`, `distance-matrix`, `elevation`, `timezone`, `weather`, `air-quality`, `static-map`, `batch-geocode-tool`, `search-along-route`, `explore-area`, `plan-route`, `compare-places`, `local-rank-tracker`. See [`skills/`](./skills/) for the Skill definitions and full parameter docs. To use the **Agent Skill without MCP**: -1. Install the entire [`skills/google-maps/`](./skills/google-maps/) folder in your agent's Skills directory, following that client's instructions. Keep `SKILL.md` and `references/` together. Clients that support `.skill` imports can use the bundled [`SKILL.skill`](./skills/google-maps/SKILL.skill) archive instead. Installing the npm package alone does not register a Skill with an agent. +1. Install the entire [`skills/`](./skills/) tree in your agent's Skills directory, following that client's instructions. Keep all three Skill folders and `_shared/` together so cross-Skill references continue to resolve. Installing the npm package alone does not register a Skill unless the package is installed through its plugin marketplace. 2. Make Node.js 18+ and `npx` available to the agent, and set `GOOGLE_MAPS_API_KEY` in its environment. Prefer the environment variable to `--apikey`, which can expose a key in shell history or process listings. 3. Ask the agent a location question. The Skill guides it to call the standalone CLI, for example `npx -y @cablate/mcp-google-map exec geocode '{"address":"Tokyo Tower"}'`. No MCP server or MCP client configuration is needed. -You can verify the CLI is available with `npx -y @cablate/mcp-google-map exec --help` before making a billable API call. +Run `npx -y @cablate/mcp-google-map doctor` for a non-billable local readiness check. After informing the user that test requests may incur Google Maps Platform charges, `doctor --live` can verify Geocoding, Places API (New), and Routes API independently. -For a reproducible no-MCP walkthrough, see the [Agent Skill demo](./examples/agent-skill-demo.md). If your agent or app displays Places reviews, photos, or AI summaries, follow the [content attribution and storage guidance](./skills/google-maps/references/content-attribution.md); the tool preserves source/disclosure metadata but does not render a compliant UI for you. +For a reproducible no-MCP walkthrough, see the [Agent Skill demo](./examples/agent-skill-demo.md). If your agent or app displays Places reviews, photos, or AI summaries, follow the [content attribution and storage guidance](./skills/_shared/content-attribution.md); the tool preserves source/disclosure metadata but does not render a compliant UI for you. ### Batch Geocode @@ -301,13 +307,18 @@ src/ tests/ └── smoke.test.ts # Smoke + E2E test suite skills/ -└── google-maps/ # Public Agent Skill — standalone CLI workflows -│ ├── SKILL.md # Tool map, recipes, invocation -│ ├── SKILL.skill # Importable archive of this Skill -│ └── references/ -│ ├── tools-api.md # Tool parameters + scenario recipes -│ ├── travel-planning.md # Travel planning methodology -│ └── local-seo.md # Local SEO / Google Business Profile ranking analysis +├── google-maps/ # General place, route, and environment workflow +│ ├── SKILL.md +│ └── references/tools-api.md # Tool parameters + generic recipes +├── google-maps-travel-planning/ # Day-trip and itinerary workflow +│ ├── SKILL.md +│ └── references/travel-planning.md +├── google-maps-local-seo/ # Business visibility and grid-rank workflow +│ ├── SKILL.md +│ └── references/local-seo.md +└── _shared/ # On-demand resources used by all three Skills + ├── setup-and-diagnostics.md + └── content-attribution.md .agents/ ├── plugins/marketplace.json # CabLate marketplace catalog └── skills/project-docs/ # Maintainer-only development Skill diff --git a/README.zh-TW.md b/README.zh-TW.md index 9b6c9b2..cb8f42e 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -21,7 +21,7 @@ - **18 個工具** — 14 個原子工具 + 4 個組合工具(explore-area、plan-route、compare-places、local-rank-tracker) - **3 種模式** — stdio、StreamableHTTP、獨立 exec CLI -- **Agent Skill** — 內建技能定義,教 AI 如何串接地理工具([`skills/google-maps/`](./skills/google-maps/)) +- **3 個 Agent Skills** — 分別處理一般地圖、旅行規劃與 Local SEO([`skills/`](./skills/)) ### vs Google Grounding Lite @@ -89,7 +89,7 @@ npx @cablate/mcp-google-map --port 3000 --apikey "YOUR_API_KEY" ## 安裝 -### Codex Plugin(Agent Skill,不需要 MCP) +### Codex Plugin(3 個 Agent Skills,不需要 MCP) 先加入 CabLate marketplace,再安裝只包含 Skill 的 plugin: @@ -98,7 +98,13 @@ codex plugin marketplace add cablate/mcp-google-map --ref main codex plugin add mcp-google-map@cablate ``` -請在 Codex 可讀取的環境中設定 `GOOGLE_MAPS_API_KEY`,並確認已安裝 Node.js 18+ 與 `npx`,然後開啟新對話。Plugin 會教代理直接執行 `npx -y @cablate/mcp-google-map exec ...`;它不會註冊或啟動 MCP server。下方的 MCP 設定仍是另一種獨立使用方式。 +請在 Codex 可讀取的環境中設定 `GOOGLE_MAPS_API_KEY`,並確認已安裝 Node.js 18+ 與 `npx`,然後開啟新對話。Plugin 會探索三個用途明確的 Skills,並教代理直接執行 `npx -y @cablate/mcp-google-map exec ...`;它不會註冊或啟動 MCP server。下方的 MCP 設定仍是另一種獨立使用方式。 + +執行階段會先看到每個 Skill 的名稱與描述,只有請求符合時才載入完整指令: + +- `google-maps` — 地點搜尋、地址解析、路線、區域與環境資訊 +- `google-maps-travel-planning` — 單日與多日旅行行程 +- `google-maps-local-seo` — Google 商家檔案能見度與地理排名分析 ### 方法一:stdio(大多數客戶端推薦) @@ -169,17 +175,17 @@ npx @cablate/mcp-google-map exec geocode '{"address":"台北101"}' npx @cablate/mcp-google-map exec search-places '{"query":"東京拉麵"}' ``` -全部 18 個工具可用:`geocode`、`reverse-geocode`、`search-nearby`、`search-places`、`place-details`、`directions`、`distance-matrix`、`elevation`、`timezone`、`weather`、`air-quality`、`static-map`、`batch-geocode-tool`、`search-along-route`、`explore-area`、`plan-route`、`compare-places`、`local-rank-tracker`。完整參數文件見 [`skills/google-maps/`](./skills/google-maps/)。 +全部 18 個工具可用:`geocode`、`reverse-geocode`、`search-nearby`、`search-places`、`place-details`、`directions`、`distance-matrix`、`elevation`、`timezone`、`weather`、`air-quality`、`static-map`、`batch-geocode-tool`、`search-along-route`、`explore-area`、`plan-route`、`compare-places`、`local-rank-tracker`。Skill 定義與完整參數文件見 [`skills/`](./skills/)。 若要**不透過 MCP、只用 Agent Skill**: -1. 依你的代理工具說明,將整個 [`skills/google-maps/`](./skills/google-maps/) 資料夾安裝到它的 Skills 目錄;`SKILL.md` 與 `references/` 必須放在一起。支援匯入 `.skill` 的工具也可使用隨附的 [`SKILL.skill`](./skills/google-maps/SKILL.skill)。只安裝 npm 套件不會自動把 Skill 註冊到代理工具。 +1. 依你的代理工具說明,將整個 [`skills/`](./skills/) 目錄樹安裝到它的 Skills 目錄;三個 Skill 資料夾與 `_shared/` 必須放在一起,跨 Skill 參考才會正常解析。除非透過 Plugin Marketplace 安裝,否則只安裝 npm 套件不會自動把 Skill 註冊到代理工具。 2. 讓代理工具可使用 Node.js 18+、`npx`,並在其環境設定 `GOOGLE_MAPS_API_KEY`。建議使用環境變數;`--apikey` 可能讓金鑰出現在 shell 歷史或程序清單。 3. 直接向代理工具提問地點問題。Skill 會引導它呼叫獨立 CLI,例如 `npx -y @cablate/mcp-google-map exec geocode '{"address":"台北101"}'`;不必啟動 MCP server 或設定 MCP client。 -呼叫可能計費的 API 前,可先用 `npx -y @cablate/mcp-google-map exec --help` 確認 CLI 可用。 +先執行 `npx -y @cablate/mcp-google-map doctor`,即可在不呼叫 Google API、不產生 API 費用的情況下檢查本機準備狀態。告知使用者測試請求可能產生 Google Maps Platform 費用後,可用 `doctor --live` 分別驗證 Geocoding、Places API (New) 與 Routes API。 -完整的非 MCP 操作步驟見 [Agent Skill 示範](./examples/agent-skill-demo.md)。若代理工具或應用程式會呈現 Places 評論、照片或 AI 摘要,請遵照[內容署名與保存指引](./skills/google-maps/references/content-attribution.md);工具會保留來源及揭露欄位,但不會替你的介面完成署名呈現。 +完整的非 MCP 操作步驟見 [Agent Skill 示範](./examples/agent-skill-demo.md)。若代理工具或應用程式會呈現 Places 評論、照片或 AI 摘要,請遵照[內容署名與保存指引](./skills/_shared/content-attribution.md);工具會保留來源及揭露欄位,但不會替你的介面完成署名呈現。 ### 批次地理編碼 @@ -298,13 +304,18 @@ src/ tests/ └── smoke.test.ts # Smoke + E2E 測試套件 skills/ -└── google-maps/ # 公開 Agent Skill — 獨立 CLI 工作流程 -│ ├── SKILL.md # 工具對照表、場景食譜、呼叫方式 -│ ├── SKILL.skill # 可匯入的 Skill 封裝檔 -│ └── references/ -│ ├── tools-api.md # 工具參數 + 場景食譜 -│ ├── travel-planning.md # 旅行規劃方法論 -│ └── local-seo.md # Local SEO / Google 商家排名分析 +├── google-maps/ # 一般地點、路線與環境資訊工作流程 +│ ├── SKILL.md +│ └── references/tools-api.md # 工具參數 + 一般場景食譜 +├── google-maps-travel-planning/ # 單日與多日行程工作流程 +│ ├── SKILL.md +│ └── references/travel-planning.md +├── google-maps-local-seo/ # 商家能見度與地理排名工作流程 +│ ├── SKILL.md +│ └── references/local-seo.md +└── _shared/ # 三個 Skills 按需載入的共用資源 + ├── setup-and-diagnostics.md + └── content-attribution.md .agents/ ├── plugins/marketplace.json # CabLate marketplace 目錄 └── skills/project-docs/ # 僅供維護者使用的開發 Skill diff --git a/docs/backlog-and-strategy.md b/docs/backlog-and-strategy.md index 60c4ae7..6f9fa6a 100644 --- a/docs/backlog-and-strategy.md +++ b/docs/backlog-and-strategy.md @@ -13,7 +13,7 @@ This is a current maintenance and product roadmap, not a release-status ledger. - Keep Google Maps content display guidance aligned with the [Places API policies](https://developers.google.com/maps/documentation/places/web-service/policies), particularly attribution for reviews/photos, AI-summary disclosures, source links, and storage limits. The package is a data/tool layer; downstream applications still own their rendered UI, terms, and privacy notices. - Triage production dependency advisories by affected path and exploit preconditions, then update compatible versions and verify build, tests, and live API behavior. An audit count alone is not proof of an exploitable path. -- Keep the packaged Agent Skill synchronized with its source using `python3 scripts/package-skill.py --check` in CI. Update its tool map and API reference whenever tools change. +- Keep all packaged Agent Skills and shared references synchronized with source tools using `python3 scripts/validate-skills.py` in CI. Update the relevant workflow Skill and API reference whenever tools change. ## Deferred ideas diff --git a/examples/agent-skill-demo.md b/examples/agent-skill-demo.md index 03b9f82..d1c7a8c 100644 --- a/examples/agent-skill-demo.md +++ b/examples/agent-skill-demo.md @@ -1,24 +1,24 @@ # Agent Skill demo (no MCP server) -This walkthrough demonstrates the two pieces separately: the Agent Skill tells an agent which geographic tool to use, and the package CLI performs the API call. It does not require an MCP client or server. Build and run the CLI from the same repository revision as the Skill so the instructions and executable stay aligned. +This walkthrough demonstrates the two pieces separately: the plugin's Agent Skills tell an agent which geographic workflow and tool to use, and the package CLI performs the API call. It does not require an MCP client or server. Build and run the CLI from the same repository revision as the Skills so the instructions and executable stay aligned. -## 1. Install the Skill +## 1. Install the Skills -Clone this repository at a chosen release tag, then copy the whole `skills/google-maps/` directory into your agent's Skills directory according to that client's instructions. Keep `SKILL.md` and `references/` together. If the client accepts `.skill` archives, import `skills/google-maps/SKILL.skill` instead. Installing npm alone does not register the Skill. +Prefer installing the Codex plugin from the CabLate marketplace. For a manual installation, clone a chosen release tag and copy the whole `skills/` tree according to the client's instructions. Keep the three Skill folders and `_shared/` together. Installing npm alone does not register a Skill unless it is installed through the plugin marketplace. -Check that your agent can discover `google-maps` and run shell commands. Provide a Google Maps Platform API key to the agent's environment as `GOOGLE_MAPS_API_KEY` through your normal secret-management method; never paste it into the prompt or commit it. Node.js 18+ and `npx` must be available. +Check that your agent can discover `google-maps`, `google-maps-travel-planning`, and `google-maps-local-seo`, and can run shell commands. Provide a Google Maps Platform API key to the agent's environment as `GOOGLE_MAPS_API_KEY` through your normal secret-management method; never paste it into the prompt or commit it. Node.js 18+ and `npx` must be available. ## 2. Validate without an API call -From the cloned repository, install and build the package, then run its CLI help: +From the cloned repository, install and build the package, then run the local doctor: ```bash npm ci npm run build -node dist/cli.js exec --help +node dist/cli.js doctor ``` -The help should list `geocode` and `search-places`. This only checks CLI availability; it does not prove that your key or Google APIs are enabled. +The report should pass the Node.js, package, and API-key checks and skip `live-api`. It makes no Google API requests. To test Geocoding, Places (New), and Routes after disclosing that the calls may be billable, run `node dist/cli.js doctor --live`. ## 3. Try one live request @@ -34,6 +34,6 @@ node dist/cli.js exec geocode '{"address":"Tokyo Tower"}' Success means the CLI exits 0 and returns JSON with `success: true`, a `data.location` latitude/longitude, and a `data.place_id`. The exact coordinates and address may change; do not compare them to a hard-coded snapshot. A failed call exits nonzero and writes JSON to stderr. This step calls a billable Google API using your key. -For a published-package check from **outside** this repository, the equivalent command is `npx -y @cablate/mcp-google-map exec --help`. Running `npx` inside a checkout of the same package can resolve the local package instead of the published executable. +For a published-package check from **outside** this repository, the equivalent command is `npx -y @cablate/mcp-google-map doctor`. Running `npx` inside a checkout of the same package can resolve the local package instead of the published executable. -If you ask for place reviews, photos, or an AI summary in a later step, read the Skill's [content-attribution guidance](../skills/google-maps/references/content-attribution.md) before displaying them. This geocoding example does not validate every Google Maps Platform policy requirement for a downstream app. +If you ask for place reviews, photos, or an AI summary in a later step, read the shared [content-attribution guidance](../skills/_shared/content-attribution.md) before displaying them. This geocoding example does not validate every Google Maps Platform policy requirement for a downstream app. diff --git a/plugin.json b/plugin.json index bfd0580..538cbbe 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "mcp-google-map", "version": "0.0.64", - "description": "Use Google Maps-backed place search, routing, geocoding, weather, air quality, and geographic analysis through a standalone CLI.", + "description": "Use focused Skills for Google Maps place research, travel planning, and local SEO through a standalone CLI.", "author": { "name": "CabLate", "url": "https://github.com/cablate" diff --git a/scripts/package-skill.py b/scripts/package-skill.py deleted file mode 100644 index c75ee5a..0000000 --- a/scripts/package-skill.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Build or verify the distributable Google Maps Agent Skill archive. - -Usage: python scripts/package-skill.py --write|--check -""" - -from __future__ import annotations - -import argparse -import re -import sys -import zipfile -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] -SKILL_DIR = ROOT / "skills" / "google-maps" -ARCHIVE = SKILL_DIR / "SKILL.skill" -TOOL_DIR = ROOT / "src" / "tools" / "maps" -TOOL_NAME = re.compile(r'^const NAME = "(maps_[a-z_]+)";', re.MULTILINE) -SKILL_ROW = re.compile(r"^\| `(maps_[a-z_]+)` \|", re.MULTILINE) -REFERENCE_HEADING = re.compile(r"^## (maps_[a-z_]+)(?:\s|$)", re.MULTILINE) - - -def skill_files() -> dict[str, bytes]: - # Git may check out text with CRLF on Windows; package canonical LF bytes so - # the same archive validates on Linux CI and Windows development machines. - return { - path.relative_to(SKILL_DIR).as_posix(): path.read_bytes().replace(b"\r\n", b"\n") - for path in sorted(SKILL_DIR.rglob("*")) - if path.is_file() and path != ARCHIVE - } - - -def validate_tool_map() -> None: - source_names = set() - for path in TOOL_DIR.glob("*.ts"): - source_names.update(TOOL_NAME.findall(path.read_text(encoding="utf-8"))) - - skill_text = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8") - reference_text = (SKILL_DIR / "references" / "tools-api.md").read_text(encoding="utf-8") - tool_map = skill_text.split("## Tool Map", 1)[1].split("## Known API Limitations", 1)[0] - skill_names = SKILL_ROW.findall(tool_map) - reference_names = REFERENCE_HEADING.findall(reference_text) - - if len(skill_names) != len(set(skill_names)): - raise ValueError("SKILL.md Tool Map contains duplicate tool rows") - if len(reference_names) != len(set(reference_names)): - raise ValueError("tools-api.md contains duplicate tool headings") - for label, names in (("SKILL.md Tool Map", skill_names), ("tools-api.md headings", reference_names)): - if set(names) != source_names: - missing = sorted(source_names - set(names)) - extra = sorted(set(names) - source_names) - raise ValueError(f"{label} differs from source tools (missing={missing}, extra={extra})") - if f"{len(source_names)} tools in five categories" not in skill_text: - raise ValueError(f"SKILL.md must state the current {len(source_names)}-tool count") - - -def write_archive(files: dict[str, bytes]) -> None: - with zipfile.ZipFile(ARCHIVE, "w") as archive: - for name, content in files.items(): - entry = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0)) - entry.compress_type = zipfile.ZIP_DEFLATED - entry.external_attr = 0o644 << 16 - archive.writestr(entry, content, compress_type=zipfile.ZIP_DEFLATED, compresslevel=9) - - -def check_archive(files: dict[str, bytes]) -> None: - if not ARCHIVE.exists(): - raise ValueError(f"Missing {ARCHIVE.relative_to(ROOT)}; run --write") - with zipfile.ZipFile(ARCHIVE) as archive: - names = archive.namelist() - if len(names) != len(set(names)) or set(names) != set(files): - raise ValueError("SKILL.skill file list differs from the source Skill; run --write") - for name, content in files.items(): - if archive.read(name) != content: - raise ValueError(f"SKILL.skill has stale {name}; run --write") - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - mode = parser.add_mutually_exclusive_group(required=True) - mode.add_argument("--write", action="store_true", help="rebuild SKILL.skill") - mode.add_argument("--check", action="store_true", help="verify sources and archive") - args = parser.parse_args() - - try: - validate_tool_map() - files = skill_files() - if args.write: - write_archive(files) - else: - check_archive(files) - except (ValueError, OSError, zipfile.BadZipFile) as exc: - print(f"Skill package check failed: {exc}", file=sys.stderr) - return 1 - - print(f"Google Maps Skill: {len(files)} files {'packaged' if args.write else 'verified'}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/validate-skills.py b/scripts/validate-skills.py new file mode 100644 index 0000000..c807660 --- /dev/null +++ b/scripts/validate-skills.py @@ -0,0 +1,78 @@ +"""Validate the packaged Google Maps Skills and their source coverage.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SKILLS_ROOT = ROOT / "skills" +SKILL_DIRS = ( + SKILLS_ROOT / "google-maps", + SKILLS_ROOT / "google-maps-travel-planning", + SKILLS_ROOT / "google-maps-local-seo", +) +TOOL_DIR = ROOT / "src" / "tools" / "maps" +TOOL_NAME = re.compile(r'^const NAME = "(maps_[a-z_]+)";', re.MULTILINE) +REFERENCE_HEADING = re.compile(r"^## (maps_[a-z_]+)(?:\s|$)", re.MULTILINE) +FRONTMATTER_NAME = re.compile(r"^---\s*\nname:\s*([^\n]+)", re.MULTILINE) +BACKTICK_MARKDOWN_PATH = re.compile(r"`((?:\.\.?/|references/)[^`]+\.md)`") + + +def validate_skill_entrypoints() -> None: + names: list[str] = [] + for skill_dir in SKILL_DIRS: + skill_path = skill_dir / "SKILL.md" + if not skill_path.is_file(): + raise ValueError(f"Missing {skill_path.relative_to(ROOT)}") + text = skill_path.read_text(encoding="utf-8") + match = FRONTMATTER_NAME.search(text) + if not match: + raise ValueError(f"Missing frontmatter name in {skill_path.relative_to(ROOT)}") + name = match.group(1).strip() + if name != skill_dir.name: + raise ValueError(f"Skill name {name!r} must match directory {skill_dir.name!r}") + names.append(name) + + for relative_path in BACKTICK_MARKDOWN_PATH.findall(text): + target = (skill_dir / relative_path).resolve() + if not target.is_relative_to(SKILLS_ROOT.resolve()) or not target.is_file(): + raise ValueError(f"Broken Skill reference {relative_path!r} in {skill_path.relative_to(ROOT)}") + + if len(names) != len(set(names)): + raise ValueError("Skill frontmatter names must be unique") + if (SKILLS_ROOT / "_shared" / "SKILL.md").exists(): + raise ValueError("skills/_shared must remain a non-routable reference directory") + + +def validate_tool_reference() -> None: + source_names: set[str] = set() + for path in TOOL_DIR.glob("*.ts"): + source_names.update(TOOL_NAME.findall(path.read_text(encoding="utf-8"))) + + reference_path = SKILLS_ROOT / "google-maps" / "references" / "tools-api.md" + reference_names = REFERENCE_HEADING.findall(reference_path.read_text(encoding="utf-8")) + if len(reference_names) != len(set(reference_names)): + raise ValueError("tools-api.md contains duplicate tool headings") + if set(reference_names) != source_names: + missing = sorted(source_names - set(reference_names)) + extra = sorted(set(reference_names) - source_names) + raise ValueError(f"tools-api.md differs from source tools (missing={missing}, extra={extra})") + + +def main() -> int: + try: + validate_skill_entrypoints() + validate_tool_reference() + except (ValueError, OSError) as exc: + print(f"Skill validation failed: {exc}", file=sys.stderr) + return 1 + + print(f"Google Maps Skills: {len(SKILL_DIRS)} entrypoints and shared references validated") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/google-maps/references/content-attribution.md b/skills/_shared/content-attribution.md similarity index 100% rename from skills/google-maps/references/content-attribution.md rename to skills/_shared/content-attribution.md diff --git a/skills/_shared/setup-and-diagnostics.md b/skills/_shared/setup-and-diagnostics.md new file mode 100644 index 0000000..975b9f1 --- /dev/null +++ b/skills/_shared/setup-and-diagnostics.md @@ -0,0 +1,59 @@ +# Setup and diagnostics + +Use this procedure before the first Google Maps CLI call in a session and when a call fails. Do not start an MCP server: these Skills use the standalone CLI. + +## Local preflight + +Run the local doctor first. It makes no Google API requests and does not print the key: + +```bash +npx -y @cablate/mcp-google-map doctor +``` + +Interpret its JSON checks: + +- `node`: Node.js 18 or newer is required. +- `package`: the npm package and CLI entrypoint were resolved. +- `api-key`: `GOOGLE_MAPS_API_KEY` is visible to the agent process; the value stays hidden. +- `live-api: skip`: expected during local-only diagnosis and not a failure. + +If the key is missing, ask the user to set `GOOGLE_MAPS_API_KEY` in the environment used to launch the agent and start a new session. Prefer the environment variable to `--apikey`; command-line keys may appear in shell history or process listings. + +## Google Cloud prerequisites + +The key must belong to a Google Cloud project with billing enabled and restrictions compatible with the environment. Enable only the APIs needed by the requested tools: + +| Capability | Google Maps Platform API | +|---|---| +| Address geocoding and reverse geocoding | Geocoding API | +| Text search, nearby search, place details, along-route place search, comparisons, local rank tracking | Places API (New) | +| Directions, distance matrix, waypoint optimization, along-route routing | Routes API | +| Elevation | Elevation API | +| Timezone | Time Zone API | +| Weather | Weather API | +| Air quality | Air Quality API | +| Static maps | Maps Static API | + +## Optional live diagnosis + +Only run this after telling the user that it makes billable test requests: + +```bash +npx -y @cablate/mcp-google-map doctor --live +``` + +It independently tests Geocoding API, Places API (New), and Routes API so one disabled capability does not hide the others. It does not validate every optional API in the table. Test an optional capability with the smallest relevant tool call only when the user's task needs it. + +## Failure classification + +| Symptom | Likely cause | Next action | +|---|---|---| +| Package or command cannot be found | Node.js/npm unavailable, registry blocked, or package resolution failed | Repair Node/npm or registry access, then rerun local doctor | +| API key missing | Environment variable was not passed to the agent process | Set the variable and start a new session | +| `REQUEST_DENIED`, permission, or API-disabled error | API disabled, billing unavailable, or key restriction mismatch | Use the failed check name to review that API and the key restrictions in Google Cloud Console | +| `OVER_QUERY_LIMIT`, quota, or rate-limit error | Project quota or billing limit | Stop retries and ask the user to review quota/billing | +| Invalid request | Wrong parameters or unsupported mode | Read the selected tool's parameter reference and correct the request | +| Network or timeout error | Connectivity, proxy, DNS, or transient service issue | Report the observed failure; retry only when reasonable and requested | +| One live check fails while others pass | Capability-specific API configuration | Fix only the named API; do not claim the entire key is invalid | + +Never include the API key in diagnostic output, copied commands, bug reports, or final answers. diff --git a/skills/google-maps-local-seo/SKILL.md b/skills/google-maps-local-seo/SKILL.md new file mode 100644 index 0000000..a2b0d73 --- /dev/null +++ b/skills/google-maps-local-seo/SKILL.md @@ -0,0 +1,47 @@ +--- +name: google-maps-local-seo +description: Analyze Google Business Profile visibility, geographic grid rankings, local search keywords, and nearby competitors through the standalone Google Maps CLI. Use for local SEO or location-strategy decisions, not ordinary place recommendations. +license: MIT +--- + +# Google Maps Local SEO + +Produce a reproducible local-search visibility analysis grounded in Google Maps results. Use the standalone CLI; no MCP connection is required. + +## Before calling the CLI + +- On the first call in a session, or after an execution failure, read `../_shared/setup-and-diagnostics.md` and run the non-billable local preflight. +- The user must supply `GOOGLE_MAPS_API_KEY`. Never expose its value. +- Read `../_shared/content-attribution.md` before presenting reviews, photos, or Google-generated summaries. + +## Workflow + +1. Confirm the target business or `place_id`, service area, relevant keywords, grid size, and decision the user needs to make. +2. Read `references/local-seo.md` before running an audit. +3. Resolve the target with `maps_search_places` and `maps_place_details`; do not assume businesses with similar names are identical. +4. Use `maps_local_rank_tracker` for geographic rankings. Keep the keyword, center, spacing, and grid size with every reported result. +5. Use place search, comparison, and area exploration to identify competitors and evidence for gaps. +6. Separate observed API results from recommendations. Explain ARP, ATRP, SoLV, unfound grid points, and sample limitations. +7. Present prioritized actions tied to the observed evidence; do not promise ranking improvements. + +## CLI + +```bash +npx -y @cablate/mcp-google-map exec '' +``` + +For exact parameters, read the relevant sections of `../google-maps/references/tools-api.md`. + +## Boundaries + +- Ordinary nearby-place discovery and place comparison belong to `google-maps`. +- Consumer travel itineraries belong to `google-maps-travel-planning`. +- Grid results are a sampled snapshot, not an exhaustive or stable representation of Google ranking. +- Do not recommend deceptive reviews, keyword stuffing, impersonation, or other policy-violating tactics. + +## References + +- `references/local-seo.md`: required audit method, competitor analysis, metrics, and reporting structure. +- `../google-maps/references/tools-api.md`: selected tool parameters and response shapes. +- `../_shared/setup-and-diagnostics.md`: first-run and failure recovery. +- `../_shared/content-attribution.md`: Places presentation requirements. diff --git a/skills/google-maps/references/local-seo.md b/skills/google-maps-local-seo/references/local-seo.md similarity index 100% rename from skills/google-maps/references/local-seo.md rename to skills/google-maps-local-seo/references/local-seo.md diff --git a/skills/google-maps-travel-planning/SKILL.md b/skills/google-maps-travel-planning/SKILL.md new file mode 100644 index 0000000..db569bf --- /dev/null +++ b/skills/google-maps-travel-planning/SKILL.md @@ -0,0 +1,48 @@ +--- +name: google-maps-travel-planning +description: Build evidence-backed day trips, multi-day itineraries, and efficient stop sequences with Google Maps data through the standalone CLI. Use when the desired output is a practical travel plan, not merely a route or list of places. +license: MIT +--- + +# Google Maps Travel Planning + +Build a time-ordered itinerary whose geography, travel times, opening constraints, and along-route stops have been checked. Use the standalone CLI; no MCP connection is required. + +## Before calling the CLI + +- On the first call in a session, or after an execution failure, read `../_shared/setup-and-diagnostics.md` and run the non-billable local preflight. +- The user must supply `GOOGLE_MAPS_API_KEY`. Never expose its value. +- Read `../_shared/content-attribution.md` before presenting reviews, photos, or Google-generated summaries. + +## Workflow + +1. Establish destination, dates or duration, fixed anchors, mobility constraints, pace, and relevant preferences. Ask only for omissions that materially change the itinerary. +2. Read `references/travel-planning.md` before designing the itinerary. +3. Use `maps_search_places` to find candidate anchors, then group nearby anchors into one-direction daily arcs. +4. Use `maps_search_along_route` for meals and breaks between anchors. Use `maps_place_details` for hours, ratings, and source metadata. +5. Validate every day with `maps_plan_route` or `maps_directions`. Do not present estimated timing as validated timing. +6. Check weather and air quality when they affect the plan. Respect regional weather limitations described in the reference. +7. When useful, produce a numbered static map after the stop order is final. +8. Present a day-by-day schedule with travel time, visit duration, reservations or opening-hour caveats, and clearly marked fallbacks. + +## CLI + +```bash +npx -y @cablate/mcp-google-map exec '' +``` + +For exact tool parameters, read `../google-maps/references/tools-api.md` only for the tools selected by the plan. + +## Boundaries + +- A request for only A-to-B directions or a simple multi-stop ordering belongs to `google-maps`. +- A request to assess business visibility or local search rankings belongs to `google-maps-local-seo`. +- Do not infer that a venue is accessible, open, safe, or suitable when the API response does not establish it. +- Do not silently substitute web estimates for unavailable Google Maps results; identify the alternate source. + +## References + +- `references/travel-planning.md`: required methodology, timing budgets, geographic arcs, and anti-patterns. +- `../google-maps/references/tools-api.md`: exact parameters and response shapes for selected tools. +- `../_shared/setup-and-diagnostics.md`: first-run and failure recovery. +- `../_shared/content-attribution.md`: Places presentation requirements. diff --git a/skills/google-maps/references/travel-planning.md b/skills/google-maps-travel-planning/references/travel-planning.md similarity index 100% rename from skills/google-maps/references/travel-planning.md rename to skills/google-maps-travel-planning/references/travel-planning.md diff --git a/skills/google-maps/SKILL.md b/skills/google-maps/SKILL.md index 6bd5a7c..5dcedeb 100644 --- a/skills/google-maps/SKILL.md +++ b/skills/google-maps/SKILL.md @@ -1,84 +1,43 @@ --- name: google-maps -description: Use Google Maps-backed geocoding, place search, routing, and geographic analysis through the standalone @cablate/mcp-google-map CLI when a user asks about real-world locations. No MCP connection is required. +description: Search places, resolve addresses, compare routes, inspect neighborhoods, and retrieve geographic or environmental facts through the standalone @cablate/mcp-google-map CLI. Use for concrete location questions; use the travel-planning or local-seo Skill for those specialized outcomes. license: MIT --- -# Google Maps - Geospatial Query Capabilities +# Google Maps -## Overview +Answer real-world location questions with the package's standalone `exec` CLI. Do not start or configure an MCP server for this workflow. -Use the package's `exec` CLI to answer location questions without starting an MCP server. The Skill supplies tool-selection and chaining guidance; the npm package makes the API calls. The user must provide their own Google Maps Platform API key. +## Before calling the CLI ---- - -## Core Principles - -| Principle | Explanation | -|-----------|-------------| -| Chain over single-shot | Most geo questions require 2-5 tool calls chained together. See Scenario Recipes in references/tools-api.md for the full patterns. | -| Match recipe to intent | Map the user's question to a recipe (Trip Planning, Local Discovery, Route Comparison, Neighborhood Analysis, Multi-Stop, Place Comparison, Along the Route) before calling any tool. | -| Precise input saves trouble | Use coordinates over address strings when available. Use place_id over name search. More precise input = more reliable output. | -| Output is structured | Every tool returns JSON. Use it directly for downstream computation or comparison — no extra parsing needed. | -| Present results clearly | Summarize comparisons in a table when it helps; do not pass raw JSON through as the final answer. | -| Preserve source context | Google Maps content has display, attribution, and retention conditions. Read `references/content-attribution.md` before presenting place reviews, photos, or AI summaries. | - ---- - -## Tool Map - -18 tools in five categories — pick by scenario: - -### Place Discovery -| Tool | When to use | Example | -|------|-------------|---------| -| `maps_geocode` | Have an address/landmark, need coordinates | "What are the coordinates of Tokyo Tower?" | -| `maps_reverse_geocode` | Have coordinates, need an address | "What's at 35.65, 139.74?" | -| `maps_search_nearby` | Know a location, find nearby places by type | "Coffee shops near my hotel" | -| `maps_search_places` | Natural language place search | "Best ramen in Tokyo" | -| `maps_place_details` | Have a place_id, need full info (+ optional photo URLs via `maxPhotos`) | "Opening hours and reviews for this restaurant?" | -| `maps_batch_geocode` | Geocode multiple addresses at once (max 50) | "Get coordinates for all these offices" | - -### Routing & Distance -| Tool | When to use | Example | -|------|-------------|---------| -| `maps_directions` | How to get from A to B, including drive-only avoid-tolls/highways controls | "Route from Taipei Main Station to airport avoiding tolls" | -| `maps_distance_matrix` | Compare distances across multiple points, including drive-only avoid-tolls/highways controls | "Which of these 3 hotels is closest to airport without highways?" | -| `maps_search_along_route` | Find places along a route (meals, stops) ranked by detour time | "Restaurants between Fushimi Inari and Kiyomizu-dera" | +- On the first call in a session, or after an execution failure, read `../_shared/setup-and-diagnostics.md` and run the non-billable local preflight. +- The user must supply `GOOGLE_MAPS_API_KEY`. Never print it or place it in a command unless the user explicitly accepts shell-history and process-list exposure. +- Before presenting Places reviews, photos, or AI summaries, read `../_shared/content-attribution.md`. -### Environment -| Tool | When to use | Example | -|------|-------------|---------| -| `maps_elevation` | Query altitude | "Elevation profile along this hiking trail" | -| `maps_timezone` | Need local time at a destination | "What time is it in Tokyo?" | -| `maps_weather` | Weather at a location (current or forecast) | "What's the weather in Paris?" | -| `maps_air_quality` | AQI, pollutants, health recommendations | "Is the air safe for jogging?" | +## Workflow -### Visualization -| Tool | When to use | Example | -|------|-------------|---------| -| `maps_static_map` | Show locations/routes on a map image | "Show me these places on a map" | +1. Identify the concrete location outcome and missing inputs. Do not request details that can be resolved by geocoding or place search. +2. Read `references/tools-api.md` when exact parameters, response fields, or a multi-tool recipe are needed. +3. Prefer coordinates over ambiguous addresses and `place_id` over repeated name searches. +4. Chain only the calls needed for the outcome. Reuse returned identifiers and coordinates. +5. Treat nonzero exit status or `{ "success": false }` as a failure, not as geographic data. +6. Summarize the useful result rather than returning raw JSON. Preserve source and attribution metadata where required. -### Composite (one-call shortcuts) -| Tool | When to use | Example | -|------|-------------|---------| -| `maps_explore_area` | Overview of a neighborhood | "What's around Tokyo Tower?" | -| `maps_plan_route` | Multi-stop optimized itinerary (Routes API waypoint optimization, up to 25 stops) with drive-only avoid-tolls/highways controls | "Visit these 5 places efficiently without tolls" | -| `maps_compare_places` | Side-by-side comparison | "Which ramen shop near Shibuya?" | -| `maps_local_rank_tracker` | Local SEO grid rank tracking | "How does this dentist rank across the area?" | +## Tool selection ---- - -## Known API Limitations - -| Tool | Limitation | Workaround | -|------|-----------|------------| -| `maps_weather` | Unsupported regions: Japan, China, South Korea, Cuba, Iran, North Korea, Syria | Use web search for weather in these regions | -| `maps_distance_matrix` | Transit mode may return null in some regions | Fall back to `driving` or `walking` mode, or use `maps_directions` for transit | -| `maps_plan_route` | Transit mode does not support waypoint optimization | Set `optimize: false` for transit mode | -| `maps_air_quality` | Works globally including Japan (unlike weather) | — | - ---- +| Outcome | Preferred tools | +|---|---| +| Address or landmark to coordinates | `maps_geocode` | +| Coordinates to address | `maps_reverse_geocode` | +| Nearby discovery | `maps_search_nearby`, then `maps_place_details` | +| Natural-language place search | `maps_search_places`, then `maps_place_details` | +| Compare candidate places | `maps_compare_places` or search → details → distance matrix | +| Route or travel-mode comparison | `maps_directions`, `maps_distance_matrix` | +| Multi-stop route without a broader itinerary | `maps_plan_route` | +| Things along a route | `maps_search_along_route` | +| Neighborhood facts | `maps_explore_area` plus targeted nearby searches | +| Elevation, timezone, weather, or air quality | corresponding environment tool; geocode first when needed | +| Visual map | `maps_static_map` after locations or a route are known | ## Invocation @@ -86,28 +45,18 @@ Use the package's `exec` CLI to answer location questions without starting an MC npx -y @cablate/mcp-google-map exec '' ``` -- **API Key**: Set `GOOGLE_MAPS_API_KEY` in the environment. The `-k` flag also works, but may expose the key in shell history or process listings. If no key is available, ask the user to configure one; do not invent results. -- **Output**: Successful calls return JSON on stdout. A failed call exits nonzero with error details on stderr; report the failure rather than treating it as map data. -- **Stateless**: each call is independent -- **Tool names**: CLI accepts both `maps_geocode` and `geocode` short forms +The CLI accepts both MCP-style names such as `maps_geocode` and short names such as `geocode`. Each call is stateless and returns JSON on stdout. ---- - -## When to Update This Skill +## Boundaries -| Trigger | What to update | -|---------|----------------| -| New tool added to the package | Tool Map table + references/tools-api.md | -| Tool parameters changed | references/tools-api.md | -| New chaining pattern discovered in practice | references/tools-api.md chaining section | - ---- +- For a multi-day itinerary, trip schedule, or travel-day optimization, use `google-maps-travel-planning` instead. +- For Google Business Profile visibility, grid ranking, keyword coverage, or competitor audits, use `google-maps-local-seo` instead. +- `maps_weather` is unavailable in Japan, China, South Korea, Cuba, Iran, North Korea, and Syria; use a current web source there. +- Transit waypoint optimization is unsupported; use `optimize: false` for transit routes. +- Geographic proximity and elevation alone do not establish safety, accessibility, flood risk, or suitability. State those limitations when relevant. -## Reference +## References -| File | Content | When to read | -|------|---------|--------------| -| `references/tools-api.md` | Full parameter specs, response formats, 7 scenario recipes, and decision guide | When you need exact parameters, response shapes, or multi-tool workflow patterns | -| `references/travel-planning.md` | Travel planning methodology — 6-layer model, Search Along Route, anti-patterns | When planning multi-day trips — **read before Recipe 1** | -| `references/local-seo.md` | Local SEO / Google Business Profile ranking analysis — competitor audit, keyword landscape, gap analysis | When analyzing business rankings, comparing competitors, or scouting locations | -| `references/content-attribution.md` | Google Maps content attribution, source links, AI disclosure, and storage limits | Before presenting reviews, photos, or AI summaries from Places | +- `references/tools-api.md`: read for exact parameters, response shapes, and generic chaining recipes. +- `../_shared/setup-and-diagnostics.md`: read for first-run checks and error classification. +- `../_shared/content-attribution.md`: read before displaying regulated Google Maps content. diff --git a/skills/google-maps/SKILL.skill b/skills/google-maps/SKILL.skill deleted file mode 100644 index 8efa885..0000000 Binary files a/skills/google-maps/SKILL.skill and /dev/null differ diff --git a/skills/google-maps/references/tools-api.md b/skills/google-maps/references/tools-api.md index e02245c..726e153 100644 --- a/skills/google-maps/references/tools-api.md +++ b/skills/google-maps/references/tools-api.md @@ -156,7 +156,7 @@ Response includes (when available from Google): | `reviews[].author_uri`, `author_photo_uri`, `google_maps_uri` | string | Author profile/avatar and individual review source | | `photos[].author_attributions`, `google_maps_uri` | object[], string | Photo credits and individual photo source, returned with `maxPhotos > 0` | -Before showing reviews, photos, or AI summaries to end users, read [`content-attribution.md`](./content-attribution.md). The old legacy-review merge was removed because it could not supply a per-review Google Maps source link; reviews now come from Places API (New) only. +Before showing reviews, photos, or AI summaries to end users, read [`content-attribution.md`](../../_shared/content-attribution.md). The old legacy-review merge was removed because it could not supply a per-review Google Maps source link; reviews now come from Places API (New) only. --- @@ -494,7 +494,7 @@ Use these recipes when the user's question maps to a multi-step workflow. Think This is the most common complex scenario. The goal is a time-ordered itinerary with routes between stops. -> **Read `references/travel-planning.md` first** — it contains the full methodology, anti-patterns, and time budget guidelines. +> This workflow belongs to the `google-maps-travel-planning` Skill. Read its `references/travel-planning.md` before planning an itinerary. **Steps:** 1. `maps_search_places` — Search "top attractions in {city}" → geographically diverse **anchor points** diff --git a/src/cli.ts b/src/cli.ts index d14032e..c2bcbb3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -116,6 +116,21 @@ interface ExecStreams { type ExecToolRunner = (toolName: string, params: any, apiKey: string) => Promise; +export type DoctorCheck = { + name: string; + status: "pass" | "fail" | "skip"; + detail: string; +}; + +export type DoctorReport = { + success: boolean; + version: string; + live: boolean; + checks: DoctorCheck[]; +}; + +type DoctorToolRunner = (toolName: string, params: any, apiKey: string) => Promise; + function isExecFailureResponse(result: unknown): result is ExecFailureResponse { return ( typeof result === "object" && @@ -297,6 +312,98 @@ export async function runExecCommand( } } +/** + * Check whether the standalone CLI is ready for Skill-driven use. Local mode + * performs no Google API requests. Live mode makes three small requests so it + * can distinguish Geocoding, Places (New), and Routes API availability. + */ +export async function runDoctor( + packageVersion: string, + apiKey: string | undefined, + live = false, + runner: DoctorToolRunner = execTool +): Promise { + const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10); + const checks: DoctorCheck[] = [ + { + name: "node", + status: nodeMajor >= 18 ? "pass" : "fail", + detail: `Node.js ${process.versions.node} detected; version 18 or newer is required.`, + }, + { + name: "package", + status: packageVersion !== "0.0.0" ? "pass" : "fail", + detail: + packageVersion !== "0.0.0" + ? `@cablate/mcp-google-map ${packageVersion} is available.` + : "Could not read the installed package version.", + }, + { + name: "api-key", + status: apiKey ? "pass" : "fail", + detail: apiKey + ? "GOOGLE_MAPS_API_KEY is configured (value hidden)." + : "GOOGLE_MAPS_API_KEY is not configured. Set it in the environment before making API calls.", + }, + ]; + + if (!live) { + checks.push({ + name: "live-api", + status: "skip", + detail: "Live API checks were not requested; no Google Maps API calls were made.", + }); + } else if (!apiKey) { + checks.push({ + name: "live-api", + status: "skip", + detail: "Live API checks require GOOGLE_MAPS_API_KEY.", + }); + } else { + const liveChecks = [ + { name: "geocoding-api", tool: "geocode", params: { address: "Tokyo Tower" } }, + { name: "places-api-new", tool: "search-places", params: { query: "Tokyo Tower" } }, + { + name: "routes-api", + tool: "directions", + params: { origin: "Tokyo Tower", destination: "Tokyo Station", mode: "walking" }, + }, + ]; + + for (const check of liveChecks) { + try { + const result = await runner(check.tool, check.params, apiKey); + if (isExecFailureResponse(result)) { + checks.push({ + name: check.name, + status: "fail", + detail: getExecFailureMessage(check.tool, result), + }); + } else { + checks.push({ + name: check.name, + status: "pass", + detail: `${check.name} request succeeded.`, + }); + } + } catch (error: unknown) { + checks.push({ + name: check.name, + status: "fail", + detail: error instanceof Error ? error.message : String(error), + }); + } + } + } + + return { + success: checks.every((check) => check.status !== "fail"), + version: packageVersion, + live, + checks, + }; +} + // --------------- Entry Point --------------- // Check if this script is being run directly @@ -320,6 +427,32 @@ if (isRunDirectly || isMainModule) { } yargs(hideBin(process.argv)) + .command( + "doctor", + "Check standalone CLI readiness without starting an MCP server", + (yargs) => + yargs + .option("live", { + type: "boolean", + default: false, + description: "Make billable test requests to Geocoding, Places (New), and Routes APIs", + }) + .option("apikey", { + alias: "k", + type: "string", + description: "Google Maps API key (prefer GOOGLE_MAPS_API_KEY to avoid shell history exposure)", + default: process.env.GOOGLE_MAPS_API_KEY, + }) + .example([ + ["$0 doctor", "Check local runtime and API key configuration without network calls"], + ["$0 doctor --live", "Also verify three Google Maps APIs with billable requests"], + ]), + async (argv) => { + const report = await runDoctor(packageVersion, argv.apikey as string | undefined, argv.live as boolean); + process.stdout.write(JSON.stringify(report, null, 2) + "\n"); + process.exitCode = report.success ? 0 : 1; + } + ) .command( "exec [params]", "Execute a tool directly and output JSON", diff --git a/tests/cliExec.unit.test.ts b/tests/cliExec.unit.test.ts index 13fde79..d797fdc 100644 --- a/tests/cliExec.unit.test.ts +++ b/tests/cliExec.unit.test.ts @@ -7,7 +7,7 @@ import { PlacesSearcher } from "../src/services/PlacesSearcher.js"; // does not start an HTTP server as a side effect. const originalArgv1 = process.argv[1]; process.argv[1] = "cli-exec-unit-test"; -const { EXEC_TOOLS, execTool, runExecCommand } = await import("../src/cli.js"); +const { EXEC_TOOLS, execTool, runExecCommand, runDoctor } = await import("../src/cli.js"); process.argv[1] = originalArgv1; interface CapturedOutput { @@ -121,6 +121,44 @@ test("exec mode exposes all 18 short tool names", () => { ); }); +test("doctor performs local checks without calling Google APIs", async () => { + let calls = 0; + const report = await runDoctor("1.2.3", "configured-key", false, async () => { + calls++; + return { success: true }; + }); + + assert.equal(report.success, true); + assert.equal(report.live, false); + assert.equal(calls, 0); + assert.equal(report.checks.find((check) => check.name === "api-key")?.status, "pass"); + assert.equal(report.checks.find((check) => check.name === "live-api")?.status, "skip"); +}); + +test("doctor fails safely when the API key is missing", async () => { + const report = await runDoctor("1.2.3", undefined, false); + + assert.equal(report.success, false); + assert.equal(report.checks.find((check) => check.name === "api-key")?.status, "fail"); + assert.equal(JSON.stringify(report).includes("configured-key"), false); +}); + +test("doctor live mode checks geocoding, places, and routes independently", async () => { + const calledTools: string[] = []; + const report = await runDoctor("1.2.3", "secret-key", true, async (toolName) => { + calledTools.push(toolName); + if (toolName === "search-places") return { success: false, error: "Places API (New) is disabled" }; + return { success: true }; + }); + + assert.deepEqual(calledTools, ["geocode", "search-places", "directions"]); + assert.equal(report.success, false); + assert.equal(report.checks.find((check) => check.name === "geocoding-api")?.status, "pass"); + assert.equal(report.checks.find((check) => check.name === "places-api-new")?.status, "fail"); + assert.equal(report.checks.find((check) => check.name === "routes-api")?.status, "pass"); + assert.equal(JSON.stringify(report).includes("secret-key"), false); +}); + test("execTool maps short and MCP geocode aliases to the same service", async () => { const originalGeocode = PlacesSearcher.prototype.geocode; const addresses: string[] = []; diff --git a/tests/pluginPackage.unit.test.ts b/tests/pluginPackage.unit.test.ts index 5f3e4b5..3ba8636 100644 --- a/tests/pluginPackage.unit.test.ts +++ b/tests/pluginPackage.unit.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import test from "node:test"; interface PluginManifest { @@ -43,6 +43,22 @@ test("plugin package is Skill-only and versioned with the npm package", () => { assert.ok(packageJson.files.includes(".codex-plugin")); }); +test("plugin exposes three focused Skills with shared diagnostics", () => { + const skillNames = ["google-maps", "google-maps-travel-planning", "google-maps-local-seo"]; + + for (const skillName of skillNames) { + const skillUrl = new URL(`../skills/${skillName}/SKILL.md`, import.meta.url); + assert.equal(existsSync(skillUrl), true, `${skillName} entrypoint exists`); + const contents = readFileSync(skillUrl, "utf8"); + assert.match(contents, new RegExp(`^name: ${skillName}$`, "m")); + assert.match(contents, /\.\.\/_shared\/setup-and-diagnostics\.md/); + } + + assert.equal(existsSync(new URL("../skills/_shared/SKILL.md", import.meta.url)), false); + assert.equal(existsSync(new URL("../skills/_shared/setup-and-diagnostics.md", import.meta.url)), true); + assert.equal(existsSync(new URL("../skills/_shared/content-attribution.md", import.meta.url)), true); +}); + test("CabLate marketplace installs the published npm plugin", () => { const marketplace = readJson(".agents/plugins/marketplace.json"); const [plugin] = marketplace.plugins;