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.
- package/README.md +40 -6
- package/build/http/handler.d.ts.map +1 -1
- package/build/http/handler.js +9 -0
- package/build/http/handler.js.map +1 -1
- package/build/lib/stables-client.d.ts +282 -23
- package/build/lib/stables-client.d.ts.map +1 -1
- package/build/lib/stables-client.js +164 -10
- package/build/lib/stables-client.js.map +1 -1
- package/build/server.d.ts +2 -1
- package/build/server.d.ts.map +1 -1
- package/build/server.js +10 -1
- package/build/server.js.map +1 -1
- package/build/tools/balances.d.ts +15 -0
- package/build/tools/balances.d.ts.map +1 -0
- package/build/tools/balances.js +140 -0
- package/build/tools/balances.js.map +1 -0
- package/build/tools/compliance.d.ts +13 -0
- package/build/tools/compliance.d.ts.map +1 -0
- package/build/tools/compliance.js +170 -0
- package/build/tools/compliance.js.map +1 -0
- package/build/tools/customers.d.ts.map +1 -1
- package/build/tools/customers.js +330 -258
- package/build/tools/customers.js.map +1 -1
- package/build/tools/documents.d.ts +12 -0
- package/build/tools/documents.d.ts.map +1 -0
- package/build/tools/documents.js +94 -0
- package/build/tools/documents.js.map +1 -0
- package/build/tools/entitlements.d.ts +12 -0
- package/build/tools/entitlements.d.ts.map +1 -0
- package/build/tools/entitlements.js +83 -0
- package/build/tools/entitlements.js.map +1 -0
- package/build/tools/transfers.d.ts.map +1 -1
- package/build/tools/transfers.js +174 -89
- package/build/tools/transfers.js.map +1 -1
- package/build/tools/virtual-accounts.d.ts.map +1 -1
- package/build/tools/virtual-accounts.js +154 -12
- package/build/tools/virtual-accounts.js.map +1 -1
- package/build/version.d.ts +2 -0
- package/build/version.d.ts.map +1 -0
- package/build/version.js +3 -0
- package/build/version.js.map +1 -0
- package/build/worker.d.ts +16 -1
- package/build/worker.d.ts.map +1 -1
- package/build/worker.js +24 -1
- package/build/worker.js.map +1 -1
- package/package.json +12 -6
package/build/tools/customers.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|