From 284ea07cb7186febd7bc1bb8859134f29172394c Mon Sep 17 00:00:00 2001 From: Eric Rodriguez Date: Thu, 9 Jul 2026 17:48:35 +0200 Subject: [PATCH] docs: MCP guide + agent-story refresh, and correct stale content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fills the narrative gaps the v0.19-v0.22 releases left (the reference is auto-generated; these are the hand-written pages). New / rewritten: - guides/mcp.mdx (new): the flagship `pdcli mcp serve` guide — read-only by default, the curated vs --allow-writes vs --topics/--all-tools tool model, the excluded surface, --tool-timeout, and claude-mcp-add / .mcp.json setup. Wired into a new "AI agents" sidebar group. - start/agents.mdx: adds an MCP section, exit rows 3 (lookup) and 8 (watch), lookup as the read-only create-vs-update branch, and the opt-in `deal context --mail` note. - Home.astro: the "Built for AI agents" card now leads with mcp serve; hero animation gains an `mcp serve` frame. Corrections: - guides/api.mdx: documents `api --paginate` (the old "does not auto-paginate" claim was false since v0.20), incl. the --jq shape shift. - start/distribution.mdx + automation/github-actions.mdx: version pins 0.20.0 -> 0.22.0. - concepts/api-model.mdx: `mail` added to the v1 topic row; `pdcli quota` introduced in the token-budget section (disambiguated from the sales "quota" metric). - guides/bulk.mdx: a `lookup ... && update || create` (exit 3) example. Claude-Session: https://claude.ai/code/session_018dRMUUj9vpKp3tqKQFsvE9 --- website/astro.config.mjs | 6 + website/src/components/Home.astro | 7 +- .../docs/automation/github-actions.mdx | 6 +- .../src/content/docs/concepts/api-model.mdx | 9 +- website/src/content/docs/guides/api.mdx | 14 +- website/src/content/docs/guides/bulk.mdx | 18 +++ website/src/content/docs/guides/mcp.mdx | 130 ++++++++++++++++++ website/src/content/docs/start/agents.mdx | 27 +++- .../src/content/docs/start/distribution.mdx | 14 +- 9 files changed, 215 insertions(+), 16 deletions(-) create mode 100644 website/src/content/docs/guides/mcp.mdx diff --git a/website/astro.config.mjs b/website/astro.config.mjs index b385118..e3b3c69 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -85,7 +85,13 @@ export default defineConfig({ { label: 'Installation', slug: 'start/installation' }, { label: 'Distribution (all channels)', slug: 'start/distribution' }, { label: 'Quickstart', slug: 'start/quickstart' }, + ], + }, + { + label: 'AI agents', + items: [ { label: 'Quickstart for AI agents', slug: 'start/agents' }, + { label: 'MCP server', slug: 'guides/mcp' }, ], }, { diff --git a/website/src/components/Home.astro b/website/src/components/Home.astro index 97a8218..59b6717 100644 --- a/website/src/components/Home.astro +++ b/website/src/components/Home.astro @@ -61,7 +61,7 @@ const features: { t: string; b: string; d: string }[] = [ }, { t: 'Built for AI agents', - b: 'Self-documenting --help on every command and clean JSON output make pdcli easy for agents to discover and drive — a ready-made tool surface for your LLM workflows.', + b: 'Runs as an MCP server — pdcli mcp serve — read-only by default, giving Claude and other hosts a safe, typed tool surface. For shell-driven agents there is also self-documenting --help, clean JSON, and deterministic exit codes.', d: '', }, { @@ -456,6 +456,11 @@ const showcaseTerm = `$ pdcli +212 new ~18 updates !3 conflicts dry run · nothing written · custom fields matched by name`, }, + { + cmd: 'pdcli mcp serve', + out: ` pdcli MCP server ready — 45 tools (read-only) + connect Claude · --allow-writes to expose create/update`, + }, ]; const cmdEl = document.getElementById('clr-aterm-cmd'); diff --git a/website/src/content/docs/automation/github-actions.mdx b/website/src/content/docs/automation/github-actions.mdx index 5accd21..01ee6c0 100644 --- a/website/src/content/docs/automation/github-actions.mdx +++ b/website/src/content/docs/automation/github-actions.mdx @@ -48,7 +48,7 @@ jobs: - uses: actions/setup-node@v5 with: node-version: 20 - - run: npm install -g @wavyx/pdcli@0.20.0 + - run: npm install -g @wavyx/pdcli@0.22.0 # Preflight: config + keychain checks, zero network egress. Exits 78 on a # config problem, failing the job before it spends any API budget. @@ -73,7 +73,7 @@ vars. jobs: deals: runs-on: ubuntu-latest - container: ghcr.io/wavyx/pdcli:0.20.0 + container: ghcr.io/wavyx/pdcli:0.22.0 env: PDCLI_COMPANY_DOMAIN: acme PDCLI_API_TOKEN: ${{ secrets.PIPEDRIVE_API_TOKEN }} @@ -82,7 +82,7 @@ jobs: - run: pdcli deal list --status open --output json --jq '.[].id' ``` -Pin the tag (`:0.20.0`) rather than `latest` for reproducible runs. See +Pin the tag (`:0.22.0`) rather than `latest` for reproducible runs. See [Distribution](/pdcli/start/distribution/) for the image details. ## Gate on the exit code diff --git a/website/src/content/docs/concepts/api-model.mdx b/website/src/content/docs/concepts/api-model.mdx index d928eb9..ed16e47 100644 --- a/website/src/content/docs/concepts/api-model.mdx +++ b/website/src/content/docs/concepts/api-model.mdx @@ -13,7 +13,7 @@ Pipedrive runs two API versions. `pdcli` routes each topic to the right one. | Topic | API | | ----- | --- | | deals, persons, organizations, products, pipelines, stages, activities, projects, fields, search | **v2** | -| leads, notes, files, filters, webhooks, goals, users | **v1** | +| leads, notes, files, filters, webhooks, goals, users, mail | **v1** | Core CRM is on **v2** because Pipedrive deprecated ~59 v1 core endpoints after 2025-12-31, so pdcli never builds core CRUD on v1. The v1-only topics above have no v2 equivalent yet and are @@ -62,6 +62,13 @@ seats, resets at midnight server time). On a `429`, pdcli reads `x-ratelimit-res (falling back to `Retry-After`, then a 2s default) to decide how long to wait before retrying. +Inspect the remaining budget with **`pdcli quota`** (alias `ratelimit`): it reports the +daily token budget remaining/limit and gates CI with `--min`/`--threshold` (exit `75` when +too low), so a batch can check headroom first — `pdcli quota --min 5000 && pdcli sync +warehouse`. The budget is **company-wide** (shared across every integration on the +account), so treat the reading as a hint, not a reservation. (This is unrelated to the +sales-goal "quota" in the [analytics guide](/pdcli/guides/analytics/).) + ### Backoff and the hard stop On a `429`, pdcli waits for the window indicated by `x-ratelimit-reset` (falling back to diff --git a/website/src/content/docs/guides/api.mdx b/website/src/content/docs/guides/api.mdx index fdd1ee8..cc5df70 100644 --- a/website/src/content/docs/guides/api.mdx +++ b/website/src/content/docs/guides/api.mdx @@ -70,9 +70,17 @@ When you hit v2 endpoints directly, remember the v2 conventions: - **Bodies are JSON only.** v2 doesn't take form-encoded data. - Lists use **cursor** pagination (`cursor`/`limit`, `limit` max 500); v1 lists use **offset** pagination (`start`/`limit`). See - [How pdcli talks to Pipedrive](/pdcli/concepts/api-model/). `pdcli api` makes a single - request — it does **not** auto-paginate — so pass `cursor`/`start` yourself for more - pages. + [How pdcli talks to Pipedrive](/pdcli/concepts/api-model/). By default `pdcli api` + makes a single request; add **`--paginate`** (alias `--all`) to follow every page and + print one concatenated array. It works on `GET` only and infers the pager from the + path (`/api/v1/` offset vs `/api/v2/` cursor); `--limit` caps the total. Note the shape + shift for `--jq`: with `--paginate` you filter the bare item array (`.[]`), without it + the full envelope (`.data[]`). + +```bash +# every open deal across all pages, as one array +pdcli api GET "/api/v2/deals?status=open" --paginate --jq '.[].id' +``` ## When to reach for it diff --git a/website/src/content/docs/guides/bulk.mdx b/website/src/content/docs/guides/bulk.mdx index ea5c18a..58a8468 100644 --- a/website/src/content/docs/guides/bulk.mdx +++ b/website/src/content/docs/guides/bulk.mdx @@ -221,3 +221,21 @@ is indexed. For repeatable sync, key on a stable external ID and let the index s between runs; a periodic `backup diff` or a search will surface any duplicates that slip through. ::: + +### Just checking: `lookup` + +When a script wants to **branch on existence itself** rather than have pdcli decide, use +`lookup` — a read-only exact-match resolver. It exits `0` with the matching row(s) when +found and **`3`** when nothing matches, so create-vs-update becomes a plain shell branch: + +```bash +if pdcli lookup deal --field "PO Number" --value PO-1234 --first --jq .id >/tmp/id; then + pdcli deal update "$(cat /tmp/id)" --body '{"value":5000}' # found → update +else + pdcli deal create --body '{"title":"PO-1234","value":5000}' # exit 3 → create +fi +``` + +It resolves a human field name to its custom-field key for you (deal, person, org, product, +lead). Matching is **case-sensitive**, and — like upsert — the search index is eventually +consistent, so a `lookup`-then-`create` loop can still double-create in fast pipelines. diff --git a/website/src/content/docs/guides/mcp.mdx b/website/src/content/docs/guides/mcp.mdx new file mode 100644 index 0000000..b57d460 --- /dev/null +++ b/website/src/content/docs/guides/mcp.mdx @@ -0,0 +1,130 @@ +--- +title: MCP server +description: Expose pdcli to Claude and other AI hosts as a Model Context Protocol server — read-only by default, with writes gated behind an explicit flag. +--- + +`pdcli mcp serve` runs pdcli as a [Model Context Protocol](https://modelcontextprotocol.io) +(MCP) server over stdio, so an MCP host — Claude Desktop, Claude Code, or any other +client — can drive your Pipedrive account as a set of typed tools. + +It is the same CLI you already trust, wrapped in a tool surface. Every tool call +re-invokes `pdcli` itself as a child process under your auth profile, so the tools +honor the same host-lock, the same keychain credentials, and the same rate-limit +handling as the commands you run by hand. Nothing new touches your token. + +## Safe by default + +The design goal is that connecting the server can't hurt you: + +- **Read-only out of the box.** The default tool set is a curated **45 read-only + tools** — core entity list/get, `search`, deal intelligence + (`deal context`/`history`/`summary`), every metric, `funnel`, `digest`, `audit`, + `rep scorecard`, `user me`, and more. No tool that writes is even registered + unless you ask for it. +- **Writes are opt-in.** Every command is classified `read`, `write`, or + `destructive`. Writes and destructive operations are exposed **only** under + `--allow-writes`. +- **Dangerous surfaces are excluded entirely** — they never appear as tools, even + with `--all-tools`. That includes the raw `api` escape hatch, `auth:*`, + `config:*`, `alias:*`, `profile:*` (all of which manage your local machine, not + CRM data), `doctor`, `watch`, `changes` (advances a stateful watermark), + `sync warehouse`, `backup` (but `backup diff`, a zero-API local read, is kept), + `webhook listen`, and `mcp serve` itself. + +## Quick start + +Register the server with Claude Code in one line: + +```bash +claude mcp add pipedrive -- pdcli mcp serve +``` + +Or add it to an `.mcp.json` (Claude Code project config, Claude Desktop, or any +MCP host) by hand: + +```json +{ + "mcpServers": { + "pipedrive": { + "command": "pdcli", + "args": ["mcp", "serve"] + } + } +} +``` + +That's the read-only server. To let the host create and update records, add the +flag to the `args` — `["mcp", "serve", "--allow-writes"]` — or to the +`claude mcp add` command after the `--`. + +Authenticate pdcli once (`pdcli auth login`, or an env-var token) before starting a +host; the server itself needs no credentials — its child processes resolve them. + +## The tool model + +Tool selection happens in two stages: **scope**, then the **write gate**. + +**Scope** decides which commands are candidates: + +| Flag | Tools exposed | +| ----------------------- | ------------------------------------------------------------------- | +| _(none)_ | the curated core set — 45 read tools, small enough not to overwhelm a host | +| `--topics deal,person` | every command under those topics, instead of the curated set | +| `--all-tools` | every non-excluded command | + +**The write gate** then filters that scope: reads are always exposed; `write` and +`destructive` tools appear only with `--allow-writes`. With the default scope, +`--allow-writes` adds ~14 core write tools — the create/update commands on core +entities plus the idempotent `upsert`s (`person`/`org`/`deal upsert`). + +```bash +pdcli mcp serve # curated, read-only (default) +pdcli mcp serve --allow-writes # curated reads + core writes +pdcli mcp serve --topics deal,person,org # everything under those topics (reads) +pdcli mcp serve --all-tools --allow-writes # the whole CLI as tools +``` + +One command is deliberately kept out of the curated default even though it's a +read: `lookup`. Its exit-3 "no match" result is a normal branch for a script, but +an MCP host reads a non-zero exit as a **tool error** — noisy for an agent. It +stays reachable via `--all-tools`; for match-or-branch logic inside an agent, +prefer `search` or the `upsert` tools. + +## Custom-field names, not hash keys + +Every tool call forces `--resolve-fields`, so the host sees human-readable custom +field **names** (and option labels) instead of Pipedrive's 40-character hash keys — +on both input and output. An agent can ask for `"Renewal date"`, not +`"a1b2c3…"`. Each call also forces `--output=json` (so the parent's stdio channel +stays clean) and `--yes` (no interactive confirm can block a headless call). + +## Timeouts and limits + +Each tool call is a child process with guardrails: + +- `--tool-timeout ` (default **120**) bounds how long a call may run + before its child is terminated (SIGTERM, escalating to SIGKILL). +- Output is capped at **16 MB** across stdout and stderr combined; a runaway + call is killed rather than flooding the host. + +Both a timeout and an overflow surface to the host as a tool error with a +self-describing message, never as a silent success. + +## MCP vs. the CLI over bash + +If your agent already has a shell — a terminal agent like Claude Code or Codex — +you may not need MCP at all. pdcli is built to be driven from bash directly: +`--output json`, `--jq`, self-describing `--help`, and +[deterministic exit codes](/pdcli/start/agents/) give a shell-capable agent +everything it needs, with the full command set and no tool-registration step. + +Reach for `mcp serve` when the host **can't** run shell commands — Claude Desktop +and other GUI/chat hosts that speak MCP but have no terminal. There, the MCP server +turns pdcli into first-class typed tools with the same safety posture you'd get on +the command line. + +:::tip +Start read-only. Add `--allow-writes` only once you trust the workflow, and prefer +scoping with `--topics` over `--all-tools` so the host sees a focused, relevant set +of tools rather than the entire CLI. +::: diff --git a/website/src/content/docs/start/agents.mdx b/website/src/content/docs/start/agents.mdx index 6cc3716..d524abe 100644 --- a/website/src/content/docs/start/agents.mdx +++ b/website/src/content/docs/start/agents.mdx @@ -7,6 +7,23 @@ pdcli is built to be driven by agents (Claude Code, Codex, CI bots). It is self-describing, emits machine-readable JSON, and uses deterministic exit codes. This page covers the conventions an agent needs. +## Expose pdcli over MCP + +If your host speaks the [Model Context Protocol](https://modelcontextprotocol.io) — +Claude Desktop, Claude Code, and other GUI or chat clients — pdcli can register as +an MCP server and hand it a set of typed tools, read-only by default. Register it +with Claude Code in one line: + +```bash +claude mcp add pipedrive -- pdcli mcp serve +``` + +Everything below still applies: the MCP tools re-invoke this same CLI under your +auth profile, with the same host-lock and exit-code semantics. If your agent has a +shell, driving pdcli from bash (as this page describes) is often simpler than MCP. +See the [MCP server guide](/pdcli/guides/mcp/) for the full tool model, the write +gate, and scoping flags. + ## Authenticate with environment variables Avoid the interactive `auth login` flow. Set credentials in the environment so no @@ -44,6 +61,8 @@ script can react to the failure class without parsing text: | ---- | ------------------------------------------------------------------------------------------------- | | 0 | Success | | 1 | Generic error | +| 3 | `lookup`: no record matched — not a failure; branch to create/upsert | +| 8 | `watch`: new findings since the last run — the trigger for `pdcli watch \|\| notify` | | 64 | Usage / bad flags or arguments (unknown flag, missing arg, invalid value, missing CSV column) | | 65 | Bad input data (API 400 / 422) | | 69 | Service unavailable (API 5xx, or unreachable) | @@ -90,7 +109,8 @@ performance, `digest` for the whole Monday packet in one fetch (`--format md|htm a cron → Slack/email artifact), `changes` for an incremental cross-entity change feed with a self-advancing watermark (no receiver to host, unlike webhooks), `deal context ` for a one-call denormalized, prompt-ready bundle (deal + person + org + activities + notes + -products + flags), `backup diff` for a zero-API field-level diff of two snapshots, `watch` for +products + flags; add `--mail` to fold in a mail summary — opt-in, since it needs the +`mail:read` scope and email sync), `backup diff` for a zero-API field-level diff of two snapshots, `watch` for an exit-code-gated anomaly poller that fires only on findings new since the last run (`pdcli watch || notify`), `sync warehouse` for an incremental NDJSON export with per-entity high-water marks, and `--updated-since` on the list commands for incremental polling. @@ -102,6 +122,11 @@ writes the wrong one. `person import`/`org import --upsert --match-on ` a match-or-create per CSV row, reporting created/updated/unchanged counts. Pair upsert with an external key (a custom field carrying your system's ID) for clean, repeatable sync. +When you'd rather branch on existence yourself before deciding to create or update, `lookup` +is the lighter read-only probe: it finds a record by a field value and exits **0 when found** +(printing it) or **3 when nothing matched**, so `pdcli lookup person --field email --value … || +pdcli person create …` is a clean create-if-missing pattern that never mutates on its own. + ## The host-locked `api` escape hatch When no dedicated command exists, call any endpoint directly. The request is diff --git a/website/src/content/docs/start/distribution.mdx b/website/src/content/docs/start/distribution.mdx index a3ce2eb..277cced 100644 --- a/website/src/content/docs/start/distribution.mdx +++ b/website/src/content/docs/start/distribution.mdx @@ -56,7 +56,7 @@ pdcli version Pin a version in CI so a background release can't change behaviour mid-pipeline: ```bash -npm install -g @wavyx/pdcli@0.20.0 +npm install -g @wavyx/pdcli@0.22.0 ``` Auth: keychain by default (`pdcli auth login`), or env vars for scripts. @@ -68,7 +68,7 @@ step where you'd rather not add an install line. ```bash npx @wavyx/pdcli deal list -npx @wavyx/pdcli@0.20.0 deal list # pinned +npx @wavyx/pdcli@0.22.0 deal list # pinned ``` The global install is faster for daily use because npx resolves and caches the package @@ -91,7 +91,7 @@ token never appears in the command or in shell history. Pin the tag in CI: ```bash docker run --rm -e PDCLI_API_TOKEN -e PDCLI_COMPANY_DOMAIN \ - ghcr.io/wavyx/pdcli:0.20.0 deal list --output json + ghcr.io/wavyx/pdcli:0.22.0 deal list --output json ``` The entrypoint is `pdcli`, so everything after the image name is passed straight @@ -141,23 +141,23 @@ Five targets are published per release: | `win32-x64` | Windows, 64-bit | Each tarball filename embeds the build's commit SHA (for example -`pdcli-v0.20.0-1a2b3c4-linux-x64.tar.gz`), so there is no fixed, predictable +`pdcli-v0.22.0-1a2b3c4-linux-x64.tar.gz`), so there is no fixed, predictable download URL. Grab the exact asset from the [Releases page](https://github.com/wavyx/pdcli/releases), or let the [`gh` CLI](https://cli.github.com/) resolve it for you: ```bash # See the tarballs attached to a release: -gh release view v0.20.0 --json assets --jq '.assets[].name' +gh release view v0.22.0 --json assets --jq '.assets[].name' # Download the one for your target by pattern (no need to know the SHA): -gh release download v0.20.0 --pattern '*linux-x64.tar.gz' +gh release download v0.22.0 --pattern '*linux-x64.tar.gz' ``` Then unpack it and put the `bin` directory on your `PATH`: ```bash -tar -xzf pdcli-v0.20.0-*-linux-x64.tar.gz +tar -xzf pdcli-v0.22.0-*-linux-x64.tar.gz ./pdcli/bin/pdcli version ```