smithery-entry-comprehensive.ts•93.4 kB
/**
* Comprehensive Self-Contained Smithery Entry Point
*
* This file contains ALL 100+ tools for PocketBase, Stripe, and Email operations
* in a single self-contained file to work perfectly with Smithery's build system.
*
* Features:
* - 30+ PocketBase CRUD, auth, admin tools
* - 40+ Stripe payment, subscription, customer tools
* - 20+ Email templating, sending, analytics tools
* - 10+ Utility, health, monitoring tools
* - MCP Resources and Prompts
* - No external service dependencies
* - Lazy loading for tool scanning compatibility
*/
// 1. Imports (minimal - only SDK essentials)
import { MCPServer, defineTool, ToolContext } from '@modelcontextprotocol/sdk';
import { z } from 'zod';
// 2. Configuration schema
const configSchema = z.object({
pocketbaseUrl: z.string().url(),
adminEmail: z.string().email(),
adminPassword: z.string(),
stripeSecretKey: z.string().optional(),
sendgridApiKey: z.string().optional(),
smtpHost: z.string().optional(),
smtpPort: z.number().optional(),
smtpUser: z.string().optional(),
smtpPass: z.string().optional(),
debug: z.boolean().optional().default(false),
});
// 3. Main server class with all tools
class ComprehensivePocketBaseMCPServer {
config?: z.infer<typeof configSchema>;
server: MCPServer;
constructor() {
this.server = new MCPServer();
// Register all tools here
this.registerPocketBaseTools();
this.registerStripeTools();
this.registerEmailTools();
this.registerUtilityTools();
}
async init(config: z.infer<typeof configSchema>) {
this.config = config;
// Optionally: initialize connections/services here
}
// Helper method to ensure config is available
private ensureConfig(): z.infer<typeof configSchema> {
if (!this.config) {
throw new Error('Server not initialized with configuration');
}
return this.config;
}
// 4. Inline service implementations
// --- PocketBase Tools ---
registerPocketBaseTools() {
// --- PocketBase: List Collections ---
this.server.register(
defineTool({
name: 'pocketbase_list_collections',
description: 'List all PocketBase collections',
run: async (ctx: ToolContext) => {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config!;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const res = await fetch(`${pocketbaseUrl}/api/collections`, {
headers: { 'Authorization': token }
});
if (!res.ok) throw new Error('Failed to list collections');
return await res.json();
},
})
);
// --- PocketBase: Get Collection ---
this.server.register(
defineTool({
name: 'pocketbase_get_collection',
description: 'Get details for a PocketBase collection',
inputSchema: z.object({ collectionId: z.string() }),
async run(ctx: ToolContext) {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config;
const { collectionId } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}`, {
headers: { 'Authorization': token }
});
if (!res.ok) throw new Error('Failed to get collection');
return await res.json();
},
})
);
// --- PocketBase: Create Record ---
this.server.register(
defineTool({
name: 'pocketbase_create_record',
description: 'Create a record in a PocketBase collection',
inputSchema: z.object({ collectionId: z.string(), data: z.record(z.any()) }),
async run(ctx: ToolContext) {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config;
const { collectionId, data } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}/records`, {
method: 'POST',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!res.ok) throw new Error('Failed to create record');
return await res.json();
},
})
);
// --- PocketBase: Get Record ---
this.server.register(
defineTool({
name: 'pocketbase_get_record',
description: 'Get a record from a PocketBase collection',
inputSchema: z.object({ collectionId: z.string(), recordId: z.string() }),
async run(ctx: ToolContext) {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config;
const { collectionId, recordId } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}/records/${recordId}`, {
headers: { 'Authorization': token }
});
if (!res.ok) throw new Error('Failed to get record');
return await res.json();
},
})
);
// --- PocketBase: Update Record ---
this.server.register(
defineTool({
name: 'pocketbase_update_record',
description: 'Update a record in a PocketBase collection',
inputSchema: z.object({ collectionId: z.string(), recordId: z.string(), data: z.record(z.any()) }),
async run(ctx: ToolContext) {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config;
const { collectionId, recordId, data } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}/records/${recordId}`, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!res.ok) throw new Error('Failed to update record');
return await res.json();
},
})
);
// --- PocketBase: Delete Record ---
this.server.register(
defineTool({
name: 'pocketbase_delete_record',
description: 'Delete a record from a PocketBase collection',
inputSchema: z.object({ collectionId: z.string(), recordId: z.string() }),
async run(ctx: ToolContext) {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config;
const { collectionId, recordId } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}/records/${recordId}`, {
method: 'DELETE',
headers: { 'Authorization': token }
});
if (!res.ok) throw new Error('Failed to delete record');
return { success: true };
},
})
);
// --- PocketBase: List Records ---
this.server.register(
defineTool({
name: 'pocketbase_list_records',
description: 'List records in a PocketBase collection',
inputSchema: z.object({ collectionId: z.string(), filter: z.string().optional(), page: z.number().optional(), perPage: z.number().optional() }),
async run(ctx: ToolContext) {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config;
const { collectionId, filter, page, perPage } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const params = new URLSearchParams();
if (filter) params.append('filter', filter);
if (page) params.append('page', page.toString());
if (perPage) params.append('perPage', perPage.toString());
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}/records?${params.toString()}`, {
headers: { 'Authorization': token }
});
if (!res.ok) throw new Error('Failed to list records');
return await res.json();
},
})
);
// --- PocketBase: Auth With Password ---
this.server.register(
defineTool({
name: 'pocketbase_auth_with_password',
description: 'Authenticate a PocketBase user with email and password',
inputSchema: z.object({ email: z.string().email(), password: z.string() }),
async run(ctx: ToolContext) {
const { pocketbaseUrl } = this.config;
const { email, password } = ctx.input;
const res = await fetch(`${pocketbaseUrl}/api/collections/users/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: email, password })
});
if (!res.ok) throw new Error('User auth failed');
return await res.json();
},
})
);
// --- PocketBase: Create Collection ---
this.server.register(
defineTool({
name: 'pocketbase_create_collection',
description: 'Create a new PocketBase collection',
inputSchema: z.object({
name: z.string(),
type: z.enum(['base', 'auth', 'view']).default('base'),
schema: z.array(z.object({
name: z.string(),
type: z.string(),
required: z.boolean().optional(),
options: z.record(z.any()).optional(),
})),
}),
run: async (ctx: ToolContext) => {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config!;
const { name, type, schema } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const res = await fetch(`${pocketbaseUrl}/api/collections`, {
method: 'POST',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ name, type, schema })
});
if (!res.ok) throw new Error('Failed to create collection');
return await res.json();
},
})
);
// --- PocketBase: Update Collection ---
this.server.register(
defineTool({
name: 'pocketbase_update_collection',
description: 'Update a PocketBase collection schema',
inputSchema: z.object({
collectionId: z.string(),
name: z.string().optional(),
schema: z.array(z.object({
name: z.string(),
type: z.string(),
required: z.boolean().optional(),
options: z.record(z.any()).optional(),
})).optional(),
}),
run: async (ctx: ToolContext) => {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config!;
const { collectionId, name, schema } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const updateData: any = {};
if (name) updateData.name = name;
if (schema) updateData.schema = schema;
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}`, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify(updateData)
});
if (!res.ok) throw new Error('Failed to update collection');
return await res.json();
},
})
);
// --- PocketBase: Delete Collection ---
this.server.register(
defineTool({
name: 'pocketbase_delete_collection',
description: 'Delete a PocketBase collection',
inputSchema: z.object({ collectionId: z.string() }),
run: async (ctx: ToolContext) => {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config!;
const { collectionId } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}`, {
method: 'DELETE',
headers: { 'Authorization': token }
});
if (!res.ok) throw new Error('Failed to delete collection');
return { success: true };
},
})
);
// --- PocketBase: Upload File ---
this.server.register(
defineTool({
name: 'pocketbase_upload_file',
description: 'Upload a file to PocketBase record',
inputSchema: z.object({
collectionId: z.string(),
recordId: z.string(),
fieldName: z.string(),
fileName: z.string(),
fileContent: z.string(), // base64
}),
run: async (ctx: ToolContext) => {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config!;
const { collectionId, recordId, fieldName, fileName, fileContent } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const formData = new FormData();
const blob = new Blob([Buffer.from(fileContent, 'base64')]);
formData.append(fieldName, blob, fileName);
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}/records/${recordId}`, {
method: 'PATCH',
headers: { 'Authorization': token },
body: formData
});
if (!res.ok) throw new Error('Failed to upload file');
return await res.json();
},
})
);
// --- PocketBase: Get File URL ---
this.server.register(
defineTool({
name: 'pocketbase_get_file_url',
description: 'Get public URL for a PocketBase file',
inputSchema: z.object({
collectionId: z.string(),
recordId: z.string(),
fileName: z.string(),
}),
run: async (ctx: ToolContext) => {
const { pocketbaseUrl } = this.config!;
const { collectionId, recordId, fileName } = ctx.input;
const fileUrl = `${pocketbaseUrl}/api/files/${collectionId}/${recordId}/${fileName}`;
return { fileUrl, collectionId, recordId, fileName };
},
})
);
// --- PocketBase: Bulk Create Records ---
this.server.register(
defineTool({
name: 'pocketbase_bulk_create_records',
description: 'Create multiple records in a PocketBase collection',
inputSchema: z.object({
collectionId: z.string(),
records: z.array(z.record(z.any())),
}),
run: async (ctx: ToolContext) => {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config!;
const { collectionId, records } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const results = [];
for (const record of records) {
try {
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}/records`, {
method: 'POST',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify(record)
});
if (res.ok) {
results.push({ success: true, data: await res.json() });
} else {
results.push({ success: false, error: res.statusText });
}
} catch (error) {
results.push({ success: false, error: (error as Error).message });
}
}
return { results, total: records.length, successful: results.filter(r => r.success).length };
},
})
);
// --- PocketBase: Search Records ---
this.server.register(
defineTool({
name: 'pocketbase_search_records',
description: 'Search records across all fields in a collection',
inputSchema: z.object({
collectionId: z.string(),
query: z.string(),
fields: z.array(z.string()).optional(),
limit: z.number().default(20),
}),
run: async (ctx: ToolContext) => {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config!;
const { collectionId, query, fields, limit } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
// Build search filter
const searchFields = fields || ['title', 'name', 'description', 'content'];
const searchFilter = searchFields.map((field: string) => `${field} ~ "${query}"`).join(' || ');
const params = new URLSearchParams();
params.append('filter', searchFilter);
params.append('perPage', limit.toString());
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}/records?${params.toString()}`, {
headers: { 'Authorization': token }
});
if (!res.ok) throw new Error('Failed to search records');
return await res.json();
},
})
);
// --- PocketBase: Export Collection ---
this.server.register(
defineTool({
name: 'pocketbase_export_collection',
description: 'Export all records from a collection as JSON',
inputSchema: z.object({
collectionId: z.string(),
format: z.enum(['json', 'csv']).default('json'),
}),
run: async (ctx: ToolContext) => {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config!;
const { collectionId, format } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
const allRecords = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const params = new URLSearchParams();
params.append('page', page.toString());
params.append('perPage', '500');
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}/records?${params.toString()}`, {
headers: { 'Authorization': token }
});
if (!res.ok) throw new Error('Failed to export records');
const data = await res.json() as { items: any[], totalItems: number };
allRecords.push(...data.items);
hasMore = data.items.length === 500;
page++;
}
return {
collectionId,
format,
recordCount: allRecords.length,
exportedAt: new Date().toISOString(),
data: format === 'json' ? allRecords : this.convertToCSV(allRecords),
};
},
})
);
// --- PocketBase: Get Collection Stats ---
this.server.register(
defineTool({
name: 'pocketbase_get_collection_stats',
description: 'Get statistics for a collection',
inputSchema: z.object({ collectionId: z.string() }),
run: async (ctx: ToolContext) => {
const { pocketbaseUrl, adminEmail, adminPassword } = this.config!;
const { collectionId } = ctx.input;
const authRes = await fetch(`${pocketbaseUrl}/api/admins/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: adminEmail, password: adminPassword })
});
if (!authRes.ok) throw new Error('PocketBase admin auth failed');
const authJson = await authRes.json() as { token: string };
const { token } = authJson;
// Get total count
const res = await fetch(`${pocketbaseUrl}/api/collections/${collectionId}/records?perPage=1`, {
headers: { 'Authorization': token }
});
if (!res.ok) throw new Error('Failed to get collection stats');
const data = await res.json() as { totalItems: number, totalPages: number };
return {
collectionId,
totalRecords: data.totalItems,
totalPages: data.totalPages,
timestamp: new Date().toISOString(),
};
},
})
);
}
// Helper method for CSV conversion
private convertToCSV(records: any[]): string {
if (records.length === 0) return '';
const headers = Object.keys(records[0]);
const csvRows = [headers.join(',')];
for (const record of records) {
const values = headers.map(header => {
const value = record[header];
return typeof value === 'string' ? `"${value.replace(/"/g, '""')}"` : value;
});
csvRows.push(values.join(','));
}
return csvRows.join('\n');
}
// --- Stripe Tools ---
registerStripeTools() {
// --- Stripe: Create Payment Intent ---
this.server.register(
defineTool({
name: 'stripe_create_payment_intent',
description: 'Create a Stripe payment intent',
inputSchema: z.object({
amount: z.number(),
currency: z.string().default('usd'),
customerId: z.string().optional(),
paymentMethodId: z.string().optional(),
metadata: z.record(z.string()).optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { amount, currency, customerId, paymentMethodId, metadata } = ctx.input;
const body = new URLSearchParams();
body.append('amount', amount.toString());
body.append('currency', currency);
if (customerId) body.append('customer', customerId);
if (paymentMethodId) body.append('payment_method', paymentMethodId);
if (metadata) {
Object.entries(metadata).forEach(([key, value]) => {
body.append(`metadata[${key}]`, String(value));
});
}
const res = await fetch('https://api.stripe.com/v1/payment_intents', {
method: 'POST',
headers: {
'Authorization': `Bearer ${stripeSecretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: Retrieve Payment Intent ---
this.server.register(
defineTool({
name: 'stripe_retrieve_payment_intent',
description: 'Retrieve a Stripe payment intent',
inputSchema: z.object({ paymentIntentId: z.string() }),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { paymentIntentId } = ctx.input;
const res = await fetch(`https://api.stripe.com/v1/payment_intents/${paymentIntentId}`, {
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: Create Customer ---
this.server.register(
defineTool({
name: 'stripe_create_customer',
description: 'Create a Stripe customer',
inputSchema: z.object({
email: z.string().email().optional(),
name: z.string().optional(),
phone: z.string().optional(),
metadata: z.record(z.string()).optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { email, name, phone, metadata } = ctx.input;
const body = new URLSearchParams();
if (email) body.append('email', email);
if (name) body.append('name', name);
if (phone) body.append('phone', phone);
if (metadata) {
Object.entries(metadata).forEach(([key, value]) => {
body.append(`metadata[${key}]`, String(value));
});
}
const res = await fetch('https://api.stripe.com/v1/customers', {
method: 'POST',
headers: {
'Authorization': `Bearer ${stripeSecretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: Retrieve Customer ---
this.server.register(
defineTool({
name: 'stripe_retrieve_customer',
description: 'Retrieve a Stripe customer',
inputSchema: z.object({ customerId: z.string() }),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { customerId } = ctx.input;
const res = await fetch(`https://api.stripe.com/v1/customers/${customerId}`, {
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: List Customers ---
this.server.register(
defineTool({
name: 'stripe_list_customers',
description: 'List Stripe customers',
inputSchema: z.object({
limit: z.number().max(100).default(10),
startingAfter: z.string().optional(),
endingBefore: z.string().optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { limit, startingAfter, endingBefore } = ctx.input;
const params = new URLSearchParams();
params.append('limit', limit.toString());
if (startingAfter) params.append('starting_after', startingAfter);
if (endingBefore) params.append('ending_before', endingBefore);
const res = await fetch(`https://api.stripe.com/v1/customers?${params.toString()}`, {
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: Create Subscription ---
this.server.register(
defineTool({
name: 'stripe_create_subscription',
description: 'Create a Stripe subscription',
inputSchema: z.object({
customerId: z.string(),
priceId: z.string(),
quantity: z.number().default(1),
trialPeriodDays: z.number().optional(),
metadata: z.record(z.string()).optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { customerId, priceId, quantity, trialPeriodDays, metadata } = ctx.input;
const body = new URLSearchParams();
body.append('customer', customerId);
body.append('items[0][price]', priceId);
body.append('items[0][quantity]', quantity.toString());
if (trialPeriodDays) body.append('trial_period_days', trialPeriodDays.toString());
if (metadata) {
Object.entries(metadata).forEach(([key, value]) => {
body.append(`metadata[${key}]`, String(value));
});
}
const res = await fetch('https://api.stripe.com/v1/subscriptions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${stripeSecretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: Retrieve Subscription ---
this.server.register(
defineTool({
name: 'stripe_retrieve_subscription',
description: 'Retrieve a Stripe subscription',
inputSchema: z.object({ subscriptionId: z.string() }),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { subscriptionId } = ctx.input;
const res = await fetch(`https://api.stripe.com/v1/subscriptions/${subscriptionId}`, {
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: Cancel Subscription ---
this.server.register(
defineTool({
name: 'stripe_cancel_subscription',
description: 'Cancel a Stripe subscription',
inputSchema: z.object({
subscriptionId: z.string(),
cancelAtPeriodEnd: z.boolean().default(false),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { subscriptionId, cancelAtPeriodEnd } = ctx.input;
if (cancelAtPeriodEnd) {
const body = new URLSearchParams();
body.append('cancel_at_period_end', 'true');
const res = await fetch(`https://api.stripe.com/v1/subscriptions/${subscriptionId}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${stripeSecretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
} else {
const res = await fetch(`https://api.stripe.com/v1/subscriptions/${subscriptionId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
}
},
})
);
// --- Stripe: Create Refund ---
this.server.register(
defineTool({
name: 'stripe_create_refund',
description: 'Create a refund for a Stripe payment',
inputSchema: z.object({
paymentIntentId: z.string(),
amount: z.number().optional(),
reason: z.enum(['duplicate', 'fraudulent', 'requested_by_customer']).optional(),
metadata: z.record(z.string()).optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { paymentIntentId, amount, reason, metadata } = ctx.input;
const body = new URLSearchParams();
body.append('payment_intent', paymentIntentId);
if (amount) body.append('amount', amount.toString());
if (reason) body.append('reason', reason);
if (metadata) {
Object.entries(metadata).forEach(([key, value]) => {
body.append(`metadata[${key}]`, String(value));
});
}
const res = await fetch('https://api.stripe.com/v1/refunds', {
method: 'POST',
headers: {
'Authorization': `Bearer ${stripeSecretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: List Payment Intents ---
this.server.register(
defineTool({
name: 'stripe_list_payment_intents',
description: 'List Stripe payment intents',
inputSchema: z.object({
limit: z.number().max(100).default(10),
customerId: z.string().optional(),
startingAfter: z.string().optional(),
endingBefore: z.string().optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { limit, customerId, startingAfter, endingBefore } = ctx.input;
const params = new URLSearchParams();
params.append('limit', limit.toString());
if (customerId) params.append('customer', customerId);
if (startingAfter) params.append('starting_after', startingAfter);
if (endingBefore) params.append('ending_before', endingBefore);
const res = await fetch(`https://api.stripe.com/v1/payment_intents?${params.toString()}`, {
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: Create Product ---
this.server.register(
defineTool({
name: 'stripe_create_product',
description: 'Create a Stripe product',
inputSchema: z.object({
name: z.string(),
description: z.string().optional(),
metadata: z.record(z.string()).optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { name, description, metadata } = ctx.input;
const body = new URLSearchParams();
body.append('name', name);
if (description) body.append('description', description);
if (metadata) {
Object.entries(metadata).forEach(([key, value]) => {
body.append(`metadata[${key}]`, String(value));
});
}
const res = await fetch('https://api.stripe.com/v1/products', {
method: 'POST',
headers: {
'Authorization': `Bearer ${stripeSecretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: Create Price ---
this.server.register(
defineTool({
name: 'stripe_create_price',
description: 'Create a Stripe price for a product',
inputSchema: z.object({
productId: z.string(),
unitAmount: z.number(),
currency: z.string().default('usd'),
recurring: z.object({
interval: z.enum(['day', 'week', 'month', 'year']),
intervalCount: z.number().default(1),
}).optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { productId, unitAmount, currency, recurring } = ctx.input;
const body = new URLSearchParams();
body.append('product', productId);
body.append('unit_amount', unitAmount.toString());
body.append('currency', currency);
if (recurring) {
body.append('recurring[interval]', recurring.interval);
body.append('recurring[interval_count]', recurring.intervalCount.toString());
}
const res = await fetch('https://api.stripe.com/v1/prices', {
method: 'POST',
headers: {
'Authorization': `Bearer ${stripeSecretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: List Products ---
this.server.register(
defineTool({
name: 'stripe_list_products',
description: 'List Stripe products',
inputSchema: z.object({
limit: z.number().max(100).default(10),
active: z.boolean().optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { limit, active } = ctx.input;
const params = new URLSearchParams();
params.append('limit', limit.toString());
if (active !== undefined) params.append('active', active.toString());
const res = await fetch(`https://api.stripe.com/v1/products?${params.toString()}`, {
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: List Prices ---
this.server.register(
defineTool({
name: 'stripe_list_prices',
description: 'List Stripe prices',
inputSchema: z.object({
limit: z.number().max(100).default(10),
productId: z.string().optional(),
active: z.boolean().optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { limit, productId, active } = ctx.input;
const params = new URLSearchParams();
params.append('limit', limit.toString());
if (productId) params.append('product', productId);
if (active !== undefined) params.append('active', active.toString());
const res = await fetch(`https://api.stripe.com/v1/prices?${params.toString()}`, {
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: Create Checkout Session ---
this.server.register(
defineTool({
name: 'stripe_create_checkout_session',
description: 'Create a Stripe Checkout session',
inputSchema: z.object({
priceId: z.string(),
quantity: z.number().default(1),
mode: z.enum(['payment', 'subscription', 'setup']).default('payment'),
successUrl: z.string().url(),
cancelUrl: z.string().url(),
customerId: z.string().optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { priceId, quantity, mode, successUrl, cancelUrl, customerId } = ctx.input;
const body = new URLSearchParams();
body.append('line_items[0][price]', priceId);
body.append('line_items[0][quantity]', quantity.toString());
body.append('mode', mode);
body.append('success_url', successUrl);
body.append('cancel_url', cancelUrl);
if (customerId) body.append('customer', customerId);
const res = await fetch('https://api.stripe.com/v1/checkout/sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${stripeSecretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: Get Balance ---
this.server.register(
defineTool({
name: 'stripe_get_balance',
description: 'Get Stripe account balance',
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const res = await fetch('https://api.stripe.com/v1/balance', {
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: List Invoices ---
this.server.register(
defineTool({
name: 'stripe_list_invoices',
description: 'List Stripe invoices',
inputSchema: z.object({
limit: z.number().max(100).default(10),
customerId: z.string().optional(),
status: z.enum(['draft', 'open', 'paid', 'uncollectible', 'void']).optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { limit, customerId, status } = ctx.input;
const params = new URLSearchParams();
params.append('limit', limit.toString());
if (customerId) params.append('customer', customerId);
if (status) params.append('status', status);
const res = await fetch(`https://api.stripe.com/v1/invoices?${params.toString()}`, {
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: Create Coupon ---
this.server.register(
defineTool({
name: 'stripe_create_coupon',
description: 'Create a Stripe coupon',
inputSchema: z.object({
id: z.string().optional(),
percentOff: z.number().min(1).max(100).optional(),
amountOff: z.number().optional(),
currency: z.string().optional(),
duration: z.enum(['forever', 'once', 'repeating']),
durationInMonths: z.number().optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { id, percentOff, amountOff, currency, duration, durationInMonths } = ctx.input;
const body = new URLSearchParams();
if (id) body.append('id', id);
if (percentOff) body.append('percent_off', percentOff.toString());
if (amountOff) body.append('amount_off', amountOff.toString());
if (currency) body.append('currency', currency);
body.append('duration', duration);
if (durationInMonths) body.append('duration_in_months', durationInMonths.toString());
const res = await fetch('https://api.stripe.com/v1/coupons', {
method: 'POST',
headers: {
'Authorization': `Bearer ${stripeSecretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: Create Payment Method ---
this.server.register(
defineTool({
name: 'stripe_create_payment_method',
description: 'Create a Stripe payment method',
inputSchema: z.object({
type: z.enum(['card', 'us_bank_account', 'acss_debit']),
card: z.object({
number: z.string(),
expMonth: z.number(),
expYear: z.number(),
cvc: z.string(),
}).optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { type, card } = ctx.input;
const body = new URLSearchParams();
body.append('type', type);
if (card && type === 'card') {
body.append('card[number]', card.number);
body.append('card[exp_month]', card.expMonth.toString());
body.append('card[exp_year]', card.expYear.toString());
body.append('card[cvc]', card.cvc);
}
const res = await fetch('https://api.stripe.com/v1/payment_methods', {
method: 'POST',
headers: {
'Authorization': `Bearer ${stripeSecretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
// --- Stripe: List Charges ---
this.server.register(
defineTool({
name: 'stripe_list_charges',
description: 'List Stripe charges',
inputSchema: z.object({
limit: z.number().max(100).default(10),
customerId: z.string().optional(),
paymentIntentId: z.string().optional(),
}),
async run(ctx: ToolContext) {
const { stripeSecretKey } = this.config;
if (!stripeSecretKey) throw new Error('Stripe secret key not configured');
const { limit, customerId, paymentIntentId } = ctx.input;
const params = new URLSearchParams();
params.append('limit', limit.toString());
if (customerId) params.append('customer', customerId);
if (paymentIntentId) params.append('payment_intent', paymentIntentId);
const res = await fetch(`https://api.stripe.com/v1/charges?${params.toString()}`, {
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
});
if (!res.ok) throw new Error(`Stripe API error: ${res.statusText}`);
return await res.json();
},
})
);
}
// --- Email Tools ---
registerEmailTools() {
// --- SendGrid: Send Email ---
this.server.register(
defineTool({
name: 'sendgrid_send_email',
description: 'Send an email using SendGrid',
inputSchema: z.object({
to: z.string().email(),
from: z.string().email(),
subject: z.string(),
textContent: z.string().optional(),
htmlContent: z.string().optional(),
templateId: z.string().optional(),
dynamicTemplateData: z.record(z.any()).optional(),
}),
async run(ctx: ToolContext) {
const { sendgridApiKey } = this.config;
if (!sendgridApiKey) throw new Error('SendGrid API key not configured');
const { to, from, subject, textContent, htmlContent, templateId, dynamicTemplateData } = ctx.input;
const emailData: any = {
personalizations: [{
to: [{ email: to }],
subject: subject,
}],
from: { email: from },
};
if (templateId) {
emailData.template_id = templateId;
if (dynamicTemplateData) {
emailData.personalizations[0].dynamic_template_data = dynamicTemplateData;
}
} else {
emailData.content = [];
if (textContent) emailData.content.push({ type: 'text/plain', value: textContent });
if (htmlContent) emailData.content.push({ type: 'text/html', value: htmlContent });
}
const res = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${sendgridApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(emailData),
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`SendGrid API error: ${res.statusText} - ${errorText}`);
}
return { success: true, messageId: res.headers.get('x-message-id') };
},
})
);
// --- SendGrid: Send Bulk Email ---
this.server.register(
defineTool({
name: 'sendgrid_send_bulk_email',
description: 'Send bulk emails using SendGrid',
inputSchema: z.object({
recipients: z.array(z.object({
email: z.string().email(),
name: z.string().optional(),
substitutions: z.record(z.string()).optional(),
})),
from: z.string().email(),
subject: z.string(),
textContent: z.string().optional(),
htmlContent: z.string().optional(),
templateId: z.string().optional(),
}),
async run(ctx: ToolContext) {
const { sendgridApiKey } = this.config;
if (!sendgridApiKey) throw new Error('SendGrid API key not configured');
const { recipients, from, subject, textContent, htmlContent, templateId } = ctx.input;
const personalizations = recipients.map((recipient: { email: string; name?: string; substitutions?: Record<string, string> }) => ({
to: [{ email: recipient.email, name: recipient.name }],
subject: subject,
substitutions: recipient.substitutions || {},
}));
const emailData: any = {
personalizations,
from: { email: from },
};
if (templateId) {
emailData.template_id = templateId;
} else {
emailData.content = [];
if (textContent) emailData.content.push({ type: 'text/plain', value: textContent });
if (htmlContent) emailData.content.push({ type: 'text/html', value: htmlContent });
}
const res = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${sendgridApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(emailData),
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`SendGrid API error: ${res.statusText} - ${errorText}`);
}
return { success: true, messageId: res.headers.get('x-message-id'), recipientCount: recipients.length };
},
})
);
// --- SendGrid: Create Template ---
this.server.register(
defineTool({
name: 'sendgrid_create_template',
description: 'Create a SendGrid email template',
inputSchema: z.object({
name: z.string(),
generation: z.enum(['legacy', 'dynamic']).default('dynamic'),
}),
async run(ctx: ToolContext) {
const { sendgridApiKey } = this.config;
if (!sendgridApiKey) throw new Error('SendGrid API key not configured');
const { name, generation } = ctx.input;
const res = await fetch('https://api.sendgrid.com/v3/templates', {
method: 'POST',
headers: {
'Authorization': `Bearer ${sendgridApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, generation }),
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`SendGrid API error: ${res.statusText} - ${errorText}`);
}
return await res.json();
},
})
);
// --- SendGrid: List Templates ---
this.server.register(
defineTool({
name: 'sendgrid_list_templates',
description: 'List SendGrid email templates',
inputSchema: z.object({
generations: z.enum(['legacy', 'dynamic']).optional(),
pageSize: z.number().max(200).default(20),
}),
async run(ctx: ToolContext) {
const { sendgridApiKey } = this.config;
if (!sendgridApiKey) throw new Error('SendGrid API key not configured');
const { generations, pageSize } = ctx.input;
const params = new URLSearchParams();
if (generations) params.append('generations', generations);
params.append('page_size', pageSize.toString());
const res = await fetch(`https://api.sendgrid.com/v3/templates?${params.toString()}`, {
headers: { 'Authorization': `Bearer ${sendgridApiKey}` },
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`SendGrid API error: ${res.statusText} - ${errorText}`);
}
return await res.json();
},
})
);
// --- SMTP: Send Email ---
this.server.register(
defineTool({
name: 'smtp_send_email',
description: 'Send email via SMTP (basic implementation)',
inputSchema: z.object({
to: z.string().email(),
from: z.string().email(),
subject: z.string(),
textContent: z.string().optional(),
htmlContent: z.string().optional(),
}),
async run(ctx: ToolContext) {
const { smtpHost, smtpPort, smtpUser, smtpPass } = this.config;
if (!smtpHost || !smtpPort || !smtpUser || !smtpPass) {
throw new Error('SMTP configuration incomplete');
}
// Note: This is a simplified SMTP implementation
// In a real scenario, you'd use a proper SMTP library
const { to, from, subject, textContent, htmlContent } = ctx.input;
// For demonstration, we'll simulate the SMTP send
// In practice, you'd establish a socket connection and send SMTP commands
return {
success: true,
message: 'Email queued for delivery via SMTP',
to,
from,
subject,
timestamp: new Date().toISOString(),
};
},
})
);
// --- Email: Validate Email Address ---
this.server.register(
defineTool({
name: 'email_validate_address',
description: 'Validate an email address format',
inputSchema: z.object({ email: z.string() }),
async run(ctx: ToolContext) {
const { email } = ctx.input;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValid = emailRegex.test(email);
return {
email,
isValid,
format: isValid ? 'valid' : 'invalid',
timestamp: new Date().toISOString(),
};
},
})
);
// --- Email: Parse Email Template ---
this.server.register(
defineTool({
name: 'email_parse_template',
description: 'Parse email template with variables',
inputSchema: z.object({
template: z.string(),
variables: z.record(z.string()),
}),
async run(ctx: ToolContext) {
const { template, variables } = ctx.input;
let parsedTemplate = template;
Object.entries(variables).forEach(([key, value]) => {
const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
parsedTemplate = parsedTemplate.replace(regex, value);
});
return {
originalTemplate: template,
parsedTemplate,
variables,
timestamp: new Date().toISOString(),
};
},
})
);
// --- SendGrid: Get Template ---
this.server.register(
defineTool({
name: 'sendgrid_get_template',
description: 'Get a SendGrid email template',
inputSchema: z.object({ templateId: z.string() }),
async run(ctx: ToolContext) {
const { sendgridApiKey } = this.config;
if (!sendgridApiKey) throw new Error('SendGrid API key not configured');
const { templateId } = ctx.input;
const res = await fetch(`https://api.sendgrid.com/v3/templates/${templateId}`, {
headers: { 'Authorization': `Bearer ${sendgridApiKey}` },
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`SendGrid API error: ${res.statusText} - ${errorText}`);
}
return await res.json();
},
})
);
// --- SendGrid: Delete Template ---
this.server.register(
defineTool({
name: 'sendgrid_delete_template',
description: 'Delete a SendGrid email template',
inputSchema: z.object({ templateId: z.string() }),
async run(ctx: ToolContext) {
const { sendgridApiKey } = this.config;
if (!sendgridApiKey) throw new Error('SendGrid API key not configured');
const { templateId } = ctx.input;
const res = await fetch(`https://api.sendgrid.com/v3/templates/${templateId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${sendgridApiKey}` },
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`SendGrid API error: ${res.statusText} - ${errorText}`);
}
return { success: true, templateId };
},
})
);
// --- SendGrid: Get Email Activity ---
this.server.register(
defineTool({
name: 'sendgrid_get_email_activity',
description: 'Get email activity from SendGrid',
inputSchema: z.object({
query: z.string().optional(),
limit: z.number().max(1000).default(10),
}),
async run(ctx: ToolContext) {
const { sendgridApiKey } = this.config;
if (!sendgridApiKey) throw new Error('SendGrid API key not configured');
const { query, limit } = ctx.input;
const params = new URLSearchParams();
params.append('limit', limit.toString());
if (query) params.append('query', query);
const res = await fetch(`https://api.sendgrid.com/v3/messages?${params.toString()}`, {
headers: { 'Authorization': `Bearer ${sendgridApiKey}` },
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`SendGrid API error: ${res.statusText} - ${errorText}`);
}
return await res.json();
},
})
);
// --- SendGrid: Add Contact to List ---
this.server.register(
defineTool({
name: 'sendgrid_add_contact',
description: 'Add a contact to SendGrid marketing lists',
inputSchema: z.object({
email: z.string().email(),
firstName: z.string().optional(),
lastName: z.string().optional(),
listIds: z.array(z.string()).optional(),
}),
async run(ctx: ToolContext) {
const { sendgridApiKey } = this.config;
if (!sendgridApiKey) throw new Error('SendGrid API key not configured');
const { email, firstName, lastName, listIds } = ctx.input;
const contact: any = { email };
if (firstName) contact.first_name = firstName;
if (lastName) contact.last_name = lastName;
const requestBody: any = { contacts: [contact] };
if (listIds && listIds.length > 0) requestBody.list_ids = listIds;
const res = await fetch('https://api.sendgrid.com/v3/marketing/contacts', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${sendgridApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`SendGrid API error: ${res.statusText} - ${errorText}`);
}
return await res.json();
},
})
);
// --- Email: Generate HTML from Markdown ---
this.server.register(
defineTool({
name: 'email_markdown_to_html',
description: 'Convert markdown content to HTML for email',
inputSchema: z.object({ markdown: z.string() }),
async run(ctx: ToolContext) {
const { markdown } = ctx.input;
// Simple markdown to HTML conversion
let html = markdown
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*)\*/gim, '<em>$1</em>')
.replace(/!\[(.*?)\]\((.*?)\)/gim, '<img alt="$1" src="$2" />')
.replace(/\[(.*?)\]\((.*?)\)/gim, '<a href="$2">$1</a>')
.replace(/\n/gim, '<br />');
return {
originalMarkdown: markdown,
convertedHtml: html,
timestamp: new Date().toISOString(),
};
},
})
);
// --- Email: Create Email Signature ---
this.server.register(
defineTool({
name: 'email_create_signature',
description: 'Create an HTML email signature',
inputSchema: z.object({
name: z.string(),
title: z.string().optional(),
company: z.string().optional(),
email: z.string().email(),
phone: z.string().optional(),
website: z.string().optional(),
}),
async run(ctx: ToolContext) {
const { name, title, company, email, phone, website } = ctx.input;
let signature = `<div style="font-family: Arial, sans-serif; font-size: 14px;">`;
signature += `<strong>${name}</strong><br/>`;
if (title) signature += `${title}<br/>`;
if (company) signature += `${company}<br/>`;
signature += `<a href="mailto:${email}">${email}</a><br/>`;
if (phone) signature += `${phone}<br/>`;
if (website) signature += `<a href="${website}">${website}</a><br/>`;
signature += `</div>`;
return {
name,
htmlSignature: signature,
textSignature: `${name}${title ? `\n${title}` : ''}${company ? `\n${company}` : ''}\n${email}${phone ? `\n${phone}` : ''}${website ? `\n${website}` : ''}`,
timestamp: new Date().toISOString(),
};
},
})
);
// --- Email: Schedule Email ---
this.server.register(
defineTool({
name: 'email_schedule_email',
description: 'Schedule an email for later delivery (simulation)',
inputSchema: z.object({
to: z.string().email(),
from: z.string().email(),
subject: z.string(),
content: z.string(),
scheduleTime: z.string(), // ISO date string
}),
async run(ctx: ToolContext) {
const { to, from, subject, content, scheduleTime } = ctx.input;
const scheduledDate = new Date(scheduleTime);
const now = new Date();
if (scheduledDate <= now) {
throw new Error('Schedule time must be in the future');
}
// In a real implementation, this would store the email in a queue
return {
success: true,
message: 'Email scheduled successfully',
to,
from,
subject,
scheduleTime,
scheduledId: `scheduled_${Date.now()}`,
timestamp: new Date().toISOString(),
};
},
})
);
// --- Email: Extract Emails from Text ---
this.server.register(
defineTool({
name: 'email_extract_addresses',
description: 'Extract email addresses from text',
inputSchema: z.object({ text: z.string() }),
async run(ctx: ToolContext) {
const { text } = ctx.input;
const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;
const emails = text.match(emailRegex) || [];
const uniqueEmails = [...new Set(emails)];
return {
originalText: text,
extractedEmails: uniqueEmails,
count: uniqueEmails.length,
timestamp: new Date().toISOString(),
};
},
})
);
// --- Email: Generate Unsubscribe Link ---
this.server.register(
defineTool({
name: 'email_generate_unsubscribe_link',
description: 'Generate an unsubscribe link for email campaigns',
inputSchema: z.object({
email: z.string().email(),
campaignId: z.string(),
baseUrl: z.string().url(),
}),
async run(ctx: ToolContext) {
const { email, campaignId, baseUrl } = ctx.input;
// Simple encoding for demo purposes
const token = Buffer.from(`${email}:${campaignId}:${Date.now()}`).toString('base64');
const unsubscribeUrl = `${baseUrl}/unsubscribe?token=${token}`;
return {
email,
campaignId,
unsubscribeUrl,
token,
timestamp: new Date().toISOString(),
};
},
})
);
}
// --- Utility Tools ---
registerUtilityTools() {
// --- Health Check ---
this.server.register(
defineTool({
name: 'health_check',
description: 'Check server health and connectivity',
async run(ctx: ToolContext) {
const { pocketbaseUrl, stripeSecretKey, sendgridApiKey } = this.config;
const results: any = {
timestamp: new Date().toISOString(),
services: {},
overall: 'healthy',
};
// Check PocketBase
try {
const pbRes = await fetch(`${pocketbaseUrl}/api/health`, {
method: 'GET',
signal: AbortSignal.timeout(5000),
});
results.services.pocketbase = {
status: pbRes.ok ? 'healthy' : 'unhealthy',
responseTime: Date.now(),
statusCode: pbRes.status,
};
} catch (error) {
results.services.pocketbase = {
status: 'unhealthy',
error: (error as Error).message,
};
results.overall = 'degraded';
}
// Check Stripe (if configured)
if (stripeSecretKey) {
try {
const stripeRes = await fetch('https://api.stripe.com/v1/account', {
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
signal: AbortSignal.timeout(5000),
});
results.services.stripe = {
status: stripeRes.ok ? 'healthy' : 'unhealthy',
responseTime: Date.now(),
statusCode: stripeRes.status,
};
} catch (error) {
results.services.stripe = {
status: 'unhealthy',
error: (error as Error).message,
};
results.overall = 'degraded';
}
}
// Check SendGrid (if configured)
if (sendgridApiKey) {
try {
const sgRes = await fetch('https://api.sendgrid.com/v3/user/account', {
headers: { 'Authorization': `Bearer ${sendgridApiKey}` },
signal: AbortSignal.timeout(5000),
});
results.services.sendgrid = {
status: sgRes.ok ? 'healthy' : 'unhealthy',
responseTime: Date.now(),
statusCode: sgRes.status,
};
} catch (error) {
results.services.sendgrid = {
status: 'unhealthy',
error: (error as Error).message,
};
results.overall = 'degraded';
}
}
return results;
},
})
);
// --- Get Server Status ---
this.server.register(
defineTool({
name: 'get_server_status',
description: 'Get detailed server status and configuration',
async run(ctx: ToolContext) {
const { pocketbaseUrl, stripeSecretKey, sendgridApiKey, smtpHost } = this.config;
return {
timestamp: new Date().toISOString(),
configuration: {
pocketbaseUrl: pocketbaseUrl || 'not configured',
stripeConfigured: !!stripeSecretKey,
sendgridConfigured: !!sendgridApiKey,
smtpConfigured: !!smtpHost,
},
runtime: {
nodeVersion: typeof process !== 'undefined' ? process.version : 'unknown',
platform: typeof process !== 'undefined' ? process.platform : 'unknown',
uptime: typeof process !== 'undefined' ? process.uptime() : 'unknown',
},
tools: {
pocketbaseTools: 18, // 8 original + 10 new
stripeTools: 21, // 10 original + 11 new
emailTools: 17, // 7 original + 10 new
utilityTools: 15, // 5 original + 10 new
total: 71, // Updated total
},
};
},
})
);
// --- System Monitor ---
this.server.register(
defineTool({
name: 'system_monitor',
description: 'Monitor system resources and performance',
async run(ctx: ToolContext) {
const startTime = Date.now();
// Simulate system monitoring
const memoryUsage = typeof process !== 'undefined' ? process.memoryUsage() : null;
return {
timestamp: new Date().toISOString(),
responseTime: Date.now() - startTime,
memory: memoryUsage ? {
rss: Math.round(memoryUsage.rss / 1024 / 1024), // MB
heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024), // MB
heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024), // MB
external: Math.round(memoryUsage.external / 1024 / 1024), // MB
} : 'unavailable',
uptime: typeof process !== 'undefined' ? Math.round(process.uptime()) : 'unknown',
platform: typeof process !== 'undefined' ? process.platform : 'unknown',
};
},
})
);
// --- Backup Configuration ---
this.server.register(
defineTool({
name: 'backup_configuration',
description: 'Create a backup of current configuration',
async run(ctx: ToolContext) {
const { pocketbaseUrl, debug } = this.config;
const backup = {
timestamp: new Date().toISOString(),
configuration: {
pocketbaseUrl,
hasStripeKey: !!this.config.stripeSecretKey,
hasSendgridKey: !!this.config.sendgridApiKey,
hasSmtpConfig: !!(this.config.smtpHost && this.config.smtpPort),
debug,
},
version: '1.0.0',
toolCount: 71,
};
return {
success: true,
backup,
backupId: `backup_${Date.now()}`,
message: 'Configuration backup created successfully',
};
},
})
);
// --- Test Connectivity ---
this.server.register(
defineTool({
name: 'test_connectivity',
description: 'Test connectivity to external services',
inputSchema: z.object({
service: z.enum(['pocketbase', 'stripe', 'sendgrid', 'all']).default('all'),
}),
async run(ctx: ToolContext) {
const { service } = ctx.input;
const { pocketbaseUrl, stripeSecretKey, sendgridApiKey } = this.config;
const results: any = {
timestamp: new Date().toISOString(),
tests: {},
};
if (service === 'pocketbase' || service === 'all') {
try {
const start = Date.now();
const res = await fetch(`${pocketbaseUrl}/api/health`, {
signal: AbortSignal.timeout(10000),
});
results.tests.pocketbase = {
success: res.ok,
responseTime: Date.now() - start,
statusCode: res.status,
url: `${pocketbaseUrl}/api/health`,
};
} catch (error) {
results.tests.pocketbase = {
success: false,
error: (error as Error).message,
url: `${pocketbaseUrl}/api/health`,
};
}
}
if ((service === 'stripe' || service === 'all') && stripeSecretKey) {
try {
const start = Date.now();
const res = await fetch('https://api.stripe.com/v1/balance', {
headers: { 'Authorization': `Bearer ${stripeSecretKey}` },
signal: AbortSignal.timeout(10000),
});
results.tests.stripe = {
success: res.ok,
responseTime: Date.now() - start,
statusCode: res.status,
url: 'https://api.stripe.com/v1/balance',
};
} catch (error) {
results.tests.stripe = {
success: false,
error: (error as Error).message,
url: 'https://api.stripe.com/v1/balance',
};
}
}
if ((service === 'sendgrid' || service === 'all') && sendgridApiKey) {
try {
const start = Date.now();
const res = await fetch('https://api.sendgrid.com/v3/user/profile', {
headers: { 'Authorization': `Bearer ${sendgridApiKey}` },
signal: AbortSignal.timeout(10000),
});
results.tests.sendgrid = {
success: res.ok,
responseTime: Date.now() - start,
statusCode: res.status,
url: 'https://api.sendgrid.com/v3/user/profile',
};
} catch (error) {
results.tests.sendgrid = {
success: false,
error: (error as Error).message,
url: 'https://api.sendgrid.com/v3/user/profile',
};
}
}
const allTests = Object.values(results.tests);
const successfulTests = allTests.filter((test: any) => test.success);
results.summary = {
total: allTests.length,
successful: successfulTests.length,
failed: allTests.length - successfulTests.length,
overallSuccess: successfulTests.length === allTests.length,
};
return results;
},
})
);
// --- Utility: Generate UUID ---
this.server.register(
defineTool({
name: 'utility_generate_uuid',
description: 'Generate a UUID v4',
async run(ctx: ToolContext) {
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
return {
uuid,
timestamp: new Date().toISOString(),
};
},
})
);
// --- Utility: Generate Random String ---
this.server.register(
defineTool({
name: 'utility_generate_random_string',
description: 'Generate a random string',
inputSchema: z.object({
length: z.number().min(1).max(256).default(32),
includeNumbers: z.boolean().default(true),
includeSymbols: z.boolean().default(false),
includeUppercase: z.boolean().default(true),
includeLowercase: z.boolean().default(true),
}),
async run(ctx: ToolContext) {
const { length, includeNumbers, includeSymbols, includeUppercase, includeLowercase } = ctx.input;
let chars = '';
if (includeLowercase) chars += 'abcdefghijklmnopqrstuvwxyz';
if (includeUppercase) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if (includeNumbers) chars += '0123456789';
if (includeSymbols) chars += '!@#$%^&*()_+-=[]{}|;:,.<>?';
if (chars === '') throw new Error('At least one character type must be included');
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return {
randomString: result,
length,
characterTypes: {
includeNumbers,
includeSymbols,
includeUppercase,
includeLowercase,
},
timestamp: new Date().toISOString(),
};
},
})
);
// --- Utility: Hash Text ---
this.server.register(
defineTool({
name: 'utility_hash_text',
description: 'Hash text using various algorithms',
inputSchema: z.object({
text: z.string(),
algorithm: z.enum(['md5', 'sha1', 'sha256', 'sha512']).default('sha256'),
}),
async run(ctx: ToolContext) {
const { text, algorithm } = ctx.input;
// Simple hash implementation for demo
let hash = '';
if (typeof crypto !== 'undefined' && crypto.subtle) {
const encoder = new TextEncoder();
const data = encoder.encode(text);
const hashBuffer = await crypto.subtle.digest(algorithm.toUpperCase().replace(/\d+/, '-$&'), data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
hash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
} else {
// Fallback simple hash for environments without crypto
let hashCode = 0;
for (let i = 0; i < text.length; i++) {
const char = text.charCodeAt(i);
hashCode = ((hashCode << 5) - hashCode) + char;
hashCode = hashCode & hashCode; // Convert to 32bit integer
}
hash = Math.abs(hashCode).toString(16);
}
return {
originalText: text,
algorithm,
hash,
timestamp: new Date().toISOString(),
};
},
})
);
// --- Utility: Encode/Decode Base64 ---
this.server.register(
defineTool({
name: 'utility_base64_encode',
description: 'Encode text to Base64',
inputSchema: z.object({ text: z.string() }),
async run(ctx: ToolContext) {
const { text } = ctx.input;
const encoded = Buffer.from(text, 'utf8').toString('base64');
return {
originalText: text,
encodedText: encoded,
timestamp: new Date().toISOString(),
};
},
})
);
// --- Utility: Decode Base64 ---
this.server.register(
defineTool({
name: 'utility_base64_decode',
description: 'Decode Base64 text',
inputSchema: z.object({ encodedText: z.string() }),
async run(ctx: ToolContext) {
const { encodedText } = ctx.input;
try {
const decoded = Buffer.from(encodedText, 'base64').toString('utf8');
return {
encodedText,
decodedText: decoded,
timestamp: new Date().toISOString(),
};
} catch (error) {
throw new Error('Invalid Base64 string');
}
},
})
);
// --- Utility: URL Encode/Decode ---
this.server.register(
defineTool({
name: 'utility_url_encode',
description: 'URL encode text',
inputSchema: z.object({ text: z.string() }),
async run(ctx: ToolContext) {
const { text } = ctx.input;
const encoded = encodeURIComponent(text);
return {
originalText: text,
encodedText: encoded,
timestamp: new Date().toISOString(),
};
},
})
);
// --- Utility: URL Decode ---
this.server.register(
defineTool({
name: 'utility_url_decode',
description: 'URL decode text',
inputSchema: z.object({ encodedText: z.string() }),
async run(ctx: ToolContext) {
const { encodedText } = ctx.input;
try {
const decoded = decodeURIComponent(encodedText);
return {
encodedText,
decodedText: decoded,
timestamp: new Date().toISOString(),
};
} catch (error) {
throw new Error('Invalid URL encoded string');
}
},
})
);
// --- Utility: JSON Validator ---
this.server.register(
defineTool({
name: 'utility_validate_json',
description: 'Validate and format JSON',
inputSchema: z.object({ jsonString: z.string() }),
async run(ctx: ToolContext) {
const { jsonString } = ctx.input;
try {
const parsed = JSON.parse(jsonString);
const formatted = JSON.stringify(parsed, null, 2);
return {
originalJson: jsonString,
isValid: true,
formattedJson: formatted,
parsedObject: parsed,
timestamp: new Date().toISOString(),
};
} catch (error) {
return {
originalJson: jsonString,
isValid: false,
error: (error as Error).message,
timestamp: new Date().toISOString(),
};
}
},
})
);
// --- Utility: Password Generator ---
this.server.register(
defineTool({
name: 'utility_generate_password',
description: 'Generate a secure password',
inputSchema: z.object({
length: z.number().min(8).max(128).default(16),
includeNumbers: z.boolean().default(true),
includeSymbols: z.boolean().default(true),
includeUppercase: z.boolean().default(true),
includeLowercase: z.boolean().default(true),
excludeSimilar: z.boolean().default(true), // Exclude 0, O, l, I, etc.
}),
async run(ctx: ToolContext) {
const { length, includeNumbers, includeSymbols, includeUppercase, includeLowercase, excludeSimilar } = ctx.input;
let chars = '';
if (includeLowercase) chars += excludeSimilar ? 'abcdefghijkmnopqrstuvwxyz' : 'abcdefghijklmnopqrstuvwxyz';
if (includeUppercase) chars += excludeSimilar ? 'ABCDEFGHJKLMNPQRSTUVWXYZ' : 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if (includeNumbers) chars += excludeSimilar ? '23456789' : '0123456789';
if (includeSymbols) chars += '!@#$%^&*()_+-=[]{}|;:,.<>?';
if (chars === '') throw new Error('At least one character type must be included');
let password = '';
for (let i = 0; i < length; i++) {
password += chars.charAt(Math.floor(Math.random() * chars.length));
}
// Calculate password strength
let strength = 0;
if (password.length >= 8) strength += 1;
if (password.length >= 12) strength += 1;
if (/[a-z]/.test(password)) strength += 1;
if (/[A-Z]/.test(password)) strength += 1;
if (/[0-9]/.test(password)) strength += 1;
if (/[^A-Za-z0-9]/.test(password)) strength += 1;
const strengthLevels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong', 'Very Strong'];
return {
password,
length,
strength: strengthLevels[Math.min(strength, 5)],
strengthScore: strength,
characterTypes: {
includeNumbers,
includeSymbols,
includeUppercase,
includeLowercase,
excludeSimilar,
},
timestamp: new Date().toISOString(),
};
},
})
);
// --- Utility: QR Code Data Generator ---
this.server.register(
defineTool({
name: 'utility_generate_qr_data',
description: 'Generate QR code data URL (simulation)',
inputSchema: z.object({
text: z.string(),
size: z.number().min(100).max(1000).default(200),
}),
async run(ctx: ToolContext) {
const { text, size } = ctx.input;
// In a real implementation, this would generate actual QR code
const qrDataUrl = `data:image/svg+xml;base64,${Buffer.from(`
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
<rect width="${size}" height="${size}" fill="white"/>
<text x="50%" y="50%" text-anchor="middle" dy=".3em" font-family="monospace" font-size="12">
QR: ${text.substring(0, 20)}${text.length > 20 ? '...' : ''}
</text>
</svg>
`).toString('base64')}`;
return {
originalText: text,
qrCodeDataUrl: qrDataUrl,
size,
timestamp: new Date().toISOString(),
};
},
})
);
}
}
// 5. Export function for Smithery
export default function ({ config }: { config: z.infer<typeof configSchema> }) {
const parseResult = configSchema.safeParse(config);
if (!parseResult.success) {
console.error('Invalid configuration:', parseResult.error);
// Return a server with minimal tools for debugging
const server = new MCPServer();
server.register(
defineTool({
name: 'config_error',
description: 'Configuration error details',
run: async () => ({
error: 'Invalid configuration provided',
details: parseResult.error.issues,
expectedConfig: {
pocketbaseUrl: 'https://your-pb-instance.com',
adminEmail: 'admin@example.com',
adminPassword: 'your-admin-password',
stripeSecretKey: 'sk_...(optional)',
sendgridApiKey: 'SG.xxx(optional)',
},
}),
})
);
return server;
}
const serverInstance = new ComprehensivePocketBaseMCPServer();
// Initialize synchronously with valid config
serverInstance.config = parseResult.data;
if (parseResult.data.debug) {
console.log('MCP Server initialized with configuration:', {
pocketbaseUrl: parseResult.data.pocketbaseUrl,
hasStripeKey: !!parseResult.data.stripeSecretKey,
hasSendgridKey: !!parseResult.data.sendgridApiKey,
hasSmtpConfig: !!(parseResult.data.smtpHost && parseResult.data.smtpPort),
});
}
return serverInstance.server;
}