---
name: composio-mcp-ops
description: Operate Rob's Composio connection from the VPS via direct streamable-HTTP MCP calls to connect.composio.dev/mcp — the working path, since Rob's ck_ keys are MCP keys, NOT CLI account keys (CLI login 401s). Trigger on any task touching Composio, external-app automation (Gmail, Slack, GitHub, Notion via Composio), "run a tool on my email", reconnecting Composio, or when the composio skill's CLI instructions fail with 401.
---

# Composio MCP Ops (VPS)

Rob's Composio account is connected from Hermes via **direct MCP-over-HTTP**, not the Composio CLI. This skill is the operational runbook. The `composio` skill (installed from composio-community/skills) holds the conceptual knowledge (tool slugs, workflows); THIS skill holds the working connection mechanics for this VPS.

## Why MCP, not CLI (hard-won gotcha)

Rob's Composio keys (`ck_...`, from the connect.composio.dev onboarding page / dashboard key page) are **MCP keys for `connect.composio.dev/mcp`**. They are NOT account API keys:
- `composio login --user-api-key ck_...` → **HTTP 401** (always)
- `composio login` browser flow → fragile; keys expire in ~10 min; cliKey links stall
- The same `ck_` key works perfectly as `Authorization: Bearer` on the MCP endpoint
- Conclusion: skip the CLI entirely for auth. The CLI is still installed at `/root/.hermes/composio/composio` (wrapper `/root/.hermes/bin/composio`) but is not the primary path.

Also: the CLI hardcodes config at `$HOME/.composio`; `/root` is read-only on this VPS. The wrapper runs it in a private mount namespace (`unshare -rm`) with a bind mount to `/root/.hermes/composio-home`. If the CLI is ever needed, always call the wrapper, never the raw binary.

## Where the key lives

The MCP key is a secret. Store/fetch it from `/root/.hermes/secrets/composio.env` (chmod 600):

```
COMPOSIO_MCP_KEY=ck_...
```

If the key is missing or 401s, ask Rob to regenerate from the Composio dashboard and update the file. Do NOT commit the key anywhere else.

## Connection pattern (verified working 2026-07-28)

All calls are POSTs to `https://connect.composio.dev/mcp` with headers:
- `Content-Type: application/json`
- `Accept: application/json, text/event-stream`  ← REQUIRED; without it you get "Not Acceptable: Client must accept both application/json and text/event-stream"
- `Authorization: Bearer $COMPOSIO_MCP_KEY`
- `mcp-session-id: <id>` (after initialize)

Responses are SSE: parse lines starting with `data: ` as JSON.

Sequence:
1. **initialize** — `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"hermes","version":"1.0"}}}` — capture `mcp-session-id` from the RESPONSE HEADERS (`-D`), not the body.
2. **notifications/initialized** — `{"jsonrpc":"2.0","method":"notifications/initialized"}` with the session id header.
3. Then call tools freely. Sessions may expire; if you get session errors, re-initialize.

### Available tools (meta-tools only)

`tools/list` returns ONLY meta-tools — actual app tools (GMAIL_*, etc.) are NOT directly callable:
- `COMPOSIO_SEARCH_TOOLS` — find tool slugs by use case: args `{"queries":[{"use_case":"..."}]}`. Returns primary_tool_slugs, plans, pitfalls, connection status.
- `COMPOSIO_GET_TOOL_SCHEMAS` — input schemas for slugs: `{"tool_slugs":["GMAIL_FETCH_EMAILS"]}`
- `COMPOSIO_MANAGE_CONNECTIONS` — `{"toolkits":[{"name":"gmail","action":"list|add|remove|rename"}]}`. `add` returns a `redirect_url` (expires ~10 min) for Rob to authorize in browser.
- `COMPOSIO_WAIT_FOR_CONNECTIONS` — `{"toolkits":["gmail"],"timeout_ms":30000}` — polls until ACTIVE. Response includes `current_user_info` (mailbox email, message counts).
- `COMPOSIO_MULTI_EXECUTE_TOOL` — THE executor: `{"tools":[{"tool_slug":"GMAIL_FETCH_EMAILS","arguments":{...}}]}`. Results nest at `data.results[i].response` (check `.successful`).
- `COMPOSIO_REMOTE_WORKBENCH`, `COMPOSIO_REMOTE_BASH_TOOL` — remote code exec helpers for large payloads.

### Minimal call template

```bash
source /root/.hermes/secrets/composio.env
# 1. init + capture session id
SID=$(curl -s -m 20 -D - -o /dev/null -X POST "https://connect.composio.dev/mcp" \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $COMPOSIO_MCP_KEY" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"hermes","version":"1.0"}}}' \
  | grep -i '^mcp-session-id:' | tr -d '\r' | awk '{print $2}')
# 2. initialized notification
curl -s -m 20 -X POST "https://connect.composio.dev/mcp" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $COMPOSIO_MCP_KEY" \
  -H "mcp-session-id: $SID" -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' > /dev/null
# 3. execute a tool
curl -s -m 60 -X POST "https://connect.composio.dev/mcp" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $COMPOSIO_MCP_KEY" \
  -H "mcp-session-id: $SID" \
  -d '{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"COMPOSIO_MULTI_EXECUTE_TOOL","arguments":{"tools":[{"tool_slug":"GMAIL_FETCH_EMAILS","arguments":{"user_id":"me","max_results":5,"verbose":false}}]}}}'
```

## Workflow for any app task

1. `COMPOSIO_SEARCH_TOOLS` with the use case → get exact slugs (never invent slugs)
2. `COMPOSIO_MANAGE_CONNECTIONS` action=list → verify ACTIVE connection for the toolkit
3. If not active: action=add → give Rob the `redirect_url` as a clickable markdown link → `COMPOSIO_WAIT_FOR_CONNECTIONS` until ACTIVE
4. `COMPOSIO_GET_TOOL_SCHEMAS` if args unclear
5. `COMPOSIO_MULTI_EXECUTE_TOOL` to run

## Safety rules (Rob's standing instructions)

- Gmail connection has FULL scopes (read/write/delete) — Rob explicitly accepted this 2026-07-28 with "you'll watch it."
- **Send and delete operations are gated**: require Rob's explicit instruction each time, in the current conversation. Never fire them autonomously, speculatively, or "helpfully."
- Read tools (fetch/list/search/profile) are free to use as tasks require.
- CLI config had `"destructive_actions": false` — same spirit applies here.

## Current state (as of 2026-08-03)

- Gmail ACTIVE: connection `ca_T-hg0CMmd5KD`, mailbox rob.blake@realresultsready.com (RRR workspace). An older scope-limited connection ("gmail_casino-dubby") may still exist — if 403 "insufficient authentication scopes" appears, check which account is default and prefer the new one (or remove the old).
- **Google Drive ACTIVE (2026-08-03):** connection `ca_kBIWjjdm3nli`, RRR workspace Drive (rob.blake@realresultsready.com). Rob's preferred escape hatch when Desktop app file downloads misbehave — "upload to my Drive and I'll get them from there."
- Verified working: GMAIL_FETCH_EMAILS, GOOGLEDRIVE_UPLOAD_FROM_URL.

## Pushing VPS files to Rob via Drive (verified 2026-08-03)

The `file_uploadable`/`s3key` pattern (attachments in GMAIL_SEND_EMAIL, GOOGLEDRIVE_UPLOAD_FILE) assumes files already staged in Composio's S3 by a prior download action — **there is no direct file-upload endpoint for MCP keys** (`backend.composio.dev/api/v3/files/upload` 404s, `connect.composio.dev/api/v1/files/upload` 500s). The working path for arbitrary VPS files:

1. Stage the file at a public HTTPS URL on the VPS with an unguessable name: temp `location /stage/ { alias /var/www/html/stage/; }` block in the nginx 443 vhost (before the `location /` proxy), `cp` file in as `<token>_<name>.png`, `systemctl reload nginx`. Verify externally: `curl -s -o /dev/null -w "%{http_code}" https://robblake.cloud/stage/<file>` → 200.
2. `GOOGLEDRIVE_UPLOAD_FROM_URL` with `source_url` = the staged URL, `name` = clean filename, `mime_type`. Works with COMPOSIO_MULTI_EXECUTE_TOOL for batches. Response includes `webViewLink` — hand that to Rob.
3. **Always clean up**: delete staged files, revert the nginx block, reload, confirm 404. Verify Drive sizes match source byte counts before declaring success.

Gmail attachments via GMAIL_SEND_EMAIL remain blocked on the same s3key problem for locally-generated files — Drive is the better delivery channel anyway (no 25MB cap, proper filenames, Rob's explicit preference).
- 403 "insufficient authentication scopes" on a toolkit = the OAuth grant was too narrow → re-add the connection (action=add) and have Rob re-authorize.

## Pitfalls

- Forgetting `Accept: application/json, text/event-stream` → protocol error that looks like auth failure.
- Parsing the session id from the body — it's in RESPONSE HEADERS.
- Calling GMAIL_* (or any app tool) directly as MCP tools → "Tool not found". Only the 7 meta-tools exist at the MCP surface.
- Trailing `\r` in the session id from grep (headers are CRLF) — strip it or calls fail.
- SSE parsing: always filter `^data: ` lines; the stream may contain `event:` lines and keep-alives.
- Empty results are VALID (empty messages list, empty nextPageToken) — don't loop on them.
