Skip to content
Merged
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: 0 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

124 changes: 20 additions & 104 deletions grain/README.md
Original file line number Diff line number Diff line change
@@ -1,115 +1,35 @@
# Grain MCP

Grain MCP is maintained by Deco and built on top of Grain public APIs. It gives AI agents structured access to meeting recordings, transcripts, and AI summaries, and keeps a searchable Supabase index updated by webhooks.
Grain MCP is maintained by deco and built on Grain's public API (v2). It gives
AI agents live access to the meeting recordings **your Grain API key can see**:
list them, get one with its summary, participants and highlights, or fetch just
the transcript or just the summary (fewer tokens).

## What This MCP Does
It keeps no state: no index, no webhooks, no database. Each call goes to
Grain with the key of the connection that made it.

- Lists recordings directly from Grain
- Retrieves full recording details by ID
- Retrieves transcript-only output to reduce token usage
- Retrieves summary-only output to reduce token usage
- Indexes webhook events into Supabase for fast local search
> **Searching what was said across deco's calls, transcripts in a database,
> call events and the account map** live in
> [decocms/grain-indexer](https://github.com/decocms/grain-indexer) — the
> index this MCP used to keep (a `grain_meeting_recordings` table fed by
> webhooks) moved there, with full transcripts, privacy rules and signed
> webhooks.

## Authentication

This MCP uses a Grain API key only (no OAuth flow).
1. Generate an API key at [Grain → Integrations → API](https://grain.com/app/settings/integrations?tab=api).
2. In the connection settings, paste it in the **Token** field.

1. Generate your API key at [Grain API Integrations](https://grain.com/app/settings/integrations?tab=api)
2. In Deco Mesh connection settings, paste the API key in the **Token** field
3. Save the connection

The MCP reads this value from `MESH_REQUEST_CONTEXT.authorization`.

## Webhook URL

The webhook is created automatically by API when the connection configuration changes.
This is done in `onChange` in `server/main.ts`:

- The MCP calls Grain webhooks endpoints (`listWebhooks` and `createWebhook`)
- It checks if the webhook already exists
- It creates the webhook only when missing
- The create payload includes `hook_type: "recording_added"` and `hook_url`

If you need to configure it manually in Grain, use this URL pattern:

`https://sites-grain.deco.site/webhooks/grain/{connectionId}`

- Replace `{connectionId}` with your Deco Mesh connection ID
- This is a dedicated public webhook route (no `/mcp` in the external URL)
- Internally, the route forwards the payload to the `MESH_PUBLIC_GRAIN_WEBHOOK` tool
- The MCP also exposes a readonly `WEBHOOK_URL` template in configuration

In runtime, the MCP automatically builds this URL from Mesh context:

`{meshUrl}/webhooks/grain/{connectionId}`

If `DEVELOPMENT_MODE=true`, the MCP uses this fixed base URL instead of `meshUrl`:

`https://localhost-c056dce8.deco.host/webhooks/grain/{connectionId}`

Optional development override:

- `DEVELOPMENT_WEBHOOK_URL` (full URL)
- When set together with `DEVELOPMENT_MODE=true`, this value is used as-is without URL reconstruction

In Grain, webhook management is available in the same Integrations/API area where the API key is generated:
[Grain API Integrations](https://grain.com/app/settings/integrations?tab=api)

## Supabase Indexing

The MCP listens to Grain webhook events and stores indexed data in Supabase table `grain_meeting_recordings`.

- Webhook setup is automatic during configuration
- New or updated recordings are upserted in Supabase
- `SEARCH_INDEXED_RECORDINGS` queries this local index for fast filtering

Create both tables manually in Supabase using [`server/db/schema.sql`](server/db/schema.sql):
- `grain_meeting_recordings` — indexed recording data
- `grain_trigger_credentials` — persists Mesh trigger callbacks across restarts

## Triggers

Two Mesh triggers are available:

| Trigger | Fires when |
| --- | --- |
| `grain.recording.added` | A new meeting recording is indexed |
| `grain.recording.updated` | An existing recording is updated (e.g. AI notes ready) |

Both triggers support the same filter params:

| Param | Description |
| --- | --- |
| `owner_email` | Only fire for recordings owned by this email |
| `tag` | Only fire when the recording has this exact tag |
| `participant_email` | Only fire when this participant was in the meeting |

Trigger payload fields: `recording_id`, `title`, `url`, `start_datetime`, `end_datetime`, `owners`, `tags`, `participants` (`[{name, email, scope}]`), `has_summary`, `indexed_at`.
The MCP reads it from `MESH_REQUEST_CONTEXT.authorization`.

## Tools

| Tool | Purpose |
| --- | --- |
| `LIST_RECORDINGS` | List and filter recordings from Grain API |
| `GET_RECORDING` | Get full details for one recording, with optional selective output (`summary`, `transcript`, `highlights`) |
| `GET_TRANSCRIPT` | Get transcript URL and optional transcript content (`json`, `txt`, `srt`, `vtt`) |
| `GET_SUMMARY` | Get only summary fields (`summary`, `summary_points`, `intelligence_notes_md`) |
| `SEARCH_INDEXED_RECORDINGS` | Search Supabase-indexed recordings by query and/or date filters |

## Environment Variables

Supabase access is required for indexed search:

- `SUPABASE_URL`
- `SUPABASE_ANON_KEY`

## Event Flow

```text
Grain webhook -> Deco Mesh event -> Grain MCP event handler -> Supabase upsert
```

The MCP also emits `grain.recording_indexed` after a successful index operation when Mesh org context is available.
| `LIST_RECORDINGS` | List and filter recordings from the Grain API |
| `GET_RECORDING` | One recording, with optional selective output (`summary`, `transcript`, `highlights`) |
| `GET_TRANSCRIPT` | Transcript URL and optional content (`json`, `txt`, `srt`, `vtt`) |
| `GET_SUMMARY` | Only the summary fields (`summary`, `summary_points`, `intelligence_notes_md`) |

## Development

Expand All @@ -120,8 +40,4 @@ bun run check
bun run build
```

## Notes

- Grain API base URL: `https://api.grain.com`
- This project is part of the Deco MCP monorepo

Grain API base URL: `https://api.grain.com/_/public-api/v2`.
1 change: 0 additions & 1 deletion grain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"dependencies": {
"@decocms/bindings": "1.0.1-alpha.23",
"@decocms/runtime": "1.3.1",
"@supabase/supabase-js": "^2.57.4",
"zod": "^4.0.0"
},
"devDependencies": {
Expand Down
119 changes: 0 additions & 119 deletions grain/scripts/backfill.ts

This file was deleted.

10 changes: 0 additions & 10 deletions grain/server/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,6 @@ export const GRAIN_BASE_URL = "https://api.grain.com/_/public-api/v2";
export const GRAIN_API_VERSION = "2026-10-01";

export const GRAIN_RECORDINGS_ENDPOINT = "/recordings";
export const GRAIN_HOOKS_ENDPOINT = "/hooks";

/** Hook types the index listens to. */
export const INDEXED_HOOK_TYPES = [
"recording_added",
"recording_updated",
] as const;

/** What every indexed recording is fetched with. */
export const INDEX_INCLUDE = { participants: true, ai_summary: true } as const;

export const DEFAULT_PAGE_SIZE = 50;
export const MAX_PAGE_SIZE = 100;
Loading
Loading