# Collaboration integration service — API contract (v1)

Read-only. Org-scoped tokens. Implemented by **`slack-bot-service.com`**.
Consumed by `api.lucos.com` → `business-mcp.lucos.com`.

**Base URL:** `{slack-bot-service}/internal/v1` (local default port `6010`)  
**Method:** `POST /tools/{tool_name}`  
**Headers:** `Authorization: Bearer <s2s-token>`, `X-Lucos-Org-Id: <uuid>`, `Content-Type: application/json`

| Side | Env var | Same secret |
| ---- | ------- | ----------- |
| slack-bot-service | `COLLAB_S2S_TOKEN` | yes |
| api.lucos.com | `COLLAB_INTEGRATION_SERVICE_TOKEN` | yes |
| api.lucos.com | `COLLAB_INTEGRATION_SERVICE_URL` | slack-bot-service origin, e.g. `http://127.0.0.1:6010` |

---

## Request (all tools)

```json
{
  "environment": "staging",
  "args": { }
}
```

| Field | Type | Required |
| ----- | ---- | -------- |
| `environment` | `"staging"` \| `"prod"` | yes |
| `args` | object | yes |

---

## Response — list tools

```json
{
  "ok": true,
  "tool": "<tool_name>",
  "source": "collaboration",
  "environment": "staging",
  "result_count": 0,
  "limit": 25,
  "capped": false,
  "rows": []
}
```

## Response — single record

Same as above; use `"record": { }` instead of `rows`. Omit `result_count` / `capped` or set `result_count: 1`.

## Error

```json
{
  "ok": false,
  "code": "invalid_argument",
  "message": "human-readable reason"
}
```

| HTTP | Typical `code` |
| ---- | -------------- |
| 400 | `invalid_argument` |
| 404 | `not_found` |
| 429 | `rate_limited` |
| 502 | `vendor_error` |
| 503 | `vendor_not_configured` |
| 504 | `vendor_timeout` |

**Limits:** `limit` default 25, max 50. Text fields max 32 KiB (`truncated: true` when clipped).

---

## Request params — how `query` works

**Callers send plain keywords, not vendor query languages.**

MCP / gateway pass human-style search terms in `args.query` (and optional filters).
Your service **translates** those into JQL, CQL, Slack search syntax, or Drive `q=` —
callers never send JQL/CQL directly in v1.

| Tool | Caller sends | You translate to |
| ---- | ------------ | ---------------- |
| `jira_search_issues` | `query` and/or filters | **JQL** |
| `jira_list_projects` | optional `query` | Jira project search |
| `jira_get_project` | `project` | Direct project lookup |
| `jira_list_boards` | optional `query`, `project`, `user` | Agile board list |
| `jira_list_sprints` | `board_id`, optional `state` | Sprint list |
| `jira_list_sprint_issues` | `sprint_id` | Sprint issue list |
| `jira_list_users` | `query` (name) | User search (may include email) |
| `jira_list_versions` | `project` | Project versions |
| `confluence_search_pages` | `query` and/or filters (`title`, `creator`, `labels`, `space_key`, dates, `ancestor`) | **CQL** (never caller CQL) |
| `confluence_get_comments` | `page_id` or `page_url` | Direct REST comments |
| `confluence_list_children` | `page_id` or `page_url` | Direct REST child pages |
| `confluence_list_spaces` | optional `query` | Space list or CQL type=space |
| `confluence_list_attachments` | `page_id` or `page_url` | Direct REST attachments |
| `confluence_get_versions` | `page_id` or `page_url` | Direct REST versions |
| `confluence_search_users` | `query` (name) | CQL `user.fullname ~` |
| `confluence_get_space_permissions` | required `space_key` | Direct REST space permissions |
| `confluence_get_user_space_access` | `query` or `account_id` | User groups + space permissions |
| `slack_search_messages` | `query` + optional `channel`, dates, `include_image_bytes` | **Slack search query** |
| `slack_ask` | `query` + optional `channel`, `dm_only`, `include_image_bytes` | Live Slack search/history + channel-find (not Qdrant RAG) |
| `slack_get_latest_messages` | required `channel`, optional `query`, `limit`, `include_threads`, `include_image_bytes` | Channel resolve then `conversations.history` (bot fallback) |
| `slack_get_file` | required `file_id`, optional `prefer_thumb` / `full` | Slack `files.info` + authenticated download |
| `slack_list_users` | optional `status`, `include_deactivated`, `user_type`, `name_prefix`, `updated_after`, `updated_before`, `include_details` | Slack `users.list` (workspace members, full crawl — no pagination) |
| `slack_get_user` | `user_id` / `user` or `email` or `username` or `name` | Slack `users.info` / `users.lookupByEmail` / fuzzy name match |
| `slack_check_scopes` | none (optional `environment`) | `auth.test` + granted vs required OAuth scopes |
| `user_map` | optional `matched_only`, `unmatched_only`, `include_deactivated_slack`, `include_deleted_google`, `include_service_accounts`, `include_unmatched_google` | Correlates Slack members with the Google Workspace directory (email/username/alias match) |
| `gdrive_search_files` | `query` and/or `scope` / `folder_id` / `folder_name` / `drive_id` / `mime_type` / `owned_by` / `modified_by` / `recursive` | **Drive `files.list` q=** |
| `gdrive_list_workspaces` | optional `query` | Shared drives list (not UI Workspaces) |
| `gdrive_drive_overview` | optional `sample_limit` | Drive left-nav samples |
| `gdrive_user_drive_info` | `user` / `as_user` (name or email), optional `sample_limit` | Per-user Drive overview (DWD) |
| `gdrive_find_duplicates` | `as_user` / `user`, optional `query`, `limit` | Owned-file name clustering |
| `gdrive_lookup_owned_file` | `query` / `file_name` or `file_id` | Local owner index (no Drive API) |
| `gdrive_list_recent_activity` | `user` / `as_user`, optional `mime_type`, `limit` | Drive Activity API (views/opens; not scope=recent) |
| `gdrive_list_permissions` | `file_id`, optional `user` / `as_user`, `limit` | Drive `permissions.list` |
| `gdrive_list_revisions` | `file_id`, optional `user` / `as_user`, `limit` | Drive `revisions.list` |
| `gdrive_list_comments` | `file_id`, optional `user` / `as_user`, `include_deleted`, `limit` | Drive `comments.list` |
| `gcal_list_events` | `user` / `as_user`, optional `time_min` / `time_max` / `query` / `also_user` | Calendar `events.list` |
| `gcal_get_event` | `event_id`, `user` / `as_user` | Calendar `events.get` |
| `gcal_list_resources` | optional `query`, `limit` | Admin Directory `resources.calendars.list` |
| `gcal_propose_rsvp` | `user`, `event_id`, `response` | Propose RSVP → approval_token |
| `gcal_confirm_write` / `gcal_cancel_write` | `approval_token` | Confirm or cancel pending calendar write |
| `gmail_list_users` | optional `query` / `domain` / `include_suspended` / `after` / `limit` | Admin Directory `users.list` (Workspace) |
| `gmail_list_labels` | `user` / `as_user` | Gmail `users.labels.list` |
| `gmail_list_messages` | `query` / `q` + optional `folder` / `from` / `to` / `after` / `page_token` / `before` / `has_attachment` / `is_unread` / `label_id` / `limit` | Gmail `users.messages.list` `q=` (one page) |
| `gmail_get_message` | `message_id` / `id`, `user` / `as_user`, optional `include_body` | Gmail metadata + text window |
| `gmail_list_threads` | same search args as `gmail_list_messages` | Gmail `users.threads.list` `q=` |
| `gmail_get_thread` | `thread_id` / `id`, optional `include_body` / `offset` / `limit` | Gmail `users.threads.get` (paged messages) |
| `gmail_get_attachment` | `message_id` + `attachment_id` | Skipped unless `GMAIL_ATTACHMENTS_ENABLED` |
| `gmail_propose_send` | `to` / `recipients`, `subject`, `body` / `text` | Propose send → approval_token |
| `gmail_propose_reply` | `message_id`, `body` / `text`, optional `reply_all` | Propose reply → approval_token |
| `gmail_propose_forward` | `message_id`, `to` / `recipients`, optional `body` | Propose forward → approval_token |
| `gmail_propose_draft` | at least `subject` or `body` / `text` | Propose draft → approval_token |
| `gmail_propose_modify_labels` | `message_id`, `add_labels` / `remove_labels` / `archive` / `unarchive` | Propose label change → approval_token |
| `gmail_confirm_write` / `gmail_cancel_write` | `approval_token` | Confirm or cancel pending Gmail write |
| `fireflies_search_meetings` | optional `query` / `keyword` / `scope` / `from` / `to` / `organizers` / `participants` / `mine` / `limit` | Fireflies GraphQL transcripts |
| `fireflies_get_meeting` | `meeting_id`, optional `mode`, `sentence_offset` / `sentence_limit` | Fireflies GraphQL transcript |
| `fireflies_list_users` | optional `environment` only | Fireflies workspace users / API key owner |
| `fireflies_ingest_recent` | optional `limit`, `skip`, `from` | Poll GraphQL + upsert local index |
| `fireflies_list_indexed` | optional `since` / `from`, `limit` | Read local meeting index |
| `fireflies_find_duplicates` | optional `limit` | Same title+day, different meeting_ids |
| `fireflies_correlate_meeting` | `meeting_id`, optional `user` / `as_user` | Fireflies + Calendar + Drive bundle |
| `fireflies_ensure_fred_invite` | optional `user`, `look_ahead_days`, `dry_run`, `force`, `calendar_id` | Add fred@fireflies.ai to Meet events |
| `slack_list_channels` | optional `query` name or ID | Fast lookup via admin search / `conversations.info` (not a full `conversations.list` crawl) |
| `jira_get_issue` | `issue_key` (e.g. `TW-330`) | Direct REST lookup — no `query` |
| `confluence_get_page` | `page_id` or `page_url` | Direct REST lookup — no `query` |
| `gdrive_read_file` | `file_id`, optional `sheet_name`, `offset`, `as_user` / `user` | Direct export — no `query`; Sheets without `sheet_name` → tab preview |
| `gdrive_query_sheet` | `file_id`, optional `sheet_name`, `filters`, `limit`, `offset`, `as_user` / `user` | Structured JSON rows from Google Sheets |
| `bb_list_projects` | optional `query` | Bitbucket Server projects |
| `bb_list_repos` | optional `project` (default `AD`) | Repos in a project |
| `bb_list_prs` | optional `project`, `repo`, `state`, `author`, `reviewer`, `merged_by`, `period`, `sort` | Pull requests |
| `bb_get_pr` | `repo`, `pr_id` | One PR |
| `bb_list_branches` | `repo` | Branches |
| `bb_list_commits` | `repo`, optional `path`, `first` | Commits |
| `bb_file_diff` | `repo`, `path`, optional `diff_from`, `diff_to` | File diff |
| `hubspot_list_accounts` | optional `verify`, `environment` | HubSpot portals + token health |
| `hubspot_search_*` | optional `account` (omit or `all` = all 4 portals), optional `query`, date filters, `limit`, `after`, `timezone` | HubSpot CRM search (contacts/companies/deals/tickets/notes) |
| `hubspot_get_*` | record id; optional `account` (omit = look up across all portals) | HubSpot CRM record detail |
| `hubspot_get_contact_related` | `contact_id`; optional `account` | Contact deals/tickets/companies/notes |
| `hubspot_list_associations` | `object_type`, `id`; optional `account` | CRM associations |
| `hubspot_get_timeline` | `object_type`, `id`; optional `account`, `types`, `limit`, `after` | Activity timeline |
| `hubspot_search_activities` | optional `account`, `type`, optional filters | Engagement search |
| `hubspot_get_attachment` | `file_id`; optional `account` | File metadata (Files API scope) |
| `hubspot_propose_create_note` | **requires** `account`, `body`, associate target | Propose note write → approval_token |
| `hubspot_confirm_write` / `hubspot_cancel_write` | `approval_token` | Confirm or cancel pending write |

### Shared optional params

| Param | Applies to | Default | Notes |
| ----- | ---------- | ------- | ----- |
| `limit` | all list tools | 25 | Max 50; return `400` if higher |
| `start` | paginated Confluence list tools | 0 | Offset; omit to start at 0 |
| `environment` | all tools | — | Selects org token set |

---

## Tools

### Jira

#### `jira_search_issues`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `query` | string | no* | Plain keywords. **Not JQL.** Required unless at least one filter is set. |
| `project` | string | no | Project key or name, e.g. `TW` or `TAS`. |
| `status` | string | no | Semantic status: `open`, `done`, `in progress`. |
| `assignee` | string | no | Display name or account id. |
| `reporter` | string | no | Reporter display name or account id. |
| `watcher` | string | no | Watcher display name or account id. |
| `priority` | string | no | e.g. `Highest`, `High`. |
| `issue_type` | string | no | e.g. `Bug`, `Story`. |
| `issue_key` | string | no | Exact key, e.g. `TW-330`. |
| `due_date` | string (YYYY-MM-DD) | no | Due date filter. |
| `period` | string | no | Relative window: `yesterday`, `today`, `this_week`, `last_month`, `last_7_days`, `last_N_days` (1–365). Aliases like `this week` work. Default field is `updated`. Do not combine with `stale_days`. |
| `date_range` | `{ from, to }` | no | Inclusive YYYY-MM-DD window. |
| `date_field` | string | no | Which date `period` / `date_range` applies to: `created`, `updated`, or `due`. |
| `updated_since` | string (YYYY-MM-DD) | no | Adds `updated >= "YYYY-MM-DD"` to JQL. Do not combine with `stale_days`. |
| `stale_days` | integer | no | Open tickets not updated in N days (1–365). Do not combine with `period` or `updated_since`. |
| `sort` | string | no | `updated` (default newest), `oldest` / `first` / `asc`, `created`, `created_asc`, `resolved`. |
| `start_at` | integer | no | Pagination offset (default 0). |
| `next_page_token` | string | no | Opaque token from the previous response. |
| `labels` | string | no | Comma-separated labels. |
| `component` | string | no | Component name. |
| `fix_version` | string | no | Fix version name. |
| `sprint` | string | no | Sprint name or id. |
| `epic_key` | string | no | Epic key. |
| `aggregate` | string | no | e.g. `activity` (per-person counts instead of issue rows). |
| `count_only` | boolean | no | Return a count only, no issue rows. |
| `comments` | boolean | no | Expand comment text in search. |
| `is_pr_request` | boolean | no | Attach linked Bitbucket PRs. |
| `assignee_from` | string | no | Previous assignee (changelog). |
| `assignee_to` | string | no | New assignee after a change. |
| `assigned_by` | string | no | Who changed the assignee. |
| `changed_field` | string | no | Changelog field (`assignee`, `status`, `priority`, …). |
| `changed_from` | string | no | Previous value of `changed_field`. |
| `changed_to` | string | no | New value of `changed_field`. |
| `changed_by` | string | no | Who changed `changed_field`. |
| `closed_by` | string | no | Who transitioned the issue to Done/closed. |
| `priority_from` | string | no | Previous priority. |
| `priority_to` | string | no | New priority. |
| `commented_by` | string | no | Issues this person commented on. |
| `mentioned` | string | no | Issues whose comments mention this person. |
| `limit` | integer | no | Max results (default 25, max 50). |

**JQL translation (your service builds this):**

```
text ~ "publisher onboarding"
AND project = TW                    -- if project set
AND statusCategory != Done          -- if status = "open"
AND assignee = "alice"              -- if assignee set
AND updated >= "2026-08-01"          -- if updated_since set
ORDER BY updated DESC
```

Example request:

```json
{
  "environment": "staging",
  "args": {
    "query": "publisher onboarding",
    "project": "TW",
    "status": "open",
    "limit": 25
  }
}
```

Project + relative day (query optional when a filter is set):

```json
{
  "environment": "staging",
  "args": {
    "period": "yesterday",
    "project": "TAS",
    "limit": 5
  }
}
```

Activity / last-month window (omit empty filters; do **not** send `entity`):

```json
{
  "environment": "staging",
  "args": {
    "period": "last_month",
    "aggregate": "activity",
    "count_only": false,
    "limit": 25
  }
}
```

**`rows[]`**

| Field | Type |
| ----- | ---- |
| `key` | string |
| `summary` | string |
| `status` | string |
| `issue_type` | string |
| `project` | string |
| `assignee` | string |
| `updated` | string |
| `url` | string |

---

#### `jira_get_issue`

Lookup by key — no search `query`.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `issue_key` | string | yes | Issue key, e.g. `TW-330`. Pattern: `{PROJECT}-{number}`. |
| `include_comments` | boolean | no | Default `false`. If `true`, return last 10 comments max. |
| `include_changelog` | boolean | no | Default `false`. Include status/field change history. |
| `include_subtasks` | boolean | no | Default `false`. If `true`, include child sub-tasks. |
| `is_pr_request` | boolean | no | Default `false`. Attach linked Bitbucket PRs. |

Example request:

```json
{
  "environment": "staging",
  "args": {
    "issue_key": "TW-330",
    "include_comments": false,
    "include_changelog": true,
    "include_subtasks": false,
    "is_pr_request": false
  }
}
```

**`record`**

| Field | Type |
| ----- | ---- |
| `key` | string |
| `summary` | string |
| `status` | string |
| `issue_type` | string |
| `project` | string |
| `assignee` | string |
| `reporter` | string |
| `description` | string |
| `labels` | string[] |
| `linked_issues` | `{ key, summary, link_type }[]` |
| `comments` | `{ author, posted_at, body }[]` |
| `subtasks` | `{ key, summary, status, issue_type }[]` (only when requested) |
| `url` | string |

---

#### `jira_list_boards`

**`args`:** optional `query` (board name), `project`, `user` (member display name or account id), `limit`.

Example request:

```json
{
  "environment": "staging",
  "args": {
    "project": "TW",
    "limit": 25
  }
}
```

**`rows[]`:** `board_id`, `name`, `type`, `project`.

---

### Confluence

#### `confluence_search_pages`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `query` | string | no* | Plain keywords. **Not CQL.** Required unless another filter is set. |
| `title` | string | no | Page title substring. |
| `creator` | string | no | Creator display name. |
| `labels` | string | no | Comma-separated labels. |
| `ancestor` | string | no | Ancestor page id (digits only). |
| `created_after` | string (YYYY-MM-DD) | no | Created on or after. |
| `created_before` | string (YYYY-MM-DD) | no | Created on or before. |
| `last_modified_after` | string (YYYY-MM-DD) | no | Last modified on or after. |
| `last_modified_before` | string (YYYY-MM-DD) | no | Last modified on or before. |
| `type` | string | no | `page` (default) or `blogpost`. |
| `space_key` | string | no | Confluence space, e.g. `EN`, `PM1`. Adds `space = KEY` to CQL. |
| `limit` | integer | no | Max results (default 25, max 50). |
| `start` | integer | no | Pagination offset (default 0). |

**CQL translation (your service builds this):**

```
type = page
AND text ~ "MNGT RO views"
AND space = EN                        -- if space_key set
ORDER BY lastModified DESC
```

Example request:

```json
{
  "environment": "staging",
  "args": {
    "query": "MNGT RO views",
    "space_key": "EN",
    "limit": 15
  }
}
```

**`rows[]`**

| Field | Type |
| ----- | ---- |
| `page_id` | string |
| `title` | string |
| `space_key` | string |
| `space_name` | string |
| `excerpt` | string |
| `last_updated` | string |
| `url` | string |

---

#### `confluence_get_page`

Lookup by id or URL — no search `query`.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `page_id` | string | one of | Confluence content id, e.g. `"633372693"`. |
| `page_url` | string | one of | Full wiki URL; you extract `page_id`. Exactly one required. |

**`record`**

| Field | Type |
| ----- | ---- |
| `page_id` | string |
| `title` | string |
| `space_key` | string |
| `last_updated` | string |
| `body_text` | string |
| `truncated` | boolean |
| `url` | string |

---

#### `confluence_get_comments`

**`args`:** `page_id` or `page_url` (exactly one), optional `limit`, `start`.

**`rows[]`:** `comment_id`, `page_id`, `author` (`account_id`, `display_name`), `created_at`, `last_updated`, `body_text`, `truncated`, `url`.

#### `confluence_list_children`

**`args`:** `page_id` or `page_url` (exactly one), optional `limit`, `start`.

**`rows[]`:** `page_id`, `title`, `space_key`, `space_name`, `last_updated`, `url`.

#### `confluence_list_spaces`

**`args`:** optional `query`, `limit`, `start`.

**`rows[]`:** `space_key`, `space_name`, `type`, `url`.

#### `confluence_list_attachments`

**`args`:** `page_id` or `page_url` (exactly one), optional `limit`, `start`.

**`rows[]`:** `attachment_id`, `page_id`, `filename`, `media_type`, `file_size`, `last_updated`, `url`. Do not return signed `download_url` secrets.

#### `confluence_get_versions`

**`args`:** `page_id` or `page_url` (exactly one), optional `limit`, `start`.

**`rows[]`:** `page_id`, `number`, `when`, `by` (`account_id`, `display_name`), `message`, `minor_edit`.

#### `confluence_search_users`

**`args`:** required `query`, optional `limit`, `start`.

**`rows[]`:** `account_id`, `display_name`, `username`, `type`.

#### `confluence_get_space_permissions`

**`args`:** required `space_key`.

**Response:** `space_key`, `space_name`, `url`. **`rows[]`:** `access`, `operations`, `users[]` (`account_id`, `display_name`, `type`), `groups[]` (`id`, `name`). No emails.

#### `confluence_get_user_space_access`

**`args`:** `query` or `account_id` (at least one), optional `space_key`, `limit`, `start`.

**Response:** `user` (`account_id`, `display_name`), `groups[]`. **`rows[]`:** `space_key`, `space_name`, `access`, `direct_user`, `via_groups`, `url`.

---

### Slack

#### `slack_search_messages`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `query` | string | yes | Plain keywords, e.g. `"lucos_ro views"`. **Not Slack advanced search syntax.** |
| `channel` | string | no | Channel name without `#` (e.g. `adops`) or channel id. Scopes search to that channel. |
| `date_from` | string (YYYY-MM-DD) | no | Inclusive start date. |
| `date_to` | string (YYYY-MM-DD) | no | Inclusive end date. |
| `limit` | integer | no | Max results (default 25, max 50). |

**Slack search translation (your service builds this):**

```
lucos_ro views in:adops after:2026-08-01 before:2026-08-14
```

| Caller arg | Slack modifier |
| ---------- | -------------- |
| `query` | Search terms (required) |
| `channel` | `in:{channel}` or `in:#{channel}` |
| `date_from` | `after:YYYY-MM-DD` |
| `date_to` | `before:YYYY-MM-DD` |

Example request:

```json
{
  "environment": "staging",
  "args": {
    "query": "lucos_ro views",
    "channel": "adops",
    "date_from": "2026-08-01",
    "date_to": "2026-08-14",
    "limit": 30
  }
}
```

**`rows[]`**

| Field | Type |
| ----- | ---- |
| `channel` | string |
| `channel_id` | string |
| `user` | string |
| `posted_at` | string (ISO 8601) |
| `text` | string |
| `permalink` | string |
| `thread_ts` | string \| null |
| `files` | `{ id, name, mimetype, size }[]` — non-image attachments (no private URLs) |
| `images` | `{ id, name, mimetype, size, width, height }[]` — screenshots / image uploads |
| `links` | `{ url, title, source }[]` |

**`image_content[]`** (when `include_image_bytes` is true, default): up to 3 thumbnail records with `file_id`, `media_type`, `encoding=base64`, `content`. MCP converts these to image content blocks so the model can see the screenshot. Call `slack_get_file` for more or full-size.

---

#### `slack_list_channels`

Find a channel by name or ID. Fast lookup via `admin.conversations.search` or `conversations.info` — not a full `conversations.list` workspace crawl.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `query` | string | no | Channel name (`testing-team`, `community`) or id (`C0BP7EVKU0K`). |
| `limit` | integer | no | Max channels returned (default 50, max 100). |

**`rows[]`**

| Field | Type |
| ----- | ---- |
| `channel_id` | string |
| `name` | string |
| `is_private` | boolean |
| `member_count` | integer |
| `topic` | string |

---

#### `slack_get_latest_messages`

Resolve a known channel, then fetch `conversations.history` (bot-token fallback if the primary token is not a member). Use when the caller wants complete/latest messages; optional `query` filters within the fetched window. Returns full `text`.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `channel` | string | yes | Channel name without `#` (e.g. `testing-team`) or channel id. |
| `query` | string | no | Optional text filter within fetched messages. |
| `limit` | integer | no | Max results (default 25, max 50). |
| `include_threads` | boolean | no | Include thread replies when true. |

**`rows[]`**

| Field | Type |
| ----- | ---- |
| `channel` | string |
| `channel_id` | string |
| `user` | string |
| `posted_at` | string (ISO 8601) |
| `text` | string |
| `thread_ts` | string \| null |
| `files` / `images` / `links` | Same as `slack_search_messages` |

**`image_content[]`:** same inline screenshot contract as `slack_search_messages` / `slack_ask`.

---

#### `slack_get_file`

Download one Slack file or screenshot by `file_id` from `images[].id`. Authenticated `files.info` + token download — never return Slack `url_private` to the model.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `file_id` | string | yes | Slack file id (`F0123ABCDEF`). Alias: `id`. |
| `prefer_thumb` | boolean | no | Thumbnail when true (default true). |
| `full` | boolean | no | Original file instead of thumbnail. |
| `max_bytes` | integer | no | Cap (default 400KiB, max 2MiB). |

**`record`:** `file_id`, `name`, `media_type`, `size`, `width`, `height`, `permalink`, `thumb`, `encoding`, `content` (base64), `bytes_returned`, `truncated`.

**`image_content[]`:** the same record when the file is an image. MCP emits this as an image content block.

---

#### `slack_list_users`

Full workspace member list via `users.list` in ONE call — no pagination, always returns every matching user. Slackbot (`USLACKBOT`) is omitted.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `status` | string | no | `active` \| `deactivated` \| `all`. Overrides `include_deactivated` when set (default `all`). |
| `include_deactivated` | boolean | no | Include deactivated accounts (default true). Ignored when `status` is set. |
| `user_type` | string | no | Filter: `bot` \| `guest` \| `external` \| `admin` \| `human`. |
| `name_prefix` | string | no | Case-insensitive prefix match on username/real_name/display_name. Alias: `starts_with`. |
| `updated_after` / `updated_before` | string | no | ISO date/time filter on Slack's profile last-updated time (not a true deactivation timestamp). |
| `include_details` | boolean | no | Include `tz`, `tz_label`, `avatar_url` (default false — keeps the list payload small). |

**`rows[]`:** `user_id`, `username`, `email`, `real_name`, `display_name`, `title`, `deleted` / `deactivated`, `is_bot`, `is_guest`, `is_admin`, `is_owner`, `updated_at`, and (with `include_details=true`) `tz`, `tz_label`, `avatar_url`.

**Response:** `is_complete: true`, `total_matched`, `excluded_count` (users dropped by `SLACK_USER_EXCLUDE_LIST`), `guidance` confirming no pagination is needed.

---

#### `slack_get_user`

Lookup one user. At least one of `user_id`/`user`, `email`, `username`, or `name` is required.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user_id` | string | no* | Slack user id (`U0123ABCDEF`). Alias: `user`. |
| `email` | string | no* | Work email (`users.lookupByEmail`). |
| `username` | string | no* | Slack username without `@` (exact `users.list` handle match, falls back to fuzzy real/display name). |
| `name` | string | no* | Real or display name to fuzzy-match when the others are unknown. |

**`user`:** same fields as `slack_list_users` rows. `ok: false` + `error: user_not_found` when no match.

---

#### `slack_check_scopes`

Diagnostic `auth.test` on the configured user/enterprise token and optional bot token. Empty `args` allowed.

**Response:** `primary_token` (`granted_scopes`, `required_scopes`, `missing_scopes`), `bot_token`, `private_channel_access`. MCP should not mention OAuth scopes to the end user unless they asked why Slack search failed.

---

#### `user_map`

Correlates Slack workspace members with the Google Workspace directory on the fly — no HR-provided list. Match order: exact company email, exact username vs Google email local-part, then a corroborated Google alias; display-name matching is never used. MCP auto-compares with `monitor_identity_lookup` and attaches portal/Jira/Bitbucket ids (`compare`, `monitor_fallback`).

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `include_deactivated_slack` | boolean | no | Include deactivated Slack accounts (default true). |
| `include_deleted_google` | boolean | no | Include deleted Google Workspace accounts (default false). |
| `matched_only` | boolean | no | Return only Slack rows with a Google match. |
| `unmatched_only` | boolean | no | Return only Slack rows without a Google match. |
| `include_service_accounts` | boolean | no | Include service/shared-mailbox and test accounts (default true). |
| `include_unmatched_google` | boolean | no | Include Google users with no Slack match in `unmatched_google_users` (default true). |

**`rows[]`:** `slack_user_id`, `slack_username`, `slack_email`, `google_user_id`, `google_primary_email`, `matched`, `matched_by` (`exact_email` \| `exact_username` \| `corroborated_alias`), `confidence`, `is_service_or_shared_mailbox`, `is_test_account`, `email_domain_mismatch`, `duplicate_google_match`.

**Response:** `slack_email_scope_granted` (false disables exact-email matching — only `exact_username` is active), `matched_count`, `unmatched_slack_count`, `unmatched_google_count`, `unmatched_google_users[]`.

---

### Google Drive

#### `gdrive_search_files`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `query` | string | no* | Plain keywords, e.g. `"staging deploy checklist"` or `"owned by nathan"`. **Not Drive `q=` syntax.** Required unless `scope`, `folder_id`, `folder_name`, `drive_id`, `mime_type`, `owned_by`, `modified_by`, `shared_by`, or `shared_to` is set. |
| `mime_type` | string | no | Shorthand: `document`, `spreadsheet`, `presentation`, `pdf`, `folder`, `video`, `image`, `audio`, `exe` — or a raw Google MIME type. |
| `folder_id` | string | no | Restrict to folder (must be in org allowlist). |
| `folder_name` | string | no | Resolve a folder by name, then list children. |
| `recursive` | boolean | no | List folder children recursively. Requires `folder_id` or `folder_name`. |
| `max_depth` | integer | no | Recursive depth (default 3, max 12). |
| `max_total` | integer | no | Recursive/folder inventory cap (default 200, max 500). |
| `scope` | string | no | `all`, `home`, `my_drive`, `shared_with_me`, `shared_drives`, `starred`, `recent`, `trash`. `shared_drives` requires `drive_id`. |
| `drive_id` | string | no | Shared drive id from `gdrive_list_workspaces` / `gdrive_drive_overview`. |
| `owned_by` | string | no | Filter files owned by this person (name or email). Query `"owned by nathan"` also works. |
| `modified_by` | string | no | Last editor display name or email. |
| `starred` | boolean | no | Shortcut for `scope=starred`. |
| `include_editors` | boolean | no | Attach editor permissions (slower). |
| `include_hidden` | boolean | no | Include hidden Shared drives. |
| `limit` | integer | no | Max results (default 25, max 100). |

**Drive `q` translation (your service builds this):**

```
fullText contains 'staging deploy checklist'
and mimeType = 'application/vnd.google-apps.document'
and 'FOLDER_ID' in parents
and trashed = false
```

| `mime_type` value | Google MIME |
| ----------------- | ------------- |
| `document` | `application/vnd.google-apps.document` |
| `spreadsheet` | `application/vnd.google-apps.spreadsheet` |
| `presentation` | `application/vnd.google-apps.presentation` |
| `pdf` | `application/pdf` |
| `folder` | `application/vnd.google-apps.folder` |
| `video` | video MIME family |
| `image` | image MIME family |
| `audio` | audio MIME family |
| `exe` | executable MIME |

Raw Google MIME types are also accepted.

Also restrict to org-configured Shared Drive / folder allowlist.

Example request:

```json
{
  "environment": "staging",
  "args": {
    "query": "business MCP staging deploy",
    "mime_type": "document",
    "limit": 20
  }
}
```

**`rows[]`**

| Field | Type |
| ----- | ---- |
| `file_id` | string |
| `name` | string |
| `mime_type` | string |
| `modified` | string (ISO 8601) |
| `viewed_by_me` | string (ISO 8601) or null |
| `owners` | string[] |
| `web_view_link` | string |
| `parent_names` | string[] | Present when `scope` is `recent` / `home`, or `include_parent_path` / folder listings |

---

#### `gdrive_read_file`

Lookup by id — no search `query`.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `file_id` | string | yes | Google Drive file id from `gdrive_search_files` results. |
| `sheet_name` | string | no | Google Sheets only: one tab exported as CSV. |
| `offset` | integer | no | Byte offset for the next content window (`record.next_offset`). |
| `as_user` | string | no | Impersonate this Google user (name or email). If omitted, read impersonates the file owner via owner-index `findOwnedFileById`. |
| `user` | string | no | Alias for `as_user`. |

When `sheet_name` is omitted on a Google Sheet, the response is a **preview** (`record.sheet_preview`: tab names and row counts) — not a concatenated multi-tab dump. Use `gdrive_query_sheet` for filtered JSON rows or pass `sheet_name` for one tab.

**`record`**

| Field | Type |
| ----- | ---- |
| `file_id` | string |
| `name` | string |
| `mime_type` | string |
| `content_type` | string |
| `content` | string or null |
| `truncated` | boolean |
| `web_view_link` | string |
| `sheet_names` | string[] | Sheets: tab names |
| `sheets` | `{ name, row_count }[]` | Sheets preview (no `sheet_name`) |
| `sheet_preview` | boolean | Sheets preview (no `sheet_name`) |
| `note` | string | Sheets preview — use `gdrive_query_sheet` or `sheet_name` |
| `viewed_by_me` | string (ISO 8601) or null |
| `parent_names` | string[] | Folders only — immediate parent folder names |

---

#### `gdrive_query_sheet`

Structured, paginated JSON rows from a Google Sheet (XLSX export server-side).

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `file_id` | string | yes | Google Drive spreadsheet file id. |
| `sheet_name` | string | no | Tab name (case-insensitive; spaces/underscores ignored). When omitted: first tab, or first tab with a `Status` column if `filters.Status` is set. |
| `filters` | object | no | Column → value map. Header match is case-insensitive. Cell match: exact (case-insensitive) or contains (case-insensitive substring). |
| `limit` | integer | no | Max matched rows per page (default 50, max 200). |
| `offset` | integer | no | Row offset into **matched** rows (not bytes). |
| `as_user` | string | no | Impersonate this Google user (name or email). |
| `user` | string | no | Alias for `as_user`. |

**Response fields (top-level, not `record`)**

| Field | Type |
| ----- | ---- |
| `sheet_names` | string[] |
| `sheet_name` | string |
| `headers` | string[] |
| `status_counts` | object | Omitted unless a `Status` column exists |
| `rows` | object[] | Header → cell value |
| `result_count` | integer |
| `total_matched` | integer |
| `has_more` | boolean |
| `limit`, `offset` | integer |
| `note` | string | Sheet auto-selection when `sheet_name` omitted |

Example: `{ "file_id": "…", "sheet_name": "Final Bug sheet", "filters": { "Status": "Closed" } }` → JSON rows of closed bugs with `status_counts`, not truncated CSV.

---

#### `gdrive_list_workspaces`

Lists Shared drives (Team Drives) plus the connected My Drive account. **Not** Drive UI Workspaces at `/drive/workspaces` — Google has no API for those.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `query` | string | no | Shared drive name substring. |
| `include_hidden` | boolean | no | Include hidden Shared drives. |
| `limit` | integer | no | Max Shared drives (default 50, max 100). |

#### `gdrive_drive_overview`

Drive left-nav samples: My Drive, Shared drives, Shared with me, Starred, Recent, Trash, storage quota. `views.activity` uses Drive Activity API when authorized (`available: true`); otherwise `available: false` with an honest note — not scope=recent.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `sample_limit` | integer | no | Sample files per view (default 5, max 25). `limit` is an alias. |
| `include_hidden` | boolean | no | Include hidden Shared drives. |

Then list files with `gdrive_search_files` `scope=home|my_drive|shared_with_me|starred|recent|trash`, or `scope=shared_drives` + `drive_id`.

#### `gdrive_user_drive_info`

Per-user Drive left-nav overview (impersonated) plus owner-index stats.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user` | string | one of | Name or email. Aliases: `as_user`, `for_user`, `user_email`. |
| `sample_limit` | integer | no | Sample files per view (default 8, max 25). |
| `include_hidden` | boolean | no | Include hidden Shared drives. |

#### `gdrive_find_duplicates`

Heuristic duplicate / summary+transcript clusters on one person's **owned** files.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `as_user` | string | one of | Name or email whose Drive to scan. Aliases: `user`, `user_email`. |
| `query` | string | no | Only files whose name contains this. Alias: `name_contains`. |
| `limit` | integer | no | Max files to scan (default 300, max 500). |

#### `gdrive_lookup_owned_file`

Owner-index lookup. Does **not** call Drive. No caller Bearer — integration service uses the local index.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `query` | string | one of | File name or chatty prompt. Aliases: `file_name`, `name`. |
| `file_id` | string | one of | Exact Drive file id in the owner index. |

#### `gdrive_list_recent_activity`

Recent Drive Activity events for one user (views/opens when Google logs them). **Not** `files.list` `viewedByMeTime` or `scope=recent`. Returns `available: false` when the Drive Activity API is unauthorized.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user` | string | one of | Name or email. Alias: `as_user`. |
| `mime_type` | string | no | `folder` — only folder-targeted rows (latest folder opened when logged). `file` — non-folder items. |
| `limit` | integer | no | Max rows (default 25, max 100). |

**Response when unauthorized:** `{ ok: true, available: false, rows: [], note: "..." }` — never substitutes recent-files listing.

#### `gdrive_list_permissions`

Sharing / ACL for one file (`permissions.list`). Org id is **not** an arg — header `X-Lucos-Org-Id` is required (same as all collab tools).

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `file_id` | string | yes | Google Drive file id. |
| `user` | string | no | Impersonate this Google user (name or email). Alias: `as_user`. |
| `limit` | integer | no | Max permissions (default 100, max 100). |

#### `gdrive_list_revisions`

Version history for one file (`revisions.list`). Org id is **not** an arg — header `X-Lucos-Org-Id` is required (same as all collab tools).

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `file_id` | string | yes | Google Drive file id. |
| `user` | string | no | Impersonate this Google user (name or email). Alias: `as_user`. |
| `limit` | integer | no | Max revisions (default 50, max 200). |

#### `gdrive_list_comments`

Comments and replies on one file (`comments.list`). Org id is **not** an arg — header `X-Lucos-Org-Id` is required (same as all collab tools).

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `file_id` | string | yes | Google Drive file id. |
| `user` | string | no | Impersonate this Google user (name or email). Alias: `as_user`. |
| `include_deleted` | boolean | no | Include deleted comments (default false). |
| `limit` | integer | no | Max comments (default 50, max 100). |

#### `gcal_list_events`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user` | string | one of | Name or email. Aliases: `as_user`, `for_user`, `user_email`. |
| `time_min` | string | no | ISO start (default now). |
| `time_max` | string | no | ISO end (default now + 7 days). |
| `query` | string | no | Calendar `q` / title. Alias: `title`. |
| `also_user` | string | no | Keep events that also involve this person. Alias: `with_user`. |
| `calendar_id` | string | no | Default `primary`. |
| `limit` | integer | no | Default 25, max 100. |

#### `gcal_get_event`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `event_id` | string | yes | From `gcal_list_events`. Alias: `id`. |
| `user` | string | one of | Name or email. |
| `calendar_id` | string | no | Default `primary`. |

#### `gcal_list_resources`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `query` | string | no | Room / resource name substring (e.g. `conf`). Alias: `q`, `name`. |
| `limit` | integer | no | Default 50, max 100. |
| `after` | string | no | Pagination cursor. Alias: `page_token`. |

#### `gcal_propose_rsvp`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user` | string | one of | Name or email whose RSVP to set. Aliases: `as_user`, `for_user`, `user_email`. |
| `event_id` | string | yes | From `gcal_list_events`. Alias: `id`. |
| `response` | string | yes | `accepted` \| `declined` \| `tentative` \| `needsAction`. Aliases: `response_status`, `rsvp`. |
| `calendar_id` | string | no | Default `primary`. |
| `send_updates` | string | no | `all` (default) \| `externalOnly` \| `none`. |

Then `gcal_confirm_write` / `gcal_cancel_write` with `approval_token`. Gated by `GCAL_WRITES_ENABLED` on slack-bot.

#### `gmail_list_users`

Google Workspace Directory `users.list` (`customer=my_customer` unless `domain` is set). Requires slack-bot DWD scope `admin.directory.user.readonly`. One page per call (default 50, max 200).

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `query` | string | no | Directory search (`name:` / `email:`). Alias: `q`. |
| `domain` | string | no | One Workspace domain. Default: entire customer. |
| `include_suspended` | boolean | no | Default false. |
| `after` / `page_token` | string | no | From `next_after`. |
| `limit` | integer | no | Page size (default 50, max 200). |

#### `gmail_list_labels`

List labels for an org mailbox (DWD impersonation). Default user = requester. Google Vault is out of scope.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user` | string | no | Name or email. Aliases: `as_user`, `for_user`, `user_email`. Default requester. |

#### `gmail_list_messages`

Search/list message metadata. Slack-bot builds Gmail `q=` from filters. `after` without `-` is a page token (`next_after`); with `-` it is a date. Bodies/snippets redact secrets when `GMAIL_REDACT_SECRETS`.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user` | string | no | Name or email. Aliases: `as_user`, `for_user`, `user_email`. Default requester. |
| `query` | string | no | Gmail search terms (plain keywords OK). Alias: `q`. |
| `folder` | string | no | `inbox` \| `sent` \| `trash` \| `archive` \| `drafts` \| `spam` \| `starred` \| `important`. Alias: `mailbox`. Archive is query-only (`-in:inbox -in:trash -in:spam`). |
| `from` | string | no | From address or name. |
| `to` | string | no | To address or name. |
| `after` | string | no | Pagination token from `next_after` (no hyphen) **or** date `YYYY-MM-DD`. Aliases: `page_token` (pagination), `newer_than` (date). |
| `before` | string | no | Date `YYYY-MM-DD`. Alias: `older_than`. |
| `has_attachment` | boolean | no | Only messages with attachments. |
| `is_unread` | boolean | no | Only unread. |
| `label_id` | string | no | Gmail label id. |
| `label_ids` | string \| string[] | no | Gmail label id(s). |
| `limit` | integer | no | Page size (default 5, max 20). Pass `after`/`page_token`=`next_after` for the next page. |

Then `gmail_get_message` / `gmail_get_thread`.

#### `gmail_get_message`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `message_id` | string | yes | From `gmail_list_messages`. Alias: `id`. |
| `user` | string | no | Name or email. Aliases: `as_user`, `for_user`, `user_email`. |
| `include_body` | boolean | no | Default true. Text window only; fat HTML/inline images are skipped (snippet). |

Drive-style read: metadata first, then text/plain when the inline MIME is within `GMAIL_MAX_FULL_RESPONSE_BYTES` (default 256 KiB). PDF/DOCX/images are not downloaded unless slack-bot `GMAIL_ATTACHMENTS_ENABLED=true`. Bodies redact secrets when `GMAIL_REDACT_SECRETS`.

#### `gmail_list_threads`

Same search args as `gmail_list_messages`. Then `gmail_get_thread`.

#### `gmail_get_thread`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `thread_id` | string | yes | From `gmail_list_threads`. Alias: `id`. |
| `user` | string | no | Name or email. |
| `include_body` | boolean | no | Default false (metadata only). |
| `offset` | integer | no | Skip this many messages (default 0). |
| `limit` | integer | no | Page size (default 5, max 20). |

Bodies redact secrets when `GMAIL_REDACT_SECRETS`.

#### `gmail_get_attachment`

Disabled by default (returns `skipped`). Use `gmail_get_message` / `gmail_get_thread` body text. Set slack-bot `GMAIL_ATTACHMENTS_ENABLED=true` to download and extract DOCX/PDF/XLSX.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `message_id` | string | yes | Message that owns the attachment. |
| `attachment_id` | string | yes | From `attachments[]`. |
| `filename` | string | no | Hint for extractors. |
| `mime_type` | string | no | Hint for extractors. |
| `sheet_name` | string | no | XLSX tab name. |
| `offset` | integer | no | Byte offset when `next_offset` was returned. |
| `extract_text` | boolean | no | Default true. |
| `raw` | boolean | no | Return base64 instead of extracted text. |
| `max_bytes` | integer | no | Max bytes for raw/base64 window. |
| `user` | string | no | Name or email. |

#### `gmail_propose_send`

Does **not** send. Returns `approval_token`. Then `gmail_confirm_write` / `gmail_cancel_write`. Gated by `GMAIL_WRITES_ENABLED` on slack-bot.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user` | string | no | Name or email. Aliases: `as_user`, `for_user`, `user_email`. |
| `to` | string \| string[] | yes | Recipients (email or name). Aliases: `recipients`, `recipient`. |
| `cc` | string \| string[] | no | Cc. |
| `bcc` | string \| string[] | no | Bcc. |
| `subject` | string | yes | Subject. |
| `body` | string | yes | Plain-text body. Alias: `text`. |

#### `gmail_propose_reply`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user` | string | no | Name or email. |
| `message_id` | string | yes | Message to reply to. |
| `body` | string | yes | Plain-text body. Alias: `text`. |
| `reply_all` | boolean | no | Reply to all. |

#### `gmail_propose_forward`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user` | string | no | Name or email. |
| `message_id` | string | yes | Message to forward. |
| `to` | string \| string[] | yes | Recipients. Aliases: `recipients`, `recipient`. |
| `body` | string | no | Optional extra body. Alias: `text`. |

#### `gmail_propose_draft`

Pass at least `subject` or `body`.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user` | string | no | Name or email. |
| `to` | string \| string[] | no | Optional recipients. |
| `subject` | string | one of | Required if `body` omitted. |
| `body` | string | one of | Required if `subject` omitted. Alias: `text`. |

#### `gmail_propose_modify_labels`

`archive=true` removes INBOX.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user` | string | no | Name or email. |
| `message_id` | string | yes | Message to relabel. |
| `add_labels` | string \| string[] | no | Label id(s) to add. |
| `remove_labels` | string \| string[] | no | Label id(s) to remove. |
| `archive` | boolean | no | Remove INBOX. |
| `unarchive` | boolean | no | Add INBOX. |

#### `gmail_confirm_write` / `gmail_cancel_write`

Confirm or cancel a pending `gmail_propose_*` token. Nothing hits Gmail until `gmail_confirm_write`. Gated by `GMAIL_WRITES_ENABLED`. Do not use Jira/Confluence `confirm=true` stash.

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `approval_token` | string | yes | Token from propose response. Alias: `token`. |
| `environment` | string | no | `staging` \| `prod`. |

Google Vault is out of scope.

#### `fireflies_search_meetings`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `query` | string | no | Keyword search (title + spoken words). |
| `keyword` | string | no | Alias for `query`. |
| `title` | string | no | Title-only filter (not spoken words). |
| `scope` | string | no | `title` \| `sentences` \| `all`. Alias: `keyword_scope`. |
| `from` | string | no | ISO datetime — meetings on/after. Aliases: `from_date`, `time_min`. |
| `to` | string | no | ISO datetime — meetings on/before. Aliases: `to_date`, `time_max`. |
| `organizers` | string[] \| string | no | Organizer emails. |
| `participants` | string[] \| string | no | Participant emails. |
| `host_email` | string | no | Host/organizer. Alias: `organizer_email`. |
| `user` | string | no | Participant name or email. Aliases: `participant_email`, `as_user`, `for_user`. |
| `mine` | boolean | no | Only meetings for the API key owner. |
| `limit` | integer | no | Default 20, max 50. |
| `skip` | integer | no | Pagination offset. |

#### `fireflies_get_meeting`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `meeting_id` | string | yes | From search or index. Aliases: `id`, `transcript_id`. |
| `mode` | string | no | `summary` (default), `full`, `both`, `transcript`. |
| `sentence_offset` | integer | no | Full transcript pagination offset (0-based). |
| `sentence_limit` | integer | no | Max sentences (max 2000). |

#### `fireflies_list_users`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| _(none)_ | | | Empty args valid. |

#### `fireflies_ingest_recent`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `limit` | integer | no | Max meetings to ingest (default 50, max 50). |
| `skip` | integer | no | Pagination offset. |
| `from` | string | no | ISO datetime — ingest on/after. Alias: `from_date`. |

Example request:

```json
{
  "environment": "staging",
  "args": {
    "limit": 50,
    "from": "2026-01-01T00:00:00Z"
  }
}
```

#### `fireflies_list_indexed`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `since` | string | no | ISO datetime — indexed meetings on/after. Alias: `from`. |
| `limit` | integer | no | Default 50, max 500. |

#### `fireflies_find_duplicates`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `limit` | integer | no | Default 50, max 50. |

#### `fireflies_correlate_meeting`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `meeting_id` | string | yes | From search or index. Aliases: `id`, `transcript_id`. |
| `user` | string | no | Calendar impersonation. Alias: `as_user`. |

Example request:

```json
{
  "environment": "staging",
  "args": {
    "meeting_id": "MEETING_ID",
    "user": "yogesh.kaurav@admedia.com"
  }
}
```

#### `fireflies_ensure_fred_invite`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `user` | string | no | Person whose calendar to scan. Aliases: `as_user`, `for_user`, `user_email`. |
| `calendar_id` | string | no | Calendar id (default `primary`). |
| `look_ahead_days` | integer | no | Days ahead (default 7, max 30). |
| `limit` | integer | no | Max events to scan (default 50, max 100). |
| `dry_run` | boolean | no | Default true — report without modifying events. |
| `force` | boolean | no | Required when `FIREFLIES_FRED_INVITE_ENABLED` is unset. |
| `fred_email` | string | no | Override Fred address (default `fred@fireflies.ai`). |
| `send_updates` | string | no | Google `sendUpdates` on patch (default `all`). |

Example request:

```json
{
  "environment": "staging",
  "args": {
    "user": "yogesh.kaurav@admedia.com",
    "look_ahead_days": 7,
    "dry_run": true,
    "force": true
  }
}
```

---

### Bitbucket

Do **not** expose the slack-bot multiplexer `bitbucket_search`. Dedicated tools below.

#### `bb_list_prs`

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `project` | string | no | Bitbucket project key (default `AD`). |
| `repo` | string | no | Repo slug; omit to scan all repos in the project. |
| `state` | string | no | `OPEN`, `MERGED`, `DECLINED`, `ALL` (default `OPEN`). |
| `author` | string | no | Author display-name substring. |
| `reviewer` | string | no | Reviewer display-name substring. |
| `merged_by` | string | no | Merger display-name substring. |
| `period` | string | no | Relative window, e.g. `last_month`. |
| `date_range` | `{ from, to }` | no | Inclusive YYYY-MM-DD window. |
| `aggregate` | string | no | e.g. `activity`. |
| `count_only` | boolean | no | Return a count only, no PR rows. |
| `sort` | string | no | `created_desc` (default), `created_asc`, `updated_desc`, `updated_asc`. |
| `limit` | integer | no | Max results (default 25, max 50). |

Example request:

```json
{
  "environment": "staging",
  "args": {
    "project": "AD",
    "state": "ALL",
    "author": "Johnty",
    "period": "last_month",
    "count_only": true,
    "sort": "created_desc",
    "limit": 25
  }
}
```

#### `bb_list_commits`

**`args`:** required `repo`; optional `project` (default `AD`), `path`, `branch`, `author`, `first`, `limit`.

Example request:

```json
{
  "environment": "staging",
  "args": {
    "project": "AD",
    "repo": "search.com",
    "path": "src/Foo.php",
    "first": false,
    "limit": 25
  }
}
```

#### `bb_file_diff`

Live file diff between two refs. Not `get_file` / `get_file_lines` (RAG chunks).

**`args`**

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `repo` | string | yes | Repo slug, e.g. `search.com`. |
| `path` | string | yes | File path, e.g. `src/Foo.php`. |
| `project` | string | no | Bitbucket project key (default `AD`). |
| `diff_from` | string | no | From ref: commit SHA, branch, or `previous` (default). |
| `diff_to` | string | no | To ref: commit SHA, branch, or `latest` (default). |

Example request:

```json
{
  "environment": "staging",
  "args": {
    "project": "AD",
    "repo": "search.com",
    "path": "src/Foo.php",
    "diff_from": "previous",
    "diff_to": "latest"
  }
}
```

---

### HubSpot CRM (`hubspot_*`) — TW-331

Private App tokens, accounts registry, timezone defaults, and write approval live on **slack-bot-service** only (`HUBSPOT_*` env). MCP registers schemas + gateway hop; no HubSpot client in business-mcp.

**Accounts** — GENERIC questions (no portal named): **omit** `account` (or pass `account=all`) so slack-bot searches all 4 portals in one call. Named portal: pass the slug or portalId. ChatGPT must report **every** portal from `by_account[]` (`account_label` + `total` / `result_count`), including zeros, and use `row.account` / `row.account_label` when listing records. Do **not** call `hubspot_list_accounts` first for generic searches. Writes (`hubspot_propose_create_note`) still **require** `account`. Pagination `after=` requires a specific account.

| Slug | Portal | Label |
| ---- | ------ | ----- |
| `influential` | 245231031 | Influential.com |
| `advertising` | 244656364 | Advertising.com |
| `adcom` | 44357896 | Ad.com (≠ advertising) |
| `monetize` | 24315290 | Monetize.com |

**All-accounts response** (when `account` is omitted or `all`): `searched_all_accounts: true`, `accounts_searched`, `by_account[]` (per-portal `account`, `account_label`, `total` / `result_count`, `next_after`, `error`), and combined `rows` (each row already has `account` / `account_label`).

**Timezone:** default **Asia/Kolkata** (GMT+5:30) for `yesterday` / `today` / week / month filters — matches HubSpot UI for India viewers. Pass `timezone` or `use_portal_timezone=true` for portal US/Eastern.

**Search tools** (`hubspot_search_contacts`, `hubspot_search_companies`, `hubspot_search_deals`, `hubspot_search_tickets`, `hubspot_search_notes`): optional `query`, date filters (`created_on`, `last_activity_on`, `month`, `closed_only`, `dealstage`, etc.), `limit` (default 50, max 200), pagination `after=next_after`. Deals: never put `closedwon` in `query` — use `closed_only` or `dealstage=closedwon`.

**Writes:** `hubspot_propose_create_note` → returns `approval_token` → `hubspot_confirm_write` (or `hubspot_cancel_write`). Gated by `HUBSPOT_WRITES_ENABLED` on slack-bot. **Requires `account`** (writes cannot target all portals).

Generic search (omit `account` — queries all 4 portals):

```json
{
  "environment": "staging",
  "args": {
    "last_activity_on": "yesterday",
    "limit": 50
  }
}
```

Named-portal search:

```json
{
  "environment": "staging",
  "args": {
    "account": "advertising",
    "last_activity_on": "yesterday",
    "limit": 50
  }
}
```

---

## Endpoint index

| Tool |
| ---- |
| `jira_search_issues` |
| `jira_get_issue` |
| `jira_list_projects` |
| `jira_get_project` |
| `jira_list_boards` |
| `jira_list_sprints` |
| `jira_list_sprint_issues` |
| `jira_list_users` |
| `jira_list_versions` |
| `confluence_search_pages` |
| `confluence_get_page` |
| `confluence_get_comments` |
| `confluence_list_children` |
| `confluence_list_spaces` |
| `confluence_list_attachments` |
| `confluence_get_versions` |
| `confluence_search_users` |
| `confluence_get_space_permissions` |
| `confluence_get_user_space_access` |
| `slack_search_messages` |
| `slack_list_channels` |
| `slack_ask` |
| `slack_get_latest_messages` |
| `slack_get_file` |
| `slack_list_users` |
| `slack_get_user` |
| `slack_check_scopes` |
| `user_map` |
| `gdrive_search_files` |
| `gdrive_read_file` |
| `gdrive_query_sheet` |
| `gdrive_list_workspaces` |
| `gdrive_drive_overview` |
| `gdrive_user_drive_info` |
| `gdrive_find_duplicates` |
| `gdrive_lookup_owned_file` |
| `gdrive_list_recent_activity` |
| `gdrive_list_permissions` |
| `gdrive_list_revisions` |
| `gdrive_list_comments` |
| `gcal_list_events` |
| `gcal_get_event` |
| `gcal_list_resources` |
| `gcal_propose_rsvp` |
| `gcal_confirm_write` |
| `gcal_cancel_write` |
| `gmail_list_users` |
| `gmail_list_labels` |
| `gmail_list_messages` |
| `gmail_get_message` |
| `gmail_list_threads` |
| `gmail_get_thread` |
| `gmail_get_attachment` |
| `gmail_propose_send` |
| `gmail_propose_reply` |
| `gmail_propose_forward` |
| `gmail_propose_draft` |
| `gmail_propose_modify_labels` |
| `gmail_confirm_write` |
| `gmail_cancel_write` |
| `fireflies_search_meetings` |
| `fireflies_get_meeting` |
| `fireflies_list_users` |
| `fireflies_ingest_recent` |
| `fireflies_list_indexed` |
| `fireflies_find_duplicates` |
| `fireflies_correlate_meeting` |
| `fireflies_ensure_fred_invite` |
| `bb_list_projects` |
| `bb_list_repos` |
| `bb_list_prs` |
| `bb_get_pr` |
| `bb_list_branches` |
| `bb_list_commits` |
| `bb_file_diff` |
| `hubspot_list_accounts` |
| `hubspot_search_contacts` |
| `hubspot_get_contact` |
| `hubspot_get_contact_related` |
| `hubspot_search_companies` |
| `hubspot_get_company` |
| `hubspot_search_deals` |
| `hubspot_get_deal` |
| `hubspot_search_tickets` |
| `hubspot_get_ticket` |
| `hubspot_list_owners` |
| `hubspot_get_owner` |
| `hubspot_search_notes` |
| `hubspot_get_note` |
| `hubspot_list_associations` |
| `hubspot_get_timeline` |
| `hubspot_search_activities` |
| `hubspot_get_attachment` |
| `hubspot_propose_create_note` |
| `hubspot_confirm_write` |
| `hubspot_cancel_write` |

All: `POST /internal/v1/tools/{tool_name}`
