A LangChain tool, a scheduled job, a FastAPI service, a worker that reconciles invoices at 3am. None of them speak MCP, and all of them end up with a Slack token or a Stripe key in an environment variable. The client SDK is the same broker, reached from ordinary code: your process proves who it is, and AgentValet holds everything else.
Node and Python are ports of each other: same endpoints, same approval semantics, same timings. Pick whichever your agent already lives in. Both read the same ~/.agentvalet/agent.key, so the tooling is interchangeable and you do not need Node installed to use Python.
# Node 18+, one dependency (jose, for RS256 signing) npm install @agentvalet/client # Python 3.9+, two dependencies (httpx, pyjwt[crypto]) pip install agentvalet
npx @agentvalet/register, or agentvalet register --code <your-code> from Python. The RSA keypair is generated on your machine; only the public half is sent.fromEnv() / from_env() and start making calls. There is no server for you to deploy.@agentvalet/mcp-server instead and call use_platform: same guarantees, no code at all. This SDK is for the agents that live outside an MCP host.Full reference for both clients, including every constructor option, the environment variables each one reads, and the approval-resume semantics: Node & TypeScript and Python.
This is the whole surface. You name a platform, an endpoint and the scope you are exercising. AgentValet signs a 60-second identity assertion for your agent, checks the call against the owner's grants and policy, decrypts the real credential in memory, makes the call, and writes an audit row. The credential never appears in a response, a log line, or the model's context.
import { AgentValet } from "@agentvalet/client"; // Needs no arguments on a machine that has run the register command. const av = AgentValet.fromEnv(); const result = await av.call({ platform: "slack", endpoint: "/api/chat.postMessage", method: "POST", scope: "chat:write", data: { channel: "#general", text: "Deploy finished." }, });
from agentvalet import AgentValet with AgentValet.from_env() as av: # Deny-by-default: whatever isn't in here will raise. grants = av.list_platforms() result = av.call( platform="slack", endpoint="/api/chat.postMessage", method="POST", scope="chat:write", data={"channel": "#general", "text": "Deploy finished."}, # Shown to whoever approves, if this scope is approval-gated. reason="Notify the team that the deploy completed", )
fromEnv() reads AGENTVALET_AGENT_ID / AGENTVALET_OWNER_ID, or the bare AGENT_ID / OWNER_ID that the register command writes. It then looks for the private key in AGENT_PRIVATE_KEY_B64, AGENT_PRIVATE_KEY_PATH, AGENT_PRIVATE_KEY, and finally ~/.agentvalet/agent.key (written mode 0600). In a container, mount the key or pass it base64-encoded; nothing else needs to travel with your agent.
Mark a scope as requiring approval and nothing about your code changes. The proxy holds the action, your call() waits, you approve from your phone, the proxy runs the call and hands you the result. From your program's point of view it simply took longer. This is the piece that makes it safe to give an unattended job a scope you would never hand it outright.
If nobody answers in time you get ApprovalTimeoutError, and that is not a failure: the action is still queued server-side. Keep the approvalId and resume with waitForApproval(approvalId) later, from this process or a completely different one.
Governance you can't handle in code is just an outage. Every failure from the SDK is a typed error, so your agent can tell "I am not allowed to do this" apart from "Slack is down" apart from "a human said no", without string-matching an error envelope.
from agentvalet import AccessDeniedError try: av.call(platform="stripe", endpoint="/v1/refunds", method="POST", scope="refunds:write", data={"charge": charge_id}) except AccessDeniedError as err: # Not a crash. A governance decision your code can act on. decision = av.request_access( platform=err.platform, scope=err.scope, reason="Refund duplicate charges flagged by support", ) # decision["status"] is "approved" | "denied" | "pending"
requestAccess() asks an org admin and polls for the decision.@agentvalet/mcp-server. Claude Code, Claude Desktop, Cursor, Codex, Factory Droid. One config block and every session is governed, with no integration code to write.@agentvalet/client or agentvalet. LangChain tools, cron jobs, plain services. This page.@agentvalet/mcp-broker. Enforce grants and policy inside a server you ship to somebody else, so your users' credentials never reach your process either.One install, one register command, and the credential stops living in your environment. Free to start, no credit card.