UNPKG

@chinchillaenterprises/mcp-stripe

Version:

Multi-tenant Stripe MCP server with account management and credential persistence

1,482 lines (1,301 loc) 54 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema, Tool, McpError, ErrorCode, } from "@modelcontextprotocol/sdk/types.js"; import Stripe from "stripe"; import { z } from "zod"; import { promises as fs } from "fs"; import path from "path"; import os from "os"; import crypto from "crypto"; // ===================================================================================== // TYPES AND INTERFACES // ===================================================================================== interface StripeAccount { id: string; // Unique identifier (e.g., 'main', 'sandbox', 'client-abc') name: string; // Human-friendly name secretKey: string; // Stripe secret key (sk_test_* or sk_live_*) publishableKey?: string; // Optional publishable key (pk_test_* or pk_live_*) accountId?: string; // Stripe account ID (for Connect accounts) environment: 'test' | 'live'; // Environment indicator isDefault?: boolean; // Flag for default account } interface AccountState { accounts: Map<string, StripeAccount>; activeAccountId: string | null; clients: Map<string, Stripe>; } interface PersistedAccount extends StripeAccount { addedAt: string; // ISO timestamp lastUsed?: string; // ISO timestamp } // ===================================================================================== // CREDENTIAL MANAGER CLASS // ===================================================================================== class CredentialManager { private readonly SERVICE_NAME = 'mcp-stripe-v3'; private readonly CONFIG_DIR = path.join(os.homedir(), '.mcp-stripe'); private readonly CONFIG_FILE = path.join(this.CONFIG_DIR, 'accounts.json'); private readonly ENCRYPTION_KEY_FILE = path.join(this.CONFIG_DIR, '.key'); private readonly ACTIVE_ACCOUNT_FILE = path.join(this.CONFIG_DIR, 'active.txt'); private keytar: any = null; private isKeytarAvailable = false; private initPromise: Promise<void>; private encryptionKey: Buffer | null = null; constructor() { this.initPromise = this.initialize(); } private async initialize() { // Initialize keytar try { this.keytar = await import('keytar'); this.isKeytarAvailable = true; console.error('[CredentialManager] Keytar initialized successfully'); } catch (error) { console.error('[CredentialManager] Keytar not available, using file-based storage:', error); this.isKeytarAvailable = false; } // Ensure config directory exists try { await fs.mkdir(this.CONFIG_DIR, { recursive: true }); // Set directory permissions to be readable only by the user await fs.chmod(this.CONFIG_DIR, 0o700); } catch (error) { console.error('[CredentialManager] Failed to create config directory:', error); } // Initialize or load encryption key for file-based storage await this.initializeEncryptionKey(); } private async initializeEncryptionKey() { try { // Try to read existing key this.encryptionKey = await fs.readFile(this.ENCRYPTION_KEY_FILE); } catch (error) { // Generate new key if doesn't exist this.encryptionKey = crypto.randomBytes(32); try { await fs.writeFile(this.ENCRYPTION_KEY_FILE, this.encryptionKey); await fs.chmod(this.ENCRYPTION_KEY_FILE, 0o600); // Read/write for owner only } catch (writeError) { console.error('[CredentialManager] Failed to save encryption key:', writeError); } } } private encrypt(text: string): string { if (!this.encryptionKey) return text; // Fallback to plain text if no key const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); return iv.toString('hex') + ':' + encrypted; } private decrypt(encryptedText: string): string { if (!this.encryptionKey || !encryptedText.includes(':')) return encryptedText; try { const [ivHex, encrypted] = encryptedText.split(':'); const iv = Buffer.from(ivHex, 'hex'); const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { console.error('[CredentialManager] Failed to decrypt:', error); return encryptedText; // Return as-is if decryption fails } } private async ensureInitialized() { await this.initPromise; } async saveAccount(account: StripeAccount): Promise<void> { await this.ensureInitialized(); const persistedAccount: PersistedAccount = { ...account, addedAt: new Date().toISOString(), lastUsed: new Date().toISOString() }; // Try keytar first if (this.isKeytarAvailable && this.keytar) { try { await this.keytar.setPassword( this.SERVICE_NAME, `stripe-${account.id}`, JSON.stringify(persistedAccount) ); console.error(`[CredentialManager] Account ${account.id} saved to keychain`); return; // Success with keytar } catch (error) { console.error('[CredentialManager] Keytar save failed, falling back to file:', error); } } // Fallback to file-based storage await this.saveToFile(persistedAccount); } private async saveToFile(account: PersistedAccount): Promise<void> { try { // Read existing accounts const accounts = await this.loadFromFile(); // Encrypt sensitive data const encryptedAccount = { ...account, secretKey: this.encrypt(account.secretKey), publishableKey: account.publishableKey ? this.encrypt(account.publishableKey) : undefined }; // Update or add account accounts[account.id] = encryptedAccount; // Save back to file await fs.writeFile( this.CONFIG_FILE, JSON.stringify(accounts, null, 2), 'utf8' ); await fs.chmod(this.CONFIG_FILE, 0o600); // Read/write for owner only console.error(`[CredentialManager] Account ${account.id} saved to file`); } catch (error) { console.error('[CredentialManager] Failed to save account to file:', error); throw error; } } private async loadFromFile(): Promise<Record<string, PersistedAccount>> { try { const data = await fs.readFile(this.CONFIG_FILE, 'utf8'); return JSON.parse(data); } catch (error) { // File doesn't exist or is invalid return {}; } } async updateAccount(account: StripeAccount): Promise<void> { await this.ensureInitialized(); // Get existing to preserve addedAt const existing = await this.getAccount(account.id); const persistedAccount: PersistedAccount = { ...account, addedAt: existing?.addedAt || new Date().toISOString(), lastUsed: new Date().toISOString() }; // Try keytar first if (this.isKeytarAvailable && this.keytar) { try { await this.keytar.setPassword( this.SERVICE_NAME, `stripe-${account.id}`, JSON.stringify(persistedAccount) ); console.error(`[CredentialManager] Account ${account.id} updated in keychain`); return; // Success with keytar } catch (error) { console.error('[CredentialManager] Keytar update failed, falling back to file:', error); } } // Fallback to file-based storage await this.saveToFile(persistedAccount); } async getAccount(accountId: string): Promise<PersistedAccount | null> { await this.ensureInitialized(); // Try keytar first if (this.isKeytarAvailable && this.keytar) { try { const password = await this.keytar.getPassword(this.SERVICE_NAME, `stripe-${accountId}`); if (password) { return JSON.parse(password) as PersistedAccount; } } catch (error) { console.error(`[CredentialManager] Failed to get account ${accountId} from keychain:`, error); } } // Fallback to file-based storage try { const accounts = await this.loadFromFile(); const encryptedAccount = accounts[accountId]; if (encryptedAccount) { // Decrypt sensitive data const account: PersistedAccount = { ...encryptedAccount, secretKey: this.decrypt(encryptedAccount.secretKey), publishableKey: encryptedAccount.publishableKey ? this.decrypt(encryptedAccount.publishableKey) : undefined }; return account; } } catch (error) { console.error(`[CredentialManager] Failed to get account ${accountId} from file:`, error); } return null; } async getAllAccounts(): Promise<PersistedAccount[]> { await this.ensureInitialized(); const accounts: PersistedAccount[] = []; // Try keytar first if (this.isKeytarAvailable && this.keytar) { try { const credentials = await this.keytar.findCredentials(this.SERVICE_NAME); for (const cred of credentials) { try { if (cred.account.startsWith('stripe-')) { const account = JSON.parse(cred.password) as PersistedAccount; accounts.push(account); } } catch (error) { console.error(`[CredentialManager] Failed to parse keytar account ${cred.account}:`, error); } } if (accounts.length > 0) { console.error(`[CredentialManager] Loaded ${accounts.length} accounts from keychain`); return accounts.sort((a: PersistedAccount, b: PersistedAccount) => new Date(b.lastUsed || b.addedAt).getTime() - new Date(a.lastUsed || a.addedAt).getTime() ); } } catch (error) { console.error('[CredentialManager] Failed to load from keychain:', error); } } // Fallback to file-based storage try { const fileAccounts = await this.loadFromFile(); for (const [id, encryptedAccount] of Object.entries(fileAccounts)) { try { // Decrypt sensitive data const account: PersistedAccount = { ...encryptedAccount, secretKey: this.decrypt(encryptedAccount.secretKey), publishableKey: encryptedAccount.publishableKey ? this.decrypt(encryptedAccount.publishableKey) : undefined }; accounts.push(account); } catch (error) { console.error(`[CredentialManager] Failed to process file account ${id}:`, error); } } console.error(`[CredentialManager] Loaded ${accounts.length} accounts from file`); } catch (error) { console.error('[CredentialManager] Failed to load from file:', error); } return accounts.sort((a: PersistedAccount, b: PersistedAccount) => new Date(b.lastUsed || b.addedAt).getTime() - new Date(a.lastUsed || a.addedAt).getTime() ); } async removeAccount(accountId: string): Promise<void> { await this.ensureInitialized(); // Try keytar first if (this.isKeytarAvailable && this.keytar) { try { await this.keytar.deletePassword(this.SERVICE_NAME, `stripe-${accountId}`); console.error(`[CredentialManager] Account ${accountId} deleted from keychain`); } catch (error) { console.error('[CredentialManager] Failed to delete from keychain:', error); } } // Also delete from file-based storage try { const accounts = await this.loadFromFile(); delete accounts[accountId]; await fs.writeFile( this.CONFIG_FILE, JSON.stringify(accounts, null, 2), 'utf8' ); console.error(`[CredentialManager] Account ${accountId} deleted from file`); } catch (error) { console.error('[CredentialManager] Failed to delete from file:', error); } } async getActiveAccountId(): Promise<string | null> { await this.ensureInitialized(); // Try keytar first if (this.isKeytarAvailable && this.keytar) { try { const activeId = await this.keytar.getPassword(this.SERVICE_NAME, 'active-account-id'); if (activeId) return activeId; } catch (error) { console.error('[CredentialManager] Failed to get active account ID from keychain:', error); } } // Fallback to file-based storage try { const activeId = await fs.readFile(this.ACTIVE_ACCOUNT_FILE, 'utf8'); return activeId.trim() || null; } catch (error) { // File doesn't exist or is invalid return null; } } async setActiveAccountId(accountId: string | null): Promise<void> { await this.ensureInitialized(); // Try keytar first if (this.isKeytarAvailable && this.keytar) { try { if (accountId) { await this.keytar.setPassword(this.SERVICE_NAME, 'active-account-id', accountId); } else { await this.keytar.deletePassword(this.SERVICE_NAME, 'active-account-id'); } console.error(`[CredentialManager] Active account ID ${accountId ? 'set to ' + accountId : 'cleared'} in keychain`); } catch (error) { console.error('[CredentialManager] Failed to set active account ID in keychain:', error); } } // Also save to file-based storage try { if (accountId) { await fs.writeFile(this.ACTIVE_ACCOUNT_FILE, accountId, 'utf8'); await fs.chmod(this.ACTIVE_ACCOUNT_FILE, 0o600); } else { // Remove the file if setting to null try { await fs.unlink(this.ACTIVE_ACCOUNT_FILE); } catch (error) { // File might not exist, which is fine } } console.error(`[CredentialManager] Active account ID ${accountId ? 'set to ' + accountId : 'cleared'} in file`); } catch (error) { console.error('[CredentialManager] Failed to set active account ID in file:', error); } } } // ===================================================================================== // SCHEMA DEFINITIONS - ACCOUNT MANAGEMENT // ===================================================================================== const AddAccountArgsSchema = z.object({ id: z.string().describe("Unique account identifier (e.g., 'main', 'sandbox')"), name: z.string().describe("Human-friendly account name"), secretKey: z.string().describe("Stripe secret key (sk_test_* or sk_live_*)"), publishableKey: z.string().optional().describe("Optional publishable key (pk_test_* or pk_live_*)"), accountId: z.string().optional().describe("Stripe account ID (for Connect accounts)"), setAsDefault: z.boolean().optional().describe("Set as default account") }); const SwitchAccountArgsSchema = z.object({ accountId: z.string().describe("Account ID to switch to") }); const RemoveAccountArgsSchema = z.object({ accountId: z.string().describe("Account ID to remove") }); const SetDefaultAccountArgsSchema = z.object({ accountId: z.string().describe("Account ID to set as default") }); const UpdateAccountArgsSchema = z.object({ accountId: z.string().describe("Account ID to update"), name: z.string().optional().describe("New account name"), secretKey: z.string().optional().describe("New secret key"), publishableKey: z.string().optional().describe("New publishable key"), stripeAccountId: z.string().optional().describe("New Stripe account ID") }); // ===================================================================================== // SCHEMA DEFINITIONS - STRIPE OPERATIONS (from original implementation) // ===================================================================================== const ListProductsArgsSchema = z.object({ limit: z.number().min(1).max(100).optional().describe("Number of products to return"), starting_after: z.string().optional().describe("Cursor for pagination") }); const ListPricesArgsSchema = z.object({ limit: z.number().min(1).max(100).optional().describe("Number of prices to return"), product: z.string().optional().describe("Product ID to filter by"), active: z.boolean().optional().describe("Filter by active status") }); const ListCouponsArgsSchema = z.object({ limit: z.number().min(1).max(100).optional().describe("Number of coupons to return"), starting_after: z.string().optional().describe("Cursor for pagination") }); const ListCustomersArgsSchema = z.object({ limit: z.number().min(1).max(100).optional().describe("Number of customers to return"), starting_after: z.string().optional().describe("Cursor for pagination"), email: z.string().optional().describe("Filter by email address") }); const ListSubscriptionsArgsSchema = z.object({ limit: z.number().min(1).max(100).optional().describe("Number of subscriptions to return"), status: z.enum(['active', 'past_due', 'unpaid', 'canceled', 'incomplete', 'incomplete_expired', 'trialing', 'all']).optional().describe("Filter by status"), customer: z.string().optional().describe("Customer ID to filter by") }); const GetProductArgsSchema = z.object({ product_id: z.string().describe("Product ID to retrieve") }); const GetPriceArgsSchema = z.object({ price_id: z.string().describe("Price ID to retrieve") }); const GetCouponArgsSchema = z.object({ coupon_id: z.string().describe("Coupon ID to retrieve") }); const GetSubscriptionArgsSchema = z.object({ subscription_id: z.string().describe("Subscription ID to retrieve") }); const CreateCouponArgsSchema = z.object({ id: z.string().optional().describe("Unique identifier for the coupon"), percent_off: z.number().min(1).max(100).optional().describe("Percent discount (1-100)"), amount_off: z.number().optional().describe("Fixed amount discount in cents"), currency: z.string().optional().describe("Currency for amount_off"), duration: z.enum(['forever', 'once', 'repeating']).describe("How long the discount applies"), duration_in_months: z.number().optional().describe("Number of months for repeating duration"), max_redemptions: z.number().optional().describe("Maximum number of times this coupon can be redeemed"), name: z.string().optional().describe("Name of the coupon") }); const DeleteCouponArgsSchema = z.object({ coupon_id: z.string().describe("Coupon ID to delete") }); const ListInvoicesArgsSchema = z.object({ limit: z.number().min(1).max(100).optional().describe("Number of invoices to return"), customer: z.string().optional().describe("Customer ID to filter by"), status: z.enum(['draft', 'open', 'paid', 'uncollectible', 'void']).optional().describe("Filter by status") }); const ListPaymentMethodsArgsSchema = z.object({ customer: z.string().describe("Customer ID to list payment methods for"), type: z.enum(['card', 'us_bank_account', 'sepa_debit', 'ideal', 'fpx', 'giropay', 'eps', 'sofort', 'bancontact', 'p24']).optional().describe("Payment method type") }); const SearchCustomersArgsSchema = z.object({ query: z.string().describe("Search query for customers"), limit: z.number().min(1).max(100).optional().describe("Number of customers to return") }); // ===================================================================================== // STRIPE MCP SERVER CLASS // ===================================================================================== class StripeMCPServer { private server: Server; private credentialManager: CredentialManager; private state: AccountState; constructor() { this.credentialManager = new CredentialManager(); this.state = { accounts: new Map(), activeAccountId: null, clients: new Map() }; this.server = new Server( { name: "mcp-stripe-v3", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); this.setupRequestHandlers(); } async initialize(): Promise<void> { await this.loadAccountsFromKeychain(); await this.initializeFromEnvironment(); } private async loadAccountsFromKeychain(): Promise<void> { try { const persistedAccounts = await this.credentialManager.getAllAccounts(); for (const account of persistedAccounts) { this.state.accounts.set(account.id, account); await this.createStripeClient(account); if (account.isDefault) { this.state.activeAccountId = account.id; } } if (!this.state.activeAccountId) { const activeId = await this.credentialManager.getActiveAccountId(); if (activeId && this.state.accounts.has(activeId)) { this.state.activeAccountId = activeId; } } if (!this.state.activeAccountId && this.state.accounts.size > 0) { this.state.activeAccountId = this.state.accounts.keys().next().value || null; } } catch (error) { console.error('Failed to initialize accounts:', error); } } private async initializeFromEnvironment(): Promise<void> { // Support legacy single-account setup via environment variables const envSecretKey = process.env.STRIPE_SECRET_KEY; const envPublishableKey = process.env.STRIPE_PUBLISHABLE_KEY; if (envSecretKey && this.state.accounts.size === 0) { console.log('Migrating from environment variable configuration...'); const environment = envSecretKey.startsWith('sk_live_') ? 'live' : 'test'; const legacyAccount: StripeAccount = { id: 'default', name: 'Default Account', secretKey: envSecretKey, publishableKey: envPublishableKey, environment, isDefault: true }; this.state.accounts.set(legacyAccount.id, legacyAccount); this.state.activeAccountId = legacyAccount.id; await this.createStripeClient(legacyAccount); await this.credentialManager.saveAccount(legacyAccount); await this.credentialManager.setActiveAccountId(legacyAccount.id); } } private detectEnvironment(secretKey: string): 'test' | 'live' { return secretKey.startsWith('sk_live_') ? 'live' : 'test'; } private async createStripeClient(account: StripeAccount): Promise<void> { try { const stripe = new Stripe(account.secretKey, { apiVersion: '2024-06-20' }); this.state.clients.set(account.id, stripe); } catch (error) { console.error(`Failed to create Stripe client for account ${account.id}:`, error); throw new McpError(ErrorCode.InternalError, `Failed to create Stripe client for account ${account.id}`); } } private getActiveStripeClient(): Stripe { if (!this.state.activeAccountId) { throw new McpError(ErrorCode.InvalidRequest, "No active Stripe account. Please add and select an account first."); } const client = this.state.clients.get(this.state.activeAccountId); if (!client) { throw new McpError(ErrorCode.InternalError, "Stripe client not found for active account"); } return client; } private getActiveAccount(): StripeAccount { if (!this.state.activeAccountId) { throw new McpError(ErrorCode.InvalidRequest, "No active Stripe account. Please add and select an account first."); } const account = this.state.accounts.get(this.state.activeAccountId); if (!account) { throw new McpError(ErrorCode.InternalError, "Active account not found"); } return account; } private handleStripeError(error: unknown): never { if (error && typeof error === 'object' && 'type' in error) { const stripeError = error as any; switch (stripeError.type) { case 'StripeAuthenticationError': throw new McpError(ErrorCode.InvalidRequest, "Authentication failed. Check your Stripe secret key."); case 'StripePermissionError': throw new McpError(ErrorCode.InvalidRequest, "Permission denied. Check your Stripe account permissions."); case 'StripeRateLimitError': throw new McpError(ErrorCode.InvalidRequest, "Rate limit exceeded. Please try again later."); case 'StripeConnectionError': throw new McpError(ErrorCode.InternalError, "Network error connecting to Stripe."); case 'StripeAPIError': throw new McpError(ErrorCode.InternalError, `Stripe API error: ${stripeError.message}`); default: throw new McpError(ErrorCode.InternalError, `Stripe error: ${stripeError.message || 'Unknown error'}`); } } throw new McpError(ErrorCode.InternalError, "An unexpected error occurred"); } // ===================================================================================== // ACCOUNT MANAGEMENT METHODS // ===================================================================================== private async addAccount(args: z.infer<typeof AddAccountArgsSchema>) { if (this.state.accounts.has(args.id)) { throw new McpError(ErrorCode.InvalidRequest, `Account with ID '${args.id}' already exists`); } const environment = this.detectEnvironment(args.secretKey); const account: StripeAccount = { id: args.id, name: args.name, secretKey: args.secretKey, publishableKey: args.publishableKey, accountId: args.accountId, environment, isDefault: args.setAsDefault || this.state.accounts.size === 0 }; // Test the credentials by creating a client await this.createStripeClient(account); // If this is set as default, unset other defaults if (account.isDefault) { for (const [id, existingAccount] of this.state.accounts) { if (existingAccount.isDefault) { const updated = { ...existingAccount, isDefault: false }; this.state.accounts.set(id, updated); await this.credentialManager.updateAccount(updated); } } } this.state.accounts.set(account.id, account); await this.credentialManager.saveAccount(account); if (this.state.accounts.size === 1 || args.setAsDefault) { this.state.activeAccountId = account.id; await this.credentialManager.setActiveAccountId(account.id); } return { success: true, account: { id: account.id, name: account.name, environment: account.environment, accountId: account.accountId, isDefault: account.isDefault, isActive: this.state.activeAccountId === account.id } }; } private async switchAccount(args: z.infer<typeof SwitchAccountArgsSchema>) { const { accountId } = args; if (!this.state.accounts.has(accountId)) { throw new McpError(ErrorCode.InvalidRequest, `Account '${accountId}' not found`); } this.state.activeAccountId = accountId; await this.credentialManager.setActiveAccountId(accountId); const account = this.state.accounts.get(accountId)!; const updated = { ...account }; this.state.accounts.set(accountId, updated); await this.credentialManager.updateAccount(updated); return { success: true, activeAccount: { id: account.id, name: account.name, environment: account.environment, accountId: account.accountId, isDefault: account.isDefault } }; } private async removeAccount(args: z.infer<typeof RemoveAccountArgsSchema>) { const { accountId } = args; if (!this.state.accounts.has(accountId)) { throw new McpError(ErrorCode.InvalidRequest, `Account '${accountId}' not found`); } this.state.accounts.delete(accountId); this.state.clients.delete(accountId); await this.credentialManager.removeAccount(accountId); if (this.state.activeAccountId === accountId) { if (this.state.accounts.size > 0) { const defaultAccount = Array.from(this.state.accounts.values()).find(acc => acc.isDefault); const newActiveId = defaultAccount?.id || this.state.accounts.keys().next().value; this.state.activeAccountId = newActiveId || null; await this.credentialManager.setActiveAccountId(this.state.activeAccountId); } else { this.state.activeAccountId = null; await this.credentialManager.setActiveAccountId(null); } } return { success: true, removedAccountId: accountId, activeAccountId: this.state.activeAccountId }; } private async listAccounts() { const accounts = Array.from(this.state.accounts.values()).map(account => ({ id: account.id, name: account.name, environment: account.environment, accountId: account.accountId, isDefault: account.isDefault || false, isActive: this.state.activeAccountId === account.id })); return { accounts, activeAccountId: this.state.activeAccountId, totalAccounts: accounts.length }; } private async getActiveAccountInfo() { if (!this.state.activeAccountId) { return { activeAccount: null, message: "No active account set" }; } const account = this.state.accounts.get(this.state.activeAccountId); if (!account) { return { activeAccount: null, message: "Active account not found" }; } return { activeAccount: { id: account.id, name: account.name, environment: account.environment, accountId: account.accountId, isDefault: account.isDefault || false } }; } private async setDefaultAccount(args: z.infer<typeof SetDefaultAccountArgsSchema>) { const { accountId } = args; if (!this.state.accounts.has(accountId)) { throw new McpError(ErrorCode.InvalidRequest, `Account '${accountId}' not found`); } for (const [id, account] of this.state.accounts) { const updated = { ...account, isDefault: id === accountId }; this.state.accounts.set(id, updated); await this.credentialManager.updateAccount(updated); } return { success: true, defaultAccountId: accountId }; } private async updateAccount(args: z.infer<typeof UpdateAccountArgsSchema>) { const { accountId, ...updates } = args; if (!this.state.accounts.has(accountId)) { throw new McpError(ErrorCode.InvalidRequest, `Account '${accountId}' not found`); } const currentAccount = this.state.accounts.get(accountId)!; const updatedAccount = { ...currentAccount, ...updates }; // Update environment if secret key changed if (updates.secretKey) { updatedAccount.environment = this.detectEnvironment(updates.secretKey); await this.createStripeClient(updatedAccount); } this.state.accounts.set(accountId, updatedAccount); await this.credentialManager.updateAccount(updatedAccount); return { success: true, account: { id: updatedAccount.id, name: updatedAccount.name, environment: updatedAccount.environment, accountId: updatedAccount.accountId, isDefault: updatedAccount.isDefault || false, isActive: this.state.activeAccountId === accountId } }; } // ===================================================================================== // STRIPE OPERATION METHODS (Adapted from original implementation) // ===================================================================================== private async listProducts(args: z.infer<typeof ListProductsArgsSchema>) { const stripe = this.getActiveStripeClient(); const products = await stripe.products.list({ limit: args.limit, starting_after: args.starting_after }); return products; } private async listPrices(args: z.infer<typeof ListPricesArgsSchema>) { const stripe = this.getActiveStripeClient(); const prices = await stripe.prices.list({ limit: args.limit, product: args.product, active: args.active }); return prices; } private async listCoupons(args: z.infer<typeof ListCouponsArgsSchema>) { const stripe = this.getActiveStripeClient(); const coupons = await stripe.coupons.list({ limit: args.limit, starting_after: args.starting_after }); return coupons; } private async listCustomers(args: z.infer<typeof ListCustomersArgsSchema>) { const stripe = this.getActiveStripeClient(); const customers = await stripe.customers.list({ limit: args.limit, starting_after: args.starting_after, email: args.email }); return customers; } private async listSubscriptions(args: z.infer<typeof ListSubscriptionsArgsSchema>) { const stripe = this.getActiveStripeClient(); const subscriptions = await stripe.subscriptions.list({ limit: args.limit, status: args.status, customer: args.customer }); return subscriptions; } private async getProduct(args: z.infer<typeof GetProductArgsSchema>) { const stripe = this.getActiveStripeClient(); const product = await stripe.products.retrieve(args.product_id); return product; } private async getPrice(args: z.infer<typeof GetPriceArgsSchema>) { const stripe = this.getActiveStripeClient(); const price = await stripe.prices.retrieve(args.price_id); return price; } private async getCoupon(args: z.infer<typeof GetCouponArgsSchema>) { const stripe = this.getActiveStripeClient(); const coupon = await stripe.coupons.retrieve(args.coupon_id); return coupon; } private async getSubscription(args: z.infer<typeof GetSubscriptionArgsSchema>) { const stripe = this.getActiveStripeClient(); const subscription = await stripe.subscriptions.retrieve(args.subscription_id); return subscription; } private async createCoupon(args: z.infer<typeof CreateCouponArgsSchema>) { const stripe = this.getActiveStripeClient(); const couponData: any = { duration: args.duration }; if (args.id) couponData.id = args.id; if (args.percent_off) couponData.percent_off = args.percent_off; if (args.amount_off) couponData.amount_off = args.amount_off; if (args.currency) couponData.currency = args.currency; if (args.duration_in_months) couponData.duration_in_months = args.duration_in_months; if (args.max_redemptions) couponData.max_redemptions = args.max_redemptions; if (args.name) couponData.name = args.name; const coupon = await stripe.coupons.create(couponData); return coupon; } private async deleteCoupon(args: z.infer<typeof DeleteCouponArgsSchema>) { const stripe = this.getActiveStripeClient(); const deleted = await stripe.coupons.del(args.coupon_id); return deleted; } private async listInvoices(args: z.infer<typeof ListInvoicesArgsSchema>) { const stripe = this.getActiveStripeClient(); const invoices = await stripe.invoices.list({ limit: args.limit, customer: args.customer, status: args.status }); return invoices; } private async listPaymentMethods(args: z.infer<typeof ListPaymentMethodsArgsSchema>) { const stripe = this.getActiveStripeClient(); const paymentMethods = await stripe.paymentMethods.list({ customer: args.customer, type: args.type }); return paymentMethods; } private async searchCustomers(args: z.infer<typeof SearchCustomersArgsSchema>) { const stripe = this.getActiveStripeClient(); const customers = await stripe.customers.search({ query: args.query, limit: args.limit }); return customers; } // ===================================================================================== // REQUEST HANDLERS SETUP // ===================================================================================== private setupRequestHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // Account Management Tools { name: "list_accounts", description: "List all configured Stripe accounts with their status", inputSchema: { type: "object", properties: {}, } }, { name: "switch_account", description: "Switch to a different Stripe account", inputSchema: { type: "object", properties: { accountId: { type: "string", description: "Account ID to switch to" } }, required: ["accountId"] } }, { name: "add_account", description: "Add a new Stripe account configuration", inputSchema: { type: "object", properties: { id: { type: "string", description: "Unique account identifier (e.g., 'main', 'sandbox')" }, name: { type: "string", description: "Human-friendly account name" }, secretKey: { type: "string", description: "Stripe secret key (sk_test_* or sk_live_*)" }, publishableKey: { type: "string", description: "Optional publishable key (pk_test_* or pk_live_*)" }, accountId: { type: "string", description: "Stripe account ID (for Connect accounts)" }, setAsDefault: { type: "boolean", description: "Set as default account" } }, required: ["id", "name", "secretKey"] } }, { name: "remove_account", description: "Remove a Stripe account configuration", inputSchema: { type: "object", properties: { accountId: { type: "string", description: "Account ID to remove" } }, required: ["accountId"] } }, { name: "get_active_account", description: "Get details of the currently active account", inputSchema: { type: "object", properties: {} } }, { name: "set_default_account", description: "Set an account as the default for new sessions", inputSchema: { type: "object", properties: { accountId: { type: "string", description: "Account ID to set as default" } }, required: ["accountId"] } }, { name: "update_account", description: "Update account credentials (e.g. for key rotation)", inputSchema: { type: "object", properties: { accountId: { type: "string", description: "Account ID to update" }, name: { type: "string", description: "New account name" }, secretKey: { type: "string", description: "New secret key" }, publishableKey: { type: "string", description: "New publishable key" }, stripeAccountId: { type: "string", description: "New Stripe account ID" } }, required: ["accountId"] } }, // Core Discovery Tools { name: "stripe_list_products", description: "List products with filtering", inputSchema: { type: "object", properties: { limit: { type: "number", minimum: 1, maximum: 100, description: "Number of products to return" }, starting_after: { type: "string", description: "Cursor for pagination" } } } }, { name: "stripe_list_prices", description: "List pricing tiers/plans", inputSchema: { type: "object", properties: { limit: { type: "number", minimum: 1, maximum: 100, description: "Number of prices to return" }, product: { type: "string", description: "Product ID to filter by" }, active: { type: "boolean", description: "Filter by active status" } } } }, { name: "stripe_list_coupons", description: "List promo codes/coupons", inputSchema: { type: "object", properties: { limit: { type: "number", minimum: 1, maximum: 100, description: "Number of coupons to return" }, starting_after: { type: "string", description: "Cursor for pagination" } } } }, { name: "stripe_list_customers", description: "View customers", inputSchema: { type: "object", properties: { limit: { type: "number", minimum: 1, maximum: 100, description: "Number of customers to return" }, starting_after: { type: "string", description: "Cursor for pagination" }, email: { type: "string", description: "Filter by email address" } } } }, { name: "stripe_list_subscriptions", description: "Check active subscriptions", inputSchema: { type: "object", properties: { limit: { type: "number", minimum: 1, maximum: 100, description: "Number of subscriptions to return" }, status: { type: "string", enum: ['active', 'past_due', 'unpaid', 'canceled', 'incomplete', 'incomplete_expired', 'trialing', 'all'], description: "Filter by status" }, customer: { type: "string", description: "Customer ID to filter by" } } } }, // Detailed GET Tools { name: "stripe_get_product", description: "Get specific product by ID", inputSchema: { type: "object", properties: { product_id: { type: "string", description: "Product ID to retrieve" } }, required: ["product_id"] } }, { name: "stripe_get_price", description: "Get specific price by ID", inputSchema: { type: "object", properties: { price_id: { type: "string", description: "Price ID to retrieve" } }, required: ["price_id"] } }, { name: "stripe_get_coupon", description: "Get specific coupon by ID", inputSchema: { type: "object", properties: { coupon_id: { type: "string", description: "Coupon ID to retrieve" } }, required: ["coupon_id"] } }, { name: "stripe_get_subscription", description: "Get specific subscription by ID", inputSchema: { type: "object", properties: { subscription_id: { type: "string", description: "Subscription ID to retrieve" } }, required: ["subscription_id"] } }, // Coupon Management Tools { name: "stripe_create_coupon", description: "Create new coupons", inputSchema: { type: "object", properties: { id: { type: "string", description: "Unique identifier for the coupon" }, percent_off: { type: "number", minimum: 1, maximum: 100, description: "Percent discount (1-100)" }, amount_off: { type: "number", description: "Fixed amount discount in cents" }, currency: { type: "string", description: "Currency for amount_off" }, duration: { type: "string", enum: ['forever', 'once', 'repeating'], description: "How long the discount applies" }, duration_in_months: { type: "number", description: "Number of months for repeating duration" }, max_redemptions: { type: "number", description: "Maximum number of times this coupon can be redeemed" }, name: { type: "string", description: "Name of the coupon" } }, required: ["duration"] } }, { name: "stripe_delete_coupon", description: "Delete coupons", inputSchema: { type: "object", properties: { coupon_id: { type: "string", description: "Coupon ID to delete" } }, required: ["coupon_id"] } }, // Additional Tools { name: "stripe_list_invoices", description: "Billing history", inputSchema: { type: "object", properties: { limit: { type: "number", minimum: 1, maximum: 100, description: "Number of invoices to return" }, customer: { type: "string", description: "Customer ID to filter by" }, status: { type: "string", enum: ['draft', 'open', 'paid', 'uncollectible', 'void'], description: "Filter by status" } } } }, { name: "stripe_list_payment_methods", description: "Customer payment methods", inputSchema: { type: "object", properties: { customer: { type: "string", description: "Customer ID to list payment methods for" }, type: { type: "string", enum: ['card', 'us_bank_account', 'sepa_debit', 'ideal', 'fpx', 'giropay', 'eps', 'sofort', 'bancontact', 'p24'], description: "Payment method type" } }, required: ["customer"] } }, { name: "stripe_search_customers", description: "Find customers by email/name", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query for customers" }, limit: { type: "number", minimum: 1, maximum: 100, description: "Number of customers to return" } }, required: ["query"] } } ], }; }); // Call tool handler this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { let result; switch (name) { // Account Management Tools case "list_accounts": result = await this.listAccounts(); break; case "switch_account": result = await this.switchAccount(SwitchAccountArgsSchema.parse(args)); bre