This is page 4 of 10. Use http://codebase.md/dodopayments/dodopayments-node?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .devcontainer
│ └── devcontainer.json
├── .github
│ └── workflows
│ ├── ci.yml
│ ├── docker-mcp.yml
│ ├── publish-npm.yml
│ └── release-doctor.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .release-please-manifest.json
├── .stats.yml
├── api.md
├── bin
│ ├── check-release-environment
│ ├── cli
│ ├── docker-tags
│ ├── migration-config.json
│ └── publish-npm
├── Brewfile
├── CHANGELOG.md
├── CONTRIBUTING.md
├── eslint.config.mjs
├── examples
│ └── .keep
├── jest.config.ts
├── LICENSE
├── MIGRATION.md
├── package.json
├── packages
│ └── mcp-server
│ ├── .dockerignore
│ ├── build
│ ├── cloudflare-worker
│ │ ├── .gitignore
│ │ ├── biome.json
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── app.ts
│ │ │ ├── index.ts
│ │ │ └── utils.ts
│ │ ├── static
│ │ │ └── home.md
│ │ ├── tsconfig.json
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── Dockerfile
│ ├── jest.config.ts
│ ├── manifest.json
│ ├── package.json
│ ├── README.md
│ ├── scripts
│ │ ├── copy-bundle-files.cjs
│ │ └── postprocess-dist-package-json.cjs
│ ├── src
│ │ ├── code-tool-paths.cts
│ │ ├── code-tool-types.ts
│ │ ├── code-tool-worker.ts
│ │ ├── code-tool.ts
│ │ ├── compat.ts
│ │ ├── docs-search-tool.ts
│ │ ├── dynamic-tools.ts
│ │ ├── filtering.ts
│ │ ├── headers.ts
│ │ ├── http.ts
│ │ ├── index.ts
│ │ ├── options.ts
│ │ ├── server.ts
│ │ ├── stdio.ts
│ │ ├── tools
│ │ │ ├── addons
│ │ │ │ ├── create-addons.ts
│ │ │ │ ├── list-addons.ts
│ │ │ │ ├── retrieve-addons.ts
│ │ │ │ ├── update-addons.ts
│ │ │ │ └── update-images-addons.ts
│ │ │ ├── brands
│ │ │ │ ├── create-brands.ts
│ │ │ │ ├── list-brands.ts
│ │ │ │ ├── retrieve-brands.ts
│ │ │ │ ├── update-brands.ts
│ │ │ │ └── update-images-brands.ts
│ │ │ ├── checkout-sessions
│ │ │ │ ├── create-checkout-sessions.ts
│ │ │ │ └── retrieve-checkout-sessions.ts
│ │ │ ├── customers
│ │ │ │ ├── create-customers.ts
│ │ │ │ ├── customer-portal
│ │ │ │ │ └── create-customers-customer-portal.ts
│ │ │ │ ├── list-customers.ts
│ │ │ │ ├── retrieve-customers.ts
│ │ │ │ ├── update-customers.ts
│ │ │ │ └── wallets
│ │ │ │ ├── ledger-entries
│ │ │ │ │ ├── create-wallets-customers-ledger-entries.ts
│ │ │ │ │ └── list-wallets-customers-ledger-entries.ts
│ │ │ │ └── list-customers-wallets.ts
│ │ │ ├── discounts
│ │ │ │ ├── create-discounts.ts
│ │ │ │ ├── delete-discounts.ts
│ │ │ │ ├── list-discounts.ts
│ │ │ │ ├── retrieve-discounts.ts
│ │ │ │ └── update-discounts.ts
│ │ │ ├── disputes
│ │ │ │ ├── list-disputes.ts
│ │ │ │ └── retrieve-disputes.ts
│ │ │ ├── index.ts
│ │ │ ├── invoices
│ │ │ │ └── payments
│ │ │ │ ├── retrieve-invoices-payments.ts
│ │ │ │ └── retrieve-refund-invoices-payments.ts
│ │ │ ├── license-key-instances
│ │ │ │ ├── list-license-key-instances.ts
│ │ │ │ ├── retrieve-license-key-instances.ts
│ │ │ │ └── update-license-key-instances.ts
│ │ │ ├── license-keys
│ │ │ │ ├── list-license-keys.ts
│ │ │ │ ├── retrieve-license-keys.ts
│ │ │ │ └── update-license-keys.ts
│ │ │ ├── licenses
│ │ │ │ ├── activate-licenses.ts
│ │ │ │ ├── deactivate-licenses.ts
│ │ │ │ └── validate-licenses.ts
│ │ │ ├── meters
│ │ │ │ ├── archive-meters.ts
│ │ │ │ ├── create-meters.ts
│ │ │ │ ├── list-meters.ts
│ │ │ │ ├── retrieve-meters.ts
│ │ │ │ └── unarchive-meters.ts
│ │ │ ├── misc
│ │ │ │ └── list-supported-countries-misc.ts
│ │ │ ├── payments
│ │ │ │ ├── create-payments.ts
│ │ │ │ ├── list-payments.ts
│ │ │ │ ├── retrieve-line-items-payments.ts
│ │ │ │ └── retrieve-payments.ts
│ │ │ ├── payouts
│ │ │ │ └── list-payouts.ts
│ │ │ ├── products
│ │ │ │ ├── archive-products.ts
│ │ │ │ ├── create-products.ts
│ │ │ │ ├── images
│ │ │ │ │ └── update-products-images.ts
│ │ │ │ ├── list-products.ts
│ │ │ │ ├── retrieve-products.ts
│ │ │ │ ├── unarchive-products.ts
│ │ │ │ ├── update-files-products.ts
│ │ │ │ └── update-products.ts
│ │ │ ├── refunds
│ │ │ │ ├── create-refunds.ts
│ │ │ │ ├── list-refunds.ts
│ │ │ │ └── retrieve-refunds.ts
│ │ │ ├── subscriptions
│ │ │ │ ├── change-plan-subscriptions.ts
│ │ │ │ ├── charge-subscriptions.ts
│ │ │ │ ├── create-subscriptions.ts
│ │ │ │ ├── list-subscriptions.ts
│ │ │ │ ├── retrieve-subscriptions.ts
│ │ │ │ ├── retrieve-usage-history-subscriptions.ts
│ │ │ │ └── update-subscriptions.ts
│ │ │ ├── types.ts
│ │ │ ├── usage-events
│ │ │ │ ├── ingest-usage-events.ts
│ │ │ │ ├── list-usage-events.ts
│ │ │ │ └── retrieve-usage-events.ts
│ │ │ └── webhooks
│ │ │ ├── create-webhooks.ts
│ │ │ ├── delete-webhooks.ts
│ │ │ ├── headers
│ │ │ │ ├── retrieve-webhooks-headers.ts
│ │ │ │ └── update-webhooks-headers.ts
│ │ │ ├── list-webhooks.ts
│ │ │ ├── retrieve-secret-webhooks.ts
│ │ │ ├── retrieve-webhooks.ts
│ │ │ └── update-webhooks.ts
│ │ └── tools.ts
│ ├── tests
│ │ ├── compat.test.ts
│ │ ├── dynamic-tools.test.ts
│ │ ├── options.test.ts
│ │ └── tools.test.ts
│ ├── tsc-multi.json
│ ├── tsconfig.build.json
│ ├── tsconfig.dist-src.json
│ ├── tsconfig.json
│ └── yarn.lock
├── README.md
├── release-please-config.json
├── scripts
│ ├── bootstrap
│ ├── build
│ ├── build-all
│ ├── fast-format
│ ├── format
│ ├── lint
│ ├── mock
│ ├── publish-packages.ts
│ ├── test
│ └── utils
│ ├── attw-report.cjs
│ ├── check-is-in-git-install.sh
│ ├── check-version.cjs
│ ├── fix-index-exports.cjs
│ ├── git-swap.sh
│ ├── make-dist-package-json.cjs
│ ├── postprocess-files.cjs
│ └── upload-artifact.sh
├── SECURITY.md
├── src
│ ├── api-promise.ts
│ ├── client.ts
│ ├── core
│ │ ├── api-promise.ts
│ │ ├── error.ts
│ │ ├── pagination.ts
│ │ ├── README.md
│ │ ├── resource.ts
│ │ └── uploads.ts
│ ├── error.ts
│ ├── index.ts
│ ├── internal
│ │ ├── builtin-types.ts
│ │ ├── detect-platform.ts
│ │ ├── errors.ts
│ │ ├── headers.ts
│ │ ├── parse.ts
│ │ ├── README.md
│ │ ├── request-options.ts
│ │ ├── shim-types.ts
│ │ ├── shims.ts
│ │ ├── to-file.ts
│ │ ├── types.ts
│ │ ├── uploads.ts
│ │ ├── utils
│ │ │ ├── base64.ts
│ │ │ ├── bytes.ts
│ │ │ ├── env.ts
│ │ │ ├── log.ts
│ │ │ ├── path.ts
│ │ │ ├── sleep.ts
│ │ │ ├── uuid.ts
│ │ │ └── values.ts
│ │ └── utils.ts
│ ├── lib
│ │ └── .keep
│ ├── pagination.ts
│ ├── resource.ts
│ ├── resources
│ │ ├── addons.ts
│ │ ├── brands.ts
│ │ ├── checkout-sessions.ts
│ │ ├── customers
│ │ │ ├── customer-portal.ts
│ │ │ ├── customers.ts
│ │ │ ├── index.ts
│ │ │ ├── wallets
│ │ │ │ ├── index.ts
│ │ │ │ ├── ledger-entries.ts
│ │ │ │ └── wallets.ts
│ │ │ └── wallets.ts
│ │ ├── customers.ts
│ │ ├── discounts.ts
│ │ ├── disputes.ts
│ │ ├── index.ts
│ │ ├── invoices
│ │ │ ├── index.ts
│ │ │ ├── invoices.ts
│ │ │ └── payments.ts
│ │ ├── invoices.ts
│ │ ├── license-key-instances.ts
│ │ ├── license-keys.ts
│ │ ├── licenses.ts
│ │ ├── meters.ts
│ │ ├── misc.ts
│ │ ├── payments.ts
│ │ ├── payouts.ts
│ │ ├── products
│ │ │ ├── images.ts
│ │ │ ├── index.ts
│ │ │ └── products.ts
│ │ ├── products.ts
│ │ ├── refunds.ts
│ │ ├── subscriptions.ts
│ │ ├── usage-events.ts
│ │ ├── webhook-events.ts
│ │ ├── webhooks
│ │ │ ├── headers.ts
│ │ │ ├── index.ts
│ │ │ └── webhooks.ts
│ │ └── webhooks.ts
│ ├── resources.ts
│ ├── uploads.ts
│ └── version.ts
├── tests
│ ├── api-resources
│ │ ├── addons.test.ts
│ │ ├── brands.test.ts
│ │ ├── checkout-sessions.test.ts
│ │ ├── customers
│ │ │ ├── customer-portal.test.ts
│ │ │ ├── customers.test.ts
│ │ │ └── wallets
│ │ │ ├── ledger-entries.test.ts
│ │ │ └── wallets.test.ts
│ │ ├── discounts.test.ts
│ │ ├── disputes.test.ts
│ │ ├── license-key-instances.test.ts
│ │ ├── license-keys.test.ts
│ │ ├── licenses.test.ts
│ │ ├── meters.test.ts
│ │ ├── misc.test.ts
│ │ ├── payments.test.ts
│ │ ├── payouts.test.ts
│ │ ├── products
│ │ │ ├── images.test.ts
│ │ │ └── products.test.ts
│ │ ├── refunds.test.ts
│ │ ├── subscriptions.test.ts
│ │ ├── usage-events.test.ts
│ │ └── webhooks
│ │ ├── headers.test.ts
│ │ └── webhooks.test.ts
│ ├── base64.test.ts
│ ├── buildHeaders.test.ts
│ ├── form.test.ts
│ ├── index.test.ts
│ ├── path.test.ts
│ ├── stringifyQuery.test.ts
│ └── uploads.test.ts
├── tsc-multi.json
├── tsconfig.build.json
├── tsconfig.deno.json
├── tsconfig.dist-src.json
├── tsconfig.json
└── yarn.lock
```
# Files
--------------------------------------------------------------------------------
/tests/api-resources/products/products.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import DodoPayments from 'dodopayments';
4 |
5 | const client = new DodoPayments({
6 | bearerToken: 'My Bearer Token',
7 | baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
8 | });
9 |
10 | describe('resource products', () => {
11 | test('create: only required params', async () => {
12 | const responsePromise = client.products.create({
13 | price: {
14 | currency: 'AED',
15 | discount: 0,
16 | price: 0,
17 | purchasing_power_parity: true,
18 | type: 'one_time_price',
19 | },
20 | tax_category: 'digital_products',
21 | });
22 | const rawResponse = await responsePromise.asResponse();
23 | expect(rawResponse).toBeInstanceOf(Response);
24 | const response = await responsePromise;
25 | expect(response).not.toBeInstanceOf(Response);
26 | const dataAndResponse = await responsePromise.withResponse();
27 | expect(dataAndResponse.data).toBe(response);
28 | expect(dataAndResponse.response).toBe(rawResponse);
29 | });
30 |
31 | test('create: required and optional params', async () => {
32 | const response = await client.products.create({
33 | price: {
34 | currency: 'AED',
35 | discount: 0,
36 | price: 0,
37 | purchasing_power_parity: true,
38 | type: 'one_time_price',
39 | pay_what_you_want: true,
40 | suggested_price: 0,
41 | tax_inclusive: true,
42 | },
43 | tax_category: 'digital_products',
44 | addons: ['string'],
45 | brand_id: 'brand_id',
46 | description: 'description',
47 | digital_product_delivery: { external_url: 'external_url', instructions: 'instructions' },
48 | license_key_activation_message: 'license_key_activation_message',
49 | license_key_activations_limit: 0,
50 | license_key_duration: { count: 0, interval: 'Day' },
51 | license_key_enabled: true,
52 | metadata: { foo: 'string' },
53 | name: 'name',
54 | });
55 | });
56 |
57 | test('retrieve', async () => {
58 | const responsePromise = client.products.retrieve('id');
59 | const rawResponse = await responsePromise.asResponse();
60 | expect(rawResponse).toBeInstanceOf(Response);
61 | const response = await responsePromise;
62 | expect(response).not.toBeInstanceOf(Response);
63 | const dataAndResponse = await responsePromise.withResponse();
64 | expect(dataAndResponse.data).toBe(response);
65 | expect(dataAndResponse.response).toBe(rawResponse);
66 | });
67 |
68 | test('update', async () => {
69 | const responsePromise = client.products.update('id', {});
70 | const rawResponse = await responsePromise.asResponse();
71 | expect(rawResponse).toBeInstanceOf(Response);
72 | const response = await responsePromise;
73 | expect(response).not.toBeInstanceOf(Response);
74 | const dataAndResponse = await responsePromise.withResponse();
75 | expect(dataAndResponse.data).toBe(response);
76 | expect(dataAndResponse.response).toBe(rawResponse);
77 | });
78 |
79 | test('list', async () => {
80 | const responsePromise = client.products.list();
81 | const rawResponse = await responsePromise.asResponse();
82 | expect(rawResponse).toBeInstanceOf(Response);
83 | const response = await responsePromise;
84 | expect(response).not.toBeInstanceOf(Response);
85 | const dataAndResponse = await responsePromise.withResponse();
86 | expect(dataAndResponse.data).toBe(response);
87 | expect(dataAndResponse.response).toBe(rawResponse);
88 | });
89 |
90 | test('list: request options and params are passed correctly', async () => {
91 | // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
92 | await expect(
93 | client.products.list(
94 | { archived: true, brand_id: 'brand_id', page_number: 0, page_size: 0, recurring: true },
95 | { path: '/_stainless_unknown_path' },
96 | ),
97 | ).rejects.toThrow(DodoPayments.NotFoundError);
98 | });
99 |
100 | test('archive', async () => {
101 | const responsePromise = client.products.archive('id');
102 | const rawResponse = await responsePromise.asResponse();
103 | expect(rawResponse).toBeInstanceOf(Response);
104 | const response = await responsePromise;
105 | expect(response).not.toBeInstanceOf(Response);
106 | const dataAndResponse = await responsePromise.withResponse();
107 | expect(dataAndResponse.data).toBe(response);
108 | expect(dataAndResponse.response).toBe(rawResponse);
109 | });
110 |
111 | test('unarchive', async () => {
112 | const responsePromise = client.products.unarchive('id');
113 | const rawResponse = await responsePromise.asResponse();
114 | expect(rawResponse).toBeInstanceOf(Response);
115 | const response = await responsePromise;
116 | expect(response).not.toBeInstanceOf(Response);
117 | const dataAndResponse = await responsePromise.withResponse();
118 | expect(dataAndResponse.data).toBe(response);
119 | expect(dataAndResponse.response).toBe(rawResponse);
120 | });
121 |
122 | test('updateFiles: only required params', async () => {
123 | const responsePromise = client.products.updateFiles('id', { file_name: 'file_name' });
124 | const rawResponse = await responsePromise.asResponse();
125 | expect(rawResponse).toBeInstanceOf(Response);
126 | const response = await responsePromise;
127 | expect(response).not.toBeInstanceOf(Response);
128 | const dataAndResponse = await responsePromise.withResponse();
129 | expect(dataAndResponse.data).toBe(response);
130 | expect(dataAndResponse.response).toBe(rawResponse);
131 | });
132 |
133 | test('updateFiles: required and optional params', async () => {
134 | const response = await client.products.updateFiles('id', { file_name: 'file_name' });
135 | });
136 | });
137 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/discounts/update-discounts.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { maybeFilter } from 'dodopayments-mcp/filtering';
4 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
5 |
6 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
7 | import DodoPayments from 'dodopayments';
8 |
9 | export const metadata: Metadata = {
10 | resource: 'discounts',
11 | operation: 'write',
12 | tags: [],
13 | httpMethod: 'patch',
14 | httpPath: '/discounts/{discount_id}',
15 | operationId: 'patch_discount_handler',
16 | };
17 |
18 | export const tool: Tool = {
19 | name: 'update_discounts',
20 | description:
21 | "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nPATCH /discounts/{discount_id}\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/discount',\n $defs: {\n discount: {\n type: 'object',\n properties: {\n amount: {\n type: 'integer',\n description: 'The discount amount.\\n\\n- If `discount_type` is `percentage`, this is in **basis points**\\n (e.g., 540 => 5.4%).\\n- Otherwise, this is **USD cents** (e.g., 100 => `$1.00`).'\n },\n business_id: {\n type: 'string',\n description: 'The business this discount belongs to.'\n },\n code: {\n type: 'string',\n description: 'The discount code (up to 16 chars).'\n },\n created_at: {\n type: 'string',\n description: 'Timestamp when the discount is created',\n format: 'date-time'\n },\n discount_id: {\n type: 'string',\n description: 'The unique discount ID'\n },\n restricted_to: {\n type: 'array',\n description: 'List of product IDs to which this discount is restricted.',\n items: {\n type: 'string'\n }\n },\n times_used: {\n type: 'integer',\n description: 'How many times this discount has been used.'\n },\n type: {\n $ref: '#/$defs/discount_type'\n },\n expires_at: {\n type: 'string',\n description: 'Optional date/time after which discount is expired.',\n format: 'date-time'\n },\n name: {\n type: 'string',\n description: 'Name for the Discount'\n },\n subscription_cycles: {\n type: 'integer',\n description: 'Number of subscription billing cycles this discount is valid for.\\nIf not provided, the discount will be applied indefinitely to\\nall recurring payments related to the subscription.'\n },\n usage_limit: {\n type: 'integer',\n description: 'Usage limit for this discount, if any.'\n }\n },\n required: [ 'amount',\n 'business_id',\n 'code',\n 'created_at',\n 'discount_id',\n 'restricted_to',\n 'times_used',\n 'type'\n ]\n },\n discount_type: {\n type: 'string',\n enum: [ 'percentage'\n ]\n }\n }\n}\n```",
22 | inputSchema: {
23 | type: 'object',
24 | properties: {
25 | discount_id: {
26 | type: 'string',
27 | },
28 | amount: {
29 | type: 'integer',
30 | description:
31 | 'If present, update the discount amount:\n- If `discount_type` is `percentage`, this represents **basis points** (e.g., `540` = `5.4%`).\n- Otherwise, this represents **USD cents** (e.g., `100` = `$1.00`).\n\nMust be at least 1 if provided.',
32 | },
33 | code: {
34 | type: 'string',
35 | description: 'If present, update the discount code (uppercase).',
36 | },
37 | expires_at: {
38 | type: 'string',
39 | format: 'date-time',
40 | },
41 | name: {
42 | type: 'string',
43 | },
44 | restricted_to: {
45 | type: 'array',
46 | description:
47 | 'If present, replaces all restricted product IDs with this new set.\nTo remove all restrictions, send empty array',
48 | items: {
49 | type: 'string',
50 | },
51 | },
52 | subscription_cycles: {
53 | type: 'integer',
54 | description:
55 | 'Number of subscription billing cycles this discount is valid for.\nIf not provided, the discount will be applied indefinitely to\nall recurring payments related to the subscription.',
56 | },
57 | type: {
58 | $ref: '#/$defs/discount_type',
59 | },
60 | usage_limit: {
61 | type: 'integer',
62 | },
63 | jq_filter: {
64 | type: 'string',
65 | title: 'jq Filter',
66 | description:
67 | 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).',
68 | },
69 | },
70 | required: ['discount_id'],
71 | $defs: {
72 | discount_type: {
73 | type: 'string',
74 | enum: ['percentage'],
75 | },
76 | },
77 | },
78 | annotations: {},
79 | };
80 |
81 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
82 | const { discount_id, jq_filter, ...body } = args as any;
83 | return asTextContentResult(await maybeFilter(jq_filter, await client.discounts.update(discount_id, body)));
84 | };
85 |
86 | export default { metadata, tool, handler };
87 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/dynamic-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import DodoPayments from 'dodopayments';
2 | import { Endpoint, asTextContentResult, ToolCallResult } from './tools/types';
3 | import { zodToJsonSchema } from 'zod-to-json-schema';
4 | import { z } from 'zod';
5 | import { Cabidela } from '@cloudflare/cabidela';
6 |
7 | function zodToInputSchema(schema: z.ZodSchema) {
8 | return {
9 | type: 'object' as const,
10 | ...(zodToJsonSchema(schema) as any),
11 | };
12 | }
13 |
14 | /**
15 | * A list of tools that expose all the endpoints in the API dynamically.
16 | *
17 | * Instead of exposing every endpoint as it's own tool, which uses up too many tokens for LLMs to use at once,
18 | * we expose a single tool that can be used to search for endpoints by name, resource, operation, or tag, and then
19 | * a generic endpoint that can be used to invoke any endpoint with the provided arguments.
20 | *
21 | * @param endpoints - The endpoints to include in the list.
22 | */
23 | export function dynamicTools(endpoints: Endpoint[]): Endpoint[] {
24 | const listEndpointsSchema = z.object({
25 | search_query: z
26 | .string()
27 | .optional()
28 | .describe(
29 | 'An optional search query to filter the endpoints by. Provide a partial name, resource, operation, or tag to filter the endpoints returned.',
30 | ),
31 | });
32 |
33 | const listEndpointsTool = {
34 | metadata: {
35 | resource: 'dynamic_tools',
36 | operation: 'read' as const,
37 | tags: [],
38 | },
39 | tool: {
40 | name: 'list_api_endpoints',
41 | description: 'List or search for all endpoints in the Dodo Payments TypeScript API',
42 | inputSchema: zodToInputSchema(listEndpointsSchema),
43 | },
44 | handler: async (
45 | client: DodoPayments,
46 | args: Record<string, unknown> | undefined,
47 | ): Promise<ToolCallResult> => {
48 | const query = args && listEndpointsSchema.parse(args).search_query?.trim();
49 |
50 | const filteredEndpoints =
51 | query && query.length > 0 ?
52 | endpoints.filter((endpoint) => {
53 | const fieldsToMatch = [
54 | endpoint.tool.name,
55 | endpoint.tool.description,
56 | endpoint.metadata.resource,
57 | endpoint.metadata.operation,
58 | ...endpoint.metadata.tags,
59 | ];
60 | return fieldsToMatch.some((field) => field && field.toLowerCase().includes(query.toLowerCase()));
61 | })
62 | : endpoints;
63 |
64 | return asTextContentResult({
65 | tools: filteredEndpoints.map(({ tool, metadata }) => ({
66 | name: tool.name,
67 | description: tool.description,
68 | resource: metadata.resource,
69 | operation: metadata.operation,
70 | tags: metadata.tags,
71 | })),
72 | });
73 | },
74 | };
75 |
76 | const getEndpointSchema = z.object({
77 | endpoint: z.string().describe('The name of the endpoint to get the schema for.'),
78 | });
79 | const getEndpointTool = {
80 | metadata: {
81 | resource: 'dynamic_tools',
82 | operation: 'read' as const,
83 | tags: [],
84 | },
85 | tool: {
86 | name: 'get_api_endpoint_schema',
87 | description:
88 | 'Get the schema for an endpoint in the Dodo Payments TypeScript API. You can use the schema returned by this tool to invoke an endpoint with the `invoke_api_endpoint` tool.',
89 | inputSchema: zodToInputSchema(getEndpointSchema),
90 | },
91 | handler: async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
92 | if (!args) {
93 | throw new Error('No endpoint provided');
94 | }
95 | const endpointName = getEndpointSchema.parse(args).endpoint;
96 |
97 | const endpoint = endpoints.find((e) => e.tool.name === endpointName);
98 | if (!endpoint) {
99 | throw new Error(`Endpoint ${endpointName} not found`);
100 | }
101 | return asTextContentResult(endpoint.tool);
102 | },
103 | };
104 |
105 | const invokeEndpointSchema = z.object({
106 | endpoint_name: z.string().describe('The name of the endpoint to invoke.'),
107 | args: z
108 | .record(z.string(), z.any())
109 | .describe(
110 | 'The arguments to pass to the endpoint. This must match the schema returned by the `get_api_endpoint_schema` tool.',
111 | ),
112 | });
113 |
114 | const invokeEndpointTool = {
115 | metadata: {
116 | resource: 'dynamic_tools',
117 | operation: 'write' as const,
118 | tags: [],
119 | },
120 | tool: {
121 | name: 'invoke_api_endpoint',
122 | description:
123 | 'Invoke an endpoint in the Dodo Payments TypeScript API. Note: use the `list_api_endpoints` tool to get the list of endpoints and `get_api_endpoint_schema` tool to get the schema for an endpoint.',
124 | inputSchema: zodToInputSchema(invokeEndpointSchema),
125 | },
126 | handler: async (
127 | client: DodoPayments,
128 | args: Record<string, unknown> | undefined,
129 | ): Promise<ToolCallResult> => {
130 | if (!args) {
131 | throw new Error('No endpoint provided');
132 | }
133 | const { success, data, error } = invokeEndpointSchema.safeParse(args);
134 | if (!success) {
135 | throw new Error(`Invalid arguments for endpoint. ${error?.format()}`);
136 | }
137 | const { endpoint_name, args: endpointArgs } = data;
138 |
139 | const endpoint = endpoints.find((e) => e.tool.name === endpoint_name);
140 | if (!endpoint) {
141 | throw new Error(
142 | `Endpoint ${endpoint_name} not found. Use the \`list_api_endpoints\` tool to get the list of available endpoints.`,
143 | );
144 | }
145 |
146 | try {
147 | // Try to validate the arguments for a better error message
148 | const cabidela = new Cabidela(endpoint.tool.inputSchema, { fullErrors: true });
149 | cabidela.validate(endpointArgs);
150 | } catch (error) {
151 | throw new Error(`Invalid arguments for endpoint ${endpoint_name}:\n${error}`);
152 | }
153 |
154 | return await endpoint.handler(client, endpointArgs);
155 | },
156 | };
157 |
158 | return [getEndpointTool, listEndpointsTool, invokeEndpointTool];
159 | }
160 |
```
--------------------------------------------------------------------------------
/src/resources/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | export {
4 | Addons,
5 | type AddonResponse,
6 | type AddonUpdateImagesResponse,
7 | type AddonCreateParams,
8 | type AddonUpdateParams,
9 | type AddonListParams,
10 | type AddonResponsesDefaultPageNumberPagination,
11 | } from './addons';
12 | export {
13 | Brands,
14 | type Brand,
15 | type BrandListResponse,
16 | type BrandUpdateImagesResponse,
17 | type BrandCreateParams,
18 | type BrandUpdateParams,
19 | } from './brands';
20 | export {
21 | CheckoutSessions,
22 | type CheckoutSessionRequest,
23 | type CheckoutSessionResponse,
24 | type CheckoutSessionStatus,
25 | type CheckoutSessionCreateParams,
26 | } from './checkout-sessions';
27 | export {
28 | Customers,
29 | type Customer,
30 | type CustomerPortalSession,
31 | type CustomerCreateParams,
32 | type CustomerUpdateParams,
33 | type CustomerListParams,
34 | type CustomersDefaultPageNumberPagination,
35 | } from './customers/customers';
36 | export {
37 | Discounts,
38 | type Discount,
39 | type DiscountType,
40 | type DiscountCreateParams,
41 | type DiscountUpdateParams,
42 | type DiscountListParams,
43 | type DiscountsDefaultPageNumberPagination,
44 | } from './discounts';
45 | export {
46 | Disputes,
47 | type Dispute,
48 | type DisputeStage,
49 | type DisputeStatus,
50 | type GetDispute,
51 | type DisputeListResponse,
52 | type DisputeListParams,
53 | type DisputeListResponsesDefaultPageNumberPagination,
54 | } from './disputes';
55 | export { Invoices } from './invoices/invoices';
56 | export {
57 | LicenseKeyInstances,
58 | type LicenseKeyInstance,
59 | type LicenseKeyInstanceUpdateParams,
60 | type LicenseKeyInstanceListParams,
61 | type LicenseKeyInstancesDefaultPageNumberPagination,
62 | } from './license-key-instances';
63 | export {
64 | LicenseKeys,
65 | type LicenseKey,
66 | type LicenseKeyStatus,
67 | type LicenseKeyUpdateParams,
68 | type LicenseKeyListParams,
69 | type LicenseKeysDefaultPageNumberPagination,
70 | } from './license-keys';
71 | export {
72 | Licenses,
73 | type LicenseActivateResponse,
74 | type LicenseValidateResponse,
75 | type LicenseActivateParams,
76 | type LicenseDeactivateParams,
77 | type LicenseValidateParams,
78 | } from './licenses';
79 | export {
80 | Meters,
81 | type Meter,
82 | type MeterAggregation,
83 | type MeterFilter,
84 | type MeterCreateParams,
85 | type MeterListParams,
86 | type MetersDefaultPageNumberPagination,
87 | } from './meters';
88 | export {
89 | Misc,
90 | type CountryCode,
91 | type Currency,
92 | type TaxCategory,
93 | type MiscListSupportedCountriesResponse,
94 | } from './misc';
95 | export {
96 | Payments,
97 | type AttachExistingCustomer,
98 | type BillingAddress,
99 | type CreateNewCustomer,
100 | type CustomerLimitedDetails,
101 | type CustomerRequest,
102 | type IntentStatus,
103 | type NewCustomer,
104 | type OneTimeProductCartItem,
105 | type Payment,
106 | type PaymentMethodTypes,
107 | type PaymentCreateResponse,
108 | type PaymentListResponse,
109 | type PaymentRetrieveLineItemsResponse,
110 | type PaymentCreateParams,
111 | type PaymentListParams,
112 | type PaymentListResponsesDefaultPageNumberPagination,
113 | } from './payments';
114 | export {
115 | Payouts,
116 | type PayoutListResponse,
117 | type PayoutListParams,
118 | type PayoutListResponsesDefaultPageNumberPagination,
119 | } from './payouts';
120 | export {
121 | Products,
122 | type AddMeterToPrice,
123 | type LicenseKeyDuration,
124 | type Price,
125 | type Product,
126 | type ProductListResponse,
127 | type ProductUpdateFilesResponse,
128 | type ProductCreateParams,
129 | type ProductUpdateParams,
130 | type ProductListParams,
131 | type ProductUpdateFilesParams,
132 | type ProductListResponsesDefaultPageNumberPagination,
133 | } from './products/products';
134 | export {
135 | Refunds,
136 | type Refund,
137 | type RefundStatus,
138 | type RefundListResponse,
139 | type RefundCreateParams,
140 | type RefundListParams,
141 | type RefundListResponsesDefaultPageNumberPagination,
142 | } from './refunds';
143 | export {
144 | Subscriptions,
145 | type AddonCartResponseItem,
146 | type AttachAddon,
147 | type OnDemandSubscription,
148 | type Subscription,
149 | type SubscriptionStatus,
150 | type TimeInterval,
151 | type SubscriptionCreateResponse,
152 | type SubscriptionListResponse,
153 | type SubscriptionChargeResponse,
154 | type SubscriptionRetrieveUsageHistoryResponse,
155 | type SubscriptionCreateParams,
156 | type SubscriptionUpdateParams,
157 | type SubscriptionListParams,
158 | type SubscriptionChangePlanParams,
159 | type SubscriptionChargeParams,
160 | type SubscriptionRetrieveUsageHistoryParams,
161 | type SubscriptionListResponsesDefaultPageNumberPagination,
162 | type SubscriptionRetrieveUsageHistoryResponsesDefaultPageNumberPagination,
163 | } from './subscriptions';
164 | export {
165 | UsageEvents,
166 | type Event,
167 | type EventInput,
168 | type UsageEventIngestResponse,
169 | type UsageEventListParams,
170 | type UsageEventIngestParams,
171 | type EventsDefaultPageNumberPagination,
172 | } from './usage-events';
173 | export { WebhookEvents, type WebhookEventType, type WebhookPayload } from './webhook-events';
174 | export {
175 | Webhooks,
176 | type WebhookDetails,
177 | type WebhookRetrieveSecretResponse,
178 | type DisputeAcceptedWebhookEvent,
179 | type DisputeCancelledWebhookEvent,
180 | type DisputeChallengedWebhookEvent,
181 | type DisputeExpiredWebhookEvent,
182 | type DisputeLostWebhookEvent,
183 | type DisputeOpenedWebhookEvent,
184 | type DisputeWonWebhookEvent,
185 | type LicenseKeyCreatedWebhookEvent,
186 | type PaymentCancelledWebhookEvent,
187 | type PaymentFailedWebhookEvent,
188 | type PaymentProcessingWebhookEvent,
189 | type PaymentSucceededWebhookEvent,
190 | type RefundFailedWebhookEvent,
191 | type RefundSucceededWebhookEvent,
192 | type SubscriptionActiveWebhookEvent,
193 | type SubscriptionCancelledWebhookEvent,
194 | type SubscriptionExpiredWebhookEvent,
195 | type SubscriptionFailedWebhookEvent,
196 | type SubscriptionOnHoldWebhookEvent,
197 | type SubscriptionPlanChangedWebhookEvent,
198 | type SubscriptionRenewedWebhookEvent,
199 | type UnsafeUnwrapWebhookEvent,
200 | type UnwrapWebhookEvent,
201 | type WebhookCreateParams,
202 | type WebhookUpdateParams,
203 | type WebhookListParams,
204 | type WebhookDetailsCursorPagePagination,
205 | } from './webhooks/webhooks';
206 |
```
--------------------------------------------------------------------------------
/tests/api-resources/webhooks/webhooks.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { Webhook } from 'standardwebhooks';
4 |
5 | import DodoPayments from 'dodopayments';
6 |
7 | const client = new DodoPayments({
8 | bearerToken: 'My Bearer Token',
9 | baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
10 | });
11 |
12 | describe('resource webhooks', () => {
13 | test('create: only required params', async () => {
14 | const responsePromise = client.webhooks.create({ url: 'url' });
15 | const rawResponse = await responsePromise.asResponse();
16 | expect(rawResponse).toBeInstanceOf(Response);
17 | const response = await responsePromise;
18 | expect(response).not.toBeInstanceOf(Response);
19 | const dataAndResponse = await responsePromise.withResponse();
20 | expect(dataAndResponse.data).toBe(response);
21 | expect(dataAndResponse.response).toBe(rawResponse);
22 | });
23 |
24 | test('create: required and optional params', async () => {
25 | const response = await client.webhooks.create({
26 | url: 'url',
27 | description: 'description',
28 | disabled: true,
29 | filter_types: ['payment.succeeded'],
30 | headers: { foo: 'string' },
31 | idempotency_key: 'idempotency_key',
32 | metadata: { foo: 'string' },
33 | rate_limit: 0,
34 | });
35 | });
36 |
37 | test('retrieve', async () => {
38 | const responsePromise = client.webhooks.retrieve('webhook_id');
39 | const rawResponse = await responsePromise.asResponse();
40 | expect(rawResponse).toBeInstanceOf(Response);
41 | const response = await responsePromise;
42 | expect(response).not.toBeInstanceOf(Response);
43 | const dataAndResponse = await responsePromise.withResponse();
44 | expect(dataAndResponse.data).toBe(response);
45 | expect(dataAndResponse.response).toBe(rawResponse);
46 | });
47 |
48 | test('update', async () => {
49 | const responsePromise = client.webhooks.update('webhook_id', {});
50 | const rawResponse = await responsePromise.asResponse();
51 | expect(rawResponse).toBeInstanceOf(Response);
52 | const response = await responsePromise;
53 | expect(response).not.toBeInstanceOf(Response);
54 | const dataAndResponse = await responsePromise.withResponse();
55 | expect(dataAndResponse.data).toBe(response);
56 | expect(dataAndResponse.response).toBe(rawResponse);
57 | });
58 |
59 | test('list', async () => {
60 | const responsePromise = client.webhooks.list();
61 | const rawResponse = await responsePromise.asResponse();
62 | expect(rawResponse).toBeInstanceOf(Response);
63 | const response = await responsePromise;
64 | expect(response).not.toBeInstanceOf(Response);
65 | const dataAndResponse = await responsePromise.withResponse();
66 | expect(dataAndResponse.data).toBe(response);
67 | expect(dataAndResponse.response).toBe(rawResponse);
68 | });
69 |
70 | test('list: request options and params are passed correctly', async () => {
71 | // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
72 | await expect(
73 | client.webhooks.list({ iterator: 'iterator', limit: 0 }, { path: '/_stainless_unknown_path' }),
74 | ).rejects.toThrow(DodoPayments.NotFoundError);
75 | });
76 |
77 | test('delete', async () => {
78 | const responsePromise = client.webhooks.delete('webhook_id');
79 | const rawResponse = await responsePromise.asResponse();
80 | expect(rawResponse).toBeInstanceOf(Response);
81 | const response = await responsePromise;
82 | expect(response).not.toBeInstanceOf(Response);
83 | const dataAndResponse = await responsePromise.withResponse();
84 | expect(dataAndResponse.data).toBe(response);
85 | expect(dataAndResponse.response).toBe(rawResponse);
86 | });
87 |
88 | test('retrieveSecret', async () => {
89 | const responsePromise = client.webhooks.retrieveSecret('webhook_id');
90 | const rawResponse = await responsePromise.asResponse();
91 | expect(rawResponse).toBeInstanceOf(Response);
92 | const response = await responsePromise;
93 | expect(response).not.toBeInstanceOf(Response);
94 | const dataAndResponse = await responsePromise.withResponse();
95 | expect(dataAndResponse.data).toBe(response);
96 | expect(dataAndResponse.response).toBe(rawResponse);
97 | });
98 |
99 | test('unwrap', async () => {
100 | const key = 'whsec_c2VjcmV0Cg==';
101 | const payload =
102 | '{"business_id":"business_id","data":{"amount":"amount","business_id":"business_id","created_at":"2019-12-27T18:11:19.117Z","currency":"currency","dispute_id":"dispute_id","dispute_stage":"pre_dispute","dispute_status":"dispute_opened","payment_id":"payment_id","remarks":"remarks","payload_type":"Dispute"},"timestamp":"2019-12-27T18:11:19.117Z","type":"dispute.accepted"}';
103 | const msgID = '1';
104 | const timestamp = new Date();
105 | const wh = new Webhook(key);
106 | const signature = wh.sign(msgID, timestamp, payload);
107 | const headers: Record<string, string> = {
108 | 'webhook-signature': signature,
109 | 'webhook-id': msgID,
110 | 'webhook-timestamp': String(Math.floor(timestamp.getTime() / 1000)),
111 | };
112 | client.webhooks.unwrap(payload, { headers, key });
113 | expect(() => {
114 | const wrongKey = 'whsec_aaaaaaaaaa==';
115 | client.webhooks.unwrap(payload, { headers, key: wrongKey });
116 | }).toThrow('No matching signature found');
117 | expect(() => {
118 | const badSig = wh.sign(msgID, timestamp, 'some other payload');
119 | client.webhooks.unwrap(payload, { headers: { ...headers, 'webhook-signature': badSig }, key });
120 | }).toThrow('No matching signature found');
121 | expect(() => {
122 | client.webhooks.unwrap(payload, { headers: { ...headers, 'webhook-timestamp': '5' }, key });
123 | }).toThrow('Message timestamp too old');
124 | expect(() => {
125 | client.webhooks.unwrap(payload, { headers: { ...headers, 'webhook-id': 'wrong' }, key });
126 | }).toThrow('No matching signature found');
127 | });
128 | });
129 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/discounts/create-discounts.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { maybeFilter } from 'dodopayments-mcp/filtering';
4 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
5 |
6 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
7 | import DodoPayments from 'dodopayments';
8 |
9 | export const metadata: Metadata = {
10 | resource: 'discounts',
11 | operation: 'write',
12 | tags: [],
13 | httpMethod: 'post',
14 | httpPath: '/discounts',
15 | operationId: 'create_discount_handler',
16 | };
17 |
18 | export const tool: Tool = {
19 | name: 'create_discounts',
20 | description:
21 | "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nPOST /discounts\nIf `code` is omitted or empty, a random 16-char uppercase code is generated.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/discount',\n $defs: {\n discount: {\n type: 'object',\n properties: {\n amount: {\n type: 'integer',\n description: 'The discount amount.\\n\\n- If `discount_type` is `percentage`, this is in **basis points**\\n (e.g., 540 => 5.4%).\\n- Otherwise, this is **USD cents** (e.g., 100 => `$1.00`).'\n },\n business_id: {\n type: 'string',\n description: 'The business this discount belongs to.'\n },\n code: {\n type: 'string',\n description: 'The discount code (up to 16 chars).'\n },\n created_at: {\n type: 'string',\n description: 'Timestamp when the discount is created',\n format: 'date-time'\n },\n discount_id: {\n type: 'string',\n description: 'The unique discount ID'\n },\n restricted_to: {\n type: 'array',\n description: 'List of product IDs to which this discount is restricted.',\n items: {\n type: 'string'\n }\n },\n times_used: {\n type: 'integer',\n description: 'How many times this discount has been used.'\n },\n type: {\n $ref: '#/$defs/discount_type'\n },\n expires_at: {\n type: 'string',\n description: 'Optional date/time after which discount is expired.',\n format: 'date-time'\n },\n name: {\n type: 'string',\n description: 'Name for the Discount'\n },\n subscription_cycles: {\n type: 'integer',\n description: 'Number of subscription billing cycles this discount is valid for.\\nIf not provided, the discount will be applied indefinitely to\\nall recurring payments related to the subscription.'\n },\n usage_limit: {\n type: 'integer',\n description: 'Usage limit for this discount, if any.'\n }\n },\n required: [ 'amount',\n 'business_id',\n 'code',\n 'created_at',\n 'discount_id',\n 'restricted_to',\n 'times_used',\n 'type'\n ]\n },\n discount_type: {\n type: 'string',\n enum: [ 'percentage'\n ]\n }\n }\n}\n```",
22 | inputSchema: {
23 | type: 'object',
24 | properties: {
25 | amount: {
26 | type: 'integer',
27 | description:
28 | 'The discount amount.\n\n- If `discount_type` is **not** `percentage`, `amount` is in **USD cents**. For example, `100` means `$1.00`.\n Only USD is allowed.\n- If `discount_type` **is** `percentage`, `amount` is in **basis points**. For example, `540` means `5.4%`.\n\nMust be at least 1.',
29 | },
30 | type: {
31 | $ref: '#/$defs/discount_type',
32 | },
33 | code: {
34 | type: 'string',
35 | description:
36 | 'Optionally supply a code (will be uppercased).\n- Must be at least 3 characters if provided.\n- If omitted, a random 16-character code is generated.',
37 | },
38 | expires_at: {
39 | type: 'string',
40 | description: 'When the discount expires, if ever.',
41 | format: 'date-time',
42 | },
43 | name: {
44 | type: 'string',
45 | },
46 | restricted_to: {
47 | type: 'array',
48 | description: 'List of product IDs to restrict usage (if any).',
49 | items: {
50 | type: 'string',
51 | },
52 | },
53 | subscription_cycles: {
54 | type: 'integer',
55 | description:
56 | 'Number of subscription billing cycles this discount is valid for.\nIf not provided, the discount will be applied indefinitely to\nall recurring payments related to the subscription.',
57 | },
58 | usage_limit: {
59 | type: 'integer',
60 | description: 'How many times this discount can be used (if any).\nMust be >= 1 if provided.',
61 | },
62 | jq_filter: {
63 | type: 'string',
64 | title: 'jq Filter',
65 | description:
66 | 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).',
67 | },
68 | },
69 | required: ['amount', 'type'],
70 | $defs: {
71 | discount_type: {
72 | type: 'string',
73 | enum: ['percentage'],
74 | },
75 | },
76 | },
77 | annotations: {},
78 | };
79 |
80 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
81 | const { jq_filter, ...body } = args as any;
82 | return asTextContentResult(await maybeFilter(jq_filter, await client.discounts.create(body)));
83 | };
84 |
85 | export default { metadata, tool, handler };
86 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/misc/list-supported-countries-misc.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { maybeFilter } from 'dodopayments-mcp/filtering';
4 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
5 |
6 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
7 | import DodoPayments from 'dodopayments';
8 |
9 | export const metadata: Metadata = {
10 | resource: 'misc',
11 | operation: 'read',
12 | tags: [],
13 | httpMethod: 'get',
14 | httpPath: '/checkout/supported_countries',
15 | operationId: 'get_supported_countries_proxy',
16 | };
17 |
18 | export const tool: Tool = {
19 | name: 'list_supported_countries_misc',
20 | description:
21 | "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\n\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/misc_list_supported_countries_response',\n $defs: {\n misc_list_supported_countries_response: {\n type: 'array',\n items: {\n $ref: '#/$defs/country_code'\n }\n },\n country_code: {\n type: 'string',\n description: 'ISO country code alpha2 variant',\n enum: [ 'AF',\n 'AX',\n 'AL',\n 'DZ',\n 'AS',\n 'AD',\n 'AO',\n 'AI',\n 'AQ',\n 'AG',\n 'AR',\n 'AM',\n 'AW',\n 'AU',\n 'AT',\n 'AZ',\n 'BS',\n 'BH',\n 'BD',\n 'BB',\n 'BY',\n 'BE',\n 'BZ',\n 'BJ',\n 'BM',\n 'BT',\n 'BO',\n 'BQ',\n 'BA',\n 'BW',\n 'BV',\n 'BR',\n 'IO',\n 'BN',\n 'BG',\n 'BF',\n 'BI',\n 'KH',\n 'CM',\n 'CA',\n 'CV',\n 'KY',\n 'CF',\n 'TD',\n 'CL',\n 'CN',\n 'CX',\n 'CC',\n 'CO',\n 'KM',\n 'CG',\n 'CD',\n 'CK',\n 'CR',\n 'CI',\n 'HR',\n 'CU',\n 'CW',\n 'CY',\n 'CZ',\n 'DK',\n 'DJ',\n 'DM',\n 'DO',\n 'EC',\n 'EG',\n 'SV',\n 'GQ',\n 'ER',\n 'EE',\n 'ET',\n 'FK',\n 'FO',\n 'FJ',\n 'FI',\n 'FR',\n 'GF',\n 'PF',\n 'TF',\n 'GA',\n 'GM',\n 'GE',\n 'DE',\n 'GH',\n 'GI',\n 'GR',\n 'GL',\n 'GD',\n 'GP',\n 'GU',\n 'GT',\n 'GG',\n 'GN',\n 'GW',\n 'GY',\n 'HT',\n 'HM',\n 'VA',\n 'HN',\n 'HK',\n 'HU',\n 'IS',\n 'IN',\n 'ID',\n 'IR',\n 'IQ',\n 'IE',\n 'IM',\n 'IL',\n 'IT',\n 'JM',\n 'JP',\n 'JE',\n 'JO',\n 'KZ',\n 'KE',\n 'KI',\n 'KP',\n 'KR',\n 'KW',\n 'KG',\n 'LA',\n 'LV',\n 'LB',\n 'LS',\n 'LR',\n 'LY',\n 'LI',\n 'LT',\n 'LU',\n 'MO',\n 'MK',\n 'MG',\n 'MW',\n 'MY',\n 'MV',\n 'ML',\n 'MT',\n 'MH',\n 'MQ',\n 'MR',\n 'MU',\n 'YT',\n 'MX',\n 'FM',\n 'MD',\n 'MC',\n 'MN',\n 'ME',\n 'MS',\n 'MA',\n 'MZ',\n 'MM',\n 'NA',\n 'NR',\n 'NP',\n 'NL',\n 'NC',\n 'NZ',\n 'NI',\n 'NE',\n 'NG',\n 'NU',\n 'NF',\n 'MP',\n 'NO',\n 'OM',\n 'PK',\n 'PW',\n 'PS',\n 'PA',\n 'PG',\n 'PY',\n 'PE',\n 'PH',\n 'PN',\n 'PL',\n 'PT',\n 'PR',\n 'QA',\n 'RE',\n 'RO',\n 'RU',\n 'RW',\n 'BL',\n 'SH',\n 'KN',\n 'LC',\n 'MF',\n 'PM',\n 'VC',\n 'WS',\n 'SM',\n 'ST',\n 'SA',\n 'SN',\n 'RS',\n 'SC',\n 'SL',\n 'SG',\n 'SX',\n 'SK',\n 'SI',\n 'SB',\n 'SO',\n 'ZA',\n 'GS',\n 'SS',\n 'ES',\n 'LK',\n 'SD',\n 'SR',\n 'SJ',\n 'SZ',\n 'SE',\n 'CH',\n 'SY',\n 'TW',\n 'TJ',\n 'TZ',\n 'TH',\n 'TL',\n 'TG',\n 'TK',\n 'TO',\n 'TT',\n 'TN',\n 'TR',\n 'TM',\n 'TC',\n 'TV',\n 'UG',\n 'UA',\n 'AE',\n 'GB',\n 'UM',\n 'US',\n 'UY',\n 'UZ',\n 'VU',\n 'VE',\n 'VN',\n 'VG',\n 'VI',\n 'WF',\n 'EH',\n 'YE',\n 'ZM',\n 'ZW'\n ]\n }\n }\n}\n```",
22 | inputSchema: {
23 | type: 'object',
24 | properties: {
25 | jq_filter: {
26 | type: 'string',
27 | title: 'jq Filter',
28 | description:
29 | 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).',
30 | },
31 | },
32 | required: [],
33 | },
34 | annotations: {
35 | readOnlyHint: true,
36 | },
37 | };
38 |
39 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
40 | const { jq_filter } = args as any;
41 | return asTextContentResult(await maybeFilter(jq_filter, await client.misc.listSupportedCountries()));
42 | };
43 |
44 | export default { metadata, tool, handler };
45 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/addons/retrieve-addons.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { maybeFilter } from 'dodopayments-mcp/filtering';
4 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
5 |
6 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
7 | import DodoPayments from 'dodopayments';
8 |
9 | export const metadata: Metadata = {
10 | resource: 'addons',
11 | operation: 'read',
12 | tags: [],
13 | httpMethod: 'get',
14 | httpPath: '/addons/{id}',
15 | operationId: 'get_addon_handler',
16 | };
17 |
18 | export const tool: Tool = {
19 | name: 'retrieve_addons',
20 | description:
21 | "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\n\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/addon_response',\n $defs: {\n addon_response: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'id of the Addon'\n },\n business_id: {\n type: 'string',\n description: 'Unique identifier for the business to which the addon belongs.'\n },\n created_at: {\n type: 'string',\n description: 'Created time',\n format: 'date-time'\n },\n currency: {\n $ref: '#/$defs/currency'\n },\n name: {\n type: 'string',\n description: 'Name of the Addon'\n },\n price: {\n type: 'integer',\n description: 'Amount of the addon'\n },\n tax_category: {\n $ref: '#/$defs/tax_category'\n },\n updated_at: {\n type: 'string',\n description: 'Updated time',\n format: 'date-time'\n },\n description: {\n type: 'string',\n description: 'Optional description of the Addon'\n },\n image: {\n type: 'string',\n description: 'Image of the Addon'\n }\n },\n required: [ 'id',\n 'business_id',\n 'created_at',\n 'currency',\n 'name',\n 'price',\n 'tax_category',\n 'updated_at'\n ]\n },\n currency: {\n type: 'string',\n enum: [ 'AED',\n 'ALL',\n 'AMD',\n 'ANG',\n 'AOA',\n 'ARS',\n 'AUD',\n 'AWG',\n 'AZN',\n 'BAM',\n 'BBD',\n 'BDT',\n 'BGN',\n 'BHD',\n 'BIF',\n 'BMD',\n 'BND',\n 'BOB',\n 'BRL',\n 'BSD',\n 'BWP',\n 'BYN',\n 'BZD',\n 'CAD',\n 'CHF',\n 'CLP',\n 'CNY',\n 'COP',\n 'CRC',\n 'CUP',\n 'CVE',\n 'CZK',\n 'DJF',\n 'DKK',\n 'DOP',\n 'DZD',\n 'EGP',\n 'ETB',\n 'EUR',\n 'FJD',\n 'FKP',\n 'GBP',\n 'GEL',\n 'GHS',\n 'GIP',\n 'GMD',\n 'GNF',\n 'GTQ',\n 'GYD',\n 'HKD',\n 'HNL',\n 'HRK',\n 'HTG',\n 'HUF',\n 'IDR',\n 'ILS',\n 'INR',\n 'IQD',\n 'JMD',\n 'JOD',\n 'JPY',\n 'KES',\n 'KGS',\n 'KHR',\n 'KMF',\n 'KRW',\n 'KWD',\n 'KYD',\n 'KZT',\n 'LAK',\n 'LBP',\n 'LKR',\n 'LRD',\n 'LSL',\n 'LYD',\n 'MAD',\n 'MDL',\n 'MGA',\n 'MKD',\n 'MMK',\n 'MNT',\n 'MOP',\n 'MRU',\n 'MUR',\n 'MVR',\n 'MWK',\n 'MXN',\n 'MYR',\n 'MZN',\n 'NAD',\n 'NGN',\n 'NIO',\n 'NOK',\n 'NPR',\n 'NZD',\n 'OMR',\n 'PAB',\n 'PEN',\n 'PGK',\n 'PHP',\n 'PKR',\n 'PLN',\n 'PYG',\n 'QAR',\n 'RON',\n 'RSD',\n 'RUB',\n 'RWF',\n 'SAR',\n 'SBD',\n 'SCR',\n 'SEK',\n 'SGD',\n 'SHP',\n 'SLE',\n 'SLL',\n 'SOS',\n 'SRD',\n 'SSP',\n 'STN',\n 'SVC',\n 'SZL',\n 'THB',\n 'TND',\n 'TOP',\n 'TRY',\n 'TTD',\n 'TWD',\n 'TZS',\n 'UAH',\n 'UGX',\n 'USD',\n 'UYU',\n 'UZS',\n 'VES',\n 'VND',\n 'VUV',\n 'WST',\n 'XAF',\n 'XCD',\n 'XOF',\n 'XPF',\n 'YER',\n 'ZAR',\n 'ZMW'\n ]\n },\n tax_category: {\n type: 'string',\n description: 'Represents the different categories of taxation applicable to various products and services.',\n enum: [ 'digital_products',\n 'saas',\n 'e_book',\n 'edtech'\n ]\n }\n }\n}\n```",
22 | inputSchema: {
23 | type: 'object',
24 | properties: {
25 | id: {
26 | type: 'string',
27 | },
28 | jq_filter: {
29 | type: 'string',
30 | title: 'jq Filter',
31 | description:
32 | 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).',
33 | },
34 | },
35 | required: ['id'],
36 | },
37 | annotations: {
38 | readOnlyHint: true,
39 | },
40 | };
41 |
42 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
43 | const { id, jq_filter, ...body } = args as any;
44 | return asTextContentResult(await maybeFilter(jq_filter, await client.addons.retrieve(id)));
45 | };
46 |
47 | export default { metadata, tool, handler };
48 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/code-tool.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { dirname } from 'node:path';
4 | import { pathToFileURL } from 'node:url';
5 | import DodoPayments, { ClientOptions } from 'dodopayments';
6 | import { Endpoint, ContentBlock, Metadata } from './tools/types';
7 |
8 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
9 |
10 | import { WorkerInput, WorkerError, WorkerSuccess } from './code-tool-types';
11 |
12 | /**
13 | * A tool that runs code against a copy of the SDK.
14 | *
15 | * Instead of exposing every endpoint as it's own tool, which uses up too many tokens for LLMs to use at once,
16 | * we expose a single tool that can be used to search for endpoints by name, resource, operation, or tag, and then
17 | * a generic endpoint that can be used to invoke any endpoint with the provided arguments.
18 | *
19 | * @param endpoints - The endpoints to include in the list.
20 | */
21 | export async function codeTool(): Promise<Endpoint> {
22 | const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] };
23 | const tool: Tool = {
24 | name: 'execute',
25 | description:
26 | 'Runs Typescript code to interact with the API.\nYou are a skilled programmer writing code to interface with the service.\nDefine an async function named "run" that takes a single parameter of an initialized client, and it will be run.\nDo not initialize a client, but instead use the client that you are given as a parameter.\nYou will be returned anything that your function returns, plus the results of any console.log statements.\nIf any code triggers an error, the tool will return an error response, so you do not need to add error handling unless you want to output something more helpful than the raw error.\nIt is not necessary to add comments to code, unless by adding those comments you believe that you can generate better code.\nThis code will run in a container, and you will not be able to use fetch or otherwise interact with the network calls other than through the client you are given.\nAny variables you define won\'t live between successive uses of this call, so make sure to return or log any data you might need later.',
27 | inputSchema: { type: 'object', properties: { code: { type: 'string' } } },
28 | };
29 |
30 | // Import dynamically to avoid failing at import time in cases where the environment is not well-supported.
31 | const { newDenoHTTPWorker } = await import('@valtown/deno-http-worker');
32 | const { workerPath } = await import('./code-tool-paths.cjs');
33 |
34 | const handler = async (client: DodoPayments, args: unknown) => {
35 | const baseURLHostname = new URL(client.baseURL).hostname;
36 | const { code } = args as { code: string };
37 |
38 | const worker = await newDenoHTTPWorker(pathToFileURL(workerPath), {
39 | runFlags: [
40 | `--node-modules-dir=manual`,
41 | `--allow-read=code-tool-worker.mjs,${workerPath.replace(/([\/\\]node_modules)[\/\\].+$/, '$1')}/`,
42 | `--allow-net=${baseURLHostname}`,
43 | // Allow environment variables because instantiating the client will try to read from them,
44 | // even though they are not set.
45 | '--allow-env',
46 | ],
47 | printOutput: true,
48 | spawnOptions: {
49 | cwd: dirname(workerPath),
50 | },
51 | });
52 |
53 | try {
54 | const resp = await new Promise<Response>((resolve, reject) => {
55 | worker.addEventListener('exit', (exitCode) => {
56 | reject(new Error(`Worker exited with code ${exitCode}`));
57 | });
58 |
59 | const opts: ClientOptions = {
60 | baseURL: client.baseURL,
61 | bearerToken: client.bearerToken,
62 | webhookKey: client.webhookKey,
63 | defaultHeaders: {
64 | 'X-Stainless-MCP': 'true',
65 | },
66 | };
67 |
68 | const req = worker.request(
69 | 'http://localhost',
70 | {
71 | headers: {
72 | 'content-type': 'application/json',
73 | },
74 | method: 'POST',
75 | },
76 | (resp) => {
77 | const body: Uint8Array[] = [];
78 | resp.on('error', (err) => {
79 | reject(err);
80 | });
81 | resp.on('data', (chunk) => {
82 | body.push(chunk);
83 | });
84 | resp.on('end', () => {
85 | resolve(
86 | new Response(Buffer.concat(body).toString(), {
87 | status: resp.statusCode ?? 200,
88 | headers: resp.headers as any,
89 | }),
90 | );
91 | });
92 | },
93 | );
94 |
95 | const body = JSON.stringify({
96 | opts,
97 | code,
98 | } satisfies WorkerInput);
99 |
100 | req.write(body, (err) => {
101 | if (err !== null && err !== undefined) {
102 | reject(err);
103 | }
104 | });
105 |
106 | req.end();
107 | });
108 |
109 | if (resp.status === 200) {
110 | const { result, logLines, errLines } = (await resp.json()) as WorkerSuccess;
111 | const returnOutput: ContentBlock | null =
112 | result === null ? null
113 | : result === undefined ? null
114 | : {
115 | type: 'text',
116 | text: typeof result === 'string' ? (result as string) : JSON.stringify(result),
117 | };
118 | const logOutput: ContentBlock | null =
119 | logLines.length === 0 ?
120 | null
121 | : {
122 | type: 'text',
123 | text: logLines.join('\n'),
124 | };
125 | const errOutput: ContentBlock | null =
126 | errLines.length === 0 ?
127 | null
128 | : {
129 | type: 'text',
130 | text: 'Error output:\n' + errLines.join('\n'),
131 | };
132 | return {
133 | content: [returnOutput, logOutput, errOutput].filter((block) => block !== null),
134 | };
135 | } else {
136 | const { message } = (await resp.json()) as WorkerError;
137 | throw new Error(message);
138 | }
139 | } catch (e) {
140 | throw e;
141 | } finally {
142 | worker.terminate();
143 | }
144 | };
145 |
146 | return { metadata, tool, handler };
147 | }
148 |
```
--------------------------------------------------------------------------------
/src/resources/discounts.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { APIResource } from '../core/resource';
4 | import { APIPromise } from '../core/api-promise';
5 | import {
6 | DefaultPageNumberPagination,
7 | type DefaultPageNumberPaginationParams,
8 | PagePromise,
9 | } from '../core/pagination';
10 | import { buildHeaders } from '../internal/headers';
11 | import { RequestOptions } from '../internal/request-options';
12 | import { path } from '../internal/utils/path';
13 |
14 | export class Discounts extends APIResource {
15 | /**
16 | * POST /discounts If `code` is omitted or empty, a random 16-char uppercase code
17 | * is generated.
18 | */
19 | create(body: DiscountCreateParams, options?: RequestOptions): APIPromise<Discount> {
20 | return this._client.post('/discounts', { body, ...options });
21 | }
22 |
23 | /**
24 | * GET /discounts/{discount_id}
25 | */
26 | retrieve(discountID: string, options?: RequestOptions): APIPromise<Discount> {
27 | return this._client.get(path`/discounts/${discountID}`, options);
28 | }
29 |
30 | /**
31 | * PATCH /discounts/{discount_id}
32 | */
33 | update(discountID: string, body: DiscountUpdateParams, options?: RequestOptions): APIPromise<Discount> {
34 | return this._client.patch(path`/discounts/${discountID}`, { body, ...options });
35 | }
36 |
37 | /**
38 | * GET /discounts
39 | */
40 | list(
41 | query: DiscountListParams | null | undefined = {},
42 | options?: RequestOptions,
43 | ): PagePromise<DiscountsDefaultPageNumberPagination, Discount> {
44 | return this._client.getAPIList('/discounts', DefaultPageNumberPagination<Discount>, {
45 | query,
46 | ...options,
47 | });
48 | }
49 |
50 | /**
51 | * DELETE /discounts/{discount_id}
52 | */
53 | delete(discountID: string, options?: RequestOptions): APIPromise<void> {
54 | return this._client.delete(path`/discounts/${discountID}`, {
55 | ...options,
56 | headers: buildHeaders([{ Accept: '*/*' }, options?.headers]),
57 | });
58 | }
59 | }
60 |
61 | export type DiscountsDefaultPageNumberPagination = DefaultPageNumberPagination<Discount>;
62 |
63 | export interface Discount {
64 | /**
65 | * The discount amount.
66 | *
67 | * - If `discount_type` is `percentage`, this is in **basis points** (e.g., 540 =>
68 | * 5.4%).
69 | * - Otherwise, this is **USD cents** (e.g., 100 => `$1.00`).
70 | */
71 | amount: number;
72 |
73 | /**
74 | * The business this discount belongs to.
75 | */
76 | business_id: string;
77 |
78 | /**
79 | * The discount code (up to 16 chars).
80 | */
81 | code: string;
82 |
83 | /**
84 | * Timestamp when the discount is created
85 | */
86 | created_at: string;
87 |
88 | /**
89 | * The unique discount ID
90 | */
91 | discount_id: string;
92 |
93 | /**
94 | * List of product IDs to which this discount is restricted.
95 | */
96 | restricted_to: Array<string>;
97 |
98 | /**
99 | * How many times this discount has been used.
100 | */
101 | times_used: number;
102 |
103 | /**
104 | * The type of discount, e.g. `percentage`, `flat`, or `flat_per_unit`.
105 | */
106 | type: DiscountType;
107 |
108 | /**
109 | * Optional date/time after which discount is expired.
110 | */
111 | expires_at?: string | null;
112 |
113 | /**
114 | * Name for the Discount
115 | */
116 | name?: string | null;
117 |
118 | /**
119 | * Number of subscription billing cycles this discount is valid for. If not
120 | * provided, the discount will be applied indefinitely to all recurring payments
121 | * related to the subscription.
122 | */
123 | subscription_cycles?: number | null;
124 |
125 | /**
126 | * Usage limit for this discount, if any.
127 | */
128 | usage_limit?: number | null;
129 | }
130 |
131 | export type DiscountType = 'percentage';
132 |
133 | export interface DiscountCreateParams {
134 | /**
135 | * The discount amount.
136 | *
137 | * - If `discount_type` is **not** `percentage`, `amount` is in **USD cents**. For
138 | * example, `100` means `$1.00`. Only USD is allowed.
139 | * - If `discount_type` **is** `percentage`, `amount` is in **basis points**. For
140 | * example, `540` means `5.4%`.
141 | *
142 | * Must be at least 1.
143 | */
144 | amount: number;
145 |
146 | /**
147 | * The discount type (e.g. `percentage`, `flat`, or `flat_per_unit`).
148 | */
149 | type: DiscountType;
150 |
151 | /**
152 | * Optionally supply a code (will be uppercased).
153 | *
154 | * - Must be at least 3 characters if provided.
155 | * - If omitted, a random 16-character code is generated.
156 | */
157 | code?: string | null;
158 |
159 | /**
160 | * When the discount expires, if ever.
161 | */
162 | expires_at?: string | null;
163 |
164 | name?: string | null;
165 |
166 | /**
167 | * List of product IDs to restrict usage (if any).
168 | */
169 | restricted_to?: Array<string> | null;
170 |
171 | /**
172 | * Number of subscription billing cycles this discount is valid for. If not
173 | * provided, the discount will be applied indefinitely to all recurring payments
174 | * related to the subscription.
175 | */
176 | subscription_cycles?: number | null;
177 |
178 | /**
179 | * How many times this discount can be used (if any). Must be >= 1 if provided.
180 | */
181 | usage_limit?: number | null;
182 | }
183 |
184 | export interface DiscountUpdateParams {
185 | /**
186 | * If present, update the discount amount:
187 | *
188 | * - If `discount_type` is `percentage`, this represents **basis points** (e.g.,
189 | * `540` = `5.4%`).
190 | * - Otherwise, this represents **USD cents** (e.g., `100` = `$1.00`).
191 | *
192 | * Must be at least 1 if provided.
193 | */
194 | amount?: number | null;
195 |
196 | /**
197 | * If present, update the discount code (uppercase).
198 | */
199 | code?: string | null;
200 |
201 | expires_at?: string | null;
202 |
203 | name?: string | null;
204 |
205 | /**
206 | * If present, replaces all restricted product IDs with this new set. To remove all
207 | * restrictions, send empty array
208 | */
209 | restricted_to?: Array<string> | null;
210 |
211 | /**
212 | * Number of subscription billing cycles this discount is valid for. If not
213 | * provided, the discount will be applied indefinitely to all recurring payments
214 | * related to the subscription.
215 | */
216 | subscription_cycles?: number | null;
217 |
218 | /**
219 | * If present, update the discount type.
220 | */
221 | type?: DiscountType | null;
222 |
223 | usage_limit?: number | null;
224 | }
225 |
226 | export interface DiscountListParams extends DefaultPageNumberPaginationParams {}
227 |
228 | export declare namespace Discounts {
229 | export {
230 | type Discount as Discount,
231 | type DiscountType as DiscountType,
232 | type DiscountsDefaultPageNumberPagination as DiscountsDefaultPageNumberPagination,
233 | type DiscountCreateParams as DiscountCreateParams,
234 | type DiscountUpdateParams as DiscountUpdateParams,
235 | type DiscountListParams as DiscountListParams,
236 | };
237 | }
238 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/addons/list-addons.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { maybeFilter } from 'dodopayments-mcp/filtering';
4 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
5 |
6 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
7 | import DodoPayments from 'dodopayments';
8 |
9 | export const metadata: Metadata = {
10 | resource: 'addons',
11 | operation: 'read',
12 | tags: [],
13 | httpMethod: 'get',
14 | httpPath: '/addons',
15 | operationId: 'list_addons',
16 | };
17 |
18 | export const tool: Tool = {
19 | name: 'list_addons',
20 | description:
21 | "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\n\n\n# Response Schema\n```json\n{\n type: 'object',\n properties: {\n items: {\n type: 'array',\n items: {\n $ref: '#/$defs/addon_response'\n }\n }\n },\n required: [ 'items'\n ],\n $defs: {\n addon_response: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'id of the Addon'\n },\n business_id: {\n type: 'string',\n description: 'Unique identifier for the business to which the addon belongs.'\n },\n created_at: {\n type: 'string',\n description: 'Created time',\n format: 'date-time'\n },\n currency: {\n $ref: '#/$defs/currency'\n },\n name: {\n type: 'string',\n description: 'Name of the Addon'\n },\n price: {\n type: 'integer',\n description: 'Amount of the addon'\n },\n tax_category: {\n $ref: '#/$defs/tax_category'\n },\n updated_at: {\n type: 'string',\n description: 'Updated time',\n format: 'date-time'\n },\n description: {\n type: 'string',\n description: 'Optional description of the Addon'\n },\n image: {\n type: 'string',\n description: 'Image of the Addon'\n }\n },\n required: [ 'id',\n 'business_id',\n 'created_at',\n 'currency',\n 'name',\n 'price',\n 'tax_category',\n 'updated_at'\n ]\n },\n currency: {\n type: 'string',\n enum: [ 'AED',\n 'ALL',\n 'AMD',\n 'ANG',\n 'AOA',\n 'ARS',\n 'AUD',\n 'AWG',\n 'AZN',\n 'BAM',\n 'BBD',\n 'BDT',\n 'BGN',\n 'BHD',\n 'BIF',\n 'BMD',\n 'BND',\n 'BOB',\n 'BRL',\n 'BSD',\n 'BWP',\n 'BYN',\n 'BZD',\n 'CAD',\n 'CHF',\n 'CLP',\n 'CNY',\n 'COP',\n 'CRC',\n 'CUP',\n 'CVE',\n 'CZK',\n 'DJF',\n 'DKK',\n 'DOP',\n 'DZD',\n 'EGP',\n 'ETB',\n 'EUR',\n 'FJD',\n 'FKP',\n 'GBP',\n 'GEL',\n 'GHS',\n 'GIP',\n 'GMD',\n 'GNF',\n 'GTQ',\n 'GYD',\n 'HKD',\n 'HNL',\n 'HRK',\n 'HTG',\n 'HUF',\n 'IDR',\n 'ILS',\n 'INR',\n 'IQD',\n 'JMD',\n 'JOD',\n 'JPY',\n 'KES',\n 'KGS',\n 'KHR',\n 'KMF',\n 'KRW',\n 'KWD',\n 'KYD',\n 'KZT',\n 'LAK',\n 'LBP',\n 'LKR',\n 'LRD',\n 'LSL',\n 'LYD',\n 'MAD',\n 'MDL',\n 'MGA',\n 'MKD',\n 'MMK',\n 'MNT',\n 'MOP',\n 'MRU',\n 'MUR',\n 'MVR',\n 'MWK',\n 'MXN',\n 'MYR',\n 'MZN',\n 'NAD',\n 'NGN',\n 'NIO',\n 'NOK',\n 'NPR',\n 'NZD',\n 'OMR',\n 'PAB',\n 'PEN',\n 'PGK',\n 'PHP',\n 'PKR',\n 'PLN',\n 'PYG',\n 'QAR',\n 'RON',\n 'RSD',\n 'RUB',\n 'RWF',\n 'SAR',\n 'SBD',\n 'SCR',\n 'SEK',\n 'SGD',\n 'SHP',\n 'SLE',\n 'SLL',\n 'SOS',\n 'SRD',\n 'SSP',\n 'STN',\n 'SVC',\n 'SZL',\n 'THB',\n 'TND',\n 'TOP',\n 'TRY',\n 'TTD',\n 'TWD',\n 'TZS',\n 'UAH',\n 'UGX',\n 'USD',\n 'UYU',\n 'UZS',\n 'VES',\n 'VND',\n 'VUV',\n 'WST',\n 'XAF',\n 'XCD',\n 'XOF',\n 'XPF',\n 'YER',\n 'ZAR',\n 'ZMW'\n ]\n },\n tax_category: {\n type: 'string',\n description: 'Represents the different categories of taxation applicable to various products and services.',\n enum: [ 'digital_products',\n 'saas',\n 'e_book',\n 'edtech'\n ]\n }\n }\n}\n```",
22 | inputSchema: {
23 | type: 'object',
24 | properties: {
25 | page_number: {
26 | type: 'integer',
27 | description: 'Page number default is 0',
28 | },
29 | page_size: {
30 | type: 'integer',
31 | description: 'Page size default is 10 max is 100',
32 | },
33 | jq_filter: {
34 | type: 'string',
35 | title: 'jq Filter',
36 | description:
37 | 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).',
38 | },
39 | },
40 | required: [],
41 | },
42 | annotations: {
43 | readOnlyHint: true,
44 | },
45 | };
46 |
47 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
48 | const { jq_filter, ...body } = args as any;
49 | const response = await client.addons.list(body).asResponse();
50 | return asTextContentResult(await maybeFilter(jq_filter, await response.json()));
51 | };
52 |
53 | export default { metadata, tool, handler };
54 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/subscriptions/charge-subscriptions.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { maybeFilter } from 'dodopayments-mcp/filtering';
4 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
5 |
6 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
7 | import DodoPayments from 'dodopayments';
8 |
9 | export const metadata: Metadata = {
10 | resource: 'subscriptions',
11 | operation: 'write',
12 | tags: [],
13 | httpMethod: 'post',
14 | httpPath: '/subscriptions/{subscription_id}/charge',
15 | operationId: 'create_subscription_charge',
16 | };
17 |
18 | export const tool: Tool = {
19 | name: 'charge_subscriptions',
20 | description:
21 | "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\n\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/subscription_charge_response',\n $defs: {\n subscription_charge_response: {\n type: 'object',\n properties: {\n payment_id: {\n type: 'string'\n }\n },\n required: [ 'payment_id'\n ]\n }\n }\n}\n```",
22 | inputSchema: {
23 | type: 'object',
24 | properties: {
25 | subscription_id: {
26 | type: 'string',
27 | },
28 | product_price: {
29 | type: 'integer',
30 | description:
31 | 'The product price. Represented in the lowest denomination of the currency (e.g., cents for USD).\nFor example, to charge $1.00, pass `100`.',
32 | },
33 | adaptive_currency_fees_inclusive: {
34 | type: 'boolean',
35 | description:
36 | '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.',
37 | },
38 | customer_balance_config: {
39 | type: 'object',
40 | description: 'Specify how customer balance is used for the payment',
41 | properties: {
42 | allow_customer_credits_purchase: {
43 | type: 'boolean',
44 | description: 'Allows Customer Credit to be purchased to settle payments',
45 | },
46 | allow_customer_credits_usage: {
47 | type: 'boolean',
48 | description: 'Allows Customer Credit Balance to be used to settle payments',
49 | },
50 | },
51 | },
52 | metadata: {
53 | type: 'object',
54 | description:
55 | 'Metadata for the payment. If not passed, the metadata of the subscription will be taken',
56 | additionalProperties: true,
57 | },
58 | product_currency: {
59 | $ref: '#/$defs/currency',
60 | },
61 | product_description: {
62 | type: 'string',
63 | description:
64 | 'Optional product description override for billing and line items.\nIf not specified, the stored description of the product will be used.',
65 | },
66 | jq_filter: {
67 | type: 'string',
68 | title: 'jq Filter',
69 | description:
70 | 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).',
71 | },
72 | },
73 | required: ['subscription_id', 'product_price'],
74 | $defs: {
75 | currency: {
76 | type: 'string',
77 | enum: [
78 | 'AED',
79 | 'ALL',
80 | 'AMD',
81 | 'ANG',
82 | 'AOA',
83 | 'ARS',
84 | 'AUD',
85 | 'AWG',
86 | 'AZN',
87 | 'BAM',
88 | 'BBD',
89 | 'BDT',
90 | 'BGN',
91 | 'BHD',
92 | 'BIF',
93 | 'BMD',
94 | 'BND',
95 | 'BOB',
96 | 'BRL',
97 | 'BSD',
98 | 'BWP',
99 | 'BYN',
100 | 'BZD',
101 | 'CAD',
102 | 'CHF',
103 | 'CLP',
104 | 'CNY',
105 | 'COP',
106 | 'CRC',
107 | 'CUP',
108 | 'CVE',
109 | 'CZK',
110 | 'DJF',
111 | 'DKK',
112 | 'DOP',
113 | 'DZD',
114 | 'EGP',
115 | 'ETB',
116 | 'EUR',
117 | 'FJD',
118 | 'FKP',
119 | 'GBP',
120 | 'GEL',
121 | 'GHS',
122 | 'GIP',
123 | 'GMD',
124 | 'GNF',
125 | 'GTQ',
126 | 'GYD',
127 | 'HKD',
128 | 'HNL',
129 | 'HRK',
130 | 'HTG',
131 | 'HUF',
132 | 'IDR',
133 | 'ILS',
134 | 'INR',
135 | 'IQD',
136 | 'JMD',
137 | 'JOD',
138 | 'JPY',
139 | 'KES',
140 | 'KGS',
141 | 'KHR',
142 | 'KMF',
143 | 'KRW',
144 | 'KWD',
145 | 'KYD',
146 | 'KZT',
147 | 'LAK',
148 | 'LBP',
149 | 'LKR',
150 | 'LRD',
151 | 'LSL',
152 | 'LYD',
153 | 'MAD',
154 | 'MDL',
155 | 'MGA',
156 | 'MKD',
157 | 'MMK',
158 | 'MNT',
159 | 'MOP',
160 | 'MRU',
161 | 'MUR',
162 | 'MVR',
163 | 'MWK',
164 | 'MXN',
165 | 'MYR',
166 | 'MZN',
167 | 'NAD',
168 | 'NGN',
169 | 'NIO',
170 | 'NOK',
171 | 'NPR',
172 | 'NZD',
173 | 'OMR',
174 | 'PAB',
175 | 'PEN',
176 | 'PGK',
177 | 'PHP',
178 | 'PKR',
179 | 'PLN',
180 | 'PYG',
181 | 'QAR',
182 | 'RON',
183 | 'RSD',
184 | 'RUB',
185 | 'RWF',
186 | 'SAR',
187 | 'SBD',
188 | 'SCR',
189 | 'SEK',
190 | 'SGD',
191 | 'SHP',
192 | 'SLE',
193 | 'SLL',
194 | 'SOS',
195 | 'SRD',
196 | 'SSP',
197 | 'STN',
198 | 'SVC',
199 | 'SZL',
200 | 'THB',
201 | 'TND',
202 | 'TOP',
203 | 'TRY',
204 | 'TTD',
205 | 'TWD',
206 | 'TZS',
207 | 'UAH',
208 | 'UGX',
209 | 'USD',
210 | 'UYU',
211 | 'UZS',
212 | 'VES',
213 | 'VND',
214 | 'VUV',
215 | 'WST',
216 | 'XAF',
217 | 'XCD',
218 | 'XOF',
219 | 'XPF',
220 | 'YER',
221 | 'ZAR',
222 | 'ZMW',
223 | ],
224 | },
225 | },
226 | },
227 | annotations: {},
228 | };
229 |
230 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
231 | const { subscription_id, jq_filter, ...body } = args as any;
232 | return asTextContentResult(
233 | await maybeFilter(jq_filter, await client.subscriptions.charge(subscription_id, body)),
234 | );
235 | };
236 |
237 | export default { metadata, tool, handler };
238 |
```
--------------------------------------------------------------------------------
/src/internal/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | export type PromiseOrValue<T> = T | Promise<T>;
4 | export type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
5 |
6 | export type KeysEnum<T> = { [P in keyof Required<T>]: true };
7 |
8 | export type FinalizedRequestInit = RequestInit & { headers: Headers };
9 |
10 | type NotAny<T> = [0] extends [1 & T] ? never : T;
11 |
12 | /**
13 | * Some environments overload the global fetch function, and Parameters<T> only gets the last signature.
14 | */
15 | type OverloadedParameters<T> =
16 | T extends (
17 | {
18 | (...args: infer A): unknown;
19 | (...args: infer B): unknown;
20 | (...args: infer C): unknown;
21 | (...args: infer D): unknown;
22 | }
23 | ) ?
24 | A | B | C | D
25 | : T extends (
26 | {
27 | (...args: infer A): unknown;
28 | (...args: infer B): unknown;
29 | (...args: infer C): unknown;
30 | }
31 | ) ?
32 | A | B | C
33 | : T extends (
34 | {
35 | (...args: infer A): unknown;
36 | (...args: infer B): unknown;
37 | }
38 | ) ?
39 | A | B
40 | : T extends (...args: infer A) => unknown ? A
41 | : never;
42 |
43 | /* eslint-disable */
44 | /**
45 | * These imports attempt to get types from a parent package's dependencies.
46 | * Unresolved bare specifiers can trigger [automatic type acquisition][1] in some projects, which
47 | * would cause typescript to show types not present at runtime. To avoid this, we import
48 | * directly from parent node_modules folders.
49 | *
50 | * We need to check multiple levels because we don't know what directory structure we'll be in.
51 | * For example, pnpm generates directories like this:
52 | * ```
53 | * node_modules
54 | * ├── .pnpm
55 | * │ └── [email protected]
56 | * │ └── node_modules
57 | * │ └── pkg
58 | * │ └── internal
59 | * │ └── types.d.ts
60 | * ├── pkg -> .pnpm/[email protected]/node_modules/pkg
61 | * └── undici
62 | * ```
63 | *
64 | * [1]: https://www.typescriptlang.org/tsconfig/#typeAcquisition
65 | */
66 | /** @ts-ignore For users with \@types/node */
67 | type UndiciTypesRequestInit = NotAny<import('../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../../node_modules/undici-types/index.d.ts').RequestInit>;
68 | /** @ts-ignore For users with undici */
69 | type UndiciRequestInit = NotAny<import('../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../../node_modules/undici/index.d.ts').RequestInit>;
70 | /** @ts-ignore For users with \@types/bun */
71 | type BunRequestInit = globalThis.FetchRequestInit;
72 | /** @ts-ignore For users with node-fetch@2 */
73 | type NodeFetch2RequestInit = NotAny<import('../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit>;
74 | /** @ts-ignore For users with node-fetch@3, doesn't need file extension because types are at ./@types/index.d.ts */
75 | type NodeFetch3RequestInit = NotAny<import('../node_modules/node-fetch').RequestInit> | NotAny<import('../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../../../../../node_modules/node-fetch').RequestInit>;
76 | /** @ts-ignore For users who use Deno */
77 | type FetchRequestInit = NonNullable<OverloadedParameters<typeof fetch>[1]>;
78 | /* eslint-enable */
79 |
80 | type RequestInits =
81 | | NotAny<UndiciTypesRequestInit>
82 | | NotAny<UndiciRequestInit>
83 | | NotAny<BunRequestInit>
84 | | NotAny<NodeFetch2RequestInit>
85 | | NotAny<NodeFetch3RequestInit>
86 | | NotAny<RequestInit>
87 | | NotAny<FetchRequestInit>;
88 |
89 | /**
90 | * This type contains `RequestInit` options that may be available on the current runtime,
91 | * including per-platform extensions like `dispatcher`, `agent`, `client`, etc.
92 | */
93 | export type MergedRequestInit = RequestInits &
94 | /** We don't include these in the types as they'll be overridden for every request. */
95 | Partial<Record<'body' | 'headers' | 'method' | 'signal', never>>;
96 |
```
--------------------------------------------------------------------------------
/src/internal/detect-platform.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { VERSION } from '../version';
4 |
5 | export const isRunningInBrowser = () => {
6 | return (
7 | // @ts-ignore
8 | typeof window !== 'undefined' &&
9 | // @ts-ignore
10 | typeof window.document !== 'undefined' &&
11 | // @ts-ignore
12 | typeof navigator !== 'undefined'
13 | );
14 | };
15 |
16 | type DetectedPlatform = 'deno' | 'node' | 'edge' | 'unknown';
17 |
18 | /**
19 | * Note this does not detect 'browser'; for that, use getBrowserInfo().
20 | */
21 | function getDetectedPlatform(): DetectedPlatform {
22 | if (typeof Deno !== 'undefined' && Deno.build != null) {
23 | return 'deno';
24 | }
25 | if (typeof EdgeRuntime !== 'undefined') {
26 | return 'edge';
27 | }
28 | if (
29 | Object.prototype.toString.call(
30 | typeof (globalThis as any).process !== 'undefined' ? (globalThis as any).process : 0,
31 | ) === '[object process]'
32 | ) {
33 | return 'node';
34 | }
35 | return 'unknown';
36 | }
37 |
38 | declare const Deno: any;
39 | declare const EdgeRuntime: any;
40 | type Arch = 'x32' | 'x64' | 'arm' | 'arm64' | `other:${string}` | 'unknown';
41 | type PlatformName =
42 | | 'MacOS'
43 | | 'Linux'
44 | | 'Windows'
45 | | 'FreeBSD'
46 | | 'OpenBSD'
47 | | 'iOS'
48 | | 'Android'
49 | | `Other:${string}`
50 | | 'Unknown';
51 | type Browser = 'ie' | 'edge' | 'chrome' | 'firefox' | 'safari';
52 | type PlatformProperties = {
53 | 'X-Stainless-Lang': 'js';
54 | 'X-Stainless-Package-Version': string;
55 | 'X-Stainless-OS': PlatformName;
56 | 'X-Stainless-Arch': Arch;
57 | 'X-Stainless-Runtime': 'node' | 'deno' | 'edge' | `browser:${Browser}` | 'unknown';
58 | 'X-Stainless-Runtime-Version': string;
59 | };
60 | const getPlatformProperties = (): PlatformProperties => {
61 | const detectedPlatform = getDetectedPlatform();
62 | if (detectedPlatform === 'deno') {
63 | return {
64 | 'X-Stainless-Lang': 'js',
65 | 'X-Stainless-Package-Version': VERSION,
66 | 'X-Stainless-OS': normalizePlatform(Deno.build.os),
67 | 'X-Stainless-Arch': normalizeArch(Deno.build.arch),
68 | 'X-Stainless-Runtime': 'deno',
69 | 'X-Stainless-Runtime-Version':
70 | typeof Deno.version === 'string' ? Deno.version : Deno.version?.deno ?? 'unknown',
71 | };
72 | }
73 | if (typeof EdgeRuntime !== 'undefined') {
74 | return {
75 | 'X-Stainless-Lang': 'js',
76 | 'X-Stainless-Package-Version': VERSION,
77 | 'X-Stainless-OS': 'Unknown',
78 | 'X-Stainless-Arch': `other:${EdgeRuntime}`,
79 | 'X-Stainless-Runtime': 'edge',
80 | 'X-Stainless-Runtime-Version': (globalThis as any).process.version,
81 | };
82 | }
83 | // Check if Node.js
84 | if (detectedPlatform === 'node') {
85 | return {
86 | 'X-Stainless-Lang': 'js',
87 | 'X-Stainless-Package-Version': VERSION,
88 | 'X-Stainless-OS': normalizePlatform((globalThis as any).process.platform ?? 'unknown'),
89 | 'X-Stainless-Arch': normalizeArch((globalThis as any).process.arch ?? 'unknown'),
90 | 'X-Stainless-Runtime': 'node',
91 | 'X-Stainless-Runtime-Version': (globalThis as any).process.version ?? 'unknown',
92 | };
93 | }
94 |
95 | const browserInfo = getBrowserInfo();
96 | if (browserInfo) {
97 | return {
98 | 'X-Stainless-Lang': 'js',
99 | 'X-Stainless-Package-Version': VERSION,
100 | 'X-Stainless-OS': 'Unknown',
101 | 'X-Stainless-Arch': 'unknown',
102 | 'X-Stainless-Runtime': `browser:${browserInfo.browser}`,
103 | 'X-Stainless-Runtime-Version': browserInfo.version,
104 | };
105 | }
106 |
107 | // TODO add support for Cloudflare workers, etc.
108 | return {
109 | 'X-Stainless-Lang': 'js',
110 | 'X-Stainless-Package-Version': VERSION,
111 | 'X-Stainless-OS': 'Unknown',
112 | 'X-Stainless-Arch': 'unknown',
113 | 'X-Stainless-Runtime': 'unknown',
114 | 'X-Stainless-Runtime-Version': 'unknown',
115 | };
116 | };
117 |
118 | type BrowserInfo = {
119 | browser: Browser;
120 | version: string;
121 | };
122 |
123 | declare const navigator: { userAgent: string } | undefined;
124 |
125 | // Note: modified from https://github.com/JS-DevTools/host-environment/blob/b1ab79ecde37db5d6e163c050e54fe7d287d7c92/src/isomorphic.browser.ts
126 | function getBrowserInfo(): BrowserInfo | null {
127 | if (typeof navigator === 'undefined' || !navigator) {
128 | return null;
129 | }
130 |
131 | // NOTE: The order matters here!
132 | const browserPatterns = [
133 | { key: 'edge' as const, pattern: /Edge(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
134 | { key: 'ie' as const, pattern: /MSIE(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
135 | { key: 'ie' as const, pattern: /Trident(?:.*rv\:(\d+)\.(\d+)(?:\.(\d+))?)?/ },
136 | { key: 'chrome' as const, pattern: /Chrome(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
137 | { key: 'firefox' as const, pattern: /Firefox(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
138 | { key: 'safari' as const, pattern: /(?:Version\W+(\d+)\.(\d+)(?:\.(\d+))?)?(?:\W+Mobile\S*)?\W+Safari/ },
139 | ];
140 |
141 | // Find the FIRST matching browser
142 | for (const { key, pattern } of browserPatterns) {
143 | const match = pattern.exec(navigator.userAgent);
144 | if (match) {
145 | const major = match[1] || 0;
146 | const minor = match[2] || 0;
147 | const patch = match[3] || 0;
148 |
149 | return { browser: key, version: `${major}.${minor}.${patch}` };
150 | }
151 | }
152 |
153 | return null;
154 | }
155 |
156 | const normalizeArch = (arch: string): Arch => {
157 | // Node docs:
158 | // - https://nodejs.org/api/process.html#processarch
159 | // Deno docs:
160 | // - https://doc.deno.land/deno/stable/~/Deno.build
161 | if (arch === 'x32') return 'x32';
162 | if (arch === 'x86_64' || arch === 'x64') return 'x64';
163 | if (arch === 'arm') return 'arm';
164 | if (arch === 'aarch64' || arch === 'arm64') return 'arm64';
165 | if (arch) return `other:${arch}`;
166 | return 'unknown';
167 | };
168 |
169 | const normalizePlatform = (platform: string): PlatformName => {
170 | // Node platforms:
171 | // - https://nodejs.org/api/process.html#processplatform
172 | // Deno platforms:
173 | // - https://doc.deno.land/deno/stable/~/Deno.build
174 | // - https://github.com/denoland/deno/issues/14799
175 |
176 | platform = platform.toLowerCase();
177 |
178 | // NOTE: this iOS check is untested and may not work
179 | // Node does not work natively on IOS, there is a fork at
180 | // https://github.com/nodejs-mobile/nodejs-mobile
181 | // however it is unknown at the time of writing how to detect if it is running
182 | if (platform.includes('ios')) return 'iOS';
183 | if (platform === 'android') return 'Android';
184 | if (platform === 'darwin') return 'MacOS';
185 | if (platform === 'win32') return 'Windows';
186 | if (platform === 'freebsd') return 'FreeBSD';
187 | if (platform === 'openbsd') return 'OpenBSD';
188 | if (platform === 'linux') return 'Linux';
189 | if (platform) return `Other:${platform}`;
190 | return 'Unknown';
191 | };
192 |
193 | let _platformHeaders: PlatformProperties;
194 | export const getPlatformHeaders = () => {
195 | return (_platformHeaders ??= getPlatformProperties());
196 | };
197 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5 | import { Endpoint, endpoints, HandlerFunction, query } from './tools';
6 | import {
7 | CallToolRequestSchema,
8 | ListToolsRequestSchema,
9 | SetLevelRequestSchema,
10 | Implementation,
11 | Tool,
12 | } from '@modelcontextprotocol/sdk/types.js';
13 | import { ClientOptions } from 'dodopayments';
14 | import DodoPayments from 'dodopayments';
15 | import {
16 | applyCompatibilityTransformations,
17 | ClientCapabilities,
18 | defaultClientCapabilities,
19 | knownClients,
20 | parseEmbeddedJSON,
21 | } from './compat';
22 | import { dynamicTools } from './dynamic-tools';
23 | import { codeTool } from './code-tool';
24 | import docsSearchTool from './docs-search-tool';
25 | import { McpOptions } from './options';
26 |
27 | export { McpOptions } from './options';
28 | export { ClientType } from './compat';
29 | export { Filter } from './tools';
30 | export { ClientOptions } from 'dodopayments';
31 | export { endpoints } from './tools';
32 |
33 | export const newMcpServer = () =>
34 | new McpServer(
35 | {
36 | name: 'dodopayments_api',
37 | version: '2.4.1',
38 | },
39 | { capabilities: { tools: {}, logging: {} } },
40 | );
41 |
42 | // Create server instance
43 | export const server = newMcpServer();
44 |
45 | /**
46 | * Initializes the provided MCP Server with the given tools and handlers.
47 | * If not provided, the default client, tools and handlers will be used.
48 | */
49 | export function initMcpServer(params: {
50 | server: Server | McpServer;
51 | clientOptions?: ClientOptions;
52 | mcpOptions?: McpOptions;
53 | }) {
54 | const server = params.server instanceof McpServer ? params.server.server : params.server;
55 | const mcpOptions = params.mcpOptions ?? {};
56 |
57 | let providedEndpoints: Endpoint[] | null = null;
58 | let endpointMap: Record<string, Endpoint> | null = null;
59 |
60 | const initTools = async (implementation?: Implementation) => {
61 | if (implementation && (!mcpOptions.client || mcpOptions.client === 'infer')) {
62 | mcpOptions.client =
63 | implementation.name.toLowerCase().includes('claude') ? 'claude'
64 | : implementation.name.toLowerCase().includes('cursor') ? 'cursor'
65 | : undefined;
66 | mcpOptions.capabilities = {
67 | ...(mcpOptions.client && knownClients[mcpOptions.client]),
68 | ...mcpOptions.capabilities,
69 | };
70 | }
71 | providedEndpoints ??= await selectTools(endpoints, mcpOptions);
72 | endpointMap ??= Object.fromEntries(providedEndpoints.map((endpoint) => [endpoint.tool.name, endpoint]));
73 | };
74 |
75 | const logAtLevel =
76 | (level: 'debug' | 'info' | 'warning' | 'error') =>
77 | (message: string, ...rest: unknown[]) => {
78 | void server.sendLoggingMessage({
79 | level,
80 | data: { message, rest },
81 | });
82 | };
83 | const logger = {
84 | debug: logAtLevel('debug'),
85 | info: logAtLevel('info'),
86 | warn: logAtLevel('warning'),
87 | error: logAtLevel('error'),
88 | };
89 |
90 | let client = new DodoPayments({
91 | ...{ environment: (readEnv('DODO_PAYMENTS_ENVIRONMENT') || undefined) as any },
92 | logger,
93 | ...params.clientOptions,
94 | defaultHeaders: {
95 | ...params.clientOptions?.defaultHeaders,
96 | 'X-Stainless-MCP': 'true',
97 | },
98 | });
99 |
100 | server.setRequestHandler(ListToolsRequestSchema, async () => {
101 | if (providedEndpoints === null) {
102 | await initTools(server.getClientVersion());
103 | }
104 | return {
105 | tools: providedEndpoints!.map((endpoint) => endpoint.tool),
106 | };
107 | });
108 |
109 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
110 | if (endpointMap === null) {
111 | await initTools(server.getClientVersion());
112 | }
113 | const { name, arguments: args } = request.params;
114 | const endpoint = endpointMap![name];
115 | if (!endpoint) {
116 | throw new Error(`Unknown tool: ${name}`);
117 | }
118 |
119 | return executeHandler(endpoint.tool, endpoint.handler, client, args, mcpOptions.capabilities);
120 | });
121 |
122 | server.setRequestHandler(SetLevelRequestSchema, async (request) => {
123 | const { level } = request.params;
124 | switch (level) {
125 | case 'debug':
126 | client = client.withOptions({ logLevel: 'debug' });
127 | break;
128 | case 'info':
129 | client = client.withOptions({ logLevel: 'info' });
130 | break;
131 | case 'notice':
132 | case 'warning':
133 | client = client.withOptions({ logLevel: 'warn' });
134 | break;
135 | case 'error':
136 | client = client.withOptions({ logLevel: 'error' });
137 | break;
138 | default:
139 | client = client.withOptions({ logLevel: 'off' });
140 | break;
141 | }
142 | return {};
143 | });
144 | }
145 |
146 | /**
147 | * Selects the tools to include in the MCP Server based on the provided options.
148 | */
149 | export async function selectTools(endpoints: Endpoint[], options?: McpOptions): Promise<Endpoint[]> {
150 | const filteredEndpoints = query(options?.filters ?? [], endpoints);
151 |
152 | let includedTools = filteredEndpoints.slice();
153 |
154 | if (includedTools.length > 0) {
155 | if (options?.includeDynamicTools) {
156 | includedTools = dynamicTools(includedTools);
157 | }
158 | } else {
159 | if (options?.includeAllTools) {
160 | includedTools = endpoints.slice();
161 | } else if (options?.includeDynamicTools) {
162 | includedTools = dynamicTools(endpoints);
163 | } else if (options?.includeCodeTools) {
164 | includedTools = [await codeTool()];
165 | } else {
166 | includedTools = endpoints.slice();
167 | }
168 | }
169 | if (options?.includeDocsTools ?? true) {
170 | includedTools.push(docsSearchTool);
171 | }
172 | const capabilities = { ...defaultClientCapabilities, ...options?.capabilities };
173 | return applyCompatibilityTransformations(includedTools, capabilities);
174 | }
175 |
176 | /**
177 | * Runs the provided handler with the given client and arguments.
178 | */
179 | export async function executeHandler(
180 | tool: Tool,
181 | handler: HandlerFunction,
182 | client: DodoPayments,
183 | args: Record<string, unknown> | undefined,
184 | compatibilityOptions?: Partial<ClientCapabilities>,
185 | ) {
186 | const options = { ...defaultClientCapabilities, ...compatibilityOptions };
187 | if (!options.validJson && args) {
188 | args = parseEmbeddedJSON(args, tool.inputSchema);
189 | }
190 | return await handler(client, args || {});
191 | }
192 |
193 | export const readEnv = (env: string): string | undefined => {
194 | if (typeof (globalThis as any).process !== 'undefined') {
195 | return (globalThis as any).process.env?.[env]?.trim();
196 | } else if (typeof (globalThis as any).Deno !== 'undefined') {
197 | return (globalThis as any).Deno.env?.get?.(env)?.trim();
198 | }
199 | return;
200 | };
201 |
202 | export const readEnvOrError = (env: string): string => {
203 | let envValue = readEnv(env);
204 | if (envValue === undefined) {
205 | throw new Error(`Environment variable ${env} is not set`);
206 | }
207 | return envValue;
208 | };
209 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/refunds/list-refunds.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { maybeFilter } from 'dodopayments-mcp/filtering';
4 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
5 |
6 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
7 | import DodoPayments from 'dodopayments';
8 |
9 | export const metadata: Metadata = {
10 | resource: 'refunds',
11 | operation: 'read',
12 | tags: [],
13 | httpMethod: 'get',
14 | httpPath: '/refunds',
15 | operationId: 'list_refunds',
16 | };
17 |
18 | export const tool: Tool = {
19 | name: 'list_refunds',
20 | description:
21 | "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\n\n\n# Response Schema\n```json\n{\n type: 'object',\n properties: {\n items: {\n type: 'array',\n items: {\n $ref: '#/$defs/refund_list_response'\n }\n }\n },\n required: [ 'items'\n ],\n $defs: {\n refund_list_response: {\n type: 'object',\n properties: {\n business_id: {\n type: 'string',\n description: 'The unique identifier of the business issuing the refund.'\n },\n created_at: {\n type: 'string',\n description: 'The timestamp of when the refund was created in UTC.',\n format: 'date-time'\n },\n is_partial: {\n type: 'boolean',\n description: 'If true the refund is a partial refund'\n },\n payment_id: {\n type: 'string',\n description: 'The unique identifier of the payment associated with the refund.'\n },\n refund_id: {\n type: 'string',\n description: 'The unique identifier of the refund.'\n },\n status: {\n $ref: '#/$defs/refund_status'\n },\n amount: {\n type: 'integer',\n description: 'The refunded amount.'\n },\n currency: {\n $ref: '#/$defs/currency'\n },\n reason: {\n type: 'string',\n description: 'The reason provided for the refund, if any. Optional.'\n }\n },\n required: [ 'business_id',\n 'created_at',\n 'is_partial',\n 'payment_id',\n 'refund_id',\n 'status'\n ]\n },\n refund_status: {\n type: 'string',\n enum: [ 'succeeded',\n 'failed',\n 'pending',\n 'review'\n ]\n },\n currency: {\n type: 'string',\n enum: [ 'AED',\n 'ALL',\n 'AMD',\n 'ANG',\n 'AOA',\n 'ARS',\n 'AUD',\n 'AWG',\n 'AZN',\n 'BAM',\n 'BBD',\n 'BDT',\n 'BGN',\n 'BHD',\n 'BIF',\n 'BMD',\n 'BND',\n 'BOB',\n 'BRL',\n 'BSD',\n 'BWP',\n 'BYN',\n 'BZD',\n 'CAD',\n 'CHF',\n 'CLP',\n 'CNY',\n 'COP',\n 'CRC',\n 'CUP',\n 'CVE',\n 'CZK',\n 'DJF',\n 'DKK',\n 'DOP',\n 'DZD',\n 'EGP',\n 'ETB',\n 'EUR',\n 'FJD',\n 'FKP',\n 'GBP',\n 'GEL',\n 'GHS',\n 'GIP',\n 'GMD',\n 'GNF',\n 'GTQ',\n 'GYD',\n 'HKD',\n 'HNL',\n 'HRK',\n 'HTG',\n 'HUF',\n 'IDR',\n 'ILS',\n 'INR',\n 'IQD',\n 'JMD',\n 'JOD',\n 'JPY',\n 'KES',\n 'KGS',\n 'KHR',\n 'KMF',\n 'KRW',\n 'KWD',\n 'KYD',\n 'KZT',\n 'LAK',\n 'LBP',\n 'LKR',\n 'LRD',\n 'LSL',\n 'LYD',\n 'MAD',\n 'MDL',\n 'MGA',\n 'MKD',\n 'MMK',\n 'MNT',\n 'MOP',\n 'MRU',\n 'MUR',\n 'MVR',\n 'MWK',\n 'MXN',\n 'MYR',\n 'MZN',\n 'NAD',\n 'NGN',\n 'NIO',\n 'NOK',\n 'NPR',\n 'NZD',\n 'OMR',\n 'PAB',\n 'PEN',\n 'PGK',\n 'PHP',\n 'PKR',\n 'PLN',\n 'PYG',\n 'QAR',\n 'RON',\n 'RSD',\n 'RUB',\n 'RWF',\n 'SAR',\n 'SBD',\n 'SCR',\n 'SEK',\n 'SGD',\n 'SHP',\n 'SLE',\n 'SLL',\n 'SOS',\n 'SRD',\n 'SSP',\n 'STN',\n 'SVC',\n 'SZL',\n 'THB',\n 'TND',\n 'TOP',\n 'TRY',\n 'TTD',\n 'TWD',\n 'TZS',\n 'UAH',\n 'UGX',\n 'USD',\n 'UYU',\n 'UZS',\n 'VES',\n 'VND',\n 'VUV',\n 'WST',\n 'XAF',\n 'XCD',\n 'XOF',\n 'XPF',\n 'YER',\n 'ZAR',\n 'ZMW'\n ]\n }\n }\n}\n```",
22 | inputSchema: {
23 | type: 'object',
24 | properties: {
25 | created_at_gte: {
26 | type: 'string',
27 | description: 'Get events after this created time',
28 | format: 'date-time',
29 | },
30 | created_at_lte: {
31 | type: 'string',
32 | description: 'Get events created before this time',
33 | format: 'date-time',
34 | },
35 | customer_id: {
36 | type: 'string',
37 | description: 'Filter by customer_id',
38 | },
39 | page_number: {
40 | type: 'integer',
41 | description: 'Page number default is 0',
42 | },
43 | page_size: {
44 | type: 'integer',
45 | description: 'Page size default is 10 max is 100',
46 | },
47 | status: {
48 | type: 'string',
49 | description: 'Filter by status',
50 | enum: ['succeeded', 'failed', 'pending', 'review'],
51 | },
52 | jq_filter: {
53 | type: 'string',
54 | title: 'jq Filter',
55 | description:
56 | 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).',
57 | },
58 | },
59 | required: [],
60 | },
61 | annotations: {
62 | readOnlyHint: true,
63 | },
64 | };
65 |
66 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
67 | const { jq_filter, ...body } = args as any;
68 | const response = await client.refunds.list(body).asResponse();
69 | return asTextContentResult(await maybeFilter(jq_filter, await response.json()));
70 | };
71 |
72 | export default { metadata, tool, handler };
73 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/tests/tools.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Endpoint, Filter, Metadata, query } from '../src/tools';
2 |
3 | describe('Endpoint filtering', () => {
4 | const endpoints: Endpoint[] = [
5 | endpoint({
6 | resource: 'user',
7 | operation: 'read',
8 | tags: ['admin'],
9 | toolName: 'retrieve_user',
10 | }),
11 | endpoint({
12 | resource: 'user.profile',
13 | operation: 'write',
14 | tags: [],
15 | toolName: 'create_user_profile',
16 | }),
17 | endpoint({
18 | resource: 'user.profile',
19 | operation: 'read',
20 | tags: [],
21 | toolName: 'get_user_profile',
22 | }),
23 | endpoint({
24 | resource: 'user.roles.permissions',
25 | operation: 'write',
26 | tags: ['admin', 'security'],
27 | toolName: 'update_user_role_permissions',
28 | }),
29 | endpoint({
30 | resource: 'documents.metadata.tags',
31 | operation: 'write',
32 | tags: ['taxonomy', 'metadata'],
33 | toolName: 'create_document_metadata_tags',
34 | }),
35 | endpoint({
36 | resource: 'organization.settings',
37 | operation: 'read',
38 | tags: ['admin', 'configuration'],
39 | toolName: 'get_organization_settings',
40 | }),
41 | ];
42 |
43 | const tests: { name: string; filters: Filter[]; expected: string[] }[] = [
44 | {
45 | name: 'match none',
46 | filters: [],
47 | expected: [],
48 | },
49 |
50 | // Resource tests
51 | {
52 | name: 'simple resource',
53 | filters: [{ type: 'resource', op: 'include', value: 'user' }],
54 | expected: ['retrieve_user'],
55 | },
56 | {
57 | name: 'exclude resource',
58 | filters: [{ type: 'resource', op: 'exclude', value: 'user' }],
59 | expected: [
60 | 'create_user_profile',
61 | 'get_user_profile',
62 | 'update_user_role_permissions',
63 | 'create_document_metadata_tags',
64 | 'get_organization_settings',
65 | ],
66 | },
67 | {
68 | name: 'resource and subresources',
69 | filters: [{ type: 'resource', op: 'include', value: 'user*' }],
70 | expected: ['retrieve_user', 'create_user_profile', 'get_user_profile', 'update_user_role_permissions'],
71 | },
72 | {
73 | name: 'just subresources',
74 | filters: [{ type: 'resource', op: 'include', value: 'user.*' }],
75 | expected: ['create_user_profile', 'get_user_profile', 'update_user_role_permissions'],
76 | },
77 | {
78 | name: 'specific subresource',
79 | filters: [{ type: 'resource', op: 'include', value: 'user.roles.permissions' }],
80 | expected: ['update_user_role_permissions'],
81 | },
82 | {
83 | name: 'deep wildcard match',
84 | filters: [{ type: 'resource', op: 'include', value: '*.*.tags' }],
85 | expected: ['create_document_metadata_tags'],
86 | },
87 |
88 | // Operation tests
89 | {
90 | name: 'read operation',
91 | filters: [{ type: 'operation', op: 'include', value: 'read' }],
92 | expected: ['retrieve_user', 'get_user_profile', 'get_organization_settings'],
93 | },
94 | {
95 | name: 'write operation',
96 | filters: [{ type: 'operation', op: 'include', value: 'write' }],
97 | expected: ['create_user_profile', 'update_user_role_permissions', 'create_document_metadata_tags'],
98 | },
99 | {
100 | name: 'resource and operation combined',
101 | filters: [
102 | { type: 'resource', op: 'include', value: 'user.profile' },
103 | { type: 'operation', op: 'exclude', value: 'write' },
104 | ],
105 | expected: ['get_user_profile'],
106 | },
107 |
108 | // Tag tests
109 | {
110 | name: 'admin tag',
111 | filters: [{ type: 'tag', op: 'include', value: 'admin' }],
112 | expected: ['retrieve_user', 'update_user_role_permissions', 'get_organization_settings'],
113 | },
114 | {
115 | name: 'taxonomy tag',
116 | filters: [{ type: 'tag', op: 'include', value: 'taxonomy' }],
117 | expected: ['create_document_metadata_tags'],
118 | },
119 | {
120 | name: 'multiple tags (OR logic)',
121 | filters: [
122 | { type: 'tag', op: 'include', value: 'admin' },
123 | { type: 'tag', op: 'include', value: 'security' },
124 | ],
125 | expected: ['retrieve_user', 'update_user_role_permissions', 'get_organization_settings'],
126 | },
127 | {
128 | name: 'excluding a tag',
129 | filters: [
130 | { type: 'tag', op: 'include', value: 'admin' },
131 | { type: 'tag', op: 'exclude', value: 'security' },
132 | ],
133 | expected: ['retrieve_user', 'get_organization_settings'],
134 | },
135 |
136 | // Tool name tests
137 | {
138 | name: 'tool name match',
139 | filters: [{ type: 'tool', op: 'include', value: 'get_organization_settings' }],
140 | expected: ['get_organization_settings'],
141 | },
142 | {
143 | name: 'two tools match',
144 | filters: [
145 | { type: 'tool', op: 'include', value: 'get_organization_settings' },
146 | { type: 'tool', op: 'include', value: 'create_user_profile' },
147 | ],
148 | expected: ['create_user_profile', 'get_organization_settings'],
149 | },
150 | {
151 | name: 'excluding tool by name',
152 | filters: [
153 | { type: 'resource', op: 'include', value: 'user*' },
154 | { type: 'tool', op: 'exclude', value: 'retrieve_user' },
155 | ],
156 | expected: ['create_user_profile', 'get_user_profile', 'update_user_role_permissions'],
157 | },
158 |
159 | // Complex combinations
160 | {
161 | name: 'complex filter: read operations with admin tag',
162 | filters: [
163 | { type: 'operation', op: 'include', value: 'read' },
164 | { type: 'tag', op: 'include', value: 'admin' },
165 | ],
166 | expected: [
167 | 'retrieve_user',
168 | 'get_user_profile',
169 | 'update_user_role_permissions',
170 | 'get_organization_settings',
171 | ],
172 | },
173 | {
174 | name: 'complex filter: user resources with no tags',
175 | filters: [
176 | { type: 'resource', op: 'include', value: 'user.profile' },
177 | { type: 'tag', op: 'exclude', value: 'admin' },
178 | ],
179 | expected: ['create_user_profile', 'get_user_profile'],
180 | },
181 | {
182 | name: 'complex filter: user resources and tags',
183 | filters: [
184 | { type: 'resource', op: 'include', value: 'user.profile' },
185 | { type: 'tag', op: 'include', value: 'admin' },
186 | ],
187 | expected: [
188 | 'retrieve_user',
189 | 'create_user_profile',
190 | 'get_user_profile',
191 | 'update_user_role_permissions',
192 | 'get_organization_settings',
193 | ],
194 | },
195 | ];
196 |
197 | tests.forEach((test) => {
198 | it(`filters by ${test.name}`, () => {
199 | const filtered = query(test.filters, endpoints);
200 | expect(filtered.map((e) => e.tool.name)).toEqual(test.expected);
201 | });
202 | });
203 | });
204 |
205 | function endpoint({
206 | resource,
207 | operation,
208 | tags,
209 | toolName,
210 | }: {
211 | resource: string;
212 | operation: Metadata['operation'];
213 | tags: string[];
214 | toolName: string;
215 | }): Endpoint {
216 | return {
217 | metadata: {
218 | resource,
219 | operation,
220 | tags,
221 | },
222 | tool: { name: toolName, inputSchema: { type: 'object', properties: {} } },
223 | handler: jest.fn(),
224 | };
225 | }
226 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/refunds/retrieve-refunds.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { maybeFilter } from 'dodopayments-mcp/filtering';
4 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
5 |
6 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
7 | import DodoPayments from 'dodopayments';
8 |
9 | export const metadata: Metadata = {
10 | resource: 'refunds',
11 | operation: 'read',
12 | tags: [],
13 | httpMethod: 'get',
14 | httpPath: '/refunds/{refund_id}',
15 | operationId: 'get_refund_handler',
16 | };
17 |
18 | export const tool: Tool = {
19 | name: 'retrieve_refunds',
20 | description:
21 | "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\n\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/refund',\n $defs: {\n refund: {\n type: 'object',\n properties: {\n business_id: {\n type: 'string',\n description: 'The unique identifier of the business issuing the refund.'\n },\n created_at: {\n type: 'string',\n description: 'The timestamp of when the refund was created in UTC.',\n format: 'date-time'\n },\n customer: {\n $ref: '#/$defs/customer_limited_details'\n },\n is_partial: {\n type: 'boolean',\n description: 'If true the refund is a partial refund'\n },\n metadata: {\n type: 'object',\n description: 'Additional metadata stored with the refund.',\n additionalProperties: true\n },\n payment_id: {\n type: 'string',\n description: 'The unique identifier of the payment associated with the refund.'\n },\n refund_id: {\n type: 'string',\n description: 'The unique identifier of the refund.'\n },\n status: {\n $ref: '#/$defs/refund_status'\n },\n amount: {\n type: 'integer',\n description: 'The refunded amount.'\n },\n currency: {\n $ref: '#/$defs/currency'\n },\n reason: {\n type: 'string',\n description: 'The reason provided for the refund, if any. Optional.'\n }\n },\n required: [ 'business_id',\n 'created_at',\n 'customer',\n 'is_partial',\n 'metadata',\n 'payment_id',\n 'refund_id',\n 'status'\n ]\n },\n customer_limited_details: {\n type: 'object',\n properties: {\n customer_id: {\n type: 'string',\n description: 'Unique identifier for the customer'\n },\n email: {\n type: 'string',\n description: 'Email address of the customer'\n },\n name: {\n type: 'string',\n description: 'Full name of the customer'\n },\n phone_number: {\n type: 'string',\n description: 'Phone number of the customer'\n }\n },\n required: [ 'customer_id',\n 'email',\n 'name'\n ]\n },\n refund_status: {\n type: 'string',\n enum: [ 'succeeded',\n 'failed',\n 'pending',\n 'review'\n ]\n },\n currency: {\n type: 'string',\n enum: [ 'AED',\n 'ALL',\n 'AMD',\n 'ANG',\n 'AOA',\n 'ARS',\n 'AUD',\n 'AWG',\n 'AZN',\n 'BAM',\n 'BBD',\n 'BDT',\n 'BGN',\n 'BHD',\n 'BIF',\n 'BMD',\n 'BND',\n 'BOB',\n 'BRL',\n 'BSD',\n 'BWP',\n 'BYN',\n 'BZD',\n 'CAD',\n 'CHF',\n 'CLP',\n 'CNY',\n 'COP',\n 'CRC',\n 'CUP',\n 'CVE',\n 'CZK',\n 'DJF',\n 'DKK',\n 'DOP',\n 'DZD',\n 'EGP',\n 'ETB',\n 'EUR',\n 'FJD',\n 'FKP',\n 'GBP',\n 'GEL',\n 'GHS',\n 'GIP',\n 'GMD',\n 'GNF',\n 'GTQ',\n 'GYD',\n 'HKD',\n 'HNL',\n 'HRK',\n 'HTG',\n 'HUF',\n 'IDR',\n 'ILS',\n 'INR',\n 'IQD',\n 'JMD',\n 'JOD',\n 'JPY',\n 'KES',\n 'KGS',\n 'KHR',\n 'KMF',\n 'KRW',\n 'KWD',\n 'KYD',\n 'KZT',\n 'LAK',\n 'LBP',\n 'LKR',\n 'LRD',\n 'LSL',\n 'LYD',\n 'MAD',\n 'MDL',\n 'MGA',\n 'MKD',\n 'MMK',\n 'MNT',\n 'MOP',\n 'MRU',\n 'MUR',\n 'MVR',\n 'MWK',\n 'MXN',\n 'MYR',\n 'MZN',\n 'NAD',\n 'NGN',\n 'NIO',\n 'NOK',\n 'NPR',\n 'NZD',\n 'OMR',\n 'PAB',\n 'PEN',\n 'PGK',\n 'PHP',\n 'PKR',\n 'PLN',\n 'PYG',\n 'QAR',\n 'RON',\n 'RSD',\n 'RUB',\n 'RWF',\n 'SAR',\n 'SBD',\n 'SCR',\n 'SEK',\n 'SGD',\n 'SHP',\n 'SLE',\n 'SLL',\n 'SOS',\n 'SRD',\n 'SSP',\n 'STN',\n 'SVC',\n 'SZL',\n 'THB',\n 'TND',\n 'TOP',\n 'TRY',\n 'TTD',\n 'TWD',\n 'TZS',\n 'UAH',\n 'UGX',\n 'USD',\n 'UYU',\n 'UZS',\n 'VES',\n 'VND',\n 'VUV',\n 'WST',\n 'XAF',\n 'XCD',\n 'XOF',\n 'XPF',\n 'YER',\n 'ZAR',\n 'ZMW'\n ]\n }\n }\n}\n```",
22 | inputSchema: {
23 | type: 'object',
24 | properties: {
25 | refund_id: {
26 | type: 'string',
27 | },
28 | jq_filter: {
29 | type: 'string',
30 | title: 'jq Filter',
31 | description:
32 | 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).',
33 | },
34 | },
35 | required: ['refund_id'],
36 | },
37 | annotations: {
38 | readOnlyHint: true,
39 | },
40 | };
41 |
42 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
43 | const { refund_id, jq_filter, ...body } = args as any;
44 | return asTextContentResult(await maybeFilter(jq_filter, await client.refunds.retrieve(refund_id)));
45 | };
46 |
47 | export default { metadata, tool, handler };
48 |
```
--------------------------------------------------------------------------------
/src/internal/uploads.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { type RequestOptions } from './request-options';
2 | import type { FilePropertyBag, Fetch } from './builtin-types';
3 | import type { DodoPayments } from '../client';
4 | import { ReadableStreamFrom } from './shims';
5 |
6 | export type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | DataView;
7 | type FsReadStream = AsyncIterable<Uint8Array> & { path: string | { toString(): string } };
8 |
9 | // https://github.com/oven-sh/bun/issues/5980
10 | interface BunFile extends Blob {
11 | readonly name?: string | undefined;
12 | }
13 |
14 | export const checkFileSupport = () => {
15 | if (typeof File === 'undefined') {
16 | const { process } = globalThis as any;
17 | const isOldNode =
18 | typeof process?.versions?.node === 'string' && parseInt(process.versions.node.split('.')) < 20;
19 | throw new Error(
20 | '`File` is not defined as a global, which is required for file uploads.' +
21 | (isOldNode ?
22 | " Update to Node 20 LTS or newer, or set `globalThis.File` to `import('node:buffer').File`."
23 | : ''),
24 | );
25 | }
26 | };
27 |
28 | /**
29 | * Typically, this is a native "File" class.
30 | *
31 | * We provide the {@link toFile} utility to convert a variety of objects
32 | * into the File class.
33 | *
34 | * For convenience, you can also pass a fetch Response, or in Node,
35 | * the result of fs.createReadStream().
36 | */
37 | export type Uploadable = File | Response | FsReadStream | BunFile;
38 |
39 | /**
40 | * Construct a `File` instance. This is used to ensure a helpful error is thrown
41 | * for environments that don't define a global `File` yet.
42 | */
43 | export function makeFile(
44 | fileBits: BlobPart[],
45 | fileName: string | undefined,
46 | options?: FilePropertyBag,
47 | ): File {
48 | checkFileSupport();
49 | return new File(fileBits as any, fileName ?? 'unknown_file', options);
50 | }
51 |
52 | export function getName(value: any): string | undefined {
53 | return (
54 | (
55 | (typeof value === 'object' &&
56 | value !== null &&
57 | (('name' in value && value.name && String(value.name)) ||
58 | ('url' in value && value.url && String(value.url)) ||
59 | ('filename' in value && value.filename && String(value.filename)) ||
60 | ('path' in value && value.path && String(value.path)))) ||
61 | ''
62 | )
63 | .split(/[\\/]/)
64 | .pop() || undefined
65 | );
66 | }
67 |
68 | export const isAsyncIterable = (value: any): value is AsyncIterable<any> =>
69 | value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function';
70 |
71 | /**
72 | * Returns a multipart/form-data request if any part of the given request body contains a File / Blob value.
73 | * Otherwise returns the request as is.
74 | */
75 | export const maybeMultipartFormRequestOptions = async (
76 | opts: RequestOptions,
77 | fetch: DodoPayments | Fetch,
78 | ): Promise<RequestOptions> => {
79 | if (!hasUploadableValue(opts.body)) return opts;
80 |
81 | return { ...opts, body: await createForm(opts.body, fetch) };
82 | };
83 |
84 | type MultipartFormRequestOptions = Omit<RequestOptions, 'body'> & { body: unknown };
85 |
86 | export const multipartFormRequestOptions = async (
87 | opts: MultipartFormRequestOptions,
88 | fetch: DodoPayments | Fetch,
89 | ): Promise<RequestOptions> => {
90 | return { ...opts, body: await createForm(opts.body, fetch) };
91 | };
92 |
93 | const supportsFormDataMap = /* @__PURE__ */ new WeakMap<Fetch, Promise<boolean>>();
94 |
95 | /**
96 | * node-fetch doesn't support the global FormData object in recent node versions. Instead of sending
97 | * properly-encoded form data, it just stringifies the object, resulting in a request body of "[object FormData]".
98 | * This function detects if the fetch function provided supports the global FormData object to avoid
99 | * confusing error messages later on.
100 | */
101 | function supportsFormData(fetchObject: DodoPayments | Fetch): Promise<boolean> {
102 | const fetch: Fetch = typeof fetchObject === 'function' ? fetchObject : (fetchObject as any).fetch;
103 | const cached = supportsFormDataMap.get(fetch);
104 | if (cached) return cached;
105 | const promise = (async () => {
106 | try {
107 | const FetchResponse = (
108 | 'Response' in fetch ?
109 | fetch.Response
110 | : (await fetch('data:,')).constructor) as typeof Response;
111 | const data = new FormData();
112 | if (data.toString() === (await new FetchResponse(data).text())) {
113 | return false;
114 | }
115 | return true;
116 | } catch {
117 | // avoid false negatives
118 | return true;
119 | }
120 | })();
121 | supportsFormDataMap.set(fetch, promise);
122 | return promise;
123 | }
124 |
125 | export const createForm = async <T = Record<string, unknown>>(
126 | body: T | undefined,
127 | fetch: DodoPayments | Fetch,
128 | ): Promise<FormData> => {
129 | if (!(await supportsFormData(fetch))) {
130 | throw new TypeError(
131 | 'The provided fetch function does not support file uploads with the current global FormData class.',
132 | );
133 | }
134 | const form = new FormData();
135 | await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value)));
136 | return form;
137 | };
138 |
139 | // We check for Blob not File because Bun.File doesn't inherit from File,
140 | // but they both inherit from Blob and have a `name` property at runtime.
141 | const isNamedBlob = (value: unknown) => value instanceof Blob && 'name' in value;
142 |
143 | const isUploadable = (value: unknown) =>
144 | typeof value === 'object' &&
145 | value !== null &&
146 | (value instanceof Response || isAsyncIterable(value) || isNamedBlob(value));
147 |
148 | const hasUploadableValue = (value: unknown): boolean => {
149 | if (isUploadable(value)) return true;
150 | if (Array.isArray(value)) return value.some(hasUploadableValue);
151 | if (value && typeof value === 'object') {
152 | for (const k in value) {
153 | if (hasUploadableValue((value as any)[k])) return true;
154 | }
155 | }
156 | return false;
157 | };
158 |
159 | const addFormValue = async (form: FormData, key: string, value: unknown): Promise<void> => {
160 | if (value === undefined) return;
161 | if (value == null) {
162 | throw new TypeError(
163 | `Received null for "${key}"; to pass null in FormData, you must use the string 'null'`,
164 | );
165 | }
166 |
167 | // TODO: make nested formats configurable
168 | if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
169 | form.append(key, String(value));
170 | } else if (value instanceof Response) {
171 | form.append(key, makeFile([await value.blob()], getName(value)));
172 | } else if (isAsyncIterable(value)) {
173 | form.append(key, makeFile([await new Response(ReadableStreamFrom(value)).blob()], getName(value)));
174 | } else if (isNamedBlob(value)) {
175 | form.append(key, value, getName(value));
176 | } else if (Array.isArray(value)) {
177 | await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry)));
178 | } else if (typeof value === 'object') {
179 | await Promise.all(
180 | Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop)),
181 | );
182 | } else {
183 | throw new TypeError(
184 | `Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`,
185 | );
186 | }
187 | };
188 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/tests/dynamic-tools.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { dynamicTools } from '../src/dynamic-tools';
2 | import { Endpoint } from '../src/tools';
3 |
4 | describe('dynamicTools', () => {
5 | const fakeClient = {} as any;
6 |
7 | const endpoints: Endpoint[] = [
8 | makeEndpoint('test_read_endpoint', 'test_resource', 'read', ['test']),
9 | makeEndpoint('test_write_endpoint', 'test_resource', 'write', ['test']),
10 | makeEndpoint('user_endpoint', 'user', 'read', ['user', 'admin']),
11 | makeEndpoint('admin_endpoint', 'admin', 'write', ['admin']),
12 | ];
13 |
14 | const tools = dynamicTools(endpoints);
15 |
16 | const toolsMap = {
17 | list_api_endpoints: toolOrError('list_api_endpoints'),
18 | get_api_endpoint_schema: toolOrError('get_api_endpoint_schema'),
19 | invoke_api_endpoint: toolOrError('invoke_api_endpoint'),
20 | };
21 |
22 | describe('list_api_endpoints', () => {
23 | it('should return all endpoints when no search query is provided', async () => {
24 | const content = await toolsMap.list_api_endpoints.handler(fakeClient, {});
25 | const result = JSON.parse(content.content[0].text);
26 |
27 | expect(result.tools).toHaveLength(endpoints.length);
28 | expect(result.tools.map((t: { name: string }) => t.name)).toContain('test_read_endpoint');
29 | expect(result.tools.map((t: { name: string }) => t.name)).toContain('test_write_endpoint');
30 | expect(result.tools.map((t: { name: string }) => t.name)).toContain('user_endpoint');
31 | expect(result.tools.map((t: { name: string }) => t.name)).toContain('admin_endpoint');
32 | });
33 |
34 | it('should filter endpoints by name', async () => {
35 | const content = await toolsMap.list_api_endpoints.handler(fakeClient, { search_query: 'user' });
36 | const result = JSON.parse(content.content[0].text);
37 |
38 | expect(result.tools).toHaveLength(1);
39 | expect(result.tools[0].name).toBe('user_endpoint');
40 | });
41 |
42 | it('should filter endpoints by resource', async () => {
43 | const content = await toolsMap.list_api_endpoints.handler(fakeClient, { search_query: 'admin' });
44 | const result = JSON.parse(content.content[0].text);
45 |
46 | expect(result.tools.some((t: { resource: string }) => t.resource === 'admin')).toBeTruthy();
47 | });
48 |
49 | it('should filter endpoints by tag', async () => {
50 | const content = await toolsMap.list_api_endpoints.handler(fakeClient, { search_query: 'admin' });
51 | const result = JSON.parse(content.content[0].text);
52 |
53 | expect(result.tools.some((t: { tags: string[] }) => t.tags.includes('admin'))).toBeTruthy();
54 | });
55 |
56 | it('should be case insensitive in search', async () => {
57 | const content = await toolsMap.list_api_endpoints.handler(fakeClient, { search_query: 'ADMIN' });
58 | const result = JSON.parse(content.content[0].text);
59 |
60 | expect(result.tools.length).toBe(2);
61 | result.tools.forEach((tool: { name: string; resource: string; tags: string[] }) => {
62 | expect(
63 | tool.name.toLowerCase().includes('admin') ||
64 | tool.resource.toLowerCase().includes('admin') ||
65 | tool.tags.some((tag: string) => tag.toLowerCase().includes('admin')),
66 | ).toBeTruthy();
67 | });
68 | });
69 |
70 | it('should filter endpoints by description', async () => {
71 | const content = await toolsMap.list_api_endpoints.handler(fakeClient, {
72 | search_query: 'Test endpoint for user_endpoint',
73 | });
74 | const result = JSON.parse(content.content[0].text);
75 |
76 | expect(result.tools).toHaveLength(1);
77 | expect(result.tools[0].name).toBe('user_endpoint');
78 | expect(result.tools[0].description).toBe('Test endpoint for user_endpoint');
79 | });
80 |
81 | it('should filter endpoints by partial description match', async () => {
82 | const content = await toolsMap.list_api_endpoints.handler(fakeClient, {
83 | search_query: 'endpoint for user',
84 | });
85 | const result = JSON.parse(content.content[0].text);
86 |
87 | expect(result.tools).toHaveLength(1);
88 | expect(result.tools[0].name).toBe('user_endpoint');
89 | });
90 | });
91 |
92 | describe('get_api_endpoint_schema', () => {
93 | it('should return schema for existing endpoint', async () => {
94 | const content = await toolsMap.get_api_endpoint_schema.handler(fakeClient, {
95 | endpoint: 'test_read_endpoint',
96 | });
97 | const result = JSON.parse(content.content[0].text);
98 |
99 | expect(result).toEqual(endpoints[0]?.tool);
100 | });
101 |
102 | it('should throw error for non-existent endpoint', async () => {
103 | await expect(
104 | toolsMap.get_api_endpoint_schema.handler(fakeClient, { endpoint: 'non_existent_endpoint' }),
105 | ).rejects.toThrow('Endpoint non_existent_endpoint not found');
106 | });
107 |
108 | it('should throw error when no endpoint provided', async () => {
109 | await expect(toolsMap.get_api_endpoint_schema.handler(fakeClient, undefined)).rejects.toThrow(
110 | 'No endpoint provided',
111 | );
112 | });
113 | });
114 |
115 | describe('invoke_api_endpoint', () => {
116 | it('should successfully invoke endpoint with valid arguments', async () => {
117 | const mockHandler = endpoints[0]?.handler as jest.Mock;
118 | mockHandler.mockClear();
119 |
120 | await toolsMap.invoke_api_endpoint.handler(fakeClient, {
121 | endpoint_name: 'test_read_endpoint',
122 | args: { testParam: 'test value' },
123 | });
124 |
125 | expect(mockHandler).toHaveBeenCalledWith(fakeClient, { testParam: 'test value' });
126 | });
127 |
128 | it('should throw error for non-existent endpoint', async () => {
129 | await expect(
130 | toolsMap.invoke_api_endpoint.handler(fakeClient, {
131 | endpoint_name: 'non_existent_endpoint',
132 | args: { testParam: 'test value' },
133 | }),
134 | ).rejects.toThrow(/Endpoint non_existent_endpoint not found/);
135 | });
136 |
137 | it('should throw error when no arguments provided', async () => {
138 | await expect(toolsMap.invoke_api_endpoint.handler(fakeClient, undefined)).rejects.toThrow(
139 | 'No endpoint provided',
140 | );
141 | });
142 |
143 | it('should throw error for invalid argument schema', async () => {
144 | await expect(
145 | toolsMap.invoke_api_endpoint.handler(fakeClient, {
146 | endpoint_name: 'test_read_endpoint',
147 | args: { wrongParam: 'test value' }, // Missing required testParam
148 | }),
149 | ).rejects.toThrow(/Invalid arguments for endpoint/);
150 | });
151 | });
152 |
153 | function toolOrError(name: string) {
154 | const tool = tools.find((tool) => tool.tool.name === name);
155 | if (!tool) throw new Error(`Tool ${name} not found`);
156 | return tool;
157 | }
158 | });
159 |
160 | function makeEndpoint(
161 | name: string,
162 | resource: string,
163 | operation: 'read' | 'write',
164 | tags: string[] = [],
165 | ): Endpoint {
166 | return {
167 | metadata: {
168 | resource,
169 | operation,
170 | tags,
171 | },
172 | tool: {
173 | name,
174 | description: `Test endpoint for ${name}`,
175 | inputSchema: {
176 | type: 'object',
177 | properties: {
178 | testParam: { type: 'string' },
179 | },
180 | required: ['testParam'],
181 | },
182 | },
183 | handler: jest.fn().mockResolvedValue({ success: true }),
184 | };
185 | }
186 |
```
--------------------------------------------------------------------------------
/src/resources/usage-events.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { APIResource } from '../core/resource';
4 | import { APIPromise } from '../core/api-promise';
5 | import {
6 | DefaultPageNumberPagination,
7 | type DefaultPageNumberPaginationParams,
8 | PagePromise,
9 | } from '../core/pagination';
10 | import { RequestOptions } from '../internal/request-options';
11 | import { path } from '../internal/utils/path';
12 |
13 | export class UsageEvents extends APIResource {
14 | /**
15 | * Fetch detailed information about a single event using its unique event ID. This
16 | * endpoint is useful for:
17 | *
18 | * - Debugging specific event ingestion issues
19 | * - Retrieving event details for customer support
20 | * - Validating that events were processed correctly
21 | * - Getting the complete metadata for an event
22 | *
23 | * ## Event ID Format:
24 | *
25 | * The event ID should be the same value that was provided during event ingestion
26 | * via the `/events/ingest` endpoint. Event IDs are case-sensitive and must match
27 | * exactly.
28 | *
29 | * ## Response Details:
30 | *
31 | * The response includes all event data including:
32 | *
33 | * - Complete metadata key-value pairs
34 | * - Original timestamp (preserved from ingestion)
35 | * - Customer and business association
36 | * - Event name and processing information
37 | *
38 | * ## Example Usage:
39 | *
40 | * ```text
41 | * GET /events/api_call_12345
42 | * ```
43 | */
44 | retrieve(eventID: string, options?: RequestOptions): APIPromise<Event> {
45 | return this._client.get(path`/events/${eventID}`, options);
46 | }
47 |
48 | /**
49 | * Fetch events from your account with powerful filtering capabilities. This
50 | * endpoint is ideal for:
51 | *
52 | * - Debugging event ingestion issues
53 | * - Analyzing customer usage patterns
54 | * - Building custom analytics dashboards
55 | * - Auditing billing-related events
56 | *
57 | * ## Filtering Options:
58 | *
59 | * - **Customer filtering**: Filter by specific customer ID
60 | * - **Event name filtering**: Filter by event type/name
61 | * - **Meter-based filtering**: Use a meter ID to apply the meter's event name and
62 | * filter criteria automatically
63 | * - **Time range filtering**: Filter events within a specific date range
64 | * - **Pagination**: Navigate through large result sets
65 | *
66 | * ## Meter Integration:
67 | *
68 | * When using `meter_id`, the endpoint automatically applies:
69 | *
70 | * - The meter's configured `event_name` filter
71 | * - The meter's custom filter criteria (if any)
72 | * - If you also provide `event_name`, it must match the meter's event name
73 | *
74 | * ## Example Queries:
75 | *
76 | * - Get all events for a customer: `?customer_id=cus_abc123`
77 | * - Get API request events: `?event_name=api_request`
78 | * - Get events from last 24 hours:
79 | * `?start=2024-01-14T10:30:00Z&end=2024-01-15T10:30:00Z`
80 | * - Get events with meter filtering: `?meter_id=mtr_xyz789`
81 | * - Paginate results: `?page_size=50&page_number=2`
82 | */
83 | list(
84 | query: UsageEventListParams | null | undefined = {},
85 | options?: RequestOptions,
86 | ): PagePromise<EventsDefaultPageNumberPagination, Event> {
87 | return this._client.getAPIList('/events', DefaultPageNumberPagination<Event>, { query, ...options });
88 | }
89 |
90 | /**
91 | * This endpoint allows you to ingest custom events that can be used for:
92 | *
93 | * - Usage-based billing and metering
94 | * - Analytics and reporting
95 | * - Customer behavior tracking
96 | *
97 | * ## Important Notes:
98 | *
99 | * - **Duplicate Prevention**:
100 | * - Duplicate `event_id` values within the same request are rejected (entire
101 | * request fails)
102 | * - Subsequent requests with existing `event_id` values are ignored (idempotent
103 | * behavior)
104 | * - **Rate Limiting**: Maximum 1000 events per request
105 | * - **Time Validation**: Events with timestamps older than 1 hour or more than 5
106 | * minutes in the future will be rejected
107 | * - **Metadata Limits**: Maximum 50 key-value pairs per event, keys max 100 chars,
108 | * values max 500 chars
109 | *
110 | * ## Example Usage:
111 | *
112 | * ```json
113 | * {
114 | * "events": [
115 | * {
116 | * "event_id": "api_call_12345",
117 | * "customer_id": "cus_abc123",
118 | * "event_name": "api_request",
119 | * "timestamp": "2024-01-15T10:30:00Z",
120 | * "metadata": {
121 | * "endpoint": "/api/v1/users",
122 | * "method": "GET",
123 | * "tokens_used": "150"
124 | * }
125 | * }
126 | * ]
127 | * }
128 | * ```
129 | */
130 | ingest(body: UsageEventIngestParams, options?: RequestOptions): APIPromise<UsageEventIngestResponse> {
131 | return this._client.post('/events/ingest', { body, ...options });
132 | }
133 | }
134 |
135 | export type EventsDefaultPageNumberPagination = DefaultPageNumberPagination<Event>;
136 |
137 | export interface Event {
138 | business_id: string;
139 |
140 | customer_id: string;
141 |
142 | event_id: string;
143 |
144 | event_name: string;
145 |
146 | timestamp: string;
147 |
148 | /**
149 | * Arbitrary key-value metadata. Values can be string, integer, number, or boolean.
150 | */
151 | metadata?: { [key: string]: string | number | boolean } | null;
152 | }
153 |
154 | export interface EventInput {
155 | /**
156 | * customer_id of the customer whose usage needs to be tracked
157 | */
158 | customer_id: string;
159 |
160 | /**
161 | * Event Id acts as an idempotency key. Any subsequent requests with the same
162 | * event_id will be ignored
163 | */
164 | event_id: string;
165 |
166 | /**
167 | * Name of the event
168 | */
169 | event_name: string;
170 |
171 | /**
172 | * Custom metadata. Only key value pairs are accepted, objects or arrays submitted
173 | * will be rejected.
174 | */
175 | metadata?: { [key: string]: string | number | boolean } | null;
176 |
177 | /**
178 | * Custom Timestamp. Defaults to current timestamp in UTC. Timestamps that are
179 | * older that 1 hour or after 5 mins, from current timestamp, will be rejected.
180 | */
181 | timestamp?: string | null;
182 | }
183 |
184 | export interface UsageEventIngestResponse {
185 | ingested_count: number;
186 | }
187 |
188 | export interface UsageEventListParams extends DefaultPageNumberPaginationParams {
189 | /**
190 | * Filter events by customer ID
191 | */
192 | customer_id?: string;
193 |
194 | /**
195 | * Filter events created before this timestamp
196 | */
197 | end?: string;
198 |
199 | /**
200 | * Filter events by event name. If both event_name and meter_id are provided, they
201 | * must match the meter's configured event_name
202 | */
203 | event_name?: string;
204 |
205 | /**
206 | * Filter events by meter ID. When provided, only events that match the meter's
207 | * event_name and filter criteria will be returned
208 | */
209 | meter_id?: string;
210 |
211 | /**
212 | * Filter events created after this timestamp
213 | */
214 | start?: string;
215 | }
216 |
217 | export interface UsageEventIngestParams {
218 | /**
219 | * List of events to be pushed
220 | */
221 | events: Array<EventInput>;
222 | }
223 |
224 | export declare namespace UsageEvents {
225 | export {
226 | type Event as Event,
227 | type EventInput as EventInput,
228 | type UsageEventIngestResponse as UsageEventIngestResponse,
229 | type EventsDefaultPageNumberPagination as EventsDefaultPageNumberPagination,
230 | type UsageEventListParams as UsageEventListParams,
231 | type UsageEventIngestParams as UsageEventIngestParams,
232 | };
233 | }
234 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/subscriptions/update-subscriptions.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
4 |
5 | import { Tool } from '@modelcontextprotocol/sdk/types.js';
6 | import DodoPayments from 'dodopayments';
7 |
8 | export const metadata: Metadata = {
9 | resource: 'subscriptions',
10 | operation: 'write',
11 | tags: [],
12 | httpMethod: 'patch',
13 | httpPath: '/subscriptions/{subscription_id}',
14 | operationId: 'patch_subscription_handler',
15 | };
16 |
17 | export const tool: Tool = {
18 | name: 'update_subscriptions',
19 | description: '',
20 | inputSchema: {
21 | type: 'object',
22 | properties: {
23 | subscription_id: {
24 | type: 'string',
25 | },
26 | billing: {
27 | $ref: '#/$defs/billing_address',
28 | },
29 | cancel_at_next_billing_date: {
30 | type: 'boolean',
31 | description: 'When set, the subscription will remain active until the end of billing period',
32 | },
33 | customer_name: {
34 | type: 'string',
35 | },
36 | disable_on_demand: {
37 | type: 'object',
38 | title: 'Disable On Demand Request',
39 | properties: {
40 | next_billing_date: {
41 | type: 'string',
42 | format: 'date-time',
43 | },
44 | },
45 | required: ['next_billing_date'],
46 | },
47 | metadata: {
48 | type: 'object',
49 | additionalProperties: true,
50 | },
51 | next_billing_date: {
52 | type: 'string',
53 | format: 'date-time',
54 | },
55 | status: {
56 | $ref: '#/$defs/subscription_status',
57 | },
58 | tax_id: {
59 | type: 'string',
60 | },
61 | },
62 | required: ['subscription_id'],
63 | $defs: {
64 | billing_address: {
65 | type: 'object',
66 | properties: {
67 | city: {
68 | type: 'string',
69 | description: 'City name',
70 | },
71 | country: {
72 | $ref: '#/$defs/country_code',
73 | },
74 | state: {
75 | type: 'string',
76 | description: 'State or province name',
77 | },
78 | street: {
79 | type: 'string',
80 | description: 'Street address including house number and unit/apartment if applicable',
81 | },
82 | zipcode: {
83 | type: 'string',
84 | description: 'Postal code or ZIP code',
85 | },
86 | },
87 | required: ['city', 'country', 'state', 'street', 'zipcode'],
88 | },
89 | country_code: {
90 | type: 'string',
91 | description: 'ISO country code alpha2 variant',
92 | enum: [
93 | 'AF',
94 | 'AX',
95 | 'AL',
96 | 'DZ',
97 | 'AS',
98 | 'AD',
99 | 'AO',
100 | 'AI',
101 | 'AQ',
102 | 'AG',
103 | 'AR',
104 | 'AM',
105 | 'AW',
106 | 'AU',
107 | 'AT',
108 | 'AZ',
109 | 'BS',
110 | 'BH',
111 | 'BD',
112 | 'BB',
113 | 'BY',
114 | 'BE',
115 | 'BZ',
116 | 'BJ',
117 | 'BM',
118 | 'BT',
119 | 'BO',
120 | 'BQ',
121 | 'BA',
122 | 'BW',
123 | 'BV',
124 | 'BR',
125 | 'IO',
126 | 'BN',
127 | 'BG',
128 | 'BF',
129 | 'BI',
130 | 'KH',
131 | 'CM',
132 | 'CA',
133 | 'CV',
134 | 'KY',
135 | 'CF',
136 | 'TD',
137 | 'CL',
138 | 'CN',
139 | 'CX',
140 | 'CC',
141 | 'CO',
142 | 'KM',
143 | 'CG',
144 | 'CD',
145 | 'CK',
146 | 'CR',
147 | 'CI',
148 | 'HR',
149 | 'CU',
150 | 'CW',
151 | 'CY',
152 | 'CZ',
153 | 'DK',
154 | 'DJ',
155 | 'DM',
156 | 'DO',
157 | 'EC',
158 | 'EG',
159 | 'SV',
160 | 'GQ',
161 | 'ER',
162 | 'EE',
163 | 'ET',
164 | 'FK',
165 | 'FO',
166 | 'FJ',
167 | 'FI',
168 | 'FR',
169 | 'GF',
170 | 'PF',
171 | 'TF',
172 | 'GA',
173 | 'GM',
174 | 'GE',
175 | 'DE',
176 | 'GH',
177 | 'GI',
178 | 'GR',
179 | 'GL',
180 | 'GD',
181 | 'GP',
182 | 'GU',
183 | 'GT',
184 | 'GG',
185 | 'GN',
186 | 'GW',
187 | 'GY',
188 | 'HT',
189 | 'HM',
190 | 'VA',
191 | 'HN',
192 | 'HK',
193 | 'HU',
194 | 'IS',
195 | 'IN',
196 | 'ID',
197 | 'IR',
198 | 'IQ',
199 | 'IE',
200 | 'IM',
201 | 'IL',
202 | 'IT',
203 | 'JM',
204 | 'JP',
205 | 'JE',
206 | 'JO',
207 | 'KZ',
208 | 'KE',
209 | 'KI',
210 | 'KP',
211 | 'KR',
212 | 'KW',
213 | 'KG',
214 | 'LA',
215 | 'LV',
216 | 'LB',
217 | 'LS',
218 | 'LR',
219 | 'LY',
220 | 'LI',
221 | 'LT',
222 | 'LU',
223 | 'MO',
224 | 'MK',
225 | 'MG',
226 | 'MW',
227 | 'MY',
228 | 'MV',
229 | 'ML',
230 | 'MT',
231 | 'MH',
232 | 'MQ',
233 | 'MR',
234 | 'MU',
235 | 'YT',
236 | 'MX',
237 | 'FM',
238 | 'MD',
239 | 'MC',
240 | 'MN',
241 | 'ME',
242 | 'MS',
243 | 'MA',
244 | 'MZ',
245 | 'MM',
246 | 'NA',
247 | 'NR',
248 | 'NP',
249 | 'NL',
250 | 'NC',
251 | 'NZ',
252 | 'NI',
253 | 'NE',
254 | 'NG',
255 | 'NU',
256 | 'NF',
257 | 'MP',
258 | 'NO',
259 | 'OM',
260 | 'PK',
261 | 'PW',
262 | 'PS',
263 | 'PA',
264 | 'PG',
265 | 'PY',
266 | 'PE',
267 | 'PH',
268 | 'PN',
269 | 'PL',
270 | 'PT',
271 | 'PR',
272 | 'QA',
273 | 'RE',
274 | 'RO',
275 | 'RU',
276 | 'RW',
277 | 'BL',
278 | 'SH',
279 | 'KN',
280 | 'LC',
281 | 'MF',
282 | 'PM',
283 | 'VC',
284 | 'WS',
285 | 'SM',
286 | 'ST',
287 | 'SA',
288 | 'SN',
289 | 'RS',
290 | 'SC',
291 | 'SL',
292 | 'SG',
293 | 'SX',
294 | 'SK',
295 | 'SI',
296 | 'SB',
297 | 'SO',
298 | 'ZA',
299 | 'GS',
300 | 'SS',
301 | 'ES',
302 | 'LK',
303 | 'SD',
304 | 'SR',
305 | 'SJ',
306 | 'SZ',
307 | 'SE',
308 | 'CH',
309 | 'SY',
310 | 'TW',
311 | 'TJ',
312 | 'TZ',
313 | 'TH',
314 | 'TL',
315 | 'TG',
316 | 'TK',
317 | 'TO',
318 | 'TT',
319 | 'TN',
320 | 'TR',
321 | 'TM',
322 | 'TC',
323 | 'TV',
324 | 'UG',
325 | 'UA',
326 | 'AE',
327 | 'GB',
328 | 'UM',
329 | 'US',
330 | 'UY',
331 | 'UZ',
332 | 'VU',
333 | 'VE',
334 | 'VN',
335 | 'VG',
336 | 'VI',
337 | 'WF',
338 | 'EH',
339 | 'YE',
340 | 'ZM',
341 | 'ZW',
342 | ],
343 | },
344 | subscription_status: {
345 | type: 'string',
346 | enum: ['pending', 'active', 'on_hold', 'cancelled', 'failed', 'expired'],
347 | },
348 | },
349 | },
350 | annotations: {},
351 | };
352 |
353 | export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
354 | const { subscription_id, ...body } = args as any;
355 | return asTextContentResult(await client.subscriptions.update(subscription_id, body));
356 | };
357 |
358 | export default { metadata, tool, handler };
359 |
```
--------------------------------------------------------------------------------
/tests/api-resources/subscriptions.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2 |
3 | import DodoPayments from 'dodopayments';
4 |
5 | const client = new DodoPayments({
6 | bearerToken: 'My Bearer Token',
7 | baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
8 | });
9 |
10 | describe('resource subscriptions', () => {
11 | test('create: only required params', async () => {
12 | const responsePromise = client.subscriptions.create({
13 | billing: { city: 'city', country: 'AF', state: 'state', street: 'street', zipcode: 'zipcode' },
14 | customer: { customer_id: 'customer_id' },
15 | product_id: 'product_id',
16 | quantity: 0,
17 | });
18 | const rawResponse = await responsePromise.asResponse();
19 | expect(rawResponse).toBeInstanceOf(Response);
20 | const response = await responsePromise;
21 | expect(response).not.toBeInstanceOf(Response);
22 | const dataAndResponse = await responsePromise.withResponse();
23 | expect(dataAndResponse.data).toBe(response);
24 | expect(dataAndResponse.response).toBe(rawResponse);
25 | });
26 |
27 | test('create: required and optional params', async () => {
28 | const response = await client.subscriptions.create({
29 | billing: { city: 'city', country: 'AF', state: 'state', street: 'street', zipcode: 'zipcode' },
30 | customer: { customer_id: 'customer_id' },
31 | product_id: 'product_id',
32 | quantity: 0,
33 | addons: [{ addon_id: 'addon_id', quantity: 0 }],
34 | allowed_payment_method_types: ['credit'],
35 | billing_currency: 'AED',
36 | discount_code: 'discount_code',
37 | force_3ds: true,
38 | metadata: { foo: 'string' },
39 | on_demand: {
40 | mandate_only: true,
41 | adaptive_currency_fees_inclusive: true,
42 | product_currency: 'AED',
43 | product_description: 'product_description',
44 | product_price: 0,
45 | },
46 | payment_link: true,
47 | return_url: 'return_url',
48 | show_saved_payment_methods: true,
49 | tax_id: 'tax_id',
50 | trial_period_days: 0,
51 | });
52 | });
53 |
54 | test('retrieve', async () => {
55 | const responsePromise = client.subscriptions.retrieve('subscription_id');
56 | const rawResponse = await responsePromise.asResponse();
57 | expect(rawResponse).toBeInstanceOf(Response);
58 | const response = await responsePromise;
59 | expect(response).not.toBeInstanceOf(Response);
60 | const dataAndResponse = await responsePromise.withResponse();
61 | expect(dataAndResponse.data).toBe(response);
62 | expect(dataAndResponse.response).toBe(rawResponse);
63 | });
64 |
65 | test('update', async () => {
66 | const responsePromise = client.subscriptions.update('subscription_id', {});
67 | const rawResponse = await responsePromise.asResponse();
68 | expect(rawResponse).toBeInstanceOf(Response);
69 | const response = await responsePromise;
70 | expect(response).not.toBeInstanceOf(Response);
71 | const dataAndResponse = await responsePromise.withResponse();
72 | expect(dataAndResponse.data).toBe(response);
73 | expect(dataAndResponse.response).toBe(rawResponse);
74 | });
75 |
76 | test('list', async () => {
77 | const responsePromise = client.subscriptions.list();
78 | const rawResponse = await responsePromise.asResponse();
79 | expect(rawResponse).toBeInstanceOf(Response);
80 | const response = await responsePromise;
81 | expect(response).not.toBeInstanceOf(Response);
82 | const dataAndResponse = await responsePromise.withResponse();
83 | expect(dataAndResponse.data).toBe(response);
84 | expect(dataAndResponse.response).toBe(rawResponse);
85 | });
86 |
87 | test('list: request options and params are passed correctly', async () => {
88 | // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
89 | await expect(
90 | client.subscriptions.list(
91 | {
92 | brand_id: 'brand_id',
93 | created_at_gte: '2019-12-27T18:11:19.117Z',
94 | created_at_lte: '2019-12-27T18:11:19.117Z',
95 | customer_id: 'customer_id',
96 | page_number: 0,
97 | page_size: 0,
98 | status: 'pending',
99 | },
100 | { path: '/_stainless_unknown_path' },
101 | ),
102 | ).rejects.toThrow(DodoPayments.NotFoundError);
103 | });
104 |
105 | test('changePlan: only required params', async () => {
106 | const responsePromise = client.subscriptions.changePlan('subscription_id', {
107 | product_id: 'product_id',
108 | proration_billing_mode: 'prorated_immediately',
109 | quantity: 0,
110 | });
111 | const rawResponse = await responsePromise.asResponse();
112 | expect(rawResponse).toBeInstanceOf(Response);
113 | const response = await responsePromise;
114 | expect(response).not.toBeInstanceOf(Response);
115 | const dataAndResponse = await responsePromise.withResponse();
116 | expect(dataAndResponse.data).toBe(response);
117 | expect(dataAndResponse.response).toBe(rawResponse);
118 | });
119 |
120 | test('changePlan: required and optional params', async () => {
121 | const response = await client.subscriptions.changePlan('subscription_id', {
122 | product_id: 'product_id',
123 | proration_billing_mode: 'prorated_immediately',
124 | quantity: 0,
125 | addons: [{ addon_id: 'addon_id', quantity: 0 }],
126 | });
127 | });
128 |
129 | test('charge: only required params', async () => {
130 | const responsePromise = client.subscriptions.charge('subscription_id', { product_price: 0 });
131 | const rawResponse = await responsePromise.asResponse();
132 | expect(rawResponse).toBeInstanceOf(Response);
133 | const response = await responsePromise;
134 | expect(response).not.toBeInstanceOf(Response);
135 | const dataAndResponse = await responsePromise.withResponse();
136 | expect(dataAndResponse.data).toBe(response);
137 | expect(dataAndResponse.response).toBe(rawResponse);
138 | });
139 |
140 | test('charge: required and optional params', async () => {
141 | const response = await client.subscriptions.charge('subscription_id', {
142 | product_price: 0,
143 | adaptive_currency_fees_inclusive: true,
144 | customer_balance_config: { allow_customer_credits_purchase: true, allow_customer_credits_usage: true },
145 | metadata: { foo: 'string' },
146 | product_currency: 'AED',
147 | product_description: 'product_description',
148 | });
149 | });
150 |
151 | test('retrieveUsageHistory', async () => {
152 | const responsePromise = client.subscriptions.retrieveUsageHistory('subscription_id');
153 | const rawResponse = await responsePromise.asResponse();
154 | expect(rawResponse).toBeInstanceOf(Response);
155 | const response = await responsePromise;
156 | expect(response).not.toBeInstanceOf(Response);
157 | const dataAndResponse = await responsePromise.withResponse();
158 | expect(dataAndResponse.data).toBe(response);
159 | expect(dataAndResponse.response).toBe(rawResponse);
160 | });
161 |
162 | test('retrieveUsageHistory: request options and params are passed correctly', async () => {
163 | // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
164 | await expect(
165 | client.subscriptions.retrieveUsageHistory(
166 | 'subscription_id',
167 | {
168 | end_date: '2019-12-27T18:11:19.117Z',
169 | meter_id: 'meter_id',
170 | page_number: 0,
171 | page_size: 0,
172 | start_date: '2019-12-27T18:11:19.117Z',
173 | },
174 | { path: '/_stainless_unknown_path' },
175 | ),
176 | ).rejects.toThrow(DodoPayments.NotFoundError);
177 | });
178 | });
179 |
```