Announcing CIMD support for MCP Client registration
Learn more

Roam MCP vs Roam API for AI Agents

Hrishikesh Premkumar
Founding Architect

TL;DR

  • Roam's hosted MCP server exposes 56 tools for meetings, chat, calendar, OnAir, and Magicasts. The v1 API adds streamed replies, polls, presence, group admin, audit logs, message export, and SCIM.
  • OAuth MCP connections are Personal access: the agent sees only meetings the user attended and posts as their personal bot. Roam-wide meeting access needs an org API key.
  • Roam has no client_credentials grant. OAuth access tokens last 7 days, and refresh tokens rotate.
  • Default OAuth MCP grants include every scope, writes included, so least privilege has to come from your tool surface.
  • Scalekit's Roam MCP connector handles DCR, token storage, and refresh per user. Virtual MCP servers scope the 56 tools per agent role, and every call lands in per-user tool call logs.

The Roam decision your agent is facing

Your agent needs to work inside Roam: pull yesterday's meeting summaries, chase action items, post into a group, schedule the follow-up. Roam ships a hosted MCP server and a documented v1 REST API, and both reach meetings and chat.

They are not interchangeable. They differ on what the agent can touch, whose identity it acts as, and how much of the workspace it can see. For a multi-tenant product, Roam's Personal vs Organization access split matters more than the transport. Here's how to pick.

What Roam MCP and Roam API actually are

Two surfaces, one vendor, one OAuth server. Start by confirming you are looking at the right Roam.

First, which Roam

This article is about Roam HQ, the virtual office product: chat, video meetings, AI transcripts, Magicast recordings, OnAir events, and a shared office map. Roam Research is an unrelated note-taking tool. Searches for "Roam MCP" mix the two, and many of the Roam Research results are community-built servers for a knowledge graph. Scalekit's Roam MCP connector targets Roam HQ.

The Roam MCP server

Roam maintains a hosted, remote MCP server on its API host, served over Streamable HTTP. Every tool maps to a Roam API endpoint and uses the same OAuth scopes.

Clients can authenticate three ways:

  • OAuth. The MCP authorization flow with Dynamic Client Registration (DCR), with S256 PKCE required for public clients.
  • Personal Access Token. A user-created token (rmp-) sent as a bearer token.
  • Organization API key. An admin-issued key (rmk-) sent as a bearer token.

The server also ships eight built-in prompts, such as morning_brief and weekly_digest. By default, a Roam admin must approve every new MCP connection before it can access data.

The Roam API

The v1 REST API uses RPC-style methods such as /chat.post, /meeting.list, and /group.members. Responses use an ok envelope, and a dated Roam-Version header pins their shape. Alongside it sit the OnAir API, a Webhooks API with dotted event names like meeting.ended, and a SCIM 2.0 API. The frozen v0 Alpha surface remains for existing callers.

Auth is always a bearer token: an admin-issued API key, an OAuth access token, or a PAT. Roam also publishes TypeScript, Go, and Python SDKs.

Comparing them where it matters for agents

Four dimensions decide this: capability coverage, auth model, operational surface, and fit. The Roam-specific twist is that the access model often decides more than the transport.

What your agent can actually do

Both paths cover the core agent loop. Scalekit catalogs all 56 server tools, prefixed roammcp_. Roam's own tool reference table lists fewer, so check the live list before hardcoding names.

Capability
Roam MCP
Roam API
Meeting list with summaries, action items, chapters
Yes
Yes
Verbatim transcript
Yes, WebVTT
Yes, JSON or WebVTT
Ask a question about one meeting
Yes
Yes
Natural-language meeting search
Yes, Personal only
Yes, Personal only

Chat, calendar, and events on both paths

Outside meetings, the overlap covers the actions an assistant takes on a user's behalf.

Capability
Roam MCP
Roam API
Chat history, search, post, edit bot messages
Yes
Yes
Scheduled messages
Yes
Yes
Calendar events with a Roam meeting link
Yes
Yes
OnAir events, guests, attendance
Yes
Yes
Webhook subscriptions
Subscribe, unsubscribe, failed deliveries
Same, plus webhook.list

Where only the API reaches

The gap concentrates in real-time chat UX, workspace administration, and compliance.

Capability
Roam MCP
Roam API
Streamed bot replies
No
Yes
Polls, ephemeral messages, typing indicator, link unfurls
No
Yes
Group membership admin
Partial: create, join, list, info
Full: members, add, remove, rename, archive
Presence: Will Return, status bubbles, map activity
No
Yes
Guest Badges
No
Yes
User audit log, daily message export
No
Yes, Organization access
SCIM 2.0 provisioning
No
Yes

Where the gap bites

Agent builders usually hit the streaming gap first. Through MCP, an agent replying in Roam chat can post a message and then edit it. It cannot stream tokens into one message the way chat.startStream, chat.appendStream, and chat.stopStream allow.

The compliance gap is structural. messageevent.export and userauditlog.list are Organization-only, and the OAuth MCP path is Personal.

MCP does have one thing the API lacks: its eight built-in prompts. Those are MCP prompts, not tools, so a tool-calling agent should encode the same workflows in its own system prompt.

The auth path each one puts you on

On the MCP path, a client registers itself through DCR, runs Authorization Code with S256 PKCE, and gets a token bound to the MCP resource. Connections created this way are Personal access.

Dynamically registered clients that don't request specific scopes receive Roam's default MCP scope set. That set covers every tool, including writes like chat_post, calendar_event_create, and the OnAir tools. Per-connection scope selection for OAuth clients is planned but not shipped.

Roam also advertises every tool regardless of the scopes you were granted. Calling an out-of-scope tool returns 403.

What the API auth path adds

The REST API accepts three credentials:

  • API key. Admin-issued, Organization access, posts with the app's bot persona.
  • OAuth app. Installed as Organization (admin consent) or Personal (user consent), chosen at authorization time.
  • PAT. Personal access. Lets the user pick narrow scope groups, but carries a 1,000-request daily quota.

There is no client_credentials grant; every call binds to a user or a Roam install. OAuth access tokens last 7 days and refresh tokens last 365 days. A refresh usually returns a rotated refresh token, and the old one should be discarded.

Access model decides how much of Roam the agent sees

This is the Roam-specific trap.

A Personal token sees only meetings the user attended, and it posts as that user's personal bot, a distinct persona such as "Alex's Notetaker". It never gets admin:meetings:read, because Roam strips that scope on Personal authorization. An Organization token without that scope usually sees an empty meeting set.

So a CRM-sync agent that must process every recorded call needs an org API key on either path. Custom MCP clients can send an rmk- key as a bearer token.

Personal access isolates per user by design. Organization access isolates per workspace.

What you own in production

On the MCP path. Roam maintains the tool schemas and model-facing descriptions. The meeting_list description, for example, tells the model to skip date filters for recent requests and use expand instead of fanning out. You still own token storage, refresh, revocation, and the mapping from your users to Roam grants. MCP tool schemas change when Roam updates the server.

On the API path. You own everything: schemas, cursors, the ok envelope, retries, and version pinning. Each Roam API client is created on the latest Roam-Version and never advances automatically. That prevents silent shape changes, but upgrades become your job.

Failure modes worth handling on both paths

Roam doesn't document separate MCP limits, so budget MCP calls against the API limits.

  • Rate limits. Roam allows a burst of 10 requests, then 1 request per second sustained, with Retry-After on 429. A recap agent that calls meeting_info once per meeting hits this fast.
  • Auth failures. invalid_token is worth a refresh. token_revoked is terminal: stop retrying and re-authorize.
  • Transcripts. transcript_pending means wait. transcript_unavailable means the transcript will never exist.
  • Webhooks. v1 deliveries can arrive twice with the same webhook-id, so deduplicate on it.

Use Roam MCP when

  • You are building an interactive assistant where a user asks about their own meetings and chats, in Claude, Codex, or your own chat UI.
  • The job is per-user meeting intelligence: recaps, action-item tracking, meeting prep from the user's calendar.
  • You want Roam to own tool schemas and descriptions, and you scope the tool surface at your own layer.
  • You orchestrate Roam alongside other MCP tools and want one transport for tool listing and execution.

Use the Roam API when

  • The agent replies inside Roam chat and needs streamed responses, polls, typing indicators, or ephemeral messages.
  • It runs headless and workspace-wide, such as syncing every call to a CRM with an org key holding admin:meetings:read.
  • You need audit logs, message export, group membership, SCIM, or Guest Badges.
  • You need Roam's consent screen to enforce narrow scopes, which OAuth MCP grants do not offer yet.

The credential problem that exists on both paths

Pick either path and you still hold one Roam credential per user or per workspace. Neither path gives you a vault, refresh coordination, or a revocation handler.

N users, N workspaces, N approval queues

Picture a B2B agent serving 200 customer workspaces on Personal access. It holds a Roam grant for every user who connected, and each access token expires in 7 days.

Refresh tokens rotate, so two workers refreshing the same grant can race. The loser may be holding a refresh token Roam has already replaced. Revocation arrives as a token.revoked webhook you have to subscribe to and handle. Each workspace's approval policy can also leave a new connection pending until an admin acts.

The token type differs between MCP and API. The storage, refresh, and revocation plumbing does not. For a deeper look at this challenge, see how to handle token refresh for AI agents.

Where Scalekit fits

Scalekit's Roam MCP connector runs DCR and OAuth 2.1 against Roam, stores each user's tokens in its vault, and refreshes them. Credentials never touch the agent runtime. The agent calls execute_tool with a user identifier, and Scalekit resolves that user's connected account.

For API-only endpoints, a custom connector routes REST calls through the same connected-account model. The MCP vs API choice stops being an auth infrastructure decision.

Building a Roam agent with Scalekit in Python

This walkthrough uses Python, the Scalekit SDK, and LangChain. It follows the connected-account model: authorize once, retrieve the tools this user can call, then execute.

Prerequisites and the connection name

First, create a Roam MCP connection in the Scalekit dashboard under AgentKit > Connections. Set SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET in your .env file.

The connection_name in every call must exactly match the connection name in the dashboard. roammcp is the name Scalekit's docs use, and a mismatch here is the most common integration error. Full tool schemas live on the Roam MCP connector docs.

pip install scalekit-sdk-python python-dotenv langchain-anthropic "langchain-mcp-adapters>=0.3,<1"

Authorize each Roam user once

Each user completes Roam's consent screen once. If their workspace uses Roam's default Require Approval policy, tool calls fail until an admin approves the connection in Roam Administration > Developer.

import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), ) actions = scalekit_client.actions # Must match the connection name configured in AgentKit > Connections exactly CONNECTION_NAME = "roammcp" IDENTIFIER = "user_123" # your app's stable user ID def ensure_roam_connected(identifier: str) -> None: response = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier, ) if response.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=CONNECTION_NAME, identifier=identifier, ) print("Authorize Roam:", link.link) input("Press Enter after completing Roam consent...") response = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier, ) if response.connected_account.status != "ACTIVE": raise RuntimeError( f"Roam connection is {response.connected_account.status}. " "Authorize it and run again." ) ensure_roam_connected(IDENTIFIER)

Retrieve the tools this user is authorized to call

Before any agent loop runs, load the tool surface for this user. list_scoped_tools does not return a connector catalog. It returns the tools this user's connected account is authorized to call. execute_tool then runs one of them, with Scalekit injecting the user's Roam credentials.

response, _ = scalekit_client.tools.list_scoped_tools( identifier=IDENTIFIER, filter={"connection_names": [CONNECTION_NAME]}, page_size=100, # Roam exposes 56 tools; the default page can truncate ) tool_names = [scoped.tool.definition["name"] for scoped in response.tools] print(len(tool_names), "tools authorized for", IDENTIFIER) result = actions.execute_tool( tool_name="roammcp_meeting_list", connection_name=CONNECTION_NAME, identifier=IDENTIFIER, tool_input={"limit": 5, "expand": "summary,actionItems"}, ) print(result.data)

Run a LangChain agent on a scoped Roam surface

An action-item agent needs 5 of the 56 tools. Passing tool_names to get_tools keeps the other 51 out of context.

At Scalekit's rule of thumb of about 200 tokens per tool, 56 tools cost roughly 11,000 tokens before the agent does any work. Treat that as a floor: Roam's descriptions run long, and the chat_search description alone is several hundred words. Scoping to five tools removes over 90% of that overhead. For more on this, see token-efficient tool calling strategies.

from langchain_anthropic import ChatAnthropic from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage ROAM_TOOLS = [ "roammcp_get_me", "roammcp_meeting_list", "roammcp_meeting_info", "roammcp_meeting_transcript", "roammcp_chat_post", ] tools = actions.langchain.get_tools( identifier=IDENTIFIER, connection_names=[CONNECTION_NAME], tool_names=ROAM_TOOLS, page_size=100, ) tool_map = {t.name: t for t in tools} llm = ChatAnthropic(model="claude-sonnet-5").bind_tools(tools) messages = [ SystemMessage( "You summarize Roam meetings. Prefer meeting_list with expand before " "calling meeting_info. Only fetch a transcript if the summary lacks the answer." ), HumanMessage( "Collect my open action items from this week's meetings and DM them to me in Roam." ), ] while True: response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: result = tool_map[tc["name"]].invoke(tc["args"]) messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))

Multi-tool agents: one Virtual MCP server for Roam and Linear

Most Roam agents act somewhere else too. A post-meeting agent that files Linear issues needs Roam read tools plus one Linear write tool, for every user in every tenant.

A Virtual MCP server declares that surface once per agent role and returns a static URL. There is no MCP server to deploy or host. Tool names for the second connection are on the Linear connector docs. For a broader discussion of how tool calling auth changes when you move from single-tenant to multi-tenant, see our dedicated guide.

from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping vmcp = scalekit_client.actions.mcp.create_config( name="roam-action-item-agent", description="Reads Roam meeting summaries, files Linear issues", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="roammcp", # must match AgentKit > Connections tools=[ "roammcp_meeting_list", "roammcp_meeting_info", "roammcp_chat_post", ], ), McpConfigConnectionToolMapping( connection_name="linear", # must match AgentKit > Connections tools=["linear_teams_list", "linear_issue_create"], ), ], ) config_id = vmcp.config.id mcp_server_url = vmcp.config.mcp_server_url # static; store it with the agent definition

Mint a per-user session token and run

Before each run, confirm the user's Roam and Linear accounts are both active. Then mint a short-lived session token bound to that user and pass the URL and token to the agent. One server definition serves every user: the endpoint stays static while the identity changes per run.

import asyncio from datetime import timedelta from langchain_anthropic import ChatAnthropic from langchain_core.messages import HumanMessage, ToolMessage from langchain_mcp_adapters.client import MultiServerMCPClient config_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] mcp_server_url = os.environ["SCALEKIT_MCP_SERVER_URL"] def mint_token_for(identifier: str) -> str: accounts = scalekit_client.actions.mcp.list_mcp_connected_accounts( config_id=config_id, identifier=identifier, include_auth_link=True, ) pending = [ (a.connection_name, a.authentication_link) for a in accounts.connected_accounts if a.connected_account_status != "ACTIVE" ] if pending: raise RuntimeError(f"Re-authorization needed: {pending}") return scalekit_client.actions.mcp.create_session_token( mcp_config_id=config_id, identifier=identifier, expiry=timedelta(minutes=30), ).token async def run(identifier: str, task: str) -> None: token = mint_token_for(identifier) client = MultiServerMCPClient( { "scalekit": { "transport": "streamable_http", "url": mcp_server_url, "headers": {"Authorization": f"Bearer {token}"}, } } ) tools = await client.get_tools() tool_map = {t.name: t for t in tools} llm = ChatAnthropic(model="claude-sonnet-5").bind_tools(tools) messages = [HumanMessage(task)] while True: response = await llm.ainvoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: result = await tool_map[tc["name"]].ainvoke(tc["args"]) messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"])) asyncio.run(run("user_123", "File a Linear issue for every open action item from yesterday's standup."))

Taking the API path through Scalekit

Scalekit's catalog ships Roam as an MCP connector. For API-only endpoints such as polls or streaming, register a custom REST connector that points Scalekit's Tool Proxy at Roam's v1 API.

The setup takes three steps:

  1. Create an OAuth app in Roam Administration > Developer.
  2. Create the custom connector with the payload below.
  3. Add a connection for it in the dashboard with the Roam app's client ID and secret, and register Scalekit's redirect URI on the Roam app.

For a headless, workspace-wide agent, use a BEARER auth pattern instead, so each customer's admin supplies an rmk- key once.

{ "display_name": "Roam API", "description": "Roam HQ v1 REST API through Scalekit Tool Proxy", "auth_patterns": [ { "type": "OAUTH", "display_name": "OAuth 2.0", "description": "Authorize with a Roam OAuth app registered in Roam Administration > Developer", "fields": [], "oauth_config": { "authorize_uri": "https://ro.am/oauth/authorize", "token_uri": "https://ro.am/oauth/token", "user_info_uri": "https://api.ro.am/v1/token.info", "available_scopes": [ {"scope": "chat:history", "display_name": "Read chat history", "description": "Read messages in chats the user can access", "required": true}, {"scope": "chat:send_message", "display_name": "Send messages", "description": "Post messages and polls", "required": true}, {"scope": "meetings:read", "display_name": "Read meetings", "description": "Meetings, summaries, and transcripts the user attended", "required": false}, {"scope": "user:read", "display_name": "Read users", "description": "Resolve workspace members", "required": false} ] } } ], "proxy_url": "https://api.ro.am/v1", "proxy_enabled": true }
# Get a Scalekit management token curl --location "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode "client_id=$SCALEKIT_CLIENT_ID" \ --data-urlencode "client_secret=$SCALEKIT_CLIENT_SECRET" # Create the custom Roam REST connector curl --fail-with-body --location "$SCALEKIT_ENVIRONMENT_URL/api/v1/custom-providers" \ --header "Authorization: Bearer $env_access_token" \ --header "Content-Type: application/json" \ --data @roam-api-connector.json

Calling an API-only endpoint

Once a user authorizes the new connection, actions.request proxies calls with their credentials. The path is relative to the connector's proxy_url. Pin Roam-Version explicitly, and stay under the 1 request per second sustained limit.

ROAM_API_CONNECTION = "roam-api" # must match the connection name in AgentKit > Connections ROAM_VERSION = {"Roam-Version": "2026-08-25"} def roam_post_poll(identifier: str, group_id: str, question: str, options: list[str]) -> dict: response = actions.request( connection_name=ROAM_API_CONNECTION, identifier=identifier, method="POST", path="/chat.post", headers=ROAM_VERSION, body={ "groupId": group_id, "poll": {"question": question, "options": options}, }, ) data = response.json() if not data.get("ok"): raise ValueError(f"Roam error: {data.get('error')}") return {"chatId": data.get("chatId")}

What going through Scalekit buys a Roam agent

The auth plumbing is the obvious win. Three other benefits show up once the agent runs for real users.

Tool call logs, per user

AgentKit records every tool call with its timestamp, connection, tool name, user identifier, and latency. Failed calls also carry the error code and full message.

The dashboard separates two kinds of failure:

  • Connector errors. Roam itself returned the error.
  • API errors. The call was rejected before it left Scalekit, for example because of invalid parameters or an expired token.

Say meeting_transcript starts failing for one customer. You filter to that connection and see whether it is Roam's transcript_pending, a missing scope, or a revoked grant, and exactly which users were affected. For more on surfacing this data effectively, see agent tool observability.

Virtual MCP for multi-tool, multi-tenant agents

Roam's OAuth MCP grants carry the full default scope set, so Roam will not narrow an agent's access for you. A Virtual MCP server enforces least privilege at the tool level: a recap agent whose surface excludes chat_delete and onair_event_cancel cannot call them.

One definition serves every tenant, and per-run session tokens keep each user's Roam grant isolated. What the user can't do, the agent can't do. Understanding credential ownership across agent tool-calling patterns is key to getting this isolation right.

One credential model across both paths

Built-in MCP connectors and custom REST connectors share connections, connected accounts, and the same authorization flow. Starting on MCP and adding an API-only capability later does not add a second credential system.

Which one to build against

Choose by what the agent has to do:

  • Start with Roam MCP through Scalekit if your agent answers one user's questions about their own Roam meetings and chats. Scope it with a Virtual MCP server.
  • Build against the API for the parts that reply inside Roam chat with streaming or polls, run headless across a whole workspace, or touch compliance and admin surfaces.

Most production Roam agents end up using both. The credential problem is identical on either side, which is why it belongs in infrastructure, not in your agent code. For a broader look at agent tool calling auth production patterns and anti-patterns, see our in-depth guide.

Start building your Roam agent

Roam agent builders can get implementation help from the Scalekit team: talk to us.

No items found.
Agent
Auth Quickstart
On this page
Share this article
Agent
Auth Quickstart

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.