Use as a library
Introduction
Section titled “Introduction”The MCP server ships as a runnable binary (npx -y @piprail/mcp), but it’s also a plain
library with no side effects. If you’re building your own MCP host, an agent runtime, or a
custom transport, you can construct the server yourself and own the connection.
For the common case there’s a single turnkey entry — startServer — that runs the whole boot
flow. For full control, the lower-level pieces are also exported: parseConfig (env →
Config), configToClientOptions (config → SDK client options), and
createMcpServer (client options → { server, client }). None of them touch the network or a chain.
import { startServer, createMcpServer, parseConfig, configToClientOptions } from '@piprail/mcp'The quickest path — startServer
Section titled “The quickest path — startServer”startServer(env?) is the entire binary in one call: it parses the env (defaults to
process.env), builds the client + server, wires confirm/guide from the config, prints the
startup banner to stderr, and connects a stdio transport. It throws ConfigError on bad env.
import { startServer } from '@piprail/mcp'
await startServer() // uses process.env// …or feed it your own env object:await startServer({ PIPRAIL_PRIVATE_KEY: KEY, PIPRAIL_CHAIN: 'base' })Reach for the lower-level wiring below only when you need a non-stdio transport or want to hold the
client yourself.
The standard wiring — own the transport
Section titled “The standard wiring — own the transport”The steps startServer runs under the hood, for when you need a non-stdio transport: parse the env
into a validated config, turn that into SDK client options, then build the server — passing the
confirm/guide flags through as the second argument (omit it and Mode B never arms and the
guide can’t be suppressed).
import { createMcpServer, parseConfig, configToClientOptions } from '@piprail/mcp'import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
const config = parseConfig(process.env) // → Config (throws ConfigError on bad env)const { server } = createMcpServer(configToClientOptions(config), { confirm: config.confirm, guide: config.guide,})
await server.connect(new StdioServerTransport())createMcpServer returns a low-level Server from @modelcontextprotocol/sdk with all seven
payment tools already registered. It connects no transport and reads no chain, so
it’s testable without a wallet or RPC.
Parsing config yourself
Section titled “Parsing config yourself”parseConfig(env) takes the env object — it never reads process.env on its own — so you can
feed it a fake env, defaults overridden in code, or values from your own config system. It
returns a normalized Config and throws ConfigError (with a
human-readable message) on anything missing or malformed.
import { parseConfig, ConfigError } from '@piprail/mcp'
try { const config = parseConfig({ PIPRAIL_PRIVATE_KEY: process.env.AGENT_KEY, PIPRAIL_CHAIN: 'base', PIPRAIL_MAX_AMOUNT: '0.10', }) // → Config { chain: 'base', maxAmount: '0.10', maxTotal: '10.00', // tokens: ['USDC'], confirm: false, guide: true, … }} catch (e) { if (e instanceof ConfigError) process.stderr.write(e.message + '\n')}The wallet key has one hard requirement and a few aliases: parseConfig reads
PIPRAIL_PRIVATE_KEY, then PIPRAIL_WALLET_KEY, then AGENT_KEY — the first non-empty one
wins. Everything else falls back to the safe defaults above (base / 0.10 / 10.00 / USDC).
configToClientOptions(config) then maps the config to PipRailClientOptions: the budget knobs
become the spend policy, and the wallet secret is shaped to
the chosen family’s WalletInput. The confirm and guide flags are MCP-server concerns and
deliberately do not appear here — pass them as the second argument to createMcpServer.
const clientOptions = configToClientOptions(config)// → PipRailClientOptions { chain: 'base', wallet: { privateKey: '…' },// policy: { maxAmount: '0.10', maxTotal: '10.00', tokens: ['USDC'], … } }Server options
Section titled “Server options”The second argument layers the opt-in features on top of the base tool wiring. All three are additive — omit them and the tools path is byte-identical.
| Option | Default | Effect |
|---|---|---|
confirm | false | Mode B: ask the human to approve each payment via MCP elicitation. |
confirmTimeoutMs | 55000 | The elicitation approval window, in milliseconds. |
guide | true | Expose the agent-guide prompt and the piprail://guide + piprail://budget resources. |
const { server } = createMcpServer(configToClientOptions(config), { confirm: config.confirm, guide: config.guide,})Holding the client
Section titled “Holding the client”createMcpServer also returns the underlying PipRailClient. You don’t need it to serve tools
(the binary discards it), but an embedder can read the live spend leash from it.
const { server, client } = createMcpServer(configToClientOptions(config))
client.spent()// → SpendSummary { count: 0, byAsset: [], records: [] } (before any payment)
client.budget()// → SessionBudget {// session: { start: '2026-06-10T…Z', expiresAt: null, secondsRemaining: null },// byAsset: [] // one row per (network, asset) once a payment lands// }spent() is the per-(network, asset) tally of everything paid so far (never a cross-token
sum — there’s no price oracle); budget() adds the remaining-per-token leash and the session’s
time envelope (expiresAt / secondsRemaining are null until you set a TTL). Both are
read-only and move no funds — use them to surface “spent so far / remaining” in your own UI.
These are the same reads the piprail://budget resource exposes.