Skip to content

Commit f21a7a1

Browse files
michelle0927claudevetrivigneshwaran
authored
feat(ramp): AI-optimized Ramp actions for MCP (read + write) (#21725)
* feat(ramp): add AI-optimized read/write actions for MCP + eval-driven fixes Adds 15 AI-optimized Ramp actions (users, transactions, spend limits, departments, locations, spend programs, transfers) covering list, get, create, update, and terminate. Iterated against the MCP eval suite (pd-connect-eval-monster/evals/ramp_sandbox, which exercises these via the Ramp demo API); suite green on Sonnet 5 (18/18, pass^2). Related to #21692. - list-users / list-transactions / list-limits / list-spend-programs: return a compact per-record projection by default + additive `fields` prop; raw responses overflowed the agent context (25k+ chars) and got truncated. [new 0.0.1] - list-* / get-*: pageSize bounded to the API's 2-100 range (was unbounded, so page_size=1 and =1000 both 422'd); pagination behavior documented. [new 0.0.1] - terminate-limit: POST /limits/{id}/deferred/termination with idempotency_key (DELETE /limits/{id} returned 405). [new 0.0.1] - get-* / update-* / create-*: worked examples + list-action cross-references for reliable id discovery. [new 0.0.1] - ramp.app.mjs: added the methods these actions call; removed an unused transactionState prop definition. App package.json bumped 0.2.2 -> 0.3.0 (minor: additive new actions). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(ramp): bump pre-existing actions and sources for shared-code change The AI-optimized action set modifies shared ramp.app.mjs and common/constants.mjs, so every component that imports them is rev'd per the Pipedream versioning standard: - actions/create-user-invite: 0.0.3 -> 0.0.4 - actions/issue-virtual-card: 0.0.4 -> 0.0.5 - actions/upload-receipt: 0.1.3 -> 0.1.4 - sources/new-transaction-created: 0.0.2 -> 0.0.3 - sources/transaction-status-updated: 0.0.2 -> 0.0.3 - sources/transfer-payment-updated: 0.0.2 -> 0.0.3 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ramp): apply code-review fixes to AI-optimized actions Addresses PR review feedback (verified against the Ramp demo API before applying): - update-user / update-limit: throw ConfigurationError when only the resource id is given and every mutable field is undefined (no-op update guard). - list-*: add an optional `start` pagination cursor and forward it; `start` is Ramp's real cursor (it appears in each response's `page.next`). Verified paging. - list-transactions: add `ALL` to the state enum (API-verified valid value) so callers can request every transaction, including declined. - create-department / create-location / update-limit: expand descriptions (when-to-use, worked examples, partial-update semantics, entity optionality). - get-*/update-*/terminate-limit/create-location: add concrete UUID examples to id-style prop descriptions while keeping their source action. Skipped (with reasons): moving pageSize/fields into shared propDefinitions (optional refactor, `fields` varies per resource); UNBUNDLED_* list-users roles (API rejects them with 422 — current ROLES enum works); PATCH /funds/{id} for update-limit (/funds requires a funds:read scope the client lacks -> 403; the existing PATCH /limits/{id} works and is eval-covered). Suite re-verified 18/18 on Sonnet 5 (evals/ramp_sandbox). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ramp): DRY list props + fix update-limit restriction schema Second round of PR-review fixes (both verified against the Ramp demo API): - Shared pagination/field props: moved pageSize, start, and fields into ramp.app.mjs propDefinitions and referenced them from all 7 list actions via propDefinition (fields keeps its per-resource description override). No behavior change; removes the duplicated inline definitions. - update-limit: corrected the spending-restriction contract. The API requires spending_restrictions.limit.{amount,currency_code} plus spending_restrictions. interval (verified: the previous flat {interval, {amount,currency}} body 422s; the nested body returns 200, and limit+interval are required together). Replaced the free-form spendingRestrictions object and top-level interval with amount + currencyCode + interval inputs; rename (display_name only) still works. Guards reject an amount-without-interval (or vice-versa) and a no-op update. Suite re-verified 18/18 on Sonnet 5 (evals/ramp_sandbox; eval 11 is model-choice flaky on page size and passes 2/2 on re-run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ramp): reject currency code without a full spending restriction in update-limit Follow-up review fix: currencyCode was only consumed when both amount and interval were provided, so passing it otherwise (e.g. display name + currency) silently dropped it. Add a ConfigurationError guard rejecting currencyCode unless both amount and interval are present. Rename path unaffected (eval 16 passes 2/2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ramp): add status filter options to list-users Add the four Ramp user-status enum values (USER_ACTIVE, USER_DRAFT, USER_INACTIVE, USER_SUSPENDED) as a USER_STATUSES constant and wire them to the list-users `status` prop as selectable options, with an example in the description. API-verified: all four are accepted by GET /users?status=; an unknown value returns 422. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ramp): consolidate shared props into propDefinitions (PR review) Addresses maintainer review comments on shared-prop reuse and ID formatting: - Add a `name` propDefinition to ramp.app.mjs; reference it from create-department and create-location (with per-action example overrides) instead of duplicating the inline schema. - Reference the existing departmentId/locationId/role propDefinitions from list-transactions, list-users, and update-user (they were redefined inline, unlike create-user-invite which already referenced them). Component-specific descriptions are kept as overrides. - Put the UUID format + a concrete example into the departmentId/locationId descriptions (both the shared propDefinitions and the filter-action overrides) so callers pass an ID, not a department/location name. - Drop the now-unused constants import from update-user. Suite re-verified 18/18 on Sonnet 5 (evals/ramp_sandbox). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ramp): move spend-limit reads/writes to the documented /funds endpoints The limit list/get/update/terminate actions targeted the undocumented /limits surface. With funds:read/funds:write scopes granted, switch them to Ramp's documented Funds API (spend limits are served by /developer/v1/funds): - listLimits -> GET /funds - getLimit -> GET /funds/{id} - updateLimit -> PATCH /funds/{id} (same nested spending_restrictions body) - terminateLimit -> DELETE /funds/{id} (synchronous; the old undocumented /limits/{id}/deferred/termination is replaced, so terminate-limit no longer sends an idempotency_key and its description reflects immediate termination) createLimit stays on /limits/deferred: it is the deferred-create used only by the pre-existing issue-virtual-card action (out of scope), and the documented POST /funds is a different, direct-create operation. User-facing naming stays "Limits" (Ramp's product term and the more discoverable name); "funds" is the API path only. Verified against the live demo API: /funds list/get/patch/delete all 200, same page/data + start-cursor pagination, same 2-100 page_size bound, compact-projection fields all present. Updated the list-limits `fields` example to the /funds field names (members, spending_restrictions). Suite 18/18 on Sonnet 5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ramp): sharpen update-limit currency validation + list-departments example Addresses PR review comments: - update-limit: reject a blank currencyCode with ConfigurationError before the API call (an omitted currency is still allowed and defaults to USD), and build the limit with nullish defaulting (`currencyCode ?? "USD"`) so an explicit value is preserved and only an omitted one defaults. - list-departments: show both id and name in the returned-record example using a concrete department UUID, so the id can be passed to Update User. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: vetrivigneshwaran <vicky@pipedream.com>
1 parent ea0e712 commit f21a7a1

25 files changed

Lines changed: 965 additions & 17 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import ramp from "../../ramp.app.mjs";
2+
3+
export default {
4+
key: "ramp-create-department",
5+
name: "Create Department",
6+
description: "Create a new Ramp department (e.g. `Engineering`, `Marketing`). Use this to add an organizational unit before assigning users to it. Returns the new department's `id`, which you can pass to **Update User** to set a user's department, or use to filter **List Transactions** / **List Users**. [See the documentation](https://docs.ramp.com/developer-api/v1/api/departments#post-developer-v1-departments).",
7+
version: "0.0.1",
8+
type: "action",
9+
annotations: {
10+
destructiveHint: false,
11+
openWorldHint: true,
12+
readOnlyHint: false,
13+
},
14+
props: {
15+
ramp,
16+
name: {
17+
propDefinition: [
18+
ramp,
19+
"name",
20+
],
21+
description: "The name of the new department (e.g. `Engineering`, `Marketing`).",
22+
},
23+
},
24+
async run({ $ }) {
25+
const response = await this.ramp.createDepartment({
26+
$,
27+
data: {
28+
name: this.name,
29+
},
30+
});
31+
$.export("$summary", `Successfully created department "${this.name}" with ID ${response.id}`);
32+
return response;
33+
},
34+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import ramp from "../../ramp.app.mjs";
2+
3+
export default {
4+
key: "ramp-create-location",
5+
name: "Create Location",
6+
description: "Create a new Ramp location (e.g. `New York HQ`). Use this to add a site before assigning users to it; the returned `id` can be passed to **Update User** or used to filter **List Transactions** / **List Users**. Associating an entity is optional — set the Entity ID only for multi-entity businesses that scope locations to a specific legal entity. [See the documentation](https://docs.ramp.com/developer-api/v1/api/locations#post-developer-v1-locations).",
7+
version: "0.0.1",
8+
type: "action",
9+
annotations: {
10+
destructiveHint: false,
11+
openWorldHint: true,
12+
readOnlyHint: false,
13+
},
14+
props: {
15+
ramp,
16+
name: {
17+
propDefinition: [
18+
ramp,
19+
"name",
20+
],
21+
description: "The name of the new location (e.g. `New York HQ`).",
22+
},
23+
entityId: {
24+
type: "string",
25+
label: "Entity ID",
26+
description: "Optional entity ID (a UUID, e.g. `dff9389a-2819-468e-a220-28e059b23f8e`) to associate with the location. Obtain it from the `entity_id` field returned by **List Locations**, or from your Ramp entities in the API. Only needed for multi-entity businesses.",
27+
optional: true,
28+
},
29+
},
30+
async run({ $ }) {
31+
const response = await this.ramp.createLocation({
32+
$,
33+
data: {
34+
name: this.name,
35+
entity_id: this.entityId,
36+
},
37+
});
38+
$.export("$summary", `Successfully created location "${this.name}" with ID ${response.id}`);
39+
return response;
40+
},
41+
};

components/ramp/actions/create-user-invite/create-user-invite.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default {
55
key: "ramp-create-user-invite",
66
name: "Create User Invite",
77
description: "Sends out an invite for a new user. [See the documentation](https://docs.ramp.com/developer-api/v1/reference/rest/users#post-developer-v1-users-deferred)",
8-
version: "0.0.3",
8+
version: "0.0.4",
99
annotations: {
1010
destructiveHint: false,
1111
openWorldHint: true,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import ramp from "../../ramp.app.mjs";
2+
3+
export default {
4+
key: "ramp-get-limit",
5+
name: "Get Limit",
6+
description: "Retrieve a single Ramp spend limit by ID. Run the **List Limits** action first to find a valid limit ID. Example: given a limit id from **List Limits**, returns the limit's display name, state (e.g. `ACTIVE`), balance, spend program, and spending restrictions. [See the documentation](https://docs.ramp.com/developer-api/v1/api/funds).",
7+
version: "0.0.1",
8+
type: "action",
9+
annotations: {
10+
destructiveHint: false,
11+
openWorldHint: true,
12+
readOnlyHint: true,
13+
},
14+
props: {
15+
ramp,
16+
limitId: {
17+
type: "string",
18+
label: "Limit ID",
19+
description: "The ID of the spend limit to retrieve — a UUID, e.g. `dcd0f7a8-557d-4dc5-bb12-49297c3abfdd`. Run the **List Limits** action to find this value.",
20+
},
21+
},
22+
async run({ $ }) {
23+
const response = await this.ramp.getLimit({
24+
$,
25+
limitId: this.limitId,
26+
});
27+
$.export("$summary", `Successfully retrieved limit ${this.limitId}`);
28+
return response;
29+
},
30+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import ramp from "../../ramp.app.mjs";
2+
3+
export default {
4+
key: "ramp-get-transaction",
5+
name: "Get Transaction",
6+
description: "Retrieve a single Ramp transaction by ID. Run the **List Transactions** action first to find a valid transaction ID. Example: given a transaction id from **List Transactions**, returns the full transaction — e.g. merchant `Facebook Ads`, amount `$135.00`, category `Advertising`, plus line items and accounting detail. [See the documentation](https://docs.ramp.com/developer-api/v1/api/transactions#get-developer-v1-transactions-transaction-id).",
7+
version: "0.0.1",
8+
type: "action",
9+
annotations: {
10+
destructiveHint: false,
11+
openWorldHint: true,
12+
readOnlyHint: true,
13+
},
14+
props: {
15+
ramp,
16+
transactionId: {
17+
type: "string",
18+
label: "Transaction ID",
19+
description: "The ID of the transaction to retrieve — a UUID, e.g. `c74326d3-a6b3-4a88-9a0c-4b61850784cd`. Run the **List Transactions** action to find this value.",
20+
},
21+
},
22+
async run({ $ }) {
23+
const response = await this.ramp.getTransaction({
24+
$,
25+
transactionId: this.transactionId,
26+
});
27+
$.export("$summary", `Successfully retrieved transaction ${this.transactionId}`);
28+
return response;
29+
},
30+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import ramp from "../../ramp.app.mjs";
2+
3+
export default {
4+
key: "ramp-get-user",
5+
name: "Get User",
6+
description: "Retrieve a single Ramp user by ID. Run the **List Users** action first to find a valid user ID. Example: given a user id from **List Users**, returns that user's full record — name, email, role (e.g. `BUSINESS_OWNER`), department id, and location id. [See the documentation](https://docs.ramp.com/developer-api/v1/api/users#get-developer-v1-users-user-id).",
7+
version: "0.0.1",
8+
type: "action",
9+
annotations: {
10+
destructiveHint: false,
11+
openWorldHint: true,
12+
readOnlyHint: true,
13+
},
14+
props: {
15+
ramp,
16+
userId: {
17+
type: "string",
18+
label: "User ID",
19+
description: "The ID of the user to retrieve — a UUID, e.g. `bcc1e4ca-d38a-4cc9-98fc-e6c2066ad0ae`. Run the **List Users** action to find this value.",
20+
},
21+
},
22+
async run({ $ }) {
23+
const response = await this.ramp.getUser({
24+
$,
25+
userId: this.userId,
26+
});
27+
$.export("$summary", `Successfully retrieved user ${this.userId}`);
28+
return response;
29+
},
30+
};

components/ramp/actions/issue-virtual-card/issue-virtual-card.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default {
55
key: "ramp-issue-virtual-card",
66
name: "Issue Virtual Card",
77
description: "Creates a new virtual card for a given user. [See the documentation](https://docs.ramp.com/developer-api/v1/reference/rest/limits#post-developer-v1-limits-deferred)",
8-
version: "0.0.4",
8+
version: "0.0.5",
99
annotations: {
1010
destructiveHint: false,
1111
openWorldHint: true,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import ramp from "../../ramp.app.mjs";
2+
3+
export default {
4+
key: "ramp-list-departments",
5+
name: "List Departments",
6+
description: "Retrieve a list of Ramp departments. Use this to find department IDs for other actions such as **Update User** and **List Transactions**. Example: returns `{ id, name }` pairs such as `{ \"id\": \"fffe6c22-698f-4dc5-b2b1-b35f86947d90\", \"name\": \"Engineering\" }` — pass the `id` to **Update User** to set a user's department. Returns one page of up to `pageSize` results (max 100); a `page.next` value in the response means more departments exist beyond this page. [See the documentation](https://docs.ramp.com/developer-api/v1/api/departments#get-developer-v1-departments).",
7+
version: "0.0.1",
8+
type: "action",
9+
annotations: {
10+
destructiveHint: false,
11+
openWorldHint: true,
12+
readOnlyHint: true,
13+
},
14+
props: {
15+
ramp,
16+
pageSize: {
17+
propDefinition: [
18+
ramp,
19+
"pageSize",
20+
],
21+
},
22+
start: {
23+
propDefinition: [
24+
ramp,
25+
"start",
26+
],
27+
},
28+
},
29+
async run({ $ }) {
30+
const response = await this.ramp.listDepartments({
31+
$,
32+
params: {
33+
page_size: this.pageSize,
34+
start: this.start,
35+
},
36+
});
37+
$.export("$summary", `Successfully retrieved ${response.data?.length ?? 0} department(s)`);
38+
return response;
39+
},
40+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import ramp from "../../ramp.app.mjs";
2+
import utils from "../../common/utils.mjs";
3+
4+
export default {
5+
key: "ramp-list-limits",
6+
name: "List Limits",
7+
description: "Retrieve a paginated list of Ramp spend limits. Returns a compact summary of each limit by default (id, name, state, balance, spend program); use **Get Limit** for the full record, or pass `fields` to include specific extra fields. Use this to find limit IDs for **Get Limit**, **Update Limit**, and **Terminate Limit**. Returns one page of up to `pageSize` results (max 100); a `page.next` value in the response means more limits exist beyond this page. [See the documentation](https://docs.ramp.com/developer-api/v1/api/funds).",
8+
version: "0.0.1",
9+
type: "action",
10+
annotations: {
11+
destructiveHint: false,
12+
openWorldHint: true,
13+
readOnlyHint: true,
14+
},
15+
props: {
16+
ramp,
17+
pageSize: {
18+
propDefinition: [
19+
ramp,
20+
"pageSize",
21+
],
22+
},
23+
start: {
24+
propDefinition: [
25+
ramp,
26+
"start",
27+
],
28+
},
29+
fields: {
30+
propDefinition: [
31+
ramp,
32+
"fields",
33+
],
34+
description: "Optional list of limit fields to include per record in addition to the compact default (e.g. `cards`, `members`, `spending_restrictions`, `permitted_spend_types`). Leave empty for the compact summary; use **Get Limit** for the complete record.",
35+
},
36+
},
37+
async run({ $ }) {
38+
const response = await this.ramp.listLimits({
39+
$,
40+
params: {
41+
page_size: this.pageSize,
42+
start: this.start,
43+
},
44+
});
45+
$.export("$summary", `Successfully retrieved ${response.data?.length ?? 0} limit(s)`);
46+
return utils.projectList(response, utils.LIMIT_COMPACT_FIELDS, this.fields);
47+
},
48+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import ramp from "../../ramp.app.mjs";
2+
3+
export default {
4+
key: "ramp-list-locations",
5+
name: "List Locations",
6+
description: "Retrieve a list of Ramp locations. Use this to find location IDs for other actions such as **Update User** and **List Transactions**. Example: returns `{ id, name, entity_id }` such as `{ \"name\": \"New York HQ\" }`. Returns one page of up to `pageSize` results (max 100); a `page.next` value in the response means more locations exist beyond this page. [See the documentation](https://docs.ramp.com/developer-api/v1/api/locations#get-developer-v1-locations).",
7+
version: "0.0.1",
8+
type: "action",
9+
annotations: {
10+
destructiveHint: false,
11+
openWorldHint: true,
12+
readOnlyHint: true,
13+
},
14+
props: {
15+
ramp,
16+
pageSize: {
17+
propDefinition: [
18+
ramp,
19+
"pageSize",
20+
],
21+
},
22+
start: {
23+
propDefinition: [
24+
ramp,
25+
"start",
26+
],
27+
},
28+
},
29+
async run({ $ }) {
30+
const response = await this.ramp.listLocations({
31+
$,
32+
params: {
33+
page_size: this.pageSize,
34+
start: this.start,
35+
},
36+
});
37+
$.export("$summary", `Successfully retrieved ${response.data?.length ?? 0} location(s)`);
38+
return response;
39+
},
40+
};

0 commit comments

Comments
 (0)