Tool Lifecycle
Three layers: internal tool → API-exposed tool → available on CLI/MCP surfaces
Layer 1 — Write an internal tool
1 Internal Tool 100s of tools
Engineer writes a
Tool subclass — docstring becomes description, Pydantic fields become paramsRegistered in
tool_repository.py as a ToolListing
ToolListing fields:
tool_cls — the Tool classscope — who can run it (USER, BUSINESS, INTERNAL)category — e.g. TRANSACTIONS, BILLS, POLICYread_only — safe to call without side effects?At this point: only available to Ramp's internal agents (chat, background tasks, etc.)
Layer 2 — Expose externally with a ToolApiAdapter
2 Add ToolApiAdapter ~50+ tools
Adding
api_adapter=ToolApiAdapter(...) to a ToolListing is what makes it external
ToolApiAdapter fields:
path — URL path, e.g. "/get-funds" → /developer/v1/agent-tools/get-fundsoauth_scope — required permission, e.g. LIMITS_READ, TRANSACTIONS_READmethod — HTTP method (default: POST)platforms — which surfaces can see it: {MCP, CLI} (default: both)alias — human-friendly verb, e.g. LIST, APPROVE, SEARCHuse_read_replica — route to read-only DB for performanceOpenAPI spec (
openapi-agent-tools.json) is generated as a build artifact; CI enforces it stays in syncNow accessible via
POST /developer/v1/agent-tools/{path} — restricted to CLI + MCP client IDs only (hardcoded allowlist)Traced example:
GetUserTransactions
Following one real tool from class definition all the way through to what CLI and MCP users see.
Tool class & docstring — this is where names and descriptions originate
class GetUserTransactions(Tool):
"""
Search and retrieve expense transactions from Ramp ← CLI shows this (summary)
This tool helps you find card transactions. Use this tool when users want to:
- View their recent transactions or expenses
- Search for specific purchases by merchant, amount, or description
- Find transactions that need approval
- Analyze spending patterns
- Review transactions by specific team members
- Look up transactions with missing documentation
...
""" MCP shows the full docstring →
"""
Search and retrieve expense transactions from Ramp ← CLI shows this (summary)
This tool helps you find card transactions. Use this tool when users want to:
- View their recent transactions or expenses
- Search for specific purchases by merchant, amount, or description
- Find transactions that need approval
- Analyze spending patterns
- Review transactions by specific team members
- Look up transactions with missing documentation
...
""" MCP shows the full docstring →
ToolListing + ToolApiAdapter — registered in tool_repository.py
ToolListing(
tool_cls=GetUserTransactions,
scope=ToolScope.USER,
category=ToolCategory.TRANSACTIONS, ← CLI groups under
read_only=True,
api_adapter=ToolApiAdapter(
path="/get-transactions", ← MCP tool name:
oauth_scope=TRANSACTIONS_READ,
alias=ApiAdapterAlias.LIST, ← CLI subcommand:
),
)
tool_cls=GetUserTransactions,
scope=ToolScope.USER,
category=ToolCategory.TRANSACTIONS, ← CLI groups under
ramp transactionsread_only=True,
api_adapter=ToolApiAdapter(
path="/get-transactions", ← MCP tool name:
get-transactionsoauth_scope=TRANSACTIONS_READ,
alias=ApiAdapterAlias.LIST, ← CLI subcommand:
ramp transactions list),
)
What users see on each surface
$ CLI
$ ramp transactions list --help
Usage: ramp transactions list [OPTIONS]
Search and retrieve expense transactions
Options:
--from_date TEXT Start date (YYYY-MM-DD)
--to_date TEXT End date (YYYY-MM-DD)
--state TEXT all, cleared, declined
--page_size INT Max results (10-200)
...
Usage: ramp transactions list [OPTIONS]
Search and retrieve expense transactions
Options:
--from_date TEXT Start date (YYYY-MM-DD)
--to_date TEXT End date (YYYY-MM-DD)
--state TEXT all, cleared, declined
--page_size INT Max results (10-200)
...
Name:
Derived from spec
transactions listDerived from spec
tags (transactions) + x-alias (list)
Description: spec
summary only (first paragraph of docstring — kept short for terminal)
Params: each Pydantic field → a
--flag with type + help text
M MCP
Tool: get-transactions
Description:
This tool helps you find card
transactions. Use when users want to:
- View recent transactions
- Search by merchant or amount
- Find items needing approval
Params: {from_date, to_date, state, ...}
Description:
This tool helps you find card
transactions. Use when users want to:
- View recent transactions
- Search by merchant or amount
- Find items needing approval
Params: {from_date, to_date, state, ...}
Name:
Derived from spec path slug after
get-transactionsDerived from spec path slug after
/agent-tools/
Description: full docstring (AI models need more context), cleaned of response metadata
Params: JSON schema object (same fields, typed properties)
Same source, different slices. The
Tool class docstring is the single source — but CLI shows only the first paragraph (brief for terminal), while MCP shows the full text (AI models need richer context). Pydantic field annotations become parameter descriptions on both surfaces.Layer 3 — When tools become available
MCP has an additional gate. Even after the spec syncs, some MCP apps require your tool to be added to an explicit whitelist in
constants.py before it appears. This is a separate PR. See MCP Nuances for which apps require it.New scopes require re-authorization. If a new tool introduces a new OAuth scope that users haven't previously authorized, they'll need to re-authorize their CLI or MCP connection to pick it up. Tools using existing scopes appear automatically.
CLI: dynamic sync vs. public releases. The CLI has two repos: internal (
ramp/ramp-cli) and public (ramp-public/ramp-cli). New tools from the spec sync dynamically to all installed binaries within ~1 hour — no release needed. However, a public release must be cut (scripts/release.sh → git tag → binaries built → synced to public repo via dist-oss.sh) when:
• A new tool needs a param type that the parser doesn't yet support
• New CLI command structure is added (command groups, subcommands)
• The sync or auth logic itself needs code changes
Key insight: There is no shared Python package. The OpenAPI spec is the contract. The
ToolApiAdapter is the gate — without it, a tool stays internal forever.Runtime Flow
What happens when an AI agent calls a Ramp tool
Both paths hit the same endpoint. Core identifies the caller from the OAuth client ID. Both use OAuth PKCE + bearer token. The response is identical.
How to Add a New Tool
From code to callable by AI agents — the full checklist
-
Write the tool class in
ramp/core
SubclassTool. Your docstring becomes the tool description everywhere (CLI help, MCP description, API docs). Define inputs as Pydantic fields with types and descriptions. -
Register in
tool_repository.py
Create aToolListingwith your tool class, scope, and category. At this point the tool is usable by internal agents only. -
Add a
ToolApiAdapterto expose it externally
This is the critical step. Define:path— the URL slug (e.g."/get-funds")
oauth_scope— which OAuth scope is required
alias— a verb likeLIST,APPROVE,SEARCH
platforms— defaults to{MCP, CLI}, restrict if needed -
Merge & deploy core
The OpenAPI spec is generated as a build artifact and CI enforces it stays in sync. The public spec endpoint updates on deploy. -
CLI picks it up automatically within ~1 hour
Hash-polling detects the change and downloads the new spec. No binary release needed for standard tool additions. A public release is only required for structural CLI changes (new param types, command groups, or auth logic). -
MCP picks it up on next cron sync (runs twice daily)
Scheduled GitHub Action fetches the new spec, opens an auto-PR inramp-mcp-remote, merged + deployed. Dynamic sync (like CLI) is planned. -
Optional: update MCP whitelists
Some MCP apps use explicit tool whitelists. See MCP Nuances for details.
"Why can't I see my new tool?" — Check: (1) Does it have a
ToolApiAdapter? (2) Does your OAuth token have the required scope? (3) Has the spec synced yet? (4) Is it on the whitelist for your MCP app?MCP Nuances
The MCP server actually hosts three separate apps with different audiences, auth, and tool sets
Three MCP apps, one server
ramp-mcp-remote is a single FastAPI server on AWS ECS, but it mounts three independent MCP applications at different paths. Each has its own auth requirements and tool whitelist. All use streamable HTTP transport. They run in parallel — a single deployment serves all three.
How they relate
1 Same deployment, different paths
All three apps are mounted on the same FastAPI server and deployed together to AWS ECS. A single deploy updates all three.
2 Same spec source, different filters
All three read from the same
agent-tool.json OpenAPI spec synced from core. The difference is what gets exposed: Admin applies an explicit whitelist (~44 tools), Employee exposes everything, Developer only has its own hand-written tools.3 Whitelists are a product decision
The Admin app whitelist in
constants.py is curated — not every tool should be available to external customers. Adding a tool to the whitelist is a separate PR from adding the tool itself. The Employee app has no such restriction.4 Description cleanup happens per-app
At tool-listing time,
_clean_tool_descriptions() strips response metadata, removes duplicate title lines, and sets MCP annotations (readOnlyHint, destructiveHint). Write-scoped tools get readOnlyHint=false.Why three apps? Different audiences need different security postures. External customers get a curated, business-auth set. Employees get full access with user-level auth. Developers get docs tools with no auth to reduce friction.