@chinchillaenterprises/mcp-stripe
Version:
Multi-tenant Stripe MCP server with account management and credential persistence
1,482 lines (1,301 loc) • 54 kB
text/typescript
#!/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