From granola-pack
Accesses Granola meeting data from local cache, MCP server, or Enterprise API for dev workflows like extracting action items into code and syncing to tools.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin granola-packThis skill is limited to using the following tools:
Access Granola meeting data programmatically using three methods: the local cache file (zero-auth, offline), the MCP server (AI agent integration), or the Enterprise API (workspace-wide access). Build developer workflows that turn meeting outcomes into code tasks, documentation, and project artifacts.
Queries and syncs Granola AI meeting notes and transcripts from local cache to Obsidian vault as Markdown files. Supports list, get, sync commands via Python CLI.
Syncs Granola meeting transcripts, summaries, and notes to local disk from macOS app auth. Enables bash searches by title, content, date, or attendees.
Guides first Granola meeting capture: live notepad notes during calls, AI-enhanced structured output via templates, and chat queries. Tests setup and workflow.
Share bugs, ideas, or general feedback.
Access Granola meeting data programmatically using three methods: the local cache file (zero-auth, offline), the MCP server (AI agent integration), or the Enterprise API (workspace-wide access). Build developer workflows that turn meeting outcomes into code tasks, documentation, and project artifacts.
Granola stores meeting data in a local JSON cache file:
# macOS cache location
CACHE_FILE="$HOME/Library/Application Support/Granola/cache-v3.json"
# Check if cache exists and get size
ls -lh "$CACHE_FILE"
The cache has a double-JSON structure (JSON string inside JSON):
#!/usr/bin/env python3
"""Extract meetings from Granola local cache."""
import json
from pathlib import Path
CACHE_PATH = Path.home() / "Library/Application Support/Granola/cache-v3.json"
def load_granola_cache():
raw = json.loads(CACHE_PATH.read_text())
# Cache contains a JSON string that needs secondary parsing
state = json.loads(raw) if isinstance(raw, str) else raw
data = state.get("state", state)
return {
"documents": data.get("documents", {}),
"transcripts": data.get("transcripts", {}),
"meetings_metadata": data.get("meetingsMetadata", {}),
}
cache = load_granola_cache()
docs = cache["documents"]
print(f"Found {len(docs)} meetings in local cache")
# List recent meetings
for doc_id, doc in sorted(docs.items(),
key=lambda x: x[1].get("updated_at", ""),
reverse=True)[:10]:
print(f" {doc.get('title', 'Untitled')} — {doc.get('updated_at', 'N/A')}")
Granola's official MCP integration connects meeting context to AI tools:
// claude_desktop_config.json or .mcp.json
{
"mcpServers": {
"granola": {
"command": "npx",
"args": ["-y", "granola-mcp-server"]
}
}
}
With MCP connected, Claude Code and Cursor can:
Community MCP servers with additional features:
pedramamini/GranolaMCP — CLI + programmatic + MCP access, reads local cachemishkinf/granola-mcp — semantic search with LanceDB vector embeddingsproofgeist/granola-mcp-server — lightweight local cache reader#!/usr/bin/env python3
"""Extract action items from Granola notes and create GitHub issues."""
import json, re, subprocess
from pathlib import Path
def extract_action_items(note_content: str) -> list[dict]:
"""Parse action items from enhanced Granola notes."""
items = []
# Matches: - [ ] @person: task description
pattern = r'- \[ \] @?(\w+):?\s+(.+)'
for match in re.finditer(pattern, note_content):
items.append({
"assignee": match.group(1),
"task": match.group(2).strip(),
})
return items
def create_github_issue(repo: str, title: str, body: str, assignee: str):
"""Create a GitHub issue using gh CLI."""
cmd = [
"gh", "issue", "create",
"--repo", repo,
"--title", title,
"--body", body,
"--assignee", assignee,
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f" Created: {result.stdout.strip()}")
else:
print(f" Error: {result.stderr.strip()}")
# Usage with cache data
cache = load_granola_cache() # from Step 1
for doc_id, doc in cache["documents"].items():
content = doc.get("last_viewed_panel", {})
# ProseMirror content needs text extraction
text = json.dumps(content) # simplified — parse nodes for production
actions = extract_action_items(text)
for action in actions:
print(f"[{action['assignee']}] {action['task']}")
#!/bin/bash
set -euo pipefail
# Sync latest Granola meeting notes to project documentation
NOTES_DIR="$HOME/dev/meeting-notes"
mkdir -p "$NOTES_DIR"
# Extract recent meeting titles and dates using Python
python3 -c "
import json
from pathlib import Path
cache_path = Path.home() / 'Library/Application Support/Granola/cache-v3.json'
if cache_path.exists():
raw = json.loads(cache_path.read_text())
state = json.loads(raw) if isinstance(raw, str) else raw
data = state.get('state', state)
docs = data.get('documents', {})
for doc_id, doc in sorted(docs.items(),
key=lambda x: x[1].get('updated_at', ''),
reverse=True)[:5]:
title = doc.get('title', 'Untitled').replace(' ', '-').lower()
date = doc.get('created_at', 'unknown')[:10]
print(f'{date}_{title}')
"
Reference Granola meetings in commits and PRs:
# Reference meeting in commit message
git commit -m "feat: implement user onboarding flow
Per meeting 2026-03-22 'Sprint Planning Q1':
- Agreed on 3-step wizard approach
- Sarah approved the design mockups
- Due by April 15
Action items from Granola note: [link]"
| Error | Cause | Fix |
|---|---|---|
| Cache file not found | Granola not installed or never launched | Install Granola and capture at least one meeting |
| JSON parse error | Double-JSON structure not handled | Parse the outer string first, then parse the inner object |
| MCP server not connecting | Wrong config path | Verify claude_desktop_config.json location for your OS |
| Empty transcripts | Transcript stored separately from document | Check cache["transcripts"] keyed by document ID |
| Stale cache data | Cache not refreshed | Restart Granola to force cache update |
Proceed to granola-sdk-patterns for Zapier automation workflows.