This is page 2 of 8. Use http://codebase.md/dodopayments/dodopayments-node?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
│ │ │ ├── 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/licenses.test.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: 'My Bearer Token',
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
});
describe('resource licenses', () => {
test('activate: only required params', async () => {
const responsePromise = client.licenses.activate({ license_key: 'license_key', name: 'name' });
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('activate: required and optional params', async () => {
const response = await client.licenses.activate({ license_key: 'license_key', name: 'name' });
});
test('deactivate: only required params', async () => {
const responsePromise = client.licenses.deactivate({
license_key: 'license_key',
license_key_instance_id: 'license_key_instance_id',
});
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('deactivate: required and optional params', async () => {
const response = await client.licenses.deactivate({
license_key: 'license_key',
license_key_instance_id: 'license_key_instance_id',
});
});
test('validate: only required params', async () => {
const responsePromise = client.licenses.validate({ license_key: '2b1f8e2d-c41e-4e8f-b2d3-d9fd61c38f43' });
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('validate: required and optional params', async () => {
const response = await client.licenses.validate({
license_key: '2b1f8e2d-c41e-4e8f-b2d3-d9fd61c38f43',
license_key_instance_id: 'lki_123',
});
});
});
```
--------------------------------------------------------------------------------
/tests/api-resources/refunds.test.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: 'My Bearer Token',
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
});
describe('resource refunds', () => {
test('create: only required params', async () => {
const responsePromise = client.refunds.create({ payment_id: 'payment_id' });
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('create: required and optional params', async () => {
const response = await client.refunds.create({
payment_id: 'payment_id',
items: [{ item_id: 'item_id', amount: 0, tax_inclusive: true }],
reason: 'reason',
});
});
test('retrieve', async () => {
const responsePromise = client.refunds.retrieve('refund_id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list', async () => {
const responsePromise = client.refunds.list();
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list: request options and params are passed correctly', async () => {
// ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
await expect(
client.refunds.list(
{
created_at_gte: '2019-12-27T18:11:19.117Z',
created_at_lte: '2019-12-27T18:11:19.117Z',
customer_id: 'customer_id',
page_number: 0,
page_size: 0,
status: 'succeeded',
},
{ path: '/_stainless_unknown_path' },
),
).rejects.toThrow(DodoPayments.NotFoundError);
});
});
```
--------------------------------------------------------------------------------
/tests/api-resources/brands.test.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: 'My Bearer Token',
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
});
describe('resource brands', () => {
test('create', async () => {
const responsePromise = client.brands.create({});
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('retrieve', async () => {
const responsePromise = client.brands.retrieve('id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('update', async () => {
const responsePromise = client.brands.update('id', {});
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list', async () => {
const responsePromise = client.brands.list();
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('updateImages', async () => {
const responsePromise = client.brands.updateImages('id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
});
```
--------------------------------------------------------------------------------
/src/resources/brands.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../core/resource';
import { APIPromise } from '../core/api-promise';
import { RequestOptions } from '../internal/request-options';
import { path } from '../internal/utils/path';
export class Brands extends APIResource {
create(body: BrandCreateParams, options?: RequestOptions): APIPromise<Brand> {
return this._client.post('/brands', { body, ...options });
}
/**
* Thin handler just calls `get_brand` and wraps in `Json(...)`
*/
retrieve(id: string, options?: RequestOptions): APIPromise<Brand> {
return this._client.get(path`/brands/${id}`, options);
}
update(id: string, body: BrandUpdateParams, options?: RequestOptions): APIPromise<Brand> {
return this._client.patch(path`/brands/${id}`, { body, ...options });
}
list(options?: RequestOptions): APIPromise<BrandListResponse> {
return this._client.get('/brands', options);
}
updateImages(id: string, options?: RequestOptions): APIPromise<BrandUpdateImagesResponse> {
return this._client.put(path`/brands/${id}/images`, options);
}
}
export interface Brand {
brand_id: string;
business_id: string;
enabled: boolean;
statement_descriptor: string;
verification_enabled: boolean;
verification_status: 'Success' | 'Fail' | 'Review' | 'Hold';
description?: string | null;
image?: string | null;
name?: string | null;
/**
* Incase the brand verification fails or is put on hold
*/
reason_for_hold?: string | null;
support_email?: string | null;
url?: string | null;
}
export interface BrandListResponse {
/**
* List of brands for this business
*/
items: Array<Brand>;
}
export interface BrandUpdateImagesResponse {
/**
* UUID that will be used as the image identifier/key suffix
*/
image_id: string;
/**
* Presigned URL to upload the image
*/
url: string;
}
export interface BrandCreateParams {
description?: string | null;
name?: string | null;
statement_descriptor?: string | null;
support_email?: string | null;
url?: string | null;
}
export interface BrandUpdateParams {
/**
* The UUID you got back from the presigned‐upload call
*/
image_id?: string | null;
name?: string | null;
statement_descriptor?: string | null;
support_email?: string | null;
}
export declare namespace Brands {
export {
type Brand as Brand,
type BrandListResponse as BrandListResponse,
type BrandUpdateImagesResponse as BrandUpdateImagesResponse,
type BrandCreateParams as BrandCreateParams,
type BrandUpdateParams as BrandUpdateParams,
};
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "dodopayments-mcp",
"version": "2.3.1",
"description": "The official MCP Server for the Dodo Payments API",
"author": "Dodo Payments <[email protected]>",
"types": "dist/index.d.ts",
"main": "dist/index.js",
"type": "commonjs",
"repository": {
"type": "git",
"url": "git+https://github.com/dodopayments/dodopayments-typescript.git",
"directory": "packages/mcp-server"
},
"homepage": "https://github.com/dodopayments/dodopayments-typescript/tree/main/packages/mcp-server#readme",
"license": "Apache-2.0",
"packageManager": "[email protected]",
"private": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"test": "jest",
"build": "bash ./build",
"prepack": "echo 'to pack, run yarn build && (cd dist; yarn pack)' && exit 1",
"prepublishOnly": "echo 'to publish, run yarn build && (cd dist; yarn publish)' && exit 1",
"format": "prettier --write --cache --cache-strategy metadata . !dist",
"prepare": "npm run build",
"tsn": "ts-node -r tsconfig-paths/register",
"lint": "eslint --ext ts,js .",
"fix": "eslint --fix --ext ts,js ."
},
"dependencies": {
"dodopayments": "file:../../dist/",
"@cloudflare/cabidela": "^0.2.4",
"@modelcontextprotocol/sdk": "^1.11.5",
"@valtown/deno-http-worker": "^0.0.21",
"cors": "^2.8.5",
"express": "^5.1.0",
"jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.6/jq-web.tar.gz",
"qs": "^6.14.0",
"yargs": "^17.7.2",
"zod": "^3.25.20",
"zod-to-json-schema": "^3.24.5",
"zod-validation-error": "^4.0.1"
},
"bin": {
"mcp-server": "dist/index.js"
},
"devDependencies": {
"@anthropic-ai/mcpb": "^1.1.0",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/jest": "^29.4.0",
"@types/qs": "^6.14.0",
"@types/yargs": "^17.0.8",
"@typescript-eslint/eslint-plugin": "8.31.1",
"@typescript-eslint/parser": "8.31.1",
"eslint": "^8.49.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-unused-imports": "^3.0.0",
"jest": "^29.4.0",
"prettier": "^3.0.0",
"ts-jest": "^29.1.0",
"ts-morph": "^19.0.0",
"ts-node": "^10.5.0",
"tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.9/tsc-multi.tgz",
"tsconfig-paths": "^4.0.0",
"typescript": "5.8.3"
},
"imports": {
"dodopayments-mcp": ".",
"dodopayments-mcp/*": "./src/*"
},
"exports": {
".": {
"require": "./dist/index.js",
"default": "./dist/index.mjs"
},
"./*.mjs": "./dist/*.mjs",
"./*.js": "./dist/*.js",
"./*": {
"require": "./dist/*.js",
"default": "./dist/*.mjs"
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/cloudflare-worker/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import { makeOAuthConsent } from './app';
import { McpAgent } from 'agents/mcp';
import OAuthProvider from '@cloudflare/workers-oauth-provider';
import { McpOptions, initMcpServer, server, ClientOptions } from 'dodopayments-mcp/server';
type MCPProps = {
clientProps: ClientOptions;
clientConfig: McpOptions;
};
/**
* The information displayed on the OAuth consent screen
*/
const serverConfig: ServerConfig = {
orgName: 'DodoPayments',
instructionsUrl: undefined, // Set a url for where you show users how to get an API key
logoUrl: undefined, // Set a custom logo url to appear during the OAuth flow
clientProperties: [
{
key: 'bearerToken',
label: 'Bearer Token',
description: 'Bearer Token for API authentication',
required: true,
default: undefined,
placeholder: 'My Bearer Token',
type: 'password',
},
{
key: 'webhookKey',
label: 'Webhook Key',
description: '',
required: false,
default: null,
placeholder: 'My Webhook Key',
type: 'string',
},
{
key: 'environment',
label: 'Environment',
description: 'The environment to use for the client',
required: false,
default: 'live_mode',
placeholder: 'live_mode',
type: 'select',
options: [
{ label: 'live_mode', value: 'live_mode' },
{ label: 'test_mode', value: 'test_mode' },
],
},
],
};
export class MyMCP extends McpAgent<Env, unknown, MCPProps> {
server = server;
async init() {
initMcpServer({
server: this.server,
clientOptions: this.props.clientProps,
mcpOptions: this.props.clientConfig,
});
}
}
export type ServerConfig = {
/**
* The name of the company/project
*/
orgName: string;
/**
* An optional company logo image
*/
logoUrl?: string;
/**
* An optional URL with instructions for users to get an API key
*/
instructionsUrl?: string;
/**
* Properties collected to initialize the client
*/
clientProperties: ClientProperty[];
};
export type ClientProperty = {
key: string;
label: string;
description?: string;
required: boolean;
default?: unknown;
placeholder?: string;
type: 'string' | 'number' | 'password' | 'select';
options?: { label: string; value: string }[];
};
// Export the OAuth handler as the default
export default new OAuthProvider({
apiHandlers: {
// @ts-expect-error
'/sse': MyMCP.serveSSE('/sse'), // legacy SSE
// @ts-expect-error
'/mcp': MyMCP.serve('/mcp'), // Streaming HTTP
},
defaultHandler: makeOAuthConsent(serverConfig),
authorizeEndpoint: '/authorize',
tokenEndpoint: '/token',
clientRegistrationEndpoint: '/register',
});
```
--------------------------------------------------------------------------------
/src/resources/customers/wallets/ledger-entries.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../../../core/resource';
import * as MiscAPI from '../../misc';
import * as WalletsAPI from './wallets';
import { APIPromise } from '../../../core/api-promise';
import {
DefaultPageNumberPagination,
type DefaultPageNumberPaginationParams,
PagePromise,
} from '../../../core/pagination';
import { RequestOptions } from '../../../internal/request-options';
import { path } from '../../../internal/utils/path';
export class LedgerEntries extends APIResource {
create(
customerID: string,
body: LedgerEntryCreateParams,
options?: RequestOptions,
): APIPromise<WalletsAPI.CustomerWallet> {
return this._client.post(path`/customers/${customerID}/wallets/ledger-entries`, { body, ...options });
}
list(
customerID: string,
query: LedgerEntryListParams | null | undefined = {},
options?: RequestOptions,
): PagePromise<CustomerWalletTransactionsDefaultPageNumberPagination, CustomerWalletTransaction> {
return this._client.getAPIList(
path`/customers/${customerID}/wallets/ledger-entries`,
DefaultPageNumberPagination<CustomerWalletTransaction>,
{ query, ...options },
);
}
}
export type CustomerWalletTransactionsDefaultPageNumberPagination =
DefaultPageNumberPagination<CustomerWalletTransaction>;
export interface CustomerWalletTransaction {
id: string;
after_balance: number;
amount: number;
before_balance: number;
business_id: string;
created_at: string;
currency: MiscAPI.Currency;
customer_id: string;
event_type:
| 'payment'
| 'payment_reversal'
| 'refund'
| 'refund_reversal'
| 'dispute'
| 'dispute_reversal'
| 'merchant_adjustment';
is_credit: boolean;
reason?: string | null;
reference_object_id?: string | null;
}
export interface LedgerEntryCreateParams {
amount: number;
/**
* Currency of the wallet to adjust
*/
currency: MiscAPI.Currency;
/**
* Type of ledger entry - credit or debit
*/
entry_type: 'credit' | 'debit';
/**
* Optional idempotency key to prevent duplicate entries
*/
idempotency_key?: string | null;
reason?: string | null;
}
export interface LedgerEntryListParams extends DefaultPageNumberPaginationParams {
/**
* Optional currency filter
*/
currency?: MiscAPI.Currency;
}
export declare namespace LedgerEntries {
export {
type CustomerWalletTransaction as CustomerWalletTransaction,
type CustomerWalletTransactionsDefaultPageNumberPagination as CustomerWalletTransactionsDefaultPageNumberPagination,
type LedgerEntryCreateParams as LedgerEntryCreateParams,
type LedgerEntryListParams as LedgerEntryListParams,
};
}
```
--------------------------------------------------------------------------------
/scripts/utils/postprocess-files.cjs:
--------------------------------------------------------------------------------
```
// @ts-check
const fs = require('fs');
const path = require('path');
const distDir =
process.env['DIST_PATH'] ?
path.resolve(process.env['DIST_PATH'])
: path.resolve(__dirname, '..', '..', 'dist');
async function* walk(dir) {
for await (const d of await fs.promises.opendir(dir)) {
const entry = path.join(dir, d.name);
if (d.isDirectory()) yield* walk(entry);
else if (d.isFile()) yield entry;
}
}
async function postprocess() {
for await (const file of walk(distDir)) {
if (!/(\.d)?[cm]?ts$/.test(file)) continue;
const code = await fs.promises.readFile(file, 'utf8');
// strip out lib="dom", types="node", and types="react" references; these
// are needed at build time, but would pollute the user's TS environment
const transformed = code.replace(
/^ *\/\/\/ *<reference +(lib="dom"|types="(node|react)").*?\n/gm,
// replace with same number of characters to avoid breaking source maps
(match) => ' '.repeat(match.length - 1) + '\n',
);
if (transformed !== code) {
console.error(`wrote ${path.relative(process.cwd(), file)}`);
await fs.promises.writeFile(file, transformed, 'utf8');
}
}
const newExports = {
'.': {
require: {
types: './index.d.ts',
default: './index.js',
},
types: './index.d.mts',
default: './index.mjs',
},
};
for (const entry of await fs.promises.readdir(distDir, { withFileTypes: true })) {
if (entry.isDirectory() && entry.name !== 'src' && entry.name !== 'internal' && entry.name !== 'bin') {
const subpath = './' + entry.name;
newExports[subpath + '/*.mjs'] = {
default: subpath + '/*.mjs',
};
newExports[subpath + '/*.js'] = {
default: subpath + '/*.js',
};
newExports[subpath + '/*'] = {
import: subpath + '/*.mjs',
require: subpath + '/*.js',
};
} else if (entry.isFile() && /\.[cm]?js$/.test(entry.name)) {
const { name, ext } = path.parse(entry.name);
const subpathWithoutExt = './' + name;
const subpath = './' + entry.name;
newExports[subpathWithoutExt] ||= { import: undefined, require: undefined };
const isModule = ext[1] === 'm';
if (isModule) {
newExports[subpathWithoutExt].import = subpath;
} else {
newExports[subpathWithoutExt].require = subpath;
}
newExports[subpath] = {
default: subpath,
};
}
}
await fs.promises.writeFile(
'dist/package.json',
JSON.stringify(
Object.assign(
/** @type {Record<String, unknown>} */ (
JSON.parse(await fs.promises.readFile('dist/package.json', 'utf-8'))
),
{
exports: newExports,
},
),
null,
2,
),
);
}
postprocess();
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/customers/list-customers.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'customers',
operation: 'read',
tags: [],
httpMethod: 'get',
httpPath: '/customers',
operationId: 'list_customers',
};
export const tool: Tool = {
name: 'list_customers',
description:
"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/customer'\n }\n }\n },\n required: [ 'items'\n ],\n $defs: {\n customer: {\n type: 'object',\n properties: {\n business_id: {\n type: 'string'\n },\n created_at: {\n type: 'string',\n format: 'date-time'\n },\n customer_id: {\n type: 'string'\n },\n email: {\n type: 'string'\n },\n name: {\n type: 'string'\n },\n phone_number: {\n type: 'string'\n }\n },\n required: [ 'business_id',\n 'created_at',\n 'customer_id',\n 'email',\n 'name'\n ]\n }\n }\n}\n```",
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Filter by customer email',
},
page_number: {
type: 'integer',
description: 'Page number default is 0',
},
page_size: {
type: 'integer',
description: 'Page size default is 10 max is 100',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: [],
},
annotations: {
readOnlyHint: true,
},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { jq_filter, ...body } = args as any;
const response = await client.customers.list(body).asResponse();
return asTextContentResult(await maybeFilter(jq_filter, await response.json()));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/tests/api-resources/usage-events.test.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: 'My Bearer Token',
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
});
describe('resource usageEvents', () => {
test('retrieve', async () => {
const responsePromise = client.usageEvents.retrieve('event_id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list', async () => {
const responsePromise = client.usageEvents.list();
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list: request options and params are passed correctly', async () => {
// ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
await expect(
client.usageEvents.list(
{
customer_id: 'customer_id',
end: '2019-12-27T18:11:19.117Z',
event_name: 'event_name',
meter_id: 'meter_id',
page_number: 0,
page_size: 0,
start: '2019-12-27T18:11:19.117Z',
},
{ path: '/_stainless_unknown_path' },
),
).rejects.toThrow(DodoPayments.NotFoundError);
});
test('ingest: only required params', async () => {
const responsePromise = client.usageEvents.ingest({
events: [{ customer_id: 'customer_id', event_id: 'event_id', event_name: 'event_name' }],
});
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('ingest: required and optional params', async () => {
const response = await client.usageEvents.ingest({
events: [
{
customer_id: 'customer_id',
event_id: 'event_id',
event_name: 'event_name',
metadata: { foo: 'string' },
timestamp: '2019-12-27T18:11:19.117Z',
},
],
});
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/license-key-instances/list-license-key-instances.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'license_key_instances',
operation: 'read',
tags: [],
httpMethod: 'get',
httpPath: '/license_key_instances',
operationId: 'list_license_key_instances',
};
export const tool: Tool = {
name: 'list_license_key_instances',
description:
"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/license_key_instance'\n }\n }\n },\n required: [ 'items'\n ],\n $defs: {\n license_key_instance: {\n type: 'object',\n properties: {\n id: {\n type: 'string'\n },\n business_id: {\n type: 'string'\n },\n created_at: {\n type: 'string',\n format: 'date-time'\n },\n license_key_id: {\n type: 'string'\n },\n name: {\n type: 'string'\n }\n },\n required: [ 'id',\n 'business_id',\n 'created_at',\n 'license_key_id',\n 'name'\n ]\n }\n }\n}\n```",
inputSchema: {
type: 'object',
properties: {
license_key_id: {
type: 'string',
description: 'Filter by license key ID',
},
page_number: {
type: 'integer',
description: 'Page number default is 0',
},
page_size: {
type: 'integer',
description: 'Page size default is 10 max is 100',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: [],
},
annotations: {
readOnlyHint: true,
},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { jq_filter, ...body } = args as any;
const response = await client.licenseKeyInstances.list(body).asResponse();
return asTextContentResult(await maybeFilter(jq_filter, await response.json()));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/src/resources/payouts.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../core/resource';
import * as MiscAPI from './misc';
import {
DefaultPageNumberPagination,
type DefaultPageNumberPaginationParams,
PagePromise,
} from '../core/pagination';
import { RequestOptions } from '../internal/request-options';
export class Payouts extends APIResource {
list(
query: PayoutListParams | null | undefined = {},
options?: RequestOptions,
): PagePromise<PayoutListResponsesDefaultPageNumberPagination, PayoutListResponse> {
return this._client.getAPIList('/payouts', DefaultPageNumberPagination<PayoutListResponse>, {
query,
...options,
});
}
}
export type PayoutListResponsesDefaultPageNumberPagination = DefaultPageNumberPagination<PayoutListResponse>;
export interface PayoutListResponse {
/**
* The total amount of the payout.
*/
amount: number;
/**
* The unique identifier of the business associated with the payout.
*/
business_id: string;
/**
* @deprecated The total value of chargebacks associated with the payout.
*/
chargebacks: number;
/**
* The timestamp when the payout was created, in UTC.
*/
created_at: string;
/**
* The currency of the payout, represented as an ISO 4217 currency code.
*/
currency: MiscAPI.Currency;
/**
* The fee charged for processing the payout.
*/
fee: number;
/**
* The payment method used for the payout (e.g., bank transfer, card, etc.).
*/
payment_method: string;
/**
* The unique identifier of the payout.
*/
payout_id: string;
/**
* @deprecated The total value of refunds associated with the payout.
*/
refunds: number;
/**
* The current status of the payout.
*/
status: 'not_initiated' | 'in_progress' | 'on_hold' | 'failed' | 'success';
/**
* @deprecated The tax applied to the payout.
*/
tax: number;
/**
* The timestamp when the payout was last updated, in UTC.
*/
updated_at: string;
/**
* The name of the payout recipient or purpose.
*/
name?: string | null;
/**
* The URL of the document associated with the payout.
*/
payout_document_url?: string | null;
/**
* Any additional remarks or notes associated with the payout.
*/
remarks?: string | null;
}
export interface PayoutListParams extends DefaultPageNumberPaginationParams {
/**
* Get payouts created after this time (inclusive)
*/
created_at_gte?: string;
/**
* Get payouts created before this time (inclusive)
*/
created_at_lte?: string;
}
export declare namespace Payouts {
export {
type PayoutListResponse as PayoutListResponse,
type PayoutListResponsesDefaultPageNumberPagination as PayoutListResponsesDefaultPageNumberPagination,
type PayoutListParams as PayoutListParams,
};
}
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
name: CI
on:
push:
branches-ignore:
- 'generated'
- 'codegen/**'
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
- 'stl-preview-base/**'
jobs:
lint:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/dodo-payments-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Bootstrap
run: ./scripts/bootstrap
- name: Check types
run: ./scripts/lint
build:
timeout-minutes: 5
name: build
runs-on: ${{ github.repository == 'stainless-sdks/dodo-payments-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Bootstrap
run: ./scripts/bootstrap
- name: Check build
run: ./scripts/build
- name: Get GitHub OIDC Token
if: github.repository == 'stainless-sdks/dodo-payments-typescript'
id: github-oidc
uses: actions/github-script@v6
with:
script: core.setOutput('github_token', await core.getIDToken());
- name: Upload tarball
if: github.repository == 'stainless-sdks/dodo-payments-typescript'
env:
URL: https://pkg.stainless.com/s
AUTH: ${{ steps.github-oidc.outputs.github_token }}
SHA: ${{ github.sha }}
run: ./scripts/utils/upload-artifact.sh
- name: Upload MCP Server tarball
if: github.repository == 'stainless-sdks/dodo-payments-typescript'
env:
URL: https://pkg.stainless.com/s?subpackage=mcp-server
AUTH: ${{ steps.github-oidc.outputs.github_token }}
SHA: ${{ github.sha }}
BASE_PATH: packages/mcp-server
run: ./scripts/utils/upload-artifact.sh
test:
timeout-minutes: 10
name: test
runs-on: ${{ github.repository == 'stainless-sdks/dodo-payments-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Bootstrap
run: ./scripts/bootstrap
- name: Build
run: ./scripts/build
- name: Run tests
run: ./scripts/test
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { selectTools } from './server';
import { Endpoint, endpoints } from './tools';
import { McpOptions, parseCLIOptions } from './options';
import { launchStdioServer } from './stdio';
import { launchStreamableHTTPServer } from './http';
async function main() {
const options = parseOptionsOrError();
if (options.list) {
listAllTools();
return;
}
const selectedTools = await selectToolsOrError(endpoints, options);
console.error(
`MCP Server starting with ${selectedTools.length} tools:`,
selectedTools.map((e) => e.tool.name),
);
switch (options.transport) {
case 'stdio':
await launchStdioServer(options);
break;
case 'http':
await launchStreamableHTTPServer(options, options.port ?? options.socket);
break;
}
}
if (require.main === module) {
main().catch((error) => {
console.error('Fatal error in main():', error);
process.exit(1);
});
}
function parseOptionsOrError() {
try {
return parseCLIOptions();
} catch (error) {
console.error('Error parsing options:', error);
process.exit(1);
}
}
async function selectToolsOrError(endpoints: Endpoint[], options: McpOptions): Promise<Endpoint[]> {
try {
const includedTools = await selectTools(endpoints, options);
if (includedTools.length === 0) {
console.error('No tools match the provided filters.');
process.exit(1);
}
return includedTools;
} catch (error) {
if (error instanceof Error) {
console.error('Error filtering tools:', error.message);
} else {
console.error('Error filtering tools:', error);
}
process.exit(1);
}
}
function listAllTools() {
if (endpoints.length === 0) {
console.log('No tools available.');
return;
}
console.log('Available tools:\n');
// Group endpoints by resource
const resourceGroups = new Map<string, typeof endpoints>();
for (const endpoint of endpoints) {
const resource = endpoint.metadata.resource;
if (!resourceGroups.has(resource)) {
resourceGroups.set(resource, []);
}
resourceGroups.get(resource)!.push(endpoint);
}
// Sort resources alphabetically
const sortedResources = Array.from(resourceGroups.keys()).sort();
// Display hierarchically by resource
for (const resource of sortedResources) {
console.log(`Resource: ${resource}`);
const resourceEndpoints = resourceGroups.get(resource)!;
// Sort endpoints by tool name
resourceEndpoints.sort((a, b) => a.tool.name.localeCompare(b.tool.name));
for (const endpoint of resourceEndpoints) {
const {
tool,
metadata: { operation, tags },
} = endpoint;
console.log(` - ${tool.name} (${operation}) ${tags.length > 0 ? `tags: ${tags.join(', ')}` : ''}`);
console.log(` Description: ${tool.description}`);
}
console.log('');
}
}
```
--------------------------------------------------------------------------------
/tests/api-resources/customers/customers.test.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: 'My Bearer Token',
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
});
describe('resource customers', () => {
test('create: only required params', async () => {
const responsePromise = client.customers.create({ email: 'email', name: 'name' });
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('create: required and optional params', async () => {
const response = await client.customers.create({
email: 'email',
name: 'name',
phone_number: 'phone_number',
});
});
test('retrieve', async () => {
const responsePromise = client.customers.retrieve('customer_id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('update', async () => {
const responsePromise = client.customers.update('customer_id', {});
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list', async () => {
const responsePromise = client.customers.list();
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list: request options and params are passed correctly', async () => {
// ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
await expect(
client.customers.list(
{ email: 'email', page_number: 0, page_size: 0 },
{ path: '/_stainless_unknown_path' },
),
).rejects.toThrow(DodoPayments.NotFoundError);
});
});
```
--------------------------------------------------------------------------------
/src/internal/builtin-types.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
export type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
/**
* An alias to the builtin `RequestInit` type so we can
* easily alias it in import statements if there are name clashes.
*
* https://developer.mozilla.org/docs/Web/API/RequestInit
*/
type _RequestInit = RequestInit;
/**
* An alias to the builtin `Response` type so we can
* easily alias it in import statements if there are name clashes.
*
* https://developer.mozilla.org/docs/Web/API/Response
*/
type _Response = Response;
/**
* The type for the first argument to `fetch`.
*
* https://developer.mozilla.org/docs/Web/API/Window/fetch#resource
*/
type _RequestInfo = Request | URL | string;
/**
* The type for constructing `RequestInit` Headers.
*
* https://developer.mozilla.org/docs/Web/API/RequestInit#setting_headers
*/
type _HeadersInit = RequestInit['headers'];
/**
* The type for constructing `RequestInit` body.
*
* https://developer.mozilla.org/docs/Web/API/RequestInit#body
*/
type _BodyInit = RequestInit['body'];
/**
* An alias to the builtin `Array<T>` type so we can
* easily alias it in import statements if there are name clashes.
*/
type _Array<T> = Array<T>;
/**
* An alias to the builtin `Record<K, T>` type so we can
* easily alias it in import statements if there are name clashes.
*/
type _Record<K extends keyof any, T> = Record<K, T>;
export type {
_Array as Array,
_BodyInit as BodyInit,
_HeadersInit as HeadersInit,
_Record as Record,
_RequestInfo as RequestInfo,
_RequestInit as RequestInit,
_Response as Response,
};
/**
* A copy of the builtin `EndingType` type as it isn't fully supported in certain
* environments and attempting to reference the global version will error.
*
* https://github.com/microsoft/TypeScript/blob/49ad1a3917a0ea57f5ff248159256e12bb1cb705/src/lib/dom.generated.d.ts#L27941
*/
type EndingType = 'native' | 'transparent';
/**
* A copy of the builtin `BlobPropertyBag` type as it isn't fully supported in certain
* environments and attempting to reference the global version will error.
*
* https://github.com/microsoft/TypeScript/blob/49ad1a3917a0ea57f5ff248159256e12bb1cb705/src/lib/dom.generated.d.ts#L154
* https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob#options
*/
export interface BlobPropertyBag {
endings?: EndingType;
type?: string;
}
/**
* A copy of the builtin `FilePropertyBag` type as it isn't fully supported in certain
* environments and attempting to reference the global version will error.
*
* https://github.com/microsoft/TypeScript/blob/49ad1a3917a0ea57f5ff248159256e12bb1cb705/src/lib/dom.generated.d.ts#L503
* https://developer.mozilla.org/en-US/docs/Web/API/File/File#options
*/
export interface FilePropertyBag extends BlobPropertyBag {
lastModified?: number;
}
```
--------------------------------------------------------------------------------
/src/internal/headers.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { isReadonlyArray } from './utils/values';
type HeaderValue = string | undefined | null;
export type HeadersLike =
| Headers
| readonly HeaderValue[][]
| Record<string, HeaderValue | readonly HeaderValue[]>
| undefined
| null
| NullableHeaders;
const brand_privateNullableHeaders = /* @__PURE__ */ Symbol('brand.privateNullableHeaders');
/**
* @internal
* Users can pass explicit nulls to unset default headers. When we parse them
* into a standard headers type we need to preserve that information.
*/
export type NullableHeaders = {
/** Brand check, prevent users from creating a NullableHeaders. */
[brand_privateNullableHeaders]: true;
/** Parsed headers. */
values: Headers;
/** Set of lowercase header names explicitly set to null. */
nulls: Set<string>;
};
function* iterateHeaders(headers: HeadersLike): IterableIterator<readonly [string, string | null]> {
if (!headers) return;
if (brand_privateNullableHeaders in headers) {
const { values, nulls } = headers;
yield* values.entries();
for (const name of nulls) {
yield [name, null];
}
return;
}
let shouldClear = false;
let iter: Iterable<readonly (HeaderValue | readonly HeaderValue[])[]>;
if (headers instanceof Headers) {
iter = headers.entries();
} else if (isReadonlyArray(headers)) {
iter = headers;
} else {
shouldClear = true;
iter = Object.entries(headers ?? {});
}
for (let row of iter) {
const name = row[0];
if (typeof name !== 'string') throw new TypeError('expected header name to be a string');
const values = isReadonlyArray(row[1]) ? row[1] : [row[1]];
let didClear = false;
for (const value of values) {
if (value === undefined) continue;
// Objects keys always overwrite older headers, they never append.
// Yield a null to clear the header before adding the new values.
if (shouldClear && !didClear) {
didClear = true;
yield [name, null];
}
yield [name, value];
}
}
}
export const buildHeaders = (newHeaders: HeadersLike[]): NullableHeaders => {
const targetHeaders = new Headers();
const nullHeaders = new Set<string>();
for (const headers of newHeaders) {
const seenHeaders = new Set<string>();
for (const [name, value] of iterateHeaders(headers)) {
const lowerName = name.toLowerCase();
if (!seenHeaders.has(lowerName)) {
targetHeaders.delete(name);
seenHeaders.add(lowerName);
}
if (value === null) {
targetHeaders.delete(name);
nullHeaders.add(lowerName);
} else {
targetHeaders.append(name, value);
nullHeaders.delete(lowerName);
}
}
}
return { [brand_privateNullableHeaders]: true, values: targetHeaders, nulls: nullHeaders };
};
export const isEmptyHeaders = (headers: HeadersLike) => {
for (const _ of iterateHeaders(headers)) return false;
return true;
};
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/brands/retrieve-brands.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'brands',
operation: 'read',
tags: [],
httpMethod: 'get',
httpPath: '/brands/{id}',
operationId: 'get_brand_handler',
};
export const tool: Tool = {
name: 'retrieve_brands',
description:
"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\nThin handler just calls `get_brand` and wraps in `Json(...)`\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/brand',\n $defs: {\n brand: {\n type: 'object',\n properties: {\n brand_id: {\n type: 'string'\n },\n business_id: {\n type: 'string'\n },\n enabled: {\n type: 'boolean'\n },\n statement_descriptor: {\n type: 'string'\n },\n verification_enabled: {\n type: 'boolean'\n },\n verification_status: {\n type: 'string',\n enum: [ 'Success',\n 'Fail',\n 'Review',\n 'Hold'\n ]\n },\n description: {\n type: 'string'\n },\n image: {\n type: 'string'\n },\n name: {\n type: 'string'\n },\n reason_for_hold: {\n type: 'string',\n description: 'Incase the brand verification fails or is put on hold'\n },\n support_email: {\n type: 'string'\n },\n url: {\n type: 'string'\n }\n },\n required: [ 'brand_id',\n 'business_id',\n 'enabled',\n 'statement_descriptor',\n 'verification_enabled',\n 'verification_status'\n ]\n }\n }\n}\n```",
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: ['id'],
},
annotations: {
readOnlyHint: true,
},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { id, jq_filter, ...body } = args as any;
return asTextContentResult(await maybeFilter(jq_filter, await client.brands.retrieve(id)));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/src/resources/customers/customers.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../../core/resource';
import * as CustomerPortalAPI from './customer-portal';
import { CustomerPortal, CustomerPortalCreateParams } from './customer-portal';
import * as WalletsAPI from './wallets/wallets';
import { CustomerWallet, WalletListResponse, Wallets } from './wallets/wallets';
import { APIPromise } from '../../core/api-promise';
import {
DefaultPageNumberPagination,
type DefaultPageNumberPaginationParams,
PagePromise,
} from '../../core/pagination';
import { RequestOptions } from '../../internal/request-options';
import { path } from '../../internal/utils/path';
export class Customers extends APIResource {
customerPortal: CustomerPortalAPI.CustomerPortal = new CustomerPortalAPI.CustomerPortal(this._client);
wallets: WalletsAPI.Wallets = new WalletsAPI.Wallets(this._client);
create(body: CustomerCreateParams, options?: RequestOptions): APIPromise<Customer> {
return this._client.post('/customers', { body, ...options });
}
retrieve(customerID: string, options?: RequestOptions): APIPromise<Customer> {
return this._client.get(path`/customers/${customerID}`, options);
}
update(customerID: string, body: CustomerUpdateParams, options?: RequestOptions): APIPromise<Customer> {
return this._client.patch(path`/customers/${customerID}`, { body, ...options });
}
list(
query: CustomerListParams | null | undefined = {},
options?: RequestOptions,
): PagePromise<CustomersDefaultPageNumberPagination, Customer> {
return this._client.getAPIList('/customers', DefaultPageNumberPagination<Customer>, {
query,
...options,
});
}
}
export type CustomersDefaultPageNumberPagination = DefaultPageNumberPagination<Customer>;
export interface Customer {
business_id: string;
created_at: string;
customer_id: string;
email: string;
name: string;
phone_number?: string | null;
}
export interface CustomerPortalSession {
link: string;
}
export interface CustomerCreateParams {
email: string;
name: string;
phone_number?: string | null;
}
export interface CustomerUpdateParams {
name?: string | null;
phone_number?: string | null;
}
export interface CustomerListParams extends DefaultPageNumberPaginationParams {
/**
* Filter by customer email
*/
email?: string;
}
Customers.CustomerPortal = CustomerPortal;
Customers.Wallets = Wallets;
export declare namespace Customers {
export {
type Customer as Customer,
type CustomerPortalSession as CustomerPortalSession,
type CustomersDefaultPageNumberPagination as CustomersDefaultPageNumberPagination,
type CustomerCreateParams as CustomerCreateParams,
type CustomerUpdateParams as CustomerUpdateParams,
type CustomerListParams as CustomerListParams,
};
export { CustomerPortal as CustomerPortal, type CustomerPortalCreateParams as CustomerPortalCreateParams };
export {
Wallets as Wallets,
type CustomerWallet as CustomerWallet,
type WalletListResponse as WalletListResponse,
};
}
```
--------------------------------------------------------------------------------
/src/internal/utils/values.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { DodoPaymentsError } from '../../core/error';
// https://url.spec.whatwg.org/#url-scheme-string
const startsWithSchemeRegexp = /^[a-z][a-z0-9+.-]*:/i;
export const isAbsoluteURL = (url: string): boolean => {
return startsWithSchemeRegexp.test(url);
};
export let isArray = (val: unknown): val is unknown[] => ((isArray = Array.isArray), isArray(val));
export let isReadonlyArray = isArray as (val: unknown) => val is readonly unknown[];
/** Returns an object if the given value isn't an object, otherwise returns as-is */
export function maybeObj(x: unknown): object {
if (typeof x !== 'object') {
return {};
}
return x ?? {};
}
// https://stackoverflow.com/a/34491287
export function isEmptyObj(obj: Object | null | undefined): boolean {
if (!obj) return true;
for (const _k in obj) return false;
return true;
}
// https://eslint.org/docs/latest/rules/no-prototype-builtins
export function hasOwn<T extends object = object>(obj: T, key: PropertyKey): key is keyof T {
return Object.prototype.hasOwnProperty.call(obj, key);
}
export function isObj(obj: unknown): obj is Record<string, unknown> {
return obj != null && typeof obj === 'object' && !Array.isArray(obj);
}
export const ensurePresent = <T>(value: T | null | undefined): T => {
if (value == null) {
throw new DodoPaymentsError(`Expected a value to be given but received ${value} instead.`);
}
return value;
};
export const validatePositiveInteger = (name: string, n: unknown): number => {
if (typeof n !== 'number' || !Number.isInteger(n)) {
throw new DodoPaymentsError(`${name} must be an integer`);
}
if (n < 0) {
throw new DodoPaymentsError(`${name} must be a positive integer`);
}
return n;
};
export const coerceInteger = (value: unknown): number => {
if (typeof value === 'number') return Math.round(value);
if (typeof value === 'string') return parseInt(value, 10);
throw new DodoPaymentsError(`Could not coerce ${value} (type: ${typeof value}) into a number`);
};
export const coerceFloat = (value: unknown): number => {
if (typeof value === 'number') return value;
if (typeof value === 'string') return parseFloat(value);
throw new DodoPaymentsError(`Could not coerce ${value} (type: ${typeof value}) into a number`);
};
export const coerceBoolean = (value: unknown): boolean => {
if (typeof value === 'boolean') return value;
if (typeof value === 'string') return value === 'true';
return Boolean(value);
};
export const maybeCoerceInteger = (value: unknown): number | undefined => {
if (value == null) {
return undefined;
}
return coerceInteger(value);
};
export const maybeCoerceFloat = (value: unknown): number | undefined => {
if (value == null) {
return undefined;
}
return coerceFloat(value);
};
export const maybeCoerceBoolean = (value: unknown): boolean | undefined => {
if (value == null) {
return undefined;
}
return coerceBoolean(value);
};
export const safeJSON = (text: string) => {
try {
return JSON.parse(text);
} catch (err) {
return undefined;
}
};
```
--------------------------------------------------------------------------------
/src/core/api-promise.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { type DodoPayments } from '../client';
import { type PromiseOrValue } from '../internal/types';
import { APIResponseProps, defaultParseResponse } from '../internal/parse';
/**
* A subclass of `Promise` providing additional helper methods
* for interacting with the SDK.
*/
export class APIPromise<T> extends Promise<T> {
private parsedPromise: Promise<T> | undefined;
#client: DodoPayments;
constructor(
client: DodoPayments,
private responsePromise: Promise<APIResponseProps>,
private parseResponse: (
client: DodoPayments,
props: APIResponseProps,
) => PromiseOrValue<T> = defaultParseResponse,
) {
super((resolve) => {
// this is maybe a bit weird but this has to be a no-op to not implicitly
// parse the response body; instead .then, .catch, .finally are overridden
// to parse the response
resolve(null as any);
});
this.#client = client;
}
_thenUnwrap<U>(transform: (data: T, props: APIResponseProps) => U): APIPromise<U> {
return new APIPromise(this.#client, this.responsePromise, async (client, props) =>
transform(await this.parseResponse(client, props), props),
);
}
/**
* Gets the raw `Response` instance instead of parsing the response
* data.
*
* If you want to parse the response body but still get the `Response`
* instance, you can use {@link withResponse()}.
*
* 👋 Getting the wrong TypeScript type for `Response`?
* Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
* to your `tsconfig.json`.
*/
asResponse(): Promise<Response> {
return this.responsePromise.then((p) => p.response);
}
/**
* Gets the parsed response data and the raw `Response` instance.
*
* If you just want to get the raw `Response` instance without parsing it,
* you can use {@link asResponse()}.
*
* 👋 Getting the wrong TypeScript type for `Response`?
* Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
* to your `tsconfig.json`.
*/
async withResponse(): Promise<{ data: T; response: Response }> {
const [data, response] = await Promise.all([this.parse(), this.asResponse()]);
return { data, response };
}
private parse(): Promise<T> {
if (!this.parsedPromise) {
this.parsedPromise = this.responsePromise.then((data) => this.parseResponse(this.#client, data));
}
return this.parsedPromise;
}
override then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
): Promise<TResult1 | TResult2> {
return this.parse().then(onfulfilled, onrejected);
}
override catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null,
): Promise<T | TResult> {
return this.parse().catch(onrejected);
}
override finally(onfinally?: (() => void) | undefined | null): Promise<T> {
return this.parse().finally(onfinally);
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/brands/create-brands.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'brands',
operation: 'write',
tags: [],
httpMethod: 'post',
httpPath: '/brands',
operationId: 'create_brand_handler',
};
export const tool: Tool = {
name: 'create_brands',
description:
"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/brand',\n $defs: {\n brand: {\n type: 'object',\n properties: {\n brand_id: {\n type: 'string'\n },\n business_id: {\n type: 'string'\n },\n enabled: {\n type: 'boolean'\n },\n statement_descriptor: {\n type: 'string'\n },\n verification_enabled: {\n type: 'boolean'\n },\n verification_status: {\n type: 'string',\n enum: [ 'Success',\n 'Fail',\n 'Review',\n 'Hold'\n ]\n },\n description: {\n type: 'string'\n },\n image: {\n type: 'string'\n },\n name: {\n type: 'string'\n },\n reason_for_hold: {\n type: 'string',\n description: 'Incase the brand verification fails or is put on hold'\n },\n support_email: {\n type: 'string'\n },\n url: {\n type: 'string'\n }\n },\n required: [ 'brand_id',\n 'business_id',\n 'enabled',\n 'statement_descriptor',\n 'verification_enabled',\n 'verification_status'\n ]\n }\n }\n}\n```",
inputSchema: {
type: 'object',
properties: {
description: {
type: 'string',
},
name: {
type: 'string',
},
statement_descriptor: {
type: 'string',
},
support_email: {
type: 'string',
},
url: {
type: 'string',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: [],
},
annotations: {},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { jq_filter, ...body } = args as any;
return asTextContentResult(await maybeFilter(jq_filter, await client.brands.create(body)));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/src/resources/licenses.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../core/resource';
import * as PaymentsAPI from './payments';
import { APIPromise } from '../core/api-promise';
import { buildHeaders } from '../internal/headers';
import { RequestOptions } from '../internal/request-options';
export class Licenses extends APIResource {
/**
* @example
* ```ts
* const response = await client.licenses.activate({
* license_key: 'license_key',
* name: 'name',
* });
* ```
*/
activate(body: LicenseActivateParams, options?: RequestOptions): APIPromise<LicenseActivateResponse> {
return this._client.post('/licenses/activate', { body, ...options });
}
/**
* @example
* ```ts
* await client.licenses.deactivate({
* license_key: 'license_key',
* license_key_instance_id: 'license_key_instance_id',
* });
* ```
*/
deactivate(body: LicenseDeactivateParams, options?: RequestOptions): APIPromise<void> {
return this._client.post('/licenses/deactivate', {
body,
...options,
headers: buildHeaders([{ Accept: '*/*' }, options?.headers]),
});
}
/**
* @example
* ```ts
* const response = await client.licenses.validate({
* license_key: '2b1f8e2d-c41e-4e8f-b2d3-d9fd61c38f43',
* });
* ```
*/
validate(body: LicenseValidateParams, options?: RequestOptions): APIPromise<LicenseValidateResponse> {
return this._client.post('/licenses/validate', { body, ...options });
}
}
export interface LicenseActivateResponse {
/**
* License key instance ID
*/
id: string;
/**
* Business ID
*/
business_id: string;
/**
* Creation timestamp
*/
created_at: string;
/**
* Limited customer details associated with the license key.
*/
customer: PaymentsAPI.CustomerLimitedDetails;
/**
* Associated license key ID
*/
license_key_id: string;
/**
* Instance name
*/
name: string;
/**
* Related product info. Present if the license key is tied to a product.
*/
product: LicenseActivateResponse.Product;
}
export namespace LicenseActivateResponse {
/**
* Related product info. Present if the license key is tied to a product.
*/
export interface Product {
/**
* Unique identifier for the product.
*/
product_id: string;
/**
* Name of the product, if set by the merchant.
*/
name?: string | null;
}
}
export interface LicenseValidateResponse {
valid: boolean;
}
export interface LicenseActivateParams {
license_key: string;
name: string;
}
export interface LicenseDeactivateParams {
license_key: string;
license_key_instance_id: string;
}
export interface LicenseValidateParams {
license_key: string;
license_key_instance_id?: string | null;
}
export declare namespace Licenses {
export {
type LicenseActivateResponse as LicenseActivateResponse,
type LicenseValidateResponse as LicenseValidateResponse,
type LicenseActivateParams as LicenseActivateParams,
type LicenseDeactivateParams as LicenseDeactivateParams,
type LicenseValidateParams as LicenseValidateParams,
};
}
```
--------------------------------------------------------------------------------
/src/internal/utils/path.ts:
--------------------------------------------------------------------------------
```typescript
import { DodoPaymentsError } from '../../core/error';
/**
* Percent-encode everything that isn't safe to have in a path without encoding safe chars.
*
* Taken from https://datatracker.ietf.org/doc/html/rfc3986#section-3.3:
* > unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* > sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
* > pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
*/
export function encodeURIPath(str: string) {
return str.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent);
}
const EMPTY = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.create(null));
export const createPathTagFunction = (pathEncoder = encodeURIPath) =>
function path(statics: readonly string[], ...params: readonly unknown[]): string {
// If there are no params, no processing is needed.
if (statics.length === 1) return statics[0]!;
let postPath = false;
const invalidSegments = [];
const path = statics.reduce((previousValue, currentValue, index) => {
if (/[?#]/.test(currentValue)) {
postPath = true;
}
const value = params[index];
let encoded = (postPath ? encodeURIComponent : pathEncoder)('' + value);
if (
index !== params.length &&
(value == null ||
(typeof value === 'object' &&
// handle values from other realms
value.toString ===
Object.getPrototypeOf(Object.getPrototypeOf((value as any).hasOwnProperty ?? EMPTY) ?? EMPTY)
?.toString))
) {
encoded = value + '';
invalidSegments.push({
start: previousValue.length + currentValue.length,
length: encoded.length,
error: `Value of type ${Object.prototype.toString
.call(value)
.slice(8, -1)} is not a valid path parameter`,
});
}
return previousValue + currentValue + (index === params.length ? '' : encoded);
}, '');
const pathOnly = path.split(/[?#]/, 1)[0]!;
const invalidSegmentPattern = /(?<=^|\/)(?:\.|%2e){1,2}(?=\/|$)/gi;
let match;
// Find all invalid segments
while ((match = invalidSegmentPattern.exec(pathOnly)) !== null) {
invalidSegments.push({
start: match.index,
length: match[0].length,
error: `Value "${match[0]}" can\'t be safely passed as a path parameter`,
});
}
invalidSegments.sort((a, b) => a.start - b.start);
if (invalidSegments.length > 0) {
let lastEnd = 0;
const underline = invalidSegments.reduce((acc, segment) => {
const spaces = ' '.repeat(segment.start - lastEnd);
const arrows = '^'.repeat(segment.length);
lastEnd = segment.start + segment.length;
return acc + spaces + arrows;
}, '');
throw new DodoPaymentsError(
`Path parameters result in path with invalid segments:\n${invalidSegments
.map((e) => e.error)
.join('\n')}\n${path}\n${underline}`,
);
}
return path;
};
/**
* URI-encodes path params and ensures no unsafe /./ or /../ path segments are introduced.
*/
export const path = /* @__PURE__ */ createPathTagFunction(encodeURIPath);
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/brands/list-brands.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'brands',
operation: 'read',
tags: [],
httpMethod: 'get',
httpPath: '/brands',
operationId: 'list_brands_handler',
};
export const tool: Tool = {
name: 'list_brands',
description:
"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/brand_list_response',\n $defs: {\n brand_list_response: {\n type: 'object',\n properties: {\n items: {\n type: 'array',\n description: 'List of brands for this business',\n items: {\n $ref: '#/$defs/brand'\n }\n }\n },\n required: [ 'items'\n ]\n },\n brand: {\n type: 'object',\n properties: {\n brand_id: {\n type: 'string'\n },\n business_id: {\n type: 'string'\n },\n enabled: {\n type: 'boolean'\n },\n statement_descriptor: {\n type: 'string'\n },\n verification_enabled: {\n type: 'boolean'\n },\n verification_status: {\n type: 'string',\n enum: [ 'Success',\n 'Fail',\n 'Review',\n 'Hold'\n ]\n },\n description: {\n type: 'string'\n },\n image: {\n type: 'string'\n },\n name: {\n type: 'string'\n },\n reason_for_hold: {\n type: 'string',\n description: 'Incase the brand verification fails or is put on hold'\n },\n support_email: {\n type: 'string'\n },\n url: {\n type: 'string'\n }\n },\n required: [ 'brand_id',\n 'business_id',\n 'enabled',\n 'statement_descriptor',\n 'verification_enabled',\n 'verification_status'\n ]\n }\n }\n}\n```",
inputSchema: {
type: 'object',
properties: {
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: [],
},
annotations: {
readOnlyHint: true,
},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { jq_filter } = args as any;
return asTextContentResult(await maybeFilter(jq_filter, await client.brands.list()));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/brands/update-brands.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'brands',
operation: 'write',
tags: [],
httpMethod: 'patch',
httpPath: '/brands/{id}',
operationId: 'patch_brand_handler',
};
export const tool: Tool = {
name: 'update_brands',
description:
"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/brand',\n $defs: {\n brand: {\n type: 'object',\n properties: {\n brand_id: {\n type: 'string'\n },\n business_id: {\n type: 'string'\n },\n enabled: {\n type: 'boolean'\n },\n statement_descriptor: {\n type: 'string'\n },\n verification_enabled: {\n type: 'boolean'\n },\n verification_status: {\n type: 'string',\n enum: [ 'Success',\n 'Fail',\n 'Review',\n 'Hold'\n ]\n },\n description: {\n type: 'string'\n },\n image: {\n type: 'string'\n },\n name: {\n type: 'string'\n },\n reason_for_hold: {\n type: 'string',\n description: 'Incase the brand verification fails or is put on hold'\n },\n support_email: {\n type: 'string'\n },\n url: {\n type: 'string'\n }\n },\n required: [ 'brand_id',\n 'business_id',\n 'enabled',\n 'statement_descriptor',\n 'verification_enabled',\n 'verification_status'\n ]\n }\n }\n}\n```",
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
},
image_id: {
type: 'string',
description: 'The UUID you got back from the presigned‐upload call',
},
name: {
type: 'string',
},
statement_descriptor: {
type: 'string',
},
support_email: {
type: 'string',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: ['id'],
},
annotations: {},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { id, jq_filter, ...body } = args as any;
return asTextContentResult(await maybeFilter(jq_filter, await client.brands.update(id, body)));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/src/internal/utils/log.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { hasOwn } from './values';
import { type DodoPayments } from '../../client';
import { RequestOptions } from '../request-options';
type LogFn = (message: string, ...rest: unknown[]) => void;
export type Logger = {
error: LogFn;
warn: LogFn;
info: LogFn;
debug: LogFn;
};
export type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug';
const levelNumbers = {
off: 0,
error: 200,
warn: 300,
info: 400,
debug: 500,
};
export const parseLogLevel = (
maybeLevel: string | undefined,
sourceName: string,
client: DodoPayments,
): LogLevel | undefined => {
if (!maybeLevel) {
return undefined;
}
if (hasOwn(levelNumbers, maybeLevel)) {
return maybeLevel;
}
loggerFor(client).warn(
`${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify(
Object.keys(levelNumbers),
)}`,
);
return undefined;
};
function noop() {}
function makeLogFn(fnLevel: keyof Logger, logger: Logger | undefined, logLevel: LogLevel) {
if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) {
return noop;
} else {
// Don't wrap logger functions, we want the stacktrace intact!
return logger[fnLevel].bind(logger);
}
}
const noopLogger = {
error: noop,
warn: noop,
info: noop,
debug: noop,
};
let cachedLoggers = /* @__PURE__ */ new WeakMap<Logger, [LogLevel, Logger]>();
export function loggerFor(client: DodoPayments): Logger {
const logger = client.logger;
const logLevel = client.logLevel ?? 'off';
if (!logger) {
return noopLogger;
}
const cachedLogger = cachedLoggers.get(logger);
if (cachedLogger && cachedLogger[0] === logLevel) {
return cachedLogger[1];
}
const levelLogger = {
error: makeLogFn('error', logger, logLevel),
warn: makeLogFn('warn', logger, logLevel),
info: makeLogFn('info', logger, logLevel),
debug: makeLogFn('debug', logger, logLevel),
};
cachedLoggers.set(logger, [logLevel, levelLogger]);
return levelLogger;
}
export const formatRequestDetails = (details: {
options?: RequestOptions | undefined;
headers?: Headers | Record<string, string> | undefined;
retryOfRequestLogID?: string | undefined;
retryOf?: string | undefined;
url?: string | undefined;
status?: number | undefined;
method?: string | undefined;
durationMs?: number | undefined;
message?: unknown;
body?: unknown;
}) => {
if (details.options) {
details.options = { ...details.options };
delete details.options['headers']; // redundant + leaks internals
}
if (details.headers) {
details.headers = Object.fromEntries(
(details.headers instanceof Headers ? [...details.headers] : Object.entries(details.headers)).map(
([name, value]) => [
name,
(
name.toLowerCase() === 'authorization' ||
name.toLowerCase() === 'cookie' ||
name.toLowerCase() === 'set-cookie'
) ?
'***'
: value,
],
),
);
}
if ('retryOfRequestLogID' in details) {
if (details.retryOfRequestLogID) {
details.retryOf = details.retryOfRequestLogID;
}
delete details.retryOfRequestLogID;
}
return details;
};
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/webhooks/retrieve-webhooks.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'webhooks',
operation: 'read',
tags: [],
httpMethod: 'get',
httpPath: '/webhooks/{webhook_id}',
operationId: 'get_webhook',
};
export const tool: Tool = {
name: 'retrieve_webhooks',
description:
"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\nGet a webhook by id\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/webhook_details',\n $defs: {\n webhook_details: {\n type: 'object',\n title: 'Webhook Details',\n properties: {\n id: {\n type: 'string',\n description: 'The webhook\\'s ID.'\n },\n created_at: {\n type: 'string',\n description: 'Created at timestamp'\n },\n description: {\n type: 'string',\n description: 'An example webhook name.'\n },\n metadata: {\n type: 'object',\n description: 'Metadata of the webhook',\n additionalProperties: true\n },\n updated_at: {\n type: 'string',\n description: 'Updated at timestamp'\n },\n url: {\n type: 'string',\n description: 'Url endpoint of the webhook'\n },\n disabled: {\n type: 'boolean',\n description: 'Status of the webhook.\\n\\nIf true, events are not sent'\n },\n filter_types: {\n type: 'array',\n description: 'Filter events to the webhook.\\n\\nWebhook event will only be sent for events in the list.',\n items: {\n type: 'string'\n }\n },\n rate_limit: {\n type: 'integer',\n description: 'Configured rate limit'\n }\n },\n required: [ 'id',\n 'created_at',\n 'description',\n 'metadata',\n 'updated_at',\n 'url'\n ]\n }\n }\n}\n```",
inputSchema: {
type: 'object',
properties: {
webhook_id: {
type: 'string',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: ['webhook_id'],
},
annotations: {
readOnlyHint: true,
},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { webhook_id, jq_filter, ...body } = args as any;
return asTextContentResult(await maybeFilter(jq_filter, await client.webhooks.retrieve(webhook_id)));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/http.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
import { fromError } from 'zod-validation-error/v3';
import { McpOptions, parseQueryOptions } from './options';
import { ClientOptions, initMcpServer, newMcpServer } from './server';
import { parseAuthHeaders } from './headers';
const newServer = ({
clientOptions,
mcpOptions: defaultMcpOptions,
req,
res,
}: {
clientOptions: ClientOptions;
mcpOptions: McpOptions;
req: express.Request;
res: express.Response;
}): McpServer | null => {
const server = newMcpServer();
let mcpOptions: McpOptions;
try {
mcpOptions = parseQueryOptions(defaultMcpOptions, req.query);
} catch (error) {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: `Invalid request: ${fromError(error)}`,
},
});
return null;
}
try {
const authOptions = parseAuthHeaders(req);
initMcpServer({
server: server,
clientOptions: {
...clientOptions,
...authOptions,
},
mcpOptions,
});
} catch {
res.status(401).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Unauthorized',
},
});
return null;
}
return server;
};
const post =
(options: { clientOptions: ClientOptions; mcpOptions: McpOptions }) =>
async (req: express.Request, res: express.Response) => {
const server = newServer({ ...options, req, res });
// If we return null, we already set the authorization error.
if (server === null) return;
const transport = new StreamableHTTPServerTransport({
// Stateless server
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
};
const get = async (req: express.Request, res: express.Response) => {
res.status(405).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Method not supported',
},
});
};
const del = async (req: express.Request, res: express.Response) => {
res.status(405).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Method not supported',
},
});
};
export const streamableHTTPApp = ({
clientOptions = {},
mcpOptions = {},
}: {
clientOptions?: ClientOptions;
mcpOptions?: McpOptions;
}): express.Express => {
const app = express();
app.set('query parser', 'extended');
app.use(express.json());
app.get('/', get);
app.post('/', post({ clientOptions, mcpOptions }));
app.delete('/', del);
return app;
};
export const launchStreamableHTTPServer = async (options: McpOptions, port: number | string | undefined) => {
const app = streamableHTTPApp({ mcpOptions: options });
const server = app.listen(port);
const address = server.address();
if (typeof address === 'string') {
console.error(`MCP Server running on streamable HTTP at ${address}`);
} else if (address !== null) {
console.error(`MCP Server running on streamable HTTP on port ${address.port}`);
} else {
console.error(`MCP Server running on streamable HTTP on port ${port}`);
}
};
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/usage-events/retrieve-usage-events.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'usage_events',
operation: 'read',
tags: [],
httpMethod: 'get',
httpPath: '/events/{event_id}',
operationId: 'get_event_by_id',
};
export const tool: Tool = {
name: 'retrieve_usage_events',
description:
"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\nFetch detailed information about a single event using its unique event ID. This endpoint is useful for:\n- Debugging specific event ingestion issues\n- Retrieving event details for customer support\n- Validating that events were processed correctly\n- Getting the complete metadata for an event\n\n## Event ID Format:\nThe event ID should be the same value that was provided during event ingestion via the `/events/ingest` endpoint.\nEvent IDs are case-sensitive and must match exactly.\n\n## Response Details:\nThe response includes all event data including:\n- Complete metadata key-value pairs\n- Original timestamp (preserved from ingestion)\n- Customer and business association\n- Event name and processing information\n\n## Example Usage:\n```text\nGET /events/api_call_12345\n```\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/event',\n $defs: {\n event: {\n type: 'object',\n properties: {\n business_id: {\n type: 'string'\n },\n customer_id: {\n type: 'string'\n },\n event_id: {\n type: 'string'\n },\n event_name: {\n type: 'string'\n },\n timestamp: {\n type: 'string',\n format: 'date-time'\n },\n metadata: {\n type: 'object',\n title: 'EventMetadata',\n description: 'Arbitrary key-value metadata. Values can be string, integer, number, or boolean.',\n additionalProperties: true\n }\n },\n required: [ 'business_id',\n 'customer_id',\n 'event_id',\n 'event_name',\n 'timestamp'\n ]\n }\n }\n}\n```",
inputSchema: {
type: 'object',
properties: {
event_id: {
type: 'string',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: ['event_id'],
},
annotations: {
readOnlyHint: true,
},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { event_id, jq_filter, ...body } = args as any;
return asTextContentResult(await maybeFilter(jq_filter, await client.usageEvents.retrieve(event_id)));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/tests/api-resources/addons.test.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: 'My Bearer Token',
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
});
describe('resource addons', () => {
test('create: only required params', async () => {
const responsePromise = client.addons.create({
currency: 'AED',
name: 'name',
price: 0,
tax_category: 'digital_products',
});
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('create: required and optional params', async () => {
const response = await client.addons.create({
currency: 'AED',
name: 'name',
price: 0,
tax_category: 'digital_products',
description: 'description',
});
});
test('retrieve', async () => {
const responsePromise = client.addons.retrieve('id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('update', async () => {
const responsePromise = client.addons.update('id', {});
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list', async () => {
const responsePromise = client.addons.list();
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list: request options and params are passed correctly', async () => {
// ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
await expect(
client.addons.list({ page_number: 0, page_size: 0 }, { path: '/_stainless_unknown_path' }),
).rejects.toThrow(DodoPayments.NotFoundError);
});
test('updateImages', async () => {
const responsePromise = client.addons.updateImages('id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
});
```
--------------------------------------------------------------------------------
/.github/workflows/docker-mcp.yml:
--------------------------------------------------------------------------------
```yaml
name: Build and Push MCP Server Docker Image
# This workflow is triggered when a GitHub release is created.
# It can also be run manually to re-publish to Docker Hub in case it failed for some reason.
# You can run this workflow by navigating to https://www.github.com/dodopayments/dodopayments-typescript/actions/workflows/docker-mcp.yml
on:
release:
types: [published]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: dodopayments/mcp
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# For OIDC token if using Docker Hub provenance
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
image=moby/buildkit:latest
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Determine Docker tags
id: tags
run: |
# Get tags from our script
TAGS=$(bash ./bin/docker-tags)
# Convert to format expected by docker/metadata-action
DOCKER_TAGS=""
while IFS= read -r tag; do
if [ -n "$DOCKER_TAGS" ]; then
DOCKER_TAGS="${DOCKER_TAGS}\n"
fi
DOCKER_TAGS="${DOCKER_TAGS}type=raw,value=${tag}"
done <<< "$TAGS"
# Output for docker/metadata-action
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo -e "$DOCKER_TAGS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Save for build summary
echo "DOCKER_TAG_LIST<<EOF" >> $GITHUB_ENV
echo "$TAGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
${{ steps.tags.outputs.tags }}
labels: |
org.opencontainers.image.title=Dodo Payments MCP Server
org.opencontainers.image.description=Model Context Protocol server for Dodo Payments API
org.opencontainers.image.vendor=Dodo Payments
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./packages/mcp-server/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
- name: Generate build summary
run: |
echo "## Docker Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Image:** \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Tags:**" >> $GITHUB_STEP_SUMMARY
echo "$DOCKER_TAG_LIST" | sed 's/^/- `/' | sed 's/$/`/' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Platforms:** linux/amd64" >> $GITHUB_STEP_SUMMARY
```
--------------------------------------------------------------------------------
/tests/api-resources/discounts.test.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: 'My Bearer Token',
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
});
describe('resource discounts', () => {
test('create: only required params', async () => {
const responsePromise = client.discounts.create({ amount: 0, type: 'percentage' });
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('create: required and optional params', async () => {
const response = await client.discounts.create({
amount: 0,
type: 'percentage',
code: 'code',
expires_at: '2019-12-27T18:11:19.117Z',
name: 'name',
restricted_to: ['string'],
subscription_cycles: 0,
usage_limit: 0,
});
});
test('retrieve', async () => {
const responsePromise = client.discounts.retrieve('discount_id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('update', async () => {
const responsePromise = client.discounts.update('discount_id', {});
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list', async () => {
const responsePromise = client.discounts.list();
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list: request options and params are passed correctly', async () => {
// ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
await expect(
client.discounts.list({ page_number: 0, page_size: 0 }, { path: '/_stainless_unknown_path' }),
).rejects.toThrow(DodoPayments.NotFoundError);
});
test('delete', async () => {
const responsePromise = client.discounts.delete('discount_id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
});
```
--------------------------------------------------------------------------------
/tests/uploads.test.ts:
--------------------------------------------------------------------------------
```typescript
import fs from 'fs';
import type { ResponseLike } from 'dodopayments/internal/to-file';
import { toFile } from 'dodopayments/core/uploads';
import { File } from 'node:buffer';
class MyClass {
name: string = 'foo';
}
function mockResponse({ url, content }: { url: string; content?: Blob }): ResponseLike {
return {
url,
blob: async () => content || new Blob([]),
};
}
describe('toFile', () => {
it('throws a helpful error for mismatched types', async () => {
await expect(
// @ts-expect-error intentionally mismatched type
toFile({ foo: 'string' }),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unexpected data type: object; constructor: Object; props: ["foo"]"`,
);
await expect(
// @ts-expect-error intentionally mismatched type
toFile(new MyClass()),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unexpected data type: object; constructor: MyClass; props: ["name"]"`,
);
});
it('disallows string at the type-level', async () => {
// @ts-expect-error we intentionally do not type support for `string`
// to help people avoid passing a file path
const file = await toFile('contents');
expect(file.text()).resolves.toEqual('contents');
});
it('extracts a file name from a Response', async () => {
const response = mockResponse({ url: 'https://example.com/my/audio.mp3' });
const file = await toFile(response);
expect(file.name).toEqual('audio.mp3');
});
it('extracts a file name from a File', async () => {
const input = new File(['foo'], 'input.jsonl');
const file = await toFile(input);
expect(file.name).toEqual('input.jsonl');
});
it('extracts a file name from a ReadStream', async () => {
const input = fs.createReadStream('tests/uploads.test.ts');
const file = await toFile(input);
expect(file.name).toEqual('uploads.test.ts');
});
it('does not copy File objects', async () => {
const input = new File(['foo'], 'input.jsonl', { type: 'jsonl' });
const file = await toFile(input);
expect(file).toBe(input);
expect(file.name).toEqual('input.jsonl');
expect(file.type).toBe('jsonl');
});
it('is assignable to File and Blob', async () => {
const input = new File(['foo'], 'input.jsonl', { type: 'jsonl' });
const result = await toFile(input);
const file: File = result;
const blob: Blob = result;
void file, blob;
});
});
describe('missing File error message', () => {
let prevGlobalFile: unknown;
let prevNodeFile: unknown;
beforeEach(() => {
// The file shim captures the global File object when it's first imported.
// Reset modules before each test so we can test the error thrown when it's undefined.
jest.resetModules();
const buffer = require('node:buffer');
// @ts-ignore
prevGlobalFile = globalThis.File;
prevNodeFile = buffer.File;
// @ts-ignore
globalThis.File = undefined;
buffer.File = undefined;
});
afterEach(() => {
// Clean up
// @ts-ignore
globalThis.File = prevGlobalFile;
require('node:buffer').File = prevNodeFile;
jest.resetModules();
});
test('is thrown', async () => {
const uploads = await import('dodopayments/core/uploads');
await expect(
uploads.toFile(mockResponse({ url: 'https://example.com/my/audio.mp3' })),
).rejects.toMatchInlineSnapshot(
`[Error: \`File\` is not defined as a global, which is required for file uploads.]`,
);
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/cloudflare-worker/src/app.ts:
--------------------------------------------------------------------------------
```typescript
import { Hono } from 'hono';
import {
layout,
homeContent,
parseApproveFormBody,
renderAuthorizationApprovedContent,
renderLoggedOutAuthorizeScreen,
renderAuthorizationRejectedContent,
} from './utils';
import type { OAuthHelpers } from '@cloudflare/workers-oauth-provider';
import { McpOptions } from 'dodopayments-mcp/server';
import { ServerConfig } from '.';
export type Bindings = Env & {
OAUTH_PROVIDER: OAuthHelpers;
};
export function makeOAuthConsent(config: ServerConfig, defaultOptions?: Partial<McpOptions>) {
const app = new Hono<{
Bindings: Bindings;
}>();
// Render a reasonable home page just to show the app is up
app.get('/', async (c) => {
const content = await homeContent(c.req.raw);
return c.html(layout(content, 'Home', config));
});
// The /authorize page has a form that will POST to /approve
app.get('/authorize', async (c) => {
const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw);
const content = await renderLoggedOutAuthorizeScreen(config, oauthReqInfo, defaultOptions);
return c.html(layout(content, 'Authorization', config));
});
// This endpoint is responsible for validating any login information and
// then completing the authorization request with the OAUTH_PROVIDER
app.post('/approve', async (c) => {
const { action, oauthReqInfo, clientProps, clientConfig } = await parseApproveFormBody(
await c.req.parseBody(),
config,
);
if (action !== 'login_approve') {
return c.html(
layout(
await renderAuthorizationRejectedContent(oauthReqInfo?.redirectUri || ''),
'Authorization Status',
config,
),
);
}
if (!oauthReqInfo || !clientProps || !clientConfig) {
return c.html('INVALID LOGIN', 401);
}
// We don't have a real user ID, just tokens, so we generate a random one
// Make this some stable ID if you want to look up the user's grants later.
const generatedUserId = crypto.randomUUID();
const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
request: oauthReqInfo,
userId: generatedUserId,
metadata: {},
scope: oauthReqInfo.scope,
props: {
clientProps,
clientConfig,
},
});
return c.html(
layout(await renderAuthorizationApprovedContent(redirectTo), 'Authorization Status', config),
);
});
// Render the authorize screen for demoing the OAuth flow (it won't actually log in)
app.get('/demo', async (c) => {
const content = await renderLoggedOutAuthorizeScreen(config, {} as any, defaultOptions);
return c.html(layout(content, 'Authorization', config));
});
// Add a resource server .well-known to point clients to the correct auth server
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': '*',
'Access-Control-Max-Age': '86400',
};
app.options('/.well-known/oauth-protected-resource', async (c) => {
Object.entries(corsHeaders).forEach(([key, value]) => c.header(key, value));
return c.body(null, 204);
});
app.get('/.well-known/oauth-protected-resource', async (c) => {
Object.entries(corsHeaders).forEach(([key, value]) => c.header(key, value));
const baseURL = new URL('/', c.req.url).toString();
return c.json({
resource: baseURL,
authorization_servers: [baseURL],
});
});
return app;
}
```
--------------------------------------------------------------------------------
/src/internal/shims.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
/**
* This module provides internal shims and utility functions for environments where certain Node.js or global types may not be available.
*
* These are used to ensure we can provide a consistent behaviour between different JavaScript environments and good error
* messages in cases where an environment isn't fully supported.
*/
import type { Fetch } from './builtin-types';
import type { ReadableStream } from './shim-types';
export function getDefaultFetch(): Fetch {
if (typeof fetch !== 'undefined') {
return fetch as any;
}
throw new Error(
'`fetch` is not defined as a global; Either pass `fetch` to the client, `new DodoPayments({ fetch })` or polyfill the global, `globalThis.fetch = fetch`',
);
}
type ReadableStreamArgs = ConstructorParameters<typeof ReadableStream>;
export function makeReadableStream(...args: ReadableStreamArgs): ReadableStream {
const ReadableStream = (globalThis as any).ReadableStream;
if (typeof ReadableStream === 'undefined') {
// Note: All of the platforms / runtimes we officially support already define
// `ReadableStream` as a global, so this should only ever be hit on unsupported runtimes.
throw new Error(
'`ReadableStream` is not defined as a global; You will need to polyfill it, `globalThis.ReadableStream = ReadableStream`',
);
}
return new ReadableStream(...args);
}
export function ReadableStreamFrom<T>(iterable: Iterable<T> | AsyncIterable<T>): ReadableStream<T> {
let iter: AsyncIterator<T> | Iterator<T> =
Symbol.asyncIterator in iterable ? iterable[Symbol.asyncIterator]() : iterable[Symbol.iterator]();
return makeReadableStream({
start() {},
async pull(controller: any) {
const { done, value } = await iter.next();
if (done) {
controller.close();
} else {
controller.enqueue(value);
}
},
async cancel() {
await iter.return?.();
},
});
}
/**
* Most browsers don't yet have async iterable support for ReadableStream,
* and Node has a very different way of reading bytes from its "ReadableStream".
*
* This polyfill was pulled from https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490
*/
export function ReadableStreamToAsyncIterable<T>(stream: any): AsyncIterableIterator<T> {
if (stream[Symbol.asyncIterator]) return stream;
const reader = stream.getReader();
return {
async next() {
try {
const result = await reader.read();
if (result?.done) reader.releaseLock(); // release lock when stream becomes closed
return result;
} catch (e) {
reader.releaseLock(); // release lock when stream becomes errored
throw e;
}
},
async return() {
const cancelPromise = reader.cancel();
reader.releaseLock();
await cancelPromise;
return { done: true, value: undefined };
},
[Symbol.asyncIterator]() {
return this;
},
};
}
/**
* Cancels a ReadableStream we don't need to consume.
* See https://undici.nodejs.org/#/?id=garbage-collection
*/
export async function CancelReadableStream(stream: any): Promise<void> {
if (stream === null || typeof stream !== 'object') return;
if (stream[Symbol.asyncIterator]) {
await stream[Symbol.asyncIterator]().return?.();
return;
}
const reader = stream.getReader();
const cancelPromise = reader.cancel();
reader.releaseLock();
await cancelPromise;
}
```
--------------------------------------------------------------------------------
/scripts/publish-packages.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Called from the `create-releases.yml` workflow with the output
* of the release please action as the first argument.
*
* Example JSON input:
*
* ```json
{
"releases_created": "true",
"release_created": "true",
"id": "137967744",
"name": "sdk: v0.14.5",
"tag_name": "sdk-v0.14.5",
"sha": "7cc2ba5c694e76a117f731d4cf0b06f8b8361f2e",
"body": "## 0.14.5 (2024-01-22)\n\n...",
"html_url": "https://github.com/$org/$repo/releases/tag/sdk-v0.14.5",
"draft": "false",
"upload_url": "https://uploads.github.com/repos/$org/$repo/releases/137967744/assets{?name,label}",
"path": ".",
"version": "0.14.5",
"major": "0",
"minor": "14",
"patch": "5",
"packages/additional-sdk--release_created": "true",
"packages/additional-sdk--id": "137967756",
"packages/additional-sdk--name": "additional-sdk: v0.5.2",
"packages/additional-sdk--tag_name": "additional-sdk-v0.5.2",
"packages/additional-sdk--sha": "7cc2ba5c694e76a117f731d4cf0b06f8b8361f2e",
"packages/additional-sdk--body": "## 0.5.2 (2024-01-22)\n\n...",
"packages/additional-sdk--html_url": "https://github.com/$org/$repo/releases/tag/additional-sdk-v0.5.2",
"packages/additional-sdk--draft": "false",
"packages/additional-sdk--upload_url": "https://uploads.github.com/repos/$org/$repo/releases/137967756/assets{?name,label}",
"packages/additional-sdk--path": "packages/additional-sdk",
"packages/additional-sdk--version": "0.5.2",
"packages/additional-sdk--major": "0",
"packages/additional-sdk--minor": "5",
"packages/additional-sdk--patch": "2",
"paths_released": "[\".\",\"packages/additional-sdk\"]"
}
```
*/
import { execSync } from 'child_process';
import path from 'path';
function main() {
const data = process.argv[2] ?? process.env['DATA'];
if (!data) {
throw new Error(`Usage: publish-packages.ts '{"json": "obj"}'`);
}
const rootDir = path.join(__dirname, '..');
console.log('root dir', rootDir);
console.log(`publish-packages called with ${data}`);
const outputs = JSON.parse(data);
const rawPaths = outputs.paths_released;
if (!rawPaths) {
console.error(JSON.stringify(outputs, null, 2));
throw new Error('Expected outputs to contain a truthy `paths_released` property');
}
if (typeof rawPaths !== 'string') {
console.error(JSON.stringify(outputs, null, 2));
throw new Error('Expected outputs `paths_released` property to be a JSON string');
}
const paths = JSON.parse(rawPaths);
if (!Array.isArray(paths)) {
console.error(JSON.stringify(outputs, null, 2));
throw new Error('Expected outputs `paths_released` property to be an array');
}
if (!paths.length) {
console.error(JSON.stringify(outputs, null, 2));
throw new Error('Expected outputs `paths_released` property to contain at least one entry');
}
const publishScriptPath = path.join(rootDir, 'bin', 'publish-npm');
console.log('Using publish script at', publishScriptPath);
console.log('Ensuring root package is built');
console.log(`$ yarn build`);
execSync(`yarn build`, { cwd: rootDir, encoding: 'utf8', stdio: 'inherit' });
for (const relPackagePath of paths) {
console.log('\n');
const packagePath = path.join(rootDir, relPackagePath);
console.log(`Publishing in directory: ${packagePath}`);
console.log(`$ yarn install`);
execSync(`yarn install`, { cwd: packagePath, encoding: 'utf8', stdio: 'inherit' });
console.log(`$ bash ${publishScriptPath}`);
execSync(`bash ${publishScriptPath}`, { cwd: packagePath, encoding: 'utf8', stdio: 'inherit' });
}
console.log('Finished publishing packages');
}
main();
```
--------------------------------------------------------------------------------
/tests/api-resources/meters.test.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: 'My Bearer Token',
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
});
describe('resource meters', () => {
test('create: only required params', async () => {
const responsePromise = client.meters.create({
aggregation: { type: 'count' },
event_name: 'event_name',
measurement_unit: 'measurement_unit',
name: 'name',
});
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('create: required and optional params', async () => {
const response = await client.meters.create({
aggregation: { type: 'count', key: 'key' },
event_name: 'event_name',
measurement_unit: 'measurement_unit',
name: 'name',
description: 'description',
filter: {
clauses: [
{ key: 'user_id', operator: 'equals', value: 'user123' },
{ key: 'amount', operator: 'greater_than', value: 100 },
],
conjunction: 'and',
},
});
});
test('retrieve', async () => {
const responsePromise = client.meters.retrieve('id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list', async () => {
const responsePromise = client.meters.list();
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list: request options and params are passed correctly', async () => {
// ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
await expect(
client.meters.list(
{ archived: true, page_number: 0, page_size: 0 },
{ path: '/_stainless_unknown_path' },
),
).rejects.toThrow(DodoPayments.NotFoundError);
});
test('archive', async () => {
const responsePromise = client.meters.archive('id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('unarchive', async () => {
const responsePromise = client.meters.unarchive('id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
});
```
--------------------------------------------------------------------------------
/src/resources/addons.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../core/resource';
import * as MiscAPI from './misc';
import { APIPromise } from '../core/api-promise';
import {
DefaultPageNumberPagination,
type DefaultPageNumberPaginationParams,
PagePromise,
} from '../core/pagination';
import { RequestOptions } from '../internal/request-options';
import { path } from '../internal/utils/path';
export class Addons extends APIResource {
create(body: AddonCreateParams, options?: RequestOptions): APIPromise<AddonResponse> {
return this._client.post('/addons', { body, ...options });
}
retrieve(id: string, options?: RequestOptions): APIPromise<AddonResponse> {
return this._client.get(path`/addons/${id}`, options);
}
update(id: string, body: AddonUpdateParams, options?: RequestOptions): APIPromise<AddonResponse> {
return this._client.patch(path`/addons/${id}`, { body, ...options });
}
list(
query: AddonListParams | null | undefined = {},
options?: RequestOptions,
): PagePromise<AddonResponsesDefaultPageNumberPagination, AddonResponse> {
return this._client.getAPIList('/addons', DefaultPageNumberPagination<AddonResponse>, {
query,
...options,
});
}
updateImages(id: string, options?: RequestOptions): APIPromise<AddonUpdateImagesResponse> {
return this._client.put(path`/addons/${id}/images`, options);
}
}
export type AddonResponsesDefaultPageNumberPagination = DefaultPageNumberPagination<AddonResponse>;
export interface AddonResponse {
/**
* id of the Addon
*/
id: string;
/**
* Unique identifier for the business to which the addon belongs.
*/
business_id: string;
/**
* Created time
*/
created_at: string;
/**
* Currency of the Addon
*/
currency: MiscAPI.Currency;
/**
* Name of the Addon
*/
name: string;
/**
* Amount of the addon
*/
price: number;
/**
* Tax category applied to this Addon
*/
tax_category: MiscAPI.TaxCategory;
/**
* Updated time
*/
updated_at: string;
/**
* Optional description of the Addon
*/
description?: string | null;
/**
* Image of the Addon
*/
image?: string | null;
}
export interface AddonUpdateImagesResponse {
image_id: string;
url: string;
}
export interface AddonCreateParams {
/**
* The currency of the Addon
*/
currency: MiscAPI.Currency;
/**
* Name of the Addon
*/
name: string;
/**
* Amount of the addon
*/
price: number;
/**
* Tax category applied to this Addon
*/
tax_category: MiscAPI.TaxCategory;
/**
* Optional description of the Addon
*/
description?: string | null;
}
export interface AddonUpdateParams {
/**
* The currency of the Addon
*/
currency?: MiscAPI.Currency | null;
/**
* Description of the Addon, optional and must be at most 1000 characters.
*/
description?: string | null;
/**
* Addon image id after its uploaded to S3
*/
image_id?: string | null;
/**
* Name of the Addon, optional and must be at most 100 characters.
*/
name?: string | null;
/**
* Amount of the addon
*/
price?: number | null;
/**
* Tax category of the Addon.
*/
tax_category?: MiscAPI.TaxCategory | null;
}
export interface AddonListParams extends DefaultPageNumberPaginationParams {}
export declare namespace Addons {
export {
type AddonResponse as AddonResponse,
type AddonUpdateImagesResponse as AddonUpdateImagesResponse,
type AddonResponsesDefaultPageNumberPagination as AddonResponsesDefaultPageNumberPagination,
type AddonCreateParams as AddonCreateParams,
type AddonUpdateParams as AddonUpdateParams,
type AddonListParams as AddonListParams,
};
}
```
--------------------------------------------------------------------------------
/tests/api-resources/payments.test.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: 'My Bearer Token',
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
});
describe('resource payments', () => {
test('create: only required params', async () => {
const responsePromise = client.payments.create({
billing: { city: 'city', country: 'AF', state: 'state', street: 'street', zipcode: 'zipcode' },
customer: { customer_id: 'customer_id' },
product_cart: [{ product_id: 'product_id', quantity: 0 }],
});
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('create: required and optional params', async () => {
const response = await client.payments.create({
billing: { city: 'city', country: 'AF', state: 'state', street: 'street', zipcode: 'zipcode' },
customer: { customer_id: 'customer_id' },
product_cart: [{ product_id: 'product_id', quantity: 0, amount: 0 }],
allowed_payment_method_types: ['credit'],
billing_currency: 'AED',
discount_code: 'discount_code',
force_3ds: true,
metadata: { foo: 'string' },
payment_link: true,
return_url: 'return_url',
show_saved_payment_methods: true,
tax_id: 'tax_id',
});
});
test('retrieve', async () => {
const responsePromise = client.payments.retrieve('payment_id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list', async () => {
const responsePromise = client.payments.list();
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
test('list: request options and params are passed correctly', async () => {
// ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
await expect(
client.payments.list(
{
brand_id: 'brand_id',
created_at_gte: '2019-12-27T18:11:19.117Z',
created_at_lte: '2019-12-27T18:11:19.117Z',
customer_id: 'customer_id',
page_number: 0,
page_size: 0,
status: 'succeeded',
subscription_id: 'subscription_id',
},
{ path: '/_stainless_unknown_path' },
),
).rejects.toThrow(DodoPayments.NotFoundError);
});
test('retrieveLineItems', async () => {
const responsePromise = client.payments.retrieveLineItems('payment_id');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/licenses/activate-licenses.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'licenses',
operation: 'write',
tags: [],
httpMethod: 'post',
httpPath: '/licenses/activate',
operationId: 'activate_license_key',
};
export const tool: Tool = {
name: 'activate_licenses',
description:
"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/license_activate_response',\n $defs: {\n license_activate_response: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'License key instance ID'\n },\n business_id: {\n type: 'string',\n description: 'Business ID'\n },\n created_at: {\n type: 'string',\n description: 'Creation timestamp',\n format: 'date-time'\n },\n customer: {\n $ref: '#/$defs/customer_limited_details'\n },\n license_key_id: {\n type: 'string',\n description: 'Associated license key ID'\n },\n name: {\n type: 'string',\n description: 'Instance name'\n },\n product: {\n type: 'object',\n description: 'Related product info. Present if the license key is tied to a product.',\n properties: {\n product_id: {\n type: 'string',\n description: 'Unique identifier for the product.'\n },\n name: {\n type: 'string',\n description: 'Name of the product, if set by the merchant.'\n }\n },\n required: [ 'product_id'\n ]\n }\n },\n required: [ 'id',\n 'business_id',\n 'created_at',\n 'customer',\n 'license_key_id',\n 'name',\n 'product'\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 }\n}\n```",
inputSchema: {
type: 'object',
properties: {
license_key: {
type: 'string',
},
name: {
type: 'string',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: ['license_key', 'name'],
},
annotations: {},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { jq_filter, ...body } = args as any;
return asTextContentResult(await maybeFilter(jq_filter, await client.licenses.activate(body)));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/src/core/error.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { castToError } from '../internal/errors';
export class DodoPaymentsError extends Error {}
export class APIError<
TStatus extends number | undefined = number | undefined,
THeaders extends Headers | undefined = Headers | undefined,
TError extends Object | undefined = Object | undefined,
> extends DodoPaymentsError {
/** HTTP status for the response that caused the error */
readonly status: TStatus;
/** HTTP headers for the response that caused the error */
readonly headers: THeaders;
/** JSON body of the response that caused the error */
readonly error: TError;
constructor(status: TStatus, error: TError, message: string | undefined, headers: THeaders) {
super(`${APIError.makeMessage(status, error, message)}`);
this.status = status;
this.headers = headers;
this.error = error;
}
private static makeMessage(status: number | undefined, error: any, message: string | undefined) {
const msg =
error?.message ?
typeof error.message === 'string' ?
error.message
: JSON.stringify(error.message)
: error ? JSON.stringify(error)
: message;
if (status && msg) {
return `${status} ${msg}`;
}
if (status) {
return `${status} status code (no body)`;
}
if (msg) {
return msg;
}
return '(no status code or body)';
}
static generate(
status: number | undefined,
errorResponse: Object | undefined,
message: string | undefined,
headers: Headers | undefined,
): APIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: castToError(errorResponse) });
}
const error = errorResponse as Record<string, any>;
if (status === 400) {
return new BadRequestError(status, error, message, headers);
}
if (status === 401) {
return new AuthenticationError(status, error, message, headers);
}
if (status === 403) {
return new PermissionDeniedError(status, error, message, headers);
}
if (status === 404) {
return new NotFoundError(status, error, message, headers);
}
if (status === 409) {
return new ConflictError(status, error, message, headers);
}
if (status === 422) {
return new UnprocessableEntityError(status, error, message, headers);
}
if (status === 429) {
return new RateLimitError(status, error, message, headers);
}
if (status >= 500) {
return new InternalServerError(status, error, message, headers);
}
return new APIError(status, error, message, headers);
}
}
export class APIUserAbortError extends APIError<undefined, undefined, undefined> {
constructor({ message }: { message?: string } = {}) {
super(undefined, undefined, message || 'Request was aborted.', undefined);
}
}
export class APIConnectionError extends APIError<undefined, undefined, undefined> {
constructor({ message, cause }: { message?: string | undefined; cause?: Error | undefined }) {
super(undefined, undefined, message || 'Connection error.', undefined);
// in some environments the 'cause' property is already declared
// @ts-ignore
if (cause) this.cause = cause;
}
}
export class APIConnectionTimeoutError extends APIConnectionError {
constructor({ message }: { message?: string } = {}) {
super({ message: message ?? 'Request timed out.' });
}
}
export class BadRequestError extends APIError<400, Headers> {}
export class AuthenticationError extends APIError<401, Headers> {}
export class PermissionDeniedError extends APIError<403, Headers> {}
export class NotFoundError extends APIError<404, Headers> {}
export class ConflictError extends APIError<409, Headers> {}
export class UnprocessableEntityError extends APIError<422, Headers> {}
export class RateLimitError extends APIError<429, Headers> {}
export class InternalServerError extends APIError<number, Headers> {}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/webhooks/list-webhooks.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'webhooks',
operation: 'read',
tags: [],
httpMethod: 'get',
httpPath: '/webhooks',
operationId: 'list_webhooks',
};
export const tool: Tool = {
name: 'list_webhooks',
description:
"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\nList all webhooks\n\n# Response Schema\n```json\n{\n type: 'object',\n properties: {\n data: {\n type: 'array',\n description: 'List of webhoooks',\n items: {\n $ref: '#/$defs/webhook_details'\n }\n },\n done: {\n type: 'boolean',\n description: 'true if no more values are to be fetched.'\n },\n iterator: {\n type: 'string',\n description: 'Cursor pointing to the next paginated object'\n },\n prev_iterator: {\n type: 'string',\n description: 'Cursor pointing to the previous paginated object'\n }\n },\n required: [ 'data',\n 'done'\n ],\n $defs: {\n webhook_details: {\n type: 'object',\n title: 'Webhook Details',\n properties: {\n id: {\n type: 'string',\n description: 'The webhook\\'s ID.'\n },\n created_at: {\n type: 'string',\n description: 'Created at timestamp'\n },\n description: {\n type: 'string',\n description: 'An example webhook name.'\n },\n metadata: {\n type: 'object',\n description: 'Metadata of the webhook',\n additionalProperties: true\n },\n updated_at: {\n type: 'string',\n description: 'Updated at timestamp'\n },\n url: {\n type: 'string',\n description: 'Url endpoint of the webhook'\n },\n disabled: {\n type: 'boolean',\n description: 'Status of the webhook.\\n\\nIf true, events are not sent'\n },\n filter_types: {\n type: 'array',\n description: 'Filter events to the webhook.\\n\\nWebhook event will only be sent for events in the list.',\n items: {\n type: 'string'\n }\n },\n rate_limit: {\n type: 'integer',\n description: 'Configured rate limit'\n }\n },\n required: [ 'id',\n 'created_at',\n 'description',\n 'metadata',\n 'updated_at',\n 'url'\n ]\n }\n }\n}\n```",
inputSchema: {
type: 'object',
properties: {
iterator: {
type: 'string',
description: 'The iterator returned from a prior invocation',
},
limit: {
type: 'integer',
description: 'Limit the number of returned items',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: [],
},
annotations: {
readOnlyHint: true,
},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { jq_filter, ...body } = args as any;
const response = await client.webhooks.list(body).asResponse();
return asTextContentResult(await maybeFilter(jq_filter, await response.json()));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/license-keys/retrieve-license-keys.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'license_keys',
operation: 'read',
tags: [],
httpMethod: 'get',
httpPath: '/license_keys/{id}',
operationId: 'get_license_key_handler',
};
export const tool: Tool = {
name: 'retrieve_license_keys',
description:
"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/license_key',\n $defs: {\n license_key: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'The unique identifier of the license key.'\n },\n business_id: {\n type: 'string',\n description: 'The unique identifier of the business associated with the license key.'\n },\n created_at: {\n type: 'string',\n description: 'The timestamp indicating when the license key was created, in UTC.',\n format: 'date-time'\n },\n customer_id: {\n type: 'string',\n description: 'The unique identifier of the customer associated with the license key.'\n },\n instances_count: {\n type: 'integer',\n description: 'The current number of instances activated for this license key.'\n },\n key: {\n type: 'string',\n description: 'The license key string.'\n },\n payment_id: {\n type: 'string',\n description: 'The unique identifier of the payment associated with the license key.'\n },\n product_id: {\n type: 'string',\n description: 'The unique identifier of the product associated with the license key.'\n },\n status: {\n $ref: '#/$defs/license_key_status'\n },\n activations_limit: {\n type: 'integer',\n description: 'The maximum number of activations allowed for this license key.'\n },\n expires_at: {\n type: 'string',\n description: 'The timestamp indicating when the license key expires, in UTC.',\n format: 'date-time'\n },\n subscription_id: {\n type: 'string',\n description: 'The unique identifier of the subscription associated with the license key, if any.'\n }\n },\n required: [ 'id',\n 'business_id',\n 'created_at',\n 'customer_id',\n 'instances_count',\n 'key',\n 'payment_id',\n 'product_id',\n 'status'\n ]\n },\n license_key_status: {\n type: 'string',\n enum: [ 'active',\n 'expired',\n 'disabled'\n ]\n }\n }\n}\n```",
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: ['id'],
},
annotations: {
readOnlyHint: true,
},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { id, jq_filter, ...body } = args as any;
return asTextContentResult(await maybeFilter(jq_filter, await client.licenseKeys.retrieve(id)));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/discounts/retrieve-discounts.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'discounts',
operation: 'read',
tags: [],
httpMethod: 'get',
httpPath: '/discounts/{discount_id}',
operationId: 'get_discount_handler',
};
export const tool: Tool = {
name: 'retrieve_discounts',
description:
"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\nGET /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```",
inputSchema: {
type: 'object',
properties: {
discount_id: {
type: 'string',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: ['discount_id'],
},
annotations: {
readOnlyHint: true,
},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { discount_id, jq_filter, ...body } = args as any;
return asTextContentResult(await maybeFilter(jq_filter, await client.discounts.retrieve(discount_id)));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/src/resources/license-keys.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../core/resource';
import { APIPromise } from '../core/api-promise';
import {
DefaultPageNumberPagination,
type DefaultPageNumberPaginationParams,
PagePromise,
} from '../core/pagination';
import { RequestOptions } from '../internal/request-options';
import { path } from '../internal/utils/path';
export class LicenseKeys extends APIResource {
/**
* @example
* ```ts
* const licenseKey = await client.licenseKeys.retrieve(
* 'lic_123',
* );
* ```
*/
retrieve(id: string, options?: RequestOptions): APIPromise<LicenseKey> {
return this._client.get(path`/license_keys/${id}`, options);
}
/**
* @example
* ```ts
* const licenseKey = await client.licenseKeys.update(
* 'lic_123',
* );
* ```
*/
update(id: string, body: LicenseKeyUpdateParams, options?: RequestOptions): APIPromise<LicenseKey> {
return this._client.patch(path`/license_keys/${id}`, { body, ...options });
}
/**
* @example
* ```ts
* // Automatically fetches more pages as needed.
* for await (const licenseKey of client.licenseKeys.list()) {
* // ...
* }
* ```
*/
list(
query: LicenseKeyListParams | null | undefined = {},
options?: RequestOptions,
): PagePromise<LicenseKeysDefaultPageNumberPagination, LicenseKey> {
return this._client.getAPIList('/license_keys', DefaultPageNumberPagination<LicenseKey>, {
query,
...options,
});
}
}
export type LicenseKeysDefaultPageNumberPagination = DefaultPageNumberPagination<LicenseKey>;
export interface LicenseKey {
/**
* The unique identifier of the license key.
*/
id: string;
/**
* The unique identifier of the business associated with the license key.
*/
business_id: string;
/**
* The timestamp indicating when the license key was created, in UTC.
*/
created_at: string;
/**
* The unique identifier of the customer associated with the license key.
*/
customer_id: string;
/**
* The current number of instances activated for this license key.
*/
instances_count: number;
/**
* The license key string.
*/
key: string;
/**
* The unique identifier of the payment associated with the license key.
*/
payment_id: string;
/**
* The unique identifier of the product associated with the license key.
*/
product_id: string;
/**
* The current status of the license key (e.g., active, inactive, expired).
*/
status: LicenseKeyStatus;
/**
* The maximum number of activations allowed for this license key.
*/
activations_limit?: number | null;
/**
* The timestamp indicating when the license key expires, in UTC.
*/
expires_at?: string | null;
/**
* The unique identifier of the subscription associated with the license key, if
* any.
*/
subscription_id?: string | null;
}
export type LicenseKeyStatus = 'active' | 'expired' | 'disabled';
export interface LicenseKeyUpdateParams {
/**
* The updated activation limit for the license key. Use `null` to remove the
* limit, or omit this field to leave it unchanged.
*/
activations_limit?: number | null;
/**
* Indicates whether the license key should be disabled. A value of `true` disables
* the key, while `false` enables it. Omit this field to leave it unchanged.
*/
disabled?: boolean | null;
/**
* The updated expiration timestamp for the license key in UTC. Use `null` to
* remove the expiration date, or omit this field to leave it unchanged.
*/
expires_at?: string | null;
}
export interface LicenseKeyListParams extends DefaultPageNumberPaginationParams {
/**
* Filter by customer ID
*/
customer_id?: string;
/**
* Filter by product ID
*/
product_id?: string;
/**
* Filter by license key status
*/
status?: 'active' | 'expired' | 'disabled';
}
export declare namespace LicenseKeys {
export {
type LicenseKey as LicenseKey,
type LicenseKeyStatus as LicenseKeyStatus,
type LicenseKeysDefaultPageNumberPagination as LicenseKeysDefaultPageNumberPagination,
type LicenseKeyUpdateParams as LicenseKeyUpdateParams,
type LicenseKeyListParams as LicenseKeyListParams,
};
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/usage-events/ingest-usage-events.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'usage_events',
operation: 'write',
tags: [],
httpMethod: 'post',
httpPath: '/events/ingest',
operationId: 'ingest_events',
};
export const tool: Tool = {
name: 'ingest_usage_events',
description:
'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\nThis endpoint allows you to ingest custom events that can be used for:\n- Usage-based billing and metering\n- Analytics and reporting\n- Customer behavior tracking\n\n## Important Notes:\n- **Duplicate Prevention**:\n - Duplicate `event_id` values within the same request are rejected (entire request fails)\n - Subsequent requests with existing `event_id` values are ignored (idempotent behavior)\n- **Rate Limiting**: Maximum 1000 events per request\n- **Time Validation**: Events with timestamps older than 1 hour or more than 5 minutes in the future will be rejected\n- **Metadata Limits**: Maximum 50 key-value pairs per event, keys max 100 chars, values max 500 chars\n\n## Example Usage:\n```json\n{\n "events": [\n {\n "event_id": "api_call_12345",\n "customer_id": "cus_abc123",\n "event_name": "api_request",\n "timestamp": "2024-01-15T10:30:00Z",\n "metadata": {\n "endpoint": "/api/v1/users",\n "method": "GET",\n "tokens_used": "150"\n }\n }\n ]\n}\n```\n\n# Response Schema\n```json\n{\n $ref: \'#/$defs/usage_event_ingest_response\',\n $defs: {\n usage_event_ingest_response: {\n type: \'object\',\n properties: {\n ingested_count: {\n type: \'integer\'\n }\n },\n required: [ \'ingested_count\'\n ]\n }\n }\n}\n```',
inputSchema: {
type: 'object',
properties: {
events: {
type: 'array',
description: 'List of events to be pushed',
items: {
$ref: '#/$defs/event_input',
},
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: ['events'],
$defs: {
event_input: {
type: 'object',
properties: {
customer_id: {
type: 'string',
description: 'customer_id of the customer whose usage needs to be tracked',
},
event_id: {
type: 'string',
description:
'Event Id acts as an idempotency key. Any subsequent requests with the same event_id will be ignored',
},
event_name: {
type: 'string',
description: 'Name of the event',
},
metadata: {
type: 'object',
title: 'EventMetadata',
description:
'Custom metadata. Only key value pairs are accepted, objects or arrays submitted will be rejected.',
additionalProperties: true,
},
timestamp: {
type: 'string',
description:
'Custom Timestamp. Defaults to current timestamp in UTC.\nTimestamps that are older that 1 hour or after 5 mins, from current timestamp, will be rejected.',
format: 'date-time',
},
},
required: ['customer_id', 'event_id', 'event_name'],
},
},
},
annotations: {},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { jq_filter, ...body } = args as any;
return asTextContentResult(await maybeFilter(jq_filter, await client.usageEvents.ingest(body)));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/discounts/list-discounts.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'discounts',
operation: 'read',
tags: [],
httpMethod: 'get',
httpPath: '/discounts',
operationId: 'list_discounts_handler',
};
export const tool: Tool = {
name: 'list_discounts',
description:
"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\nGET /discounts\n\n# Response Schema\n```json\n{\n type: 'object',\n properties: {\n items: {\n type: 'array',\n description: 'Array of active (non-deleted) discounts for the current page.',\n items: {\n $ref: '#/$defs/discount'\n }\n }\n },\n required: [ 'items'\n ],\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```",
inputSchema: {
type: 'object',
properties: {
page_number: {
type: 'integer',
description: 'Page number (default = 0).',
},
page_size: {
type: 'integer',
description: 'Page size (default = 10, max = 100).',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: [],
},
annotations: {
readOnlyHint: true,
},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { jq_filter, ...body } = args as any;
const response = await client.discounts.list(body).asResponse();
return asTextContentResult(await maybeFilter(jq_filter, await response.json()));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/disputes/retrieve-disputes.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'disputes',
operation: 'read',
tags: [],
httpMethod: 'get',
httpPath: '/disputes/{dispute_id}',
operationId: 'get_dispute_handler',
};
export const tool: Tool = {
name: 'retrieve_disputes',
description:
"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/get_dispute',\n $defs: {\n get_dispute: {\n type: 'object',\n properties: {\n amount: {\n type: 'string',\n description: 'The amount involved in the dispute, represented as a string to accommodate precision.'\n },\n business_id: {\n type: 'string',\n description: 'The unique identifier of the business involved in the dispute.'\n },\n created_at: {\n type: 'string',\n description: 'The timestamp of when the dispute was created, in UTC.',\n format: 'date-time'\n },\n currency: {\n type: 'string',\n description: 'The currency of the disputed amount, represented as an ISO 4217 currency code.'\n },\n customer: {\n $ref: '#/$defs/customer_limited_details'\n },\n dispute_id: {\n type: 'string',\n description: 'The unique identifier of the dispute.'\n },\n dispute_stage: {\n $ref: '#/$defs/dispute_stage'\n },\n dispute_status: {\n $ref: '#/$defs/dispute_status'\n },\n payment_id: {\n type: 'string',\n description: 'The unique identifier of the payment associated with the dispute.'\n },\n reason: {\n type: 'string',\n description: 'Reason for the dispute'\n },\n remarks: {\n type: 'string',\n description: 'Remarks'\n }\n },\n required: [ 'amount',\n 'business_id',\n 'created_at',\n 'currency',\n 'customer',\n 'dispute_id',\n 'dispute_stage',\n 'dispute_status',\n 'payment_id'\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 dispute_stage: {\n type: 'string',\n enum: [ 'pre_dispute',\n 'dispute',\n 'pre_arbitration'\n ]\n },\n dispute_status: {\n type: 'string',\n enum: [ 'dispute_opened',\n 'dispute_expired',\n 'dispute_accepted',\n 'dispute_cancelled',\n 'dispute_challenged',\n 'dispute_won',\n 'dispute_lost'\n ]\n }\n }\n}\n```",
inputSchema: {
type: 'object',
properties: {
dispute_id: {
type: 'string',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: ['dispute_id'],
},
annotations: {
readOnlyHint: true,
},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { dispute_id, jq_filter, ...body } = args as any;
return asTextContentResult(await maybeFilter(jq_filter, await client.disputes.retrieve(dispute_id)));
};
export default { metadata, tool, handler };
```
--------------------------------------------------------------------------------
/src/resources/refunds.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../core/resource';
import * as MiscAPI from './misc';
import * as PaymentsAPI from './payments';
import { APIPromise } from '../core/api-promise';
import {
DefaultPageNumberPagination,
type DefaultPageNumberPaginationParams,
PagePromise,
} from '../core/pagination';
import { RequestOptions } from '../internal/request-options';
import { path } from '../internal/utils/path';
export class Refunds extends APIResource {
create(body: RefundCreateParams, options?: RequestOptions): APIPromise<Refund> {
return this._client.post('/refunds', { body, ...options });
}
retrieve(refundID: string, options?: RequestOptions): APIPromise<Refund> {
return this._client.get(path`/refunds/${refundID}`, options);
}
list(
query: RefundListParams | null | undefined = {},
options?: RequestOptions,
): PagePromise<RefundListResponsesDefaultPageNumberPagination, RefundListResponse> {
return this._client.getAPIList('/refunds', DefaultPageNumberPagination<RefundListResponse>, {
query,
...options,
});
}
}
export type RefundListResponsesDefaultPageNumberPagination = DefaultPageNumberPagination<RefundListResponse>;
export interface Refund {
/**
* The unique identifier of the business issuing the refund.
*/
business_id: string;
/**
* The timestamp of when the refund was created in UTC.
*/
created_at: string;
/**
* Details about the customer for this refund (from the associated payment)
*/
customer: PaymentsAPI.CustomerLimitedDetails;
/**
* If true the refund is a partial refund
*/
is_partial: boolean;
/**
* The unique identifier of the payment associated with the refund.
*/
payment_id: string;
/**
* The unique identifier of the refund.
*/
refund_id: string;
/**
* The current status of the refund.
*/
status: RefundStatus;
/**
* The refunded amount.
*/
amount?: number | null;
/**
* The currency of the refund, represented as an ISO 4217 currency code.
*/
currency?: MiscAPI.Currency | null;
/**
* The reason provided for the refund, if any. Optional.
*/
reason?: string | null;
}
export type RefundStatus = 'succeeded' | 'failed' | 'pending' | 'review';
export interface RefundListResponse {
/**
* The unique identifier of the business issuing the refund.
*/
business_id: string;
/**
* The timestamp of when the refund was created in UTC.
*/
created_at: string;
/**
* If true the refund is a partial refund
*/
is_partial: boolean;
/**
* The unique identifier of the payment associated with the refund.
*/
payment_id: string;
/**
* The unique identifier of the refund.
*/
refund_id: string;
/**
* The current status of the refund.
*/
status: RefundStatus;
/**
* The refunded amount.
*/
amount?: number | null;
/**
* The currency of the refund, represented as an ISO 4217 currency code.
*/
currency?: MiscAPI.Currency | null;
/**
* The reason provided for the refund, if any. Optional.
*/
reason?: string | null;
}
export interface RefundCreateParams {
/**
* The unique identifier of the payment to be refunded.
*/
payment_id: string;
/**
* Partially Refund an Individual Item
*/
items?: Array<RefundCreateParams.Item> | null;
/**
* The reason for the refund, if any. Maximum length is 3000 characters. Optional.
*/
reason?: string | null;
}
export namespace RefundCreateParams {
export interface Item {
/**
* The id of the item (i.e. `product_id` or `addon_id`)
*/
item_id: string;
/**
* The amount to refund. if None the whole item is refunded
*/
amount?: number | null;
/**
* Specify if tax is inclusive of the refund. Default true.
*/
tax_inclusive?: boolean;
}
}
export interface RefundListParams extends DefaultPageNumberPaginationParams {
/**
* Get events after this created time
*/
created_at_gte?: string;
/**
* Get events created before this time
*/
created_at_lte?: string;
/**
* Filter by customer_id
*/
customer_id?: string;
/**
* Filter by status
*/
status?: 'succeeded' | 'failed' | 'pending' | 'review';
}
export declare namespace Refunds {
export {
type Refund as Refund,
type RefundStatus as RefundStatus,
type RefundListResponse as RefundListResponse,
type RefundListResponsesDefaultPageNumberPagination as RefundListResponsesDefaultPageNumberPagination,
type RefundCreateParams as RefundCreateParams,
type RefundListParams as RefundListParams,
};
}
```
--------------------------------------------------------------------------------
/src/resources/misc.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../core/resource';
import { APIPromise } from '../core/api-promise';
import { RequestOptions } from '../internal/request-options';
export class Misc extends APIResource {
listSupportedCountries(options?: RequestOptions): APIPromise<MiscListSupportedCountriesResponse> {
return this._client.get('/checkout/supported_countries', options);
}
}
/**
* ISO country code alpha2 variant
*/
export type CountryCode =
| 'AF'
| 'AX'
| 'AL'
| 'DZ'
| 'AS'
| 'AD'
| 'AO'
| 'AI'
| 'AQ'
| 'AG'
| 'AR'
| 'AM'
| 'AW'
| 'AU'
| 'AT'
| 'AZ'
| 'BS'
| 'BH'
| 'BD'
| 'BB'
| 'BY'
| 'BE'
| 'BZ'
| 'BJ'
| 'BM'
| 'BT'
| 'BO'
| 'BQ'
| 'BA'
| 'BW'
| 'BV'
| 'BR'
| 'IO'
| 'BN'
| 'BG'
| 'BF'
| 'BI'
| 'KH'
| 'CM'
| 'CA'
| 'CV'
| 'KY'
| 'CF'
| 'TD'
| 'CL'
| 'CN'
| 'CX'
| 'CC'
| 'CO'
| 'KM'
| 'CG'
| 'CD'
| 'CK'
| 'CR'
| 'CI'
| 'HR'
| 'CU'
| 'CW'
| 'CY'
| 'CZ'
| 'DK'
| 'DJ'
| 'DM'
| 'DO'
| 'EC'
| 'EG'
| 'SV'
| 'GQ'
| 'ER'
| 'EE'
| 'ET'
| 'FK'
| 'FO'
| 'FJ'
| 'FI'
| 'FR'
| 'GF'
| 'PF'
| 'TF'
| 'GA'
| 'GM'
| 'GE'
| 'DE'
| 'GH'
| 'GI'
| 'GR'
| 'GL'
| 'GD'
| 'GP'
| 'GU'
| 'GT'
| 'GG'
| 'GN'
| 'GW'
| 'GY'
| 'HT'
| 'HM'
| 'VA'
| 'HN'
| 'HK'
| 'HU'
| 'IS'
| 'IN'
| 'ID'
| 'IR'
| 'IQ'
| 'IE'
| 'IM'
| 'IL'
| 'IT'
| 'JM'
| 'JP'
| 'JE'
| 'JO'
| 'KZ'
| 'KE'
| 'KI'
| 'KP'
| 'KR'
| 'KW'
| 'KG'
| 'LA'
| 'LV'
| 'LB'
| 'LS'
| 'LR'
| 'LY'
| 'LI'
| 'LT'
| 'LU'
| 'MO'
| 'MK'
| 'MG'
| 'MW'
| 'MY'
| 'MV'
| 'ML'
| 'MT'
| 'MH'
| 'MQ'
| 'MR'
| 'MU'
| 'YT'
| 'MX'
| 'FM'
| 'MD'
| 'MC'
| 'MN'
| 'ME'
| 'MS'
| 'MA'
| 'MZ'
| 'MM'
| 'NA'
| 'NR'
| 'NP'
| 'NL'
| 'NC'
| 'NZ'
| 'NI'
| 'NE'
| 'NG'
| 'NU'
| 'NF'
| 'MP'
| 'NO'
| 'OM'
| 'PK'
| 'PW'
| 'PS'
| 'PA'
| 'PG'
| 'PY'
| 'PE'
| 'PH'
| 'PN'
| 'PL'
| 'PT'
| 'PR'
| 'QA'
| 'RE'
| 'RO'
| 'RU'
| 'RW'
| 'BL'
| 'SH'
| 'KN'
| 'LC'
| 'MF'
| 'PM'
| 'VC'
| 'WS'
| 'SM'
| 'ST'
| 'SA'
| 'SN'
| 'RS'
| 'SC'
| 'SL'
| 'SG'
| 'SX'
| 'SK'
| 'SI'
| 'SB'
| 'SO'
| 'ZA'
| 'GS'
| 'SS'
| 'ES'
| 'LK'
| 'SD'
| 'SR'
| 'SJ'
| 'SZ'
| 'SE'
| 'CH'
| 'SY'
| 'TW'
| 'TJ'
| 'TZ'
| 'TH'
| 'TL'
| 'TG'
| 'TK'
| 'TO'
| 'TT'
| 'TN'
| 'TR'
| 'TM'
| 'TC'
| 'TV'
| 'UG'
| 'UA'
| 'AE'
| 'GB'
| 'UM'
| 'US'
| 'UY'
| 'UZ'
| 'VU'
| 'VE'
| 'VN'
| 'VG'
| 'VI'
| 'WF'
| 'EH'
| 'YE'
| 'ZM'
| 'ZW';
export type Currency =
| 'AED'
| 'ALL'
| 'AMD'
| 'ANG'
| 'AOA'
| 'ARS'
| 'AUD'
| 'AWG'
| 'AZN'
| 'BAM'
| 'BBD'
| 'BDT'
| 'BGN'
| 'BHD'
| 'BIF'
| 'BMD'
| 'BND'
| 'BOB'
| 'BRL'
| 'BSD'
| 'BWP'
| 'BYN'
| 'BZD'
| 'CAD'
| 'CHF'
| 'CLP'
| 'CNY'
| 'COP'
| 'CRC'
| 'CUP'
| 'CVE'
| 'CZK'
| 'DJF'
| 'DKK'
| 'DOP'
| 'DZD'
| 'EGP'
| 'ETB'
| 'EUR'
| 'FJD'
| 'FKP'
| 'GBP'
| 'GEL'
| 'GHS'
| 'GIP'
| 'GMD'
| 'GNF'
| 'GTQ'
| 'GYD'
| 'HKD'
| 'HNL'
| 'HRK'
| 'HTG'
| 'HUF'
| 'IDR'
| 'ILS'
| 'INR'
| 'IQD'
| 'JMD'
| 'JOD'
| 'JPY'
| 'KES'
| 'KGS'
| 'KHR'
| 'KMF'
| 'KRW'
| 'KWD'
| 'KYD'
| 'KZT'
| 'LAK'
| 'LBP'
| 'LKR'
| 'LRD'
| 'LSL'
| 'LYD'
| 'MAD'
| 'MDL'
| 'MGA'
| 'MKD'
| 'MMK'
| 'MNT'
| 'MOP'
| 'MRU'
| 'MUR'
| 'MVR'
| 'MWK'
| 'MXN'
| 'MYR'
| 'MZN'
| 'NAD'
| 'NGN'
| 'NIO'
| 'NOK'
| 'NPR'
| 'NZD'
| 'OMR'
| 'PAB'
| 'PEN'
| 'PGK'
| 'PHP'
| 'PKR'
| 'PLN'
| 'PYG'
| 'QAR'
| 'RON'
| 'RSD'
| 'RUB'
| 'RWF'
| 'SAR'
| 'SBD'
| 'SCR'
| 'SEK'
| 'SGD'
| 'SHP'
| 'SLE'
| 'SLL'
| 'SOS'
| 'SRD'
| 'SSP'
| 'STN'
| 'SVC'
| 'SZL'
| 'THB'
| 'TND'
| 'TOP'
| 'TRY'
| 'TTD'
| 'TWD'
| 'TZS'
| 'UAH'
| 'UGX'
| 'USD'
| 'UYU'
| 'UZS'
| 'VES'
| 'VND'
| 'VUV'
| 'WST'
| 'XAF'
| 'XCD'
| 'XOF'
| 'XPF'
| 'YER'
| 'ZAR'
| 'ZMW';
/**
* Represents the different categories of taxation applicable to various products
* and services.
*/
export type TaxCategory = 'digital_products' | 'saas' | 'e_book' | 'edtech';
export type MiscListSupportedCountriesResponse = Array<CountryCode>;
export declare namespace Misc {
export {
type CountryCode as CountryCode,
type Currency as Currency,
type TaxCategory as TaxCategory,
type MiscListSupportedCountriesResponse as MiscListSupportedCountriesResponse,
};
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/license-keys/update-license-keys.ts:
--------------------------------------------------------------------------------
```typescript
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { maybeFilter } from 'dodopayments-mcp/filtering';
import { Metadata, asTextContentResult } from 'dodopayments-mcp/tools/types';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import DodoPayments from 'dodopayments';
export const metadata: Metadata = {
resource: 'license_keys',
operation: 'write',
tags: [],
httpMethod: 'patch',
httpPath: '/license_keys/{id}',
operationId: 'update_license_key',
};
export const tool: Tool = {
name: 'update_license_keys',
description:
"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/license_key',\n $defs: {\n license_key: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'The unique identifier of the license key.'\n },\n business_id: {\n type: 'string',\n description: 'The unique identifier of the business associated with the license key.'\n },\n created_at: {\n type: 'string',\n description: 'The timestamp indicating when the license key was created, in UTC.',\n format: 'date-time'\n },\n customer_id: {\n type: 'string',\n description: 'The unique identifier of the customer associated with the license key.'\n },\n instances_count: {\n type: 'integer',\n description: 'The current number of instances activated for this license key.'\n },\n key: {\n type: 'string',\n description: 'The license key string.'\n },\n payment_id: {\n type: 'string',\n description: 'The unique identifier of the payment associated with the license key.'\n },\n product_id: {\n type: 'string',\n description: 'The unique identifier of the product associated with the license key.'\n },\n status: {\n $ref: '#/$defs/license_key_status'\n },\n activations_limit: {\n type: 'integer',\n description: 'The maximum number of activations allowed for this license key.'\n },\n expires_at: {\n type: 'string',\n description: 'The timestamp indicating when the license key expires, in UTC.',\n format: 'date-time'\n },\n subscription_id: {\n type: 'string',\n description: 'The unique identifier of the subscription associated with the license key, if any.'\n }\n },\n required: [ 'id',\n 'business_id',\n 'created_at',\n 'customer_id',\n 'instances_count',\n 'key',\n 'payment_id',\n 'product_id',\n 'status'\n ]\n },\n license_key_status: {\n type: 'string',\n enum: [ 'active',\n 'expired',\n 'disabled'\n ]\n }\n }\n}\n```",
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
},
activations_limit: {
type: 'integer',
description:
'The updated activation limit for the license key.\nUse `null` to remove the limit, or omit this field to leave it unchanged.',
},
disabled: {
type: 'boolean',
description:
'Indicates whether the license key should be disabled.\nA value of `true` disables the key, while `false` enables it. Omit this field to leave it unchanged.',
},
expires_at: {
type: 'string',
description:
'The updated expiration timestamp for the license key in UTC.\nUse `null` to remove the expiration date, or omit this field to leave it unchanged.',
format: 'date-time',
},
jq_filter: {
type: 'string',
title: 'jq Filter',
description:
'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/).',
},
},
required: ['id'],
},
annotations: {},
};
export const handler = async (client: DodoPayments, args: Record<string, unknown> | undefined) => {
const { id, jq_filter, ...body } = args as any;
return asTextContentResult(await maybeFilter(jq_filter, await client.licenseKeys.update(id, body)));
};
export default { metadata, tool, handler };
```