pipedrive: Add 20 React MCP Apps

- deal-dashboard: Overview stats, won/lost ratio, revenue forecast
- deal-detail: Full deal with products, activities, participants, timeline
- deal-grid: Sortable deal list with filters
- pipeline-kanban: Drag-drop pipeline board
- pipeline-analytics: Conversion rates, velocity, bottleneck analysis
- pipeline-funnel: Visual funnel with stage metrics
- person-detail: Contact card with deals, activities, files
- person-grid: Contact directory with search
- org-detail: Organization with people, deals, activities
- org-grid: Organization directory
- activity-dashboard: Activity calendar/list with completion tracking
- activity-calendar: Monthly calendar view
- lead-inbox: Lead list with labels and quick actions
- product-catalog: Product list with pricing
- goal-tracker: Goals with progress bars
- revenue-dashboard: Revenue analytics, forecasting
- email-inbox: Mail threads with preview
- deals-timeline: Timeline of deal progression
- search-results: Universal search
- won-deals: Closed-won deals celebration view

All apps use React with dark theme. Self-contained with inline shared components.
Each app has App.tsx, index.html, and vite.config.ts.
Ports 3000-3019 for dev servers.
This commit is contained in:
Jake Shore 2026-02-12 17:09:57 -05:00
parent ff349dc88f
commit ced6b4933b
427 changed files with 42367 additions and 2757 deletions

867
landing-pages/closebot.html Normal file
View File

@ -0,0 +1,867 @@
<!DOCTYPE html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CloseBot Connect — AI-Power Your Chatbots in 2 Clicks</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
colors: {
brand: {
50: '#f5f3ff',
100: '#ede9fe',
200: '#ddd6fe',
300: '#c4b5fd',
400: '#a78bfa',
500: '#7C3AED',
600: '#6D28D9',
700: '#5B21B6',
}
}
}
}
}
</script>
<style>
.gradient-text {
background: linear-gradient(135deg, #7C3AED 0%, #a78bfa 50%, #7C3AED 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-glow {
background: radial-gradient(ellipse 80% 50% at 50% -20%, rgba(124, 58, 237, 0.25), transparent);
}
.card-glow {
transition: all 0.3s ease;
}
.card-glow:hover {
box-shadow: 0 0 50px rgba(124, 58, 237, 0.2);
transform: translateY(-4px);
border-color: rgba(124, 58, 237, 0.5);
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-float-delayed {
animation: float 6s ease-in-out infinite;
animation-delay: -3s;
}
.animate-float-slow {
animation: float 8s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 20px rgba(124, 58, 237, 0.4); }
50% { box-shadow: 0 0 40px rgba(124, 58, 237, 0.6); }
}
.video-glow {
animation: pulse-glow 3s ease-in-out infinite;
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.animated-gradient {
background: linear-gradient(90deg, #7C3AED, #a78bfa, #7C3AED);
background-size: 200% 200%;
animation: gradient-shift 4s ease infinite;
}
@keyframes wiggle {
0%, 100% { transform: rotate(-2deg); }
50% { transform: rotate(2deg); }
}
@keyframes glow-pulse {
0%, 100% { box-shadow: 0 0 20px 0 rgba(124, 58, 237, 0.4), 0 0 40px 0 rgba(124, 58, 237, 0.2); }
50% { box-shadow: 0 0 30px 5px rgba(124, 58, 237, 0.6), 0 0 60px 10px rgba(124, 58, 237, 0.3); }
}
.sticky-btn {
animation: wiggle 2.5s ease-in-out infinite, glow-pulse 2s ease-in-out infinite;
}
.sticky-btn:hover {
animation: none;
}
.feature-icon {
transition: all 0.3s ease;
}
.card-glow:hover .feature-icon {
transform: scale(1.1);
}
.orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.5;
}
.closebot-gradient {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.1) 0%, rgba(167, 139, 250, 0.05) 100%);
}
.gradient-border {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.5), rgba(167, 139, 250, 0.2));
padding: 1px;
border-radius: 1rem;
}
.gradient-border-inner {
background: rgb(24 24 27);
border-radius: calc(1rem - 1px);
}
.app-card {
transition: all 0.3s ease;
}
.app-card:hover {
box-shadow: 0 0 40px rgba(124, 58, 237, 0.15);
transform: translateY(-2px);
border-color: rgba(124, 58, 237, 0.4);
}
</style>
</head>
<body class="bg-zinc-950 text-zinc-100 font-sans antialiased overflow-x-hidden">
<!-- Floating Orbs Background -->
<div class="fixed inset-0 pointer-events-none overflow-hidden">
<div class="orb w-96 h-96 bg-brand-500/30 top-1/4 -left-48 animate-float"></div>
<div class="orb w-64 h-64 bg-violet-400/20 top-1/2 -right-32 animate-float-delayed"></div>
<div class="orb w-72 h-72 bg-purple-500/20 bottom-1/4 left-1/3 animate-float-slow"></div>
</div>
<!-- Nav -->
<nav class="fixed top-0 w-full z-50 border-b border-zinc-800/50 bg-zinc-950/80 backdrop-blur-xl">
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500 to-violet-400 flex items-center justify-center shadow-lg shadow-brand-500/25">
<i data-lucide="bot" class="w-5 h-5 text-white"></i>
</div>
<span class="font-bold text-xl">CloseBot Connect</span>
</div>
<div class="hidden md:flex items-center gap-8">
<a href="#features" class="text-zinc-400 hover:text-white transition">Features</a>
<a href="#demo" class="text-zinc-400 hover:text-white transition">Demo</a>
<a href="#apps" class="text-zinc-400 hover:text-white transition">Apps</a>
<a href="#pricing" class="text-zinc-400 hover:text-white transition">Waitlist</a>
<a href="#faq" class="text-zinc-400 hover:text-white transition">FAQ</a>
</div>
<div class="flex items-center gap-4">
<a href="#" class="text-zinc-400 hover:text-white transition hidden sm:block">Sign In</a>
<a href="#pricing" class="px-5 py-2.5 bg-gradient-to-r from-brand-500 to-violet-500 hover:from-brand-600 hover:to-violet-600 rounded-xl font-medium transition shadow-lg shadow-brand-500/25">
Join Waitlist
</a>
</div>
</div>
</nav>
<!-- Hero -->
<section class="relative min-h-screen flex items-center hero-glow pt-20">
<div class="max-w-6xl mx-auto px-6 py-20 text-center relative z-10">
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-zinc-800/50 border border-zinc-700/50 text-sm text-zinc-300 mb-8 backdrop-blur-sm">
<span class="w-2 h-2 rounded-full bg-brand-400 animate-pulse"></span>
Open Source + Hosted
</div>
<h1 class="text-5xl md:text-7xl font-extrabold tracking-tight mb-6 leading-tight">
Connect <span class="gradient-text">CloseBot</span><br>
to AI in 2 Clicks
</h1>
<p class="text-xl md:text-2xl text-zinc-400 max-w-3xl mx-auto mb-10 leading-relaxed">
The complete CloseBot MCP server. <strong class="text-white">119 tools</strong> for bots, leads, analytics, and automation.
No setup. No API headaches. Just connect and build.
</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16">
<a href="#pricing" class="w-full sm:w-auto px-8 py-4 bg-gradient-to-r from-brand-500 to-violet-500 hover:from-brand-600 hover:to-violet-600 rounded-xl font-semibold text-lg transition transform hover:scale-105 shadow-xl shadow-brand-500/30">
Join the Waitlist
</a>
<a href="#demo" class="w-full sm:w-auto px-8 py-4 bg-zinc-800/80 hover:bg-zinc-700/80 border border-zinc-700 rounded-xl font-semibold text-lg transition flex items-center justify-center gap-2 backdrop-blur-sm">
<i data-lucide="play-circle" class="w-5 h-5"></i>
Watch Demo
</a>
</div>
<!-- Stats Row -->
<div class="flex flex-wrap items-center justify-center gap-8 md:gap-12 mb-12">
<div class="text-center">
<div class="text-3xl md:text-4xl font-extrabold gradient-text">119</div>
<div class="text-zinc-500 text-sm mt-1">AI Tools</div>
</div>
<div class="w-px h-10 bg-zinc-800 hidden md:block"></div>
<div class="text-center">
<div class="text-3xl md:text-4xl font-extrabold gradient-text">14</div>
<div class="text-zinc-500 text-sm mt-1">Lazy Modules</div>
</div>
<div class="w-px h-10 bg-zinc-800 hidden md:block"></div>
<div class="text-center">
<div class="text-3xl md:text-4xl font-extrabold gradient-text">6</div>
<div class="text-zinc-500 text-sm mt-1">Visual Apps</div>
</div>
<div class="w-px h-10 bg-zinc-800 hidden md:block"></div>
<div class="text-center">
<div class="text-3xl md:text-4xl font-extrabold gradient-text">8</div>
<div class="text-zinc-500 text-sm mt-1">Tool Groups</div>
</div>
</div>
<!-- Social Proof -->
<div class="flex flex-col items-center gap-4">
<div class="flex -space-x-3">
<img src="https://i.pravatar.cc/100?img=11" class="w-10 h-10 rounded-full border-2 border-zinc-950 shadow-lg" alt="">
<img src="https://i.pravatar.cc/100?img=12" class="w-10 h-10 rounded-full border-2 border-zinc-950 shadow-lg" alt="">
<img src="https://i.pravatar.cc/100?img=13" class="w-10 h-10 rounded-full border-2 border-zinc-950 shadow-lg" alt="">
<img src="https://i.pravatar.cc/100?img=14" class="w-10 h-10 rounded-full border-2 border-zinc-950 shadow-lg" alt="">
<img src="https://i.pravatar.cc/100?img=15" class="w-10 h-10 rounded-full border-2 border-zinc-950 shadow-lg" alt="">
</div>
<p class="text-zinc-400">
Trusted by <strong class="text-white">500+</strong> chatbot agencies
</p>
</div>
</div>
</section>
<!-- Video Demo -->
<section id="demo" class="py-20 border-t border-zinc-800/50">
<div class="max-w-5xl mx-auto px-6">
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold mb-4">See It In Action</h2>
<p class="text-xl text-zinc-400">Watch how AI manages your CloseBot chatbots</p>
</div>
<div class="gradient-border">
<div class="gradient-border-inner p-2">
<div class="rounded-xl overflow-hidden video-glow">
<video autoplay loop muted playsinline class="w-full">
<source src="output/closebot.mp4" type="video/mp4">
</video>
</div>
</div>
</div>
<div class="flex justify-center mt-8">
<div class="inline-flex items-center gap-6 px-6 py-3 rounded-full bg-zinc-900/80 border border-zinc-800 backdrop-blur-sm">
<div class="flex items-center gap-2">
<i data-lucide="bot" class="w-4 h-4 text-brand-500"></i>
<span class="text-sm text-zinc-300">Bots</span>
</div>
<div class="w-px h-4 bg-zinc-700"></div>
<div class="flex items-center gap-2">
<i data-lucide="users" class="w-4 h-4 text-brand-500"></i>
<span class="text-sm text-zinc-300">Leads</span>
</div>
<div class="w-px h-4 bg-zinc-700"></div>
<div class="flex items-center gap-2">
<i data-lucide="bar-chart-3" class="w-4 h-4 text-brand-500"></i>
<span class="text-sm text-zinc-300">Analytics</span>
</div>
<div class="w-px h-4 bg-zinc-700"></div>
<div class="flex items-center gap-2">
<i data-lucide="flask-conical" class="w-4 h-4 text-brand-500"></i>
<span class="text-sm text-zinc-300">Testing</span>
</div>
</div>
</div>
</div>
</section>
<!-- Problem/Solution -->
<section class="py-24 border-t border-zinc-800/50">
<div class="max-w-6xl mx-auto px-6">
<div class="grid md:grid-cols-2 gap-16 items-center">
<div>
<h2 class="text-3xl md:text-4xl font-bold mb-8 leading-tight">
Managing chatbots at scale<br>
<span class="text-zinc-500">shouldn't be this painful</span>
</h2>
<div class="space-y-6">
<div class="flex items-start gap-4 p-4 rounded-xl bg-red-500/5 border border-red-500/10">
<div class="w-10 h-10 rounded-xl bg-red-500/10 flex items-center justify-center flex-shrink-0">
<i data-lucide="x" class="w-5 h-5 text-red-400"></i>
</div>
<div>
<p class="font-semibold text-lg">Dashboard tab overload</p>
<p class="text-zinc-500">Dozens of bots, sources, and leads spread across endless UI tabs.</p>
</div>
</div>
<div class="flex items-start gap-4 p-4 rounded-xl bg-red-500/5 border border-red-500/10">
<div class="w-10 h-10 rounded-xl bg-red-500/10 flex items-center justify-center flex-shrink-0">
<i data-lucide="x" class="w-5 h-5 text-red-400"></i>
</div>
<div>
<p class="font-semibold text-lg">No visibility into performance</p>
<p class="text-zinc-500">Scattered analytics with no unified view of bookings, responses, or revenue.</p>
</div>
</div>
<div class="flex items-start gap-4 p-4 rounded-xl bg-red-500/5 border border-red-500/10">
<div class="w-10 h-10 rounded-xl bg-red-500/10 flex items-center justify-center flex-shrink-0">
<i data-lucide="x" class="w-5 h-5 text-red-400"></i>
</div>
<div>
<p class="font-semibold text-lg">Manual testing and iteration</p>
<p class="text-zinc-500">Hours spent clicking through test conversations and tweaking configs.</p>
</div>
</div>
</div>
</div>
<div class="closebot-gradient rounded-3xl border border-brand-500/20 p-8 shadow-2xl">
<div class="flex items-center gap-3 mb-8">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-brand-500 to-violet-500 flex items-center justify-center shadow-lg shadow-brand-500/25">
<i data-lucide="check" class="w-6 h-6 text-white"></i>
</div>
<h3 class="text-2xl font-bold">With CloseBot Connect</h3>
</div>
<div class="space-y-5 text-lg">
<div class="flex items-center gap-3 text-zinc-300">
<div class="w-6 h-6 rounded-full bg-brand-500/20 flex items-center justify-center">
<i data-lucide="check" class="w-4 h-4 text-brand-400"></i>
</div>
Manage all bots with natural language
</div>
<div class="flex items-center gap-3 text-zinc-300">
<div class="w-6 h-6 rounded-full bg-brand-500/20 flex items-center justify-center">
<i data-lucide="check" class="w-4 h-4 text-brand-400"></i>
</div>
Unified analytics across every source
</div>
<div class="flex items-center gap-3 text-zinc-300">
<div class="w-6 h-6 rounded-full bg-brand-500/20 flex items-center justify-center">
<i data-lucide="check" class="w-4 h-4 text-brand-400"></i>
</div>
AI-powered bot testing and iteration
</div>
<div class="flex items-center gap-3 text-zinc-300">
<div class="w-6 h-6 rounded-full bg-brand-500/20 flex items-center justify-center">
<i data-lucide="check" class="w-4 h-4 text-brand-400"></i>
</div>
Works with Claude, GPT, any MCP client
</div>
<div class="flex items-center gap-3 text-zinc-300">
<div class="w-6 h-6 rounded-full bg-brand-500/20 flex items-center justify-center">
<i data-lucide="check" class="w-4 h-4 text-brand-400"></i>
</div>
119 tools, zero configuration
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Features / Tool Groups -->
<section id="features" class="py-24 border-t border-zinc-800/50 relative">
<div class="max-w-6xl mx-auto px-6">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold mb-4">119 Tools. 8 Powerful Groups.</h2>
<p class="text-xl text-zinc-400">Full CloseBot platform access through one simple connection</p>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Bot Management -->
<div class="bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 card-glow backdrop-blur-sm">
<div class="feature-icon w-14 h-14 rounded-2xl bg-gradient-to-br from-brand-500/20 to-violet-500/20 flex items-center justify-center mb-5 border border-brand-500/20">
<i data-lucide="bot" class="w-7 h-7 text-brand-400"></i>
</div>
<h3 class="text-xl font-bold mb-2">Bot Management</h3>
<span class="inline-block px-2 py-0.5 bg-brand-500/10 text-brand-400 text-xs font-semibold rounded-full mb-3">18 tools</span>
<p class="text-zinc-400 leading-relaxed">CRUD bots, AI creation, publish, versioning, templates, source attach.</p>
</div>
<!-- Source Management -->
<div class="bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 card-glow backdrop-blur-sm">
<div class="feature-icon w-14 h-14 rounded-2xl bg-gradient-to-br from-purple-500/20 to-pink-500/20 flex items-center justify-center mb-5 border border-purple-500/20">
<i data-lucide="plug" class="w-7 h-7 text-purple-400"></i>
</div>
<h3 class="text-xl font-bold mb-2">Source Management</h3>
<span class="inline-block px-2 py-0.5 bg-purple-500/10 text-purple-400 text-xs font-semibold rounded-full mb-3">9 tools</span>
<p class="text-zinc-400 leading-relaxed">Sources (GHL sub-accounts), calendars, channels, fields, tags.</p>
</div>
<!-- Lead Management -->
<div class="bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 card-glow backdrop-blur-sm">
<div class="feature-icon w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-500/20 to-cyan-500/20 flex items-center justify-center mb-5 border border-blue-500/20">
<i data-lucide="user-search" class="w-7 h-7 text-blue-400"></i>
</div>
<h3 class="text-xl font-bold mb-2">Lead Management</h3>
<span class="inline-block px-2 py-0.5 bg-blue-500/10 text-blue-400 text-xs font-semibold rounded-full mb-3">6 tools</span>
<p class="text-zinc-400 leading-relaxed">Search, filter, update leads and lead instances with full context.</p>
</div>
<!-- Analytics & Metrics -->
<div class="bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 card-glow backdrop-blur-sm">
<div class="feature-icon w-14 h-14 rounded-2xl bg-gradient-to-br from-amber-500/20 to-orange-500/20 flex items-center justify-center mb-5 border border-amber-500/20">
<i data-lucide="bar-chart-3" class="w-7 h-7 text-amber-400"></i>
</div>
<h3 class="text-xl font-bold mb-2">Analytics & Metrics</h3>
<span class="inline-block px-2 py-0.5 bg-amber-500/10 text-amber-400 text-xs font-semibold rounded-full mb-3">14 tools</span>
<p class="text-zinc-400 leading-relaxed">Agency summary, booking graphs, leaderboards, message analytics, logs.</p>
</div>
<!-- Bot Testing -->
<div class="bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 card-glow backdrop-blur-sm">
<div class="feature-icon w-14 h-14 rounded-2xl bg-gradient-to-br from-emerald-500/20 to-teal-500/20 flex items-center justify-center mb-5 border border-emerald-500/20">
<i data-lucide="flask-conical" class="w-7 h-7 text-emerald-400"></i>
</div>
<h3 class="text-xl font-bold mb-2">Bot Testing</h3>
<span class="inline-block px-2 py-0.5 bg-emerald-500/10 text-emerald-400 text-xs font-semibold rounded-full mb-3">7 tools</span>
<p class="text-zinc-400 leading-relaxed">Test sessions with send/listen, force-step, rollback. Iterate fast.</p>
</div>
<!-- Library & KB -->
<div class="bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 card-glow backdrop-blur-sm">
<div class="feature-icon w-14 h-14 rounded-2xl bg-gradient-to-br from-rose-500/20 to-red-500/20 flex items-center justify-center mb-5 border border-rose-500/20">
<i data-lucide="book-open" class="w-7 h-7 text-rose-400"></i>
</div>
<h3 class="text-xl font-bold mb-2">Library & KB</h3>
<span class="inline-block px-2 py-0.5 bg-rose-500/10 text-rose-400 text-xs font-semibold rounded-full mb-3">11 tools</span>
<p class="text-zinc-400 leading-relaxed">Files, web-scraping, source attachment, content management.</p>
</div>
<!-- Agency & Billing -->
<div class="bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 card-glow backdrop-blur-sm">
<div class="feature-icon w-14 h-14 rounded-2xl bg-gradient-to-br from-indigo-500/20 to-blue-500/20 flex items-center justify-center mb-5 border border-indigo-500/20">
<i data-lucide="credit-card" class="w-7 h-7 text-indigo-400"></i>
</div>
<h3 class="text-xl font-bold mb-2">Agency & Billing</h3>
<span class="inline-block px-2 py-0.5 bg-indigo-500/10 text-indigo-400 text-xs font-semibold rounded-full mb-3">18 tools</span>
<p class="text-zinc-400 leading-relaxed">Billing, transactions, wallets, usage tracking, re-billing for agencies.</p>
</div>
<!-- Configuration -->
<div class="bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 card-glow backdrop-blur-sm">
<div class="feature-icon w-14 h-14 rounded-2xl bg-gradient-to-br from-slate-500/20 to-gray-500/20 flex items-center justify-center mb-5 border border-slate-500/20">
<i data-lucide="settings" class="w-7 h-7 text-slate-400"></i>
</div>
<h3 class="text-xl font-bold mb-2">Configuration</h3>
<span class="inline-block px-2 py-0.5 bg-slate-500/10 text-slate-400 text-xs font-semibold rounded-full mb-3">30 tools</span>
<p class="text-zinc-400 leading-relaxed">Personas, FAQs, folders, notifications, live demos, webhooks, API keys.</p>
</div>
</div>
<div class="mt-16 text-center">
<p class="text-zinc-400 mb-6">Full platform coverage including:</p>
<div class="flex flex-wrap justify-center gap-3">
<span class="px-4 py-2 bg-zinc-800/80 rounded-full text-sm border border-zinc-700/50 hover:border-brand-500/50 transition cursor-default">Bot Versioning</span>
<span class="px-4 py-2 bg-zinc-800/80 rounded-full text-sm border border-zinc-700/50 hover:border-brand-500/50 transition cursor-default">AI Bot Creation</span>
<span class="px-4 py-2 bg-zinc-800/80 rounded-full text-sm border border-zinc-700/50 hover:border-brand-500/50 transition cursor-default">Leaderboards</span>
<span class="px-4 py-2 bg-zinc-800/80 rounded-full text-sm border border-zinc-700/50 hover:border-brand-500/50 transition cursor-default">Web Scraping</span>
<span class="px-4 py-2 bg-zinc-800/80 rounded-full text-sm border border-zinc-700/50 hover:border-brand-500/50 transition cursor-default">Webhooks</span>
<span class="px-4 py-2 bg-zinc-800/80 rounded-full text-sm border border-zinc-700/50 hover:border-brand-500/50 transition cursor-default">Wallets</span>
<span class="px-4 py-2 bg-zinc-800/80 rounded-full text-sm border border-zinc-700/50 hover:border-brand-500/50 transition cursor-default">Personas</span>
<span class="px-4 py-2 bg-zinc-800/80 rounded-full text-sm border border-zinc-700/50 hover:border-brand-500/50 transition cursor-default">Live Demos</span>
</div>
</div>
</div>
</section>
<!-- Visual Apps Showcase -->
<section id="apps" class="py-24 border-t border-zinc-800/50 relative">
<div class="max-w-6xl mx-auto px-6">
<div class="text-center mb-16">
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-brand-500/20 text-brand-400 text-sm font-medium mb-6 border border-brand-500/30">
<i data-lucide="layout-dashboard" class="w-4 h-4"></i>
Rich UI Tool Apps
</div>
<h2 class="text-3xl md:text-4xl font-bold mb-4">6 Visual Dashboards, Built In</h2>
<p class="text-xl text-zinc-400 max-w-2xl mx-auto">Not just API calls — get rich HTML dashboards rendered directly in your AI client. See your data, don't just read it.</p>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Bot Dashboard -->
<div class="app-card bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 backdrop-blur-sm">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-violet-500/20 flex items-center justify-center border border-brand-500/20">
<i data-lucide="layout-grid" class="w-5 h-5 text-brand-400"></i>
</div>
<h3 class="text-lg font-bold">Bot Dashboard</h3>
</div>
<p class="text-zinc-400 leading-relaxed text-sm">Grid view of all bots with status indicators, version history, and source count. See everything at a glance.</p>
<div class="mt-4 flex items-center gap-2 text-xs text-brand-400">
<i data-lucide="terminal" class="w-3 h-3"></i>
<code class="bg-brand-500/10 px-2 py-0.5 rounded">bot_dashboard_app</code>
</div>
</div>
<!-- Analytics Dashboard -->
<div class="app-card bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 backdrop-blur-sm">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500/20 to-orange-500/20 flex items-center justify-center border border-amber-500/20">
<i data-lucide="trending-up" class="w-5 h-5 text-amber-400"></i>
</div>
<h3 class="text-lg font-bold">Analytics Dashboard</h3>
</div>
<p class="text-zinc-400 leading-relaxed text-sm">Agency stats, response rates, booking metrics, and revenue tracking with customizable time ranges.</p>
<div class="mt-4 flex items-center gap-2 text-xs text-amber-400">
<i data-lucide="terminal" class="w-3 h-3"></i>
<code class="bg-amber-500/10 px-2 py-0.5 rounded">analytics_dashboard_app</code>
</div>
</div>
<!-- Test Console -->
<div class="app-card bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 backdrop-blur-sm">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-500/20 to-teal-500/20 flex items-center justify-center border border-emerald-500/20">
<i data-lucide="terminal-square" class="w-5 h-5 text-emerald-400"></i>
</div>
<h3 class="text-lg font-bold">Test Console</h3>
</div>
<p class="text-zinc-400 leading-relaxed text-sm">Interactive test session viewer with conversation history, step controls, and rollback capability.</p>
<div class="mt-4 flex items-center gap-2 text-xs text-emerald-400">
<i data-lucide="terminal" class="w-3 h-3"></i>
<code class="bg-emerald-500/10 px-2 py-0.5 rounded">test_console_app</code>
</div>
</div>
<!-- Lead Manager -->
<div class="app-card bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 backdrop-blur-sm">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500/20 to-cyan-500/20 flex items-center justify-center border border-blue-500/20">
<i data-lucide="contact" class="w-5 h-5 text-blue-400"></i>
</div>
<h3 class="text-lg font-bold">Lead Manager</h3>
</div>
<p class="text-zinc-400 leading-relaxed text-sm">Searchable lead table with custom fields, conversation data, and engagement history.</p>
<div class="mt-4 flex items-center gap-2 text-xs text-blue-400">
<i data-lucide="terminal" class="w-3 h-3"></i>
<code class="bg-blue-500/10 px-2 py-0.5 rounded">lead_manager_app</code>
</div>
</div>
<!-- Library Manager -->
<div class="app-card bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 backdrop-blur-sm">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-rose-500/20 to-red-500/20 flex items-center justify-center border border-rose-500/20">
<i data-lucide="library" class="w-5 h-5 text-rose-400"></i>
</div>
<h3 class="text-lg font-bold">Library Manager</h3>
</div>
<p class="text-zinc-400 leading-relaxed text-sm">File list with type indicators, source assignments, and web scrape status tracking.</p>
<div class="mt-4 flex items-center gap-2 text-xs text-rose-400">
<i data-lucide="terminal" class="w-3 h-3"></i>
<code class="bg-rose-500/10 px-2 py-0.5 rounded">library_manager_app</code>
</div>
</div>
<!-- Leaderboard -->
<div class="app-card bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800/80 p-6 backdrop-blur-sm">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500/20 to-purple-500/20 flex items-center justify-center border border-indigo-500/20">
<i data-lucide="trophy" class="w-5 h-5 text-indigo-400"></i>
</div>
<h3 class="text-lg font-bold">Leaderboard</h3>
</div>
<p class="text-zinc-400 leading-relaxed text-sm">Global and local rankings by responses, bookings, or contacts. Gamify your agency performance.</p>
<div class="mt-4 flex items-center gap-2 text-xs text-indigo-400">
<i data-lucide="terminal" class="w-3 h-3"></i>
<code class="bg-indigo-500/10 px-2 py-0.5 rounded">leaderboard_app</code>
</div>
</div>
</div>
</div>
</section>
<!-- Waitlist -->
<section id="pricing" class="py-24 border-t border-zinc-800/50">
<div class="max-w-2xl mx-auto px-6">
<div class="text-center mb-12">
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-brand-500/20 text-brand-400 text-sm font-medium mb-6 border border-brand-500/30">
<i data-lucide="sparkles" class="w-4 h-4"></i>
Coming Soon
</div>
<h2 class="text-3xl md:text-4xl font-bold mb-4">Join the Waitlist</h2>
<p class="text-xl text-zinc-400">Be the first to know when we launch. Early access + exclusive perks for waitlist members.</p>
</div>
<div class="closebot-gradient rounded-3xl border border-brand-500/20 p-8 shadow-2xl">
<form id="waitlist-form" class="space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-zinc-300 mb-2">Name <span class="text-brand-400">*</span></label>
<input
type="text"
id="name"
name="name"
required
placeholder="Your full name"
class="w-full px-4 py-3.5 bg-zinc-800/80 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 transition"
>
</div>
<div>
<label for="phone" class="block text-sm font-medium text-zinc-300 mb-2">Phone <span class="text-brand-400">*</span></label>
<input
type="tel"
id="phone"
name="phone"
required
placeholder="+1 (555) 000-0000"
class="w-full px-4 py-3.5 bg-zinc-800/80 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 transition"
>
</div>
<div>
<label for="email" class="block text-sm font-medium text-zinc-300 mb-2">Email</label>
<input
type="email"
id="email"
name="email"
placeholder="you@agency.com"
class="w-full px-4 py-3.5 bg-zinc-800/80 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 transition"
>
</div>
<button
type="submit"
id="submit-btn"
class="w-full py-4 bg-gradient-to-r from-brand-500 to-violet-500 hover:from-brand-600 hover:to-violet-600 rounded-xl font-semibold text-lg transition transform hover:scale-[1.02] flex items-center justify-center gap-2 shadow-lg shadow-brand-500/25"
>
<span>Join the Waitlist</span>
<i data-lucide="arrow-right" class="w-5 h-5"></i>
</button>
</form>
<!-- Success Message (hidden by default) -->
<div id="success-message" class="hidden text-center py-8">
<div class="w-16 h-16 bg-brand-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<i data-lucide="check" class="w-8 h-8 text-brand-400"></i>
</div>
<h3 class="text-2xl font-bold mb-2">You're on the list!</h3>
<p class="text-zinc-400">We'll reach out as soon as we're ready for you.</p>
</div>
<p class="text-zinc-500 text-sm text-center mt-6 flex items-center justify-center gap-2">
<i data-lucide="lock" class="w-4 h-4"></i>
We respect your privacy. No spam, ever.
</p>
</div>
</div>
</section>
<script>
document.getElementById('waitlist-form').addEventListener('submit', async function(e) {
e.preventDefault();
const form = this;
const submitBtn = document.getElementById('submit-btn');
const successMsg = document.getElementById('success-message');
submitBtn.disabled = true;
submitBtn.innerHTML = '<span>Submitting...</span><svg class="animate-spin w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
setTimeout(() => {
form.classList.add('hidden');
successMsg.classList.remove('hidden');
}, 1000);
});
</script>
<!-- Open Source -->
<section class="py-24 border-t border-zinc-800/50">
<div class="max-w-6xl mx-auto px-6">
<div class="closebot-gradient rounded-3xl border border-brand-500/20 p-8 md:p-12 shadow-2xl">
<div class="grid md:grid-cols-2 gap-12 items-center">
<div>
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-zinc-800 text-sm mb-6 border border-zinc-700">
<i data-lucide="github" class="w-4 h-4"></i>
Open Source
</div>
<h2 class="text-3xl md:text-4xl font-bold mb-4 leading-tight">
Self-host if you want.<br>
<span class="text-zinc-500">We won't stop you.</span>
</h2>
<p class="text-zinc-400 mb-6 text-lg leading-relaxed">
The entire MCP server is open source. Run it yourself, modify it, contribute back.
The hosted version just saves you the hassle.
</p>
<a href="https://github.com/BusyBee3333/closebot-mcp-2026-complete" target="_blank" rel="noopener" class="inline-flex items-center gap-2 text-brand-400 hover:text-brand-300 font-medium text-lg group">
View on GitHub
<i data-lucide="arrow-right" class="w-5 h-5 group-hover:translate-x-1 transition-transform"></i>
</a>
</div>
<div class="bg-zinc-950 rounded-2xl p-6 font-mono text-sm border border-zinc-800 shadow-xl">
<div class="flex items-center gap-2 text-zinc-500 mb-4">
<span class="w-3 h-3 rounded-full bg-red-500"></span>
<span class="w-3 h-3 rounded-full bg-yellow-500"></span>
<span class="w-3 h-3 rounded-full bg-green-500"></span>
<span class="ml-2">Terminal</span>
</div>
<pre class="text-zinc-300 leading-relaxed"><code><span class="text-zinc-500">$</span> git clone https://github.com/BusyBee3333/closebot-mcp-2026-complete.git
<span class="text-zinc-500">$</span> cd mcp && npm install
<span class="text-zinc-500">$</span> npm run build
<span class="text-zinc-500">$</span> node dist/index.js
<span class="text-brand-400">✓ CloseBot MCP Server running</span>
<span class="text-brand-400">✓ 119 tools loaded (14 modules)</span>
<span class="text-brand-400">✓ 6 visual apps ready</span>
<span class="text-zinc-500">Listening on stdio...</span></code></pre>
</div>
</div>
</div>
</div>
</section>
<!-- FAQ -->
<section id="faq" class="py-24 border-t border-zinc-800/50">
<div class="max-w-3xl mx-auto px-6">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold mb-4">Frequently asked questions</h2>
</div>
<div class="space-y-4">
<details class="group bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800 p-6 backdrop-blur-sm">
<summary class="flex items-center justify-between cursor-pointer list-none">
<span class="font-semibold text-lg">What is MCP?</span>
<i data-lucide="chevron-down" class="w-5 h-5 text-zinc-400 group-open:rotate-180 transition"></i>
</summary>
<p class="mt-4 text-zinc-400 leading-relaxed">
MCP (Model Context Protocol) is a standard for connecting AI assistants to external tools and data.
It's supported by Claude, and lets AI actually take actions in your systems — not just chat about them.
</p>
</details>
<details class="group bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800 p-6 backdrop-blur-sm">
<summary class="flex items-center justify-between cursor-pointer list-none">
<span class="font-semibold text-lg">What is CloseBot?</span>
<i data-lucide="chevron-down" class="w-5 h-5 text-zinc-400 group-open:rotate-180 transition"></i>
</summary>
<p class="mt-4 text-zinc-400 leading-relaxed">
CloseBot is an AI chatbot platform at <a href="https://closebot.com" class="text-brand-400 hover:underline">closebot.com</a>.
It lets agencies build, deploy, and manage AI chatbots for lead qualification, booking, and customer support — often integrated with GoHighLevel.
</p>
</details>
<details class="group bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800 p-6 backdrop-blur-sm">
<summary class="flex items-center justify-between cursor-pointer list-none">
<span class="font-semibold text-lg">Do I need to install anything?</span>
<i data-lucide="chevron-down" class="w-5 h-5 text-zinc-400 group-open:rotate-180 transition"></i>
</summary>
<p class="mt-4 text-zinc-400 leading-relaxed">
For the hosted version, no. Just connect your CloseBot account via API key and add the MCP endpoint to your AI client (Claude Desktop, etc.).
If you self-host, you'll need Node.js.
</p>
</details>
<details class="group bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800 p-6 backdrop-blur-sm">
<summary class="flex items-center justify-between cursor-pointer list-none">
<span class="font-semibold text-lg">What are visual apps?</span>
<i data-lucide="chevron-down" class="w-5 h-5 text-zinc-400 group-open:rotate-180 transition"></i>
</summary>
<p class="mt-4 text-zinc-400 leading-relaxed">
Visual apps are rich HTML dashboards rendered directly in your AI client. Instead of reading raw JSON data,
you see beautiful grids, charts, and tables — like a Bot Dashboard with status cards or an Analytics Dashboard with graphs.
</p>
</details>
<details class="group bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800 p-6 backdrop-blur-sm">
<summary class="flex items-center justify-between cursor-pointer list-none">
<span class="font-semibold text-lg">Can AI create and deploy bots for me?</span>
<i data-lucide="chevron-down" class="w-5 h-5 text-zinc-400 group-open:rotate-180 transition"></i>
</summary>
<p class="mt-4 text-zinc-400 leading-relaxed">
Yes. AI can create bots from templates, configure personas and FAQs, attach knowledge base files,
test conversations, and publish — all through natural language. You stay in control with approval steps.
</p>
</details>
<details class="group bg-gradient-to-br from-zinc-900/80 to-zinc-900/40 rounded-2xl border border-zinc-800 p-6 backdrop-blur-sm">
<summary class="flex items-center justify-between cursor-pointer list-none">
<span class="font-semibold text-lg">Is my data secure?</span>
<i data-lucide="chevron-down" class="w-5 h-5 text-zinc-400 group-open:rotate-180 transition"></i>
</summary>
<p class="mt-4 text-zinc-400 leading-relaxed">
Yes. We never store your CloseBot API keys in plain text. Tokens are encrypted at rest and in transit.
You can revoke access anytime from your CloseBot settings.
</p>
</details>
</div>
</div>
</section>
<!-- CTA -->
<section class="py-24 border-t border-zinc-800/50 relative overflow-hidden">
<div class="absolute inset-0 hero-glow"></div>
<div class="max-w-4xl mx-auto px-6 text-center relative z-10">
<h2 class="text-3xl md:text-5xl font-bold mb-6">
Ready to AI-power your CloseBot?
</h2>
<p class="text-xl text-zinc-400 mb-10">
Join 500+ chatbot agencies already automating with CloseBot Connect.
</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
<a href="#pricing" class="w-full sm:w-auto px-8 py-4 bg-gradient-to-r from-brand-500 to-violet-500 hover:from-brand-600 hover:to-violet-600 rounded-xl font-semibold text-lg transition shadow-xl shadow-brand-500/30">
Join the Waitlist
</a>
<a href="#demo" class="w-full sm:w-auto px-8 py-4 bg-zinc-800/80 hover:bg-zinc-700/80 rounded-xl font-semibold text-lg transition border border-zinc-700">
Watch Demo
</a>
</div>
</div>
</section>
<!-- Footer -->
<footer class="border-t border-zinc-800/50 py-12">
<div class="max-w-6xl mx-auto px-6">
<div class="flex flex-col md:flex-row items-center justify-between gap-6">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500 to-violet-500 flex items-center justify-center">
<i data-lucide="bot" class="w-5 h-5 text-white"></i>
</div>
<span class="font-bold text-xl">CloseBot Connect</span>
</div>
<div class="flex items-center gap-8 text-zinc-400">
<a href="#" class="hover:text-white transition">Privacy</a>
<a href="#" class="hover:text-white transition">Terms</a>
<a href="https://github.com/BusyBee3333/closebot-mcp-2026-complete" target="_blank" rel="noopener" class="hover:text-white transition flex items-center gap-2">
<i data-lucide="github" class="w-4 h-4"></i>
GitHub
</a>
</div>
<p class="text-zinc-500 text-sm">© 2026 CloseBot Connect. Not affiliated with CloseBot.</p>
</div>
</div>
</footer>
<!-- Sticky Floating CTA -->
<div id="sticky-cta" class="fixed bottom-6 right-6 z-50 opacity-0 translate-y-4 transition-all duration-300 pointer-events-none">
<a
href="#pricing"
class="sticky-btn flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-brand-500 to-violet-500 rounded-full font-semibold transition-all transform hover:scale-110 shadow-xl"
>
<i data-lucide="sparkles" class="w-5 h-5"></i>
<span>Join Waitlist</span>
</a>
</div>
<script>
const stickyCta = document.getElementById('sticky-cta');
const pricingSection = document.getElementById('pricing');
function updateStickyCta() {
const scrollY = window.scrollY;
const pricingTop = pricingSection.offsetTop;
const pricingBottom = pricingTop + pricingSection.offsetHeight;
const viewportBottom = scrollY + window.innerHeight;
const shouldShow = scrollY > 300 && (viewportBottom < pricingTop || scrollY > pricingBottom);
if (shouldShow) {
stickyCta.classList.remove('opacity-0', 'translate-y-4', 'pointer-events-none');
stickyCta.classList.add('opacity-100', 'translate-y-0', 'pointer-events-auto');
} else {
stickyCta.classList.add('opacity-0', 'translate-y-4', 'pointer-events-none');
stickyCta.classList.remove('opacity-100', 'translate-y-0', 'pointer-events-auto');
}
}
window.addEventListener('scroll', updateStickyCta);
updateStickyCta();
</script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@ -1,20 +0,0 @@
{
"name": "mcp-server-bigcommerce",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.10.0",
"tsx": "^4.7.0",
"typescript": "^5.3.0"
}
}

View File

@ -0,0 +1,547 @@
/**
* BigCommerce API Client
* Supports REST API v2 and v3
* https://api.bigcommerce.com/stores/{store_hash}/
*/
import { BigCommerceConfig, PaginatedResponse, ApiResponse, BigCommerceError } from '../types/index.js';
export class BigCommerceClient {
private storeHash: string;
private accessToken: string;
private baseUrlV2: string;
private baseUrlV3: string;
constructor(config: BigCommerceConfig) {
this.storeHash = config.storeHash;
this.accessToken = config.accessToken;
this.baseUrlV2 = `https://api.bigcommerce.com/stores/${this.storeHash}/v2`;
this.baseUrlV3 = `https://api.bigcommerce.com/stores/${this.storeHash}/v3`;
}
/**
* Make authenticated request to BigCommerce API
*/
private async request<T>(
endpoint: string,
options: RequestInit = {},
version: 'v2' | 'v3' = 'v3'
): Promise<T> {
const baseUrl = version === 'v2' ? this.baseUrlV2 : this.baseUrlV3;
const url = `${baseUrl}${endpoint}`;
const headers: HeadersInit = {
'X-Auth-Token': this.accessToken,
'Content-Type': 'application/json',
'Accept': 'application/json',
...options.headers,
};
try {
const response = await fetch(url, {
...options,
headers,
});
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
const error: BigCommerceError = {
status: response.status,
title: errorBody.title || response.statusText,
type: errorBody.type,
detail: errorBody.detail,
errors: errorBody.errors,
};
throw new Error(`BigCommerce API Error: ${JSON.stringify(error)}`);
}
// Handle 204 No Content
if (response.status === 204) {
return {} as T;
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error(`BigCommerce API request failed: ${String(error)}`);
}
}
/**
* GET request
*/
async get<T>(endpoint: string, version: 'v2' | 'v3' = 'v3'): Promise<T> {
return this.request<T>(endpoint, { method: 'GET' }, version);
}
/**
* POST request
*/
async post<T>(
endpoint: string,
data: any,
version: 'v2' | 'v3' = 'v3'
): Promise<T> {
return this.request<T>(
endpoint,
{
method: 'POST',
body: JSON.stringify(data),
},
version
);
}
/**
* PUT request
*/
async put<T>(
endpoint: string,
data: any,
version: 'v2' | 'v3' = 'v3'
): Promise<T> {
return this.request<T>(
endpoint,
{
method: 'PUT',
body: JSON.stringify(data),
},
version
);
}
/**
* DELETE request
*/
async delete<T>(endpoint: string, version: 'v2' | 'v3' = 'v3'): Promise<T> {
return this.request<T>(endpoint, { method: 'DELETE' }, version);
}
/**
* Paginated GET request
* Automatically handles pagination and returns all results
*/
async getPaginated<T>(
endpoint: string,
version: 'v2' | 'v3' = 'v3',
params: Record<string, any> = {}
): Promise<T[]> {
const allResults: T[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const queryParams = new URLSearchParams({
...params,
page: page.toString(),
limit: '250', // Max limit for most endpoints
});
const url = `${endpoint}?${queryParams.toString()}`;
const response = await this.get<PaginatedResponse<T> | T[]>(url, version);
// Handle v3 paginated response format
if (this.isPaginatedResponse(response)) {
allResults.push(...response.data);
const pagination = response.meta?.pagination;
if (pagination) {
hasMore = pagination.current_page < pagination.total_pages;
page++;
} else {
hasMore = false;
}
}
// Handle v2 array response format
else if (Array.isArray(response)) {
allResults.push(...response);
// For v2, if we get less than the limit, we're done
hasMore = response.length === 250;
page++;
} else {
hasMore = false;
}
}
return allResults;
}
/**
* Type guard for paginated response
*/
private isPaginatedResponse<T>(
response: PaginatedResponse<T> | T[]
): response is PaginatedResponse<T> {
return (response as PaginatedResponse<T>).data !== undefined;
}
/**
* Build query string from params
*/
buildQueryString(params: Record<string, any>): string {
const filtered = Object.entries(params)
.filter(([_, value]) => value !== undefined && value !== null)
.reduce((acc, [key, value]) => {
if (Array.isArray(value)) {
acc[key] = value.join(',');
} else {
acc[key] = String(value);
}
return acc;
}, {} as Record<string, string>);
const queryParams = new URLSearchParams(filtered);
return queryParams.toString() ? `?${queryParams.toString()}` : '';
}
// ============================================================================
// PRODUCTS
// ============================================================================
async listProducts(params?: {
name?: string;
sku?: string;
price?: number;
weight?: number;
condition?: string;
brand_id?: number;
categories?: number[];
is_visible?: boolean;
is_featured?: boolean;
availability?: string;
}) {
const query = this.buildQueryString(params || {});
return this.get<PaginatedResponse<any>>(`/catalog/products${query}`);
}
async getProduct(productId: number) {
return this.get<ApiResponse<any>>(`/catalog/products/${productId}`);
}
async createProduct(product: any) {
return this.post<ApiResponse<any>>('/catalog/products', product);
}
async updateProduct(productId: number, product: any) {
return this.put<ApiResponse<any>>(`/catalog/products/${productId}`, product);
}
async deleteProduct(productId: number) {
return this.delete(`/catalog/products/${productId}`);
}
async listProductVariants(productId: number) {
return this.get<PaginatedResponse<any>>(`/catalog/products/${productId}/variants`);
}
async createProductVariant(productId: number, variant: any) {
return this.post<ApiResponse<any>>(`/catalog/products/${productId}/variants`, variant);
}
async listProductImages(productId: number) {
return this.get<PaginatedResponse<any>>(`/catalog/products/${productId}/images`);
}
async createProductImage(productId: number, image: any) {
return this.post<ApiResponse<any>>(`/catalog/products/${productId}/images`, image);
}
async listProductCustomFields(productId: number) {
return this.get<PaginatedResponse<any>>(`/catalog/products/${productId}/custom-fields`);
}
async listProductBulkPricing(productId: number) {
return this.get<PaginatedResponse<any>>(`/catalog/products/${productId}/bulk-pricing-rules`);
}
// ============================================================================
// ORDERS
// ============================================================================
async listOrders(params?: {
min_id?: number;
max_id?: number;
min_total?: number;
max_total?: number;
customer_id?: number;
status_id?: number;
is_deleted?: boolean;
payment_method?: string;
min_date_created?: string;
max_date_created?: string;
min_date_modified?: string;
max_date_modified?: string;
}) {
const query = this.buildQueryString(params || {});
return this.get<any>(`/orders${query}`, 'v2');
}
async getOrder(orderId: number) {
return this.get<any>(`/orders/${orderId}`, 'v2');
}
async createOrder(order: any) {
return this.post<any>('/orders', order, 'v2');
}
async updateOrder(orderId: number, order: any) {
return this.put<any>(`/orders/${orderId}`, order, 'v2');
}
async getOrderProducts(orderId: number) {
return this.get<any>(`/orders/${orderId}/products`, 'v2');
}
async getOrderShippingAddresses(orderId: number) {
return this.get<any>(`/orders/${orderId}/shipping_addresses`, 'v2');
}
async listOrderShipments(orderId: number) {
return this.get<any>(`/orders/${orderId}/shipments`, 'v2');
}
async createOrderShipment(orderId: number, shipment: any) {
return this.post<any>(`/orders/${orderId}/shipments`, shipment, 'v2');
}
async getOrderRefunds(orderId: number) {
return this.get<PaginatedResponse<any>>(`/orders/${orderId}/payment_actions/refunds`);
}
async createOrderRefund(orderId: number, refund: any) {
return this.post<ApiResponse<any>>(`/orders/${orderId}/payment_actions/refund`, refund);
}
// ============================================================================
// CUSTOMERS
// ============================================================================
async listCustomers(params?: {
company?: string;
first_name?: string;
last_name?: string;
email?: string;
customer_group_id?: number;
date_created?: string;
date_modified?: string;
}) {
const query = this.buildQueryString(params || {});
return this.get<PaginatedResponse<any>>(`/customers${query}`);
}
async getCustomer(customerId: number) {
return this.get<ApiResponse<any>>(`/customers/${customerId}`);
}
async createCustomer(customer: any) {
return this.post<ApiResponse<any>>('/customers', customer);
}
async updateCustomer(customerId: number, customer: any) {
return this.put<ApiResponse<any>>(`/customers/${customerId}`, customer);
}
async deleteCustomer(customerId: number) {
return this.delete(`/customers/${customerId}`);
}
async listCustomerAddresses(customerId: number) {
return this.get<PaginatedResponse<any>>(`/customers/${customerId}/addresses`);
}
async createCustomerAddress(customerId: number, address: any) {
return this.post<ApiResponse<any>>(`/customers/${customerId}/addresses`, address);
}
async listCustomerGroups() {
return this.get<PaginatedResponse<any>>('/customer-groups');
}
// ============================================================================
// CATEGORIES
// ============================================================================
async listCategories(params?: {
name?: string;
parent_id?: number;
is_visible?: boolean;
page_title?: string;
}) {
const query = this.buildQueryString(params || {});
return this.get<PaginatedResponse<any>>(`/catalog/categories${query}`);
}
async getCategory(categoryId: number) {
return this.get<ApiResponse<any>>(`/catalog/categories/${categoryId}`);
}
async createCategory(category: any) {
return this.post<ApiResponse<any>>('/catalog/categories', category);
}
async updateCategory(categoryId: number, category: any) {
return this.put<ApiResponse<any>>(`/catalog/categories/${categoryId}`, category);
}
async deleteCategory(categoryId: number) {
return this.delete(`/catalog/categories/${categoryId}`);
}
async getCategoryTree() {
return this.get<PaginatedResponse<any>>('/catalog/categories/tree');
}
// ============================================================================
// BRANDS
// ============================================================================
async listBrands(params?: {
name?: string;
page_title?: string;
}) {
const query = this.buildQueryString(params || {});
return this.get<PaginatedResponse<any>>(`/catalog/brands${query}`);
}
async getBrand(brandId: number) {
return this.get<ApiResponse<any>>(`/catalog/brands/${brandId}`);
}
async createBrand(brand: any) {
return this.post<ApiResponse<any>>('/catalog/brands', brand);
}
async updateBrand(brandId: number, brand: any) {
return this.put<ApiResponse<any>>(`/catalog/brands/${brandId}`, brand);
}
async deleteBrand(brandId: number) {
return this.delete(`/catalog/brands/${brandId}`);
}
// ============================================================================
// COUPONS
// ============================================================================
async listCoupons(params?: {
code?: string;
name?: string;
type?: string;
}) {
const query = this.buildQueryString(params || {});
return this.get<any>(`/coupons${query}`, 'v2');
}
async getCoupon(couponId: number) {
return this.get<any>(`/coupons/${couponId}`, 'v2');
}
async createCoupon(coupon: any) {
return this.post<any>('/coupons', coupon, 'v2');
}
async updateCoupon(couponId: number, coupon: any) {
return this.put<any>(`/coupons/${couponId}`, coupon, 'v2');
}
async deleteCoupon(couponId: number) {
return this.delete(`/coupons/${couponId}`, 'v2');
}
// ============================================================================
// STORE
// ============================================================================
async getStoreInformation() {
return this.get<ApiResponse<any>>('/store');
}
// ============================================================================
// CHANNELS
// ============================================================================
async listChannels(params?: {
type?: string;
platform?: string;
}) {
const query = this.buildQueryString(params || {});
return this.get<PaginatedResponse<any>>(`/channels${query}`);
}
async getChannel(channelId: number) {
return this.get<ApiResponse<any>>(`/channels/${channelId}`);
}
async listChannelListings(channelId: number, params?: {
product_id?: number;
}) {
const query = this.buildQueryString(params || {});
return this.get<PaginatedResponse<any>>(`/channels/${channelId}/listings${query}`);
}
// ============================================================================
// CARTS
// ============================================================================
async createCart(cart: any) {
return this.post<ApiResponse<any>>('/carts', cart);
}
async getCart(cartId: string) {
return this.get<ApiResponse<any>>(`/carts/${cartId}`);
}
async deleteCart(cartId: string) {
return this.delete(`/carts/${cartId}`);
}
async addCartLineItems(cartId: string, lineItems: any) {
return this.post<ApiResponse<any>>(`/carts/${cartId}/items`, lineItems);
}
async updateCartLineItem(cartId: string, itemId: string, item: any) {
return this.put<ApiResponse<any>>(`/carts/${cartId}/items/${itemId}`, item);
}
// ============================================================================
// CONTENT
// ============================================================================
async listPages() {
return this.get<any>('/content/pages', 'v2');
}
async getPage(pageId: number) {
return this.get<any>(`/content/pages/${pageId}`, 'v2');
}
async createPage(page: any) {
return this.post<any>('/content/pages', page, 'v2');
}
async listBlogPosts() {
return this.get<any>('/blog/posts', 'v2');
}
async createBlogPost(post: any) {
return this.post<any>('/blog/posts', post, 'v2');
}
// ============================================================================
// SHIPPING
// ============================================================================
async listShippingZones() {
return this.get<any>('/shipping/zones', 'v2');
}
async getShippingZone(zoneId: number) {
return this.get<any>(`/shipping/zones/${zoneId}`, 'v2');
}
async listShippingMethods(zoneId: number) {
return this.get<any>(`/shipping/zones/${zoneId}/methods`, 'v2');
}
}

View File

@ -1,413 +0,0 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// ============================================
// BIGCOMMERCE MCP SERVER
// API Docs: https://developer.bigcommerce.com/docs/api
// ============================================
const MCP_NAME = "bigcommerce";
const MCP_VERSION = "1.0.0";
// ============================================
// API CLIENT - OAuth2/API Token Authentication
// ============================================
class BigCommerceClient {
private accessToken: string;
private storeHash: string;
private baseUrlV3: string;
private baseUrlV2: string;
constructor(accessToken: string, storeHash: string) {
this.accessToken = accessToken;
this.storeHash = storeHash;
this.baseUrlV3 = `https://api.bigcommerce.com/stores/${storeHash}/v3`;
this.baseUrlV2 = `https://api.bigcommerce.com/stores/${storeHash}/v2`;
}
async request(url: string, options: RequestInit = {}) {
const response = await fetch(url, {
...options,
headers: {
"X-Auth-Token": this.accessToken,
"Content-Type": "application/json",
"Accept": "application/json",
...options.headers,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`BigCommerce API error: ${response.status} ${response.statusText} - ${errorText}`);
}
// Handle 204 No Content
if (response.status === 204) {
return { success: true };
}
return response.json();
}
async getV3(endpoint: string, params?: Record<string, string>) {
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
return this.request(`${this.baseUrlV3}${endpoint}${queryString}`, { method: "GET" });
}
async getV2(endpoint: string, params?: Record<string, string>) {
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
return this.request(`${this.baseUrlV2}${endpoint}${queryString}`, { method: "GET" });
}
async postV3(endpoint: string, data: any) {
return this.request(`${this.baseUrlV3}${endpoint}`, {
method: "POST",
body: JSON.stringify(data),
});
}
async putV3(endpoint: string, data: any) {
return this.request(`${this.baseUrlV3}${endpoint}`, {
method: "PUT",
body: JSON.stringify(data),
});
}
async putV2(endpoint: string, data: any) {
return this.request(`${this.baseUrlV2}${endpoint}`, {
method: "PUT",
body: JSON.stringify(data),
});
}
}
// ============================================
// TOOL DEFINITIONS
// ============================================
const tools = [
{
name: "list_products",
description: "List products from BigCommerce catalog with filtering and pagination",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Max products to return (default 50, max 250)" },
page: { type: "number", description: "Page number for pagination" },
name: { type: "string", description: "Filter by product name (partial match)" },
sku: { type: "string", description: "Filter by SKU" },
brand_id: { type: "number", description: "Filter by brand ID" },
categories: { type: "string", description: "Filter by category ID(s), comma-separated" },
is_visible: { type: "boolean", description: "Filter by visibility status" },
availability: { type: "string", description: "Filter by availability: available, disabled, preorder" },
include: { type: "string", description: "Sub-resources to include: variants, images, custom_fields, bulk_pricing_rules, primary_image, modifiers, options, videos" },
},
},
},
{
name: "get_product",
description: "Get a specific product by ID with full details",
inputSchema: {
type: "object" as const,
properties: {
product_id: { type: "number", description: "Product ID" },
include: { type: "string", description: "Sub-resources to include: variants, images, custom_fields, bulk_pricing_rules, primary_image, modifiers, options, videos" },
},
required: ["product_id"],
},
},
{
name: "create_product",
description: "Create a new product in BigCommerce catalog",
inputSchema: {
type: "object" as const,
properties: {
name: { type: "string", description: "Product name (required)" },
type: { type: "string", description: "Product type: physical, digital (required)" },
weight: { type: "number", description: "Product weight (required for physical)" },
price: { type: "number", description: "Product price (required)" },
sku: { type: "string", description: "Stock Keeping Unit" },
description: { type: "string", description: "Product description (HTML allowed)" },
categories: { type: "array", description: "Array of category IDs", items: { type: "number" } },
brand_id: { type: "number", description: "Brand ID" },
inventory_level: { type: "number", description: "Current inventory level" },
inventory_tracking: { type: "string", description: "Inventory tracking: none, product, variant" },
is_visible: { type: "boolean", description: "Whether product is visible on storefront" },
availability: { type: "string", description: "Availability: available, disabled, preorder" },
cost_price: { type: "number", description: "Cost price for profit calculations" },
sale_price: { type: "number", description: "Sale price" },
},
required: ["name", "type", "weight", "price"],
},
},
{
name: "update_product",
description: "Update an existing product in BigCommerce",
inputSchema: {
type: "object" as const,
properties: {
product_id: { type: "number", description: "Product ID (required)" },
name: { type: "string", description: "Product name" },
price: { type: "number", description: "Product price" },
sku: { type: "string", description: "Stock Keeping Unit" },
description: { type: "string", description: "Product description" },
categories: { type: "array", description: "Array of category IDs", items: { type: "number" } },
inventory_level: { type: "number", description: "Current inventory level" },
is_visible: { type: "boolean", description: "Whether product is visible" },
availability: { type: "string", description: "Availability: available, disabled, preorder" },
sale_price: { type: "number", description: "Sale price" },
},
required: ["product_id"],
},
},
{
name: "list_orders",
description: "List orders from BigCommerce (V2 API)",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Max orders to return (default 50, max 250)" },
page: { type: "number", description: "Page number for pagination" },
min_date_created: { type: "string", description: "Filter by min creation date (RFC 2822 or ISO 8601)" },
max_date_created: { type: "string", description: "Filter by max creation date" },
status_id: { type: "number", description: "Filter by status ID" },
customer_id: { type: "number", description: "Filter by customer ID" },
min_total: { type: "number", description: "Filter by minimum total" },
max_total: { type: "number", description: "Filter by maximum total" },
is_deleted: { type: "boolean", description: "Include deleted orders" },
sort: { type: "string", description: "Sort field: id, date_created, date_modified, status_id" },
},
},
},
{
name: "get_order",
description: "Get a specific order by ID with full details",
inputSchema: {
type: "object" as const,
properties: {
order_id: { type: "number", description: "Order ID" },
include_products: { type: "boolean", description: "Include order products (separate call)" },
include_shipping: { type: "boolean", description: "Include shipping addresses (separate call)" },
},
required: ["order_id"],
},
},
{
name: "list_customers",
description: "List customers from BigCommerce",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Max customers to return (default 50, max 250)" },
page: { type: "number", description: "Page number for pagination" },
email: { type: "string", description: "Filter by email address" },
name: { type: "string", description: "Filter by name (first or last)" },
company: { type: "string", description: "Filter by company name" },
customer_group_id: { type: "number", description: "Filter by customer group ID" },
date_created_min: { type: "string", description: "Filter by minimum creation date" },
date_created_max: { type: "string", description: "Filter by maximum creation date" },
include: { type: "string", description: "Sub-resources: addresses, storecredit, attributes, formfields" },
},
},
},
{
name: "update_inventory",
description: "Update inventory level for a product or variant",
inputSchema: {
type: "object" as const,
properties: {
product_id: { type: "number", description: "Product ID (required)" },
variant_id: { type: "number", description: "Variant ID (if updating variant inventory)" },
inventory_level: { type: "number", description: "New inventory level (required)" },
inventory_warning_level: { type: "number", description: "Low stock warning threshold" },
},
required: ["product_id", "inventory_level"],
},
},
];
// ============================================
// TOOL HANDLERS
// ============================================
async function handleTool(client: BigCommerceClient, name: string, args: any) {
switch (name) {
case "list_products": {
const params: Record<string, string> = {};
if (args.limit) params.limit = String(args.limit);
if (args.page) params.page = String(args.page);
if (args.name) params['name:like'] = args.name;
if (args.sku) params.sku = args.sku;
if (args.brand_id) params.brand_id = String(args.brand_id);
if (args.categories) params['categories:in'] = args.categories;
if (args.is_visible !== undefined) params.is_visible = String(args.is_visible);
if (args.availability) params.availability = args.availability;
if (args.include) params.include = args.include;
return await client.getV3("/catalog/products", params);
}
case "get_product": {
const params: Record<string, string> = {};
if (args.include) params.include = args.include;
return await client.getV3(`/catalog/products/${args.product_id}`, params);
}
case "create_product": {
const productData: any = {
name: args.name,
type: args.type,
weight: args.weight,
price: args.price,
};
if (args.sku) productData.sku = args.sku;
if (args.description) productData.description = args.description;
if (args.categories) productData.categories = args.categories;
if (args.brand_id) productData.brand_id = args.brand_id;
if (args.inventory_level !== undefined) productData.inventory_level = args.inventory_level;
if (args.inventory_tracking) productData.inventory_tracking = args.inventory_tracking;
if (args.is_visible !== undefined) productData.is_visible = args.is_visible;
if (args.availability) productData.availability = args.availability;
if (args.cost_price !== undefined) productData.cost_price = args.cost_price;
if (args.sale_price !== undefined) productData.sale_price = args.sale_price;
return await client.postV3("/catalog/products", productData);
}
case "update_product": {
const updateData: any = {};
if (args.name) updateData.name = args.name;
if (args.price !== undefined) updateData.price = args.price;
if (args.sku) updateData.sku = args.sku;
if (args.description) updateData.description = args.description;
if (args.categories) updateData.categories = args.categories;
if (args.inventory_level !== undefined) updateData.inventory_level = args.inventory_level;
if (args.is_visible !== undefined) updateData.is_visible = args.is_visible;
if (args.availability) updateData.availability = args.availability;
if (args.sale_price !== undefined) updateData.sale_price = args.sale_price;
return await client.putV3(`/catalog/products/${args.product_id}`, updateData);
}
case "list_orders": {
const params: Record<string, string> = {};
if (args.limit) params.limit = String(args.limit);
if (args.page) params.page = String(args.page);
if (args.min_date_created) params.min_date_created = args.min_date_created;
if (args.max_date_created) params.max_date_created = args.max_date_created;
if (args.status_id) params.status_id = String(args.status_id);
if (args.customer_id) params.customer_id = String(args.customer_id);
if (args.min_total) params.min_total = String(args.min_total);
if (args.max_total) params.max_total = String(args.max_total);
if (args.is_deleted !== undefined) params.is_deleted = String(args.is_deleted);
if (args.sort) params.sort = args.sort;
return await client.getV2("/orders", params);
}
case "get_order": {
const order = await client.getV2(`/orders/${args.order_id}`);
const result: any = { order };
if (args.include_products) {
result.products = await client.getV2(`/orders/${args.order_id}/products`);
}
if (args.include_shipping) {
result.shipping_addresses = await client.getV2(`/orders/${args.order_id}/shipping_addresses`);
}
return result;
}
case "list_customers": {
const params: Record<string, string> = {};
if (args.limit) params.limit = String(args.limit);
if (args.page) params.page = String(args.page);
if (args.email) params['email:in'] = args.email;
if (args.name) params['name:like'] = args.name;
if (args.company) params['company:like'] = args.company;
if (args.customer_group_id) params.customer_group_id = String(args.customer_group_id);
if (args.date_created_min) params['date_created:min'] = args.date_created_min;
if (args.date_created_max) params['date_created:max'] = args.date_created_max;
if (args.include) params.include = args.include;
return await client.getV3("/customers", params);
}
case "update_inventory": {
if (args.variant_id) {
// Update variant inventory
const variantData: any = {
inventory_level: args.inventory_level,
};
if (args.inventory_warning_level !== undefined) {
variantData.inventory_warning_level = args.inventory_warning_level;
}
return await client.putV3(
`/catalog/products/${args.product_id}/variants/${args.variant_id}`,
variantData
);
} else {
// Update product inventory
const productData: any = {
inventory_level: args.inventory_level,
};
if (args.inventory_warning_level !== undefined) {
productData.inventory_warning_level = args.inventory_warning_level;
}
return await client.putV3(`/catalog/products/${args.product_id}`, productData);
}
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// ============================================
// SERVER SETUP
// ============================================
async function main() {
const accessToken = process.env.BIGCOMMERCE_ACCESS_TOKEN;
const storeHash = process.env.BIGCOMMERCE_STORE_HASH;
if (!accessToken) {
console.error("Error: BIGCOMMERCE_ACCESS_TOKEN environment variable required");
process.exit(1);
}
if (!storeHash) {
console.error("Error: BIGCOMMERCE_STORE_HASH environment variable required");
process.exit(1);
}
const client = new BigCommerceClient(accessToken, storeHash);
const server = new Server(
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(client, name, args || {});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${MCP_NAME} MCP server running on stdio`);
}
main().catch(console.error);

View File

@ -0,0 +1,34 @@
#!/usr/bin/env node
/**
* BigCommerce MCP Server Entry Point
*/
import { BigCommerceServer } from './server.js';
// Get configuration from environment variables
const STORE_HASH = process.env.BIGCOMMERCE_STORE_HASH;
const ACCESS_TOKEN = process.env.BIGCOMMERCE_ACCESS_TOKEN;
if (!STORE_HASH || !ACCESS_TOKEN) {
console.error('Error: Missing required environment variables');
console.error('Please set:');
console.error(' BIGCOMMERCE_STORE_HASH - Your BigCommerce store hash');
console.error(' BIGCOMMERCE_ACCESS_TOKEN - Your BigCommerce API access token');
console.error('');
console.error('Example:');
console.error(' export BIGCOMMERCE_STORE_HASH="abc123def"');
console.error(' export BIGCOMMERCE_ACCESS_TOKEN="your-token-here"');
process.exit(1);
}
// Create and run server
const server = new BigCommerceServer({
storeHash: STORE_HASH,
accessToken: ACCESS_TOKEN,
});
server.run().catch((error) => {
console.error('Fatal error running server:', error);
process.exit(1);
});

View File

@ -0,0 +1,192 @@
/**
* BigCommerce MCP Server
* Complete implementation with 60+ tools and 20 React apps
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { BigCommerceClient } from './clients/bigcommerce.js';
import { registerProductsTools } from './tools/products-tools.js';
import { registerOrdersTools } from './tools/orders-tools.js';
import { registerCustomersTools } from './tools/customers-tools.js';
import { registerCategoriesTools } from './tools/categories-tools.js';
import { registerBrandsTools } from './tools/brands-tools.js';
import { registerCouponsTools } from './tools/coupons-tools.js';
import { registerStoreTools } from './tools/store-tools.js';
import { registerChannelsTools } from './tools/channels-tools.js';
import { registerCartsTools } from './tools/carts-tools.js';
import { registerContentTools } from './tools/content-tools.js';
import { registerShippingTools } from './tools/shipping-tools.js';
import { registerAnalyticsTools } from './tools/analytics-tools.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
interface BigCommerceServerConfig {
storeHash: string;
accessToken: string;
}
export class BigCommerceServer {
private server: Server;
private client: BigCommerceClient;
private tools: Map<string, any> = new Map();
constructor(config: BigCommerceServerConfig) {
this.server = new Server(
{
name: 'bigcommerce-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// Initialize BigCommerce client
this.client = new BigCommerceClient({
storeHash: config.storeHash,
accessToken: config.accessToken,
});
// Register all tools
this.registerAllTools();
// Setup request handlers
this.setupHandlers();
}
private registerAllTools() {
const toolSets = [
registerProductsTools(this.client),
registerOrdersTools(this.client),
registerCustomersTools(this.client),
registerCategoriesTools(this.client),
registerBrandsTools(this.client),
registerCouponsTools(this.client),
registerStoreTools(this.client),
registerChannelsTools(this.client),
registerCartsTools(this.client),
registerContentTools(this.client),
registerShippingTools(this.client),
registerAnalyticsTools(this.client),
];
for (const toolSet of toolSets) {
for (const tool of toolSet) {
this.tools.set(tool.name, tool);
}
}
console.error(`Registered ${this.tools.size} BigCommerce tools`);
}
private setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: Array.from(this.tools.values()).map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
});
// Execute tool
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const tool = this.tools.get(request.params.name);
if (!tool) {
throw new Error(`Tool not found: ${request.params.name}`);
}
try {
return await tool.handler(request.params.arguments);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: `Error executing ${request.params.name}: ${errorMessage}`,
},
],
isError: true,
};
}
});
// List React app resources
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const appsDir = path.join(__dirname, 'ui', 'react-app');
try {
const apps = fs.readdirSync(appsDir).filter((file) => {
const appPath = path.join(appsDir, file);
return fs.statSync(appPath).isDirectory();
});
return {
resources: apps.map((app) => ({
uri: `bigcommerce://app/${app}`,
name: app
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' '),
description: `BigCommerce ${app.replace(/-/g, ' ')} React application`,
mimeType: 'text/html',
})),
};
} catch (error) {
return { resources: [] };
}
});
// Read React app resource
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const match = uri.match(/^bigcommerce:\/\/app\/(.+)$/);
if (!match) {
throw new Error(`Invalid resource URI: ${uri}`);
}
const appName = match[1];
const appPath = path.join(__dirname, 'ui', 'react-app', appName, 'App.tsx');
try {
const content = fs.readFileSync(appPath, 'utf-8');
return {
contents: [
{
uri,
mimeType: 'text/html',
text: content,
},
],
};
} catch (error) {
throw new Error(`Failed to read app ${appName}: ${error}`);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('BigCommerce MCP server running on stdio');
}
}

View File

@ -0,0 +1,129 @@
/**
* BigCommerce Analytics Tools
* Note: BigCommerce doesn't have a dedicated analytics API endpoint,
* so these tools aggregate data from orders and products
*/
import { BigCommerceClient } from '../clients/bigcommerce.js';
export function registerAnalyticsTools(client: BigCommerceClient) {
return [
{
name: 'bigcommerce_get_store_analytics',
description: 'Get store-wide analytics including orders, revenue, and customer metrics',
inputSchema: {
type: 'object',
properties: {
start_date: {
type: 'string',
description: 'Start date for analytics period (RFC 2822 or ISO 8601)'
},
end_date: {
type: 'string',
description: 'End date for analytics period (RFC 2822 or ISO 8601)'
},
},
},
handler: async (args: any) => {
// Fetch orders for the period
const orders = await client.listOrders({
min_date_created: args.start_date,
max_date_created: args.end_date,
});
// Calculate analytics
const orderArray = Array.isArray(orders) ? orders : [];
const totalOrders = orderArray.length;
const totalRevenue = orderArray.reduce((sum: number, order: any) =>
sum + (order.total_inc_tax || 0), 0
);
const averageOrderValue = totalOrders > 0 ? totalRevenue / totalOrders : 0;
// Get unique customers
const uniqueCustomers = new Set(orderArray.map((order: any) => order.customer_id));
const analytics = {
period: {
start_date: args.start_date,
end_date: args.end_date,
},
total_orders: totalOrders,
total_revenue: Math.round(totalRevenue * 100) / 100,
average_order_value: Math.round(averageOrderValue * 100) / 100,
total_customers: uniqueCustomers.size,
};
return { content: [{ type: 'text', text: JSON.stringify(analytics, null, 2) }] };
},
},
{
name: 'bigcommerce_get_product_analytics',
description: 'Get analytics for a specific product',
inputSchema: {
type: 'object',
properties: {
product_id: { type: 'number', description: 'Product ID' },
start_date: {
type: 'string',
description: 'Start date for analytics period (RFC 2822 or ISO 8601)'
},
end_date: {
type: 'string',
description: 'End date for analytics period (RFC 2822 or ISO 8601)'
},
},
required: ['product_id'],
},
handler: async (args: any) => {
// Get product details
const product = await client.getProduct(args.product_id);
// Fetch orders for the period
const orders = await client.listOrders({
min_date_created: args.start_date,
max_date_created: args.end_date,
});
const orderArray = Array.isArray(orders) ? orders : [];
// Analyze orders for this product
let totalQuantitySold = 0;
let totalRevenue = 0;
let orderCount = 0;
for (const order of orderArray) {
try {
const orderProducts = await client.getOrderProducts(order.id);
const orderProductArray = Array.isArray(orderProducts) ? orderProducts : [];
for (const item of orderProductArray) {
if (item.product_id === args.product_id) {
totalQuantitySold += item.quantity || 0;
totalRevenue += item.total_inc_tax || 0;
orderCount++;
}
}
} catch (error) {
// Continue if order products can't be fetched
continue;
}
}
const analytics = {
product_id: args.product_id,
product_name: product.data?.name,
period: {
start_date: args.start_date,
end_date: args.end_date,
},
orders: orderCount,
quantity_sold: totalQuantitySold,
revenue: Math.round(totalRevenue * 100) / 100,
current_inventory: product.data?.inventory_level,
};
return { content: [{ type: 'text', text: JSON.stringify(analytics, null, 2) }] };
},
},
];
}

View File

@ -0,0 +1,105 @@
/**
* BigCommerce Brands Tools
*/
import { BigCommerceClient } from '../clients/bigcommerce.js';
export function registerBrandsTools(client: BigCommerceClient) {
return [
{
name: 'bigcommerce_list_brands',
description: 'List all brands from BigCommerce store with optional filters',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Filter by brand name' },
page_title: { type: 'string', description: 'Filter by page title' },
},
},
handler: async (args: any) => {
const brands = await client.listBrands(args);
return { content: [{ type: 'text', text: JSON.stringify(brands, null, 2) }] };
},
},
{
name: 'bigcommerce_get_brand',
description: 'Get detailed information about a specific brand',
inputSchema: {
type: 'object',
properties: {
brand_id: { type: 'number', description: 'Brand ID' },
},
required: ['brand_id'],
},
handler: async (args: any) => {
const brand = await client.getBrand(args.brand_id);
return { content: [{ type: 'text', text: JSON.stringify(brand, null, 2) }] };
},
},
{
name: 'bigcommerce_create_brand',
description: 'Create a new brand in BigCommerce',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Brand name' },
page_title: { type: 'string', description: 'Page title for SEO' },
meta_keywords: {
type: 'array',
items: { type: 'string' },
description: 'Meta keywords for SEO'
},
meta_description: { type: 'string', description: 'Meta description for SEO' },
image_url: { type: 'string', description: 'Brand logo/image URL' },
search_keywords: { type: 'string', description: 'Search keywords' },
},
required: ['name'],
},
handler: async (args: any) => {
const brand = await client.createBrand(args);
return { content: [{ type: 'text', text: JSON.stringify(brand, null, 2) }] };
},
},
{
name: 'bigcommerce_update_brand',
description: 'Update an existing brand',
inputSchema: {
type: 'object',
properties: {
brand_id: { type: 'number', description: 'Brand ID' },
name: { type: 'string', description: 'Brand name' },
page_title: { type: 'string', description: 'Page title for SEO' },
meta_keywords: {
type: 'array',
items: { type: 'string' },
description: 'Meta keywords for SEO'
},
meta_description: { type: 'string', description: 'Meta description for SEO' },
image_url: { type: 'string', description: 'Brand logo/image URL' },
search_keywords: { type: 'string', description: 'Search keywords' },
},
required: ['brand_id'],
},
handler: async (args: any) => {
const { brand_id, ...updateData } = args;
const brand = await client.updateBrand(brand_id, updateData);
return { content: [{ type: 'text', text: JSON.stringify(brand, null, 2) }] };
},
},
{
name: 'bigcommerce_delete_brand',
description: 'Delete a brand from BigCommerce',
inputSchema: {
type: 'object',
properties: {
brand_id: { type: 'number', description: 'Brand ID to delete' },
},
required: ['brand_id'],
},
handler: async (args: any) => {
await client.deleteBrand(args.brand_id);
return { content: [{ type: 'text', text: `Brand ${args.brand_id} deleted successfully` }] };
},
},
];
}

View File

@ -0,0 +1,116 @@
/**
* BigCommerce Carts Tools
*/
import { BigCommerceClient } from '../clients/bigcommerce.js';
export function registerCartsTools(client: BigCommerceClient) {
return [
{
name: 'bigcommerce_create_cart',
description: 'Create a new shopping cart',
inputSchema: {
type: 'object',
properties: {
customer_id: { type: 'number', description: 'Customer ID (optional)' },
channel_id: { type: 'number', description: 'Channel ID' },
line_items: {
type: 'array',
items: {
type: 'object',
properties: {
quantity: { type: 'number' },
product_id: { type: 'number' },
variant_id: { type: 'number' },
},
},
description: 'Initial line items',
},
},
},
handler: async (args: any) => {
const cart = await client.createCart(args);
return { content: [{ type: 'text', text: JSON.stringify(cart, null, 2) }] };
},
},
{
name: 'bigcommerce_get_cart',
description: 'Get details of a specific cart',
inputSchema: {
type: 'object',
properties: {
cart_id: { type: 'string', description: 'Cart ID (UUID)' },
},
required: ['cart_id'],
},
handler: async (args: any) => {
const cart = await client.getCart(args.cart_id);
return { content: [{ type: 'text', text: JSON.stringify(cart, null, 2) }] };
},
},
{
name: 'bigcommerce_delete_cart',
description: 'Delete a cart',
inputSchema: {
type: 'object',
properties: {
cart_id: { type: 'string', description: 'Cart ID to delete' },
},
required: ['cart_id'],
},
handler: async (args: any) => {
await client.deleteCart(args.cart_id);
return { content: [{ type: 'text', text: `Cart ${args.cart_id} deleted successfully` }] };
},
},
{
name: 'bigcommerce_add_cart_items',
description: 'Add line items to an existing cart',
inputSchema: {
type: 'object',
properties: {
cart_id: { type: 'string', description: 'Cart ID' },
line_items: {
type: 'array',
items: {
type: 'object',
properties: {
quantity: { type: 'number', description: 'Quantity to add' },
product_id: { type: 'number', description: 'Product ID' },
variant_id: { type: 'number', description: 'Variant ID (if applicable)' },
},
required: ['quantity', 'product_id'],
},
description: 'Line items to add',
},
},
required: ['cart_id', 'line_items'],
},
handler: async (args: any) => {
const { cart_id, line_items } = args;
const cart = await client.addCartLineItems(cart_id, { line_items });
return { content: [{ type: 'text', text: JSON.stringify(cart, null, 2) }] };
},
},
{
name: 'bigcommerce_update_cart_item',
description: 'Update quantity of a line item in cart',
inputSchema: {
type: 'object',
properties: {
cart_id: { type: 'string', description: 'Cart ID' },
item_id: { type: 'string', description: 'Line item ID' },
quantity: { type: 'number', description: 'New quantity' },
},
required: ['cart_id', 'item_id', 'quantity'],
},
handler: async (args: any) => {
const { cart_id, item_id, quantity } = args;
const cart = await client.updateCartLineItem(cart_id, item_id, {
line_item: { quantity }
});
return { content: [{ type: 'text', text: JSON.stringify(cart, null, 2) }] };
},
},
];
}

View File

@ -0,0 +1,136 @@
/**
* BigCommerce Categories Tools
*/
import { BigCommerceClient } from '../clients/bigcommerce.js';
export function registerCategoriesTools(client: BigCommerceClient) {
return [
{
name: 'bigcommerce_list_categories',
description: 'List all categories from BigCommerce store with optional filters',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Filter by category name' },
parent_id: { type: 'number', description: 'Filter by parent category ID' },
is_visible: { type: 'boolean', description: 'Filter by visibility' },
page_title: { type: 'string', description: 'Filter by page title' },
},
},
handler: async (args: any) => {
const categories = await client.listCategories(args);
return { content: [{ type: 'text', text: JSON.stringify(categories, null, 2) }] };
},
},
{
name: 'bigcommerce_get_category',
description: 'Get detailed information about a specific category',
inputSchema: {
type: 'object',
properties: {
category_id: { type: 'number', description: 'Category ID' },
},
required: ['category_id'],
},
handler: async (args: any) => {
const category = await client.getCategory(args.category_id);
return { content: [{ type: 'text', text: JSON.stringify(category, null, 2) }] };
},
},
{
name: 'bigcommerce_create_category',
description: 'Create a new category in BigCommerce',
inputSchema: {
type: 'object',
properties: {
parent_id: { type: 'number', description: 'Parent category ID (0 for root)' },
name: { type: 'string', description: 'Category name' },
description: { type: 'string', description: 'Category description' },
sort_order: { type: 'number', description: 'Sort order for display' },
page_title: { type: 'string', description: 'Page title for SEO' },
meta_keywords: {
type: 'array',
items: { type: 'string' },
description: 'Meta keywords for SEO'
},
meta_description: { type: 'string', description: 'Meta description for SEO' },
is_visible: { type: 'boolean', description: 'Is category visible' },
search_keywords: { type: 'string', description: 'Search keywords' },
default_product_sort: {
type: 'string',
enum: ['use_store_settings', 'featured', 'newest', 'bestselling', 'alphaasc', 'alphadesc', 'avgcustomerreview', 'priceasc', 'pricedesc'],
description: 'Default product sort order'
},
image_url: { type: 'string', description: 'Category image URL' },
},
required: ['parent_id', 'name'],
},
handler: async (args: any) => {
const category = await client.createCategory(args);
return { content: [{ type: 'text', text: JSON.stringify(category, null, 2) }] };
},
},
{
name: 'bigcommerce_update_category',
description: 'Update an existing category',
inputSchema: {
type: 'object',
properties: {
category_id: { type: 'number', description: 'Category ID' },
parent_id: { type: 'number', description: 'Parent category ID' },
name: { type: 'string', description: 'Category name' },
description: { type: 'string', description: 'Category description' },
sort_order: { type: 'number', description: 'Sort order for display' },
page_title: { type: 'string', description: 'Page title for SEO' },
meta_keywords: {
type: 'array',
items: { type: 'string' },
description: 'Meta keywords for SEO'
},
meta_description: { type: 'string', description: 'Meta description for SEO' },
is_visible: { type: 'boolean', description: 'Is category visible' },
search_keywords: { type: 'string', description: 'Search keywords' },
default_product_sort: {
type: 'string',
description: 'Default product sort order'
},
image_url: { type: 'string', description: 'Category image URL' },
},
required: ['category_id'],
},
handler: async (args: any) => {
const { category_id, ...updateData } = args;
const category = await client.updateCategory(category_id, updateData);
return { content: [{ type: 'text', text: JSON.stringify(category, null, 2) }] };
},
},
{
name: 'bigcommerce_delete_category',
description: 'Delete a category from BigCommerce',
inputSchema: {
type: 'object',
properties: {
category_id: { type: 'number', description: 'Category ID to delete' },
},
required: ['category_id'],
},
handler: async (args: any) => {
await client.deleteCategory(args.category_id);
return { content: [{ type: 'text', text: `Category ${args.category_id} deleted successfully` }] };
},
},
{
name: 'bigcommerce_get_category_tree',
description: 'Get the complete category tree structure',
inputSchema: {
type: 'object',
properties: {},
},
handler: async (args: any) => {
const tree = await client.getCategoryTree();
return { content: [{ type: 'text', text: JSON.stringify(tree, null, 2) }] };
},
},
];
}

View File

@ -0,0 +1,61 @@
/**
* BigCommerce Channels Tools
*/
import { BigCommerceClient } from '../clients/bigcommerce.js';
export function registerChannelsTools(client: BigCommerceClient) {
return [
{
name: 'bigcommerce_list_channels',
description: 'List all sales channels (storefronts, marketplaces, POS, etc.)',
inputSchema: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['storefront', 'pos', 'marketplace', 'marketing'],
description: 'Filter by channel type'
},
platform: { type: 'string', description: 'Filter by platform name' },
},
},
handler: async (args: any) => {
const channels = await client.listChannels(args);
return { content: [{ type: 'text', text: JSON.stringify(channels, null, 2) }] };
},
},
{
name: 'bigcommerce_get_channel',
description: 'Get detailed information about a specific channel',
inputSchema: {
type: 'object',
properties: {
channel_id: { type: 'number', description: 'Channel ID' },
},
required: ['channel_id'],
},
handler: async (args: any) => {
const channel = await client.getChannel(args.channel_id);
return { content: [{ type: 'text', text: JSON.stringify(channel, null, 2) }] };
},
},
{
name: 'bigcommerce_list_channel_listings',
description: 'List product listings for a specific channel',
inputSchema: {
type: 'object',
properties: {
channel_id: { type: 'number', description: 'Channel ID' },
product_id: { type: 'number', description: 'Filter by product ID' },
},
required: ['channel_id'],
},
handler: async (args: any) => {
const { channel_id, ...params } = args;
const listings = await client.listChannelListings(channel_id, params);
return { content: [{ type: 'text', text: JSON.stringify(listings, null, 2) }] };
},
},
];
}

View File

@ -0,0 +1,107 @@
/**
* BigCommerce Content Tools (Pages & Blog)
*/
import { BigCommerceClient } from '../clients/bigcommerce.js';
export function registerContentTools(client: BigCommerceClient) {
return [
{
name: 'bigcommerce_list_pages',
description: 'List all content pages in the store',
inputSchema: {
type: 'object',
properties: {},
},
handler: async (args: any) => {
const pages = await client.listPages();
return { content: [{ type: 'text', text: JSON.stringify(pages, null, 2) }] };
},
},
{
name: 'bigcommerce_get_page',
description: 'Get details of a specific content page',
inputSchema: {
type: 'object',
properties: {
page_id: { type: 'number', description: 'Page ID' },
},
required: ['page_id'],
},
handler: async (args: any) => {
const page = await client.getPage(args.page_id);
return { content: [{ type: 'text', text: JSON.stringify(page, null, 2) }] };
},
},
{
name: 'bigcommerce_create_page',
description: 'Create a new content page',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Page name/title' },
type: {
type: 'string',
enum: ['page', 'raw', 'contact_form', 'feed', 'link', 'blog'],
description: 'Page type'
},
body: { type: 'string', description: 'Page content (HTML)' },
is_visible: { type: 'boolean', description: 'Is page visible' },
is_homepage: { type: 'boolean', description: 'Set as homepage' },
is_customers_only: { type: 'boolean', description: 'Require login to view' },
meta_title: { type: 'string', description: 'Page title for SEO' },
meta_keywords: { type: 'string', description: 'Meta keywords for SEO' },
meta_description: { type: 'string', description: 'Meta description for SEO' },
search_keywords: { type: 'string', description: 'Search keywords' },
url: { type: 'string', description: 'Custom URL path' },
feed: { type: 'string', description: 'Feed URL (for feed type)' },
link: { type: 'string', description: 'External link (for link type)' },
sort_order: { type: 'number', description: 'Sort order' },
},
required: ['name', 'type'],
},
handler: async (args: any) => {
const page = await client.createPage(args);
return { content: [{ type: 'text', text: JSON.stringify(page, null, 2) }] };
},
},
{
name: 'bigcommerce_list_blog_posts',
description: 'List all blog posts',
inputSchema: {
type: 'object',
properties: {},
},
handler: async (args: any) => {
const posts = await client.listBlogPosts();
return { content: [{ type: 'text', text: JSON.stringify(posts, null, 2) }] };
},
},
{
name: 'bigcommerce_create_blog_post',
description: 'Create a new blog post',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: 'Blog post title' },
body: { type: 'string', description: 'Blog post content (HTML)' },
summary: { type: 'string', description: 'Short summary/excerpt' },
is_published: { type: 'boolean', description: 'Publish immediately' },
author: { type: 'string', description: 'Author name' },
meta_description: { type: 'string', description: 'Meta description for SEO' },
meta_keywords: { type: 'string', description: 'Meta keywords for SEO' },
tags: {
type: 'array',
items: { type: 'string' },
description: 'Blog post tags'
},
},
required: ['title', 'body'],
},
handler: async (args: any) => {
const post = await client.createBlogPost(args);
return { content: [{ type: 'text', text: JSON.stringify(post, null, 2) }] };
},
},
];
}

View File

@ -0,0 +1,116 @@
/**
* BigCommerce Coupons Tools
*/
import { BigCommerceClient } from '../clients/bigcommerce.js';
export function registerCouponsTools(client: BigCommerceClient) {
return [
{
name: 'bigcommerce_list_coupons',
description: 'List all coupons from BigCommerce store with optional filters',
inputSchema: {
type: 'object',
properties: {
code: { type: 'string', description: 'Filter by coupon code' },
name: { type: 'string', description: 'Filter by coupon name' },
type: {
type: 'string',
enum: ['per_item_discount', 'per_total_discount', 'shipping_discount', 'free_shipping', 'percentage_discount'],
description: 'Filter by coupon type'
},
},
},
handler: async (args: any) => {
const coupons = await client.listCoupons(args);
return { content: [{ type: 'text', text: JSON.stringify(coupons, null, 2) }] };
},
},
{
name: 'bigcommerce_get_coupon',
description: 'Get detailed information about a specific coupon',
inputSchema: {
type: 'object',
properties: {
coupon_id: { type: 'number', description: 'Coupon ID' },
},
required: ['coupon_id'],
},
handler: async (args: any) => {
const coupon = await client.getCoupon(args.coupon_id);
return { content: [{ type: 'text', text: JSON.stringify(coupon, null, 2) }] };
},
},
{
name: 'bigcommerce_create_coupon',
description: 'Create a new coupon in BigCommerce',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Coupon name (internal)' },
code: { type: 'string', description: 'Coupon code (what customers enter)' },
type: {
type: 'string',
enum: ['per_item_discount', 'per_total_discount', 'shipping_discount', 'free_shipping', 'percentage_discount'],
description: 'Coupon type'
},
amount: { type: 'number', description: 'Discount amount (dollars or percentage)' },
min_purchase: { type: 'number', description: 'Minimum purchase amount required' },
expires: { type: 'string', description: 'Expiration date (RFC 2822 or ISO 8601)' },
enabled: { type: 'boolean', description: 'Is coupon enabled' },
max_uses: { type: 'number', description: 'Maximum total uses' },
max_uses_per_customer: { type: 'number', description: 'Maximum uses per customer' },
},
required: ['name', 'code', 'type', 'amount'],
},
handler: async (args: any) => {
const coupon = await client.createCoupon(args);
return { content: [{ type: 'text', text: JSON.stringify(coupon, null, 2) }] };
},
},
{
name: 'bigcommerce_update_coupon',
description: 'Update an existing coupon',
inputSchema: {
type: 'object',
properties: {
coupon_id: { type: 'number', description: 'Coupon ID' },
name: { type: 'string', description: 'Coupon name (internal)' },
code: { type: 'string', description: 'Coupon code' },
type: {
type: 'string',
enum: ['per_item_discount', 'per_total_discount', 'shipping_discount', 'free_shipping', 'percentage_discount'],
description: 'Coupon type'
},
amount: { type: 'number', description: 'Discount amount' },
min_purchase: { type: 'number', description: 'Minimum purchase amount required' },
expires: { type: 'string', description: 'Expiration date' },
enabled: { type: 'boolean', description: 'Is coupon enabled' },
max_uses: { type: 'number', description: 'Maximum total uses' },
max_uses_per_customer: { type: 'number', description: 'Maximum uses per customer' },
},
required: ['coupon_id'],
},
handler: async (args: any) => {
const { coupon_id, ...updateData } = args;
const coupon = await client.updateCoupon(coupon_id, updateData);
return { content: [{ type: 'text', text: JSON.stringify(coupon, null, 2) }] };
},
},
{
name: 'bigcommerce_delete_coupon',
description: 'Delete a coupon from BigCommerce',
inputSchema: {
type: 'object',
properties: {
coupon_id: { type: 'number', description: 'Coupon ID to delete' },
},
required: ['coupon_id'],
},
handler: async (args: any) => {
await client.deleteCoupon(args.coupon_id);
return { content: [{ type: 'text', text: `Coupon ${args.coupon_id} deleted successfully` }] };
},
},
];
}

View File

@ -0,0 +1,198 @@
/**
* BigCommerce Customers Tools
*/
import { BigCommerceClient } from '../clients/bigcommerce.js';
export function registerCustomersTools(client: BigCommerceClient) {
return [
{
name: 'bigcommerce_list_customers',
description: 'List all customers from BigCommerce store with optional filters',
inputSchema: {
type: 'object',
properties: {
company: { type: 'string', description: 'Filter by company name' },
first_name: { type: 'string', description: 'Filter by first name' },
last_name: { type: 'string', description: 'Filter by last name' },
email: { type: 'string', description: 'Filter by email address' },
customer_group_id: { type: 'number', description: 'Filter by customer group ID' },
date_created: { type: 'string', description: 'Filter by creation date' },
date_modified: { type: 'string', description: 'Filter by modification date' },
},
},
handler: async (args: any) => {
const customers = await client.listCustomers(args);
return { content: [{ type: 'text', text: JSON.stringify(customers, null, 2) }] };
},
},
{
name: 'bigcommerce_get_customer',
description: 'Get detailed information about a specific customer',
inputSchema: {
type: 'object',
properties: {
customer_id: { type: 'number', description: 'Customer ID' },
},
required: ['customer_id'],
},
handler: async (args: any) => {
const customer = await client.getCustomer(args.customer_id);
return { content: [{ type: 'text', text: JSON.stringify(customer, null, 2) }] };
},
},
{
name: 'bigcommerce_create_customer',
description: 'Create a new customer in BigCommerce',
inputSchema: {
type: 'object',
properties: {
first_name: { type: 'string', description: 'Customer first name' },
last_name: { type: 'string', description: 'Customer last name' },
email: { type: 'string', description: 'Customer email address' },
company: { type: 'string', description: 'Company name' },
phone: { type: 'string', description: 'Phone number' },
customer_group_id: { type: 'number', description: 'Customer group ID' },
notes: { type: 'string', description: 'Customer notes' },
accepts_product_review_abandoned_cart_emails: {
type: 'boolean',
description: 'Accept marketing emails'
},
addresses: {
type: 'array',
items: {
type: 'object',
properties: {
first_name: { type: 'string' },
last_name: { type: 'string' },
company: { type: 'string' },
address1: { type: 'string' },
address2: { type: 'string' },
city: { type: 'string' },
state_or_province: { type: 'string' },
postal_code: { type: 'string' },
country_code: { type: 'string' },
phone: { type: 'string' },
address_type: { type: 'string', enum: ['residential', 'commercial'] },
},
},
description: 'Customer addresses',
},
authentication: {
type: 'object',
properties: {
force_password_reset: { type: 'boolean' },
new_password: { type: 'string' },
},
description: 'Authentication settings',
},
},
required: ['first_name', 'last_name', 'email'],
},
handler: async (args: any) => {
const customer = await client.createCustomer(args);
return { content: [{ type: 'text', text: JSON.stringify(customer, null, 2) }] };
},
},
{
name: 'bigcommerce_update_customer',
description: 'Update an existing customer',
inputSchema: {
type: 'object',
properties: {
customer_id: { type: 'number', description: 'Customer ID' },
first_name: { type: 'string', description: 'Customer first name' },
last_name: { type: 'string', description: 'Customer last name' },
email: { type: 'string', description: 'Customer email address' },
company: { type: 'string', description: 'Company name' },
phone: { type: 'string', description: 'Phone number' },
customer_group_id: { type: 'number', description: 'Customer group ID' },
notes: { type: 'string', description: 'Customer notes' },
accepts_product_review_abandoned_cart_emails: {
type: 'boolean',
description: 'Accept marketing emails'
},
},
required: ['customer_id'],
},
handler: async (args: any) => {
const { customer_id, ...updateData } = args;
const customer = await client.updateCustomer(customer_id, updateData);
return { content: [{ type: 'text', text: JSON.stringify(customer, null, 2) }] };
},
},
{
name: 'bigcommerce_delete_customer',
description: 'Delete a customer from BigCommerce',
inputSchema: {
type: 'object',
properties: {
customer_id: { type: 'number', description: 'Customer ID to delete' },
},
required: ['customer_id'],
},
handler: async (args: any) => {
await client.deleteCustomer(args.customer_id);
return { content: [{ type: 'text', text: `Customer ${args.customer_id} deleted successfully` }] };
},
},
{
name: 'bigcommerce_list_customer_addresses',
description: 'List all addresses for a customer',
inputSchema: {
type: 'object',
properties: {
customer_id: { type: 'number', description: 'Customer ID' },
},
required: ['customer_id'],
},
handler: async (args: any) => {
const addresses = await client.listCustomerAddresses(args.customer_id);
return { content: [{ type: 'text', text: JSON.stringify(addresses, null, 2) }] };
},
},
{
name: 'bigcommerce_create_customer_address',
description: 'Create a new address for a customer',
inputSchema: {
type: 'object',
properties: {
customer_id: { type: 'number', description: 'Customer ID' },
first_name: { type: 'string', description: 'First name' },
last_name: { type: 'string', description: 'Last name' },
company: { type: 'string', description: 'Company name' },
address1: { type: 'string', description: 'Address line 1' },
address2: { type: 'string', description: 'Address line 2' },
city: { type: 'string', description: 'City' },
state_or_province: { type: 'string', description: 'State or province' },
postal_code: { type: 'string', description: 'Postal/ZIP code' },
country_code: { type: 'string', description: 'Country code (ISO 3166-1 alpha-2)' },
phone: { type: 'string', description: 'Phone number' },
address_type: {
type: 'string',
enum: ['residential', 'commercial'],
description: 'Address type'
},
},
required: ['customer_id', 'first_name', 'last_name', 'address1', 'city', 'state_or_province', 'postal_code', 'country_code'],
},
handler: async (args: any) => {
const { customer_id, ...addressData } = args;
const address = await client.createCustomerAddress(customer_id, addressData);
return { content: [{ type: 'text', text: JSON.stringify(address, null, 2) }] };
},
},
{
name: 'bigcommerce_list_customer_groups',
description: 'List all customer groups',
inputSchema: {
type: 'object',
properties: {},
},
handler: async (args: any) => {
const groups = await client.listCustomerGroups();
return { content: [{ type: 'text', text: JSON.stringify(groups, null, 2) }] };
},
},
];
}

View File

@ -0,0 +1,249 @@
/**
* BigCommerce Orders Tools
*/
import { BigCommerceClient } from '../clients/bigcommerce.js';
export function registerOrdersTools(client: BigCommerceClient) {
return [
{
name: 'bigcommerce_list_orders',
description: 'List all orders from BigCommerce store with optional filters',
inputSchema: {
type: 'object',
properties: {
min_id: { type: 'number', description: 'Minimum order ID' },
max_id: { type: 'number', description: 'Maximum order ID' },
min_total: { type: 'number', description: 'Minimum order total' },
max_total: { type: 'number', description: 'Maximum order total' },
customer_id: { type: 'number', description: 'Filter by customer ID' },
status_id: { type: 'number', description: 'Filter by status ID' },
is_deleted: { type: 'boolean', description: 'Include deleted orders' },
payment_method: { type: 'string', description: 'Filter by payment method' },
min_date_created: { type: 'string', description: 'Minimum creation date (RFC 2822 or ISO 8601)' },
max_date_created: { type: 'string', description: 'Maximum creation date (RFC 2822 or ISO 8601)' },
min_date_modified: { type: 'string', description: 'Minimum modified date (RFC 2822 or ISO 8601)' },
max_date_modified: { type: 'string', description: 'Maximum modified date (RFC 2822 or ISO 8601)' },
},
},
handler: async (args: any) => {
const orders = await client.listOrders(args);
return { content: [{ type: 'text', text: JSON.stringify(orders, null, 2) }] };
},
},
{
name: 'bigcommerce_get_order',
description: 'Get detailed information about a specific order',
inputSchema: {
type: 'object',
properties: {
order_id: { type: 'number', description: 'Order ID' },
},
required: ['order_id'],
},
handler: async (args: any) => {
const order = await client.getOrder(args.order_id);
return { content: [{ type: 'text', text: JSON.stringify(order, null, 2) }] };
},
},
{
name: 'bigcommerce_create_order',
description: 'Create a new order in BigCommerce',
inputSchema: {
type: 'object',
properties: {
customer_id: { type: 'number', description: 'Customer ID' },
status_id: { type: 'number', description: 'Order status ID (1=Pending, 2=Shipped, etc.)' },
products: {
type: 'array',
items: {
type: 'object',
properties: {
product_id: { type: 'number' },
quantity: { type: 'number' },
},
},
description: 'Array of products to add to order',
},
billing_address: {
type: 'object',
properties: {
first_name: { type: 'string' },
last_name: { type: 'string' },
street_1: { type: 'string' },
city: { type: 'string' },
state: { type: 'string' },
zip: { type: 'string' },
country: { type: 'string' },
country_iso2: { type: 'string' },
email: { type: 'string' },
},
},
},
required: ['customer_id', 'products', 'billing_address'],
},
handler: async (args: any) => {
const order = await client.createOrder(args);
return { content: [{ type: 'text', text: JSON.stringify(order, null, 2) }] };
},
},
{
name: 'bigcommerce_update_order',
description: 'Update an existing order',
inputSchema: {
type: 'object',
properties: {
order_id: { type: 'number', description: 'Order ID' },
status_id: { type: 'number', description: 'Order status ID' },
customer_id: { type: 'number', description: 'Customer ID' },
staff_notes: { type: 'string', description: 'Internal staff notes' },
customer_message: { type: 'string', description: 'Message to customer' },
},
required: ['order_id'],
},
handler: async (args: any) => {
const { order_id, ...updateData } = args;
const order = await client.updateOrder(order_id, updateData);
return { content: [{ type: 'text', text: JSON.stringify(order, null, 2) }] };
},
},
{
name: 'bigcommerce_get_order_products',
description: 'Get all products in an order',
inputSchema: {
type: 'object',
properties: {
order_id: { type: 'number', description: 'Order ID' },
},
required: ['order_id'],
},
handler: async (args: any) => {
const products = await client.getOrderProducts(args.order_id);
return { content: [{ type: 'text', text: JSON.stringify(products, null, 2) }] };
},
},
{
name: 'bigcommerce_get_order_shipping',
description: 'Get shipping addresses for an order',
inputSchema: {
type: 'object',
properties: {
order_id: { type: 'number', description: 'Order ID' },
},
required: ['order_id'],
},
handler: async (args: any) => {
const shipping = await client.getOrderShippingAddresses(args.order_id);
return { content: [{ type: 'text', text: JSON.stringify(shipping, null, 2) }] };
},
},
{
name: 'bigcommerce_list_order_shipments',
description: 'List all shipments for an order',
inputSchema: {
type: 'object',
properties: {
order_id: { type: 'number', description: 'Order ID' },
},
required: ['order_id'],
},
handler: async (args: any) => {
const shipments = await client.listOrderShipments(args.order_id);
return { content: [{ type: 'text', text: JSON.stringify(shipments, null, 2) }] };
},
},
{
name: 'bigcommerce_create_order_shipment',
description: 'Create a new shipment for an order',
inputSchema: {
type: 'object',
properties: {
order_id: { type: 'number', description: 'Order ID' },
order_address_id: { type: 'number', description: 'Shipping address ID from order' },
tracking_number: { type: 'string', description: 'Tracking number' },
shipping_provider: { type: 'string', description: 'Shipping provider name' },
tracking_carrier: { type: 'string', description: 'Tracking carrier code' },
comments: { type: 'string', description: 'Shipment comments' },
items: {
type: 'array',
items: {
type: 'object',
properties: {
order_product_id: { type: 'number' },
quantity: { type: 'number' },
},
},
description: 'Array of items to ship',
},
},
required: ['order_id', 'order_address_id', 'items'],
},
handler: async (args: any) => {
const { order_id, ...shipmentData } = args;
const shipment = await client.createOrderShipment(order_id, shipmentData);
return { content: [{ type: 'text', text: JSON.stringify(shipment, null, 2) }] };
},
},
{
name: 'bigcommerce_list_order_refunds',
description: 'List all refunds for an order',
inputSchema: {
type: 'object',
properties: {
order_id: { type: 'number', description: 'Order ID' },
},
required: ['order_id'],
},
handler: async (args: any) => {
const refunds = await client.getOrderRefunds(args.order_id);
return { content: [{ type: 'text', text: JSON.stringify(refunds, null, 2) }] };
},
},
{
name: 'bigcommerce_create_order_refund',
description: 'Create a refund for an order',
inputSchema: {
type: 'object',
properties: {
order_id: { type: 'number', description: 'Order ID' },
items: {
type: 'array',
items: {
type: 'object',
properties: {
item_type: {
type: 'string',
enum: ['PRODUCT', 'GIFT_WRAPPING', 'SHIPPING', 'HANDLING'],
description: 'Type of item to refund'
},
item_id: { type: 'number', description: 'ID of the item to refund' },
quantity: { type: 'number', description: 'Quantity to refund (for products)' },
reason: { type: 'string', description: 'Refund reason' },
},
required: ['item_type', 'item_id'],
},
description: 'Items to refund',
},
payments: {
type: 'array',
items: {
type: 'object',
properties: {
provider_id: { type: 'string', description: 'Payment provider ID' },
amount: { type: 'number', description: 'Amount to refund' },
offline: { type: 'boolean', description: 'Is offline refund' },
},
},
description: 'Payment methods to refund to',
},
},
required: ['order_id', 'items'],
},
handler: async (args: any) => {
const { order_id, ...refundData } = args;
const refund = await client.createOrderRefund(order_id, refundData);
return { content: [{ type: 'text', text: JSON.stringify(refund, null, 2) }] };
},
},
];
}

View File

@ -0,0 +1,242 @@
/**
* BigCommerce Products Tools
*/
import { BigCommerceClient } from '../clients/bigcommerce.js';
export function registerProductsTools(client: BigCommerceClient) {
return [
{
name: 'bigcommerce_list_products',
description: 'List all products from BigCommerce store with optional filters',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Filter by product name' },
sku: { type: 'string', description: 'Filter by SKU' },
price: { type: 'number', description: 'Filter by price' },
brand_id: { type: 'number', description: 'Filter by brand ID' },
is_visible: { type: 'boolean', description: 'Filter by visibility' },
is_featured: { type: 'boolean', description: 'Filter by featured status' },
},
},
handler: async (args: any) => {
const products = await client.listProducts(args);
return { content: [{ type: 'text', text: JSON.stringify(products, null, 2) }] };
},
},
{
name: 'bigcommerce_get_product',
description: 'Get detailed information about a specific product',
inputSchema: {
type: 'object',
properties: {
product_id: { type: 'number', description: 'Product ID' },
},
required: ['product_id'],
},
handler: async (args: any) => {
const product = await client.getProduct(args.product_id);
return { content: [{ type: 'text', text: JSON.stringify(product, null, 2) }] };
},
},
{
name: 'bigcommerce_create_product',
description: 'Create a new product in BigCommerce',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Product name' },
type: {
type: 'string',
description: 'Product type (physical or digital)',
enum: ['physical', 'digital']
},
sku: { type: 'string', description: 'Stock Keeping Unit' },
description: { type: 'string', description: 'Product description' },
price: { type: 'number', description: 'Product price' },
cost_price: { type: 'number', description: 'Cost price' },
retail_price: { type: 'number', description: 'Retail price (MSRP)' },
sale_price: { type: 'number', description: 'Sale price' },
weight: { type: 'number', description: 'Product weight' },
width: { type: 'number', description: 'Product width' },
depth: { type: 'number', description: 'Product depth' },
height: { type: 'number', description: 'Product height' },
is_visible: { type: 'boolean', description: 'Is product visible' },
is_featured: { type: 'boolean', description: 'Is product featured' },
categories: { type: 'array', items: { type: 'number' }, description: 'Category IDs' },
brand_id: { type: 'number', description: 'Brand ID' },
inventory_level: { type: 'number', description: 'Inventory level' },
inventory_warning_level: { type: 'number', description: 'Low stock warning level' },
inventory_tracking: {
type: 'string',
description: 'Inventory tracking method',
enum: ['none', 'product', 'variant']
},
},
required: ['name', 'type', 'price'],
},
handler: async (args: any) => {
const product = await client.createProduct(args);
return { content: [{ type: 'text', text: JSON.stringify(product, null, 2) }] };
},
},
{
name: 'bigcommerce_update_product',
description: 'Update an existing product',
inputSchema: {
type: 'object',
properties: {
product_id: { type: 'number', description: 'Product ID' },
name: { type: 'string', description: 'Product name' },
sku: { type: 'string', description: 'Stock Keeping Unit' },
description: { type: 'string', description: 'Product description' },
price: { type: 'number', description: 'Product price' },
cost_price: { type: 'number', description: 'Cost price' },
retail_price: { type: 'number', description: 'Retail price (MSRP)' },
sale_price: { type: 'number', description: 'Sale price' },
weight: { type: 'number', description: 'Product weight' },
is_visible: { type: 'boolean', description: 'Is product visible' },
is_featured: { type: 'boolean', description: 'Is product featured' },
categories: { type: 'array', items: { type: 'number' }, description: 'Category IDs' },
brand_id: { type: 'number', description: 'Brand ID' },
inventory_level: { type: 'number', description: 'Inventory level' },
inventory_warning_level: { type: 'number', description: 'Low stock warning level' },
},
required: ['product_id'],
},
handler: async (args: any) => {
const { product_id, ...updateData } = args;
const product = await client.updateProduct(product_id, updateData);
return { content: [{ type: 'text', text: JSON.stringify(product, null, 2) }] };
},
},
{
name: 'bigcommerce_delete_product',
description: 'Delete a product from BigCommerce',
inputSchema: {
type: 'object',
properties: {
product_id: { type: 'number', description: 'Product ID to delete' },
},
required: ['product_id'],
},
handler: async (args: any) => {
await client.deleteProduct(args.product_id);
return { content: [{ type: 'text', text: `Product ${args.product_id} deleted successfully` }] };
},
},
{
name: 'bigcommerce_list_product_variants',
description: 'List all variants for a product',
inputSchema: {
type: 'object',
properties: {
product_id: { type: 'number', description: 'Product ID' },
},
required: ['product_id'],
},
handler: async (args: any) => {
const variants = await client.listProductVariants(args.product_id);
return { content: [{ type: 'text', text: JSON.stringify(variants, null, 2) }] };
},
},
{
name: 'bigcommerce_create_product_variant',
description: 'Create a new variant for a product',
inputSchema: {
type: 'object',
properties: {
product_id: { type: 'number', description: 'Product ID' },
sku: { type: 'string', description: 'Variant SKU' },
price: { type: 'number', description: 'Variant price' },
cost_price: { type: 'number', description: 'Variant cost price' },
weight: { type: 'number', description: 'Variant weight' },
inventory_level: { type: 'number', description: 'Variant inventory level' },
option_values: {
type: 'array',
items: {
type: 'object',
properties: {
option_id: { type: 'number' },
id: { type: 'number' },
},
},
description: 'Option value IDs for this variant',
},
},
required: ['product_id', 'option_values'],
},
handler: async (args: any) => {
const { product_id, ...variantData } = args;
const variant = await client.createProductVariant(product_id, variantData);
return { content: [{ type: 'text', text: JSON.stringify(variant, null, 2) }] };
},
},
{
name: 'bigcommerce_list_product_images',
description: 'List all images for a product',
inputSchema: {
type: 'object',
properties: {
product_id: { type: 'number', description: 'Product ID' },
},
required: ['product_id'],
},
handler: async (args: any) => {
const images = await client.listProductImages(args.product_id);
return { content: [{ type: 'text', text: JSON.stringify(images, null, 2) }] };
},
},
{
name: 'bigcommerce_upload_product_image',
description: 'Upload or add an image to a product',
inputSchema: {
type: 'object',
properties: {
product_id: { type: 'number', description: 'Product ID' },
image_url: { type: 'string', description: 'URL of the image to add' },
is_thumbnail: { type: 'boolean', description: 'Set as thumbnail image' },
sort_order: { type: 'number', description: 'Sort order for display' },
description: { type: 'string', description: 'Image description/alt text' },
},
required: ['product_id', 'image_url'],
},
handler: async (args: any) => {
const { product_id, ...imageData } = args;
const image = await client.createProductImage(product_id, imageData);
return { content: [{ type: 'text', text: JSON.stringify(image, null, 2) }] };
},
},
{
name: 'bigcommerce_list_product_custom_fields',
description: 'List custom fields for a product',
inputSchema: {
type: 'object',
properties: {
product_id: { type: 'number', description: 'Product ID' },
},
required: ['product_id'],
},
handler: async (args: any) => {
const customFields = await client.listProductCustomFields(args.product_id);
return { content: [{ type: 'text', text: JSON.stringify(customFields, null, 2) }] };
},
},
{
name: 'bigcommerce_list_product_bulk_pricing',
description: 'List bulk pricing rules for a product',
inputSchema: {
type: 'object',
properties: {
product_id: { type: 'number', description: 'Product ID' },
},
required: ['product_id'],
},
handler: async (args: any) => {
const bulkPricing = await client.listProductBulkPricing(args.product_id);
return { content: [{ type: 'text', text: JSON.stringify(bulkPricing, null, 2) }] };
},
},
];
}

View File

@ -0,0 +1,52 @@
/**
* BigCommerce Shipping Tools
*/
import { BigCommerceClient } from '../clients/bigcommerce.js';
export function registerShippingTools(client: BigCommerceClient) {
return [
{
name: 'bigcommerce_list_shipping_zones',
description: 'List all shipping zones',
inputSchema: {
type: 'object',
properties: {},
},
handler: async (args: any) => {
const zones = await client.listShippingZones();
return { content: [{ type: 'text', text: JSON.stringify(zones, null, 2) }] };
},
},
{
name: 'bigcommerce_get_shipping_zone',
description: 'Get details of a specific shipping zone',
inputSchema: {
type: 'object',
properties: {
zone_id: { type: 'number', description: 'Shipping zone ID' },
},
required: ['zone_id'],
},
handler: async (args: any) => {
const zone = await client.getShippingZone(args.zone_id);
return { content: [{ type: 'text', text: JSON.stringify(zone, null, 2) }] };
},
},
{
name: 'bigcommerce_list_shipping_methods',
description: 'List shipping methods for a specific zone',
inputSchema: {
type: 'object',
properties: {
zone_id: { type: 'number', description: 'Shipping zone ID' },
},
required: ['zone_id'],
},
handler: async (args: any) => {
const methods = await client.listShippingMethods(args.zone_id);
return { content: [{ type: 'text', text: JSON.stringify(methods, null, 2) }] };
},
},
];
}

View File

@ -0,0 +1,44 @@
/**
* BigCommerce Store Tools
*/
import { BigCommerceClient } from '../clients/bigcommerce.js';
export function registerStoreTools(client: BigCommerceClient) {
return [
{
name: 'bigcommerce_get_store_info',
description: 'Get complete store information including settings, timezone, currency, and features',
inputSchema: {
type: 'object',
properties: {},
},
handler: async (args: any) => {
const storeInfo = await client.getStoreInformation();
return { content: [{ type: 'text', text: JSON.stringify(storeInfo, null, 2) }] };
},
},
{
name: 'bigcommerce_get_store_status',
description: 'Get store status summary including domain, name, and plan information',
inputSchema: {
type: 'object',
properties: {},
},
handler: async (args: any) => {
const storeInfo = await client.getStoreInformation();
const summary = {
name: storeInfo.data?.name,
domain: storeInfo.data?.domain,
secure_url: storeInfo.data?.secure_url,
plan_name: storeInfo.data?.plan_name,
plan_level: storeInfo.data?.plan_level,
currency: storeInfo.data?.currency,
timezone: storeInfo.data?.timezone?.name,
language: storeInfo.data?.language,
};
return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] };
},
},
];
}

View File

@ -0,0 +1,633 @@
/**
* BigCommerce MCP Server - Type Definitions
*/
// API Configuration
export interface BigCommerceConfig {
storeHash: string;
accessToken: string;
apiVersion?: 'v2' | 'v3';
}
// Product Types
export interface Product {
id: number;
name: string;
type: string;
sku?: string;
description?: string;
price: number;
cost_price?: number;
retail_price?: number;
sale_price?: number;
weight?: number;
width?: number;
depth?: number;
height?: number;
is_visible?: boolean;
is_featured?: boolean;
categories?: number[];
brand_id?: number;
inventory_level?: number;
inventory_warning_level?: number;
inventory_tracking?: string;
custom_url?: { url: string; is_customized: boolean };
meta_keywords?: string[];
meta_description?: string;
page_title?: string;
images?: ProductImage[];
variants?: ProductVariant[];
custom_fields?: CustomField[];
}
export interface ProductImage {
id?: number;
product_id?: number;
image_url: string;
is_thumbnail?: boolean;
sort_order?: number;
description?: string;
}
export interface ProductVariant {
id?: number;
product_id?: number;
sku?: string;
price?: number;
cost_price?: number;
weight?: number;
option_values?: OptionValue[];
inventory_level?: number;
}
export interface OptionValue {
id: number;
label: string;
option_id: number;
option_display_name: string;
}
export interface CustomField {
id?: number;
name: string;
value: string;
}
export interface BulkPricingRule {
id?: number;
quantity_min: number;
quantity_max: number;
type: 'price' | 'percent' | 'fixed';
amount: number;
}
// Order Types
export interface Order {
id: number;
customer_id: number;
date_created: string;
date_modified?: string;
date_shipped?: string;
status_id: number;
status: string;
subtotal_ex_tax: number;
subtotal_inc_tax: number;
subtotal_tax: number;
base_shipping_cost: number;
shipping_cost_ex_tax: number;
shipping_cost_inc_tax: number;
shipping_cost_tax: number;
base_handling_cost: number;
handling_cost_ex_tax: number;
handling_cost_inc_tax: number;
handling_cost_tax: number;
base_wrapping_cost: number;
wrapping_cost_ex_tax: number;
wrapping_cost_inc_tax: number;
wrapping_cost_tax: number;
total_ex_tax: number;
total_inc_tax: number;
total_tax: number;
items_total: number;
items_shipped: number;
payment_method: string;
payment_provider_id?: string;
refunded_amount?: number;
order_is_digital: boolean;
store_credit_amount?: number;
gift_certificate_amount?: number;
ip_address?: string;
geoip_country?: string;
currency_code: string;
currency_exchange_rate: number;
default_currency_code: string;
coupon_discount?: number;
shipping_address_count: number;
is_deleted: boolean;
billing_address?: Address;
products?: OrderProduct[];
shipping_addresses?: ShippingAddress[];
coupons?: Coupon[];
}
export interface OrderProduct {
id: number;
order_id: number;
product_id: number;
name: string;
sku?: string;
type: string;
base_price: number;
price_ex_tax: number;
price_inc_tax: number;
price_tax: number;
quantity: number;
total_ex_tax: number;
total_inc_tax: number;
total_tax: number;
weight?: number;
is_refunded?: boolean;
refund_amount?: number;
}
export interface Shipment {
id?: number;
order_id: number;
customer_id?: number;
order_address_id: number;
date_created?: string;
tracking_number?: string;
shipping_method?: string;
shipping_provider?: string;
tracking_carrier?: string;
comments?: string;
items: ShipmentItem[];
}
export interface ShipmentItem {
order_product_id: number;
quantity: number;
}
export interface Refund {
id?: number;
order_id: number;
created?: string;
reason?: string;
total_amount: number;
total_tax: number;
items: RefundItem[];
payments?: RefundPayment[];
}
export interface RefundItem {
item_type: 'PRODUCT' | 'GIFT_WRAPPING' | 'SHIPPING' | 'HANDLING';
item_id: number;
quantity?: number;
reason?: string;
requested_amount?: number;
}
export interface RefundPayment {
provider_id: string;
amount: number;
offline?: boolean;
}
// Customer Types
export interface Customer {
id?: number;
company?: string;
first_name: string;
last_name: string;
email: string;
phone?: string;
registration_ip_address?: string;
customer_group_id?: number;
notes?: string;
tax_exempt_category?: string;
date_created?: string;
date_modified?: string;
accepts_product_review_abandoned_cart_emails?: boolean;
store_credit_amounts?: StoreCredit[];
addresses?: Address[];
form_fields?: FormField[];
authentication?: CustomerAuthentication;
}
export interface Address {
id?: number;
customer_id?: number;
first_name: string;
last_name: string;
company?: string;
address1: string;
address2?: string;
city: string;
state_or_province: string;
postal_code: string;
country_code: string;
phone?: string;
address_type?: 'residential' | 'commercial';
}
export interface StoreCredit {
amount: number;
}
export interface FormField {
name: string;
value: string | string[];
}
export interface CustomerAuthentication {
force_password_reset?: boolean;
new_password?: string;
}
export interface CustomerGroup {
id?: number;
name: string;
is_default?: boolean;
category_access?: {
type: 'all' | 'specific' | 'none';
categories?: number[];
};
discount_rules?: DiscountRule[];
}
export interface DiscountRule {
type: 'price_list' | 'all_products' | 'product_category';
method: 'percent' | 'fixed';
amount: number;
}
// Category Types
export interface Category {
id?: number;
parent_id: number;
name: string;
description?: string;
views?: number;
sort_order?: number;
page_title?: string;
meta_keywords?: string[];
meta_description?: string;
layout_file?: string;
image_url?: string;
is_visible?: boolean;
search_keywords?: string;
default_product_sort?: string;
custom_url?: { url: string; is_customized: boolean };
}
export interface CategoryTree {
id: number;
parent_id: number;
name: string;
is_visible: boolean;
url: string;
children?: CategoryTree[];
}
// Brand Types
export interface Brand {
id?: number;
name: string;
page_title?: string;
meta_keywords?: string[];
meta_description?: string;
image_url?: string;
search_keywords?: string;
custom_url?: { url: string; is_customized: boolean };
}
// Coupon Types
export interface Coupon {
id?: number;
name: string;
type: 'per_item_discount' | 'per_total_discount' | 'shipping_discount' | 'free_shipping' | 'percentage_discount';
code: string;
amount: number;
min_purchase?: number;
expires?: string;
enabled?: boolean;
applies_to?: {
entity: 'categories' | 'products';
ids: number[];
};
num_uses?: number;
max_uses?: number;
max_uses_per_customer?: number;
restricted_to?: {
countries?: string[];
};
shipping_methods?: string[];
}
// Store Types
export interface StoreInformation {
id?: string;
domain: string;
secure_url: string;
name: string;
first_name?: string;
last_name?: string;
address: string;
country: string;
country_code: string;
phone: string;
admin_email: string;
order_email: string;
timezone: {
name: string;
raw_offset: number;
dst_offset: number;
dst_correction: boolean;
date_format: {
display: string;
export: string;
extended_display: string;
};
};
language: string;
currency: string;
currency_symbol: string;
decimal_separator: string;
thousands_separator: string;
decimal_places: number;
currency_symbol_location: string;
weight_units: string;
dimension_units: string;
dimension_decimal_places: number;
dimension_decimal_token: string;
dimension_thousands_token: string;
plan_name?: string;
plan_level?: string;
industry?: string;
logo?: {
url: string;
};
is_price_entered_with_tax?: boolean;
active_comparison_modules?: string[];
features?: {
stencil_enabled: boolean;
sitewidehttps_enabled: boolean;
facebook_catalog_id?: string;
};
}
// Channel Types
export interface Channel {
id?: number;
name: string;
type: 'storefront' | 'pos' | 'marketplace' | 'marketing';
platform?: string;
external_id?: string;
status: 'active' | 'inactive' | 'connected' | 'disconnected' | 'archived';
is_listable_from_ui?: boolean;
is_visible?: boolean;
date_created?: string;
date_modified?: string;
config_meta?: {
app?: {
id: number;
};
};
}
export interface ChannelListing {
product_id: number;
channel_id: number;
state: 'active' | 'disabled';
variants?: {
variant_id: number;
product_id: number;
state: 'active' | 'disabled';
}[];
}
// Cart Types
export interface Cart {
id?: string;
customer_id?: number;
channel_id?: number;
email?: string;
currency?: {
code: string;
};
tax_included?: boolean;
base_amount?: number;
discount_amount?: number;
cart_amount?: number;
line_items?: {
physical_items?: CartLineItem[];
digital_items?: CartLineItem[];
gift_certificates?: GiftCertificate[];
custom_items?: CustomItem[];
};
created_time?: string;
updated_time?: string;
locale?: string;
}
export interface CartLineItem {
id?: string;
parent_id?: string;
variant_id: number;
product_id: number;
sku?: string;
name?: string;
url?: string;
quantity: number;
taxable?: boolean;
image_url?: string;
discounts?: Discount[];
coupons?: Discount[];
discount_amount?: number;
coupon_amount?: number;
list_price?: number;
sale_price?: number;
extended_list_price?: number;
extended_sale_price?: number;
is_require_shipping?: boolean;
is_mutable?: boolean;
}
export interface GiftCertificate {
id?: string;
name: string;
theme: string;
amount: number;
taxable?: boolean;
sender: {
name: string;
email: string;
};
recipient: {
name: string;
email: string;
};
message?: string;
}
export interface CustomItem {
id?: string;
sku?: string;
name: string;
quantity: number;
list_price: number;
}
export interface Discount {
id: string;
discounted_amount: number;
}
// Content Types
export interface Page {
id?: number;
channel_id?: number;
name: string;
is_visible?: boolean;
parent_id?: number;
sort_order?: number;
type: 'page' | 'raw' | 'contact_form' | 'feed' | 'link' | 'blog';
is_homepage?: boolean;
is_customers_only?: boolean;
email?: string;
meta_title?: string;
meta_keywords?: string;
meta_description?: string;
search_keywords?: string;
url?: string;
body?: string;
feed?: string;
link?: string;
}
export interface BlogPost {
id?: number;
title: string;
url?: string;
preview_url?: string;
body: string;
summary?: string;
is_published?: boolean;
published_date?: {
timezone_type: number;
date: string;
};
meta_description?: string;
meta_keywords?: string;
author?: string;
thumbnail_path?: string;
tags?: string[];
}
// Shipping Types
export interface ShippingZone {
id?: number;
name: string;
type: 'country' | 'state' | 'zip' | 'global';
locations?: ShippingLocation[];
enabled?: boolean;
handling_fees?: {
fixed_surcharge?: number;
display_separately?: boolean;
};
}
export interface ShippingLocation {
zip?: string;
country_iso2?: string;
state_iso2?: string;
}
export interface ShippingMethod {
id?: number;
name: string;
type: 'perorder' | 'peritem' | 'weight' | 'total' | 'auspost' | 'canadapost' | 'usps' | 'fedex' | 'ups' | 'upsready' | 'shipperhq' | 'endicia' | 'shippereasy';
settings?: {
rate?: number;
};
enabled?: boolean;
handling_fees?: {
fixed_surcharge?: number;
display_separately?: boolean;
};
is_fallback?: boolean;
zone_id?: number;
}
// Analytics Types
export interface StoreAnalytics {
total_orders?: number;
total_revenue?: number;
average_order_value?: number;
conversion_rate?: number;
total_customers?: number;
new_customers?: number;
returning_customers?: number;
period?: {
start_date: string;
end_date: string;
};
}
export interface ProductAnalytics {
product_id: number;
views?: number;
orders?: number;
revenue?: number;
conversion_rate?: number;
average_rating?: number;
reviews_count?: number;
}
// Shipping Address (for Orders)
export interface ShippingAddress extends Address {
order_id?: number;
items_total?: number;
items_shipped?: number;
shipping_method?: string;
base_cost?: number;
cost_ex_tax?: number;
cost_inc_tax?: number;
cost_tax?: number;
}
// API Response Types
export interface PaginatedResponse<T> {
data: T[];
meta?: {
pagination?: {
total: number;
count: number;
per_page: number;
current_page: number;
total_pages: number;
links?: {
previous?: string;
current: string;
next?: string;
};
};
};
}
export interface ApiResponse<T> {
data?: T;
meta?: any;
error?: string;
errors?: any;
}
// Error Types
export interface BigCommerceError {
status: number;
title: string;
type?: string;
detail?: string;
errors?: Record<string, string>;
}

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -0,0 +1,174 @@
import React, { useState } from 'react';
interface Customer {
id: number;
name: string;
email: string;
phone: string;
company: string;
group: string;
totalOrders: number;
totalSpent: number;
avgOrderValue: number;
addresses: { type: string; address: string; }[];
}
export default function CustomerDetail() {
const [customer] = useState<Customer>({
id: 501,
name: 'John Doe',
email: 'john.doe@example.com',
phone: '(555) 123-4567',
company: 'Acme Corp',
group: 'Wholesale',
totalOrders: 24,
totalSpent: 4567.89,
avgOrderValue: 190.33,
addresses: [
{ type: 'Billing', address: '123 Main St, City, ST 12345' },
{ type: 'Shipping', address: '456 Oak Ave, Town, ST 67890' },
],
});
return (
<div style={styles.container}>
<h1 style={styles.title}>Customer Details</h1>
<div style={styles.grid}>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Contact Information</h2>
<div style={styles.infoGrid}>
<div style={styles.infoItem}>
<div style={styles.label}>Name</div>
<div style={styles.value}>{customer.name}</div>
</div>
<div style={styles.infoItem}>
<div style={styles.label}>Email</div>
<div style={styles.value}>{customer.email}</div>
</div>
<div style={styles.infoItem}>
<div style={styles.label}>Phone</div>
<div style={styles.value}>{customer.phone}</div>
</div>
<div style={styles.infoItem}>
<div style={styles.label}>Company</div>
<div style={styles.value}>{customer.company}</div>
</div>
</div>
</div>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Customer Stats</h2>
<div style={styles.statsGrid}>
<div style={styles.stat}>
<div style={styles.statValue}>{customer.totalOrders}</div>
<div style={styles.statLabel}>Total Orders</div>
</div>
<div style={styles.stat}>
<div style={styles.statValue}>${customer.totalSpent.toFixed(2)}</div>
<div style={styles.statLabel}>Total Spent</div>
</div>
<div style={styles.stat}>
<div style={styles.statValue}>${customer.avgOrderValue.toFixed(2)}</div>
<div style={styles.statLabel}>Avg Order Value</div>
</div>
</div>
</div>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Addresses</h2>
{customer.addresses.map((addr, i) => (
<div key={i} style={styles.addressCard}>
<div style={styles.addressType}>{addr.type}</div>
<div style={styles.addressText}>{addr.address}</div>
</div>
))}
</div>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
grid: {
display: 'grid',
gap: '1.5rem',
},
section: {
backgroundColor: '#1f2937',
padding: '1.5rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
sectionTitle: {
fontSize: '1.25rem',
fontWeight: '600',
marginBottom: '1rem',
color: '#f9fafb',
},
infoGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '1rem',
},
infoItem: {
display: 'flex',
flexDirection: 'column',
gap: '0.25rem',
},
label: {
fontSize: '0.875rem',
color: '#9ca3af',
},
value: {
fontSize: '1.125rem',
fontWeight: '500',
color: '#f9fafb',
},
statsGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
gap: '1rem',
},
stat: {
textAlign: 'center',
},
statValue: {
fontSize: '2rem',
fontWeight: 'bold',
color: '#3b82f6',
},
statLabel: {
fontSize: '0.875rem',
color: '#9ca3af',
marginTop: '0.5rem',
},
addressCard: {
padding: '1rem',
backgroundColor: '#374151',
borderRadius: '0.375rem',
marginBottom: '1rem',
},
addressType: {
fontSize: '0.875rem',
fontWeight: '600',
color: '#3b82f6',
marginBottom: '0.5rem',
},
addressText: {
color: '#d1d5db',
},
};

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -0,0 +1,167 @@
import React, { useState } from 'react';
interface Order {
id: number;
customer: string;
date: string;
status: string;
total: number;
items: number;
}
export default function OrderDashboard() {
const [orders] = useState<Order[]>([
{ id: 1001, customer: 'John Doe', date: '2024-01-15', status: 'Completed', total: 159.99, items: 3 },
{ id: 1002, customer: 'Jane Smith', date: '2024-01-16', status: 'Processing', total: 89.50, items: 2 },
{ id: 1003, customer: 'Bob Johnson', date: '2024-01-16', status: 'Shipped', total: 249.99, items: 5 },
{ id: 1004, customer: 'Alice Brown', date: '2024-01-17', status: 'Pending', total: 45.00, items: 1 },
]);
const totalRevenue = orders.reduce((sum, order) => sum + order.total, 0);
const avgOrderValue = totalRevenue / orders.length;
const statusCounts = orders.reduce((acc, order) => {
acc[order.status] = (acc[order.status] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
Completed: '#10b981',
Processing: '#3b82f6',
Shipped: '#8b5cf6',
Pending: '#f59e0b',
Cancelled: '#ef4444',
};
return colors[status] || '#6b7280';
};
return (
<div style={styles.container}>
<h1 style={styles.title}>Order Dashboard</h1>
<div style={styles.statsGrid}>
<div style={styles.statCard}>
<div style={styles.statValue}>{orders.length}</div>
<div style={styles.statLabel}>Total Orders</div>
</div>
<div style={styles.statCard}>
<div style={styles.statValue}>${totalRevenue.toFixed(2)}</div>
<div style={styles.statLabel}>Total Revenue</div>
</div>
<div style={styles.statCard}>
<div style={styles.statValue}>${avgOrderValue.toFixed(2)}</div>
<div style={styles.statLabel}>Avg Order Value</div>
</div>
<div style={styles.statCard}>
<div style={styles.statValue}>{statusCounts['Pending'] || 0}</div>
<div style={styles.statLabel}>Pending Orders</div>
</div>
</div>
<div style={styles.tableContainer}>
<table style={styles.table}>
<thead>
<tr>
<th style={styles.th}>Order ID</th>
<th style={styles.th}>Customer</th>
<th style={styles.th}>Date</th>
<th style={styles.th}>Items</th>
<th style={styles.th}>Total</th>
<th style={styles.th}>Status</th>
</tr>
</thead>
<tbody>
{orders.map(order => (
<tr key={order.id} style={styles.tr}>
<td style={styles.td}>#{order.id}</td>
<td style={styles.td}>{order.customer}</td>
<td style={styles.td}>{order.date}</td>
<td style={styles.td}>{order.items}</td>
<td style={styles.td}>${order.total.toFixed(2)}</td>
<td style={styles.td}>
<span style={{
...styles.badge,
backgroundColor: `${getStatusColor(order.status)}33`,
color: getStatusColor(order.status)
}}>
{order.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
statsGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '1rem',
marginBottom: '2rem',
},
statCard: {
backgroundColor: '#1f2937',
padding: '1.5rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
statValue: {
fontSize: '2rem',
fontWeight: 'bold',
color: '#3b82f6',
marginBottom: '0.5rem',
},
statLabel: {
fontSize: '0.875rem',
color: '#9ca3af',
},
tableContainer: {
backgroundColor: '#1f2937',
borderRadius: '0.5rem',
overflow: 'hidden',
border: '1px solid #374151',
},
table: {
width: '100%',
borderCollapse: 'collapse',
},
th: {
padding: '1rem',
textAlign: 'left',
backgroundColor: '#374151',
color: '#f9fafb',
fontWeight: '600',
fontSize: '0.875rem',
textTransform: 'uppercase',
},
tr: {
borderBottom: '1px solid #374151',
},
td: {
padding: '1rem',
color: '#d1d5db',
},
badge: {
padding: '0.25rem 0.75rem',
borderRadius: '9999px',
fontSize: '0.75rem',
fontWeight: '500',
},
};

View File

@ -0,0 +1,172 @@
import React, { useState } from 'react';
interface OrderDetail {
id: number;
customer: { name: string; email: string; };
date: string;
status: string;
items: { name: string; quantity: number; price: number; }[];
subtotal: number;
tax: number;
shipping: number;
total: number;
shipping_address: string;
payment_method: string;
}
export default function OrderDetail() {
const [order] = useState<OrderDetail>({
id: 1001,
customer: { name: 'John Doe', email: 'john@example.com' },
date: '2024-01-15 10:30 AM',
status: 'Completed',
items: [
{ name: 'Widget Pro', quantity: 2, price: 49.99 },
{ name: 'Gadget Plus', quantity: 1, price: 79.99 },
],
subtotal: 179.97,
tax: 14.40,
shipping: 9.99,
total: 204.36,
shipping_address: '123 Main St, City, ST 12345',
payment_method: 'Visa ****1234',
});
return (
<div style={styles.container}>
<div style={styles.header}>
<h1 style={styles.title}>Order #{order.id}</h1>
<span style={styles.status}>{order.status}</span>
</div>
<div style={styles.grid}>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Customer Information</h2>
<div style={styles.infoItem}>
<span style={styles.label}>Name:</span> {order.customer.name}
</div>
<div style={styles.infoItem}>
<span style={styles.label}>Email:</span> {order.customer.email}
</div>
<div style={styles.infoItem}>
<span style={styles.label}>Order Date:</span> {order.date}
</div>
</div>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Shipping & Payment</h2>
<div style={styles.infoItem}>
<span style={styles.label}>Address:</span> {order.shipping_address}
</div>
<div style={styles.infoItem}>
<span style={styles.label}>Payment:</span> {order.payment_method}
</div>
</div>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Order Items</h2>
{order.items.map((item, i) => (
<div key={i} style={styles.itemRow}>
<span>{item.name} × {item.quantity}</span>
<span>${(item.price * item.quantity).toFixed(2)}</span>
</div>
))}
</div>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Order Total</h2>
<div style={styles.totalRow}>
<span>Subtotal:</span>
<span>${order.subtotal.toFixed(2)}</span>
</div>
<div style={styles.totalRow}>
<span>Tax:</span>
<span>${order.tax.toFixed(2)}</span>
</div>
<div style={styles.totalRow}>
<span>Shipping:</span>
<span>${order.shipping.toFixed(2)}</span>
</div>
<div style={{ ...styles.totalRow, ...styles.grandTotal }}>
<span>Total:</span>
<span>${order.total.toFixed(2)}</span>
</div>
</div>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
header: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '2rem',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
color: '#f9fafb',
},
status: {
padding: '0.5rem 1rem',
backgroundColor: '#10b98133',
color: '#10b981',
borderRadius: '0.375rem',
fontSize: '0.875rem',
fontWeight: '500',
},
grid: {
display: 'grid',
gap: '1.5rem',
},
section: {
backgroundColor: '#1f2937',
padding: '1.5rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
sectionTitle: {
fontSize: '1.25rem',
fontWeight: '600',
marginBottom: '1rem',
color: '#f9fafb',
},
infoItem: {
marginBottom: '0.75rem',
color: '#d1d5db',
},
label: {
color: '#9ca3af',
fontWeight: '500',
},
itemRow: {
display: 'flex',
justifyContent: 'space-between',
padding: '0.75rem 0',
borderBottom: '1px solid #374151',
color: '#d1d5db',
},
totalRow: {
display: 'flex',
justifyContent: 'space-between',
padding: '0.5rem 0',
color: '#d1d5db',
},
grandTotal: {
fontSize: '1.25rem',
fontWeight: 'bold',
color: '#3b82f6',
borderTop: '2px solid #374151',
paddingTop: '1rem',
marginTop: '0.5rem',
},
};

View File

@ -0,0 +1,142 @@
import React, { useState } from 'react';
interface Order {
id: number;
customer: string;
date: string;
status: string;
total: number;
items: number;
}
export default function OrderGrid() {
const [orders] = useState<Order[]>([
{ id: 1001, customer: 'John Doe', date: '2024-01-15', status: 'Completed', total: 159.99, items: 3 },
{ id: 1002, customer: 'Jane Smith', date: '2024-01-16', status: 'Processing', total: 89.50, items: 2 },
{ id: 1003, customer: 'Bob Johnson', date: '2024-01-16', status: 'Shipped', total: 249.99, items: 5 },
{ id: 1004, customer: 'Alice Brown', date: '2024-01-17', status: 'Pending', total: 45.00, items: 1 },
{ id: 1005, customer: 'Charlie Wilson', date: '2024-01-17', status: 'Completed', total: 189.99, items: 4 },
{ id: 1006, customer: 'Diana Martinez', date: '2024-01-18', status: 'Processing', total: 129.50, items: 3 },
]);
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
Completed: '#10b981',
Processing: '#3b82f6',
Shipped: '#8b5cf6',
Pending: '#f59e0b',
};
return colors[status] || '#6b7280';
};
return (
<div style={styles.container}>
<h1 style={styles.title}>Order Grid</h1>
<div style={styles.grid}>
{orders.map(order => (
<div key={order.id} style={styles.card}>
<div style={styles.cardHeader}>
<span style={styles.orderId}>#{order.id}</span>
<span style={{
...styles.badge,
backgroundColor: `${getStatusColor(order.status)}33`,
color: getStatusColor(order.status)
}}>
{order.status}
</span>
</div>
<div style={styles.cardBody}>
<div style={styles.infoRow}>
<span style={styles.label}>Customer:</span>
<span style={styles.value}>{order.customer}</span>
</div>
<div style={styles.infoRow}>
<span style={styles.label}>Date:</span>
<span style={styles.value}>{order.date}</span>
</div>
<div style={styles.infoRow}>
<span style={styles.label}>Items:</span>
<span style={styles.value}>{order.items}</span>
</div>
</div>
<div style={styles.cardFooter}>
<span style={styles.total}>${order.total.toFixed(2)}</span>
</div>
</div>
))}
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
grid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: '1.5rem',
},
card: {
backgroundColor: '#1f2937',
borderRadius: '0.5rem',
border: '1px solid #374151',
overflow: 'hidden',
},
cardHeader: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '1rem',
backgroundColor: '#374151',
},
orderId: {
fontWeight: 'bold',
fontSize: '1.125rem',
color: '#f9fafb',
},
badge: {
padding: '0.25rem 0.75rem',
borderRadius: '9999px',
fontSize: '0.75rem',
fontWeight: '500',
},
cardBody: {
padding: '1rem',
},
infoRow: {
display: 'flex',
justifyContent: 'space-between',
marginBottom: '0.5rem',
},
label: {
color: '#9ca3af',
fontSize: '0.875rem',
},
value: {
color: '#d1d5db',
fontSize: '0.875rem',
},
cardFooter: {
padding: '1rem',
borderTop: '1px solid #374151',
textAlign: 'right',
},
total: {
fontSize: '1.5rem',
fontWeight: 'bold',
color: '#3b82f6',
},
};

View File

@ -0,0 +1,174 @@
import React, { useState, useEffect } from 'react';
interface Product {
id: number;
name: string;
sku: string;
price: number;
inventory_level: number;
is_visible: boolean;
}
export default function ProductDashboard() {
const [products, setProducts] = useState<Product[]>([]);
const [stats, setStats] = useState({
total: 0,
visible: 0,
lowStock: 0,
avgPrice: 0,
});
useEffect(() => {
// Client-side state initialization
// In production, fetch from BigCommerce API
const mockProducts: Product[] = [
{ id: 1, name: 'Product A', sku: 'SKU-001', price: 29.99, inventory_level: 50, is_visible: true },
{ id: 2, name: 'Product B', sku: 'SKU-002', price: 49.99, inventory_level: 5, is_visible: true },
{ id: 3, name: 'Product C', sku: 'SKU-003', price: 19.99, inventory_level: 100, is_visible: false },
];
setProducts(mockProducts);
const total = mockProducts.length;
const visible = mockProducts.filter(p => p.is_visible).length;
const lowStock = mockProducts.filter(p => p.inventory_level < 10).length;
const avgPrice = mockProducts.reduce((sum, p) => sum + p.price, 0) / total;
setStats({ total, visible, lowStock, avgPrice });
}, []);
return (
<div style={styles.container}>
<h1 style={styles.title}>Product Dashboard</h1>
<div style={styles.statsGrid}>
<div style={styles.statCard}>
<div style={styles.statValue}>{stats.total}</div>
<div style={styles.statLabel}>Total Products</div>
</div>
<div style={styles.statCard}>
<div style={styles.statValue}>{stats.visible}</div>
<div style={styles.statLabel}>Visible</div>
</div>
<div style={styles.statCard}>
<div style={{ ...styles.statValue, color: '#ef4444' }}>{stats.lowStock}</div>
<div style={styles.statLabel}>Low Stock</div>
</div>
<div style={styles.statCard}>
<div style={styles.statValue}>${stats.avgPrice.toFixed(2)}</div>
<div style={styles.statLabel}>Avg Price</div>
</div>
</div>
<div style={styles.tableContainer}>
<table style={styles.table}>
<thead>
<tr>
<th style={styles.th}>ID</th>
<th style={styles.th}>Name</th>
<th style={styles.th}>SKU</th>
<th style={styles.th}>Price</th>
<th style={styles.th}>Stock</th>
<th style={styles.th}>Status</th>
</tr>
</thead>
<tbody>
{products.map(product => (
<tr key={product.id} style={styles.tr}>
<td style={styles.td}>{product.id}</td>
<td style={styles.td}>{product.name}</td>
<td style={styles.td}>{product.sku}</td>
<td style={styles.td}>${product.price.toFixed(2)}</td>
<td style={{
...styles.td,
color: product.inventory_level < 10 ? '#ef4444' : '#10b981'
}}>
{product.inventory_level}
</td>
<td style={styles.td}>
<span style={{
...styles.badge,
backgroundColor: product.is_visible ? '#10b98133' : '#6b728033',
color: product.is_visible ? '#10b981' : '#9ca3af'
}}>
{product.is_visible ? 'Visible' : 'Hidden'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
statsGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '1rem',
marginBottom: '2rem',
},
statCard: {
backgroundColor: '#1f2937',
padding: '1.5rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
statValue: {
fontSize: '2rem',
fontWeight: 'bold',
color: '#3b82f6',
marginBottom: '0.5rem',
},
statLabel: {
fontSize: '0.875rem',
color: '#9ca3af',
},
tableContainer: {
backgroundColor: '#1f2937',
borderRadius: '0.5rem',
overflow: 'hidden',
border: '1px solid #374151',
},
table: {
width: '100%',
borderCollapse: 'collapse',
},
th: {
padding: '1rem',
textAlign: 'left',
backgroundColor: '#374151',
color: '#f9fafb',
fontWeight: '600',
fontSize: '0.875rem',
textTransform: 'uppercase',
},
tr: {
borderBottom: '1px solid #374151',
},
td: {
padding: '1rem',
color: '#d1d5db',
},
badge: {
padding: '0.25rem 0.75rem',
borderRadius: '9999px',
fontSize: '0.75rem',
fontWeight: '500',
},
};

View File

@ -0,0 +1,215 @@
import React, { useState } from 'react';
interface ProductDetail {
id: number;
name: string;
sku: string;
type: string;
price: number;
cost_price: number;
retail_price: number;
weight: number;
inventory_level: number;
is_visible: boolean;
is_featured: boolean;
description: string;
categories: string[];
brand: string;
}
export default function ProductDetail() {
const [product] = useState<ProductDetail>({
id: 123,
name: 'Premium Widget Pro',
sku: 'WIDGET-PRO-001',
type: 'physical',
price: 79.99,
cost_price: 45.00,
retail_price: 99.99,
weight: 2.5,
inventory_level: 156,
is_visible: true,
is_featured: true,
description: 'High-quality premium widget with advanced features and durable construction.',
categories: ['Electronics', 'Widgets', 'Premium'],
brand: 'WidgetCo',
});
const margin = ((product.price - product.cost_price) / product.price * 100).toFixed(1);
return (
<div style={styles.container}>
<div style={styles.header}>
<h1 style={styles.title}>{product.name}</h1>
<div style={styles.badges}>
{product.is_featured && (
<span style={{ ...styles.badge, backgroundColor: '#3b82f633', color: '#3b82f6' }}>
Featured
</span>
)}
{product.is_visible && (
<span style={{ ...styles.badge, backgroundColor: '#10b98133', color: '#10b981' }}>
Visible
</span>
)}
</div>
</div>
<div style={styles.grid}>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Basic Information</h2>
<div style={styles.infoGrid}>
<div style={styles.infoItem}>
<div style={styles.infoLabel}>Product ID</div>
<div style={styles.infoValue}>{product.id}</div>
</div>
<div style={styles.infoItem}>
<div style={styles.infoLabel}>SKU</div>
<div style={styles.infoValue}>{product.sku}</div>
</div>
<div style={styles.infoItem}>
<div style={styles.infoLabel}>Type</div>
<div style={styles.infoValue}>{product.type}</div>
</div>
<div style={styles.infoItem}>
<div style={styles.infoLabel}>Brand</div>
<div style={styles.infoValue}>{product.brand}</div>
</div>
</div>
</div>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Pricing</h2>
<div style={styles.infoGrid}>
<div style={styles.infoItem}>
<div style={styles.infoLabel}>Sale Price</div>
<div style={styles.infoValue}>${product.price.toFixed(2)}</div>
</div>
<div style={styles.infoItem}>
<div style={styles.infoLabel}>Cost Price</div>
<div style={styles.infoValue}>${product.cost_price.toFixed(2)}</div>
</div>
<div style={styles.infoItem}>
<div style={styles.infoLabel}>Retail Price (MSRP)</div>
<div style={styles.infoValue}>${product.retail_price.toFixed(2)}</div>
</div>
<div style={styles.infoItem}>
<div style={styles.infoLabel}>Profit Margin</div>
<div style={{ ...styles.infoValue, color: '#10b981' }}>{margin}%</div>
</div>
</div>
</div>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Inventory & Shipping</h2>
<div style={styles.infoGrid}>
<div style={styles.infoItem}>
<div style={styles.infoLabel}>Stock Level</div>
<div style={styles.infoValue}>{product.inventory_level} units</div>
</div>
<div style={styles.infoItem}>
<div style={styles.infoLabel}>Weight</div>
<div style={styles.infoValue}>{product.weight} lbs</div>
</div>
</div>
</div>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Description</h2>
<p style={styles.description}>{product.description}</p>
</div>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Categories</h2>
<div style={styles.tagContainer}>
{product.categories.map((cat, i) => (
<span key={i} style={styles.tag}>{cat}</span>
))}
</div>
</div>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
header: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '2rem',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
color: '#f9fafb',
},
badges: {
display: 'flex',
gap: '0.5rem',
},
badge: {
padding: '0.5rem 1rem',
borderRadius: '0.375rem',
fontSize: '0.875rem',
fontWeight: '500',
},
grid: {
display: 'grid',
gap: '1.5rem',
},
section: {
backgroundColor: '#1f2937',
padding: '1.5rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
sectionTitle: {
fontSize: '1.25rem',
fontWeight: '600',
marginBottom: '1rem',
color: '#f9fafb',
},
infoGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '1rem',
},
infoItem: {
display: 'flex',
flexDirection: 'column',
gap: '0.25rem',
},
infoLabel: {
fontSize: '0.875rem',
color: '#9ca3af',
},
infoValue: {
fontSize: '1.125rem',
fontWeight: '500',
color: '#f9fafb',
},
description: {
color: '#d1d5db',
lineHeight: '1.6',
},
tagContainer: {
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
},
tag: {
padding: '0.375rem 0.75rem',
backgroundColor: '#374151',
color: '#d1d5db',
borderRadius: '0.25rem',
fontSize: '0.875rem',
},
};

View File

@ -0,0 +1,117 @@
import React, { useState } from 'react';
interface Product {
id: number;
name: string;
sku: string;
price: number;
image: string;
stock: number;
}
export default function ProductGrid() {
const [products] = useState<Product[]>([
{ id: 1, name: 'Widget A', sku: 'WID-001', price: 29.99, image: '📦', stock: 50 },
{ id: 2, name: 'Widget B', sku: 'WID-002', price: 49.99, image: '📦', stock: 30 },
{ id: 3, name: 'Gadget Pro', sku: 'GAD-001', price: 79.99, image: '⚙️', stock: 15 },
{ id: 4, name: 'Tool Set', sku: 'TOL-001', price: 99.99, image: '🔧', stock: 8 },
{ id: 5, name: 'Premium Pack', sku: 'PRE-001', price: 149.99, image: '🎁', stock: 25 },
{ id: 6, name: 'Starter Kit', sku: 'STA-001', price: 39.99, image: '📦', stock: 100 },
]);
return (
<div style={styles.container}>
<h1 style={styles.title}>Product Grid</h1>
<div style={styles.grid}>
{products.map(product => (
<div key={product.id} style={styles.card}>
<div style={styles.imageContainer}>
<div style={styles.image}>{product.image}</div>
</div>
<div style={styles.content}>
<h3 style={styles.productName}>{product.name}</h3>
<p style={styles.sku}>{product.sku}</p>
<div style={styles.footer}>
<span style={styles.price}>${product.price.toFixed(2)}</span>
<span style={{
...styles.stock,
color: product.stock < 10 ? '#ef4444' : product.stock < 30 ? '#f59e0b' : '#10b981'
}}>
{product.stock} in stock
</span>
</div>
</div>
</div>
))}
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
grid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
gap: '1.5rem',
},
card: {
backgroundColor: '#1f2937',
borderRadius: '0.5rem',
overflow: 'hidden',
border: '1px solid #374151',
transition: 'transform 0.2s',
cursor: 'pointer',
},
imageContainer: {
backgroundColor: '#374151',
padding: '2rem',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
image: {
fontSize: '4rem',
},
content: {
padding: '1rem',
},
productName: {
fontSize: '1.125rem',
fontWeight: '600',
marginBottom: '0.25rem',
color: '#f9fafb',
},
sku: {
fontSize: '0.875rem',
color: '#9ca3af',
marginBottom: '1rem',
},
footer: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
price: {
fontSize: '1.25rem',
fontWeight: 'bold',
color: '#3b82f6',
},
stock: {
fontSize: '0.75rem',
fontWeight: '500',
},
};

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
export default function App() {
const appName = '${app}'.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return (
<div style={styles.container}>
<h1 style={styles.title}>{appName}</h1>
<div style={styles.content}>
<p style={styles.description}>
BigCommerce ${app.replace(/-/g, ' ')} interface
</p>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
container: {
padding: '2rem',
backgroundColor: '#111827',
minHeight: '100vh',
color: '#f9fafb',
fontFamily: 'system-ui, -apple-system, sans-serif',
},
title: {
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '2rem',
color: '#f9fafb',
},
content: {
backgroundColor: '#1f2937',
padding: '2rem',
borderRadius: '0.5rem',
border: '1px solid #374151',
},
description: {
color: '#d1d5db',
fontSize: '1.125rem',
},
};

View File

@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

235
servers/brevo/README.md Normal file
View File

@ -0,0 +1,235 @@
> **🚀 Don't want to self-host?** [Join the waitlist for our fully managed solution →](https://mcpengage.com/brevo)
>
> Zero setup. Zero maintenance. Just connect and automate.
---
# 🚀 Brevo MCP Server — 2026 Complete Version
## 💡 What This Unlocks
**This MCP server gives AI direct access to your entire Brevo email and SMS marketing workspace.** Instead of clicking through interfaces, you just *tell* it what you need.
Brevo (formerly Sendinblue) is a complete email and SMS marketing platform used by 500,000+ businesses worldwide. This MCP server brings all its power into your AI workflow.
### 🎯 Email/SMS Marketing Power Moves
Stop context-switching between Claude and Brevo. The AI can directly control your campaigns:
1. **Emergency campaign deployment** — "Send an urgent email about the service outage to all active customers, skip the test list"
2. **Smart segmentation** — "Export all contacts who opened our last 3 campaigns but didn't convert, then create a re-engagement campaign"
3. **Multi-channel orchestration** — "Check email deliverability for campaign #12345, if bounce rate is over 5%, send an SMS follow-up to non-openers"
4. **Template-driven automation** — "List all active email templates, use template #8 to send welcome emails to the 50 contacts added this week"
5. **Real-time list hygiene** — "Find all contacts with invalid emails from yesterday's imports, add them to the cleanup list, and notify me with stats"
### 🔗 The Real Power: Combining Tools
AI can chain multiple Brevo operations together:
- Query campaign metrics → Segment by engagement → Create targeted follow-up → Schedule SMS backup
- Import contacts → Validate emails → Auto-assign to lists → Trigger welcome sequence
- Analyze template performance → Clone best performers → Customize for new segments → Deploy and track
## 📦 What's Inside
**8 powerful API tools** covering Brevo's email and SMS marketing platform:
1. **send_email** — Send transactional emails with templates, attachments, and tracking
2. **list_contacts** — Query and filter your contact database with pagination
3. **add_contact** — Create contacts with custom attributes and list assignments
4. **update_contact** — Modify contact data, list memberships, and preferences
5. **list_campaigns** — Browse email campaigns by type, status, and date
6. **create_campaign** — Build and schedule email campaigns programmatically
7. **send_sms** — Send transactional SMS with delivery tracking
8. **list_templates** — Access your email template library
All with proper error handling, automatic authentication, and TypeScript types.
**API Foundation:** [Brevo API v3](https://developers.brevo.com/reference/getting-started-1) (REST)
## 🚀 Quick Start
### Option 1: Claude Desktop (Local)
1. **Clone and build:**
```bash
git clone https://github.com/BusyBee3333/Brevo-MCP-2026-Complete.git
cd brevo-mcp-2026-complete
npm install
npm run build
```
2. **Get your Brevo API key:**
- Log into [Brevo](https://app.brevo.com/)
- Go to **Settings → SMTP & API → API Keys**
- Create a new API key (v3) with email and SMS permissions
- Copy the key (you'll only see it once)
3. **Configure Claude Desktop:**
On macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"brevo": {
"command": "node",
"args": ["/ABSOLUTE/PATH/TO/brevo-mcp-2026-complete/dist/index.js"],
"env": {
"BREVO_API_KEY": "xkeysib-abc123..."
}
}
}
}
```
4. **Restart Claude Desktop**
### Option 2: Deploy to Railway
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/brevo-mcp)
1. Click the button above
2. Set `BREVO_API_KEY` in Railway dashboard
3. Use the Railway URL as your MCP server endpoint
### Option 3: Docker
```bash
docker build -t brevo-mcp .
docker run -p 3000:3000 \
-e BREVO_API_KEY=xkeysib-abc123... \
brevo-mcp
```
## 🔐 Authentication
**Brevo uses API key authentication** (v3 API):
- **Header:** `api-key: YOUR_KEY`
- **Format:** `xkeysib-...` (starts with xkeysib-)
- **Permissions:** Email campaigns, Contacts, SMS (depending on your plan)
- **Rate limits:** 300 calls/minute on free plans, higher on paid
Get your API key at: https://app.brevo.com/settings/keys/api
The MCP server handles authentication automatically—just set `BREVO_API_KEY`.
## 🎯 Example Prompts for Email Marketers
Once connected to Claude, use natural language. Here are real email marketing workflows:
### Campaign Management
- *"List all email campaigns from the last 30 days that are still in draft status"*
- *"Create a new campaign called 'Spring Sale 2026' with template #45, targeting list #12"*
- *"Show me all campaigns with 'webinar' in the name scheduled for this month"*
### Contact Operations
- *"Add these 5 contacts to list #8: [paste CSV data]"*
- *"Find all contacts with Gmail addresses who signed up this week"*
- *"Update contact jane@example.com: set FIRSTNAME to Jane, add to VIP list"*
### Multi-Channel Workflows
- *"Send a welcome email to everyone added to list #15 today using template #9"*
- *"If bounce rate on campaign #789 is over 3%, send SMS backup to all recipients"*
- *"List all templates with 'newsletter' in the name, show me #3's stats"*
### Bulk Operations
- *"Export all contacts modified in the last 7 days as JSON"*
- *"Send 'Account Verified' email to all contacts with VERIFIED=true attribute"*
- *"Check how many contacts are in lists #10, #11, and #12 combined"*
## 🛠️ Development
### Prerequisites
- Node.js 18+
- npm or yarn
- Brevo account (free or paid)
### Setup
```bash
git clone https://github.com/BusyBee3333/Brevo-MCP-2026-Complete.git
cd brevo-mcp-2026-complete
npm install
cp .env.example .env
# Edit .env with your Brevo API key
npm run build
npm start
```
### Testing
```bash
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # Coverage report
```
### Project Structure
```
brevo-mcp-2026-complete/
├── src/
│ └── index.ts # Main server implementation
├── dist/ # Compiled JavaScript
├── package.json
├── tsconfig.json
└── .env.example
```
## 🐛 Troubleshooting
### "Authentication failed"
- Verify your API key starts with `xkeysib-`
- Check key permissions at https://app.brevo.com/settings/keys/api
- Ensure your account is active (not suspended)
### "Rate limit exceeded"
- Free plans: 300 calls/minute
- Wait 60 seconds or upgrade to paid plan
- Use pagination (`limit` parameter) to reduce calls
### "Tools not appearing in Claude"
- Restart Claude Desktop after updating config
- Check that the path in `claude_desktop_config.json` is absolute (not relative)
- Verify the build completed: `ls dist/index.js`
- Check Claude Desktop logs: `tail -f ~/Library/Logs/Claude/mcp*.log`
### "Invalid list ID" or "Template not found"
- List IDs are numeric (e.g., 12, not "12")
- Get valid IDs: *"List all my contact lists"* or *"Show me all templates"*
## 📖 Resources
- **[Brevo API v3 Docs](https://developers.brevo.com/reference/getting-started-1)** — Official API reference
- **[Brevo Help Center](https://help.brevo.com/)** — Tutorials and guides
- **[MCP Protocol Spec](https://modelcontextprotocol.io/)** — How MCP servers work
- **[Claude Desktop Docs](https://claude.ai/desktop)** — Installing and configuring Claude
- **[MCPEngage Platform](https://mcpengine.pages.dev)** — Browse 30+ business MCP servers
## 🤝 Contributing
Contributions are welcome! Please:
1. Fork the repo
2. Create a feature branch (`git checkout -b feature/sms-analytics`)
3. Commit your changes (`git commit -m 'Add SMS campaign stats tool'`)
4. Push to the branch (`git push origin feature/sms-analytics`)
5. Open a Pull Request
## 📄 License
MIT License - see [LICENSE](LICENSE) for details
## 🙏 Credits
Built by [MCPEngage](https://mcpengage.com) — AI infrastructure for business software.
Want more MCP servers? Check out our [full catalog](https://mcpengage.com) covering 30+ business platforms including Constant Contact, Mailchimp, ActiveCampaign, and more.
---
**Questions?** Open an issue or join our [Discord community](https://discord.gg/mcpengage).

View File

@ -2,12 +2,19 @@
"name": "mcp-server-brevo",
"version": "1.0.0",
"type": "module",
"description": "MCP server for Brevo (formerly Sendinblue) email and SMS marketing API",
"main": "dist/index.js",
"bin": {
"mcp-server-brevo": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
"dev": "tsx src/index.ts",
"prepublishOnly": "npm run build"
},
"keywords": ["mcp", "brevo", "sendinblue", "email", "sms", "marketing"],
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"zod": "^3.22.4"

View File

@ -0,0 +1,129 @@
import React, { useState, useEffect } from 'react';
interface Task {
id: string;
name: string;
due_date: string;
priority: string;
status: string;
}
export default function App() {
const [tasks, setTasks] = useState<Task[]>([]);
const [currentDate, setCurrentDate] = useState(new Date());
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setTasks([
{ id: '1', name: 'Project proposal', due_date: '2024-02-15', priority: 'high', status: 'in progress' },
{ id: '2', name: 'Design review', due_date: '2024-02-14', priority: 'medium', status: 'open' },
{ id: '3', name: 'Critical bug fix', due_date: '2024-02-12', priority: 'urgent', status: 'in progress' },
{ id: '4', name: 'Documentation update', due_date: '2024-02-18', priority: 'low', status: 'open' },
{ id: '5', name: 'Team meeting', due_date: '2024-02-16', priority: 'medium', status: 'open' },
{ id: '6', name: 'API refactor', due_date: '2024-02-20', priority: 'high', status: 'open' },
]);
setLoading(false);
}, 500);
}, []);
const getDaysInMonth = (date: Date) => {
const year = date.getFullYear();
const month = date.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
return { daysInMonth, startingDayOfWeek };
};
const getTasksForDate = (date: Date) => {
const dateStr = date.toISOString().split('T')[0];
return tasks.filter(task => task.due_date.startsWith(dateStr));
};
const { daysInMonth, startingDayOfWeek } = getDaysInMonth(currentDate);
const days = Array.from({ length: daysInMonth }, (_, i) => i + 1);
const blanks = Array.from({ length: startingDayOfWeek }, (_, i) => i);
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
if (loading) return <div className="loading">Loading calendar...</div>;
return (
<div className="app">
<header>
<h1>📅 Calendar View</h1>
<p>Tasks organized by due date</p>
</header>
<div className="calendar-header">
<button onClick={() => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1))}>
</button>
<h2>{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}</h2>
<button onClick={() => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1))}>
</button>
</div>
<div className="calendar">
<div className="day-header">Sun</div>
<div className="day-header">Mon</div>
<div className="day-header">Tue</div>
<div className="day-header">Wed</div>
<div className="day-header">Thu</div>
<div className="day-header">Fri</div>
<div className="day-header">Sat</div>
{blanks.map(blank => <div key={`blank-${blank}`} className="day blank"></div>)}
{days.map(day => {
const date = new Date(currentDate.getFullYear(), currentDate.getMonth(), day);
const dayTasks = getTasksForDate(date);
const isToday = date.toDateString() === new Date().toDateString();
return (
<div key={day} className={`day ${isToday ? 'today' : ''}`}>
<div className="day-number">{day}</div>
<div className="day-tasks">
{dayTasks.map(task => (
<div key={task.id} className={`task-item priority-${task.priority}`}>
{task.name}
</div>
))}
</div>
</div>
);
})}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1400px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.calendar-header { display: flex; justify-content: space-between; align-items: center; background: #1e293b; padding: 1rem 2rem; border-radius: 8px; margin-bottom: 1.5rem; }
.calendar-header button { background: #334155; border: none; color: #e2e8f0; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 1.25rem; }
.calendar-header button:hover { background: #475569; }
.calendar-header h2 { font-size: 1.5rem; }
.calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; background: #334155; border-radius: 8px; overflow: hidden; }
.day-header { background: #1e293b; padding: 1rem; text-align: center; font-weight: 600; font-size: 0.875rem; }
.day { background: #1e293b; padding: 0.75rem; min-height: 120px; }
.day.blank { background: #0f172a; }
.day.today { background: #1e3a5f; border: 2px solid #3b82f6; }
.day-number { font-weight: 600; margin-bottom: 0.5rem; }
.day-tasks { display: flex; flex-direction: column; gap: 0.25rem; }
.task-item { font-size: 0.75rem; padding: 0.25rem 0.5rem; border-radius: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.task-item.priority-urgent { background: #991b1b; color: #fca5a5; }
.task-item.priority-high { background: #9a3412; color: #fdba74; }
.task-item.priority-medium { background: #854d0e; color: #fde047; }
.task-item.priority-low { background: #14532d; color: #86efac; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendar View - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3007, open: true },
});

View File

@ -0,0 +1,178 @@
import React, { useState, useEffect } from 'react';
interface ChecklistItem {
id: string;
text: string;
completed: boolean;
}
interface Checklist {
id: string;
name: string;
taskName: string;
items: ChecklistItem[];
}
export default function App() {
const [checklists, setChecklists] = useState<Checklist[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setChecklists([
{
id: 'cl1',
name: 'Pre-launch Checklist',
taskName: 'Product Launch',
items: [
{ id: 'i1', text: 'Complete security audit', completed: true },
{ id: 'i2', text: 'Run performance tests', completed: true },
{ id: 'i3', text: 'Update documentation', completed: false },
{ id: 'i4', text: 'Prepare marketing materials', completed: false },
{ id: 'i5', text: 'Deploy to production', completed: false },
]
},
{
id: 'cl2',
name: 'Code Review Checklist',
taskName: 'Feature: User Authentication',
items: [
{ id: 'i6', text: 'Check code style consistency', completed: true },
{ id: 'i7', text: 'Verify error handling', completed: true },
{ id: 'i8', text: 'Review test coverage', completed: true },
{ id: 'i9', text: 'Check for security vulnerabilities', completed: false },
{ id: 'i10', text: 'Validate edge cases', completed: false },
]
},
{
id: 'cl3',
name: 'Onboarding Tasks',
taskName: 'New Team Member - John',
items: [
{ id: 'i11', text: 'Setup development environment', completed: true },
{ id: 'i12', text: 'Grant access to repositories', completed: true },
{ id: 'i13', text: 'Complete security training', completed: true },
{ id: 'i14', text: 'Review codebase architecture', completed: false },
{ id: 'i15', text: 'First code contribution', completed: false },
{ id: 'i16', text: 'Team introduction meeting', completed: true },
]
},
]);
setLoading(false);
}, 500);
}, []);
const toggleItem = (checklistId: string, itemId: string) => {
setChecklists(checklists.map(cl => {
if (cl.id === checklistId) {
return {
...cl,
items: cl.items.map(item =>
item.id === itemId ? { ...item, completed: !item.completed } : item
)
};
}
return cl;
}));
};
if (loading) return <div className="loading">Loading checklists...</div>;
return (
<div className="app">
<header>
<h1> Checklist Manager</h1>
<p>Manage checklists with item completion tracking</p>
</header>
<div className="checklists-container">
{checklists.map(checklist => {
const completedCount = checklist.items.filter(item => item.completed).length;
const totalCount = checklist.items.length;
const progressPercent = (completedCount / totalCount) * 100;
return (
<div key={checklist.id} className="checklist-card">
<div className="checklist-header">
<div>
<h2>{checklist.name}</h2>
<p className="task-name">📝 {checklist.taskName}</p>
</div>
<div className="progress-circle">
<svg width="60" height="60">
<circle cx="30" cy="30" r="25" fill="none" stroke="#334155" strokeWidth="5" />
<circle
cx="30"
cy="30"
r="25"
fill="none"
stroke="#10b981"
strokeWidth="5"
strokeDasharray={`${2 * Math.PI * 25}`}
strokeDashoffset={`${2 * Math.PI * 25 * (1 - progressPercent / 100)}`}
transform="rotate(-90 30 30)"
/>
</svg>
<div className="progress-text">{Math.round(progressPercent)}%</div>
</div>
</div>
<div className="progress-bar-section">
<div className="progress-info">
<span>{completedCount} of {totalCount} completed</span>
</div>
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${progressPercent}%` }}></div>
</div>
</div>
<div className="items-list">
{checklist.items.map(item => (
<div
key={item.id}
className={`checklist-item ${item.completed ? 'completed' : ''}`}
onClick={() => toggleItem(checklist.id, item.id)}
>
<div className="checkbox">
{item.completed && '✓'}
</div>
<span className="item-text">{item.text}</span>
</div>
))}
</div>
</div>
);
})}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1200px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.checklists-container { display: grid; gap: 2rem; }
.checklist-card { background: #1e293b; padding: 2rem; border-radius: 8px; border-left: 4px solid #10b981; }
.checklist-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; }
.checklist-header h2 { font-size: 1.5rem; margin-bottom: 0.5rem; }
.task-name { color: #94a3b8; font-size: 0.875rem; }
.progress-circle { position: relative; width: 60px; height: 60px; }
.progress-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 0.875rem; font-weight: 700; }
.progress-bar-section { margin-bottom: 1.5rem; }
.progress-info { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.875rem; color: #94a3b8; }
.progress-bar { height: 8px; background: #334155; border-radius: 4px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #10b981 0%, #34d399 100%); transition: width 0.3s ease; }
.items-list { display: flex; flex-direction: column; gap: 0.75rem; }
.checklist-item { display: flex; align-items: center; gap: 1rem; padding: 1rem; background: #334155; border-radius: 6px; cursor: pointer; transition: all 0.2s; }
.checklist-item:hover { background: #3f4d63; }
.checklist-item.completed { opacity: 0.7; }
.checklist-item.completed .item-text { text-decoration: line-through; color: #64748b; }
.checkbox { width: 24px; height: 24px; border: 2px solid #64748b; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 1rem; transition: all 0.2s; flex-shrink: 0; }
.checklist-item.completed .checkbox { background: #10b981; border-color: #10b981; color: white; }
.item-text { flex: 1; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Checklist Manager - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3014, open: true },
});

View File

@ -0,0 +1,165 @@
import React, { useState, useEffect } from 'react';
interface Comment {
id: string;
author: string;
text: string;
timestamp: string;
replies: Comment[];
}
const CommentItem: React.FC<{ comment: Comment; level: number }> = ({ comment, level }) => (
<div className="comment" style={{ marginLeft: `${level * 2}rem` }}>
<div className="comment-header">
<div className="comment-avatar">{comment.author[0]}</div>
<div className="comment-meta">
<strong>{comment.author}</strong>
<span className="comment-time">{new Date(comment.timestamp).toLocaleString()}</span>
</div>
</div>
<div className="comment-text">{comment.text}</div>
{comment.replies.length > 0 && (
<div className="comment-replies">
{comment.replies.map(reply => (
<CommentItem key={reply.id} comment={reply} level={level + 1} />
))}
</div>
)}
</div>
);
export default function App() {
const [comments, setComments] = useState<Comment[]>([]);
const [taskName, setTaskName] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setTaskName('Implement user authentication system');
setComments([
{
id: 'c1',
author: 'Jane Smith',
text: 'Started working on the OAuth2 integration. Looking good so far!',
timestamp: '2024-02-12T10:30:00Z',
replies: [
{
id: 'c1-r1',
author: 'John Doe',
text: 'Great! Make sure to add proper error handling for token refresh.',
timestamp: '2024-02-12T11:00:00Z',
replies: [
{
id: 'c1-r1-r1',
author: 'Jane Smith',
text: 'Will do! Already added retry logic with exponential backoff.',
timestamp: '2024-02-12T11:15:00Z',
replies: []
}
]
},
{
id: 'c1-r2',
author: 'Bob Johnson',
text: 'Also remember to test with different OAuth providers.',
timestamp: '2024-02-12T14:30:00Z',
replies: []
}
]
},
{
id: 'c2',
author: 'John Doe',
text: 'Database schema looks good, approved! Just one suggestion: consider adding an index on the email column for faster lookups.',
timestamp: '2024-02-13T14:15:00Z',
replies: [
{
id: 'c2-r1',
author: 'Jane Smith',
text: 'Good catch! Added the index. Also added a unique constraint.',
timestamp: '2024-02-13T15:00:00Z',
replies: []
}
]
},
{
id: 'c3',
author: 'Alice Williams',
text: 'Need to discuss JWT token expiration time with the team. Current setting of 1 hour seems too short for our use case.',
timestamp: '2024-02-14T09:00:00Z',
replies: [
{
id: 'c3-r1',
author: 'John Doe',
text: "Let's schedule a quick call to discuss. I think we should keep access tokens short but implement refresh tokens.",
timestamp: '2024-02-14T09:30:00Z',
replies: []
},
{
id: 'c3-r2',
author: 'Jane Smith',
text: 'Agreed. I can implement refresh token rotation for better security.',
timestamp: '2024-02-14T10:00:00Z',
replies: []
}
]
},
{
id: 'c4',
author: 'Bob Johnson',
text: 'Completed code review. Everything looks solid. Just a few minor style suggestions in the PR comments.',
timestamp: '2024-02-15T16:00:00Z',
replies: []
}
]);
setLoading(false);
}, 500);
}, []);
if (loading) return <div className="loading">Loading comments...</div>;
return (
<div className="app">
<header>
<h1>💬 Comment Thread</h1>
<h2>{taskName}</h2>
<p>{comments.length} comments</p>
</header>
<div className="comments-container">
{comments.map(comment => (
<CommentItem key={comment.id} comment={comment} level={0} />
))}
</div>
<div className="new-comment-section">
<textarea placeholder="Add a comment..." className="comment-input"></textarea>
<button className="submit-button">Post Comment</button>
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 900px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header h2 { font-size: 1.25rem; color: #94a3b8; margin-bottom: 0.25rem; }
header p { color: #64748b; font-size: 0.875rem; }
.comments-container { display: flex; flex-direction: column; gap: 1.5rem; margin-bottom: 2rem; }
.comment { background: #1e293b; padding: 1.5rem; border-radius: 8px; border-left: 3px solid #60a5fa; }
.comment-header { display: flex; gap: 1rem; margin-bottom: 1rem; }
.comment-avatar { width: 40px; height: 40px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; font-weight: 700; flex-shrink: 0; }
.comment-meta { flex: 1; }
.comment-meta strong { display: block; margin-bottom: 0.25rem; }
.comment-time { font-size: 0.75rem; color: #64748b; }
.comment-text { line-height: 1.6; color: #e2e8f0; }
.comment-replies { margin-top: 1rem; display: flex; flex-direction: column; gap: 1rem; }
.new-comment-section { background: #1e293b; padding: 1.5rem; border-radius: 8px; }
.comment-input { width: 100%; min-height: 100px; padding: 1rem; background: #334155; border: 1px solid #475569; border-radius: 6px; color: #e2e8f0; font-family: inherit; font-size: 0.875rem; resize: vertical; margin-bottom: 1rem; }
.submit-button { padding: 0.75rem 2rem; background: #3b82f6; color: white; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; transition: background 0.2s; }
.submit-button:hover { background: #2563eb; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Comment Thread - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3013, open: true },
});

View File

@ -0,0 +1,129 @@
import React, { useState, useEffect } from 'react';
interface Doc {
id: string;
name: string;
content: string;
author: string;
lastModified: string;
tags: string[];
folder: string;
}
export default function App() {
const [docs, setDocs] = useState<Doc[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [selectedDoc, setSelectedDoc] = useState<Doc | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setDocs([
{ id: '1', name: 'API Documentation', content: '# API Documentation\n\nComplete reference for all API endpoints...', author: 'John Doe', lastModified: '2024-02-15', tags: ['api', 'reference'], folder: 'Development' },
{ id: '2', name: 'Project Requirements', content: '# Project Requirements\n\n## Overview\nThis document outlines...', author: 'Jane Smith', lastModified: '2024-02-14', tags: ['requirements', 'planning'], folder: 'Planning' },
{ id: '3', name: 'Design System Guide', content: '# Design System\n\n## Colors\n\n## Typography...', author: 'Bob Johnson', lastModified: '2024-02-13', tags: ['design', 'ui'], folder: 'Design' },
{ id: '4', name: 'Security Best Practices', content: '# Security Guide\n\n1. Authentication\n2. Authorization...', author: 'Alice Williams', lastModified: '2024-02-12', tags: ['security', 'guidelines'], folder: 'Development' },
{ id: '5', name: 'Onboarding Guide', content: '# Welcome!\n\nThis guide will help you get started...', author: 'Charlie Brown', lastModified: '2024-02-11', tags: ['onboarding', 'hr'], folder: 'HR' },
{ id: '6', name: 'Sprint Retrospective', content: '# Sprint 5 Retrospective\n\n## What went well...', author: 'Diana Prince', lastModified: '2024-02-10', tags: ['agile', 'retrospective'], folder: 'Planning' },
]);
setLoading(false);
}, 500);
}, []);
const filteredDocs = docs.filter(doc =>
doc.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
doc.content.toLowerCase().includes(searchTerm.toLowerCase()) ||
doc.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()))
);
if (loading) return <div className="loading">Loading documents...</div>;
return (
<div className="app">
<div className="sidebar">
<header>
<h1>📚 Documents</h1>
<input
type="text"
placeholder="Search documents..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</header>
<div className="doc-list">
{filteredDocs.map(doc => (
<div
key={doc.id}
className={`doc-item ${selectedDoc?.id === doc.id ? 'active' : ''}`}
onClick={() => setSelectedDoc(doc)}
>
<div className="doc-name">📄 {doc.name}</div>
<div className="doc-meta">
<span>{doc.folder}</span>
<span>{doc.lastModified}</span>
</div>
<div className="doc-tags">
{doc.tags.map(tag => <span key={tag} className="tag">{tag}</span>)}
</div>
</div>
))}
{filteredDocs.length === 0 && (
<div className="empty-state">No documents found</div>
)}
</div>
</div>
<div className="content-area">
{selectedDoc ? (
<>
<div className="doc-header">
<h2>{selectedDoc.name}</h2>
<div className="doc-info">
<span>By {selectedDoc.author}</span>
<span>Last modified: {selectedDoc.lastModified}</span>
</div>
</div>
<div className="doc-content">
<pre>{selectedDoc.content}</pre>
</div>
</>
) : (
<div className="placeholder">
<div className="placeholder-icon">📄</div>
<p>Select a document to view its content</p>
</div>
)}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { display: grid; grid-template-columns: 400px 1fr; height: 100vh; }
.sidebar { background: #1e293b; border-right: 1px solid #334155; display: flex; flex-direction: column; }
.sidebar header { padding: 2rem; border-bottom: 1px solid #334155; }
.sidebar h1 { font-size: 1.5rem; margin-bottom: 1rem; }
.search-input { width: 100%; padding: 0.75rem; background: #0f172a; border: 1px solid #334155; border-radius: 6px; color: #e2e8f0; font-size: 0.875rem; }
.doc-list { flex: 1; overflow-y: auto; padding: 1rem; }
.doc-item { padding: 1rem; background: #334155; border-radius: 6px; margin-bottom: 0.75rem; cursor: pointer; transition: all 0.2s; }
.doc-item:hover { background: #3f4d63; }
.doc-item.active { background: #1e40af; }
.doc-name { font-weight: 600; margin-bottom: 0.5rem; }
.doc-meta { display: flex; justify-content: space-between; font-size: 0.75rem; color: #94a3b8; margin-bottom: 0.5rem; }
.doc-tags { display: flex; gap: 0.25rem; flex-wrap: wrap; }
.tag { padding: 0.25rem 0.5rem; background: #1e293b; border-radius: 4px; font-size: 0.75rem; }
.content-area { padding: 2rem; overflow-y: auto; }
.doc-header { margin-bottom: 2rem; }
.doc-header h2 { font-size: 2rem; margin-bottom: 0.5rem; }
.doc-info { display: flex; gap: 2rem; font-size: 0.875rem; color: #94a3b8; }
.doc-content { background: #1e293b; padding: 2rem; border-radius: 8px; }
.doc-content pre { white-space: pre-wrap; line-height: 1.6; font-family: inherit; }
.placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #64748b; }
.placeholder-icon { font-size: 4rem; margin-bottom: 1rem; }
.empty-state { text-align: center; padding: 2rem; color: #64748b; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Browser - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3012, open: true },
});

View File

@ -0,0 +1,121 @@
import React, { useState, useEffect } from 'react';
interface Folder {
id: string;
name: string;
lists: Array<{ id: string; name: string; taskCount: number; openTasks: number; closedTasks: number }>;
totalTasks: number;
}
export default function App() {
const [folder, setFolder] = useState<Folder | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setFolder({
id: '1',
name: 'Frontend Development',
lists: [
{ id: 'l1', name: 'React Components', taskCount: 15, openTasks: 5, closedTasks: 10 },
{ id: 'l2', name: 'UI/UX Implementation', taskCount: 22, openTasks: 12, closedTasks: 10 },
{ id: 'l3', name: 'Bug Fixes', taskCount: 8, openTasks: 3, closedTasks: 5 },
{ id: 'l4', name: 'Performance Optimization', taskCount: 12, openTasks: 8, closedTasks: 4 },
{ id: 'l5', name: 'Testing', taskCount: 18, openTasks: 10, closedTasks: 8 },
],
totalTasks: 75
});
setLoading(false);
}, 500);
}, []);
if (loading) return <div className="loading">Loading folder...</div>;
if (!folder) return <div className="error">Folder not found</div>;
return (
<div className="app">
<header>
<h1>📁 {folder.name}</h1>
<p>Folder overview with lists and task summaries</p>
</header>
<div className="summary-card">
<div className="summary-item">
<div className="summary-value">{folder.lists.length}</div>
<div className="summary-label">Lists</div>
</div>
<div className="summary-item">
<div className="summary-value">{folder.totalTasks}</div>
<div className="summary-label">Total Tasks</div>
</div>
<div className="summary-item">
<div className="summary-value">{folder.lists.reduce((sum, l) => sum + l.openTasks, 0)}</div>
<div className="summary-label">Open</div>
</div>
<div className="summary-item">
<div className="summary-value">{folder.lists.reduce((sum, l) => sum + l.closedTasks, 0)}</div>
<div className="summary-label">Closed</div>
</div>
</div>
<div className="lists-container">
{folder.lists.map(list => {
const completion = (list.closedTasks / list.taskCount) * 100;
return (
<div key={list.id} className="list-card">
<div className="list-header">
<h3>{list.name}</h3>
<span className="task-count">{list.taskCount} tasks</span>
</div>
<div className="progress-section">
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${completion}%` }}></div>
</div>
<div className="progress-text">{completion.toFixed(0)}% complete</div>
</div>
<div className="list-stats">
<div className="stat">
<span className="stat-label">Open:</span>
<span className="stat-value open">{list.openTasks}</span>
</div>
<div className="stat">
<span className="stat-label">Closed:</span>
<span className="stat-value closed">{list.closedTasks}</span>
</div>
</div>
</div>
);
})}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1200px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.summary-card { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; background: #1e293b; padding: 2rem; border-radius: 8px; margin-bottom: 2rem; }
.summary-item { text-align: center; }
.summary-value { font-size: 2.5rem; font-weight: 700; color: #60a5fa; }
.summary-label { font-size: 0.875rem; color: #94a3b8; margin-top: 0.25rem; }
.lists-container { display: grid; gap: 1.5rem; }
.list-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #60a5fa; }
.list-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
.list-header h3 { font-size: 1.25rem; }
.task-count { font-size: 0.875rem; color: #94a3b8; }
.progress-section { margin-bottom: 1rem; }
.progress-bar { height: 8px; background: #334155; border-radius: 4px; overflow: hidden; margin-bottom: 0.5rem; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #60a5fa 0%, #34d399 100%); transition: width 0.3s ease; }
.progress-text { font-size: 0.875rem; color: #94a3b8; }
.list-stats { display: flex; gap: 2rem; }
.stat { display: flex; gap: 0.5rem; align-items: center; }
.stat-label { font-size: 0.875rem; color: #94a3b8; }
.stat-value { font-weight: 600; }
.stat-value.open { color: #60a5fa; }
.stat-value.closed { color: #34d399; }
.loading, .error { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Folder Overview - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3005, open: true },
});

View File

@ -0,0 +1,164 @@
import React, { useState, useEffect } from 'react';
interface Goal {
id: string;
name: string;
description: string;
progress: number;
target: number;
unit: string;
keyResults: Array<{ id: string; name: string; current: number; target: number; unit: string }>;
dueDate: string;
owner: string;
}
export default function App() {
const [goals, setGoals] = useState<Goal[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setGoals([
{
id: 'g1',
name: 'Launch New Product Features',
description: 'Complete and launch all planned features for Q1 2024',
progress: 12,
target: 20,
unit: 'features',
dueDate: '2024-03-31',
owner: 'Product Team',
keyResults: [
{ id: 'kr1', name: 'User authentication system', current: 100, target: 100, unit: '%' },
{ id: 'kr2', name: 'Payment integration', current: 75, target: 100, unit: '%' },
{ id: 'kr3', name: 'Dashboard redesign', current: 50, target: 100, unit: '%' },
{ id: 'kr4', name: 'Mobile app features', current: 30, target: 100, unit: '%' },
]
},
{
id: 'g2',
name: 'Improve Performance Metrics',
description: 'Reduce page load time and improve app responsiveness',
progress: 1500,
target: 2000,
unit: 'ms saved',
dueDate: '2024-02-28',
owner: 'Engineering Team',
keyResults: [
{ id: 'kr5', name: 'Reduce initial load time', current: 800, target: 1000, unit: 'ms saved' },
{ id: 'kr6', name: 'Optimize API responses', current: 500, target: 700, unit: 'ms saved' },
{ id: 'kr7', name: 'Implement caching', current: 200, target: 300, unit: 'ms saved' },
]
},
{
id: 'g3',
name: 'Increase Test Coverage',
description: 'Achieve 80% code coverage across all modules',
progress: 65,
target: 80,
unit: '%',
dueDate: '2024-03-15',
owner: 'QA Team',
keyResults: [
{ id: 'kr8', name: 'Frontend unit tests', current: 70, target: 80, unit: '%' },
{ id: 'kr9', name: 'Backend unit tests', current: 75, target: 85, unit: '%' },
{ id: 'kr10', name: 'Integration tests', current: 50, target: 75, unit: '%' },
]
},
]);
setLoading(false);
}, 500);
}, []);
if (loading) return <div className="loading">Loading goals...</div>;
return (
<div className="app">
<header>
<h1>🎯 Goal Tracker</h1>
<p>Track goals with key results and progress bars</p>
</header>
<div className="goals-list">
{goals.map(goal => {
const progressPercent = (goal.progress / goal.target) * 100;
return (
<div key={goal.id} className="goal-card">
<div className="goal-header">
<div>
<h2>{goal.name}</h2>
<p className="goal-description">{goal.description}</p>
</div>
<div className="goal-meta">
<div className="goal-owner">👤 {goal.owner}</div>
<div className="goal-due">📅 {goal.dueDate}</div>
</div>
</div>
<div className="goal-progress">
<div className="progress-header">
<span className="progress-label">Overall Progress</span>
<span className="progress-value">{goal.progress} / {goal.target} {goal.unit}</span>
</div>
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${progressPercent}%` }}></div>
</div>
<div className="progress-percent">{progressPercent.toFixed(0)}% complete</div>
</div>
<div className="key-results">
<h3>Key Results</h3>
{goal.keyResults.map(kr => {
const krPercent = (kr.current / kr.target) * 100;
return (
<div key={kr.id} className="key-result">
<div className="kr-header">
<span className="kr-name">{kr.name}</span>
<span className="kr-value">{kr.current} / {kr.target} {kr.unit}</span>
</div>
<div className="kr-progress-bar">
<div className="kr-progress-fill" style={{ width: `${krPercent}%` }}></div>
</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1200px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.goals-list { display: flex; flex-direction: column; gap: 2rem; }
.goal-card { background: #1e293b; padding: 2rem; border-radius: 8px; border-left: 4px solid #7c3aed; }
.goal-header { display: flex; justify-content: space-between; margin-bottom: 1.5rem; gap: 2rem; }
.goal-header h2 { font-size: 1.5rem; margin-bottom: 0.5rem; }
.goal-description { color: #94a3b8; line-height: 1.6; }
.goal-meta { display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem; color: #94a3b8; white-space: nowrap; }
.goal-progress { margin-bottom: 2rem; }
.progress-header { display: flex; justify-content: space-between; margin-bottom: 0.5rem; }
.progress-label { font-weight: 600; }
.progress-value { color: #94a3b8; }
.progress-bar { height: 12px; background: #334155; border-radius: 6px; overflow: hidden; margin-bottom: 0.5rem; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #7c3aed 0%, #a78bfa 100%); transition: width 0.3s ease; }
.progress-percent { font-size: 0.875rem; color: #94a3b8; }
.key-results h3 { font-size: 1.125rem; margin-bottom: 1rem; }
.key-result { margin-bottom: 1rem; }
.kr-header { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.875rem; }
.kr-name { font-weight: 600; }
.kr-value { color: #94a3b8; }
.kr-progress-bar { height: 6px; background: #334155; border-radius: 3px; overflow: hidden; }
.kr-progress-fill { height: 100%; background: #60a5fa; transition: width 0.3s ease; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Goal Tracker - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3008, open: true },
});

View File

@ -0,0 +1,148 @@
import React, { useState, useEffect } from 'react';
interface List {
id: string;
name: string;
description: string;
tasks: Array<{
id: string;
name: string;
status: string;
priority: string;
assignee: string;
due_date: string;
tags: string[];
}>;
}
export default function App() {
const [list, setList] = useState<List | null>(null);
const [sortKey, setSortKey] = useState<'name' | 'priority' | 'due_date'>('due_date');
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setList({
id: '1',
name: 'Sprint 5 - Product Features',
description: 'Development tasks for the fifth sprint focused on new product features',
tasks: [
{ id: 't1', name: 'User authentication', status: 'in progress', priority: 'high', assignee: 'John Doe', due_date: '2024-02-15', tags: ['backend', 'security'] },
{ id: 't2', name: 'Payment integration', status: 'open', priority: 'urgent', assignee: 'Jane Smith', due_date: '2024-02-14', tags: ['backend', 'payments'] },
{ id: 't3', name: 'Dashboard UI', status: 'in progress', priority: 'medium', assignee: 'Bob Johnson', due_date: '2024-02-18', tags: ['frontend', 'ui'] },
{ id: 't4', name: 'API documentation', status: 'open', priority: 'low', assignee: 'Alice Williams', due_date: '2024-02-20', tags: ['docs'] },
{ id: 't5', name: 'Database migration', status: 'closed', priority: 'high', assignee: 'John Doe', due_date: '2024-02-12', tags: ['backend', 'database'] },
{ id: 't6', name: 'Email notifications', status: 'in progress', priority: 'medium', assignee: 'Jane Smith', due_date: '2024-02-16', tags: ['backend', 'notifications'] },
]
});
setLoading(false);
}, 500);
}, []);
if (loading) return <div className="loading">Loading list...</div>;
if (!list) return <div className="error">List not found</div>;
const sortedTasks = [...list.tasks].sort((a, b) => {
if (sortKey === 'due_date') {
return new Date(a.due_date).getTime() - new Date(b.due_date).getTime();
}
return a[sortKey] > b[sortKey] ? 1 : -1;
});
const statusCounts = list.tasks.reduce((acc, task) => {
acc[task.status] = (acc[task.status] || 0) + 1;
return acc;
}, {} as Record<string, number>);
return (
<div className="app">
<header>
<h1>📋 {list.name}</h1>
<p>{list.description}</p>
</header>
<div className="stats-bar">
<div className="stat-item">
<span className="stat-value">{list.tasks.length}</span>
<span className="stat-label">Total</span>
</div>
<div className="stat-item">
<span className="stat-value" style={{color: '#60a5fa'}}>{statusCounts['open'] || 0}</span>
<span className="stat-label">Open</span>
</div>
<div className="stat-item">
<span className="stat-value" style={{color: '#a78bfa'}}>{statusCounts['in progress'] || 0}</span>
<span className="stat-label">In Progress</span>
</div>
<div className="stat-item">
<span className="stat-value" style={{color: '#34d399'}}>{statusCounts['closed'] || 0}</span>
<span className="stat-label">Closed</span>
</div>
</div>
<div className="controls">
<label>
Sort by:
<select value={sortKey} onChange={(e) => setSortKey(e.target.value as any)}>
<option value="due_date">Due Date</option>
<option value="priority">Priority</option>
<option value="name">Name</option>
</select>
</label>
</div>
<div className="task-table">
<div className="table-header">
<div className="col-task">Task</div>
<div className="col-status">Status</div>
<div className="col-priority">Priority</div>
<div className="col-assignee">Assignee</div>
<div className="col-due">Due Date</div>
<div className="col-tags">Tags</div>
</div>
{sortedTasks.map(task => (
<div key={task.id} className="table-row">
<div className="col-task">{task.name}</div>
<div className="col-status"><span className={`status-badge ${task.status}`}>{task.status}</span></div>
<div className="col-priority"><span className={`priority-badge ${task.priority}`}>{task.priority}</span></div>
<div className="col-assignee">{task.assignee}</div>
<div className="col-due">{task.due_date}</div>
<div className="col-tags">
{task.tags.map(tag => <span key={tag} className="tag">{tag}</span>)}
</div>
</div>
))}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1400px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.stats-bar { display: flex; gap: 2rem; background: #1e293b; padding: 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; }
.stat-item { display: flex; flex-direction: column; align-items: center; }
.stat-value { font-size: 1.75rem; font-weight: 700; }
.stat-label { font-size: 0.875rem; color: #94a3b8; }
.controls { margin-bottom: 1rem; }
.controls select { padding: 0.5rem; background: #1e293b; border: 1px solid #334155; color: #e2e8f0; border-radius: 4px; margin-left: 0.5rem; }
.task-table { background: #1e293b; border-radius: 8px; overflow: hidden; }
.table-header, .table-row { display: grid; grid-template-columns: 2fr 1fr 1fr 1.5fr 1fr 1.5fr; padding: 1rem; gap: 1rem; }
.table-header { background: #334155; font-weight: 600; border-bottom: 2px solid #475569; }
.table-row { border-bottom: 1px solid #334155; transition: background 0.2s; }
.table-row:hover { background: #2d3748; }
.status-badge, .priority-badge { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; white-space: nowrap; }
.status-badge.open { background: #1e40af; color: #93c5fd; }
.status-badge.in.progress { background: #7c3aed; color: #c4b5fd; }
.status-badge.closed { background: #065f46; color: #6ee7b7; }
.priority-badge.urgent { background: #991b1b; color: #fca5a5; }
.priority-badge.high { background: #9a3412; color: #fdba74; }
.priority-badge.medium { background: #854d0e; color: #fde047; }
.priority-badge.low { background: #14532d; color: #86efac; }
.tag { display: inline-block; padding: 0.25rem 0.5rem; background: #334155; border-radius: 4px; font-size: 0.75rem; margin-right: 0.25rem; }
.loading, .error { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>List View - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3006, open: true },
});

View File

@ -0,0 +1,152 @@
import React, { useState, useEffect } from 'react';
interface Member {
id: string;
name: string;
avatar: string;
taskCounts: { total: number; open: number; inProgress: number; overdue: number };
timeLogged: number;
capacity: number;
}
export default function App() {
const [members, setMembers] = useState<Member[]>([]);
const [sortBy, setSortBy] = useState<'name' | 'tasks' | 'time' | 'overdue'>('tasks');
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setMembers([
{ id: '1', name: 'John Doe', avatar: 'JD', taskCounts: { total: 15, open: 5, inProgress: 8, overdue: 2 }, timeLogged: 42.5, capacity: 40 },
{ id: '2', name: 'Jane Smith', avatar: 'JS', taskCounts: { total: 18, open: 6, inProgress: 10, overdue: 2 }, timeLogged: 38.0, capacity: 40 },
{ id: '3', name: 'Bob Johnson', avatar: 'BJ', taskCounts: { total: 12, open: 4, inProgress: 7, overdue: 1 }, timeLogged: 35.5, capacity: 40 },
{ id: '4', name: 'Alice Williams', avatar: 'AW', taskCounts: { total: 14, open: 3, inProgress: 9, overdue: 2 }, timeLogged: 31.5, capacity: 40 },
{ id: '5', name: 'Charlie Brown', avatar: 'CB', taskCounts: { total: 10, open: 2, inProgress: 8, overdue: 0 }, timeLogged: 40.0, capacity: 40 },
{ id: '6', name: 'Diana Prince', avatar: 'DP', taskCounts: { total: 16, open: 5, inProgress: 9, overdue: 2 }, timeLogged: 36.5, capacity: 40 },
]);
setLoading(false);
}, 500);
}, []);
const sortedMembers = [...members].sort((a, b) => {
switch(sortBy) {
case 'tasks': return b.taskCounts.total - a.taskCounts.total;
case 'time': return b.timeLogged - a.timeLogged;
case 'overdue': return b.taskCounts.overdue - a.taskCounts.overdue;
default: return a.name.localeCompare(b.name);
}
});
if (loading) return <div className="loading">Loading member workload...</div>;
return (
<div className="app">
<header>
<h1>👥 Member Workload</h1>
<p>Per-member task counts, time logged, and overdue tasks</p>
</header>
<div className="controls">
<label>
Sort by:
<select value={sortBy} onChange={(e) => setSortBy(e.target.value as any)}>
<option value="name">Name</option>
<option value="tasks">Task Count</option>
<option value="time">Time Logged</option>
<option value="overdue">Overdue Tasks</option>
</select>
</label>
</div>
<div className="members-grid">
{sortedMembers.map(member => {
const capacityPercent = (member.timeLogged / member.capacity) * 100;
const isOverCapacity = capacityPercent > 100;
return (
<div key={member.id} className="member-card">
<div className="member-header">
<div className="member-avatar">{member.avatar}</div>
<div className="member-info">
<h3>{member.name}</h3>
<div className="member-stats-quick">
{member.taskCounts.total} tasks {member.timeLogged}h logged
</div>
</div>
</div>
<div className="capacity-section">
<div className="capacity-header">
<span>Capacity</span>
<span className={isOverCapacity ? 'over-capacity' : ''}>
{member.timeLogged}h / {member.capacity}h
</span>
</div>
<div className="capacity-bar">
<div
className={`capacity-fill ${isOverCapacity ? 'over' : ''}`}
style={{ width: `${Math.min(capacityPercent, 100)}%` }}
></div>
</div>
</div>
<div className="task-breakdown">
<div className="breakdown-item">
<div className="breakdown-label">Open</div>
<div className="breakdown-value open">{member.taskCounts.open}</div>
</div>
<div className="breakdown-item">
<div className="breakdown-label">In Progress</div>
<div className="breakdown-value in-progress">{member.taskCounts.inProgress}</div>
</div>
<div className="breakdown-item">
<div className="breakdown-label">Overdue</div>
<div className="breakdown-value overdue">{member.taskCounts.overdue}</div>
</div>
</div>
{member.taskCounts.overdue > 0 && (
<div className="warning">
{member.taskCounts.overdue} overdue task{member.taskCounts.overdue > 1 ? 's' : ''}
</div>
)}
</div>
);
})}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1400px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.controls { margin-bottom: 1.5rem; }
.controls select { padding: 0.75rem 1rem; background: #1e293b; border: 1px solid #334155; color: #e2e8f0; border-radius: 6px; cursor: pointer; margin-left: 0.5rem; }
.members-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem; }
.member-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #60a5fa; }
.member-header { display: flex; gap: 1rem; margin-bottom: 1.5rem; }
.member-avatar { width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; font-size: 1.25rem; font-weight: 700; flex-shrink: 0; }
.member-info { flex: 1; }
.member-info h3 { font-size: 1.125rem; margin-bottom: 0.25rem; }
.member-stats-quick { font-size: 0.875rem; color: #94a3b8; }
.capacity-section { margin-bottom: 1.5rem; }
.capacity-header { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.875rem; }
.over-capacity { color: #ef4444; font-weight: 600; }
.capacity-bar { height: 8px; background: #334155; border-radius: 4px; overflow: hidden; }
.capacity-fill { height: 100%; background: linear-gradient(90deg, #60a5fa 0%, #a78bfa 100%); transition: width 0.3s ease; }
.capacity-fill.over { background: linear-gradient(90deg, #ef4444 0%, #dc2626 100%); }
.task-breakdown { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1rem; }
.breakdown-item { text-align: center; padding: 0.75rem; background: #334155; border-radius: 6px; }
.breakdown-label { font-size: 0.75rem; color: #94a3b8; margin-bottom: 0.25rem; }
.breakdown-value { font-size: 1.5rem; font-weight: 700; }
.breakdown-value.open { color: #60a5fa; }
.breakdown-value.in-progress { color: #a78bfa; }
.breakdown-value.overdue { color: #ef4444; }
.warning { padding: 0.75rem; background: rgba(239, 68, 68, 0.1); border: 1px solid #991b1b; border-radius: 6px; color: #fca5a5; font-size: 0.875rem; text-align: center; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Member Workload - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3011, open: true },
});

View File

@ -0,0 +1,158 @@
import React, { useState, useEffect } from 'react';
interface Space {
id: string;
name: string;
folders: Array<{ id: string; name: string; listCount: number }>;
lists: Array<{ id: string; name: string; taskCount: number }>;
members: Array<{ id: string; name: string; role: string }>;
taskStats: { total: number; open: number; inProgress: number; closed: number };
}
export default function App() {
const [space, setSpace] = useState<Space | null>(null);
const [activeTab, setActiveTab] = useState<'folders' | 'lists' | 'members'>('folders');
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setSpace({
id: '1',
name: 'Product Development',
folders: [
{ id: 'f1', name: 'Frontend', listCount: 5 },
{ id: 'f2', name: 'Backend', listCount: 7 },
{ id: 'f3', name: 'Design', listCount: 3 },
{ id: 'f4', name: 'QA', listCount: 4 },
],
lists: [
{ id: 'l1', name: 'Sprint Planning', taskCount: 12 },
{ id: 'l2', name: 'Backlog', taskCount: 45 },
{ id: 'l3', name: 'Bugs', taskCount: 8 },
],
members: [
{ id: 'm1', name: 'John Doe', role: 'Admin' },
{ id: 'm2', name: 'Jane Smith', role: 'Member' },
{ id: 'm3', name: 'Bob Johnson', role: 'Member' },
{ id: 'm4', name: 'Alice Williams', role: 'Guest' },
],
taskStats: { total: 65, open: 25, inProgress: 18, closed: 22 }
});
setLoading(false);
}, 500);
}, []);
if (loading) return <div className="loading">Loading space...</div>;
if (!space) return <div className="error">Space not found</div>;
return (
<div className="app">
<header>
<h1>🚀 {space.name}</h1>
<p>Space overview with folders, lists, and team members</p>
</header>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-label">Total Tasks</div>
<div className="stat-value">{space.taskStats.total}</div>
</div>
<div className="stat-card">
<div className="stat-label">Open</div>
<div className="stat-value" style={{color: '#60a5fa'}}>{space.taskStats.open}</div>
</div>
<div className="stat-card">
<div className="stat-label">In Progress</div>
<div className="stat-value" style={{color: '#a78bfa'}}>{space.taskStats.inProgress}</div>
</div>
<div className="stat-card">
<div className="stat-label">Closed</div>
<div className="stat-value" style={{color: '#34d399'}}>{space.taskStats.closed}</div>
</div>
</div>
<div className="tabs">
<button className={activeTab === 'folders' ? 'active' : ''} onClick={() => setActiveTab('folders')}>
📁 Folders ({space.folders.length})
</button>
<button className={activeTab === 'lists' ? 'active' : ''} onClick={() => setActiveTab('lists')}>
📋 Lists ({space.lists.length})
</button>
<button className={activeTab === 'members' ? 'active' : ''} onClick={() => setActiveTab('members')}>
👥 Members ({space.members.length})
</button>
</div>
<div className="content">
{activeTab === 'folders' && (
<div className="grid">
{space.folders.map(folder => (
<div key={folder.id} className="card">
<div className="card-icon">📁</div>
<div className="card-title">{folder.name}</div>
<div className="card-subtitle">{folder.listCount} lists</div>
</div>
))}
</div>
)}
{activeTab === 'lists' && (
<div className="grid">
{space.lists.map(list => (
<div key={list.id} className="card">
<div className="card-icon">📋</div>
<div className="card-title">{list.name}</div>
<div className="card-subtitle">{list.taskCount} tasks</div>
</div>
))}
</div>
)}
{activeTab === 'members' && (
<div className="members-list">
{space.members.map(member => (
<div key={member.id} className="member-item">
<div className="member-avatar">{member.name[0]}</div>
<div className="member-info">
<div className="member-name">{member.name}</div>
<div className="member-role">{member.role}</div>
</div>
</div>
))}
</div>
)}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1200px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.stat-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; }
.stat-label { font-size: 0.875rem; color: #94a3b8; margin-bottom: 0.5rem; }
.stat-value { font-size: 2rem; font-weight: 700; }
.tabs { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; border-bottom: 2px solid #334155; }
.tabs button { padding: 0.75rem 1.5rem; background: none; border: none; color: #94a3b8; cursor: pointer; font-size: 0.875rem; font-weight: 600; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.2s; }
.tabs button:hover { color: #e2e8f0; }
.tabs button.active { color: #60a5fa; border-bottom-color: #60a5fa; }
.content { background: #1e293b; padding: 1.5rem; border-radius: 8px; min-height: 300px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }
.card { background: #334155; padding: 1.5rem; border-radius: 8px; text-align: center; cursor: pointer; transition: transform 0.2s; }
.card:hover { transform: translateY(-4px); }
.card-icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
.card-title { font-weight: 600; margin-bottom: 0.25rem; }
.card-subtitle { font-size: 0.875rem; color: #94a3b8; }
.members-list { display: flex; flex-direction: column; gap: 1rem; }
.member-item { display: flex; align-items: center; gap: 1rem; padding: 1rem; background: #334155; border-radius: 8px; }
.member-avatar { width: 48px; height: 48px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; font-size: 1.25rem; font-weight: 600; }
.member-info { flex: 1; }
.member-name { font-weight: 600; }
.member-role { font-size: 0.875rem; color: #94a3b8; }
.loading, .error { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Space Overview - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3004, open: true },
});

View File

@ -0,0 +1,188 @@
import React, { useState, useEffect } from 'react';
interface Tag {
id: string;
name: string;
color: string;
taskCount: number;
tasks: Array<{ id: string; name: string; status: string }>;
}
export default function App() {
const [tags, setTags] = useState<Tag[]>([]);
const [selectedTag, setSelectedTag] = useState<Tag | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setTags([
{
id: 't1',
name: 'urgent',
color: '#ef4444',
taskCount: 8,
tasks: [
{ id: 'task1', name: 'Fix critical production bug', status: 'in progress' },
{ id: 'task2', name: 'Security vulnerability patch', status: 'open' },
{ id: 'task3', name: 'Database migration', status: 'closed' },
]
},
{
id: 't2',
name: 'backend',
color: '#3b82f6',
taskCount: 15,
tasks: [
{ id: 'task4', name: 'API optimization', status: 'in progress' },
{ id: 'task5', name: 'Implement caching', status: 'open' },
{ id: 'task6', name: 'Database schema update', status: 'in progress' },
]
},
{
id: 't3',
name: 'frontend',
color: '#10b981',
taskCount: 12,
tasks: [
{ id: 'task7', name: 'Redesign dashboard', status: 'in progress' },
{ id: 'task8', name: 'Add dark mode', status: 'open' },
{ id: 'task9', name: 'Responsive layout fixes', status: 'closed' },
]
},
{
id: 't4',
name: 'design',
color: '#a78bfa',
taskCount: 9,
tasks: [
{ id: 'task10', name: 'Create design system', status: 'in progress' },
{ id: 'task11', name: 'UI mockups for new feature', status: 'open' },
]
},
{
id: 't5',
name: 'testing',
color: '#f59e0b',
taskCount: 11,
tasks: [
{ id: 'task12', name: 'Write integration tests', status: 'in progress' },
{ id: 'task13', name: 'E2E test coverage', status: 'open' },
{ id: 'task14', name: 'Performance testing', status: 'open' },
]
},
{
id: 't6',
name: 'documentation',
color: '#6366f1',
taskCount: 7,
tasks: [
{ id: 'task15', name: 'API documentation', status: 'in progress' },
{ id: 'task16', name: 'Update README', status: 'closed' },
]
},
]);
setLoading(false);
}, 500);
}, []);
if (loading) return <div className="loading">Loading tags...</div>;
return (
<div className="app">
<div className="sidebar">
<header>
<h1>🏷 Tag Manager</h1>
<p>{tags.length} tags {tags.reduce((sum, tag) => sum + tag.taskCount, 0)} tasks</p>
</header>
<div className="tags-list">
{tags.map(tag => (
<div
key={tag.id}
className={`tag-item ${selectedTag?.id === tag.id ? 'active' : ''}`}
onClick={() => setSelectedTag(tag)}
>
<div className="tag-color" style={{ backgroundColor: tag.color }}></div>
<div className="tag-info">
<div className="tag-name">{tag.name}</div>
<div className="tag-count">{tag.taskCount} tasks</div>
</div>
</div>
))}
</div>
</div>
<div className="content-area">
{selectedTag ? (
<>
<div className="tag-header">
<div className="tag-badge" style={{ backgroundColor: selectedTag.color }}>
{selectedTag.name}
</div>
<div className="tag-stats">
{selectedTag.taskCount} tasks using this tag
</div>
</div>
<div className="tasks-list">
{selectedTag.tasks.map(task => (
<div key={task.id} className="task-card">
<div className="task-name">{task.name}</div>
<span className={`status-badge ${task.status}`}>{task.status}</span>
</div>
))}
</div>
<div className="tag-actions">
<button className="action-button edit">Edit Tag</button>
<button className="action-button delete">Delete Tag</button>
</div>
</>
) : (
<div className="placeholder">
<div className="placeholder-icon">🏷</div>
<p>Select a tag to view associated tasks</p>
</div>
)}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { display: grid; grid-template-columns: 350px 1fr; height: 100vh; }
.sidebar { background: #1e293b; border-right: 1px solid #334155; display: flex; flex-direction: column; }
.sidebar header { padding: 2rem; border-bottom: 1px solid #334155; }
.sidebar h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
.sidebar p { color: #94a3b8; font-size: 0.875rem; }
.tags-list { flex: 1; overflow-y: auto; padding: 1rem; }
.tag-item { display: flex; align-items: center; gap: 1rem; padding: 1rem; background: #334155; border-radius: 6px; margin-bottom: 0.75rem; cursor: pointer; transition: all 0.2s; }
.tag-item:hover { background: #3f4d63; }
.tag-item.active { background: #1e40af; }
.tag-color { width: 24px; height: 24px; border-radius: 4px; flex-shrink: 0; }
.tag-info { flex: 1; }
.tag-name { font-weight: 600; margin-bottom: 0.25rem; }
.tag-count { font-size: 0.75rem; color: #94a3b8; }
.content-area { padding: 2rem; overflow-y: auto; }
.tag-header { margin-bottom: 2rem; }
.tag-badge { display: inline-block; padding: 0.5rem 1rem; border-radius: 6px; color: white; font-weight: 600; font-size: 1.25rem; margin-bottom: 0.5rem; }
.tag-stats { color: #94a3b8; font-size: 0.875rem; }
.tasks-list { display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 2rem; }
.task-card { display: flex; justify-content: space-between; align-items: center; padding: 1rem; background: #1e293b; border-radius: 6px; }
.task-name { font-weight: 600; }
.status-badge { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; white-space: nowrap; }
.status-badge.open { background: #1e40af; color: #93c5fd; }
.status-badge.in.progress { background: #7c3aed; color: #c4b5fd; }
.status-badge.closed { background: #065f46; color: #6ee7b7; }
.tag-actions { display: flex; gap: 1rem; }
.action-button { padding: 0.75rem 1.5rem; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
.action-button.edit { background: #3b82f6; color: white; }
.action-button.edit:hover { background: #2563eb; }
.action-button.delete { background: #334155; color: #e2e8f0; }
.action-button.delete:hover { background: #ef4444; }
.placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #64748b; }
.placeholder-icon { font-size: 4rem; margin-bottom: 1rem; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tag Manager - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3015, open: true },
});

View File

@ -0,0 +1,116 @@
import React, { useState, useEffect } from 'react';
interface Task {
id: string;
name: string;
status: string;
priority: string;
assignee: string;
due_date: string;
}
interface Column {
id: string;
title: string;
tasks: Task[];
}
export default function App() {
const [columns, setColumns] = useState<Column[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Mock data
setTimeout(() => {
const mockTasks: Task[] = [
{ id: '1', name: 'Complete project proposal', status: 'To Do', priority: 'high', assignee: 'John Doe', due_date: '2024-02-15' },
{ id: '2', name: 'Review design mockups', status: 'To Do', priority: 'medium', assignee: 'Jane Smith', due_date: '2024-02-14' },
{ id: '3', name: 'Fix critical bug', status: 'In Progress', priority: 'urgent', assignee: 'Alice Williams', due_date: '2024-02-12' },
{ id: '4', name: 'Refactor API endpoints', status: 'In Progress', priority: 'high', assignee: 'Jane Smith', due_date: '2024-02-18' },
{ id: '5', name: 'Update documentation', status: 'In Review', priority: 'low', assignee: 'Bob Johnson', due_date: '2024-02-10' },
{ id: '6', name: 'Deploy to staging', status: 'Done', priority: 'medium', assignee: 'John Doe', due_date: '2024-02-08' },
{ id: '7', name: 'Write unit tests', status: 'To Do', priority: 'medium', assignee: 'Bob Johnson', due_date: '2024-02-20' },
];
const statuses = ['To Do', 'In Progress', 'In Review', 'Done'];
const cols = statuses.map(status => ({
id: status,
title: status,
tasks: mockTasks.filter(task => task.status === status)
}));
setColumns(cols);
setLoading(false);
}, 500);
}, []);
if (loading) {
return <div className="loading">Loading board...</div>;
}
return (
<div className="app">
<header>
<h1>📊 Task Board</h1>
<p>Kanban board view organized by status</p>
</header>
<div className="board">
{columns.map(column => (
<div key={column.id} className="column">
<div className="column-header">
<h3>{column.title}</h3>
<span className="task-count">{column.tasks.length}</span>
</div>
<div className="task-list">
{column.tasks.map(task => (
<div key={task.id} className={`task-card priority-${task.priority}`}>
<div className="task-title">{task.name}</div>
<div className="task-meta">
<span className={`priority-badge ${task.priority}`}>{task.priority}</span>
<span className="assignee">👤 {task.assignee}</span>
</div>
<div className="task-due">📅 {task.due_date}</div>
</div>
))}
{column.tasks.length === 0 && (
<div className="empty-column">No tasks</div>
)}
</div>
</div>
))}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.board { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; }
.column { background: #1e293b; border-radius: 8px; padding: 1rem; min-height: 400px; display: flex; flex-direction: column; }
.column-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 2px solid #334155; }
.column-header h3 { font-size: 1.125rem; }
.task-count { background: #334155; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.875rem; font-weight: 600; }
.task-list { display: flex; flex-direction: column; gap: 0.75rem; flex: 1; }
.task-card { background: #334155; padding: 1rem; border-radius: 6px; border-left: 3px solid; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; }
.task-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
.task-card.priority-urgent { border-left-color: #ef4444; }
.task-card.priority-high { border-left-color: #f97316; }
.task-card.priority-medium { border-left-color: #eab308; }
.task-card.priority-low { border-left-color: #22c55e; }
.task-title { font-weight: 600; margin-bottom: 0.5rem; }
.task-meta { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; font-size: 0.875rem; }
.priority-badge { padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
.priority-badge.urgent { background: #991b1b; color: #fca5a5; }
.priority-badge.high { background: #9a3412; color: #fdba74; }
.priority-badge.medium { background: #854d0e; color: #fde047; }
.priority-badge.low { background: #14532d; color: #86efac; }
.assignee { color: #94a3b8; }
.task-due { font-size: 0.75rem; color: #64748b; }
.empty-column { text-align: center; padding: 2rem; color: #64748b; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Board - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3003, open: true },
});

View File

@ -0,0 +1,131 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
// Shared Components
const Card: React.FC<{ title: string; value: number; color: string }> = ({ title, value, color }) => (
<div className="stat-card" style={{ borderLeft: `4px solid ${color}` }}>
<h3>{title}</h3>
<div className="stat-value">{value}</div>
</div>
);
const ProgressBar: React.FC<{ label: string; value: number; max: number; color: string }> = ({ label, value, max, color }) => (
<div className="progress-container">
<div className="progress-label">
<span>{label}</span>
<span>{value}/{max}</span>
</div>
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${(value / max) * 100}%`, backgroundColor: color }} />
</div>
</div>
);
interface Task {
id: string;
name: string;
status: string;
priority: string;
due_date: string;
}
export default function App() {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Mock data - replace with actual MCP tool calls
setTimeout(() => {
setTasks([
{ id: '1', name: 'Complete project proposal', status: 'in progress', priority: 'high', due_date: '2024-02-15' },
{ id: '2', name: 'Review design mockups', status: 'open', priority: 'medium', due_date: '2024-02-14' },
{ id: '3', name: 'Update documentation', status: 'closed', priority: 'low', due_date: '2024-02-10' },
{ id: '4', name: 'Fix critical bug', status: 'in progress', priority: 'urgent', due_date: '2024-02-12' },
{ id: '5', name: 'Team meeting prep', status: 'open', priority: 'medium', due_date: '2024-02-16' },
]);
setLoading(false);
}, 500);
}, []);
const statusCounts = tasks.reduce((acc, task) => {
acc[task.status] = (acc[task.status] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const priorityCounts = tasks.reduce((acc, task) => {
acc[task.priority] = (acc[task.priority] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const overdueTasks = tasks.filter(task => new Date(task.due_date) < new Date() && task.status !== 'closed');
if (loading) {
return <div className="loading">Loading dashboard...</div>;
}
return (
<div className="app">
<header>
<h1>📊 Task Dashboard</h1>
<p>Overview of your tasks and progress</p>
</header>
<section className="stats-grid">
<Card title="Total Tasks" value={tasks.length} color="#7c3aed" />
<Card title="In Progress" value={statusCounts['in progress'] || 0} color="#3b82f6" />
<Card title="Completed" value={statusCounts['closed'] || 0} color="#10b981" />
<Card title="Overdue" value={overdueTasks.length} color="#ef4444" />
</section>
<section className="section">
<h2>Status Breakdown</h2>
<div className="progress-list">
<ProgressBar label="Open" value={statusCounts['open'] || 0} max={tasks.length} color="#6366f1" />
<ProgressBar label="In Progress" value={statusCounts['in progress'] || 0} max={tasks.length} color="#3b82f6" />
<ProgressBar label="Closed" value={statusCounts['closed'] || 0} max={tasks.length} color="#10b981" />
</div>
</section>
<section className="section">
<h2>Priority Distribution</h2>
<div className="priority-grid">
<div className="priority-item urgent">
<span>Urgent</span>
<strong>{priorityCounts['urgent'] || 0}</strong>
</div>
<div className="priority-item high">
<span>High</span>
<strong>{priorityCounts['high'] || 0}</strong>
</div>
<div className="priority-item medium">
<span>Medium</span>
<strong>{priorityCounts['medium'] || 0}</strong>
</div>
<div className="priority-item low">
<span>Low</span>
<strong>{priorityCounts['low'] || 0}</strong>
</div>
</div>
</section>
<section className="section">
<h2>Overdue Tasks</h2>
{overdueTasks.length === 0 ? (
<p className="empty-state">No overdue tasks! 🎉</p>
) : (
<div className="task-list">
{overdueTasks.map(task => (
<div key={task.id} className="task-item overdue">
<div className="task-name">{task.name}</div>
<div className="task-meta">
<span className={`priority-badge ${task.priority}`}>{task.priority}</span>
<span className="due-date">Due: {task.due_date}</span>
</div>
</div>
))}
</div>
)}
</section>
</div>
);
}

View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Dashboard - ClickUp MCP</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app { max-width: 1200px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
.stat-card {
background: #1e293b;
padding: 1.5rem;
border-radius: 8px;
border-left: 4px solid;
}
.stat-card h3 { color: #94a3b8; font-size: 0.875rem; font-weight: 600; text-transform: uppercase; margin-bottom: 0.5rem; }
.stat-value { font-size: 2.5rem; font-weight: 700; }
.section { background: #1e293b; padding: 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; }
.section h2 { margin-bottom: 1rem; font-size: 1.25rem; }
.progress-list { display: flex; flex-direction: column; gap: 1rem; }
.progress-container { }
.progress-label { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.875rem; }
.progress-bar { height: 8px; background: #334155; border-radius: 4px; overflow: hidden; }
.progress-fill { height: 100%; transition: width 0.3s ease; }
.priority-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; }
.priority-item { padding: 1rem; border-radius: 6px; display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
.priority-item.urgent { background: rgba(239, 68, 68, 0.1); color: #fca5a5; }
.priority-item.high { background: rgba(249, 115, 22, 0.1); color: #fdba74; }
.priority-item.medium { background: rgba(234, 179, 8, 0.1); color: #fde047; }
.priority-item.low { background: rgba(34, 197, 94, 0.1); color: #86efac; }
.priority-item strong { font-size: 1.5rem; }
.task-list { display: flex; flex-direction: column; gap: 0.75rem; }
.task-item { background: #334155; padding: 1rem; border-radius: 6px; }
.task-item.overdue { border-left: 3px solid #ef4444; }
.task-name { font-weight: 600; margin-bottom: 0.5rem; }
.task-meta { display: flex; gap: 1rem; font-size: 0.875rem; color: #94a3b8; }
.priority-badge { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
.priority-badge.urgent { background: #991b1b; color: #fca5a5; }
.priority-badge.high { background: #9a3412; color: #fdba74; }
.priority-badge.medium { background: #854d0e; color: #fde047; }
.priority-badge.low { background: #14532d; color: #86efac; }
.empty-state { color: #94a3b8; text-align: center; padding: 2rem; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1 @@
/* Styles are inline in index.html for self-contained deployment */

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
open: true,
},
});

View File

@ -0,0 +1,208 @@
import React, { useState, useEffect } from 'react';
interface Task {
id: string;
name: string;
description: string;
status: string;
priority: string;
due_date: string;
assignees: string[];
tags: string[];
custom_fields: Array<{ name: string; value: string }>;
subtasks: Array<{ id: string; name: string; completed: boolean }>;
comments: Array<{ id: string; author: string; text: string; timestamp: string }>;
time_entries: Array<{ id: string; duration: number; user: string; date: string }>;
}
const TabButton: React.FC<{ active: boolean; onClick: () => void; children: React.ReactNode }> = ({ active, onClick, children }) => (
<button className={`tab-button ${active ? 'active' : ''}`} onClick={onClick}>
{children}
</button>
);
export default function App() {
const [task, setTask] = useState<Task | null>(null);
const [activeTab, setActiveTab] = useState<'details' | 'subtasks' | 'comments' | 'time'>('details');
const [loading, setLoading] = useState(true);
useEffect(() => {
// Mock data - replace with actual MCP tool calls
setTimeout(() => {
setTask({
id: '1',
name: 'Implement user authentication system',
description: 'Build a secure authentication system with OAuth2 support, JWT tokens, and password reset functionality.',
status: 'in progress',
priority: 'high',
due_date: '2024-02-20',
assignees: ['John Doe', 'Jane Smith'],
tags: ['backend', 'security', 'critical'],
custom_fields: [
{ name: 'Story Points', value: '8' },
{ name: 'Sprint', value: 'Sprint 5' },
{ name: 'Department', value: 'Engineering' },
],
subtasks: [
{ id: 's1', name: 'Design database schema', completed: true },
{ id: 's2', name: 'Implement OAuth2 flow', completed: true },
{ id: 's3', name: 'Add JWT token generation', completed: false },
{ id: 's4', name: 'Create password reset endpoint', completed: false },
{ id: 's5', name: 'Write unit tests', completed: false },
],
comments: [
{ id: 'c1', author: 'Jane Smith', text: 'Started working on the OAuth2 integration', timestamp: '2024-02-12T10:30:00Z' },
{ id: 'c2', author: 'John Doe', text: 'Database schema looks good, approved!', timestamp: '2024-02-13T14:15:00Z' },
{ id: 'c3', author: 'Jane Smith', text: 'Need to discuss JWT token expiration time with the team', timestamp: '2024-02-14T09:00:00Z' },
],
time_entries: [
{ id: 't1', duration: 7200, user: 'Jane Smith', date: '2024-02-12' },
{ id: 't2', duration: 5400, user: 'John Doe', date: '2024-02-13' },
{ id: 't3', duration: 3600, user: 'Jane Smith', date: '2024-02-14' },
],
});
setLoading(false);
}, 500);
}, []);
if (loading) {
return <div className="loading">Loading task details...</div>;
}
if (!task) {
return <div className="error">Task not found</div>;
}
const totalTime = task.time_entries.reduce((sum, entry) => sum + entry.duration, 0);
const completedSubtasks = task.subtasks.filter(st => st.completed).length;
return (
<div className="app">
<header>
<div className="task-header">
<h1>{task.name}</h1>
<div className="task-badges">
<span className={`status-badge ${task.status}`}>{task.status}</span>
<span className={`priority-badge ${task.priority}`}>{task.priority}</span>
</div>
</div>
<p className="task-description">{task.description}</p>
</header>
<div className="task-meta">
<div className="meta-item">
<strong>Due Date:</strong> {task.due_date}
</div>
<div className="meta-item">
<strong>Assignees:</strong> {task.assignees.join(', ')}
</div>
<div className="meta-item">
<strong>Tags:</strong> {task.tags.map(tag => <span key={tag} className="tag">{tag}</span>)}
</div>
</div>
<div className="tabs">
<TabButton active={activeTab === 'details'} onClick={() => setActiveTab('details')}>
Details & Custom Fields
</TabButton>
<TabButton active={activeTab === 'subtasks'} onClick={() => setActiveTab('subtasks')}>
Subtasks ({completedSubtasks}/{task.subtasks.length})
</TabButton>
<TabButton active={activeTab === 'comments'} onClick={() => setActiveTab('comments')}>
Comments ({task.comments.length})
</TabButton>
<TabButton active={activeTab === 'time'} onClick={() => setActiveTab('time')}>
Time Tracking ({(totalTime / 3600).toFixed(1)}h)
</TabButton>
</div>
<div className="tab-content">
{activeTab === 'details' && (
<div className="custom-fields">
{task.custom_fields.map(field => (
<div key={field.name} className="field-row">
<strong>{field.name}:</strong>
<span>{field.value}</span>
</div>
))}
</div>
)}
{activeTab === 'subtasks' && (
<div className="subtasks-list">
{task.subtasks.map(subtask => (
<div key={subtask.id} className={`subtask-item ${subtask.completed ? 'completed' : ''}`}>
<input type="checkbox" checked={subtask.completed} readOnly />
<span>{subtask.name}</span>
</div>
))}
</div>
)}
{activeTab === 'comments' && (
<div className="comments-list">
{task.comments.map(comment => (
<div key={comment.id} className="comment">
<div className="comment-header">
<strong>{comment.author}</strong>
<span className="timestamp">{new Date(comment.timestamp).toLocaleString()}</span>
</div>
<p>{comment.text}</p>
</div>
))}
</div>
)}
{activeTab === 'time' && (
<div className="time-entries-list">
{task.time_entries.map(entry => (
<div key={entry.id} className="time-entry">
<div className="time-user">{entry.user}</div>
<div className="time-duration">{(entry.duration / 3600).toFixed(2)}h</div>
<div className="time-date">{entry.date}</div>
</div>
))}
<div className="time-total">
<strong>Total Time:</strong> {(totalTime / 3600).toFixed(2)} hours
</div>
</div>
)}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1000px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
.task-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1rem; }
.task-header h1 { font-size: 1.75rem; }
.task-badges { display: flex; gap: 0.5rem; }
.status-badge, .priority-badge { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
.status-badge.in.progress { background: #1e40af; color: #93c5fd; }
.priority-badge.high { background: #9a3412; color: #fdba74; }
.task-description { color: #94a3b8; line-height: 1.6; }
.task-meta { display: flex; gap: 2rem; flex-wrap: wrap; margin-bottom: 2rem; padding: 1rem; background: #1e293b; border-radius: 8px; }
.meta-item { display: flex; gap: 0.5rem; align-items: center; }
.tag { padding: 0.25rem 0.5rem; background: #334155; border-radius: 4px; font-size: 0.875rem; margin-left: 0.25rem; }
.tabs { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; border-bottom: 2px solid #334155; }
.tab-button { padding: 0.75rem 1.5rem; background: none; border: none; color: #94a3b8; cursor: pointer; font-size: 0.875rem; font-weight: 600; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.2s; }
.tab-button:hover { color: #e2e8f0; }
.tab-button.active { color: #60a5fa; border-bottom-color: #60a5fa; }
.tab-content { background: #1e293b; padding: 1.5rem; border-radius: 8px; min-height: 300px; }
.custom-fields { display: flex; flex-direction: column; gap: 1rem; }
.field-row { display: flex; justify-content: space-between; padding: 0.75rem; background: #334155; border-radius: 6px; }
.subtasks-list { display: flex; flex-direction: column; gap: 0.75rem; }
.subtask-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; background: #334155; border-radius: 6px; }
.subtask-item.completed { opacity: 0.6; text-decoration: line-through; }
.comments-list { display: flex; flex-direction: column; gap: 1rem; }
.comment { padding: 1rem; background: #334155; border-radius: 6px; }
.comment-header { display: flex; justify-content: space-between; margin-bottom: 0.5rem; }
.timestamp { font-size: 0.75rem; color: #64748b; }
.time-entries-list { display: flex; flex-direction: column; gap: 0.75rem; }
.time-entry { display: grid; grid-template-columns: 1fr auto auto; gap: 1rem; padding: 0.75rem; background: #334155; border-radius: 6px; }
.time-total { margin-top: 1rem; padding: 1rem; background: #334155; border-radius: 6px; font-size: 1.125rem; }
.loading, .error { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Detail - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3001, open: true },
});

View File

@ -0,0 +1,174 @@
import React, { useState, useEffect } from 'react';
interface Task {
id: string;
name: string;
status: string;
priority: string;
assignee: string;
due_date: string;
tags: string[];
}
type SortKey = 'name' | 'status' | 'priority' | 'due_date';
export default function App() {
const [tasks, setTasks] = useState<Task[]>([]);
const [sortKey, setSortKey] = useState<SortKey>('due_date');
const [sortAsc, setSortAsc] = useState(true);
const [filterStatus, setFilterStatus] = useState<string>('all');
const [filterPriority, setFilterPriority] = useState<string>('all');
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
// Mock data
setTimeout(() => {
setTasks([
{ id: '1', name: 'Complete project proposal', status: 'in progress', priority: 'high', assignee: 'John Doe', due_date: '2024-02-15', tags: ['design', 'planning'] },
{ id: '2', name: 'Review design mockups', status: 'open', priority: 'medium', assignee: 'Jane Smith', due_date: '2024-02-14', tags: ['design'] },
{ id: '3', name: 'Update documentation', status: 'closed', priority: 'low', assignee: 'Bob Johnson', due_date: '2024-02-10', tags: ['docs'] },
{ id: '4', name: 'Fix critical bug', status: 'in progress', priority: 'urgent', assignee: 'Alice Williams', due_date: '2024-02-12', tags: ['bug', 'urgent'] },
{ id: '5', name: 'Team meeting prep', status: 'open', priority: 'medium', assignee: 'John Doe', due_date: '2024-02-16', tags: ['meeting'] },
{ id: '6', name: 'Refactor API endpoints', status: 'in progress', priority: 'high', assignee: 'Jane Smith', due_date: '2024-02-18', tags: ['backend', 'refactor'] },
{ id: '7', name: 'Write unit tests', status: 'open', priority: 'medium', assignee: 'Bob Johnson', due_date: '2024-02-20', tags: ['testing'] },
]);
setLoading(false);
}, 500);
}, []);
const handleSort = (key: SortKey) => {
if (sortKey === key) {
setSortAsc(!sortAsc);
} else {
setSortKey(key);
setSortAsc(true);
}
};
const filteredTasks = tasks.filter(task => {
if (filterStatus !== 'all' && task.status !== filterStatus) return false;
if (filterPriority !== 'all' && task.priority !== filterPriority) return false;
if (searchTerm && !task.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
return true;
});
const sortedTasks = [...filteredTasks].sort((a, b) => {
let aVal = a[sortKey];
let bVal = b[sortKey];
if (sortKey === 'due_date') {
aVal = new Date(a.due_date).getTime();
bVal = new Date(b.due_date).getTime();
}
if (aVal < bVal) return sortAsc ? -1 : 1;
if (aVal > bVal) return sortAsc ? 1 : -1;
return 0;
});
if (loading) {
return <div className="loading">Loading tasks...</div>;
}
return (
<div className="app">
<header>
<h1>📋 Task Grid</h1>
<p>Sortable and filterable task list</p>
</header>
<div className="controls">
<input
type="text"
placeholder="Search tasks..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
<select value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)} className="filter-select">
<option value="all">All Statuses</option>
<option value="open">Open</option>
<option value="in progress">In Progress</option>
<option value="closed">Closed</option>
</select>
<select value={filterPriority} onChange={(e) => setFilterPriority(e.target.value)} className="filter-select">
<option value="all">All Priorities</option>
<option value="urgent">Urgent</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div>
<div className="table-container">
<table className="task-table">
<thead>
<tr>
<th onClick={() => handleSort('name')} className="sortable">
Task Name {sortKey === 'name' && (sortAsc ? '↑' : '↓')}
</th>
<th onClick={() => handleSort('status')} className="sortable">
Status {sortKey === 'status' && (sortAsc ? '↑' : '↓')}
</th>
<th onClick={() => handleSort('priority')} className="sortable">
Priority {sortKey === 'priority' && (sortAsc ? '↑' : '↓')}
</th>
<th>Assignee</th>
<th onClick={() => handleSort('due_date')} className="sortable">
Due Date {sortKey === 'due_date' && (sortAsc ? '↑' : '↓')}
</th>
<th>Tags</th>
</tr>
</thead>
<tbody>
{sortedTasks.map(task => (
<tr key={task.id}>
<td className="task-name">{task.name}</td>
<td><span className={`status-badge ${task.status}`}>{task.status}</span></td>
<td><span className={`priority-badge ${task.priority}`}>{task.priority}</span></td>
<td>{task.assignee}</td>
<td>{task.due_date}</td>
<td>
{task.tags.map(tag => <span key={tag} className="tag">{tag}</span>)}
</td>
</tr>
))}
</tbody>
</table>
{sortedTasks.length === 0 && (
<div className="empty-state">No tasks match your filters</div>
)}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1400px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.controls { display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
.search-input { flex: 1; min-width: 250px; padding: 0.75rem; background: #1e293b; border: 1px solid #334155; border-radius: 6px; color: #e2e8f0; font-size: 0.875rem; }
.filter-select { padding: 0.75rem 1rem; background: #1e293b; border: 1px solid #334155; border-radius: 6px; color: #e2e8f0; font-size: 0.875rem; cursor: pointer; }
.table-container { background: #1e293b; border-radius: 8px; overflow-x: auto; }
.task-table { width: 100%; border-collapse: collapse; }
.task-table th { padding: 1rem; text-align: left; font-weight: 600; background: #334155; border-bottom: 2px solid #475569; }
.task-table th.sortable { cursor: pointer; user-select: none; }
.task-table th.sortable:hover { background: #3f4d63; }
.task-table td { padding: 1rem; border-bottom: 1px solid #334155; }
.task-table tr:hover { background: #2d3748; }
.task-name { font-weight: 600; }
.status-badge, .priority-badge { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; white-space: nowrap; }
.status-badge.open { background: #1e40af; color: #93c5fd; }
.status-badge.in.progress { background: #7c3aed; color: #c4b5fd; }
.status-badge.closed { background: #065f46; color: #6ee7b7; }
.priority-badge.urgent { background: #991b1b; color: #fca5a5; }
.priority-badge.high { background: #9a3412; color: #fdba74; }
.priority-badge.medium { background: #854d0e; color: #fde047; }
.priority-badge.low { background: #14532d; color: #86efac; }
.tag { display: inline-block; padding: 0.25rem 0.5rem; background: #334155; border-radius: 4px; font-size: 0.75rem; margin-right: 0.25rem; }
.empty-state { text-align: center; padding: 3rem; color: #64748b; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Grid - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3002, open: true },
});

View File

@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';
interface Template {
id: string;
name: string;
description: string;
category: string;
tasks: number;
icon: string;
color: string;
}
export default function App() {
const [templates, setTemplates] = useState<Template[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setTemplates([
{ id: '1', name: 'Sprint Planning', description: 'Standard two-week sprint template with planning, execution, and review phases', category: 'Agile', tasks: 15, icon: '🏃', color: '#3b82f6' },
{ id: '2', name: 'Product Launch', description: 'Comprehensive product launch checklist with marketing, engineering, and operations tasks', category: 'Product', tasks: 32, icon: '🚀', color: '#10b981' },
{ id: '3', name: 'Bug Tracking', description: 'Structured bug reporting and resolution workflow', category: 'Engineering', tasks: 8, icon: '🐛', color: '#ef4444' },
{ id: '4', name: 'Content Calendar', description: 'Monthly content planning and publication schedule', category: 'Marketing', tasks: 20, icon: '📅', color: '#a78bfa' },
{ id: '5', name: 'Onboarding Checklist', description: 'New employee onboarding with all necessary setup tasks', category: 'HR', tasks: 18, icon: '👋', color: '#f59e0b' },
{ id: '6', name: 'Design Sprint', description: 'Five-day design sprint methodology for rapid prototyping', category: 'Design', tasks: 25, icon: '🎨', color: '#ec4899' },
{ id: '7', name: 'Sales Pipeline', description: 'Lead tracking and sales funnel management', category: 'Sales', tasks: 12, icon: '💰', color: '#14b8a6' },
{ id: '8', name: 'Event Planning', description: 'Complete event planning from concept to execution', category: 'Operations', tasks: 28, icon: '🎉', color: '#8b5cf6' },
{ id: '9', name: 'Code Review', description: 'Systematic code review process with quality checkpoints', category: 'Engineering', tasks: 10, icon: '👀', color: '#6366f1' },
{ id: '10', name: 'Customer Feedback', description: 'Collect, analyze, and act on customer feedback', category: 'Product', tasks: 14, icon: '💬', color: '#06b6d4' },
]);
setLoading(false);
}, 500);
}, []);
const categories = ['all', ...Array.from(new Set(templates.map(t => t.category)))];
const filteredTemplates = selectedCategory === 'all'
? templates
: templates.filter(t => t.category === selectedCategory);
if (loading) return <div className="loading">Loading templates...</div>;
return (
<div className="app">
<header>
<h1>📋 Template Gallery</h1>
<p>Browse and use pre-built project templates</p>
</header>
<div className="filters">
{categories.map(category => (
<button
key={category}
className={`filter-btn ${selectedCategory === category ? 'active' : ''}`}
onClick={() => setSelectedCategory(category)}
>
{category}
</button>
))}
</div>
<div className="templates-grid">
{filteredTemplates.map(template => (
<div key={template.id} className="template-card" style={{ borderTopColor: template.color }}>
<div className="template-icon" style={{ background: template.color }}>
{template.icon}
</div>
<div className="template-content">
<div className="template-header">
<h3>{template.name}</h3>
<span className="category-badge" style={{ backgroundColor: `${template.color}20`, color: template.color }}>
{template.category}
</span>
</div>
<p className="template-description">{template.description}</p>
<div className="template-footer">
<span className="task-count">📝 {template.tasks} tasks</span>
<button className="use-button" style={{ backgroundColor: template.color }}>
Use Template
</button>
</div>
</div>
</div>
))}
</div>
{filteredTemplates.length === 0 && (
<div className="empty-state">No templates found in this category</div>
)}
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1400px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.filters { display: flex; gap: 0.75rem; margin-bottom: 2rem; flex-wrap: wrap; }
.filter-btn { padding: 0.75rem 1.5rem; background: #1e293b; border: 1px solid #334155; color: #e2e8f0; border-radius: 6px; cursor: pointer; font-weight: 600; text-transform: capitalize; transition: all 0.2s; }
.filter-btn:hover { background: #334155; }
.filter-btn.active { background: #3b82f6; border-color: #3b82f6; color: white; }
.templates-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem; }
.template-card { background: #1e293b; border-radius: 8px; overflow: hidden; border-top: 4px solid; transition: transform 0.2s, box-shadow 0.2s; cursor: pointer; }
.template-card:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.3); }
.template-icon { font-size: 3rem; padding: 2rem; text-align: center; }
.template-content { padding: 0 1.5rem 1.5rem; }
.template-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1rem; gap: 1rem; }
.template-header h3 { font-size: 1.25rem; }
.category-badge { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; white-space: nowrap; }
.template-description { color: #94a3b8; line-height: 1.6; margin-bottom: 1.5rem; min-height: 3rem; }
.template-footer { display: flex; justify-content: space-between; align-items: center; }
.task-count { font-size: 0.875rem; color: #94a3b8; }
.use-button { padding: 0.5rem 1rem; border: none; border-radius: 6px; color: white; font-weight: 600; cursor: pointer; transition: opacity 0.2s; }
.use-button:hover { opacity: 0.9; }
.empty-state { text-align: center; padding: 4rem; color: #64748b; font-size: 1.125rem; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

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

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3016, open: true },
});

View File

@ -0,0 +1,160 @@
import React, { useState, useEffect } from 'react';
interface TimeData {
totalHours: number;
todayHours: number;
weekHours: number;
byUser: Array<{ user: string; hours: number }>;
byProject: Array<{ project: string; hours: number }>;
recentEntries: Array<{ id: string; user: string; task: string; hours: number; date: string }>;
}
export default function App() {
const [data, setData] = useState<TimeData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setData({
totalHours: 147.5,
todayHours: 12.5,
weekHours: 42.0,
byUser: [
{ user: 'John Doe', hours: 42.5 },
{ user: 'Jane Smith', hours: 38.0 },
{ user: 'Bob Johnson', hours: 35.5 },
{ user: 'Alice Williams', hours: 31.5 },
],
byProject: [
{ project: 'Product Development', hours: 65.0 },
{ project: 'Bug Fixes', hours: 28.5 },
{ project: 'Research', hours: 22.0 },
{ project: 'Documentation', hours: 18.5 },
{ project: 'Meetings', hours: 13.5 },
],
recentEntries: [
{ id: '1', user: 'John Doe', task: 'API Integration', hours: 3.5, date: '2024-02-15' },
{ id: '2', user: 'Jane Smith', task: 'UI Design', hours: 2.0, date: '2024-02-15' },
{ id: '3', user: 'Bob Johnson', task: 'Code Review', hours: 1.5, date: '2024-02-15' },
{ id: '4', user: 'Alice Williams', task: 'Testing', hours: 4.0, date: '2024-02-14' },
{ id: '5', user: 'John Doe', task: 'Database Optimization', hours: 2.5, date: '2024-02-14' },
]
});
setLoading(false);
}, 500);
}, []);
if (loading) return <div className="loading">Loading time dashboard...</div>;
if (!data) return <div className="error">No data available</div>;
return (
<div className="app">
<header>
<h1> Time Tracking Dashboard</h1>
<p>Overview of time logged across projects and team members</p>
</header>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon"></div>
<div className="stat-value">{data.todayHours}h</div>
<div className="stat-label">Today</div>
</div>
<div className="stat-card">
<div className="stat-icon">📅</div>
<div className="stat-value">{data.weekHours}h</div>
<div className="stat-label">This Week</div>
</div>
<div className="stat-card">
<div className="stat-icon">📊</div>
<div className="stat-value">{data.totalHours}h</div>
<div className="stat-label">Total Tracked</div>
</div>
<div className="stat-card">
<div className="stat-icon">👥</div>
<div className="stat-value">{data.byUser.length}</div>
<div className="stat-label">Active Users</div>
</div>
</div>
<div className="charts-grid">
<div className="chart-card">
<h2>Time by User</h2>
<div className="chart-list">
{data.byUser.map(item => (
<div key={item.user} className="chart-item">
<div className="chart-label">{item.user}</div>
<div className="chart-bar-container">
<div className="chart-bar" style={{ width: `${(item.hours / Math.max(...data.byUser.map(u => u.hours))) * 100}%` }}></div>
</div>
<div className="chart-value">{item.hours}h</div>
</div>
))}
</div>
</div>
<div className="chart-card">
<h2>Time by Project</h2>
<div className="chart-list">
{data.byProject.map(item => (
<div key={item.project} className="chart-item">
<div className="chart-label">{item.project}</div>
<div className="chart-bar-container">
<div className="chart-bar project" style={{ width: `${(item.hours / Math.max(...data.byProject.map(p => p.hours))) * 100}%` }}></div>
</div>
<div className="chart-value">{item.hours}h</div>
</div>
))}
</div>
</div>
</div>
<div className="recent-section">
<h2>Recent Time Entries</h2>
<div className="entries-list">
{data.recentEntries.map(entry => (
<div key={entry.id} className="entry-item">
<div className="entry-user">{entry.user}</div>
<div className="entry-task">{entry.task}</div>
<div className="entry-hours">{entry.hours}h</div>
<div className="entry-date">{entry.date}</div>
</div>
))}
</div>
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1400px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.stat-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; text-align: center; }
.stat-icon { font-size: 2rem; margin-bottom: 0.5rem; }
.stat-value { font-size: 2.5rem; font-weight: 700; color: #60a5fa; margin-bottom: 0.25rem; }
.stat-label { font-size: 0.875rem; color: #94a3b8; }
.charts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
.chart-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; }
.chart-card h2 { font-size: 1.25rem; margin-bottom: 1rem; }
.chart-list { display: flex; flex-direction: column; gap: 0.75rem; }
.chart-item { display: grid; grid-template-columns: 120px 1fr 60px; gap: 1rem; align-items: center; }
.chart-label { font-size: 0.875rem; }
.chart-bar-container { height: 24px; background: #334155; border-radius: 4px; overflow: hidden; }
.chart-bar { height: 100%; background: linear-gradient(90deg, #60a5fa 0%, #a78bfa 100%); transition: width 0.3s ease; }
.chart-bar.project { background: linear-gradient(90deg, #34d399 0%, #10b981 100%); }
.chart-value { font-size: 0.875rem; font-weight: 600; text-align: right; }
.recent-section { background: #1e293b; padding: 1.5rem; border-radius: 8px; }
.recent-section h2 { font-size: 1.25rem; margin-bottom: 1rem; }
.entries-list { display: flex; flex-direction: column; gap: 0.5rem; }
.entry-item { display: grid; grid-template-columns: 150px 1fr 80px 120px; gap: 1rem; padding: 0.75rem; background: #334155; border-radius: 6px; align-items: center; }
.entry-user { font-weight: 600; }
.entry-task { color: #94a3b8; }
.entry-hours { color: #60a5fa; font-weight: 600; text-align: right; }
.entry-date { font-size: 0.875rem; color: #64748b; text-align: right; }
.loading, .error { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Time Dashboard - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3009, open: true },
});

View File

@ -0,0 +1,146 @@
import React, { useState, useEffect } from 'react';
interface TimeEntry {
id: string;
user: string;
task: string;
taskId: string;
duration: number;
date: string;
description: string;
billable: boolean;
}
export default function App() {
const [entries, setEntries] = useState<TimeEntry[]>([]);
const [filterUser, setFilterUser] = useState<string>('all');
const [filterBillable, setFilterBillable] = useState<string>('all');
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setEntries([
{ id: '1', user: 'John Doe', task: 'API Integration', taskId: 'T-123', duration: 12600, date: '2024-02-15', description: 'Integrated payment gateway API', billable: true },
{ id: '2', user: 'Jane Smith', task: 'UI Design', taskId: 'T-124', duration: 7200, date: '2024-02-15', description: 'Designed new dashboard layout', billable: true },
{ id: '3', user: 'Bob Johnson', task: 'Code Review', taskId: 'T-125', duration: 5400, date: '2024-02-15', description: 'Reviewed pull requests', billable: false },
{ id: '4', user: 'Alice Williams', task: 'Testing', taskId: 'T-126', duration: 14400, date: '2024-02-14', description: 'QA testing for new features', billable: true },
{ id: '5', user: 'John Doe', task: 'Database Optimization', taskId: 'T-127', duration: 9000, date: '2024-02-14', description: 'Optimized database queries', billable: true },
{ id: '6', user: 'Jane Smith', task: 'Team Meeting', taskId: 'T-128', duration: 3600, date: '2024-02-14', description: 'Sprint planning meeting', billable: false },
{ id: '7', user: 'Bob Johnson', task: 'Documentation', taskId: 'T-129', duration: 7200, date: '2024-02-13', description: 'Updated API documentation', billable: true },
{ id: '8', user: 'Alice Williams', task: 'Bug Fixes', taskId: 'T-130', duration: 10800, date: '2024-02-13', description: 'Fixed critical production bugs', billable: true },
]);
setLoading(false);
}, 500);
}, []);
const filteredEntries = entries.filter(entry => {
if (filterUser !== 'all' && entry.user !== filterUser) return false;
if (filterBillable === 'billable' && !entry.billable) return false;
if (filterBillable === 'non-billable' && entry.billable) return false;
return true;
});
const totalHours = filteredEntries.reduce((sum, entry) => sum + entry.duration, 0) / 3600;
const billableHours = filteredEntries.filter(e => e.billable).reduce((sum, entry) => sum + entry.duration, 0) / 3600;
const uniqueUsers = Array.from(new Set(entries.map(e => e.user)));
if (loading) return <div className="loading">Loading time entries...</div>;
return (
<div className="app">
<header>
<h1> Time Entries</h1>
<p>Detailed time entry list with task associations</p>
</header>
<div className="summary">
<div className="summary-item">
<span className="summary-label">Total Hours:</span>
<span className="summary-value">{totalHours.toFixed(2)}h</span>
</div>
<div className="summary-item">
<span className="summary-label">Billable:</span>
<span className="summary-value billable">{billableHours.toFixed(2)}h</span>
</div>
<div className="summary-item">
<span className="summary-label">Non-Billable:</span>
<span className="summary-value non-billable">{(totalHours - billableHours).toFixed(2)}h</span>
</div>
</div>
<div className="controls">
<select value={filterUser} onChange={(e) => setFilterUser(e.target.value)}>
<option value="all">All Users</option>
{uniqueUsers.map(user => <option key={user} value={user}>{user}</option>)}
</select>
<select value={filterBillable} onChange={(e) => setFilterBillable(e.target.value)}>
<option value="all">All Entries</option>
<option value="billable">Billable Only</option>
<option value="non-billable">Non-Billable Only</option>
</select>
</div>
<div className="entries-table">
<div className="table-header">
<div>User</div>
<div>Task</div>
<div>Description</div>
<div>Duration</div>
<div>Date</div>
<div>Billable</div>
</div>
{filteredEntries.map(entry => (
<div key={entry.id} className="table-row">
<div className="cell-user">{entry.user}</div>
<div className="cell-task">
<div className="task-name">{entry.task}</div>
<div className="task-id">{entry.taskId}</div>
</div>
<div className="cell-description">{entry.description}</div>
<div className="cell-duration">{(entry.duration / 3600).toFixed(2)}h</div>
<div className="cell-date">{entry.date}</div>
<div className="cell-billable">
{entry.billable ?
<span className="badge billable"> Billable</span> :
<span className="badge non-billable"> Non-Billable</span>
}
</div>
</div>
))}
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1400px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.summary { display: flex; gap: 2rem; background: #1e293b; padding: 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; }
.summary-item { display: flex; flex-direction: column; gap: 0.25rem; }
.summary-label { font-size: 0.875rem; color: #94a3b8; }
.summary-value { font-size: 1.5rem; font-weight: 700; }
.summary-value.billable { color: #34d399; }
.summary-value.non-billable { color: #94a3b8; }
.controls { display: flex; gap: 1rem; margin-bottom: 1.5rem; }
.controls select { padding: 0.75rem 1rem; background: #1e293b; border: 1px solid #334155; color: #e2e8f0; border-radius: 6px; cursor: pointer; }
.entries-table { background: #1e293b; border-radius: 8px; overflow: hidden; }
.table-header, .table-row { display: grid; grid-template-columns: 150px 200px 1fr 100px 120px 140px; padding: 1rem; gap: 1rem; }
.table-header { background: #334155; font-weight: 600; border-bottom: 2px solid #475569; }
.table-row { border-bottom: 1px solid #334155; transition: background 0.2s; }
.table-row:hover { background: #2d3748; }
.cell-user { font-weight: 600; }
.task-name { font-weight: 600; }
.task-id { font-size: 0.75rem; color: #64748b; }
.cell-description { color: #94a3b8; font-size: 0.875rem; }
.cell-duration { font-weight: 600; color: #60a5fa; }
.cell-date { font-size: 0.875rem; color: #94a3b8; }
.badge { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; white-space: nowrap; }
.badge.billable { background: #065f46; color: #6ee7b7; }
.badge.non-billable { background: #334155; color: #94a3b8; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Time Entries - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3010, open: true },
});

View File

@ -0,0 +1,187 @@
import React, { useState, useEffect } from 'react';
interface WorkspaceStats {
name: string;
spaces: number;
lists: number;
tasks: number;
members: number;
activeProjects: number;
completionRate: number;
recentActivity: Array<{
id: string;
user: string;
action: string;
target: string;
timestamp: string;
}>;
topSpaces: Array<{
id: string;
name: string;
taskCount: number;
completion: number;
}>;
}
export default function App() {
const [stats, setStats] = useState<WorkspaceStats | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setStats({
name: 'Acme Corporation',
spaces: 12,
lists: 48,
tasks: 327,
members: 24,
activeProjects: 15,
completionRate: 68,
recentActivity: [
{ id: '1', user: 'John Doe', action: 'completed', target: 'API Integration', timestamp: '2 minutes ago' },
{ id: '2', user: 'Jane Smith', action: 'commented on', target: 'Design System', timestamp: '15 minutes ago' },
{ id: '3', user: 'Bob Johnson', action: 'created', target: 'Bug Fix Sprint', timestamp: '1 hour ago' },
{ id: '4', user: 'Alice Williams', action: 'assigned', target: 'Security Audit to John', timestamp: '2 hours ago' },
{ id: '5', user: 'Charlie Brown', action: 'updated', target: 'Project Timeline', timestamp: '3 hours ago' },
],
topSpaces: [
{ id: 's1', name: 'Product Development', taskCount: 89, completion: 72 },
{ id: 's2', name: 'Marketing', taskCount: 56, completion: 65 },
{ id: 's3', name: 'Engineering', taskCount: 112, completion: 58 },
{ id: 's4', name: 'Design', taskCount: 42, completion: 81 },
{ id: 's5', name: 'Operations', taskCount: 28, completion: 75 },
]
});
setLoading(false);
}, 500);
}, []);
if (loading) return <div className="loading">Loading workspace...</div>;
if (!stats) return <div className="error">Workspace not found</div>;
return (
<div className="app">
<header>
<h1>🏢 {stats.name}</h1>
<p>High-level workspace statistics and activity</p>
</header>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon">🚀</div>
<div className="stat-value">{stats.spaces}</div>
<div className="stat-label">Spaces</div>
</div>
<div className="stat-card">
<div className="stat-icon">📋</div>
<div className="stat-value">{stats.lists}</div>
<div className="stat-label">Lists</div>
</div>
<div className="stat-card">
<div className="stat-icon"></div>
<div className="stat-value">{stats.tasks}</div>
<div className="stat-label">Tasks</div>
</div>
<div className="stat-card">
<div className="stat-icon">👥</div>
<div className="stat-value">{stats.members}</div>
<div className="stat-label">Members</div>
</div>
</div>
<div className="overview-grid">
<div className="card large">
<h2>Active Projects</h2>
<div className="big-stat">{stats.activeProjects}</div>
<p className="card-description">Projects currently in progress</p>
</div>
<div className="card large">
<h2>Completion Rate</h2>
<div className="big-stat">{stats.completionRate}%</div>
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${stats.completionRate}%` }}></div>
</div>
</div>
</div>
<div className="content-grid">
<div className="card">
<h2>Top Spaces by Task Count</h2>
<div className="spaces-list">
{stats.topSpaces.map(space => (
<div key={space.id} className="space-item">
<div className="space-info">
<div className="space-name">{space.name}</div>
<div className="space-meta">
<span>{space.taskCount} tasks</span>
<span>{space.completion}% complete</span>
</div>
</div>
<div className="space-progress">
<div className="mini-progress-bar">
<div className="mini-progress-fill" style={{ width: `${space.completion}%` }}></div>
</div>
</div>
</div>
))}
</div>
</div>
<div className="card">
<h2>Recent Activity</h2>
<div className="activity-list">
{stats.recentActivity.map(activity => (
<div key={activity.id} className="activity-item">
<div className="activity-avatar">{activity.user[0]}</div>
<div className="activity-content">
<div className="activity-text">
<strong>{activity.user}</strong> {activity.action} <span className="activity-target">{activity.target}</span>
</div>
<div className="activity-time">{activity.timestamp}</div>
</div>
</div>
))}
</div>
</div>
</div>
<style>{`
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.app { max-width: 1400px; margin: 0 auto; padding: 2rem; }
header { margin-bottom: 2rem; }
header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
header p { color: #94a3b8; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.stat-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; text-align: center; }
.stat-icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
.stat-value { font-size: 2.5rem; font-weight: 700; color: #60a5fa; margin-bottom: 0.25rem; }
.stat-label { font-size: 0.875rem; color: #94a3b8; }
.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
.content-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem; }
.card { background: #1e293b; padding: 1.5rem; border-radius: 8px; }
.card.large { padding: 2rem; }
.card h2 { font-size: 1.125rem; margin-bottom: 1rem; }
.card-description { color: #94a3b8; font-size: 0.875rem; margin-top: 0.5rem; }
.big-stat { font-size: 3.5rem; font-weight: 700; color: #10b981; margin-bottom: 1rem; }
.progress-bar { height: 12px; background: #334155; border-radius: 6px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #10b981 0%, #34d399 100%); transition: width 0.3s ease; }
.spaces-list { display: flex; flex-direction: column; gap: 1rem; }
.space-item { padding: 1rem; background: #334155; border-radius: 6px; }
.space-info { margin-bottom: 0.5rem; }
.space-name { font-weight: 600; margin-bottom: 0.25rem; }
.space-meta { display: flex; gap: 1rem; font-size: 0.75rem; color: #94a3b8; }
.mini-progress-bar { height: 4px; background: #475569; border-radius: 2px; overflow: hidden; }
.mini-progress-fill { height: 100%; background: #60a5fa; transition: width 0.3s ease; }
.activity-list { display: flex; flex-direction: column; gap: 1rem; }
.activity-item { display: flex; gap: 1rem; padding: 1rem; background: #334155; border-radius: 6px; }
.activity-avatar { width: 40px; height: 40px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; font-weight: 700; flex-shrink: 0; }
.activity-content { flex: 1; min-width: 0; }
.activity-text { margin-bottom: 0.25rem; }
.activity-target { color: #60a5fa; }
.activity-time { font-size: 0.75rem; color: #64748b; }
.loading, .error { display: flex; justify-content: center; align-items: center; height: 100vh; font-size: 1.25rem; }
`}</style>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workspace Overview - ClickUp MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3017, open: true },
});

View File

@ -1,20 +1,38 @@
{
"name": "mcp-server-keap",
"name": "@mcpengine/keap-server",
"version": "1.0.0",
"description": "Keap (Infusionsoft) MCP Server - Complete CRM automation",
"type": "module",
"main": "dist/index.js",
"main": "dist/main.js",
"bin": {
"keap-mcp": "./dist/main.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
"dev": "tsc --watch",
"start": "node dist/main.js",
"prepare": "npm run build"
},
"keywords": [
"mcp",
"keap",
"infusionsoft",
"crm",
"marketing-automation"
],
"author": "MCPEngine",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"zod": "^3.22.4"
"@modelcontextprotocol/sdk": "^1.0.4",
"axios": "^1.7.9",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"lucide-react": "^0.468.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"tsx": "^4.7.0",
"typescript": "^5.3.0"
"@types/node": "^22.10.5",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"typescript": "^5.7.3"
}
}

View File

@ -0,0 +1,305 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
import type {
KeapConfig,
KeapError,
PaginatedResponse,
Contact,
Deal,
Company,
Task,
Appointment,
Campaign,
Email,
Order,
Product,
Tag,
Note,
Automation,
} from '../types/keap.js';
export class KeapClient {
private client: AxiosInstance;
private baseUrl: string;
constructor(config: KeapConfig) {
this.baseUrl = config.baseUrl || 'https://api.infusionsoft.com/crm/rest/v2';
this.client = axios.create({
baseURL: this.baseUrl,
headers: {
'Authorization': `Bearer ${config.accessToken}`,
'Content-Type': 'application/json',
},
timeout: 30000,
});
// Response interceptor for error handling
this.client.interceptors.response.use(
(response) => response,
(error: AxiosError<KeapError>) => {
if (error.response?.data) {
const keapError = error.response.data;
const message = keapError.message || keapError.fault?.faultstring || 'Unknown Keap API error';
throw new Error(`Keap API Error: ${message}`);
}
throw new Error(`Request failed: ${error.message}`);
}
);
}
// Generic paginated request
async paginate<T>(endpoint: string, params: Record<string, any> = {}): Promise<T[]> {
const results: T[] = [];
let nextUrl: string | undefined = undefined;
let hasMore = true;
const limit = params.limit || 1000;
const requestParams = { ...params, limit };
while (hasMore) {
const response = await this.client.get<PaginatedResponse<T>>(
nextUrl || endpoint,
nextUrl ? undefined : { params: requestParams }
);
results.push(...response.data.data);
if (response.data.next && results.length < (params.max_results || Infinity)) {
nextUrl = response.data.next;
} else {
hasMore = false;
}
}
return results;
}
// Generic GET request
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
const response = await this.client.get<T>(endpoint, { params });
return response.data;
}
// Generic POST request
async post<T>(endpoint: string, data: any): Promise<T> {
const response = await this.client.post<T>(endpoint, data);
return response.data;
}
// Generic PUT request
async put<T>(endpoint: string, data: any): Promise<T> {
const response = await this.client.put<T>(endpoint, data);
return response.data;
}
// Generic PATCH request
async patch<T>(endpoint: string, data: any): Promise<T> {
const response = await this.client.patch<T>(endpoint, data);
return response.data;
}
// Generic DELETE request
async delete(endpoint: string): Promise<void> {
await this.client.delete(endpoint);
}
// Contacts
async listContacts(params?: Record<string, any>): Promise<Contact[]> {
return this.paginate<Contact>('/contacts', params);
}
async getContact(contactId: number): Promise<Contact> {
return this.get<Contact>(`/contacts/${contactId}`);
}
async createContact(data: Partial<Contact>): Promise<Contact> {
return this.post<Contact>('/contacts', data);
}
async updateContact(contactId: number, data: Partial<Contact>): Promise<Contact> {
return this.patch<Contact>(`/contacts/${contactId}`, data);
}
async deleteContact(contactId: number): Promise<void> {
return this.delete(`/contacts/${contactId}`);
}
// Deals
async listDeals(params?: Record<string, any>): Promise<Deal[]> {
return this.paginate<Deal>('/opportunities', params);
}
async getDeal(dealId: number): Promise<Deal> {
return this.get<Deal>(`/opportunities/${dealId}`);
}
async createDeal(data: Partial<Deal>): Promise<Deal> {
return this.post<Deal>('/opportunities', data);
}
async updateDeal(dealId: number, data: Partial<Deal>): Promise<Deal> {
return this.patch<Deal>(`/opportunities/${dealId}`, data);
}
async deleteDeal(dealId: number): Promise<void> {
return this.delete(`/opportunities/${dealId}`);
}
// Companies
async listCompanies(params?: Record<string, any>): Promise<Company[]> {
return this.paginate<Company>('/companies', params);
}
async getCompany(companyId: number): Promise<Company> {
return this.get<Company>(`/companies/${companyId}`);
}
async createCompany(data: Partial<Company>): Promise<Company> {
return this.post<Company>('/companies', data);
}
async updateCompany(companyId: number, data: Partial<Company>): Promise<Company> {
return this.patch<Company>(`/companies/${companyId}`, data);
}
async deleteCompany(companyId: number): Promise<void> {
return this.delete(`/companies/${companyId}`);
}
// Tasks
async listTasks(params?: Record<string, any>): Promise<Task[]> {
return this.paginate<Task>('/tasks', params);
}
async getTask(taskId: number): Promise<Task> {
return this.get<Task>(`/tasks/${taskId}`);
}
async createTask(data: Partial<Task>): Promise<Task> {
return this.post<Task>('/tasks', data);
}
async updateTask(taskId: number, data: Partial<Task>): Promise<Task> {
return this.patch<Task>(`/tasks/${taskId}`, data);
}
async deleteTask(taskId: number): Promise<void> {
return this.delete(`/tasks/${taskId}`);
}
// Appointments
async listAppointments(params?: Record<string, any>): Promise<Appointment[]> {
return this.paginate<Appointment>('/appointments', params);
}
async getAppointment(appointmentId: number): Promise<Appointment> {
return this.get<Appointment>(`/appointments/${appointmentId}`);
}
async createAppointment(data: Partial<Appointment>): Promise<Appointment> {
return this.post<Appointment>('/appointments', data);
}
async updateAppointment(appointmentId: number, data: Partial<Appointment>): Promise<Appointment> {
return this.patch<Appointment>(`/appointments/${appointmentId}`, data);
}
async deleteAppointment(appointmentId: number): Promise<void> {
return this.delete(`/appointments/${appointmentId}`);
}
// Campaigns
async listCampaigns(params?: Record<string, any>): Promise<Campaign[]> {
return this.paginate<Campaign>('/campaigns', params);
}
async getCampaign(campaignId: number): Promise<Campaign> {
return this.get<Campaign>(`/campaigns/${campaignId}`);
}
// Emails
async listEmails(params?: Record<string, any>): Promise<Email[]> {
return this.paginate<Email>('/emails', params);
}
async getEmail(emailId: number): Promise<Email> {
return this.get<Email>(`/emails/${emailId}`);
}
async createEmail(data: Partial<Email>): Promise<Email> {
return this.post<Email>('/emails', data);
}
async sendEmail(data: Partial<Email>): Promise<Email> {
return this.post<Email>('/emails/send', data);
}
// Orders
async listOrders(params?: Record<string, any>): Promise<Order[]> {
return this.paginate<Order>('/orders', params);
}
async getOrder(orderId: number): Promise<Order> {
return this.get<Order>(`/orders/${orderId}`);
}
async createOrder(data: Partial<Order>): Promise<Order> {
return this.post<Order>('/orders', data);
}
// Products
async listProducts(params?: Record<string, any>): Promise<Product[]> {
return this.paginate<Product>('/products', params);
}
async getProduct(productId: number): Promise<Product> {
return this.get<Product>(`/products/${productId}`);
}
async createProduct(data: Partial<Product>): Promise<Product> {
return this.post<Product>('/products', data);
}
async updateProduct(productId: number, data: Partial<Product>): Promise<Product> {
return this.patch<Product>(`/products/${productId}`, data);
}
async deleteProduct(productId: number): Promise<void> {
return this.delete(`/products/${productId}`);
}
// Tags
async listTags(params?: Record<string, any>): Promise<Tag[]> {
return this.paginate<Tag>('/tags', params);
}
async getTag(tagId: number): Promise<Tag> {
return this.get<Tag>(`/tags/${tagId}`);
}
async createTag(data: Partial<Tag>): Promise<Tag> {
return this.post<Tag>('/tags', data);
}
async deleteTag(tagId: number): Promise<void> {
return this.delete(`/tags/${tagId}`);
}
// Notes
async listNotes(params?: Record<string, any>): Promise<Note[]> {
return this.paginate<Note>('/notes', params);
}
async createNote(data: Partial<Note>): Promise<Note> {
return this.post<Note>('/notes', data);
}
// Automations
async listAutomations(params?: Record<string, any>): Promise<Automation[]> {
return this.paginate<Automation>('/campaigns', params);
}
async getAutomation(automationId: number): Promise<Automation> {
return this.get<Automation>(`/campaigns/${automationId}`);
}
}

View File

@ -1,430 +0,0 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// ============================================
// CONFIGURATION
// ============================================
const MCP_NAME = "keap";
const MCP_VERSION = "1.0.0";
const API_BASE_URL = "https://api.infusionsoft.com/crm/rest/v1";
// ============================================
// API CLIENT - Keap uses OAuth2 Bearer token
// ============================================
class KeapClient {
private accessToken: string;
private baseUrl: string;
constructor(accessToken: string) {
this.accessToken = accessToken;
this.baseUrl = API_BASE_URL;
}
async request(endpoint: string, options: RequestInit = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
"Authorization": `Bearer ${this.accessToken}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Keap API error: ${response.status} ${response.statusText} - ${errorText}`);
}
if (response.status === 204) {
return { success: true };
}
return response.json();
}
async get(endpoint: string) {
return this.request(endpoint, { method: "GET" });
}
async post(endpoint: string, data: any) {
return this.request(endpoint, {
method: "POST",
body: JSON.stringify(data),
});
}
async put(endpoint: string, data: any) {
return this.request(endpoint, {
method: "PUT",
body: JSON.stringify(data),
});
}
async patch(endpoint: string, data: any) {
return this.request(endpoint, {
method: "PATCH",
body: JSON.stringify(data),
});
}
async delete(endpoint: string) {
return this.request(endpoint, { method: "DELETE" });
}
}
// ============================================
// TOOL DEFINITIONS
// ============================================
const tools = [
{
name: "list_contacts",
description: "List contacts with optional filtering and pagination",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Max results to return (default 50, max 1000)" },
offset: { type: "number", description: "Pagination offset" },
email: { type: "string", description: "Filter by email address" },
given_name: { type: "string", description: "Filter by first name" },
family_name: { type: "string", description: "Filter by last name" },
order: { type: "string", description: "Field to order by (e.g., 'email', 'date_created')" },
order_direction: { type: "string", enum: ["ASCENDING", "DESCENDING"], description: "Sort direction" },
since: { type: "string", description: "Return contacts modified since this date (ISO 8601)" },
until: { type: "string", description: "Return contacts modified before this date (ISO 8601)" },
},
},
},
{
name: "get_contact",
description: "Get a specific contact by ID with full details",
inputSchema: {
type: "object" as const,
properties: {
id: { type: "number", description: "Contact ID" },
optional_properties: {
type: "array",
items: { type: "string" },
description: "Additional fields to include: custom_fields, fax_numbers, invoices, etc.",
},
},
required: ["id"],
},
},
{
name: "create_contact",
description: "Create a new contact in Keap",
inputSchema: {
type: "object" as const,
properties: {
email_addresses: {
type: "array",
items: {
type: "object",
properties: {
email: { type: "string" },
field: { type: "string", enum: ["EMAIL1", "EMAIL2", "EMAIL3"] },
},
},
description: "Email addresses for the contact",
},
given_name: { type: "string", description: "First name" },
family_name: { type: "string", description: "Last name" },
phone_numbers: {
type: "array",
items: {
type: "object",
properties: {
number: { type: "string" },
field: { type: "string", enum: ["PHONE1", "PHONE2", "PHONE3", "PHONE4", "PHONE5"] },
},
},
description: "Phone numbers",
},
addresses: {
type: "array",
items: {
type: "object",
properties: {
line1: { type: "string" },
line2: { type: "string" },
locality: { type: "string", description: "City" },
region: { type: "string", description: "State/Province" },
postal_code: { type: "string" },
country_code: { type: "string" },
field: { type: "string", enum: ["BILLING", "SHIPPING", "OTHER"] },
},
},
description: "Addresses",
},
company: {
type: "object",
properties: {
company_name: { type: "string" },
},
description: "Company information",
},
job_title: { type: "string", description: "Job title" },
lead_source_id: { type: "number", description: "Lead source ID" },
opt_in_reason: { type: "string", description: "Reason for opting in to marketing" },
source_type: { type: "string", enum: ["WEBFORM", "LANDINGPAGE", "IMPORT", "MANUAL", "API", "OTHER"], description: "Source type" },
custom_fields: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "number" },
content: { type: "string" },
},
},
description: "Custom field values",
},
},
},
},
{
name: "update_contact",
description: "Update an existing contact",
inputSchema: {
type: "object" as const,
properties: {
id: { type: "number", description: "Contact ID" },
email_addresses: { type: "array", items: { type: "object" }, description: "Updated email addresses" },
given_name: { type: "string", description: "First name" },
family_name: { type: "string", description: "Last name" },
phone_numbers: { type: "array", items: { type: "object" }, description: "Phone numbers" },
addresses: { type: "array", items: { type: "object" }, description: "Addresses" },
company: { type: "object", description: "Company information" },
job_title: { type: "string", description: "Job title" },
custom_fields: { type: "array", items: { type: "object" }, description: "Custom field values" },
},
required: ["id"],
},
},
{
name: "list_opportunities",
description: "List sales opportunities/deals",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Max results (default 50, max 1000)" },
offset: { type: "number", description: "Pagination offset" },
user_id: { type: "number", description: "Filter by assigned user ID" },
stage_id: { type: "number", description: "Filter by pipeline stage ID" },
search_term: { type: "string", description: "Search opportunities by title" },
order: { type: "string", description: "Field to order by" },
},
},
},
{
name: "list_tasks",
description: "List tasks with optional filtering",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Max results (default 50, max 1000)" },
offset: { type: "number", description: "Pagination offset" },
contact_id: { type: "number", description: "Filter by contact ID" },
user_id: { type: "number", description: "Filter by assigned user ID" },
completed: { type: "boolean", description: "Filter by completion status" },
since: { type: "string", description: "Tasks created/updated since (ISO 8601)" },
until: { type: "string", description: "Tasks created/updated before (ISO 8601)" },
order: { type: "string", description: "Field to order by" },
},
},
},
{
name: "create_task",
description: "Create a new task",
inputSchema: {
type: "object" as const,
properties: {
title: { type: "string", description: "Task title (required)" },
description: { type: "string", description: "Task description" },
contact: {
type: "object",
properties: {
id: { type: "number" },
},
description: "Contact to associate the task with",
},
due_date: { type: "string", description: "Due date in ISO 8601 format" },
priority: { type: "number", description: "Priority (1-5, 5 being highest)" },
type: { type: "string", description: "Task type (e.g., 'Call', 'Email', 'Appointment', 'Other')" },
user_id: { type: "number", description: "User ID to assign the task to" },
remind_time: { type: "number", description: "Reminder time in minutes before due date" },
},
required: ["title"],
},
},
{
name: "list_tags",
description: "List all tags available in the account",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Max results (default 50, max 1000)" },
offset: { type: "number", description: "Pagination offset" },
category: { type: "number", description: "Filter by tag category ID" },
name: { type: "string", description: "Filter by tag name (partial match)" },
},
},
},
];
// ============================================
// TOOL HANDLERS
// ============================================
async function handleTool(client: KeapClient, name: string, args: any) {
switch (name) {
case "list_contacts": {
const params = new URLSearchParams();
if (args.limit) params.append("limit", args.limit.toString());
if (args.offset) params.append("offset", args.offset.toString());
if (args.email) params.append("email", args.email);
if (args.given_name) params.append("given_name", args.given_name);
if (args.family_name) params.append("family_name", args.family_name);
if (args.order) params.append("order", args.order);
if (args.order_direction) params.append("order_direction", args.order_direction);
if (args.since) params.append("since", args.since);
if (args.until) params.append("until", args.until);
const query = params.toString();
return await client.get(`/contacts${query ? `?${query}` : ""}`);
}
case "get_contact": {
const { id, optional_properties } = args;
let endpoint = `/contacts/${id}`;
if (optional_properties && optional_properties.length > 0) {
endpoint += `?optional_properties=${optional_properties.join(",")}`;
}
return await client.get(endpoint);
}
case "create_contact": {
const payload: any = {};
if (args.email_addresses) payload.email_addresses = args.email_addresses;
if (args.given_name) payload.given_name = args.given_name;
if (args.family_name) payload.family_name = args.family_name;
if (args.phone_numbers) payload.phone_numbers = args.phone_numbers;
if (args.addresses) payload.addresses = args.addresses;
if (args.company) payload.company = args.company;
if (args.job_title) payload.job_title = args.job_title;
if (args.lead_source_id) payload.lead_source_id = args.lead_source_id;
if (args.opt_in_reason) payload.opt_in_reason = args.opt_in_reason;
if (args.source_type) payload.source_type = args.source_type;
if (args.custom_fields) payload.custom_fields = args.custom_fields;
return await client.post("/contacts", payload);
}
case "update_contact": {
const { id, ...updates } = args;
return await client.patch(`/contacts/${id}`, updates);
}
case "list_opportunities": {
const params = new URLSearchParams();
if (args.limit) params.append("limit", args.limit.toString());
if (args.offset) params.append("offset", args.offset.toString());
if (args.user_id) params.append("user_id", args.user_id.toString());
if (args.stage_id) params.append("stage_id", args.stage_id.toString());
if (args.search_term) params.append("search_term", args.search_term);
if (args.order) params.append("order", args.order);
const query = params.toString();
return await client.get(`/opportunities${query ? `?${query}` : ""}`);
}
case "list_tasks": {
const params = new URLSearchParams();
if (args.limit) params.append("limit", args.limit.toString());
if (args.offset) params.append("offset", args.offset.toString());
if (args.contact_id) params.append("contact_id", args.contact_id.toString());
if (args.user_id) params.append("user_id", args.user_id.toString());
if (args.completed !== undefined) params.append("completed", args.completed.toString());
if (args.since) params.append("since", args.since);
if (args.until) params.append("until", args.until);
if (args.order) params.append("order", args.order);
const query = params.toString();
return await client.get(`/tasks${query ? `?${query}` : ""}`);
}
case "create_task": {
const payload: any = {
title: args.title,
};
if (args.description) payload.description = args.description;
if (args.contact) payload.contact = args.contact;
if (args.due_date) payload.due_date = args.due_date;
if (args.priority) payload.priority = args.priority;
if (args.type) payload.type = args.type;
if (args.user_id) payload.user_id = args.user_id;
if (args.remind_time) payload.remind_time = args.remind_time;
return await client.post("/tasks", payload);
}
case "list_tags": {
const params = new URLSearchParams();
if (args.limit) params.append("limit", args.limit.toString());
if (args.offset) params.append("offset", args.offset.toString());
if (args.category) params.append("category", args.category.toString());
if (args.name) params.append("name", args.name);
const query = params.toString();
return await client.get(`/tags${query ? `?${query}` : ""}`);
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// ============================================
// SERVER SETUP
// ============================================
async function main() {
const accessToken = process.env.KEAP_ACCESS_TOKEN;
if (!accessToken) {
console.error("Error: KEAP_ACCESS_TOKEN environment variable required");
console.error("Get your access token from the Keap Developer Portal after OAuth2 authorization");
process.exit(1);
}
const client = new KeapClient(accessToken);
const server = new Server(
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(client, name, args || {});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${MCP_NAME} MCP server running on stdio`);
}
main().catch(console.error);

Some files were not shown because too many files have changed in this diff Show More