stables-mcp-server 2.2.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +40 -6
  2. package/build/http/handler.d.ts.map +1 -1
  3. package/build/http/handler.js +9 -0
  4. package/build/http/handler.js.map +1 -1
  5. package/build/lib/stables-client.d.ts +282 -23
  6. package/build/lib/stables-client.d.ts.map +1 -1
  7. package/build/lib/stables-client.js +164 -10
  8. package/build/lib/stables-client.js.map +1 -1
  9. package/build/server.d.ts +2 -1
  10. package/build/server.d.ts.map +1 -1
  11. package/build/server.js +10 -1
  12. package/build/server.js.map +1 -1
  13. package/build/tools/balances.d.ts +15 -0
  14. package/build/tools/balances.d.ts.map +1 -0
  15. package/build/tools/balances.js +140 -0
  16. package/build/tools/balances.js.map +1 -0
  17. package/build/tools/compliance.d.ts +13 -0
  18. package/build/tools/compliance.d.ts.map +1 -0
  19. package/build/tools/compliance.js +170 -0
  20. package/build/tools/compliance.js.map +1 -0
  21. package/build/tools/customers.d.ts.map +1 -1
  22. package/build/tools/customers.js +330 -258
  23. package/build/tools/customers.js.map +1 -1
  24. package/build/tools/documents.d.ts +12 -0
  25. package/build/tools/documents.d.ts.map +1 -0
  26. package/build/tools/documents.js +94 -0
  27. package/build/tools/documents.js.map +1 -0
  28. package/build/tools/entitlements.d.ts +12 -0
  29. package/build/tools/entitlements.d.ts.map +1 -0
  30. package/build/tools/entitlements.js +83 -0
  31. package/build/tools/entitlements.js.map +1 -0
  32. package/build/tools/transfers.d.ts.map +1 -1
  33. package/build/tools/transfers.js +174 -89
  34. package/build/tools/transfers.js.map +1 -1
  35. package/build/tools/virtual-accounts.d.ts.map +1 -1
  36. package/build/tools/virtual-accounts.js +154 -12
  37. package/build/tools/virtual-accounts.js.map +1 -1
  38. package/build/version.d.ts +2 -0
  39. package/build/version.d.ts.map +1 -0
  40. package/build/version.js +3 -0
  41. package/build/version.js.map +1 -0
  42. package/build/worker.d.ts +16 -1
  43. package/build/worker.d.ts.map +1 -1
  44. package/build/worker.js +24 -1
  45. package/build/worker.js.map +1 -1
  46. package/package.json +12 -6
@@ -3,257 +3,261 @@
3
3
  * Synced with OpenAPI spec from https://api.stables.money/docs
4
4
  */
5
5
  import { z } from "zod";
6
- import { StablesApiError, } from "../lib/stables-client.js";
6
+ import { StablesApiError, ENTITLEMENT_IDS, } from "../lib/stables-client.js";
7
7
  function formatError(error) {
8
8
  if (error instanceof StablesApiError) {
9
9
  return `${error.message} (HTTP ${error.statusCode})`;
10
10
  }
11
11
  return error instanceof Error ? error.message : "Unknown error";
12
12
  }
13
+ const customerShape = {
14
+ email: z.string().email().optional().describe("Customer's email address"),
15
+ customerType: z
16
+ .enum(["individual", "business"])
17
+ .describe("Type of customer - 'individual' for personal, 'business' for companies"),
18
+ // Individual fields
19
+ firstName: z.string().optional().describe("First name (required for individuals)"),
20
+ lastName: z.string().optional().describe("Last name (required for individuals)"),
21
+ middleName: z.string().optional().describe("Middle name"),
22
+ dob: z.string().optional().describe("Date of birth in YYYY-MM-DD format (e.g., '1990-01-15')"),
23
+ nationality: z.string().optional().describe("Two-letter country code (e.g., 'US', 'GB')"),
24
+ // Business fields
25
+ companyName: z.string().optional().describe("Company name (required for businesses)"),
26
+ country: z
27
+ .string()
28
+ .optional()
29
+ .describe("Country code ISO 3166-1 alpha-2 (required for businesses, e.g., 'US')"),
30
+ registrationNumber: z.string().optional().describe("Business registration number"),
31
+ incorporatedOn: z.string().optional().describe("Date of incorporation in YYYY-MM-DD format"),
32
+ type: z.string().optional().describe("Company type (e.g., 'Private Company Limited by Shares')"),
33
+ taxId: z.string().optional().describe("Tax ID (e.g., '12-3456789')"),
34
+ registrationLocation: z
35
+ .string()
36
+ .optional()
37
+ .describe("Registration location (e.g., state for USA)"),
38
+ website: z.string().optional().describe("Website URL"),
39
+ describeBusiness: z.string().optional().describe("Description of the business"),
40
+ conductMoneyServices: z
41
+ .boolean()
42
+ .optional()
43
+ .describe("Whether the company conducts money services"),
44
+ describeMoneyServices: z
45
+ .string()
46
+ .optional()
47
+ .describe("Describe money services (required if conductMoneyServices is true)"),
48
+ describeComplianceControls: z.string().optional().describe("Description of compliance controls"),
49
+ accountPurpose: z
50
+ .enum([
51
+ "CHARITABLE_DONATIONS",
52
+ "ECOMMERCE_RETAIL_PAYMENTS",
53
+ "INVESTMENT_PURPOSES",
54
+ "PAYMENTS_TO_FRIENDS_OR_FAMILY_ABROAD",
55
+ "PAYROLL",
56
+ "PERSONAL_OR_LIVING_EXPENSES",
57
+ "PROTECT_WEALTH",
58
+ "PURCHASE_GOODS_AND_SERVICES",
59
+ "RECEIVE_PAYMENTS_FOR_GOODS_AND_SERVICES",
60
+ "TAX_OPTIMIZATION",
61
+ "THIRD_PARTY_MONEY_TRANSMISSION",
62
+ "TREASURY_MANAGEMENT",
63
+ "OTHER",
64
+ ])
65
+ .optional()
66
+ .describe("What will you use Stables for?"),
67
+ accountPurposeOther: z
68
+ .string()
69
+ .optional()
70
+ .describe("Explain purpose (required if accountPurpose is OTHER)"),
71
+ expectedAnnualRevenue: z
72
+ .enum([
73
+ "0_99999",
74
+ "100000_999999",
75
+ "1000000_9999999",
76
+ "10000000_49999999",
77
+ "50000000_249999999",
78
+ "250000000_plus",
79
+ ])
80
+ .optional()
81
+ .describe("Estimated annual revenue in USD"),
82
+ expectedMonthlyPayments: z.string().optional().describe("Expected monthly payments in USD"),
83
+ sourceOfFunds: z
84
+ .enum([
85
+ "BUSINESS_LOANS",
86
+ "GRANTS",
87
+ "INTER_COMPANY_FUNDS",
88
+ "INVESTMENT_PROCEEDS",
89
+ "LEGAL_SETTLEMENT",
90
+ "OWNERS_CAPITAL",
91
+ "PENSION_RETIREMENT",
92
+ "SALE_OF_ASSETS",
93
+ "SALES_OF_GOODS_AND_SERVICES",
94
+ "THIRD_PARTY_FUNDS",
95
+ "TREASURY_RESERVES",
96
+ ])
97
+ .optional()
98
+ .describe("Source of funds"),
99
+ sourceOfFundsDescription: z
100
+ .string()
101
+ .optional()
102
+ .describe("Describe where your business funds come from"),
103
+ operateInProhibitedCountry: z
104
+ .boolean()
105
+ .optional()
106
+ .describe("Does your business operate in any prohibited countries?"),
107
+ doesYourBusinessEngageInHighRiskActivities: z
108
+ .enum(["yes", "no"])
109
+ .optional()
110
+ .describe("Does your business engage in high risk activities?"),
111
+ acceptTerms: z
112
+ .boolean()
113
+ .optional()
114
+ .describe("Accept Stables' terms of service and privacy policy"),
115
+ // Shared fields
116
+ externalCustomerId: z.string().optional().describe("Your own reference ID for this customer"),
117
+ phone: z.string().optional().describe("Phone number with country code (e.g., '+14155552671')"),
118
+ entitlements: z
119
+ .array(z.enum(ENTITLEMENT_IDS))
120
+ .optional()
121
+ .describe("List of entitlements to request"),
122
+ // Address fields (for individuals)
123
+ addressLine1: z.string().optional().describe("Street address line 1"),
124
+ addressLine2: z.string().optional().describe("Street address line 2"),
125
+ addressCity: z.string().optional().describe("City"),
126
+ addressState: z.string().optional().describe("State or region"),
127
+ addressPostalCode: z.string().optional().describe("Postal/ZIP code"),
128
+ addressCountry: z.string().optional().describe("Two-letter country code (e.g., 'US')"),
129
+ // Legal address fields (for businesses)
130
+ legalAddressLine1: z.string().optional().describe("Legal address line 1 (for businesses)"),
131
+ legalAddressLine2: z.string().optional().describe("Legal address line 2 (for businesses)"),
132
+ legalAddressCity: z.string().optional().describe("Legal address city (for businesses)"),
133
+ legalAddressState: z.string().optional().describe("Legal address state (for businesses)"),
134
+ legalAddressPostalCode: z
135
+ .string()
136
+ .optional()
137
+ .describe("Legal address postal code (for businesses)"),
138
+ legalAddressCountry: z
139
+ .string()
140
+ .optional()
141
+ .describe("Legal address country code (for businesses)"),
142
+ };
143
+ /**
144
+ * Builds the snake_case wire body for an individual or business customer. Shared
145
+ * by create_customer and create_customer_onboarding_link, which take the same
146
+ * customer record.
147
+ */
148
+ function buildCustomerBody(params) {
149
+ const isBusiness = params.customerType === "business";
150
+ // Tool inputs stay camelCase for the agent's benefit; the wire body is
151
+ // snake_case, and unknown keys are stripped rather than rejected — which
152
+ // is why the old camelCase body arrived empty and 400'd on the missing
153
+ // required fields instead of on the wrong ones.
154
+ const address = !isBusiness && params.addressLine1
155
+ ? {
156
+ line1: params.addressLine1,
157
+ line2: params.addressLine2,
158
+ city: params.addressCity || "",
159
+ state: params.addressState,
160
+ postal_code: params.addressPostalCode,
161
+ country: params.addressCountry || "",
162
+ }
163
+ : undefined;
164
+ const legalAddress = isBusiness && params.legalAddressLine1
165
+ ? {
166
+ line1: params.legalAddressLine1,
167
+ line2: params.legalAddressLine2,
168
+ city: params.legalAddressCity || "",
169
+ state: params.legalAddressState,
170
+ postal_code: params.legalAddressPostalCode,
171
+ country: params.legalAddressCountry || "",
172
+ }
173
+ : undefined;
174
+ const requestBody = {
175
+ email: params.email,
176
+ customer_type: isBusiness ? "business" : "individual",
177
+ external_customer_id: params.externalCustomerId || crypto.randomUUID(),
178
+ phone: params.phone,
179
+ entitlements: params.entitlements,
180
+ };
181
+ if (isBusiness) {
182
+ Object.assign(requestBody, {
183
+ company_name: params.companyName,
184
+ country: params.country,
185
+ registration_number: params.registrationNumber,
186
+ incorporated_on: params.incorporatedOn,
187
+ type: params.type,
188
+ tax_id: params.taxId,
189
+ registration_location: params.registrationLocation,
190
+ website: params.website,
191
+ legal_address: legalAddress,
192
+ describe_business: params.describeBusiness,
193
+ conduct_money_services: params.conductMoneyServices,
194
+ describe_money_services: params.describeMoneyServices,
195
+ describe_compliance_controls: params.describeComplianceControls,
196
+ account_purpose: params.accountPurpose,
197
+ account_purpose_other: params.accountPurposeOther,
198
+ expected_annual_revenue: params.expectedAnnualRevenue,
199
+ expected_monthly_payments: params.expectedMonthlyPayments,
200
+ source_of_funds: params.sourceOfFunds,
201
+ source_of_funds_description: params.sourceOfFundsDescription,
202
+ operate_in_prohibited_country: params.operateInProhibitedCountry,
203
+ does_your_business_engage_in_high_risk_activities: params.doesYourBusinessEngageInHighRiskActivities,
204
+ accept_terms: params.acceptTerms,
205
+ });
206
+ }
207
+ else {
208
+ Object.assign(requestBody, {
209
+ first_name: params.firstName,
210
+ last_name: params.lastName,
211
+ middle_name: params.middleName,
212
+ dob: params.dob,
213
+ nationality: params.nationality,
214
+ address,
215
+ });
216
+ }
217
+ // Remove undefined values
218
+ for (const key of Object.keys(requestBody)) {
219
+ if (requestBody[key] === undefined)
220
+ delete requestBody[key];
221
+ }
222
+ return requestBody;
223
+ }
224
+ /** Fields get_customer always prints, or that are not customer data. */
225
+ const SUMMARY_FIELDS = new Set([
226
+ "customer_id",
227
+ "external_customer_id",
228
+ "status",
229
+ "email",
230
+ "customer_type",
231
+ "first_name",
232
+ "last_name",
233
+ "company_name",
234
+ "phone",
235
+ "entitlements",
236
+ "verification_levels",
237
+ "created_at",
238
+ "updated_at",
239
+ ]);
240
+ /**
241
+ * `include` flattens the verification profile onto the customer (date_of_birth,
242
+ * nationality, address and so on) and adds doc_images. Print whatever came
243
+ * back rather than a fixed list: the published spec does not list these
244
+ * fields, so a fixed list would be a guess. Images are counted, not dumped.
245
+ */
246
+ function describeIncluded(customer) {
247
+ const extra = Object.entries(customer).filter(([key, value]) => !SUMMARY_FIELDS.has(key) && value !== null && value !== undefined);
248
+ if (extra.length === 0) {
249
+ return "\n\nVerification profile: nothing on file yet (it fills in as verification is submitted).";
250
+ }
251
+ const lines = extra.map(([key, value]) => key === "doc_images" && Array.isArray(value)
252
+ ? `${key}: ${value.length} image(s)`
253
+ : `${key}: ${typeof value === "object" ? JSON.stringify(value) : String(value)}`);
254
+ return `\n\nVerification profile:\n${lines.join("\n")}`;
255
+ }
13
256
  export function registerCustomerTools(server, client) {
14
257
  // Create Customer
15
- server.tool("create_customer", "Create a new customer in Stables for KYC verification and transfers. Use 'individual' for personal accounts or 'business' for company accounts. Include entitlements like 'base_payout' to enable transactions.", {
16
- email: z.string().email().optional().describe("Customer's email address"),
17
- customerType: z
18
- .enum(["individual", "business"])
19
- .describe("Type of customer - 'individual' for personal, 'business' for companies"),
20
- // Individual fields
21
- firstName: z.string().optional().describe("First name (required for individuals)"),
22
- lastName: z.string().optional().describe("Last name (required for individuals)"),
23
- middleName: z.string().optional().describe("Middle name"),
24
- dob: z
25
- .string()
26
- .optional()
27
- .describe("Date of birth in YYYY-MM-DD format (e.g., '1990-01-15')"),
28
- nationality: z.string().optional().describe("Two-letter country code (e.g., 'US', 'GB')"),
29
- // Business fields
30
- companyName: z.string().optional().describe("Company name (required for businesses)"),
31
- country: z
32
- .string()
33
- .optional()
34
- .describe("Country code ISO 3166-1 alpha-2 (required for businesses, e.g., 'US')"),
35
- registrationNumber: z.string().optional().describe("Business registration number"),
36
- incorporatedOn: z.string().optional().describe("Date of incorporation in YYYY-MM-DD format"),
37
- type: z
38
- .string()
39
- .optional()
40
- .describe("Company type (e.g., 'Private Company Limited by Shares')"),
41
- taxId: z.string().optional().describe("Tax ID (e.g., '12-3456789')"),
42
- registrationLocation: z
43
- .string()
44
- .optional()
45
- .describe("Registration location (e.g., state for USA)"),
46
- website: z.string().optional().describe("Website URL"),
47
- describeBusiness: z.string().optional().describe("Description of the business"),
48
- conductMoneyServices: z
49
- .boolean()
50
- .optional()
51
- .describe("Whether the company conducts money services"),
52
- describeMoneyServices: z
53
- .string()
54
- .optional()
55
- .describe("Describe money services (required if conductMoneyServices is true)"),
56
- describeComplianceControls: z
57
- .string()
58
- .optional()
59
- .describe("Description of compliance controls"),
60
- mainSourceOfFunds: z
61
- .enum([
62
- "BUSINESS_LOANS",
63
- "GRANTS",
64
- "INTER_COMPANY_FUNDS",
65
- "INVESTMENT_PROCEEDS",
66
- "LEGAL_SETTLEMENT",
67
- "OWNERS_CAPITAL",
68
- "PENSION_RETIREMENT",
69
- "SALE_OF_ASSETS",
70
- "SALES_OF_GOODS_AND_SERVICES",
71
- "THIRD_PARTY_FUNDS",
72
- "TREASURY_RESERVES",
73
- ])
74
- .optional()
75
- .describe("Main source of funds"),
76
- accountPurpose: z
77
- .enum([
78
- "CHARITABLE_DONATIONS",
79
- "ECOMMERCE_RETAIL_PAYMENTS",
80
- "INVESTMENT_PURPOSES",
81
- "PAYMENTS_TO_FRIENDS_OR_FAMILY_ABROAD",
82
- "PAYROLL",
83
- "PERSONAL_OR_LIVING_EXPENSES",
84
- "PROTECT_WEALTH",
85
- "PURCHASE_GOODS_AND_SERVICES",
86
- "RECEIVE_PAYMENTS_FOR_GOODS_AND_SERVICES",
87
- "TAX_OPTIMIZATION",
88
- "THIRD_PARTY_MONEY_TRANSMISSION",
89
- "TREASURY_MANAGEMENT",
90
- "OTHER",
91
- ])
92
- .optional()
93
- .describe("What will you use Stables for?"),
94
- accountPurposeOther: z
95
- .string()
96
- .optional()
97
- .describe("Explain purpose (required if accountPurpose is OTHER)"),
98
- isYourBusinessADao: z.boolean().optional().describe("Is your business a DAO?"),
99
- industrySelection: z.string().optional().describe("NAICS industry code (e.g., '5415')"),
100
- expectedAnnualRevenue: z
101
- .enum([
102
- "0_99999",
103
- "100000_999999",
104
- "1000000_9999999",
105
- "10000000_49999999",
106
- "50000000_249999999",
107
- "250000000_plus",
108
- ])
109
- .optional()
110
- .describe("Estimated annual revenue in USD"),
111
- expectedMonthlyPayments: z.string().optional().describe("Expected monthly payments in USD"),
112
- sourceOfFunds: z
113
- .enum([
114
- "BUSINESS_LOANS",
115
- "GRANTS",
116
- "INTER_COMPANY_FUNDS",
117
- "INVESTMENT_PROCEEDS",
118
- "LEGAL_SETTLEMENT",
119
- "OWNERS_CAPITAL",
120
- "PENSION_RETIREMENT",
121
- "SALE_OF_ASSETS",
122
- "SALES_OF_GOODS_AND_SERVICES",
123
- "THIRD_PARTY_FUNDS",
124
- "TREASURY_RESERVES",
125
- ])
126
- .optional()
127
- .describe("Source of funds"),
128
- sourceOfFundsDescription: z
129
- .string()
130
- .optional()
131
- .describe("Describe where your business funds come from"),
132
- operateInProhibitedCountry: z
133
- .boolean()
134
- .optional()
135
- .describe("Does your business operate in any prohibited countries?"),
136
- doesYourBusinessEngageInHighRiskActivities: z
137
- .enum(["yes", "no"])
138
- .optional()
139
- .describe("Does your business engage in high risk activities?"),
140
- acceptTerms: z
141
- .boolean()
142
- .optional()
143
- .describe("Accept Stables' terms of service and privacy policy"),
144
- howDidYouComeAcrossStables: z
145
- .string()
146
- .optional()
147
- .describe("How did you come across Stables?"),
148
- // Shared fields
149
- externalCustomerId: z.string().optional().describe("Your own reference ID for this customer"),
150
- phone: z
151
- .string()
152
- .optional()
153
- .describe("Phone number with country code (e.g., '+14155552671')"),
154
- entitlements: z
155
- .array(z.enum(["base_payout", "virtual_account"]))
156
- .optional()
157
- .describe("List of entitlements to request"),
158
- // Address fields (for individuals)
159
- addressLine1: z.string().optional().describe("Street address line 1"),
160
- addressLine2: z.string().optional().describe("Street address line 2"),
161
- addressCity: z.string().optional().describe("City"),
162
- addressState: z.string().optional().describe("State or region"),
163
- addressPostalCode: z.string().optional().describe("Postal/ZIP code"),
164
- addressCountry: z.string().optional().describe("Two-letter country code (e.g., 'US')"),
165
- // Legal address fields (for businesses)
166
- legalAddressLine1: z.string().optional().describe("Legal address line 1 (for businesses)"),
167
- legalAddressLine2: z.string().optional().describe("Legal address line 2 (for businesses)"),
168
- legalAddressCity: z.string().optional().describe("Legal address city (for businesses)"),
169
- legalAddressState: z.string().optional().describe("Legal address state (for businesses)"),
170
- legalAddressPostalCode: z
171
- .string()
172
- .optional()
173
- .describe("Legal address postal code (for businesses)"),
174
- legalAddressCountry: z
175
- .string()
176
- .optional()
177
- .describe("Legal address country code (for businesses)"),
178
- }, async (params) => {
258
+ server.tool("create_customer", "Create a new customer in Stables for KYC verification and transfers. Use 'individual' for personal accounts or 'business' for company accounts. Include entitlements like 'base_payout' to enable transactions.", customerShape, async (params) => {
179
259
  try {
180
- const isBusiness = params.customerType === "business";
181
- // Tool inputs stay camelCase for the agent's benefit; the wire body is
182
- // snake_case, and unknown keys are stripped rather than rejected — which
183
- // is why the old camelCase body arrived empty and 400'd on the missing
184
- // required fields instead of on the wrong ones.
185
- const address = !isBusiness && params.addressLine1
186
- ? {
187
- line1: params.addressLine1,
188
- line2: params.addressLine2,
189
- city: params.addressCity || "",
190
- state: params.addressState,
191
- postal_code: params.addressPostalCode,
192
- country: params.addressCountry || "",
193
- }
194
- : undefined;
195
- const legalAddress = isBusiness && params.legalAddressLine1
196
- ? {
197
- line1: params.legalAddressLine1,
198
- line2: params.legalAddressLine2,
199
- city: params.legalAddressCity || "",
200
- state: params.legalAddressState,
201
- postal_code: params.legalAddressPostalCode,
202
- country: params.legalAddressCountry || "",
203
- }
204
- : undefined;
205
- const requestBody = {
206
- email: params.email,
207
- customer_type: isBusiness ? "business" : "individual",
208
- external_customer_id: params.externalCustomerId || crypto.randomUUID(),
209
- phone: params.phone,
210
- entitlements: params.entitlements,
211
- };
212
- if (isBusiness) {
213
- Object.assign(requestBody, {
214
- company_name: params.companyName,
215
- country: params.country,
216
- registration_number: params.registrationNumber,
217
- incorporated_on: params.incorporatedOn,
218
- type: params.type,
219
- tax_id: params.taxId,
220
- registration_location: params.registrationLocation,
221
- website: params.website,
222
- legal_address: legalAddress,
223
- describe_business: params.describeBusiness,
224
- conduct_money_services: params.conductMoneyServices,
225
- describe_money_services: params.describeMoneyServices,
226
- describe_compliance_controls: params.describeComplianceControls,
227
- main_source_of_funds: params.mainSourceOfFunds,
228
- account_purpose: params.accountPurpose,
229
- account_purpose_other: params.accountPurposeOther,
230
- is_your_business_a_dao: params.isYourBusinessADao,
231
- industry_selection: params.industrySelection,
232
- expected_annual_revenue: params.expectedAnnualRevenue,
233
- expected_monthly_payments: params.expectedMonthlyPayments,
234
- source_of_funds: params.sourceOfFunds,
235
- source_of_funds_description: params.sourceOfFundsDescription,
236
- operate_in_prohibited_country: params.operateInProhibitedCountry,
237
- does_your_business_engage_in_high_risk_activities: params.doesYourBusinessEngageInHighRiskActivities,
238
- accept_terms: params.acceptTerms,
239
- how_did_you_come_across_stables: params.howDidYouComeAcrossStables,
240
- });
241
- }
242
- else {
243
- Object.assign(requestBody, {
244
- first_name: params.firstName,
245
- last_name: params.lastName,
246
- middle_name: params.middleName,
247
- dob: params.dob,
248
- nationality: params.nationality,
249
- address,
250
- });
251
- }
252
- // Remove undefined values
253
- for (const key of Object.keys(requestBody)) {
254
- if (requestBody[key] === undefined)
255
- delete requestBody[key];
256
- }
260
+ const requestBody = buildCustomerBody(params);
257
261
  const customer = await client.createCustomer(requestBody);
258
262
  const verificationStatus = customer.verification_levels?.[0]?.status || "not_started";
259
263
  const entitlementsList = customer.entitlements?.map((e) => `${e.name}: ${e.status}`).join(", ") || "None";
@@ -291,12 +295,73 @@ Next step: Use 'get_verification_link' to get a KYC verification link for this c
291
295
  };
292
296
  }
293
297
  });
298
+ // Create Customer via Link
299
+ server.tool("create_customer_onboarding_link", "Create a customer and get a hosted verification link for them in one call, so the customer enters their own KYC/KYB details. An existing customer with the same email is reused (customer_created is false) and the fields sent are ignored. Takes the same fields as create_customer; send what you already know.", {
300
+ ...customerShape,
301
+ ttlInSecs: z
302
+ .number()
303
+ .int()
304
+ .positive()
305
+ .optional()
306
+ .describe("Link lifetime in seconds (default 1800)"),
307
+ successUrl: z
308
+ .string()
309
+ .optional()
310
+ .describe("Where to send the customer after verification succeeds"),
311
+ rejectUrl: z
312
+ .string()
313
+ .optional()
314
+ .describe("Where to send the customer if verification is rejected"),
315
+ verificationType: z
316
+ .enum(["liveness", "age_risk", "passport", "source_of_wealth", "proof_of_address"])
317
+ .optional()
318
+ .describe("Run one verification step instead of the full flow"),
319
+ }, async ({ ttlInSecs, successUrl, rejectUrl, verificationType, ...params }) => {
320
+ try {
321
+ const result = await client.createCustomerOnboardingLink({
322
+ ...buildCustomerBody(params),
323
+ ...(ttlInSecs && { ttl_in_secs: ttlInSecs }),
324
+ ...((successUrl || rejectUrl) && {
325
+ redirect: {
326
+ ...(successUrl && { success_url: successUrl }),
327
+ ...(rejectUrl && { reject_url: rejectUrl }),
328
+ },
329
+ }),
330
+ ...(verificationType && { verification_type: verificationType }),
331
+ });
332
+ return {
333
+ content: [
334
+ {
335
+ type: "text",
336
+ text: `${result.customer_created ? "Customer created." : "Existing customer reused (matched on email); the fields sent were ignored."}
337
+
338
+ Customer ID: ${result.customer_id}
339
+ Verification Link: ${result.kyc_link}
340
+
341
+ Share the link with the customer to complete verification.`,
342
+ },
343
+ ],
344
+ };
345
+ }
346
+ catch (error) {
347
+ return {
348
+ content: [
349
+ { type: "text", text: `Failed to create onboarding link: ${formatError(error)}` },
350
+ ],
351
+ isError: true,
352
+ };
353
+ }
354
+ });
294
355
  // Get Customer
295
356
  server.tool("get_customer", "Get details about a specific customer including their verification status", {
296
357
  customerId: z.string().describe("The customer ID to look up"),
297
- }, async ({ customerId }) => {
358
+ include: z
359
+ .array(z.enum(["verification-profile", "doc-images", "all"]))
360
+ .optional()
361
+ .describe("Also return the normalized KYC/KYB data ('verification-profile') or submitted document images ('doc-images'). Slower; only to sync verification data"),
362
+ }, async ({ customerId, include }) => {
298
363
  try {
299
- const customer = await client.getCustomer(customerId);
364
+ const customer = await client.getCustomer(customerId, include);
300
365
  const verificationStatus = customer.verification_levels?.[0]?.status || "not_started";
301
366
  const isVerified = verificationStatus === "approved";
302
367
  const entitlementsList = customer.entitlements?.map((e) => `${e.name}: ${e.status}`).join(", ") || "None";
@@ -316,7 +381,7 @@ Entitlements: ${entitlementsList}
316
381
  Verification Status: ${verificationStatus}
317
382
  Can Transfer: ${isVerified ? "Yes" : "No - needs KYC verification"}
318
383
  Created: ${customer.created_at}
319
- Updated: ${customer.updated_at}`,
384
+ Updated: ${customer.updated_at}${include?.length ? describeIncluded(customer) : ""}`,
320
385
  },
321
386
  ],
322
387
  };
@@ -334,9 +399,12 @@ Updated: ${customer.updated_at}`,
334
399
  }
335
400
  });
336
401
  // List Customers
337
- server.tool("list_customers", "List all customers for the authenticated tenant", {}, async () => {
402
+ server.tool("list_customers", "List customers for the authenticated tenant. Without pageSize the API returns its default first page; pass pageSize and follow pageToken to read them all", {
403
+ pageSize: z.number().int().min(1).max(100).optional().describe("Customers per page"),
404
+ pageToken: z.string().optional().describe("next_page_token from the previous page"),
405
+ }, async ({ pageSize, pageToken }) => {
338
406
  try {
339
- const response = await client.listCustomers();
407
+ const response = await client.listCustomers({ pageSize, pageToken });
340
408
  if (response.customers.length === 0) {
341
409
  return {
342
410
  content: [
@@ -357,9 +425,9 @@ Updated: ${customer.updated_at}`,
357
425
  content: [
358
426
  {
359
427
  type: "text",
360
- text: `Customers (${response.customers.length}):
428
+ text: `Customers (${response.customers.length}${response.page ? ` of ${response.page.total}` : ""}):
361
429
 
362
- ${customerList}`,
430
+ ${customerList}${response.page?.next_page_token ? `\n\nMore results: pass pageToken "${response.page.next_page_token}".` : ""}`,
363
431
  },
364
432
  ],
365
433
  };
@@ -388,17 +456,24 @@ ${customerList}`,
388
456
  .optional()
389
457
  .describe("URL to redirect to after successful verification"),
390
458
  rejectUrl: z.string().optional().describe("URL to redirect to after rejected verification"),
391
- }, async ({ customerId, ttlInSecs, successUrl, rejectUrl }) => {
459
+ verificationType: z
460
+ .enum(["liveness", "passport", "source_of_wealth", "proof_of_address", "business_edd"])
461
+ .optional()
462
+ .describe("Run one verification step instead of the full flow, e.g. 'proof_of_address' when an RFI asks for it"),
463
+ }, async ({ customerId, ttlInSecs, successUrl, rejectUrl, verificationType }) => {
392
464
  try {
465
+ // The body is snake_case. The camelCase keys sent before were stripped
466
+ // by the API, so the TTL and redirect URLs were silently ignored.
393
467
  const redirect = successUrl || rejectUrl
394
468
  ? {
395
- successUrl,
396
- rejectUrl,
469
+ ...(successUrl && { success_url: successUrl }),
470
+ ...(rejectUrl && { reject_url: rejectUrl }),
397
471
  }
398
472
  : undefined;
399
473
  const result = await client.generateVerificationLink(customerId, {
400
- ttlInSecs,
401
- redirect,
474
+ ...(ttlInSecs && { ttl_in_secs: ttlInSecs }),
475
+ ...(redirect && { redirect }),
476
+ ...(verificationType && { verification_type: verificationType }),
402
477
  });
403
478
  const ttlDisplay = ttlInSecs
404
479
  ? `${Math.floor(ttlInSecs / 60)} minutes`
@@ -441,10 +516,7 @@ Share this link with the customer to complete their identity verification.`,
441
516
  dob: z.string().optional().describe("Updated date of birth (YYYY-MM-DD)"),
442
517
  nationality: z.string().optional().describe("Updated nationality (two-letter country code)"),
443
518
  companyName: z.string().optional().describe("Updated company name (for businesses)"),
444
- entitlements: z
445
- .array(z.enum(["base_payout", "virtual_account"]))
446
- .optional()
447
- .describe("Updated entitlements"),
519
+ entitlements: z.array(z.enum(ENTITLEMENT_IDS)).optional().describe("Updated entitlements"),
448
520
  }, async ({ customerId, email, phone, firstName, lastName, middleName, dob, nationality, companyName, entitlements, }) => {
449
521
  try {
450
522
  // Every field on this endpoint is optional, so camelCase keys were not