The layers
AI Agent
Any MCP-compatible client. It never sees a raw Stellar address it has to get right by hand — see MCP Tools for how tool schemas are generated and how missing addresses default sensibly.stellar-agentgate MCP Server
A thin, replaceable layer (src/mcp-server.ts) that:
- Discovers the wallet’s current on-chain allow-list (
agent-policy.get_config) and turns it into MCP tool definitions. - Signs the resulting transaction with the agent’s own, independently powerless Ed25519 key.
- Surfaces on-chain rejections as ordinary MCP tool errors — never crashes, never retries around a rejection.
Stellar RPC
StandardsimulateTransaction → sign → sendTransaction → getTransaction flow, with one non-obvious wrinkle specific to this project’s custom-account wallet — see Signing Flow.
Smart Wallet (passkey-kit custom account)
The wallet’s own__check_auth decides, for every requested context, which registered signer(s) may authorize it. The agent’s signer is registered with SignerLimits that require the policy contract as a required co-signer for the target contract — not SignerLimits(None) (unlimited), which would let the agent’s signature alone satisfy auth with the policy never consulted at all. Getting this exactly backwards was a real bug caught during development; see the Security Model page for the full story.
Agent Policy Contract
The actual security boundary. See Security Model for the full decision logic.Target Contract
Any Soroban contract the wallet owner chooses to expose —example-vault in this repository’s demo, but the pattern generalizes to any contract whose methods make sense to gate this way.
Why this shape, not a simpler one
Why not just check permissions in the MCP server?
Why not just check permissions in the MCP server?
Because the MCP server is exactly the thing that can be compromised, misconfigured, or have a bug. An off-chain check is advisory; an on-chain check enforced by the wallet’s own
__check_auth cannot be skipped by anything that isn’t a registered, sufficiently-authorized signer.Why does the policy need to be a required CO-SIGNER, not just an independent signer?
Why does the policy need to be a required CO-SIGNER, not just an independent signer?
A signer with
SignerLimits(None) is unlimited — its own signature is sufficient for anything, and the wallet’s __check_auth never needs to consult any other signer. Scoping the agent’s signer to require the policy as a co-signer for the target contract is what actually forces every call through the policy’s allow-list and spend-cap logic.Why generate MCP tools from the chain instead of hardcoding them?
Why generate MCP tools from the chain instead of hardcoding them?
Two wallets with different policies should expose different tools automatically. Reading
agent-policy.get_config live means the MCP server’s tool list is always exactly what the current on-chain configuration allows — see MCP Tools.