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
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.
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
List meetings within a time range — this_week, last_week, last_30_days, or a custom ISO date range. Returns titles and metadata
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.