Guides
Integrate with Claude Code (MCP)
Expose Alter-managed credentials to Claude Code via a Model Context Protocol server.
By the end of this guide, Claude Code can call third-party APIs through Alter using an MCP server. Tool calls in the conversation become agent.request() calls under the hood, scoped to a managed agent, audited per-tool.
The flow:
- An operator provisions an MCP-bound agent in Alter.
- The MCP server (built with
alter-sdk’s MCP helpers) runs locally or in a container, holding only the agent’s API key. - Claude Code is configured to talk to the MCP server.
- Each tool call is bound to the agent identity in the audit log.
Prerequisites
Section titled “Prerequisites”- An app and an agent created for the MCP server.
- Claude Code installed locally (
claude --version). - Python 3.10+ to run the MCP server.
Walkthrough
Section titled “Walkthrough”1. Provision the agent
Section titled “1. Provision the agent”In the developer portal, create a new agent named claude-code-mcp. Mint a per-agent key from Agents → claude-code-mcp → Keys (per-agent keys currently mint with the legacy alter_key_… prefix) and store it where the MCP process will read it. Bind whichever providers the MCP server should be able to reach:
- For user-owned data, have the user run a Connect session with
agent=<this_agent_id>to delegate access. - For operator-owned credentials, issue managed-secret grants to the agent.
2. Install the SDK with MCP extras
Section titled “2. Install the SDK with MCP extras”pip install 'alter-sdk[mcp]'3. Write the MCP server
Section titled “3. Write the MCP server”import osfrom fastmcp import FastMCPfrom alter_sdk import Agentfrom alter_sdk.mcp import AlterContext, AlterMCP
mcp = FastMCP("alter-tools")agent = Agent(api_key=os.environ["AGENT_API_KEY"])alter = AlterMCP(agent)
@mcp.tool()@alter.tool(provider="google")async def list_events(ctx: AlterContext, query: str) -> dict: """List the user's Google Calendar events matching a query.""" response = await ctx.request( "GET", "https://www.googleapis.com/calendar/v3/calendars/primary/events", query_params={"q": query, "maxResults": "10"}, ) return response.json()
@mcp.tool()@alter.tool(provider="slack")async def send_slack(ctx: AlterContext, channel: str, text: str) -> dict: """Post a message to a Slack channel.""" response = await ctx.request( "POST", "https://slack.com/api/chat.postMessage", json={"channel": channel, "text": text}, ) return response.json()
if __name__ == "__main__": mcp.run()AlterMCP(agent) wraps an SDK client and exposes the @alter.tool(...) decorator. Stack it under FastMCP’s own @mcp.tool() to register the function as an MCP tool. The @alter.tool decorator injects an AlterContext parameter (hidden from the generated MCP tool schema); tool bodies call ctx.request(...) and the provider, audit context, and agent attribution are forwarded automatically. Missing-grant and insufficient-scope errors are translated into MCP-friendly responses (with a Connect URL when re-authorization is required).
4. Register the server with Claude Code
Section titled “4. Register the server with Claude Code”Claude Code reads project-scoped MCP servers from a .mcp.json file in the project root (the same file alter init creates when it registers the Alter onboarding MCP server). Add an entry for the server:
{ "mcpServers": { "alter": { "command": "python", "args": ["/absolute/path/to/server.py"], "env": { "AGENT_API_KEY": "alter_key_..." } } }}Restart Claude Code. The tools appear in the conversation.
5. Try it
Section titled “5. Try it”In Claude Code, ask the model to use the tools:
Find my next three calendar events and send a summary to #standup.
Claude calls list_events, then send_slack. Each call appears in the audit log under the claude-code-mcp agent, with the tool name (list_events, send_slack) recorded in the context.
Patterns
Section titled “Patterns”Scoping the MCP agent to one user
Section titled “Scoping the MCP agent to one user”For a single-user MCP install, the simplest setup is one Alter agent per Claude Code installation, with delegations from that one user. The agent only ever needs that one user’s grants, so there’s no per-call user disambiguation.
For multi-tenant MCP deployments (one server, many users), wire user_token_getter and pass the calling user’s identity through the MCP request context.
Distinguishing tool calls in audit
Section titled “Distinguishing tool calls in audit”The @alter.tool() decorator automatically records the tool name in the audit context. To add more (run ID, conversation ID, custom tags), wrap the tool body in an agent.trace() block:
@alter.tool(provider="google")async def list_events(ctx: AlterContext, query: str) -> dict: async with agent.trace(run_id=conversation_id): response = await ctx.request( "GET", "https://www.googleapis.com/calendar/v3/calendars/primary/events", query_params={"q": query}, ) return response.json()Human-in-the-loop for destructive tools
Section titled “Human-in-the-loop for destructive tools”For tools that mutate state (sending email, posting to Slack, refunding a charge), configure an approval policy on the agent so destructive calls pause for explicit approval. See Add human-in-the-loop approvals.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Fix |
|---|---|---|
| Tools don’t appear in Claude Code | MCP server failed to start. | Check Claude Code’s MCP logs; run python server.py directly to surface errors. |
NoDelegatedGrantError on a tool call | The agent has no delegation for the provider. | Run a Connect session with agent=<this_agent_id> to delegate. |
AgentRevokedError | Operator revoked the agent. | Provision a new agent and update the MCP env var. |
| Conversation context not in audit | agent.trace() not used inside tool bodies. | Wrap tool bodies in an agent.trace() block. |