This is page 2 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/prompts.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {listSubscriptionsPrompt} from '@/shared/subscriptions/listSubscriptions'; 2 | import {cancelSubscriptionPrompt} from '@/shared/subscriptions/cancelSubscription'; 3 | import {updateSubscriptionPrompt} from '@/shared/subscriptions/updateSubscription'; 4 | 5 | describe('listSubscriptionsPrompt', () => { 6 | it('should return the correct prompt with no context', () => { 7 | const prompt = listSubscriptionsPrompt({}); 8 | 9 | expect(prompt).toContain('This tool will list all subscriptions in Stripe'); 10 | expect(prompt).toContain('four arguments'); 11 | expect(prompt).toContain('- customer (str, optional)'); 12 | expect(prompt).toContain('- price (str, optional)'); 13 | expect(prompt).toContain('- status (str, optional)'); 14 | expect(prompt).toContain('- limit (int, optional)'); 15 | }); 16 | 17 | it('should return the correct prompt with customer in context', () => { 18 | const prompt = listSubscriptionsPrompt({customer: 'cus_123'}); 19 | 20 | expect(prompt).toContain('This tool will list all subscriptions in Stripe'); 21 | expect(prompt).toContain('three arguments'); 22 | expect(prompt).not.toContain('- customer (str, optional)'); 23 | expect(prompt).toContain('- price (str, optional)'); 24 | expect(prompt).toContain('- status (str, optional)'); 25 | expect(prompt).toContain('- limit (int, optional)'); 26 | }); 27 | }); 28 | 29 | describe('cancelSubscriptionPrompt', () => { 30 | it('should return the correct prompt', () => { 31 | const prompt = cancelSubscriptionPrompt({}); 32 | 33 | expect(prompt).toContain('This tool will cancel a subscription in Stripe'); 34 | expect(prompt).toContain('- subscription (str, required)'); 35 | }); 36 | }); 37 | 38 | describe('updateSubscriptionPrompt', () => { 39 | it('should return the correct prompt', () => { 40 | const prompt = updateSubscriptionPrompt({}); 41 | 42 | expect(prompt).toContain( 43 | 'This tool will update an existing subscription in Stripe' 44 | ); 45 | expect(prompt).toContain('- subscription (str, required)'); 46 | expect(prompt).toContain('- proration_behavior (str, optional)'); 47 | expect(prompt).toContain('- items (array, optional)'); 48 | }); 49 | }); 50 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Bug report 2 | description: Create a report to help us improve 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! 9 | - type: textarea 10 | id: what-happened 11 | attributes: 12 | label: Describe the bug 13 | description: A clear and concise description of what the bug is. 14 | placeholder: Tell us what you see! 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: repro-steps 19 | attributes: 20 | label: To Reproduce 21 | description: Steps to reproduce the behavior 22 | placeholder: | 23 | 1. Fetch a '...' 24 | 2. Update the '....' 25 | 3. See error 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: expected-behavior 30 | attributes: 31 | label: Expected behavior 32 | description: A clear and concise description of what you expected to happen. 33 | validations: 34 | required: true 35 | - type: textarea 36 | id: code-snippets 37 | attributes: 38 | label: Code snippets 39 | description: If applicable, add code snippets to help explain your problem. 40 | render: Python 41 | validations: 42 | required: false 43 | - type: input 44 | id: os 45 | attributes: 46 | label: OS 47 | placeholder: macOS 48 | validations: 49 | required: true 50 | - type: input 51 | id: language-version 52 | attributes: 53 | label: Language version 54 | placeholder: Python 3.10.4 55 | validations: 56 | required: true 57 | - type: input 58 | id: lib-version 59 | attributes: 60 | label: Library version 61 | placeholder: stripe-python v2.73.0 62 | validations: 63 | required: true 64 | - type: input 65 | id: api-version 66 | attributes: 67 | label: API version 68 | description: See [Versioning](https://stripe.com/docs/api/versioning) in the API Reference to find which version you're using 69 | placeholder: "2020-08-27" 70 | validations: 71 | required: true 72 | - type: textarea 73 | id: additional-context 74 | attributes: 75 | label: Additional context 76 | description: Add any other context about the problem here. 77 | validations: 78 | required: false 79 | ``` -------------------------------------------------------------------------------- /typescript/src/shared/disputes/listDisputes.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Stripe from 'stripe'; 2 | import {z} from 'zod'; 3 | import type {Context} from '@/shared/configuration'; 4 | import type {Tool} from '@/shared/tools'; 5 | 6 | export const listDisputesPrompt = (_context: Context = {}) => ` 7 | This tool will fetch a list of disputes in Stripe. 8 | 9 | It takes the following arguments: 10 | - charge (string, optional): Only return disputes associated to the charge specified by this charge ID. 11 | - payment_intent (string, optional): Only return disputes associated to the PaymentIntent specified by this PaymentIntent ID. 12 | `; 13 | 14 | export const listDisputesParameters = (_context: Context = {}) => 15 | z.object({ 16 | charge: z 17 | .string() 18 | .optional() 19 | .describe( 20 | 'Only return disputes associated to the charge specified by this charge ID.' 21 | ), 22 | payment_intent: z 23 | .string() 24 | .optional() 25 | .describe( 26 | 'Only return disputes associated to the PaymentIntent specified by this PaymentIntent ID.' 27 | ), 28 | limit: z 29 | .number() 30 | .int() 31 | .min(1) 32 | .max(100) 33 | .default(10) 34 | .optional() 35 | .describe( 36 | 'A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 10.' 37 | ), 38 | }); 39 | 40 | export const listDisputesAnnotations = () => ({ 41 | destructiveHint: false, 42 | idempotentHint: true, 43 | openWorldHint: true, 44 | readOnlyHint: true, 45 | title: 'List disputes', 46 | }); 47 | 48 | export const listDisputes = async ( 49 | stripe: Stripe, 50 | context: Context, 51 | params: z.infer<ReturnType<typeof listDisputesParameters>> 52 | ) => { 53 | try { 54 | const disputes = await stripe.disputes.list( 55 | params, 56 | context.account ? {stripeAccount: context.account} : undefined 57 | ); 58 | 59 | return disputes.data.map((dispute) => ({id: dispute.id})); 60 | } catch (error) { 61 | return 'Failed to list disputes'; 62 | } 63 | }; 64 | 65 | const tool = (context: Context): Tool => ({ 66 | method: 'list_disputes', 67 | name: 'List Disputes', 68 | description: listDisputesPrompt(context), 69 | parameters: listDisputesParameters(context), 70 | annotations: listDisputesAnnotations(), 71 | actions: { 72 | disputes: { 73 | read: true, 74 | }, 75 | }, 76 | execute: listDisputes, 77 | }); 78 | 79 | export default tool; 80 | ``` -------------------------------------------------------------------------------- /typescript/src/test/shared/invoiceItems/functions.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {createInvoiceItem} from '@/shared/invoiceItems/createInvoiceItem'; 2 | 3 | const Stripe = jest.fn().mockImplementation(() => ({ 4 | invoiceItems: { 5 | create: jest.fn(), 6 | }, 7 | })); 8 | 9 | let stripe: ReturnType<typeof Stripe>; 10 | 11 | beforeEach(() => { 12 | stripe = new Stripe('fake-api-key'); 13 | }); 14 | 15 | describe('createInvoiceItem', () => { 16 | it('should create an invoice item and return it', async () => { 17 | const params = { 18 | customer: 'cus_123456', 19 | price: 'price_123456', 20 | invoice: 'in_123456', 21 | }; 22 | 23 | const mockInvoiceItem = {id: 'ii_123456', invoice: 'in_123456'}; 24 | 25 | const context = {}; 26 | 27 | stripe.invoiceItems.create.mockResolvedValue(mockInvoiceItem); 28 | 29 | const result = await createInvoiceItem(stripe, context, params); 30 | 31 | expect(stripe.invoiceItems.create).toHaveBeenCalledWith(params, undefined); 32 | expect(result).toEqual(mockInvoiceItem); 33 | }); 34 | 35 | it('should specify the connected account if included in context', async () => { 36 | const params = { 37 | customer: 'cus_123456', 38 | price: 'price_123456', 39 | invoice: 'in_123456', 40 | }; 41 | 42 | const mockInvoiceItem = {id: 'ii_123456', invoice: 'in_123456'}; 43 | 44 | const context = { 45 | account: 'acct_123456', 46 | }; 47 | 48 | stripe.invoiceItems.create.mockResolvedValue(mockInvoiceItem); 49 | 50 | const result = await createInvoiceItem(stripe, context, params); 51 | 52 | expect(stripe.invoiceItems.create).toHaveBeenCalledWith(params, { 53 | stripeAccount: context.account, 54 | }); 55 | expect(result).toEqual(mockInvoiceItem); 56 | }); 57 | 58 | it('should create an invoice item with a customer if included in context', async () => { 59 | const params = { 60 | price: 'price_123456', 61 | invoice: 'in_123456', 62 | }; 63 | 64 | const mockInvoiceItem = {id: 'ii_123456', invoice: 'in_123456'}; 65 | 66 | const context = { 67 | customer: 'cus_123456', 68 | }; 69 | 70 | stripe.invoiceItems.create.mockResolvedValue(mockInvoiceItem); 71 | 72 | const result = await createInvoiceItem(stripe, context, params); 73 | 74 | expect(stripe.invoiceItems.create).toHaveBeenCalledWith( 75 | { 76 | ...params, 77 | customer: context.customer, 78 | }, 79 | undefined 80 | ); 81 | expect(result).toEqual(mockInvoiceItem); 82 | }); 83 | }); 84 | ``` -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: {} 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | typescript-build: 14 | name: Build - TypeScript 15 | runs-on: ubuntu-latest 16 | 17 | defaults: 18 | run: 19 | working-directory: ./typescript 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | 25 | - name: pnpm 26 | uses: pnpm/action-setup@v4 27 | with: 28 | version: 9.11.0 29 | 30 | - name: Node 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: "18" 34 | 35 | - name: Install 36 | run: pnpm install --frozen-lockfile 37 | 38 | - name: Build 39 | run: pnpm run build 40 | 41 | - name: Clean 42 | run: pnpm run clean 43 | 44 | - name: Lint 45 | run: pnpm run lint 46 | 47 | - name: Prettier 48 | run: pnpm run prettier-check 49 | 50 | - name: Test 51 | run: pnpm run test 52 | 53 | modelcontextprotocol-build: 54 | name: Build - Model Context Protocol 55 | runs-on: ubuntu-latest 56 | 57 | defaults: 58 | run: 59 | working-directory: ./modelcontextprotocol 60 | 61 | steps: 62 | - name: Checkout 63 | uses: actions/checkout@v3 64 | 65 | - name: pnpm 66 | uses: pnpm/action-setup@v4 67 | with: 68 | version: 9.11.0 69 | 70 | - name: Node 71 | uses: actions/setup-node@v4 72 | with: 73 | node-version: "18" 74 | 75 | - name: Install 76 | run: pnpm install --frozen-lockfile 77 | 78 | - name: Build 79 | run: pnpm run build 80 | 81 | - name: Clean 82 | run: pnpm run clean 83 | 84 | - name: Lint 85 | run: pnpm run lint 86 | 87 | - name: Prettier 88 | run: pnpm run prettier-check 89 | 90 | - name: Test 91 | run: pnpm run test 92 | 93 | python-build: 94 | name: Build - Python 95 | runs-on: ubuntu-latest 96 | 97 | defaults: 98 | run: 99 | working-directory: ./python 100 | 101 | steps: 102 | - name: Checkout 103 | uses: actions/checkout@v3 104 | 105 | - name: Python 106 | uses: actions/setup-python@v4 107 | with: 108 | python-version: "3.11" 109 | 110 | - name: Install 111 | run: make venv 112 | 113 | - name: Build 114 | run: | 115 | set -x 116 | source venv/bin/activate 117 | rm -rf build dist *.egg-info 118 | make build 119 | python -m twine check dist/* 120 | 121 | - name: Test 122 | run: | 123 | make venv 124 | make test 125 | ``` -------------------------------------------------------------------------------- /python/examples/openai/file_search/main.py: -------------------------------------------------------------------------------- ```python 1 | import asyncio 2 | import os 3 | from pydantic import BaseModel, Field 4 | 5 | from dotenv import load_dotenv 6 | load_dotenv() 7 | 8 | from agents import Agent, Runner 9 | from agents.tool import FileSearchTool 10 | 11 | from stripe_agent_toolkit.openai.toolkit import StripeAgentToolkit 12 | 13 | stripe_agent_toolkit = StripeAgentToolkit( 14 | secret_key=os.getenv("STRIPE_SECRET_KEY"), 15 | configuration={ 16 | "actions": { 17 | "customers": { 18 | "create": True, 19 | }, 20 | "products": { 21 | "create": True, 22 | }, 23 | "prices": { 24 | "create": True, 25 | }, 26 | "invoice_items": { 27 | "create": True, 28 | }, 29 | "invoices": { 30 | "create": True, 31 | "update": True, 32 | }, 33 | } 34 | }, 35 | ) 36 | 37 | class InvoiceOutput(BaseModel): 38 | name: str = Field(description="The name of the customer") 39 | email: str = Field(description="The email of the customer") 40 | service: str = Field(description="The service that the customer is invoiced for") 41 | amount_due: int = Field(description="The dollar amount due for the invoice. Convert text to dollar amounts if needed.") 42 | id: str = Field(description="The id of the stripe invoice") 43 | 44 | class InvoiceListOutput(BaseModel): 45 | invoices: list[InvoiceOutput] 46 | 47 | invoice_agent = Agent( 48 | name="Invoice Agent", 49 | instructions="You are an expert at using the Stripe API to create, finalize, and send invoices to customers.", 50 | tools=stripe_agent_toolkit.get_tools(), 51 | ) 52 | 53 | file_search_agent = Agent( 54 | name="File Search Agent", 55 | instructions="You are an expert at searching for financial documents.", 56 | tools=[ 57 | FileSearchTool( 58 | max_num_results=50, 59 | vector_store_ids=[os.getenv("OPENAI_VECTOR_STORE_ID")], 60 | ) 61 | ], 62 | output_type=InvoiceListOutput, 63 | handoffs=[invoice_agent] 64 | ) 65 | 66 | async def main(): 67 | assignment = "Search for all customers that haven't paid across all of my documents. Handoff to the invoice agent to create, finalize, and send an invoice for each." 68 | 69 | outstanding_invoices = await Runner.run( 70 | file_search_agent, 71 | assignment, 72 | ) 73 | 74 | invoices = outstanding_invoices.final_output 75 | 76 | print(invoices) 77 | 78 | if __name__ == "__main__": 79 | asyncio.run(main()) 80 | ``` -------------------------------------------------------------------------------- /typescript/src/shared/paymentIntents/listPaymentIntents.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Stripe from 'stripe'; 2 | import {z} from 'zod'; 3 | import type {Context} from '@/shared/configuration'; 4 | import type {Tool} from '@/shared/tools'; 5 | 6 | export const listPaymentIntentsPrompt = (context: Context = {}) => { 7 | const customerArg = context.customer 8 | ? `The customer is already set in the context: ${context.customer}.` 9 | : `- customer (str, optional): The ID of the customer to list payment intents for.\n`; 10 | 11 | return ` 12 | This tool will list payment intents in Stripe. 13 | 14 | It takes ${context.customer ? 'one' : 'two'} argument${context.customer ? '' : 's'}: 15 | ${customerArg} 16 | - limit (int, optional): The number of payment intents to return. 17 | `; 18 | }; 19 | 20 | export const listPaymentIntents = async ( 21 | stripe: Stripe, 22 | context: Context, 23 | params: z.infer<ReturnType<typeof listPaymentIntentsParameters>> 24 | ) => { 25 | try { 26 | if (context.customer) { 27 | params.customer = context.customer; 28 | } 29 | 30 | const paymentIntents = await stripe.paymentIntents.list( 31 | params, 32 | context.account ? {stripeAccount: context.account} : undefined 33 | ); 34 | 35 | return paymentIntents.data; 36 | } catch (error) { 37 | return 'Failed to list payment intents'; 38 | } 39 | }; 40 | 41 | export const listPaymentIntentsAnnotations = () => ({ 42 | destructiveHint: false, 43 | idempotentHint: true, 44 | openWorldHint: true, 45 | readOnlyHint: true, 46 | title: 'List payment intents', 47 | }); 48 | 49 | export const listPaymentIntentsParameters = ( 50 | context: Context = {} 51 | ): z.AnyZodObject => { 52 | const schema = z.object({ 53 | customer: z 54 | .string() 55 | .optional() 56 | .describe('The ID of the customer to list payment intents for.'), 57 | limit: z 58 | .number() 59 | .int() 60 | .min(1) 61 | .max(100) 62 | .optional() 63 | .describe( 64 | 'A limit on the number of objects to be returned. Limit can range between 1 and 100.' 65 | ), 66 | }); 67 | 68 | if (context.customer) { 69 | return schema.omit({customer: true}); 70 | } else { 71 | return schema; 72 | } 73 | }; 74 | 75 | const tool = (context: Context): Tool => ({ 76 | method: 'list_payment_intents', 77 | name: 'List Payment Intents', 78 | description: listPaymentIntentsPrompt(context), 79 | parameters: listPaymentIntentsParameters(context), 80 | annotations: listPaymentIntentsAnnotations(), 81 | actions: { 82 | paymentIntents: { 83 | read: true, 84 | }, 85 | }, 86 | execute: listPaymentIntents, 87 | }); 88 | 89 | export default tool; 90 | ``` -------------------------------------------------------------------------------- /typescript/src/shared/paymentLinks/createPaymentLink.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Stripe from 'stripe'; 2 | import {z} from 'zod'; 3 | import type {Context} from '@/shared/configuration'; 4 | import type {Tool} from '@/shared/tools'; 5 | 6 | export const createPaymentLinkPrompt = (_context: Context = {}) => ` 7 | This tool will create a payment link in Stripe. 8 | 9 | It takes two arguments: 10 | - price (str): The ID of the price to create the payment link for. 11 | - quantity (int): The quantity of the product to include in the payment link. 12 | - redirect_url (str, optional): The URL to redirect to after the payment is completed. 13 | `; 14 | 15 | export const createPaymentLink = async ( 16 | stripe: Stripe, 17 | context: Context, 18 | params: z.infer<ReturnType<typeof createPaymentLinkParameters>> 19 | ) => { 20 | try { 21 | const paymentLink = await stripe.paymentLinks.create( 22 | { 23 | line_items: [{price: params.price, quantity: params.quantity}], 24 | ...(params.redirect_url 25 | ? { 26 | after_completion: { 27 | type: 'redirect', 28 | redirect: { 29 | url: params.redirect_url, 30 | }, 31 | }, 32 | } 33 | : undefined), 34 | }, 35 | context.account ? {stripeAccount: context.account} : undefined 36 | ); 37 | 38 | return {id: paymentLink.id, url: paymentLink.url}; 39 | } catch (error) { 40 | return 'Failed to create payment link'; 41 | } 42 | }; 43 | 44 | export const createPaymentLinkAnnotations = () => ({ 45 | destructiveHint: false, 46 | idempotentHint: false, 47 | openWorldHint: true, 48 | readOnlyHint: false, 49 | title: 'Create payment link', 50 | }); 51 | 52 | export const createPaymentLinkParameters = (_context: Context = {}) => 53 | z.object({ 54 | price: z 55 | .string() 56 | .describe('The ID of the price to create the payment link for.'), 57 | quantity: z 58 | .number() 59 | .int() 60 | .describe('The quantity of the product to include.'), 61 | redirect_url: z 62 | .string() 63 | .optional() 64 | .describe('The URL to redirect to after the payment is completed.'), 65 | }); 66 | 67 | const tool = (context: Context): Tool => ({ 68 | method: 'create_payment_link', 69 | name: 'Create Payment Link', 70 | description: createPaymentLinkPrompt(context), 71 | parameters: createPaymentLinkParameters(context), 72 | annotations: createPaymentLinkAnnotations(), 73 | actions: { 74 | paymentLinks: { 75 | create: true, 76 | }, 77 | }, 78 | execute: createPaymentLink, 79 | }); 80 | 81 | export default tool; 82 | ``` -------------------------------------------------------------------------------- /typescript/src/shared/invoices/createInvoice.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Stripe from 'stripe'; 2 | import {z} from 'zod'; 3 | import type {Context} from '@/shared/configuration'; 4 | import type {Tool} from '@/shared/tools'; 5 | 6 | export const createInvoicePrompt = (context: Context = {}) => { 7 | const customerArg = context.customer 8 | ? `The customer is already set in the context: ${context.customer}.` 9 | : `- customer (str): The ID of the customer to create the invoice for.\n`; 10 | 11 | return ` 12 | This tool will create an invoice in Stripe. 13 | 14 | It takes ${context.customer ? 'one' : 'two'} argument${context.customer ? '' : 's'}: 15 | ${customerArg} 16 | - days_until_due (int, optional): The number of days until the invoice is due. 17 | `; 18 | }; 19 | 20 | export const createInvoiceParameters = ( 21 | context: Context = {} 22 | ): z.AnyZodObject => { 23 | const schema = z.object({ 24 | customer: z 25 | .string() 26 | .describe('The ID of the customer to create the invoice for.'), 27 | days_until_due: z 28 | .number() 29 | .int() 30 | .optional() 31 | .describe('The number of days until the invoice is due.'), 32 | }); 33 | 34 | if (context.customer) { 35 | return schema.omit({customer: true}); 36 | } else { 37 | return schema; 38 | } 39 | }; 40 | 41 | export const createInvoiceAnnotations = () => ({ 42 | destructiveHint: false, 43 | idempotentHint: false, 44 | openWorldHint: true, 45 | readOnlyHint: false, 46 | title: 'Create invoice', 47 | }); 48 | 49 | export const createInvoice = async ( 50 | stripe: Stripe, 51 | context: Context, 52 | params: z.infer<ReturnType<typeof createInvoiceParameters>> 53 | ) => { 54 | try { 55 | if (context.customer) { 56 | params.customer = context.customer; 57 | } 58 | 59 | const invoice = await stripe.invoices.create( 60 | { 61 | ...params, 62 | collection_method: 'send_invoice', 63 | }, 64 | context.account ? {stripeAccount: context.account} : undefined 65 | ); 66 | 67 | return { 68 | id: invoice.id, 69 | url: invoice.hosted_invoice_url, 70 | customer: invoice.customer, 71 | status: invoice.status, 72 | }; 73 | } catch (error) { 74 | return 'Failed to create invoice'; 75 | } 76 | }; 77 | 78 | const tool = (context: Context): Tool => ({ 79 | method: 'create_invoice', 80 | name: 'Create Invoice', 81 | description: createInvoicePrompt(context), 82 | parameters: createInvoiceParameters(context), 83 | annotations: createInvoiceAnnotations(), 84 | actions: { 85 | invoices: { 86 | create: true, 87 | }, 88 | }, 89 | execute: createInvoice, 90 | }); 91 | 92 | export default tool; 93 | ``` -------------------------------------------------------------------------------- /typescript/src/shared/invoiceItems/createInvoiceItem.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Stripe from 'stripe'; 2 | import {z} from 'zod'; 3 | import type {Context} from '@/shared/configuration'; 4 | import type {Tool} from '@/shared/tools'; 5 | 6 | export const createInvoiceItem = async ( 7 | stripe: Stripe, 8 | context: Context, 9 | params: z.infer<ReturnType<typeof createInvoiceItemParameters>> 10 | ) => { 11 | try { 12 | if (context.customer) { 13 | params.customer = context.customer; 14 | } 15 | 16 | const invoiceItem = await stripe.invoiceItems.create( 17 | // @ts-ignore 18 | params, 19 | context.account ? {stripeAccount: context.account} : undefined 20 | ); 21 | 22 | return { 23 | id: invoiceItem.id, 24 | invoice: invoiceItem.invoice, 25 | }; 26 | } catch (error) { 27 | return 'Failed to create invoice item'; 28 | } 29 | }; 30 | 31 | export const createInvoiceItemAnnotations = () => ({ 32 | destructiveHint: false, 33 | idempotentHint: false, 34 | openWorldHint: true, 35 | readOnlyHint: false, 36 | title: 'Create invoice item', 37 | }); 38 | 39 | export const createInvoiceItemParameters = ( 40 | context: Context = {} 41 | ): z.AnyZodObject => { 42 | const schema = z.object({ 43 | customer: z 44 | .string() 45 | .describe('The ID of the customer to create the invoice item for.'), 46 | price: z.string().describe('The ID of the price for the item.'), 47 | invoice: z 48 | .string() 49 | .describe('The ID of the invoice to create the item for.'), 50 | }); 51 | 52 | if (context.customer) { 53 | return schema.omit({customer: true}); 54 | } else { 55 | return schema; 56 | } 57 | }; 58 | 59 | export const createInvoiceItemPrompt = (context: Context = {}) => { 60 | const customerArg = context.customer 61 | ? `The customer is already set in the context: ${context.customer}.` 62 | : `- customer (str): The ID of the customer to create the invoice item for.\n`; 63 | 64 | return ` 65 | This tool will create an invoice item in Stripe. 66 | 67 | It takes ${context.customer ? 'two' : 'three'} arguments'}: 68 | ${customerArg} 69 | - price (str): The ID of the price to create the invoice item for. 70 | - invoice (str): The ID of the invoice to create the invoice item for. 71 | `; 72 | }; 73 | 74 | const tool = (context: Context): Tool => ({ 75 | method: 'create_invoice_item', 76 | name: 'Create Invoice Item', 77 | description: createInvoiceItemPrompt(context), 78 | parameters: createInvoiceItemParameters(context), 79 | annotations: createInvoiceItemAnnotations(), 80 | actions: { 81 | invoiceItems: { 82 | create: true, 83 | }, 84 | }, 85 | execute: createInvoiceItem, 86 | }); 87 | 88 | export default tool; 89 | ``` -------------------------------------------------------------------------------- /typescript/src/shared/invoices/listInvoices.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Stripe from 'stripe'; 2 | import {z} from 'zod'; 3 | import type {Context} from '@/shared/configuration'; 4 | import type {Tool} from '@/shared/tools'; 5 | 6 | export const listInvoices = async ( 7 | stripe: Stripe, 8 | context: Context, 9 | params: z.infer<ReturnType<typeof listInvoicesParameters>> 10 | ) => { 11 | try { 12 | if (context.customer) { 13 | params.customer = context.customer; 14 | } 15 | 16 | const invoices = await stripe.invoices.list( 17 | params, 18 | context.account ? {stripeAccount: context.account} : undefined 19 | ); 20 | 21 | return invoices.data; 22 | } catch (error) { 23 | return 'Failed to list invoices'; 24 | } 25 | }; 26 | 27 | export const listInvoicesAnnotations = () => ({ 28 | destructiveHint: false, 29 | idempotentHint: true, 30 | openWorldHint: true, 31 | readOnlyHint: true, 32 | title: 'List invoices', 33 | }); 34 | 35 | export const listInvoicesParameters = ( 36 | context: Context = {} 37 | ): z.AnyZodObject => { 38 | const schema = z.object({ 39 | customer: z 40 | .string() 41 | .optional() 42 | .describe('The ID of the customer to list invoices for.'), 43 | limit: z 44 | .number() 45 | .int() 46 | .min(1) 47 | .max(100) 48 | .optional() 49 | .describe( 50 | 'A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 10.' 51 | ), 52 | }); 53 | 54 | if (context.customer) { 55 | return schema.omit({customer: true}); 56 | } else { 57 | return schema; 58 | } 59 | }; 60 | 61 | export const listInvoicesPrompt = (context: Context = {}) => { 62 | const customerArg = context.customer 63 | ? `The customer is already set in the context: ${context.customer}.` 64 | : `- customer (str, optional): The ID of the customer to list invoices for.\n`; 65 | 66 | return ` 67 | This tool will fetch a list of Invoices from Stripe. 68 | 69 | It takes ${context.customer ? 'one' : 'two'} argument${context.customer ? '' : 's'}: 70 | ${customerArg} 71 | - limit (int, optional): The number of invoices to return. 72 | `; 73 | }; 74 | 75 | export const finalizeInvoicePrompt = (_context: Context = {}) => ` 76 | This tool will finalize an invoice in Stripe. 77 | 78 | It takes one argument: 79 | - invoice (str): The ID of the invoice to finalize. 80 | `; 81 | 82 | const tool = (context: Context): Tool => ({ 83 | method: 'list_invoices', 84 | name: 'List Invoices', 85 | description: listInvoicesPrompt(context), 86 | parameters: listInvoicesParameters(context), 87 | annotations: listInvoicesAnnotations(), 88 | actions: { 89 | invoices: { 90 | read: true, 91 | }, 92 | }, 93 | execute: listInvoices, 94 | }); 95 | 96 | export default tool; 97 | ``` -------------------------------------------------------------------------------- /typescript/src/test/shared/products/functions.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {createProduct} from '@/shared/products/createProduct'; 2 | import {listProducts} from '@/shared/products/listProducts'; 3 | 4 | const Stripe = jest.fn().mockImplementation(() => ({ 5 | products: { 6 | create: jest.fn(), 7 | list: jest.fn(), 8 | }, 9 | })); 10 | 11 | let stripe: ReturnType<typeof Stripe>; 12 | 13 | beforeEach(() => { 14 | stripe = new Stripe('fake-api-key'); 15 | }); 16 | 17 | describe('createProduct', () => { 18 | it('should create a product and return it', async () => { 19 | const params = { 20 | name: 'Test Product', 21 | }; 22 | 23 | const context = {}; 24 | 25 | const mockProduct = {id: 'prod_123456', name: 'Test Product'}; 26 | stripe.products.create.mockResolvedValue(mockProduct); 27 | 28 | const result = await createProduct(stripe, context, params); 29 | 30 | expect(stripe.products.create).toHaveBeenCalledWith(params, undefined); 31 | expect(result).toEqual(mockProduct); 32 | }); 33 | 34 | it('should specify the connected account if included in context', async () => { 35 | const params = { 36 | name: 'Test Product', 37 | }; 38 | 39 | const context = { 40 | account: 'acct_123456', 41 | }; 42 | 43 | const mockProduct = {id: 'prod_123456', name: 'Test Product'}; 44 | stripe.products.create.mockResolvedValue(mockProduct); 45 | 46 | const result = await createProduct(stripe, context, params); 47 | 48 | expect(stripe.products.create).toHaveBeenCalledWith(params, { 49 | stripeAccount: context.account, 50 | }); 51 | expect(result).toEqual(mockProduct); 52 | }); 53 | }); 54 | 55 | describe('listProducts', () => { 56 | it('should list products and return them', async () => { 57 | const mockProducts = [ 58 | {id: 'prod_123456', name: 'Test Product 1'}, 59 | {id: 'prod_789012', name: 'Test Product 2'}, 60 | ]; 61 | 62 | const context = {}; 63 | 64 | stripe.products.list.mockResolvedValue({data: mockProducts}); 65 | const result = await listProducts(stripe, context, {}); 66 | 67 | expect(stripe.products.list).toHaveBeenCalledWith({}, undefined); 68 | expect(result).toEqual(mockProducts); 69 | }); 70 | 71 | it('should specify the connected account if included in context', async () => { 72 | const mockProducts = [ 73 | {id: 'prod_123456', name: 'Test Product 1'}, 74 | {id: 'prod_789012', name: 'Test Product 2'}, 75 | ]; 76 | 77 | const context = { 78 | account: 'acct_123456', 79 | }; 80 | 81 | stripe.products.list.mockResolvedValue({data: mockProducts}); 82 | const result = await listProducts(stripe, context, {}); 83 | 84 | expect(stripe.products.list).toHaveBeenCalledWith( 85 | {}, 86 | { 87 | stripeAccount: context.account, 88 | } 89 | ); 90 | expect(result).toEqual(mockProducts); 91 | }); 92 | }); 93 | ``` -------------------------------------------------------------------------------- /typescript/src/test/shared/paymentLinks/functions.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {createPaymentLink} from '@/shared/paymentLinks/createPaymentLink'; 2 | 3 | const Stripe = jest.fn().mockImplementation(() => ({ 4 | paymentLinks: { 5 | create: jest.fn(), 6 | }, 7 | })); 8 | 9 | let stripe: ReturnType<typeof Stripe>; 10 | 11 | beforeEach(() => { 12 | stripe = new Stripe('fake-api-key'); 13 | }); 14 | 15 | describe('createPaymentLink', () => { 16 | it('should create a payment link and return it', async () => { 17 | const params = { 18 | line_items: [ 19 | { 20 | price: 'price_123456', 21 | quantity: 1, 22 | }, 23 | ], 24 | }; 25 | 26 | const mockPaymentLink = { 27 | id: 'pl_123456', 28 | url: 'https://example.com', 29 | }; 30 | 31 | const context = {}; 32 | 33 | stripe.paymentLinks.create.mockResolvedValue(mockPaymentLink); 34 | 35 | const result = await createPaymentLink(stripe, context, { 36 | price: 'price_123456', 37 | quantity: 1, 38 | }); 39 | 40 | expect(stripe.paymentLinks.create).toHaveBeenCalledWith(params, undefined); 41 | expect(result).toEqual(mockPaymentLink); 42 | }); 43 | 44 | it('should specify the connected account if included in context', async () => { 45 | const params = { 46 | line_items: [ 47 | { 48 | price: 'price_123456', 49 | quantity: 1, 50 | }, 51 | ], 52 | }; 53 | 54 | const mockPaymentLink = { 55 | id: 'pl_123456', 56 | url: 'https://example.com', 57 | }; 58 | 59 | const context = { 60 | account: 'acct_123456', 61 | }; 62 | 63 | stripe.paymentLinks.create.mockResolvedValue(mockPaymentLink); 64 | 65 | const result = await createPaymentLink(stripe, context, { 66 | price: 'price_123456', 67 | quantity: 1, 68 | }); 69 | 70 | expect(stripe.paymentLinks.create).toHaveBeenCalledWith(params, { 71 | stripeAccount: context.account, 72 | }); 73 | expect(result).toEqual(mockPaymentLink); 74 | }); 75 | 76 | it('should specify the redirect URL if included in params', async () => { 77 | const params = { 78 | line_items: [ 79 | { 80 | price: 'price_123456', 81 | quantity: 1, 82 | }, 83 | ], 84 | after_completion: { 85 | type: 'redirect', 86 | redirect: { 87 | url: 'https://example.com', 88 | }, 89 | }, 90 | }; 91 | 92 | const mockPaymentLink = { 93 | id: 'pl_123456', 94 | url: 'https://example.com', 95 | }; 96 | 97 | const context = {}; 98 | 99 | stripe.paymentLinks.create.mockResolvedValue(mockPaymentLink); 100 | 101 | const result = await createPaymentLink(stripe, context, { 102 | price: 'price_123456', 103 | quantity: 1, 104 | redirect_url: 'https://example.com', 105 | }); 106 | 107 | expect(stripe.paymentLinks.create).toHaveBeenCalledWith(params, undefined); 108 | expect(result).toEqual(mockPaymentLink); 109 | }); 110 | }); 111 | ``` -------------------------------------------------------------------------------- /typescript/src/test/shared/documentation/functions.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {searchDocumentation} from '@/shared/documentation/searchDocumentation'; 2 | import {z} from 'zod'; 3 | import {searchDocumentationParameters} from '@/shared/documentation/searchDocumentation'; 4 | 5 | const Stripe = jest.fn().mockImplementation(() => ({})); 6 | 7 | let stripe: ReturnType<typeof Stripe>; 8 | 9 | beforeEach(() => { 10 | stripe = new Stripe('fake-api-key'); 11 | }); 12 | 13 | const EXPECTED_HEADERS = { 14 | 'Content-Type': 'application/json', 15 | 'X-Requested-With': 'fetch', 16 | 'User-Agent': 'stripe-agent-toolkit-typescript', 17 | }; 18 | 19 | describe('searchDocumentation', () => { 20 | it('should search for Stripe documentation and return sources', async () => { 21 | const question = 'How to create Stripe checkout session?'; 22 | const requestBody: z.infer< 23 | ReturnType<typeof searchDocumentationParameters> 24 | > = { 25 | question: question, 26 | language: 'ruby', 27 | }; 28 | 29 | const sources = [ 30 | { 31 | type: 'docs', 32 | url: 'https://docs.stripe.com/payments/checkout/how-checkout-works', 33 | title: 'How checkout works', 34 | content: '...', 35 | }, 36 | ]; 37 | const mockResponse = { 38 | question: question, 39 | status: 'success', 40 | sources: sources, 41 | }; 42 | 43 | const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValueOnce({ 44 | ok: true, 45 | status: 200, 46 | json: jest.fn().mockResolvedValueOnce(mockResponse), 47 | } as unknown as Response); 48 | 49 | const result = await searchDocumentation(stripe, {}, requestBody); 50 | 51 | expect(fetchMock).toHaveBeenCalledWith('https://ai.stripe.com/search', { 52 | method: 'POST', 53 | headers: EXPECTED_HEADERS, 54 | body: JSON.stringify(requestBody), 55 | }); 56 | 57 | expect(result).toEqual(sources); 58 | }); 59 | 60 | it('should return failure string if search failed', async () => { 61 | const question = 'What is the meaning of life?'; 62 | const requestBody = { 63 | question: question, 64 | }; 65 | 66 | const mockError = { 67 | error: 'Invalid query', 68 | message: 69 | 'Unable to process your question. Please rephrase it to be more specific.', 70 | }; 71 | 72 | const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValueOnce({ 73 | ok: false, 74 | status: 400, 75 | json: jest.fn().mockResolvedValueOnce(mockError), 76 | } as unknown as Response); 77 | 78 | const result = await searchDocumentation(stripe, {}, requestBody); 79 | 80 | expect(fetchMock).toHaveBeenCalledWith('https://ai.stripe.com/search', { 81 | method: 'POST', 82 | headers: EXPECTED_HEADERS, 83 | body: JSON.stringify(requestBody), 84 | }); 85 | 86 | expect(result).toEqual('Failed to search documentation'); 87 | }); 88 | }); 89 | ``` -------------------------------------------------------------------------------- /typescript/src/shared/documentation/searchDocumentation.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Stripe from 'stripe'; 2 | import {z} from 'zod'; 3 | import type {Context} from '@/shared/configuration'; 4 | import type {Tool} from '@/shared/tools'; 5 | export const searchDocumentationPrompt = (_context: Context = {}) => ` 6 | This tool will take in a user question about integrating with Stripe in their application, then search and retrieve relevant Stripe documentation to answer the question. 7 | 8 | It takes two arguments: 9 | - question (str): The user question to search an answer for in the Stripe documentation. 10 | - language (str, optional): The programming language to search for in the the documentation. 11 | `; 12 | 13 | export const searchDocumentationParameters = ( 14 | _context: Context = {} 15 | ): z.AnyZodObject => 16 | z.object({ 17 | question: z 18 | .string() 19 | .describe( 20 | 'The user question about integrating with Stripe will be used to search the documentation.' 21 | ), 22 | language: z 23 | .enum(['dotnet', 'go', 'java', 'node', 'php', 'ruby', 'python', 'curl']) 24 | .optional() 25 | .describe( 26 | 'The programming language to search for in the the documentation.' 27 | ), 28 | }); 29 | 30 | export const searchDocumentationAnnotations = () => ({ 31 | destructiveHint: false, 32 | idempotentHint: true, 33 | openWorldHint: true, 34 | readOnlyHint: true, 35 | title: 'Search Stripe documentation', 36 | }); 37 | 38 | export const searchDocumentation = async ( 39 | _stripe: Stripe, 40 | context: Context, 41 | params: z.infer<ReturnType<typeof searchDocumentationParameters>> 42 | ) => { 43 | try { 44 | const endpoint = 'https://ai.stripe.com/search'; 45 | const response = await fetch(endpoint, { 46 | method: 'POST', 47 | headers: { 48 | 'Content-Type': 'application/json', 49 | 'X-Requested-With': 'fetch', 50 | 'User-Agent': 51 | context.mode === 'modelcontextprotocol' 52 | ? 'stripe-mcp' 53 | : 'stripe-agent-toolkit-typescript', 54 | }, 55 | body: JSON.stringify(params), 56 | }); 57 | 58 | // If status not in 200-299 range, throw error 59 | if (!response.ok) { 60 | throw new Error(`HTTP error! Status: ${response.status}`); 61 | } 62 | 63 | const data = await response.json(); 64 | return data?.sources; 65 | } catch (error) { 66 | return 'Failed to search documentation'; 67 | } 68 | }; 69 | 70 | const tool = (context: Context): Tool => ({ 71 | method: 'search_stripe_documentation', 72 | name: 'Search Stripe Documentation', 73 | description: searchDocumentationPrompt(context), 74 | parameters: searchDocumentationParameters(context), 75 | annotations: searchDocumentationAnnotations(), 76 | actions: { 77 | documentation: { 78 | read: true, 79 | }, 80 | }, 81 | execute: searchDocumentation, 82 | }); 83 | 84 | export default tool; 85 | ``` -------------------------------------------------------------------------------- /typescript/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@stripe/agent-toolkit", 3 | "version": "0.7.11", 4 | "homepage": "https://github.com/stripe/agent-toolkit", 5 | "scripts": { 6 | "build": "tsup", 7 | "clean": "rm -rf langchain ai-sdk modelcontextprotocol openai cloudflare", 8 | "lint": "eslint \"./**/*.ts*\"", 9 | "prettier": "prettier './**/*.{js,ts,md,html,css}' --write", 10 | "prettier-check": "prettier './**/*.{js,ts,md,html,css}' --check", 11 | "test": "jest", 12 | "eval": "cd ../evals && pnpm test" 13 | }, 14 | "exports": { 15 | "./langchain": { 16 | "types": "./langchain/index.d.ts", 17 | "require": "./langchain/index.js", 18 | "import": "./langchain/index.mjs" 19 | }, 20 | "./ai-sdk": { 21 | "types": "./ai-sdk/index.d.ts", 22 | "require": "./ai-sdk/index.js", 23 | "import": "./ai-sdk/index.mjs" 24 | }, 25 | "./openai": { 26 | "types": "./openai/index.d.ts", 27 | "require": "./openai/index.js", 28 | "import": "./openai/index.mjs" 29 | }, 30 | "./modelcontextprotocol": { 31 | "types": "./modelcontextprotocol/index.d.ts", 32 | "require": "./modelcontextprotocol/index.js", 33 | "import": "./modelcontextprotocol/index.mjs" 34 | }, 35 | "./cloudflare": { 36 | "types": "./cloudflare/index.d.ts", 37 | "require": "./cloudflare/index.js", 38 | "import": "./cloudflare/index.mjs" 39 | } 40 | }, 41 | "packageManager": "[email protected]", 42 | "engines": { 43 | "node": ">=18" 44 | }, 45 | "author": "Stripe <[email protected]> (https://stripe.com/)", 46 | "contributors": [ 47 | "Steve Kaliski <[email protected]>" 48 | ], 49 | "license": "MIT", 50 | "devDependencies": { 51 | "@eslint/compat": "^1.2.4", 52 | "@types/jest": "^29.5.14", 53 | "@types/node": "^22.10.5", 54 | "@typescript-eslint/eslint-plugin": "^8.19.1", 55 | "eslint": "^9.17.0", 56 | "eslint-config-prettier": "^9.1.0", 57 | "eslint-plugin-import": "^2.31.0", 58 | "eslint-plugin-jest": "^28.10.0", 59 | "eslint-plugin-prettier": "^5.2.1", 60 | "globals": "^15.14.0", 61 | "jest": "^29.7.0", 62 | "prettier": "^3.4.2", 63 | "ts-jest": "^29.2.5", 64 | "ts-node": "^10.9.2", 65 | "tsup": "^8.3.5", 66 | "typescript": "^5.8.3" 67 | }, 68 | "dependencies": { 69 | "stripe": "^17.5.0", 70 | "zod": "^3.24.1", 71 | "zod-to-json-schema": "^3.24.3" 72 | }, 73 | "peerDependencies": { 74 | "openai": "^4.86.1", 75 | "@langchain/core": "^0.3.6", 76 | "@modelcontextprotocol/sdk": "^1.17.1", 77 | "ai": "^3.4.7 || ^4.0.0", 78 | "agents": "^0.0.84" 79 | }, 80 | "workspaces": [ 81 | ".", 82 | "examples/*" 83 | ], 84 | "files": [ 85 | "ai-sdk/**/*", 86 | "langchain/**/*", 87 | "modelcontextprotocol/**/*", 88 | "openai/**/*", 89 | "cloudflare/**/*", 90 | "LICENSE", 91 | "README.md", 92 | "VERSION", 93 | "package.json" 94 | ] 95 | } 96 | ``` -------------------------------------------------------------------------------- /typescript/src/test/shared/prices/functions.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {createPrice} from '@/shared/prices/createPrice'; 2 | import {listPrices} from '@/shared/prices/listPrices'; 3 | 4 | const Stripe = jest.fn().mockImplementation(() => ({ 5 | prices: { 6 | create: jest.fn(), 7 | list: jest.fn(), 8 | }, 9 | })); 10 | 11 | let stripe: ReturnType<typeof Stripe>; 12 | 13 | beforeEach(() => { 14 | stripe = new Stripe('fake-api-key'); 15 | }); 16 | 17 | describe('createPrice', () => { 18 | it('should create a price and return it', async () => { 19 | const params = { 20 | unit_amount: 1000, 21 | currency: 'usd', 22 | product: 'prod_123456', 23 | }; 24 | 25 | const context = {}; 26 | 27 | const mockPrice = {id: 'price_123456', unit_amount: 1000, currency: 'usd'}; 28 | stripe.prices.create.mockResolvedValue(mockPrice); 29 | 30 | const result = await createPrice(stripe, context, params); 31 | 32 | expect(stripe.prices.create).toHaveBeenCalledWith(params, undefined); 33 | expect(result).toEqual(mockPrice); 34 | }); 35 | 36 | it('should specify the connected account if included in context', async () => { 37 | const params = { 38 | unit_amount: 1000, 39 | currency: 'usd', 40 | product: 'prod_123456', 41 | }; 42 | 43 | const context = { 44 | account: 'acct_123456', 45 | }; 46 | 47 | const mockPrice = {id: 'price_123456', unit_amount: 1000, currency: 'usd'}; 48 | stripe.prices.create.mockResolvedValue(mockPrice); 49 | 50 | const result = await createPrice(stripe, context, params); 51 | 52 | expect(stripe.prices.create).toHaveBeenCalledWith(params, { 53 | stripeAccount: context.account, 54 | }); 55 | expect(result).toEqual(mockPrice); 56 | }); 57 | }); 58 | 59 | describe('listPrices', () => { 60 | it('should list prices and return them', async () => { 61 | const mockPrices = [ 62 | {id: 'price_123456', unit_amount: 1000, currency: 'usd'}, 63 | {id: 'price_789012', unit_amount: 2000, currency: 'usd'}, 64 | ]; 65 | 66 | const context = {}; 67 | 68 | stripe.prices.list.mockResolvedValue({data: mockPrices}); 69 | const result = await listPrices(stripe, context, {}); 70 | 71 | expect(stripe.prices.list).toHaveBeenCalledWith({}, undefined); 72 | expect(result).toEqual(mockPrices); 73 | }); 74 | 75 | it('should specify the connected account if included in context', async () => { 76 | const mockPrices = [ 77 | {id: 'price_123456', unit_amount: 1000, currency: 'usd'}, 78 | {id: 'price_789012', unit_amount: 2000, currency: 'usd'}, 79 | ]; 80 | 81 | const context = { 82 | account: 'acct_123456', 83 | }; 84 | 85 | stripe.prices.list.mockResolvedValue({data: mockPrices}); 86 | const result = await listPrices(stripe, context, {}); 87 | 88 | expect(stripe.prices.list).toHaveBeenCalledWith( 89 | {}, 90 | { 91 | stripeAccount: context.account, 92 | } 93 | ); 94 | expect(result).toEqual(mockPrices); 95 | }); 96 | }); 97 | ``` -------------------------------------------------------------------------------- /typescript/src/test/shared/customers/functions.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {createCustomer} from '@/shared/customers/createCustomer'; 2 | import {listCustomers} from '@/shared/customers/listCustomers'; 3 | 4 | const Stripe = jest.fn().mockImplementation(() => ({ 5 | customers: { 6 | create: jest.fn(), 7 | list: jest.fn(), 8 | }, 9 | })); 10 | 11 | let stripe: ReturnType<typeof Stripe>; 12 | 13 | beforeEach(() => { 14 | stripe = new Stripe('fake-api-key'); 15 | }); 16 | 17 | describe('createCustomer', () => { 18 | it('should create a customer and return the id', async () => { 19 | const params = { 20 | email: '[email protected]', 21 | name: 'Test User', 22 | }; 23 | 24 | const context = {}; 25 | 26 | const mockCustomer = {id: 'cus_123456', email: '[email protected]'}; 27 | stripe.customers.create.mockResolvedValue(mockCustomer); 28 | 29 | const result = await createCustomer(stripe, context, params); 30 | 31 | expect(stripe.customers.create).toHaveBeenCalledWith(params, undefined); 32 | expect(result).toEqual({id: mockCustomer.id}); 33 | }); 34 | 35 | it('should specify the connected account if included in context', async () => { 36 | const params = { 37 | email: '[email protected]', 38 | name: 'Test User', 39 | }; 40 | 41 | const context = { 42 | account: 'acct_123456', 43 | }; 44 | 45 | const mockCustomer = {id: 'cus_123456', email: '[email protected]'}; 46 | stripe.customers.create.mockResolvedValue(mockCustomer); 47 | 48 | const result = await createCustomer(stripe, context, params); 49 | 50 | expect(stripe.customers.create).toHaveBeenCalledWith(params, { 51 | stripeAccount: context.account, 52 | }); 53 | expect(result).toEqual({id: mockCustomer.id}); 54 | }); 55 | }); 56 | 57 | describe('listCustomers', () => { 58 | it('should list customers and return their ids', async () => { 59 | const mockCustomers = [ 60 | {id: 'cus_123456', email: '[email protected]'}, 61 | {id: 'cus_789012', email: '[email protected]'}, 62 | ]; 63 | 64 | const context = {}; 65 | 66 | stripe.customers.list.mockResolvedValue({data: mockCustomers}); 67 | const result = await listCustomers(stripe, context, {}); 68 | 69 | expect(stripe.customers.list).toHaveBeenCalledWith({}, undefined); 70 | expect(result).toEqual(mockCustomers.map(({id}) => ({id}))); 71 | }); 72 | 73 | it('should specify the connected account if included in context', async () => { 74 | const mockCustomers = [ 75 | {id: 'cus_123456', email: '[email protected]'}, 76 | {id: 'cus_789012', email: '[email protected]'}, 77 | ]; 78 | 79 | const context = { 80 | account: 'acct_123456', 81 | }; 82 | 83 | stripe.customers.list.mockResolvedValue({data: mockCustomers}); 84 | const result = await listCustomers(stripe, context, {}); 85 | 86 | expect(stripe.customers.list).toHaveBeenCalledWith( 87 | {}, 88 | { 89 | stripeAccount: context.account, 90 | } 91 | ); 92 | expect(result).toEqual(mockCustomers.map(({id}) => ({id}))); 93 | }); 94 | }); 95 | ``` -------------------------------------------------------------------------------- /python/examples/openai/customer_support/main.py: -------------------------------------------------------------------------------- ```python 1 | import env 2 | import asyncio 3 | from emailer import Emailer, Email 4 | from typing import Union, List 5 | import support_agent 6 | import markdown as markdown 7 | import json 8 | 9 | from agents import ( 10 | ItemHelpers, 11 | TResponseInputItem, 12 | ) 13 | 14 | 15 | env.ensure("STRIPE_SECRET_KEY") 16 | env.ensure("OPENAI_API_KEY") 17 | 18 | email_address = env.ensure("EMAIL_ADDRESS") 19 | support_address = env.get_or("SUPPORT_ADDRESS", email_address) 20 | email_password = env.ensure("EMAIL_PASSWORD") 21 | emailer = Emailer(email_address, email_password, support_address) 22 | 23 | 24 | def unsure(str: str) -> bool: 25 | return ( 26 | "not sure" in str 27 | or "unsure" in str 28 | or "don't know" in str 29 | or "dont know" in str 30 | or "do not know" in str 31 | ) 32 | 33 | 34 | async def respond(thread: List[Email]) -> Union[Email, None]: 35 | most_recent = thread[-1] 36 | print(f"Got unread email:\n {json.dumps(most_recent.to_dict())}") 37 | 38 | # Loop through the entire thread to add historical context for the agent 39 | input_items: list[TResponseInputItem] = [] 40 | for email in thread: 41 | input_items.append( 42 | { 43 | "content": ( 44 | "This is an earlier email:" 45 | f"Email from: {email.from_address}\n" 46 | f"To: {email.to_address}\n" 47 | f"Subject: {email.subject}\n\n" 48 | f"{email.body}" 49 | ), 50 | "role": "user", 51 | } 52 | ) 53 | 54 | input_items.append( 55 | { 56 | "content": ( 57 | "This the latest email" 58 | "You can use context from earlier emails" 59 | "but reply specifically to the following email:" 60 | f"Email from: {most_recent.from_address}\n" 61 | f"To: {most_recent.to_address}\n" 62 | f"Subject: {most_recent.subject}\n\n" 63 | f"{most_recent.body}" 64 | ), 65 | "role": "user", 66 | } 67 | ) 68 | 69 | print(f"Sending to agent:\n {json.dumps(input_items)}") 70 | 71 | output = await support_agent.run(input_items) 72 | body_md = ItemHelpers.text_message_outputs(output.new_items) 73 | 74 | # Handle answers that the agent doesn't know 75 | if unsure(body_md.lower()): 76 | print( 77 | f"Agent doesn't know, ignore response and keep email in the inbox:\n{body_md}" 78 | ) 79 | return None 80 | 81 | # OpenAI often returns the body in html fences, trim those 82 | body_html = markdown.markdown(body_md, extensions=["tables"]) 83 | 84 | return Email( 85 | from_address=most_recent.to_address, 86 | to_address=most_recent.from_address, 87 | subject=most_recent.subject, 88 | body=body_html, 89 | ) 90 | 91 | 92 | async def main(): 93 | await emailer.run(respond, delay=30, mark_read=True) 94 | 95 | 96 | if __name__ == "__main__": 97 | asyncio.run(main()) 98 | ``` -------------------------------------------------------------------------------- /typescript/src/test/shared/configuration.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import z from 'zod'; 2 | import {isToolAllowed} from '@/shared/configuration'; 3 | 4 | describe('isToolAllowed', () => { 5 | it('should return true if all permissions are allowed', () => { 6 | const tool = { 7 | method: 'test', 8 | name: 'Test', 9 | description: 'Test', 10 | parameters: z.object({ 11 | foo: z.string(), 12 | }), 13 | annotations: { 14 | destructiveHint: false, 15 | idempotentHint: false, 16 | openWorldHint: true, 17 | readOnlyHint: false, 18 | title: 'Test', 19 | }, 20 | execute: async (a: any, b: any, c: any) => {}, 21 | actions: { 22 | customers: { 23 | create: true, 24 | read: true, 25 | }, 26 | invoices: { 27 | create: true, 28 | read: true, 29 | }, 30 | }, 31 | }; 32 | 33 | const configuration = { 34 | actions: { 35 | customers: { 36 | create: true, 37 | read: true, 38 | }, 39 | invoices: { 40 | create: true, 41 | read: true, 42 | }, 43 | }, 44 | }; 45 | 46 | expect(isToolAllowed(tool, configuration)).toBe(true); 47 | }); 48 | 49 | it('should return false if any permission is denied', () => { 50 | const tool = { 51 | method: 'test', 52 | name: 'Test', 53 | description: 'Test', 54 | parameters: z.object({ 55 | foo: z.string(), 56 | }), 57 | annotations: { 58 | destructiveHint: false, 59 | idempotentHint: false, 60 | openWorldHint: true, 61 | readOnlyHint: false, 62 | title: 'Test', 63 | }, 64 | execute: async (a: any, b: any, c: any) => {}, 65 | actions: { 66 | customers: { 67 | create: true, 68 | read: true, 69 | }, 70 | invoices: { 71 | create: true, 72 | read: true, 73 | }, 74 | }, 75 | }; 76 | 77 | const configuration = { 78 | actions: { 79 | customers: { 80 | create: true, 81 | read: true, 82 | }, 83 | invoices: { 84 | create: true, 85 | read: false, 86 | }, 87 | }, 88 | }; 89 | 90 | expect(isToolAllowed(tool, configuration)).toBe(false); 91 | }); 92 | 93 | it('should return false if any resource is not allowed', () => { 94 | const tool = { 95 | method: 'test', 96 | name: 'Test', 97 | description: 'Test', 98 | parameters: z.object({ 99 | foo: z.string(), 100 | }), 101 | annotations: { 102 | destructiveHint: false, 103 | idempotentHint: false, 104 | openWorldHint: true, 105 | readOnlyHint: false, 106 | title: 'Test', 107 | }, 108 | execute: async (a: any, b: any, c: any) => {}, 109 | actions: { 110 | paymentLinks: { 111 | create: true, 112 | }, 113 | }, 114 | }; 115 | 116 | const configuration = { 117 | actions: { 118 | customers: { 119 | create: true, 120 | read: true, 121 | }, 122 | invoices: { 123 | create: true, 124 | read: true, 125 | }, 126 | }, 127 | }; 128 | 129 | expect(isToolAllowed(tool, configuration)).toBe(false); 130 | }); 131 | }); 132 | ``` -------------------------------------------------------------------------------- /typescript/src/shared/subscriptions/listSubscriptions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Stripe from 'stripe'; 2 | import {z} from 'zod'; 3 | import type {Context} from '@/shared/configuration'; 4 | import type {Tool} from '@/shared/tools'; 5 | 6 | export const listSubscriptions = async ( 7 | stripe: Stripe, 8 | context: Context, 9 | params: z.infer<ReturnType<typeof listSubscriptionsParameters>> 10 | ) => { 11 | try { 12 | if (context.customer) { 13 | params.customer = context.customer; 14 | } 15 | 16 | const subscriptions = await stripe.subscriptions.list( 17 | params, 18 | context.account ? {stripeAccount: context.account} : undefined 19 | ); 20 | 21 | return subscriptions.data; 22 | } catch (error) { 23 | return 'Failed to list subscriptions'; 24 | } 25 | }; 26 | 27 | export const listSubscriptionsParameters = ( 28 | context: Context = {} 29 | ): z.AnyZodObject => { 30 | const schema = z.object({ 31 | customer: z 32 | .string() 33 | .optional() 34 | .describe('The ID of the customer to list subscriptions for.'), 35 | price: z 36 | .string() 37 | .optional() 38 | .describe('The ID of the price to list subscriptions for.'), 39 | status: z 40 | .enum([ 41 | 'active', 42 | 'past_due', 43 | 'unpaid', 44 | 'canceled', 45 | 'incomplete', 46 | 'incomplete_expired', 47 | 'trialing', 48 | 'all', 49 | ]) 50 | .optional() 51 | .describe('The status of the subscriptions to retrieve.'), 52 | limit: z 53 | .number() 54 | .int() 55 | .min(1) 56 | .max(100) 57 | .optional() 58 | .describe( 59 | 'A limit on the number of objects to be returned. Limit can range between 1 and 100.' 60 | ), 61 | }); 62 | 63 | if (context.customer) { 64 | return schema.omit({customer: true}); 65 | } else { 66 | return schema; 67 | } 68 | }; 69 | 70 | export const listSubscriptionsPrompt = (context: Context = {}): string => { 71 | const customerArg = context.customer 72 | ? `The customer is already set in the context: ${context.customer}.` 73 | : `- customer (str, optional): The ID of the customer to list subscriptions for.\n`; 74 | 75 | return ` 76 | This tool will list all subscriptions in Stripe. 77 | 78 | It takes ${context.customer ? 'three' : 'four'} arguments: 79 | ${customerArg} 80 | - price (str, optional): The ID of the price to list subscriptions for. 81 | - status (str, optional): The status of the subscriptions to list. 82 | - limit (int, optional): The number of subscriptions to return. 83 | `; 84 | }; 85 | 86 | export const listSubscriptionsAnnotations = () => ({ 87 | destructiveHint: false, 88 | idempotentHint: true, 89 | openWorldHint: true, 90 | readOnlyHint: true, 91 | title: 'List subscriptions', 92 | }); 93 | 94 | const tool = (context: Context): Tool => ({ 95 | method: 'list_subscriptions', 96 | name: 'List Subscriptions', 97 | description: listSubscriptionsPrompt(context), 98 | parameters: listSubscriptionsParameters(context), 99 | annotations: listSubscriptionsAnnotations(), 100 | actions: { 101 | subscriptions: { 102 | read: true, 103 | }, 104 | }, 105 | execute: listSubscriptions, 106 | }); 107 | 108 | export default tool; 109 | ``` -------------------------------------------------------------------------------- /typescript/src/shared/tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {z} from 'zod'; 2 | 3 | import createCustomerTool from '@/shared/customers/createCustomer'; 4 | import listCustomersTool from '@/shared/customers/listCustomers'; 5 | import createProductTool from '@/shared/products/createProduct'; 6 | import listProductsTool from '@/shared/products/listProducts'; 7 | import createPriceTool from '@/shared/prices/createPrice'; 8 | import listPricesTool from '@/shared/prices/listPrices'; 9 | import createPaymentLinkTool from '@/shared/paymentLinks/createPaymentLink'; 10 | import createInvoiceTool from '@/shared/invoices/createInvoice'; 11 | import listInvoicesTool from '@/shared/invoices/listInvoices'; 12 | import createInvoiceItemTool from '@/shared/invoiceItems/createInvoiceItem'; 13 | import finalizeInvoiceTool from '@/shared/invoices/finalizeInvoice'; 14 | import retrieveBalanceTool from '@/shared/balance/retrieveBalance'; 15 | import listCouponsTool from '@/shared/coupons/listCoupons'; 16 | import createCouponTool from '@/shared/coupons/createCoupon'; 17 | import createRefundTool from '@/shared/refunds/createRefund'; 18 | import listPaymentIntentsTool from '@/shared/paymentIntents/listPaymentIntents'; 19 | import listSubscriptionsTool from '@/shared/subscriptions/listSubscriptions'; 20 | import cancelSubscriptionTool from '@/shared/subscriptions/cancelSubscription'; 21 | import updateSubscriptionTool from '@/shared/subscriptions/updateSubscription'; 22 | import searchDocumentationTool from '@/shared/documentation/searchDocumentation'; 23 | import listDisputesTool from '@/shared/disputes/listDisputes'; 24 | import updateDisputeTool from '@/shared/disputes/updateDispute'; 25 | 26 | import {Context} from './configuration'; 27 | import Stripe from 'stripe'; 28 | 29 | export type Tool = { 30 | method: string; 31 | name: string; 32 | description: string; 33 | parameters: z.ZodObject<any, any, any, any>; 34 | annotations: { 35 | destructiveHint?: boolean; 36 | idempotentHint?: boolean; 37 | openWorldHint?: boolean; 38 | readOnlyHint?: boolean; 39 | title?: string; 40 | }; 41 | actions: { 42 | [key: string]: { 43 | [action: string]: boolean; 44 | }; 45 | }; 46 | execute: (stripe: Stripe, context: Context, params: any) => Promise<any>; 47 | }; 48 | 49 | const tools = (context: Context): Tool[] => [ 50 | createCustomerTool(context), 51 | listCustomersTool(context), 52 | createProductTool(context), 53 | listProductsTool(context), 54 | createPriceTool(context), 55 | listPricesTool(context), 56 | createPaymentLinkTool(context), 57 | createInvoiceTool(context), 58 | listInvoicesTool(context), 59 | createInvoiceItemTool(context), 60 | finalizeInvoiceTool(context), 61 | retrieveBalanceTool(context), 62 | createRefundTool(context), 63 | listPaymentIntentsTool(context), 64 | listSubscriptionsTool(context), 65 | cancelSubscriptionTool(context), 66 | updateSubscriptionTool(context), 67 | searchDocumentationTool(context), 68 | listCouponsTool(context), 69 | createCouponTool(context), 70 | updateDisputeTool(context), 71 | listDisputesTool(context), 72 | ]; 73 | 74 | export default tools; 75 | ``` -------------------------------------------------------------------------------- /typescript/src/ai-sdk/toolkit.ts: -------------------------------------------------------------------------------- ```typescript 1 | import StripeAPI from '../shared/api'; 2 | import tools from '../shared/tools'; 3 | import {isToolAllowed, type Configuration} from '../shared/configuration'; 4 | import type { 5 | CoreTool, 6 | LanguageModelV1StreamPart, 7 | Experimental_LanguageModelV1Middleware as LanguageModelV1Middleware, 8 | } from 'ai'; 9 | import StripeTool from './tool'; 10 | 11 | type StripeMiddlewareConfig = { 12 | billing?: { 13 | type?: 'token'; 14 | customer: string; 15 | meters: { 16 | input?: string; 17 | output?: string; 18 | }; 19 | }; 20 | }; 21 | 22 | class StripeAgentToolkit { 23 | private _stripe: StripeAPI; 24 | 25 | tools: {[key: string]: CoreTool}; 26 | 27 | constructor({ 28 | secretKey, 29 | configuration, 30 | }: { 31 | secretKey: string; 32 | configuration: Configuration; 33 | }) { 34 | this._stripe = new StripeAPI(secretKey, configuration.context); 35 | this.tools = {}; 36 | 37 | const context = configuration.context || {}; 38 | const filteredTools = tools(context).filter((tool) => 39 | isToolAllowed(tool, configuration) 40 | ); 41 | 42 | filteredTools.forEach((tool) => { 43 | // @ts-ignore 44 | this.tools[tool.method] = StripeTool( 45 | this._stripe, 46 | tool.method, 47 | tool.description, 48 | tool.parameters 49 | ); 50 | }); 51 | } 52 | 53 | middleware(config: StripeMiddlewareConfig): LanguageModelV1Middleware { 54 | const bill = async ({ 55 | promptTokens, 56 | completionTokens, 57 | }: { 58 | promptTokens: number; 59 | completionTokens: number; 60 | }) => { 61 | if (config.billing) { 62 | if (config.billing.meters.input) { 63 | await this._stripe.createMeterEvent({ 64 | event: config.billing.meters.input, 65 | customer: config.billing.customer, 66 | value: promptTokens.toString(), 67 | }); 68 | } 69 | if (config.billing.meters.output) { 70 | await this._stripe.createMeterEvent({ 71 | event: config.billing.meters.output, 72 | customer: config.billing.customer, 73 | value: completionTokens.toString(), 74 | }); 75 | } 76 | } 77 | }; 78 | 79 | return { 80 | wrapGenerate: async ({doGenerate}) => { 81 | const result = await doGenerate(); 82 | 83 | if (config.billing) { 84 | await bill(result.usage); 85 | } 86 | 87 | return result; 88 | }, 89 | 90 | wrapStream: async ({doStream}) => { 91 | const {stream, ...rest} = await doStream(); 92 | 93 | const transformStream = new TransformStream< 94 | LanguageModelV1StreamPart, 95 | LanguageModelV1StreamPart 96 | >({ 97 | async transform(chunk, controller) { 98 | if (chunk.type === 'finish') { 99 | if (config.billing) { 100 | await bill(chunk.usage); 101 | } 102 | } 103 | 104 | controller.enqueue(chunk); 105 | }, 106 | }); 107 | 108 | return { 109 | stream: stream.pipeThrough(transformStream), 110 | ...rest, 111 | }; 112 | }, 113 | }; 114 | } 115 | 116 | getTools(): {[key: string]: CoreTool} { 117 | return this.tools; 118 | } 119 | } 120 | 121 | export default StripeAgentToolkit; 122 | ``` -------------------------------------------------------------------------------- /evals/eval.ts: -------------------------------------------------------------------------------- ```typescript 1 | require("dotenv").config(); 2 | 3 | import { StripeAgentToolkit } from "../typescript/src/openai"; 4 | import type { 5 | ChatCompletion, 6 | ChatCompletionMessageParam, 7 | } from "openai/resources"; 8 | import { Eval } from "braintrust"; 9 | import { AssertionScorer, EvalCaseFunction, EvalInput } from "./scorer"; 10 | import { getEvalTestCases } from "./cases"; 11 | import braintrustOpenai from "./braintrust_openai"; 12 | 13 | // This is the core "workhorse" function that accepts an input and returns a response 14 | // which calls stripe agent tookit 15 | async function task(evalInput: EvalInput): Promise<EvalOutput> { 16 | const stripeAgentToolkit = new StripeAgentToolkit({ 17 | secretKey: process.env.STRIPE_SECRET_KEY!, 18 | configuration: { 19 | actions: { 20 | paymentLinks: { 21 | create: true, 22 | read: true, 23 | }, 24 | products: { 25 | create: true, 26 | read: true, 27 | }, 28 | prices: { 29 | create: true, 30 | read: true, 31 | }, 32 | coupons: { 33 | create: true, 34 | read: true, 35 | }, 36 | customers: { 37 | create: true, 38 | read: true, 39 | }, 40 | paymentIntents: { 41 | create: true, 42 | read: true, 43 | }, 44 | invoices: { 45 | create: true, 46 | read: true, 47 | }, 48 | invoiceItems: { 49 | create: true, 50 | read: true, 51 | }, 52 | refunds: { 53 | create: true, 54 | read: true, 55 | }, 56 | subscriptions: { 57 | read: true, 58 | update: true, 59 | }, 60 | balance: { 61 | read: true, 62 | }, 63 | disputes: { 64 | read: true, 65 | update: true, 66 | }, 67 | }, 68 | ...evalInput.toolkitConfigOverride, 69 | }, 70 | }); 71 | 72 | let messages: ChatCompletionMessageParam[] = [ 73 | { 74 | role: "user", 75 | content: evalInput.userPrompt, 76 | }, 77 | ]; 78 | 79 | let completion: ChatCompletion; 80 | 81 | const tools = stripeAgentToolkit.getTools(); 82 | 83 | while (true) { 84 | // eslint-disable-next-line no-await-in-loop 85 | completion = await braintrustOpenai.chat.completions.create({ 86 | model: "gpt-4o", 87 | messages, 88 | tools, 89 | }); 90 | 91 | const message = completion.choices[0].message; 92 | 93 | messages.push(message); 94 | 95 | if (message.tool_calls?.length! > 0) { 96 | // eslint-disable-next-line no-await-in-loop 97 | const toolMessages = await Promise.all( 98 | message.tool_calls!.map((tc) => stripeAgentToolkit.handleToolCall(tc)) 99 | ); 100 | 101 | messages = [...messages, ...toolMessages]; 102 | } else { 103 | break; 104 | } 105 | } 106 | 107 | return messages; 108 | } 109 | 110 | const BRAINTRUST_PROJECT = "agent-toolkit"; 111 | 112 | export type EvalOutput = ChatCompletionMessageParam[]; 113 | 114 | async function main() { 115 | await Eval<EvalInput, EvalOutput, EvalCaseFunction>(BRAINTRUST_PROJECT, { 116 | data: await getEvalTestCases(), 117 | task: async (input) => { 118 | const result = await task(input); 119 | return result; 120 | }, 121 | scores: [AssertionScorer], 122 | }); 123 | } 124 | 125 | main(); 126 | ``` -------------------------------------------------------------------------------- /typescript/src/test/shared/paymentIntents/functions.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {listPaymentIntents} from '@/shared/paymentIntents/listPaymentIntents'; 2 | 3 | const Stripe = jest.fn().mockImplementation(() => ({ 4 | paymentIntents: { 5 | list: jest.fn(), 6 | }, 7 | })); 8 | 9 | let stripe: ReturnType<typeof Stripe>; 10 | 11 | beforeEach(() => { 12 | stripe = new Stripe('fake-api-key'); 13 | }); 14 | 15 | describe('listPaymentIntents', () => { 16 | it('should list payment intents and return them', async () => { 17 | const mockPaymentIntents = [ 18 | { 19 | id: 'pi_123456', 20 | customer: 'cus_123456', 21 | amount: 1000, 22 | status: 'succeeded', 23 | description: 'Test Payment Intent', 24 | }, 25 | ]; 26 | 27 | const context = {}; 28 | 29 | stripe.paymentIntents.list.mockResolvedValue({data: mockPaymentIntents}); 30 | 31 | const result = await listPaymentIntents(stripe, context, {}); 32 | 33 | expect(stripe.paymentIntents.list).toHaveBeenCalledWith({}, undefined); 34 | expect(result).toEqual(mockPaymentIntents); 35 | }); 36 | 37 | it('should list payment intents for a specific customer', async () => { 38 | const mockPaymentIntents = [ 39 | { 40 | id: 'pi_123456', 41 | customer: 'cus_123456', 42 | amount: 1000, 43 | status: 'succeeded', 44 | description: 'Test Payment Intent', 45 | }, 46 | ]; 47 | 48 | const context = {}; 49 | 50 | stripe.paymentIntents.list.mockResolvedValue({data: mockPaymentIntents}); 51 | 52 | const result = await listPaymentIntents(stripe, context, { 53 | customer: 'cus_123456', 54 | }); 55 | 56 | expect(stripe.paymentIntents.list).toHaveBeenCalledWith( 57 | { 58 | customer: 'cus_123456', 59 | }, 60 | undefined 61 | ); 62 | expect(result).toEqual(mockPaymentIntents); 63 | }); 64 | 65 | it('should specify the connected account if included in context', async () => { 66 | const mockPaymentIntents = [ 67 | { 68 | id: 'pi_123456', 69 | customer: 'cus_123456', 70 | amount: 1000, 71 | status: 'succeeded', 72 | description: 'Test Payment Intent', 73 | }, 74 | ]; 75 | 76 | const context = { 77 | account: 'acct_123456', 78 | }; 79 | 80 | stripe.paymentIntents.list.mockResolvedValue({data: mockPaymentIntents}); 81 | 82 | const result = await listPaymentIntents(stripe, context, {}); 83 | 84 | expect(stripe.paymentIntents.list).toHaveBeenCalledWith( 85 | {}, 86 | { 87 | stripeAccount: context.account, 88 | } 89 | ); 90 | expect(result).toEqual(mockPaymentIntents); 91 | }); 92 | 93 | it('should list payment intents for a specific customer if included in context', async () => { 94 | const mockPaymentIntents = [ 95 | { 96 | id: 'pi_123456', 97 | customer: 'cus_123456', 98 | amount: 1000, 99 | status: 'succeeded', 100 | description: 'Test Payment Intent', 101 | }, 102 | ]; 103 | 104 | const context = { 105 | customer: 'cus_123456', 106 | }; 107 | 108 | stripe.paymentIntents.list.mockResolvedValue({data: mockPaymentIntents}); 109 | 110 | const result = await listPaymentIntents(stripe, context, {}); 111 | 112 | expect(stripe.paymentIntents.list).toHaveBeenCalledWith( 113 | {customer: context.customer}, 114 | undefined 115 | ); 116 | expect(result).toEqual(mockPaymentIntents); 117 | }); 118 | }); 119 | ``` -------------------------------------------------------------------------------- /typescript/src/shared/coupons/createCoupon.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Stripe from 'stripe'; 2 | import {z} from 'zod'; 3 | import type {Context} from '@/shared/configuration'; 4 | import type {Tool} from '@/shared/tools'; 5 | 6 | export const createCouponPrompt = (_context: Context = {}) => ` 7 | This tool will create a coupon in Stripe. 8 | 9 | 10 | It takes several arguments: 11 | - name (str): The name of the coupon. 12 | 13 | Only use one of percent_off or amount_off, not both: 14 | - percent_off (number, optional): The percentage discount to apply (between 0 and 100). 15 | - amount_off (number, optional): The amount to subtract from an invoice (in cents). 16 | 17 | Optional arguments for duration. Use if specific duration is desired, otherwise default to 'once'. 18 | - duration (str, optional): How long the discount will last ('once', 'repeating', or 'forever'). Defaults to 'once'. 19 | - duration_in_months (number, optional): The number of months the discount will last if duration is repeating. 20 | `; 21 | 22 | export const createCouponParameters = ( 23 | _context: Context = {} 24 | ): z.AnyZodObject => 25 | z.object({ 26 | name: z 27 | .string() 28 | .describe( 29 | 'Name of the coupon displayed to customers on invoices or receipts' 30 | ), 31 | percent_off: z 32 | .number() 33 | .min(0) 34 | .max(100) 35 | .optional() 36 | .describe( 37 | 'A positive float larger than 0, and smaller or equal to 100, that represents the discount the coupon will apply (required if amount_off is not passed)' 38 | ), 39 | amount_off: z 40 | .number() 41 | .describe( 42 | 'A positive integer representing the amount to subtract from an invoice total (required if percent_off is not passed)' 43 | ), 44 | currency: z 45 | .string() 46 | .optional() 47 | .default('USD') 48 | .describe( 49 | 'Three-letter ISO code for the currency of the amount_off parameter (required if amount_off is passed). Infer based on the amount_off. For example, if a coupon is $2 off, set currency to be USD.' 50 | ), 51 | duration: z 52 | .enum(['once', 'repeating', 'forever']) 53 | .default('once') 54 | .optional() 55 | .describe('How long the discount will last. Defaults to "once"'), 56 | duration_in_months: z 57 | .number() 58 | .optional() 59 | .describe( 60 | 'The number of months the discount will last if duration is repeating' 61 | ), 62 | }); 63 | 64 | export const createCouponAnnotations = () => ({ 65 | destructiveHint: false, 66 | idempotentHint: false, 67 | openWorldHint: true, 68 | readOnlyHint: false, 69 | title: 'Create coupon', 70 | }); 71 | 72 | export const createCoupon = async ( 73 | stripe: Stripe, 74 | context: Context, 75 | params: z.infer<ReturnType<typeof createCouponParameters>> 76 | ) => { 77 | try { 78 | const coupon = await stripe.coupons.create( 79 | params, 80 | context.account ? {stripeAccount: context.account} : undefined 81 | ); 82 | 83 | return {id: coupon.id}; 84 | } catch (error: any) { 85 | return `Failed to create coupon: ${error.message}`; 86 | } 87 | }; 88 | 89 | const tool = (context: Context): Tool => ({ 90 | method: 'create_coupon', 91 | name: 'Create Coupon', 92 | description: createCouponPrompt(context), 93 | parameters: createCouponParameters(context), 94 | annotations: createCouponAnnotations(), 95 | actions: { 96 | coupons: { 97 | create: true, 98 | }, 99 | }, 100 | execute: createCoupon, 101 | }); 102 | 103 | export default tool; 104 | ``` -------------------------------------------------------------------------------- /typescript/src/shared/disputes/updateDispute.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Stripe from 'stripe'; 2 | import {z} from 'zod'; 3 | import type {Context} from '@/shared/configuration'; 4 | import type {Tool} from '@/shared/tools'; 5 | 6 | export const updateDisputePrompt = (_context: Context = {}) => ` 7 | When you receive a dispute, contacting your customer is always the best first step. If that doesn't work, you can submit evidence to help resolve the dispute in your favor. This tool helps. 8 | 9 | It takes the following arguments: 10 | - dispute (string): The ID of the dispute to update 11 | - evidence (object, optional): Evidence to upload for the dispute. 12 | - cancellation_policy_disclosure (string) 13 | - cancellation_rebuttal (string) 14 | - duplicate_charge_explanation (string) 15 | - uncategorized_text (string, optional): Any additional evidence or statements. 16 | - submit (boolean, optional): Whether to immediately submit evidence to the bank. If false, evidence is staged on the dispute. 17 | `; 18 | 19 | export const updateDisputeParameters = (_context: Context = {}) => 20 | z.object({ 21 | dispute: z.string().describe('The ID of the dispute to update'), 22 | evidence: z 23 | .object({ 24 | cancellation_policy_disclosure: z 25 | .string() 26 | .max(20000) 27 | .optional() 28 | .describe( 29 | 'An explanation of how and when the customer was shown your refund policy prior to purchase.' 30 | ), 31 | duplicate_charge_explanation: z 32 | .string() 33 | .max(20000) 34 | .optional() 35 | .describe( 36 | 'An explanation of the difference between the disputed charge versus the prior charge that appears to be a duplicate.' 37 | ), 38 | uncategorized_text: z 39 | .string() 40 | .max(20000) 41 | .optional() 42 | .describe('Any additional evidence or statements.'), 43 | }) 44 | .optional() 45 | .describe( 46 | 'Evidence to upload, to respond to a dispute. Updating any field in the hash will submit all fields in the hash for review.' 47 | ), 48 | submit: z 49 | .boolean() 50 | .optional() 51 | .describe( 52 | 'Whether to immediately submit evidence to the bank. If false, evidence is staged on the dispute.' 53 | ), 54 | }); 55 | 56 | export const updateDisputeAnnotations = () => ({ 57 | destructiveHint: false, 58 | idempotentHint: false, 59 | openWorldHint: true, 60 | readOnlyHint: false, 61 | title: 'Update dispute', 62 | }); 63 | 64 | export const updateDispute = async ( 65 | stripe: Stripe, 66 | context: Context, 67 | params: z.infer<ReturnType<typeof updateDisputeParameters>> 68 | ) => { 69 | try { 70 | const updateParams: Stripe.DisputeUpdateParams = { 71 | evidence: params.evidence, 72 | submit: params.submit, 73 | }; 74 | 75 | const updatedDispute = await stripe.disputes.update( 76 | params.dispute, 77 | updateParams, 78 | context.account ? {stripeAccount: context.account} : undefined 79 | ); 80 | 81 | return {id: updatedDispute.id}; 82 | } catch (error) { 83 | return 'Failed to update dispute'; 84 | } 85 | }; 86 | 87 | const tool = (context: Context): Tool => ({ 88 | method: 'update_dispute', 89 | name: 'Update Dispute', 90 | description: updateDisputePrompt(context), 91 | parameters: updateDisputeParameters(context), 92 | annotations: updateDisputeAnnotations(), 93 | actions: { 94 | disputes: { 95 | update: true, 96 | }, 97 | }, 98 | execute: updateDispute, 99 | }); 100 | 101 | export default tool; 102 | ``` -------------------------------------------------------------------------------- /typescript/src/test/shared/disputes/functions.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {updateDispute} from '@/shared/disputes/updateDispute'; 2 | import {listDisputes} from '@/shared/disputes/listDisputes'; 3 | 4 | const Stripe = jest.fn().mockImplementation(() => ({ 5 | disputes: { 6 | update: jest.fn(), 7 | list: jest.fn(), 8 | }, 9 | })); 10 | 11 | let stripe: ReturnType<typeof Stripe>; 12 | 13 | beforeEach(() => { 14 | stripe = new Stripe('fake-api-key'); 15 | }); 16 | 17 | describe('updateDispute', () => { 18 | it('should update a dispute and return the id', async () => { 19 | const params = { 20 | dispute: 'dp_123456', 21 | evidence: { 22 | uncategorized_text: 'Test product', 23 | }, 24 | submit: true, 25 | }; 26 | 27 | const context = {}; 28 | 29 | const mockDispute = {id: 'dp_123456'}; 30 | stripe.disputes.update.mockResolvedValue(mockDispute); 31 | 32 | const result = await updateDispute(stripe, context, params); 33 | 34 | expect(stripe.disputes.update).toHaveBeenCalledWith( 35 | params.dispute, 36 | { 37 | evidence: params.evidence, 38 | submit: params.submit, 39 | }, 40 | undefined 41 | ); 42 | expect(result).toEqual({id: mockDispute.id}); 43 | }); 44 | 45 | it('should specify the connected account if included in context', async () => { 46 | const params = { 47 | dispute: 'dp_123456', 48 | evidence: { 49 | uncategorized_text: 'Test product', 50 | }, 51 | submit: true, 52 | }; 53 | 54 | const context = { 55 | account: 'acct_123456', 56 | }; 57 | 58 | const mockDispute = {id: 'dp_123456'}; 59 | stripe.disputes.update.mockResolvedValue(mockDispute); 60 | 61 | const result = await updateDispute(stripe, context, params); 62 | 63 | expect(stripe.disputes.update).toHaveBeenCalledWith( 64 | params.dispute, 65 | { 66 | evidence: params.evidence, 67 | submit: params.submit, 68 | }, 69 | { 70 | stripeAccount: context.account, 71 | } 72 | ); 73 | expect(result).toEqual({id: mockDispute.id}); 74 | }); 75 | }); 76 | 77 | describe('listDisputes', () => { 78 | it('should list disputes and return their ids', async () => { 79 | const mockDisputes = [{id: 'dp_123456'}, {id: 'dp_789012'}]; 80 | 81 | const context = {}; 82 | 83 | stripe.disputes.list.mockResolvedValue({data: mockDisputes}); 84 | const result = await listDisputes(stripe, context, {}); 85 | 86 | expect(stripe.disputes.list).toHaveBeenCalledWith({}, undefined); 87 | expect(result).toEqual(mockDisputes.map(({id}) => ({id}))); 88 | }); 89 | 90 | it('should specify the connected account if included in context', async () => { 91 | const mockDisputes = [{id: 'dp_123456'}, {id: 'dp_789012'}]; 92 | 93 | const context = { 94 | account: 'acct_123456', 95 | }; 96 | 97 | stripe.disputes.list.mockResolvedValue({data: mockDisputes}); 98 | const result = await listDisputes(stripe, context, {}); 99 | 100 | expect(stripe.disputes.list).toHaveBeenCalledWith( 101 | {}, 102 | { 103 | stripeAccount: context.account, 104 | } 105 | ); 106 | expect(result).toEqual(mockDisputes.map(({id}) => ({id}))); 107 | }); 108 | 109 | it('should pass through list parameters', async () => { 110 | const params = { 111 | charge: 'ch_123456', 112 | payment_intent: 'pi_123456', 113 | limit: 5, 114 | }; 115 | 116 | const mockDisputes = [{id: 'dp_123456'}, {id: 'dp_789012'}]; 117 | 118 | const context = {}; 119 | 120 | stripe.disputes.list.mockResolvedValue({data: mockDisputes}); 121 | const result = await listDisputes(stripe, context, params); 122 | 123 | expect(stripe.disputes.list).toHaveBeenCalledWith(params, undefined); 124 | expect(result).toEqual(mockDisputes.map(({id}) => ({id}))); 125 | }); 126 | }); 127 | ``` -------------------------------------------------------------------------------- /typescript/src/shared/subscriptions/updateSubscription.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Stripe from 'stripe'; 2 | import {z} from 'zod'; 3 | import type {Context} from '@/shared/configuration'; 4 | import type {Tool} from '@/shared/tools'; 5 | 6 | export const updateSubscriptionPrompt = (_context: Context = {}): string => { 7 | return `This tool will update an existing subscription in Stripe. If changing an existing subscription item, the existing subscription item has to be set to deleted and the new one has to be added. 8 | 9 | It takes the following arguments: 10 | - subscription (str, required): The ID of the subscription to update. 11 | - proration_behavior (str, optional): Determines how to handle prorations when the subscription items change. Options: 'create_prorations', 'none', 'always_invoice', 'none_implicit'. 12 | - items (array, optional): A list of subscription items to update, add, or remove. Each item can have the following properties: 13 | - id (str, optional): The ID of the subscription item to modify. 14 | - price (str, optional): The ID of the price to switch to. 15 | - quantity (int, optional): The quantity of the plan to subscribe to. 16 | - deleted (bool, optional): Whether to delete this item. 17 | `; 18 | }; 19 | 20 | export const updateSubscription = async ( 21 | stripe: Stripe, 22 | context: Context, 23 | params: z.infer<ReturnType<typeof updateSubscriptionParameters>> 24 | ) => { 25 | try { 26 | const {subscription: subscriptionId, ...updateParams} = params; 27 | 28 | const subscription = await stripe.subscriptions.update( 29 | subscriptionId, 30 | updateParams, 31 | context.account ? {stripeAccount: context.account} : undefined 32 | ); 33 | 34 | return subscription; 35 | } catch (error) { 36 | return 'Failed to update subscription'; 37 | } 38 | }; 39 | 40 | export const updateSubscriptionParameters = ( 41 | _context: Context = {} 42 | ): z.AnyZodObject => { 43 | return z.object({ 44 | subscription: z.string().describe('The ID of the subscription to update.'), 45 | proration_behavior: z 46 | .enum(['create_prorations', 'none', 'always_invoice', 'none_implicit']) 47 | .optional() 48 | .describe( 49 | 'Determines how to handle prorations when the subscription items change.' 50 | ), 51 | items: z 52 | .array( 53 | z.object({ 54 | id: z 55 | .string() 56 | .optional() 57 | .describe('The ID of the subscription item to modify.'), 58 | price: z 59 | .string() 60 | .optional() 61 | .describe('The ID of the price to switch to.'), 62 | quantity: z 63 | .number() 64 | .int() 65 | .min(1) 66 | .optional() 67 | .describe('The quantity of the plan to subscribe to.'), 68 | deleted: z 69 | .boolean() 70 | .optional() 71 | .describe('Whether to delete this item.'), 72 | }) 73 | ) 74 | .optional() 75 | .describe('A list of subscription items to update, add, or remove.'), 76 | }); 77 | }; 78 | 79 | export const updateSubscriptionAnnotations = () => ({ 80 | destructiveHint: false, 81 | idempotentHint: false, 82 | openWorldHint: true, 83 | readOnlyHint: false, 84 | title: 'Update subscription', 85 | }); 86 | 87 | const tool = (context: Context): Tool => ({ 88 | method: 'update_subscription', 89 | name: 'Update Subscription', 90 | description: updateSubscriptionPrompt(context), 91 | parameters: updateSubscriptionParameters(context), 92 | annotations: updateSubscriptionAnnotations(), 93 | actions: { 94 | subscriptions: { 95 | update: true, 96 | }, 97 | }, 98 | execute: updateSubscription, 99 | }); 100 | 101 | export default tool; 102 | ``` -------------------------------------------------------------------------------- /python/stripe_agent_toolkit/prompts.py: -------------------------------------------------------------------------------- ```python 1 | CREATE_CUSTOMER_PROMPT = """ 2 | This tool will create a customer in Stripe. 3 | 4 | It takes two arguments: 5 | - name (str): The name of the customer. 6 | - email (str, optional): The email of the customer. 7 | """ 8 | 9 | LIST_CUSTOMERS_PROMPT = """ 10 | This tool will fetch a list of Customers from Stripe. 11 | 12 | It takes no input. 13 | """ 14 | 15 | CREATE_PRODUCT_PROMPT = """ 16 | This tool will create a product in Stripe. 17 | 18 | It takes two arguments: 19 | - name (str): The name of the product. 20 | - description (str, optional): The description of the product. 21 | """ 22 | 23 | LIST_PRODUCTS_PROMPT = """ 24 | This tool will fetch a list of Products from Stripe. 25 | 26 | It takes one optional argument: 27 | - limit (int, optional): The number of products to return. 28 | """ 29 | 30 | CREATE_PRICE_PROMPT = """ 31 | This tool will create a price in Stripe. If a product has not already been 32 | specified, a product should be created first. 33 | 34 | It takes three arguments: 35 | - product (str): The ID of the product to create the price for. 36 | - unit_amount (int): The unit amount of the price in cents. 37 | - currency (str): The currency of the price. 38 | """ 39 | 40 | LIST_PRICES_PROMPT = """ 41 | This tool will fetch a list of Prices from Stripe. 42 | 43 | It takes two arguments: 44 | - product (str, optional): The ID of the product to list prices for. 45 | - limit (int, optional): The number of prices to return. 46 | """ 47 | 48 | CREATE_PAYMENT_LINK_PROMPT = """ 49 | This tool will create a payment link in Stripe. 50 | 51 | It takes two arguments: 52 | - price (str): The ID of the price to create the payment link for. 53 | - quantity (int): The quantity of the product to include in the payment link. 54 | - redirect_url (string, optional): The URL the customer will be redirected to after the purchase is complete. 55 | """ 56 | 57 | LIST_INVOICES_PROMPT = """ 58 | This tool will list invoices in Stripe. 59 | 60 | It takes two arguments: 61 | - customer (str, optional): The ID of the customer to list the invoices for. 62 | - limit (int, optional): The number of prices to return. 63 | """ 64 | 65 | CREATE_INVOICE_PROMPT = """ 66 | This tool will create an invoice in Stripe. 67 | 68 | It takes one argument: 69 | - customer (str): The ID of the customer to create the invoice for. 70 | """ 71 | 72 | CREATE_INVOICE_ITEM_PROMPT = """ 73 | This tool will create an invoice item in Stripe. 74 | 75 | It takes two arguments: 76 | - customer (str): The ID of the customer to create the invoice item for. 77 | - price (str): The ID of the price to create the invoice item for. 78 | - invoice (str): The ID of the invoice to create the invoice item for. 79 | """ 80 | 81 | FINALIZE_INVOICE_PROMPT = """ 82 | This tool will finalize an invoice in Stripe. 83 | 84 | It takes one argument: 85 | - invoice (str): The ID of the invoice to finalize. 86 | """ 87 | 88 | RETRIEVE_BALANCE_PROMPT = """ 89 | This tool will retrieve the balance from Stripe. It takes no input. 90 | """ 91 | 92 | CREATE_REFUND_PROMPT = """ 93 | This tool will refund a payment intent in Stripe. 94 | 95 | It takes three arguments: 96 | - payment_intent (str): The ID of the payment intent to refund. 97 | - amount (int, optional): The amount to refund in cents. 98 | - reason (str, optional): The reason for the refund. 99 | """ 100 | 101 | LIST_PAYMENT_INTENTS_PROMPT = """ 102 | This tool will list payment intents in Stripe. 103 | 104 | It takes two arguments: 105 | - customer (str, optional): The ID of the customer to list payment intents for. 106 | - limit (int, optional): The number of payment intents to return. 107 | """ 108 | 109 | CREATE_BILLING_PORTAL_SESSION_PROMPT = """ 110 | This tool will create a billing portal session. 111 | 112 | It takes two arguments: 113 | - customer (str): The ID of the customer to create the invoice item for. 114 | - return_url (str, optional): The default URL to return to afterwards. 115 | """ 116 | ``` -------------------------------------------------------------------------------- /typescript/examples/cloudflare/src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import {z} from 'zod'; 3 | import { 4 | PaymentState, 5 | experimental_PaidMcpAgent as PaidMcpAgent, 6 | } from '@stripe/agent-toolkit/cloudflare'; 7 | import {generateImage} from './imageGenerator'; 8 | import {OAuthProvider} from '@cloudflare/workers-oauth-provider'; 9 | import app from './app'; 10 | 11 | type Bindings = Env; 12 | 13 | type Props = { 14 | userEmail: string; 15 | }; 16 | 17 | type State = PaymentState & {}; 18 | 19 | export class MyMCP extends PaidMcpAgent<Bindings, State, Props> { 20 | server = new McpServer({ 21 | name: 'Demo', 22 | version: '1.0.0', 23 | }); 24 | 25 | initialState: State = {}; 26 | 27 | async init() { 28 | this.server.tool('add', {a: z.number(), b: z.number()}, ({a, b}) => { 29 | return { 30 | content: [{type: 'text', text: `Result: ${a + b}`}], 31 | }; 32 | }); 33 | 34 | // One-time payment, then the tool is usable forever 35 | this.paidTool( 36 | 'buy_premium', 37 | 'Buy a premium account', 38 | {}, 39 | () => { 40 | return { 41 | content: [{type: 'text', text: `You now have a premium account!`}], 42 | }; 43 | }, 44 | { 45 | checkout: { 46 | success_url: 'http://localhost:4242/payment/success', 47 | line_items: [ 48 | { 49 | price: process.env.STRIPE_PRICE_ID_ONE_TIME_PAYMENT, 50 | quantity: 1, 51 | }, 52 | ], 53 | mode: 'payment', 54 | }, 55 | paymentReason: 56 | 'Open the checkout link in the browser to buy a premium account.', 57 | } 58 | ); 59 | 60 | // Subscription, then the tool is usable as long as the subscription is active 61 | this.paidTool( 62 | 'big_add', 63 | 'Add two numbers together', 64 | { 65 | a: z.number(), 66 | b: z.number(), 67 | }, 68 | ({a, b}) => { 69 | return { 70 | content: [{type: 'text', text: `Result: ${a + b}`}], 71 | }; 72 | }, 73 | { 74 | checkout: { 75 | success_url: 'http://localhost:4242/payment/success', 76 | line_items: [ 77 | { 78 | price: process.env.STRIPE_PRICE_ID_SUBSCRIPTION, 79 | quantity: 1, 80 | }, 81 | ], 82 | mode: 'subscription', 83 | }, 84 | paymentReason: 85 | 'You must pay a subscription to add two big numbers together.', 86 | } 87 | ); 88 | 89 | // Usage-based metered payments (Each tool call requires a payment) 90 | this.paidTool( 91 | 'generate_emoji', 92 | 'Generate an emoji given a single word (the `object` parameter describing the emoji)', 93 | { 94 | object: z.string().describe('one word'), 95 | }, 96 | ({object}) => { 97 | return { 98 | content: [{type: 'text', text: generateImage(object)}], 99 | }; 100 | }, 101 | { 102 | checkout: { 103 | success_url: 'http://localhost:4242/payment/success', 104 | line_items: [ 105 | { 106 | price: process.env.STRIPE_PRICE_ID_USAGE_BASED_SUBSCRIPTION, 107 | }, 108 | ], 109 | mode: 'subscription', 110 | }, 111 | meterEvent: 'image_generation', 112 | paymentReason: 113 | 'You get 3 free generations, then we charge 10 cents per generation.', 114 | } 115 | ); 116 | } 117 | } 118 | 119 | // Export the OAuth handler as the default 120 | export default new OAuthProvider({ 121 | apiRoute: '/sse', 122 | apiHandlers: { 123 | // @ts-ignore 124 | '/sse': MyMCP.serveSSE('/sse'), 125 | // @ts-ignore 126 | '/mcp': MyMCP.serve('/mcp'), 127 | }, 128 | // @ts-ignore 129 | defaultHandler: app, 130 | authorizeEndpoint: '/authorize', 131 | tokenEndpoint: '/token', 132 | clientRegistrationEndpoint: '/register', 133 | }); 134 | ``` -------------------------------------------------------------------------------- /python/stripe_agent_toolkit/strands/hooks.py: -------------------------------------------------------------------------------- ```python 1 | from typing import Any, Optional, Dict 2 | from ..api import StripeAPI 3 | 4 | 5 | class BillingHooks: 6 | """Billing hooks for Strands framework to track usage and create meter events.""" 7 | 8 | def __init__( 9 | self, 10 | stripe: StripeAPI, 11 | type: str, 12 | customer: str, 13 | meter: Optional[str] = None, 14 | meters: Optional[Dict[str, str]] = None 15 | ): 16 | """ 17 | Initialize billing hooks. 18 | 19 | Args: 20 | stripe: StripeAPI instance 21 | type: Type of billing - "outcome" or "token" 22 | customer: Customer ID for billing 23 | meter: Single meter ID for outcome-based billing 24 | meters: Dictionary of meter IDs for token-based billing (input/output) 25 | """ 26 | self.type = type 27 | self.stripe = stripe 28 | self.customer = customer 29 | self.meter = meter 30 | self.meters = meters or {} 31 | 32 | def on_start(self, context: Any = None) -> None: 33 | """Called when agent execution starts.""" 34 | pass 35 | 36 | def on_end(self, context: Any = None, output: Any = None, usage: Any = None) -> None: 37 | """ 38 | Called when agent execution ends. 39 | 40 | Args: 41 | context: Execution context (may contain usage information) 42 | output: Agent output 43 | usage: Usage information (tokens, etc.) 44 | """ 45 | if self.type == "outcome": 46 | # Create a single meter event for outcome-based billing 47 | if self.meter: 48 | self.stripe.create_meter_event(self.meter, self.customer) 49 | 50 | elif self.type == "token": 51 | # Create meter events for token-based billing 52 | if usage: 53 | # Try to extract token usage from different possible formats 54 | input_tokens = self._extract_input_tokens(usage, context) 55 | output_tokens = self._extract_output_tokens(usage, context) 56 | 57 | if input_tokens and self.meters.get("input"): 58 | self.stripe.create_meter_event( 59 | self.meters["input"], 60 | self.customer, 61 | str(input_tokens) 62 | ) 63 | 64 | if output_tokens and self.meters.get("output"): 65 | self.stripe.create_meter_event( 66 | self.meters["output"], 67 | self.customer, 68 | str(output_tokens) 69 | ) 70 | 71 | def on_error(self, context: Any = None, error: Exception = None) -> None: 72 | """Called when agent execution encounters an error.""" 73 | pass 74 | 75 | def _extract_input_tokens(self, usage: Any, context: Any = None) -> Optional[int]: 76 | """Extract input token count from usage information.""" 77 | if hasattr(usage, 'input_tokens'): 78 | return usage.input_tokens 79 | elif isinstance(usage, dict): 80 | return usage.get('input_tokens') or usage.get('prompt_tokens') 81 | elif context and hasattr(context, 'usage') and hasattr(context.usage, 'input_tokens'): 82 | return context.usage.input_tokens 83 | return None 84 | 85 | def _extract_output_tokens(self, usage: Any, context: Any = None) -> Optional[int]: 86 | """Extract output token count from usage information.""" 87 | if hasattr(usage, 'output_tokens'): 88 | return usage.output_tokens 89 | elif isinstance(usage, dict): 90 | return usage.get('output_tokens') or usage.get('completion_tokens') 91 | elif context and hasattr(context, 'usage') and hasattr(context.usage, 'output_tokens'): 92 | return context.usage.output_tokens 93 | return None 94 | ``` -------------------------------------------------------------------------------- /python/stripe_agent_toolkit/api.py: -------------------------------------------------------------------------------- ```python 1 | """Util that calls Stripe.""" 2 | 3 | from __future__ import annotations 4 | 5 | import json 6 | import stripe 7 | from typing import Optional 8 | from pydantic import BaseModel 9 | 10 | from .configuration import Context 11 | 12 | from .functions import ( 13 | create_customer, 14 | list_customers, 15 | create_product, 16 | list_products, 17 | create_price, 18 | list_prices, 19 | create_payment_link, 20 | list_invoices, 21 | create_invoice, 22 | create_invoice_item, 23 | finalize_invoice, 24 | retrieve_balance, 25 | create_refund, 26 | list_payment_intents, 27 | create_billing_portal_session, 28 | ) 29 | 30 | 31 | class StripeAPI(BaseModel): 32 | """ "Wrapper for Stripe API""" 33 | 34 | _context: Context 35 | 36 | def __init__(self, secret_key: str, context: Optional[Context]): 37 | super().__init__() 38 | 39 | self._context = context if context is not None else Context() 40 | 41 | stripe.api_key = secret_key 42 | stripe.set_app_info( 43 | "stripe-agent-toolkit-python", 44 | version="0.6.1", 45 | url="https://github.com/stripe/agent-toolkit", 46 | ) 47 | 48 | def create_meter_event(self, event: str, customer: str, value: Optional[str] = None) -> str: 49 | meter_event_data: dict = { 50 | "event_name": event, 51 | "payload": { 52 | "stripe_customer_id": customer, 53 | }, 54 | } 55 | if value is not None: 56 | meter_event_data["payload"]["value"] = value 57 | 58 | if self._context.get("account") is not None: 59 | account = self._context.get("account") 60 | if account is not None: 61 | meter_event_data["stripe_account"] = account 62 | 63 | stripe.billing.MeterEvent.create(**meter_event_data) 64 | 65 | def run(self, method: str, *args, **kwargs) -> str: 66 | if method == "create_customer": 67 | return json.dumps(create_customer(self._context, *args, **kwargs)) 68 | elif method == "list_customers": 69 | return json.dumps(list_customers(self._context, *args, **kwargs)) 70 | elif method == "create_product": 71 | return json.dumps(create_product(self._context, *args, **kwargs)) 72 | elif method == "list_products": 73 | return json.dumps(list_products(self._context, *args, **kwargs)) 74 | elif method == "create_price": 75 | return json.dumps(create_price(self._context, *args, **kwargs)) 76 | elif method == "list_prices": 77 | return json.dumps(list_prices(self._context, *args, **kwargs)) 78 | elif method == "create_payment_link": 79 | return json.dumps( 80 | create_payment_link(self._context, *args, **kwargs) 81 | ) 82 | elif method == "list_invoices": 83 | return json.dumps(list_invoices(self._context, *args, **kwargs)) 84 | elif method == "create_invoice": 85 | return json.dumps(create_invoice(self._context, *args, **kwargs)) 86 | elif method == "create_invoice_item": 87 | return json.dumps( 88 | create_invoice_item(self._context, *args, **kwargs) 89 | ) 90 | elif method == "finalize_invoice": 91 | return json.dumps(finalize_invoice(self._context, *args, **kwargs)) 92 | elif method == "retrieve_balance": 93 | return json.dumps(retrieve_balance(self._context, *args, **kwargs)) 94 | elif method == "create_refund": 95 | return json.dumps(create_refund(self._context, *args, **kwargs)) 96 | elif method == "list_payment_intents": 97 | return json.dumps( 98 | list_payment_intents(self._context, *args, **kwargs) 99 | ) 100 | elif method == "create_billing_portal_session": 101 | return json.dumps( 102 | create_billing_portal_session(self._context, *args, **kwargs) 103 | ) 104 | else: 105 | raise ValueError("Invalid method " + method) 106 | ``` -------------------------------------------------------------------------------- /typescript/examples/cloudflare/src/app.ts: -------------------------------------------------------------------------------- ```typescript 1 | // From: https://github.com/cloudflare/ai/blob/main/demos/remote-mcp-server/src/app.ts 2 | 3 | import {Hono} from 'hono'; 4 | import { 5 | layout, 6 | homeContent, 7 | parseApproveFormBody, 8 | renderAuthorizationRejectedContent, 9 | renderAuthorizationApprovedContent, 10 | renderLoggedInAuthorizeScreen, 11 | renderLoggedOutAuthorizeScreen, 12 | renderPaymentSuccessContent, 13 | } from './utils'; 14 | import type {OAuthHelpers} from '@cloudflare/workers-oauth-provider'; 15 | 16 | export type Bindings = Env & { 17 | OAUTH_PROVIDER: OAuthHelpers; 18 | }; 19 | 20 | const app = new Hono<{ 21 | Bindings: Bindings; 22 | }>(); 23 | 24 | // Render a basic homepage placeholder to make sure the app is up 25 | app.get('/', async (c) => { 26 | const content = await homeContent(c.req.raw); 27 | return c.html(layout(content, 'MCP Remote Auth Demo - Home')); 28 | }); 29 | 30 | // Render an authorization page 31 | // If the user is logged in, we'll show a form to approve the appropriate scopes 32 | // If the user is not logged in, we'll show a form to both login and approve the scopes 33 | app.get('/authorize', async (c) => { 34 | // We don't have an actual auth system, so to demonstrate both paths, you can 35 | // hard-code whether the user is logged in or not. We'll default to true 36 | // const isLoggedIn = false; 37 | const isLoggedIn = true; 38 | 39 | const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw); 40 | 41 | const oauthScopes = [ 42 | { 43 | name: 'read_profile', 44 | description: 'Read your basic profile information', 45 | }, 46 | {name: 'read_data', description: 'Access your stored data'}, 47 | {name: 'write_data', description: 'Create and modify your data'}, 48 | ]; 49 | 50 | if (isLoggedIn) { 51 | const content = await renderLoggedInAuthorizeScreen( 52 | oauthScopes, 53 | oauthReqInfo 54 | ); 55 | return c.html(layout(content, 'MCP Remote Auth Demo - Authorization')); 56 | } 57 | 58 | const content = await renderLoggedOutAuthorizeScreen( 59 | oauthScopes, 60 | oauthReqInfo 61 | ); 62 | return c.html(layout(content, 'MCP Remote Auth Demo - Authorization')); 63 | }); 64 | 65 | app.get('/payment/success', async (c) => { 66 | return c.html( 67 | layout( 68 | await renderPaymentSuccessContent(), 69 | 'MCP Remote Auth Demo - Payment Success' 70 | ) 71 | ); 72 | }); 73 | 74 | // The /authorize page has a form that will POST to /approve 75 | // This endpoint is responsible for validating any login information and 76 | // then completing the authorization request with the OAUTH_PROVIDER 77 | app.post('/approve', async (c) => { 78 | const {action, oauthReqInfo, email, password} = await parseApproveFormBody( 79 | await c.req.parseBody() 80 | ); 81 | 82 | if (!oauthReqInfo) { 83 | return c.html('INVALID LOGIN', 401); 84 | } 85 | 86 | // If the user needs to both login and approve, we should validate the login first 87 | if (action === 'login_approve') { 88 | // We'll allow any values for email and password for this demo 89 | // but you could validate them here 90 | // Ex: 91 | // if (email !== "[email protected]" || password !== "password") { 92 | // biome-ignore lint/correctness/noConstantCondition: This is a demo 93 | if (false) { 94 | return c.html( 95 | layout( 96 | await renderAuthorizationRejectedContent('/'), 97 | 'MCP Remote Auth Demo - Authorization Status' 98 | ) 99 | ); 100 | } 101 | } 102 | 103 | // The user must be successfully logged in and have approved the scopes, so we 104 | // can complete the authorization request 105 | const {redirectTo} = await c.env.OAUTH_PROVIDER.completeAuthorization({ 106 | request: oauthReqInfo, 107 | userId: email, 108 | metadata: { 109 | label: 'Test User', 110 | }, 111 | scope: oauthReqInfo.scope, 112 | props: { 113 | // Here, you can send data to the MCP server 114 | userEmail: email, 115 | }, 116 | }); 117 | 118 | // Store the redirect URL per email in KV somewhere 119 | c.env.OAUTH_KV.put(email, redirectTo); 120 | 121 | return c.html( 122 | layout( 123 | await renderAuthorizationApprovedContent(redirectTo), 124 | 'MCP Remote Auth Demo - Authorization Status' 125 | ) 126 | ); 127 | }); 128 | 129 | export default app; 130 | ``` -------------------------------------------------------------------------------- /typescript/examples/cloudflare/src/oauth.ts: -------------------------------------------------------------------------------- ```typescript 1 | // From: https://github.com/cloudflare/ai/blob/main/demos/remote-mcp-server/src/app.ts 2 | 3 | import {Hono} from 'hono'; 4 | import { 5 | layout, 6 | homeContent, 7 | parseApproveFormBody, 8 | renderAuthorizationRejectedContent, 9 | renderAuthorizationApprovedContent, 10 | renderLoggedInAuthorizeScreen, 11 | renderLoggedOutAuthorizeScreen, 12 | renderPaymentSuccessContent, 13 | } from './utils'; 14 | import type {OAuthHelpers} from '@cloudflare/workers-oauth-provider'; 15 | 16 | export type Bindings = Env & { 17 | OAUTH_PROVIDER: OAuthHelpers; 18 | }; 19 | 20 | const app = new Hono<{ 21 | Bindings: Bindings; 22 | }>(); 23 | 24 | // Render a basic homepage placeholder to make sure the app is up 25 | app.get('/', async (c) => { 26 | const content = await homeContent(c.req.raw); 27 | return c.html(layout(content, 'MCP Remote Auth Demo - Home')); 28 | }); 29 | 30 | // Render an authorization page 31 | // If the user is logged in, we'll show a form to approve the appropriate scopes 32 | // If the user is not logged in, we'll show a form to both login and approve the scopes 33 | app.get('/authorize', async (c) => { 34 | // We don't have an actual auth system, so to demonstrate both paths, you can 35 | // hard-code whether the user is logged in or not. We'll default to true 36 | // const isLoggedIn = false; 37 | const isLoggedIn = true; 38 | 39 | const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw); 40 | 41 | const oauthScopes = [ 42 | { 43 | name: 'read_profile', 44 | description: 'Read your basic profile information', 45 | }, 46 | {name: 'read_data', description: 'Access your stored data'}, 47 | {name: 'write_data', description: 'Create and modify your data'}, 48 | ]; 49 | 50 | if (isLoggedIn) { 51 | const content = await renderLoggedInAuthorizeScreen( 52 | oauthScopes, 53 | oauthReqInfo 54 | ); 55 | return c.html(layout(content, 'MCP Remote Auth Demo - Authorization')); 56 | } 57 | 58 | const content = await renderLoggedOutAuthorizeScreen( 59 | oauthScopes, 60 | oauthReqInfo 61 | ); 62 | return c.html(layout(content, 'MCP Remote Auth Demo - Authorization')); 63 | }); 64 | 65 | app.get('/payment/success', async (c) => { 66 | return c.html( 67 | layout( 68 | await renderPaymentSuccessContent(), 69 | 'MCP Remote Auth Demo - Payment Success' 70 | ) 71 | ); 72 | }); 73 | 74 | // The /authorize page has a form that will POST to /approve 75 | // This endpoint is responsible for validating any login information and 76 | // then completing the authorization request with the OAUTH_PROVIDER 77 | app.post('/approve', async (c) => { 78 | const {action, oauthReqInfo, email, password} = await parseApproveFormBody( 79 | await c.req.parseBody() 80 | ); 81 | 82 | if (!oauthReqInfo) { 83 | return c.html('INVALID LOGIN', 401); 84 | } 85 | 86 | // If the user needs to both login and approve, we should validate the login first 87 | if (action === 'login_approve') { 88 | // We'll allow any values for email and password for this demo 89 | // but you could validate them here 90 | // Ex: 91 | // if (email !== "[email protected]" || password !== "password") { 92 | // biome-ignore lint/correctness/noConstantCondition: This is a demo 93 | if (false) { 94 | return c.html( 95 | layout( 96 | await renderAuthorizationRejectedContent('/'), 97 | 'MCP Remote Auth Demo - Authorization Status' 98 | ) 99 | ); 100 | } 101 | } 102 | 103 | // The user must be successfully logged in and have approved the scopes, so we 104 | // can complete the authorization request 105 | const {redirectTo} = await c.env.OAUTH_PROVIDER.completeAuthorization({ 106 | request: oauthReqInfo, 107 | userId: email, 108 | metadata: { 109 | label: 'Test User', 110 | }, 111 | scope: oauthReqInfo.scope, 112 | props: { 113 | // Here, you can send data to the MCP server 114 | userEmail: email, 115 | }, 116 | }); 117 | 118 | // Store the redirect URL per email in KV somewhere 119 | c.env.OAUTH_KV.put(email, redirectTo); 120 | 121 | return c.html( 122 | layout( 123 | await renderAuthorizationApprovedContent(redirectTo), 124 | 'MCP Remote Auth Demo - Authorization Status' 125 | ) 126 | ); 127 | }); 128 | 129 | export default app; 130 | ``` -------------------------------------------------------------------------------- /modelcontextprotocol/src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import {StripeAgentToolkit} from '@stripe/agent-toolkit/modelcontextprotocol'; 4 | import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; 5 | import {green, red, yellow} from 'colors'; 6 | 7 | type ToolkitConfig = { 8 | actions: { 9 | [product: string]: {[action: string]: boolean}; 10 | }; 11 | context?: { 12 | account?: string; 13 | mode: 'modelcontextprotocol'; 14 | }; 15 | }; 16 | 17 | type Options = { 18 | tools?: string[]; 19 | apiKey?: string; 20 | stripeAccount?: string; 21 | }; 22 | 23 | const ACCEPTED_ARGS = ['api-key', 'tools', 'stripe-account']; 24 | const ACCEPTED_TOOLS = [ 25 | 'coupons.create', 26 | 'coupons.read', 27 | 'customers.create', 28 | 'customers.read', 29 | 'products.create', 30 | 'products.read', 31 | 'prices.create', 32 | 'prices.read', 33 | 'paymentLinks.create', 34 | 'invoices.create', 35 | 'invoices.read', 36 | 'invoices.update', 37 | 'invoiceItems.create', 38 | 'balance.read', 39 | 'refunds.create', 40 | 'paymentIntents.read', 41 | 'subscriptions.read', 42 | 'subscriptions.update', 43 | 'disputes.read', 44 | 'disputes.update', 45 | 'documentation.read', 46 | ]; 47 | 48 | export function parseArgs(args: string[]): Options { 49 | const options: Options = {}; 50 | 51 | args.forEach((arg) => { 52 | if (arg.startsWith('--')) { 53 | const [key, value] = arg.slice(2).split('='); 54 | 55 | if (key == 'tools') { 56 | options.tools = value.split(','); 57 | } else if (key == 'api-key') { 58 | if (!value.startsWith('sk_') && !value.startsWith('rk_')) { 59 | throw new Error('API key must start with "sk_" or "rk_".'); 60 | } 61 | options.apiKey = value; 62 | } else if (key == 'stripe-account') { 63 | // Validate api-key format 64 | if (!value.startsWith('acct_')) { 65 | throw new Error('Stripe account must start with "acct_".'); 66 | } 67 | options.stripeAccount = value; 68 | } else { 69 | throw new Error( 70 | `Invalid argument: ${key}. Accepted arguments are: ${ACCEPTED_ARGS.join( 71 | ', ' 72 | )}` 73 | ); 74 | } 75 | } 76 | }); 77 | 78 | // Check if required tools arguments is present 79 | if (!options.tools) { 80 | throw new Error('The --tools arguments must be provided.'); 81 | } 82 | 83 | // Validate tools against accepted enum values 84 | options.tools.forEach((tool: string) => { 85 | if (tool == 'all') { 86 | return; 87 | } 88 | if (!ACCEPTED_TOOLS.includes(tool.trim())) { 89 | throw new Error( 90 | `Invalid tool: ${tool}. Accepted tools are: ${ACCEPTED_TOOLS.join( 91 | ', ' 92 | )}` 93 | ); 94 | } 95 | }); 96 | 97 | // Check if API key is either provided in args or set in environment variables 98 | const apiKey = options.apiKey || process.env.STRIPE_SECRET_KEY; 99 | if (!apiKey) { 100 | throw new Error( 101 | 'Stripe API key not provided. Please either pass it as an argument --api-key=$KEY or set the STRIPE_SECRET_KEY environment variable.' 102 | ); 103 | } 104 | options.apiKey = apiKey; 105 | 106 | return options; 107 | } 108 | 109 | function handleError(error: any) { 110 | console.error(red('\n🚨 Error initializing Stripe MCP server:\n')); 111 | console.error(yellow(` ${error.message}\n`)); 112 | } 113 | 114 | export async function main() { 115 | const options = parseArgs(process.argv.slice(2)); 116 | 117 | // Create the StripeAgentToolkit instance 118 | const selectedTools = options.tools!; 119 | const configuration: ToolkitConfig = {actions: {}}; 120 | 121 | if (selectedTools.includes('all')) { 122 | ACCEPTED_TOOLS.forEach((tool) => { 123 | const [product, action] = tool.split('.'); 124 | configuration.actions[product] = { 125 | ...configuration.actions[product], 126 | [action]: true, 127 | }; 128 | }); 129 | } else { 130 | selectedTools.forEach((tool: any) => { 131 | const [product, action] = tool.split('.'); 132 | configuration.actions[product] = {[action]: true}; 133 | }); 134 | } 135 | 136 | configuration.context = { 137 | mode: 'modelcontextprotocol', 138 | }; 139 | 140 | // Append stripe account to configuration if provided 141 | if (options.stripeAccount) { 142 | configuration.context.account = options.stripeAccount; 143 | } 144 | 145 | const server = new StripeAgentToolkit({ 146 | secretKey: options.apiKey!, 147 | configuration: configuration, 148 | }); 149 | 150 | const transport = new StdioServerTransport(); 151 | await server.connect(transport); 152 | // We use console.error instead of console.log since console.log will output to stdio, which will confuse the MCP server 153 | console.error(green('✅ Stripe MCP Server running on stdio')); 154 | } 155 | 156 | if (require.main === module) { 157 | main().catch((error) => { 158 | handleError(error); 159 | }); 160 | } 161 | ``` -------------------------------------------------------------------------------- /python/stripe_agent_toolkit/tools.py: -------------------------------------------------------------------------------- ```python 1 | from typing import Dict, List 2 | 3 | from .prompts import ( 4 | CREATE_CUSTOMER_PROMPT, 5 | LIST_CUSTOMERS_PROMPT, 6 | CREATE_PRODUCT_PROMPT, 7 | LIST_PRODUCTS_PROMPT, 8 | CREATE_PRICE_PROMPT, 9 | LIST_PRICES_PROMPT, 10 | CREATE_PAYMENT_LINK_PROMPT, 11 | LIST_INVOICES_PROMPT, 12 | CREATE_INVOICE_PROMPT, 13 | CREATE_INVOICE_ITEM_PROMPT, 14 | FINALIZE_INVOICE_PROMPT, 15 | RETRIEVE_BALANCE_PROMPT, 16 | CREATE_REFUND_PROMPT, 17 | LIST_PAYMENT_INTENTS_PROMPT, 18 | CREATE_BILLING_PORTAL_SESSION_PROMPT, 19 | ) 20 | 21 | from .schema import ( 22 | CreateCustomer, 23 | ListCustomers, 24 | CreateProduct, 25 | ListProducts, 26 | CreatePrice, 27 | ListPrices, 28 | CreatePaymentLink, 29 | ListInvoices, 30 | CreateInvoice, 31 | CreateInvoiceItem, 32 | FinalizeInvoice, 33 | RetrieveBalance, 34 | CreateRefund, 35 | ListPaymentIntents, 36 | CreateBillingPortalSession, 37 | ) 38 | 39 | tools: List[Dict] = [ 40 | { 41 | "method": "create_customer", 42 | "name": "Create Customer", 43 | "description": CREATE_CUSTOMER_PROMPT, 44 | "args_schema": CreateCustomer, 45 | "actions": { 46 | "customers": { 47 | "create": True, 48 | } 49 | }, 50 | }, 51 | { 52 | "method": "list_customers", 53 | "name": "List Customers", 54 | "description": LIST_CUSTOMERS_PROMPT, 55 | "args_schema": ListCustomers, 56 | "actions": { 57 | "customers": { 58 | "read": True, 59 | } 60 | }, 61 | }, 62 | { 63 | "method": "create_product", 64 | "name": "Create Product", 65 | "description": CREATE_PRODUCT_PROMPT, 66 | "args_schema": CreateProduct, 67 | "actions": { 68 | "products": { 69 | "create": True, 70 | } 71 | }, 72 | }, 73 | { 74 | "method": "list_products", 75 | "name": "List Products", 76 | "description": LIST_PRODUCTS_PROMPT, 77 | "args_schema": ListProducts, 78 | "actions": { 79 | "products": { 80 | "read": True, 81 | } 82 | }, 83 | }, 84 | { 85 | "method": "create_price", 86 | "name": "Create Price", 87 | "description": CREATE_PRICE_PROMPT, 88 | "args_schema": CreatePrice, 89 | "actions": { 90 | "prices": { 91 | "create": True, 92 | } 93 | }, 94 | }, 95 | { 96 | "method": "list_prices", 97 | "name": "List Prices", 98 | "description": LIST_PRICES_PROMPT, 99 | "args_schema": ListPrices, 100 | "actions": { 101 | "prices": { 102 | "read": True, 103 | } 104 | }, 105 | }, 106 | { 107 | "method": "create_payment_link", 108 | "name": "Create Payment Link", 109 | "description": CREATE_PAYMENT_LINK_PROMPT, 110 | "args_schema": CreatePaymentLink, 111 | "actions": { 112 | "payment_links": { 113 | "create": True, 114 | } 115 | }, 116 | }, 117 | { 118 | "method": "list_invoices", 119 | "name": "List Invoices", 120 | "description": LIST_INVOICES_PROMPT, 121 | "args_schema": ListInvoices, 122 | "actions": { 123 | "invoices": { 124 | "read": True, 125 | } 126 | }, 127 | }, 128 | { 129 | "method": "create_invoice", 130 | "name": "Create Invoice", 131 | "description": CREATE_INVOICE_PROMPT, 132 | "args_schema": CreateInvoice, 133 | "actions": { 134 | "invoices": { 135 | "create": True, 136 | } 137 | }, 138 | }, 139 | { 140 | "method": "create_invoice_item", 141 | "name": "Create Invoice Item", 142 | "description": CREATE_INVOICE_ITEM_PROMPT, 143 | "args_schema": CreateInvoiceItem, 144 | "actions": { 145 | "invoice_items": { 146 | "create": True, 147 | } 148 | }, 149 | }, 150 | { 151 | "method": "finalize_invoice", 152 | "name": "Finalize Invoice", 153 | "description": FINALIZE_INVOICE_PROMPT, 154 | "args_schema": FinalizeInvoice, 155 | "actions": { 156 | "invoices": { 157 | "update": True, 158 | } 159 | }, 160 | }, 161 | { 162 | "method": "retrieve_balance", 163 | "name": "Retrieve Balance", 164 | "description": RETRIEVE_BALANCE_PROMPT, 165 | "args_schema": RetrieveBalance, 166 | "actions": { 167 | "balance": { 168 | "read": True, 169 | } 170 | }, 171 | }, 172 | { 173 | "method": "create_refund", 174 | "name": "Create Refund", 175 | "description": CREATE_REFUND_PROMPT, 176 | "args_schema": CreateRefund, 177 | "actions": { 178 | "refunds": { 179 | "create": True, 180 | } 181 | }, 182 | }, 183 | { 184 | "method": "list_payment_intents", 185 | "name": "List Payment Intents", 186 | "description": LIST_PAYMENT_INTENTS_PROMPT, 187 | "args_schema": ListPaymentIntents, 188 | "actions": { 189 | "payment_intents": { 190 | "read": True, 191 | } 192 | }, 193 | }, 194 | { 195 | "method": "create_billing_portal_session", 196 | "name": "Create Billing Portal Session", 197 | "description": CREATE_BILLING_PORTAL_SESSION_PROMPT, 198 | "args_schema": CreateBillingPortalSession, 199 | "actions": { 200 | "billing_portal_sessions": { 201 | "create": True, 202 | } 203 | }, 204 | }, 205 | ] 206 | ``` -------------------------------------------------------------------------------- /typescript/src/test/shared/invoices/functions.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {createInvoice} from '@/shared/invoices/createInvoice'; 2 | import {listInvoices} from '@/shared/invoices/listInvoices'; 3 | import {finalizeInvoice} from '@/shared/invoices/finalizeInvoice'; 4 | 5 | const Stripe = jest.fn().mockImplementation(() => ({ 6 | invoices: { 7 | create: jest.fn(), 8 | finalizeInvoice: jest.fn(), 9 | retrieve: jest.fn(), 10 | list: jest.fn(), 11 | }, 12 | })); 13 | 14 | let stripe: ReturnType<typeof Stripe>; 15 | 16 | beforeEach(() => { 17 | stripe = new Stripe('fake-api-key'); 18 | }); 19 | 20 | describe('createInvoice', () => { 21 | it('should create an invoice and return it', async () => { 22 | const params = { 23 | customer: 'cus_123456', 24 | days_until_due: 30, 25 | }; 26 | 27 | const mockInvoice = {id: 'in_123456', customer: 'cus_123456'}; 28 | 29 | const context = {}; 30 | 31 | stripe.invoices.create.mockResolvedValue(mockInvoice); 32 | 33 | const result = await createInvoice(stripe, context, params); 34 | 35 | expect(stripe.invoices.create).toHaveBeenCalledWith( 36 | {...params, collection_method: 'send_invoice'}, 37 | undefined 38 | ); 39 | expect(result).toEqual(mockInvoice); 40 | }); 41 | 42 | it('should specify the connected account if included in context', async () => { 43 | const params = { 44 | customer: 'cus_123456', 45 | days_until_due: 30, 46 | }; 47 | 48 | const mockInvoice = {id: 'in_123456', customer: 'cus_123456'}; 49 | 50 | const context = { 51 | account: 'acct_123456', 52 | }; 53 | 54 | stripe.invoices.create.mockResolvedValue(mockInvoice); 55 | 56 | const result = await createInvoice(stripe, context, params); 57 | 58 | expect(stripe.invoices.create).toHaveBeenCalledWith( 59 | { 60 | ...params, 61 | collection_method: 'send_invoice', 62 | }, 63 | {stripeAccount: context.account} 64 | ); 65 | expect(result).toEqual(mockInvoice); 66 | }); 67 | 68 | it('should create an invoice with a customer if included in context', async () => { 69 | const params = { 70 | days_until_due: 30, 71 | }; 72 | 73 | const mockInvoice = {id: 'in_123456', customer: 'cus_123456'}; 74 | 75 | const context = { 76 | customer: 'cus_123456', 77 | }; 78 | 79 | stripe.invoices.create.mockResolvedValue(mockInvoice); 80 | 81 | const result = await createInvoice(stripe, context, params); 82 | 83 | expect(stripe.invoices.create).toHaveBeenCalledWith( 84 | { 85 | ...params, 86 | customer: context.customer, 87 | collection_method: 'send_invoice', 88 | }, 89 | undefined 90 | ); 91 | expect(result).toEqual(mockInvoice); 92 | }); 93 | }); 94 | 95 | describe('listInvoices', () => { 96 | it('should list invoices and return them', async () => { 97 | const mockInvoices = [ 98 | {id: 'in_123456', customer: 'cus_123456'}, 99 | {id: 'in_789012', customer: 'cus_789012'}, 100 | ]; 101 | 102 | const context = {}; 103 | 104 | stripe.invoices.list.mockResolvedValue({data: mockInvoices}); 105 | 106 | const result = await listInvoices(stripe, context, {}); 107 | 108 | expect(stripe.invoices.list).toHaveBeenCalledWith({}, undefined); 109 | expect(result).toEqual(mockInvoices); 110 | }); 111 | 112 | it('should specify the connected account if included in context', async () => { 113 | const mockInvoices = [ 114 | {id: 'in_123456', customer: 'cus_123456'}, 115 | {id: 'in_789012', customer: 'cus_789012'}, 116 | ]; 117 | 118 | const context = { 119 | account: 'acct_123456', 120 | }; 121 | 122 | stripe.invoices.list.mockResolvedValue({data: mockInvoices}); 123 | 124 | const result = await listInvoices(stripe, context, {}); 125 | 126 | expect(stripe.invoices.list).toHaveBeenCalledWith( 127 | {}, 128 | {stripeAccount: context.account} 129 | ); 130 | expect(result).toEqual(mockInvoices); 131 | }); 132 | 133 | it('should list invoices for a specific customer', async () => { 134 | const mockInvoices = [ 135 | {id: 'in_123456', customer: 'cus_123456'}, 136 | {id: 'in_789012', customer: 'cus_789012'}, 137 | ]; 138 | 139 | const context = { 140 | customer: 'cus_123456', 141 | }; 142 | 143 | stripe.invoices.list.mockResolvedValue({data: mockInvoices}); 144 | 145 | const result = await listInvoices(stripe, context, {}); 146 | 147 | expect(stripe.invoices.list).toHaveBeenCalledWith( 148 | {customer: context.customer}, 149 | undefined 150 | ); 151 | expect(result).toEqual(mockInvoices); 152 | }); 153 | }); 154 | 155 | describe('finalizeInvoice', () => { 156 | it('should finalize an invoice and return it', async () => { 157 | const invoiceId = 'in_123456'; 158 | 159 | const mockInvoice = {id: invoiceId, customer: 'cus_123456'}; 160 | 161 | const context = {}; 162 | 163 | stripe.invoices.finalizeInvoice.mockResolvedValue(mockInvoice); 164 | 165 | const result = await finalizeInvoice(stripe, context, {invoice: invoiceId}); 166 | 167 | expect(stripe.invoices.finalizeInvoice).toHaveBeenCalledWith( 168 | invoiceId, 169 | undefined 170 | ); 171 | expect(result).toEqual(mockInvoice); 172 | }); 173 | 174 | it('should specify the connected account if included in context', async () => { 175 | const invoiceId = 'in_123456'; 176 | 177 | const mockInvoice = {id: invoiceId, customer: 'cus_123456'}; 178 | 179 | const context = { 180 | account: 'acct_123456', 181 | }; 182 | 183 | stripe.invoices.finalizeInvoice.mockResolvedValue(mockInvoice); 184 | 185 | const result = await finalizeInvoice(stripe, context, {invoice: invoiceId}); 186 | 187 | expect(stripe.invoices.finalizeInvoice).toHaveBeenCalledWith(invoiceId, { 188 | stripeAccount: context.account, 189 | }); 190 | expect(result).toEqual(mockInvoice); 191 | }); 192 | }); 193 | ``` -------------------------------------------------------------------------------- /python/stripe_agent_toolkit/schema.py: -------------------------------------------------------------------------------- ```python 1 | from typing import Optional 2 | from pydantic import BaseModel, Field 3 | 4 | 5 | class CreateCustomer(BaseModel): 6 | """Schema for the ``create_customer`` operation.""" 7 | 8 | name: str = Field( 9 | ..., 10 | description="The name of the customer.", 11 | ) 12 | 13 | email: Optional[str] = Field( 14 | None, 15 | description="The email of the customer.", 16 | ) 17 | 18 | 19 | class ListCustomers(BaseModel): 20 | """Schema for the ``list_customers`` operation.""" 21 | 22 | limit: Optional[int] = Field( 23 | None, 24 | description=( 25 | "A limit on the number of objects to be returned." 26 | " Limit can range between 1 and 100." 27 | ), 28 | ) 29 | 30 | email: Optional[str] = Field( 31 | None, 32 | description=( 33 | "A case-sensitive filter on the list based on" 34 | " the customer's email field. The value must be a string." 35 | ), 36 | ) 37 | 38 | 39 | class CreateProduct(BaseModel): 40 | """Schema for the ``create_product`` operation.""" 41 | 42 | name: str = Field( 43 | ..., 44 | description="The name of the product.", 45 | ) 46 | description: Optional[str] = Field( 47 | None, 48 | description="The description of the product.", 49 | ) 50 | 51 | 52 | class ListProducts(BaseModel): 53 | """Schema for the ``list_products`` operation.""" 54 | 55 | limit: Optional[int] = Field( 56 | None, 57 | description=( 58 | "A limit on the number of objects to be returned." 59 | " Limit can range between 1 and 100, and the default is 10." 60 | ), 61 | ) 62 | 63 | 64 | class CreatePrice(BaseModel): 65 | """Schema for the ``create_price`` operation.""" 66 | 67 | product: str = Field( 68 | ..., description="The ID of the product to create the price for." 69 | ) 70 | unit_amount: int = Field( 71 | ..., 72 | description="The unit amount of the price in cents.", 73 | ) 74 | currency: str = Field( 75 | ..., 76 | description="The currency of the price.", 77 | ) 78 | 79 | 80 | class ListPrices(BaseModel): 81 | """Schema for the ``list_prices`` operation.""" 82 | 83 | product: Optional[str] = Field( 84 | None, 85 | description="The ID of the product to list prices for.", 86 | ) 87 | limit: Optional[int] = Field( 88 | None, 89 | description=( 90 | "A limit on the number of objects to be returned." 91 | " Limit can range between 1 and 100, and the default is 10." 92 | ), 93 | ) 94 | 95 | 96 | class CreatePaymentLink(BaseModel): 97 | """Schema for the ``create_payment_link`` operation.""" 98 | 99 | price: str = Field( 100 | ..., 101 | description="The ID of the price to create the payment link for.", 102 | ) 103 | quantity: int = Field( 104 | ..., 105 | description="The quantity of the product to include.", 106 | ) 107 | redirect_url: Optional[str] = Field( 108 | None, 109 | description="The URL the customer will be redirected to after the purchase is complete.", 110 | ) 111 | 112 | 113 | class ListInvoices(BaseModel): 114 | """Schema for the ``list_invoices`` operation.""" 115 | 116 | customer: Optional[str] = Field( 117 | None, 118 | description="The ID of the customer to list invoices for.", 119 | ) 120 | limit: Optional[int] = Field( 121 | None, 122 | description=( 123 | "A limit on the number of objects to be returned." 124 | " Limit can range between 1 and 100, and the default is 10." 125 | ), 126 | ) 127 | 128 | 129 | class CreateInvoice(BaseModel): 130 | """Schema for the ``create_invoice`` operation.""" 131 | 132 | customer: str = Field( 133 | ..., description="The ID of the customer to create the invoice for." 134 | ) 135 | 136 | days_until_due: Optional[int] = Field( 137 | None, 138 | description="The number of days until the invoice is due.", 139 | ) 140 | 141 | 142 | class CreateInvoiceItem(BaseModel): 143 | """Schema for the ``create_invoice_item`` operation.""" 144 | 145 | customer: str = Field( 146 | ..., 147 | description="The ID of the customer to create the invoice item for.", 148 | ) 149 | price: str = Field( 150 | ..., 151 | description="The ID of the price for the item.", 152 | ) 153 | invoice: str = Field( 154 | ..., 155 | description="The ID of the invoice to create the item for.", 156 | ) 157 | 158 | 159 | class FinalizeInvoice(BaseModel): 160 | """Schema for the ``finalize_invoice`` operation.""" 161 | 162 | invoice: str = Field( 163 | ..., 164 | description="The ID of the invoice to finalize.", 165 | ) 166 | 167 | 168 | class RetrieveBalance(BaseModel): 169 | """Schema for the ``retrieve_balance`` operation.""" 170 | 171 | pass 172 | 173 | 174 | class CreateRefund(BaseModel): 175 | """Schema for the ``create_refund`` operation.""" 176 | 177 | payment_intent: str = Field( 178 | ..., 179 | description="The ID of the PaymentIntent to refund.", 180 | ) 181 | amount: Optional[int] = Field( 182 | ..., 183 | description="The amount to refund in cents.", 184 | ) 185 | 186 | class ListPaymentIntents(BaseModel): 187 | """Schema for the ``list_payment_intents`` operation.""" 188 | 189 | customer: Optional[str] = Field( 190 | None, 191 | description="The ID of the customer to list payment intents for.", 192 | ) 193 | limit: Optional[int] = Field( 194 | None, 195 | description=( 196 | "A limit on the number of objects to be returned." 197 | " Limit can range between 1 and 100." 198 | ), 199 | ) 200 | 201 | class CreateBillingPortalSession(BaseModel): 202 | """Schema for the ``create_billing_portal_session`` operation.""" 203 | 204 | customer: str = Field( 205 | None, 206 | description="The ID of the customer to create the billing portal session for.", 207 | ) 208 | return_url: Optional[str] = Field( 209 | None, 210 | description=( 211 | "The default URL to return to afterwards." 212 | ), 213 | ) 214 | ``` -------------------------------------------------------------------------------- /typescript/src/modelcontextprotocol/register-paid-tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {z, type ZodRawShape} from 'zod'; 2 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'; 3 | import {ToolCallback} from '@modelcontextprotocol/sdk/server/mcp.js'; 4 | import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js'; 5 | import Stripe from 'stripe'; 6 | 7 | /* 8 | * This supports one-time payment, subscription, usage-based metered payment. 9 | * For usage-based, set a `meterEvent` 10 | */ 11 | export type PaidToolOptions = { 12 | paymentReason: string; 13 | meterEvent?: string; 14 | stripeSecretKey: string; 15 | userEmail: string; 16 | checkout: Stripe.Checkout.SessionCreateParams; 17 | }; 18 | 19 | export async function registerPaidTool<Args extends ZodRawShape>( 20 | mcpServer: McpServer, 21 | toolName: string, 22 | toolDescription: string, 23 | paramsSchema: Args, 24 | // @ts-ignore: The typescript compiler complains this is an infinitely deep type 25 | paidCallback: ToolCallback<Args>, 26 | options: PaidToolOptions 27 | ) { 28 | const priceId = options.checkout.line_items?.find((li) => li.price)?.price; 29 | 30 | if (!priceId) { 31 | throw new Error( 32 | 'Price ID is required for a paid MCP tool. Learn more about prices: https://docs.stripe.com/products-prices/how-products-and-prices-work' 33 | ); 34 | } 35 | 36 | const stripe = new Stripe(options.stripeSecretKey, { 37 | appInfo: { 38 | name: 'stripe-agent-toolkit-mcp-payments', 39 | version: '0.7.11', 40 | url: 'https://github.com/stripe/agent-toolkit', 41 | }, 42 | }); 43 | 44 | const getCurrentCustomerID = async () => { 45 | const customers = await stripe.customers.list({ 46 | email: options.userEmail, 47 | }); 48 | let customerId: null | string = null; 49 | if (customers.data.length > 0) { 50 | customerId = 51 | customers.data.find((customer) => { 52 | return customer.email === options.userEmail; 53 | })?.id || null; 54 | } 55 | if (!customerId) { 56 | const customer = await stripe.customers.create({ 57 | email: options.userEmail, 58 | }); 59 | customerId = customer.id; 60 | } 61 | 62 | return customerId; 63 | }; 64 | 65 | const isToolPaidFor = async (toolName: string, customerId: string) => { 66 | // Check for paid checkout session for this tool (by metadata) 67 | const sessions = await stripe.checkout.sessions.list({ 68 | customer: customerId, 69 | limit: 100, 70 | }); 71 | const paidSession = sessions.data.find( 72 | (session) => 73 | session.metadata?.toolName === toolName && 74 | session.payment_status === 'paid' 75 | ); 76 | 77 | if (paidSession?.subscription) { 78 | // Check for active subscription for the priceId 79 | const subs = await stripe.subscriptions.list({ 80 | customer: customerId || '', 81 | status: 'active', 82 | }); 83 | const activeSub = subs.data.find((sub) => 84 | sub.items.data.find((item) => item.price.id === priceId) 85 | ); 86 | if (activeSub) { 87 | return true; 88 | } 89 | } 90 | 91 | if (paidSession) { 92 | return true; 93 | } 94 | return false; 95 | }; 96 | 97 | const createCheckoutSession = async ( 98 | paymentType: string, 99 | customerId: string 100 | ): Promise<CallToolResult | null> => { 101 | try { 102 | const session = await stripe.checkout.sessions.create({ 103 | ...options.checkout, 104 | metadata: { 105 | ...options.checkout.metadata, 106 | toolName, 107 | }, 108 | customer: customerId || undefined, 109 | }); 110 | const result = { 111 | status: 'payment_required', 112 | data: { 113 | paymentType, 114 | checkoutUrl: session.url, 115 | paymentReason: options.paymentReason, 116 | }, 117 | }; 118 | return { 119 | content: [ 120 | { 121 | type: 'text', 122 | text: JSON.stringify(result), 123 | } as {type: 'text'; text: string}, 124 | ], 125 | }; 126 | } catch (error: unknown) { 127 | let errMsg = 'Unknown error'; 128 | if (typeof error === 'object' && error !== null) { 129 | if ( 130 | 'raw' in error && 131 | typeof (error as {raw?: {message?: string}}).raw?.message === 'string' 132 | ) { 133 | errMsg = (error as {raw: {message: string}}).raw.message; 134 | } else if ( 135 | 'message' in error && 136 | typeof (error as {message?: string}).message === 'string' 137 | ) { 138 | errMsg = (error as {message: string}).message; 139 | } 140 | } 141 | console.error('Error creating stripe checkout session', errMsg); 142 | return { 143 | content: [ 144 | { 145 | type: 'text', 146 | text: JSON.stringify({ 147 | status: 'error', 148 | error: errMsg, 149 | }), 150 | } as {type: 'text'; text: string}, 151 | ], 152 | isError: true, 153 | }; 154 | } 155 | }; 156 | 157 | const recordUsage = async (customerId: string) => { 158 | if (!options.meterEvent) return; 159 | await stripe.billing.meterEvents.create({ 160 | event_name: options.meterEvent, 161 | payload: { 162 | stripe_customer_id: customerId, 163 | value: '1', 164 | }, 165 | }); 166 | }; 167 | 168 | // biome-ignore lint/suspicious/noExplicitAny: <explanation> 169 | const callback = async (args: any, extra: any): Promise<CallToolResult> => { 170 | const customerId = await getCurrentCustomerID(); 171 | const paidForTool = await isToolPaidFor(toolName, customerId); 172 | const paymentType = options.meterEvent 173 | ? 'usageBased' 174 | : 'oneTimeSubscription'; 175 | if (!paidForTool) { 176 | const checkoutResult = await createCheckoutSession( 177 | paymentType, 178 | customerId 179 | ); 180 | if (checkoutResult) return checkoutResult; 181 | } 182 | if (paymentType === 'usageBased') { 183 | await recordUsage(customerId); 184 | } 185 | // @ts-ignore: The typescript compiler complains this is an infinitely deep type 186 | return paidCallback(args, extra); 187 | }; 188 | 189 | // @ts-ignore: The typescript compiler complains this is an infinitely deep type 190 | mcpServer.tool(toolName, toolDescription, paramsSchema, callback as any); 191 | 192 | await Promise.resolve(); 193 | } 194 | ``` -------------------------------------------------------------------------------- /evals/scorer.ts: -------------------------------------------------------------------------------- ```typescript 1 | require("dotenv").config(); 2 | 3 | import { ClosedQA } from "autoevals"; 4 | import every from "lodash/every"; 5 | import braintrustOpenai from "./braintrust_openai"; 6 | import { EvalOutput } from "./eval"; 7 | import { ChatCompletionMessageParam } from "openai/resources/chat/completions.mjs"; 8 | import { ChatCompletionMessageToolCall } from "openai/resources/chat/completions.mjs"; 9 | import { Configuration as StripeAgentToolkitConfig } from "../typescript/src/shared/configuration"; 10 | import isEqual from "lodash/isEqual"; 11 | 12 | /* 13 | * EvalInput is what is passed into the agent. 14 | * It contains a userPrompt and configuration that can be 15 | * used to override the toolkit configuration. 16 | */ 17 | export type EvalInput = { 18 | toolkitConfigOverride: StripeAgentToolkitConfig; 19 | userPrompt: string; 20 | }; 21 | 22 | /* 23 | * EvalCaseFunction is the helper function that is used to 24 | * run assertions on the output of the agent. It does some 25 | * parsing of the raw completetion messages and tool calls 26 | * to make it easier to write assertions. 27 | */ 28 | export type EvalCaseFunction = ({ 29 | toolCalls, 30 | messages, 31 | assistantMessages, 32 | }: { 33 | toolCalls: ChatCompletionMessageToolCall[]; 34 | messages: ChatCompletionMessageParam[]; 35 | assistantMessages: string[]; 36 | }) => Array<AssertionResult | Promise<AssertionResult>>; 37 | 38 | export const AssertionScorer = async ({ 39 | output: responseMessages, 40 | expected: evalCaseFunction, 41 | }: { 42 | output: EvalOutput; 43 | expected: EvalCaseFunction; 44 | }) => { 45 | const toolCalls = responseMessages.flatMap((m) => { 46 | if ("tool_calls" in m && m.tool_calls) { 47 | return m.tool_calls; 48 | } else { 49 | return []; 50 | } 51 | }); 52 | 53 | const assistantMessages = responseMessages 54 | .filter((m) => m.role === "assistant") 55 | .map((m) => (typeof m.content === "string" ? m.content : "")); 56 | 57 | const rawResults = evalCaseFunction({ 58 | toolCalls, 59 | messages: responseMessages, 60 | assistantMessages, 61 | }); 62 | 63 | const assertionResults = await Promise.all(rawResults); 64 | 65 | const allPassed = every(assertionResults, (r) => r.status === "passed"); 66 | 67 | return { 68 | name: "Assertions Score", 69 | score: allPassed ? 1 : 0, 70 | metadata: { 71 | assertionResults, 72 | }, 73 | }; 74 | }; 75 | 76 | /* 77 | Below are assertion functions that can be used to evaluate the output of the agent. 78 | Similar to test framework helpers like Jest. 79 | */ 80 | 81 | export type AssertionResult = { 82 | status: "passed" | "failed"; 83 | assertion_type: string; 84 | expected?: string; 85 | actualValue?: string; 86 | message?: string; 87 | }; 88 | 89 | /** 90 | * Uses an LLM call to classify if a substring is semantically contained in a text. 91 | * @param text1 The full text you want to check against 92 | * @param text2 The string you want to check if it is contained in the text 93 | */ 94 | export async function semanticContains({ 95 | text1, 96 | text2, 97 | }: { 98 | text1: string; 99 | text2: string; 100 | }): Promise<AssertionResult> { 101 | const system = ` 102 | You are a highly intelligent AI that can determine if a piece of text semantically contains another piece of text. 103 | You will be given two pieces of text and you need to determine if the first piece of text semantically contains the second piece of text. 104 | Answer with just "yes" or "no". 105 | `; 106 | 107 | const completion = await braintrustOpenai.chat.completions.create({ 108 | model: "gpt-4o", 109 | messages: [ 110 | { role: "system", content: system }, 111 | { 112 | role: "user", 113 | content: `Text 1: ${text1}\n\nText 2: ${text2}\n\nDoes Text 1 semantically contain Text 2? Answer with just "yes" or "no".`, 114 | }, 115 | ], 116 | }); 117 | 118 | const response = completion.choices[0].message.content?.toLowerCase(); 119 | return { 120 | status: response === "yes" ? "passed" : "failed", 121 | assertion_type: "semantic_contains", 122 | expected: text2, 123 | actualValue: text1, 124 | }; 125 | } 126 | 127 | export const expectToolCall = ( 128 | actualToolCalls: ChatCompletionMessageToolCall[], 129 | expectedToolCalls: string[] 130 | ): AssertionResult => { 131 | const actualToolCallNames = actualToolCalls.map((tc) => tc.function.name); 132 | 133 | const pass = actualToolCallNames.some((tc) => expectedToolCalls.includes(tc)); 134 | 135 | return { 136 | status: pass ? "passed" : "failed", 137 | assertion_type: "expectToolCall", 138 | expected: expectedToolCalls.join(", "), 139 | actualValue: actualToolCallNames.join(", "), 140 | }; 141 | }; 142 | 143 | export const expectToolCallArgs = ( 144 | actualToolCalls: ChatCompletionMessageToolCall[], 145 | expectedArgs: Array<{ name: string; arguments: any; shallow?: boolean }> 146 | ): AssertionResult => { 147 | const actualToolCallNamesAndArgs = actualToolCalls.map((tc) => ({ 148 | name: tc.function.name, 149 | arguments: JSON.parse(tc.function.arguments), 150 | })); 151 | const pass = actualToolCallNamesAndArgs.some((tc) => { 152 | return expectedArgs.some((ea) => { 153 | if (ea.name !== tc.name) { 154 | return false; 155 | } 156 | 157 | if (ea.shallow === true) { 158 | return Object.keys(ea.arguments).every((key) => { 159 | return isEqual(ea.arguments[key], tc.arguments[key]); 160 | }); 161 | } else { 162 | return isEqual(ea.arguments, tc.arguments); 163 | } 164 | }); 165 | }); 166 | return { 167 | status: pass ? "passed" : "failed", 168 | assertion_type: "expectToolCallArgs", 169 | expected: expectedArgs 170 | .map((ea) => `${ea.name}: ${JSON.stringify(ea.arguments)}`) 171 | .join(", "), 172 | actualValue: actualToolCallNamesAndArgs 173 | .map((tc) => `${tc.name}: ${JSON.stringify(tc.arguments)}`) 174 | .join(", "), 175 | }; 176 | }; 177 | 178 | export const llmCriteriaMet = async ( 179 | messages: ChatCompletionMessageParam[], 180 | criteria: string 181 | ): Promise<AssertionResult> => { 182 | const assistantMessages = messages 183 | .filter((m) => m.role === "assistant") 184 | .map((m) => m.content) 185 | .join("\n"); 186 | 187 | const closedQA = await ClosedQA({ 188 | client: braintrustOpenai, 189 | input: "According to the provided criterion is the submission correct?", 190 | criteria, 191 | output: assistantMessages, 192 | }); 193 | 194 | const pass = !!closedQA.score && closedQA.score > 0.5; 195 | 196 | return { 197 | status: pass ? "passed" : "failed", 198 | assertion_type: "llm_criteria_met", 199 | expected: criteria, 200 | actualValue: assistantMessages, 201 | }; 202 | }; 203 | 204 | export const assert = ( 205 | condition: boolean, 206 | message: string 207 | ): AssertionResult => { 208 | return { 209 | status: condition ? "passed" : "failed", 210 | assertion_type: "plain_assert", 211 | message, 212 | }; 213 | }; 214 | ``` -------------------------------------------------------------------------------- /modelcontextprotocol/src/test/index.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import {main} from '../index'; 2 | import {parseArgs} from '../index'; 3 | import {StripeAgentToolkit} from '@stripe/agent-toolkit/modelcontextprotocol'; 4 | import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; 5 | describe('parseArgs function', () => { 6 | describe('success cases', () => { 7 | it('should parse api-key, tools and stripe-header arguments correctly', () => { 8 | const args = [ 9 | '--api-key=sk_test_123', 10 | '--tools=all', 11 | '--stripe-account=acct_123', 12 | ]; 13 | const options = parseArgs(args); 14 | expect(options.apiKey).toBe('sk_test_123'); 15 | expect(options.tools).toEqual(['all']); 16 | expect(options.stripeAccount).toBe('acct_123'); 17 | }); 18 | 19 | it('should parse api-key and tools arguments correctly', () => { 20 | const args = ['--api-key=sk_test_123', '--tools=all']; 21 | const options = parseArgs(args); 22 | expect(options.apiKey).toBe('sk_test_123'); 23 | expect(options.tools).toEqual(['all']); 24 | }); 25 | 26 | it('should parse restricted api key correctly', () => { 27 | const args = ['--api-key=rk_test_123', '--tools=all']; 28 | const options = parseArgs(args); 29 | expect(options.apiKey).toBe('rk_test_123'); 30 | expect(options.tools).toEqual(['all']); 31 | }); 32 | 33 | it('if api key set in env variable, should parse tools argument correctly', () => { 34 | process.env.STRIPE_SECRET_KEY = 'sk_test_123'; 35 | const args = ['--tools=all']; 36 | const options = parseArgs(args); 37 | expect(options.apiKey).toBe('sk_test_123'); 38 | expect(options.tools).toEqual(['all']); 39 | }); 40 | 41 | it('if api key set in env variable but also passed into args, should prefer args key', () => { 42 | process.env.STRIPE_SECRET_KEY = 'sk_test_123'; 43 | const args = ['--api-key=sk_test_456', '--tools=all']; 44 | const options = parseArgs(args); 45 | expect(options.apiKey).toBe('sk_test_456'); 46 | expect(options.tools).toEqual(['all']); 47 | delete process.env.STRIPE_SECRET_KEY; 48 | }); 49 | 50 | it('should parse tools argument correctly if a list of tools is provided', () => { 51 | const args = [ 52 | '--api-key=sk_test_123', 53 | '--tools=customers.create,products.read,documentation.read', 54 | ]; 55 | const options = parseArgs(args); 56 | expect(options.tools).toEqual([ 57 | 'customers.create', 58 | 'products.read', 59 | 'documentation.read', 60 | ]); 61 | expect(options.apiKey).toBe('sk_test_123'); 62 | }); 63 | 64 | it('ignore all arguments not prefixed with --', () => { 65 | const args = [ 66 | '--api-key=sk_test_123', 67 | '--tools=all', 68 | 'stripe-account=acct_123', 69 | ]; 70 | const options = parseArgs(args); 71 | expect(options.apiKey).toBe('sk_test_123'); 72 | expect(options.tools).toEqual(['all']); 73 | expect(options.stripeAccount).toBeUndefined(); 74 | }); 75 | }); 76 | 77 | describe('error cases', () => { 78 | it("should throw an error if api-key does not start with 'sk_' or 'rk_'", () => { 79 | const args = ['--api-key=test_123', '--tools=all']; 80 | expect(() => parseArgs(args)).toThrow( 81 | 'API key must start with "sk_" or "rk_".' 82 | ); 83 | }); 84 | 85 | it('should throw an error if api-key is not provided', () => { 86 | const args = ['--tools=all']; 87 | expect(() => parseArgs(args)).toThrow( 88 | 'Stripe API key not provided. Please either pass it as an argument --api-key=$KEY or set the STRIPE_SECRET_KEY environment variable.' 89 | ); 90 | }); 91 | 92 | it("should throw an error if stripe-account does not start with 'acct_'", () => { 93 | const args = [ 94 | '--api-key=sk_test_123', 95 | '--tools=all', 96 | '--stripe-account=test_123', 97 | ]; 98 | expect(() => parseArgs(args)).toThrow( 99 | 'Stripe account must start with "acct_".' 100 | ); 101 | }); 102 | 103 | it('should throw an error if tools argument is not provided', () => { 104 | const args = ['--api-key=sk_test_123']; 105 | expect(() => parseArgs(args)).toThrow( 106 | 'The --tools arguments must be provided.' 107 | ); 108 | }); 109 | 110 | it('should throw an error if an invalid argument is provided', () => { 111 | const args = [ 112 | '--invalid-arg=value', 113 | '--api-key=sk_test_123', 114 | '--tools=all', 115 | ]; 116 | expect(() => parseArgs(args)).toThrow( 117 | 'Invalid argument: invalid-arg. Accepted arguments are: api-key, tools, stripe-account' 118 | ); 119 | }); 120 | 121 | it('should throw an error if tools is not in accepted tool list', () => { 122 | const args = [ 123 | '--api-key=sk_test_123', 124 | '--tools=customers.create,products.read,fake.tool', 125 | ]; 126 | expect(() => parseArgs(args)).toThrow( 127 | /Invalid tool: fake\.tool\. Accepted tools are: .*$/ 128 | ); 129 | }); 130 | }); 131 | }); 132 | 133 | jest.mock('@stripe/agent-toolkit/modelcontextprotocol'); 134 | jest.mock('@modelcontextprotocol/sdk/server/stdio.js'); 135 | 136 | describe('main function', () => { 137 | beforeEach(() => { 138 | jest.clearAllMocks(); 139 | }); 140 | 141 | it('should initialize the server with tools=all correctly', async () => { 142 | process.argv = ['node', 'index.js', '--api-key=sk_test_123', '--tools=all']; 143 | 144 | await main(); 145 | 146 | expect(StripeAgentToolkit).toHaveBeenCalledWith({ 147 | secretKey: 'sk_test_123', 148 | configuration: { 149 | actions: ALL_ACTIONS, 150 | context: {mode: 'modelcontextprotocol'}, 151 | }, 152 | }); 153 | 154 | expect(StdioServerTransport).toHaveBeenCalled(); 155 | }); 156 | 157 | it('should initialize the server with specific list of tools correctly', async () => { 158 | process.argv = [ 159 | 'node', 160 | 'index.js', 161 | '--api-key=sk_test_123', 162 | '--tools=customers.create,products.read,documentation.read', 163 | ]; 164 | 165 | await main(); 166 | 167 | expect(StripeAgentToolkit).toHaveBeenCalledWith({ 168 | secretKey: 'sk_test_123', 169 | configuration: { 170 | actions: { 171 | customers: { 172 | create: true, 173 | }, 174 | products: { 175 | read: true, 176 | }, 177 | documentation: { 178 | read: true, 179 | }, 180 | }, 181 | context: { 182 | mode: 'modelcontextprotocol', 183 | }, 184 | }, 185 | }); 186 | 187 | expect(StdioServerTransport).toHaveBeenCalled(); 188 | }); 189 | 190 | it('should initialize the server with stripe header', async () => { 191 | process.argv = [ 192 | 'node', 193 | 'index.js', 194 | '--api-key=sk_test_123', 195 | '--tools=all', 196 | '--stripe-account=acct_123', 197 | ]; 198 | 199 | await main(); 200 | 201 | expect(StripeAgentToolkit).toHaveBeenCalledWith({ 202 | secretKey: 'sk_test_123', 203 | configuration: { 204 | actions: ALL_ACTIONS, 205 | context: {account: 'acct_123', mode: 'modelcontextprotocol'}, 206 | }, 207 | }); 208 | 209 | expect(StdioServerTransport).toHaveBeenCalled(); 210 | }); 211 | }); 212 | 213 | const ALL_ACTIONS = { 214 | customers: { 215 | create: true, 216 | read: true, 217 | }, 218 | coupons: { 219 | create: true, 220 | read: true, 221 | }, 222 | invoices: { 223 | create: true, 224 | update: true, 225 | read: true, 226 | }, 227 | invoiceItems: { 228 | create: true, 229 | }, 230 | paymentLinks: { 231 | create: true, 232 | }, 233 | products: { 234 | create: true, 235 | read: true, 236 | }, 237 | prices: { 238 | create: true, 239 | read: true, 240 | }, 241 | balance: { 242 | read: true, 243 | }, 244 | refunds: { 245 | create: true, 246 | }, 247 | subscriptions: { 248 | read: true, 249 | update: true, 250 | }, 251 | paymentIntents: { 252 | read: true, 253 | }, 254 | disputes: { 255 | read: true, 256 | update: true, 257 | }, 258 | documentation: { 259 | read: true, 260 | }, 261 | }; 262 | ```