# granola-mcp
MCP server for semantic search across Granola meeting notes. Extracts insights, themes (pain-points, feature-requests, decisions, etc.), and key quotes with speaker attribution. Uses LanceDB for fast local vector search.
Based on reverse engineering research by [Joseph Thacker](https://josephthacker.com/hacking/2025/05/08/reverse-engineering-granola-notes.html) and [getprobo](https://github.com/getprobo/reverse-engineering-granola-api).
## Features
- **Export**: Extract all your Granola meetings with transcripts
- **Semantic Search**: Vector-indexed search across meetings with pre-extracted insights
- **Speaker Attribution**: Distinguishes between host (`me`) and participants
- **Theme Extraction**: Auto-categorizes content into themes (pain-points, feature-requests, etc.)
- **MCP Server**: Exposes search to Claude Code, Claude Desktop, and other AI tools
## Prerequisites
- Node.js 18+
- Granola desktop app installed and logged in
- OpenAI API key (for embeddings and insight extraction)
## Installation
```bash
npm install
npm run build
```
## Quick Start
```bash
# One command to sync everything (export + index)
OPENAI_API_KEY=sk-... node dist/index.js sync
# Or with a custom data directory
OPENAI_API_KEY=sk-... node dist/index.js sync ./my-data
# Test search
OPENAI_API_KEY=sk-... node dist/index.js search "user pain points"
```
## CLI Commands
### Sync (Recommended)
The easiest way to keep your data up to date - exports from Granola and rebuilds the index in one step:
```bash
OPENAI_API_KEY=sk-... node dist/index.js sync
# With options
OPENAI_API_KEY=sk-... node dist/index.js sync ./my-data
OPENAI_API_KEY=sk-... node dist/index.js sync --skip-extraction # Faster, reuses existing insights
```
### Export from Granola
Export only (without indexing):
```bash
node dist/index.js export ./output
node dist/index.js export ./output --format markdown
node dist/index.js export ./output --format json
```
### Build Search Index
```bash
# Full indexing with insight extraction (~$0.02/document)
OPENAI_API_KEY=sk-... node dist/index.js index ./export
# Skip extraction (use existing insights, just rebuild embeddings)
OPENAI_API_KEY=sk-... node dist/index.js index ./export --skip-extraction
```
### Search from CLI
```bash
OPENAI_API_KEY=sk-... node dist/index.js search "pricing concerns"
OPENAI_API_KEY=sk-... node dist/index.js search "feature requests" --folder "User interviews"
```
### Export for ChatGPT
```bash
OPENAI_API_KEY=sk-... node dist/index.js export-combined ./chatgpt.md
OPENAI_API_KEY=sk-... node dist/index.js export-combined ./chatgpt.md --query "user feedback"
```
### Other Commands
```bash
node dist/index.js list # List documents
node dist/index.js workspaces # List workspaces
node dist/index.js folders # List folders
node dist/index.js transcript <id> # Get specific transcript
```
## MCP Server Setup
### Claude Code
Add to `.mcp.json` in your project:
```json
{
"mcpServers": {
"granola": {
"type": "stdio",
"command": "node",
"args": ["/path/to/granola-mcp/dist/mcp/server.js"],
"env": {
"GRANOLA_DATA_DIR": "/path/to/granola-mcp/export",
"OPENAI_API_KEY": "sk-..."
}
}
}
}
```
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"granola": {
"command": "node",
"args": ["/path/to/granola-mcp/dist/mcp/server.js"],
"env": {
"GRANOLA_DATA_DIR": "/path/to/granola-mcp/export",
"OPENAI_API_KEY": "sk-..."
}
}
}
}
```
## MCP Tools
| Tool | Description |
|------|-------------|
| `search` | Semantic search across meetings, returns summaries + quotes |
| `search_themes` | Find documents by theme (pain-points, feature-requests, etc.) |
| `list_folders` | List all folders with document counts |
| `list_documents` | List documents with brief summaries |
| `get_document` | Get full document details (all themes + quotes) |
| `get_transcript` | Get raw transcript (use sparingly) |
| `get_themes` | List available themes with definitions |
## Speaker Attribution
The system distinguishes between speakers:
- `speaker: "me"` - The meeting host (you)
- `speaker: "participant"` - Other people in the meeting
This helps AI understand what's your own idea vs external feedback.
## Pre-defined Themes
- **pain-points**: User frustrations, problems, complaints
- **feature-requests**: Desired features, wishlist items
- **positive-feedback**: What users liked, praised
- **pricing**: Cost concerns, value perception
- **competition**: Competitor mentions, alternatives
- **workflow**: How users currently do things
- **decisions**: Key decisions made, action items
- **questions**: Open questions needing clarification
## Output Structure
```
export/
├── vectors.lance/ # LanceDB vector index
├── Meeting_Title_1/
│ ├── document.json # Raw document data
│ ├── notes.md # Converted notes
│ ├── transcript.json # Raw transcript with speaker info
│ ├── transcript.md # Formatted transcript
│ └── transcript.txt # Plain text transcript
└── Meeting_Title_2/
└── ...
```
## Keeping Data Updated
The system doesn't auto-sync with Granola. Run `sync` manually after new meetings, or set up a cron job:
### Manual Update
```bash
OPENAI_API_KEY=sk-... node dist/index.js sync
```
### Automated Updates (Cron)
Add to your crontab (`crontab -e`):
```bash
# Sync every night at 2am
0 2 * * * cd /path/to/granola-mcp && OPENAI_API_KEY=sk-... /usr/local/bin/node dist/index.js sync >> /tmp/granola-sync.log 2>&1
# Or every 6 hours
0 */6 * * * cd /path/to/granola-mcp && OPENAI_API_KEY=sk-... /usr/local/bin/node dist/index.js sync >> /tmp/granola-sync.log 2>&1
```
### macOS LaunchAgent
Create `~/Library/LaunchAgents/com.granola-mcp.sync.plist`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.granola-mcp.sync</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/node</string>
<string>/path/to/granola-mcp/dist/index.js</string>
<string>sync</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>OPENAI_API_KEY</key>
<string>sk-...</string>
</dict>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>2</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/tmp/granola-sync.log</string>
<key>StandardErrorPath</key>
<string>/tmp/granola-sync.log</string>
</dict>
</plist>
```
Load it with: `launchctl load ~/Library/LaunchAgents/com.granola-mcp.sync.plist`
## How It Works
1. **Export**: Reads credentials from `~/Library/Application Support/Granola/supabase.json` and fetches all documents via Granola's API
2. **Index**:
- Extracts themes and key quotes using GPT-4o-mini
- Generates embeddings using text-embedding-3-small
- Stores in LanceDB for fast vector search
3. **Search**:
- Embeds your query
- Finds semantically similar documents
- Returns summaries + relevant quotes (not raw transcripts)
## Cost Estimates
| Documents | Insight Extraction | Embeddings | Total |
|-----------|-------------------|------------|-------|
| 25 | ~$0.50 | ~$0.01 | ~$0.51 |
| 100 | ~$2.00 | ~$0.02 | ~$2.02 |
| 500 | ~$10.00 | ~$0.10 | ~$10.10 |
Search queries are free (vector similarity, no LLM calls).
## License
MIT