TL;DR
- Jam's hosted MCP server is the primary programmatic surface: 33 documented tools, two of them aliases. Jam publishes no general-purpose REST API reference.
- The direct path is the Jam CLI plus webhooks. It adds what MCP lacks: creating Jams from files or Playwright traces, recording a screen, and reacting to
jam.created. - Jam MCP auth is not OAuth-only. Personal access tokens (PATs) cover headless clients, but each is tied to one user and workspace and expires within a year.
- In a multi-tenant agent, both paths leave one Jam credential per user, with storage, refresh, and revocation on you.
- Scalekit's Jam MCP connector vaults and refreshes each user's token, logs every
execute_toolcall, and scopes tools per agent role through Virtual MCP servers.
Why Jam breaks the usual MCP vs API choice?
Your agent needs to open a Jam and do something useful with it: read the failing requests, correlate the console errors, file the ticket. Jam ships a hosted MCP server, a CLI, and webhooks. What it does not ship is the thing most teams look for first: a documented public REST API. That flips the usual MCP vs API question. For Jam, "the API" is a CLI binary and an event stream, and the MCP server is where most of the capability lives. Here's how to pick, and what you still own on either path.
What Jam MCP and the alternatives actually are
Two objects are being compared, but not the two most MCP vs API comparisons assume.
Jam MCP
Jam's MCP server is a remote server hosted and maintained by Jam, available since August 2025. Jam's MCP documentation covers setup for Claude, ChatGPT, Claude Code, Cursor, VS Code, Windsurf, Codex, and OpenCode. The agent receives a Jam link or ID, and the tools load that Jam's recording, console, network, and event data into context.
Auth takes two forms. Interactive clients complete a browser OAuth flow and pick a workspace. Headless clients send a PAT as a bearer token. Either way, MCP mirrors the user's existing Jam permissions: it grants nothing that user could not already see in the Jam web or mobile apps.
A CLI and webhooks, not a REST reference
Jam's documentation index has no public REST API reference as of September 2026. The non-MCP surface is two things. The Jam CLI is a native binary for macOS, Linux, and Windows that exposes every read and write as a command, emits JSON when piped, and publishes its machine-readable command schema through jam agent-context.
Jam webhooks push jam.created and recording_link.created events to your HTTPS endpoint, signed in the Standard Webhooks format through Svix. The CLI talks to a Jam backend, but that backend is not documented as a contract you can build against directly. Treat the CLI as the API.
Comparing them where it matters for agents
The four dimensions below are fixed across this series. For Jam, the gap runs in an unusual direction: the MCP server is the broader surface, and the direct path fills specific holes around creation and events.
What your agent can actually do when reading Jams
For reading Jams, the two surfaces are near parity; MCP leads on video understanding.
| Capability | Jam MCP | Jam CLI and webhooks |
|---|---|---|
| Jam details and custom metadata | Yes (getDetails, getMetadata) |
Yes (jam get jam) |
| Console logs, filtered by level | Yes (getConsoleLogs) |
Yes (jam get console) |
| Network requests, filtered by status or host | Yes (getNetworkRequests) |
Yes (jam get network) |
| User events timeline | Yes (getUserEvents) |
Yes (jam get events) |
| AI video analysis of user intents | Yes (analyzeVideo) |
Partial (cached summary) |
| Video transcript, chapters, frames | Yes | Yes |
| Search and filter Jams | Yes (listJams) |
Yes (jam list jams) |
What your agent can do when writing, creating, and reacting
The real gap is on the write side. MCP can manage existing Jams; only the direct path can create one or react when one appears.
| Capability | Jam MCP | Jam CLI and webhooks |
|---|---|---|
| Comments, reactions, folders, deletes | Yes | Yes |
| Recording Links management | Yes | Yes |
| Create a Jam from a file or Playwright trace | No | Yes (jam create jam) |
| Record a window or browser into a Jam | No | Yes (jam record) |
| React to new Jams in real time | No | Yes (jam.created) |
Where the MCP surface is thinner than it looks
Three constraints surface in production, not in demos. The video tools (analyzeVideo, getVideoTranscript, getVideoChapters, getFrames) and getScreenshots are unavailable for Instant Replay Jams. getFrames works only on video Jams hosted on Cloudflare Stream. And Jam recommends feeding an agent one Jam at a time, because a single Jam's logs and network payloads can exhaust a context window.
There is also a data path to review. Jam states that some MCP tools use Google's Gemini, with training opted out and data de-identified. For regulated customers, that belongs in the security questionnaire before launch, not after.
The auth path each one puts you on
Jam MCP accepts two credential types. Browser OAuth is the default for Claude, ChatGPT, and IDE clients: the user signs in, picks a workspace, and the client holds the token. PATs skip the browser entirely and travel as Authorization: Bearer jam_pat_....
The CLI accepts the same two. jam auth login runs OAuth and stores access and refresh tokens in ~/.config/jam/credentials.json with 0600 permissions; alternatively a PAT is piped to jam auth login --token or set as JAM_TOKEN. Webhooks use a third credential: a per-endpoint whsec_ signing secret you verify with HMAC-SHA256 over the svix-id, svix-timestamp, and body.
PATs solve headless, not multi-tenant
PATs are the headless answer, and Jam designed them carefully. Each token is scoped to one workspace, tied to one user, carries mcp:read, mcp:write, or both, and has a mandatory expiry of 7, 30, or 90 days, or one year. Jam stores only a hash, so the plaintext is shown once.
That design is right for a developer's Cursor config. It is awkward for a B2B product: every customer user mints a token in Jam settings, pastes it into your app, and repeats the ritual at expiry. Both paths require per-user credential isolation in a multi-tenant B2B agent. In neither case does the path solve storage, rotation, or revocation; those are infrastructure problems regardless of which path you choose.
What Jam MCP manages for you
With MCP, Jam owns the tool schemas, the filtering logic, and the video analysis pipeline. That is real leverage. getNetworkRequests already filters by status code, host, method, and content type, which is the difference between a handful of failing requests and every request the page made landing in context.
What you still own: token storage, refresh, and handling the moment a user revokes access under Settings > MCP. The surface also moves on Jam's schedule. Jam's docs list 33 tools today, while Scalekit's Jam connector catalog lists 15, a gap that shows how quickly the server-side tool set has grown. Jam publishes no versioning scheme for MCP tool schemas, so re-list tools per run rather than hardcoding them.
What the CLI and webhooks leave with you
The CLI path means owning a binary in your agent runtime: process spawning, JSON parsing, exit codes (3 for auth failures, 7 for HTTP 429), cursor pagination capped at 500 items per page, and version pinning with jam upgrade --target. Set JAM_SKIP_UPDATE_CHECK=1 in CI so the pinned binary stays pinned.
Webhooks add a public HTTPS endpoint, signature verification, and idempotency keyed on svix-id. Jam retries failed deliveries on a fixed schedule that starts immediately and backs off to 10-hour intervals, so design for redelivery: a consumer that times out after processing will see the same svix-id again.
When Jam MCP is the right path
- Engineers paste a Jam link into Claude Code, Cursor, or VS Code and want the agent to go from bug to fix with console, network, and user events already in context
- A support or product agent triages batches of customer Jams and files grouped tickets in Linear or Jira
- The agent needs video understanding: extracted user intents, transcripts, chapters, or frames at specific timestamps
- The agent orchestrates Jam alongside other MCP tools, where one protocol across every connector keeps tool discovery uniform
When the CLI and webhooks win
- A triage agent must start the moment a Jam is created, without polling, using
jam.created - A coding agent must prove its fix by recording the working flow into a new Jam and attaching the link to the pull request
- CI turns a failing Playwright test and its
trace.zipinto a Jam with console and network events synced to the video - The agent runs in a sandbox where shelling out to a pinned binary is simpler than holding an MCP session
The credential problem that exists on both paths
Whichever path you choose, a multi-tenant Jam agent holds one Jam credential per user. Two hundred customer workspaces with 1,800 connected users means 1,800 credential lifecycles.
Tokens expire, get revoked, and fail quietly
OAuth tokens from the MCP flow need refreshing. PATs hit a hard expiry with no refresh at all; the user has to mint a new one. Any user can revoke an MCP client or a PAT from Settings > MCP, and the next tool call fails. An agent that does not check connection state before a run finds out mid-task, usually as a triage that silently never happened.
Each token also has to live somewhere: encrypted at rest, isolated per tenant, never logged, and never in the LLM context. Neither Jam path provides that.
Where Scalekit fits
Scalekit's Jam MCP connector handles the OAuth flow, token storage, and refresh, so credentials never touch the agent runtime. The user authorizes once in a browser; every later run, including background runs, resolves that user's vaulted token server-side. One caveat stated plainly: Scalekit covers the MCP path. If you also run the Jam CLI in CI for recording, that PAT stays yours to manage.
Connecting Jam to your agent through Scalekit
Scalekit exposes Jam through one connector, Jam MCP (jammcp), which routes tool calls to Jam's own MCP server. There is no separate Jam API connector, which matches Jam's own surface. The examples use Python: the Anthropic SDK for direct tool calling, then LangChain over a Virtual MCP server.
Prerequisites
- Scalekit credentials from Developers > API Credentials:
SCALEKIT_ENVIRONMENT_URL,SCALEKIT_CLIENT_ID,SCALEKIT_CLIENT_SECRET - A Jam MCP connection created under AgentKit > Connections. The
connection_namein code must match the dashboard name exactly; a mismatch is the most common integration error and surfaces as a missing tool, not an auth failure. - Packages:
pip install scalekit-sdk-python anthropic python-dotenv
Authorize the user once
The connected account is the per-user instance of the Jam connection. Check its status, send the user through the authorization link if it is not active, and fail closed if it still is not. Production apps surface the link in their own UI.
import os
from dotenv import load_dotenv
import scalekit.client
load_dotenv()
scalekit_client = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
)
actions = scalekit_client.actions
# Must match the connection name in AgentKit > Connections exactly
CONNECTION_NAME = "jammcp"
identifier = "user_123" # your app's unique ID for this user
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 Jam:", link.link)
input("Press Enter after authorizing...")
response = actions.get_or_create_connected_account(
connection_name=CONNECTION_NAME, identifier=identifier
)
if response.connected_account.status != "ACTIVE":
raise RuntimeError(
f"Jam is {response.connected_account.status}, not ACTIVE. Authorize and rerun."
)
Retrieve the tools this user can call
list_scoped_tools does not return a flat connector catalog. It returns the tools the current user's connected account is authorized to call. The code then narrows that surface to the five tools a triage role needs, because the fix for tool bloat is not better prompting. It is surface reduction.
from google.protobuf.json_format import MessageToDict
TRIAGE_TOOLS = {
"jammcp_getdetails",
"jammcp_getconsolelogs",
"jammcp_getnetworkrequests",
"jammcp_getuserevents",
"jammcp_createcomment",
}
scoped_response, _ = actions.tools.list_scoped_tools(
identifier=identifier,
filter={"connection_names": [CONNECTION_NAME]},
page_size=100,
)
llm_tools = []
for t in scoped_response.tools:
definition = MessageToDict(t.tool).get("definition", {})
if definition.get("name") in TRIAGE_TOOLS:
llm_tools.append({
"name": definition.get("name"),
"description": definition.get("description", ""),
"input_schema": definition.get("input_schema", {}),
})
Run the Claude tool-use loop
Every tool call goes through execute_tool, which resolves the user's Jam token inside Scalekit. The agent sees tool results, never credentials. The loop continues until Claude stops requesting tools.
import anthropic
client = anthropic.Anthropic()
jam_ref = "<paste a Jam link or Jam ID>"
messages = [{
"role": "user",
"content": (
f"Triage this Jam: {jam_ref}. Pull the failing network requests and "
"console errors, identify the likely root cause, and post a short "
"Markdown summary as a comment on the Jam."
),
}]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
tools=llm_tools,
messages=messages,
)
if response.stop_reason != "tool_use":
print("".join(b.text for b in response.content if b.type == "text"))
break
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = actions.execute_tool(
tool_name=block.name,
identifier=identifier,
connection_name=CONNECTION_NAME,
tool_input=block.input,
)
print("executed", block.name, getattr(result, "execution_id", None))
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result.data),
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
Multi-tool Jam agents with a Virtual MCP server
A Jam agent rarely stops at Jam. Triage ends in Linear, Jira, or GitHub. A Virtual MCP server gives that agent one endpoint exposing only the tools you allow from each connection, plus a short-lived session token per user per run. No MCP server to deploy, host, or maintain.
Define the server once per agent role
Create the server once, not once per user. The response carries a static mcp_server_url that every user and every run reuses. This one pairs four read-only Jam tools with three Linear tools; both connection names must exist in AgentKit > Connections.
from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping
vmcp_response = actions.mcp.create_config(
name="jam-bug-triage-agent",
connection_tool_mappings=[
McpConfigConnectionToolMapping(
connection_name="jammcp",
tools=[
"jammcp_getdetails",
"jammcp_getconsolelogs",
"jammcp_getnetworkrequests",
"jammcp_getuserevents",
],
),
McpConfigConnectionToolMapping(
connection_name="linear",
tools=[
"linear_teams_list",
"linear_issue_search",
"linear_issue_create",
],
),
],
)
# Store both values; every agent session reuses them
config_id = vmcp_response.config.id
mcp_server_url = vmcp_response.config.mcp_server_url
Check connections and mint a session token
Before each run, confirm the user's Jam and Linear accounts are still active, then mint a token scoped to that user. Set expiry above the expected run time. The Node.js SDK does not mint session tokens yet, so this step belongs on a Python backend.
from datetime import timedelta
accounts_response = actions.mcp.list_mcp_connected_accounts(
config_id=config_id,
identifier=identifier,
include_auth_link=True,
)
inactive = [
a for a in accounts_response.connected_accounts
if a.connected_account_status != "ACTIVE"
]
for account in inactive:
print(f"{account.connection_name} needs auth: {account.authentication_link}")
if inactive:
raise SystemExit("Authorize the connections above, then rerun.")
mcp_token = actions.mcp.create_session_token(
mcp_config_id=config_id,
identifier=identifier,
expiry=timedelta(minutes=30),
).token
Run LangChain against the scoped endpoint
LangChain connects through langchain-mcp-adapters (pip install "langchain-mcp-adapters>=0.3,<1" langchain-openai). The agent sees seven tools, acts as this user in both Jam and Linear, and nothing else is reachable.
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, ToolMessage
async def run():
client = MultiServerMCPClient(
{
"scalekit": {
"transport": "streamable_http",
"url": mcp_server_url,
"headers": {"Authorization": f"Bearer {mcp_token}"},
}
}
)
tools = await client.get_tools()
tool_map = {t.name: t for t in tools}
llm = ChatOpenAI(model="gpt-4o").bind_tools(tools)
messages = [HumanMessage(
f"Triage this Jam: {jam_ref}. Find the root cause, search Linear for "
"a duplicate, and file a new issue in the right team if none exists."
)]
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())
What the Scalekit path adds for Jam agent builders
The code above is short because the hard parts moved out of it. Here is what moved.
Downstream auth and tool-call logs
Every execute_tool call is recorded with the identifier, the connected account, and an execution ID. When a triage agent posts a wrong comment on a customer's Jam, "which user's credential did this, and when" is one lookup, not a grep across agent logs. Connected account status also tells you when a user must re-authorize, which matters because Jam revocations happen in Jam's settings, outside your app.
Least privilege at the tool level
Jam's MCP server documents destructive tools, including deleteJam and deleteFolder, which removes every Jam inside a folder with no restore. Scalekit's catalog does not list them today, but tool surfaces grow. A Virtual MCP server with an explicit four-tool allowlist keeps anything added later unreachable, rather than merely discouraged by a prompt. What the user can't do, the agent can't do; what the role doesn't need, the agent can't see.
Per-user isolation across tenants
One server definition serves every customer. Each run gets a session token bound to one user's connected accounts, so an agent acting for one customer's engineer cannot reach another customer's Jam workspace. The endpoint is static; the identity is not. Adding GitHub or Jira later is a mapping change, not a new OAuth integration.
Which one to build against
If your agent reads Jams to debug, triage, or plan fixes, build on Jam MCP. It is the broader surface, Jam maintains the schemas, and the video and network tooling is already agent-shaped.
Reach for the CLI and webhooks when the agent must create evidence rather than consume it: recording a fix, turning a Playwright trace into a Jam, or starting work the instant jam.created fires. Many production setups use both, with a webhook triggering an MCP-based triage run.
Either way, the credential problem is identical. One Jam credential per user, each needing a vault, refresh, revocation handling, and an audit trail. That is what needs production-grade infrastructure.
Top comments (0)