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:
parent
ff349dc88f
commit
ced6b4933b
867
landing-pages/closebot.html
Normal file
867
landing-pages/closebot.html
Normal 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>
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
547
servers/bigcommerce/src/clients/bigcommerce.ts
Normal file
547
servers/bigcommerce/src/clients/bigcommerce.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
34
servers/bigcommerce/src/main.ts
Normal file
34
servers/bigcommerce/src/main.ts
Normal 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);
|
||||
});
|
||||
192
servers/bigcommerce/src/server.ts
Normal file
192
servers/bigcommerce/src/server.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
129
servers/bigcommerce/src/tools/analytics-tools.ts
Normal file
129
servers/bigcommerce/src/tools/analytics-tools.ts
Normal 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) }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
105
servers/bigcommerce/src/tools/brands-tools.ts
Normal file
105
servers/bigcommerce/src/tools/brands-tools.ts
Normal 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` }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
116
servers/bigcommerce/src/tools/carts-tools.ts
Normal file
116
servers/bigcommerce/src/tools/carts-tools.ts
Normal 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) }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
136
servers/bigcommerce/src/tools/categories-tools.ts
Normal file
136
servers/bigcommerce/src/tools/categories-tools.ts
Normal 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) }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
61
servers/bigcommerce/src/tools/channels-tools.ts
Normal file
61
servers/bigcommerce/src/tools/channels-tools.ts
Normal 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) }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
107
servers/bigcommerce/src/tools/content-tools.ts
Normal file
107
servers/bigcommerce/src/tools/content-tools.ts
Normal 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) }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
116
servers/bigcommerce/src/tools/coupons-tools.ts
Normal file
116
servers/bigcommerce/src/tools/coupons-tools.ts
Normal 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` }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
198
servers/bigcommerce/src/tools/customers-tools.ts
Normal file
198
servers/bigcommerce/src/tools/customers-tools.ts
Normal 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) }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
249
servers/bigcommerce/src/tools/orders-tools.ts
Normal file
249
servers/bigcommerce/src/tools/orders-tools.ts
Normal 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) }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
242
servers/bigcommerce/src/tools/products-tools.ts
Normal file
242
servers/bigcommerce/src/tools/products-tools.ts
Normal 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) }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
52
servers/bigcommerce/src/tools/shipping-tools.ts
Normal file
52
servers/bigcommerce/src/tools/shipping-tools.ts
Normal 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) }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
44
servers/bigcommerce/src/tools/store-tools.ts
Normal file
44
servers/bigcommerce/src/tools/store-tools.ts
Normal 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) }] };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
633
servers/bigcommerce/src/types/index.ts
Normal file
633
servers/bigcommerce/src/types/index.ts
Normal 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>;
|
||||
}
|
||||
@ -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',
|
||||
},
|
||||
};
|
||||
42
servers/bigcommerce/src/ui/react-app/blog-manager/App.tsx
Normal file
42
servers/bigcommerce/src/ui/react-app/blog-manager/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
42
servers/bigcommerce/src/ui/react-app/brand-manager/App.tsx
Normal file
42
servers/bigcommerce/src/ui/react-app/brand-manager/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
42
servers/bigcommerce/src/ui/react-app/cart-viewer/App.tsx
Normal file
42
servers/bigcommerce/src/ui/react-app/cart-viewer/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
42
servers/bigcommerce/src/ui/react-app/category-tree/App.tsx
Normal file
42
servers/bigcommerce/src/ui/react-app/category-tree/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
42
servers/bigcommerce/src/ui/react-app/channel-manager/App.tsx
Normal file
42
servers/bigcommerce/src/ui/react-app/channel-manager/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
42
servers/bigcommerce/src/ui/react-app/content-pages/App.tsx
Normal file
42
servers/bigcommerce/src/ui/react-app/content-pages/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
42
servers/bigcommerce/src/ui/react-app/coupon-manager/App.tsx
Normal file
42
servers/bigcommerce/src/ui/react-app/coupon-manager/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
174
servers/bigcommerce/src/ui/react-app/customer-detail/App.tsx
Normal file
174
servers/bigcommerce/src/ui/react-app/customer-detail/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
42
servers/bigcommerce/src/ui/react-app/customer-grid/App.tsx
Normal file
42
servers/bigcommerce/src/ui/react-app/customer-grid/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
@ -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',
|
||||
},
|
||||
};
|
||||
167
servers/bigcommerce/src/ui/react-app/order-dashboard/App.tsx
Normal file
167
servers/bigcommerce/src/ui/react-app/order-dashboard/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
172
servers/bigcommerce/src/ui/react-app/order-detail/App.tsx
Normal file
172
servers/bigcommerce/src/ui/react-app/order-detail/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
142
servers/bigcommerce/src/ui/react-app/order-grid/App.tsx
Normal file
142
servers/bigcommerce/src/ui/react-app/order-grid/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
174
servers/bigcommerce/src/ui/react-app/product-dashboard/App.tsx
Normal file
174
servers/bigcommerce/src/ui/react-app/product-dashboard/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
215
servers/bigcommerce/src/ui/react-app/product-detail/App.tsx
Normal file
215
servers/bigcommerce/src/ui/react-app/product-detail/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
117
servers/bigcommerce/src/ui/react-app/product-grid/App.tsx
Normal file
117
servers/bigcommerce/src/ui/react-app/product-grid/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
@ -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',
|
||||
},
|
||||
};
|
||||
@ -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',
|
||||
},
|
||||
};
|
||||
42
servers/bigcommerce/src/ui/react-app/store-overview/App.tsx
Normal file
42
servers/bigcommerce/src/ui/react-app/store-overview/App.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
@ -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
235
servers/brevo/README.md
Normal 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
|
||||
|
||||
[](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).
|
||||
@ -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"
|
||||
|
||||
129
servers/clickup/src/ui/react-app/calendar-view/App.tsx
Normal file
129
servers/clickup/src/ui/react-app/calendar-view/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/calendar-view/index.html
Normal file
12
servers/clickup/src/ui/react-app/calendar-view/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3007, open: true },
|
||||
});
|
||||
178
servers/clickup/src/ui/react-app/checklist-manager/App.tsx
Normal file
178
servers/clickup/src/ui/react-app/checklist-manager/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3014, open: true },
|
||||
});
|
||||
165
servers/clickup/src/ui/react-app/comment-thread/App.tsx
Normal file
165
servers/clickup/src/ui/react-app/comment-thread/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/comment-thread/index.html
Normal file
12
servers/clickup/src/ui/react-app/comment-thread/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3013, open: true },
|
||||
});
|
||||
129
servers/clickup/src/ui/react-app/doc-browser/App.tsx
Normal file
129
servers/clickup/src/ui/react-app/doc-browser/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/doc-browser/index.html
Normal file
12
servers/clickup/src/ui/react-app/doc-browser/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3012, open: true },
|
||||
});
|
||||
121
servers/clickup/src/ui/react-app/folder-overview/App.tsx
Normal file
121
servers/clickup/src/ui/react-app/folder-overview/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/folder-overview/index.html
Normal file
12
servers/clickup/src/ui/react-app/folder-overview/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3005, open: true },
|
||||
});
|
||||
164
servers/clickup/src/ui/react-app/goal-tracker/App.tsx
Normal file
164
servers/clickup/src/ui/react-app/goal-tracker/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/goal-tracker/index.html
Normal file
12
servers/clickup/src/ui/react-app/goal-tracker/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3008, open: true },
|
||||
});
|
||||
148
servers/clickup/src/ui/react-app/list-view/App.tsx
Normal file
148
servers/clickup/src/ui/react-app/list-view/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/list-view/index.html
Normal file
12
servers/clickup/src/ui/react-app/list-view/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3006, open: true },
|
||||
});
|
||||
152
servers/clickup/src/ui/react-app/member-workload/App.tsx
Normal file
152
servers/clickup/src/ui/react-app/member-workload/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/member-workload/index.html
Normal file
12
servers/clickup/src/ui/react-app/member-workload/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3011, open: true },
|
||||
});
|
||||
158
servers/clickup/src/ui/react-app/space-overview/App.tsx
Normal file
158
servers/clickup/src/ui/react-app/space-overview/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/space-overview/index.html
Normal file
12
servers/clickup/src/ui/react-app/space-overview/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3004, open: true },
|
||||
});
|
||||
188
servers/clickup/src/ui/react-app/tag-manager/App.tsx
Normal file
188
servers/clickup/src/ui/react-app/tag-manager/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/tag-manager/index.html
Normal file
12
servers/clickup/src/ui/react-app/tag-manager/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3015, open: true },
|
||||
});
|
||||
116
servers/clickup/src/ui/react-app/task-board/App.tsx
Normal file
116
servers/clickup/src/ui/react-app/task-board/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/task-board/index.html
Normal file
12
servers/clickup/src/ui/react-app/task-board/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3003, open: true },
|
||||
});
|
||||
131
servers/clickup/src/ui/react-app/task-dashboard/App.tsx
Normal file
131
servers/clickup/src/ui/react-app/task-dashboard/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
60
servers/clickup/src/ui/react-app/task-dashboard/index.html
Normal file
60
servers/clickup/src/ui/react-app/task-dashboard/index.html
Normal 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>
|
||||
@ -0,0 +1 @@
|
||||
/* Styles are inline in index.html for self-contained deployment */
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
208
servers/clickup/src/ui/react-app/task-detail/App.tsx
Normal file
208
servers/clickup/src/ui/react-app/task-detail/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/task-detail/index.html
Normal file
12
servers/clickup/src/ui/react-app/task-detail/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3001, open: true },
|
||||
});
|
||||
174
servers/clickup/src/ui/react-app/task-grid/App.tsx
Normal file
174
servers/clickup/src/ui/react-app/task-grid/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/task-grid/index.html
Normal file
12
servers/clickup/src/ui/react-app/task-grid/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3002, open: true },
|
||||
});
|
||||
120
servers/clickup/src/ui/react-app/template-gallery/App.tsx
Normal file
120
servers/clickup/src/ui/react-app/template-gallery/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/template-gallery/index.html
Normal file
12
servers/clickup/src/ui/react-app/template-gallery/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3016, open: true },
|
||||
});
|
||||
160
servers/clickup/src/ui/react-app/time-dashboard/App.tsx
Normal file
160
servers/clickup/src/ui/react-app/time-dashboard/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/time-dashboard/index.html
Normal file
12
servers/clickup/src/ui/react-app/time-dashboard/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3009, open: true },
|
||||
});
|
||||
146
servers/clickup/src/ui/react-app/time-entries/App.tsx
Normal file
146
servers/clickup/src/ui/react-app/time-entries/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/clickup/src/ui/react-app/time-entries/index.html
Normal file
12
servers/clickup/src/ui/react-app/time-entries/index.html
Normal 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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3010, open: true },
|
||||
});
|
||||
187
servers/clickup/src/ui/react-app/workspace-overview/App.tsx
Normal file
187
servers/clickup/src/ui/react-app/workspace-overview/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 3017, open: true },
|
||||
});
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
305
servers/keap/src/clients/keap.ts
Normal file
305
servers/keap/src/clients/keap.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user