MCP Integrations
July 15, 2026 · View on GitHub
Connect external services (Linear, Notion, …) to Prime Agent over the Model Context Protocol.
Consistent with Prime Agent's single-tool design, MCP integrations are not exposed as new agent tools. Each integration is a Python-backed skill that the model imports and calls from the IPython kernel:
import linear
issues = await linear.list_issues(team="Engineering")
The MCP connection runs inside the kernel via the official mcp Python SDK. The
host's only jobs are interactive login (browser OAuth) and minting/refreshing
credentials in auth.json.
Table of Contents
- Using a built-in integration
- How a call works
- Authoring your own integration
- The
McpIntegrationAPI - Enable-by-login lifecycle
- Caveats
Using a built-in integration
Built-in integrations (Linear, Notion) ship disabled. Logging in enables them:
- Open
/login, switch to MCP Connections, pick the integration, and complete OAuth in the browser./mcp login <name>does the same from the CLI. - Once connected, the integration's skill becomes visible to the model and is auto-imported into the kernel.
/mcplists integrations and connection status;/mcp logout <name>disconnects.
Credentials are stored once in ~/.prime/agent/auth.json under mcp:<name>.
Enablement is derived from whether valid credentials exist — there is no separate
on/off switch.
How a call works
The tool set is defined by the server, not the skill, so discover before you call — don't assume tool names or arguments:
import linear
# 1. Discover available tools
for tool in await linear.list_tools():
print(tool["name"], "-", tool["description"])
# 2. Inspect a tool's argument schema
help(linear.list_issues) # populated once list_tools() has run
# 3. Call it; keyword args match the tool's JSON Schema
result = await linear.list_issues(team="Engineering")
- Every tool is an
asyncmethod — alwaysawait. - Results are already-parsed Python: a
dictfor structured output, a string for text, or a list of content blocks otherwise. No need tojson.loadsthem. - A tool whose name isn't a valid Python identifier (e.g. Notion's
notion-search) is called via the escape hatch:await notion.call_tool("notion-search", {...}). - A call against an integration with no credentials raises
NotEnabled(telling the user to/mcp login); a tool that returns an error raisesMcpToolError.
Authoring your own integration
An integration is a Python skill package whose
module subclasses McpIntegration. The built-in linear / notion packages are
the reference implementations.
1. Declare the server
Add it under mcpServers in ~/.prime/agent/settings.json (or project
.prime/agent/settings.json):
// ~/.prime/agent/settings.json
{
"mcpServers": {
"acme": {
"type": "http",
"url": "https://mcp.acme.com/mcp",
"oauth": true
}
}
}
Currently only remote "http" servers are supported by McpIntegration. HTTP
server fields:
| Field | Meaning |
|---|---|
type | Must be "http" |
url | The MCP endpoint |
oauth | true to use the browser OAuth flow (requires the server to support dynamic client registration) |
bearerTokenEnvVar | Name of an env var holding a static bearer token, instead of OAuth |
headers | Extra static HTTP headers sent on every request |
enabled | Set false to force-disable even when credentials exist |
stdio(local-subprocess) servers are not yet wired through to the kernel — the host drops non-HTTP entries — so an integration must target an HTTP endpoint.
2. Ship the skill package
Create a skill directory (any skills location, e.g.
~/.prime/agent/skills/acme/) with the standard Python-skill layout:
acme/
SKILL.md
pyproject.toml
src/acme/__init__.py
pyproject.toml (depends on mcp, httpx, and prime-agent-runtime):
[project]
name = "prime-agent-skill-acme"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["mcp", "httpx", "prime-agent-runtime"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/acme"]
src/acme/__init__.py:
from rlm import McpIntegration
class Acme(McpIntegration):
server = "acme" # matches the mcpServers key / auth.json `mcp:acme`
url = "https://mcp.acme.com/mcp"
acme = Acme()
# Forward bare module access (`import acme; await acme.<tool>(...)`) to the
# instance, but NOT the names the kernel bootstrap probes — forwarding `run`
# would make it treat the module as a callable skill and break tool dispatch.
_RESERVED = {"run", "__wrapped__", "__call__"}
def __getattr__(name):
if name.startswith("_") or name in _RESERVED:
raise AttributeError(name)
return getattr(acme, name)
The base class connects with the mcp SDK, resolves the URL/headers from the host
(honoring the mcpServers config), injects the bearer token from auth.json
(refreshing when expired), and binds the server's tools as async methods. Authoring
is a few lines — the package above is the whole integration.
Authentication
- OAuth (
"oauth": true): the user runs/login→ MCP Connections → your server (or/mcp login acme). Works when the server supports OAuth 2.1 dynamic client registration (RFC 7591); login discovers the auth server, registers a client, and runs PKCE. Servers requiring a pre-registered client id are not yet supported viamcpServers. - Static bearer token (
"bearerTokenEnvVar": "ACME_TOKEN"): no login needed; the integration is "connected" whenever that env var is set. Set the matchingbearer_token_env = "ACME_TOKEN"on the subclass.
The McpIntegration API
Imported from rlm (from rlm import McpIntegration).
Class attributes to set on your subclass:
server: str— required; themcpServerskey andauth.jsoncredential id.url: str | None— the remote endpoint (required unless you override_open_sessionfor a non-HTTP transport).bearer_token_env: str | None— optional env var holding a static bearer token.
Methods:
await list_tools() -> list[dict]— the server's tools as[{name, description, inputSchema}]. Also populates the docstrings shown byhelp(integration.<tool>).await call_tool(name, arguments={}) -> Any— explicit call; the escape hatch for non-identifier tool names.integration.<tool>(**kwargs)— auto-bound async method for any discovered tool.
Exceptions (both importable from rlm):
NotEnabled— raised when no usable credentials exist (not logged in).McpToolError— raised when a tool call returns a result flagged as an error.
Enable-by-login lifecycle
This auth-gating applies to the built-in integrations (Linear, Notion):
- The built-in skill ships installed but disabled — excluded from the prompt and not imported into the kernel — because no credentials exist.
- The user logs in; credentials land in
auth.jsonundermcp:<server>. - A resource reload (automatic after
/login//mcp login, or/reload) detects the credentials, enables the skill, and the kernel installs + imports the package. - Logout (or losing credentials) disables it again.
If you log in mid-turn, the reload is deferred — run /reload after the turn to
activate the integration.
User-authored integrations are not auth-gated this way. A skill you drop into
a skills directory is loaded like any other skill — visible to the model and
imported into the kernel immediately, regardless of auth.json. It simply fails
at call time with NotEnabled until credentials exist. So make the skill's
SKILL.md tell the model how to connect when a call raises NotEnabled, matching
the auth mode you configured:
- OAuth (
"oauth": true): instruct the user to run/mcp login <server>(or/login→ MCP Connections)./mcp loginonly works for OAuth servers. - Bearer token (
bearerTokenEnvVar): instruct the user to set that env var — do not point them at/mcp login, which has no provider for a bearer-only server and reports "Unknown MCP integration".
Caveats
- Discover before assuming. Tool names and argument schemas come from the
server and can change; call
list_tools()/help()rather than hardcoding. - Custom kernel + name collisions. The kernel import name is the
servervalue. On a customPRIME_AGENT_KERNEL_PYTHONthat already has an unrelated PyPI package of the same name (e.g.notion),import <name>may resolve to that package instead. Use the default managed kernel venv to avoid this. - Overriding a built-in name. Declaring an
mcpServersentry whose key matches a built-in (e.g.linear) with a customurlpoints the integration at your URL. A previously stored official credential is not reused for the override, to avoid sending the official token to your endpoint. Authenticate such an override viabearerTokenEnvVaronly — OAuth credentials are not honored for a catalog-name override. (Use a name that isn't a built-in to get OAuth.) - Multi-session daemon. OAuth provider registration is process-global; a user-declared server unique to one daemon session is re-registered on that session's next reload.