This is page 3 of 4. Use http://codebase.md/stripe/agent-toolkit?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ └── workflows │ ├── main.yml │ ├── npm_agent_toolkit_release.yml │ ├── npm_mcp_release.yml │ └── pypi_release.yml ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── evals │ ├── .env.example │ ├── .gitignore │ ├── braintrust_openai.ts │ ├── cases.ts │ ├── eval.ts │ ├── package.json │ ├── pnpm-lock.yaml │ ├── README.md │ ├── scorer.ts │ └── tsconfig.json ├── gemini-extension.json ├── LICENSE ├── modelcontextprotocol │ ├── .dxtignore │ ├── .gitignore │ ├── .node-version │ ├── .prettierrc │ ├── build-dxt.js │ ├── Dockerfile │ ├── eslint.config.mjs │ ├── jest.config.ts │ ├── manifest.json │ ├── package.json │ ├── pnpm-lock.yaml │ ├── README.md │ ├── src │ │ ├── index.ts │ │ └── test │ │ └── index.test.ts │ ├── stripe_icon.png │ └── tsconfig.json ├── python │ ├── .editorconfig │ ├── .flake8 │ ├── examples │ │ ├── crewai │ │ │ ├── .env.template │ │ │ ├── main.py │ │ │ └── README.md │ │ ├── langchain │ │ │ ├── __init__.py │ │ │ ├── .env.template │ │ │ ├── main.py │ │ │ └── README.md │ │ ├── openai │ │ │ ├── .env.template │ │ │ ├── customer_support │ │ │ │ ├── .env.template │ │ │ │ ├── emailer.py │ │ │ │ ├── env.py │ │ │ │ ├── main.py │ │ │ │ ├── pyproject.toml │ │ │ │ ├── README.md │ │ │ │ ├── repl.py │ │ │ │ └── support_agent.py │ │ │ ├── file_search │ │ │ │ ├── main.py │ │ │ │ └── README.md │ │ │ └── web_search │ │ │ ├── .env.template │ │ │ ├── main.py │ │ │ └── README.md │ │ └── strands │ │ └── main.py │ ├── Makefile │ ├── pyproject.toml │ ├── README.md │ ├── requirements.txt │ ├── stripe_agent_toolkit │ │ ├── __init__.py │ │ ├── api.py │ │ ├── configuration.py │ │ ├── crewai │ │ │ ├── tool.py │ │ │ └── toolkit.py │ │ ├── functions.py │ │ ├── langchain │ │ │ ├── tool.py │ │ │ └── toolkit.py │ │ ├── openai │ │ │ ├── hooks.py │ │ │ ├── tool.py │ │ │ └── toolkit.py │ │ ├── prompts.py │ │ ├── schema.py │ │ ├── strands │ │ │ ├── __init__.py │ │ │ ├── hooks.py │ │ │ ├── tool.py │ │ │ └── toolkit.py │ │ └── tools.py │ └── tests │ ├── __init__.py │ ├── test_configuration.py │ └── test_functions.py ├── README.md ├── SECURITY.md └── typescript ├── .gitignore ├── .prettierrc ├── eslint.config.mjs ├── examples │ ├── ai-sdk │ │ ├── .env.template │ │ ├── index.ts │ │ ├── package.json │ │ ├── README.md │ │ └── tsconfig.json │ ├── cloudflare │ │ ├── .dev.vars.example │ │ ├── .gitignore │ │ ├── biome.json │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app.ts │ │ │ ├── imageGenerator.ts │ │ │ ├── index.ts │ │ │ ├── oauth.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── langchain │ │ ├── .env.template │ │ ├── index.ts │ │ ├── package.json │ │ ├── README.md │ │ └── tsconfig.json │ └── openai │ ├── .env.template │ ├── index.ts │ ├── package.json │ ├── README.md │ └── tsconfig.json ├── jest.config.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── src │ ├── ai-sdk │ │ ├── index.ts │ │ ├── tool.ts │ │ └── toolkit.ts │ ├── cloudflare │ │ ├── index.ts │ │ └── README.md │ ├── langchain │ │ ├── index.ts │ │ ├── tool.ts │ │ └── toolkit.ts │ ├── modelcontextprotocol │ │ ├── index.ts │ │ ├── README.md │ │ ├── register-paid-tool.ts │ │ └── toolkit.ts │ ├── openai │ │ ├── index.ts │ │ └── toolkit.ts │ ├── shared │ │ ├── api.ts │ │ ├── balance │ │ │ └── retrieveBalance.ts │ │ ├── configuration.ts │ │ ├── coupons │ │ │ ├── createCoupon.ts │ │ │ └── listCoupons.ts │ │ ├── customers │ │ │ ├── createCustomer.ts │ │ │ └── listCustomers.ts │ │ ├── disputes │ │ │ ├── listDisputes.ts │ │ │ └── updateDispute.ts │ │ ├── documentation │ │ │ └── searchDocumentation.ts │ │ ├── invoiceItems │ │ │ └── createInvoiceItem.ts │ │ ├── invoices │ │ │ ├── createInvoice.ts │ │ │ ├── finalizeInvoice.ts │ │ │ └── listInvoices.ts │ │ ├── paymentIntents │ │ │ └── listPaymentIntents.ts │ │ ├── paymentLinks │ │ │ └── createPaymentLink.ts │ │ ├── prices │ │ │ ├── createPrice.ts │ │ │ └── listPrices.ts │ │ ├── products │ │ │ ├── createProduct.ts │ │ │ └── listProducts.ts │ │ ├── refunds │ │ │ └── createRefund.ts │ │ ├── subscriptions │ │ │ ├── cancelSubscription.ts │ │ │ ├── listSubscriptions.ts │ │ │ └── updateSubscription.ts │ │ └── tools.ts │ └── test │ ├── modelcontextprotocol │ │ └── register-paid-tool.test.ts │ └── shared │ ├── balance │ │ ├── functions.test.ts │ │ └── parameters.test.ts │ ├── configuration.test.ts │ ├── customers │ │ ├── functions.test.ts │ │ └── parameters.test.ts │ ├── disputes │ │ └── functions.test.ts │ ├── documentation │ │ ├── functions.test.ts │ │ └── parameters.test.ts │ ├── invoiceItems │ │ ├── functions.test.ts │ │ ├── parameters.test.ts │ │ └── prompts.test.ts │ ├── invoices │ │ ├── functions.test.ts │ │ ├── parameters.test.ts │ │ └── prompts.test.ts │ ├── paymentIntents │ │ ├── functions.test.ts │ │ ├── parameters.test.ts │ │ └── prompts.test.ts │ ├── paymentLinks │ │ ├── functions.test.ts │ │ ├── parameters.test.ts │ │ └── prompts.test.ts │ ├── prices │ │ ├── functions.test.ts │ │ └── parameters.test.ts │ ├── products │ │ ├── functions.test.ts │ │ └── parameters.test.ts │ ├── refunds │ │ ├── functions.test.ts │ │ └── parameters.test.ts │ └── subscriptions │ ├── functions.test.ts │ ├── parameters.test.ts │ └── prompts.test.ts ├── tsconfig.json └── tsup.config.ts ``` # Files -------------------------------------------------------------------------------- /typescript/src/test/shared/subscriptions/functions.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {listSubscriptions} from '@/shared/subscriptions/listSubscriptions'; 2 | import {cancelSubscription} from '@/shared/subscriptions/cancelSubscription'; 3 | import {updateSubscription} from '@/shared/subscriptions/updateSubscription'; 4 | 5 | const Stripe = jest.fn().mockImplementation(() => ({ 6 | subscriptions: { 7 | list: jest.fn(), 8 | cancel: jest.fn(), 9 | update: jest.fn(), 10 | }, 11 | })); 12 | 13 | let stripe: ReturnType<typeof Stripe>; 14 | 15 | beforeEach(() => { 16 | stripe = new Stripe('fake-api-key'); 17 | }); 18 | 19 | describe('listSubscriptions', () => { 20 | it('should list subscriptions and return data', async () => { 21 | const mockSubscriptions = [ 22 | { 23 | id: 'sub_123456', 24 | customer: 'cus_123456', 25 | status: 'active', 26 | current_period_start: 1609459200, // 2021-01-01 27 | current_period_end: 1612137600, // 2021-02-01 28 | items: { 29 | data: [ 30 | { 31 | id: 'si_123', 32 | price: 'price_123', 33 | quantity: 1, 34 | }, 35 | ], 36 | }, 37 | }, 38 | { 39 | id: 'sub_789012', 40 | customer: 'cus_123456', 41 | status: 'canceled', 42 | current_period_start: 1609459200, // 2021-01-01 43 | current_period_end: 1612137600, // 2021-02-01 44 | items: { 45 | data: [ 46 | { 47 | id: 'si_456', 48 | price: 'price_456', 49 | quantity: 2, 50 | }, 51 | ], 52 | }, 53 | }, 54 | ]; 55 | 56 | const context = {}; 57 | const params = {}; 58 | 59 | stripe.subscriptions.list.mockResolvedValue({data: mockSubscriptions}); 60 | const result = await listSubscriptions(stripe, context, params); 61 | 62 | expect(stripe.subscriptions.list).toHaveBeenCalledWith(params, undefined); 63 | expect(result).toEqual(mockSubscriptions); 64 | }); 65 | 66 | it('should add customer from context if provided', async () => { 67 | const mockSubscriptions = [ 68 | { 69 | id: 'sub_123456', 70 | customer: 'cus_123456', 71 | status: 'active', 72 | current_period_start: 1609459200, 73 | current_period_end: 1612137600, 74 | items: { 75 | data: [ 76 | { 77 | id: 'si_123', 78 | price: 'price_123', 79 | quantity: 1, 80 | }, 81 | ], 82 | }, 83 | }, 84 | ]; 85 | 86 | const context = { 87 | customer: 'cus_123456', 88 | }; 89 | const params = {}; 90 | 91 | stripe.subscriptions.list.mockResolvedValue({data: mockSubscriptions}); 92 | const result = await listSubscriptions(stripe, context, params); 93 | 94 | expect(stripe.subscriptions.list).toHaveBeenCalledWith( 95 | {customer: 'cus_123456'}, 96 | undefined 97 | ); 98 | expect(result).toEqual(mockSubscriptions); 99 | }); 100 | 101 | it('should specify the connected account if included in context', async () => { 102 | const mockSubscriptions = [ 103 | { 104 | id: 'sub_123456', 105 | customer: 'cus_123456', 106 | status: 'active', 107 | current_period_start: 1609459200, 108 | current_period_end: 1612137600, 109 | items: { 110 | data: [ 111 | { 112 | id: 'si_123', 113 | price: 'price_123', 114 | quantity: 1, 115 | }, 116 | ], 117 | }, 118 | }, 119 | ]; 120 | 121 | const context = { 122 | account: 'acct_123456', 123 | }; 124 | const params = {}; 125 | 126 | stripe.subscriptions.list.mockResolvedValue({data: mockSubscriptions}); 127 | const result = await listSubscriptions(stripe, context, params); 128 | 129 | expect(stripe.subscriptions.list).toHaveBeenCalledWith(params, { 130 | stripeAccount: context.account, 131 | }); 132 | expect(result).toEqual(mockSubscriptions); 133 | }); 134 | 135 | it('should handle errors gracefully', async () => { 136 | const context = {}; 137 | const params = {}; 138 | 139 | stripe.subscriptions.list.mockRejectedValue(new Error('API Error')); 140 | const result = await listSubscriptions(stripe, context, params); 141 | 142 | expect(result).toBe('Failed to list subscriptions'); 143 | }); 144 | }); 145 | 146 | describe('cancelSubscription', () => { 147 | it('should cancel a subscription and return the result', async () => { 148 | const mockSubscription = { 149 | id: 'sub_123456', 150 | customer: 'cus_123456', 151 | status: 'active', 152 | current_period_start: 1609459200, 153 | current_period_end: 1612137600, 154 | items: { 155 | data: [ 156 | { 157 | id: 'si_123', 158 | price: 'price_123', 159 | quantity: 1, 160 | }, 161 | ], 162 | }, 163 | }; 164 | 165 | const context = {}; 166 | const params = { 167 | subscription: 'sub_123456', 168 | }; 169 | 170 | stripe.subscriptions.cancel.mockResolvedValue(mockSubscription); 171 | const result = await cancelSubscription(stripe, context, params); 172 | 173 | expect(stripe.subscriptions.cancel).toHaveBeenCalledWith( 174 | 'sub_123456', 175 | {}, 176 | undefined 177 | ); 178 | expect(result).toEqual(mockSubscription); 179 | }); 180 | 181 | it('should handle errors gracefully', async () => { 182 | const context = {}; 183 | const params = { 184 | subscription: 'sub_123456', 185 | }; 186 | 187 | stripe.subscriptions.cancel.mockRejectedValue(new Error('API Error')); 188 | const result = await cancelSubscription(stripe, context, params); 189 | 190 | expect(result).toBe('Failed to cancel subscription'); 191 | }); 192 | }); 193 | 194 | describe('updateSubscription', () => { 195 | it('should update a subscription and return the result', async () => { 196 | const mockSubscription = { 197 | id: 'sub_123456', 198 | customer: 'cus_123456', 199 | status: 'active', 200 | current_period_start: 1609459200, 201 | current_period_end: 1612137600, 202 | items: { 203 | data: [ 204 | { 205 | id: 'si_123', 206 | price: 'price_123', 207 | quantity: 1, 208 | }, 209 | ], 210 | }, 211 | }; 212 | 213 | const context = {}; 214 | const params = { 215 | subscription: 'sub_123456', 216 | items: [ 217 | { 218 | id: 'si_123', 219 | quantity: 2, 220 | }, 221 | ], 222 | }; 223 | 224 | stripe.subscriptions.update.mockResolvedValue(mockSubscription); 225 | const result = await updateSubscription(stripe, context, params); 226 | 227 | expect(stripe.subscriptions.update).toHaveBeenCalledWith( 228 | 'sub_123456', 229 | { 230 | items: [ 231 | { 232 | id: 'si_123', 233 | quantity: 2, 234 | }, 235 | ], 236 | }, 237 | undefined 238 | ); 239 | expect(result).toEqual(mockSubscription); 240 | }); 241 | 242 | it('should handle errors gracefully', async () => { 243 | const context = {}; 244 | const params = { 245 | subscription: 'sub_123456', 246 | items: [ 247 | { 248 | id: 'si_123', 249 | quantity: 2, 250 | }, 251 | ], 252 | }; 253 | 254 | stripe.subscriptions.update.mockRejectedValue(new Error('API Error')); 255 | const result = await updateSubscription(stripe, context, params); 256 | 257 | expect(result).toBe('Failed to update subscription'); 258 | }); 259 | 260 | it('should specify the connected account if included in context', async () => { 261 | const mockSubscription = { 262 | id: 'sub_123456', 263 | customer: 'cus_123456', 264 | status: 'active', 265 | current_period_start: 1609459200, 266 | current_period_end: 1612137600, 267 | items: { 268 | data: [ 269 | { 270 | id: 'si_123', 271 | price: 'price_123', 272 | quantity: 1, 273 | }, 274 | ], 275 | }, 276 | }; 277 | 278 | const context = { 279 | account: 'acct_123456', 280 | }; 281 | const params = { 282 | subscription: 'sub_123456', 283 | cancel_at_period_end: true, 284 | }; 285 | 286 | stripe.subscriptions.update.mockResolvedValue(mockSubscription); 287 | const result = await updateSubscription(stripe, context, params); 288 | 289 | expect(stripe.subscriptions.update).toHaveBeenCalledWith( 290 | 'sub_123456', 291 | { 292 | cancel_at_period_end: true, 293 | }, 294 | { 295 | stripeAccount: context.account, 296 | } 297 | ); 298 | expect(result).toEqual(mockSubscription); 299 | }); 300 | }); 301 | ``` -------------------------------------------------------------------------------- /typescript/src/test/modelcontextprotocol/register-paid-tool.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {z} from 'zod'; 2 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'; 3 | import {registerPaidTool} from '../../modelcontextprotocol/register-paid-tool'; 4 | import Stripe from 'stripe'; 5 | import type { 6 | ServerNotification, 7 | ServerRequest, 8 | } from '@modelcontextprotocol/sdk/types.js'; 9 | import type {RequestHandlerExtra} from '@modelcontextprotocol/sdk/shared/protocol.js'; 10 | 11 | // Mock Stripe 12 | jest.mock('stripe'); 13 | const mockSecretKey = 'sk_test_123'; 14 | 15 | describe('registerPaidTool', () => { 16 | let mockMcpServer: jest.Mocked<McpServer>; 17 | let mockStripe: jest.Mocked<any>; 18 | let mockExtra: RequestHandlerExtra<ServerRequest, ServerNotification>; 19 | 20 | beforeEach(() => { 21 | // Reset all mocks 22 | jest.clearAllMocks(); 23 | 24 | // Mock McpServer 25 | mockMcpServer = { 26 | tool: jest.fn(), 27 | } as any; 28 | 29 | // Mock Stripe instance and methods 30 | mockStripe = { 31 | customers: { 32 | list: jest.fn(), 33 | create: jest.fn(), 34 | }, 35 | checkout: { 36 | sessions: { 37 | create: jest.fn(), 38 | retrieve: jest.fn(), 39 | list: jest.fn(), 40 | }, 41 | }, 42 | subscriptions: { 43 | list: jest.fn(), 44 | }, 45 | billing: { 46 | meterEvents: { 47 | create: jest.fn(), 48 | }, 49 | }, 50 | }; 51 | 52 | (Stripe as unknown as jest.Mock).mockImplementation(() => mockStripe); 53 | 54 | // Mock request handler extra 55 | mockExtra = { 56 | signal: new AbortController().signal, 57 | sendNotification: jest.fn(), 58 | sendRequest: jest.fn(), 59 | requestId: '123', 60 | }; 61 | }); 62 | 63 | it('should register a tool with the McpServer', async () => { 64 | const toolName = 'testTool'; 65 | const toolDescription = 'Test tool description'; 66 | const paramsSchema = { 67 | testParam: z.string(), 68 | }; 69 | const callback = jest.fn(); 70 | 71 | // @ts-ignore: https://github.com/modelcontextprotocol/typescript-sdk/issues/494 72 | await registerPaidTool( 73 | mockMcpServer, 74 | toolName, 75 | toolDescription, 76 | paramsSchema, 77 | callback, 78 | { 79 | paymentReason: 'Test payment', 80 | stripeSecretKey: mockSecretKey, 81 | userEmail: '[email protected]', 82 | checkout: { 83 | success_url: 'https://example.com/success', 84 | line_items: [{price: 'price_123', quantity: 1}], 85 | mode: 'subscription', 86 | }, 87 | } 88 | ); 89 | 90 | expect(mockMcpServer.tool).toHaveBeenCalledWith( 91 | toolName, 92 | toolDescription, 93 | paramsSchema, 94 | expect.any(Function) 95 | ); 96 | }); 97 | 98 | it('should create a new customer if one does not exist', async () => { 99 | mockStripe.customers.list.mockResolvedValue({data: []}); 100 | mockStripe.customers.create.mockResolvedValue({id: 'cus_123'}); 101 | mockStripe.subscriptions.list.mockResolvedValue({ 102 | data: [ 103 | { 104 | items: { 105 | data: [ 106 | { 107 | price: { 108 | id: 'price_123', 109 | }, 110 | }, 111 | ], 112 | }, 113 | }, 114 | ], 115 | }); 116 | mockStripe.checkout.sessions.list.mockResolvedValue({data: []}); 117 | mockStripe.checkout.sessions.create.mockResolvedValue({ 118 | id: 'cs_123', 119 | url: 'https://checkout.stripe.com/123', 120 | }); 121 | 122 | const toolName = 'testTool'; 123 | const callback = jest.fn(); 124 | 125 | await registerPaidTool( 126 | mockMcpServer, 127 | toolName, 128 | 'Test description', 129 | {testParam: z.string()}, 130 | callback, 131 | { 132 | paymentReason: 'Test payment', 133 | stripeSecretKey: mockSecretKey, 134 | userEmail: '[email protected]', 135 | checkout: { 136 | success_url: 'https://example.com/success', 137 | line_items: [{price: 'price_123', quantity: 1}], 138 | mode: 'subscription', 139 | }, 140 | } 141 | ); 142 | 143 | const registeredCallback = mockMcpServer.tool.mock.calls[0]?.[3]; 144 | // @ts-ignore: TypeScript can't disambiguate between params schema and annotations 145 | await registeredCallback({testParam: 'test'}, mockExtra); 146 | 147 | expect(mockStripe.customers.list).toHaveBeenCalledWith({ 148 | email: '[email protected]', 149 | }); 150 | expect(mockStripe.customers.create).toHaveBeenCalledWith({ 151 | email: '[email protected]', 152 | }); 153 | }); 154 | 155 | it('should create a checkout session for unpaid tools', async () => { 156 | mockStripe.customers.list.mockResolvedValue({ 157 | data: [{id: 'cus_123', email: '[email protected]'}], 158 | }); 159 | mockStripe.checkout.sessions.create.mockResolvedValue({ 160 | id: 'cs_123', 161 | url: 'https://checkout.stripe.com/123', 162 | }); 163 | mockStripe.subscriptions.list.mockResolvedValue({ 164 | data: [], // No active subscriptions 165 | }); 166 | mockStripe.checkout.sessions.list.mockResolvedValue({ 167 | data: [], // No paid sessions 168 | }); 169 | 170 | const toolName = 'testTool'; 171 | const callback = jest.fn(); 172 | 173 | await registerPaidTool( 174 | mockMcpServer, 175 | toolName, 176 | 'Test description', 177 | {testParam: z.string()}, 178 | callback, 179 | { 180 | paymentReason: 'Test payment', 181 | stripeSecretKey: mockSecretKey, 182 | userEmail: '[email protected]', 183 | checkout: { 184 | success_url: 'https://example.com/success', 185 | line_items: [{price: 'price_123', quantity: 1}], 186 | mode: 'subscription', 187 | }, 188 | } 189 | ); 190 | 191 | const registeredCallback = mockMcpServer.tool.mock.calls[0]?.[3]; 192 | // @ts-ignore: TypeScript can't disambiguate between params schema and annotations 193 | const result = await registeredCallback({testParam: 'test'}, mockExtra); 194 | 195 | expect(mockStripe.checkout.sessions.create).toHaveBeenCalledWith({ 196 | success_url: 'https://example.com/success', 197 | line_items: [ 198 | { 199 | price: 'price_123', 200 | quantity: 1, 201 | }, 202 | ], 203 | mode: 'subscription', 204 | customer: 'cus_123', 205 | metadata: {toolName}, 206 | }); 207 | expect(result).toEqual({ 208 | content: [ 209 | { 210 | type: 'text', 211 | text: JSON.stringify({ 212 | status: 'payment_required', 213 | data: { 214 | paymentType: 'oneTimeSubscription', 215 | checkoutUrl: 'https://checkout.stripe.com/123', 216 | paymentReason: 'Test payment', 217 | }, 218 | }), 219 | }, 220 | ], 221 | }); 222 | expect(callback).not.toHaveBeenCalled(); 223 | }); 224 | 225 | it('should handle usage-based billing when meterEvent is provided', async () => { 226 | const toolName = 'testTool'; 227 | mockStripe.customers.list.mockResolvedValue({ 228 | data: [{id: 'cus_123', email: '[email protected]'}], 229 | }); 230 | mockStripe.checkout.sessions.list.mockResolvedValue({ 231 | data: [ 232 | { 233 | id: 'cs_123', 234 | metadata: {toolName}, 235 | payment_status: 'paid', 236 | subscription: 'sub_123', 237 | }, 238 | ], 239 | }); 240 | mockStripe.subscriptions.list.mockResolvedValue({ 241 | data: [ 242 | { 243 | items: { 244 | data: [ 245 | { 246 | price: { 247 | id: 'price_123', 248 | }, 249 | }, 250 | ], 251 | }, 252 | }, 253 | ], 254 | }); 255 | const callback = jest.fn().mockResolvedValue({ 256 | content: [{type: 'text', text: 'Success'}], 257 | }); 258 | 259 | await registerPaidTool( 260 | mockMcpServer, 261 | toolName, 262 | 'Test description', 263 | {testParam: z.string()}, 264 | callback, 265 | { 266 | paymentReason: 'Test payment', 267 | meterEvent: 'test.event', 268 | stripeSecretKey: mockSecretKey, 269 | userEmail: '[email protected]', 270 | checkout: { 271 | success_url: 'https://example.com/success', 272 | line_items: [{price: 'price_123'}], 273 | mode: 'subscription', 274 | }, 275 | } 276 | ); 277 | 278 | const registeredCallback = mockMcpServer.tool.mock.calls[0]?.[3]; 279 | // @ts-ignore: TypeScript can't disambiguate between params schema and annotations 280 | await registeredCallback({testParam: 'test'}, mockExtra); 281 | 282 | expect(mockStripe.billing.meterEvents.create).toHaveBeenCalledWith({ 283 | event_name: 'test.event', 284 | payload: { 285 | stripe_customer_id: 'cus_123', 286 | value: '1', 287 | }, 288 | }); 289 | }); 290 | }); 291 | ``` -------------------------------------------------------------------------------- /evals/cases.ts: -------------------------------------------------------------------------------- ```typescript 1 | require("dotenv").config(); 2 | 3 | import { 4 | assert, 5 | EvalCaseFunction, 6 | EvalInput, 7 | expectToolCall, 8 | expectToolCallArgs, 9 | llmCriteriaMet, 10 | } from "./scorer"; 11 | import { Configuration as StripeAgentToolkitConfig } from "../typescript/src/shared/configuration"; 12 | import Stripe from "stripe"; 13 | 14 | /* 15 | * A single test case that is used to evaluate the agent. 16 | * It contains an input, a toolkit config, and an function to use to run 17 | * assertions on the output of the agent. It is structured to be used with 18 | * Braintrust. 19 | */ 20 | type BraintrustTestCase = { 21 | input: EvalInput; 22 | toolkitConfig?: StripeAgentToolkitConfig; 23 | expected: EvalCaseFunction; 24 | }; 25 | 26 | /* This is used in a Braintrust Eval. Our test framework appends new test cases to this array.*/ 27 | const _testCases: Array<BraintrustTestCase | Promise<BraintrustTestCase>> = []; 28 | 29 | /* 30 | * Helper type for adding test cases to the Braintrust Eval. 31 | */ 32 | type TestCaseData = { 33 | // The user prompt to pass into the agent. 34 | prompt: string; 35 | // The function to use to run assertions on the output of the agent. 36 | fn: EvalCaseFunction; 37 | // Optional toolkit config to set into the agent to override the default set in eval.ts. 38 | toolkitConfig?: StripeAgentToolkitConfig; 39 | }; 40 | 41 | const argsToTestCase = (args: TestCaseData): BraintrustTestCase => ({ 42 | input: { 43 | toolkitConfigOverride: args.toolkitConfig || {}, 44 | userPrompt: args.prompt, 45 | }, 46 | expected: args.fn, 47 | }); 48 | 49 | /* 50 | * Helper function for adding test cases to the Braintrust Eval. 51 | */ 52 | const test = (args: TestCaseData | (() => Promise<TestCaseData>)) => { 53 | if (typeof args == "function") { 54 | const promise = args().then(argsToTestCase); 55 | _testCases.push(promise); 56 | } else { 57 | _testCases.push(argsToTestCase(args)); 58 | } 59 | }; 60 | 61 | test({ 62 | prompt: 63 | "Create a product called 'Test Product' with a description 'A test product for evaluation'", 64 | fn: ({ toolCalls, messages }) => [ 65 | expectToolCall(toolCalls, ["create_product"]), 66 | llmCriteriaMet( 67 | messages, 68 | "The message should include a successful production creation response" 69 | ), 70 | ], 71 | }); 72 | 73 | test({ 74 | prompt: "List all available products", 75 | fn: ({ toolCalls, messages }) => [ 76 | expectToolCall(toolCalls, ["list_products"]), 77 | llmCriteriaMet(messages, "The message should include a list of products"), 78 | ], 79 | }); 80 | 81 | test({ 82 | prompt: 83 | "Create a customer with a name of a Philadelphia Eagles player and email (you can make it up). Charge them $100.", 84 | fn: ({ toolCalls, messages }) => [ 85 | expectToolCall(toolCalls, ["create_customer"]), 86 | ], 87 | }); 88 | 89 | test({ 90 | prompt: 91 | "Create a payment link for a new product called 'test' with a price of $70. Come up with a haiku for the description.", 92 | fn: ({ toolCalls, messages }) => [ 93 | llmCriteriaMet(messages, "The message should include a payment link"), 94 | expectToolCall(toolCalls, ["create_payment_link"]), 95 | ], 96 | }); 97 | 98 | test({ 99 | prompt: 100 | "Create a payment link for a new product called 'test' with a price of $35.99, if the user completes the purchase they should be redirected to https://www.stripe.com", 101 | fn: ({ toolCalls, messages }) => [ 102 | expectToolCall(toolCalls, ["create_payment_link"]), 103 | expectToolCallArgs(toolCalls, [ 104 | { 105 | name: "create_payment_link", 106 | arguments: { 107 | redirect_url: "https://www.stripe.com", 108 | }, 109 | shallow: true, 110 | }, 111 | ]), 112 | llmCriteriaMet(messages, "The message should include a payment link and indicate the redirect url"), 113 | ], 114 | }); 115 | 116 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); 117 | 118 | test(async () => { 119 | const customer = await stripe.customers.create({ 120 | name: "Joel E", 121 | email: "[email protected]", 122 | }); 123 | 124 | const joelsPayment = await stripe.paymentIntents.create({ 125 | amount: 2000, 126 | currency: "usd", 127 | customer: customer.id, 128 | }); 129 | 130 | const otherPi = await stripe.paymentIntents.create({ 131 | amount: 3000, 132 | currency: "usd", 133 | }); 134 | 135 | return { 136 | prompt: "List payment intents", 137 | toolkitConfig: { 138 | context: { 139 | customer: customer.id, 140 | }, 141 | }, 142 | fn: ({ assistantMessages }) => [ 143 | assert( 144 | (function () { 145 | return ( 146 | assistantMessages.some((m) => m.includes(joelsPayment.id)) && 147 | assistantMessages.every((m) => !m.includes(otherPi.id)) 148 | ); 149 | })(), 150 | `messages only includes customers payment intent ${joelsPayment.id}` 151 | ), 152 | ], 153 | }; 154 | }); 155 | 156 | test({ 157 | prompt: "List all subscriptions", 158 | fn: ({ toolCalls, messages }) => [ 159 | llmCriteriaMet( 160 | messages, 161 | "The message should include a list of subscriptions" 162 | ), 163 | expectToolCall(toolCalls, ["list_subscriptions"]), 164 | ], 165 | }); 166 | 167 | test({ 168 | prompt: "Create a coupon called SUMMER25 that gives 25% off", 169 | fn: ({ toolCalls, messages }) => [ 170 | expectToolCall(toolCalls, ["create_coupon"]), 171 | llmCriteriaMet( 172 | messages, 173 | "The message should include a coupon creation response" 174 | ), 175 | ], 176 | }); 177 | 178 | test({ 179 | prompt: "Create a coupon called WINTERTEN that gives $10 off", 180 | fn: ({ toolCalls, messages }) => [ 181 | expectToolCall(toolCalls, ["create_coupon"]), 182 | expectToolCallArgs(toolCalls, [ 183 | { 184 | name: "create_coupon", 185 | arguments: { 186 | amount_off: 1000, 187 | currency: "USD", 188 | name: "WINTERTEN", 189 | } 190 | }, 191 | ]), 192 | llmCriteriaMet( 193 | messages, 194 | "The message should include a coupon creation response" 195 | ), 196 | ], 197 | }); 198 | 199 | test({ 200 | prompt: "List all coupons", 201 | fn: ({ toolCalls, messages }) => [ 202 | expectToolCall(toolCalls, ["list_coupons"]), 203 | ], 204 | }); 205 | 206 | test(async () => { 207 | const customer = await stripe.customers.create({ 208 | name: "Joel E", 209 | email: "[email protected]", 210 | payment_method: "pm_card_visa", 211 | }); 212 | 213 | const paymentMethod = await stripe.paymentMethods.create({ 214 | type: "card", 215 | card: { 216 | token: "tok_visa", 217 | }, 218 | }); 219 | 220 | await stripe.paymentMethods.attach(paymentMethod.id, { 221 | customer: customer.id, 222 | }); 223 | 224 | // // Set as default payment method 225 | await stripe.customers.update(customer.id, { 226 | invoice_settings: { default_payment_method: paymentMethod.id }, 227 | }); 228 | 229 | const product = await stripe.products.create({ 230 | name: "Subscription Product", 231 | description: "A test subscription product", 232 | }); 233 | 234 | const price = await stripe.prices.create({ 235 | product: product.id, 236 | unit_amount: 1000, 237 | currency: "usd", 238 | recurring: { interval: "month" }, 239 | }); 240 | 241 | await stripe.subscriptions.create({ 242 | customer: customer.id, 243 | items: [{ price: price.id }], 244 | }); 245 | 246 | return { 247 | prompt: `Cancel the users subscription`, 248 | toolkitConfig: { 249 | context: { 250 | customer: customer.id, 251 | }, 252 | }, 253 | fn: ({ toolCalls, messages }) => [ 254 | expectToolCall(toolCalls, ["list_subscriptions", "cancel_subscription"]), 255 | llmCriteriaMet( 256 | messages, 257 | "The message should include a successful subscription cancellation response" 258 | ), 259 | ], 260 | }; 261 | }); 262 | 263 | // New test for update subscription 264 | test(async () => { 265 | const customer = await stripe.customers.create({ 266 | name: "User Example", 267 | email: "[email protected]", 268 | }); 269 | 270 | const paymentMethod = await stripe.paymentMethods.create({ 271 | type: "card", 272 | card: { 273 | token: "tok_visa", 274 | }, 275 | }); 276 | 277 | await stripe.paymentMethods.attach(paymentMethod.id, { 278 | customer: customer.id, 279 | }); 280 | 281 | // // Set as default payment method 282 | await stripe.customers.update(customer.id, { 283 | invoice_settings: { default_payment_method: paymentMethod.id }, 284 | }); 285 | 286 | const product = await stripe.products.create({ 287 | name: "SaaS Product", 288 | description: "A test subscription product", 289 | }); 290 | 291 | const basicPrice = await stripe.prices.create({ 292 | product: product.id, 293 | unit_amount: 1000, 294 | currency: "usd", 295 | recurring: { interval: "month" }, 296 | }); 297 | 298 | const premiumPrice = await stripe.prices.create({ 299 | product: product.id, 300 | unit_amount: 2000, 301 | currency: "usd", 302 | recurring: { interval: "month" }, 303 | }); 304 | 305 | const subscription = await stripe.subscriptions.create({ 306 | customer: customer.id, 307 | items: [{ price: basicPrice.id, quantity: 1 }], 308 | }); 309 | 310 | return { 311 | prompt: `Upgrade the user's subscription to the premium plan`, 312 | toolkitConfig: { 313 | context: { 314 | customer: customer.id, 315 | }, 316 | }, 317 | fn: ({ toolCalls, messages }) => [ 318 | expectToolCall(toolCalls, ["list_subscriptions", "update_subscription"]), 319 | llmCriteriaMet( 320 | messages, 321 | "The message should include a successful subscription update response. The subscription should have been updated to the premium plan and have only one item." 322 | ), 323 | ], 324 | }; 325 | }); 326 | 327 | test({ 328 | prompt: "List all disputes", 329 | fn: ({ toolCalls, messages }) => [ 330 | expectToolCall(toolCalls, ["list_disputes"]), 331 | ], 332 | }); 333 | 334 | export const getEvalTestCases = async () => Promise.all(_testCases); 335 | ``` -------------------------------------------------------------------------------- /python/examples/openai/customer_support/emailer.py: -------------------------------------------------------------------------------- ```python 1 | # pyright: strict 2 | 3 | import imaplib 4 | import email 5 | import smtplib 6 | from email.mime.text import MIMEText 7 | from email.message import Message 8 | from email.mime.multipart import MIMEMultipart 9 | from email.utils import parseaddr 10 | from typing import List, Tuple, Callable, Union, Awaitable 11 | import asyncio 12 | import json 13 | import re 14 | from datetime import datetime 15 | from email.utils import parsedate_to_datetime 16 | 17 | 18 | class Email: 19 | def __init__( 20 | self, 21 | from_address: str, 22 | to_address: str, 23 | subject: str, 24 | body: str, 25 | id: str = "", 26 | date: datetime = datetime.now(), 27 | ): 28 | self.id = id 29 | self.to_address = to_address 30 | self.from_address = from_address 31 | self.subject = subject 32 | self.body = body 33 | self.date = date 34 | 35 | def to_message(self, reply_id: str, reply_to: str) -> MIMEMultipart: 36 | msg = MIMEMultipart() 37 | msg["From"] = self.from_address 38 | msg["To"] = self.to_address 39 | msg["Subject"] = self.subject 40 | msg["In-Reply-To"] = reply_id 41 | msg["References"] = reply_id 42 | msg["Reply-To"] = reply_to 43 | msg.attach(MIMEText(f"<html><body>{self.body}</body></html>", "html")) 44 | return msg 45 | 46 | def to_dict(self): 47 | return { 48 | "id": self.id, 49 | "to": self.to_address, 50 | "from": self.from_address, 51 | "subject": self.subject, 52 | "body": self.body, 53 | "date": self.date.strftime("%a, %d %b %Y %H:%M:%S %z"), 54 | } 55 | 56 | 57 | class Emailer: 58 | """ 59 | Emailer is an IMAP/SMTP client that can be used to fetch and respond to emails. 60 | It was mostly vibe-coded so please make improvements! 61 | TODO: add agent replies to the context 62 | """ 63 | 64 | def __init__( 65 | self, 66 | email_address: str, 67 | email_password: str, 68 | support_address: str = "", 69 | imap_server: str = "imap.gmail.com", 70 | imap_port: int = 993, 71 | smtp_server: str = "smtp.gmail.com", 72 | smtp_port: int = 587, 73 | ): 74 | # Email configuration 75 | self.email_address = email_address 76 | self.support_address = support_address if support_address else email_address 77 | self.email_password = email_password 78 | self.imap_server = imap_server 79 | self.imap_port = imap_port 80 | self.smtp_server = smtp_server 81 | self.smtp_port = smtp_port 82 | 83 | def _connect_to_email(self) -> Tuple[imaplib.IMAP4_SSL, smtplib.SMTP]: 84 | """Establish connections to email servers.""" 85 | # Connect to IMAP server 86 | imap_conn = imaplib.IMAP4_SSL(self.imap_server, self.imap_port) 87 | imap_conn.login(self.email_address, self.email_password) 88 | 89 | # Connect to SMTP server 90 | smtp_conn = smtplib.SMTP(self.smtp_server, self.smtp_port) 91 | smtp_conn.starttls() 92 | smtp_conn.login(self.email_address, self.email_password) 93 | 94 | return imap_conn, smtp_conn 95 | 96 | def _get_body(self, email_message: Message) -> str: 97 | body: str = "" 98 | if email_message.is_multipart(): 99 | for part in email_message.walk(): 100 | if part.get_content_type() == "text/plain": 101 | payload = part.get_payload(decode=True) 102 | if isinstance(payload, bytes): 103 | body = payload.decode() 104 | break 105 | else: 106 | payload = email_message.get_payload(decode=True) 107 | if isinstance(payload, bytes): 108 | body = payload.decode() 109 | else: 110 | body = str(payload) 111 | return self._strip_replies(body) 112 | 113 | def _strip_replies(self, raw_body: str) -> str: 114 | lines = raw_body.split("\n") 115 | pruned: List[str] = [] 116 | for line in lines: 117 | # Stop if we see a typical reply indicator 118 | if line.strip().startswith("On ") and " wrote:" in line: 119 | break 120 | pruned.append(line) 121 | return "\n".join(pruned).strip() 122 | 123 | def _parse_email( 124 | self, imap_conn: imaplib.IMAP4_SSL, email_id: bytes 125 | ) -> Union[Email, None]: 126 | _, msg_data = imap_conn.fetch(email_id.decode(), "(BODY.PEEK[])") 127 | if not msg_data or not msg_data[0]: 128 | return None 129 | msg_resp = msg_data[0] 130 | if isinstance(msg_resp, tuple) and len(msg_resp) == 2: 131 | email_body = msg_resp[1] 132 | else: 133 | return None 134 | 135 | email_message = email.message_from_bytes(email_body) 136 | subject = email_message["subject"] or "" 137 | from_address = parseaddr(email_message.get("From", ""))[1] 138 | to_address = parseaddr(email_message.get("To", ""))[1] 139 | date_str = email_message.get("Date", "") 140 | date = datetime.now() 141 | if date_str: 142 | try: 143 | date = parsedate_to_datetime(date_str) 144 | except Exception: 145 | pass 146 | 147 | body = self._get_body(email_message) 148 | return Email( 149 | id=email_id.decode(), 150 | from_address=from_address, 151 | to_address=to_address, 152 | subject=subject, 153 | body=body, 154 | date=date, 155 | ) 156 | 157 | def _get_email_thread( 158 | self, imap_conn: imaplib.IMAP4_SSL, email_id_bytes: bytes 159 | ) -> List[Email]: 160 | email = self._parse_email(imap_conn, email_id_bytes) 161 | if not email: 162 | return [] 163 | 164 | thread = [email] 165 | 166 | # Try thread via X-GM-THRID (Gmail extension) 167 | _, thrid_data = imap_conn.fetch(email.id, "(X-GM-THRID)") 168 | match = None 169 | if thrid_data and thrid_data[0]: 170 | data = thrid_data[0] 171 | if isinstance(data, bytes): 172 | match = re.search(r"X-GM-THRID\s+(\d+)", data.decode()) 173 | else: 174 | match = re.search(r"X-GM-THRID\s+(\d+)", str(data)) 175 | if match: 176 | thread_id = match.group(1) 177 | _, thread_ids = imap_conn.search(None, f"X-GM-THRID {thread_id}") 178 | if thread_ids and thread_ids[0]: 179 | thread = [ 180 | self._parse_email(imap_conn, mid) for mid in thread_ids[0].split() 181 | ] 182 | thread = [e for e in thread if e] 183 | thread.sort(key=lambda e: e.date) 184 | return thread 185 | 186 | # Fallback: use REFERENCES header 187 | _, ref_data = imap_conn.fetch( 188 | email.id, "(BODY.PEEK[HEADER.FIELDS (REFERENCES)])" 189 | ) 190 | if ref_data and ref_data[0]: 191 | ref_line = ( 192 | ref_data[0][1].decode() if isinstance(ref_data[0][1], bytes) else "" 193 | ) 194 | refs = re.findall(r"<([^>]+)>", ref_line) 195 | for ref in refs: 196 | _, ref_ids = imap_conn.search(None, f'(HEADER Message-ID "<{ref}>")') 197 | if ref_ids and ref_ids[0]: 198 | for ref_id in ref_ids[0].split(): 199 | ref_email = self._parse_email(imap_conn, ref_id) 200 | if ref_email and ref_email.id not in [e.id for e in thread]: 201 | thread.append(ref_email) 202 | 203 | # Sort emails in the thread by date (ascending order) 204 | thread.sort(key=lambda e: e.date) 205 | return thread 206 | 207 | return thread 208 | 209 | def _get_unread_emails(self, imap_conn: imaplib.IMAP4_SSL) -> List[List[Email]]: 210 | imap_conn.select("INBOX") 211 | _, msg_nums = imap_conn.search(None, f'(UNSEEN TO "{self.support_address}")') 212 | emails: List[List[Email]] = [] 213 | 214 | for email_id in msg_nums[0].split(): 215 | thread = self._get_email_thread(imap_conn, email_id) 216 | emails.append(thread) 217 | 218 | return emails 219 | 220 | def mark_as_read(self, imap_conn: imaplib.IMAP4_SSL, message_id: str): 221 | imap_conn.store(message_id, "+FLAGS", "\\Seen") 222 | 223 | def get_email_thread(self, email_id: str) -> List[Email]: 224 | # Connect to email servers 225 | imap_conn, smtp_conn = self._connect_to_email() 226 | imap_conn.select("INBOX") 227 | 228 | # Get the thread 229 | thread = self._get_email_thread( 230 | imap_conn=imap_conn, email_id_bytes=email_id.encode() 231 | ) 232 | 233 | # Close connections 234 | imap_conn.logout() 235 | smtp_conn.quit() 236 | 237 | return thread 238 | 239 | async def process( 240 | self, 241 | respond: Callable[[List[Email]], Awaitable[Union[Email, None]]], 242 | mark_read: bool = True, 243 | ): 244 | # Connect to email servers 245 | imap_conn, smtp_conn = self._connect_to_email() 246 | 247 | # Get unread emails 248 | print("Fetching unread emails...") 249 | unread_emails = self._get_unread_emails(imap_conn) 250 | for email_thread in unread_emails: 251 | # Get the most recent email in the thread 252 | most_recent = email_thread[-1] 253 | 254 | # Generate the response 255 | response = await respond(email_thread) 256 | 257 | # If there is no response, skip this email and keep as unread 258 | # in the inbox 259 | if response is None: 260 | continue 261 | 262 | # Send the response 263 | # Get the most recent email in the thread to reply to 264 | print( 265 | f"Replying to '{response.to_address}' with:\n {json.dumps(response.body)}" 266 | ) 267 | smtp_conn.send_message( 268 | response.to_message(most_recent.id, self.support_address) 269 | ) 270 | 271 | # Mark the original email as read 272 | if mark_read: 273 | self.mark_as_read(imap_conn, most_recent.id) 274 | 275 | # Close connections 276 | imap_conn.logout() 277 | smtp_conn.quit() 278 | 279 | async def run( 280 | self, 281 | respond: Callable[[List[Email]], Awaitable[Union[Email, None]]], 282 | mark_read: bool = True, 283 | delay: int = 60, 284 | ): 285 | while True: 286 | # Process emails 287 | await self.process(respond, mark_read) 288 | # Wait before next check 289 | print(f"Sleeping for {delay}s...") 290 | await asyncio.sleep(delay) 291 | ``` -------------------------------------------------------------------------------- /modelcontextprotocol/eslint.config.mjs: -------------------------------------------------------------------------------- ``` 1 | import prettier from "eslint-plugin-prettier"; 2 | import _import from "eslint-plugin-import"; 3 | import { fixupPluginRules } from "@eslint/compat"; 4 | import globals from "globals"; 5 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 6 | import path from "node:path"; 7 | import { fileURLToPath } from "node:url"; 8 | import js from "@eslint/js"; 9 | import { FlatCompat } from "@eslint/eslintrc"; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | const compat = new FlatCompat({ 14 | baseDirectory: __dirname, 15 | recommendedConfig: js.configs.recommended, 16 | allConfig: js.configs.all 17 | }); 18 | 19 | export default [...compat.extends("plugin:prettier/recommended"), { 20 | plugins: { 21 | prettier, 22 | import: fixupPluginRules(_import), 23 | }, 24 | 25 | languageOptions: { 26 | globals: { 27 | ...globals.node, 28 | }, 29 | 30 | ecmaVersion: 2018, 31 | sourceType: "commonjs", 32 | }, 33 | 34 | rules: { 35 | "accessor-pairs": "error", 36 | "array-bracket-spacing": ["error", "never"], 37 | "array-callback-return": "off", 38 | "arrow-parens": "error", 39 | "arrow-spacing": "error", 40 | "block-scoped-var": "off", 41 | "block-spacing": "off", 42 | 43 | "brace-style": ["error", "1tbs", { 44 | allowSingleLine: true, 45 | }], 46 | 47 | "capitalized-comments": "off", 48 | "class-methods-use-this": "off", 49 | "comma-dangle": "off", 50 | "comma-spacing": "off", 51 | "comma-style": ["error", "last"], 52 | complexity: "error", 53 | "computed-property-spacing": ["error", "never"], 54 | "consistent-return": "off", 55 | "consistent-this": "off", 56 | curly: "error", 57 | "default-case": "off", 58 | "dot-location": ["error", "property"], 59 | "dot-notation": "error", 60 | "eol-last": "error", 61 | eqeqeq: "off", 62 | "func-call-spacing": "error", 63 | "func-name-matching": "error", 64 | "func-names": "off", 65 | 66 | "func-style": ["error", "declaration", { 67 | allowArrowFunctions: true, 68 | }], 69 | 70 | "generator-star-spacing": "error", 71 | "global-require": "off", 72 | "guard-for-in": "error", 73 | "handle-callback-err": "off", 74 | "id-blacklist": "error", 75 | "id-length": "off", 76 | "id-match": "error", 77 | "import/extensions": "off", 78 | "init-declarations": "off", 79 | "jsx-quotes": "error", 80 | "key-spacing": "error", 81 | 82 | "keyword-spacing": ["error", { 83 | after: true, 84 | before: true, 85 | }], 86 | 87 | "line-comment-position": "off", 88 | "linebreak-style": ["error", "unix"], 89 | "lines-around-directive": "error", 90 | "max-depth": "error", 91 | "max-len": "off", 92 | "max-lines": "off", 93 | "max-nested-callbacks": "error", 94 | "max-params": "off", 95 | "max-statements": "off", 96 | "max-statements-per-line": "off", 97 | "multiline-ternary": "off", 98 | "new-cap": "off", 99 | "new-parens": "error", 100 | "newline-after-var": "off", 101 | "newline-before-return": "off", 102 | "newline-per-chained-call": "off", 103 | "no-alert": "error", 104 | "no-array-constructor": "error", 105 | "no-await-in-loop": "error", 106 | "no-bitwise": "off", 107 | "no-caller": "error", 108 | "no-catch-shadow": "off", 109 | "no-compare-neg-zero": "error", 110 | "no-confusing-arrow": "error", 111 | "no-continue": "off", 112 | "no-div-regex": "error", 113 | "no-duplicate-imports": "off", 114 | "no-else-return": "off", 115 | "no-empty-function": "off", 116 | "no-eq-null": "off", 117 | "no-eval": "error", 118 | "no-extend-native": "error", 119 | "no-extra-bind": "error", 120 | "no-extra-label": "error", 121 | "no-extra-parens": "off", 122 | "no-floating-decimal": "error", 123 | "no-implicit-globals": "error", 124 | "no-implied-eval": "error", 125 | "no-inline-comments": "off", 126 | "no-inner-declarations": ["error", "functions"], 127 | "no-invalid-this": "off", 128 | "no-iterator": "error", 129 | "no-label-var": "error", 130 | "no-labels": "error", 131 | "no-lone-blocks": "error", 132 | "no-lonely-if": "error", 133 | "no-loop-func": "error", 134 | "no-magic-numbers": "off", 135 | "no-mixed-requires": "error", 136 | "no-multi-assign": "off", 137 | "no-multi-spaces": "error", 138 | "no-multi-str": "error", 139 | "no-multiple-empty-lines": "error", 140 | "no-native-reassign": "error", 141 | "no-negated-condition": "off", 142 | "no-negated-in-lhs": "error", 143 | "no-nested-ternary": "error", 144 | "no-new": "error", 145 | "no-new-func": "error", 146 | "no-new-object": "error", 147 | "no-new-require": "error", 148 | "no-new-wrappers": "error", 149 | "no-octal-escape": "error", 150 | "no-param-reassign": "off", 151 | "no-path-concat": "error", 152 | 153 | "no-plusplus": ["error", { 154 | allowForLoopAfterthoughts: true, 155 | }], 156 | 157 | "no-process-env": "off", 158 | "no-process-exit": "error", 159 | "no-proto": "error", 160 | "no-prototype-builtins": "off", 161 | "no-restricted-globals": "error", 162 | "no-restricted-imports": "error", 163 | "no-restricted-modules": "error", 164 | "no-restricted-properties": "error", 165 | "no-restricted-syntax": "error", 166 | "no-return-assign": "error", 167 | "no-return-await": "error", 168 | "no-script-url": "error", 169 | "no-self-compare": "error", 170 | "no-sequences": "error", 171 | "no-shadow": "off", 172 | "no-shadow-restricted-names": "error", 173 | "no-spaced-func": "error", 174 | "no-sync": "error", 175 | "no-tabs": "error", 176 | "no-template-curly-in-string": "error", 177 | "no-ternary": "off", 178 | "no-throw-literal": "error", 179 | "no-trailing-spaces": "error", 180 | "no-undef-init": "error", 181 | "no-undefined": "off", 182 | "no-underscore-dangle": "off", 183 | "no-unmodified-loop-condition": "error", 184 | "no-unneeded-ternary": "error", 185 | "no-unused-expressions": "error", 186 | 187 | "no-unused-vars": ["error", { 188 | args: "none", 189 | }], 190 | 191 | "no-use-before-define": "off", 192 | "no-useless-call": "error", 193 | "no-useless-computed-key": "error", 194 | "no-useless-concat": "error", 195 | "no-useless-constructor": "error", 196 | "no-useless-escape": "off", 197 | "no-useless-rename": "error", 198 | "no-useless-return": "error", 199 | "no-var": "off", 200 | "no-void": "error", 201 | "no-warning-comments": "error", 202 | "no-whitespace-before-property": "error", 203 | "no-with": "error", 204 | "nonblock-statement-body-position": "error", 205 | "object-curly-newline": "off", 206 | "object-curly-spacing": ["error", "never"], 207 | "object-property-newline": "off", 208 | "object-shorthand": "off", 209 | "one-var": "off", 210 | "one-var-declaration-per-line": "error", 211 | "operator-assignment": ["error", "always"], 212 | "operator-linebreak": "off", 213 | "padded-blocks": "off", 214 | "prefer-arrow-callback": "off", 215 | "prefer-const": "error", 216 | 217 | "prefer-destructuring": ["error", { 218 | array: false, 219 | object: false, 220 | }], 221 | 222 | "prefer-numeric-literals": "error", 223 | "prefer-promise-reject-errors": "error", 224 | "prefer-reflect": "off", 225 | "prefer-rest-params": "off", 226 | "prefer-spread": "off", 227 | "prefer-template": "off", 228 | "quote-props": "off", 229 | 230 | quotes: ["error", "single", { 231 | avoidEscape: true, 232 | }], 233 | 234 | radix: "error", 235 | "require-await": "error", 236 | "require-jsdoc": "off", 237 | "rest-spread-spacing": "error", 238 | semi: "off", 239 | 240 | "semi-spacing": ["error", { 241 | after: true, 242 | before: false, 243 | }], 244 | 245 | "sort-imports": "off", 246 | "sort-keys": "off", 247 | "sort-vars": "error", 248 | "space-before-blocks": "error", 249 | "space-before-function-paren": "off", 250 | "space-in-parens": ["error", "never"], 251 | "space-infix-ops": "error", 252 | "space-unary-ops": "error", 253 | "spaced-comment": ["error", "always"], 254 | strict: "off", 255 | "symbol-description": "error", 256 | "template-curly-spacing": "error", 257 | "template-tag-spacing": "error", 258 | "unicode-bom": ["error", "never"], 259 | "valid-jsdoc": "off", 260 | "vars-on-top": "off", 261 | "wrap-regex": "off", 262 | "yield-star-spacing": "error", 263 | yoda: ["error", "never"], 264 | }, 265 | }, ...compat.extends( 266 | "eslint:recommended", 267 | "plugin:@typescript-eslint/eslint-recommended", 268 | "plugin:@typescript-eslint/recommended", 269 | "plugin:prettier/recommended", 270 | ).map(config => ({ 271 | ...config, 272 | files: ["**/*.ts"], 273 | })), { 274 | files: ["**/*.ts"], 275 | 276 | plugins: { 277 | "@typescript-eslint": typescriptEslint, 278 | prettier, 279 | }, 280 | 281 | rules: { 282 | "@typescript-eslint/no-use-before-define": 0, 283 | "@typescript-eslint/no-empty-interface": 0, 284 | "@typescript-eslint/no-unused-vars": 0, 285 | "@typescript-eslint/triple-slash-reference": 0, 286 | "@typescript-eslint/ban-ts-comment": "off", 287 | "@typescript-eslint/no-empty-function": 0, 288 | "@typescript-eslint/no-require-imports": 0, 289 | 290 | "@typescript-eslint/naming-convention": ["error", { 291 | selector: "default", 292 | format: ["camelCase", "UPPER_CASE", "PascalCase"], 293 | leadingUnderscore: "allow", 294 | }, { 295 | selector: "property", 296 | format: null, 297 | }], 298 | 299 | "@typescript-eslint/no-explicit-any": 0, 300 | "@typescript-eslint/explicit-function-return-type": "off", 301 | "@typescript-eslint/no-this-alias": "off", 302 | "@typescript-eslint/no-var-requires": 0, 303 | "prefer-rest-params": "off", 304 | }, 305 | }, { 306 | files: ["test/**/*.ts"], 307 | 308 | rules: { 309 | "@typescript-eslint/explicit-function-return-type": "off", 310 | }, 311 | }]; 312 | ``` -------------------------------------------------------------------------------- /typescript/eslint.config.mjs: -------------------------------------------------------------------------------- ``` 1 | import prettier from "eslint-plugin-prettier"; 2 | import _import from "eslint-plugin-import"; 3 | import { fixupPluginRules } from "@eslint/compat"; 4 | import globals from "globals"; 5 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 6 | import path from "node:path"; 7 | import { fileURLToPath } from "node:url"; 8 | import js from "@eslint/js"; 9 | import { FlatCompat } from "@eslint/eslintrc"; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | const compat = new FlatCompat({ 14 | baseDirectory: __dirname, 15 | recommendedConfig: js.configs.recommended, 16 | allConfig: js.configs.all 17 | }); 18 | 19 | export default [...compat.extends("plugin:prettier/recommended"), { 20 | plugins: { 21 | prettier, 22 | import: fixupPluginRules(_import), 23 | }, 24 | 25 | languageOptions: { 26 | globals: { 27 | ...globals.node, 28 | }, 29 | 30 | ecmaVersion: 2018, 31 | sourceType: "commonjs", 32 | }, 33 | 34 | rules: { 35 | "accessor-pairs": "error", 36 | "array-bracket-spacing": ["error", "never"], 37 | "array-callback-return": "off", 38 | "arrow-parens": "error", 39 | "arrow-spacing": "error", 40 | "block-scoped-var": "off", 41 | "block-spacing": "off", 42 | 43 | "brace-style": ["error", "1tbs", { 44 | allowSingleLine: true, 45 | }], 46 | 47 | "capitalized-comments": "off", 48 | "class-methods-use-this": "off", 49 | "comma-dangle": "off", 50 | "comma-spacing": "off", 51 | "comma-style": ["error", "last"], 52 | complexity: "error", 53 | "computed-property-spacing": ["error", "never"], 54 | "consistent-return": "off", 55 | "consistent-this": "off", 56 | curly: "error", 57 | "default-case": "off", 58 | "dot-location": ["error", "property"], 59 | "dot-notation": "error", 60 | "eol-last": "error", 61 | eqeqeq: "off", 62 | "func-call-spacing": "error", 63 | "func-name-matching": "error", 64 | "func-names": "off", 65 | 66 | "func-style": ["error", "declaration", { 67 | allowArrowFunctions: true, 68 | }], 69 | 70 | "generator-star-spacing": "error", 71 | "global-require": "off", 72 | "guard-for-in": "error", 73 | "handle-callback-err": "off", 74 | "id-blacklist": "error", 75 | "id-length": "off", 76 | "id-match": "error", 77 | "import/extensions": "off", 78 | "init-declarations": "off", 79 | "jsx-quotes": "error", 80 | "key-spacing": "error", 81 | 82 | "keyword-spacing": ["error", { 83 | after: true, 84 | before: true, 85 | }], 86 | 87 | "line-comment-position": "off", 88 | "linebreak-style": ["error", "unix"], 89 | "lines-around-directive": "error", 90 | "max-depth": "error", 91 | "max-len": "off", 92 | "max-lines": "off", 93 | "max-nested-callbacks": "error", 94 | "max-params": "off", 95 | "max-statements": "off", 96 | "max-statements-per-line": "off", 97 | "multiline-ternary": "off", 98 | "new-cap": "off", 99 | "new-parens": "error", 100 | "newline-after-var": "off", 101 | "newline-before-return": "off", 102 | "newline-per-chained-call": "off", 103 | "no-alert": "error", 104 | "no-array-constructor": "error", 105 | "no-await-in-loop": "error", 106 | "no-bitwise": "off", 107 | "no-caller": "error", 108 | "no-catch-shadow": "off", 109 | "no-compare-neg-zero": "error", 110 | "no-confusing-arrow": "error", 111 | "no-continue": "off", 112 | "no-div-regex": "error", 113 | "no-duplicate-imports": "off", 114 | "no-else-return": "off", 115 | "no-empty-function": "off", 116 | "no-eq-null": "off", 117 | "no-eval": "error", 118 | "no-extend-native": "error", 119 | "no-extra-bind": "error", 120 | "no-extra-label": "error", 121 | "no-extra-parens": "off", 122 | "no-floating-decimal": "error", 123 | "no-implicit-globals": "error", 124 | "no-implied-eval": "error", 125 | "no-inline-comments": "off", 126 | "no-inner-declarations": ["error", "functions"], 127 | "no-invalid-this": "off", 128 | "no-iterator": "error", 129 | "no-label-var": "error", 130 | "no-labels": "error", 131 | "no-lone-blocks": "error", 132 | "no-lonely-if": "error", 133 | "no-loop-func": "error", 134 | "no-magic-numbers": "off", 135 | "no-mixed-requires": "error", 136 | "no-multi-assign": "off", 137 | "no-multi-spaces": "error", 138 | "no-multi-str": "error", 139 | "no-multiple-empty-lines": "error", 140 | "no-native-reassign": "error", 141 | "no-negated-condition": "off", 142 | "no-negated-in-lhs": "error", 143 | "no-nested-ternary": "error", 144 | "no-new": "error", 145 | "no-new-func": "error", 146 | "no-new-object": "error", 147 | "no-new-require": "error", 148 | "no-new-wrappers": "error", 149 | "no-octal-escape": "error", 150 | "no-param-reassign": "off", 151 | "no-path-concat": "error", 152 | 153 | "no-plusplus": ["error", { 154 | allowForLoopAfterthoughts: true, 155 | }], 156 | 157 | "no-process-env": "off", 158 | "no-process-exit": "error", 159 | "no-proto": "error", 160 | "no-prototype-builtins": "off", 161 | "no-restricted-globals": "error", 162 | "no-restricted-imports": "error", 163 | "no-restricted-modules": "error", 164 | "no-restricted-properties": "error", 165 | "no-restricted-syntax": "error", 166 | "no-return-assign": "error", 167 | "no-return-await": "error", 168 | "no-script-url": "error", 169 | "no-self-compare": "error", 170 | "no-sequences": "error", 171 | "no-shadow": "off", 172 | "no-shadow-restricted-names": "error", 173 | "no-spaced-func": "error", 174 | "no-sync": "error", 175 | "no-tabs": "error", 176 | "no-template-curly-in-string": "error", 177 | "no-ternary": "off", 178 | "no-throw-literal": "error", 179 | "no-trailing-spaces": "error", 180 | "no-undef-init": "error", 181 | "no-undefined": "off", 182 | "no-underscore-dangle": "off", 183 | "no-unmodified-loop-condition": "error", 184 | "no-unneeded-ternary": "error", 185 | "no-unused-expressions": "error", 186 | 187 | "no-unused-vars": ["error", { 188 | args: "none", 189 | }], 190 | 191 | "no-use-before-define": "off", 192 | "no-useless-call": "error", 193 | "no-useless-computed-key": "error", 194 | "no-useless-concat": "error", 195 | "no-useless-constructor": "error", 196 | "no-useless-escape": "off", 197 | "no-useless-rename": "error", 198 | "no-useless-return": "error", 199 | "no-var": "off", 200 | "no-void": "error", 201 | "no-warning-comments": "error", 202 | "no-whitespace-before-property": "error", 203 | "no-with": "error", 204 | "nonblock-statement-body-position": "error", 205 | "object-curly-newline": "off", 206 | "object-curly-spacing": ["error", "never"], 207 | "object-property-newline": "off", 208 | "object-shorthand": "off", 209 | "one-var": "off", 210 | "one-var-declaration-per-line": "error", 211 | "operator-assignment": ["error", "always"], 212 | "operator-linebreak": "off", 213 | "padded-blocks": "off", 214 | "prefer-arrow-callback": "off", 215 | "prefer-const": "error", 216 | 217 | "prefer-destructuring": ["error", { 218 | array: false, 219 | object: false, 220 | }], 221 | 222 | "prefer-numeric-literals": "error", 223 | "prefer-promise-reject-errors": "error", 224 | "prefer-reflect": "off", 225 | "prefer-rest-params": "off", 226 | "prefer-spread": "off", 227 | "prefer-template": "off", 228 | "quote-props": "off", 229 | 230 | quotes: ["error", "single", { 231 | avoidEscape: true, 232 | }], 233 | 234 | radix: "error", 235 | "require-await": "error", 236 | "require-jsdoc": "off", 237 | "rest-spread-spacing": "error", 238 | semi: "off", 239 | 240 | "semi-spacing": ["error", { 241 | after: true, 242 | before: false, 243 | }], 244 | 245 | "sort-imports": "off", 246 | "sort-keys": "off", 247 | "sort-vars": "error", 248 | "space-before-blocks": "error", 249 | "space-before-function-paren": "off", 250 | "space-in-parens": ["error", "never"], 251 | "space-infix-ops": "error", 252 | "space-unary-ops": "error", 253 | "spaced-comment": ["error", "always"], 254 | strict: "off", 255 | "symbol-description": "error", 256 | "template-curly-spacing": "error", 257 | "template-tag-spacing": "error", 258 | "unicode-bom": ["error", "never"], 259 | "valid-jsdoc": "off", 260 | "vars-on-top": "off", 261 | "wrap-regex": "off", 262 | "yield-star-spacing": "error", 263 | yoda: ["error", "never"], 264 | }, 265 | }, ...compat.extends( 266 | "eslint:recommended", 267 | "plugin:@typescript-eslint/eslint-recommended", 268 | "plugin:@typescript-eslint/recommended", 269 | "plugin:prettier/recommended", 270 | ).map(config => ({ 271 | ...config, 272 | files: ["**/*.ts"], 273 | })), { 274 | files: ["**/*.ts"], 275 | 276 | plugins: { 277 | "@typescript-eslint": typescriptEslint, 278 | prettier, 279 | }, 280 | 281 | rules: { 282 | "@typescript-eslint/no-use-before-define": 0, 283 | "@typescript-eslint/no-empty-interface": 0, 284 | "@typescript-eslint/no-unused-vars": 0, 285 | "@typescript-eslint/triple-slash-reference": 0, 286 | "@typescript-eslint/ban-ts-comment": "off", 287 | "@typescript-eslint/no-empty-function": 0, 288 | "@typescript-eslint/no-require-imports": 0, 289 | 290 | "@typescript-eslint/naming-convention": ["error", { 291 | selector: "default", 292 | format: ["camelCase", "UPPER_CASE", "PascalCase"], 293 | leadingUnderscore: "allow", 294 | }, { 295 | selector: "property", 296 | format: null, 297 | }], 298 | 299 | "@typescript-eslint/no-explicit-any": 0, 300 | "@typescript-eslint/explicit-function-return-type": "off", 301 | "@typescript-eslint/no-this-alias": "off", 302 | "@typescript-eslint/no-var-requires": 0, 303 | "prefer-rest-params": "off", 304 | }, 305 | }, { 306 | files: ["test/**/*.ts"], 307 | 308 | rules: { 309 | "@typescript-eslint/explicit-function-return-type": "off", 310 | }, 311 | }, { 312 | files: ["examples/cloudflare/**/*.ts", "examples/cloudflare/**/*.js", "examples/cloudflare/**/*.mjs"], 313 | ignores: [], 314 | rules: { 315 | // Disable all rules for cloudflare examples 316 | ...Object.fromEntries( 317 | Object.keys(typescriptEslint.rules).map(rule => [`@typescript-eslint/${rule}`, "off"]) 318 | ), 319 | // Disable all base rules 320 | "no-unused-vars": "off", 321 | "no-undef": "off", 322 | "no-console": "off", 323 | "require-await": "off", 324 | "prettier/prettier": "off", 325 | "func-style": "off", 326 | "no-warning-comments": "off", 327 | "no-constant-condition": "off", 328 | // Add any other rules you want to disable 329 | } 330 | }]; 331 | ``` -------------------------------------------------------------------------------- /python/stripe_agent_toolkit/functions.py: -------------------------------------------------------------------------------- ```python 1 | import stripe 2 | from typing import Optional 3 | from .configuration import Context 4 | 5 | 6 | def create_customer(context: Context, name: str, email: Optional[str] = None): 7 | """ 8 | Create a customer. 9 | 10 | Parameters: 11 | name (str): The name of the customer. 12 | email (str, optional): The email address of the customer. 13 | 14 | Returns: 15 | stripe.Customer: The created customer. 16 | """ 17 | customer_data: dict = {"name": name} 18 | if email: 19 | customer_data["email"] = email 20 | if context.get("account") is not None: 21 | account = context.get("account") 22 | if account is not None: 23 | customer_data["stripe_account"] = account 24 | 25 | customer = stripe.Customer.create(**customer_data) 26 | return {"id": customer.id} 27 | 28 | 29 | def list_customers( 30 | context: Context, 31 | email: Optional[str] = None, 32 | limit: Optional[int] = None, 33 | ): 34 | """ 35 | List Customers. 36 | 37 | Parameters: 38 | email (str, optional): The email address of the customer. 39 | limit (int, optional): The number of customers to return. 40 | 41 | Returns: 42 | stripe.ListObject: A list of customers. 43 | """ 44 | customer_data: dict = {} 45 | if email: 46 | customer_data["email"] = email 47 | if limit: 48 | customer_data["limit"] = limit 49 | if context.get("account") is not None: 50 | account = context.get("account") 51 | if account is not None: 52 | customer_data["stripe_account"] = account 53 | 54 | customers = stripe.Customer.list(**customer_data) 55 | return [{"id": customer.id} for customer in customers.data] 56 | 57 | 58 | def create_product( 59 | context: Context, name: str, description: Optional[str] = None 60 | ): 61 | """ 62 | Create a product. 63 | 64 | Parameters: 65 | name (str): The name of the product. 66 | description (str, optional): The description of the product. 67 | 68 | Returns: 69 | stripe.Product: The created product. 70 | """ 71 | product_data: dict = {"name": name} 72 | if description: 73 | product_data["description"] = description 74 | if context.get("account") is not None: 75 | account = context.get("account") 76 | if account is not None: 77 | product_data["stripe_account"] = account 78 | 79 | return stripe.Product.create(**product_data) 80 | 81 | 82 | def list_products(context: Context, limit: Optional[int] = None): 83 | """ 84 | List Products. 85 | Parameters: 86 | limit (int, optional): The number of products to return. 87 | 88 | Returns: 89 | stripe.ListObject: A list of products. 90 | """ 91 | product_data: dict = {} 92 | if limit: 93 | product_data["limit"] = limit 94 | if context.get("account") is not None: 95 | account = context.get("account") 96 | if account is not None: 97 | product_data["stripe_account"] = account 98 | 99 | return stripe.Product.list(**product_data).data 100 | 101 | 102 | def create_price( 103 | context: Context, product: str, currency: str, unit_amount: int 104 | ): 105 | """ 106 | Create a price. 107 | 108 | Parameters: 109 | product (str): The ID of the product. 110 | currency (str): The currency of the price. 111 | unit_amount (int): The unit amount of the price. 112 | 113 | Returns: 114 | stripe.Price: The created price. 115 | """ 116 | price_data: dict = { 117 | "product": product, 118 | "currency": currency, 119 | "unit_amount": unit_amount, 120 | } 121 | if context.get("account") is not None: 122 | account = context.get("account") 123 | if account is not None: 124 | price_data["stripe_account"] = account 125 | 126 | return stripe.Price.create(**price_data) 127 | 128 | 129 | def list_prices( 130 | context: Context, 131 | product: Optional[str] = None, 132 | limit: Optional[int] = None, 133 | ): 134 | """ 135 | List Prices. 136 | 137 | Parameters: 138 | product (str, optional): The ID of the product to list prices for. 139 | limit (int, optional): The number of prices to return. 140 | 141 | Returns: 142 | stripe.ListObject: A list of prices. 143 | """ 144 | prices_data: dict = {} 145 | if product: 146 | prices_data["product"] = product 147 | if limit: 148 | prices_data["limit"] = limit 149 | if context.get("account") is not None: 150 | account = context.get("account") 151 | if account is not None: 152 | prices_data["stripe_account"] = account 153 | 154 | return stripe.Price.list(**prices_data).data 155 | 156 | 157 | def create_payment_link(context: Context, price: str, quantity: int, redirect_url: Optional[str] = None): 158 | """ 159 | Create a payment link. 160 | 161 | Parameters: 162 | price (str): The ID of the price. 163 | quantity (int): The quantity of the product. 164 | redirect_url (string, optional): The URL the customer will be redirected to after the purchase is complete. 165 | 166 | Returns: 167 | stripe.PaymentLink: The created payment link. 168 | """ 169 | payment_link_data: dict = { 170 | "line_items": [{"price": price, "quantity": quantity}], 171 | } 172 | if context.get("account") is not None: 173 | account = context.get("account") 174 | if account is not None: 175 | payment_link_data["stripe_account"] = account 176 | 177 | if redirect_url: 178 | payment_link_data["after_completion"] = {"type": "redirect", "redirect": {"url": redirect_url}} 179 | 180 | payment_link = stripe.PaymentLink.create(**payment_link_data) 181 | 182 | return {"id": payment_link.id, "url": payment_link.url} 183 | 184 | 185 | def list_invoices( 186 | context: Context, 187 | customer: Optional[str] = None, 188 | limit: Optional[int] = None, 189 | ): 190 | """ 191 | List invoices. 192 | 193 | Parameters: 194 | customer (str, optional): The ID of the customer. 195 | limit (int, optional): The number of invoices to return. 196 | 197 | Returns: 198 | stripe.ListObject: A list of invoices. 199 | """ 200 | invoice_data: dict = {} 201 | if customer: 202 | invoice_data["customer"] = customer 203 | if limit: 204 | invoice_data["limit"] = limit 205 | if context.get("account") is not None: 206 | account = context.get("account") 207 | if account is not None: 208 | invoice_data["stripe_account"] = account 209 | 210 | return stripe.Invoice.list(**invoice_data).data 211 | 212 | 213 | def create_invoice(context: Context, customer: str, days_until_due: int = 30): 214 | """ 215 | Create an invoice. 216 | 217 | Parameters: 218 | customer (str): The ID of the customer. 219 | days_until_due (int, optional): The number of days until the 220 | invoice is due. 221 | 222 | Returns: 223 | stripe.Invoice: The created invoice. 224 | """ 225 | invoice_data: dict = { 226 | "customer": customer, 227 | "collection_method": "send_invoice", 228 | "days_until_due": days_until_due, 229 | } 230 | if context.get("account") is not None: 231 | account = context.get("account") 232 | if account is not None: 233 | invoice_data["stripe_account"] = account 234 | 235 | invoice = stripe.Invoice.create(**invoice_data) 236 | 237 | return { 238 | "id": invoice.id, 239 | "hosted_invoice_url": invoice.hosted_invoice_url, 240 | "customer": invoice.customer, 241 | "status": invoice.status, 242 | } 243 | 244 | 245 | def create_invoice_item( 246 | context: Context, customer: str, price: str, invoice: str 247 | ): 248 | """ 249 | Create an invoice item. 250 | 251 | Parameters: 252 | customer (str): The ID of the customer. 253 | price (str): The ID of the price. 254 | invoice (str): The ID of the invoice. 255 | 256 | Returns: 257 | stripe.InvoiceItem: The created invoice item. 258 | """ 259 | invoice_item_data: dict = { 260 | "customer": customer, 261 | "price": price, 262 | "invoice": invoice, 263 | } 264 | if context.get("account") is not None: 265 | account = context.get("account") 266 | if account is not None: 267 | invoice_item_data["stripe_account"] = account 268 | 269 | invoice_item = stripe.InvoiceItem.create(**invoice_item_data) 270 | 271 | return {"id": invoice_item.id, "invoice": invoice_item.invoice} 272 | 273 | 274 | def finalize_invoice(context: Context, invoice: str): 275 | """ 276 | Finalize an invoice. 277 | 278 | Parameters: 279 | invoice (str): The ID of the invoice. 280 | 281 | Returns: 282 | stripe.Invoice: The finalized invoice. 283 | """ 284 | invoice_data: dict = {"invoice": invoice} 285 | if context.get("account") is not None: 286 | account = context.get("account") 287 | if account is not None: 288 | invoice_data["stripe_account"] = account 289 | 290 | invoice_object = stripe.Invoice.finalize_invoice(**invoice_data) 291 | 292 | return { 293 | "id": invoice_object.id, 294 | "hosted_invoice_url": invoice_object.hosted_invoice_url, 295 | "customer": invoice_object.customer, 296 | "status": invoice_object.status, 297 | } 298 | 299 | 300 | def retrieve_balance( 301 | context: Context, 302 | ): 303 | """ 304 | Retrieve the balance. 305 | 306 | Returns: 307 | stripe.Balance: The balance. 308 | """ 309 | balance_data: dict = {} 310 | if context.get("account") is not None: 311 | account = context.get("account") 312 | if account is not None: 313 | balance_data["stripe_account"] = account 314 | 315 | return stripe.Balance.retrieve(**balance_data) 316 | 317 | 318 | def create_refund( 319 | context: Context, payment_intent: str, amount: Optional[int] = None 320 | ): 321 | """ 322 | Create a refund. 323 | 324 | Parameters: 325 | payment_intent (str): The ID of the payment intent. 326 | amount (int, optional): The amount to refund in cents. 327 | 328 | Returns: 329 | stripe.Refund: The created refund. 330 | """ 331 | refund_data: dict = { 332 | "payment_intent": payment_intent, 333 | } 334 | if amount: 335 | refund_data["amount"] = amount 336 | if context.get("account") is not None: 337 | account = context.get("account") 338 | if account is not None: 339 | refund_data["stripe_account"] = account 340 | 341 | return stripe.Refund.create(**refund_data) 342 | 343 | def list_payment_intents(context: Context, customer: Optional[str] = None, limit: Optional[int] = None): 344 | """ 345 | List payment intents. 346 | 347 | Parameters: 348 | customer (str, optional): The ID of the customer to list payment intents for. 349 | limit (int, optional): The number of payment intents to return. 350 | 351 | Returns: 352 | stripe.ListObject: A list of payment intents. 353 | """ 354 | payment_intent_data: dict = {} 355 | if customer: 356 | payment_intent_data["customer"] = customer 357 | if limit: 358 | payment_intent_data["limit"] = limit 359 | if context.get("account") is not None: 360 | account = context.get("account") 361 | if account is not None: 362 | payment_intent_data["stripe_account"] = account 363 | 364 | return stripe.PaymentIntent.list(**payment_intent_data).data 365 | 366 | def create_billing_portal_session(context: Context, customer: str, return_url: Optional[str] = None): 367 | """ 368 | Creates a session of the customer portal. 369 | 370 | Parameters: 371 | customer (str): The ID of the customer to list payment intents for. 372 | return_url (str, optional): The URL to return to after the session is complete. 373 | 374 | Returns: 375 | stripe.BillingPortalSession: The created billing portal session. 376 | """ 377 | billing_portal_session_data: dict = { 378 | "customer": customer, 379 | } 380 | if return_url: 381 | billing_portal_session_data["return_url"] = return_url 382 | if context.get("account") is not None: 383 | account = context.get("account") 384 | if account is not None: 385 | billing_portal_session_data["stripe_account"] = account 386 | 387 | session_object = stripe.billing_portal.Session.create(**billing_portal_session_data) 388 | 389 | return { 390 | "id": session_object.id, 391 | "customer": session_object.customer, 392 | "url": session_object.url, 393 | } 394 | ``` -------------------------------------------------------------------------------- /typescript/examples/cloudflare/src/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | // From: https://github.com/cloudflare/ai/blob/main/demos/remote-mcp-server/src/utils.ts 2 | 3 | // Helper to generate the layout 4 | import {html, raw} from 'hono/html'; 5 | import type {HtmlEscapedString} from 'hono/utils/html'; 6 | import type {AuthRequest} from '@cloudflare/workers-oauth-provider'; 7 | 8 | // This file mainly exists as a dumping ground for uninteresting html and CSS 9 | // to remove clutter and noise from the auth logic. You likely do not need 10 | // anything from this file. 11 | 12 | export const layout = ( 13 | content: HtmlEscapedString | string, 14 | title: string 15 | ) => html` 16 | <!DOCTYPE html> 17 | <html lang="en"> 18 | <head> 19 | <meta charset="UTF-8" /> 20 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 21 | <title>${title}</title> 22 | <script src="https://cdn.tailwindcss.com"></script> 23 | <script> 24 | tailwind.config = { 25 | theme: { 26 | extend: { 27 | colors: { 28 | primary: '#3498db', 29 | secondary: '#2ecc71', 30 | accent: '#f39c12', 31 | }, 32 | fontFamily: { 33 | sans: ['Inter', 'system-ui', 'sans-serif'], 34 | heading: ['Roboto', 'system-ui', 'sans-serif'], 35 | }, 36 | }, 37 | }, 38 | }; 39 | </script> 40 | <style> 41 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap'); 42 | 43 | /* Custom styling for markdown content */ 44 | .markdown h1 { 45 | font-size: 2.25rem; 46 | font-weight: 700; 47 | font-family: 'Roboto', system-ui, sans-serif; 48 | color: #1a202c; 49 | margin-bottom: 1rem; 50 | line-height: 1.2; 51 | } 52 | 53 | .markdown h2 { 54 | font-size: 1.5rem; 55 | font-weight: 600; 56 | font-family: 'Roboto', system-ui, sans-serif; 57 | color: #2d3748; 58 | margin-top: 1.5rem; 59 | margin-bottom: 0.75rem; 60 | line-height: 1.3; 61 | } 62 | 63 | .markdown h3 { 64 | font-size: 1.25rem; 65 | font-weight: 600; 66 | font-family: 'Roboto', system-ui, sans-serif; 67 | color: #2d3748; 68 | margin-top: 1.25rem; 69 | margin-bottom: 0.5rem; 70 | } 71 | 72 | .markdown p { 73 | font-size: 1.125rem; 74 | color: #4a5568; 75 | margin-bottom: 1rem; 76 | line-height: 1.6; 77 | } 78 | 79 | .markdown a { 80 | color: #3498db; 81 | font-weight: 500; 82 | text-decoration: none; 83 | } 84 | 85 | .markdown a:hover { 86 | text-decoration: underline; 87 | } 88 | 89 | .markdown blockquote { 90 | border-left: 4px solid #f39c12; 91 | padding-left: 1rem; 92 | padding-top: 0.75rem; 93 | padding-bottom: 0.75rem; 94 | margin-top: 1.5rem; 95 | margin-bottom: 1.5rem; 96 | background-color: #fffbeb; 97 | font-style: italic; 98 | } 99 | 100 | .markdown blockquote p { 101 | margin-bottom: 0.25rem; 102 | } 103 | 104 | .markdown ul, 105 | .markdown ol { 106 | margin-top: 1rem; 107 | margin-bottom: 1rem; 108 | margin-left: 1.5rem; 109 | font-size: 1.125rem; 110 | color: #4a5568; 111 | } 112 | 113 | .markdown li { 114 | margin-bottom: 0.5rem; 115 | } 116 | 117 | .markdown ul li { 118 | list-style-type: disc; 119 | } 120 | 121 | .markdown ol li { 122 | list-style-type: decimal; 123 | } 124 | 125 | .markdown pre { 126 | background-color: #f7fafc; 127 | padding: 1rem; 128 | border-radius: 0.375rem; 129 | margin-top: 1rem; 130 | margin-bottom: 1rem; 131 | overflow-x: auto; 132 | } 133 | 134 | .markdown code { 135 | font-family: monospace; 136 | font-size: 0.875rem; 137 | background-color: #f7fafc; 138 | padding: 0.125rem 0.25rem; 139 | border-radius: 0.25rem; 140 | } 141 | 142 | .markdown pre code { 143 | background-color: transparent; 144 | padding: 0; 145 | } 146 | </style> 147 | </head> 148 | <body 149 | class="bg-gray-50 text-gray-800 font-sans leading-relaxed flex flex-col min-h-screen" 150 | > 151 | <header class="bg-white shadow-sm mb-8"> 152 | <div 153 | class="container mx-auto px-4 py-4 flex justify-between items-center" 154 | > 155 | <a 156 | href="/" 157 | class="text-xl font-heading font-bold text-primary hover:text-primary/80 transition-colors" 158 | >MCP Remote Auth Demo</a 159 | > 160 | </div> 161 | </header> 162 | <main class="container mx-auto px-4 pb-12 flex-grow">${content}</main> 163 | <footer class="bg-gray-100 py-6 mt-12"> 164 | <div class="container mx-auto px-4 text-center text-gray-600"> 165 | <p> 166 | © ${new Date().getFullYear()} MCP Remote Auth Demo. All rights 167 | reserved. 168 | </p> 169 | </div> 170 | </footer> 171 | </body> 172 | </html> 173 | `; 174 | 175 | export const homeContent = async (req: Request): Promise<HtmlEscapedString> => { 176 | return html` 177 | <div class="max-w-4xl mx-auto markdown">Example Paid MCP Server</div> 178 | `; 179 | }; 180 | 181 | export const renderLoggedInAuthorizeScreen = async ( 182 | oauthScopes: {name: string; description: string}[], 183 | oauthReqInfo: AuthRequest 184 | ) => { 185 | return html` 186 | <div class="max-w-md mx-auto bg-white p-8 rounded-lg shadow-md"> 187 | <h1 class="text-2xl font-heading font-bold mb-6 text-gray-900"> 188 | Authorization Request 189 | </h1> 190 | 191 | <div class="mb-8"> 192 | <h2 class="text-lg font-semibold mb-3 text-gray-800"> 193 | MCP Remote Auth Demo would like permission to: 194 | </h2> 195 | <ul class="space-y-2"> 196 | ${oauthScopes.map( 197 | (scope) => html` 198 | <li class="flex items-start"> 199 | <span class="inline-block mr-2 mt-1 text-secondary">✓</span> 200 | <div> 201 | <p class="font-medium">${scope.name}</p> 202 | <p class="text-gray-600 text-sm">${scope.description}</p> 203 | </div> 204 | </li> 205 | ` 206 | )} 207 | </ul> 208 | </div> 209 | <form action="/approve" method="POST" class="space-y-4"> 210 | <input 211 | type="hidden" 212 | name="oauthReqInfo" 213 | value="${JSON.stringify(oauthReqInfo)}" 214 | /> 215 | <input 216 | name="email" 217 | value="[email protected]" 218 | required 219 | placeholder="Enter email" 220 | class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary" 221 | /> 222 | <button 223 | type="submit" 224 | name="action" 225 | value="approve" 226 | class="w-full py-3 px-4 bg-secondary text-white rounded-md font-medium hover:bg-secondary/90 transition-colors" 227 | > 228 | Approve 229 | </button> 230 | <button 231 | type="submit" 232 | name="action" 233 | value="reject" 234 | class="w-full py-3 px-4 border border-gray-300 text-gray-700 rounded-md font-medium hover:bg-gray-50 transition-colors" 235 | > 236 | Reject 237 | </button> 238 | </form> 239 | </div> 240 | `; 241 | }; 242 | 243 | export const renderLoggedOutAuthorizeScreen = async ( 244 | oauthScopes: {name: string; description: string}[], 245 | oauthReqInfo: AuthRequest 246 | ) => { 247 | return html` 248 | <div class="max-w-md mx-auto bg-white p-8 rounded-lg shadow-md"> 249 | <h1 class="text-2xl font-heading font-bold mb-6 text-gray-900"> 250 | Authorization Request 251 | </h1> 252 | 253 | <div class="mb-8"> 254 | <h2 class="text-lg font-semibold mb-3 text-gray-800"> 255 | MCP Remote Auth Demo would like permission to: 256 | </h2> 257 | <ul class="space-y-2"> 258 | ${oauthScopes.map( 259 | (scope) => html` 260 | <li class="flex items-start"> 261 | <span class="inline-block mr-2 mt-1 text-secondary">✓</span> 262 | <div> 263 | <p class="font-medium">${scope.name}</p> 264 | <p class="text-gray-600 text-sm">${scope.description}</p> 265 | </div> 266 | </li> 267 | ` 268 | )} 269 | </ul> 270 | </div> 271 | <form action="/approve" method="POST" class="space-y-4"> 272 | <input 273 | type="hidden" 274 | name="oauthReqInfo" 275 | value="${JSON.stringify(oauthReqInfo)}" 276 | /> 277 | <div class="space-y-4"> 278 | <div> 279 | <label 280 | for="email" 281 | class="block text-sm font-medium text-gray-700 mb-1" 282 | >Email</label 283 | > 284 | <input 285 | type="email" 286 | id="email" 287 | name="email" 288 | required 289 | class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary" 290 | /> 291 | </div> 292 | <div> 293 | <label 294 | for="password" 295 | class="block text-sm font-medium text-gray-700 mb-1" 296 | >Password</label 297 | > 298 | <input 299 | type="password" 300 | id="password" 301 | name="password" 302 | required 303 | class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary" 304 | /> 305 | </div> 306 | </div> 307 | <button 308 | type="submit" 309 | name="action" 310 | value="login_approve" 311 | class="w-full py-3 px-4 bg-primary text-white rounded-md font-medium hover:bg-primary/90 transition-colors" 312 | > 313 | Log in and Approve 314 | </button> 315 | <button 316 | type="submit" 317 | name="action" 318 | value="reject" 319 | class="w-full py-3 px-4 border border-gray-300 text-gray-700 rounded-md font-medium hover:bg-gray-50 transition-colors" 320 | > 321 | Reject 322 | </button> 323 | </form> 324 | </div> 325 | `; 326 | }; 327 | 328 | export const renderApproveContent = async ( 329 | message: string, 330 | status: string, 331 | redirectUrl: string 332 | ) => { 333 | return html` 334 | <div class="max-w-md mx-auto bg-white p-8 rounded-lg shadow-md text-center"> 335 | <div class="mb-4"> 336 | <span 337 | class="inline-block p-3 ${status === 'success' 338 | ? 'bg-green-100 text-green-800' 339 | : 'bg-red-100 text-red-800'} rounded-full" 340 | > 341 | ${status === 'success' ? '✓' : '✗'} 342 | </span> 343 | </div> 344 | <h1 class="text-2xl font-heading font-bold mb-4 text-gray-900"> 345 | ${message} 346 | </h1> 347 | <p class="mb-8 text-gray-600"> 348 | You will be redirected back to the application shortly. 349 | </p> 350 | ${raw(` 351 | <script> 352 | setTimeout(() => { 353 | window.location.href = "${redirectUrl}"; 354 | }, 1000); 355 | </script> 356 | `)} 357 | </div> 358 | `; 359 | }; 360 | 361 | export const renderAuthorizationApprovedContent = async ( 362 | redirectUrl: string 363 | ) => { 364 | return renderApproveContent( 365 | 'Authorization approved!', 366 | 'success', 367 | redirectUrl 368 | ); 369 | }; 370 | 371 | export const renderAuthorizationRejectedContent = async ( 372 | redirectUrl: string 373 | ) => { 374 | return renderApproveContent('Authorization rejected.', 'error', redirectUrl); 375 | }; 376 | 377 | export const parseApproveFormBody = async (body: { 378 | [x: string]: string | File; 379 | }) => { 380 | const action = body.action as string; 381 | const email = body.email as string; 382 | const password = body.password as string; 383 | let oauthReqInfo: AuthRequest | null = null; 384 | try { 385 | oauthReqInfo = JSON.parse(body.oauthReqInfo as string) as AuthRequest; 386 | } catch (e) { 387 | oauthReqInfo = null; 388 | } 389 | 390 | return {action, oauthReqInfo, email, password}; 391 | }; 392 | 393 | export const renderPaymentSuccessContent = 394 | async (): Promise<HtmlEscapedString> => { 395 | return html` 396 | <div 397 | class="max-w-md mx-auto bg-white p-8 rounded-lg shadow-md text-center" 398 | > 399 | <h1 class="text-2xl font-heading font-bold mb-4 text-gray-900"> 400 | Payment Successful! 401 | </h1> 402 | <p class="mb-8 text-gray-600"> 403 | You can return to the MCP client now and rerun the tool. 404 | </p> 405 | </div> 406 | `; 407 | }; 408 | ``` -------------------------------------------------------------------------------- /python/tests/test_functions.py: -------------------------------------------------------------------------------- ```python 1 | import unittest 2 | import stripe 3 | from unittest import mock 4 | from stripe_agent_toolkit.functions import ( 5 | create_customer, 6 | list_customers, 7 | create_product, 8 | list_products, 9 | create_price, 10 | list_prices, 11 | create_payment_link, 12 | list_invoices, 13 | create_invoice, 14 | create_invoice_item, 15 | finalize_invoice, 16 | retrieve_balance, 17 | create_refund, 18 | list_payment_intents, 19 | create_billing_portal_session, 20 | ) 21 | 22 | 23 | class TestStripeFunctions(unittest.TestCase): 24 | def test_create_customer(self): 25 | with mock.patch("stripe.Customer.create") as mock_function: 26 | mock_customer = {"id": "cus_123"} 27 | mock_function.return_value = stripe.Customer.construct_from( 28 | mock_customer, "sk_test_123" 29 | ) 30 | 31 | result = create_customer( 32 | context={}, name="Test User", email="[email protected]" 33 | ) 34 | 35 | mock_function.assert_called_with( 36 | name="Test User", email="[email protected]" 37 | ) 38 | 39 | self.assertEqual(result, {"id": mock_customer["id"]}) 40 | 41 | def test_create_customer_with_context(self): 42 | with mock.patch("stripe.Customer.create") as mock_function: 43 | mock_customer = {"id": "cus_123"} 44 | mock_function.return_value = stripe.Customer.construct_from( 45 | mock_customer, "sk_test_123" 46 | ) 47 | 48 | result = create_customer( 49 | context={"account": "acct_123"}, 50 | name="Test User", 51 | email="[email protected]", 52 | ) 53 | 54 | mock_function.assert_called_with( 55 | name="Test User", 56 | email="[email protected]", 57 | stripe_account="acct_123", 58 | ) 59 | 60 | self.assertEqual(result, {"id": mock_customer["id"]}) 61 | 62 | def test_list_customers(self): 63 | with mock.patch("stripe.Customer.list") as mock_function: 64 | mock_customers = [{"id": "cus_123"}, {"id": "cus_456"}] 65 | 66 | mock_function.return_value = stripe.ListObject.construct_from( 67 | { 68 | "object": "list", 69 | "data": [ 70 | stripe.Customer.construct_from( 71 | { 72 | "id": "cus_123", 73 | "email": "[email protected]", 74 | "name": "Customer One", 75 | }, 76 | "sk_test_123", 77 | ), 78 | stripe.Customer.construct_from( 79 | { 80 | "id": "cus_456", 81 | "email": "[email protected]", 82 | "name": "Customer Two", 83 | }, 84 | "sk_test_123", 85 | ), 86 | ], 87 | "has_more": False, 88 | "url": "/v1/customers", 89 | }, 90 | "sk_test_123", 91 | ) 92 | 93 | result = list_customers(context={}) 94 | 95 | mock_function.assert_called_with() 96 | 97 | self.assertEqual(result, mock_customers) 98 | 99 | def test_list_customers_with_context(self): 100 | with mock.patch("stripe.Customer.list") as mock_function: 101 | mock_customers = [{"id": "cus_123"}, {"id": "cus_456"}] 102 | 103 | mock_function.return_value = stripe.ListObject.construct_from( 104 | { 105 | "object": "list", 106 | "data": [ 107 | stripe.Customer.construct_from( 108 | { 109 | "id": "cus_123", 110 | "email": "[email protected]", 111 | "name": "Customer One", 112 | }, 113 | "sk_test_123", 114 | ), 115 | stripe.Customer.construct_from( 116 | { 117 | "id": "cus_456", 118 | "email": "[email protected]", 119 | "name": "Customer Two", 120 | }, 121 | "sk_test_123", 122 | ), 123 | ], 124 | "has_more": False, 125 | "url": "/v1/customers", 126 | }, 127 | "sk_test_123", 128 | ) 129 | 130 | result = list_customers(context={"account": "acct_123"}) 131 | 132 | mock_function.assert_called_with( 133 | stripe_account="acct_123", 134 | ) 135 | 136 | self.assertEqual(result, mock_customers) 137 | 138 | def test_create_product(self): 139 | with mock.patch("stripe.Product.create") as mock_function: 140 | mock_product = {"id": "prod_123"} 141 | mock_function.return_value = stripe.Product.construct_from( 142 | mock_product, "sk_test_123" 143 | ) 144 | 145 | result = create_product(context={}, name="Test Product") 146 | 147 | mock_function.assert_called_with( 148 | name="Test Product", 149 | ) 150 | 151 | self.assertEqual(result, {"id": mock_product["id"]}) 152 | 153 | def test_create_product_with_context(self): 154 | with mock.patch("stripe.Product.create") as mock_function: 155 | mock_product = {"id": "prod_123"} 156 | mock_function.return_value = stripe.Product.construct_from( 157 | mock_product, "sk_test_123" 158 | ) 159 | 160 | result = create_product( 161 | context={"account": "acct_123"}, name="Test Product" 162 | ) 163 | 164 | mock_function.assert_called_with( 165 | name="Test Product", stripe_account="acct_123" 166 | ) 167 | 168 | self.assertEqual(result, {"id": mock_product["id"]}) 169 | 170 | def test_list_products(self): 171 | with mock.patch("stripe.Product.list") as mock_function: 172 | mock_products = [ 173 | {"id": "prod_123", "name": "Product One"}, 174 | {"id": "prod_456", "name": "Product Two"}, 175 | ] 176 | 177 | mock_function.return_value = stripe.ListObject.construct_from( 178 | { 179 | "object": "list", 180 | "data": [ 181 | stripe.Product.construct_from( 182 | { 183 | "id": "prod_123", 184 | "name": "Product One", 185 | }, 186 | "sk_test_123", 187 | ), 188 | stripe.Product.construct_from( 189 | { 190 | "id": "prod_456", 191 | "name": "Product Two", 192 | }, 193 | "sk_test_123", 194 | ), 195 | ], 196 | "has_more": False, 197 | "url": "/v1/products", 198 | }, 199 | "sk_test_123", 200 | ) 201 | 202 | result = list_products(context={}) 203 | 204 | mock_function.assert_called_with() 205 | 206 | self.assertEqual(result, mock_products) 207 | 208 | def test_create_price(self): 209 | with mock.patch("stripe.Price.create") as mock_function: 210 | mock_price = {"id": "price_123"} 211 | mock_function.return_value = stripe.Price.construct_from( 212 | mock_price, "sk_test_123" 213 | ) 214 | 215 | result = create_price( 216 | context={}, 217 | product="prod_123", 218 | currency="usd", 219 | unit_amount=1000, 220 | ) 221 | 222 | mock_function.assert_called_with( 223 | product="prod_123", 224 | currency="usd", 225 | unit_amount=1000, 226 | ) 227 | 228 | self.assertEqual(result, {"id": mock_price["id"]}) 229 | 230 | def test_create_price_with_context(self): 231 | with mock.patch("stripe.Price.create") as mock_function: 232 | mock_price = {"id": "price_123"} 233 | mock_function.return_value = stripe.Price.construct_from( 234 | mock_price, "sk_test_123" 235 | ) 236 | 237 | result = create_price( 238 | context={"account": "acct_123"}, 239 | product="prod_123", 240 | currency="usd", 241 | unit_amount=1000, 242 | ) 243 | 244 | mock_function.assert_called_with( 245 | product="prod_123", 246 | currency="usd", 247 | unit_amount=1000, 248 | stripe_account="acct_123", 249 | ) 250 | 251 | self.assertEqual(result, {"id": mock_price["id"]}) 252 | 253 | def test_list_prices(self): 254 | with mock.patch("stripe.Price.list") as mock_function: 255 | mock_prices = [ 256 | {"id": "price_123", "product": "prod_123"}, 257 | {"id": "price_456", "product": "prod_456"}, 258 | ] 259 | 260 | mock_function.return_value = stripe.ListObject.construct_from( 261 | { 262 | "object": "list", 263 | "data": [ 264 | stripe.Price.construct_from( 265 | { 266 | "id": "price_123", 267 | "product": "prod_123", 268 | }, 269 | "sk_test_123", 270 | ), 271 | stripe.Price.construct_from( 272 | { 273 | "id": "price_456", 274 | "product": "prod_456", 275 | }, 276 | "sk_test_123", 277 | ), 278 | ], 279 | "has_more": False, 280 | "url": "/v1/prices", 281 | }, 282 | "sk_test_123", 283 | ) 284 | 285 | result = list_prices({}) 286 | 287 | mock_function.assert_called_with() 288 | 289 | self.assertEqual(result, mock_prices) 290 | 291 | def test_list_prices_with_context(self): 292 | with mock.patch("stripe.Price.list") as mock_function: 293 | mock_prices = [ 294 | {"id": "price_123", "product": "prod_123"}, 295 | {"id": "price_456", "product": "prod_456"}, 296 | ] 297 | 298 | mock_function.return_value = stripe.ListObject.construct_from( 299 | { 300 | "object": "list", 301 | "data": [ 302 | stripe.Price.construct_from( 303 | { 304 | "id": "price_123", 305 | "product": "prod_123", 306 | }, 307 | "sk_test_123", 308 | ), 309 | stripe.Price.construct_from( 310 | { 311 | "id": "price_456", 312 | "product": "prod_456", 313 | }, 314 | "sk_test_123", 315 | ), 316 | ], 317 | "has_more": False, 318 | "url": "/v1/prices", 319 | }, 320 | "sk_test_123", 321 | ) 322 | 323 | result = list_prices({"account": "acct_123"}) 324 | 325 | mock_function.assert_called_with(stripe_account="acct_123") 326 | 327 | self.assertEqual(result, mock_prices) 328 | 329 | def test_create_payment_link(self): 330 | with mock.patch("stripe.PaymentLink.create") as mock_function: 331 | mock_payment_link = {"id": "pl_123", "url": "https://example.com"} 332 | mock_function.return_value = stripe.PaymentLink.construct_from( 333 | mock_payment_link, "sk_test_123" 334 | ) 335 | 336 | result = create_payment_link( 337 | context={}, price="price_123", quantity=1 338 | ) 339 | 340 | mock_function.assert_called_with( 341 | line_items=[{"price": "price_123", "quantity": 1}], 342 | ) 343 | 344 | self.assertEqual(result, mock_payment_link) 345 | 346 | def test_create_payment_link_with_redirect_url(self): 347 | with mock.patch("stripe.PaymentLink.create") as mock_function: 348 | mock_payment_link = {"id": "pl_123", "url": "https://example.com"} 349 | mock_function.return_value = stripe.PaymentLink.construct_from( 350 | mock_payment_link, "sk_test_123" 351 | ) 352 | 353 | result = create_payment_link( 354 | context={}, price="price_123", quantity=1, redirect_url="https://example.com" 355 | ) 356 | 357 | mock_function.assert_called_with( 358 | line_items=[{"price": "price_123", "quantity": 1, }], 359 | after_completion={"type": "redirect", "redirect": {"url": "https://example.com"}} 360 | ) 361 | 362 | self.assertEqual(result, mock_payment_link) 363 | 364 | def test_create_payment_link_with_context(self): 365 | with mock.patch("stripe.PaymentLink.create") as mock_function: 366 | mock_payment_link = {"id": "pl_123", "url": "https://example.com"} 367 | mock_function.return_value = stripe.PaymentLink.construct_from( 368 | mock_payment_link, "sk_test_123" 369 | ) 370 | 371 | result = create_payment_link( 372 | context={"account": "acct_123"}, price="price_123", quantity=1 373 | ) 374 | 375 | mock_function.assert_called_with( 376 | line_items=[{"price": "price_123", "quantity": 1}], 377 | stripe_account="acct_123", 378 | ) 379 | 380 | self.assertEqual(result, mock_payment_link) 381 | 382 | def test_list_invoices(self): 383 | with mock.patch("stripe.Invoice.list") as mock_function: 384 | mock_invoice = { 385 | "id": "in_123", 386 | "hosted_invoice_url": "https://example.com", 387 | "customer": "cus_123", 388 | "status": "open", 389 | } 390 | mock_invoices = { 391 | "object": "list", 392 | "data": [ 393 | stripe.Invoice.construct_from( 394 | mock_invoice, 395 | "sk_test_123", 396 | ), 397 | ], 398 | "has_more": False, 399 | "url": "/v1/invoices", 400 | } 401 | 402 | mock_function.return_value = stripe.Invoice.construct_from( 403 | mock_invoices, "sk_test_123" 404 | ) 405 | 406 | result = list_invoices(context={}) 407 | 408 | mock_function.assert_called_with() 409 | 410 | self.assertEqual( 411 | result, 412 | [ 413 | { 414 | "id": mock_invoice["id"], 415 | "hosted_invoice_url": mock_invoice["hosted_invoice_url"], 416 | "customer": mock_invoice["customer"], 417 | "status": mock_invoice["status"], 418 | } 419 | ], 420 | ) 421 | 422 | def test_list_invoices_with_customer(self): 423 | with mock.patch("stripe.Invoice.list") as mock_function: 424 | mock_invoice = { 425 | "id": "in_123", 426 | "hosted_invoice_url": "https://example.com", 427 | "customer": "cus_123", 428 | "status": "open", 429 | } 430 | mock_invoices = { 431 | "object": "list", 432 | "data": [ 433 | stripe.Invoice.construct_from( 434 | mock_invoice, 435 | "sk_test_123", 436 | ), 437 | ], 438 | "has_more": False, 439 | "url": "/v1/invoices", 440 | } 441 | 442 | mock_function.return_value = stripe.Invoice.construct_from( 443 | mock_invoices, "sk_test_123" 444 | ) 445 | 446 | result = list_invoices(context={}, customer="cus_123") 447 | 448 | mock_function.assert_called_with( 449 | customer="cus_123", 450 | ) 451 | 452 | self.assertEqual( 453 | result, 454 | [ 455 | { 456 | "id": mock_invoice["id"], 457 | "hosted_invoice_url": mock_invoice["hosted_invoice_url"], 458 | "customer": mock_invoice["customer"], 459 | "status": mock_invoice["status"], 460 | } 461 | ], 462 | ) 463 | 464 | def test_list_invoices_with_customer_and_limit(self): 465 | with mock.patch("stripe.Invoice.list") as mock_function: 466 | mock_invoice = { 467 | "id": "in_123", 468 | "hosted_invoice_url": "https://example.com", 469 | "customer": "cus_123", 470 | "status": "open", 471 | } 472 | mock_invoices = { 473 | "object": "list", 474 | "data": [ 475 | stripe.Invoice.construct_from( 476 | mock_invoice, 477 | "sk_test_123", 478 | ), 479 | ], 480 | "has_more": False, 481 | "url": "/v1/invoices", 482 | } 483 | 484 | mock_function.return_value = stripe.Invoice.construct_from( 485 | mock_invoices, "sk_test_123" 486 | ) 487 | 488 | result = list_invoices(context={}, customer="cus_123", limit=100) 489 | 490 | mock_function.assert_called_with( 491 | customer="cus_123", 492 | limit=100, 493 | ) 494 | 495 | self.assertEqual( 496 | result, 497 | [ 498 | { 499 | "id": mock_invoice["id"], 500 | "hosted_invoice_url": mock_invoice["hosted_invoice_url"], 501 | "customer": mock_invoice["customer"], 502 | "status": mock_invoice["status"], 503 | } 504 | ], 505 | ) 506 | 507 | def test_list_invoices_with_context(self): 508 | with mock.patch("stripe.Invoice.list") as mock_function: 509 | mock_invoice = { 510 | "id": "in_123", 511 | "hosted_invoice_url": "https://example.com", 512 | "customer": "cus_123", 513 | "status": "open", 514 | } 515 | mock_invoices = { 516 | "object": "list", 517 | "data": [ 518 | stripe.Invoice.construct_from( 519 | mock_invoice, 520 | "sk_test_123", 521 | ), 522 | ], 523 | "has_more": False, 524 | "url": "/v1/invoices", 525 | } 526 | 527 | mock_function.return_value = stripe.Invoice.construct_from( 528 | mock_invoices, "sk_test_123" 529 | ) 530 | 531 | result = list_invoices(context={"account": "acct_123"}, customer="cus_123") 532 | 533 | mock_function.assert_called_with( 534 | customer="cus_123", 535 | stripe_account="acct_123", 536 | ) 537 | 538 | self.assertEqual( 539 | result, 540 | [ 541 | { 542 | "id": mock_invoice["id"], 543 | "hosted_invoice_url": mock_invoice["hosted_invoice_url"], 544 | "customer": mock_invoice["customer"], 545 | "status": mock_invoice["status"], 546 | } 547 | ], 548 | ) 549 | 550 | def test_create_invoice(self): 551 | with mock.patch("stripe.Invoice.create") as mock_function: 552 | mock_invoice = { 553 | "id": "in_123", 554 | "hosted_invoice_url": "https://example.com", 555 | "customer": "cus_123", 556 | "status": "open", 557 | } 558 | 559 | mock_function.return_value = stripe.Invoice.construct_from( 560 | mock_invoice, "sk_test_123" 561 | ) 562 | 563 | result = create_invoice(context={}, customer="cus_123") 564 | 565 | mock_function.assert_called_with( 566 | customer="cus_123", 567 | collection_method="send_invoice", 568 | days_until_due=30, 569 | ) 570 | 571 | self.assertEqual( 572 | result, 573 | { 574 | "id": mock_invoice["id"], 575 | "hosted_invoice_url": mock_invoice["hosted_invoice_url"], 576 | "customer": mock_invoice["customer"], 577 | "status": mock_invoice["status"], 578 | }, 579 | ) 580 | 581 | def test_create_invoice_with_context(self): 582 | with mock.patch("stripe.Invoice.create") as mock_function: 583 | mock_invoice = { 584 | "id": "in_123", 585 | "hosted_invoice_url": "https://example.com", 586 | "customer": "cus_123", 587 | "status": "open", 588 | } 589 | 590 | mock_function.return_value = stripe.Invoice.construct_from( 591 | mock_invoice, "sk_test_123" 592 | ) 593 | 594 | result = create_invoice( 595 | context={"account": "acct_123"}, customer="cus_123" 596 | ) 597 | 598 | mock_function.assert_called_with( 599 | customer="cus_123", 600 | collection_method="send_invoice", 601 | days_until_due=30, 602 | stripe_account="acct_123", 603 | ) 604 | 605 | self.assertEqual( 606 | result, 607 | { 608 | "id": mock_invoice["id"], 609 | "hosted_invoice_url": mock_invoice["hosted_invoice_url"], 610 | "customer": mock_invoice["customer"], 611 | "status": mock_invoice["status"], 612 | }, 613 | ) 614 | 615 | def test_create_invoice_item(self): 616 | with mock.patch("stripe.InvoiceItem.create") as mock_function: 617 | mock_invoice_item = {"id": "ii_123", "invoice": "in_123"} 618 | mock_function.return_value = stripe.InvoiceItem.construct_from( 619 | mock_invoice_item, "sk_test_123" 620 | ) 621 | 622 | result = create_invoice_item( 623 | context={}, 624 | customer="cus_123", 625 | price="price_123", 626 | invoice="in_123", 627 | ) 628 | 629 | mock_function.assert_called_with( 630 | customer="cus_123", price="price_123", invoice="in_123" 631 | ) 632 | 633 | self.assertEqual( 634 | result, 635 | { 636 | "id": mock_invoice_item["id"], 637 | "invoice": mock_invoice_item["invoice"], 638 | }, 639 | ) 640 | 641 | def test_create_invoice_item_with_context(self): 642 | with mock.patch("stripe.InvoiceItem.create") as mock_function: 643 | mock_invoice_item = {"id": "ii_123", "invoice": "in_123"} 644 | mock_function.return_value = stripe.InvoiceItem.construct_from( 645 | mock_invoice_item, "sk_test_123" 646 | ) 647 | 648 | result = create_invoice_item( 649 | context={"account": "acct_123"}, 650 | customer="cus_123", 651 | price="price_123", 652 | invoice="in_123", 653 | ) 654 | 655 | mock_function.assert_called_with( 656 | customer="cus_123", 657 | price="price_123", 658 | invoice="in_123", 659 | stripe_account="acct_123", 660 | ) 661 | 662 | self.assertEqual( 663 | result, 664 | { 665 | "id": mock_invoice_item["id"], 666 | "invoice": mock_invoice_item["invoice"], 667 | }, 668 | ) 669 | 670 | def test_finalize_invoice(self): 671 | with mock.patch("stripe.Invoice.finalize_invoice") as mock_function: 672 | mock_invoice = { 673 | "id": "in_123", 674 | "hosted_invoice_url": "https://example.com", 675 | "customer": "cus_123", 676 | "status": "open", 677 | } 678 | 679 | mock_function.return_value = stripe.Invoice.construct_from( 680 | mock_invoice, "sk_test_123" 681 | ) 682 | 683 | result = finalize_invoice(context={}, invoice="in_123") 684 | 685 | mock_function.assert_called_with(invoice="in_123") 686 | 687 | self.assertEqual( 688 | result, 689 | { 690 | "id": mock_invoice["id"], 691 | "hosted_invoice_url": mock_invoice["hosted_invoice_url"], 692 | "customer": mock_invoice["customer"], 693 | "status": mock_invoice["status"], 694 | }, 695 | ) 696 | 697 | def test_finalize_invoice_with_context(self): 698 | with mock.patch("stripe.Invoice.finalize_invoice") as mock_function: 699 | mock_invoice = { 700 | "id": "in_123", 701 | "hosted_invoice_url": "https://example.com", 702 | "customer": "cus_123", 703 | "status": "open", 704 | } 705 | 706 | mock_function.return_value = stripe.Invoice.construct_from( 707 | mock_invoice, "sk_test_123" 708 | ) 709 | 710 | result = finalize_invoice( 711 | context={"account": "acct_123"}, invoice="in_123" 712 | ) 713 | 714 | mock_function.assert_called_with( 715 | invoice="in_123", stripe_account="acct_123" 716 | ) 717 | 718 | self.assertEqual( 719 | result, 720 | { 721 | "id": mock_invoice["id"], 722 | "hosted_invoice_url": mock_invoice["hosted_invoice_url"], 723 | "customer": mock_invoice["customer"], 724 | "status": mock_invoice["status"], 725 | }, 726 | ) 727 | 728 | def test_retrieve_balance(self): 729 | with mock.patch("stripe.Balance.retrieve") as mock_function: 730 | mock_balance = {"available": [{"amount": 1000, "currency": "usd"}]} 731 | 732 | mock_function.return_value = stripe.Balance.construct_from( 733 | mock_balance, "sk_test_123" 734 | ) 735 | 736 | result = retrieve_balance(context={}) 737 | 738 | mock_function.assert_called_with() 739 | 740 | self.assertEqual(result, mock_balance) 741 | 742 | def test_retrieve_balance_with_context(self): 743 | with mock.patch("stripe.Balance.retrieve") as mock_function: 744 | mock_balance = {"available": [{"amount": 1000, "currency": "usd"}]} 745 | 746 | mock_function.return_value = stripe.Balance.construct_from( 747 | mock_balance, "sk_test_123" 748 | ) 749 | 750 | result = retrieve_balance(context={"account": "acct_123"}) 751 | 752 | mock_function.assert_called_with(stripe_account="acct_123") 753 | 754 | self.assertEqual(result, mock_balance) 755 | 756 | def test_create_refund(self): 757 | with mock.patch("stripe.Refund.create") as mock_function: 758 | mock_refund = {"id": "re_123"} 759 | mock_function.return_value = stripe.Refund.construct_from( 760 | mock_refund, "sk_test_123" 761 | ) 762 | 763 | result = create_refund(context={}, payment_intent="pi_123") 764 | 765 | mock_function.assert_called_with(payment_intent="pi_123") 766 | 767 | self.assertEqual(result, {"id": mock_refund["id"]}) 768 | 769 | def test_create_partial_refund(self): 770 | with mock.patch("stripe.Refund.create") as mock_function: 771 | mock_refund = {"id": "re_123"} 772 | mock_function.return_value = stripe.Refund.construct_from( 773 | mock_refund, "sk_test_123" 774 | ) 775 | 776 | result = create_refund( 777 | context={}, payment_intent="pi_123", amount=1000 778 | ) 779 | 780 | mock_function.assert_called_with( 781 | payment_intent="pi_123", amount=1000 782 | ) 783 | 784 | self.assertEqual(result, {"id": mock_refund["id"]}) 785 | 786 | def test_create_refund_with_context(self): 787 | with mock.patch("stripe.Refund.create") as mock_function: 788 | mock_refund = {"id": "re_123"} 789 | mock_function.return_value = stripe.Refund.construct_from( 790 | mock_refund, "sk_test_123" 791 | ) 792 | 793 | result = create_refund( 794 | context={"account": "acct_123"}, 795 | payment_intent="pi_123", 796 | amount=1000, 797 | ) 798 | 799 | mock_function.assert_called_with( 800 | payment_intent="pi_123", amount=1000, stripe_account="acct_123" 801 | ) 802 | 803 | self.assertEqual(result, {"id": mock_refund["id"]}) 804 | 805 | def test_list_payment_intents(self): 806 | with mock.patch("stripe.PaymentIntent.list") as mock_function: 807 | mock_payment_intents = [{"id": "pi_123"}, {"id": "pi_456"}] 808 | mock_function.return_value = stripe.ListObject.construct_from( 809 | {"data": mock_payment_intents}, "sk_test_123" 810 | ) 811 | 812 | result = list_payment_intents(context={}) 813 | 814 | mock_function.assert_called_with() 815 | 816 | self.assertEqual(result, mock_payment_intents) 817 | 818 | def test_list_payment_intents_with_context(self): 819 | with mock.patch("stripe.PaymentIntent.list") as mock_function: 820 | mock_payment_intents = [{"id": "pi_123"}, {"id": "pi_456"}] 821 | mock_function.return_value = stripe.ListObject.construct_from( 822 | {"data": mock_payment_intents}, "sk_test_123" 823 | ) 824 | 825 | result = list_payment_intents(context={"account": "acct_123"}) 826 | 827 | mock_function.assert_called_with(stripe_account="acct_123") 828 | 829 | self.assertEqual(result, mock_payment_intents) 830 | 831 | 832 | def test_create_billing_portal_session(self): 833 | with mock.patch("stripe.billing_portal.Session.create") as mock_function: 834 | mock_billing_portal_session = { 835 | "id": "bps_123", 836 | "url": "https://example.com", 837 | "customer": "cus_123", 838 | "configuration": "bpc_123", 839 | } 840 | mock_function.return_value = stripe.billing_portal.Session.construct_from( 841 | mock_billing_portal_session, "sk_test_123" 842 | ) 843 | 844 | result = create_billing_portal_session(context={}, customer="cus_123") 845 | 846 | mock_function.assert_called_with(customer="cus_123") 847 | 848 | self.assertEqual(result, { 849 | "id": mock_billing_portal_session["id"], 850 | "url": mock_billing_portal_session["url"], 851 | "customer": mock_billing_portal_session["customer"], 852 | }) 853 | 854 | def test_create_billing_portal_session_with_return_url(self): 855 | with mock.patch("stripe.billing_portal.Session.create") as mock_function: 856 | mock_billing_portal_session = { 857 | "id": "bps_123", 858 | "url": "https://example.com", 859 | "customer": "cus_123", 860 | "configuration": "bpc_123", 861 | } 862 | mock_function.return_value = stripe.billing_portal.Session.construct_from( 863 | mock_billing_portal_session, "sk_test_123" 864 | ) 865 | 866 | result = create_billing_portal_session( 867 | context={}, 868 | customer="cus_123", 869 | return_url="http://example.com" 870 | ) 871 | 872 | mock_function.assert_called_with( 873 | customer="cus_123", 874 | return_url="http://example.com", 875 | ) 876 | 877 | self.assertEqual(result, { 878 | "id": mock_billing_portal_session["id"], 879 | "url": mock_billing_portal_session["url"], 880 | "customer": mock_billing_portal_session["customer"], 881 | }) 882 | 883 | def test_create_billing_portal_session_with_context(self): 884 | with mock.patch("stripe.billing_portal.Session.create") as mock_function: 885 | mock_billing_portal_session = { 886 | "id": "bps_123", 887 | "url": "https://example.com", 888 | "customer": "cus_123", 889 | "configuration": "bpc_123", 890 | } 891 | mock_function.return_value = stripe.billing_portal.Session.construct_from( 892 | mock_billing_portal_session, "sk_test_123" 893 | ) 894 | 895 | result = create_billing_portal_session( 896 | context={"account": "acct_123"}, 897 | customer="cus_123", 898 | return_url="http://example.com" 899 | ) 900 | 901 | mock_function.assert_called_with( 902 | customer="cus_123", 903 | return_url="http://example.com", 904 | stripe_account="acct_123" 905 | ) 906 | 907 | self.assertEqual(result, { 908 | "id": mock_billing_portal_session["id"], 909 | "url": mock_billing_portal_session["url"], 910 | "customer": mock_billing_portal_session["customer"], 911 | }) 912 | 913 | if __name__ == "__main__": 914 | unittest.main() 915 | ```