Tool Lifecycle
Three layers: internal tool → API-exposed tool → available on CLI/MCP surfaces

1 Internal Tool 100s of tools

Engineer writes a Tool subclass — docstring becomes description, Pydantic fields become params
Registered in tool_repository.py as a ToolListing
ToolListing fields:
tool_cls — the Tool class
scope — who can run it (USER, BUSINESS, INTERNAL)
category — e.g. TRANSACTIONS, BILLS, POLICY
read_only — safe to call without side effects?
At this point: only available to Ramp's internal agents (chat, background tasks, etc.)

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-funds
oauth_scope — required permission, e.g. LIMITS_READ, TRANSACTIONS_READ
method — HTTP method (default: POST)
platforms — which surfaces can see it: {MCP, CLI} (default: both)
alias — human-friendly verb, e.g. LIST, APPROVE, SEARCH
use_read_replica — route to read-only DB for performance
OpenAPI spec (openapi-agent-tools.json) is generated as a build artifact; CI enforces it stays in sync
Now accessible via POST /developer/v1/agent-tools/{path} — restricted to CLI + MCP client IDs only (hardcoded allowlist)
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 →

ToolListing + ToolApiAdapter — registered in tool_repository.py

ToolListing(
  tool_cls=GetUserTransactions,
  scope=ToolScope.USER,
  category=ToolCategory.TRANSACTIONS, ← CLI groups under ramp transactions
  read_only=True,
  api_adapter=ToolApiAdapter(
    path="/get-transactions", ← MCP tool name: get-transactions
    oauth_scope=TRANSACTIONS_READ,
    alias=ApiAdapterAlias.LIST, ← CLI subcommand: ramp transactions list
  ),
)

$ 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)
  ...
Name: transactions list
Derived 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, ...}
Name: get-transactions
Derived 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)
i
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.
Surface Sync timing How it works
ramp-cli ~1 hour Polls spec hash endpoint hourly, downloads if changed. No reinstall needed for new tools. See note below on public releases.
ramp-mcp-remote Twice daily GitHub Action syncs spec on a cron → auto-PR → deploy. Dynamic sync (like CLI) planned. See MCP Nuances for the different MCP apps.
!
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.
i
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

AI Agent

Claude Code, Cursor, Codex — agent decides to call a Ramp tool

CLI Path

Subprocess: ramp transactions list --agent
OAuth PKCE + bearer token (one-time browser login, cached)

MCP Path

MCP protocol call via streamable HTTP
OAuth PKCE + bearer token (/token/info validates connection)

Authentication

Token verified → extracts user, business, scopes → client ID checked against allowlist (CLI + MCP only)

Ramp Core API

POST /developer/v1/agent-tools/{tool-name} → resolves tool, checks scopes, runs Tool class → returns structured JSON
!
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
  1. Write the tool class in ramp/core
    Subclass Tool. Your docstring becomes the tool description everywhere (CLI help, MCP description, API docs). Define inputs as Pydantic fields with types and descriptions.
  2. Register in tool_repository.py
    Create a ToolListing with your tool class, scope, and category. At this point the tool is usable by internal agents only.
  3. Add a ToolApiAdapter to 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 like LIST, APPROVE, SEARCH
    platforms — defaults to {MCP, CLI}, restrict if needed
  4. 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.
  5. 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).
  6. MCP picks it up on next cron sync (runs twice daily)
    Scheduled GitHub Action fetches the new spec, opens an auto-PR in ramp-mcp-remote, merged + deployed. Dynamic sync (like CLI) is planned.
  7. 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
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.
Admin Employee Developer
Path /mcp /employee/mcp /developer/mcp
Audience All Ramp users Ramp employees using AI assistants External developers building on Ramp API
Auth level Hybrid (auto — business or user-level) User-level OAuth None (public)
Transport Streamable HTTP Streamable HTTP Streamable HTTP
Tools available ~44 (explicit whitelist in constants.py) All agent tools (no whitelist) 3 (docs search, schema lookup, feedback)
Extra features SQLite data tools — loads spend exports into in-memory DB, exposes SQL query tool Token info endpoint Bundled .mdx docs with keyword search
Scope filtering Yes — tools filtered by granted OAuth scopes Yes — tools filtered by granted OAuth scopes N/A — no auth

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.
i
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.