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:
- 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.11+ 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 Agent, PendingApprovalfrom 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.
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 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.
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): 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()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. |
alter policy rules create exits 2 on an ip_allowlist rule | The 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. |