# Connectors

The connector framework lands in Phase 4 (`LEGATE_AGENT_BUILD_PLAN.md` Section 9).

A connector is a Python class conforming to a common `Connector` protocol:
`validate_config`, `test_connection`, `list_tools`, and `async execute`.
Connectors register their tools at startup; adding one requires only dropping in
a conforming module — no core changes.

Planned v1 connectors: HTTP/REST, outbound Webhook, Email (IMAP/SMTP), Slack,
Stripe, Database (Postgres/MySQL), and the internal RAG knowledge base.

## Status

Implemented:

| Type | Tools | Notes |
|---|---|---|
| `http` | `http.call` | Authenticated, any method. Sensitive (write-capable). |
| `slack` | `slack.post_message`, `slack.list_channels`, `slack.reply_thread` | Slack Web API. |
| `email` | `email.send`, `email.draft`, `email.search` | SMTP/IMAP; `email.send` is sensitive. |
| `github` | `github.get_issue`, `github.list_issues`, `github.create_issue`, `github.comment_issue` | GitHub REST API; `base_url` supports Enterprise. Writes are sensitive. |
| `notion` | `notion.search`, `notion.query_database`, `notion.create_page`, `notion.append_block` | Notion API; writes are sensitive. |
| `webhook` | `webhook.send` | POST JSON to a fixed URL with optional HMAC-SHA256 signing. Sensitive. |
| `postgres` | `postgres.query`, `postgres.execute` | External PostgreSQL. `query` is read-only (SELECT/CTE only, run in a READ ONLY transaction, row-capped); `execute` is sensitive and refuses statement chaining. |
| `mysql` | `mysql.query`, `mysql.execute` | MySQL/MariaDB sibling of `postgres` (shared guard + row caps); reads run in a session READ ONLY transaction. |
| `stripe` | `stripe.get_customer`, `stripe.list_charges`, `stripe.create_refund` | Acts on a workspace's own Stripe account; `create_refund` is sensitive. Separate from the platform's internal billing. |
| `google` | `gsheets.read_range`, `gsheets.append_row`, `gdrive.list_files`, `gdrive.get_file` | Google Sheets + Drive under one OAuth2 credential (refresh-token flow). `gsheets.append_row` is sensitive. |
| `mcp` | *dynamic* | A remote **MCP server** (Streamable HTTP). Tools are discovered from the server and namespaced `mcp.<id>.<tool>`; see below. |

Each connector's write tools are marked `is_sensitive`, so they route through the
policy engine and (when configured) the human-approval inbox; reads run without
approval. New connector types register in `legate/connectors/registry.py` and are
exposed automatically through `GET /connector-types`, the schema-driven install
wizard, and agent tool allow-lists — no core changes required.

Credentials are stored with envelope encryption (`legate/vault`) and never
returned by the API. The built-in read-only `http.request` tool (no credentials)
remains available separately from the `http` connector.

### MCP servers (`mcp`)

Legate is an MCP **client**: register a remote MCP server by URL (config `url`,
optional secret `token`) and its tools become available to agents and workflows.

- **Transport:** Streamable HTTP (JSON or SSE responses). Handshake is
  `initialize` -> `notifications/initialized` -> `tools/list` / `tools/call`
  (`legate/mcp/client.py`).
- **Discovery + caching:** on install and on "test connection" the server's tools
  are fetched and cached on the connector row (`config_json.mcp_tools`), so tool
  resolution needs no network call per run. Re-test to refresh after the server's
  tools change.
- **Naming:** each tool is registered as `mcp.<connector-id8>.<tool>` (unique
  across servers, grouped under one "MCP" section in the UI); the server is still
  called with its original tool name.
- **Safety:** a tool is treated as sensitive (routed through approval) unless the
  server marks it `annotations.readOnlyHint = true`.

### SQL safety (`postgres`, `mysql`)

Both SQL connectors share `BaseSqlConnector`, so the guard is identical.
`*.query` is defended in depth: the statement is checked (single statement,
must start with `SELECT`/`WITH`, no data-modifying keywords, comments stripped
first) **and** executed inside a `READ ONLY` transaction, so a write cannot slip
through even if the text check is fooled. Results are capped (default 100 rows,
hard max 1000). `postgres.execute` permits a single data-modifying statement and,
being sensitive, is policy/approval gated.
