# Collaboration tool integration — design pack (planning)

Planning doc for exposing **Jira, Confluence, Slack, and Google Drive** through the same
`lucos-business-mcp` server. **No implementation in this pack** — contract only.

| Item | Lock |
| ---- | ---- |
| Branch | `feature/external-tool-integrations` |
| MCP server | Same `business-mcp.lucos.com` / `lucos-business-mcp` |
| Backend | **`slack-bot-service.com`** (collab integration APIs) |
| Auth | **Org tokens** per vendor (not per-user OAuth in v1) |
| MCP runtime pattern | **Gateway execute** (like `code_search` / `get_file`) |
| v1 scope | **Read-only** — search + get detail |
| Spike order | **Jira first**, then Confluence, Slack, Drive |
| Clients | ChatGPT tunnel **and** Cursor MCP (same tools) |

---

## Architecture

```
User (natural language)
  → ChatGPT / Cursor
  → business-mcp.lucos.com          tools/list + tools/call (MCP)
  → api.lucos.com                   POST /api/v1/tools/{tool_name}
                                    JWT authz + audit + org id
  → slack-bot-service.com           POST /internal/v1/tools/{tool_name}
                                    vendor API clients + org tokens
  → Jira / Confluence / Slack / Google Drive APIs
```

**What each layer owns**

| Layer | Owns |
| ----- | ---- |
| **business-mcp** | Tool registry, Zod schemas, descriptions, enablement flags, `runGatewayToolCall` routing |
| **api.lucos.com** | JWT validation, org RBAC (“may this user search Slack?”), audit log, forward to integration service |
| **slack-bot-service.com** | Org vendor tokens, vendor REST calls, JQL/CQL/Drive query translation, redaction, truncation |

MCP **never** holds vendor tokens or calls Jira/Slack/Drive directly.

---

## How natural language maps to tools

The model (ChatGPT/Cursor) chooses tools from `tools/list` — there is no NL parser in MCP.

1. User asks in plain English.
2. Model reads tool **names + descriptions** (these teach when to call what).
3. Model fills **structured args** (`query`, `issue_key`, `limit`, …).
4. Gateway + integration service call the vendor API.
5. **JSON rows** return to the model; model writes the human answer.

**Org-token implication:** every query sees the **company workspace** visible to the integration
account — not “my personal Jira” or “my Drive.” Tool descriptions must say this explicitly.

**Query translation:** MCP args use plain `query` / filters where possible. The integration
service builds JQL, CQL, Slack search syntax, and Drive `q=` — models are unreliable at vendor
query languages.

---

## v1 tool set (33 read tools)

Do **not** expose slack-bot multiplexers (`entity` on `jira_search_issues` / `jira_get_issue`, or `bitbucket_search`) as MCP tools. Dedicated names below are the contract.

| # | MCP tool | Vendor | Primary NL intents |
| - | -------- | ------ | ------------------ |
| 1 | `jira_search_issues` | Jira | find tickets, bugs, epics by keywords/filters |
| 2 | `jira_get_issue` | Jira | one ticket by key (`TW-330`) |
| 3 | `jira_list_projects` | Jira | which projects exist |
| 4 | `jira_get_project` | Jira | one project record |
| 5 | `jira_list_boards` | Jira | boards (then sprints) |
| 6 | `jira_list_sprints` | Jira | sprints on a board |
| 7 | `jira_list_sprint_issues` | Jira | tickets in a sprint |
| 8 | `jira_list_users` | Jira | people lookup (may include email) |
| 9 | `jira_list_versions` | Jira | fix versions / releases |
| 10 | `confluence_search_pages` | Confluence | find docs, runbooks, SOPs |
| 11 | `confluence_get_page` | Confluence | read one page body |
| 12 | `confluence_get_comments` | Confluence | comments on a page |
| 13 | `confluence_list_children` | Confluence | child pages / wiki tree |
| 14 | `confluence_list_spaces` | Confluence | which spaces exist |
| 15 | `confluence_list_attachments` | Confluence | attachment metadata (no bytes) |
| 16 | `confluence_get_versions` | Confluence | page version history |
| 17 | `confluence_search_users` | Confluence | people lookup |
| 18 | `confluence_get_space_permissions` | Confluence | who can read/write/admin a space |
| 19 | `confluence_get_user_space_access` | Confluence | which spaces a person can access |
| 20 | `slack_search_messages` | Slack | live message search |
| 21 | `slack_list_channels` | Slack | find by name/ID (admin search / conversations.info) |
| 22 | `slack_ask` | Slack | live search/history + channel-find |
| 23 | `slack_get_latest_messages` | Slack | latest channel messages (conversations.history) |
| 24 | `slack_get_file` | Slack | download file/screenshot by file_id |
| 25 | `slack_list_users` | Slack | workspace members, full crawl — no pagination (`users.list`) |
| 26 | `slack_get_user` | Slack | lookup by id / email / username / name |
| 27 | `slack_check_scopes` | Slack | token OAuth scope diagnostic |
| 28 | `gdrive_search_files` | Drive | find docs, sheets, decks |
| 29 | `gdrive_read_file` | Drive | read exported text from one file (Sheets: preview or one tab) |
| 30 | `gdrive_query_sheet` | Drive | structured JSON rows from a Google Sheet |
| 31 | `gdrive_list_workspaces` | Drive | Shared drives (Team Drives), not UI Workspaces |
| 32 | `gdrive_drive_overview` | Drive | left-nav overview (My Drive / Shared with me / Starred / …) |
| 33 | `gdrive_user_drive_info` | Drive | per-user Drive overview (impersonated) |
| 34 | `gdrive_find_duplicates` | Drive | likely duplicate / summary+transcript pairs |
| 35 | `gdrive_lookup_owned_file` | Drive | who owns a file (owner index) |
| 36 | `gdrive_list_recent_activity` | Drive | recent Drive Activity (views/opens) |
| 37 | `gdrive_list_permissions` | Drive | sharing permissions for one file |
| 38 | `gdrive_list_revisions` | Drive | version history for one file |
| 39 | `gdrive_list_comments` | Drive | comments/replies on one file |
| 40 | `gcal_list_events` | Calendar | upcoming/past events for a person |
| 41 | `gcal_get_event` | Calendar | one event by id |
| 42 | `gcal_list_resources` | Calendar | Workspace rooms / resources |
| 43 | `gcal_propose_rsvp` | Calendar | propose RSVP (approval token) |
| 44 | `gcal_confirm_write` | Calendar | confirm pending calendar write |
| 45 | `gcal_cancel_write` | Calendar | cancel pending calendar write |
| 46 | `gmail_list_users` | Gmail | Workspace Directory users |
| 47 | `gmail_list_labels` | Gmail | mailbox labels |
| 48 | `gmail_list_messages` | Gmail | search/list message metadata |
| 48 | `gmail_get_message` | Gmail | headers + Drive-style text window |
| 49 | `gmail_list_threads` | Gmail | list thread ids |
| 50 | `gmail_get_thread` | Gmail | paged thread messages |
| 51 | `gmail_get_attachment` | Gmail | download attachment + extract text |
| 52 | `gmail_propose_send` | Gmail | propose new email (approval token) |
| 53 | `gmail_propose_reply` | Gmail | propose reply (approval token) |
| 54 | `gmail_propose_forward` | Gmail | propose forward (approval token) |
| 55 | `gmail_propose_draft` | Gmail | propose draft (approval token) |
| 56 | `gmail_propose_modify_labels` | Gmail | propose label/archive change |
| 57 | `gmail_confirm_write` | Gmail | confirm pending Gmail write |
| 58 | `gmail_cancel_write` | Gmail | cancel pending Gmail write |
| 59 | `fireflies_search_meetings` | Fireflies | search meeting transcripts |
| 60 | `fireflies_get_meeting` | Fireflies | summary or full transcript |
| 61 | `fireflies_list_users` | Fireflies | API key owner / seat list |
| 62 | `fireflies_ingest_recent` | Fireflies | poll + upsert local index |
| 63 | `fireflies_list_indexed` | Fireflies | read local meeting index |
| 64 | `fireflies_find_duplicates` | Fireflies | same title+day, different ids |
| 65 | `fireflies_correlate_meeting` | Fireflies | Fireflies + Calendar + Drive |
| 66 | `fireflies_ensure_fred_invite` | Fireflies | add fred@fireflies.ai to Meet events |
| 67 | `bb_list_projects` | Bitbucket | projects |
| 68 | `bb_list_repos` | Bitbucket | repos in a project |
| 69 | `bb_list_prs` | Bitbucket | pull requests |
| 70 | `bb_get_pr` | Bitbucket | one PR |
| 71 | `bb_list_branches` | Bitbucket | branches |
| 72 | `bb_list_commits` | Bitbucket | commits |
| 73 | `bb_file_diff` | Bitbucket | file diff between two refs |
| 74 | `hubspot_list_accounts` | HubSpot | configured portals + token health |
| 75 | `hubspot_search_contacts` | HubSpot | contact keyword/date search |
| 76 | `hubspot_get_contact` | HubSpot | one contact |
| 77 | `hubspot_get_contact_related` | HubSpot | contact associations bundle |
| 78 | `hubspot_search_companies` | HubSpot | company search |
| 79 | `hubspot_get_company` | HubSpot | one company |
| 80 | `hubspot_search_deals` | HubSpot | deal search (month/stage filters) |
| 81 | `hubspot_get_deal` | HubSpot | one deal |
| 82 | `hubspot_search_tickets` | HubSpot | ticket search |
| 83 | `hubspot_get_ticket` | HubSpot | one ticket |
| 84 | `hubspot_list_owners` | HubSpot | CRM owners |
| 85 | `hubspot_get_owner` | HubSpot | one owner |
| 86 | `hubspot_search_notes` | HubSpot | note search |
| 87 | `hubspot_get_note` | HubSpot | one note |
| 88 | `hubspot_list_associations` | HubSpot | object associations |
| 89 | `hubspot_get_timeline` | HubSpot | merged activity timeline |
| 90 | `hubspot_search_activities` | HubSpot | engagement search |
| 91 | `hubspot_get_attachment` | HubSpot | file metadata |
| 92 | `hubspot_propose_create_note` | HubSpot | propose note write (approval token) |
| 93 | `hubspot_confirm_write` | HubSpot | confirm pending write |
| 94 | `hubspot_cancel_write` | HubSpot | cancel pending write |
| 95 | `user_map` | Directory | correlate Slack members with the Google Workspace directory |

`bb_*` is live Bitbucket Server, distinct from `monitor_bitbucket_activity` (Admin DB aggregates).

All list tools: default **limit 25**, max **50**. All body/content fields: max **32 KiB** text;
set `truncated: true` when clipped.

---

## Gateway route contract

Every collaboration tool uses the same gateway path shape as engineering tools:

```http
POST /api/v1/tools/{tool_name}
Authorization: Bearer <Lucos JWT>
Content-Type: application/json

{
  "environment": "staging",
  ...tool-specific args...
}
```

**Success (200):** JSON body returned verbatim to MCP → model.

**Failure:** `401/403` authz, `502/504` upstream, `422` bad args — MCP surfaces reason text.

Gateway resolves **org id** from JWT and passes it to the integration service (header or body).
Integration service loads the org’s vendor token from vault.

---

## Global response envelope

All tools return a consistent shape so the model and tests stay predictable:

```json
{
  "tool": "jira_search_issues",
  "ticket": "TW-XXX",
  "source": "collaboration",
  "environment": "staging",
  "result_count": 12,
  "limit": 25,
  "capped": false,
  "note": "Org-wide AdMedia Jira workspace via integration account.",
  "rows": []
}
```

Single-record tools (`jira_get_issue`, `jira_get_project`, `confluence_get_page`, `gdrive_read_file`, `gcal_get_event`, `gmail_get_message`, `fireflies_get_meeting`, `bb_get_pr`) use a `record`
object instead of `rows`, same outer fields otherwise.

**Global deny fields (never in any response):** OAuth tokens, refresh tokens, raw attachment
bytes, full user email addresses (use display name or redacted), signed download URLs with
embedded secrets, webhook secrets.

---

## Tool contracts

### 1. `jira_search_issues`

**When the model should use it:** User mentions tickets, bugs, stories, epics, sprints, assignee,
status, or keywords without a single `TW-123` key.

**Example NL prompts**

- “Open Jira tickets about publisher onboarding”
- “Bugs in AdPilot still in progress”
- “Tickets updated this week mentioning budget cap”
- “TAS tickets from yesterday”

**MCP args**

| Arg | Type | Required | Default | Description |
| --- | ---- | -------- | ------- | ----------- |
| `query` | string | no* | — | Free-text search; required unless a filter is set |
| `project` | string | no | — | e.g. `TW` or `TAS` — narrows JQL |
| `status` | string | no | — | e.g. `open`, `done`, `in progress` — mapped to JQL |
| `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. `High` |
| `issue_type` | string | no | — | e.g. `Bug` |
| `issue_key` | string | no | — | Exact key |
| `due_date` | date | no | — | ISO date `YYYY-MM-DD` |
| `period` | string | no | — | `yesterday`, `today`, `this_week`, `last_month`, `last_7_days`, `last_N_days` |
| `date_range` | object | no | — | `{ from, to }` YYYY-MM-DD |
| `date_field` | string | no | — | `created`, `updated`, or `due` |
| `updated_since` | date | no | — | ISO date `YYYY-MM-DD` |
| `stale_days` | int | no | — | Open tickets not updated in N days (not with `period`) |
| `sort` | string | no | newest | `oldest` / `created` / `updated` / `resolved` |
| `start_at` | int | no | 0 | Pagination offset |
| `next_page_token` | string | no | — | From previous `next_page_token` |
| `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` |
| `count_only` | bool | no | false | Count only, no issue rows |
| `comments` | bool | no | false | Expand comments |
| `is_pr_request` | bool | no | false | Attach Bitbucket PRs |
| `assignee_from` / `assignee_to` / `assigned_by` | string | no | — | Assignee changelog |
| `changed_field` + `changed_from` / `changed_to` / `changed_by` | string | no | — | Generic changelog |
| `closed_by` | string | no | — | Who closed/Done the issue |
| `priority_from` / `priority_to` | string | no | — | Priority changelog |
| `commented_by` / `mentioned` | string | no | — | Comment author / mention |
| `limit` | int | no | 25 | Max 50 |
| `environment` | enum | no | staging | `staging` \| `prod` |

**Integration service → Jira API**

```http
GET /rest/api/3/search/jql
  ?jql=<built from args>
  &maxResults={limit}
  &fields=summary,status,assignee,issuetype,updated,project
```

Example built JQL (internal, not exposed to model):

```text
project = TW AND text ~ "publisher onboarding" AND statusCategory != Done ORDER BY updated DESC
```

**Response `rows[]` fields**

| Field | Example |
| ----- | ------- |
| `key` | `TW-331` |
| `summary` | `List publishers onboarded in date window` |
| `status` | `Done` |
| `issue_type` | `Story` |
| `project` | `TW` |
| `assignee` | `alice` (display name, not email) |
| `updated` | `2026-08-14` |
| `url` | `https://admedia-jira.atlassian.net/browse/TW-331` |

---

### 2. `jira_get_issue`

**When:** User gives a ticket key (`TW-330`) or asks for full detail on one issue.

**Example NL prompts**

- “What's the status of TW-330?”
- “Show me the description on TW-309”
- “Who is assigned to TW-331 and what's linked?”

**MCP args**

| Arg | Type | Required | Default | Description |
| --- | ---- | -------- | ------- | ----------- |
| `issue_key` | string | yes | — | e.g. `TW-330` |
| `include_comments` | bool | no | false | Last N comments only (max 10) |
| `include_changelog` | bool | no | false | Status/field change history |
| `include_subtasks` | bool | no | false | Child sub-tasks (key, summary, status, issue_type) |
| `environment` | enum | no | staging | |

**Integration service → Jira API**

```http
GET /rest/api/3/issue/{issue_key}
  ?expand=renderedFields,names
  [&comment limit if include_comments]
```

**Response `record` fields**

| Field | Notes |
| ----- | ----- |
| `key`, `summary`, `status`, `issue_type`, `project` | Same as search |
| `assignee`, `reporter` | Display names |
| `description` | Plain text, max 8 KiB |
| `labels` | string[] |
| `linked_issues` | `{ key, summary, link_type }[]` max 20 |
| `comments` | Only if requested; `{ author, posted_at, body }[]` max 10, body 2 KiB each |
| `subtasks` | Only if `include_subtasks`; `{ key, summary, status, issue_type }[]` max 50 |
| `url` | Browse link |

---

### 3. `confluence_search_pages`

**When:** User asks for documentation, wiki pages, runbooks, SOPs, design packs.

**Example NL prompts**

- “Confluence docs on MNGT RO views”
- “Publisher onboarding SOP”
- “Staging deploy runbook for business MCP”

**MCP args**

| Arg | Type | Required | Default | Description |
| --- | ---- | -------- | ------- | ----------- |
| `query` | string | no* | — | Keywords; service builds CQL. Optional if another filter is set. **Not CQL.** |
| `title` | string | no | — | Page title substring |
| `creator` | string | no | — | Creator display name |
| `labels` | string | no | — | Comma-separated labels |
| `ancestor` | string | no | — | Parent page id (digits) |
| `created_after` / `created_before` | date | no | — | YYYY-MM-DD |
| `last_modified_after` / `last_modified_before` | date | no | — | YYYY-MM-DD |
| `type` | enum | no | page | `page` or `blogpost` |
| `space_key` | string | no | — | e.g. `EN`, `PM1` |
| `limit` | int | no | 25 | Max 50 |
| `start` | int | no | 0 | Pagination offset |
| `environment` | enum | no | staging | |

**Integration service → Confluence API**

```http
GET /wiki/rest/api/content/search
  ?cql=<built from args>
  &limit={limit}
  &expand=space,version,history.lastUpdated
```

Example built CQL:

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

**Response `rows[]` fields**

| Field | Example |
| ----- | ------- |
| `page_id` | `633372693` |
| `title` | `MNGT RO Views Design` |
| `space_key` | `EN` |
| `space_name` | `Engineering` |
| `excerpt` | First ~300 chars of matched snippet |
| `last_updated` | `2026-08-10` |
| `url` | Wiki browse URL |

---

### 4. `confluence_get_page`

**When:** User wants full page content after search, or provides a page URL/id.

**Example NL prompts**

- “What does the MNGT deployment doc say about PM2?”
- “Read Confluence page 633372693”
- “Summarize the publisher onboarding SOP”

**MCP args**

| Arg | Type | Required | Default | Description |
| --- | ---- | -------- | ------- | ----------- |
| `page_id` | string | one of | — | Confluence content id |
| `page_url` | string | one of | — | Wiki URL (service extracts id) |
| `environment` | enum | no | staging | |

**Integration service → Confluence API**

```http
GET /wiki/rest/api/content/{page_id}
  ?expand=body.storage,space,version,history.lastUpdated
```

Service converts `body.storage` HTML → **plain text or markdown**, truncates to 32 KiB.

**Response `record` fields**

| Field | Notes |
| ----- | ----- |
| `page_id`, `title`, `space_key`, `url` | |
| `last_updated` | ISO date |
| `body_text` | Plain text / markdown, max 32 KiB |
| `truncated` | bool |

---

### 4.1 `confluence_get_comments`

**When:** Comments / review thread on a wiki page.

**MCP args:** `page_id` or `page_url`, optional `limit` / `start`.

**Response `rows[]`:** `comment_id`, `page_id`, `author`, `created_at`, `last_updated`, `body_text`, `truncated`, `url`.

### 4.2 `confluence_list_children`

**When:** Child pages under a parent / wiki tree.

**MCP args:** `page_id` or `page_url`, optional `limit` / `start`.

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

### 4.3 `confluence_list_spaces`

**When:** Which Confluence spaces exist, or resolve EN / PM1.

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

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

### 4.4 `confluence_list_attachments`

**When:** Files attached to a page. Metadata only — no bytes, no signed download URLs.

**MCP args:** `page_id` or `page_url`, optional `limit` / `start`.

**Response `rows[]`:** `attachment_id`, `page_id`, `filename`, `media_type`, `file_size`, `last_updated`, `url`. Drop `download_url` if it is a signed secret.

### 4.5 `confluence_get_versions`

**When:** Who edited a page / version history.

**MCP args:** `page_id` or `page_url`, optional `limit` / `start`.

**Response `rows[]`:** `page_id`, `number`, `when`, `by`, `message`, `minor_edit`.

### 4.6 `confluence_search_users`

**When:** Find a Confluence person by name (not `jira_list_users`).

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

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

### 4.7 `confluence_get_space_permissions`

**When:** Who can read / write / admin a wiki space (e.g. EN, PM1).

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

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

### 4.8 `confluence_get_user_space_access`

**When:** Which spaces a Confluence person can access, or whether they can access one space.

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

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

---

### 5. `slack_search_messages`

**When:** User asks what was said in Slack, deploy discussions, channel chatter.

**Example NL prompts**

- “Slack messages about lucos_ro views last week”
- “What did #adops say about staging MCP deploy?”
- “Threads mentioning TW-330”

**MCP args**

| Arg | Type | Required | Default | Description |
| --- | ---- | -------- | ------- | ----------- |
| `query` | string | yes | — | Search terms |
| `channel` | string | no | — | Channel name (`adops`) or id — narrows search |
| `date_from` | date | no | — | ISO date |
| `date_to` | date | no | — | ISO date |
| `limit` | int | no | 25 | Max 50 |
| `environment` | enum | no | staging | |

**Integration service → Slack API**

```http
GET /api/search.messages
  ?query=<built: terms + optional channel: + after:/before:>
  &count={limit}
```

Requires a Slack **user token** (`xoxp-`, `SLACK_USER_TOKEN`) with **`search:read`**. Bot tokens (`xoxb-`) cannot call `search.messages` and fail with `missing_scope`. If that happens, slack-bot-service falls back to `slack_ask` (indexed history) and sets `fallback: "slack_ask"`. Prefer `slack_ask` from MCP for Slack questions. The user token’s account must also be in the channel.

**Response `rows[]` fields**

| Field | Example |
| ----- | ------- |
| `channel` | `#adops` |
| `channel_id` | `C0123...` |
| `user` | `bob` (display name) |
| `posted_at` | ISO timestamp |
| `text` | Message text, max 4 KiB |
| `permalink` | Slack archive link |
| `thread_ts` | Present if reply — for future `slack_get_thread` |

**v1 deny:** DMs, file attachments with download URLs, email addresses in `@mentions`.

---

### 6. `slack_list_channels`

**When:** Model needs to find a channel by name or ID before history/search.

**Example NL prompts**

- “Find the Slack channel named testing-team”
- “What is the channel ID for community?”
- “List channels with 'lucos' in the name”

**MCP args**

| Arg | Type | Required | Default | Description |
| --- | ---- | -------- | ------- | ----------- |
| `query` | string | no | — | Channel name (`testing-team`) or id (`C0BP7EVKU0K`) |
| `limit` | int | no | 50 | Max 100 |
| `environment` | enum | no | staging | |

**Integration service → Slack API**

Fast lookup via `admin.conversations.search` or `conversations.info` — not a full `conversations.list` workspace crawl.

**Response `rows[]` fields**

| Field | Example |
| ----- | ------- |
| `channel_id` | `C0123...` |
| `name` | `adops` |
| `is_private` | false |
| `member_count` | 42 |
| `topic` | Truncated topic, max 200 chars |

---

### 6.1 `slack_get_latest_messages`

**When:** User wants complete/latest messages from a known channel.

**Example NL prompts**

- “Complete message from #Private-channel about bid limit”
- “Latest messages in testing-team”
- “What was just posted in C0BP7EVKU0K”

**MCP args**

| Arg | Type | Required | Default | Description |
| --- | ---- | -------- | ------- | ----------- |
| `channel` | string | yes | — | Channel name (`testing-team`) or id (`C0BP7EVKU0K`) |
| `query` | string | no | — | Optional text filter within fetched messages |
| `limit` | int | no | 25 | Max 50 |
| `include_threads` | bool | no | false | Include thread replies |
| `environment` | enum | no | staging | |

**Integration service → Slack API**

Resolve the channel (admin search / `conversations.info`), then `conversations.history`. If the primary token is not in the channel, retry with the org bot token.

**Response `rows[]` fields**

| Field | Example |
| ----- | ------- |
| `channel` | `testing-team` |
| `channel_id` | `C0BP7EVKU0K` |
| `user` | display name |
| `posted_at` | ISO timestamp |
| `text` | Full message text |
| `thread_ts` | Present if reply |

Returns full `text`.

---

### 6.2 `slack_list_users`

**When:** User wants workspace members, guests, bots, or deactivated accounts.

**MCP args:** optional `status` (`active`|`deactivated`|`all`), `include_deactivated` (default true, ignored when `status` is set), `user_type` (`bot`|`guest`|`external`|`admin`|`human`), `name_prefix` (alias `starts_with`), `updated_after` / `updated_before`, `include_details` (adds `tz`/`avatar_url`, default false). No pagination — always returns every matching user.

### 6.3 `slack_get_user`

**When:** User asks who a Slack person is.

**MCP args:** at least one of `user_id` / `user`, `email`, `username`, or `name` (fuzzy real/display name match).

### 6.4 `slack_check_scopes`

**When:** Slack tools fail with `missing_scope` or private-channel access issues. Not for ordinary Slack questions.

### 6.5 `user_map`

**When:** User asks to correlate/cross-reference Slack members with the Google Workspace directory (e.g. who's on Slack but not in Google, or vice versa). MCP auto-compares with `monitor_identity_lookup` and attaches portal/Jira/Bitbucket ids.

**MCP args:** optional `include_deactivated_slack` (default true), `include_deleted_google` (default false), `matched_only`, `unmatched_only`, `include_service_accounts` (default true), `include_unmatched_google` (default true).

Match order: exact company email → exact username vs Google email local-part → corroborated Google alias. Display-name matching is never used. Response flags `slack_email_scope_granted` (false disables exact-email matching), plus per-row `is_service_or_shared_mailbox`/`is_test_account` naming heuristics — verify before excluding real employees.

---

### 7. `gdrive_search_files`

**When:** User asks for files, spreadsheets, decks, checklists on Drive.

**Example NL prompts**

- “Drive docs about business MCP architecture”
- “Latest staging deploy checklist spreadsheet”
- “Lucos Q3 planning deck”

**MCP args**

| Arg | Type | Required | Default | Description |
| --- | ---- | -------- | ------- | ----------- |
| `query` | string | no* | — | Keywords; service builds Drive `q`. Optional when `scope` / folder / `mime_type` / `owned_by` / `modified_by` is set. |
| `mime_type` | string | no | — | `document`, `spreadsheet`, `presentation`, `pdf`, `folder`, `video`, `image`, `audio`, `exe`, or a raw Google MIME type |
| `folder_id` | string | no | — | Restrict to Shared Drive folder allowlist |
| `folder_name` | string | no | — | Resolve a folder by name, then list children |
| `recursive` | boolean | no | false | Folder inventory: list children recursively (`folder_id` or `folder_name` required) |
| `max_depth` | int | no | 3 | Recursive depth (max 12) |
| `max_total` | int | no | 200 | Recursive/folder inventory cap (max 500) |
| `owned_by` | string | no | — | Filter files owned by this person (name or email) |
| `modified_by` | string | no | — | Last editor display name or email |
| `limit` | int | no | 25 | Max 100 |
| `environment` | enum | no | staging | |

**Integration service → Google Drive API**

```http
GET /drive/v3/files
  ?q=<built from args + trashed=false + allowlisted shared drives>
  &pageSize={limit}
  &fields=files(id,name,mimeType,modifiedTime,owners,webViewLink)
  &supportsAllDrives=true
  &includeItemsFromAllDrives=true
```

**Response `rows[]` fields**

| Field | Example |
| ----- | ------- |
| `file_id` | `1abc...` |
| `name` | `STAGING-DEPLOY checklist` |
| `mime_type` | `application/vnd.google-apps.spreadsheet` |
| `modified` | ISO date |
| `owners` | `["platform-team"]` (group/name, not raw email) |
| `web_view_link` | Drive UI URL |

Scope v1 to **allowlisted Shared Drives** configured per org — not arbitrary My Drive.

---

### 8. `gdrive_read_file`

**When:** User wants content of a specific file after search, or names a known file.

**Example NL prompts**

- “What's in the staging deploy checklist?”
- “Summarize the Lucos architecture doc on Drive”
- “Read file 1abc...”

**MCP args**

| Arg | Type | Required | Default | Description |
| --- | ---- | -------- | ------- | ----------- |
| `file_id` | string | yes | — | From search results |
| `sheet_name` | string | no | — | Google Sheets only: one tab as CSV |
| `as_user` | string | no | — | Impersonate this Google user; if omitted, read uses owner from index |
| `user` | string | no | — | Alias for `as_user` |
| `environment` | enum | no | staging | |

**Google Sheets without `sheet_name`:** returns a preview (`sheet_names`, per-tab `row_count`, `note`) — not all tabs concatenated. Use `gdrive_query_sheet` for filtered JSON or pass `sheet_name`.

**Integration service → Google Drive API**

| MIME | Export |
| ---- | ------ |
| Google Docs | `GET /files/{id}/export?mimeType=text/plain` |
| Google Sheets | XLSX export; one tab as CSV when `sheet_name` set; preview when omitted |
| Google Slides | Export plain text or skip with “use web link” |
| PDF | Extract text server-side with size cap |

**Response `record` fields**

| Field | Notes |
| ----- | ----- |
| `file_id`, `name`, `mime_type`, `web_view_link` | |
| `content_type` | `text/plain` or `text/csv` |
| `content` | Text/CSV or null for Sheets preview |
| `truncated` | bool |
| `sheet_preview`, `sheets`, `note` | Sheets preview when no `sheet_name` |

**v1 deny:** inline binary, full multi-sheet dumps, files outside allowlisted drives.

---

### 9. `gdrive_query_sheet`

**When:** User needs filtered or counted rows from a Google Sheet (e.g. “how many bugs are Closed?”).

**MCP args:** `file_id` (required), optional `sheet_name`, `filters` (column → value object), `limit` (default 50, max 200), `offset` (row offset into matches), `as_user` / `user` (alias).

**Response:** top-level `rows` (objects keyed by header), `headers`, `total_matched`, `status_counts` (when a Status column exists), pagination fields.

---

### 10. `gdrive_user_drive_info`

**When:** User asks what is on **a specific person's** Drive.

**MCP args:** `user` (name or email; aliases `as_user` / `for_user` / `user_email`), optional `sample_limit` (default 8, max 25), `include_hidden`.

Returns the same left-nav samples as `gdrive_drive_overview` plus `index_stats` for that owner.

---

### 10. `gdrive_find_duplicates`

**When:** Duplicate files or Fireflies summary + transcript pairs on one person's Drive.

**MCP args:** `as_user` or `user`, optional `query` / `name_contains`, `limit` (default 300, max 500).

---

### 11. `gdrive_lookup_owned_file`

**When:** “Who owns this file?” Uses the local owner index (no live Drive call).

**MCP args:** `query` / `file_name` / `name` (title) **or** `file_id`.

---

### 12. `gdrive_list_permissions`

**When:** “Who has access to this file?” Sharing / ACL for one Drive file.

**MCP args:** `file_id` (required), optional `user` / `as_user`, `limit` (default 100, max 100). Org id is header `X-Lucos-Org-Id`, not an arg.

---

### 13. `gdrive_list_revisions`

**When:** Version history for one Drive file.

**MCP args:** `file_id` (required), optional `user` / `as_user`, `limit` (default 50, max 200). Org id is header `X-Lucos-Org-Id`, not an arg.

---

### 14. `gdrive_list_comments`

**When:** Comments and replies on one Drive file.

**MCP args:** `file_id` (required), optional `user` / `as_user`, `include_deleted`, `limit` (default 50, max 100). Org id is header `X-Lucos-Org-Id`, not an arg.

---

### 15. `gcal_list_events` / `gcal_get_event` / `gcal_list_resources` / `gcal_propose_rsvp`

**When:** Calendar schedule or one meeting.

**MCP args (list):** `user`, optional `time_min` / `time_max` (default now → +7 days), `query` / `title`, `also_user`, `calendar_id`, `limit` (default 25, max 100).

**MCP args (get):** `event_id`, `user`, optional `calendar_id`.

### `gcal_list_resources`

**When:** Find a meeting room / resource calendar.

**MCP args:** optional `query` / `q` / `name` (substring, e.g. `conf`), `limit` (default 50, max 100), `after` / `page_token`.

### `gcal_propose_rsvp` / `gcal_confirm_write` / `gcal_cancel_write`

**When:** Accept / decline / tentative an invite. Propose returns `approval_token`; nothing hits Google until `gcal_confirm_write`.

**MCP args (propose):** `user`, `event_id`, `response` (`accepted` | `declined` | `tentative` | `needsAction`). Optional `calendar_id`, `send_updates` (`all` | `externalOnly` | `none`).

**MCP args (confirm/cancel):** `approval_token` (alias `token`).

---

### 16. Gmail (`gmail_*`)

**When:** Org mailbox search, threads, attachments, Workspace user directory, or gated send/reply/forward/draft/label. Google Vault is out of scope.

#### `gmail_list_users`

**MCP args:** optional `query` / `q` (Directory name/email search), `domain`, `include_suspended` (default false), `after` / `page_token`, `limit` (default 50, max 200). One page of Workspace users (Admin Directory `users.list`). Pass `after`=`next_after` for the next page.

#### `gmail_list_labels`

**MCP args:** optional `user` / `as_user` / `for_user` / `user_email` (default = requester).

#### `gmail_list_messages`

**MCP args:** optional `user` aliases, `query` / `q`, `folder` / `mailbox` (`inbox` | `sent` | `trash` | `archive` | `drafts` | `spam` | `starred` | `important`), `from`, `to`, `after` (pagination token from `next_after` if no hyphen, else date `YYYY-MM-DD`; aliases `page_token`, `newer_than`), `before` / `older_than`, `has_attachment`, `is_unread`, `label_id` / `label_ids`, `limit` (default 5, max 20). One page per call; pass `after`/`page_token`=`next_after` for the next page. Then `gmail_get_message` / `gmail_get_thread`.

#### `gmail_get_message`

**MCP args:** `message_id` (alias `id`), optional `user` aliases, `include_body` (default true). Text window only; fat HTML/inline images are skipped (snippet). Attachments are off unless `GMAIL_ATTACHMENTS_ENABLED`.

#### `gmail_list_threads`

**MCP args:** same search args as `gmail_list_messages`. Then `gmail_get_thread`.

#### `gmail_get_thread`

**MCP args:** `thread_id` (alias `id`), optional `user` aliases, `include_body` (default false), `offset` (default 0), `limit` (default 5, max 20). One page of thread messages per call.

#### `gmail_get_attachment`

**MCP args:** `message_id`, `attachment_id` (from `gmail_get_message` `attachments[]`), optional `filename`, `mime_type`, `sheet_name`, `offset`, `extract_text`, `raw`, `max_bytes`, `user` aliases. Text extracted automatically (DOCX/PDF/XLSX, not base64) unless `raw=true`. Pass `offset` when `next_offset` returned.

#### `gmail_propose_send` / `gmail_propose_reply` / `gmail_propose_forward` / `gmail_propose_draft` / `gmail_propose_modify_labels` / `gmail_confirm_write` / `gmail_cancel_write`

**When:** Send, reply, forward, draft, or change labels. Propose returns `approval_token`; nothing hits Gmail until `gmail_confirm_write`. Writes gated by slack-bot `GMAIL_WRITES_ENABLED`. Bodies redact secrets when `GMAIL_REDACT_SECRETS`.

**MCP args (send):** `user` aliases, `to` / `recipients` / `recipient` (string or array), optional `cc` / `bcc`, `subject`, `body` / `text`.

**MCP args (reply):** `user` aliases, `message_id`, `body` / `text`, optional `reply_all`.

**MCP args (forward):** `user` aliases, `message_id`, `to` / `recipients` / `recipient`, optional `body` / `text`.

**MCP args (draft):** `user` aliases, optional `to`, at least `subject` or `body` / `text`.

**MCP args (modify_labels):** `user` aliases, `message_id`, optional `add_labels` / `remove_labels` (string or array), `archive` (removes INBOX), `unarchive`.

**MCP args (confirm/cancel):** `approval_token` (alias `token`). Do not use Jira/Confluence `confirm=true` stash.

---

### 17. Fireflies (`fireflies_*`)

**When:** Meeting transcripts / standup notes in Fireflies; local index; cross-system correlation.

#### `fireflies_search_meetings`

**MCP args:** optional `query` / `keyword` (spoken words + title; preferred), `title` (title-only), `scope` / `keyword_scope` (`title` | `sentences` | `all`), `from` / `to` (ISO; aliases `from_date`/`time_min`, `to_date`/`time_max`), `organizers` / `participants` (emails), `organizer_email` / `host_email` / `participant_email` / `user`, `mine`, `limit` (default 20, max 50), `skip`. Does not default to requester email.

#### `fireflies_get_meeting`

**MCP args:** `meeting_id` (aliases `id`, `transcript_id`), optional `mode` (`summary` default, `full`, `both`, `transcript`), optional `sentence_offset` / `sentence_limit` (max 2000) for full transcript pagination.

#### `fireflies_list_users`

**MCP args:** optional `environment` only. Empty args valid.

#### `fireflies_ingest_recent`

**MCP args:** optional `limit` (default 50, max 50), `skip`, `from` / `from_date` (ISO datetime). Empty args valid.

#### `fireflies_list_indexed`

**MCP args:** optional `since` / `from` (ISO datetime), `limit` (default 50, max 500). Empty args valid. No title query — that is `fireflies_search_meetings`.

#### `fireflies_find_duplicates`

**MCP args:** optional `limit` (default 50, max 50), `environment`. Empty args valid.

#### `fireflies_correlate_meeting`

**MCP args:** `meeting_id` (aliases `id`, `transcript_id`), optional `user` / `as_user` / `for_user`, `environment`.

#### `fireflies_ensure_fred_invite`

**MCP args:** optional `user` / `as_user` / `for_user`, `calendar_id`, `look_ahead_days` (default 7, max 30), `limit` (max 100), `dry_run` (default true), `force` (required unless `FIREFLIES_FRED_INVITE_ENABLED`), `fred_email`, `send_updates`.

---

## Multi-tool chains (examples)

| User question | Tool sequence |
| ------------- | ------------- |
| “TW-330 status and linked design doc” | `jira_get_issue` → parse links → `confluence_get_page` |
| “Tickets about onboarding + Slack discussion” | `jira_search_issues` → `slack_search_messages` with same keywords |
| “Drive sidebar / Shared drives vs My Drive” | `gdrive_drive_overview` then `gdrive_search_files` with `scope` / `drive_id` |
| “What's on Alice's Drive?” | `gdrive_user_drive_info` then `gdrive_search_files` with `as_user` |
| “List everything in this Drive folder” | `gdrive_search_files` with `folder_id` / `folder_name` + `recursive=true` |
| “Who owns this file / duplicates on Drive?” | `gdrive_lookup_owned_file` or `gdrive_find_duplicates` |
| “Who has access / version history / comments on this file?” | `gdrive_list_permissions` / `gdrive_list_revisions` / `gdrive_list_comments` with `file_id` + `user` |
| “Alice's calendar / this meeting notes” | `gcal_list_events` → `gcal_get_event` and/or `fireflies_search_meetings` → `fireflies_get_meeting` |
| “Find a conf room / RSVP to this invite” | `gcal_list_resources` and/or `gcal_propose_rsvp` → `gcal_confirm_write` |
| “Alice's inbox / this thread / this attachment” | `gmail_list_messages` → `gmail_get_message` / `gmail_get_thread`; `gmail_get_attachment` |
| “Send / reply / archive this mail” | `gmail_propose_send` / `gmail_propose_reply` / `gmail_propose_modify_labels` → `gmail_confirm_write` |
| “Indexed Fireflies + calendar/Drive bundle” | `fireflies_list_indexed` → `fireflies_correlate_meeting` |
| “Ensure Fred records upcoming Meets” | `fireflies_ensure_fred_invite` with `user` + optional `dry_run` |
| “Confluence MNGT doc + related Jira epic” | `confluence_search_pages` → `jira_search_issues` with title keywords |

No composite mega-tool — the model chains small tools using URLs/keys in responses.

---

## MCP registry sketch (for later implementation)

Each tool entry in `server/registry.ts`:

- `sourceSystem: 'collaboration'` (new enum value)
- `strictArgs: true` (match cake pattern — reject invented filters)
- Routed via `runGatewayToolCall` (extend `isGatewayExecuteTool`)
- `enabled: false` in `policies/tool_enablement.json` until staging OAuth/tokens exist

Descriptions must include: org scope, read-only, row limits, companion tools.

---

## Integration service API (separate repo)

Internal contract between gateway and integration service (exact path TBD):

```http
POST /internal/v1/execute
Authorization: Bearer <service-to-service token>
X-Lucos-Org-Id: <org uuid>

{
  "tool": "jira_search_issues",
  "environment": "staging",
  "args": { "query": "publisher onboarding", "limit": 25 }
}
```

Response: same envelope as gateway returns to MCP.

---

## Phased delivery

| Phase | Deliverable |
| ----- | ----------- |
| **0** | This doc + integration service skeleton + org token vault design |
| **1** | `jira_search_issues` + `jira_get_issue` end-to-end on staging |
| **2** | Confluence search + get page |
| **3** | Slack search + list channels |
| **4** | Drive search + read file |
| **5** | MCP registry entries + enablement + staging smoke prompts |

Write tools (create ticket, post message, upload file) — **out of scope for v1**.

---

## Open items (decide before Phase 1 code)

| # | Question | Proposed default |
| - | -------- | ---------------- |
| 1 | Integration service name / base URL? | TBD — e.g. `collab-api.internal.lucos.com` |
| 2 | Atlassian: one org token for Jira + Confluence? | Yes — same Atlassian site |
| 3 | Slack: which channels is bot in? | Document allowlist; search only those |
| 4 | Drive: which Shared Drive IDs? | Org config table, not hardcoded in MCP |
| 5 | Gateway RBAC: per-tool or per-vendor? | Per-tool enablement (matches existing policy) |
| 6 | Ticket id for epic? | Assign TW-XXX when Jira epic is created |

---

## References

| Topic | Path |
| ----- | ---- |
| Gateway execute pattern | `server/tool-runtime.ts`, `broker_client/index.ts` |
| Engineering tools precedent | `AGENTS.md` § Engineering code tools |
| Tool enablement | `policies/tool_enablement.json` |
| Staging deploy | `docs/STAGING-DEPLOY.md` |
