Skip to content

Guides

Expose credentials to Claude Code

Build and run your OWN MCP server that exposes Alter-managed credentials to Claude Code as callable tools. To connect Alter's hosted documentation server instead, see the Alter Docs MCP 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.proxy_request() calls under the hood, scoped to a managed agent, and attributed per tool in audit events. Proxy mode is required for delegated grants and human-in-the-loop policies.

The flow:

  1. An operator provisions an MCP-bound agent in Alter.
  2. The MCP server (built with alter-sdk’s MCP helpers) runs locally or in a container, holding only the agent’s API key.
  3. Claude Code is configured to talk to the MCP server.
  4. Each tool call is bound to the agent identity in the audit log.
  • An app and an agent created for the MCP server.
  • Claude Code installed locally (claude --version).
  • Python 3.11+ to run the MCP server.

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.
Terminal window
pip install 'alter-sdk[mcp]'
server.py
import os
from fastmcp import FastMCP
from alter_sdk import Agent, PendingApproval
from 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."""
result = await ctx.proxy_request(
"GET",
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
query_params={"q": query, "maxResults": "10"},
)
if isinstance(result, PendingApproval):
return {
"status": "approval_required",
"approval_id": str(result.approval_id),
"urls": [gate.approval_url for gate in result.gates],
}
return result.body_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."""
result = await ctx.proxy_request(
"POST",
"https://slack.com/api/chat.postMessage",
json={"channel": channel, "text": text},
)
if isinstance(result, PendingApproval):
return {
"status": "approval_required",
"approval_id": str(result.approval_id),
"urls": [gate.approval_url for gate in result.gates],
}
return result.body_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.proxy_request(...) and the provider, audit context, and agent attribution are forwarded automatically. A scope-reauthorization error is translated into an MCP-friendly remediation response. If the agent has no delegated grant, NoDelegatedGrantError is surfaced so the application can start a Connect session bound to that agent.

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 Docs 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.

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.

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.

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):
result = await ctx.proxy_request(
"GET",
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
query_params={"q": query},
)
if isinstance(result, PendingApproval):
return {
"status": "approval_required",
"approval_id": str(result.approval_id),
"urls": [gate.approval_url for gate in result.gates],
}
return result.body_json()

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.

SymptomLikely causeFix
Tools don’t appear in Claude CodeMCP server failed to start.Check Claude Code’s MCP logs; run python server.py directly to surface errors.
NoDelegatedGrantError on a tool callThe agent has no delegation for the provider.Run a Connect session with agent=<this_agent_id> to delegate.
AgentRevokedErrorOperator revoked the agent.Provision a new agent and update the MCP env var.
Conversation context not in auditagent.trace() not used inside tool bodies.Wrap tool bodies in an agent.trace() block.
alter policy rules create exits 2 on an ip_allowlist ruleThe allow list matches every source IP — a /0 catch-all (including zero-padded /00 / /000), or a split-range set whose union covers the whole space (e.g. 0.0.0.0/1 + 128.0.0.0/1).Use concrete addresses or narrower CIDRs; an allow-all is rejected locally before any network call.

Report an issue with this page

Necessary

Required for sign-in, security, authorization, and remembering your choices.

Always active

Analytics

Helps us understand which product and documentation features are useful.

Performance diagnostics

Uses performance tracing and privacy-masked session replay to diagnose problems.

You can change these choices at any time from Cookie settings.