Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a934373
fix(mcp): obtain Google OAuth refresh tokens for Workspace connectors
hordruma Jun 17, 2026
513edac
fix(mcp): complete connector OAuth via polling, not COOP-broken popup…
hordruma Jun 19, 2026
21ff4d7
fix(mcp): surface concise server permission errors
willchen96 Jul 26, 2026
3ac1acc
chore(mcp): log sanitized tool error responses
willchen96 Jul 26, 2026
f6ef5c6
fix(mcp-oauth): make `oauthConnected` an honest completion signal (F1…
amal66 Aug 1, 2026
d520e03
fix(mcp): wrap transport-error content as untrusted (F4) + record rea…
amal66 Aug 1, 2026
4934ed5
fix(connectors): cancelable OAuth wait — escape hatch + unmount clean…
amal66 Aug 1, 2026
bf7de60
fix(mcp): fail fast with copy-pasteable setup steps when the Google O…
amal66 Aug 4, 2026
cc6634a
fix(mcp): concise connector errors + the Google versioned-endpoint trap
amal66 Aug 4, 2026
1f6fd79
refactor(mcp): fold per-provider OAuth quirks into a single registry
amal66 Aug 4, 2026
27e25ec
feat(mcp): connect to Slack's hosted MCP server
amal66 Aug 4, 2026
d49811d
docs(mcp): ship a ready-made Slack app manifest for the MCP connector
amal66 Aug 4, 2026
779b40c
fix(mcp): seed the SDK's OAuth discovery so scope survives to the aut…
amal66 Aug 4, 2026
78f4633
chore(mcp): delete the unreachable manual token-refresh path
amal66 Aug 5, 2026
0e25f69
fix(mcp): clear only token columns on redirect, not the whole OAuth row
amal66 Aug 6, 2026
86e7a48
fix(connectors): let the user cancel a stuck reconnect OAuth wait
amal66 Aug 6, 2026
2adfa7c
test(mcp): restore env vars and fetch mock in afterEach, not test bodies
amal66 Aug 6, 2026
8009913
refactor(mcp): drop the test-only isGoogleOAuthHost wrapper
amal66 Aug 6, 2026
7eddfd6
feat: first-party Google Drive integration — search and read Drive fi…
amal66 Aug 4, 2026
2535cdf
fix(connectors): route Google Drive connect/disconnect through the pa…
amal66 Aug 6, 2026
bd2e2e1
fix(connectors): close the Drive OAuth popup on every exit path, not …
amal66 Aug 6, 2026
82df141
fix(db): grant hardening for the Google Drive token tables
amal66 Aug 6, 2026
8815f4d
fix(connectors): surface "not configured" instead of a Connect button…
amal66 Aug 6, 2026
22d5bd1
fix(connectors): port the Google Drive card to the settings design sy…
amal66 Aug 16, 2026
4aba3d5
feat(connectors): two-pathway setup story — Slack preset, re-dated mi…
amal66 Aug 16, 2026
34a337a
docs(connectors): close the three gaps a setup-audit found in the ope…
amal66 Aug 16, 2026
412c31a
docs(drive): split the consent-screen guidance by who will connect
amal66 Aug 18, 2026
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
133 changes: 133 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,139 @@ authentication behavior, Ollama setup, and first-run guidance.
- [Contributing](CONTRIBUTING.md)
- [Security policy](SECURITY.md)

## Connectors

Mike connects to the systems a legal team already works in — Slack, Google
Drive, and any remote [MCP](https://modelcontextprotocol.io) server — from
**Settings > Connectors**. There are two setup pathways, and every connector
uses one of them:

**Zero-setup (the server registers itself).** Most hosted MCP servers support
OAuth dynamic client registration (RFC 7591). For these, nothing is configured
on the Mike server at all: a user clicks **Add**, pastes the server URL (or
picks a preset), and completes the provider's consent screen in a popup. Servers
that use a bearer token or custom headers instead of OAuth also fall in this
pathway — the credentials are entered in the same modal and stored encrypted.

**Bring-your-own OAuth app (you register a client once).** Some providers do
not implement dynamic client registration, so the person hosting Mike creates
an OAuth client with that provider once, puts its credentials in
`backend/.env`, and every user of the deployment can then connect their own
account with one click:

- **Google Drive** — first-party integration, see
[Google Drive Integration](#google-drive-integration) below.
- **Google-hosted MCP servers** (`*.googleapis.com`) — create a Google Cloud
OAuth client and set `GOOGLE_MCP_OAUTH_CLIENT_ID` / `_SECRET`
(see `backend/.env.example`).
- **Slack** — see [Slack](#slack) below.

If a user starts an OAuth connect before the deployment is configured, the
error message contains the exact provider-console steps and the redirect URI
to paste — nothing fails silently.

### Slack

Slack's hosted MCP server (`https://mcp.slack.com/mcp`) gives the assistant
access to the channels and DMs the connecting user can see. The requested
scopes are mostly read/search, plus a few write scopes (`chat:write`,
`reactions:write`, `canvases:write`) — a user approving the consent screen is
granting those too. Slack does not support dynamic client registration, so
the deployment needs a Slack app (created once, by someone with app-creation
rights in the workspace):

1. Create an app at [api.slack.com/apps](https://api.slack.com/apps) — the
fastest path is **From an app manifest**, pasting
`docs/slack-mcp-app-manifest.example.json` and replacing the redirect URL
placeholder. The manifest configures the bot user, the agent feature
(`features.assistant_view`), and the OAuth scopes. (Building by hand
instead: add the bot user and agent feature yourself.)
2. Two settings the manifest cannot express, required on **either** path:
turn on the **Slack MCP Server** toggle under the app's *Agents* settings,
and enable **PKCE** under *OAuth & Permissions*.
3. Add your backend's callback,
`https://<your-backend-host>/user/mcp-connectors/oauth/callback`, as a
redirect URL. Slack requires HTTPS — for local development use an HTTPS
tunnel and set `API_PUBLIC_URL` to the tunnel URL so the callback matches.
4. Set `SLACK_MCP_OAUTH_CLIENT_ID` and `SLACK_MCP_OAUTH_CLIENT_SECRET` in
`backend/.env` and restart the backend.

Each user then clicks **Add** on **Settings > Connectors**, picks the
**Slack** preset, and approves Slack's consent screen. On workspaces with
app approval enabled, a Workspace Owner/Admin must approve the app before
members can authorize it. Tokens are encrypted at rest, and individual tools
can be toggled per connector.

## Google Drive Integration

Mike can search and read a user's Google Drive files directly from chat — ask
*"Search my Google Drive for the consulting agreement and summarize it"* and
the assistant uses its `google_drive_search` / `google_drive_read_file` /
`google_drive_list_recent` tools (read-only; Google Docs/Sheets/Slides are
exported as text, PDF and Word files are converted). Each user connects their
own Google account with one click from **Settings > Connectors > Google
Drive**; tokens are encrypted at rest and access is limited to the
`drive.readonly` scope.

This is a first-party integration over the GA Google Drive REST API. It does
**not** use Google's hosted Drive MCP server, which is gated behind the
Google Workspace Developer Preview Program — no preview enrollment is needed.

### Setup (one-time, per deployment)

These steps apply to every deployment — a firm self-hosting its own fork and
an operator hosting Mike for others alike. The one decision that differs is
step 3, because `drive.readonly` is a Google *restricted* scope and Google's
verification rules depend on **who connects**, not on who wrote the code.

1. In [Google Cloud Console](https://console.cloud.google.com), pick or
create a project.
2. **APIs & Services > Library**: enable the **Google Drive API**
(`drive.googleapis.com`).
3. **APIs & Services > OAuth consent screen** — pick the user type for your
audience:

- **Self-hosting for your own organization** (everyone who will connect
is in your Google Workspace org — the typical law firm): choose
**Internal**. No user cap, no Google verification, no security
assessment, no token expiry — at any firm size. The Cloud project must
be owned by that Workspace organization.
- **Hosting for users outside your organization** (consumer Gmail
accounts, multiple firms, a public instance): choose **External** and
plan for Google's verification. In *Testing* mode, only 100 listed
test users can connect **and their refresh tokens expire every
7 days** — each user must reconnect weekly, so Testing is for pilots,
not steady state. Published but unverified, the app has a *lifetime*
cap of 100 users (Google does not reset it) behind an "unverified
app" warning. Growing past that requires Google's restricted-scope
verification, including an annual third-party security assessment
(CASA). That cost lands once, on the operator of the deployment — one
verified client covers every user of the instance; individual users
never deal with it.
4. **APIs & Services > Credentials > Create credentials > OAuth client ID >
Web application**, and add your backend's callback as an authorized
redirect URI:

https://<your-backend-host>/user/integrations/google-drive/oauth/callback

(local development: `http://localhost:3001/user/integrations/google-drive/oauth/callback`)
5. Set the client in `backend/.env` and restart the backend:

GOOGLE_DRIVE_OAUTH_CLIENT_ID=...apps.googleusercontent.com
GOOGLE_DRIVE_OAUTH_CLIENT_SECRET=...

If you already configured `GOOGLE_MCP_OAUTH_CLIENT_ID`/`_SECRET` for MCP
connectors, the Drive integration reuses them automatically — just add
the extra redirect URI from step 4 to the same OAuth client.

Fresh databases created from `backend/schema.sql` already include the Drive
token tables. Existing deployments should apply
`backend/migrations/20260816_01_google_drive_integration.sql`.

Each user then clicks **Connect** on **Settings > Connectors**, approves the
Google consent screen once, and the assistant's Drive tools activate for
their chats. Disconnecting revokes the grant and deletes the stored tokens.

## System workflows

Mike's system assistant and tabular-review workflows are maintained in the
Expand Down
58 changes: 58 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ WORD_ADDIN_URL=
# Optional comma-separated origins for additional trusted clients.
ALLOWED_ORIGINS=

# Public base URL of this backend, used to build the OAuth redirect URIs for
# connectors (…/user/mcp-connectors/oauth/callback and
# …/user/integrations/google-drive/oauth/callback). Required whenever the
# backend sits behind a reverse proxy or an HTTPS tunnel: the value must
# exactly match the origin registered with the OAuth provider, or the flow
# fails with redirect_uri_mismatch. Falls back to BACKEND_URL, then to the
# incoming request's own host.
API_PUBLIC_URL=

# HMAC key used to sign /download/:token URLs. Required at startup.
# Generate with: openssl rand -hex 32
# Use a dedicated secret distinct from SUPABASE_SECRET_KEY.
Expand Down Expand Up @@ -39,3 +48,52 @@ COURTLISTENER_API_TOKEN=your-courtlistener-token
# GET /manifest-signing-key. Rotating the key does not invalidate past exports,
# but whoever checks one needs the key that was current when it was made.
MANIFEST_SIGNING_KEY=

# Optional: OAuth client used when connecting remote MCP servers that require
# OAuth (Account → Connectors). Most MCP servers support dynamic client
# registration (RFC 7591) and need nothing here. Google (*.googleapis.com —
# e.g. the Google Drive MCP at https://drivemcp.googleapis.com/mcp/v1) does NOT:
# in Google Cloud Console, create an OAuth client (APIs & Services →
# Credentials → Create credentials → OAuth client ID → Web application) and
# add your backend's callback as an authorized redirect URI, e.g.
# http://localhost:3001/user/mcp-connectors/oauth/callback
# and enable BOTH services in the same project — the base API *and* the MCP
# service (each Google MCP server has its own): for Drive that is
# drive.googleapis.com AND drivemcp.googleapis.com. A missing MCP service
# surfaces later as "The caller does not have permission" on every tool call
# even though OAuth succeeded.
GOOGLE_MCP_OAUTH_CLIENT_ID=
GOOGLE_MCP_OAUTH_CLIENT_SECRET=
# Optional: override the scopes requested from Google; defaults to the scopes
# the MCP server itself advertises.
# GOOGLE_MCP_OAUTH_SCOPE=
# Slack's hosted MCP server (https://mcp.slack.com/mcp) also has no dynamic
# client registration: create a Slack app (https://api.slack.com/apps) with a
# bot user and the agent feature (features.assistant_view), turn on the
# "Slack MCP Server" toggle under the app's Agents settings, enable PKCE under
# OAuth & Permissions, and register
# https://<your-backend-host>/user/mcp-connectors/oauth/callback
# as a redirect URL — Slack requires HTTPS, so local dev needs an HTTPS tunnel
# (set API_PUBLIC_URL to the tunnel URL so the callback matches). A ready-made
# app manifest lives in docs/slack-mcp-app-manifest.example.json.
SLACK_MCP_OAUTH_CLIENT_ID=
SLACK_MCP_OAUTH_CLIENT_SECRET=
# Optional: override the user scopes requested from Slack; defaults to the
# scopes the MCP server itself advertises.
# SLACK_MCP_OAUTH_SCOPE=
# Optional fallbacks for any other MCP server that requires a pre-registered
# OAuth client instead of dynamic registration:
# MCP_OAUTH_CLIENT_ID=
# MCP_OAUTH_CLIENT_SECRET=
# MCP_OAUTH_DEFAULT_SCOPE=

# Optional: first-party Google Drive integration (Account → Connectors →
# Google Drive; chat tools google_drive_search / read_file / list_recent).
# Uses the GA Drive REST API — no Google preview program needed. Create a Web
# OAuth client in Google Cloud Console with redirect URI
# <backend>/user/integrations/google-drive/oauth/callback
# and enable the Google Drive API (drive.googleapis.com). Falls back to
# GOOGLE_MCP_OAUTH_CLIENT_ID/_SECRET when unset, so one client can serve both
# features (add both redirect URIs to it).
GOOGLE_DRIVE_OAUTH_CLIENT_ID=
GOOGLE_DRIVE_OAUTH_CLIENT_SECRET=
51 changes: 51 additions & 0 deletions backend/migrations/20260816_01_google_drive_integration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
-- 2026-08-16: Native Google Drive integration.
--
-- First-party Drive tools that call the GA Drive REST API directly with a
-- per-user OAuth token — no dependency on Google's preview-gated MCP server.
-- One token row per user (connecting again overwrites), plus short-lived
-- OAuth state rows for the PKCE flow. Both tables are service-role only:
-- RLS is enabled with no user policies, so only the backend (service key)
-- can read the encrypted tokens.

CREATE TABLE IF NOT EXISTS public.user_google_drive_tokens (
user_id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
encrypted_access_token text,
access_token_iv text,
access_token_tag text,
encrypted_refresh_token text,
refresh_token_iv text,
refresh_token_tag text,
scope text,
expires_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);

ALTER TABLE public.user_google_drive_tokens ENABLE ROW LEVEL SECURITY;

CREATE TABLE IF NOT EXISTS public.google_drive_oauth_states (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
state_hash text NOT NULL UNIQUE,
encrypted_state_config text NOT NULL,
state_config_iv text NOT NULL,
state_config_tag text NOT NULL,
expires_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);

ALTER TABLE public.google_drive_oauth_states ENABLE ROW LEVEL SECURITY;

-- Grant hardening, mirroring backend/schema.sql. On hosted Supabase, default
-- privileges hand anon/authenticated access to every new table in public;
-- these tables hold encrypted OAuth tokens, so strip the browser roles and
-- grant the backend's service_role its data privileges explicitly.
revoke all on public.user_google_drive_tokens from anon, authenticated;
revoke all on public.google_drive_oauth_states from anon, authenticated;

grant select, insert, update, delete
on public.user_google_drive_tokens
to service_role;
grant select, insert, update, delete
on public.google_drive_oauth_states
to service_role;
53 changes: 53 additions & 0 deletions backend/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2397,3 +2397,56 @@ grant select, insert, update, delete
grant usage, select
on all sequences in schema public
to service_role;
-- 2026-08-04: Native Google Drive integration.
--
-- First-party Drive tools that call the GA Drive REST API directly with a
-- per-user OAuth token — no dependency on Google's preview-gated MCP server.
-- One token row per user (connecting again overwrites), plus short-lived
-- OAuth state rows for the PKCE flow. Both tables are service-role only:
-- RLS is enabled with no user policies, so only the backend (service key)
-- can read the encrypted tokens.

CREATE TABLE IF NOT EXISTS public.user_google_drive_tokens (
user_id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
encrypted_access_token text,
access_token_iv text,
access_token_tag text,
encrypted_refresh_token text,
refresh_token_iv text,
refresh_token_tag text,
scope text,
expires_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);

ALTER TABLE public.user_google_drive_tokens ENABLE ROW LEVEL SECURITY;

CREATE TABLE IF NOT EXISTS public.google_drive_oauth_states (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
state_hash text NOT NULL UNIQUE,
encrypted_state_config text NOT NULL,
state_config_iv text NOT NULL,
state_config_tag text NOT NULL,
expires_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);

ALTER TABLE public.google_drive_oauth_states ENABLE ROW LEVEL SECURITY;

-- These two tables are created after the "Direct client grant hardening"
-- section above ran, so its per-table revokes and its one-shot
-- `grant ... on all tables in schema public to service_role` never saw them.
-- Repeat both statements here explicitly, following the same pattern as the
-- MCP OAuth tables: browser roles get nothing (the backend fronts all
-- access), service_role gets the data privileges the backend needs.
revoke all on public.user_google_drive_tokens from anon, authenticated;
revoke all on public.google_drive_oauth_states from anon, authenticated;

grant select, insert, update, delete
on public.user_google_drive_tokens
to service_role;
grant select, insert, update, delete
on public.google_drive_oauth_states
to service_role;
6 changes: 4 additions & 2 deletions backend/src/lib/chat/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { resolveRequestedModel } from "../routerModels";
import { safeErrorMessage } from "../safeError";
import { createServerSupabase } from "../supabase";
import { buildUserMcpTools, type McpToolEvent } from "../mcpConnectors";
import { buildGoogleDriveTools } from "../integrations/googleDrive";
import type { SourceDocument } from "../sourceDocuments";
import {
COURTLISTENER_TOOLS,
Expand Down Expand Up @@ -204,13 +205,14 @@ export async function runLLMStream(params: {
} = params;
const researchTools = includeResearchTools ? COURTLISTENER_TOOLS : [];
const mcpTools = await buildUserMcpTools(userId, db);
const googleDriveTools = await buildGoogleDriveTools(userId, db);
const conversationTools = includeAskInputs
? TOOLS
: TOOLS.filter((tool) => tool.function.name !== "ask_inputs");
const baseTools = [...conversationTools, ...researchTools, ...WORKFLOW_TOOLS];
const activeTools = extraTools?.length
? [...baseTools, ...mcpTools, ...extraTools]
: [...baseTools, ...mcpTools];
? [...baseTools, ...mcpTools, ...googleDriveTools, ...extraTools]
: [...baseTools, ...mcpTools, ...googleDriveTools];

// Extract system prompt; pass remaining turns to the adapter as
// plain user/assistant messages.
Expand Down
38 changes: 38 additions & 0 deletions backend/src/lib/chat/tools/toolDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
} from "./courtlistenerTools";
import { executeMcpToolCall, type McpToolEvent } from "../../mcpConnectors";
import { createServerSupabase } from "../../supabase";
import {
GOOGLE_DRIVE_TOOL_PREFIX,
executeGoogleDriveToolCall,
} from "../../integrations/googleDrive";
import {
type DocStore,
type DocIndex,
Expand Down Expand Up @@ -429,6 +433,40 @@ export async function runToolCalls(
/* ignore */
}

if (tc.function.name.startsWith(GOOGLE_DRIVE_TOOL_PREFIX)) {
// Native Drive tools reuse the MCP event surface so the UI renders
// them with the existing connector treatment.
write(
`data: ${JSON.stringify({
type: "mcp_tool_start",
name: tc.function.name,
})}\n\n`,
);
const { content, event } = await executeGoogleDriveToolCall(
userId,
tc.function.name,
args,
db,
);
toolResults.push({
role: "tool",
tool_call_id: tc.id,
content,
});
mcpEvents.push(event);
write(
`data: ${JSON.stringify({
type: "mcp_tool_result",
name: tc.function.name,
connector_name: event.connector_name,
tool_name: event.tool_name,
status: event.status,
error: event.error,
})}\n\n`,
);
continue;
}

if (tc.function.name.startsWith("mcp_")) {
write(
`data: ${JSON.stringify({
Expand Down
Loading
Loading