Connectors
/
Granola MCP
Live · 4 tools

Granola MCP Integration for AI Agents

Granola's meeting notes are right there in the app. Getting your agent to query them correctly — for real users, across tenants, without wrestling with MCP OAuth and dynamic client registration — is the part that takes longer than it should.
Granola MCP
Live

Meeting Intelligence

Productivity

Status
Live
Tools
4 pre-built
Auth
OAuth 2.1 + PKCE
Credential storage
Sandbox support

OAuth 2.1 + PKCE

Auto token refresh

Revocation detection

Dynamic client registration

The real problem

Why this is harder than it looks

Granola exposes its meeting data through an official MCP server at https://mcp.granola.ai/mcp. The fact that it's MCP — not a conventional REST API — is what catches most developers off guard. MCP authentication uses OAuth 2.1 with PKCE plus dynamic client registration: your application registers itself with the authorization server at runtime rather than through a static developer portal. There is no "create an app, get a Client ID" flow you can complete once and hardcode. Each deployment negotiates its own client credentials dynamically, which means the standard OAuth patterns most developers know don't apply here as-is.

In a multi-tenant product — where each user connects their own Granola account — you also need per-user token storage that's properly isolated. One user's Granola session must never be accessible to another, even if both are employees of the same company. Access tokens expire and need proactive refresh, not reactive retry. And if a user revokes your app's access from Granola's settings, your system needs to detect that and stop making calls on their behalf rather than returning silent errors.

There's also a plan-tier constraint worth knowing before you build: on Granola's Basic (free) plan, the MCP server only surfaces notes from the last 30 days, and transcript access is not available. On Business and above, full history and transcripts are unlocked. This is a Granola access-control decision, not something your integration can override — but it shapes what your agent can reliably promise to users.

Scalekit handles the dynamic client registration, token vault, per-user isolation, refresh lifecycle, and revocation detection. Your agent names a tool and passes parameters.

Capabilities

What your agent can do with Granola MCP

Once connected, your agent has 4 pre-built tools covering Granola's core meeting intelligence workflows:

  • Ask natural-language questions across meeting history: query decisions, action items, follow-ups, and themes across all past meetings in a single call
  • List meetings by time range: fetch meeting titles and metadata for any window — this week, last week, last 30 days, or a custom date range
  • Fetch full meeting details: retrieve AI-generated summaries, private notes, attendees, and metadata for one or more meetings by ID
  • Pull verbatim transcripts: retrieve the exact spoken wording from any meeting when summaries aren't precise enough
Setup context

What we're building

This guide connects a meeting intelligence agent to Granola MCP — helping users surface decisions, action items, and context from past meetings without leaving your product.

🤖
Example agent
Meeting intelligence assistant querying and summarizing Granola notes on behalf of each user
🔐
Auth model
B2B SaaS — each user connects their own Granola account. identifier = your user ID
⚙️
Scalekit account
app.scalekit.com — Client ID, Secret, Env URL
🐍
Runtime
Python 3.8+ · Node.js also available
Setup

1 Setup: One SDK, One credential

Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no Granola secrets, no user tokens, nothing belonging to your users.

pip install scalekit-sdk-python
npm install @scalekit-sdk/node
import scalekit.client import os from dotenv import load_dotenv load_dotenv() scalekit = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENV_URL"), ) actions = scalekit.actions
import { ScalekitClient } from '@scalekit-sdk/node'; import 'dotenv/config'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENV_URL, process.env.SCALEKIT_CLIENT_ID, process.env.SCALEKIT_CLIENT_SECRET ); const actions = scalekit.actions;
Already have credentials?
Connected Accounts

2 Per-User Auth: Creating connected accounts

Each user gets their own Connected Account, giving them a dedicated auth context. The identifier is any unique string from your system — a UUID, email, whatever you use internally.

response = actions.get_or_create_connected_account( connection_name="granolamcp", identifier="user_gr_456" # your internal user ID ) connected_account = response.connected_account print(f"Status: {connected_account.status}") # Status: PENDING — user hasn't authorized yet
const response = await actions.getOrCreateConnectedAccount({ connectionName: "granolamcp", identifier: "user_gr_456" // your internal user ID }); const connectedAccount = response.connectedAccount; console.log(`Status: ${connectedAccount.status}`); // Status: PENDING — user hasn't authorized yet

This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.

Authorization Flow

3 The authorization flow

The user authorizes your agent once. Scalekit generates the OAuth URL with PKCE and handles dynamic client registration with Granola's MCP server automatically. After approval, you never see the token.

if connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name="granolamcp", identifier="user_gr_456" ) # Redirect user → Granola's OAuth consent screen # Scalekit captures the token on callback return redirect(link.link)
if (connectedAccount.status !== "ACTIVE") { const { link } = await actions.getAuthorizationLink({ connectionName: "granolamcp", identifier: "user_gr_456" }); // Redirect user → Granola's OAuth consent screen // Scalekit captures the token on callback return redirect(link); }
Token management is automatic
After the user approves, Scalekit stores encrypted tokens and the connected account moves to ACTIVE. Access tokens refresh before expiry. If a user revokes access from Granola's settings, the account moves to REVOKED — no silent failures. Check account.status before critical operations.
No manual app registration required
Granola MCP uses dynamic client registration — there is no Granola developer portal where you register an app and obtain static credentials. Scalekit handles the registration handshake with Granola's MCP server automatically. No client ID or secret from Granola's side is needed to get started.
Calling Granola MCP

4 Calling Granola MCP: What your agent writes

With the connected account active, your agent calls Granola actions via execute_tool. Name the tool, pass parameters. Scalekit handles token retrieval and request construction against Granola's MCP server.

Query meetings with natural language

The most powerful starting point. Pass any natural-language question and Granola's MCP server returns a synthesized answer with inline citations back to source meeting notes. Use this for open-ended questions about decisions, action items, or recurring themes.

result = actions.execute_tool( identifier="user_gr_456", tool_name="granolamcp_query_granola_meetings", tool_input={ "query": "What decisions and follow-ups came out of last week's customer calls?" } ) # Returns a synthesized answer with citations to source meeting notes
const result = await actions.executeTool({ identifier: "user_gr_456", toolName: "granolamcp_query_granola_meetings", toolInput: { "query": "What decisions and follow-ups came out of last week's customer calls?" } }); // Returns a synthesized answer with citations to source meeting notes

List meetings by time range

Fetch meeting titles and metadata within a window before drilling into specific meetings. Use time_range for presets or pass custom_start and custom_end as ISO dates for a precise window.

result = actions.execute_tool( identifier="user_gr_456", tool_name="granolamcp_list_meetings", tool_input={ "time_range": "last_week" # options: "this_week", "last_week", "last_30_days", "custom" } ) # Returns list of meetings with titles, IDs, and metadata
const result = await actions.executeTool({ identifier: "user_gr_456", toolName: "granolamcp_list_meetings", toolInput: { "time_range": "last_week" // options: "this_week", "last_week", "last_30_days", "custom" } }); // Returns list of meetings with titles, IDs, and metadata

Fetch full meeting details

Once you have meeting IDs from a list or query, fetch rich details — AI-generated summary, private notes, attendees, and metadata — for up to 10 meetings in one call.

result = actions.execute_tool( identifier="user_gr_456", tool_name="granolamcp_get_meetings", tool_input={ "meeting_ids": [ "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "b2c3d4e5-f6a7-8901-bcde-f12345678901" ] } ) # Returns AI summary, private notes, attendees, and metadata per meeting
const result = await actions.executeTool({ identifier: "user_gr_456", toolName: "granolamcp_get_meetings", toolInput: { "meeting_ids": [ "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "b2c3d4e5-f6a7-8901-bcde-f12345678901" ] } }); // Returns AI summary, private notes, attendees, and metadata per meeting

Retrieve a verbatim transcript

When exact wording matters — legal review, compliance, precise quoting — fetch the full transcript for a specific meeting. Returns only verbatim content, not summaries or notes.

result = actions.execute_tool( identifier="user_gr_456", tool_name="granolamcp_get_meeting_transcript", tool_input={ "meeting_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ) # Returns verbatim transcript text for the meeting
const result = await actions.executeTool({ identifier: "user_gr_456", toolName: "granolamcp_get_meeting_transcript", toolInput: { "meeting_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }); // Returns verbatim transcript text for the meeting
Framework wiring

5 Wiring into your agent framework

Scalekit integrates directly with LangChain. The agent decides what to query; Scalekit handles auth on every invocation. No token plumbing in your agent logic.

from langchain_anthropic import ChatAnthropic from langchain.agents import AgentExecutor, create_tool_calling_agent from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from scalekit.langchain import get_tools granola_tools = get_tools( connection_name="granolamcp", identifier="user_gr_456" ) prompt = ChatPromptTemplate.from_messages([ ("system", "You are a meeting intelligence assistant. Use the available tools to help the user find decisions, action items, and context from their Granola meeting notes."), MessagesPlaceholder("chat_history", optional=True), ("human", "{input}"), MessagesPlaceholder("agent_scratchpad"), ]) agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), granola_tools, prompt) result = AgentExecutor(agent=agent, tools=granola_tools).invoke({ "input": "Summarize all action items from my meetings this week and group them by person" })
import { ChatAnthropic } from "@langchain/anthropic"; import { AgentExecutor, createToolCallingAgent } from "langchain/agents"; import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts"; import { getTools } from "@scalekit-sdk/langchain"; const granolaTools = getTools({ connectionName: "granolamcp", identifier: "user_gr_456" }); const prompt = ChatPromptTemplate.fromMessages([ ["system", "You are a meeting intelligence assistant. Use the available tools to help the user find decisions, action items, and context from their Granola meeting notes."], new MessagesPlaceholder("chat_history", true), ["human", "{input}"], new MessagesPlaceholder("agent_scratchpad"), ]); const agent = await createToolCallingAgent({ llm: new ChatAnthropic({ model: "claude-sonnet-4-6" }), tools: granolaTools, prompt }); const result = await AgentExecutor.fromAgentAndTools({ agent, tools: granolaTools }).invoke({ input: "Summarize all action items from my meetings this week and group them by person" });
Other frameworks supported
Tool reference

All 4 Granola MCP tools

Grouped by function. Your agent calls tools by name — no API wrappers to write.

Query & Discovery
granolamcp_query_granola_meetings
Query meeting notes using natural language. Returns a synthesized answer with inline citations to source meetings. Optionally scope to specific meeting IDs via document_ids
granolamcp_list_meetings
List meetings within a time range — this_week, last_week, last_30_days, or a custom ISO date range. Returns titles and metadata
Meeting Details & Transcripts
granolamcp_get_meetings
Fetch AI-generated summary, private notes, attendees, and metadata for up to 10 meetings by UUID. Use after granolamcp_list_meetings to drill into specific meetings
granolamcp_get_meeting_transcript
Retrieve the full verbatim transcript for a specific meeting by UUID. Returns spoken content only — not summaries or notes
Connector notes

Granola MCP-specific behavior

Access scope is limited to the user's own notes
Granola's MCP server only surfaces notes where the authenticated user is the owner. Notes that have been shared with the user by a colleague are not accessible via MCP. Build your agent's prompts and UX around this constraint — querying for "all notes from our team's meetings" will only return meetings the connected user personally recorded.
Plan tier determines history depth and transcript access
On Granola's Basic (free) plan, granolamcp_list_meetings and granolamcp_query_granola_meetings are limited to the last 30 days, and granolamcp_get_meeting_transcript is not available. Business plan and above unlock full history and transcripts. This is enforced by Granola's MCP server — your agent will receive an access error for out-of-scope requests, not a silent empty result.
Preserve citations in user-facing responses
granolamcp_query_granola_meetings returns inline citations back to the source meeting notes. When surfacing the response to your users, keep those citations intact — they let users verify the answer against the original meeting context rather than trusting the synthesis blindly.
Infrastructure decision

Why not build this yourself

Granola's MCP server is documented. The OAuth flow is public. But here's what you're actually signing up for:

PROBLEM 01
MCP OAuth uses dynamic client registration — your app negotiates credentials with Granola's server at runtime rather than through a static developer portal, which is a non-standard pattern most OAuth libraries don't handle out of the box
PROBLEM 02
Access token refresh must be proactive, running before expiry — a stale token mid-query returns an error with no automatic retry, breaking the user's agent session
PROBLEM 03
Per-user token isolation in a multi-tenant system — one user's Granola session must never be accessible to another user, even within the same organization
PROBLEM 04
Revocation detection when a user disconnects your app from Granola — and immediate handling so the agent stops attempting calls rather than returning opaque errors

That's one connector. Your agent product will eventually need Gmail, Slack, Salesforce, Notion, and whatever else your customers ask for. Each has its own OAuth quirks and failure modes.

Scalekit maintains every connector. You maintain none of them.

Ready to ship

Tap into your meetings

Free to start. MCP OAuth fully handled.
Granola MCP
Live

Meeting Intelligence

Productivity

Status
Live
Tools
4 pre-built
Auth
OAuth 2.1 + PKCE
Credential storage
Sandbox support