This is page 5 of 8. Use http://codebase.md/dodopayments/dodopayments-node?lines=false&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
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/subscriptions/create-subscriptions.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'subscriptions',
operation: 'write',
tags: [],
httpMethod: 'post',
httpPath: '/subscriptions',
operationId: 'create_subscription_handler',
};
export const tool: Tool = {
name: 'create_subscriptions',
description: '',
inputSchema: {
type: 'object',
properties: {
billing: {
$ref: '#/$defs/billing_address',
},
customer: {
$ref: '#/$defs/customer_request',
},
product_id: {
type: 'string',
description: 'Unique identifier of the product to subscribe to',
},
quantity: {
type: 'integer',
description: 'Number of units to subscribe for. Must be at least 1.',
},
addons: {
type: 'array',
description: 'Attach addons to this subscription',
items: {
$ref: '#/$defs/attach_addon',
},
},
allowed_payment_method_types: {
type: 'array',
description:
'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).',
items: {
$ref: '#/$defs/payment_method_types',
},
},
billing_currency: {
$ref: '#/$defs/currency',
},
discount_code: {
type: 'string',
description: 'Discount Code to apply to the subscription',
},
force_3ds: {
type: 'boolean',
description: 'Override merchant default 3DS behaviour for this subscription',
},
metadata: {
type: 'object',
description: 'Additional metadata for the subscription\nDefaults to empty if not specified',
additionalProperties: true,
},
on_demand: {
$ref: '#/$defs/on_demand_subscription',
},
payment_link: {
type: 'boolean',
description: 'If true, generates a payment link.\nDefaults to false if not specified.',
},
return_url: {
type: 'string',
description: 'Optional URL to redirect after successful subscription creation',
},
show_saved_payment_methods: {
type: 'boolean',
description: 'Display saved payment methods of a returning customer\nFalse by default',
},
tax_id: {
type: 'string',
description:
'Tax ID in case the payment is B2B. If tax id validation fails the payment creation will fail',
},
trial_period_days: {
type: 'integer',
description:
"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",
},
},
required: ['billing', 'customer', 'product_id', 'quantity'],
$defs: {
billing_address: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'City name',
},
country: {
$ref: '#/$defs/country_code',
},
state: {
type: 'string',
description: 'State or province name',
},
street: {
type: 'string',
description: 'Street address including house number and unit/apartment if applicable',
},
zipcode: {
type: 'string',
description: 'Postal code or ZIP code',
},
},
required: ['city', 'country', 'state', 'street', 'zipcode'],
},
country_code: {
type: 'string',
description: 'ISO country code alpha2 variant',
enum: [
'AF',
'AX',
'AL',
'DZ',
'AS',
'AD',
'AO',
'AI',
'AQ',
'AG',
'AR',
'AM',
'AW',
'AU',
'AT',
'AZ',
'BS',
'BH',
'BD',
'BB',
'BY',
'BE',
'BZ',
'BJ',
'BM',
'BT',
'BO',
'BQ',
'BA',
'BW',
'BV',
'BR',
'IO',
'BN',
'BG',
'BF',
'BI',
'KH',
'CM',
'CA',
'CV',
'KY',
'CF',
'TD',
'CL',
'CN',
'CX',
'CC',
'CO',
'KM',
'CG',
'CD',
'CK',
'CR',
'CI',
'HR',
'CU',
'CW',
'CY',
'CZ',
'DK',
'DJ',
'DM',
'DO',
'EC',
'EG',
'SV',
'GQ',
'ER',
'EE',
'ET',
'FK',
'FO',
'FJ',
'FI',
'FR',
'GF',
'PF',
'TF',
'GA',
'GM',
'GE',
'DE',
'GH',
'GI',
'GR',
'GL',
'GD',
'GP',
'GU',
'GT',
'GG',
'GN',
'GW',
'GY',
'HT',
'HM',
'VA',
'HN',
'HK',
'HU',
'IS',
'IN',
'ID',
'IR',
'IQ',
'IE',
'IM',
'IL',
'IT',
'JM',
'JP',
'JE',
'JO',
'KZ',
'KE',
'KI',
'KP',
'KR',
'KW',
'KG',
'LA',
'LV',
'LB',
'LS',
'LR',
'LY',
'LI',
'LT',
'LU',
'MO',
'MK',
'MG',
'MW',
'MY',
'MV',
'ML',
'MT',
'MH',
'MQ',
'MR',
'MU',
'YT',
'MX',
'FM',
'MD',
'MC',
'MN',
'ME',
'MS',
'MA',
'MZ',
'MM',
'NA',
'NR',
'NP',
'NL',
'NC',
'NZ',
'NI',
'NE',
'NG',
'NU',
'NF',
'MP',
'NO',
'OM',
'PK',
'PW',
'PS',
'PA',
'PG',
'PY',
'PE',
'PH',
'PN',
'PL',
'PT',
'PR',
'QA',
'RE',
'RO',
'RU',
'RW',
'BL',
'SH',
'KN',
'LC',
'MF',
'PM',
'VC',
'WS',
'SM',
'ST',
'SA',
'SN',
'RS',
'SC',
'SL',
'SG',
'SX',
'SK',
'SI',
'SB',
'SO',
'ZA',
'GS',
'SS',
'ES',
'LK',
'SD',
'SR',
'SJ',
'SZ',
'SE',
'CH',
'SY',
'TW',
'TJ',
'TZ',
'TH',
'TL',
'TG',
'TK',
'TO',
'TT',
'TN',
'TR',
'TM',
'TC',
'TV',
'UG',
'UA',
'AE',
'GB',
'UM',
'US',
'UY',
'UZ',
'VU',
'VE',
'VN',
'VG',
'VI',
'WF',
'EH',
'YE',
'ZM',
'ZW',
],
},
customer_request: {
anyOf: [
{
$ref: '#/$defs/attach_existing_customer',
},
{
$ref: '#/$defs/new_customer',
},
],
title: 'Customer Request',
},
attach_existing_customer: {
type: 'object',
title: 'Attach Existing Customer',
properties: {
customer_id: {
type: 'string',
},
},
required: ['customer_id'],
},
new_customer: {
type: 'object',
title: 'New Customer',
properties: {
email: {
type: 'string',
description: 'Email is required for creating a new customer',
},
name: {
type: 'string',
description:
'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.',
},
phone_number: {
type: 'string',
},
},
required: ['email'],
},
attach_addon: {
type: 'object',
title: 'Attach Addon Request',
properties: {
addon_id: {
type: 'string',
},
quantity: {
type: 'integer',
},
},
required: ['addon_id', 'quantity'],
},
payment_method_types: {
type: 'string',
enum: [
'credit',
'debit',
'upi_collect',
'upi_intent',
'apple_pay',
'cashapp',
'google_pay',
'multibanco',
'bancontact_card',
'eps',
'ideal',
'przelewy24',
'paypal',
'affirm',
'klarna',
'sepa',
'ach',
'amazon_pay',
'afterpay_clearpay',
],
},
currency: {
type: 'string',
enum: [
'AED',
'ALL',
'AMD',
'ANG',
'AOA',
'ARS',
'AUD',
'AWG',
'AZN',
'BAM',
'BBD',
'BDT',
'BGN',
'BHD',
'BIF',
'BMD',
'BND',
'BOB',
'BRL',
'BSD',
'BWP',
'BYN',
'BZD',
'CAD',
'CHF',
'CLP',
'CNY',
'COP',
'CRC',
'CUP',
'CVE',
'CZK',
'DJF',
'DKK',
'DOP',
'DZD',
'EGP',
'ETB',
'EUR',
'FJD',
'FKP',
'GBP',
'GEL',
'GHS',
'GIP',
'GMD',
'GNF',
'GTQ',
'GYD',
'HKD',
'HNL',
'HRK',
'HTG',
'HUF',
'IDR',
'ILS',
'INR',
'IQD',
'JMD',
'JOD',
'JPY',
'KES',
'KGS',
'KHR',
'KMF',
'KRW',
'KWD',
'KYD',
'KZT',
'LAK',
'LBP',
'LKR',
'LRD',
'LSL',
'LYD',
'MAD',
'MDL',
'MGA',
'MKD',
'MMK',
'MNT',
'MOP',
'MRU',
'MUR',
'MVR',
'MWK',
'MXN',
'MYR',
'MZN',
'NAD',
'NGN',
'NIO',
'NOK',
'NPR',
'NZD',
'OMR',
'PAB',
'PEN',
'PGK',
'PHP',
'PKR',
'PLN',
'PYG',
'QAR',
'RON',
'RSD',
'RUB',
'RWF',
'SAR',
'SBD',
'SCR',
'SEK',
'SGD',
'SHP',
'SLE',
'SLL',
'SOS',
'SRD',
'SSP',
'STN',
'SVC',
'SZL',
'THB',
'TND',
'TOP',
'TRY',
'TTD',
'TWD',
'TZS',
'UAH',
'UGX',
'USD',
'UYU',
'UZS',
'VES',
'VND',
'VUV',
'WST',
'XAF',
'XCD',
'XOF',
'XPF',
'YER',
'ZAR',
'ZMW',
],
},
on_demand_subscription: {
type: 'object',
title: 'On Demand Subscription Request',
properties: {
mandate_only: {
type: 'boolean',
description:
'If set as True, does not perform any charge and only authorizes payment method details for future use.',
},
adaptive_currency_fees_inclusive: {
type: 'boolean',
description:
'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.',
},
product_currency: {
$ref: '#/$defs/currency',
},
product_description: {
type: 'string',
description:
'Optional product description override for billing and line items.\nIf not specified, the stored description of the product will be used.',
},
product_price: {
type: 'integer',
description:
'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`.',
},
},
required: ['mandate_only'],
},
},
},
annotations: {},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const body = args as any;
return asTextContentResult(await client.subscriptions.create(body));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/products/create-products.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'products',
operation: 'write',
tags: [],
httpMethod: 'post',
httpPath: '/products',
operationId: 'create_product',
};
export const tool: Tool = {
name: 'create_products',
description: '',
inputSchema: {
type: 'object',
properties: {
price: {
$ref: '#/$defs/price',
},
tax_category: {
$ref: '#/$defs/tax_category',
},
addons: {
type: 'array',
description: 'Addons available for subscription product',
items: {
type: 'string',
},
},
brand_id: {
type: 'string',
description: 'Brand id for the product, if not provided will default to primary brand',
},
description: {
type: 'string',
description: 'Optional description of the product',
},
digital_product_delivery: {
type: 'object',
title: 'Create Digital Product Delivery Request',
description: 'Choose how you would like you digital product delivered',
properties: {
external_url: {
type: 'string',
description: 'External URL to digital product',
},
instructions: {
type: 'string',
description: 'Instructions to download and use the digital product',
},
},
},
license_key_activation_message: {
type: 'string',
description: 'Optional message displayed during license key activation',
},
license_key_activations_limit: {
type: 'integer',
description: 'The number of times the license key can be activated.\nMust be 0 or greater',
},
license_key_duration: {
$ref: '#/$defs/license_key_duration',
},
license_key_enabled: {
type: 'boolean',
description: 'When true, generates and sends a license key to your customer.\nDefaults to false',
},
metadata: {
type: 'object',
description: 'Additional metadata for the product',
additionalProperties: true,
},
name: {
type: 'string',
description: 'Optional name of the product',
},
},
required: ['price', 'tax_category'],
$defs: {
price: {
anyOf: [
{
type: 'object',
title: 'One Time Price',
description: 'One-time price details.',
properties: {
currency: {
$ref: '#/$defs/currency',
},
discount: {
type: 'integer',
description: 'Discount applied to the price, represented as a percentage (0 to 100).',
},
price: {
type: 'integer',
description:
'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.',
},
purchasing_power_parity: {
type: 'boolean',
description:
'Indicates if purchasing power parity adjustments are applied to the price.\nPurchasing power parity feature is not available as of now.',
},
type: {
type: 'string',
enum: ['one_time_price'],
},
pay_what_you_want: {
type: 'boolean',
description:
'Indicates whether the customer can pay any amount they choose.\nIf set to `true`, the [`price`](Self::price) field is the minimum amount.',
},
suggested_price: {
type: 'integer',
description:
'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.',
},
tax_inclusive: {
type: 'boolean',
description: 'Indicates if the price is tax inclusive.',
},
},
required: ['currency', 'discount', 'price', 'purchasing_power_parity', 'type'],
},
{
type: 'object',
title: 'Recurring Price',
description: 'Recurring price details.',
properties: {
currency: {
$ref: '#/$defs/currency',
},
discount: {
type: 'integer',
description: 'Discount applied to the price, represented as a percentage (0 to 100).',
},
payment_frequency_count: {
type: 'integer',
description:
'Number of units for the payment frequency.\nFor example, a value of `1` with a `payment_frequency_interval` of `month` represents monthly payments.',
},
payment_frequency_interval: {
$ref: '#/$defs/time_interval',
},
price: {
type: 'integer',
description:
'The payment amount. Represented in the lowest denomination of the currency (e.g., cents for USD).\nFor example, to charge $1.00, pass `100`.',
},
purchasing_power_parity: {
type: 'boolean',
description:
'Indicates if purchasing power parity adjustments are applied to the price.\nPurchasing power parity feature is not available as of now',
},
subscription_period_count: {
type: 'integer',
description:
'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.',
},
subscription_period_interval: {
$ref: '#/$defs/time_interval',
},
type: {
type: 'string',
enum: ['recurring_price'],
},
tax_inclusive: {
type: 'boolean',
description: 'Indicates if the price is tax inclusive',
},
trial_period_days: {
type: 'integer',
description: 'Number of days for the trial period. A value of `0` indicates no trial period.',
},
},
required: [
'currency',
'discount',
'payment_frequency_count',
'payment_frequency_interval',
'price',
'purchasing_power_parity',
'subscription_period_count',
'subscription_period_interval',
'type',
],
},
{
type: 'object',
title: 'Usage Based Price',
description: 'Usage Based price details.',
properties: {
currency: {
$ref: '#/$defs/currency',
},
discount: {
type: 'integer',
description: 'Discount applied to the price, represented as a percentage (0 to 100).',
},
fixed_price: {
type: 'integer',
description:
'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`.',
},
payment_frequency_count: {
type: 'integer',
description:
'Number of units for the payment frequency.\nFor example, a value of `1` with a `payment_frequency_interval` of `month` represents monthly payments.',
},
payment_frequency_interval: {
$ref: '#/$defs/time_interval',
},
purchasing_power_parity: {
type: 'boolean',
description:
'Indicates if purchasing power parity adjustments are applied to the price.\nPurchasing power parity feature is not available as of now',
},
subscription_period_count: {
type: 'integer',
description:
'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.',
},
subscription_period_interval: {
$ref: '#/$defs/time_interval',
},
type: {
type: 'string',
enum: ['usage_based_price'],
},
meters: {
type: 'array',
items: {
$ref: '#/$defs/add_meter_to_price',
},
},
tax_inclusive: {
type: 'boolean',
description: 'Indicates if the price is tax inclusive',
},
},
required: [
'currency',
'discount',
'fixed_price',
'payment_frequency_count',
'payment_frequency_interval',
'purchasing_power_parity',
'subscription_period_count',
'subscription_period_interval',
'type',
],
},
],
description: 'One-time price details.',
},
currency: {
type: 'string',
enum: [
'AED',
'ALL',
'AMD',
'ANG',
'AOA',
'ARS',
'AUD',
'AWG',
'AZN',
'BAM',
'BBD',
'BDT',
'BGN',
'BHD',
'BIF',
'BMD',
'BND',
'BOB',
'BRL',
'BSD',
'BWP',
'BYN',
'BZD',
'CAD',
'CHF',
'CLP',
'CNY',
'COP',
'CRC',
'CUP',
'CVE',
'CZK',
'DJF',
'DKK',
'DOP',
'DZD',
'EGP',
'ETB',
'EUR',
'FJD',
'FKP',
'GBP',
'GEL',
'GHS',
'GIP',
'GMD',
'GNF',
'GTQ',
'GYD',
'HKD',
'HNL',
'HRK',
'HTG',
'HUF',
'IDR',
'ILS',
'INR',
'IQD',
'JMD',
'JOD',
'JPY',
'KES',
'KGS',
'KHR',
'KMF',
'KRW',
'KWD',
'KYD',
'KZT',
'LAK',
'LBP',
'LKR',
'LRD',
'LSL',
'LYD',
'MAD',
'MDL',
'MGA',
'MKD',
'MMK',
'MNT',
'MOP',
'MRU',
'MUR',
'MVR',
'MWK',
'MXN',
'MYR',
'MZN',
'NAD',
'NGN',
'NIO',
'NOK',
'NPR',
'NZD',
'OMR',
'PAB',
'PEN',
'PGK',
'PHP',
'PKR',
'PLN',
'PYG',
'QAR',
'RON',
'RSD',
'RUB',
'RWF',
'SAR',
'SBD',
'SCR',
'SEK',
'SGD',
'SHP',
'SLE',
'SLL',
'SOS',
'SRD',
'SSP',
'STN',
'SVC',
'SZL',
'THB',
'TND',
'TOP',
'TRY',
'TTD',
'TWD',
'TZS',
'UAH',
'UGX',
'USD',
'UYU',
'UZS',
'VES',
'VND',
'VUV',
'WST',
'XAF',
'XCD',
'XOF',
'XPF',
'YER',
'ZAR',
'ZMW',
],
},
time_interval: {
type: 'string',
enum: ['Day', 'Week', 'Month', 'Year'],
},
add_meter_to_price: {
type: 'object',
title: 'Add Meter To Price',
properties: {
meter_id: {
type: 'string',
},
price_per_unit: {
type: 'string',
description:
'The price per unit in lowest denomination. Must be greater than zero. Supports up to 5 digits before decimal point and 12 decimal places.',
},
description: {
type: 'string',
description: 'Meter description. Will ignored on Request, but will be shown in response',
},
free_threshold: {
type: 'integer',
},
measurement_unit: {
type: 'string',
description: 'Meter measurement unit. Will ignored on Request, but will be shown in response',
},
name: {
type: 'string',
description: 'Meter name. Will ignored on Request, but will be shown in response',
},
},
required: ['meter_id', 'price_per_unit'],
},
tax_category: {
type: 'string',
description:
'Represents the different categories of taxation applicable to various products and services.',
enum: ['digital_products', 'saas', 'e_book', 'edtech'],
},
license_key_duration: {
type: 'object',
title: 'License Key Duration',
properties: {
count: {
type: 'integer',
},
interval: {
$ref: '#/$defs/time_interval',
},
},
required: ['count', 'interval'],
},
},
},
annotations: {},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const body = args as any;
return asTextContentResult(await client.products.create(body));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/meters/create-meters.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'meters',
operation: 'write',
tags: [],
httpMethod: 'post',
httpPath: '/meters',
operationId: 'create_meter',
};
export const tool: Tool = {
name: 'create_meters',
description: '',
inputSchema: {
type: 'object',
properties: {
aggregation: {
$ref: '#/$defs/meter_aggregation',
},
event_name: {
type: 'string',
description: 'Event name to track',
},
measurement_unit: {
type: 'string',
description: 'measurement unit',
},
name: {
type: 'string',
description: 'Name of the meter',
},
description: {
type: 'string',
description: 'Optional description of the meter',
},
filter: {
$ref: '#/$defs/meter_filter',
},
},
required: ['aggregation', 'event_name', 'measurement_unit', 'name'],
$defs: {
meter_aggregation: {
type: 'object',
title: 'Meter Aggregation',
properties: {
type: {
type: 'string',
description: 'Aggregation type for the meter',
enum: ['count', 'sum', 'max', 'last'],
},
key: {
type: 'string',
description: 'Required when type is not COUNT',
},
},
required: ['type'],
},
meter_filter: {
type: 'object',
title: 'Meter Filter',
description:
'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.',
properties: {
clauses: {
anyOf: [
{
type: 'array',
title: 'Direct Filter Conditions',
description:
'Direct filter conditions - array of condition objects with key, operator, and value',
items: {
type: 'object',
title: 'MeterFilterCondition',
description: 'Filter condition with key, operator, and value',
properties: {
key: {
type: 'string',
description: 'Filter key to apply',
},
operator: {
type: 'string',
enum: [
'equals',
'not_equals',
'greater_than',
'greater_than_or_equals',
'less_than',
'less_than_or_equals',
'contains',
'does_not_contain',
],
},
value: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
type: 'boolean',
},
],
title: 'Filter Value',
description: 'Filter value - can be string, number, or boolean',
},
},
required: ['key', 'operator', 'value'],
},
},
{
type: 'array',
title: 'Nested Meter Filters',
description: 'Nested filters - supports up to 3 levels deep',
items: {
type: 'object',
title: 'MeterFilter',
description: 'Level 1 nested filter - can contain Level 2 filters',
properties: {
clauses: {
anyOf: [
{
type: 'array',
title: 'Level 1 Filter Conditions',
description: 'Array of filter conditions',
items: {
type: 'object',
title: 'MeterFilterCondition',
description: 'Filter condition with key, operator, and value',
properties: {
key: {
type: 'string',
description: 'Filter key to apply',
},
operator: {
type: 'string',
enum: [
'equals',
'not_equals',
'greater_than',
'greater_than_or_equals',
'less_than',
'less_than_or_equals',
'contains',
'does_not_contain',
],
},
value: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
type: 'boolean',
},
],
title: 'Filter Value',
description: 'Filter value - can be string, number, or boolean',
},
},
required: ['key', 'operator', 'value'],
},
},
{
type: 'array',
title: 'Level 1 Nested Filters',
description: 'Array of level 2 nested filters',
items: {
type: 'object',
title: 'MeterFilter',
description: 'Level 2 nested filter',
properties: {
clauses: {
anyOf: [
{
type: 'array',
title: 'Level 2 Filter Conditions',
description: 'Array of filter conditions',
items: {
type: 'object',
title: 'MeterFilterCondition',
description: 'Filter condition with key, operator, and value',
properties: {
key: {
type: 'string',
description: 'Filter key to apply',
},
operator: {
type: 'string',
enum: [
'equals',
'not_equals',
'greater_than',
'greater_than_or_equals',
'less_than',
'less_than_or_equals',
'contains',
'does_not_contain',
],
},
value: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
type: 'boolean',
},
],
title: 'Filter Value',
description: 'Filter value - can be string, number, or boolean',
},
},
required: ['key', 'operator', 'value'],
},
},
{
type: 'array',
title: 'Level 2 Nested Filters',
description: 'Array of level 3 nested filters (final level)',
items: {
type: 'object',
title: 'MeterFilter',
description: 'Level 3 nested filter (final nesting level)',
properties: {
clauses: {
type: 'array',
title: 'Level 3 Filter Conditions',
description: 'Level 3: Filter conditions only (max depth reached)',
items: {
type: 'object',
title: 'MeterFilterCondition',
description: 'Filter condition with key, operator, and value',
properties: {
key: {
type: 'string',
description: 'Filter key to apply',
},
operator: {
type: 'string',
enum: [
'equals',
'not_equals',
'greater_than',
'greater_than_or_equals',
'less_than',
'less_than_or_equals',
'contains',
'does_not_contain',
],
},
value: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
{
type: 'boolean',
},
],
title: 'Filter Value',
description:
'Filter value - can be string, number, or boolean',
},
},
required: ['key', 'operator', 'value'],
},
},
conjunction: {
type: 'string',
enum: ['and', 'or'],
},
},
required: ['clauses', 'conjunction'],
},
},
],
title: 'Level 2 Clause',
description:
'Level 2: Can be conditions or nested filters (1 more level allowed)',
},
conjunction: {
type: 'string',
enum: ['and', 'or'],
},
},
required: ['clauses', 'conjunction'],
},
},
],
title: 'Level 1 Clause',
description: 'Level 1: Can be conditions or nested filters (2 more levels allowed)',
},
conjunction: {
type: 'string',
enum: ['and', 'or'],
},
},
required: ['clauses', 'conjunction'],
},
},
],
title: 'FilterType',
description: 'Filter clauses - can be direct conditions or nested filters (up to 3 levels deep)',
},
conjunction: {
type: 'string',
description: 'Logical conjunction to apply between clauses (and/or)',
enum: ['and', 'or'],
},
},
required: ['clauses', 'conjunction'],
},
},
},
annotations: {},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const body = args as any;
return asTextContentResult(await client.meters.create(body));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/products/update-products.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'products',
operation: 'write',
tags: [],
httpMethod: 'patch',
httpPath: '/products/{id}',
operationId: 'patch_product',
};
export const tool: Tool = {
name: 'update_products',
description: '',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
},
addons: {
type: 'array',
description: 'Available Addons for subscription products',
items: {
type: 'string',
},
},
brand_id: {
type: 'string',
},
description: {
type: 'string',
description: 'Description of the product, optional and must be at most 1000 characters.',
},
digital_product_delivery: {
type: 'object',
title: 'Patch Digital Product Delivery Request',
description: 'Choose how you would like you digital product delivered',
properties: {
external_url: {
type: 'string',
description: 'External URL to digital product',
},
files: {
type: 'array',
description: 'Uploaded files ids of digital product',
items: {
type: 'string',
},
},
instructions: {
type: 'string',
description: 'Instructions to download and use the digital product',
},
},
},
image_id: {
type: 'string',
description: 'Product image id after its uploaded to S3',
},
license_key_activation_message: {
type: 'string',
description:
'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.',
},
license_key_activations_limit: {
type: 'integer',
description:
'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.',
},
license_key_duration: {
$ref: '#/$defs/license_key_duration',
},
license_key_enabled: {
type: 'boolean',
description:
'Whether the product requires a license key.\n\nIf `true`, additional fields related to license key (duration, activations limit, activation message)\nbecome applicable.',
},
metadata: {
type: 'object',
description: 'Additional metadata for the product',
additionalProperties: true,
},
name: {
type: 'string',
description: 'Name of the product, optional and must be at most 100 characters.',
},
price: {
$ref: '#/$defs/price',
},
tax_category: {
$ref: '#/$defs/tax_category',
},
},
required: ['id'],
$defs: {
license_key_duration: {
type: 'object',
title: 'License Key Duration',
properties: {
count: {
type: 'integer',
},
interval: {
$ref: '#/$defs/time_interval',
},
},
required: ['count', 'interval'],
},
time_interval: {
type: 'string',
enum: ['Day', 'Week', 'Month', 'Year'],
},
price: {
anyOf: [
{
type: 'object',
title: 'One Time Price',
description: 'One-time price details.',
properties: {
currency: {
$ref: '#/$defs/currency',
},
discount: {
type: 'integer',
description: 'Discount applied to the price, represented as a percentage (0 to 100).',
},
price: {
type: 'integer',
description:
'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.',
},
purchasing_power_parity: {
type: 'boolean',
description:
'Indicates if purchasing power parity adjustments are applied to the price.\nPurchasing power parity feature is not available as of now.',
},
type: {
type: 'string',
enum: ['one_time_price'],
},
pay_what_you_want: {
type: 'boolean',
description:
'Indicates whether the customer can pay any amount they choose.\nIf set to `true`, the [`price`](Self::price) field is the minimum amount.',
},
suggested_price: {
type: 'integer',
description:
'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.',
},
tax_inclusive: {
type: 'boolean',
description: 'Indicates if the price is tax inclusive.',
},
},
required: ['currency', 'discount', 'price', 'purchasing_power_parity', 'type'],
},
{
type: 'object',
title: 'Recurring Price',
description: 'Recurring price details.',
properties: {
currency: {
$ref: '#/$defs/currency',
},
discount: {
type: 'integer',
description: 'Discount applied to the price, represented as a percentage (0 to 100).',
},
payment_frequency_count: {
type: 'integer',
description:
'Number of units for the payment frequency.\nFor example, a value of `1` with a `payment_frequency_interval` of `month` represents monthly payments.',
},
payment_frequency_interval: {
$ref: '#/$defs/time_interval',
},
price: {
type: 'integer',
description:
'The payment amount. Represented in the lowest denomination of the currency (e.g., cents for USD).\nFor example, to charge $1.00, pass `100`.',
},
purchasing_power_parity: {
type: 'boolean',
description:
'Indicates if purchasing power parity adjustments are applied to the price.\nPurchasing power parity feature is not available as of now',
},
subscription_period_count: {
type: 'integer',
description:
'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.',
},
subscription_period_interval: {
$ref: '#/$defs/time_interval',
},
type: {
type: 'string',
enum: ['recurring_price'],
},
tax_inclusive: {
type: 'boolean',
description: 'Indicates if the price is tax inclusive',
},
trial_period_days: {
type: 'integer',
description: 'Number of days for the trial period. A value of `0` indicates no trial period.',
},
},
required: [
'currency',
'discount',
'payment_frequency_count',
'payment_frequency_interval',
'price',
'purchasing_power_parity',
'subscription_period_count',
'subscription_period_interval',
'type',
],
},
{
type: 'object',
title: 'Usage Based Price',
description: 'Usage Based price details.',
properties: {
currency: {
$ref: '#/$defs/currency',
},
discount: {
type: 'integer',
description: 'Discount applied to the price, represented as a percentage (0 to 100).',
},
fixed_price: {
type: 'integer',
description:
'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`.',
},
payment_frequency_count: {
type: 'integer',
description:
'Number of units for the payment frequency.\nFor example, a value of `1` with a `payment_frequency_interval` of `month` represents monthly payments.',
},
payment_frequency_interval: {
$ref: '#/$defs/time_interval',
},
purchasing_power_parity: {
type: 'boolean',
description:
'Indicates if purchasing power parity adjustments are applied to the price.\nPurchasing power parity feature is not available as of now',
},
subscription_period_count: {
type: 'integer',
description:
'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.',
},
subscription_period_interval: {
$ref: '#/$defs/time_interval',
},
type: {
type: 'string',
enum: ['usage_based_price'],
},
meters: {
type: 'array',
items: {
$ref: '#/$defs/add_meter_to_price',
},
},
tax_inclusive: {
type: 'boolean',
description: 'Indicates if the price is tax inclusive',
},
},
required: [
'currency',
'discount',
'fixed_price',
'payment_frequency_count',
'payment_frequency_interval',
'purchasing_power_parity',
'subscription_period_count',
'subscription_period_interval',
'type',
],
},
],
description: 'One-time price details.',
},
currency: {
type: 'string',
enum: [
'AED',
'ALL',
'AMD',
'ANG',
'AOA',
'ARS',
'AUD',
'AWG',
'AZN',
'BAM',
'BBD',
'BDT',
'BGN',
'BHD',
'BIF',
'BMD',
'BND',
'BOB',
'BRL',
'BSD',
'BWP',
'BYN',
'BZD',
'CAD',
'CHF',
'CLP',
'CNY',
'COP',
'CRC',
'CUP',
'CVE',
'CZK',
'DJF',
'DKK',
'DOP',
'DZD',
'EGP',
'ETB',
'EUR',
'FJD',
'FKP',
'GBP',
'GEL',
'GHS',
'GIP',
'GMD',
'GNF',
'GTQ',
'GYD',
'HKD',
'HNL',
'HRK',
'HTG',
'HUF',
'IDR',
'ILS',
'INR',
'IQD',
'JMD',
'JOD',
'JPY',
'KES',
'KGS',
'KHR',
'KMF',
'KRW',
'KWD',
'KYD',
'KZT',
'LAK',
'LBP',
'LKR',
'LRD',
'LSL',
'LYD',
'MAD',
'MDL',
'MGA',
'MKD',
'MMK',
'MNT',
'MOP',
'MRU',
'MUR',
'MVR',
'MWK',
'MXN',
'MYR',
'MZN',
'NAD',
'NGN',
'NIO',
'NOK',
'NPR',
'NZD',
'OMR',
'PAB',
'PEN',
'PGK',
'PHP',
'PKR',
'PLN',
'PYG',
'QAR',
'RON',
'RSD',
'RUB',
'RWF',
'SAR',
'SBD',
'SCR',
'SEK',
'SGD',
'SHP',
'SLE',
'SLL',
'SOS',
'SRD',
'SSP',
'STN',
'SVC',
'SZL',
'THB',
'TND',
'TOP',
'TRY',
'TTD',
'TWD',
'TZS',
'UAH',
'UGX',
'USD',
'UYU',
'UZS',
'VES',
'VND',
'VUV',
'WST',
'XAF',
'XCD',
'XOF',
'XPF',
'YER',
'ZAR',
'ZMW',
],
},
add_meter_to_price: {
type: 'object',
title: 'Add Meter To Price',
properties: {
meter_id: {
type: 'string',
},
price_per_unit: {
type: 'string',
description:
'The price per unit in lowest denomination. Must be greater than zero. Supports up to 5 digits before decimal point and 12 decimal places.',
},
description: {
type: 'string',
description: 'Meter description. Will ignored on Request, but will be shown in response',
},
free_threshold: {
type: 'integer',
},
measurement_unit: {
type: 'string',
description: 'Meter measurement unit. Will ignored on Request, but will be shown in response',
},
name: {
type: 'string',
description: 'Meter name. Will ignored on Request, but will be shown in response',
},
},
required: ['meter_id', 'price_per_unit'],
},
tax_category: {
type: 'string',
description:
'Represents the different categories of taxation applicable to various products and services.',
enum: ['digital_products', 'saas', 'e_book', 'edtech'],
},
},
},
annotations: {},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { id, ...body } = args as any;
const response = await client.products.update(id, body).asResponse();
return asTextContentResult(await response.text());
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/packages/mcp-server/tests/options.test.ts:
--------------------------------------------------------------------------------
```typescript
import { parseCLIOptions, parseQueryOptions } from '../src/options';
import { Filter } from '../src/tools';
import { parseEmbeddedJSON } from '../src/compat';
// Mock process.argv
const mockArgv = (args: string[]) => {
const originalArgv = process.argv;
process.argv = ['node', 'test.js', ...args];
return () => {
process.argv = originalArgv;
};
};
describe('parseCLIOptions', () => {
it('should parse basic filter options', () => {
const cleanup = mockArgv([
'--tool=test-tool',
'--resource=test-resource',
'--operation=read',
'--tag=test-tag',
]);
const result = parseCLIOptions();
expect(result.filters).toEqual([
{ type: 'tag', op: 'include', value: 'test-tag' },
{ type: 'resource', op: 'include', value: 'test-resource' },
{ type: 'tool', op: 'include', value: 'test-tool' },
{ type: 'operation', op: 'include', value: 'read' },
] as Filter[]);
expect(result.capabilities).toEqual({});
expect(result.list).toBe(false);
cleanup();
});
it('should parse exclusion filters', () => {
const cleanup = mockArgv([
'--no-tool=exclude-tool',
'--no-resource=exclude-resource',
'--no-operation=write',
'--no-tag=exclude-tag',
]);
const result = parseCLIOptions();
expect(result.filters).toEqual([
{ type: 'tag', op: 'exclude', value: 'exclude-tag' },
{ type: 'resource', op: 'exclude', value: 'exclude-resource' },
{ type: 'tool', op: 'exclude', value: 'exclude-tool' },
{ type: 'operation', op: 'exclude', value: 'write' },
] as Filter[]);
expect(result.capabilities).toEqual({});
cleanup();
});
it('should parse client presets', () => {
const cleanup = mockArgv(['--client=openai-agents']);
const result = parseCLIOptions();
expect(result.client).toEqual('openai-agents');
cleanup();
});
it('should parse individual capabilities', () => {
const cleanup = mockArgv([
'--capability=top-level-unions',
'--capability=valid-json',
'--capability=refs',
'--capability=unions',
'--capability=tool-name-length=40',
]);
const result = parseCLIOptions();
expect(result.capabilities).toEqual({
topLevelUnions: true,
validJson: true,
refs: true,
unions: true,
toolNameLength: 40,
});
cleanup();
});
it('should handle list option', () => {
const cleanup = mockArgv(['--list']);
const result = parseCLIOptions();
expect(result.list).toBe(true);
cleanup();
});
it('should handle multiple filters of the same type', () => {
const cleanup = mockArgv(['--tool=tool1', '--tool=tool2', '--resource=res1', '--resource=res2']);
const result = parseCLIOptions();
expect(result.filters).toEqual([
{ type: 'resource', op: 'include', value: 'res1' },
{ type: 'resource', op: 'include', value: 'res2' },
{ type: 'tool', op: 'include', value: 'tool1' },
{ type: 'tool', op: 'include', value: 'tool2' },
] as Filter[]);
cleanup();
});
it('should handle comma-separated values in array options', () => {
const cleanup = mockArgv([
'--tool=tool1,tool2',
'--resource=res1,res2',
'--capability=top-level-unions,valid-json,unions',
]);
const result = parseCLIOptions();
expect(result.filters).toEqual([
{ type: 'resource', op: 'include', value: 'res1' },
{ type: 'resource', op: 'include', value: 'res2' },
{ type: 'tool', op: 'include', value: 'tool1' },
{ type: 'tool', op: 'include', value: 'tool2' },
] as Filter[]);
expect(result.capabilities).toEqual({
topLevelUnions: true,
validJson: true,
unions: true,
});
cleanup();
});
it('should handle invalid tool-name-length format', () => {
const cleanup = mockArgv(['--capability=tool-name-length=invalid']);
// Mock console.error to prevent output during test
const originalError = console.error;
console.error = jest.fn();
expect(() => parseCLIOptions()).toThrow();
console.error = originalError;
cleanup();
});
it('should handle unknown capability', () => {
const cleanup = mockArgv(['--capability=unknown-capability']);
// Mock console.error to prevent output during test
const originalError = console.error;
console.error = jest.fn();
expect(() => parseCLIOptions()).toThrow();
console.error = originalError;
cleanup();
});
});
describe('parseQueryOptions', () => {
const defaultOptions = {
client: undefined,
includeDynamicTools: undefined,
includeAllTools: undefined,
filters: [],
capabilities: {
topLevelUnions: true,
validJson: true,
refs: true,
unions: true,
formats: true,
toolNameLength: undefined,
},
};
it('should parse basic filter options from query string', () => {
const query = 'tool=test-tool&resource=test-resource&operation=read&tag=test-tag';
const result = parseQueryOptions(defaultOptions, query);
expect(result.filters).toEqual([
{ type: 'resource', op: 'include', value: 'test-resource' },
{ type: 'operation', op: 'include', value: 'read' },
{ type: 'tag', op: 'include', value: 'test-tag' },
{ type: 'tool', op: 'include', value: 'test-tool' },
]);
expect(result.capabilities).toEqual({
topLevelUnions: true,
validJson: true,
refs: true,
unions: true,
formats: true,
toolNameLength: undefined,
});
});
it('should parse exclusion filters from query string', () => {
const query = 'no_tool=exclude-tool&no_resource=exclude-resource&no_operation=write&no_tag=exclude-tag';
const result = parseQueryOptions(defaultOptions, query);
expect(result.filters).toEqual([
{ type: 'resource', op: 'exclude', value: 'exclude-resource' },
{ type: 'operation', op: 'exclude', value: 'write' },
{ type: 'tag', op: 'exclude', value: 'exclude-tag' },
{ type: 'tool', op: 'exclude', value: 'exclude-tool' },
]);
});
it('should parse client option from query string', () => {
const query = 'client=openai-agents';
const result = parseQueryOptions(defaultOptions, query);
expect(result.client).toBe('openai-agents');
});
it('should parse client capabilities from query string', () => {
const query = 'capability=top-level-unions&capability=valid-json&capability=tool-name-length%3D40';
const result = parseQueryOptions(defaultOptions, query);
expect(result.capabilities).toEqual({
topLevelUnions: true,
validJson: true,
refs: true,
unions: true,
formats: true,
toolNameLength: 40,
});
});
it('should parse no-capability options from query string', () => {
const query = 'no_capability=top-level-unions&no_capability=refs&no_capability=formats';
const result = parseQueryOptions(defaultOptions, query);
expect(result.capabilities).toEqual({
topLevelUnions: false,
validJson: true,
refs: false,
unions: true,
formats: false,
toolNameLength: undefined,
});
});
it('should parse tools options from query string', () => {
const query = 'tools=dynamic&tools=all';
const result = parseQueryOptions(defaultOptions, query);
expect(result.includeDynamicTools).toBe(true);
expect(result.includeAllTools).toBe(true);
});
it('should parse no-tools options from query string', () => {
const query = 'tools=dynamic&tools=all&no_tools=dynamic';
const result = parseQueryOptions(defaultOptions, query);
expect(result.includeDynamicTools).toBe(false);
expect(result.includeAllTools).toBe(true);
});
it('should handle array values in query string', () => {
const query = 'tool[]=tool1&tool[]=tool2&resource[]=res1&resource[]=res2';
const result = parseQueryOptions(defaultOptions, query);
expect(result.filters).toEqual([
{ type: 'resource', op: 'include', value: 'res1' },
{ type: 'resource', op: 'include', value: 'res2' },
{ type: 'tool', op: 'include', value: 'tool1' },
{ type: 'tool', op: 'include', value: 'tool2' },
]);
});
it('should merge with default options', () => {
const defaultWithFilters = {
...defaultOptions,
filters: [{ type: 'tag' as const, op: 'include' as const, value: 'existing-tag' }],
client: 'cursor' as const,
includeDynamicTools: true,
};
const query = 'tool=new-tool&resource=new-resource';
const result = parseQueryOptions(defaultWithFilters, query);
expect(result.filters).toEqual([
{ type: 'tag', op: 'include', value: 'existing-tag' },
{ type: 'resource', op: 'include', value: 'new-resource' },
{ type: 'tool', op: 'include', value: 'new-tool' },
]);
expect(result.client).toBe('cursor');
expect(result.includeDynamicTools).toBe(true);
});
it('should override client from default options', () => {
const defaultWithClient = {
...defaultOptions,
client: 'cursor' as const,
};
const query = 'client=openai-agents';
const result = parseQueryOptions(defaultWithClient, query);
expect(result.client).toBe('openai-agents');
});
it('should merge capabilities with default options', () => {
const defaultWithCapabilities = {
...defaultOptions,
capabilities: {
topLevelUnions: false,
validJson: false,
refs: true,
unions: true,
formats: true,
toolNameLength: 30,
},
};
const query = 'capability=top-level-unions&no_capability=refs';
const result = parseQueryOptions(defaultWithCapabilities, query);
expect(result.capabilities).toEqual({
topLevelUnions: true,
validJson: false,
refs: false,
unions: true,
formats: true,
toolNameLength: 30,
});
});
it('should handle empty query string', () => {
const query = '';
const result = parseQueryOptions(defaultOptions, query);
expect(result).toEqual(defaultOptions);
});
it('should handle invalid query string gracefully', () => {
const query = 'invalid=value&operation=invalid-operation';
// Should throw due to Zod validation for invalid operation
expect(() => parseQueryOptions(defaultOptions, query)).toThrow();
});
it('should preserve default undefined values when not specified', () => {
const defaultWithUndefined = {
...defaultOptions,
client: undefined,
includeDynamicTools: undefined,
includeAllTools: undefined,
};
const query = 'tool=test-tool';
const result = parseQueryOptions(defaultWithUndefined, query);
expect(result.client).toBeUndefined();
expect(result.includeDynamicTools).toBeFalsy();
expect(result.includeAllTools).toBeFalsy();
});
it('should handle complex query with mixed include and exclude filters', () => {
const query =
'tool=include-tool&no_tool=exclude-tool&resource=include-res&no_resource=exclude-res&operation=read&tag=include-tag&no_tag=exclude-tag';
const result = parseQueryOptions(defaultOptions, query);
expect(result.filters).toEqual([
{ type: 'resource', op: 'include', value: 'include-res' },
{ type: 'operation', op: 'include', value: 'read' },
{ type: 'tag', op: 'include', value: 'include-tag' },
{ type: 'tool', op: 'include', value: 'include-tool' },
{ type: 'resource', op: 'exclude', value: 'exclude-res' },
{ type: 'tag', op: 'exclude', value: 'exclude-tag' },
{ type: 'tool', op: 'exclude', value: 'exclude-tool' },
]);
});
});
describe('parseEmbeddedJSON', () => {
it('should not change non-string values', () => {
const args = {
numberProp: 42,
booleanProp: true,
objectProp: { nested: 'value' },
arrayProp: [1, 2, 3],
nullProp: null,
undefinedProp: undefined,
};
const schema = {};
const result = parseEmbeddedJSON(args, schema);
expect(result).toBe(args); // Should return original object since no changes made
expect(result['numberProp']).toBe(42);
expect(result['booleanProp']).toBe(true);
expect(result['objectProp']).toEqual({ nested: 'value' });
expect(result['arrayProp']).toEqual([1, 2, 3]);
expect(result['nullProp']).toBe(null);
expect(result['undefinedProp']).toBe(undefined);
});
it('should parse valid JSON objects in string properties', () => {
const args = {
jsonObjectString: '{"key": "value", "number": 123}',
regularString: 'not json',
};
const schema = {};
const result = parseEmbeddedJSON(args, schema);
expect(result).not.toBe(args); // Should return new object since changes were made
expect(result['jsonObjectString']).toEqual({ key: 'value', number: 123 });
expect(result['regularString']).toBe('not json');
});
it('should leave invalid JSON in string properties unchanged', () => {
const args = {
invalidJson1: '{"key": value}', // Missing quotes around value
invalidJson2: '{key: "value"}', // Missing quotes around key
invalidJson3: '{"key": "value",}', // Trailing comma
invalidJson4: 'just a regular string',
emptyString: '',
};
const schema = {};
const result = parseEmbeddedJSON(args, schema);
expect(result).toBe(args); // Should return original object since no changes made
expect(result['invalidJson1']).toBe('{"key": value}');
expect(result['invalidJson2']).toBe('{key: "value"}');
expect(result['invalidJson3']).toBe('{"key": "value",}');
expect(result['invalidJson4']).toBe('just a regular string');
expect(result['emptyString']).toBe('');
});
it('should not parse JSON primitives in string properties', () => {
const args = {
numberString: '123',
floatString: '45.67',
negativeNumberString: '-89',
booleanTrueString: 'true',
booleanFalseString: 'false',
nullString: 'null',
jsonArrayString: '[1, 2, 3, "test"]',
regularString: 'not json',
};
const schema = {};
const result = parseEmbeddedJSON(args, schema);
expect(result).toBe(args); // Should return original object since no changes made
expect(result['numberString']).toBe('123');
expect(result['floatString']).toBe('45.67');
expect(result['negativeNumberString']).toBe('-89');
expect(result['booleanTrueString']).toBe('true');
expect(result['booleanFalseString']).toBe('false');
expect(result['nullString']).toBe('null');
expect(result['jsonArrayString']).toBe('[1, 2, 3, "test"]');
expect(result['regularString']).toBe('not json');
});
it('should handle mixed valid objects and other JSON types', () => {
const args = {
validObject: '{"success": true}',
invalidObject: '{"missing": quote}',
validNumber: '42',
validArray: '[1, 2, 3]',
keepAsString: 'hello world',
nonString: 123,
};
const schema = {};
const result = parseEmbeddedJSON(args, schema);
expect(result).not.toBe(args); // Should return new object since some changes were made
expect(result['validObject']).toEqual({ success: true });
expect(result['invalidObject']).toBe('{"missing": quote}');
expect(result['validNumber']).toBe('42'); // Not parsed, remains string
expect(result['validArray']).toBe('[1, 2, 3]'); // Not parsed, remains string
expect(result['keepAsString']).toBe('hello world');
expect(result['nonString']).toBe(123);
});
it('should return original object when no strings are present', () => {
const args = {
number: 42,
boolean: true,
object: { key: 'value' },
};
const schema = {};
const result = parseEmbeddedJSON(args, schema);
expect(result).toBe(args); // Should return original object since no changes made
});
it('should return original object when all strings are invalid JSON', () => {
const args = {
string1: 'hello',
string2: 'world',
string3: 'not json at all',
};
const schema = {};
const result = parseEmbeddedJSON(args, schema);
expect(result).toBe(args); // Should return original object since no changes made
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/checkout-sessions/create-checkout-sessions.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'checkout_sessions',
operation: 'write',
tags: [],
httpMethod: 'post',
httpPath: '/checkouts',
operationId: 'create_session',
};
export const tool: Tool = {
name: 'create_checkout_sessions',
description: '',
inputSchema: {
type: 'object',
properties: {
product_cart: {
type: 'array',
items: {
type: 'object',
title: 'Product Item Request',
properties: {
product_id: {
type: 'string',
description: 'unique id of the product',
},
quantity: {
type: 'integer',
},
addons: {
type: 'array',
description: 'only valid if product is a subscription',
items: {
$ref: '#/$defs/attach_addon',
},
},
amount: {
type: 'integer',
description:
'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.',
},
},
required: ['product_id', 'quantity'],
},
},
allowed_payment_method_types: {
type: 'array',
description:
"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.",
items: {
$ref: '#/$defs/payment_method_types',
},
},
billing_address: {
type: 'object',
title: 'Checkout Session Billing Address',
description: 'Billing address information for the session',
properties: {
country: {
$ref: '#/$defs/country_code',
},
city: {
type: 'string',
description: 'City name',
},
state: {
type: 'string',
description: 'State or province name',
},
street: {
type: 'string',
description: 'Street address including house number and unit/apartment if applicable',
},
zipcode: {
type: 'string',
description: 'Postal code or ZIP code',
},
},
required: ['country'],
},
billing_currency: {
$ref: '#/$defs/currency',
},
confirm: {
type: 'boolean',
description:
'If confirm is true, all the details will be finalized. If required data is missing, an API error is thrown.',
},
customer: {
$ref: '#/$defs/customer_request',
},
customization: {
type: 'object',
title: 'Checkout Session Customization',
description: 'Customization for the checkout session page',
properties: {
force_language: {
type: 'string',
description: 'Force the checkout interface to render in a specific language (e.g. `en`, `es`)',
},
show_on_demand_tag: {
type: 'boolean',
description: 'Show on demand tag\n\nDefault is true',
},
show_order_details: {
type: 'boolean',
description: 'Show order details by default\n\nDefault is true',
},
theme: {
type: 'string',
description: 'Theme of the page\n\nDefault is `System`.',
enum: ['dark', 'light', 'system'],
},
},
},
discount_code: {
type: 'string',
},
feature_flags: {
type: 'object',
title: 'Checkout Session Flags',
properties: {
allow_currency_selection: {
type: 'boolean',
description: 'if customer is allowed to change currency, set it to true\n\nDefault is true',
},
allow_discount_code: {
type: 'boolean',
description:
'If the customer is allowed to apply discount code, set it to true.\n\nDefault is true',
},
allow_phone_number_collection: {
type: 'boolean',
description: 'If phone number is collected from customer, set it to rue\n\nDefault is true',
},
allow_tax_id: {
type: 'boolean',
description: 'If the customer is allowed to add tax id, set it to true\n\nDefault is true',
},
always_create_new_customer: {
type: 'boolean',
description:
'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',
},
},
},
force_3ds: {
type: 'boolean',
description: 'Override merchant default 3DS behaviour for this session',
},
metadata: {
type: 'object',
description: 'Additional metadata associated with the payment. Defaults to empty if not provided.',
additionalProperties: true,
},
return_url: {
type: 'string',
description: 'The url to redirect after payment failure or success.',
},
show_saved_payment_methods: {
type: 'boolean',
description: 'Display saved payment methods of a returning customer False by default',
},
subscription_data: {
type: 'object',
title: 'Subscription Data',
properties: {
on_demand: {
$ref: '#/$defs/on_demand_subscription',
},
trial_period_days: {
type: 'integer',
description:
"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",
},
},
},
},
required: ['product_cart'],
$defs: {
attach_addon: {
type: 'object',
title: 'Attach Addon Request',
properties: {
addon_id: {
type: 'string',
},
quantity: {
type: 'integer',
},
},
required: ['addon_id', 'quantity'],
},
payment_method_types: {
type: 'string',
enum: [
'credit',
'debit',
'upi_collect',
'upi_intent',
'apple_pay',
'cashapp',
'google_pay',
'multibanco',
'bancontact_card',
'eps',
'ideal',
'przelewy24',
'paypal',
'affirm',
'klarna',
'sepa',
'ach',
'amazon_pay',
'afterpay_clearpay',
],
},
country_code: {
type: 'string',
description: 'ISO country code alpha2 variant',
enum: [
'AF',
'AX',
'AL',
'DZ',
'AS',
'AD',
'AO',
'AI',
'AQ',
'AG',
'AR',
'AM',
'AW',
'AU',
'AT',
'AZ',
'BS',
'BH',
'BD',
'BB',
'BY',
'BE',
'BZ',
'BJ',
'BM',
'BT',
'BO',
'BQ',
'BA',
'BW',
'BV',
'BR',
'IO',
'BN',
'BG',
'BF',
'BI',
'KH',
'CM',
'CA',
'CV',
'KY',
'CF',
'TD',
'CL',
'CN',
'CX',
'CC',
'CO',
'KM',
'CG',
'CD',
'CK',
'CR',
'CI',
'HR',
'CU',
'CW',
'CY',
'CZ',
'DK',
'DJ',
'DM',
'DO',
'EC',
'EG',
'SV',
'GQ',
'ER',
'EE',
'ET',
'FK',
'FO',
'FJ',
'FI',
'FR',
'GF',
'PF',
'TF',
'GA',
'GM',
'GE',
'DE',
'GH',
'GI',
'GR',
'GL',
'GD',
'GP',
'GU',
'GT',
'GG',
'GN',
'GW',
'GY',
'HT',
'HM',
'VA',
'HN',
'HK',
'HU',
'IS',
'IN',
'ID',
'IR',
'IQ',
'IE',
'IM',
'IL',
'IT',
'JM',
'JP',
'JE',
'JO',
'KZ',
'KE',
'KI',
'KP',
'KR',
'KW',
'KG',
'LA',
'LV',
'LB',
'LS',
'LR',
'LY',
'LI',
'LT',
'LU',
'MO',
'MK',
'MG',
'MW',
'MY',
'MV',
'ML',
'MT',
'MH',
'MQ',
'MR',
'MU',
'YT',
'MX',
'FM',
'MD',
'MC',
'MN',
'ME',
'MS',
'MA',
'MZ',
'MM',
'NA',
'NR',
'NP',
'NL',
'NC',
'NZ',
'NI',
'NE',
'NG',
'NU',
'NF',
'MP',
'NO',
'OM',
'PK',
'PW',
'PS',
'PA',
'PG',
'PY',
'PE',
'PH',
'PN',
'PL',
'PT',
'PR',
'QA',
'RE',
'RO',
'RU',
'RW',
'BL',
'SH',
'KN',
'LC',
'MF',
'PM',
'VC',
'WS',
'SM',
'ST',
'SA',
'SN',
'RS',
'SC',
'SL',
'SG',
'SX',
'SK',
'SI',
'SB',
'SO',
'ZA',
'GS',
'SS',
'ES',
'LK',
'SD',
'SR',
'SJ',
'SZ',
'SE',
'CH',
'SY',
'TW',
'TJ',
'TZ',
'TH',
'TL',
'TG',
'TK',
'TO',
'TT',
'TN',
'TR',
'TM',
'TC',
'TV',
'UG',
'UA',
'AE',
'GB',
'UM',
'US',
'UY',
'UZ',
'VU',
'VE',
'VN',
'VG',
'VI',
'WF',
'EH',
'YE',
'ZM',
'ZW',
],
},
currency: {
type: 'string',
enum: [
'AED',
'ALL',
'AMD',
'ANG',
'AOA',
'ARS',
'AUD',
'AWG',
'AZN',
'BAM',
'BBD',
'BDT',
'BGN',
'BHD',
'BIF',
'BMD',
'BND',
'BOB',
'BRL',
'BSD',
'BWP',
'BYN',
'BZD',
'CAD',
'CHF',
'CLP',
'CNY',
'COP',
'CRC',
'CUP',
'CVE',
'CZK',
'DJF',
'DKK',
'DOP',
'DZD',
'EGP',
'ETB',
'EUR',
'FJD',
'FKP',
'GBP',
'GEL',
'GHS',
'GIP',
'GMD',
'GNF',
'GTQ',
'GYD',
'HKD',
'HNL',
'HRK',
'HTG',
'HUF',
'IDR',
'ILS',
'INR',
'IQD',
'JMD',
'JOD',
'JPY',
'KES',
'KGS',
'KHR',
'KMF',
'KRW',
'KWD',
'KYD',
'KZT',
'LAK',
'LBP',
'LKR',
'LRD',
'LSL',
'LYD',
'MAD',
'MDL',
'MGA',
'MKD',
'MMK',
'MNT',
'MOP',
'MRU',
'MUR',
'MVR',
'MWK',
'MXN',
'MYR',
'MZN',
'NAD',
'NGN',
'NIO',
'NOK',
'NPR',
'NZD',
'OMR',
'PAB',
'PEN',
'PGK',
'PHP',
'PKR',
'PLN',
'PYG',
'QAR',
'RON',
'RSD',
'RUB',
'RWF',
'SAR',
'SBD',
'SCR',
'SEK',
'SGD',
'SHP',
'SLE',
'SLL',
'SOS',
'SRD',
'SSP',
'STN',
'SVC',
'SZL',
'THB',
'TND',
'TOP',
'TRY',
'TTD',
'TWD',
'TZS',
'UAH',
'UGX',
'USD',
'UYU',
'UZS',
'VES',
'VND',
'VUV',
'WST',
'XAF',
'XCD',
'XOF',
'XPF',
'YER',
'ZAR',
'ZMW',
],
},
customer_request: {
anyOf: [
{
$ref: '#/$defs/attach_existing_customer',
},
{
$ref: '#/$defs/new_customer',
},
],
title: 'Customer Request',
},
attach_existing_customer: {
type: 'object',
title: 'Attach Existing Customer',
properties: {
customer_id: {
type: 'string',
},
},
required: ['customer_id'],
},
new_customer: {
type: 'object',
title: 'New Customer',
properties: {
email: {
type: 'string',
description: 'Email is required for creating a new customer',
},
name: {
type: 'string',
description:
'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.',
},
phone_number: {
type: 'string',
},
},
required: ['email'],
},
on_demand_subscription: {
type: 'object',
title: 'On Demand Subscription Request',
properties: {
mandate_only: {
type: 'boolean',
description:
'If set as True, does not perform any charge and only authorizes payment method details for future use.',
},
adaptive_currency_fees_inclusive: {
type: 'boolean',
description:
'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.',
},
product_currency: {
$ref: '#/$defs/currency',
},
product_description: {
type: 'string',
description:
'Optional product description override for billing and line items.\nIf not specified, the stored description of the product will be used.',
},
product_price: {
type: 'integer',
description:
'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`.',
},
},
required: ['mandate_only'],
},
},
},
annotations: {},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const body = args as any;
return asTextContentResult(await client.checkoutSessions.create(body));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/options.ts:
--------------------------------------------------------------------------------
```typescript
import qs from 'qs';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import z from 'zod';
import { endpoints, Filter } from './tools';
import { ClientCapabilities, knownClients, ClientType } from './compat';
export type CLIOptions = McpOptions & {
list: boolean;
transport: 'stdio' | 'http';
port: number | undefined;
socket: string | undefined;
};
export type McpOptions = {
client?: ClientType | undefined;
includeDynamicTools?: boolean | undefined;
includeAllTools?: boolean | undefined;
includeCodeTools?: boolean | undefined;
includeDocsTools?: boolean | undefined;
filters?: Filter[] | undefined;
capabilities?: Partial<ClientCapabilities> | undefined;
};
const CAPABILITY_CHOICES = [
'top-level-unions',
'valid-json',
'refs',
'unions',
'formats',
'tool-name-length',
] as const;
type Capability = (typeof CAPABILITY_CHOICES)[number];
function parseCapabilityValue(cap: string): { name: Capability; value?: number } {
if (cap.startsWith('tool-name-length=')) {
const parts = cap.split('=');
if (parts.length === 2) {
const length = parseInt(parts[1]!, 10);
if (!isNaN(length)) {
return { name: 'tool-name-length', value: length };
}
throw new Error(`Invalid tool-name-length value: ${parts[1]}. Expected a number.`);
}
throw new Error(`Invalid format for tool-name-length. Expected tool-name-length=N.`);
}
if (!CAPABILITY_CHOICES.includes(cap as Capability)) {
throw new Error(`Unknown capability: ${cap}. Valid capabilities are: ${CAPABILITY_CHOICES.join(', ')}`);
}
return { name: cap as Capability };
}
export function parseCLIOptions(): CLIOptions {
const opts = yargs(hideBin(process.argv))
.option('tools', {
type: 'string',
array: true,
choices: ['dynamic', 'all', 'code', 'docs'],
description: 'Use dynamic tools or all tools',
})
.option('no-tools', {
type: 'string',
array: true,
choices: ['dynamic', 'all', 'code', 'docs'],
description: 'Do not use any dynamic or all tools',
})
.option('tool', {
type: 'string',
array: true,
description: 'Include tools matching the specified names',
})
.option('resource', {
type: 'string',
array: true,
description: 'Include tools matching the specified resources',
})
.option('operation', {
type: 'string',
array: true,
choices: ['read', 'write'],
description: 'Include tools matching the specified operations',
})
.option('tag', {
type: 'string',
array: true,
description: 'Include tools with the specified tags',
})
.option('no-tool', {
type: 'string',
array: true,
description: 'Exclude tools matching the specified names',
})
.option('no-resource', {
type: 'string',
array: true,
description: 'Exclude tools matching the specified resources',
})
.option('no-operation', {
type: 'string',
array: true,
description: 'Exclude tools matching the specified operations',
})
.option('no-tag', {
type: 'string',
array: true,
description: 'Exclude tools with the specified tags',
})
.option('list', {
type: 'boolean',
description: 'List all tools and exit',
})
.option('client', {
type: 'string',
choices: Object.keys(knownClients),
description: 'Specify the MCP client being used',
})
.option('capability', {
type: 'string',
array: true,
description: 'Specify client capabilities',
coerce: (values: string[]) => {
return values.flatMap((v) => v.split(','));
},
})
.option('no-capability', {
type: 'string',
array: true,
description: 'Unset client capabilities',
choices: CAPABILITY_CHOICES,
coerce: (values: string[]) => {
return values.flatMap((v) => v.split(','));
},
})
.option('describe-capabilities', {
type: 'boolean',
description: 'Print detailed explanation of client capabilities and exit',
})
.option('transport', {
type: 'string',
choices: ['stdio', 'http'],
default: 'stdio',
description: 'What transport to use; stdio for local servers or http for remote servers',
})
.option('port', {
type: 'number',
description: 'Port to serve on if using http transport',
})
.option('socket', {
type: 'string',
description: 'Unix socket to serve on if using http transport',
})
.help();
for (const [command, desc] of examples()) {
opts.example(command, desc);
}
const argv = opts.parseSync();
// Handle describe-capabilities flag
if (argv.describeCapabilities) {
console.log(getCapabilitiesExplanation());
process.exit(0);
}
const filters: Filter[] = [];
// Helper function to support comma-separated values
const splitValues = (values: string[] | undefined): string[] => {
if (!values) return [];
return values.flatMap((v) => v.split(','));
};
for (const tag of splitValues(argv.tag)) {
filters.push({ type: 'tag', op: 'include', value: tag });
}
for (const tag of splitValues(argv.noTag)) {
filters.push({ type: 'tag', op: 'exclude', value: tag });
}
for (const resource of splitValues(argv.resource)) {
filters.push({ type: 'resource', op: 'include', value: resource });
}
for (const resource of splitValues(argv.noResource)) {
filters.push({ type: 'resource', op: 'exclude', value: resource });
}
for (const tool of splitValues(argv.tool)) {
filters.push({ type: 'tool', op: 'include', value: tool });
}
for (const tool of splitValues(argv.noTool)) {
filters.push({ type: 'tool', op: 'exclude', value: tool });
}
for (const operation of splitValues(argv.operation)) {
filters.push({ type: 'operation', op: 'include', value: operation });
}
for (const operation of splitValues(argv.noOperation)) {
filters.push({ type: 'operation', op: 'exclude', value: operation });
}
// Parse client capabilities
const clientCapabilities: Partial<ClientCapabilities> = {};
// Apply individual capability overrides
if (Array.isArray(argv.capability)) {
for (const cap of argv.capability) {
const parsedCap = parseCapabilityValue(cap);
if (parsedCap.name === 'top-level-unions') {
clientCapabilities.topLevelUnions = true;
} else if (parsedCap.name === 'valid-json') {
clientCapabilities.validJson = true;
} else if (parsedCap.name === 'refs') {
clientCapabilities.refs = true;
} else if (parsedCap.name === 'unions') {
clientCapabilities.unions = true;
} else if (parsedCap.name === 'formats') {
clientCapabilities.formats = true;
} else if (parsedCap.name === 'tool-name-length') {
clientCapabilities.toolNameLength = parsedCap.value;
}
}
}
// Handle no-capability options to unset capabilities
if (Array.isArray(argv.noCapability)) {
for (const cap of argv.noCapability) {
if (cap === 'top-level-unions') {
clientCapabilities.topLevelUnions = false;
} else if (cap === 'valid-json') {
clientCapabilities.validJson = false;
} else if (cap === 'refs') {
clientCapabilities.refs = false;
} else if (cap === 'unions') {
clientCapabilities.unions = false;
} else if (cap === 'formats') {
clientCapabilities.formats = false;
} else if (cap === 'tool-name-length') {
clientCapabilities.toolNameLength = undefined;
}
}
}
const shouldIncludeToolType = (toolType: 'dynamic' | 'all' | 'code' | 'docs') =>
argv.noTools?.includes(toolType) ? false
: argv.tools?.includes(toolType) ? true
: undefined;
const includeDynamicTools = shouldIncludeToolType('dynamic');
const includeAllTools = shouldIncludeToolType('all');
const includeCodeTools = shouldIncludeToolType('code');
const includeDocsTools = shouldIncludeToolType('docs');
const transport = argv.transport as 'stdio' | 'http';
const client = argv.client as ClientType;
return {
client: client && client !== 'infer' && knownClients[client] ? client : undefined,
includeDynamicTools,
includeAllTools,
includeCodeTools,
includeDocsTools,
filters,
capabilities: clientCapabilities,
list: argv.list || false,
transport,
port: argv.port,
socket: argv.socket,
};
}
const coerceArray = <T extends z.ZodTypeAny>(zodType: T) =>
z.preprocess(
(val) =>
Array.isArray(val) ? val
: val ? [val]
: val,
z.array(zodType).optional(),
);
const QueryOptions = z.object({
tools: coerceArray(z.enum(['dynamic', 'all', 'docs'])).describe('Use dynamic tools or all tools'),
no_tools: coerceArray(z.enum(['dynamic', 'all', 'docs'])).describe('Do not use dynamic tools or all tools'),
tool: coerceArray(z.string()).describe('Include tools matching the specified names'),
resource: coerceArray(z.string()).describe('Include tools matching the specified resources'),
operation: coerceArray(z.enum(['read', 'write'])).describe(
'Include tools matching the specified operations',
),
tag: coerceArray(z.string()).describe('Include tools with the specified tags'),
no_tool: coerceArray(z.string()).describe('Exclude tools matching the specified names'),
no_resource: coerceArray(z.string()).describe('Exclude tools matching the specified resources'),
no_operation: coerceArray(z.enum(['read', 'write'])).describe(
'Exclude tools matching the specified operations',
),
no_tag: coerceArray(z.string()).describe('Exclude tools with the specified tags'),
client: ClientType.optional().describe('Specify the MCP client being used'),
capability: coerceArray(z.string()).describe('Specify client capabilities'),
no_capability: coerceArray(z.enum(CAPABILITY_CHOICES)).describe('Unset client capabilities'),
});
export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): McpOptions {
const queryObject = typeof query === 'string' ? qs.parse(query) : query;
const queryOptions = QueryOptions.parse(queryObject);
const filters: Filter[] = [...(defaultOptions.filters ?? [])];
for (const resource of queryOptions.resource || []) {
filters.push({ type: 'resource', op: 'include', value: resource });
}
for (const operation of queryOptions.operation || []) {
filters.push({ type: 'operation', op: 'include', value: operation });
}
for (const tag of queryOptions.tag || []) {
filters.push({ type: 'tag', op: 'include', value: tag });
}
for (const tool of queryOptions.tool || []) {
filters.push({ type: 'tool', op: 'include', value: tool });
}
for (const resource of queryOptions.no_resource || []) {
filters.push({ type: 'resource', op: 'exclude', value: resource });
}
for (const operation of queryOptions.no_operation || []) {
filters.push({ type: 'operation', op: 'exclude', value: operation });
}
for (const tag of queryOptions.no_tag || []) {
filters.push({ type: 'tag', op: 'exclude', value: tag });
}
for (const tool of queryOptions.no_tool || []) {
filters.push({ type: 'tool', op: 'exclude', value: tool });
}
// Parse client capabilities
const clientCapabilities: Partial<ClientCapabilities> = { ...defaultOptions.capabilities };
for (const cap of queryOptions.capability || []) {
const parsed = parseCapabilityValue(cap);
if (parsed.name === 'top-level-unions') {
clientCapabilities.topLevelUnions = true;
} else if (parsed.name === 'valid-json') {
clientCapabilities.validJson = true;
} else if (parsed.name === 'refs') {
clientCapabilities.refs = true;
} else if (parsed.name === 'unions') {
clientCapabilities.unions = true;
} else if (parsed.name === 'formats') {
clientCapabilities.formats = true;
} else if (parsed.name === 'tool-name-length') {
clientCapabilities.toolNameLength = parsed.value;
}
}
for (const cap of queryOptions.no_capability || []) {
if (cap === 'top-level-unions') {
clientCapabilities.topLevelUnions = false;
} else if (cap === 'valid-json') {
clientCapabilities.validJson = false;
} else if (cap === 'refs') {
clientCapabilities.refs = false;
} else if (cap === 'unions') {
clientCapabilities.unions = false;
} else if (cap === 'formats') {
clientCapabilities.formats = false;
} else if (cap === 'tool-name-length') {
clientCapabilities.toolNameLength = undefined;
}
}
let dynamicTools: boolean | undefined =
queryOptions.no_tools && queryOptions.no_tools?.includes('dynamic') ? false
: queryOptions.tools?.includes('dynamic') ? true
: defaultOptions.includeDynamicTools;
let allTools: boolean | undefined =
queryOptions.no_tools && queryOptions.no_tools?.includes('all') ? false
: queryOptions.tools?.includes('all') ? true
: defaultOptions.includeAllTools;
let docsTools: boolean | undefined =
queryOptions.no_tools && queryOptions.no_tools?.includes('docs') ? false
: queryOptions.tools?.includes('docs') ? true
: defaultOptions.includeDocsTools;
return {
client: queryOptions.client ?? defaultOptions.client,
includeDynamicTools: dynamicTools,
includeAllTools: allTools,
includeCodeTools: undefined,
includeDocsTools: docsTools,
filters,
capabilities: clientCapabilities,
};
}
function getCapabilitiesExplanation(): string {
return `
Client Capabilities Explanation:
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.
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.
Available Capabilities:
# top-level-unions
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.
# refs
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.
# valid-json
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.
# unions
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.
# formats
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.
# tool-name-length=N
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.
Client Presets (--client):
Presets like '--client=openai-agents' or '--client=cursor' automatically configure these capabilities based on current known limitations of those clients, simplifying setup.
Current presets:
${JSON.stringify(knownClients, null, 2)}
`;
}
function examples(): [string, string][] {
const firstEndpoint = endpoints[0]!;
const secondEndpoint =
endpoints.find((e) => e.metadata.resource !== firstEndpoint.metadata.resource) || endpoints[1];
const tag = endpoints.find((e) => e.metadata.tags.length > 0)?.metadata.tags[0];
const otherEndpoint = secondEndpoint || firstEndpoint;
return [
[
`--tool="${firstEndpoint.tool.name}" ${secondEndpoint ? `--tool="${secondEndpoint.tool.name}"` : ''}`,
'Include tools by name',
],
[
`--resource="${firstEndpoint.metadata.resource}" --operation="read"`,
'Filter by resource and operation',
],
[
`--resource="${otherEndpoint.metadata.resource}*" --no-tool="${otherEndpoint.tool.name}"`,
'Use resource wildcards and exclusions',
],
[`--client="cursor"`, 'Adjust schemas to be more compatible with Cursor'],
[
`--capability="top-level-unions" --capability="tool-name-length=40"`,
'Specify individual client capabilities',
],
[
`--client="cursor" --no-capability="tool-name-length"`,
'Use cursor client preset but remove tool name length limit',
],
...(tag ? [[`--tag="${tag}"`, 'Filter based on tags'] as [string, string]] : []),
];
}
```
--------------------------------------------------------------------------------
/tests/path.test.ts:
--------------------------------------------------------------------------------
```typescript
import { createPathTagFunction, encodeURIPath } from 'dodopayments/internal/utils/path';
import { inspect } from 'node:util';
import { runInNewContext } from 'node:vm';
describe('path template tag function', () => {
test('validates input', () => {
const testParams = ['', '.', '..', 'x', '%2e', '%2E', '%2e%2e', '%2E%2e', '%2e%2E', '%2E%2E'];
const testCases = [
['/path_params/', '/a'],
['/path_params/', '/'],
['/path_params/', ''],
['', '/a'],
['', '/'],
['', ''],
['a'],
[''],
['/path_params/', ':initiate'],
['/path_params/', '.json'],
['/path_params/', '?beta=true'],
['/path_params/', '.?beta=true'],
['/path_params/', '/', '/download'],
['/path_params/', '-', '/download'],
['/path_params/', '', '/download'],
['/path_params/', '.', '/download'],
['/path_params/', '..', '/download'],
['/plain/path'],
];
function paramPermutations(len: number): string[][] {
if (len === 0) return [];
if (len === 1) return testParams.map((e) => [e]);
const rest = paramPermutations(len - 1);
return testParams.flatMap((e) => rest.map((r) => [e, ...r]));
}
// We need to test how %2E is handled, so we use a custom encoder that does no escaping.
const rawPath = createPathTagFunction((s) => s);
const emptyObject = {};
const mathObject = Math;
const numberObject = new Number();
const stringObject = new String();
const basicClass = new (class {})();
const classWithToString = new (class {
toString() {
return 'ok';
}
})();
// Invalid values
expect(() => rawPath`/a/${null}/b`).toThrow(
'Path parameters result in path with invalid segments:\n' +
'Value of type Null is not a valid path parameter\n' +
'/a/null/b\n' +
' ^^^^',
);
expect(() => rawPath`/a/${undefined}/b`).toThrow(
'Path parameters result in path with invalid segments:\n' +
'Value of type Undefined is not a valid path parameter\n' +
'/a/undefined/b\n' +
' ^^^^^^^^^',
);
expect(() => rawPath`/a/${emptyObject}/b`).toThrow(
'Path parameters result in path with invalid segments:\n' +
'Value of type Object is not a valid path parameter\n' +
'/a/[object Object]/b\n' +
' ^^^^^^^^^^^^^^^',
);
expect(() => rawPath`?${mathObject}`).toThrow(
'Path parameters result in path with invalid segments:\n' +
'Value of type Math is not a valid path parameter\n' +
'?[object Math]\n' +
' ^^^^^^^^^^^^^',
);
expect(() => rawPath`/${basicClass}`).toThrow(
'Path parameters result in path with invalid segments:\n' +
'Value of type Object is not a valid path parameter\n' +
'/[object Object]\n' +
' ^^^^^^^^^^^^^^',
);
expect(() => rawPath`/../${''}`).toThrow(
'Path parameters result in path with invalid segments:\n' +
'Value ".." can\'t be safely passed as a path parameter\n' +
'/../\n' +
' ^^',
);
expect(() => rawPath`/../${{}}`).toThrow(
'Path parameters result in path with invalid segments:\n' +
'Value ".." can\'t be safely passed as a path parameter\n' +
'Value of type Object is not a valid path parameter\n' +
'/../[object Object]\n' +
' ^^ ^^^^^^^^^^^^^^',
);
// Valid values
expect(rawPath`/${0}`).toBe('/0');
expect(rawPath`/${''}`).toBe('/');
expect(rawPath`/${numberObject}`).toBe('/0');
expect(rawPath`${stringObject}/`).toBe('/');
expect(rawPath`/${classWithToString}`).toBe('/ok');
// We need to check what happens with cross-realm values, which we might get from
// Jest or other frames in a browser.
const newRealm = runInNewContext('globalThis');
expect(newRealm.Object).not.toBe(Object);
const crossRealmObject = newRealm.Object();
const crossRealmMathObject = newRealm.Math;
const crossRealmNumber = new newRealm.Number();
const crossRealmString = new newRealm.String();
const crossRealmClass = new (class extends newRealm.Object {})();
const crossRealmClassWithToString = new (class extends newRealm.Object {
toString() {
return 'ok';
}
})();
// Invalid cross-realm values
expect(() => rawPath`/a/${crossRealmObject}/b`).toThrow(
'Path parameters result in path with invalid segments:\n' +
'Value of type Object is not a valid path parameter\n' +
'/a/[object Object]/b\n' +
' ^^^^^^^^^^^^^^^',
);
expect(() => rawPath`?${crossRealmMathObject}`).toThrow(
'Path parameters result in path with invalid segments:\n' +
'Value of type Math is not a valid path parameter\n' +
'?[object Math]\n' +
' ^^^^^^^^^^^^^',
);
expect(() => rawPath`/${crossRealmClass}`).toThrow(
'Path parameters result in path with invalid segments:\n' +
'Value of type Object is not a valid path parameter\n' +
'/[object Object]\n' +
' ^^^^^^^^^^^^^^^',
);
// Valid cross-realm values
expect(rawPath`/${crossRealmNumber}`).toBe('/0');
expect(rawPath`${crossRealmString}/`).toBe('/');
expect(rawPath`/${crossRealmClassWithToString}`).toBe('/ok');
const results: {
[pathParts: string]: {
[params: string]: { valid: boolean; result?: string; error?: string };
};
} = {};
for (const pathParts of testCases) {
const pathResults: Record<string, { valid: boolean; result?: string; error?: string }> = {};
results[JSON.stringify(pathParts)] = pathResults;
for (const params of paramPermutations(pathParts.length - 1)) {
const stringRaw = String.raw({ raw: pathParts }, ...params);
const plainString = String.raw(
{ raw: pathParts.map((e) => e.replace(/\./g, 'x')) },
...params.map((e) => 'X'.repeat(e.length)),
);
const normalizedStringRaw = new URL(stringRaw, 'https://example.com').href;
const normalizedPlainString = new URL(plainString, 'https://example.com').href;
const pathResultsKey = JSON.stringify(params);
try {
const result = rawPath(pathParts, ...params);
expect(result).toBe(stringRaw);
// there are no special segments, so the length of the normalized path is
// equal to the length of the normalized plain path.
expect(normalizedStringRaw.length).toBe(normalizedPlainString.length);
pathResults[pathResultsKey] = {
valid: true,
result,
};
} catch (e) {
const error = String(e);
expect(error).toMatch(/Path parameters result in path with invalid segment/);
// there are special segments, so the length of the normalized path is
// different than the length of the normalized plain path.
expect(normalizedStringRaw.length).not.toBe(normalizedPlainString.length);
pathResults[pathResultsKey] = {
valid: false,
error,
};
}
}
}
expect(results).toMatchObject({
'["/path_params/","/a"]': {
'["x"]': { valid: true, result: '/path_params/x/a' },
'[""]': { valid: true, result: '/path_params//a' },
'["%2E%2e"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
'/path_params/%2E%2e/a\n' +
' ^^^^^^',
},
'["%2E"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2E" can\'t be safely passed as a path parameter\n' +
'/path_params/%2E/a\n' +
' ^^^',
},
},
'["/path_params/","/"]': {
'["x"]': { valid: true, result: '/path_params/x/' },
'[""]': { valid: true, result: '/path_params//' },
'["%2e%2E"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2e%2E" can\'t be safely passed as a path parameter\n' +
'/path_params/%2e%2E/\n' +
' ^^^^^^',
},
'["%2e"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2e" can\'t be safely passed as a path parameter\n' +
'/path_params/%2e/\n' +
' ^^^',
},
},
'["/path_params/",""]': {
'[""]': { valid: true, result: '/path_params/' },
'["x"]': { valid: true, result: '/path_params/x' },
'["%2E"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2E" can\'t be safely passed as a path parameter\n' +
'/path_params/%2E\n' +
' ^^^',
},
'["%2E%2e"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
'/path_params/%2E%2e\n' +
' ^^^^^^',
},
},
'["","/a"]': {
'[""]': { valid: true, result: '/a' },
'["x"]': { valid: true, result: 'x/a' },
'["%2E"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2E" can\'t be safely passed as a path parameter\n%2E/a\n^^^',
},
'["%2e%2E"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2e%2E" can\'t be safely passed as a path parameter\n' +
'%2e%2E/a\n' +
'^^^^^^',
},
},
'["","/"]': {
'["x"]': { valid: true, result: 'x/' },
'[""]': { valid: true, result: '/' },
'["%2E%2e"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
'%2E%2e/\n' +
'^^^^^^',
},
'["."]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "." can\'t be safely passed as a path parameter\n' +
'./\n^',
},
},
'["",""]': {
'[""]': { valid: true, result: '' },
'["x"]': { valid: true, result: 'x' },
'[".."]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value ".." can\'t be safely passed as a path parameter\n' +
'..\n^^',
},
'["."]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "." can\'t be safely passed as a path parameter\n' +
'.\n^',
},
},
'["a"]': {},
'[""]': {},
'["/path_params/",":initiate"]': {
'[""]': { valid: true, result: '/path_params/:initiate' },
'["."]': { valid: true, result: '/path_params/.:initiate' },
},
'["/path_params/",".json"]': {
'["x"]': { valid: true, result: '/path_params/x.json' },
'["."]': { valid: true, result: '/path_params/..json' },
},
'["/path_params/","?beta=true"]': {
'["x"]': { valid: true, result: '/path_params/x?beta=true' },
'[""]': { valid: true, result: '/path_params/?beta=true' },
'["%2E%2E"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2E%2E" can\'t be safely passed as a path parameter\n' +
'/path_params/%2E%2E?beta=true\n' +
' ^^^^^^',
},
'["%2e%2E"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2e%2E" can\'t be safely passed as a path parameter\n' +
'/path_params/%2e%2E?beta=true\n' +
' ^^^^^^',
},
},
'["/path_params/",".?beta=true"]': {
'[".."]': { valid: true, result: '/path_params/...?beta=true' },
'["x"]': { valid: true, result: '/path_params/x.?beta=true' },
'[""]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "." can\'t be safely passed as a path parameter\n' +
'/path_params/.?beta=true\n' +
' ^',
},
'["%2e"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2e." can\'t be safely passed as a path parameter\n' +
'/path_params/%2e.?beta=true\n' +
' ^^^^',
},
},
'["/path_params/","/","/download"]': {
'["",""]': { valid: true, result: '/path_params///download' },
'["","x"]': { valid: true, result: '/path_params//x/download' },
'[".","%2e"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "." can\'t be safely passed as a path parameter\n' +
'Value "%2e" can\'t be safely passed as a path parameter\n' +
'/path_params/./%2e/download\n' +
' ^ ^^^',
},
'["%2E%2e","%2e"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
'Value "%2e" can\'t be safely passed as a path parameter\n' +
'/path_params/%2E%2e/%2e/download\n' +
' ^^^^^^ ^^^',
},
},
'["/path_params/","-","/download"]': {
'["","%2e"]': { valid: true, result: '/path_params/-%2e/download' },
'["%2E",".."]': { valid: true, result: '/path_params/%2E-../download' },
},
'["/path_params/","","/download"]': {
'["%2E%2e","%2e%2E"]': { valid: true, result: '/path_params/%2E%2e%2e%2E/download' },
'["%2E",".."]': { valid: true, result: '/path_params/%2E../download' },
'["","%2E"]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2E" can\'t be safely passed as a path parameter\n' +
'/path_params/%2E/download\n' +
' ^^^',
},
'["%2E","."]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "%2E." can\'t be safely passed as a path parameter\n' +
'/path_params/%2E./download\n' +
' ^^^^',
},
},
'["/path_params/",".","/download"]': {
'["%2e%2e",""]': { valid: true, result: '/path_params/%2e%2e./download' },
'["","%2e%2e"]': { valid: true, result: '/path_params/.%2e%2e/download' },
'["",""]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value "." can\'t be safely passed as a path parameter\n' +
'/path_params/./download\n' +
' ^',
},
'["","."]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value ".." can\'t be safely passed as a path parameter\n' +
'/path_params/../download\n' +
' ^^',
},
},
'["/path_params/","..","/download"]': {
'["","%2E"]': { valid: true, result: '/path_params/..%2E/download' },
'["","x"]': { valid: true, result: '/path_params/..x/download' },
'["",""]': {
valid: false,
error:
'Error: Path parameters result in path with invalid segments:\n' +
'Value ".." can\'t be safely passed as a path parameter\n' +
'/path_params/../download\n' +
' ^^',
},
},
});
});
});
describe('encodeURIPath', () => {
const testCases: string[] = [
'',
// Every ASCII character
...Array.from({ length: 0x7f }, (_, i) => String.fromCharCode(i)),
// Unicode BMP codepoint
'å',
// Unicode supplementary codepoint
'😃',
];
for (const param of testCases) {
test('properly encodes ' + inspect(param), () => {
const encoded = encodeURIPath(param);
const naiveEncoded = encodeURIComponent(param);
// we should never encode more characters than encodeURIComponent
expect(naiveEncoded.length).toBeGreaterThanOrEqual(encoded.length);
expect(decodeURIComponent(encoded)).toBe(param);
});
}
test("leaves ':' intact", () => {
expect(encodeURIPath(':')).toBe(':');
});
test("leaves '@' intact", () => {
expect(encodeURIPath('@')).toBe('@');
});
});
```
--------------------------------------------------------------------------------
/src/resources/products/products.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../../core/resource';
import * as ProductsAPI from './products';
import * as MiscAPI from '../misc';
import * as SubscriptionsAPI from '../subscriptions';
import * as ImagesAPI from './images';
import { ImageUpdateParams, ImageUpdateResponse, Images } from './images';
import { APIPromise } from '../../core/api-promise';
import {
DefaultPageNumberPagination,
type DefaultPageNumberPaginationParams,
PagePromise,
} from '../../core/pagination';
import { buildHeaders } from '../../internal/headers';
import { RequestOptions } from '../../internal/request-options';
import { path } from '../../internal/utils/path';
export class Products extends APIResource {
images: ImagesAPI.Images = new ImagesAPI.Images(this._client);
create(body: ProductCreateParams, options?: RequestOptions): APIPromise<Product> {
return this._client.post('/products', { body, ...options });
}
retrieve(id: string, options?: RequestOptions): APIPromise<Product> {
return this._client.get(path`/products/${id}`, options);
}
update(id: string, body: ProductUpdateParams, options?: RequestOptions): APIPromise<void> {
return this._client.patch(path`/products/${id}`, {
body,
...options,
headers: buildHeaders([{ Accept: '*/*' }, options?.headers]),
});
}
list(
query: ProductListParams | null | undefined = {},
options?: RequestOptions,
): PagePromise<ProductListResponsesDefaultPageNumberPagination, ProductListResponse> {
return this._client.getAPIList('/products', DefaultPageNumberPagination<ProductListResponse>, {
query,
...options,
});
}
archive(id: string, options?: RequestOptions): APIPromise<void> {
return this._client.delete(path`/products/${id}`, {
...options,
headers: buildHeaders([{ Accept: '*/*' }, options?.headers]),
});
}
unarchive(id: string, options?: RequestOptions): APIPromise<void> {
return this._client.post(path`/products/${id}/unarchive`, {
...options,
headers: buildHeaders([{ Accept: '*/*' }, options?.headers]),
});
}
updateFiles(
id: string,
body: ProductUpdateFilesParams,
options?: RequestOptions,
): APIPromise<ProductUpdateFilesResponse> {
return this._client.put(path`/products/${id}/files`, { body, ...options });
}
}
export type ProductListResponsesDefaultPageNumberPagination =
DefaultPageNumberPagination<ProductListResponse>;
export interface AddMeterToPrice {
meter_id: string;
/**
* The price per unit in lowest denomination. Must be greater than zero. Supports
* up to 5 digits before decimal point and 12 decimal places.
*/
price_per_unit: string;
/**
* Meter description. Will ignored on Request, but will be shown in response
*/
description?: string | null;
free_threshold?: number | null;
/**
* Meter measurement unit. Will ignored on Request, but will be shown in response
*/
measurement_unit?: string | null;
/**
* Meter name. Will ignored on Request, but will be shown in response
*/
name?: string | null;
}
export interface LicenseKeyDuration {
count: number;
interval: SubscriptionsAPI.TimeInterval;
}
/**
* One-time price details.
*/
export type Price = Price.OneTimePrice | Price.RecurringPrice | Price.UsageBasedPrice;
export namespace Price {
/**
* One-time price details.
*/
export interface OneTimePrice {
/**
* The currency in which the payment is made.
*/
currency: MiscAPI.Currency;
/**
* Discount applied to the price, represented as a percentage (0 to 100).
*/
discount: number;
/**
* The payment amount, in the smallest denomination of the currency (e.g., cents
* for USD). For example, to charge $1.00, pass `100`.
*
* If [`pay_what_you_want`](Self::pay_what_you_want) is set to `true`, this field
* represents the **minimum** amount the customer must pay.
*/
price: number;
/**
* Indicates if purchasing power parity adjustments are applied to the price.
* Purchasing power parity feature is not available as of now.
*/
purchasing_power_parity: boolean;
type: 'one_time_price';
/**
* Indicates whether the customer can pay any amount they choose. If set to `true`,
* the [`price`](Self::price) field is the minimum amount.
*/
pay_what_you_want?: boolean;
/**
* A suggested price for the user to pay. This value is only considered if
* [`pay_what_you_want`](Self::pay_what_you_want) is `true`. Otherwise, it is
* ignored.
*/
suggested_price?: number | null;
/**
* Indicates if the price is tax inclusive.
*/
tax_inclusive?: boolean | null;
}
/**
* Recurring price details.
*/
export interface RecurringPrice {
/**
* The currency in which the payment is made.
*/
currency: MiscAPI.Currency;
/**
* Discount applied to the price, represented as a percentage (0 to 100).
*/
discount: number;
/**
* Number of units for the payment frequency. For example, a value of `1` with a
* `payment_frequency_interval` of `month` represents monthly payments.
*/
payment_frequency_count: number;
/**
* The time interval for the payment frequency (e.g., day, month, year).
*/
payment_frequency_interval: SubscriptionsAPI.TimeInterval;
/**
* The payment amount. Represented in the lowest denomination of the currency
* (e.g., cents for USD). For example, to charge $1.00, pass `100`.
*/
price: number;
/**
* Indicates if purchasing power parity adjustments are applied to the price.
* Purchasing power parity feature is not available as of now
*/
purchasing_power_parity: boolean;
/**
* Number of units for the subscription period. For example, a value of `12` with a
* `subscription_period_interval` of `month` represents a one-year subscription.
*/
subscription_period_count: number;
/**
* The time interval for the subscription period (e.g., day, month, year).
*/
subscription_period_interval: SubscriptionsAPI.TimeInterval;
type: 'recurring_price';
/**
* Indicates if the price is tax inclusive
*/
tax_inclusive?: boolean | null;
/**
* Number of days for the trial period. A value of `0` indicates no trial period.
*/
trial_period_days?: number;
}
/**
* Usage Based price details.
*/
export interface UsageBasedPrice {
/**
* The currency in which the payment is made.
*/
currency: MiscAPI.Currency;
/**
* Discount applied to the price, represented as a percentage (0 to 100).
*/
discount: number;
/**
* The fixed payment amount. Represented in the lowest denomination of the currency
* (e.g., cents for USD). For example, to charge $1.00, pass `100`.
*/
fixed_price: number;
/**
* Number of units for the payment frequency. For example, a value of `1` with a
* `payment_frequency_interval` of `month` represents monthly payments.
*/
payment_frequency_count: number;
/**
* The time interval for the payment frequency (e.g., day, month, year).
*/
payment_frequency_interval: SubscriptionsAPI.TimeInterval;
/**
* Indicates if purchasing power parity adjustments are applied to the price.
* Purchasing power parity feature is not available as of now
*/
purchasing_power_parity: boolean;
/**
* Number of units for the subscription period. For example, a value of `12` with a
* `subscription_period_interval` of `month` represents a one-year subscription.
*/
subscription_period_count: number;
/**
* The time interval for the subscription period (e.g., day, month, year).
*/
subscription_period_interval: SubscriptionsAPI.TimeInterval;
type: 'usage_based_price';
meters?: Array<ProductsAPI.AddMeterToPrice> | null;
/**
* Indicates if the price is tax inclusive
*/
tax_inclusive?: boolean | null;
}
}
export interface Product {
brand_id: string;
/**
* Unique identifier for the business to which the product belongs.
*/
business_id: string;
/**
* Timestamp when the product was created.
*/
created_at: string;
/**
* Indicates if the product is recurring (e.g., subscriptions).
*/
is_recurring: boolean;
/**
* Indicates whether the product requires a license key.
*/
license_key_enabled: boolean;
/**
* Additional custom data associated with the product
*/
metadata: { [key: string]: string };
/**
* Pricing information for the product.
*/
price: Price;
/**
* Unique identifier for the product.
*/
product_id: string;
/**
* Tax category associated with the product.
*/
tax_category: MiscAPI.TaxCategory;
/**
* Timestamp when the product was last updated.
*/
updated_at: string;
/**
* Available Addons for subscription products
*/
addons?: Array<string> | null;
/**
* Description of the product, optional.
*/
description?: string | null;
digital_product_delivery?: Product.DigitalProductDelivery | null;
/**
* URL of the product image, optional.
*/
image?: string | null;
/**
* Message sent upon license key activation, if applicable.
*/
license_key_activation_message?: string | null;
/**
* Limit on the number of activations for the license key, if enabled.
*/
license_key_activations_limit?: number | null;
/**
* Duration of the license key validity, if enabled.
*/
license_key_duration?: LicenseKeyDuration | null;
/**
* Name of the product, optional.
*/
name?: string | null;
}
export namespace Product {
export interface DigitalProductDelivery {
/**
* External URL to digital product
*/
external_url?: string | null;
/**
* Uploaded files ids of digital product
*/
files?: Array<DigitalProductDelivery.File> | null;
/**
* Instructions to download and use the digital product
*/
instructions?: string | null;
}
export namespace DigitalProductDelivery {
export interface File {
file_id: string;
file_name: string;
url: string;
}
}
}
export interface ProductListResponse {
/**
* Unique identifier for the business to which the product belongs.
*/
business_id: string;
/**
* Timestamp when the product was created.
*/
created_at: string;
/**
* Indicates if the product is recurring (e.g., subscriptions).
*/
is_recurring: boolean;
/**
* Additional custom data associated with the product
*/
metadata: { [key: string]: string };
/**
* Unique identifier for the product.
*/
product_id: string;
/**
* Tax category associated with the product.
*/
tax_category: MiscAPI.TaxCategory;
/**
* Timestamp when the product was last updated.
*/
updated_at: string;
/**
* Currency of the price
*/
currency?: MiscAPI.Currency | null;
/**
* Description of the product, optional.
*/
description?: string | null;
/**
* URL of the product image, optional.
*/
image?: string | null;
/**
* Name of the product, optional.
*/
name?: string | null;
/**
* Price of the product, optional.
*
* The price is represented in the lowest denomination of the currency. For
* example:
*
* - In USD, a price of `$12.34` would be represented as `1234` (cents).
* - In JPY, a price of `¥1500` would be represented as `1500` (yen).
* - In INR, a price of `₹1234.56` would be represented as `123456` (paise).
*
* This ensures precision and avoids floating-point rounding errors.
*/
price?: number | null;
/**
* Details of the price
*/
price_detail?: Price | null;
/**
* Indicates if the price is tax inclusive
*/
tax_inclusive?: boolean | null;
}
export interface ProductUpdateFilesResponse {
file_id: string;
url: string;
}
export interface ProductCreateParams {
/**
* Price configuration for the product
*/
price: Price;
/**
* Tax category applied to this product
*/
tax_category: MiscAPI.TaxCategory;
/**
* Addons available for subscription product
*/
addons?: Array<string> | null;
/**
* Brand id for the product, if not provided will default to primary brand
*/
brand_id?: string | null;
/**
* Optional description of the product
*/
description?: string | null;
/**
* Choose how you would like you digital product delivered
*/
digital_product_delivery?: ProductCreateParams.DigitalProductDelivery | null;
/**
* Optional message displayed during license key activation
*/
license_key_activation_message?: string | null;
/**
* The number of times the license key can be activated. Must be 0 or greater
*/
license_key_activations_limit?: number | null;
/**
* Duration configuration for the license key. Set to null if you don't want the
* license key to expire. For subscriptions, the lifetime of the license key is
* tied to the subscription period
*/
license_key_duration?: LicenseKeyDuration | null;
/**
* When true, generates and sends a license key to your customer. Defaults to false
*/
license_key_enabled?: boolean | null;
/**
* Additional metadata for the product
*/
metadata?: { [key: string]: string };
/**
* Optional name of the product
*/
name?: string | null;
}
export namespace ProductCreateParams {
/**
* Choose how you would like you digital product delivered
*/
export interface DigitalProductDelivery {
/**
* External URL to digital product
*/
external_url?: string | null;
/**
* Instructions to download and use the digital product
*/
instructions?: string | null;
}
}
export interface ProductUpdateParams {
/**
* Available Addons for subscription products
*/
addons?: Array<string> | null;
brand_id?: string | null;
/**
* Description of the product, optional and must be at most 1000 characters.
*/
description?: string | null;
/**
* Choose how you would like you digital product delivered
*/
digital_product_delivery?: ProductUpdateParams.DigitalProductDelivery | null;
/**
* Product image id after its uploaded to S3
*/
image_id?: string | null;
/**
* Message sent to the customer upon license key activation.
*
* Only applicable if `license_key_enabled` is `true`. This message contains
* instructions for activating the license key.
*/
license_key_activation_message?: string | null;
/**
* Limit for the number of activations for the license key.
*
* Only applicable if `license_key_enabled` is `true`. Represents the maximum
* number of times the license key can be activated.
*/
license_key_activations_limit?: number | null;
/**
* Duration of the license key if enabled.
*
* Only applicable if `license_key_enabled` is `true`. Represents the duration in
* days for which the license key is valid.
*/
license_key_duration?: LicenseKeyDuration | null;
/**
* Whether the product requires a license key.
*
* If `true`, additional fields related to license key (duration, activations
* limit, activation message) become applicable.
*/
license_key_enabled?: boolean | null;
/**
* Additional metadata for the product
*/
metadata?: { [key: string]: string } | null;
/**
* Name of the product, optional and must be at most 100 characters.
*/
name?: string | null;
/**
* Price details of the product.
*/
price?: Price | null;
/**
* Tax category of the product.
*/
tax_category?: MiscAPI.TaxCategory | null;
}
export namespace ProductUpdateParams {
/**
* Choose how you would like you digital product delivered
*/
export interface DigitalProductDelivery {
/**
* External URL to digital product
*/
external_url?: string | null;
/**
* Uploaded files ids of digital product
*/
files?: Array<string> | null;
/**
* Instructions to download and use the digital product
*/
instructions?: string | null;
}
}
export interface ProductListParams extends DefaultPageNumberPaginationParams {
/**
* List archived products
*/
archived?: boolean;
/**
* filter by Brand id
*/
brand_id?: string;
/**
* Filter products by pricing type:
*
* - `true`: Show only recurring pricing products (e.g. subscriptions)
* - `false`: Show only one-time price products
* - `null` or absent: Show both types of products
*/
recurring?: boolean;
}
export interface ProductUpdateFilesParams {
file_name: string;
}
Products.Images = Images;
export declare namespace Products {
export {
type AddMeterToPrice as AddMeterToPrice,
type LicenseKeyDuration as LicenseKeyDuration,
type Price as Price,
type Product as Product,
type ProductListResponse as ProductListResponse,
type ProductUpdateFilesResponse as ProductUpdateFilesResponse,
type ProductListResponsesDefaultPageNumberPagination as ProductListResponsesDefaultPageNumberPagination,
type ProductCreateParams as ProductCreateParams,
type ProductUpdateParams as ProductUpdateParams,
type ProductListParams as ProductListParams,
type ProductUpdateFilesParams as ProductUpdateFilesParams,
};
export {
Images as Images,
type ImageUpdateResponse as ImageUpdateResponse,
type ImageUpdateParams as ImageUpdateParams,
};
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/cloudflare-worker/src/utils.ts:
--------------------------------------------------------------------------------
```typescript
// Helper to generate the layout
import { html, raw } from 'hono/html';
import type { HtmlEscapedString } from 'hono/utils/html';
import { marked } from 'marked';
import type { AuthRequest } from '@cloudflare/workers-oauth-provider';
import { env } from 'cloudflare:workers';
import { ServerConfig, McpOptions, ClientType, Filter, ClientProperty } from 'dodopayments-mcp/server';
export const layout = (content: HtmlEscapedString | string, title: string, config: ServerConfig) => html`
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${title} - ${config.orgName} MCP server</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3498db',
secondary: '#2ecc71',
accent: '#f39c12',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
heading: ['Roboto', 'system-ui', 'sans-serif'],
},
},
},
};
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap');
/* Custom styling for markdown content */
.markdown h1 {
font-size: 2.25rem;
font-weight: 700;
font-family: 'Roboto', system-ui, sans-serif;
color: #1a202c;
margin-bottom: 1rem;
line-height: 1.2;
}
.markdown h2 {
font-size: 1.5rem;
font-weight: 600;
font-family: 'Roboto', system-ui, sans-serif;
color: #2d3748;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
line-height: 1.3;
}
.markdown h3 {
font-size: 1.25rem;
font-weight: 600;
font-family: 'Roboto', system-ui, sans-serif;
color: #2d3748;
margin-top: 1.25rem;
margin-bottom: 0.5rem;
}
.markdown p {
font-size: 1.125rem;
color: #4a5568;
margin-bottom: 1rem;
line-height: 1.6;
}
.markdown a {
color: #3498db;
font-weight: 500;
text-decoration: none;
}
.markdown a:hover {
text-decoration: underline;
}
.markdown blockquote {
border-left: 4px solid #f39c12;
padding-left: 1rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
margin-top: 1.5rem;
margin-bottom: 1.5rem;
background-color: #fffbeb;
font-style: italic;
}
.markdown blockquote p {
margin-bottom: 0.25rem;
}
.markdown ul,
.markdown ol {
margin-top: 1rem;
margin-bottom: 1rem;
margin-left: 1.5rem;
font-size: 1.125rem;
color: #4a5568;
}
.markdown li {
margin-bottom: 0.5rem;
}
.markdown ul li {
list-style-type: disc;
}
.markdown ol li {
list-style-type: decimal;
}
.markdown pre {
background-color: #f7fafc;
padding: 1rem;
border-radius: 0.375rem;
margin-top: 1rem;
margin-bottom: 1rem;
overflow-x: auto;
}
.markdown code {
font-family: monospace;
font-size: 0.875rem;
background-color: #f7fafc;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
}
.markdown pre code {
background-color: transparent;
padding: 0;
}
</style>
</head>
<body class="bg-gray-50 text-gray-800 font-sans leading-relaxed flex flex-col min-h-screen">
<main class="container mx-auto px-4 pb-12 flex-grow">${content}</main>
<footer class="bg-gray-100 py-6 mt-12">
<div class="container mx-auto px-4 text-center text-gray-600">
<p>© ${new Date().getFullYear()} ${config.orgName}. All rights reserved.</p>
</div>
</footer>
</body>
</html>
`;
export const homeContent = async (req: Request): Promise<HtmlEscapedString> => {
// We have the README symlinked into the static directory, so we can fetch it
// and render it into HTML
const origin = new URL(req.url).origin;
const res = await env.ASSETS.fetch(`${origin}/home.md`);
let markdown = await res.text();
markdown = markdown.replaceAll('{{cloudflareWorkerUrl}}', origin + '/sse');
const content = await marked(markdown);
return html` <div class="max-w-4xl mx-auto markdown">${raw(content)}</div> `;
};
export const renderLoggedOutAuthorizeScreen = async (
config: ServerConfig,
oauthReqInfo: AuthRequest,
defaultOptions?: Partial<McpOptions>,
) => {
const checked = (condition: boolean) => (condition ? 'checked' : '');
const selected = (condition: boolean) => (condition ? 'selected' : '');
// Helper to check if a capability is enabled by default
const hasCapability = (capability: string) => {
if (!defaultOptions?.capabilities) {
// Default capabilities when none specified
return ['refs', 'unions', 'formats'].includes(capability);
}
switch (capability) {
case 'top-level-unions':
return defaultOptions.capabilities.topLevelUnions || false;
case 'valid-json':
return defaultOptions.capabilities.validJson || false;
case 'refs':
return defaultOptions.capabilities.refs || false;
case 'unions':
return defaultOptions.capabilities.unions || false;
case 'formats':
return defaultOptions.capabilities.formats || false;
default:
return false;
}
};
// Helper to check if an operation is enabled by default
const hasOperation = (operation: string) => {
if (!defaultOptions?.filters) {
// Default operations when none specified
return ['read', 'write'].includes(operation);
}
return defaultOptions.filters.some(
(f) => f.type === 'operation' && f.op === 'include' && f.value === operation,
);
};
const renderField = (field: ClientProperty) => {
if (field.type === 'select' && field.options) {
return html`
<div>
<label for="${`clientopt_${field.key}`}" class="block text-sm font-medium text-gray-700 mb-1"
>${field.label}</label
>
<select
id="${`clientopt_${field.key}`}"
name="${`clientopt_${field.key}`}"
${field.required ? 'required' : ''}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
>
${field.options.map(
(opt: { label: string; value: string }) => html`
<option value="${opt.value}" ${field.default === opt.value ? 'selected' : ''}>
${opt.label}
</option>
`,
)}
</select>
</div>
`;
}
return html`
<div>
<label for="${`clientopt_${field.key}`}" class="block text-sm font-medium text-gray-700 mb-1"
>${field.label}</label
>
<input
type="${field.type}"
id="${`clientopt_${field.key}`}"
name="${`clientopt_${field.key}`}"
${field.required ? 'required' : ''}
${field.placeholder ? html`placeholder="${field.placeholder}"` : ''}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
/>
</div>
`;
};
return html`
<div class="max-w-md mx-auto bg-white p-8 rounded-lg shadow-md">
${config.logoUrl ? html`<img src="${config.logoUrl}" class="w-24 mb-6 mx-auto" />` : ''}
<h1 class="text-2xl font-heading font-bold mb-6 text-gray-900">
Authorizing ${config.orgName} MCP server
</h1>
<div class="mb-8">
<h2 class="text-lg font-semibold mb-3 text-gray-800">
Enter your credentials to initialize the connection with your MCP client.
</h2>
If you're not sure how to configure your client, see the
${config.instructionsUrl ?
html`<a
href="${config.instructionsUrl}"
class="text-primary hover:text-primary/80 transition-colors"
>instructions</a
>`
: 'instructions'}
to get started.
</div>
<form action="/approve" method="POST" class="space-y-4">
<input type="hidden" name="oauthReqInfo" value="${JSON.stringify(oauthReqInfo)}" />
<div class="space-y-4">${config.clientProperties.map(renderField)}</div>
<div class="mt-6 border-t pt-4">
<details class="w-full">
<summary class="font-medium text-primary cursor-pointer hover:text-primary/80 transition-colors">
Configuration Options
</summary>
<div class="mt-4 space-y-5 bg-gray-50 p-4 rounded-md">
<div>
<label for="client" class="flex items-center text-sm font-medium text-gray-700 mb-1">
MCP Client
<span class="relative group ml-1 align-middle">
<span
tabindex="0"
class="inline-block w-4 h-4 rounded-full bg-gray-200 text-gray-600 text-xs font-bold flex items-center justify-center cursor-pointer group-hover:bg-gray-300 focus:bg-gray-300"
aria-label="Help"
>
?
</span>
<span
class="absolute left-1/2 z-10 w-64 -translate-x-1/2 mt-2 px-3 py-2 rounded bg-gray-800 text-xs text-white opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 pointer-events-none group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-opacity duration-200"
>
Specify the client that you're connecting from. If yours is not listed, Claude is a
reasonable default.
</span>
</span>
</label>
<select
id="client"
name="client"
onchange="toggleClientCapabilities()"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
>
<option
value="infer"
${selected(defaultOptions?.client === 'infer' || !defaultOptions?.client)}
>
Infer client
</option>
<option value="claude" ${selected(defaultOptions?.client === 'claude')}>Claude</option>
<option value="cursor" ${selected(defaultOptions?.client === 'cursor')}>Cursor</option>
<option value="claude-code" ${selected(defaultOptions?.client === 'claude-code')}>
Claude Code
</option>
<option value="openai-agents" ${selected(defaultOptions?.client === 'openai-agents')}>
OpenAI Agents SDK
</option>
</select>
</div>
<div class="flex items-center">
<input
type="checkbox"
id="dynamic_tools"
name="dynamic_tools"
${checked(defaultOptions?.includeDynamicTools || false)}
class="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
/>
<label for="dynamic_tools" class="ml-2 block text-sm text-gray-700"> Dynamic Tools </label>
<div class="relative group ml-2">
<span
tabindex="0"
class="inline-block w-4 h-4 rounded-full bg-gray-200 text-gray-600 text-xs font-bold flex items-center justify-center cursor-pointer group-hover:bg-gray-300 focus:bg-gray-300"
aria-label="Help"
>
?
</span>
<div
class="absolute left-1/2 z-10 w-64 -translate-x-1/2 mt-2 px-3 py-2 rounded bg-gray-800 text-xs text-white opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 pointer-events-none group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-opacity duration-200"
>
Have the LLM dynamically discover the endpoints, instead of directly exposing one tool per
endpoint.
</div>
</div>
</div>
<div>
<div class="flex items-center">
<input
type="checkbox"
id="read_only_operations"
name="read_only_operations"
${!hasOperation('write') ? 'checked' : ''}
class="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
/>
<label for="read_only_operations" class="ml-2 block text-sm text-gray-700">
Read-only
</label>
<div class="relative group ml-2">
<span
tabindex="0"
class="inline-block w-4 h-4 rounded-full bg-gray-200 text-gray-600 text-xs font-bold flex items-center justify-center cursor-pointer group-hover:bg-gray-300 focus:bg-gray-300"
aria-label="Help"
>
?
</span>
<div
class="absolute left-1/2 z-10 w-64 -translate-x-1/2 mt-2 px-3 py-2 rounded bg-gray-800 text-xs text-white opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 pointer-events-none group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-opacity duration-200"
>
Restrict the available tools to only be able to read data.
</div>
</div>
</div>
</div>
</div>
</details>
</div>
<button
type="submit"
name="action"
value="login_approve"
class="w-full py-3 px-4 bg-primary text-white rounded-md font-medium hover:bg-primary/90 transition-colors"
>
Log in and Approve
</button>
<button
type="submit"
name="action"
value="reject"
class="w-full py-3 px-4 border border-gray-300 text-gray-700 rounded-md font-medium hover:bg-gray-50 transition-colors"
>
Reject
</button>
</form>
</div>
`;
};
export const renderApproveContent = async (message: string, status: string, redirectUrl: string) => {
return html`
<div class="max-w-md mx-auto bg-white p-8 rounded-lg shadow-md text-center">
<div class="mb-4">
<span
class="inline-block p-3 ${status === 'success' ?
'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'} rounded-full"
>
${status === 'success' ? '✓' : '✗'}
</span>
</div>
<h1 class="text-2xl font-heading font-bold mb-4 text-gray-900">${message}</h1>
<p class="mb-8 text-gray-600">You will be redirected back to the application shortly.</p>
<a
href="/"
class="inline-block py-2 px-4 bg-primary text-white rounded-md font-medium hover:bg-primary/90 transition-colors"
>
Return to Home
</a>
${raw(`
<script>
setTimeout(() => {
window.location.href = "${redirectUrl}";
}, 2000);
</script>
`)}
</div>
`;
};
export const renderAuthorizationApprovedContent = async (redirectUrl: string) => {
return renderApproveContent('Authorization approved!', 'success', redirectUrl);
};
export const renderAuthorizationRejectedContent = async (redirectUrl: string) => {
return renderApproveContent('Authorization rejected.', 'error', redirectUrl);
};
export const parseApproveFormBody = async (
body: {
[x: string]: string | File;
},
config: ServerConfig,
) => {
const parsedClientProps = Object.fromEntries(
config.clientProperties.map((prop: ClientProperty) => {
const rawValue = body[`clientopt_${prop.key}`];
const value = prop.type === 'number' ? Number(rawValue) : rawValue;
return [prop.key, value];
}),
);
const filters: Filter[] = [];
if (body.read_only_operations === 'on') {
filters.push({
type: 'operation',
op: 'exclude',
value: 'write',
});
}
// Parse advanced options
const clientConfig: McpOptions = {
client: (body.client as ClientType) || undefined,
includeDynamicTools: body.dynamic_tools === 'on',
includeAllTools: body.dynamic_tools !== 'on',
filters,
};
let oauthReqInfo: AuthRequest | null = null;
try {
oauthReqInfo = JSON.parse(body.oauthReqInfo as string) as AuthRequest;
if (Object.keys(oauthReqInfo).length === 0) {
oauthReqInfo = null;
}
} catch (e) {
oauthReqInfo = null;
}
return { oauthReqInfo, clientProps: parsedClientProps, clientConfig, action: body.action };
};
```
--------------------------------------------------------------------------------
/src/resources/subscriptions.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../core/resource';
import * as MiscAPI from './misc';
import * as PaymentsAPI from './payments';
import { APIPromise } from '../core/api-promise';
import {
DefaultPageNumberPagination,
type DefaultPageNumberPaginationParams,
PagePromise,
} from '../core/pagination';
import { buildHeaders } from '../internal/headers';
import { RequestOptions } from '../internal/request-options';
import { path } from '../internal/utils/path';
export class Subscriptions extends APIResource {
create(body: SubscriptionCreateParams, options?: RequestOptions): APIPromise<SubscriptionCreateResponse> {
return this._client.post('/subscriptions', { body, ...options });
}
retrieve(subscriptionID: string, options?: RequestOptions): APIPromise<Subscription> {
return this._client.get(path`/subscriptions/${subscriptionID}`, options);
}
update(
subscriptionID: string,
body: SubscriptionUpdateParams,
options?: RequestOptions,
): APIPromise<Subscription> {
return this._client.patch(path`/subscriptions/${subscriptionID}`, { body, ...options });
}
list(
query: SubscriptionListParams | null | undefined = {},
options?: RequestOptions,
): PagePromise<SubscriptionListResponsesDefaultPageNumberPagination, SubscriptionListResponse> {
return this._client.getAPIList('/subscriptions', DefaultPageNumberPagination<SubscriptionListResponse>, {
query,
...options,
});
}
changePlan(
subscriptionID: string,
body: SubscriptionChangePlanParams,
options?: RequestOptions,
): APIPromise<void> {
return this._client.post(path`/subscriptions/${subscriptionID}/change-plan`, {
body,
...options,
headers: buildHeaders([{ Accept: '*/*' }, options?.headers]),
});
}
charge(
subscriptionID: string,
body: SubscriptionChargeParams,
options?: RequestOptions,
): APIPromise<SubscriptionChargeResponse> {
return this._client.post(path`/subscriptions/${subscriptionID}/charge`, { body, ...options });
}
/**
* Get detailed usage history for a subscription that includes usage-based billing
* (metered components). This endpoint provides insights into customer usage
* patterns and billing calculations over time.
*
* ## What You'll Get:
*
* - **Billing periods**: Each item represents a billing cycle with start and end
* dates
* - **Meter usage**: Detailed breakdown of usage for each meter configured on the
* subscription
* - **Usage calculations**: Total units consumed, free threshold units, and
* chargeable units
* - **Historical tracking**: Complete audit trail of usage-based charges
*
* ## Use Cases:
*
* - **Customer support**: Investigate billing questions and usage discrepancies
* - **Usage analytics**: Analyze customer consumption patterns over time
* - **Billing transparency**: Provide customers with detailed usage breakdowns
* - **Revenue optimization**: Identify usage trends to optimize pricing strategies
*
* ## Filtering Options:
*
* - **Date range filtering**: Get usage history for specific time periods
* - **Meter-specific filtering**: Focus on usage for a particular meter
* - **Pagination**: Navigate through large usage histories efficiently
*
* ## Important Notes:
*
* - Only returns data for subscriptions with usage-based (metered) components
* - Usage history is organized by billing periods (subscription cycles)
* - Free threshold units are calculated and displayed separately from chargeable
* units
* - Historical data is preserved even if meter configurations change
*
* ## Example Query Patterns:
*
* - Get last 3 months:
* `?start_date=2024-01-01T00:00:00Z&end_date=2024-03-31T23:59:59Z`
* - Filter by meter: `?meter_id=mtr_api_requests`
* - Paginate results: `?page_size=20&page_number=1`
* - Recent usage: `?start_date=2024-03-01T00:00:00Z` (from March 1st to now)
*/
retrieveUsageHistory(
subscriptionID: string,
query: SubscriptionRetrieveUsageHistoryParams | null | undefined = {},
options?: RequestOptions,
): PagePromise<
SubscriptionRetrieveUsageHistoryResponsesDefaultPageNumberPagination,
SubscriptionRetrieveUsageHistoryResponse
> {
return this._client.getAPIList(
path`/subscriptions/${subscriptionID}/usage-history`,
DefaultPageNumberPagination<SubscriptionRetrieveUsageHistoryResponse>,
{ query, ...options },
);
}
}
export type SubscriptionListResponsesDefaultPageNumberPagination =
DefaultPageNumberPagination<SubscriptionListResponse>;
export type SubscriptionRetrieveUsageHistoryResponsesDefaultPageNumberPagination =
DefaultPageNumberPagination<SubscriptionRetrieveUsageHistoryResponse>;
/**
* Response struct representing subscription details
*/
export interface AddonCartResponseItem {
addon_id: string;
quantity: number;
}
export interface AttachAddon {
addon_id: string;
quantity: number;
}
export interface OnDemandSubscription {
/**
* If set as True, does not perform any charge and only authorizes payment method
* details for future use.
*/
mandate_only: boolean;
/**
* Whether adaptive currency fees should be included in the product_price (true) or
* added on top (false). This field is ignored if adaptive pricing is not enabled
* for the business.
*/
adaptive_currency_fees_inclusive?: boolean | null;
/**
* Optional currency of the product price. If not specified, defaults to the
* currency of the product.
*/
product_currency?: MiscAPI.Currency | null;
/**
* Optional product description override for billing and line items. If not
* specified, the stored description of the product will be used.
*/
product_description?: string | null;
/**
* Product price for the initial charge to customer If not specified the stored
* price of the product will be used Represented in the lowest denomination of the
* currency (e.g., cents for USD). For example, to charge $1.00, pass `100`.
*/
product_price?: number | null;
}
/**
* Response struct representing subscription details
*/
export interface Subscription {
/**
* Addons associated with this subscription
*/
addons: Array<AddonCartResponseItem>;
/**
* Billing address details for payments
*/
billing: PaymentsAPI.BillingAddress;
/**
* Indicates if the subscription will cancel at the next billing date
*/
cancel_at_next_billing_date: boolean;
/**
* Timestamp when the subscription was created
*/
created_at: string;
/**
* Currency used for the subscription payments
*/
currency: MiscAPI.Currency;
/**
* Customer details associated with the subscription
*/
customer: PaymentsAPI.CustomerLimitedDetails;
/**
* Additional custom data associated with the subscription
*/
metadata: { [key: string]: string };
/**
* Meters associated with this subscription (for usage-based billing)
*/
meters: Array<Subscription.Meter>;
/**
* Timestamp of the next scheduled billing. Indicates the end of current billing
* period
*/
next_billing_date: string;
/**
* Wether the subscription is on-demand or not
*/
on_demand: boolean;
/**
* Number of payment frequency intervals
*/
payment_frequency_count: number;
/**
* Time interval for payment frequency (e.g. month, year)
*/
payment_frequency_interval: TimeInterval;
/**
* Timestamp of the last payment. Indicates the start of current billing period
*/
previous_billing_date: string;
/**
* Identifier of the product associated with this subscription
*/
product_id: string;
/**
* Number of units/items included in the subscription
*/
quantity: number;
/**
* Amount charged before tax for each recurring payment in smallest currency unit
* (e.g. cents)
*/
recurring_pre_tax_amount: number;
/**
* Current status of the subscription
*/
status: SubscriptionStatus;
/**
* Unique identifier for the subscription
*/
subscription_id: string;
/**
* Number of subscription period intervals
*/
subscription_period_count: number;
/**
* Time interval for the subscription period (e.g. month, year)
*/
subscription_period_interval: TimeInterval;
/**
* Indicates if the recurring_pre_tax_amount is tax inclusive
*/
tax_inclusive: boolean;
/**
* Number of days in the trial period (0 if no trial)
*/
trial_period_days: number;
/**
* Cancelled timestamp if the subscription is cancelled
*/
cancelled_at?: string | null;
/**
* Number of remaining discount cycles if discount is applied
*/
discount_cycles_remaining?: number | null;
/**
* The discount id if discount is applied
*/
discount_id?: string | null;
/**
* Timestamp when the subscription will expire
*/
expires_at?: string | null;
/**
* Tax identifier provided for this subscription (if applicable)
*/
tax_id?: string | null;
}
export namespace Subscription {
/**
* Response struct representing usage-based meter cart details for a subscription
*/
export interface Meter {
currency: MiscAPI.Currency;
free_threshold: number;
measurement_unit: string;
meter_id: string;
name: string;
price_per_unit: string;
description?: string | null;
}
}
export type SubscriptionStatus = 'pending' | 'active' | 'on_hold' | 'cancelled' | 'failed' | 'expired';
export type TimeInterval = 'Day' | 'Week' | 'Month' | 'Year';
export interface SubscriptionCreateResponse {
/**
* Addons associated with this subscription
*/
addons: Array<AddonCartResponseItem>;
/**
* Customer details associated with this subscription
*/
customer: PaymentsAPI.CustomerLimitedDetails;
/**
* Additional metadata associated with the subscription
*/
metadata: { [key: string]: string };
/**
* First payment id for the subscription
*/
payment_id: string;
/**
* Tax will be added to the amount and charged to the customer on each billing
* cycle
*/
recurring_pre_tax_amount: number;
/**
* Unique identifier for the subscription
*/
subscription_id: string;
/**
* Client secret used to load Dodo checkout SDK NOTE : Dodo checkout SDK will be
* coming soon
*/
client_secret?: string | null;
/**
* The discount id if discount is applied
*/
discount_id?: string | null;
/**
* Expiry timestamp of the payment link
*/
expires_on?: string | null;
/**
* URL to checkout page
*/
payment_link?: string | null;
}
/**
* Response struct representing subscription details
*/
export interface SubscriptionListResponse {
/**
* Billing address details for payments
*/
billing: PaymentsAPI.BillingAddress;
/**
* Indicates if the subscription will cancel at the next billing date
*/
cancel_at_next_billing_date: boolean;
/**
* Timestamp when the subscription was created
*/
created_at: string;
/**
* Currency used for the subscription payments
*/
currency: MiscAPI.Currency;
/**
* Customer details associated with the subscription
*/
customer: PaymentsAPI.CustomerLimitedDetails;
/**
* Additional custom data associated with the subscription
*/
metadata: { [key: string]: string };
/**
* Timestamp of the next scheduled billing. Indicates the end of current billing
* period
*/
next_billing_date: string;
/**
* Wether the subscription is on-demand or not
*/
on_demand: boolean;
/**
* Number of payment frequency intervals
*/
payment_frequency_count: number;
/**
* Time interval for payment frequency (e.g. month, year)
*/
payment_frequency_interval: TimeInterval;
/**
* Timestamp of the last payment. Indicates the start of current billing period
*/
previous_billing_date: string;
/**
* Identifier of the product associated with this subscription
*/
product_id: string;
/**
* Number of units/items included in the subscription
*/
quantity: number;
/**
* Amount charged before tax for each recurring payment in smallest currency unit
* (e.g. cents)
*/
recurring_pre_tax_amount: number;
/**
* Current status of the subscription
*/
status: SubscriptionStatus;
/**
* Unique identifier for the subscription
*/
subscription_id: string;
/**
* Number of subscription period intervals
*/
subscription_period_count: number;
/**
* Time interval for the subscription period (e.g. month, year)
*/
subscription_period_interval: TimeInterval;
/**
* Indicates if the recurring_pre_tax_amount is tax inclusive
*/
tax_inclusive: boolean;
/**
* Number of days in the trial period (0 if no trial)
*/
trial_period_days: number;
/**
* Cancelled timestamp if the subscription is cancelled
*/
cancelled_at?: string | null;
/**
* Number of remaining discount cycles if discount is applied
*/
discount_cycles_remaining?: number | null;
/**
* The discount id if discount is applied
*/
discount_id?: string | null;
/**
* Tax identifier provided for this subscription (if applicable)
*/
tax_id?: string | null;
}
export interface SubscriptionChargeResponse {
payment_id: string;
}
export interface SubscriptionRetrieveUsageHistoryResponse {
/**
* End date of the billing period
*/
end_date: string;
/**
* List of meters and their usage for this billing period
*/
meters: Array<SubscriptionRetrieveUsageHistoryResponse.Meter>;
/**
* Start date of the billing period
*/
start_date: string;
}
export namespace SubscriptionRetrieveUsageHistoryResponse {
export interface Meter {
/**
* Meter identifier
*/
id: string;
/**
* Chargeable units (after free threshold) as string for precision
*/
chargeable_units: string;
/**
* Total units consumed as string for precision
*/
consumed_units: string;
/**
* Currency for the price per unit
*/
currency: MiscAPI.Currency;
/**
* Free threshold units for this meter
*/
free_threshold: number;
/**
* Meter name
*/
name: string;
/**
* Price per unit in string format for precision
*/
price_per_unit: string;
/**
* Total price charged for this meter in smallest currency unit (cents)
*/
total_price: number;
}
}
export interface SubscriptionCreateParams {
/**
* Billing address information for the subscription
*/
billing: PaymentsAPI.BillingAddress;
/**
* Customer details for the subscription
*/
customer: PaymentsAPI.CustomerRequest;
/**
* Unique identifier of the product to subscribe to
*/
product_id: string;
/**
* Number of units to subscribe for. Must be at least 1.
*/
quantity: number;
/**
* Attach addons to this subscription
*/
addons?: Array<AttachAddon> | null;
/**
* List of payment methods allowed during checkout.
*
* Customers will **never** see payment methods that are **not** in this list.
* However, adding a method here **does not guarantee** customers will see it.
* Availability still depends on other factors (e.g., customer location, merchant
* settings).
*/
allowed_payment_method_types?: Array<PaymentsAPI.PaymentMethodTypes> | null;
/**
* Fix the currency in which the end customer is billed. If Dodo Payments cannot
* support that currency for this transaction, it will not proceed
*/
billing_currency?: MiscAPI.Currency | null;
/**
* Discount Code to apply to the subscription
*/
discount_code?: string | null;
/**
* Override merchant default 3DS behaviour for this subscription
*/
force_3ds?: boolean | null;
/**
* Additional metadata for the subscription Defaults to empty if not specified
*/
metadata?: { [key: string]: string };
on_demand?: OnDemandSubscription | null;
/**
* If true, generates a payment link. Defaults to false if not specified.
*/
payment_link?: boolean | null;
/**
* Optional URL to redirect after successful subscription creation
*/
return_url?: string | null;
/**
* Display saved payment methods of a returning customer False by default
*/
show_saved_payment_methods?: boolean;
/**
* Tax ID in case the payment is B2B. If tax id validation fails the payment
* creation will fail
*/
tax_id?: string | null;
/**
* 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
*/
trial_period_days?: number | null;
}
export interface SubscriptionUpdateParams {
billing?: PaymentsAPI.BillingAddress | null;
/**
* When set, the subscription will remain active until the end of billing period
*/
cancel_at_next_billing_date?: boolean | null;
customer_name?: string | null;
disable_on_demand?: SubscriptionUpdateParams.DisableOnDemand | null;
metadata?: { [key: string]: string } | null;
next_billing_date?: string | null;
status?: SubscriptionStatus | null;
tax_id?: string | null;
}
export namespace SubscriptionUpdateParams {
export interface DisableOnDemand {
next_billing_date: string;
}
}
export interface SubscriptionListParams extends DefaultPageNumberPaginationParams {
/**
* filter by Brand id
*/
brand_id?: string;
/**
* Get events after this created time
*/
created_at_gte?: string;
/**
* Get events created before this time
*/
created_at_lte?: string;
/**
* Filter by customer id
*/
customer_id?: string;
/**
* Filter by status
*/
status?: 'pending' | 'active' | 'on_hold' | 'cancelled' | 'failed' | 'expired';
}
export interface SubscriptionChangePlanParams {
/**
* Unique identifier of the product to subscribe to
*/
product_id: string;
/**
* Proration Billing Mode
*/
proration_billing_mode: 'prorated_immediately' | 'full_immediately' | 'difference_immediately';
/**
* Number of units to subscribe for. Must be at least 1.
*/
quantity: number;
/**
* Addons for the new plan. Note : Leaving this empty would remove any existing
* addons
*/
addons?: Array<AttachAddon> | null;
}
export interface SubscriptionChargeParams {
/**
* The product price. Represented in the lowest denomination of the currency (e.g.,
* cents for USD). For example, to charge $1.00, pass `100`.
*/
product_price: number;
/**
* Whether adaptive currency fees should be included in the product_price (true) or
* added on top (false). This field is ignored if adaptive pricing is not enabled
* for the business.
*/
adaptive_currency_fees_inclusive?: boolean | null;
/**
* Specify how customer balance is used for the payment
*/
customer_balance_config?: SubscriptionChargeParams.CustomerBalanceConfig | null;
/**
* Metadata for the payment. If not passed, the metadata of the subscription will
* be taken
*/
metadata?: { [key: string]: string } | null;
/**
* Optional currency of the product price. If not specified, defaults to the
* currency of the product.
*/
product_currency?: MiscAPI.Currency | null;
/**
* Optional product description override for billing and line items. If not
* specified, the stored description of the product will be used.
*/
product_description?: string | null;
}
export namespace SubscriptionChargeParams {
/**
* Specify how customer balance is used for the payment
*/
export interface CustomerBalanceConfig {
/**
* Allows Customer Credit to be purchased to settle payments
*/
allow_customer_credits_purchase?: boolean | null;
/**
* Allows Customer Credit Balance to be used to settle payments
*/
allow_customer_credits_usage?: boolean | null;
}
}
export interface SubscriptionRetrieveUsageHistoryParams extends DefaultPageNumberPaginationParams {
/**
* Filter by end date (inclusive)
*/
end_date?: string | null;
/**
* Filter by specific meter ID
*/
meter_id?: string | null;
/**
* Filter by start date (inclusive)
*/
start_date?: string | null;
}
export declare namespace Subscriptions {
export {
type AddonCartResponseItem as AddonCartResponseItem,
type AttachAddon as AttachAddon,
type OnDemandSubscription as OnDemandSubscription,
type Subscription as Subscription,
type SubscriptionStatus as SubscriptionStatus,
type TimeInterval as TimeInterval,
type SubscriptionCreateResponse as SubscriptionCreateResponse,
type SubscriptionListResponse as SubscriptionListResponse,
type SubscriptionChargeResponse as SubscriptionChargeResponse,
type SubscriptionRetrieveUsageHistoryResponse as SubscriptionRetrieveUsageHistoryResponse,
type SubscriptionListResponsesDefaultPageNumberPagination as SubscriptionListResponsesDefaultPageNumberPagination,
type SubscriptionRetrieveUsageHistoryResponsesDefaultPageNumberPagination as SubscriptionRetrieveUsageHistoryResponsesDefaultPageNumberPagination,
type SubscriptionCreateParams as SubscriptionCreateParams,
type SubscriptionUpdateParams as SubscriptionUpdateParams,
type SubscriptionListParams as SubscriptionListParams,
type SubscriptionChangePlanParams as SubscriptionChangePlanParams,
type SubscriptionChargeParams as SubscriptionChargeParams,
type SubscriptionRetrieveUsageHistoryParams as SubscriptionRetrieveUsageHistoryParams,
};
}
```