Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/lifeboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
| [Bridge setup](./bridge.md) | Running turns through the AI subscriptions you already pay for |
| [QA guide](./qa-guide.md) | Testing it — automated checks and a manual test plan |
| [Persistence audit](./persistence-audit.md) | How state is stored, and what was wrong with it |
| [Integrations plan](./integrations-plan.md) | How to reach Gmail, Drive, Dropbox and Maps — MCP, not connectors |
| [Package README](../../packages/lifeboard/README.md) | Building on it: architecture and layout |

## The design deck
Expand Down
194 changes: 194 additions & 0 deletions docs/lifeboard/integrations-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Plan: third-party integrations

*How lifeboard should reach Gmail, Drive, Dropbox, Maps and everything after them.*

## The short version

We have been building the expensive version of a solved problem.

`src/google/` is a hand-rolled OAuth client, a calendar client and a Gmail client: PKCE, token
refresh, scope management, response mapping — several hundred lines, per provider, that we now own
forever. Dropbox would be that again. Maps would be that again.

Meanwhile the bridge already runs Claude Code, Codex CLI and Gemini CLI, **and those are already MCP
clients**. Any Model Context Protocol server the household has configured — Gmail, Drive, Dropbox,
Maps, Slack, a home-automation bridge — is reachable through the agent we are already talking to,
authenticated as them, with no connector code from us.

The plan is therefore: **MCP is the integration substrate. Hand-rolled clients are the exception,
justified only where MCP cannot reach.**

---

## Three tiers, in the order we should prefer them

### Tier 1 — through the bridge, to the agent's own MCP servers *(no integration code from us)*

The household configures an MCP server once in their CLI agent. The bridge asks that agent to do the
work. We write no OAuth, hold no tokens, and inherit every integration that ecosystem produces.

```
lifeboard ──HTTP──▶ bridge ──stdio──▶ claude ──MCP──▶ gmail-mcp-server ──▶ Gmail
└─MCP──▶ gdrive-mcp-server ──▶ Drive
└─MCP──▶ maps-mcp-server ──▶ Places API
```

What this costs us: a way to *ask* for a tool call rather than a conversation, and a way to know
what tools exist. Both are small.

**Why this is the right default:** the auth lives where the person already granted it, on their own
machine, in a process they started. Nothing new to review, nothing new to store, nothing new to leak.

### Tier 2 — remote MCP servers, straight from the browser *(no bridge)*

MCP is not only a stdio protocol; servers can be remote and speak HTTP with SSE, with OAuth for
authorisation. A browser can be an MCP client. That gives the no-bridge install the same integration
surface, at the cost of an OAuth dance per server — but one dance, against a standard, rather than a
bespoke client per provider.

### Tier 3 — hand-rolled, in the browser *(the exception)*

Justified only when a thing must work **with no bridge and no network round trip through an agent**:

- **Calendar read**, because the week board must render offline from local records and stay correct.
- **Gmail read for triage**, because triage is a deterministic pipeline (classify → extract →
reconcile) that must not cost a model call per message.

That is what `src/google/` already is, and it should stop there. It is not a pattern to extend to
Dropbox, Maps, or anything else.

---

## What has to be built

### 1. Tool discovery and invocation on the bridge

```
GET /tools -> { tools: ToolManifest[] }
POST /tools/:server/:tool -> { result } // arguments in the body
```

```ts
interface ToolManifest {
server: string; // "gmail", "gdrive"
name: string; // "search_messages"
title: string; // human-readable, for the permission prompt
description: string;
inputSchema: JsonSchema; // straight from the MCP server
/** Which agent exposes it, since two agents may have different servers configured. */
via: string;
}
```

**Open question, and the first thing to verify:** whether the CLI agents expose their configured MCP
servers for *direct tool invocation*, or only through a conversation. If only the latter, tier 1
becomes "ask the agent, in words, to use its tool" — which works but is non-deterministic and costs a
model call. If the former, we get deterministic tool calls for free.

*If neither, the fallback is for the bridge to be an MCP client itself* — read the same config file
the agent reads, spawn the same servers, and speak MCP directly. That is a known quantity: the
protocol is documented and the client side is small. It is more code than tier 1, but far less than
one OAuth client per provider, and it still holds no credentials of ours.

### 2. A permission model that matches the one we already have

A tool call is a capability, and packs already declare capabilities. Extend the pack schema:

```jsonc
{
"id": "trip-planner",
"uses": ["maps.search_places", "gmail.search_messages"], // allow-list, validated on install
"writes": ["event", "item"]
}
```

Rules, consistent with everything else in the design:

- A pack may call **only** the tools it declares, checked at the point of invocation, not trusted.
- A tool that **reads** may run inside a turn. A tool that **writes to a third party** — sending
mail, creating a calendar event on someone else's calendar, posting anything — goes through the
existing approval type: it names what it will do and waits.
- The first use of any tool asks once, per household, and remembers. The trust ladder we built for
mail senders is the same shape and should be reused rather than reinvented.

### 3. Provenance is not optional

Every fact that arrives through a tool must carry a `source` with a **deep link**, or the lineage
view silently degrades to "came from somewhere". MCP results are text and JSON blobs with no
guaranteed link field, so this is real work:

- a per-server adapter that maps a result to `{ title, ref, locator?, excerpt }`;
- and where a server genuinely cannot give a link, the source records that plainly rather than
fabricating one.

This is the piece most likely to be skipped and most likely to matter. `src/connectors/deeplinks.ts`
is already the right shape for it — a table of formats, treated as data.

### 4. Rendering

Mostly done. The canvas already has `Map`, `MapLink`, `MapDetail`, `PlacesExplorer`, `ImageCarousel`
and `TileGrid`. A Maps search is a capability returning rows plus a view builder that already exists:

```
maps.search_places -> rows -> buildFromData("cards" | "places") -> a board
```

No new rendering work for the first three integrations.

---

## Reusing T3 Code rather than rewriting it

The bridge's fiddly part is not the HTTP. It is the **driver table**: which binaries exist, which
flags produce non-interactive output, what "signed out" looks like on stderr for each one. That is
empirical, it changes when any of those tools ship a release, and it is exactly the thing a shared
project maintains better than we can.

Concretely, what to reuse and how:

1. **Check the licence first.** If it is permissive (MIT/Apache-2.0), either depend on their package
if one is published, or vendor the driver definitions with attribution and a link. If it is
copyleft, vendoring into this repo changes this repo's obligations — that is a decision for the
owner, not for me.
2. **Prefer taking the data, not the code.** The driver table is a JSON-shaped list. Taking it as
data keeps our implementation ours and makes updates a diff of a table.
3. **Contribute back.** If our detection handles a case theirs does not, that belongs upstream.

What is already ours and worth keeping either way: the loopback/token/origin security model, the job
runner with its step budget, and the approval type on outbound messages. Those are product
decisions, not commodity.

*I could not evaluate their licence or source in this session — no network access. This is the
sequence I would follow, not a conclusion about their code.*

---

## What I am not certain of

Stated plainly, because a plan that hides its unknowns is a plan that fails late:

- **Whether the CLI agents allow direct tool invocation** of their configured MCP servers, or only
conversational use. This determines whether tier 1 is cheap or medium. *Verify first.*
- **Which official MCP servers exist for Gmail, Drive, Dropbox and Maps, and their maturity.** There
is an ecosystem; I cannot check its current state offline, and "there is a server for that" is not
the same as "it is good".
- **The state of remote MCP + OAuth in browsers.** The protocol supports it; how well it works from
a page with no backend is the question tier 2 depends on.
- **Google Places licensing.** Displaying and caching Places data has terms attached. Worth reading
before a board caches search results.

## Sequence

| Step | Work | Unblocks |
|---|---|---|
| 0 | Verify the four unknowns above | Everything |
| 1 | `GET /tools` on the bridge — discovery only, no invocation | Seeing what a household already has |
| 2 | `POST /tools/:server/:tool`, with `uses` on packs enforced | Maps search, Drive search |
| 3 | Source adapters + deep links per server | Lineage keeps working |
| 4 | A permission prompt reusing the trust ladder | Write-capable tools |
| 5 | Remote MCP from the browser | Integrations with no bridge |
| 6 | Revisit `src/google/` | Possibly delete most of it |

Step 6 is the point of the whole plan. If tiers 1 and 2 work, the hand-rolled Google client shrinks
to the offline path for calendar and triage, and everything else — Dropbox, Maps, whatever is next —
costs a line in a pack rather than a fortnight.
Loading