This is page 6 of 10. Use http://codebase.md/dodopayments/dodopayments-node?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .devcontainer
│ └── devcontainer.json
├── .github
│ └── workflows
│ ├── ci.yml
│ ├── docker-mcp.yml
│ ├── publish-npm.yml
│ └── release-doctor.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .release-please-manifest.json
├── .stats.yml
├── api.md
├── bin
│ ├── check-release-environment
│ ├── cli
│ ├── docker-tags
│ ├── migration-config.json
│ └── publish-npm
├── Brewfile
├── CHANGELOG.md
├── CONTRIBUTING.md
├── eslint.config.mjs
├── examples
│ └── .keep
├── jest.config.ts
├── LICENSE
├── MIGRATION.md
├── package.json
├── packages
│ └── mcp-server
│ ├── .dockerignore
│ ├── build
│ ├── cloudflare-worker
│ │ ├── .gitignore
│ │ ├── biome.json
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── app.ts
│ │ │ ├── index.ts
│ │ │ └── utils.ts
│ │ ├── static
│ │ │ └── home.md
│ │ ├── tsconfig.json
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── Dockerfile
│ ├── jest.config.ts
│ ├── manifest.json
│ ├── package.json
│ ├── README.md
│ ├── scripts
│ │ ├── copy-bundle-files.cjs
│ │ └── postprocess-dist-package-json.cjs
│ ├── src
│ │ ├── code-tool-paths.cts
│ │ ├── code-tool-types.ts
│ │ ├── code-tool-worker.ts
│ │ ├── code-tool.ts
│ │ ├── compat.ts
│ │ ├── docs-search-tool.ts
│ │ ├── dynamic-tools.ts
│ │ ├── filtering.ts
│ │ ├── headers.ts
│ │ ├── http.ts
│ │ ├── index.ts
│ │ ├── options.ts
│ │ ├── server.ts
│ │ ├── stdio.ts
│ │ ├── tools
│ │ │ ├── addons
│ │ │ │ ├── create-addons.ts
│ │ │ │ ├── list-addons.ts
│ │ │ │ ├── retrieve-addons.ts
│ │ │ │ ├── update-addons.ts
│ │ │ │ └── update-images-addons.ts
│ │ │ ├── brands
│ │ │ │ ├── create-brands.ts
│ │ │ │ ├── list-brands.ts
│ │ │ │ ├── retrieve-brands.ts
│ │ │ │ ├── update-brands.ts
│ │ │ │ └── update-images-brands.ts
│ │ │ ├── checkout-sessions
│ │ │ │ ├── create-checkout-sessions.ts
│ │ │ │ └── retrieve-checkout-sessions.ts
│ │ │ ├── customers
│ │ │ │ ├── create-customers.ts
│ │ │ │ ├── customer-portal
│ │ │ │ │ └── create-customers-customer-portal.ts
│ │ │ │ ├── list-customers.ts
│ │ │ │ ├── retrieve-customers.ts
│ │ │ │ ├── update-customers.ts
│ │ │ │ └── wallets
│ │ │ │ ├── ledger-entries
│ │ │ │ │ ├── create-wallets-customers-ledger-entries.ts
│ │ │ │ │ └── list-wallets-customers-ledger-entries.ts
│ │ │ │ └── list-customers-wallets.ts
│ │ │ ├── discounts
│ │ │ │ ├── create-discounts.ts
│ │ │ │ ├── delete-discounts.ts
│ │ │ │ ├── list-discounts.ts
│ │ │ │ ├── retrieve-discounts.ts
│ │ │ │ └── update-discounts.ts
│ │ │ ├── disputes
│ │ │ │ ├── list-disputes.ts
│ │ │ │ └── retrieve-disputes.ts
│ │ │ ├── index.ts
│ │ │ ├── invoices
│ │ │ │ └── payments
│ │ │ │ ├── retrieve-invoices-payments.ts
│ │ │ │ └── retrieve-refund-invoices-payments.ts
│ │ │ ├── license-key-instances
│ │ │ │ ├── list-license-key-instances.ts
│ │ │ │ ├── retrieve-license-key-instances.ts
│ │ │ │ └── update-license-key-instances.ts
│ │ │ ├── license-keys
│ │ │ │ ├── list-license-keys.ts
│ │ │ │ ├── retrieve-license-keys.ts
│ │ │ │ └── update-license-keys.ts
│ │ │ ├── licenses
│ │ │ │ ├── activate-licenses.ts
│ │ │ │ ├── deactivate-licenses.ts
│ │ │ │ └── validate-licenses.ts
│ │ │ ├── meters
│ │ │ │ ├── archive-meters.ts
│ │ │ │ ├── create-meters.ts
│ │ │ │ ├── list-meters.ts
│ │ │ │ ├── retrieve-meters.ts
│ │ │ │ └── unarchive-meters.ts
│ │ │ ├── misc
│ │ │ │ └── list-supported-countries-misc.ts
│ │ │ ├── payments
│ │ │ │ ├── create-payments.ts
│ │ │ │ ├── list-payments.ts
│ │ │ │ ├── retrieve-line-items-payments.ts
│ │ │ │ └── retrieve-payments.ts
│ │ │ ├── payouts
│ │ │ │ └── list-payouts.ts
│ │ │ ├── products
│ │ │ │ ├── archive-products.ts
│ │ │ │ ├── create-products.ts
│ │ │ │ ├── images
│ │ │ │ │ └── update-products-images.ts
│ │ │ │ ├── list-products.ts
│ │ │ │ ├── retrieve-products.ts
│ │ │ │ ├── unarchive-products.ts
│ │ │ │ ├── update-files-products.ts
│ │ │ │ └── update-products.ts
│ │ │ ├── refunds
│ │ │ │ ├── create-refunds.ts
│ │ │ │ ├── list-refunds.ts
│ │ │ │ └── retrieve-refunds.ts
│ │ │ ├── subscriptions
│ │ │ │ ├── change-plan-subscriptions.ts
│ │ │ │ ├── charge-subscriptions.ts
│ │ │ │ ├── create-subscriptions.ts
│ │ │ │ ├── list-subscriptions.ts
│ │ │ │ ├── retrieve-subscriptions.ts
│ │ │ │ ├── retrieve-usage-history-subscriptions.ts
│ │ │ │ └── update-subscriptions.ts
│ │ │ ├── types.ts
│ │ │ ├── usage-events
│ │ │ │ ├── ingest-usage-events.ts
│ │ │ │ ├── list-usage-events.ts
│ │ │ │ └── retrieve-usage-events.ts
│ │ │ └── webhooks
│ │ │ ├── create-webhooks.ts
│ │ │ ├── delete-webhooks.ts
│ │ │ ├── headers
│ │ │ │ ├── retrieve-webhooks-headers.ts
│ │ │ │ └── update-webhooks-headers.ts
│ │ │ ├── list-webhooks.ts
│ │ │ ├── retrieve-secret-webhooks.ts
│ │ │ ├── retrieve-webhooks.ts
│ │ │ └── update-webhooks.ts
│ │ └── tools.ts
│ ├── tests
│ │ ├── compat.test.ts
│ │ ├── dynamic-tools.test.ts
│ │ ├── options.test.ts
│ │ └── tools.test.ts
│ ├── tsc-multi.json
│ ├── tsconfig.build.json
│ ├── tsconfig.dist-src.json
│ ├── tsconfig.json
│ └── yarn.lock
├── README.md
├── release-please-config.json
├── scripts
│ ├── bootstrap
│ ├── build
│ ├── build-all
│ ├── fast-format
│ ├── format
│ ├── lint
│ ├── mock
│ ├── publish-packages.ts
│ ├── test
│ └── utils
│ ├── attw-report.cjs
│ ├── check-is-in-git-install.sh
│ ├── check-version.cjs
│ ├── fix-index-exports.cjs
│ ├── git-swap.sh
│ ├── make-dist-package-json.cjs
│ ├── postprocess-files.cjs
│ └── upload-artifact.sh
├── SECURITY.md
├── src
│ ├── api-promise.ts
│ ├── client.ts
│ ├── core
│ │ ├── api-promise.ts
│ │ ├── error.ts
│ │ ├── pagination.ts
│ │ ├── README.md
│ │ ├── resource.ts
│ │ └── uploads.ts
│ ├── error.ts
│ ├── index.ts
│ ├── internal
│ │ ├── builtin-types.ts
│ │ ├── detect-platform.ts
│ │ ├── errors.ts
│ │ ├── headers.ts
│ │ ├── parse.ts
│ │ ├── README.md
│ │ ├── request-options.ts
│ │ ├── shim-types.ts
│ │ ├── shims.ts
│ │ ├── to-file.ts
│ │ ├── types.ts
│ │ ├── uploads.ts
│ │ ├── utils
│ │ │ ├── base64.ts
│ │ │ ├── bytes.ts
│ │ │ ├── env.ts
│ │ │ ├── log.ts
│ │ │ ├── path.ts
│ │ │ ├── sleep.ts
│ │ │ ├── uuid.ts
│ │ │ └── values.ts
│ │ └── utils.ts
│ ├── lib
│ │ └── .keep
│ ├── pagination.ts
│ ├── resource.ts
│ ├── resources
│ │ ├── addons.ts
│ │ ├── brands.ts
│ │ ├── checkout-sessions.ts
│ │ ├── customers
│ │ │ ├── customer-portal.ts
│ │ │ ├── customers.ts
│ │ │ ├── index.ts
│ │ │ ├── wallets
│ │ │ │ ├── index.ts
│ │ │ │ ├── ledger-entries.ts
│ │ │ │ └── wallets.ts
│ │ │ └── wallets.ts
│ │ ├── customers.ts
│ │ ├── discounts.ts
│ │ ├── disputes.ts
│ │ ├── index.ts
│ │ ├── invoices
│ │ │ ├── index.ts
│ │ │ ├── invoices.ts
│ │ │ └── payments.ts
│ │ ├── invoices.ts
│ │ ├── license-key-instances.ts
│ │ ├── license-keys.ts
│ │ ├── licenses.ts
│ │ ├── meters.ts
│ │ ├── misc.ts
│ │ ├── payments.ts
│ │ ├── payouts.ts
│ │ ├── products
│ │ │ ├── images.ts
│ │ │ ├── index.ts
│ │ │ └── products.ts
│ │ ├── products.ts
│ │ ├── refunds.ts
│ │ ├── subscriptions.ts
│ │ ├── usage-events.ts
│ │ ├── webhook-events.ts
│ │ ├── webhooks
│ │ │ ├── headers.ts
│ │ │ ├── index.ts
│ │ │ └── webhooks.ts
│ │ └── webhooks.ts
│ ├── resources.ts
│ ├── uploads.ts
│ └── version.ts
├── tests
│ ├── api-resources
│ │ ├── addons.test.ts
│ │ ├── brands.test.ts
│ │ ├── checkout-sessions.test.ts
│ │ ├── customers
│ │ │ ├── customer-portal.test.ts
│ │ │ ├── customers.test.ts
│ │ │ └── wallets
│ │ │ ├── ledger-entries.test.ts
│ │ │ └── wallets.test.ts
│ │ ├── discounts.test.ts
│ │ ├── disputes.test.ts
│ │ ├── license-key-instances.test.ts
│ │ ├── license-keys.test.ts
│ │ ├── licenses.test.ts
│ │ ├── meters.test.ts
│ │ ├── misc.test.ts
│ │ ├── payments.test.ts
│ │ ├── payouts.test.ts
│ │ ├── products
│ │ │ ├── images.test.ts
│ │ │ └── products.test.ts
│ │ ├── refunds.test.ts
│ │ ├── subscriptions.test.ts
│ │ ├── usage-events.test.ts
│ │ └── webhooks
│ │ ├── headers.test.ts
│ │ └── webhooks.test.ts
│ ├── base64.test.ts
│ ├── buildHeaders.test.ts
│ ├── form.test.ts
│ ├── index.test.ts
│ ├── path.test.ts
│ ├── stringifyQuery.test.ts
│ └── uploads.test.ts
├── tsc-multi.json
├── tsconfig.build.json
├── tsconfig.deno.json
├── tsconfig.dist-src.json
├── tsconfig.json
└── yarn.lock
```
# Files
--------------------------------------------------------------------------------
/src/resources/payments.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { APIResource } from '../core/resource';
4 | import * as DisputesAPI from './disputes';
5 | import * as MiscAPI from './misc';
6 | import * as RefundsAPI from './refunds';
7 | import { APIPromise } from '../core/api-promise';
8 | import {
9 | DefaultPageNumberPagination,
10 | type DefaultPageNumberPaginationParams,
11 | PagePromise,
12 | } from '../core/pagination';
13 | import { RequestOptions } from '../internal/request-options';
14 | import { path } from '../internal/utils/path';
15 |
16 | export class Payments extends APIResource {
17 | create(body: PaymentCreateParams, options?: RequestOptions): APIPromise<PaymentCreateResponse> {
18 | return this._client.post('/payments', { body, ...options });
19 | }
20 |
21 | retrieve(paymentID: string, options?: RequestOptions): APIPromise<Payment> {
22 | return this._client.get(path`/payments/${paymentID}`, options);
23 | }
24 |
25 | list(
26 | query: PaymentListParams | null | undefined = {},
27 | options?: RequestOptions,
28 | ): PagePromise<PaymentListResponsesDefaultPageNumberPagination, PaymentListResponse> {
29 | return this._client.getAPIList('/payments', DefaultPageNumberPagination<PaymentListResponse>, {
30 | query,
31 | ...options,
32 | });
33 | }
34 |
35 | retrieveLineItems(
36 | paymentID: string,
37 | options?: RequestOptions,
38 | ): APIPromise<PaymentRetrieveLineItemsResponse> {
39 | return this._client.get(path`/payments/${paymentID}/line-items`, options);
40 | }
41 | }
42 |
43 | export type PaymentListResponsesDefaultPageNumberPagination =
44 | DefaultPageNumberPagination<PaymentListResponse>;
45 |
46 | export interface AttachExistingCustomer {
47 | customer_id: string;
48 | }
49 |
50 | export interface BillingAddress {
51 | /**
52 | * City name
53 | */
54 | city: string;
55 |
56 | /**
57 | * Two-letter ISO country code (ISO 3166-1 alpha-2)
58 | */
59 | country: MiscAPI.CountryCode;
60 |
61 | /**
62 | * State or province name
63 | */
64 | state: string;
65 |
66 | /**
67 | * Street address including house number and unit/apartment if applicable
68 | */
69 | street: string;
70 |
71 | /**
72 | * Postal code or ZIP code
73 | */
74 | zipcode: string;
75 | }
76 |
77 | export interface CreateNewCustomer {
78 | email: string;
79 |
80 | name: string;
81 |
82 | /**
83 | * When false, the most recently created customer object with the given email is
84 | * used if exists. When true, a new customer object is always created False by
85 | * default
86 | */
87 | create_new_customer?: boolean;
88 |
89 | phone_number?: string | null;
90 | }
91 |
92 | export interface CustomerLimitedDetails {
93 | /**
94 | * Unique identifier for the customer
95 | */
96 | customer_id: string;
97 |
98 | /**
99 | * Email address of the customer
100 | */
101 | email: string;
102 |
103 | /**
104 | * Full name of the customer
105 | */
106 | name: string;
107 |
108 | /**
109 | * Phone number of the customer
110 | */
111 | phone_number?: string | null;
112 | }
113 |
114 | export type CustomerRequest = AttachExistingCustomer | NewCustomer;
115 |
116 | export type IntentStatus =
117 | | 'succeeded'
118 | | 'failed'
119 | | 'cancelled'
120 | | 'processing'
121 | | 'requires_customer_action'
122 | | 'requires_merchant_action'
123 | | 'requires_payment_method'
124 | | 'requires_confirmation'
125 | | 'requires_capture'
126 | | 'partially_captured'
127 | | 'partially_captured_and_capturable';
128 |
129 | export interface NewCustomer {
130 | /**
131 | * Email is required for creating a new customer
132 | */
133 | email: string;
134 |
135 | /**
136 | * Optional full name of the customer. If provided during session creation, it is
137 | * persisted and becomes immutable for the session. If omitted here, it can be
138 | * provided later via the confirm API.
139 | */
140 | name?: string | null;
141 |
142 | phone_number?: string | null;
143 | }
144 |
145 | export interface OneTimeProductCartItem {
146 | product_id: string;
147 |
148 | quantity: number;
149 |
150 | /**
151 | * Amount the customer pays if pay_what_you_want is enabled. If disabled then
152 | * amount will be ignored Represented in the lowest denomination of the currency
153 | * (e.g., cents for USD). For example, to charge $1.00, pass `100`.
154 | */
155 | amount?: number | null;
156 | }
157 |
158 | export interface Payment {
159 | /**
160 | * Billing address details for payments
161 | */
162 | billing: BillingAddress;
163 |
164 | /**
165 | * brand id this payment belongs to
166 | */
167 | brand_id: string;
168 |
169 | /**
170 | * Identifier of the business associated with the payment
171 | */
172 | business_id: string;
173 |
174 | /**
175 | * Timestamp when the payment was created
176 | */
177 | created_at: string;
178 |
179 | /**
180 | * Currency used for the payment
181 | */
182 | currency: MiscAPI.Currency;
183 |
184 | /**
185 | * Details about the customer who made the payment
186 | */
187 | customer: CustomerLimitedDetails;
188 |
189 | /**
190 | * brand id this payment belongs to
191 | */
192 | digital_products_delivered: boolean;
193 |
194 | /**
195 | * List of disputes associated with this payment
196 | */
197 | disputes: Array<DisputesAPI.Dispute>;
198 |
199 | /**
200 | * Additional custom data associated with the payment
201 | */
202 | metadata: { [key: string]: string };
203 |
204 | /**
205 | * Unique identifier for the payment
206 | */
207 | payment_id: string;
208 |
209 | /**
210 | * List of refunds issued for this payment
211 | */
212 | refunds: Array<Payment.Refund>;
213 |
214 | /**
215 | * The amount that will be credited to your Dodo balance after currency conversion
216 | * and processing. Especially relevant for adaptive pricing where the customer's
217 | * payment currency differs from your settlement currency.
218 | */
219 | settlement_amount: number;
220 |
221 | /**
222 | * The currency in which the settlement_amount will be credited to your Dodo
223 | * balance. This may differ from the customer's payment currency in adaptive
224 | * pricing scenarios.
225 | */
226 | settlement_currency: MiscAPI.Currency;
227 |
228 | /**
229 | * Total amount charged to the customer including tax, in smallest currency unit
230 | * (e.g. cents)
231 | */
232 | total_amount: number;
233 |
234 | /**
235 | * ISO2 country code of the card
236 | */
237 | card_issuing_country?: MiscAPI.CountryCode | null;
238 |
239 | /**
240 | * The last four digits of the card
241 | */
242 | card_last_four?: string | null;
243 |
244 | /**
245 | * Card network like VISA, MASTERCARD etc.
246 | */
247 | card_network?: string | null;
248 |
249 | /**
250 | * The type of card DEBIT or CREDIT
251 | */
252 | card_type?: string | null;
253 |
254 | /**
255 | * If payment is made using a checkout session, this field is set to the id of the
256 | * session.
257 | */
258 | checkout_session_id?: string | null;
259 |
260 | /**
261 | * The discount id if discount is applied
262 | */
263 | discount_id?: string | null;
264 |
265 | /**
266 | * An error code if the payment failed
267 | */
268 | error_code?: string | null;
269 |
270 | /**
271 | * An error message if the payment failed
272 | */
273 | error_message?: string | null;
274 |
275 | /**
276 | * Checkout URL
277 | */
278 | payment_link?: string | null;
279 |
280 | /**
281 | * Payment method used by customer (e.g. "card", "bank_transfer")
282 | */
283 | payment_method?: string | null;
284 |
285 | /**
286 | * Specific type of payment method (e.g. "visa", "mastercard")
287 | */
288 | payment_method_type?: string | null;
289 |
290 | /**
291 | * List of products purchased in a one-time payment
292 | */
293 | product_cart?: Array<Payment.ProductCart> | null;
294 |
295 | /**
296 | * This represents the portion of settlement_amount that corresponds to taxes
297 | * collected. Especially relevant for adaptive pricing where the tax component must
298 | * be tracked separately in your Dodo balance.
299 | */
300 | settlement_tax?: number | null;
301 |
302 | /**
303 | * Current status of the payment intent
304 | */
305 | status?: IntentStatus | null;
306 |
307 | /**
308 | * Identifier of the subscription if payment is part of a subscription
309 | */
310 | subscription_id?: string | null;
311 |
312 | /**
313 | * Amount of tax collected in smallest currency unit (e.g. cents)
314 | */
315 | tax?: number | null;
316 |
317 | /**
318 | * Timestamp when the payment was last updated
319 | */
320 | updated_at?: string | null;
321 | }
322 |
323 | export namespace Payment {
324 | export interface Refund {
325 | /**
326 | * The unique identifier of the business issuing the refund.
327 | */
328 | business_id: string;
329 |
330 | /**
331 | * The timestamp of when the refund was created in UTC.
332 | */
333 | created_at: string;
334 |
335 | /**
336 | * If true the refund is a partial refund
337 | */
338 | is_partial: boolean;
339 |
340 | /**
341 | * The unique identifier of the payment associated with the refund.
342 | */
343 | payment_id: string;
344 |
345 | /**
346 | * The unique identifier of the refund.
347 | */
348 | refund_id: string;
349 |
350 | /**
351 | * The current status of the refund.
352 | */
353 | status: RefundsAPI.RefundStatus;
354 |
355 | /**
356 | * The refunded amount.
357 | */
358 | amount?: number | null;
359 |
360 | /**
361 | * The currency of the refund, represented as an ISO 4217 currency code.
362 | */
363 | currency?: MiscAPI.Currency | null;
364 |
365 | /**
366 | * The reason provided for the refund, if any. Optional.
367 | */
368 | reason?: string | null;
369 | }
370 |
371 | export interface ProductCart {
372 | product_id: string;
373 |
374 | quantity: number;
375 | }
376 | }
377 |
378 | export type PaymentMethodTypes =
379 | | 'credit'
380 | | 'debit'
381 | | 'upi_collect'
382 | | 'upi_intent'
383 | | 'apple_pay'
384 | | 'cashapp'
385 | | 'google_pay'
386 | | 'multibanco'
387 | | 'bancontact_card'
388 | | 'eps'
389 | | 'ideal'
390 | | 'przelewy24'
391 | | 'paypal'
392 | | 'affirm'
393 | | 'klarna'
394 | | 'sepa'
395 | | 'ach'
396 | | 'amazon_pay'
397 | | 'afterpay_clearpay';
398 |
399 | export interface PaymentCreateResponse {
400 | /**
401 | * Client secret used to load Dodo checkout SDK NOTE : Dodo checkout SDK will be
402 | * coming soon
403 | */
404 | client_secret: string;
405 |
406 | /**
407 | * Limited details about the customer making the payment
408 | */
409 | customer: CustomerLimitedDetails;
410 |
411 | /**
412 | * Additional metadata associated with the payment
413 | */
414 | metadata: { [key: string]: string };
415 |
416 | /**
417 | * Unique identifier for the payment
418 | */
419 | payment_id: string;
420 |
421 | /**
422 | * Total amount of the payment in smallest currency unit (e.g. cents)
423 | */
424 | total_amount: number;
425 |
426 | /**
427 | * The discount id if discount is applied
428 | */
429 | discount_id?: string | null;
430 |
431 | /**
432 | * Expiry timestamp of the payment link
433 | */
434 | expires_on?: string | null;
435 |
436 | /**
437 | * Optional URL to a hosted payment page
438 | */
439 | payment_link?: string | null;
440 |
441 | /**
442 | * Optional list of products included in the payment
443 | */
444 | product_cart?: Array<OneTimeProductCartItem> | null;
445 | }
446 |
447 | export interface PaymentListResponse {
448 | brand_id: string;
449 |
450 | created_at: string;
451 |
452 | currency: MiscAPI.Currency;
453 |
454 | customer: CustomerLimitedDetails;
455 |
456 | digital_products_delivered: boolean;
457 |
458 | metadata: { [key: string]: string };
459 |
460 | payment_id: string;
461 |
462 | total_amount: number;
463 |
464 | payment_method?: string | null;
465 |
466 | payment_method_type?: string | null;
467 |
468 | status?: IntentStatus | null;
469 |
470 | subscription_id?: string | null;
471 | }
472 |
473 | export interface PaymentRetrieveLineItemsResponse {
474 | currency: MiscAPI.Currency;
475 |
476 | items: Array<PaymentRetrieveLineItemsResponse.Item>;
477 | }
478 |
479 | export namespace PaymentRetrieveLineItemsResponse {
480 | export interface Item {
481 | amount: number;
482 |
483 | items_id: string;
484 |
485 | refundable_amount: number;
486 |
487 | tax: number;
488 |
489 | description?: string | null;
490 |
491 | name?: string | null;
492 | }
493 | }
494 |
495 | export interface PaymentCreateParams {
496 | /**
497 | * Billing address details for the payment
498 | */
499 | billing: BillingAddress;
500 |
501 | /**
502 | * Customer information for the payment
503 | */
504 | customer: CustomerRequest;
505 |
506 | /**
507 | * List of products in the cart. Must contain at least 1 and at most 100 items.
508 | */
509 | product_cart: Array<OneTimeProductCartItem>;
510 |
511 | /**
512 | * List of payment methods allowed during checkout.
513 | *
514 | * Customers will **never** see payment methods that are **not** in this list.
515 | * However, adding a method here **does not guarantee** customers will see it.
516 | * Availability still depends on other factors (e.g., customer location, merchant
517 | * settings).
518 | */
519 | allowed_payment_method_types?: Array<PaymentMethodTypes> | null;
520 |
521 | /**
522 | * Fix the currency in which the end customer is billed. If Dodo Payments cannot
523 | * support that currency for this transaction, it will not proceed
524 | */
525 | billing_currency?: MiscAPI.Currency | null;
526 |
527 | /**
528 | * Discount Code to apply to the transaction
529 | */
530 | discount_code?: string | null;
531 |
532 | /**
533 | * Override merchant default 3DS behaviour for this payment
534 | */
535 | force_3ds?: boolean | null;
536 |
537 | /**
538 | * Additional metadata associated with the payment. Defaults to empty if not
539 | * provided.
540 | */
541 | metadata?: { [key: string]: string };
542 |
543 | /**
544 | * Whether to generate a payment link. Defaults to false if not specified.
545 | */
546 | payment_link?: boolean | null;
547 |
548 | /**
549 | * Optional URL to redirect the customer after payment. Must be a valid URL if
550 | * provided.
551 | */
552 | return_url?: string | null;
553 |
554 | /**
555 | * Display saved payment methods of a returning customer False by default
556 | */
557 | show_saved_payment_methods?: boolean;
558 |
559 | /**
560 | * Tax ID in case the payment is B2B. If tax id validation fails the payment
561 | * creation will fail
562 | */
563 | tax_id?: string | null;
564 | }
565 |
566 | export interface PaymentListParams extends DefaultPageNumberPaginationParams {
567 | /**
568 | * filter by Brand id
569 | */
570 | brand_id?: string;
571 |
572 | /**
573 | * Get events after this created time
574 | */
575 | created_at_gte?: string;
576 |
577 | /**
578 | * Get events created before this time
579 | */
580 | created_at_lte?: string;
581 |
582 | /**
583 | * Filter by customer id
584 | */
585 | customer_id?: string;
586 |
587 | /**
588 | * Filter by status
589 | */
590 | status?:
591 | | 'succeeded'
592 | | 'failed'
593 | | 'cancelled'
594 | | 'processing'
595 | | 'requires_customer_action'
596 | | 'requires_merchant_action'
597 | | 'requires_payment_method'
598 | | 'requires_confirmation'
599 | | 'requires_capture'
600 | | 'partially_captured'
601 | | 'partially_captured_and_capturable';
602 |
603 | /**
604 | * Filter by subscription id
605 | */
606 | subscription_id?: string;
607 | }
608 |
609 | export declare namespace Payments {
610 | export {
611 | type AttachExistingCustomer as AttachExistingCustomer,
612 | type BillingAddress as BillingAddress,
613 | type CreateNewCustomer as CreateNewCustomer,
614 | type CustomerLimitedDetails as CustomerLimitedDetails,
615 | type CustomerRequest as CustomerRequest,
616 | type IntentStatus as IntentStatus,
617 | type NewCustomer as NewCustomer,
618 | type OneTimeProductCartItem as OneTimeProductCartItem,
619 | type Payment as Payment,
620 | type PaymentMethodTypes as PaymentMethodTypes,
621 | type PaymentCreateResponse as PaymentCreateResponse,
622 | type PaymentListResponse as PaymentListResponse,
623 | type PaymentRetrieveLineItemsResponse as PaymentRetrieveLineItemsResponse,
624 | type PaymentListResponsesDefaultPageNumberPagination as PaymentListResponsesDefaultPageNumberPagination,
625 | type PaymentCreateParams as PaymentCreateParams,
626 | type PaymentListParams as PaymentListParams,
627 | };
628 | }
629 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/subscriptions/create-subscriptions.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
4 |
5 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
6 | import DodoPayments from 'dodopayments';
7 |
8 | export const metadata: Metadata = {
9 | resource: 'subscriptions',
10 | operation: 'write',
11 | tags: [],
12 | httpMethod: 'post',
13 | httpPath: '/subscriptions',
14 | operationId: 'create_subscription_handler',
15 | };
16 |
17 | export const tool: Tool = {
18 | name: 'create_subscriptions',
19 | description: '',
20 | inputSchema: {
21 | type: 'object',
22 | properties: {
23 | billing: {
24 | $ref: '#/$defs/billing_address',
25 | },
26 | customer: {
27 | $ref: '#/$defs/customer_request',
28 | },
29 | product_id: {
30 | type: 'string',
31 | description: 'Unique identifier of the product to subscribe to',
32 | },
33 | quantity: {
34 | type: 'integer',
35 | description: 'Number of units to subscribe for. Must be at least 1.',
36 | },
37 | addons: {
38 | type: 'array',
39 | description: 'Attach addons to this subscription',
40 | items: {
41 | $ref: '#/$defs/attach_addon',
42 | },
43 | },
44 | allowed_payment_method_types: {
45 | type: 'array',
46 | description:
47 | 'List of payment methods allowed during checkout.\n\nCustomers will **never** see payment methods that are **not** in this list.\nHowever, adding a method here **does not guarantee** customers will see it.\nAvailability still depends on other factors (e.g., customer location, merchant settings).',
48 | items: {
49 | $ref: '#/$defs/payment_method_types',
50 | },
51 | },
52 | billing_currency: {
53 | $ref: '#/$defs/currency',
54 | },
55 | discount_code: {
56 | type: 'string',
57 | description: 'Discount Code to apply to the subscription',
58 | },
59 | force_3ds: {
60 | type: 'boolean',
61 | description: 'Override merchant default 3DS behaviour for this subscription',
62 | },
63 | metadata: {
64 | type: 'object',
65 | description: 'Additional metadata for the subscription\nDefaults to empty if not specified',
66 | additionalProperties: true,
67 | },
68 | on_demand: {
69 | $ref: '#/$defs/on_demand_subscription',
70 | },
71 | payment_link: {
72 | type: 'boolean',
73 | description: 'If true, generates a payment link.\nDefaults to false if not specified.',
74 | },
75 | return_url: {
76 | type: 'string',
77 | description: 'Optional URL to redirect after successful subscription creation',
78 | },
79 | show_saved_payment_methods: {
80 | type: 'boolean',
81 | description: 'Display saved payment methods of a returning customer\nFalse by default',
82 | },
83 | tax_id: {
84 | type: 'string',
85 | description:
86 | 'Tax ID in case the payment is B2B. If tax id validation fails the payment creation will fail',
87 | },
88 | trial_period_days: {
89 | type: 'integer',
90 | description:
91 | "Optional trial period in days\nIf specified, this value overrides the trial period set in the product's price\nMust be between 0 and 10000 days",
92 | },
93 | },
94 | required: ['billing', 'customer', 'product_id', 'quantity'],
95 | $defs: {
96 | billing_address: {
97 | type: 'object',
98 | properties: {
99 | city: {
100 | type: 'string',
101 | description: 'City name',
102 | },
103 | country: {
104 | $ref: '#/$defs/country_code',
105 | },
106 | state: {
107 | type: 'string',
108 | description: 'State or province name',
109 | },
110 | street: {
111 | type: 'string',
112 | description: 'Street address including house number and unit/apartment if applicable',
113 | },
114 | zipcode: {
115 | type: 'string',
116 | description: 'Postal code or ZIP code',
117 | },
118 | },
119 | required: ['city', 'country', 'state', 'street', 'zipcode'],
120 | },
121 | country_code: {
122 | type: 'string',
123 | description: 'ISO country code alpha2 variant',
124 | enum: [
125 | 'AF',
126 | 'AX',
127 | 'AL',
128 | 'DZ',
129 | 'AS',
130 | 'AD',
131 | 'AO',
132 | 'AI',
133 | 'AQ',
134 | 'AG',
135 | 'AR',
136 | 'AM',
137 | 'AW',
138 | 'AU',
139 | 'AT',
140 | 'AZ',
141 | 'BS',
142 | 'BH',
143 | 'BD',
144 | 'BB',
145 | 'BY',
146 | 'BE',
147 | 'BZ',
148 | 'BJ',
149 | 'BM',
150 | 'BT',
151 | 'BO',
152 | 'BQ',
153 | 'BA',
154 | 'BW',
155 | 'BV',
156 | 'BR',
157 | 'IO',
158 | 'BN',
159 | 'BG',
160 | 'BF',
161 | 'BI',
162 | 'KH',
163 | 'CM',
164 | 'CA',
165 | 'CV',
166 | 'KY',
167 | 'CF',
168 | 'TD',
169 | 'CL',
170 | 'CN',
171 | 'CX',
172 | 'CC',
173 | 'CO',
174 | 'KM',
175 | 'CG',
176 | 'CD',
177 | 'CK',
178 | 'CR',
179 | 'CI',
180 | 'HR',
181 | 'CU',
182 | 'CW',
183 | 'CY',
184 | 'CZ',
185 | 'DK',
186 | 'DJ',
187 | 'DM',
188 | 'DO',
189 | 'EC',
190 | 'EG',
191 | 'SV',
192 | 'GQ',
193 | 'ER',
194 | 'EE',
195 | 'ET',
196 | 'FK',
197 | 'FO',
198 | 'FJ',
199 | 'FI',
200 | 'FR',
201 | 'GF',
202 | 'PF',
203 | 'TF',
204 | 'GA',
205 | 'GM',
206 | 'GE',
207 | 'DE',
208 | 'GH',
209 | 'GI',
210 | 'GR',
211 | 'GL',
212 | 'GD',
213 | 'GP',
214 | 'GU',
215 | 'GT',
216 | 'GG',
217 | 'GN',
218 | 'GW',
219 | 'GY',
220 | 'HT',
221 | 'HM',
222 | 'VA',
223 | 'HN',
224 | 'HK',
225 | 'HU',
226 | 'IS',
227 | 'IN',
228 | 'ID',
229 | 'IR',
230 | 'IQ',
231 | 'IE',
232 | 'IM',
233 | 'IL',
234 | 'IT',
235 | 'JM',
236 | 'JP',
237 | 'JE',
238 | 'JO',
239 | 'KZ',
240 | 'KE',
241 | 'KI',
242 | 'KP',
243 | 'KR',
244 | 'KW',
245 | 'KG',
246 | 'LA',
247 | 'LV',
248 | 'LB',
249 | 'LS',
250 | 'LR',
251 | 'LY',
252 | 'LI',
253 | 'LT',
254 | 'LU',
255 | 'MO',
256 | 'MK',
257 | 'MG',
258 | 'MW',
259 | 'MY',
260 | 'MV',
261 | 'ML',
262 | 'MT',
263 | 'MH',
264 | 'MQ',
265 | 'MR',
266 | 'MU',
267 | 'YT',
268 | 'MX',
269 | 'FM',
270 | 'MD',
271 | 'MC',
272 | 'MN',
273 | 'ME',
274 | 'MS',
275 | 'MA',
276 | 'MZ',
277 | 'MM',
278 | 'NA',
279 | 'NR',
280 | 'NP',
281 | 'NL',
282 | 'NC',
283 | 'NZ',
284 | 'NI',
285 | 'NE',
286 | 'NG',
287 | 'NU',
288 | 'NF',
289 | 'MP',
290 | 'NO',
291 | 'OM',
292 | 'PK',
293 | 'PW',
294 | 'PS',
295 | 'PA',
296 | 'PG',
297 | 'PY',
298 | 'PE',
299 | 'PH',
300 | 'PN',
301 | 'PL',
302 | 'PT',
303 | 'PR',
304 | 'QA',
305 | 'RE',
306 | 'RO',
307 | 'RU',
308 | 'RW',
309 | 'BL',
310 | 'SH',
311 | 'KN',
312 | 'LC',
313 | 'MF',
314 | 'PM',
315 | 'VC',
316 | 'WS',
317 | 'SM',
318 | 'ST',
319 | 'SA',
320 | 'SN',
321 | 'RS',
322 | 'SC',
323 | 'SL',
324 | 'SG',
325 | 'SX',
326 | 'SK',
327 | 'SI',
328 | 'SB',
329 | 'SO',
330 | 'ZA',
331 | 'GS',
332 | 'SS',
333 | 'ES',
334 | 'LK',
335 | 'SD',
336 | 'SR',
337 | 'SJ',
338 | 'SZ',
339 | 'SE',
340 | 'CH',
341 | 'SY',
342 | 'TW',
343 | 'TJ',
344 | 'TZ',
345 | 'TH',
346 | 'TL',
347 | 'TG',
348 | 'TK',
349 | 'TO',
350 | 'TT',
351 | 'TN',
352 | 'TR',
353 | 'TM',
354 | 'TC',
355 | 'TV',
356 | 'UG',
357 | 'UA',
358 | 'AE',
359 | 'GB',
360 | 'UM',
361 | 'US',
362 | 'UY',
363 | 'UZ',
364 | 'VU',
365 | 'VE',
366 | 'VN',
367 | 'VG',
368 | 'VI',
369 | 'WF',
370 | 'EH',
371 | 'YE',
372 | 'ZM',
373 | 'ZW',
374 | ],
375 | },
376 | customer_request: {
377 | anyOf: [
378 | {
379 | $ref: '#/$defs/attach_existing_customer',
380 | },
381 | {
382 | $ref: '#/$defs/new_customer',
383 | },
384 | ],
385 | title: 'Customer Request',
386 | },
387 | attach_existing_customer: {
388 | type: 'object',
389 | title: 'Attach Existing Customer',
390 | properties: {
391 | customer_id: {
392 | type: 'string',
393 | },
394 | },
395 | required: ['customer_id'],
396 | },
397 | new_customer: {
398 | type: 'object',
399 | title: 'New Customer',
400 | properties: {
401 | email: {
402 | type: 'string',
403 | description: 'Email is required for creating a new customer',
404 | },
405 | name: {
406 | type: 'string',
407 | description:
408 | 'Optional full name of the customer. If provided during session creation,\nit is persisted and becomes immutable for the session. If omitted here,\nit can be provided later via the confirm API.',
409 | },
410 | phone_number: {
411 | type: 'string',
412 | },
413 | },
414 | required: ['email'],
415 | },
416 | attach_addon: {
417 | type: 'object',
418 | title: 'Attach Addon Request',
419 | properties: {
420 | addon_id: {
421 | type: 'string',
422 | },
423 | quantity: {
424 | type: 'integer',
425 | },
426 | },
427 | required: ['addon_id', 'quantity'],
428 | },
429 | payment_method_types: {
430 | type: 'string',
431 | enum: [
432 | 'credit',
433 | 'debit',
434 | 'upi_collect',
435 | 'upi_intent',
436 | 'apple_pay',
437 | 'cashapp',
438 | 'google_pay',
439 | 'multibanco',
440 | 'bancontact_card',
441 | 'eps',
442 | 'ideal',
443 | 'przelewy24',
444 | 'paypal',
445 | 'affirm',
446 | 'klarna',
447 | 'sepa',
448 | 'ach',
449 | 'amazon_pay',
450 | 'afterpay_clearpay',
451 | ],
452 | },
453 | currency: {
454 | type: 'string',
455 | enum: [
456 | 'AED',
457 | 'ALL',
458 | 'AMD',
459 | 'ANG',
460 | 'AOA',
461 | 'ARS',
462 | 'AUD',
463 | 'AWG',
464 | 'AZN',
465 | 'BAM',
466 | 'BBD',
467 | 'BDT',
468 | 'BGN',
469 | 'BHD',
470 | 'BIF',
471 | 'BMD',
472 | 'BND',
473 | 'BOB',
474 | 'BRL',
475 | 'BSD',
476 | 'BWP',
477 | 'BYN',
478 | 'BZD',
479 | 'CAD',
480 | 'CHF',
481 | 'CLP',
482 | 'CNY',
483 | 'COP',
484 | 'CRC',
485 | 'CUP',
486 | 'CVE',
487 | 'CZK',
488 | 'DJF',
489 | 'DKK',
490 | 'DOP',
491 | 'DZD',
492 | 'EGP',
493 | 'ETB',
494 | 'EUR',
495 | 'FJD',
496 | 'FKP',
497 | 'GBP',
498 | 'GEL',
499 | 'GHS',
500 | 'GIP',
501 | 'GMD',
502 | 'GNF',
503 | 'GTQ',
504 | 'GYD',
505 | 'HKD',
506 | 'HNL',
507 | 'HRK',
508 | 'HTG',
509 | 'HUF',
510 | 'IDR',
511 | 'ILS',
512 | 'INR',
513 | 'IQD',
514 | 'JMD',
515 | 'JOD',
516 | 'JPY',
517 | 'KES',
518 | 'KGS',
519 | 'KHR',
520 | 'KMF',
521 | 'KRW',
522 | 'KWD',
523 | 'KYD',
524 | 'KZT',
525 | 'LAK',
526 | 'LBP',
527 | 'LKR',
528 | 'LRD',
529 | 'LSL',
530 | 'LYD',
531 | 'MAD',
532 | 'MDL',
533 | 'MGA',
534 | 'MKD',
535 | 'MMK',
536 | 'MNT',
537 | 'MOP',
538 | 'MRU',
539 | 'MUR',
540 | 'MVR',
541 | 'MWK',
542 | 'MXN',
543 | 'MYR',
544 | 'MZN',
545 | 'NAD',
546 | 'NGN',
547 | 'NIO',
548 | 'NOK',
549 | 'NPR',
550 | 'NZD',
551 | 'OMR',
552 | 'PAB',
553 | 'PEN',
554 | 'PGK',
555 | 'PHP',
556 | 'PKR',
557 | 'PLN',
558 | 'PYG',
559 | 'QAR',
560 | 'RON',
561 | 'RSD',
562 | 'RUB',
563 | 'RWF',
564 | 'SAR',
565 | 'SBD',
566 | 'SCR',
567 | 'SEK',
568 | 'SGD',
569 | 'SHP',
570 | 'SLE',
571 | 'SLL',
572 | 'SOS',
573 | 'SRD',
574 | 'SSP',
575 | 'STN',
576 | 'SVC',
577 | 'SZL',
578 | 'THB',
579 | 'TND',
580 | 'TOP',
581 | 'TRY',
582 | 'TTD',
583 | 'TWD',
584 | 'TZS',
585 | 'UAH',
586 | 'UGX',
587 | 'USD',
588 | 'UYU',
589 | 'UZS',
590 | 'VES',
591 | 'VND',
592 | 'VUV',
593 | 'WST',
594 | 'XAF',
595 | 'XCD',
596 | 'XOF',
597 | 'XPF',
598 | 'YER',
599 | 'ZAR',
600 | 'ZMW',
601 | ],
602 | },
603 | on_demand_subscription: {
604 | type: 'object',
605 | title: 'On Demand Subscription Request',
606 | properties: {
607 | mandate_only: {
608 | type: 'boolean',
609 | description:
610 | 'If set as True, does not perform any charge and only authorizes payment method details for future use.',
611 | },
612 | adaptive_currency_fees_inclusive: {
613 | type: 'boolean',
614 | description:
615 | 'Whether adaptive currency fees should be included in the product_price (true) or added on top (false).\nThis field is ignored if adaptive pricing is not enabled for the business.',
616 | },
617 | product_currency: {
618 | $ref: '#/$defs/currency',
619 | },
620 | product_description: {
621 | type: 'string',
622 | description:
623 | 'Optional product description override for billing and line items.\nIf not specified, the stored description of the product will be used.',
624 | },
625 | product_price: {
626 | type: 'integer',
627 | description:
628 | 'Product price for the initial charge to customer\nIf not specified the stored price of the product will be used\nRepresented in the lowest denomination of the currency (e.g., cents for USD).\nFor example, to charge $1.00, pass `100`.',
629 | },
630 | },
631 | required: ['mandate_only'],
632 | },
633 | },
634 | },
635 | annotations: {},
636 | };
637 |
638 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
639 | const body = args as any;
640 | return asTextContentResult(await client.subscriptions.create(body));
641 | };
642 |
643 | export default { metadata, tool, handler };
644 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/products/create-products.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
4 |
5 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
6 | import DodoPayments from 'dodopayments';
7 |
8 | export const metadata: Metadata = {
9 | resource: 'products',
10 | operation: 'write',
11 | tags: [],
12 | httpMethod: 'post',
13 | httpPath: '/products',
14 | operationId: 'create_product',
15 | };
16 |
17 | export const tool: Tool = {
18 | name: 'create_products',
19 | description: '',
20 | inputSchema: {
21 | type: 'object',
22 | properties: {
23 | price: {
24 | $ref: '#/$defs/price',
25 | },
26 | tax_category: {
27 | $ref: '#/$defs/tax_category',
28 | },
29 | addons: {
30 | type: 'array',
31 | description: 'Addons available for subscription product',
32 | items: {
33 | type: 'string',
34 | },
35 | },
36 | brand_id: {
37 | type: 'string',
38 | description: 'Brand id for the product, if not provided will default to primary brand',
39 | },
40 | description: {
41 | type: 'string',
42 | description: 'Optional description of the product',
43 | },
44 | digital_product_delivery: {
45 | type: 'object',
46 | title: 'Create Digital Product Delivery Request',
47 | description: 'Choose how you would like you digital product delivered',
48 | properties: {
49 | external_url: {
50 | type: 'string',
51 | description: 'External URL to digital product',
52 | },
53 | instructions: {
54 | type: 'string',
55 | description: 'Instructions to download and use the digital product',
56 | },
57 | },
58 | },
59 | license_key_activation_message: {
60 | type: 'string',
61 | description: 'Optional message displayed during license key activation',
62 | },
63 | license_key_activations_limit: {
64 | type: 'integer',
65 | description: 'The number of times the license key can be activated.\nMust be 0 or greater',
66 | },
67 | license_key_duration: {
68 | $ref: '#/$defs/license_key_duration',
69 | },
70 | license_key_enabled: {
71 | type: 'boolean',
72 | description: 'When true, generates and sends a license key to your customer.\nDefaults to false',
73 | },
74 | metadata: {
75 | type: 'object',
76 | description: 'Additional metadata for the product',
77 | additionalProperties: true,
78 | },
79 | name: {
80 | type: 'string',
81 | description: 'Optional name of the product',
82 | },
83 | },
84 | required: ['price', 'tax_category'],
85 | $defs: {
86 | price: {
87 | anyOf: [
88 | {
89 | type: 'object',
90 | title: 'One Time Price',
91 | description: 'One-time price details.',
92 | properties: {
93 | currency: {
94 | $ref: '#/$defs/currency',
95 | },
96 | discount: {
97 | type: 'integer',
98 | description: 'Discount applied to the price, represented as a percentage (0 to 100).',
99 | },
100 | price: {
101 | type: 'integer',
102 | description:
103 | 'The payment amount, in the smallest denomination of the currency (e.g., cents for USD).\nFor example, to charge $1.00, pass `100`.\n\nIf [`pay_what_you_want`](Self::pay_what_you_want) is set to `true`, this field represents\nthe **minimum** amount the customer must pay.',
104 | },
105 | purchasing_power_parity: {
106 | type: 'boolean',
107 | description:
108 | 'Indicates if purchasing power parity adjustments are applied to the price.\nPurchasing power parity feature is not available as of now.',
109 | },
110 | type: {
111 | type: 'string',
112 | enum: ['one_time_price'],
113 | },
114 | pay_what_you_want: {
115 | type: 'boolean',
116 | description:
117 | 'Indicates whether the customer can pay any amount they choose.\nIf set to `true`, the [`price`](Self::price) field is the minimum amount.',
118 | },
119 | suggested_price: {
120 | type: 'integer',
121 | description:
122 | 'A suggested price for the user to pay. This value is only considered if\n[`pay_what_you_want`](Self::pay_what_you_want) is `true`. Otherwise, it is ignored.',
123 | },
124 | tax_inclusive: {
125 | type: 'boolean',
126 | description: 'Indicates if the price is tax inclusive.',
127 | },
128 | },
129 | required: ['currency', 'discount', 'price', 'purchasing_power_parity', 'type'],
130 | },
131 | {
132 | type: 'object',
133 | title: 'Recurring Price',
134 | description: 'Recurring price details.',
135 | properties: {
136 | currency: {
137 | $ref: '#/$defs/currency',
138 | },
139 | discount: {
140 | type: 'integer',
141 | description: 'Discount applied to the price, represented as a percentage (0 to 100).',
142 | },
143 | payment_frequency_count: {
144 | type: 'integer',
145 | description:
146 | 'Number of units for the payment frequency.\nFor example, a value of `1` with a `payment_frequency_interval` of `month` represents monthly payments.',
147 | },
148 | payment_frequency_interval: {
149 | $ref: '#/$defs/time_interval',
150 | },
151 | price: {
152 | type: 'integer',
153 | description:
154 | 'The payment amount. Represented in the lowest denomination of the currency (e.g., cents for USD).\nFor example, to charge $1.00, pass `100`.',
155 | },
156 | purchasing_power_parity: {
157 | type: 'boolean',
158 | description:
159 | 'Indicates if purchasing power parity adjustments are applied to the price.\nPurchasing power parity feature is not available as of now',
160 | },
161 | subscription_period_count: {
162 | type: 'integer',
163 | description:
164 | 'Number of units for the subscription period.\nFor example, a value of `12` with a `subscription_period_interval` of `month` represents a one-year subscription.',
165 | },
166 | subscription_period_interval: {
167 | $ref: '#/$defs/time_interval',
168 | },
169 | type: {
170 | type: 'string',
171 | enum: ['recurring_price'],
172 | },
173 | tax_inclusive: {
174 | type: 'boolean',
175 | description: 'Indicates if the price is tax inclusive',
176 | },
177 | trial_period_days: {
178 | type: 'integer',
179 | description: 'Number of days for the trial period. A value of `0` indicates no trial period.',
180 | },
181 | },
182 | required: [
183 | 'currency',
184 | 'discount',
185 | 'payment_frequency_count',
186 | 'payment_frequency_interval',
187 | 'price',
188 | 'purchasing_power_parity',
189 | 'subscription_period_count',
190 | 'subscription_period_interval',
191 | 'type',
192 | ],
193 | },
194 | {
195 | type: 'object',
196 | title: 'Usage Based Price',
197 | description: 'Usage Based price details.',
198 | properties: {
199 | currency: {
200 | $ref: '#/$defs/currency',
201 | },
202 | discount: {
203 | type: 'integer',
204 | description: 'Discount applied to the price, represented as a percentage (0 to 100).',
205 | },
206 | fixed_price: {
207 | type: 'integer',
208 | description:
209 | 'The fixed payment amount. Represented in the lowest denomination of the currency (e.g., cents for USD).\nFor example, to charge $1.00, pass `100`.',
210 | },
211 | payment_frequency_count: {
212 | type: 'integer',
213 | description:
214 | 'Number of units for the payment frequency.\nFor example, a value of `1` with a `payment_frequency_interval` of `month` represents monthly payments.',
215 | },
216 | payment_frequency_interval: {
217 | $ref: '#/$defs/time_interval',
218 | },
219 | purchasing_power_parity: {
220 | type: 'boolean',
221 | description:
222 | 'Indicates if purchasing power parity adjustments are applied to the price.\nPurchasing power parity feature is not available as of now',
223 | },
224 | subscription_period_count: {
225 | type: 'integer',
226 | description:
227 | 'Number of units for the subscription period.\nFor example, a value of `12` with a `subscription_period_interval` of `month` represents a one-year subscription.',
228 | },
229 | subscription_period_interval: {
230 | $ref: '#/$defs/time_interval',
231 | },
232 | type: {
233 | type: 'string',
234 | enum: ['usage_based_price'],
235 | },
236 | meters: {
237 | type: 'array',
238 | items: {
239 | $ref: '#/$defs/add_meter_to_price',
240 | },
241 | },
242 | tax_inclusive: {
243 | type: 'boolean',
244 | description: 'Indicates if the price is tax inclusive',
245 | },
246 | },
247 | required: [
248 | 'currency',
249 | 'discount',
250 | 'fixed_price',
251 | 'payment_frequency_count',
252 | 'payment_frequency_interval',
253 | 'purchasing_power_parity',
254 | 'subscription_period_count',
255 | 'subscription_period_interval',
256 | 'type',
257 | ],
258 | },
259 | ],
260 | description: 'One-time price details.',
261 | },
262 | currency: {
263 | type: 'string',
264 | enum: [
265 | 'AED',
266 | 'ALL',
267 | 'AMD',
268 | 'ANG',
269 | 'AOA',
270 | 'ARS',
271 | 'AUD',
272 | 'AWG',
273 | 'AZN',
274 | 'BAM',
275 | 'BBD',
276 | 'BDT',
277 | 'BGN',
278 | 'BHD',
279 | 'BIF',
280 | 'BMD',
281 | 'BND',
282 | 'BOB',
283 | 'BRL',
284 | 'BSD',
285 | 'BWP',
286 | 'BYN',
287 | 'BZD',
288 | 'CAD',
289 | 'CHF',
290 | 'CLP',
291 | 'CNY',
292 | 'COP',
293 | 'CRC',
294 | 'CUP',
295 | 'CVE',
296 | 'CZK',
297 | 'DJF',
298 | 'DKK',
299 | 'DOP',
300 | 'DZD',
301 | 'EGP',
302 | 'ETB',
303 | 'EUR',
304 | 'FJD',
305 | 'FKP',
306 | 'GBP',
307 | 'GEL',
308 | 'GHS',
309 | 'GIP',
310 | 'GMD',
311 | 'GNF',
312 | 'GTQ',
313 | 'GYD',
314 | 'HKD',
315 | 'HNL',
316 | 'HRK',
317 | 'HTG',
318 | 'HUF',
319 | 'IDR',
320 | 'ILS',
321 | 'INR',
322 | 'IQD',
323 | 'JMD',
324 | 'JOD',
325 | 'JPY',
326 | 'KES',
327 | 'KGS',
328 | 'KHR',
329 | 'KMF',
330 | 'KRW',
331 | 'KWD',
332 | 'KYD',
333 | 'KZT',
334 | 'LAK',
335 | 'LBP',
336 | 'LKR',
337 | 'LRD',
338 | 'LSL',
339 | 'LYD',
340 | 'MAD',
341 | 'MDL',
342 | 'MGA',
343 | 'MKD',
344 | 'MMK',
345 | 'MNT',
346 | 'MOP',
347 | 'MRU',
348 | 'MUR',
349 | 'MVR',
350 | 'MWK',
351 | 'MXN',
352 | 'MYR',
353 | 'MZN',
354 | 'NAD',
355 | 'NGN',
356 | 'NIO',
357 | 'NOK',
358 | 'NPR',
359 | 'NZD',
360 | 'OMR',
361 | 'PAB',
362 | 'PEN',
363 | 'PGK',
364 | 'PHP',
365 | 'PKR',
366 | 'PLN',
367 | 'PYG',
368 | 'QAR',
369 | 'RON',
370 | 'RSD',
371 | 'RUB',
372 | 'RWF',
373 | 'SAR',
374 | 'SBD',
375 | 'SCR',
376 | 'SEK',
377 | 'SGD',
378 | 'SHP',
379 | 'SLE',
380 | 'SLL',
381 | 'SOS',
382 | 'SRD',
383 | 'SSP',
384 | 'STN',
385 | 'SVC',
386 | 'SZL',
387 | 'THB',
388 | 'TND',
389 | 'TOP',
390 | 'TRY',
391 | 'TTD',
392 | 'TWD',
393 | 'TZS',
394 | 'UAH',
395 | 'UGX',
396 | 'USD',
397 | 'UYU',
398 | 'UZS',
399 | 'VES',
400 | 'VND',
401 | 'VUV',
402 | 'WST',
403 | 'XAF',
404 | 'XCD',
405 | 'XOF',
406 | 'XPF',
407 | 'YER',
408 | 'ZAR',
409 | 'ZMW',
410 | ],
411 | },
412 | time_interval: {
413 | type: 'string',
414 | enum: ['Day', 'Week', 'Month', 'Year'],
415 | },
416 | add_meter_to_price: {
417 | type: 'object',
418 | title: 'Add Meter To Price',
419 | properties: {
420 | meter_id: {
421 | type: 'string',
422 | },
423 | price_per_unit: {
424 | type: 'string',
425 | description:
426 | 'The price per unit in lowest denomination. Must be greater than zero. Supports up to 5 digits before decimal point and 12 decimal places.',
427 | },
428 | description: {
429 | type: 'string',
430 | description: 'Meter description. Will ignored on Request, but will be shown in response',
431 | },
432 | free_threshold: {
433 | type: 'integer',
434 | },
435 | measurement_unit: {
436 | type: 'string',
437 | description: 'Meter measurement unit. Will ignored on Request, but will be shown in response',
438 | },
439 | name: {
440 | type: 'string',
441 | description: 'Meter name. Will ignored on Request, but will be shown in response',
442 | },
443 | },
444 | required: ['meter_id', 'price_per_unit'],
445 | },
446 | tax_category: {
447 | type: 'string',
448 | description:
449 | 'Represents the different categories of taxation applicable to various products and services.',
450 | enum: ['digital_products', 'saas', 'e_book', 'edtech'],
451 | },
452 | license_key_duration: {
453 | type: 'object',
454 | title: 'License Key Duration',
455 | properties: {
456 | count: {
457 | type: 'integer',
458 | },
459 | interval: {
460 | $ref: '#/$defs/time_interval',
461 | },
462 | },
463 | required: ['count', 'interval'],
464 | },
465 | },
466 | },
467 | annotations: {},
468 | };
469 |
470 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
471 | const body = args as any;
472 | return asTextContentResult(await client.products.create(body));
473 | };
474 |
475 | export default { metadata, tool, handler };
476 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/meters/create-meters.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
4 |
5 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
6 | import DodoPayments from 'dodopayments';
7 |
8 | export const metadata: Metadata = {
9 | resource: 'meters',
10 | operation: 'write',
11 | tags: [],
12 | httpMethod: 'post',
13 | httpPath: '/meters',
14 | operationId: 'create_meter',
15 | };
16 |
17 | export const tool: Tool = {
18 | name: 'create_meters',
19 | description: '',
20 | inputSchema: {
21 | type: 'object',
22 | properties: {
23 | aggregation: {
24 | $ref: '#/$defs/meter_aggregation',
25 | },
26 | event_name: {
27 | type: 'string',
28 | description: 'Event name to track',
29 | },
30 | measurement_unit: {
31 | type: 'string',
32 | description: 'measurement unit',
33 | },
34 | name: {
35 | type: 'string',
36 | description: 'Name of the meter',
37 | },
38 | description: {
39 | type: 'string',
40 | description: 'Optional description of the meter',
41 | },
42 | filter: {
43 | $ref: '#/$defs/meter_filter',
44 | },
45 | },
46 | required: ['aggregation', 'event_name', 'measurement_unit', 'name'],
47 | $defs: {
48 | meter_aggregation: {
49 | type: 'object',
50 | title: 'Meter Aggregation',
51 | properties: {
52 | type: {
53 | type: 'string',
54 | description: 'Aggregation type for the meter',
55 | enum: ['count', 'sum', 'max', 'last'],
56 | },
57 | key: {
58 | type: 'string',
59 | description: 'Required when type is not COUNT',
60 | },
61 | },
62 | required: ['type'],
63 | },
64 | meter_filter: {
65 | type: 'object',
66 | title: 'Meter Filter',
67 | description:
68 | 'A filter structure that combines multiple conditions with logical conjunctions (AND/OR).\n\nSupports up to 3 levels of nesting to create complex filter expressions.\nEach filter has a conjunction (and/or) and clauses that can be either direct conditions or nested filters.',
69 | properties: {
70 | clauses: {
71 | anyOf: [
72 | {
73 | type: 'array',
74 | title: 'Direct Filter Conditions',
75 | description:
76 | 'Direct filter conditions - array of condition objects with key, operator, and value',
77 | items: {
78 | type: 'object',
79 | title: 'MeterFilterCondition',
80 | description: 'Filter condition with key, operator, and value',
81 | properties: {
82 | key: {
83 | type: 'string',
84 | description: 'Filter key to apply',
85 | },
86 | operator: {
87 | type: 'string',
88 | enum: [
89 | 'equals',
90 | 'not_equals',
91 | 'greater_than',
92 | 'greater_than_or_equals',
93 | 'less_than',
94 | 'less_than_or_equals',
95 | 'contains',
96 | 'does_not_contain',
97 | ],
98 | },
99 | value: {
100 | anyOf: [
101 | {
102 | type: 'string',
103 | },
104 | {
105 | type: 'number',
106 | },
107 | {
108 | type: 'boolean',
109 | },
110 | ],
111 | title: 'Filter Value',
112 | description: 'Filter value - can be string, number, or boolean',
113 | },
114 | },
115 | required: ['key', 'operator', 'value'],
116 | },
117 | },
118 | {
119 | type: 'array',
120 | title: 'Nested Meter Filters',
121 | description: 'Nested filters - supports up to 3 levels deep',
122 | items: {
123 | type: 'object',
124 | title: 'MeterFilter',
125 | description: 'Level 1 nested filter - can contain Level 2 filters',
126 | properties: {
127 | clauses: {
128 | anyOf: [
129 | {
130 | type: 'array',
131 | title: 'Level 1 Filter Conditions',
132 | description: 'Array of filter conditions',
133 | items: {
134 | type: 'object',
135 | title: 'MeterFilterCondition',
136 | description: 'Filter condition with key, operator, and value',
137 | properties: {
138 | key: {
139 | type: 'string',
140 | description: 'Filter key to apply',
141 | },
142 | operator: {
143 | type: 'string',
144 | enum: [
145 | 'equals',
146 | 'not_equals',
147 | 'greater_than',
148 | 'greater_than_or_equals',
149 | 'less_than',
150 | 'less_than_or_equals',
151 | 'contains',
152 | 'does_not_contain',
153 | ],
154 | },
155 | value: {
156 | anyOf: [
157 | {
158 | type: 'string',
159 | },
160 | {
161 | type: 'number',
162 | },
163 | {
164 | type: 'boolean',
165 | },
166 | ],
167 | title: 'Filter Value',
168 | description: 'Filter value - can be string, number, or boolean',
169 | },
170 | },
171 | required: ['key', 'operator', 'value'],
172 | },
173 | },
174 | {
175 | type: 'array',
176 | title: 'Level 1 Nested Filters',
177 | description: 'Array of level 2 nested filters',
178 | items: {
179 | type: 'object',
180 | title: 'MeterFilter',
181 | description: 'Level 2 nested filter',
182 | properties: {
183 | clauses: {
184 | anyOf: [
185 | {
186 | type: 'array',
187 | title: 'Level 2 Filter Conditions',
188 | description: 'Array of filter conditions',
189 | items: {
190 | type: 'object',
191 | title: 'MeterFilterCondition',
192 | description: 'Filter condition with key, operator, and value',
193 | properties: {
194 | key: {
195 | type: 'string',
196 | description: 'Filter key to apply',
197 | },
198 | operator: {
199 | type: 'string',
200 | enum: [
201 | 'equals',
202 | 'not_equals',
203 | 'greater_than',
204 | 'greater_than_or_equals',
205 | 'less_than',
206 | 'less_than_or_equals',
207 | 'contains',
208 | 'does_not_contain',
209 | ],
210 | },
211 | value: {
212 | anyOf: [
213 | {
214 | type: 'string',
215 | },
216 | {
217 | type: 'number',
218 | },
219 | {
220 | type: 'boolean',
221 | },
222 | ],
223 | title: 'Filter Value',
224 | description: 'Filter value - can be string, number, or boolean',
225 | },
226 | },
227 | required: ['key', 'operator', 'value'],
228 | },
229 | },
230 | {
231 | type: 'array',
232 | title: 'Level 2 Nested Filters',
233 | description: 'Array of level 3 nested filters (final level)',
234 | items: {
235 | type: 'object',
236 | title: 'MeterFilter',
237 | description: 'Level 3 nested filter (final nesting level)',
238 | properties: {
239 | clauses: {
240 | type: 'array',
241 | title: 'Level 3 Filter Conditions',
242 | description: 'Level 3: Filter conditions only (max depth reached)',
243 | items: {
244 | type: 'object',
245 | title: 'MeterFilterCondition',
246 | description: 'Filter condition with key, operator, and value',
247 | properties: {
248 | key: {
249 | type: 'string',
250 | description: 'Filter key to apply',
251 | },
252 | operator: {
253 | type: 'string',
254 | enum: [
255 | 'equals',
256 | 'not_equals',
257 | 'greater_than',
258 | 'greater_than_or_equals',
259 | 'less_than',
260 | 'less_than_or_equals',
261 | 'contains',
262 | 'does_not_contain',
263 | ],
264 | },
265 | value: {
266 | anyOf: [
267 | {
268 | type: 'string',
269 | },
270 | {
271 | type: 'number',
272 | },
273 | {
274 | type: 'boolean',
275 | },
276 | ],
277 | title: 'Filter Value',
278 | description:
279 | 'Filter value - can be string, number, or boolean',
280 | },
281 | },
282 | required: ['key', 'operator', 'value'],
283 | },
284 | },
285 | conjunction: {
286 | type: 'string',
287 | enum: ['and', 'or'],
288 | },
289 | },
290 | required: ['clauses', 'conjunction'],
291 | },
292 | },
293 | ],
294 | title: 'Level 2 Clause',
295 | description:
296 | 'Level 2: Can be conditions or nested filters (1 more level allowed)',
297 | },
298 | conjunction: {
299 | type: 'string',
300 | enum: ['and', 'or'],
301 | },
302 | },
303 | required: ['clauses', 'conjunction'],
304 | },
305 | },
306 | ],
307 | title: 'Level 1 Clause',
308 | description: 'Level 1: Can be conditions or nested filters (2 more levels allowed)',
309 | },
310 | conjunction: {
311 | type: 'string',
312 | enum: ['and', 'or'],
313 | },
314 | },
315 | required: ['clauses', 'conjunction'],
316 | },
317 | },
318 | ],
319 | title: 'FilterType',
320 | description: 'Filter clauses - can be direct conditions or nested filters (up to 3 levels deep)',
321 | },
322 | conjunction: {
323 | type: 'string',
324 | description: 'Logical conjunction to apply between clauses (and/or)',
325 | enum: ['and', 'or'],
326 | },
327 | },
328 | required: ['clauses', 'conjunction'],
329 | },
330 | },
331 | },
332 | annotations: {},
333 | };
334 |
335 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
336 | const body = args as any;
337 | return asTextContentResult(await client.meters.create(body));
338 | };
339 |
340 | export default { metadata, tool, handler };
341 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/products/update-products.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
4 |
5 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
6 | import DodoPayments from 'dodopayments';
7 |
8 | export const metadata: Metadata = {
9 | resource: 'products',
10 | operation: 'write',
11 | tags: [],
12 | httpMethod: 'patch',
13 | httpPath: '/products/{id}',
14 | operationId: 'patch_product',
15 | };
16 |
17 | export const tool: Tool = {
18 | name: 'update_products',
19 | description: '',
20 | inputSchema: {
21 | type: 'object',
22 | properties: {
23 | id: {
24 | type: 'string',
25 | },
26 | addons: {
27 | type: 'array',
28 | description: 'Available Addons for subscription products',
29 | items: {
30 | type: 'string',
31 | },
32 | },
33 | brand_id: {
34 | type: 'string',
35 | },
36 | description: {
37 | type: 'string',
38 | description: 'Description of the product, optional and must be at most 1000 characters.',
39 | },
40 | digital_product_delivery: {
41 | type: 'object',
42 | title: 'Patch Digital Product Delivery Request',
43 | description: 'Choose how you would like you digital product delivered',
44 | properties: {
45 | external_url: {
46 | type: 'string',
47 | description: 'External URL to digital product',
48 | },
49 | files: {
50 | type: 'array',
51 | description: 'Uploaded files ids of digital product',
52 | items: {
53 | type: 'string',
54 | },
55 | },
56 | instructions: {
57 | type: 'string',
58 | description: 'Instructions to download and use the digital product',
59 | },
60 | },
61 | },
62 | image_id: {
63 | type: 'string',
64 | description: 'Product image id after its uploaded to S3',
65 | },
66 | license_key_activation_message: {
67 | type: 'string',
68 | description:
69 | 'Message sent to the customer upon license key activation.\n\nOnly applicable if `license_key_enabled` is `true`. This message contains instructions for\nactivating the license key.',
70 | },
71 | license_key_activations_limit: {
72 | type: 'integer',
73 | description:
74 | 'Limit for the number of activations for the license key.\n\nOnly applicable if `license_key_enabled` is `true`. Represents the maximum number of times\nthe license key can be activated.',
75 | },
76 | license_key_duration: {
77 | $ref: '#/$defs/license_key_duration',
78 | },
79 | license_key_enabled: {
80 | type: 'boolean',
81 | description:
82 | 'Whether the product requires a license key.\n\nIf `true`, additional fields related to license key (duration, activations limit, activation message)\nbecome applicable.',
83 | },
84 | metadata: {
85 | type: 'object',
86 | description: 'Additional metadata for the product',
87 | additionalProperties: true,
88 | },
89 | name: {
90 | type: 'string',
91 | description: 'Name of the product, optional and must be at most 100 characters.',
92 | },
93 | price: {
94 | $ref: '#/$defs/price',
95 | },
96 | tax_category: {
97 | $ref: '#/$defs/tax_category',
98 | },
99 | },
100 | required: ['id'],
101 | $defs: {
102 | license_key_duration: {
103 | type: 'object',
104 | title: 'License Key Duration',
105 | properties: {
106 | count: {
107 | type: 'integer',
108 | },
109 | interval: {
110 | $ref: '#/$defs/time_interval',
111 | },
112 | },
113 | required: ['count', 'interval'],
114 | },
115 | time_interval: {
116 | type: 'string',
117 | enum: ['Day', 'Week', 'Month', 'Year'],
118 | },
119 | price: {
120 | anyOf: [
121 | {
122 | type: 'object',
123 | title: 'One Time Price',
124 | description: 'One-time price details.',
125 | properties: {
126 | currency: {
127 | $ref: '#/$defs/currency',
128 | },
129 | discount: {
130 | type: 'integer',
131 | description: 'Discount applied to the price, represented as a percentage (0 to 100).',
132 | },
133 | price: {
134 | type: 'integer',
135 | description:
136 | 'The payment amount, in the smallest denomination of the currency (e.g., cents for USD).\nFor example, to charge $1.00, pass `100`.\n\nIf [`pay_what_you_want`](Self::pay_what_you_want) is set to `true`, this field represents\nthe **minimum** amount the customer must pay.',
137 | },
138 | purchasing_power_parity: {
139 | type: 'boolean',
140 | description:
141 | 'Indicates if purchasing power parity adjustments are applied to the price.\nPurchasing power parity feature is not available as of now.',
142 | },
143 | type: {
144 | type: 'string',
145 | enum: ['one_time_price'],
146 | },
147 | pay_what_you_want: {
148 | type: 'boolean',
149 | description:
150 | 'Indicates whether the customer can pay any amount they choose.\nIf set to `true`, the [`price`](Self::price) field is the minimum amount.',
151 | },
152 | suggested_price: {
153 | type: 'integer',
154 | description:
155 | 'A suggested price for the user to pay. This value is only considered if\n[`pay_what_you_want`](Self::pay_what_you_want) is `true`. Otherwise, it is ignored.',
156 | },
157 | tax_inclusive: {
158 | type: 'boolean',
159 | description: 'Indicates if the price is tax inclusive.',
160 | },
161 | },
162 | required: ['currency', 'discount', 'price', 'purchasing_power_parity', 'type'],
163 | },
164 | {
165 | type: 'object',
166 | title: 'Recurring Price',
167 | description: 'Recurring price details.',
168 | properties: {
169 | currency: {
170 | $ref: '#/$defs/currency',
171 | },
172 | discount: {
173 | type: 'integer',
174 | description: 'Discount applied to the price, represented as a percentage (0 to 100).',
175 | },
176 | payment_frequency_count: {
177 | type: 'integer',
178 | description:
179 | 'Number of units for the payment frequency.\nFor example, a value of `1` with a `payment_frequency_interval` of `month` represents monthly payments.',
180 | },
181 | payment_frequency_interval: {
182 | $ref: '#/$defs/time_interval',
183 | },
184 | price: {
185 | type: 'integer',
186 | description:
187 | 'The payment amount. Represented in the lowest denomination of the currency (e.g., cents for USD).\nFor example, to charge $1.00, pass `100`.',
188 | },
189 | purchasing_power_parity: {
190 | type: 'boolean',
191 | description:
192 | 'Indicates if purchasing power parity adjustments are applied to the price.\nPurchasing power parity feature is not available as of now',
193 | },
194 | subscription_period_count: {
195 | type: 'integer',
196 | description:
197 | 'Number of units for the subscription period.\nFor example, a value of `12` with a `subscription_period_interval` of `month` represents a one-year subscription.',
198 | },
199 | subscription_period_interval: {
200 | $ref: '#/$defs/time_interval',
201 | },
202 | type: {
203 | type: 'string',
204 | enum: ['recurring_price'],
205 | },
206 | tax_inclusive: {
207 | type: 'boolean',
208 | description: 'Indicates if the price is tax inclusive',
209 | },
210 | trial_period_days: {
211 | type: 'integer',
212 | description: 'Number of days for the trial period. A value of `0` indicates no trial period.',
213 | },
214 | },
215 | required: [
216 | 'currency',
217 | 'discount',
218 | 'payment_frequency_count',
219 | 'payment_frequency_interval',
220 | 'price',
221 | 'purchasing_power_parity',
222 | 'subscription_period_count',
223 | 'subscription_period_interval',
224 | 'type',
225 | ],
226 | },
227 | {
228 | type: 'object',
229 | title: 'Usage Based Price',
230 | description: 'Usage Based price details.',
231 | properties: {
232 | currency: {
233 | $ref: '#/$defs/currency',
234 | },
235 | discount: {
236 | type: 'integer',
237 | description: 'Discount applied to the price, represented as a percentage (0 to 100).',
238 | },
239 | fixed_price: {
240 | type: 'integer',
241 | description:
242 | 'The fixed payment amount. Represented in the lowest denomination of the currency (e.g., cents for USD).\nFor example, to charge $1.00, pass `100`.',
243 | },
244 | payment_frequency_count: {
245 | type: 'integer',
246 | description:
247 | 'Number of units for the payment frequency.\nFor example, a value of `1` with a `payment_frequency_interval` of `month` represents monthly payments.',
248 | },
249 | payment_frequency_interval: {
250 | $ref: '#/$defs/time_interval',
251 | },
252 | purchasing_power_parity: {
253 | type: 'boolean',
254 | description:
255 | 'Indicates if purchasing power parity adjustments are applied to the price.\nPurchasing power parity feature is not available as of now',
256 | },
257 | subscription_period_count: {
258 | type: 'integer',
259 | description:
260 | 'Number of units for the subscription period.\nFor example, a value of `12` with a `subscription_period_interval` of `month` represents a one-year subscription.',
261 | },
262 | subscription_period_interval: {
263 | $ref: '#/$defs/time_interval',
264 | },
265 | type: {
266 | type: 'string',
267 | enum: ['usage_based_price'],
268 | },
269 | meters: {
270 | type: 'array',
271 | items: {
272 | $ref: '#/$defs/add_meter_to_price',
273 | },
274 | },
275 | tax_inclusive: {
276 | type: 'boolean',
277 | description: 'Indicates if the price is tax inclusive',
278 | },
279 | },
280 | required: [
281 | 'currency',
282 | 'discount',
283 | 'fixed_price',
284 | 'payment_frequency_count',
285 | 'payment_frequency_interval',
286 | 'purchasing_power_parity',
287 | 'subscription_period_count',
288 | 'subscription_period_interval',
289 | 'type',
290 | ],
291 | },
292 | ],
293 | description: 'One-time price details.',
294 | },
295 | currency: {
296 | type: 'string',
297 | enum: [
298 | 'AED',
299 | 'ALL',
300 | 'AMD',
301 | 'ANG',
302 | 'AOA',
303 | 'ARS',
304 | 'AUD',
305 | 'AWG',
306 | 'AZN',
307 | 'BAM',
308 | 'BBD',
309 | 'BDT',
310 | 'BGN',
311 | 'BHD',
312 | 'BIF',
313 | 'BMD',
314 | 'BND',
315 | 'BOB',
316 | 'BRL',
317 | 'BSD',
318 | 'BWP',
319 | 'BYN',
320 | 'BZD',
321 | 'CAD',
322 | 'CHF',
323 | 'CLP',
324 | 'CNY',
325 | 'COP',
326 | 'CRC',
327 | 'CUP',
328 | 'CVE',
329 | 'CZK',
330 | 'DJF',
331 | 'DKK',
332 | 'DOP',
333 | 'DZD',
334 | 'EGP',
335 | 'ETB',
336 | 'EUR',
337 | 'FJD',
338 | 'FKP',
339 | 'GBP',
340 | 'GEL',
341 | 'GHS',
342 | 'GIP',
343 | 'GMD',
344 | 'GNF',
345 | 'GTQ',
346 | 'GYD',
347 | 'HKD',
348 | 'HNL',
349 | 'HRK',
350 | 'HTG',
351 | 'HUF',
352 | 'IDR',
353 | 'ILS',
354 | 'INR',
355 | 'IQD',
356 | 'JMD',
357 | 'JOD',
358 | 'JPY',
359 | 'KES',
360 | 'KGS',
361 | 'KHR',
362 | 'KMF',
363 | 'KRW',
364 | 'KWD',
365 | 'KYD',
366 | 'KZT',
367 | 'LAK',
368 | 'LBP',
369 | 'LKR',
370 | 'LRD',
371 | 'LSL',
372 | 'LYD',
373 | 'MAD',
374 | 'MDL',
375 | 'MGA',
376 | 'MKD',
377 | 'MMK',
378 | 'MNT',
379 | 'MOP',
380 | 'MRU',
381 | 'MUR',
382 | 'MVR',
383 | 'MWK',
384 | 'MXN',
385 | 'MYR',
386 | 'MZN',
387 | 'NAD',
388 | 'NGN',
389 | 'NIO',
390 | 'NOK',
391 | 'NPR',
392 | 'NZD',
393 | 'OMR',
394 | 'PAB',
395 | 'PEN',
396 | 'PGK',
397 | 'PHP',
398 | 'PKR',
399 | 'PLN',
400 | 'PYG',
401 | 'QAR',
402 | 'RON',
403 | 'RSD',
404 | 'RUB',
405 | 'RWF',
406 | 'SAR',
407 | 'SBD',
408 | 'SCR',
409 | 'SEK',
410 | 'SGD',
411 | 'SHP',
412 | 'SLE',
413 | 'SLL',
414 | 'SOS',
415 | 'SRD',
416 | 'SSP',
417 | 'STN',
418 | 'SVC',
419 | 'SZL',
420 | 'THB',
421 | 'TND',
422 | 'TOP',
423 | 'TRY',
424 | 'TTD',
425 | 'TWD',
426 | 'TZS',
427 | 'UAH',
428 | 'UGX',
429 | 'USD',
430 | 'UYU',
431 | 'UZS',
432 | 'VES',
433 | 'VND',
434 | 'VUV',
435 | 'WST',
436 | 'XAF',
437 | 'XCD',
438 | 'XOF',
439 | 'XPF',
440 | 'YER',
441 | 'ZAR',
442 | 'ZMW',
443 | ],
444 | },
445 | add_meter_to_price: {
446 | type: 'object',
447 | title: 'Add Meter To Price',
448 | properties: {
449 | meter_id: {
450 | type: 'string',
451 | },
452 | price_per_unit: {
453 | type: 'string',
454 | description:
455 | 'The price per unit in lowest denomination. Must be greater than zero. Supports up to 5 digits before decimal point and 12 decimal places.',
456 | },
457 | description: {
458 | type: 'string',
459 | description: 'Meter description. Will ignored on Request, but will be shown in response',
460 | },
461 | free_threshold: {
462 | type: 'integer',
463 | },
464 | measurement_unit: {
465 | type: 'string',
466 | description: 'Meter measurement unit. Will ignored on Request, but will be shown in response',
467 | },
468 | name: {
469 | type: 'string',
470 | description: 'Meter name. Will ignored on Request, but will be shown in response',
471 | },
472 | },
473 | required: ['meter_id', 'price_per_unit'],
474 | },
475 | tax_category: {
476 | type: 'string',
477 | description:
478 | 'Represents the different categories of taxation applicable to various products and services.',
479 | enum: ['digital_products', 'saas', 'e_book', 'edtech'],
480 | },
481 | },
482 | },
483 | annotations: {},
484 | };
485 |
486 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
487 | const { id, ...body } = args as any;
488 | const response = await client.products.update(id, body).asResponse();
489 | return asTextContentResult(await response.text());
490 | };
491 |
492 | export default { metadata, tool, handler };
493 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/tests/options.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { parseCLIOptions, parseQueryOptions } from '../src/options';
2 | import { Filter } from '../src/tools';
3 | import { parseEmbeddedJSON } from '../src/compat';
4 |
5 | // Mock process.argv
6 | const mockArgv = (args: string[]) => {
7 | const originalArgv = process.argv;
8 | process.argv = ['node', 'test.js', ...args];
9 | return () => {
10 | process.argv = originalArgv;
11 | };
12 | };
13 |
14 | describe('parseCLIOptions', () => {
15 | it('should parse basic filter options', () => {
16 | const cleanup = mockArgv([
17 | '--tool=test-tool',
18 | '--resource=test-resource',
19 | '--operation=read',
20 | '--tag=test-tag',
21 | ]);
22 |
23 | const result = parseCLIOptions();
24 |
25 | expect(result.filters).toEqual([
26 | { type: 'tag', op: 'include', value: 'test-tag' },
27 | { type: 'resource', op: 'include', value: 'test-resource' },
28 | { type: 'tool', op: 'include', value: 'test-tool' },
29 | { type: 'operation', op: 'include', value: 'read' },
30 | ] as Filter[]);
31 |
32 | expect(result.capabilities).toEqual({});
33 |
34 | expect(result.list).toBe(false);
35 |
36 | cleanup();
37 | });
38 |
39 | it('should parse exclusion filters', () => {
40 | const cleanup = mockArgv([
41 | '--no-tool=exclude-tool',
42 | '--no-resource=exclude-resource',
43 | '--no-operation=write',
44 | '--no-tag=exclude-tag',
45 | ]);
46 |
47 | const result = parseCLIOptions();
48 |
49 | expect(result.filters).toEqual([
50 | { type: 'tag', op: 'exclude', value: 'exclude-tag' },
51 | { type: 'resource', op: 'exclude', value: 'exclude-resource' },
52 | { type: 'tool', op: 'exclude', value: 'exclude-tool' },
53 | { type: 'operation', op: 'exclude', value: 'write' },
54 | ] as Filter[]);
55 |
56 | expect(result.capabilities).toEqual({});
57 |
58 | cleanup();
59 | });
60 |
61 | it('should parse client presets', () => {
62 | const cleanup = mockArgv(['--client=openai-agents']);
63 |
64 | const result = parseCLIOptions();
65 |
66 | expect(result.client).toEqual('openai-agents');
67 |
68 | cleanup();
69 | });
70 |
71 | it('should parse individual capabilities', () => {
72 | const cleanup = mockArgv([
73 | '--capability=top-level-unions',
74 | '--capability=valid-json',
75 | '--capability=refs',
76 | '--capability=unions',
77 | '--capability=tool-name-length=40',
78 | ]);
79 |
80 | const result = parseCLIOptions();
81 |
82 | expect(result.capabilities).toEqual({
83 | topLevelUnions: true,
84 | validJson: true,
85 | refs: true,
86 | unions: true,
87 | toolNameLength: 40,
88 | });
89 |
90 | cleanup();
91 | });
92 |
93 | it('should handle list option', () => {
94 | const cleanup = mockArgv(['--list']);
95 |
96 | const result = parseCLIOptions();
97 |
98 | expect(result.list).toBe(true);
99 |
100 | cleanup();
101 | });
102 |
103 | it('should handle multiple filters of the same type', () => {
104 | const cleanup = mockArgv(['--tool=tool1', '--tool=tool2', '--resource=res1', '--resource=res2']);
105 |
106 | const result = parseCLIOptions();
107 |
108 | expect(result.filters).toEqual([
109 | { type: 'resource', op: 'include', value: 'res1' },
110 | { type: 'resource', op: 'include', value: 'res2' },
111 | { type: 'tool', op: 'include', value: 'tool1' },
112 | { type: 'tool', op: 'include', value: 'tool2' },
113 | ] as Filter[]);
114 |
115 | cleanup();
116 | });
117 |
118 | it('should handle comma-separated values in array options', () => {
119 | const cleanup = mockArgv([
120 | '--tool=tool1,tool2',
121 | '--resource=res1,res2',
122 | '--capability=top-level-unions,valid-json,unions',
123 | ]);
124 |
125 | const result = parseCLIOptions();
126 |
127 | expect(result.filters).toEqual([
128 | { type: 'resource', op: 'include', value: 'res1' },
129 | { type: 'resource', op: 'include', value: 'res2' },
130 | { type: 'tool', op: 'include', value: 'tool1' },
131 | { type: 'tool', op: 'include', value: 'tool2' },
132 | ] as Filter[]);
133 |
134 | expect(result.capabilities).toEqual({
135 | topLevelUnions: true,
136 | validJson: true,
137 | unions: true,
138 | });
139 |
140 | cleanup();
141 | });
142 |
143 | it('should handle invalid tool-name-length format', () => {
144 | const cleanup = mockArgv(['--capability=tool-name-length=invalid']);
145 |
146 | // Mock console.error to prevent output during test
147 | const originalError = console.error;
148 | console.error = jest.fn();
149 |
150 | expect(() => parseCLIOptions()).toThrow();
151 |
152 | console.error = originalError;
153 | cleanup();
154 | });
155 |
156 | it('should handle unknown capability', () => {
157 | const cleanup = mockArgv(['--capability=unknown-capability']);
158 |
159 | // Mock console.error to prevent output during test
160 | const originalError = console.error;
161 | console.error = jest.fn();
162 |
163 | expect(() => parseCLIOptions()).toThrow();
164 |
165 | console.error = originalError;
166 | cleanup();
167 | });
168 | });
169 |
170 | describe('parseQueryOptions', () => {
171 | const defaultOptions = {
172 | client: undefined,
173 | includeDynamicTools: undefined,
174 | includeAllTools: undefined,
175 | filters: [],
176 | capabilities: {
177 | topLevelUnions: true,
178 | validJson: true,
179 | refs: true,
180 | unions: true,
181 | formats: true,
182 | toolNameLength: undefined,
183 | },
184 | };
185 |
186 | it('should parse basic filter options from query string', () => {
187 | const query = 'tool=test-tool&resource=test-resource&operation=read&tag=test-tag';
188 | const result = parseQueryOptions(defaultOptions, query);
189 |
190 | expect(result.filters).toEqual([
191 | { type: 'resource', op: 'include', value: 'test-resource' },
192 | { type: 'operation', op: 'include', value: 'read' },
193 | { type: 'tag', op: 'include', value: 'test-tag' },
194 | { type: 'tool', op: 'include', value: 'test-tool' },
195 | ]);
196 |
197 | expect(result.capabilities).toEqual({
198 | topLevelUnions: true,
199 | validJson: true,
200 | refs: true,
201 | unions: true,
202 | formats: true,
203 | toolNameLength: undefined,
204 | });
205 | });
206 |
207 | it('should parse exclusion filters from query string', () => {
208 | const query = 'no_tool=exclude-tool&no_resource=exclude-resource&no_operation=write&no_tag=exclude-tag';
209 | const result = parseQueryOptions(defaultOptions, query);
210 |
211 | expect(result.filters).toEqual([
212 | { type: 'resource', op: 'exclude', value: 'exclude-resource' },
213 | { type: 'operation', op: 'exclude', value: 'write' },
214 | { type: 'tag', op: 'exclude', value: 'exclude-tag' },
215 | { type: 'tool', op: 'exclude', value: 'exclude-tool' },
216 | ]);
217 | });
218 |
219 | it('should parse client option from query string', () => {
220 | const query = 'client=openai-agents';
221 | const result = parseQueryOptions(defaultOptions, query);
222 |
223 | expect(result.client).toBe('openai-agents');
224 | });
225 |
226 | it('should parse client capabilities from query string', () => {
227 | const query = 'capability=top-level-unions&capability=valid-json&capability=tool-name-length%3D40';
228 | const result = parseQueryOptions(defaultOptions, query);
229 |
230 | expect(result.capabilities).toEqual({
231 | topLevelUnions: true,
232 | validJson: true,
233 | refs: true,
234 | unions: true,
235 | formats: true,
236 | toolNameLength: 40,
237 | });
238 | });
239 |
240 | it('should parse no-capability options from query string', () => {
241 | const query = 'no_capability=top-level-unions&no_capability=refs&no_capability=formats';
242 | const result = parseQueryOptions(defaultOptions, query);
243 |
244 | expect(result.capabilities).toEqual({
245 | topLevelUnions: false,
246 | validJson: true,
247 | refs: false,
248 | unions: true,
249 | formats: false,
250 | toolNameLength: undefined,
251 | });
252 | });
253 |
254 | it('should parse tools options from query string', () => {
255 | const query = 'tools=dynamic&tools=all';
256 | const result = parseQueryOptions(defaultOptions, query);
257 |
258 | expect(result.includeDynamicTools).toBe(true);
259 | expect(result.includeAllTools).toBe(true);
260 | });
261 |
262 | it('should parse no-tools options from query string', () => {
263 | const query = 'tools=dynamic&tools=all&no_tools=dynamic';
264 | const result = parseQueryOptions(defaultOptions, query);
265 |
266 | expect(result.includeDynamicTools).toBe(false);
267 | expect(result.includeAllTools).toBe(true);
268 | });
269 |
270 | it('should handle array values in query string', () => {
271 | const query = 'tool[]=tool1&tool[]=tool2&resource[]=res1&resource[]=res2';
272 | const result = parseQueryOptions(defaultOptions, query);
273 |
274 | expect(result.filters).toEqual([
275 | { type: 'resource', op: 'include', value: 'res1' },
276 | { type: 'resource', op: 'include', value: 'res2' },
277 | { type: 'tool', op: 'include', value: 'tool1' },
278 | { type: 'tool', op: 'include', value: 'tool2' },
279 | ]);
280 | });
281 |
282 | it('should merge with default options', () => {
283 | const defaultWithFilters = {
284 | ...defaultOptions,
285 | filters: [{ type: 'tag' as const, op: 'include' as const, value: 'existing-tag' }],
286 | client: 'cursor' as const,
287 | includeDynamicTools: true,
288 | };
289 |
290 | const query = 'tool=new-tool&resource=new-resource';
291 | const result = parseQueryOptions(defaultWithFilters, query);
292 |
293 | expect(result.filters).toEqual([
294 | { type: 'tag', op: 'include', value: 'existing-tag' },
295 | { type: 'resource', op: 'include', value: 'new-resource' },
296 | { type: 'tool', op: 'include', value: 'new-tool' },
297 | ]);
298 |
299 | expect(result.client).toBe('cursor');
300 | expect(result.includeDynamicTools).toBe(true);
301 | });
302 |
303 | it('should override client from default options', () => {
304 | const defaultWithClient = {
305 | ...defaultOptions,
306 | client: 'cursor' as const,
307 | };
308 |
309 | const query = 'client=openai-agents';
310 | const result = parseQueryOptions(defaultWithClient, query);
311 |
312 | expect(result.client).toBe('openai-agents');
313 | });
314 |
315 | it('should merge capabilities with default options', () => {
316 | const defaultWithCapabilities = {
317 | ...defaultOptions,
318 | capabilities: {
319 | topLevelUnions: false,
320 | validJson: false,
321 | refs: true,
322 | unions: true,
323 | formats: true,
324 | toolNameLength: 30,
325 | },
326 | };
327 |
328 | const query = 'capability=top-level-unions&no_capability=refs';
329 | const result = parseQueryOptions(defaultWithCapabilities, query);
330 |
331 | expect(result.capabilities).toEqual({
332 | topLevelUnions: true,
333 | validJson: false,
334 | refs: false,
335 | unions: true,
336 | formats: true,
337 | toolNameLength: 30,
338 | });
339 | });
340 |
341 | it('should handle empty query string', () => {
342 | const query = '';
343 | const result = parseQueryOptions(defaultOptions, query);
344 |
345 | expect(result).toEqual(defaultOptions);
346 | });
347 |
348 | it('should handle invalid query string gracefully', () => {
349 | const query = 'invalid=value&operation=invalid-operation';
350 |
351 | // Should throw due to Zod validation for invalid operation
352 | expect(() => parseQueryOptions(defaultOptions, query)).toThrow();
353 | });
354 |
355 | it('should preserve default undefined values when not specified', () => {
356 | const defaultWithUndefined = {
357 | ...defaultOptions,
358 | client: undefined,
359 | includeDynamicTools: undefined,
360 | includeAllTools: undefined,
361 | };
362 |
363 | const query = 'tool=test-tool';
364 | const result = parseQueryOptions(defaultWithUndefined, query);
365 |
366 | expect(result.client).toBeUndefined();
367 | expect(result.includeDynamicTools).toBeFalsy();
368 | expect(result.includeAllTools).toBeFalsy();
369 | });
370 |
371 | it('should handle complex query with mixed include and exclude filters', () => {
372 | const query =
373 | 'tool=include-tool&no_tool=exclude-tool&resource=include-res&no_resource=exclude-res&operation=read&tag=include-tag&no_tag=exclude-tag';
374 | const result = parseQueryOptions(defaultOptions, query);
375 |
376 | expect(result.filters).toEqual([
377 | { type: 'resource', op: 'include', value: 'include-res' },
378 | { type: 'operation', op: 'include', value: 'read' },
379 | { type: 'tag', op: 'include', value: 'include-tag' },
380 | { type: 'tool', op: 'include', value: 'include-tool' },
381 | { type: 'resource', op: 'exclude', value: 'exclude-res' },
382 | { type: 'tag', op: 'exclude', value: 'exclude-tag' },
383 | { type: 'tool', op: 'exclude', value: 'exclude-tool' },
384 | ]);
385 | });
386 | });
387 |
388 | describe('parseEmbeddedJSON', () => {
389 | it('should not change non-string values', () => {
390 | const args = {
391 | numberProp: 42,
392 | booleanProp: true,
393 | objectProp: { nested: 'value' },
394 | arrayProp: [1, 2, 3],
395 | nullProp: null,
396 | undefinedProp: undefined,
397 | };
398 | const schema = {};
399 |
400 | const result = parseEmbeddedJSON(args, schema);
401 |
402 | expect(result).toBe(args); // Should return original object since no changes made
403 | expect(result['numberProp']).toBe(42);
404 | expect(result['booleanProp']).toBe(true);
405 | expect(result['objectProp']).toEqual({ nested: 'value' });
406 | expect(result['arrayProp']).toEqual([1, 2, 3]);
407 | expect(result['nullProp']).toBe(null);
408 | expect(result['undefinedProp']).toBe(undefined);
409 | });
410 |
411 | it('should parse valid JSON objects in string properties', () => {
412 | const args = {
413 | jsonObjectString: '{"key": "value", "number": 123}',
414 | regularString: 'not json',
415 | };
416 | const schema = {};
417 |
418 | const result = parseEmbeddedJSON(args, schema);
419 |
420 | expect(result).not.toBe(args); // Should return new object since changes were made
421 | expect(result['jsonObjectString']).toEqual({ key: 'value', number: 123 });
422 | expect(result['regularString']).toBe('not json');
423 | });
424 |
425 | it('should leave invalid JSON in string properties unchanged', () => {
426 | const args = {
427 | invalidJson1: '{"key": value}', // Missing quotes around value
428 | invalidJson2: '{key: "value"}', // Missing quotes around key
429 | invalidJson3: '{"key": "value",}', // Trailing comma
430 | invalidJson4: 'just a regular string',
431 | emptyString: '',
432 | };
433 | const schema = {};
434 |
435 | const result = parseEmbeddedJSON(args, schema);
436 |
437 | expect(result).toBe(args); // Should return original object since no changes made
438 | expect(result['invalidJson1']).toBe('{"key": value}');
439 | expect(result['invalidJson2']).toBe('{key: "value"}');
440 | expect(result['invalidJson3']).toBe('{"key": "value",}');
441 | expect(result['invalidJson4']).toBe('just a regular string');
442 | expect(result['emptyString']).toBe('');
443 | });
444 |
445 | it('should not parse JSON primitives in string properties', () => {
446 | const args = {
447 | numberString: '123',
448 | floatString: '45.67',
449 | negativeNumberString: '-89',
450 | booleanTrueString: 'true',
451 | booleanFalseString: 'false',
452 | nullString: 'null',
453 | jsonArrayString: '[1, 2, 3, "test"]',
454 | regularString: 'not json',
455 | };
456 | const schema = {};
457 |
458 | const result = parseEmbeddedJSON(args, schema);
459 |
460 | expect(result).toBe(args); // Should return original object since no changes made
461 | expect(result['numberString']).toBe('123');
462 | expect(result['floatString']).toBe('45.67');
463 | expect(result['negativeNumberString']).toBe('-89');
464 | expect(result['booleanTrueString']).toBe('true');
465 | expect(result['booleanFalseString']).toBe('false');
466 | expect(result['nullString']).toBe('null');
467 | expect(result['jsonArrayString']).toBe('[1, 2, 3, "test"]');
468 | expect(result['regularString']).toBe('not json');
469 | });
470 |
471 | it('should handle mixed valid objects and other JSON types', () => {
472 | const args = {
473 | validObject: '{"success": true}',
474 | invalidObject: '{"missing": quote}',
475 | validNumber: '42',
476 | validArray: '[1, 2, 3]',
477 | keepAsString: 'hello world',
478 | nonString: 123,
479 | };
480 | const schema = {};
481 |
482 | const result = parseEmbeddedJSON(args, schema);
483 |
484 | expect(result).not.toBe(args); // Should return new object since some changes were made
485 | expect(result['validObject']).toEqual({ success: true });
486 | expect(result['invalidObject']).toBe('{"missing": quote}');
487 | expect(result['validNumber']).toBe('42'); // Not parsed, remains string
488 | expect(result['validArray']).toBe('[1, 2, 3]'); // Not parsed, remains string
489 | expect(result['keepAsString']).toBe('hello world');
490 | expect(result['nonString']).toBe(123);
491 | });
492 |
493 | it('should return original object when no strings are present', () => {
494 | const args = {
495 | number: 42,
496 | boolean: true,
497 | object: { key: 'value' },
498 | };
499 | const schema = {};
500 |
501 | const result = parseEmbeddedJSON(args, schema);
502 |
503 | expect(result).toBe(args); // Should return original object since no changes made
504 | });
505 |
506 | it('should return original object when all strings are invalid JSON', () => {
507 | const args = {
508 | string1: 'hello',
509 | string2: 'world',
510 | string3: 'not json at all',
511 | };
512 | const schema = {};
513 |
514 | const result = parseEmbeddedJSON(args, schema);
515 |
516 | expect(result).toBe(args); // Should return original object since no changes made
517 | });
518 | });
519 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/checkout-sessions/create-checkout-sessions.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
4 |
5 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
6 | import DodoPayments from 'dodopayments';
7 |
8 | export const metadata: Metadata = {
9 | resource: 'checkout_sessions',
10 | operation: 'write',
11 | tags: [],
12 | httpMethod: 'post',
13 | httpPath: '/checkouts',
14 | operationId: 'create_session',
15 | };
16 |
17 | export const tool: Tool = {
18 | name: 'create_checkout_sessions',
19 | description: '',
20 | inputSchema: {
21 | type: 'object',
22 | properties: {
23 | product_cart: {
24 | type: 'array',
25 | items: {
26 | type: 'object',
27 | title: 'Product Item Request',
28 | properties: {
29 | product_id: {
30 | type: 'string',
31 | description: 'unique id of the product',
32 | },
33 | quantity: {
34 | type: 'integer',
35 | },
36 | addons: {
37 | type: 'array',
38 | description: 'only valid if product is a subscription',
39 | items: {
40 | $ref: '#/$defs/attach_addon',
41 | },
42 | },
43 | amount: {
44 | type: 'integer',
45 | description:
46 | 'Amount the customer pays if pay_what_you_want is enabled. If disabled then amount will be ignored\nRepresented in the lowest denomination of the currency (e.g., cents for USD).\nFor example, to charge $1.00, pass `100`.\nOnly applicable for one time payments\n\nIf amount is not set for pay_what_you_want product,\ncustomer is allowed to select the amount.',
47 | },
48 | },
49 | required: ['product_id', 'quantity'],
50 | },
51 | },
52 | allowed_payment_method_types: {
53 | type: 'array',
54 | description:
55 | "Customers will never see payment methods that are not in this list.\nHowever, adding a method here does not guarantee customers will see it.\nAvailability still depends on other factors (e.g., customer location, merchant settings).\n\nDisclaimar: Always provide 'credit' and 'debit' as a fallback.\nIf all payment methods are unavailable, checkout session will fail.",
56 | items: {
57 | $ref: '#/$defs/payment_method_types',
58 | },
59 | },
60 | billing_address: {
61 | type: 'object',
62 | title: 'Checkout Session Billing Address',
63 | description: 'Billing address information for the session',
64 | properties: {
65 | country: {
66 | $ref: '#/$defs/country_code',
67 | },
68 | city: {
69 | type: 'string',
70 | description: 'City name',
71 | },
72 | state: {
73 | type: 'string',
74 | description: 'State or province name',
75 | },
76 | street: {
77 | type: 'string',
78 | description: 'Street address including house number and unit/apartment if applicable',
79 | },
80 | zipcode: {
81 | type: 'string',
82 | description: 'Postal code or ZIP code',
83 | },
84 | },
85 | required: ['country'],
86 | },
87 | billing_currency: {
88 | $ref: '#/$defs/currency',
89 | },
90 | confirm: {
91 | type: 'boolean',
92 | description:
93 | 'If confirm is true, all the details will be finalized. If required data is missing, an API error is thrown.',
94 | },
95 | customer: {
96 | $ref: '#/$defs/customer_request',
97 | },
98 | customization: {
99 | type: 'object',
100 | title: 'Checkout Session Customization',
101 | description: 'Customization for the checkout session page',
102 | properties: {
103 | force_language: {
104 | type: 'string',
105 | description: 'Force the checkout interface to render in a specific language (e.g. `en`, `es`)',
106 | },
107 | show_on_demand_tag: {
108 | type: 'boolean',
109 | description: 'Show on demand tag\n\nDefault is true',
110 | },
111 | show_order_details: {
112 | type: 'boolean',
113 | description: 'Show order details by default\n\nDefault is true',
114 | },
115 | theme: {
116 | type: 'string',
117 | description: 'Theme of the page\n\nDefault is `System`.',
118 | enum: ['dark', 'light', 'system'],
119 | },
120 | },
121 | },
122 | discount_code: {
123 | type: 'string',
124 | },
125 | feature_flags: {
126 | type: 'object',
127 | title: 'Checkout Session Flags',
128 | properties: {
129 | allow_currency_selection: {
130 | type: 'boolean',
131 | description: 'if customer is allowed to change currency, set it to true\n\nDefault is true',
132 | },
133 | allow_discount_code: {
134 | type: 'boolean',
135 | description:
136 | 'If the customer is allowed to apply discount code, set it to true.\n\nDefault is true',
137 | },
138 | allow_phone_number_collection: {
139 | type: 'boolean',
140 | description: 'If phone number is collected from customer, set it to rue\n\nDefault is true',
141 | },
142 | allow_tax_id: {
143 | type: 'boolean',
144 | description: 'If the customer is allowed to add tax id, set it to true\n\nDefault is true',
145 | },
146 | always_create_new_customer: {
147 | type: 'boolean',
148 | description:
149 | 'Set to true if a new customer object should be created.\nBy default email is used to find an existing customer to attach the session to\n\nDefault is false',
150 | },
151 | },
152 | },
153 | force_3ds: {
154 | type: 'boolean',
155 | description: 'Override merchant default 3DS behaviour for this session',
156 | },
157 | metadata: {
158 | type: 'object',
159 | description: 'Additional metadata associated with the payment. Defaults to empty if not provided.',
160 | additionalProperties: true,
161 | },
162 | return_url: {
163 | type: 'string',
164 | description: 'The url to redirect after payment failure or success.',
165 | },
166 | show_saved_payment_methods: {
167 | type: 'boolean',
168 | description: 'Display saved payment methods of a returning customer False by default',
169 | },
170 | subscription_data: {
171 | type: 'object',
172 | title: 'Subscription Data',
173 | properties: {
174 | on_demand: {
175 | $ref: '#/$defs/on_demand_subscription',
176 | },
177 | trial_period_days: {
178 | type: 'integer',
179 | description:
180 | "Optional trial period in days If specified, this value overrides the trial period set in the product's price Must be between 0 and 10000 days",
181 | },
182 | },
183 | },
184 | },
185 | required: ['product_cart'],
186 | $defs: {
187 | attach_addon: {
188 | type: 'object',
189 | title: 'Attach Addon Request',
190 | properties: {
191 | addon_id: {
192 | type: 'string',
193 | },
194 | quantity: {
195 | type: 'integer',
196 | },
197 | },
198 | required: ['addon_id', 'quantity'],
199 | },
200 | payment_method_types: {
201 | type: 'string',
202 | enum: [
203 | 'credit',
204 | 'debit',
205 | 'upi_collect',
206 | 'upi_intent',
207 | 'apple_pay',
208 | 'cashapp',
209 | 'google_pay',
210 | 'multibanco',
211 | 'bancontact_card',
212 | 'eps',
213 | 'ideal',
214 | 'przelewy24',
215 | 'paypal',
216 | 'affirm',
217 | 'klarna',
218 | 'sepa',
219 | 'ach',
220 | 'amazon_pay',
221 | 'afterpay_clearpay',
222 | ],
223 | },
224 | country_code: {
225 | type: 'string',
226 | description: 'ISO country code alpha2 variant',
227 | enum: [
228 | 'AF',
229 | 'AX',
230 | 'AL',
231 | 'DZ',
232 | 'AS',
233 | 'AD',
234 | 'AO',
235 | 'AI',
236 | 'AQ',
237 | 'AG',
238 | 'AR',
239 | 'AM',
240 | 'AW',
241 | 'AU',
242 | 'AT',
243 | 'AZ',
244 | 'BS',
245 | 'BH',
246 | 'BD',
247 | 'BB',
248 | 'BY',
249 | 'BE',
250 | 'BZ',
251 | 'BJ',
252 | 'BM',
253 | 'BT',
254 | 'BO',
255 | 'BQ',
256 | 'BA',
257 | 'BW',
258 | 'BV',
259 | 'BR',
260 | 'IO',
261 | 'BN',
262 | 'BG',
263 | 'BF',
264 | 'BI',
265 | 'KH',
266 | 'CM',
267 | 'CA',
268 | 'CV',
269 | 'KY',
270 | 'CF',
271 | 'TD',
272 | 'CL',
273 | 'CN',
274 | 'CX',
275 | 'CC',
276 | 'CO',
277 | 'KM',
278 | 'CG',
279 | 'CD',
280 | 'CK',
281 | 'CR',
282 | 'CI',
283 | 'HR',
284 | 'CU',
285 | 'CW',
286 | 'CY',
287 | 'CZ',
288 | 'DK',
289 | 'DJ',
290 | 'DM',
291 | 'DO',
292 | 'EC',
293 | 'EG',
294 | 'SV',
295 | 'GQ',
296 | 'ER',
297 | 'EE',
298 | 'ET',
299 | 'FK',
300 | 'FO',
301 | 'FJ',
302 | 'FI',
303 | 'FR',
304 | 'GF',
305 | 'PF',
306 | 'TF',
307 | 'GA',
308 | 'GM',
309 | 'GE',
310 | 'DE',
311 | 'GH',
312 | 'GI',
313 | 'GR',
314 | 'GL',
315 | 'GD',
316 | 'GP',
317 | 'GU',
318 | 'GT',
319 | 'GG',
320 | 'GN',
321 | 'GW',
322 | 'GY',
323 | 'HT',
324 | 'HM',
325 | 'VA',
326 | 'HN',
327 | 'HK',
328 | 'HU',
329 | 'IS',
330 | 'IN',
331 | 'ID',
332 | 'IR',
333 | 'IQ',
334 | 'IE',
335 | 'IM',
336 | 'IL',
337 | 'IT',
338 | 'JM',
339 | 'JP',
340 | 'JE',
341 | 'JO',
342 | 'KZ',
343 | 'KE',
344 | 'KI',
345 | 'KP',
346 | 'KR',
347 | 'KW',
348 | 'KG',
349 | 'LA',
350 | 'LV',
351 | 'LB',
352 | 'LS',
353 | 'LR',
354 | 'LY',
355 | 'LI',
356 | 'LT',
357 | 'LU',
358 | 'MO',
359 | 'MK',
360 | 'MG',
361 | 'MW',
362 | 'MY',
363 | 'MV',
364 | 'ML',
365 | 'MT',
366 | 'MH',
367 | 'MQ',
368 | 'MR',
369 | 'MU',
370 | 'YT',
371 | 'MX',
372 | 'FM',
373 | 'MD',
374 | 'MC',
375 | 'MN',
376 | 'ME',
377 | 'MS',
378 | 'MA',
379 | 'MZ',
380 | 'MM',
381 | 'NA',
382 | 'NR',
383 | 'NP',
384 | 'NL',
385 | 'NC',
386 | 'NZ',
387 | 'NI',
388 | 'NE',
389 | 'NG',
390 | 'NU',
391 | 'NF',
392 | 'MP',
393 | 'NO',
394 | 'OM',
395 | 'PK',
396 | 'PW',
397 | 'PS',
398 | 'PA',
399 | 'PG',
400 | 'PY',
401 | 'PE',
402 | 'PH',
403 | 'PN',
404 | 'PL',
405 | 'PT',
406 | 'PR',
407 | 'QA',
408 | 'RE',
409 | 'RO',
410 | 'RU',
411 | 'RW',
412 | 'BL',
413 | 'SH',
414 | 'KN',
415 | 'LC',
416 | 'MF',
417 | 'PM',
418 | 'VC',
419 | 'WS',
420 | 'SM',
421 | 'ST',
422 | 'SA',
423 | 'SN',
424 | 'RS',
425 | 'SC',
426 | 'SL',
427 | 'SG',
428 | 'SX',
429 | 'SK',
430 | 'SI',
431 | 'SB',
432 | 'SO',
433 | 'ZA',
434 | 'GS',
435 | 'SS',
436 | 'ES',
437 | 'LK',
438 | 'SD',
439 | 'SR',
440 | 'SJ',
441 | 'SZ',
442 | 'SE',
443 | 'CH',
444 | 'SY',
445 | 'TW',
446 | 'TJ',
447 | 'TZ',
448 | 'TH',
449 | 'TL',
450 | 'TG',
451 | 'TK',
452 | 'TO',
453 | 'TT',
454 | 'TN',
455 | 'TR',
456 | 'TM',
457 | 'TC',
458 | 'TV',
459 | 'UG',
460 | 'UA',
461 | 'AE',
462 | 'GB',
463 | 'UM',
464 | 'US',
465 | 'UY',
466 | 'UZ',
467 | 'VU',
468 | 'VE',
469 | 'VN',
470 | 'VG',
471 | 'VI',
472 | 'WF',
473 | 'EH',
474 | 'YE',
475 | 'ZM',
476 | 'ZW',
477 | ],
478 | },
479 | currency: {
480 | type: 'string',
481 | enum: [
482 | 'AED',
483 | 'ALL',
484 | 'AMD',
485 | 'ANG',
486 | 'AOA',
487 | 'ARS',
488 | 'AUD',
489 | 'AWG',
490 | 'AZN',
491 | 'BAM',
492 | 'BBD',
493 | 'BDT',
494 | 'BGN',
495 | 'BHD',
496 | 'BIF',
497 | 'BMD',
498 | 'BND',
499 | 'BOB',
500 | 'BRL',
501 | 'BSD',
502 | 'BWP',
503 | 'BYN',
504 | 'BZD',
505 | 'CAD',
506 | 'CHF',
507 | 'CLP',
508 | 'CNY',
509 | 'COP',
510 | 'CRC',
511 | 'CUP',
512 | 'CVE',
513 | 'CZK',
514 | 'DJF',
515 | 'DKK',
516 | 'DOP',
517 | 'DZD',
518 | 'EGP',
519 | 'ETB',
520 | 'EUR',
521 | 'FJD',
522 | 'FKP',
523 | 'GBP',
524 | 'GEL',
525 | 'GHS',
526 | 'GIP',
527 | 'GMD',
528 | 'GNF',
529 | 'GTQ',
530 | 'GYD',
531 | 'HKD',
532 | 'HNL',
533 | 'HRK',
534 | 'HTG',
535 | 'HUF',
536 | 'IDR',
537 | 'ILS',
538 | 'INR',
539 | 'IQD',
540 | 'JMD',
541 | 'JOD',
542 | 'JPY',
543 | 'KES',
544 | 'KGS',
545 | 'KHR',
546 | 'KMF',
547 | 'KRW',
548 | 'KWD',
549 | 'KYD',
550 | 'KZT',
551 | 'LAK',
552 | 'LBP',
553 | 'LKR',
554 | 'LRD',
555 | 'LSL',
556 | 'LYD',
557 | 'MAD',
558 | 'MDL',
559 | 'MGA',
560 | 'MKD',
561 | 'MMK',
562 | 'MNT',
563 | 'MOP',
564 | 'MRU',
565 | 'MUR',
566 | 'MVR',
567 | 'MWK',
568 | 'MXN',
569 | 'MYR',
570 | 'MZN',
571 | 'NAD',
572 | 'NGN',
573 | 'NIO',
574 | 'NOK',
575 | 'NPR',
576 | 'NZD',
577 | 'OMR',
578 | 'PAB',
579 | 'PEN',
580 | 'PGK',
581 | 'PHP',
582 | 'PKR',
583 | 'PLN',
584 | 'PYG',
585 | 'QAR',
586 | 'RON',
587 | 'RSD',
588 | 'RUB',
589 | 'RWF',
590 | 'SAR',
591 | 'SBD',
592 | 'SCR',
593 | 'SEK',
594 | 'SGD',
595 | 'SHP',
596 | 'SLE',
597 | 'SLL',
598 | 'SOS',
599 | 'SRD',
600 | 'SSP',
601 | 'STN',
602 | 'SVC',
603 | 'SZL',
604 | 'THB',
605 | 'TND',
606 | 'TOP',
607 | 'TRY',
608 | 'TTD',
609 | 'TWD',
610 | 'TZS',
611 | 'UAH',
612 | 'UGX',
613 | 'USD',
614 | 'UYU',
615 | 'UZS',
616 | 'VES',
617 | 'VND',
618 | 'VUV',
619 | 'WST',
620 | 'XAF',
621 | 'XCD',
622 | 'XOF',
623 | 'XPF',
624 | 'YER',
625 | 'ZAR',
626 | 'ZMW',
627 | ],
628 | },
629 | customer_request: {
630 | anyOf: [
631 | {
632 | $ref: '#/$defs/attach_existing_customer',
633 | },
634 | {
635 | $ref: '#/$defs/new_customer',
636 | },
637 | ],
638 | title: 'Customer Request',
639 | },
640 | attach_existing_customer: {
641 | type: 'object',
642 | title: 'Attach Existing Customer',
643 | properties: {
644 | customer_id: {
645 | type: 'string',
646 | },
647 | },
648 | required: ['customer_id'],
649 | },
650 | new_customer: {
651 | type: 'object',
652 | title: 'New Customer',
653 | properties: {
654 | email: {
655 | type: 'string',
656 | description: 'Email is required for creating a new customer',
657 | },
658 | name: {
659 | type: 'string',
660 | description:
661 | 'Optional full name of the customer. If provided during session creation,\nit is persisted and becomes immutable for the session. If omitted here,\nit can be provided later via the confirm API.',
662 | },
663 | phone_number: {
664 | type: 'string',
665 | },
666 | },
667 | required: ['email'],
668 | },
669 | on_demand_subscription: {
670 | type: 'object',
671 | title: 'On Demand Subscription Request',
672 | properties: {
673 | mandate_only: {
674 | type: 'boolean',
675 | description:
676 | 'If set as True, does not perform any charge and only authorizes payment method details for future use.',
677 | },
678 | adaptive_currency_fees_inclusive: {
679 | type: 'boolean',
680 | description:
681 | 'Whether adaptive currency fees should be included in the product_price (true) or added on top (false).\nThis field is ignored if adaptive pricing is not enabled for the business.',
682 | },
683 | product_currency: {
684 | $ref: '#/$defs/currency',
685 | },
686 | product_description: {
687 | type: 'string',
688 | description:
689 | 'Optional product description override for billing and line items.\nIf not specified, the stored description of the product will be used.',
690 | },
691 | product_price: {
692 | type: 'integer',
693 | description:
694 | 'Product price for the initial charge to customer\nIf not specified the stored price of the product will be used\nRepresented in the lowest denomination of the currency (e.g., cents for USD).\nFor example, to charge $1.00, pass `100`.',
695 | },
696 | },
697 | required: ['mandate_only'],
698 | },
699 | },
700 | },
701 | annotations: {},
702 | };
703 |
704 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
705 | const body = args as any;
706 | return asTextContentResult(await client.checkoutSessions.create(body));
707 | };
708 |
709 | export default { metadata, tool, handler };
710 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/options.ts:
--------------------------------------------------------------------------------
```typescript
1 | import qs from 'qs';
2 | import yargs from 'yargs';
3 | import { hideBin } from 'yargs/helpers';
4 | import z from 'zod';
5 | import { endpoints, Filter } from './tools';
6 | import { ClientCapabilities, knownClients, ClientType } from './compat';
7 |
8 | export type CLIOptions = McpOptions & {
9 | list: boolean;
10 | transport: 'stdio' | 'http';
11 | port: number | undefined;
12 | socket: string | undefined;
13 | };
14 |
15 | export type McpOptions = {
16 | client?: ClientType | undefined;
17 | includeDynamicTools?: boolean | undefined;
18 | includeAllTools?: boolean | undefined;
19 | includeCodeTools?: boolean | undefined;
20 | includeDocsTools?: boolean | undefined;
21 | filters?: Filter[] | undefined;
22 | capabilities?: Partial<ClientCapabilities> | undefined;
23 | };
24 |
25 | const CAPABILITY_CHOICES = [
26 | 'top-level-unions',
27 | 'valid-json',
28 | 'refs',
29 | 'unions',
30 | 'formats',
31 | 'tool-name-length',
32 | ] as const;
33 |
34 | type Capability = (typeof CAPABILITY_CHOICES)[number];
35 |
36 | function parseCapabilityValue(cap: string): { name: Capability; value?: number } {
37 | if (cap.startsWith('tool-name-length=')) {
38 | const parts = cap.split('=');
39 | if (parts.length === 2) {
40 | const length = parseInt(parts[1]!, 10);
41 | if (!isNaN(length)) {
42 | return { name: 'tool-name-length', value: length };
43 | }
44 | throw new Error(`Invalid tool-name-length value: ${parts[1]}. Expected a number.`);
45 | }
46 | throw new Error(`Invalid format for tool-name-length. Expected tool-name-length=N.`);
47 | }
48 | if (!CAPABILITY_CHOICES.includes(cap as Capability)) {
49 | throw new Error(`Unknown capability: ${cap}. Valid capabilities are: ${CAPABILITY_CHOICES.join(', ')}`);
50 | }
51 | return { name: cap as Capability };
52 | }
53 |
54 | export function parseCLIOptions(): CLIOptions {
55 | const opts = yargs(hideBin(process.argv))
56 | .option('tools', {
57 | type: 'string',
58 | array: true,
59 | choices: ['dynamic', 'all', 'code', 'docs'],
60 | description: 'Use dynamic tools or all tools',
61 | })
62 | .option('no-tools', {
63 | type: 'string',
64 | array: true,
65 | choices: ['dynamic', 'all', 'code', 'docs'],
66 | description: 'Do not use any dynamic or all tools',
67 | })
68 | .option('tool', {
69 | type: 'string',
70 | array: true,
71 | description: 'Include tools matching the specified names',
72 | })
73 | .option('resource', {
74 | type: 'string',
75 | array: true,
76 | description: 'Include tools matching the specified resources',
77 | })
78 | .option('operation', {
79 | type: 'string',
80 | array: true,
81 | choices: ['read', 'write'],
82 | description: 'Include tools matching the specified operations',
83 | })
84 | .option('tag', {
85 | type: 'string',
86 | array: true,
87 | description: 'Include tools with the specified tags',
88 | })
89 | .option('no-tool', {
90 | type: 'string',
91 | array: true,
92 | description: 'Exclude tools matching the specified names',
93 | })
94 | .option('no-resource', {
95 | type: 'string',
96 | array: true,
97 | description: 'Exclude tools matching the specified resources',
98 | })
99 | .option('no-operation', {
100 | type: 'string',
101 | array: true,
102 | description: 'Exclude tools matching the specified operations',
103 | })
104 | .option('no-tag', {
105 | type: 'string',
106 | array: true,
107 | description: 'Exclude tools with the specified tags',
108 | })
109 | .option('list', {
110 | type: 'boolean',
111 | description: 'List all tools and exit',
112 | })
113 | .option('client', {
114 | type: 'string',
115 | choices: Object.keys(knownClients),
116 | description: 'Specify the MCP client being used',
117 | })
118 | .option('capability', {
119 | type: 'string',
120 | array: true,
121 | description: 'Specify client capabilities',
122 | coerce: (values: string[]) => {
123 | return values.flatMap((v) => v.split(','));
124 | },
125 | })
126 | .option('no-capability', {
127 | type: 'string',
128 | array: true,
129 | description: 'Unset client capabilities',
130 | choices: CAPABILITY_CHOICES,
131 | coerce: (values: string[]) => {
132 | return values.flatMap((v) => v.split(','));
133 | },
134 | })
135 | .option('describe-capabilities', {
136 | type: 'boolean',
137 | description: 'Print detailed explanation of client capabilities and exit',
138 | })
139 | .option('transport', {
140 | type: 'string',
141 | choices: ['stdio', 'http'],
142 | default: 'stdio',
143 | description: 'What transport to use; stdio for local servers or http for remote servers',
144 | })
145 | .option('port', {
146 | type: 'number',
147 | description: 'Port to serve on if using http transport',
148 | })
149 | .option('socket', {
150 | type: 'string',
151 | description: 'Unix socket to serve on if using http transport',
152 | })
153 | .help();
154 |
155 | for (const [command, desc] of examples()) {
156 | opts.example(command, desc);
157 | }
158 |
159 | const argv = opts.parseSync();
160 |
161 | // Handle describe-capabilities flag
162 | if (argv.describeCapabilities) {
163 | console.log(getCapabilitiesExplanation());
164 | process.exit(0);
165 | }
166 |
167 | const filters: Filter[] = [];
168 |
169 | // Helper function to support comma-separated values
170 | const splitValues = (values: string[] | undefined): string[] => {
171 | if (!values) return [];
172 | return values.flatMap((v) => v.split(','));
173 | };
174 |
175 | for (const tag of splitValues(argv.tag)) {
176 | filters.push({ type: 'tag', op: 'include', value: tag });
177 | }
178 |
179 | for (const tag of splitValues(argv.noTag)) {
180 | filters.push({ type: 'tag', op: 'exclude', value: tag });
181 | }
182 |
183 | for (const resource of splitValues(argv.resource)) {
184 | filters.push({ type: 'resource', op: 'include', value: resource });
185 | }
186 |
187 | for (const resource of splitValues(argv.noResource)) {
188 | filters.push({ type: 'resource', op: 'exclude', value: resource });
189 | }
190 |
191 | for (const tool of splitValues(argv.tool)) {
192 | filters.push({ type: 'tool', op: 'include', value: tool });
193 | }
194 |
195 | for (const tool of splitValues(argv.noTool)) {
196 | filters.push({ type: 'tool', op: 'exclude', value: tool });
197 | }
198 |
199 | for (const operation of splitValues(argv.operation)) {
200 | filters.push({ type: 'operation', op: 'include', value: operation });
201 | }
202 |
203 | for (const operation of splitValues(argv.noOperation)) {
204 | filters.push({ type: 'operation', op: 'exclude', value: operation });
205 | }
206 |
207 | // Parse client capabilities
208 | const clientCapabilities: Partial<ClientCapabilities> = {};
209 |
210 | // Apply individual capability overrides
211 | if (Array.isArray(argv.capability)) {
212 | for (const cap of argv.capability) {
213 | const parsedCap = parseCapabilityValue(cap);
214 | if (parsedCap.name === 'top-level-unions') {
215 | clientCapabilities.topLevelUnions = true;
216 | } else if (parsedCap.name === 'valid-json') {
217 | clientCapabilities.validJson = true;
218 | } else if (parsedCap.name === 'refs') {
219 | clientCapabilities.refs = true;
220 | } else if (parsedCap.name === 'unions') {
221 | clientCapabilities.unions = true;
222 | } else if (parsedCap.name === 'formats') {
223 | clientCapabilities.formats = true;
224 | } else if (parsedCap.name === 'tool-name-length') {
225 | clientCapabilities.toolNameLength = parsedCap.value;
226 | }
227 | }
228 | }
229 |
230 | // Handle no-capability options to unset capabilities
231 | if (Array.isArray(argv.noCapability)) {
232 | for (const cap of argv.noCapability) {
233 | if (cap === 'top-level-unions') {
234 | clientCapabilities.topLevelUnions = false;
235 | } else if (cap === 'valid-json') {
236 | clientCapabilities.validJson = false;
237 | } else if (cap === 'refs') {
238 | clientCapabilities.refs = false;
239 | } else if (cap === 'unions') {
240 | clientCapabilities.unions = false;
241 | } else if (cap === 'formats') {
242 | clientCapabilities.formats = false;
243 | } else if (cap === 'tool-name-length') {
244 | clientCapabilities.toolNameLength = undefined;
245 | }
246 | }
247 | }
248 |
249 | const shouldIncludeToolType = (toolType: 'dynamic' | 'all' | 'code' | 'docs') =>
250 | argv.noTools?.includes(toolType) ? false
251 | : argv.tools?.includes(toolType) ? true
252 | : undefined;
253 |
254 | const includeDynamicTools = shouldIncludeToolType('dynamic');
255 | const includeAllTools = shouldIncludeToolType('all');
256 | const includeCodeTools = shouldIncludeToolType('code');
257 | const includeDocsTools = shouldIncludeToolType('docs');
258 |
259 | const transport = argv.transport as 'stdio' | 'http';
260 |
261 | const client = argv.client as ClientType;
262 | return {
263 | client: client && client !== 'infer' && knownClients[client] ? client : undefined,
264 | includeDynamicTools,
265 | includeAllTools,
266 | includeCodeTools,
267 | includeDocsTools,
268 | filters,
269 | capabilities: clientCapabilities,
270 | list: argv.list || false,
271 | transport,
272 | port: argv.port,
273 | socket: argv.socket,
274 | };
275 | }
276 |
277 | const coerceArray = <T extends z.ZodTypeAny>(zodType: T) =>
278 | z.preprocess(
279 | (val) =>
280 | Array.isArray(val) ? val
281 | : val ? [val]
282 | : val,
283 | z.array(zodType).optional(),
284 | );
285 |
286 | const QueryOptions = z.object({
287 | tools: coerceArray(z.enum(['dynamic', 'all', 'docs'])).describe('Use dynamic tools or all tools'),
288 | no_tools: coerceArray(z.enum(['dynamic', 'all', 'docs'])).describe('Do not use dynamic tools or all tools'),
289 | tool: coerceArray(z.string()).describe('Include tools matching the specified names'),
290 | resource: coerceArray(z.string()).describe('Include tools matching the specified resources'),
291 | operation: coerceArray(z.enum(['read', 'write'])).describe(
292 | 'Include tools matching the specified operations',
293 | ),
294 | tag: coerceArray(z.string()).describe('Include tools with the specified tags'),
295 | no_tool: coerceArray(z.string()).describe('Exclude tools matching the specified names'),
296 | no_resource: coerceArray(z.string()).describe('Exclude tools matching the specified resources'),
297 | no_operation: coerceArray(z.enum(['read', 'write'])).describe(
298 | 'Exclude tools matching the specified operations',
299 | ),
300 | no_tag: coerceArray(z.string()).describe('Exclude tools with the specified tags'),
301 | client: ClientType.optional().describe('Specify the MCP client being used'),
302 | capability: coerceArray(z.string()).describe('Specify client capabilities'),
303 | no_capability: coerceArray(z.enum(CAPABILITY_CHOICES)).describe('Unset client capabilities'),
304 | });
305 |
306 | export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): McpOptions {
307 | const queryObject = typeof query === 'string' ? qs.parse(query) : query;
308 | const queryOptions = QueryOptions.parse(queryObject);
309 |
310 | const filters: Filter[] = [...(defaultOptions.filters ?? [])];
311 |
312 | for (const resource of queryOptions.resource || []) {
313 | filters.push({ type: 'resource', op: 'include', value: resource });
314 | }
315 | for (const operation of queryOptions.operation || []) {
316 | filters.push({ type: 'operation', op: 'include', value: operation });
317 | }
318 | for (const tag of queryOptions.tag || []) {
319 | filters.push({ type: 'tag', op: 'include', value: tag });
320 | }
321 | for (const tool of queryOptions.tool || []) {
322 | filters.push({ type: 'tool', op: 'include', value: tool });
323 | }
324 | for (const resource of queryOptions.no_resource || []) {
325 | filters.push({ type: 'resource', op: 'exclude', value: resource });
326 | }
327 | for (const operation of queryOptions.no_operation || []) {
328 | filters.push({ type: 'operation', op: 'exclude', value: operation });
329 | }
330 | for (const tag of queryOptions.no_tag || []) {
331 | filters.push({ type: 'tag', op: 'exclude', value: tag });
332 | }
333 | for (const tool of queryOptions.no_tool || []) {
334 | filters.push({ type: 'tool', op: 'exclude', value: tool });
335 | }
336 |
337 | // Parse client capabilities
338 | const clientCapabilities: Partial<ClientCapabilities> = { ...defaultOptions.capabilities };
339 |
340 | for (const cap of queryOptions.capability || []) {
341 | const parsed = parseCapabilityValue(cap);
342 | if (parsed.name === 'top-level-unions') {
343 | clientCapabilities.topLevelUnions = true;
344 | } else if (parsed.name === 'valid-json') {
345 | clientCapabilities.validJson = true;
346 | } else if (parsed.name === 'refs') {
347 | clientCapabilities.refs = true;
348 | } else if (parsed.name === 'unions') {
349 | clientCapabilities.unions = true;
350 | } else if (parsed.name === 'formats') {
351 | clientCapabilities.formats = true;
352 | } else if (parsed.name === 'tool-name-length') {
353 | clientCapabilities.toolNameLength = parsed.value;
354 | }
355 | }
356 |
357 | for (const cap of queryOptions.no_capability || []) {
358 | if (cap === 'top-level-unions') {
359 | clientCapabilities.topLevelUnions = false;
360 | } else if (cap === 'valid-json') {
361 | clientCapabilities.validJson = false;
362 | } else if (cap === 'refs') {
363 | clientCapabilities.refs = false;
364 | } else if (cap === 'unions') {
365 | clientCapabilities.unions = false;
366 | } else if (cap === 'formats') {
367 | clientCapabilities.formats = false;
368 | } else if (cap === 'tool-name-length') {
369 | clientCapabilities.toolNameLength = undefined;
370 | }
371 | }
372 |
373 | let dynamicTools: boolean | undefined =
374 | queryOptions.no_tools && queryOptions.no_tools?.includes('dynamic') ? false
375 | : queryOptions.tools?.includes('dynamic') ? true
376 | : defaultOptions.includeDynamicTools;
377 |
378 | let allTools: boolean | undefined =
379 | queryOptions.no_tools && queryOptions.no_tools?.includes('all') ? false
380 | : queryOptions.tools?.includes('all') ? true
381 | : defaultOptions.includeAllTools;
382 |
383 | let docsTools: boolean | undefined =
384 | queryOptions.no_tools && queryOptions.no_tools?.includes('docs') ? false
385 | : queryOptions.tools?.includes('docs') ? true
386 | : defaultOptions.includeDocsTools;
387 |
388 | return {
389 | client: queryOptions.client ?? defaultOptions.client,
390 | includeDynamicTools: dynamicTools,
391 | includeAllTools: allTools,
392 | includeCodeTools: undefined,
393 | includeDocsTools: docsTools,
394 | filters,
395 | capabilities: clientCapabilities,
396 | };
397 | }
398 |
399 | function getCapabilitiesExplanation(): string {
400 | return `
401 | Client Capabilities Explanation:
402 |
403 | Different Language Models (LLMs) and the MCP clients that use them have varying limitations in how they handle tool schemas. Capability flags allow you to inform the MCP server about these limitations.
404 |
405 | When a capability flag is set to false, the MCP server will automatically adjust the tool schemas to work around that limitation, ensuring broader compatibility.
406 |
407 | Available Capabilities:
408 |
409 | # top-level-unions
410 | Some clients/LLMs do not support JSON schemas with a union type (anyOf) at the root level. If a client lacks this capability, the MCP server splits tools with top-level unions into multiple separate tools, one for each variant in the union.
411 |
412 | # refs
413 | Some clients/LLMs do not support $ref pointers for schema reuse. If a client lacks this capability, the MCP server automatically inlines all references ($defs) directly into the schema. Properties that would cause circular references are removed during this process.
414 |
415 | # valid-json
416 | Some clients/LLMs may incorrectly send arguments as a JSON-encoded string instead of a proper JSON object. If a client *has* this capability, the MCP server will attempt to parse string values as JSON if the initial validation against the schema fails.
417 |
418 | # unions
419 | Some clients/LLMs do not support union types (anyOf) in JSON schemas. If a client lacks this capability, the MCP server removes all anyOf fields and uses only the first variant as the schema.
420 |
421 | # formats
422 | Some clients/LLMs do not support the 'format' keyword in JSON Schema specifications. If a client lacks this capability, the MCP server removes all format fields and appends the format information to the field's description in parentheses.
423 |
424 | # tool-name-length=N
425 | Some clients/LLMs impose a maximum length on tool names. If this capability is set, the MCP server will automatically truncate tool names exceeding the specified length (N), ensuring uniqueness by appending numbers if necessary.
426 |
427 | Client Presets (--client):
428 | Presets like '--client=openai-agents' or '--client=cursor' automatically configure these capabilities based on current known limitations of those clients, simplifying setup.
429 |
430 | Current presets:
431 | ${JSON.stringify(knownClients, null, 2)}
432 | `;
433 | }
434 |
435 | function examples(): [string, string][] {
436 | const firstEndpoint = endpoints[0]!;
437 | const secondEndpoint =
438 | endpoints.find((e) => e.metadata.resource !== firstEndpoint.metadata.resource) || endpoints[1];
439 | const tag = endpoints.find((e) => e.metadata.tags.length > 0)?.metadata.tags[0];
440 | const otherEndpoint = secondEndpoint || firstEndpoint;
441 |
442 | return [
443 | [
444 | `--tool="${firstEndpoint.tool.name}" ${secondEndpoint ? `--tool="${secondEndpoint.tool.name}"` : ''}`,
445 | 'Include tools by name',
446 | ],
447 | [
448 | `--resource="${firstEndpoint.metadata.resource}" --operation="read"`,
449 | 'Filter by resource and operation',
450 | ],
451 | [
452 | `--resource="${otherEndpoint.metadata.resource}*" --no-tool="${otherEndpoint.tool.name}"`,
453 | 'Use resource wildcards and exclusions',
454 | ],
455 | [`--client="cursor"`, 'Adjust schemas to be more compatible with Cursor'],
456 | [
457 | `--capability="top-level-unions" --capability="tool-name-length=40"`,
458 | 'Specify individual client capabilities',
459 | ],
460 | [
461 | `--client="cursor" --no-capability="tool-name-length"`,
462 | 'Use cursor client preset but remove tool name length limit',
463 | ],
464 | ...(tag ? [[`--tag="${tag}"`, 'Filter based on tags'] as [string, string]] : []),
465 | ];
466 | }
467 |
```
--------------------------------------------------------------------------------
/tests/path.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createPathTagFunction, encodeURIPath } from 'dodopayments/internal/utils/path';
2 | import { inspect } from 'node:util';
3 | import { runInNewContext } from 'node:vm';
4 |
5 | describe('path template tag function', () => {
6 | test('validates input', () => {
7 | const testParams = ['', '.', '..', 'x', '%2e', '%2E', '%2e%2e', '%2E%2e', '%2e%2E', '%2E%2E'];
8 | const testCases = [
9 | ['/path_params/', '/a'],
10 | ['/path_params/', '/'],
11 | ['/path_params/', ''],
12 | ['', '/a'],
13 | ['', '/'],
14 | ['', ''],
15 | ['a'],
16 | [''],
17 | ['/path_params/', ':initiate'],
18 | ['/path_params/', '.json'],
19 | ['/path_params/', '?beta=true'],
20 | ['/path_params/', '.?beta=true'],
21 | ['/path_params/', '/', '/download'],
22 | ['/path_params/', '-', '/download'],
23 | ['/path_params/', '', '/download'],
24 | ['/path_params/', '.', '/download'],
25 | ['/path_params/', '..', '/download'],
26 | ['/plain/path'],
27 | ];
28 |
29 | function paramPermutations(len: number): string[][] {
30 | if (len === 0) return [];
31 | if (len === 1) return testParams.map((e) => [e]);
32 | const rest = paramPermutations(len - 1);
33 | return testParams.flatMap((e) => rest.map((r) => [e, ...r]));
34 | }
35 |
36 | // We need to test how %2E is handled, so we use a custom encoder that does no escaping.
37 | const rawPath = createPathTagFunction((s) => s);
38 |
39 | const emptyObject = {};
40 | const mathObject = Math;
41 | const numberObject = new Number();
42 | const stringObject = new String();
43 | const basicClass = new (class {})();
44 | const classWithToString = new (class {
45 | toString() {
46 | return 'ok';
47 | }
48 | })();
49 |
50 | // Invalid values
51 | expect(() => rawPath`/a/${null}/b`).toThrow(
52 | 'Path parameters result in path with invalid segments:\n' +
53 | 'Value of type Null is not a valid path parameter\n' +
54 | '/a/null/b\n' +
55 | ' ^^^^',
56 | );
57 | expect(() => rawPath`/a/${undefined}/b`).toThrow(
58 | 'Path parameters result in path with invalid segments:\n' +
59 | 'Value of type Undefined is not a valid path parameter\n' +
60 | '/a/undefined/b\n' +
61 | ' ^^^^^^^^^',
62 | );
63 | expect(() => rawPath`/a/${emptyObject}/b`).toThrow(
64 | 'Path parameters result in path with invalid segments:\n' +
65 | 'Value of type Object is not a valid path parameter\n' +
66 | '/a/[object Object]/b\n' +
67 | ' ^^^^^^^^^^^^^^^',
68 | );
69 | expect(() => rawPath`?${mathObject}`).toThrow(
70 | 'Path parameters result in path with invalid segments:\n' +
71 | 'Value of type Math is not a valid path parameter\n' +
72 | '?[object Math]\n' +
73 | ' ^^^^^^^^^^^^^',
74 | );
75 | expect(() => rawPath`/${basicClass}`).toThrow(
76 | 'Path parameters result in path with invalid segments:\n' +
77 | 'Value of type Object is not a valid path parameter\n' +
78 | '/[object Object]\n' +
79 | ' ^^^^^^^^^^^^^^',
80 | );
81 | expect(() => rawPath`/../${''}`).toThrow(
82 | 'Path parameters result in path with invalid segments:\n' +
83 | 'Value ".." can\'t be safely passed as a path parameter\n' +
84 | '/../\n' +
85 | ' ^^',
86 | );
87 | expect(() => rawPath`/../${{}}`).toThrow(
88 | 'Path parameters result in path with invalid segments:\n' +
89 | 'Value ".." can\'t be safely passed as a path parameter\n' +
90 | 'Value of type Object is not a valid path parameter\n' +
91 | '/../[object Object]\n' +
92 | ' ^^ ^^^^^^^^^^^^^^',
93 | );
94 |
95 | // Valid values
96 | expect(rawPath`/${0}`).toBe('/0');
97 | expect(rawPath`/${''}`).toBe('/');
98 | expect(rawPath`/${numberObject}`).toBe('/0');
99 | expect(rawPath`${stringObject}/`).toBe('/');
100 | expect(rawPath`/${classWithToString}`).toBe('/ok');
101 |
102 | // We need to check what happens with cross-realm values, which we might get from
103 | // Jest or other frames in a browser.
104 |
105 | const newRealm = runInNewContext('globalThis');
106 | expect(newRealm.Object).not.toBe(Object);
107 |
108 | const crossRealmObject = newRealm.Object();
109 | const crossRealmMathObject = newRealm.Math;
110 | const crossRealmNumber = new newRealm.Number();
111 | const crossRealmString = new newRealm.String();
112 | const crossRealmClass = new (class extends newRealm.Object {})();
113 | const crossRealmClassWithToString = new (class extends newRealm.Object {
114 | toString() {
115 | return 'ok';
116 | }
117 | })();
118 |
119 | // Invalid cross-realm values
120 | expect(() => rawPath`/a/${crossRealmObject}/b`).toThrow(
121 | 'Path parameters result in path with invalid segments:\n' +
122 | 'Value of type Object is not a valid path parameter\n' +
123 | '/a/[object Object]/b\n' +
124 | ' ^^^^^^^^^^^^^^^',
125 | );
126 | expect(() => rawPath`?${crossRealmMathObject}`).toThrow(
127 | 'Path parameters result in path with invalid segments:\n' +
128 | 'Value of type Math is not a valid path parameter\n' +
129 | '?[object Math]\n' +
130 | ' ^^^^^^^^^^^^^',
131 | );
132 | expect(() => rawPath`/${crossRealmClass}`).toThrow(
133 | 'Path parameters result in path with invalid segments:\n' +
134 | 'Value of type Object is not a valid path parameter\n' +
135 | '/[object Object]\n' +
136 | ' ^^^^^^^^^^^^^^^',
137 | );
138 |
139 | // Valid cross-realm values
140 | expect(rawPath`/${crossRealmNumber}`).toBe('/0');
141 | expect(rawPath`${crossRealmString}/`).toBe('/');
142 | expect(rawPath`/${crossRealmClassWithToString}`).toBe('/ok');
143 |
144 | const results: {
145 | [pathParts: string]: {
146 | [params: string]: { valid: boolean; result?: string; error?: string };
147 | };
148 | } = {};
149 |
150 | for (const pathParts of testCases) {
151 | const pathResults: Record<string, { valid: boolean; result?: string; error?: string }> = {};
152 | results[JSON.stringify(pathParts)] = pathResults;
153 | for (const params of paramPermutations(pathParts.length - 1)) {
154 | const stringRaw = String.raw({ raw: pathParts }, ...params);
155 | const plainString = String.raw(
156 | { raw: pathParts.map((e) => e.replace(/\./g, 'x')) },
157 | ...params.map((e) => 'X'.repeat(e.length)),
158 | );
159 | const normalizedStringRaw = new URL(stringRaw, 'https://example.com').href;
160 | const normalizedPlainString = new URL(plainString, 'https://example.com').href;
161 | const pathResultsKey = JSON.stringify(params);
162 | try {
163 | const result = rawPath(pathParts, ...params);
164 | expect(result).toBe(stringRaw);
165 | // there are no special segments, so the length of the normalized path is
166 | // equal to the length of the normalized plain path.
167 | expect(normalizedStringRaw.length).toBe(normalizedPlainString.length);
168 | pathResults[pathResultsKey] = {
169 | valid: true,
170 | result,
171 | };
172 | } catch (e) {
173 | const error = String(e);
174 | expect(error).toMatch(/Path parameters result in path with invalid segment/);
175 | // there are special segments, so the length of the normalized path is
176 | // different than the length of the normalized plain path.
177 | expect(normalizedStringRaw.length).not.toBe(normalizedPlainString.length);
178 | pathResults[pathResultsKey] = {
179 | valid: false,
180 | error,
181 | };
182 | }
183 | }
184 | }
185 |
186 | expect(results).toMatchObject({
187 | '["/path_params/","/a"]': {
188 | '["x"]': { valid: true, result: '/path_params/x/a' },
189 | '[""]': { valid: true, result: '/path_params//a' },
190 | '["%2E%2e"]': {
191 | valid: false,
192 | error:
193 | 'Error: Path parameters result in path with invalid segments:\n' +
194 | 'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
195 | '/path_params/%2E%2e/a\n' +
196 | ' ^^^^^^',
197 | },
198 | '["%2E"]': {
199 | valid: false,
200 | error:
201 | 'Error: Path parameters result in path with invalid segments:\n' +
202 | 'Value "%2E" can\'t be safely passed as a path parameter\n' +
203 | '/path_params/%2E/a\n' +
204 | ' ^^^',
205 | },
206 | },
207 | '["/path_params/","/"]': {
208 | '["x"]': { valid: true, result: '/path_params/x/' },
209 | '[""]': { valid: true, result: '/path_params//' },
210 | '["%2e%2E"]': {
211 | valid: false,
212 | error:
213 | 'Error: Path parameters result in path with invalid segments:\n' +
214 | 'Value "%2e%2E" can\'t be safely passed as a path parameter\n' +
215 | '/path_params/%2e%2E/\n' +
216 | ' ^^^^^^',
217 | },
218 | '["%2e"]': {
219 | valid: false,
220 | error:
221 | 'Error: Path parameters result in path with invalid segments:\n' +
222 | 'Value "%2e" can\'t be safely passed as a path parameter\n' +
223 | '/path_params/%2e/\n' +
224 | ' ^^^',
225 | },
226 | },
227 | '["/path_params/",""]': {
228 | '[""]': { valid: true, result: '/path_params/' },
229 | '["x"]': { valid: true, result: '/path_params/x' },
230 | '["%2E"]': {
231 | valid: false,
232 | error:
233 | 'Error: Path parameters result in path with invalid segments:\n' +
234 | 'Value "%2E" can\'t be safely passed as a path parameter\n' +
235 | '/path_params/%2E\n' +
236 | ' ^^^',
237 | },
238 | '["%2E%2e"]': {
239 | valid: false,
240 | error:
241 | 'Error: Path parameters result in path with invalid segments:\n' +
242 | 'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
243 | '/path_params/%2E%2e\n' +
244 | ' ^^^^^^',
245 | },
246 | },
247 | '["","/a"]': {
248 | '[""]': { valid: true, result: '/a' },
249 | '["x"]': { valid: true, result: 'x/a' },
250 | '["%2E"]': {
251 | valid: false,
252 | error:
253 | 'Error: Path parameters result in path with invalid segments:\n' +
254 | 'Value "%2E" can\'t be safely passed as a path parameter\n%2E/a\n^^^',
255 | },
256 | '["%2e%2E"]': {
257 | valid: false,
258 | error:
259 | 'Error: Path parameters result in path with invalid segments:\n' +
260 | 'Value "%2e%2E" can\'t be safely passed as a path parameter\n' +
261 | '%2e%2E/a\n' +
262 | '^^^^^^',
263 | },
264 | },
265 | '["","/"]': {
266 | '["x"]': { valid: true, result: 'x/' },
267 | '[""]': { valid: true, result: '/' },
268 | '["%2E%2e"]': {
269 | valid: false,
270 | error:
271 | 'Error: Path parameters result in path with invalid segments:\n' +
272 | 'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
273 | '%2E%2e/\n' +
274 | '^^^^^^',
275 | },
276 | '["."]': {
277 | valid: false,
278 | error:
279 | 'Error: Path parameters result in path with invalid segments:\n' +
280 | 'Value "." can\'t be safely passed as a path parameter\n' +
281 | './\n^',
282 | },
283 | },
284 | '["",""]': {
285 | '[""]': { valid: true, result: '' },
286 | '["x"]': { valid: true, result: 'x' },
287 | '[".."]': {
288 | valid: false,
289 | error:
290 | 'Error: Path parameters result in path with invalid segments:\n' +
291 | 'Value ".." can\'t be safely passed as a path parameter\n' +
292 | '..\n^^',
293 | },
294 | '["."]': {
295 | valid: false,
296 | error:
297 | 'Error: Path parameters result in path with invalid segments:\n' +
298 | 'Value "." can\'t be safely passed as a path parameter\n' +
299 | '.\n^',
300 | },
301 | },
302 | '["a"]': {},
303 | '[""]': {},
304 | '["/path_params/",":initiate"]': {
305 | '[""]': { valid: true, result: '/path_params/:initiate' },
306 | '["."]': { valid: true, result: '/path_params/.:initiate' },
307 | },
308 | '["/path_params/",".json"]': {
309 | '["x"]': { valid: true, result: '/path_params/x.json' },
310 | '["."]': { valid: true, result: '/path_params/..json' },
311 | },
312 | '["/path_params/","?beta=true"]': {
313 | '["x"]': { valid: true, result: '/path_params/x?beta=true' },
314 | '[""]': { valid: true, result: '/path_params/?beta=true' },
315 | '["%2E%2E"]': {
316 | valid: false,
317 | error:
318 | 'Error: Path parameters result in path with invalid segments:\n' +
319 | 'Value "%2E%2E" can\'t be safely passed as a path parameter\n' +
320 | '/path_params/%2E%2E?beta=true\n' +
321 | ' ^^^^^^',
322 | },
323 | '["%2e%2E"]': {
324 | valid: false,
325 | error:
326 | 'Error: Path parameters result in path with invalid segments:\n' +
327 | 'Value "%2e%2E" can\'t be safely passed as a path parameter\n' +
328 | '/path_params/%2e%2E?beta=true\n' +
329 | ' ^^^^^^',
330 | },
331 | },
332 | '["/path_params/",".?beta=true"]': {
333 | '[".."]': { valid: true, result: '/path_params/...?beta=true' },
334 | '["x"]': { valid: true, result: '/path_params/x.?beta=true' },
335 | '[""]': {
336 | valid: false,
337 | error:
338 | 'Error: Path parameters result in path with invalid segments:\n' +
339 | 'Value "." can\'t be safely passed as a path parameter\n' +
340 | '/path_params/.?beta=true\n' +
341 | ' ^',
342 | },
343 | '["%2e"]': {
344 | valid: false,
345 | error:
346 | 'Error: Path parameters result in path with invalid segments:\n' +
347 | 'Value "%2e." can\'t be safely passed as a path parameter\n' +
348 | '/path_params/%2e.?beta=true\n' +
349 | ' ^^^^',
350 | },
351 | },
352 | '["/path_params/","/","/download"]': {
353 | '["",""]': { valid: true, result: '/path_params///download' },
354 | '["","x"]': { valid: true, result: '/path_params//x/download' },
355 | '[".","%2e"]': {
356 | valid: false,
357 | error:
358 | 'Error: Path parameters result in path with invalid segments:\n' +
359 | 'Value "." can\'t be safely passed as a path parameter\n' +
360 | 'Value "%2e" can\'t be safely passed as a path parameter\n' +
361 | '/path_params/./%2e/download\n' +
362 | ' ^ ^^^',
363 | },
364 | '["%2E%2e","%2e"]': {
365 | valid: false,
366 | error:
367 | 'Error: Path parameters result in path with invalid segments:\n' +
368 | 'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
369 | 'Value "%2e" can\'t be safely passed as a path parameter\n' +
370 | '/path_params/%2E%2e/%2e/download\n' +
371 | ' ^^^^^^ ^^^',
372 | },
373 | },
374 | '["/path_params/","-","/download"]': {
375 | '["","%2e"]': { valid: true, result: '/path_params/-%2e/download' },
376 | '["%2E",".."]': { valid: true, result: '/path_params/%2E-../download' },
377 | },
378 | '["/path_params/","","/download"]': {
379 | '["%2E%2e","%2e%2E"]': { valid: true, result: '/path_params/%2E%2e%2e%2E/download' },
380 | '["%2E",".."]': { valid: true, result: '/path_params/%2E../download' },
381 | '["","%2E"]': {
382 | valid: false,
383 | error:
384 | 'Error: Path parameters result in path with invalid segments:\n' +
385 | 'Value "%2E" can\'t be safely passed as a path parameter\n' +
386 | '/path_params/%2E/download\n' +
387 | ' ^^^',
388 | },
389 | '["%2E","."]': {
390 | valid: false,
391 | error:
392 | 'Error: Path parameters result in path with invalid segments:\n' +
393 | 'Value "%2E." can\'t be safely passed as a path parameter\n' +
394 | '/path_params/%2E./download\n' +
395 | ' ^^^^',
396 | },
397 | },
398 | '["/path_params/",".","/download"]': {
399 | '["%2e%2e",""]': { valid: true, result: '/path_params/%2e%2e./download' },
400 | '["","%2e%2e"]': { valid: true, result: '/path_params/.%2e%2e/download' },
401 | '["",""]': {
402 | valid: false,
403 | error:
404 | 'Error: Path parameters result in path with invalid segments:\n' +
405 | 'Value "." can\'t be safely passed as a path parameter\n' +
406 | '/path_params/./download\n' +
407 | ' ^',
408 | },
409 | '["","."]': {
410 | valid: false,
411 | error:
412 | 'Error: Path parameters result in path with invalid segments:\n' +
413 | 'Value ".." can\'t be safely passed as a path parameter\n' +
414 | '/path_params/../download\n' +
415 | ' ^^',
416 | },
417 | },
418 | '["/path_params/","..","/download"]': {
419 | '["","%2E"]': { valid: true, result: '/path_params/..%2E/download' },
420 | '["","x"]': { valid: true, result: '/path_params/..x/download' },
421 | '["",""]': {
422 | valid: false,
423 | error:
424 | 'Error: Path parameters result in path with invalid segments:\n' +
425 | 'Value ".." can\'t be safely passed as a path parameter\n' +
426 | '/path_params/../download\n' +
427 | ' ^^',
428 | },
429 | },
430 | });
431 | });
432 | });
433 |
434 | describe('encodeURIPath', () => {
435 | const testCases: string[] = [
436 | '',
437 | // Every ASCII character
438 | ...Array.from({ length: 0x7f }, (_, i) => String.fromCharCode(i)),
439 | // Unicode BMP codepoint
440 | 'å',
441 | // Unicode supplementary codepoint
442 | '😃',
443 | ];
444 |
445 | for (const param of testCases) {
446 | test('properly encodes ' + inspect(param), () => {
447 | const encoded = encodeURIPath(param);
448 | const naiveEncoded = encodeURIComponent(param);
449 | // we should never encode more characters than encodeURIComponent
450 | expect(naiveEncoded.length).toBeGreaterThanOrEqual(encoded.length);
451 | expect(decodeURIComponent(encoded)).toBe(param);
452 | });
453 | }
454 |
455 | test("leaves ':' intact", () => {
456 | expect(encodeURIPath(':')).toBe(':');
457 | });
458 |
459 | test("leaves '@' intact", () => {
460 | expect(encodeURIPath('@')).toBe('@');
461 | });
462 | });
463 |
```