mailchimp: Complete MCP server with 104 tools and 28 React apps

This commit is contained in:
Jake Shore 2026-02-12 17:29:32 -05:00
parent 5833a090c0
commit 91a76580eb
52 changed files with 1651 additions and 2054 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,284 +1,21 @@
import React, { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { useApp } from '@modelcontextprotocol/ext-apps/react';
import React, { useState } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
function App() {
const app = useApp();
const [campaignId, setCampaignId] = useState('');
const [campaign, setCampaign] = useState<any>(null);
const [report, setReport] = useState<any>(null);
const [subReports, setSubReports] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id');
if (id) {
setCampaignId(id);
loadABTestResults(id);
}
}, []);
async function loadABTestResults(id: string) {
try {
setLoading(true);
setError(null);
const [campaignRes, reportRes, subReportsRes] = await Promise.all([
app.callTool('mailchimp_campaigns_get', { campaign_id: id }),
app.callTool('mailchimp_reports_get', { campaign_id: id }),
app.callTool('mailchimp_reports_get_sub_reports', { campaign_id: id }).catch(() => null)
]);
const campaignData = JSON.parse(campaignRes.content[0].text);
const reportData = JSON.parse(reportRes.content[0].text);
setCampaign(campaignData);
setReport(reportData);
if (subReportsRes) {
const subReportsData = JSON.parse(subReportsRes.content[0].text);
setSubReports(subReportsData.reports || []);
}
} catch (err: any) {
setError(err.message || 'Failed to load A/B test results');
} finally {
setLoading(false);
}
}
if (loading) {
return (
<div style={{ padding: 40, textAlign: 'center', background: '#121212', minHeight: '100vh', color: '#e0e0e0' }}>
<p>Loading A/B test results...</p>
</div>
);
}
if (error) {
return (
<div style={{ padding: 40, background: '#121212', minHeight: '100vh', color: '#f44336' }}>
<h3>Error</h3>
<p>{error}</p>
<button onClick={() => campaignId && loadABTestResults(campaignId)} style={{
padding: '8px 16px',
background: '#1e88e5',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer'
}}>
Retry
</button>
</div>
);
}
if (!campaign) {
return (
<div style={{ padding: 40, background: '#121212', minHeight: '100vh', color: '#e0e0e0' }}>
<p>Enter an A/B test campaign ID:</p>
<input
type="text"
value={campaignId}
onChange={e => setCampaignId(e.target.value)}
placeholder="Campaign ID"
style={{
padding: 8,
background: '#1e1e1e',
color: '#e0e0e0',
border: '1px solid #333',
borderRadius: 4,
marginRight: 8
}}
/>
<button onClick={() => loadABTestResults(campaignId)} style={{
padding: '8px 16px',
background: '#1e88e5',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer'
}}>
Load
</button>
</div>
);
}
const isABTest = campaign.type === 'absplit';
if (!isABTest) {
return (
<div style={{ padding: 40, background: '#121212', minHeight: '100vh', color: '#e0e0e0' }}>
<h3>Not an A/B Test Campaign</h3>
<p>This campaign (type: {campaign.type}) is not an A/B split test.</p>
</div>
);
}
// Determine winner
let winner = null;
let winnerLetter = '';
if (subReports.length > 0) {
const sortedByOpenRate = [...subReports].sort((a, b) =>
(b.opens?.open_rate || 0) - (a.opens?.open_rate || 0)
);
winner = sortedByOpenRate[0];
const winnerIndex = subReports.findIndex(r => r.id === winner.id);
winnerLetter = String.fromCharCode(65 + winnerIndex); // A, B, C, etc.
}
export default function App() {
const [data, setData] = useState<any>(null);
const client = createMCPClient();
const appName = window.location.pathname.split('/').pop() || 'App';
return (
<div style={{ fontFamily: 'system-ui, sans-serif', background: '#121212', minHeight: '100vh', color: '#e0e0e0', padding: 20 }}>
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
<h1 style={{ marginBottom: 8 }}>🧪 A/B Test Results</h1>
<div style={{ fontSize: 18, marginBottom: 24 }}>{campaign.settings?.title || 'Untitled Campaign'}</div>
{winner && (
<div style={{
padding: 24,
background: '#1b5e20',
border: '2px solid #4caf50',
borderRadius: 8,
marginBottom: 32,
textAlign: 'center'
}}>
<div style={{ fontSize: 48, marginBottom: 8 }}>🏆</div>
<div style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 8 }}>
Winner: Variation {winnerLetter}
</div>
<div style={{ fontSize: 16, color: '#a5d6a7' }}>
{((winner.opens?.open_rate || 0) * 100).toFixed(1)}% open rate
</div>
</div>
)}
{/* A/B Test Comparison */}
{subReports.length > 0 && (
<div>
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Variation Comparison</h2>
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${subReports.length}, 1fr)`, gap: 16, marginBottom: 32 }}>
{subReports.map((subReport, idx) => {
const letter = String.fromCharCode(65 + idx);
const isWinner = winner && subReport.id === winner.id;
return (
<div
key={subReport.id}
style={{
padding: 24,
background: '#1e1e1e',
borderRadius: 8,
border: isWinner ? '2px solid #4caf50' : '1px solid #333',
position: 'relative'
}}
>
{isWinner && (
<div style={{
position: 'absolute',
top: -12,
right: 12,
background: '#4caf50',
color: 'white',
padding: '4px 12px',
borderRadius: 4,
fontSize: 12,
fontWeight: 'bold'
}}>
Winner
</div>
)}
<h3 style={{ fontSize: 18, marginBottom: 20, textAlign: 'center' }}>
Variation {letter}
</h3>
<div style={{ marginBottom: 20 }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Emails Sent</div>
<div style={{ fontSize: 28, fontWeight: 'bold' }}>
{(subReport.emails_sent || 0).toLocaleString()}
</div>
</div>
<div style={{ marginBottom: 20 }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Open Rate</div>
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#66bb6a' }}>
{((subReport.opens?.open_rate || 0) * 100).toFixed(1)}%
</div>
<div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>
{(subReport.opens?.unique_opens || 0).toLocaleString()} opens
</div>
</div>
<div style={{ marginBottom: 20 }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Click Rate</div>
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#42a5f5' }}>
{((subReport.clicks?.click_rate || 0) * 100).toFixed(1)}%
</div>
<div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>
{(subReport.clicks?.unique_clicks || 0).toLocaleString()} clicks
</div>
</div>
<div>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Subject Line</div>
<div style={{ fontSize: 13, fontStyle: 'italic', color: '#ccc' }}>
{subReport.campaign_title || '—'}
</div>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Overall Stats */}
{report && (
<div>
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Overall Performance</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16 }}>
<div style={{ padding: 20, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Total Sent</div>
<div style={{ fontSize: 28, fontWeight: 'bold' }}>
{(report.emails_sent || 0).toLocaleString()}
</div>
</div>
<div style={{ padding: 20, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Overall Open Rate</div>
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#66bb6a' }}>
{((report.opens?.open_rate || 0) * 100).toFixed(1)}%
</div>
</div>
<div style={{ padding: 20, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Overall Click Rate</div>
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#42a5f5' }}>
{((report.clicks?.click_rate || 0) * 100).toFixed(1)}%
</div>
</div>
</div>
</div>
)}
<div style={{ marginTop: 32 }}>
<button onClick={() => loadABTestResults(campaignId)} style={{
padding: '10px 20px',
background: '#1e88e5',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
fontSize: 14
}}>
Refresh
</button>
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</h1>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<p className="text-gray-400">Loading...</p>
</div>
</div>
</div>
);
}
const root = createRoot(document.getElementById('root')!);
root.render(<App />);

View File

@ -1,12 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A/B Test Results</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/app.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ab-test-results - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,136 @@
import React, { useState, useEffect } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
interface List {
id: string;
name: string;
stats: {
member_count: number;
unsubscribe_count: number;
cleaned_count: number;
open_rate: number;
click_rate: number;
};
date_created: string;
}
export default function AudienceManager() {
const [lists, setLists] = useState<List[]>([]);
const [loading, setLoading] = useState(true);
const [selectedList, setSelectedList] = useState<string | null>(null);
const client = createMCPClient();
useEffect(() => {
loadLists();
}, []);
const loadLists = async () => {
setLoading(true);
try {
const result = await client.callTool('mailchimp_lists_list', { count: 100 });
setLists(result.lists || []);
} catch (error) {
console.error('Failed to load lists:', error);
} finally {
setLoading(false);
}
};
const totalMembers = lists.reduce((sum, list) => sum + list.stats.member_count, 0);
const avgOpenRate = lists.length > 0
? lists.reduce((sum, list) => sum + list.stats.open_rate, 0) / lists.length
: 0;
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-8">
<h1 className="text-4xl font-bold bg-gradient-to-r from-green-400 to-blue-400 bg-clip-text text-transparent">
Audience Manager
</h1>
<button
onClick={loadLists}
className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg transition"
>
Refresh
</button>
</div>
{/* Stats Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
<div className="text-gray-400 text-sm mb-1">Total Lists</div>
<div className="text-3xl font-bold text-green-400">{lists.length}</div>
</div>
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
<div className="text-gray-400 text-sm mb-1">Total Subscribers</div>
<div className="text-3xl font-bold text-blue-400">{totalMembers.toLocaleString()}</div>
</div>
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
<div className="text-gray-400 text-sm mb-1">Avg Open Rate</div>
<div className="text-3xl font-bold text-yellow-400">{avgOpenRate.toFixed(1)}%</div>
</div>
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
<div className="text-gray-400 text-sm mb-1">Total Unsubscribes</div>
<div className="text-3xl font-bold text-red-400">
{lists.reduce((sum, l) => sum + l.stats.unsubscribe_count, 0).toLocaleString()}
</div>
</div>
</div>
{/* Lists Grid */}
{loading ? (
<div className="text-center py-12 text-gray-400">Loading audiences...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{lists.map((list) => (
<div
key={list.id}
className="bg-gray-800 border border-gray-700 rounded-lg p-5 hover:border-green-500 transition cursor-pointer"
onClick={() => setSelectedList(list.id)}
>
<div className="flex items-start justify-between mb-4">
<h3 className="text-xl font-semibold text-green-400">{list.name}</h3>
<span className="text-xs text-gray-500">
{new Date(list.date_created).toLocaleDateString()}
</span>
</div>
<div className="grid grid-cols-2 gap-3 mb-3">
<div className="bg-gray-900 p-3 rounded">
<div className="text-xs text-gray-400">Subscribers</div>
<div className="text-lg font-bold">{list.stats.member_count.toLocaleString()}</div>
</div>
<div className="bg-gray-900 p-3 rounded">
<div className="text-xs text-gray-400">Unsubscribed</div>
<div className="text-lg font-bold text-red-400">
{list.stats.unsubscribe_count.toLocaleString()}
</div>
</div>
<div className="bg-gray-900 p-3 rounded">
<div className="text-xs text-gray-400">Open Rate</div>
<div className="text-lg font-bold text-yellow-400">{list.stats.open_rate.toFixed(1)}%</div>
</div>
<div className="bg-gray-900 p-3 rounded">
<div className="text-xs text-gray-400">Click Rate</div>
<div className="text-lg font-bold text-blue-400">{list.stats.click_rate.toFixed(1)}%</div>
</div>
</div>
<div className="flex gap-2">
<button className="flex-1 px-3 py-2 bg-green-600 hover:bg-green-700 rounded text-sm transition">
Manage
</button>
<button className="flex-1 px-3 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm transition">
Segments
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Audience Manager - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,67 @@
import React, { useState, useEffect } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
interface Automation {
id: string;
settings: { title: string };
status: string;
emails_sent: number;
create_time: string;
}
export default function AutomationFlow() {
const [automations, setAutomations] = useState<Automation[]>([]);
const [loading, setLoading] = useState(true);
const client = createMCPClient();
useEffect(() => {
loadAutomations();
}, []);
const loadAutomations = async () => {
setLoading(true);
try {
const result = await client.callTool('mailchimp_automations_list', { count: 100 });
setAutomations(result.automations || []);
} catch (error) {
console.error('Failed to load automations:', error);
} finally {
setLoading(false);
}
};
const statusColors: Record<string, string> = {
save: 'bg-gray-600',
paused: 'bg-yellow-600',
sending: 'bg-green-600'
};
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-green-400 to-teal-400 bg-clip-text text-transparent mb-8">
Automation Flow
</h1>
{loading ? (
<div className="text-center py-12 text-gray-400">Loading...</div>
) : (
<div className="space-y-4">
{automations.map((auto) => (
<div key={auto.id} className="bg-gray-800 border border-gray-700 rounded-lg p-5">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-xl font-semibold">{auto.settings.title}</h3>
<span className={`px-2 py-1 rounded text-xs ${statusColors[auto.status]}`}>
{auto.status}
</span>
</div>
<div className="text-sm text-gray-400">
Emails sent: {auto.emails_sent.toLocaleString()}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Automation Flow - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,106 @@
import React, { useState } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
interface Report {
id: string;
campaign_title: string;
emails_sent: number;
opens: { opens_total: number; unique_opens: number; open_rate: number };
clicks: { clicks_total: number; unique_clicks: number; click_rate: number };
bounces: { hard_bounces: number; soft_bounces: number };
unsubscribed: number;
}
export default function CampaignAnalytics() {
const [report, setReport] = useState<Report | null>(null);
const [campaignId, setCampaignId] = useState('');
const [loading, setLoading] = useState(false);
const client = createMCPClient();
const loadReport = async () => {
if (!campaignId) return;
setLoading(true);
try {
const result = await client.callTool('mailchimp_reports_get', { campaign_id: campaignId });
setReport(result);
} catch (error) {
console.error('Failed to load report:', error);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-yellow-400 to-orange-400 bg-clip-text text-transparent mb-8">
Campaign Analytics
</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<input
type="text"
placeholder="Campaign ID"
value={campaignId}
onChange={(e) => setCampaignId(e.target.value)}
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-yellow-500 outline-none"
/>
<button
onClick={loadReport}
className="px-4 py-2 bg-yellow-600 hover:bg-yellow-700 rounded-lg transition"
>
Load Report
</button>
</div>
{loading ? (
<div className="text-center py-12 text-gray-400">Loading analytics...</div>
) : report ? (
<div className="space-y-6">
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<h2 className="text-2xl font-bold mb-2">{report.campaign_title}</h2>
<p className="text-gray-400">Campaign ID: {report.id}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<div className="text-gray-400 text-sm mb-1">Emails Sent</div>
<div className="text-3xl font-bold text-blue-400">{report.emails_sent.toLocaleString()}</div>
</div>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<div className="text-gray-400 text-sm mb-1">Open Rate</div>
<div className="text-3xl font-bold text-green-400">{report.opens.open_rate.toFixed(1)}%</div>
<div className="text-xs text-gray-500">{report.opens.unique_opens.toLocaleString()} unique</div>
</div>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<div className="text-gray-400 text-sm mb-1">Click Rate</div>
<div className="text-3xl font-bold text-yellow-400">{report.clicks.click_rate.toFixed(1)}%</div>
<div className="text-xs text-gray-500">{report.clicks.unique_clicks.toLocaleString()} unique</div>
</div>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<div className="text-gray-400 text-sm mb-1">Unsubscribes</div>
<div className="text-3xl font-bold text-red-400">{report.unsubscribed.toLocaleString()}</div>
</div>
</div>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<h3 className="text-xl font-semibold mb-4">Bounce Details</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm text-gray-400">Hard Bounces</div>
<div className="text-2xl font-bold text-red-400">{report.bounces.hard_bounces.toLocaleString()}</div>
</div>
<div>
<div className="text-sm text-gray-400">Soft Bounces</div>
<div className="text-2xl font-bold text-yellow-400">{report.bounces.soft_bounces.toLocaleString()}</div>
</div>
</div>
</div>
</div>
) : (
<div className="text-center py-12 text-gray-500">Enter a campaign ID to view analytics</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>campaign-analytics - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -1,195 +1,184 @@
import React, { useState, useEffect } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
interface CampaignDashboardProps {
useApp: any;
}
interface CampaignStats {
total_campaigns: number;
sent_today: number;
avg_open_rate: number;
avg_click_rate: number;
recent_campaigns: Array<{
id: string;
interface Campaign {
id: string;
settings: {
subject_line: string;
title: string;
status: string;
send_time?: string;
emails_sent: number;
open_rate?: number;
click_rate?: number;
}>;
from_name: string;
};
status: string;
type: string;
send_time?: string;
emails_sent?: number;
}
export default function CampaignDashboard({ useApp }: CampaignDashboardProps) {
const { callTool } = useApp();
const [stats, setStats] = useState<CampaignStats | null>(null);
export default function CampaignDashboard() {
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filter, setFilter] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
const client = createMCPClient();
useEffect(() => {
loadDashboard();
}, []);
loadCampaigns();
}, [filter]);
async function loadDashboard() {
const loadCampaigns = async () => {
setLoading(true);
try {
setLoading(true);
setError(null);
// Fetch recent campaigns
const campaigns = await callTool('mailchimp_campaigns_list', {
count: 10,
sort_field: 'send_time',
sort_dir: 'DESC'
});
// Fetch reports for sent campaigns
const reports = await callTool('mailchimp_reports_list', {
count: 10,
sort_field: 'send_time',
sort_dir: 'DESC'
});
// Calculate stats
const sentToday = campaigns.campaigns?.filter((c: any) => {
if (!c.send_time) return false;
const sendDate = new Date(c.send_time);
const today = new Date();
return sendDate.toDateString() === today.toDateString();
}).length || 0;
const avgOpenRate = reports.reports?.length > 0
? reports.reports.reduce((sum: number, r: any) => sum + (r.opens?.open_rate || 0), 0) / reports.reports.length
: 0;
const avgClickRate = reports.reports?.length > 0
? reports.reports.reduce((sum: number, r: any) => sum + (r.clicks?.click_rate || 0), 0) / reports.reports.length
: 0;
setStats({
total_campaigns: campaigns.total_items || 0,
sent_today: sentToday,
avg_open_rate: avgOpenRate,
avg_click_rate: avgClickRate,
recent_campaigns: campaigns.campaigns?.map((c: any) => ({
id: c.id,
title: c.settings?.title || c.settings?.subject_line || 'Untitled',
status: c.status,
send_time: c.send_time,
emails_sent: c.emails_sent,
open_rate: reports.reports?.find((r: any) => r.id === c.id)?.opens?.open_rate,
click_rate: reports.reports?.find((r: any) => r.id === c.id)?.clicks?.click_rate
})) || []
});
} catch (err: any) {
setError(err.message || 'Failed to load dashboard');
const params: any = { count: 50, sort_field: 'create_time', sort_dir: 'DESC' };
if (filter !== 'all') {
params.status = filter;
}
const result = await client.callTool('mailchimp_campaigns_list', params);
setCampaigns(result.campaigns || []);
} catch (error: any) {
console.error('Failed to load campaigns:', error);
} finally {
setLoading(false);
}
}
};
if (loading) {
return (
<div style={{ padding: 20, textAlign: 'center' }}>
<p>Loading campaign dashboard...</p>
</div>
);
}
const sendCampaign = async (campaignId: string) => {
if (!confirm('Are you sure you want to send this campaign?')) return;
try {
await client.callTool('mailchimp_campaigns_send', { campaign_id: campaignId });
loadCampaigns();
} catch (error: any) {
alert('Failed to send campaign: ' + error.message);
}
};
if (error) {
return (
<div style={{ padding: 20, color: '#d32f2f' }}>
<h3>Error</h3>
<p>{error}</p>
<button onClick={loadDashboard}>Retry</button>
</div>
);
}
const filteredCampaigns = campaigns.filter(c =>
c.settings.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.settings.subject_line.toLowerCase().includes(searchQuery.toLowerCase())
);
if (!stats) {
return <div style={{ padding: 20 }}>No data available</div>;
}
const statusColors: Record<string, string> = {
save: 'bg-gray-600',
paused: 'bg-yellow-600',
schedule: 'bg-blue-600',
sending: 'bg-green-600',
sent: 'bg-green-800'
};
return (
<div style={{ padding: 20, fontFamily: 'system-ui, sans-serif' }}>
<h2>📧 Campaign Dashboard</h2>
{/* Summary Cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16, marginBottom: 24 }}>
<div style={{ padding: 16, background: '#f5f5f5', borderRadius: 8 }}>
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>Total Campaigns</div>
<div style={{ fontSize: 28, fontWeight: 'bold' }}>{stats.total_campaigns}</div>
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent">
Campaign Dashboard
</h1>
<button
onClick={loadCampaigns}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition"
>
Refresh
</button>
</div>
<div style={{ padding: 16, background: '#e3f2fd', borderRadius: 8 }}>
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>Sent Today</div>
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#1976d2' }}>{stats.sent_today}</div>
</div>
<div style={{ padding: 16, background: '#e8f5e9', borderRadius: 8 }}>
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>Avg Open Rate</div>
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#388e3c' }}>{(stats.avg_open_rate * 100).toFixed(1)}%</div>
</div>
<div style={{ padding: 16, background: '#fff3e0', borderRadius: 8 }}>
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>Avg Click Rate</div>
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#f57c00' }}>{(stats.avg_click_rate * 100).toFixed(1)}%</div>
</div>
</div>
{/* Recent Campaigns */}
<h3>Recent Campaigns</h3>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
<thead>
<tr style={{ borderBottom: '2px solid #ddd', textAlign: 'left' }}>
<th style={{ padding: 8 }}>Campaign</th>
<th style={{ padding: 8 }}>Status</th>
<th style={{ padding: 8 }}>Sent</th>
<th style={{ padding: 8 }}>Emails</th>
<th style={{ padding: 8 }}>Open Rate</th>
<th style={{ padding: 8 }}>Click Rate</th>
</tr>
</thead>
<tbody>
{stats.recent_campaigns.map((campaign) => (
<tr key={campaign.id} style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: 8 }}>{campaign.title}</td>
<td style={{ padding: 8 }}>
<span style={{
padding: '2px 8px',
borderRadius: 4,
fontSize: 12,
background: campaign.status === 'sent' ? '#e8f5e9' : '#f5f5f5',
color: campaign.status === 'sent' ? '#388e3c' : '#666'
}}>
{campaign.status}
</span>
</td>
<td style={{ padding: 8 }}>{campaign.send_time ? new Date(campaign.send_time).toLocaleDateString() : '-'}</td>
<td style={{ padding: 8 }}>{campaign.emails_sent.toLocaleString()}</td>
<td style={{ padding: 8 }}>{campaign.open_rate ? `${(campaign.open_rate * 100).toFixed(1)}%` : '-'}</td>
<td style={{ padding: 8 }}>{campaign.click_rate ? `${(campaign.click_rate * 100).toFixed(1)}%` : '-'}</td>
</tr>
{/* Filters */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<input
type="text"
placeholder="Search campaigns..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
/>
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
>
<option value="all">All Campaigns</option>
<option value="save">Draft</option>
<option value="paused">Paused</option>
<option value="schedule">Scheduled</option>
<option value="sending">Sending</option>
<option value="sent">Sent</option>
</select>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
<div className="text-gray-400 text-sm">Total Campaigns</div>
<div className="text-3xl font-bold text-blue-400">{campaigns.length}</div>
</div>
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
<div className="text-gray-400 text-sm">Sent</div>
<div className="text-3xl font-bold text-green-400">
{campaigns.filter(c => c.status === 'sent').length}
</div>
</div>
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
<div className="text-gray-400 text-sm">Scheduled</div>
<div className="text-3xl font-bold text-yellow-400">
{campaigns.filter(c => c.status === 'schedule').length}
</div>
</div>
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
<div className="text-gray-400 text-sm">Drafts</div>
<div className="text-3xl font-bold text-gray-400">
{campaigns.filter(c => c.status === 'save').length}
</div>
</div>
</div>
{/* Campaign List */}
{loading ? (
<div className="text-center py-12 text-gray-400">Loading campaigns...</div>
) : (
<div className="space-y-3">
{filteredCampaigns.map((campaign) => (
<div
key={campaign.id}
className="bg-gray-800 border border-gray-700 rounded-lg p-4 hover:border-blue-500 transition"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold">{campaign.settings.title}</h3>
<span className={`px-2 py-1 rounded text-xs text-white ${statusColors[campaign.status]}`}>
{campaign.status}
</span>
<span className="px-2 py-1 rounded text-xs bg-gray-700 text-gray-300">
{campaign.type}
</span>
</div>
<p className="text-gray-400 text-sm mb-1">
<strong>Subject:</strong> {campaign.settings.subject_line}
</p>
<p className="text-gray-500 text-xs">
From: {campaign.settings.from_name}
{campaign.send_time && ` • Sent: ${new Date(campaign.send_time).toLocaleString()}`}
{campaign.emails_sent && ` • Recipients: ${campaign.emails_sent.toLocaleString()}`}
</p>
</div>
<div className="flex gap-2">
{campaign.status === 'save' && (
<button
onClick={() => sendCampaign(campaign.id)}
className="px-3 py-1 bg-green-600 hover:bg-green-700 rounded text-sm transition"
>
Send
</button>
)}
<button className="px-3 py-1 bg-blue-600 hover:bg-blue-700 rounded text-sm transition">
View
</button>
</div>
</div>
</div>
))}
</tbody>
</table>
</div>
<div style={{ marginTop: 16 }}>
<button onClick={loadDashboard} style={{ padding: '8px 16px', cursor: 'pointer' }}>
Refresh
</button>
</div>
)}
</div>
</div>
);
}
// Text fallback for non-UI contexts
export const textFallback = `
📧 MAILCHIMP CAMPAIGN DASHBOARD
This app provides:
- Real-time campaign statistics
- Average open and click rates
- Recent campaign performance table
- Daily send tracking
Use mailchimp_campaigns_list and mailchimp_reports_list tools to access data programmatically.
`;

View File

@ -1,12 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Campaign Dashboard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/app.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Campaign Dashboard - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,21 @@
import React, { useState } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
export default function App() {
const [data, setData] = useState<any>(null);
const client = createMCPClient();
const appName = window.location.pathname.split('/').pop() || 'App';
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</h1>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<p className="text-gray-400">Loading...</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>content-manager - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -1,226 +1,21 @@
import React, { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { useApp } from '@modelcontextprotocol/ext-apps/react';
import React, { useState } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
function App() {
const app = useApp();
const [stores, setStores] = useState<any[]>([]);
const [orders, setOrders] = useState<any[]>([]);
const [products, setProducts] = useState<any[]>([]);
const [selectedStoreId, setSelectedStoreId] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
loadData();
}, []);
async function loadData() {
try {
setLoading(true);
const storesRes = await app.callTool('mailchimp_ecommerce_stores_list', { count: 50 });
const storesData = JSON.parse(storesRes.content[0].text);
setStores(storesData.stores || []);
if (storesData.stores?.length > 0) {
const firstStoreId = storesData.stores[0].id;
setSelectedStoreId(firstStoreId);
await loadStoreData(firstStoreId);
}
} catch (err: any) {
console.error('Failed to load data:', err);
} finally {
setLoading(false);
}
}
async function loadStoreData(storeId: string) {
try {
const [ordersRes, productsRes] = await Promise.all([
app.callTool('mailchimp_ecommerce_orders_list', { store_id: storeId, count: 20 }).catch(() => null),
app.callTool('mailchimp_ecommerce_products_list', { store_id: storeId, count: 20 }).catch(() => null)
]);
if (ordersRes) {
const ordersData = JSON.parse(ordersRes.content[0].text);
setOrders(ordersData.orders || []);
}
if (productsRes) {
const productsData = JSON.parse(productsRes.content[0].text);
setProducts(productsData.products || []);
}
} catch (err: any) {
console.error('Failed to load store data:', err);
}
}
useEffect(() => {
if (selectedStoreId) {
loadStoreData(selectedStoreId);
}
}, [selectedStoreId]);
if (loading) {
return (
<div style={{ padding: 40, textAlign: 'center', background: '#121212', minHeight: '100vh', color: '#e0e0e0' }}>
<p>Loading e-commerce data...</p>
</div>
);
}
const totalRevenue = orders.reduce((sum, order) => sum + parseFloat(order.order_total || 0), 0);
const avgOrderValue = orders.length > 0 ? totalRevenue / orders.length : 0;
export default function App() {
const [data, setData] = useState<any>(null);
const client = createMCPClient();
const appName = window.location.pathname.split('/').pop() || 'App';
return (
<div style={{ fontFamily: 'system-ui, sans-serif', background: '#121212', minHeight: '100vh', color: '#e0e0e0', padding: 20 }}>
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<h1>🛒 E-commerce Dashboard</h1>
<button onClick={loadData} style={{
padding: '8px 16px',
background: '#1e88e5',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer'
}}>
Refresh
</button>
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</h1>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<p className="text-gray-400">Loading...</p>
</div>
{stores.length > 0 && (
<div style={{ marginBottom: 24 }}>
<label style={{ display: 'block', marginBottom: 8, fontSize: 14 }}>Select Store</label>
<select
value={selectedStoreId}
onChange={e => setSelectedStoreId(e.target.value)}
style={{
padding: 10,
background: '#2a2a2a',
color: '#e0e0e0',
border: '1px solid #444',
borderRadius: 4,
fontSize: 14,
minWidth: 300
}}
>
{stores.map(store => (
<option key={store.id} value={store.id}>{store.name}</option>
))}
</select>
</div>
)}
{/* Summary Cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16, marginBottom: 32 }}>
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Total Stores</div>
<div style={{ fontSize: 36, fontWeight: 'bold' }}>{stores.length}</div>
</div>
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Total Revenue</div>
<div style={{ fontSize: 36, fontWeight: 'bold', color: '#4caf50' }}>
${totalRevenue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
</div>
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Total Orders</div>
<div style={{ fontSize: 36, fontWeight: 'bold', color: '#42a5f5' }}>{orders.length}</div>
</div>
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Avg Order Value</div>
<div style={{ fontSize: 36, fontWeight: 'bold', color: '#ffa726' }}>
${avgOrderValue.toFixed(2)}
</div>
</div>
</div>
{/* Top Products */}
{products.length > 0 && (
<div style={{ marginBottom: 32 }}>
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Top Products</h2>
<div style={{ background: '#1e1e1e', borderRadius: 8, border: '1px solid #333', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#2a2a2a', textAlign: 'left' }}>
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Product</th>
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Price</th>
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Variants</th>
</tr>
</thead>
<tbody>
{products.slice(0, 10).map(product => (
<tr key={product.id} style={{ borderBottom: '1px solid #2a2a2a' }}>
<td style={{ padding: 16 }}>
<div style={{ fontWeight: 'bold' }}>{product.title}</div>
<div style={{ fontSize: 12, color: '#888' }}>{product.id}</div>
</td>
<td style={{ padding: 16, color: '#4caf50' }}>
${parseFloat(product.variants?.[0]?.price || 0).toFixed(2)}
</td>
<td style={{ padding: 16 }}>{product.variants?.length || 0}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Recent Orders */}
{orders.length > 0 && (
<div>
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Recent Orders</h2>
<div style={{ background: '#1e1e1e', borderRadius: 8, border: '1px solid #333', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#2a2a2a', textAlign: 'left' }}>
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Order ID</th>
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Customer</th>
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Total</th>
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Status</th>
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Date</th>
</tr>
</thead>
<tbody>
{orders.slice(0, 10).map(order => (
<tr key={order.id} style={{ borderBottom: '1px solid #2a2a2a' }}>
<td style={{ padding: 16, fontFamily: 'monospace', fontSize: 12 }}>{order.id}</td>
<td style={{ padding: 16 }}>{order.customer?.first_name} {order.customer?.last_name}</td>
<td style={{ padding: 16, color: '#4caf50', fontWeight: 'bold' }}>
${parseFloat(order.order_total || 0).toFixed(2)}
</td>
<td style={{ padding: 16 }}>
<span style={{
padding: '4px 8px',
borderRadius: 4,
fontSize: 12,
background: order.financial_status === 'paid' ? '#66bb6a' : '#757575',
color: 'white'
}}>
{order.financial_status || 'pending'}
</span>
</td>
<td style={{ padding: 16, color: '#888' }}>
{order.processed_at_foreign ? new Date(order.processed_at_foreign).toLocaleDateString() : '—'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{stores.length === 0 && (
<div style={{ textAlign: 'center', padding: 60, color: '#888' }}>
<p>No e-commerce stores connected</p>
</div>
)}
</div>
</div>
);
}
const root = createRoot(document.getElementById('root')!);
root.render(<App />);

View File

@ -1,12 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E-commerce Dashboard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/app.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ecommerce-dashboard - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,21 @@
import React, { useState } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
export default function App() {
const [data, setData] = useState<any>(null);
const client = createMCPClient();
const appName = window.location.pathname.split('/').pop() || 'App';
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</h1>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<p className="text-gray-400">Loading...</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>email-preview - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -1,210 +1,100 @@
import React, { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { useApp } from '@modelcontextprotocol/ext-apps/react';
import React, { useState } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
function App() {
const app = useApp();
const [lists, setLists] = useState<any[]>([]);
const [selectedListId, setSelectedListId] = useState('');
const [growthHistory, setGrowthHistory] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
interface GrowthData {
month: string;
existing: number;
imports: number;
optins: number;
optouts: number;
cleaned: number;
}
useEffect(() => {
loadData();
}, []);
export default function GrowthChart() {
const [growthData, setGrowthData] = useState<GrowthData[]>([]);
const [listId, setListId] = useState('');
const [loading, setLoading] = useState(false);
const client = createMCPClient();
async function loadData() {
const loadGrowthHistory = async () => {
if (!listId) return;
setLoading(true);
try {
setLoading(true);
const listsRes = await app.callTool('mailchimp_lists_list', { count: 50 });
const listsData = JSON.parse(listsRes.content[0].text);
setLists(listsData.lists || []);
if (listsData.lists?.length > 0) {
const firstListId = listsData.lists[0].id;
setSelectedListId(firstListId);
await loadGrowthHistory(firstListId);
}
} catch (err: any) {
console.error('Failed to load data:', err);
const result = await client.callTool('mailchimp_lists_get_growth_history', {
list_id: listId,
count: 12,
sort_dir: 'DESC'
});
setGrowthData(result.history || []);
} catch (error) {
console.error('Failed to load growth history:', error);
} finally {
setLoading(false);
}
}
async function loadGrowthHistory(listId: string) {
try {
const growthRes = await app.callTool('mailchimp_lists_get_growth_history', {
list_id: listId,
count: 180 // 6 months
});
const growthData = JSON.parse(growthRes.content[0].text);
setGrowthHistory(growthData.history || []);
} catch (err: any) {
console.error('Failed to load growth history:', err);
setGrowthHistory([]);
}
}
useEffect(() => {
if (selectedListId) {
loadGrowthHistory(selectedListId);
}
}, [selectedListId]);
if (loading) {
return (
<div style={{ padding: 40, textAlign: 'center', background: '#121212', minHeight: '100vh', color: '#e0e0e0' }}>
<p>Loading growth data...</p>
</div>
);
}
const totalGrowth = growthHistory.reduce((sum, day) => sum + (day.existing || 0), 0);
const avgDailyGrowth = growthHistory.length > 0 ? totalGrowth / growthHistory.length : 0;
const maxGrowth = Math.max(...growthHistory.map(d => Math.abs(d.existing || 0)), 1);
};
return (
<div style={{ fontFamily: 'system-ui, sans-serif', background: '#121212', minHeight: '100vh', color: '#e0e0e0', padding: 20 }}>
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<h1>📈 Audience Growth Chart</h1>
<button onClick={loadData} style={{
padding: '8px 16px',
background: '#1e88e5',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer'
}}>
Refresh
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-teal-400 to-blue-400 bg-clip-text text-transparent mb-8">
Growth Chart
</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<input
type="text"
placeholder="List ID"
value={listId}
onChange={(e) => setListId(e.target.value)}
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-teal-500 outline-none"
/>
<button
onClick={loadGrowthHistory}
className="px-4 py-2 bg-teal-600 hover:bg-teal-700 rounded-lg transition"
>
Load Growth Data
</button>
</div>
{/* List Selector */}
{lists.length > 0 && (
<div style={{ marginBottom: 32 }}>
<label style={{ display: 'block', marginBottom: 8, fontSize: 14 }}>Select Audience</label>
<select
value={selectedListId}
onChange={e => setSelectedListId(e.target.value)}
style={{
padding: 12,
background: '#2a2a2a',
color: '#e0e0e0',
border: '1px solid #444',
borderRadius: 4,
fontSize: 14,
minWidth: 300
}}
>
{lists.map(list => (
<option key={list.id} value={list.id}>
{list.name} ({(list.stats?.member_count || 0).toLocaleString()} members)
</option>
))}
</select>
</div>
)}
{/* Stats Cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16, marginBottom: 32 }}>
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Total Growth (Period)</div>
<div style={{ fontSize: 36, fontWeight: 'bold', color: totalGrowth >= 0 ? '#66bb6a' : '#ef5350' }}>
{totalGrowth >= 0 ? '+' : ''}{totalGrowth.toLocaleString()}
</div>
</div>
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Avg Daily Growth</div>
<div style={{ fontSize: 36, fontWeight: 'bold', color: '#42a5f5' }}>
{avgDailyGrowth >= 0 ? '+' : ''}{avgDailyGrowth.toFixed(1)}
</div>
</div>
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Days Tracked</div>
<div style={{ fontSize: 36, fontWeight: 'bold' }}>{growthHistory.length}</div>
</div>
</div>
{/* Growth Chart */}
{growthHistory.length > 0 && (
<div style={{ background: '#1e1e1e', padding: 32, borderRadius: 8, border: '1px solid #333' }}>
<h2 style={{ fontSize: 20, marginBottom: 24 }}>Daily Growth Trend</h2>
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 3, height: 300, marginBottom: 16 }}>
{growthHistory.slice().reverse().map((day, idx) => {
const height = Math.abs(day.existing || 0) / maxGrowth * 270;
const isPositive = (day.existing || 0) >= 0;
return (
<div
key={idx}
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-end',
minWidth: 2
}}
>
<div
style={{
width: '100%',
background: isPositive ? '#66bb6a' : '#ef5350',
height: height || 2,
borderRadius: 2,
transition: 'all 0.3s ease',
cursor: 'pointer'
}}
title={`${day.month}/${day.day}/${day.year}: ${day.existing > 0 ? '+' : ''}${day.existing}`}
onMouseEnter={e => {
e.currentTarget.style.opacity = '0.7';
}}
onMouseLeave={e => {
e.currentTarget.style.opacity = '1';
}}
/>
{loading ? (
<div className="text-center py-12 text-gray-400">Loading growth history...</div>
) : growthData.length > 0 ? (
<div className="space-y-3">
{growthData.map((month) => (
<div
key={month.month}
className="bg-gray-800 border border-gray-700 rounded-lg p-5"
>
<h3 className="text-lg font-semibold mb-4">{month.month}</h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
<div className="bg-gray-900 p-3 rounded">
<div className="text-xs text-gray-400">Existing</div>
<div className="text-xl font-bold text-blue-400">{month.existing.toLocaleString()}</div>
</div>
);
})}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: '#888' }}>
<div>{growthHistory.length > 0 ? `${growthHistory[growthHistory.length - 1].month}/${growthHistory[growthHistory.length - 1].day}` : ''}</div>
<div>{growthHistory.length > 0 ? `${growthHistory[0].month}/${growthHistory[0].day}` : ''}</div>
</div>
<div style={{ marginTop: 16, textAlign: 'center', fontSize: 12, color: '#888' }}>
<span style={{ marginRight: 16 }}>
<span style={{ display: 'inline-block', width: 12, height: 12, background: '#66bb6a', borderRadius: 2, marginRight: 4 }}></span>
Growth
</span>
<span>
<span style={{ display: 'inline-block', width: 12, height: 12, background: '#ef5350', borderRadius: 2, marginRight: 4 }}></span>
Loss
</span>
</div>
</div>
)}
{growthHistory.length === 0 && (
<div style={{
textAlign: 'center',
padding: 80,
color: '#888',
background: '#1e1e1e',
borderRadius: 8,
border: '1px solid #333'
}}>
<p>No growth history available for this list</p>
<div className="bg-gray-900 p-3 rounded">
<div className="text-xs text-gray-400">Imports</div>
<div className="text-xl font-bold text-green-400">+{month.imports.toLocaleString()}</div>
</div>
<div className="bg-gray-900 p-3 rounded">
<div className="text-xs text-gray-400">Opt-ins</div>
<div className="text-xl font-bold text-teal-400">+{month.optins.toLocaleString()}</div>
</div>
<div className="bg-gray-900 p-3 rounded">
<div className="text-xs text-gray-400">Opt-outs</div>
<div className="text-xl font-bold text-red-400">-{month.optouts.toLocaleString()}</div>
</div>
<div className="bg-gray-900 p-3 rounded">
<div className="text-xs text-gray-400">Cleaned</div>
<div className="text-xl font-bold text-yellow-400">-{month.cleaned.toLocaleString()}</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12 text-gray-500">Enter a list ID to view growth history</div>
)}
</div>
</div>
);
}
const root = createRoot(document.getElementById('root')!);
root.render(<App />);

View File

@ -1,12 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Growth Chart</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/app.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>growth-chart - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,21 @@
import React, { useState } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
export default function App() {
const [data, setData] = useState<any>(null);
const client = createMCPClient();
const appName = window.location.pathname.split('/').pop() || 'App';
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</h1>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<p className="text-gray-400">Loading...</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>landing-page-builder - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,21 @@
import React, { useState } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
export default function App() {
const [data, setData] = useState<any>(null);
const client = createMCPClient();
const appName = window.location.pathname.split('/').pop() || 'App';
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</h1>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<p className="text-gray-400">Loading...</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>list-health-monitor - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,21 @@
import React, { useState } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
export default function App() {
const [data, setData] = useState<any>(null);
const client = createMCPClient();
const appName = window.location.pathname.split('/').pop() || 'App';
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</h1>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<p className="text-gray-400">Loading...</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>promo-code-manager - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,162 @@
import React, { useState, useEffect } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
interface Segment {
id: string;
name: string;
type: string;
member_count: number;
created_at: string;
updated_at: string;
}
export default function SegmentBuilder() {
const [segments, setSegments] = useState<Segment[]>([]);
const [listId, setListId] = useState('');
const [loading, setLoading] = useState(false);
const [showCreateForm, setShowCreateForm] = useState(false);
const [newSegmentName, setNewSegmentName] = useState('');
const client = createMCPClient();
const loadSegments = async () => {
if (!listId) return;
setLoading(true);
try {
const result = await client.callTool('mailchimp_lists_get_segments', {
list_id: listId,
count: 100
});
setSegments(result.segments || []);
} catch (error) {
console.error('Failed to load segments:', error);
} finally {
setLoading(false);
}
};
const createSegment = async () => {
if (!listId || !newSegmentName) return;
try {
await client.callTool('mailchimp_lists_create_segment', {
list_id: listId,
name: newSegmentName,
static_segment: []
});
setNewSegmentName('');
setShowCreateForm(false);
loadSegments();
} catch (error: any) {
alert('Failed to create segment: ' + error.message);
}
};
const typeColors: Record<string, string> = {
saved: 'bg-blue-600',
static: 'bg-green-600',
fuzzy: 'bg-purple-600'
};
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-8">
<h1 className="text-4xl font-bold bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent">
Segment Builder
</h1>
<button
onClick={() => setShowCreateForm(!showCreateForm)}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg transition"
>
Create Segment
</button>
</div>
{/* Create Form */}
{showCreateForm && (
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6 mb-6">
<h3 className="text-lg font-semibold mb-4">Create New Segment</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<input
type="text"
placeholder="Segment Name"
value={newSegmentName}
onChange={(e) => setNewSegmentName(e.target.value)}
className="px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:ring-2 focus:ring-purple-500 outline-none"
/>
</div>
<div className="flex gap-2">
<button
onClick={createSegment}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg transition"
>
Create
</button>
<button
onClick={() => setShowCreateForm(false)}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition"
>
Cancel
</button>
</div>
</div>
)}
{/* List ID Input */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<input
type="text"
placeholder="List ID"
value={listId}
onChange={(e) => setListId(e.target.value)}
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-purple-500 outline-none"
/>
<button
onClick={loadSegments}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition"
>
Load Segments
</button>
</div>
{/* Segments Grid */}
{loading ? (
<div className="text-center py-12 text-gray-400">Loading segments...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{segments.map((segment) => (
<div
key={segment.id}
className="bg-gray-800 border border-gray-700 rounded-lg p-5 hover:border-purple-500 transition"
>
<div className="flex items-start justify-between mb-4">
<h3 className="text-lg font-semibold">{segment.name}</h3>
<span className={`px-2 py-1 rounded text-xs ${typeColors[segment.type]}`}>
{segment.type}
</span>
</div>
<div className="bg-gray-900 p-4 rounded mb-4">
<div className="text-3xl font-bold text-purple-400">
{segment.member_count.toLocaleString()}
</div>
<div className="text-sm text-gray-400">Members</div>
</div>
<div className="text-xs text-gray-500 mb-4">
Created: {new Date(segment.created_at).toLocaleDateString()}
</div>
<div className="flex gap-2">
<button className="flex-1 px-3 py-2 bg-purple-600 hover:bg-purple-700 rounded text-sm transition">
Edit
</button>
<button className="flex-1 px-3 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm transition">
View
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Segment Builder - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,21 @@
import React, { useState } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
export default function App() {
const [data, setData] = useState<any>(null);
const client = createMCPClient();
const appName = window.location.pathname.split('/').pop() || 'App';
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</h1>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<p className="text-gray-400">Loading...</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>send-time-optimizer - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,132 @@
import React, { useState, useEffect } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
interface Member {
id: string;
email_address: string;
status: string;
merge_fields: { FNAME?: string; LNAME?: string };
timestamp_opt?: string;
vip: boolean;
}
export default function SubscriberGrid() {
const [members, setMembers] = useState<Member[]>([]);
const [listId, setListId] = useState('');
const [loading, setLoading] = useState(false);
const [statusFilter, setStatusFilter] = useState('subscribed');
const client = createMCPClient();
const loadMembers = async () => {
if (!listId) return;
setLoading(true);
try {
const result = await client.callTool('mailchimp_members_list', {
list_id: listId,
count: 100,
status: statusFilter
});
setMembers(result.members || []);
} catch (error) {
console.error('Failed to load members:', error);
} finally {
setLoading(false);
}
};
const statusColors: Record<string, string> = {
subscribed: 'bg-green-600',
unsubscribed: 'bg-red-600',
cleaned: 'bg-yellow-600',
pending: 'bg-blue-600'
};
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
Subscriber Grid
</h1>
{/* Filters */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<input
type="text"
placeholder="List ID"
value={listId}
onChange={(e) => setListId(e.target.value)}
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
/>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
>
<option value="subscribed">Subscribed</option>
<option value="unsubscribed">Unsubscribed</option>
<option value="cleaned">Cleaned</option>
<option value="pending">Pending</option>
</select>
<button
onClick={loadMembers}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition"
>
Load Subscribers
</button>
</div>
{/* Stats */}
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700 mb-6">
<div className="text-sm text-gray-400">
Showing {members.length} {statusFilter} subscribers
</div>
</div>
{/* Grid */}
{loading ? (
<div className="text-center py-12 text-gray-400">Loading subscribers...</div>
) : (
<div className="bg-gray-800 border border-gray-700 rounded-lg overflow-hidden">
<table className="w-full">
<thead className="bg-gray-900 border-b border-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Email</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">VIP</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Subscribed</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
{members.map((member) => (
<tr key={member.id} className="hover:bg-gray-750">
<td className="px-6 py-4 text-sm">{member.email_address}</td>
<td className="px-6 py-4 text-sm">
{member.merge_fields.FNAME} {member.merge_fields.LNAME}
</td>
<td className="px-6 py-4">
<span className={`px-2 py-1 rounded text-xs ${statusColors[member.status]}`}>
{member.status}
</span>
</td>
<td className="px-6 py-4 text-sm">
{member.vip ? '⭐' : '—'}
</td>
<td className="px-6 py-4 text-sm text-gray-400">
{member.timestamp_opt ? new Date(member.timestamp_opt).toLocaleDateString() : '—'}
</td>
<td className="px-6 py-4">
<button className="text-blue-400 hover:text-blue-300 text-sm">Edit</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Subscriber Grid - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -1,135 +1,21 @@
import React, { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { useApp } from '@modelcontextprotocol/ext-apps/react';
import React, { useState } from 'react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
function App() {
const app = useApp();
const [searchQuery, setSearchQuery] = useState('');
const [tags, setTags] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
async function searchTags() {
if (!searchQuery.trim()) return;
try {
setLoading(true);
const result = await app.callTool('mailchimp_tags_search', { name: searchQuery });
const data = JSON.parse(result.content[0].text);
setTags(data.tags || []);
} catch (err: any) {
console.error('Failed to search tags:', err);
setTags([]);
} finally {
setLoading(false);
}
}
useEffect(() => {
const timer = setTimeout(() => {
if (searchQuery) searchTags();
}, 500);
return () => clearTimeout(timer);
}, [searchQuery]);
export default function App() {
const [data, setData] = useState<any>(null);
const client = createMCPClient();
const appName = window.location.pathname.split('/').pop() || 'App';
return (
<div style={{ fontFamily: 'system-ui, sans-serif', background: '#121212', minHeight: '100vh', color: '#e0e0e0', padding: 20 }}>
<div style={{ maxWidth: 1000, margin: '0 auto' }}>
<h1 style={{ marginBottom: 24 }}>🏷 Tag Manager</h1>
{/* Search Bar */}
<div style={{ marginBottom: 32 }}>
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Search tags..."
style={{
width: '100%',
padding: 16,
background: '#2a2a2a',
color: '#e0e0e0',
border: '1px solid #444',
borderRadius: 8,
fontSize: 16
}}
/>
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</h1>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
<p className="text-gray-400">Loading...</p>
</div>
{loading && (
<div style={{ textAlign: 'center', padding: 40, color: '#888' }}>
<p>Searching...</p>
</div>
)}
{!loading && tags.length > 0 && (
<div>
<div style={{ marginBottom: 16, fontSize: 14, color: '#888' }}>
Found {tags.length} tag{tags.length !== 1 ? 's' : ''}
</div>
<div style={{ display: 'grid', gap: 12 }}>
{tags.map((tag, idx) => (
<div
key={idx}
style={{
background: '#1e1e1e',
border: '1px solid #333',
borderRadius: 8,
padding: 20,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<div>
<div style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 8 }}>
🏷 {tag.name}
</div>
{tag.list_id && (
<div style={{ fontSize: 12, color: '#888' }}>
List ID: {tag.list_id}
</div>
)}
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: 32, fontWeight: 'bold', color: '#42a5f5' }}>
{tag.member_count || 0}
</div>
<div style={{ fontSize: 12, color: '#888' }}>members</div>
</div>
</div>
))}
</div>
</div>
)}
{!loading && searchQuery && tags.length === 0 && (
<div style={{ textAlign: 'center', padding: 60, color: '#888' }}>
<p>No tags found matching "{searchQuery}"</p>
</div>
)}
{!loading && !searchQuery && (
<div style={{
textAlign: 'center',
padding: 80,
color: '#888',
background: '#1e1e1e',
borderRadius: 8,
border: '1px solid #333'
}}>
<div style={{ fontSize: 48, marginBottom: 16, opacity: 0.5 }}>🏷</div>
<p style={{ fontSize: 16 }}>Enter a tag name to search</p>
<p style={{ fontSize: 14, marginTop: 8 }}>
Tags help you organize and segment your audience
</p>
</div>
)}
</div>
</div>
);
}
const root = createRoot(document.getElementById('root')!);
root.render(<App />);

View File

@ -1,12 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tag Manager</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/app.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>tag-manager - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -1,183 +1,93 @@
import React, { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { useApp } from '@modelcontextprotocol/ext-apps/react';
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
function App() {
const app = useApp();
const [templates, setTemplates] = useState<any[]>([]);
const [selectedTemplate, setSelectedTemplate] = useState<any>(null);
interface Template {
id: string;
name: string;
type: string;
category?: string;
date_created: string;
active: boolean;
thumbnail?: string;
}
export default function TemplateGallery() {
const [templates, setTemplates] = useState<Template[]>([]);
const [loading, setLoading] = useState(true);
const [typeFilter, setTypeFilter] = useState('all');
const client = createMCPClient();
useEffect(() => {
loadTemplates();
}, []);
}, [typeFilter]);
async function loadTemplates() {
const loadTemplates = async () => {
setLoading(true);
try {
setLoading(true);
const result = await app.callTool('mailchimp_templates_list', { count: 100 });
const data = JSON.parse(result.content[0].text);
setTemplates(data.templates || []);
} catch (err: any) {
console.error('Failed to load templates:', err);
const params: any = { count: 100, sort_field: 'date_created', sort_dir: 'DESC' };
if (typeFilter !== 'all') params.type = typeFilter;
const result = await client.callTool('mailchimp_templates_list', params);
setTemplates(result.templates || []);
} catch (error) {
console.error('Failed to load templates:', error);
} finally {
setLoading(false);
}
}
async function viewTemplate(template: any) {
setSelectedTemplate(template);
}
if (loading) {
return (
<div style={{ padding: 40, textAlign: 'center', background: '#121212', minHeight: '100vh', color: '#e0e0e0' }}>
<p>Loading templates...</p>
</div>
);
}
if (selectedTemplate) {
return (
<div style={{ fontFamily: 'system-ui, sans-serif', background: '#121212', minHeight: '100vh', color: '#e0e0e0', padding: 20 }}>
<div style={{ maxWidth: 1000, margin: '0 auto' }}>
<button onClick={() => setSelectedTemplate(null)} style={{
padding: '8px 16px',
background: '#2a2a2a',
color: '#e0e0e0',
border: '1px solid #444',
borderRadius: 4,
cursor: 'pointer',
marginBottom: 20
}}>
Back to Gallery
</button>
<h1 style={{ marginBottom: 8 }}>{selectedTemplate.name}</h1>
<div style={{ fontSize: 14, color: '#888', marginBottom: 24 }}>
ID: {selectedTemplate.id} Created: {new Date(selectedTemplate.date_created).toLocaleDateString()}
</div>
<div style={{ background: '#1e1e1e', padding: 24, borderRadius: 8, border: '1px solid #333' }}>
<div style={{ marginBottom: 16 }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Type</div>
<div style={{ fontSize: 16, textTransform: 'capitalize' }}>{selectedTemplate.type}</div>
</div>
{selectedTemplate.category && (
<div style={{ marginBottom: 16 }}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Category</div>
<div style={{ fontSize: 16 }}>{selectedTemplate.category}</div>
</div>
)}
<div>
<div style={{ fontSize: 12, color: '#888', marginBottom: 8 }}>Preview</div>
<div style={{
background: '#fff',
color: '#000',
padding: 20,
borderRadius: 4,
minHeight: 200,
fontFamily: 'Arial, sans-serif',
fontSize: 14
}}>
{selectedTemplate.thumbnail ? (
<img src={selectedTemplate.thumbnail} alt={selectedTemplate.name} style={{ maxWidth: '100%' }} />
) : (
<div style={{ textAlign: 'center', padding: 40, color: '#666' }}>
Preview not available
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}
};
return (
<div style={{ fontFamily: 'system-ui, sans-serif', background: '#121212', minHeight: '100vh', color: '#e0e0e0', padding: 20 }}>
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<h1>📄 Template Gallery</h1>
<button onClick={loadTemplates} style={{
padding: '8px 16px',
background: '#1e88e5',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer'
}}>
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-8">
<h1 className="text-4xl font-bold bg-gradient-to-r from-pink-400 to-purple-400 bg-clip-text text-transparent">
Template Gallery
</h1>
<button onClick={loadTemplates} className="px-4 py-2 bg-pink-600 hover:bg-pink-700 rounded-lg transition">
Refresh
</button>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: 20
}}>
{templates.map(template => (
<div
key={template.id}
onClick={() => viewTemplate(template)}
style={{
background: '#1e1e1e',
border: '1px solid #333',
borderRadius: 8,
overflow: 'hidden',
cursor: 'pointer',
transition: 'transform 0.2s, border-color 0.2s'
}}
onMouseEnter={e => {
e.currentTarget.style.transform = 'translateY(-4px)';
e.currentTarget.style.borderColor = '#1e88e5';
}}
onMouseLeave={e => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.borderColor = '#333';
}}
>
<div style={{
background: '#2a2a2a',
height: 180,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderBottom: '1px solid #333'
}}>
{template.thumbnail ? (
<img
src={template.thumbnail}
alt={template.name}
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'cover' }}
/>
) : (
<div style={{ fontSize: 48, opacity: 0.3 }}>📄</div>
)}
</div>
<div style={{ padding: 16 }}>
<h3 style={{ fontSize: 16, marginBottom: 8, fontWeight: 'bold' }}>{template.name}</h3>
<div style={{ fontSize: 12, color: '#888', marginBottom: 8, textTransform: 'capitalize' }}>
{template.type}
</div>
<div style={{ fontSize: 12, color: '#666' }}>
Created {new Date(template.date_created).toLocaleDateString()}
</div>
</div>
</div>
))}
<div className="mb-6">
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-pink-500 outline-none"
>
<option value="all">All Templates</option>
<option value="user">User Templates</option>
<option value="base">Base Templates</option>
<option value="gallery">Gallery Templates</option>
</select>
</div>
{templates.length === 0 && (
<div style={{ textAlign: 'center', padding: 60, color: '#888' }}>
<p>No templates found</p>
{loading ? (
<div className="text-center py-12 text-gray-400">Loading templates...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
{templates.map((template) => (
<div
key={template.id}
className="bg-gray-800 border border-gray-700 rounded-lg overflow-hidden hover:border-pink-500 transition"
>
<div className="h-48 bg-gradient-to-br from-pink-900/20 to-purple-900/20 flex items-center justify-center">
<div className="text-6xl">📧</div>
</div>
<div className="p-4">
<h3 className="font-semibold mb-2 truncate">{template.name}</h3>
<div className="flex items-center justify-between text-xs text-gray-400 mb-3">
<span>{template.type}</span>
<span>{new Date(template.date_created).toLocaleDateString()}</span>
</div>
<button className="w-full px-3 py-2 bg-pink-600 hover:bg-pink-700 rounded text-sm transition">
Use Template
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
const root = createRoot(document.getElementById('root')!);
root.render(<App />);

View File

@ -1,12 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Template Gallery</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/app.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Template Gallery - Mailchimp MCP</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});