# cortex-gateway MCP Tool Reference

> The map of what Claude can actually do in this workspace. When a task seems blocked, the answer is usually
> another tool here — not "the operator should do it manually in Ninjo". Read this with
> `knowledge/self-sufficiency.md` (what to do when a call fails).

## Ground rules (apply to every call)

- **`reason` on every call.** Every cortex-gateway tool takes a `reason` — one honest sentence in the user's
  language tying the call to what they asked. It's tracing metadata only (never changes behavior) but it's the
  team's only window into tool usage. Be specific, not boilerplate.
- **Surface.** The native OAuth connection points at the full `/mcp` surface, so the **whole** lifecycle is
  available — including `create_agent` / `delete_agent`. If one of those appears missing, the fix is the connection
  (`.mcp.json` / re-auth via `/mcp`), not your DB role.
- **`get_me` reads identity, not a capability ceiling.** It returns `influencer_id` (active for this request),
  `user_id`, `scope` (`single-influencer` | `multi-influencer` | `admin`), `available_influencer_ids` (the
  influencer(s) this token may act on), and `influencers` (a map `influencer_id → { handle, name }`). It does **not**
  return a DB role, and nothing in `get_me` gates these tools — what you can do is set by the `/mcp` surface being
  exposed, not by any role. So never read `get_me` as "I can't do this". Use `influencers` to identify a creator by
  **handle** in conversation, but always pass the **`influencer_id`** to the tools (the handle is not unique).
- **Influencer scope.** A `scope: "single-influencer"` token is pinned to one influencer (inferred from the JWT) — you
  pass nothing. A `scope: "multi-influencer"` token (agency: `influencer_id` is `null`, `available_influencer_ids` has
  the set) is **unscoped**: `get_me` and the playbook reads still work, but every influencer-scoped tool then
  **requires an explicit `influencer_id`**. Pass the one from the agent's `meta.md` (**Influencer ID**); resolve the
  human's "@handle" to its id via `get_me`'s `influencers` map. To run the same operation on **another** of the
  agency's clients ("replicá esto en mi otro cliente"), pass **that** client's `influencer_id` — e.g.
  `get_agent_config(agent_id, influencer_id: A)` to read, then `deploy_agent_sdk(..., influencer_id: B)` to replicate.
  The id must be one of `available_influencer_ids`; the gateway re-validates it on **every** call and rejects anything
  else. (Single-influencer tokens ignore the param — RLS pins them — so passing the `meta.md` id is always safe.)
- **Deploy only via `deploy_agent_sdk`** (PATCH semantics: omitted field = untouched; nothing can be set to null).
  Verify the report; roll back with `restore_prompt_version` on `verified:false` / `partial:true`.

## Playbook (self-serve docs — read these first)

These read-only tools serve **this very playbook** (operating manual + `knowledge/` + `templates/` + worked sample
SDKs) over the MCP connection itself. They exist so a **Mode-B** client — regular claude.ai web/desktop on the Ninjo
connector, with no local files and no `/cortex-*` skills — can load the playbook without uploading the project zip.
On Claude Code you already have these files on disk, so you rarely need them; on the web connector they are the
manual. They fetch the published Pages mirror, so a doc edit shows up within ~10 min, no redeploy.

| Tool | What it does | Notes |
|---|---|---|
| `get_playbook_index` | Catalog of every doc: `{ id, title, kind, one_line, url }` (`kind` ∈ `manual`/`reference`/`template`/`example`) | **Call first.** `id`s are a closed set the other two tools accept. |
| `get_playbook_doc` | Fetch one doc by `id`. Sample SDKs return a file manifest; pass `section` (`system`, `examples`, `objections`, `keywords`, `meta`, …) to read one file | Large docs paginate: when `has_more`, call again with `next_cursor`. |
| `search_playbook` | Keyword search → ranked snippets with `id` (+ `section`) to fetch | Pull only what's relevant instead of whole docs. |

## Lifecycle & deploy

| Tool | What it does | Notes |
|---|---|---|
| `create_agent` | Provision a new agent | Full `/mcp` surface. Used by `cortex-create`. |
| `delete_agent` | Remove an agent | Full `/mcp` surface. Destructive — confirm intent. |
| `deploy_agent_sdk` | **The** deploy path: diffs each field vs live, writes only changes, re-fetches & verifies | Thick/correct-by-construction. `keywords` is dual-written to `v5Config.keywords` AND synced to `trigger_keywords`. Don't hand-roll the 3-step flow. Pass `keywords`/`program`/`resources`/`case_studies` as JSON values (object/array), **never JSON-encoded strings** — the gateway normalizes them (a stringified object is auto-parsed; an invalid/scalar string 400s), so post-deploy `get_agent_config` must show them as objects/arrays (platform-internals §3e). |
| `update_prompt` | Surgical: bump just the system prompt | Prefer `deploy_agent_sdk`. |
| `update_v5_config` | Surgical: write specific v5Config fields | Prefer `deploy_agent_sdk`. |
| `sync_keyword_triggers` | Re-sync `keywords.json` → `trigger_keywords` | Idempotent; heals drift. Sync only **active** entries (PAT-055). |
| `restore_prompt_version` | Roll back to a prior prompt version | Use the `rollback_version_id` from a failed deploy report. |
| `update_agent` / `update_agent_config` | Agent-level settings (model, temperature, flags) and the CoT filter | `update_agent_config` takes `response_filter { enabled, extra_patterns, disabled_defaults }` → tunes the reasoning-leak filter (`config.responseFilter`). See `knowledge/reasoning-leak.md`. |
| `update_agent_mode` / `update_agent_voice` / `set_agent_status` | Mode, voice, active/paused status | — |
| `update_resource_sending` | Toggle/configure the agent sending audio/image clips mid-conversation (`config.resourceSending`) | Direct config write like `update_agent_config` — **not** a v5Config field, no version bump, **outside** the `deploy_agent_sdk` contract. See §Creator resources. |

## Reads & analytics (read-only — safe to call freely)

| Tool | What it does |
|---|---|
| `list_agents` | All agents you can see (find an `agent_id`). |
| `get_agent_config` | Pull the live SDK (system + v5Config + triggers) — the verify/pull half of the deploy loop. |
| `get_agent_metrics` | KPI metrics for an agent over a window. |
| `get_agent_insights` | Operational insights / quality signals. |
| `get_conversations` | Sampled real conversations by **curated mode** (`recent`/`bad`/`stuck`/`booking`/…) — quick presets, implicit min-message floor per mode. The substrate for `cortex-analyze` / `cortex-iterate` and leak sweeps. |
| `search_conversations` | Same conversations, selected by **composable AND filters** (no implicit message minimum) + pagination. Use for precision/cross-criteria pulls, a single `conversation_id`/`contact_username`, or sweeping the full history. |
| `search_contacts` | Contact directory by **composable AND filters** + pagination — the **only** tool that returns `contact_id` (for WhatsApp template send). Native + custom-property filters, plus `from_ads`/`ad_id` to target contacts who came from a Meta ad (enriches each with first-touch ad metadata). Detail: `knowledge/outbound-whatsapp-playbook.md`. |
| `list_agent_tools` / `update_tool` | Inspect / edit the tools an agent exposes. |
| `get_me` | Caller identity: `scope`, `available_influencer_ids`, `influencers` (id → handle/name). Not a capability gate — see ground rules above. |
| `list_influencer_connected_accounts` | **All** the influencer's connected IG/WA sub-accounts, linked or not — the tool that surfaces the `connected_account_id` to link. Use this **first** when wiring a fresh agent. |
| `list_connected_accounts` / `set_connected_account` | Accounts wired **to a given agent** (`list_` is **agent-scoped** → **empty for a new agent**; that does *not* mean nothing is connected — check `list_influencer_connected_accounts`), then link/unlink one. Flow: discover the id with `list_influencer_connected_accounts`, then `set_connected_account`. |

## Triggers (see `cortex-triggers` skill + `knowledge/triggers.md`)

`list_triggers`, `create_trigger`, `update_trigger`, `delete_trigger`; `sync_keyword_triggers`;
`list_trigger_audios` / `sync_trigger_audios`; `list_trigger_canned_responses` / `sync_trigger_canned_responses`;
`get_trigger_negative_config` / `upsert_trigger_negative_config` / `delete_trigger_negative_config` (temporary
blocks); `trigger_fetch_info` (inspect a referenced post/story/ad). Trigger types: keyword, comment, ad, all-DM,
outgoing, story. **Gotcha:** reel-reply keyword triggers can silently fail to fire (platform bug — see
`platform-internals.md` §3c).

## Creator resources — mid-conversation audio/image

Audio/image clips the **agent itself sends mid-conversation** — distinct from the §Triggers audios, which
play when a *trigger* fires. Two halves: turn the capability on per-agent, then manage the clip catalog.

- `update_resource_sending` — enable/configure sending on the agent. Writes `config.resourceSending`
  `{ enabled, allowed_types: (IMAGE|AUDIO)[], max_per_conversation }` **directly** (sibling of
  `update_agent_config`): PATCH semantics, **not** a v5Config field and does **not** bump a prompt version,
  so it sits **outside** the `deploy_agent_sdk` contract. Only `IMAGE` / `AUDIO` are deliverable.
- `list_creator_resources` — list the catalog (filter `type` IMAGE|AUDIO, `agent_usable_only`). Read-only.
- **Upload is two-step / presigned** — the file bytes never pass through the model context:
  1. `create_resource_upload_url` — `{ name, type, content_type }` → `{ upload_url, s3_key }`. **AUDIO must be
     M4A/AAC (MP3 is rejected); IMAGE must be JPG/PNG/GIF.**
  2. PUT the file to `upload_url` **yourself** with the same `Content-Type` — run it via bash, e.g.
     `curl -T file.m4a -H 'Content-Type: audio/mp4' "<upload_url>"`. (The URL has a short TTL.)
  3. `register_creator_resource` — register the row from `s3_key` + make it agent-usable:
     `{ name, type, s3_key, content_type, size?, usage_context?, caption_guidance?, description?, agent_usable? }`
     → `{ resource }`. `usage_context` (the concrete "when to send it") is **required** when `agent_usable` is
     true (the default); `name` must include the file extension.
- `update_creator_resource` — edit name / agent metadata (`agent_usable`, `usage_context`,
  `caption_guidance`). PATCH semantics — omitted fields untouched.
- `delete_creator_resource` — **destructive**: removes the S3 object **and** the DB row. Confirm intent.

The catalog is the creator's shared media library (`knowledge/platform-features.md` §Creator Resources);
the S3 upload, format validation, and metadata invariants live in **mk1-chat** — the gateway just forwards.
Influencer is inferred from the JWT; admin/API-key **and multi-influencer tokens** pass `influencer_id` (see Ground rules › **Influencer scope**).

## Knowledge / documents (RAG)

`list_knowledge` / `get_knowledge`; `create_document` / `list_documents` / `delete_document`;
`get_peer_managed_files` / `update_managed_file`. These feed the agent's retrieval context (PDFs, docs, images).

## Custom properties & contact limits

- Properties: `list_custom_properties`, `upsert_custom_property`, `delete_custom_property`. Properties are set by an
  **external evaluator**, not the conversational agent — the SDK reads them, never writes them.
- Contact limits (silent handoff / cap logic): `list_contact_limits`, `upsert_contact_limit`,
  `delete_contact_limit`, `get_contact_limits_context`.

## Workflows & objectives

`list_workflows`, `upsert_workflow`, `delete_workflow`, `get_workflow_activity`; `replace_objectives`. **Gotchas:**
follow-up workflows can be silently blocked by NULL conditions (`platform-internals.md` §3a) and by
`needs_attention` (§3b). When a workflow "doesn't fire", check `get_workflow_activity` first, then suspect those two.

## Notifications

`list_custom_notifications` / `upsert_custom_notification` / `delete_custom_notification`;
`list_notification_destinations` / `upsert_notification_destination` / `delete_notification_destination`.

## Live conversation control

`pause_conversation_agent` / `resume_conversation_agent` (hand a conversation to a human / take it back);
`create_thread` / `delete_thread` / `list_threads` / `get_thread_messages` / `send_thread_message`.

## Testing & synthetic

`generate_synthetic_conversations` (scripted, deterministic lead messages → run agent against live config — the
`cortex-test` engine) and `get_synthetic_conversations`. **Gotcha:** runs on **prod** only; a `stag` env fails with
an IAM error on the prod-connected gateway.

## Media & external data

`generate_image` / `save_image`; `get_instagram_posts` / `get_instagram_profile`; `get_self_serve_data` (onboarding
brief); `associate_whatsapp_direct_numbers`; `bootstrap_discord_channel`.

## Feedback — report a gap to the dev team

When a tool is genuinely broken, a capability is missing, or the docs don't cover what's needed — **after** the
self-sufficiency loop confirms it (`knowledge/self-sufficiency.md`) — file it so the team can fix it. Files a
Linear ticket + Slack ping. See `knowledge/feedback-reporting.md` for when/how and the field rubric.

- `report_feedback` — `{ operation, problem, type: ERROR|FEATURE_REQUEST|MISSING_INFO, session_s3_key? }` →
  `{ ok, issue_id, issue_identifier, issue_url, slack_posted }`. **Takes no `reason`** — the tool *is* the
  feedback. `slack_posted:false` is partial success (the issue was still created). Filing it does **not** unblock
  the current task — still give the user a concrete result + next step.
- `create_feedback_upload_url` — `{}` → `{ upload_url, s3_key }`. Optional first step to attach the full session:
  PUT the file to `upload_url` yourself (no special header, short TTL), then pass `s3_key` as `session_s3_key`.

---

**When the right tool genuinely doesn't exist** (e.g. robust story-trigger binding repair), say so precisely — name
what's missing and what you did with the tools that exist instead, **and call `report_feedback`** so it reaches the
dev team. That is the self-sufficient response; deflecting to "do it manually" without that diagnosis is the failure
mode this workspace exists to avoid.
