calendly: Complete MCP server with 27 tools and 12 React apps

- Calendly API v2 client with auth, pagination, error handling
- 27 MCP tools across 6 categories (events, event types, scheduling, users, orgs, webhooks)
- 12 React MCP apps with dark theme and client-side state
- Both stdio and HTTP modes supported
- Full TypeScript types and documentation
This commit is contained in:
Jake Shore 2026-02-12 17:08:15 -05:00
parent 716f99056d
commit 8e9d1ffb87
85 changed files with 6386 additions and 788 deletions

181
servers/calendly/README.md Normal file
View File

@ -0,0 +1,181 @@
# Calendly MCP Server
Complete Model Context Protocol (MCP) server for Calendly API v2 with 27 tools and 12 React UI apps.
## Features
### 🛠️ 27 MCP Tools
**Events (8 tools)**
- `calendly_list_scheduled_events` - List events with filters
- `calendly_get_event` - Get event details
- `calendly_cancel_event` - Cancel an event
- `calendly_list_event_invitees` - List invitees for an event
- `calendly_get_invitee` - Get invitee details
- `calendly_list_no_shows` - List no-shows
- `calendly_mark_no_show` - Mark invitee as no-show
- `calendly_unmark_no_show` - Remove no-show status
**Event Types (3 tools)**
- `calendly_list_event_types` - List event types
- `calendly_get_event_type` - Get event type details
- `calendly_list_available_times` - List available time slots
**Scheduling (3 tools)**
- `calendly_create_scheduling_link` - Create single-use scheduling link
- `calendly_list_routing_forms` - List routing forms
- `calendly_get_routing_form` - Get routing form details
**Users (3 tools)**
- `calendly_get_current_user` - Get current user info
- `calendly_get_user` - Get user by URI
- `calendly_list_user_busy_times` - List user busy times
**Organizations (6 tools)**
- `calendly_get_organization` - Get organization details
- `calendly_list_organization_members` - List members
- `calendly_list_organization_invitations` - List invitations
- `calendly_invite_user` - Invite user to organization
- `calendly_revoke_invitation` - Revoke invitation
- `calendly_remove_organization_member` - Remove member
**Webhooks (4 tools)**
- `calendly_list_webhook_subscriptions` - List webhooks
- `calendly_create_webhook_subscription` - Create webhook
- `calendly_get_webhook_subscription` - Get webhook details
- `calendly_delete_webhook_subscription` - Delete webhook
### 🎨 12 React MCP Apps
All apps feature dark theme and client-side state management:
1. **Event Dashboard** (`src/ui/react-app/event-dashboard`) - Overview of scheduled events
2. **Event Detail** (`src/ui/react-app/event-detail`) - Detailed event information
3. **Event Grid** (`src/ui/react-app/event-grid`) - Calendar grid view
4. **Event Type Manager** (`src/ui/react-app/event-type-manager`) - Manage event types
5. **Availability Calendar** (`src/ui/react-app/availability-calendar`) - View available times
6. **Invitee List** (`src/ui/react-app/invitee-list`) - Manage event invitees
7. **Scheduling Links** (`src/ui/react-app/scheduling-links`) - Create scheduling links
8. **Organization Members** (`src/ui/react-app/org-members`) - Manage team members
9. **Webhook Manager** (`src/ui/react-app/webhook-manager`) - Manage webhooks
10. **Booking Flow** (`src/ui/react-app/booking-flow`) - Multi-step booking interface
11. **No-Show Tracker** (`src/ui/react-app/no-show-tracker`) - Track no-shows
12. **Analytics Dashboard** (`src/ui/react-app/analytics-dashboard`) - Metrics and insights
## Installation
```bash
npm install
npm run build
```
## Configuration
Set your Calendly API key as an environment variable:
```bash
export CALENDLY_API_KEY="your_api_key_here"
```
Get your API key from: https://calendly.com/integrations/api_webhooks
## Usage
### Stdio Mode (Default for MCP)
```bash
npm start
```
Use in your MCP client configuration:
```json
{
"mcpServers": {
"calendly": {
"command": "node",
"args": ["/path/to/calendly/dist/main.js"],
"env": {
"CALENDLY_API_KEY": "your_api_key"
}
}
}
}
```
### HTTP Mode
```bash
npm run start:http
```
Server runs on `http://localhost:3000`
Endpoints:
- `GET /health` - Health check
- `POST /` - MCP requests (tools/list, tools/call, resources/list, resources/read)
## API Client
The Calendly client (`src/clients/calendly.ts`) provides:
- ✅ Full Calendly API v2 support
- ✅ Bearer token authentication
- ✅ Automatic pagination handling
- ✅ Error handling with detailed messages
- ✅ Type-safe responses
## Architecture
```
src/
├── clients/
│ └── calendly.ts # Calendly API v2 client
├── tools/
│ ├── events-tools.ts # Event management tools
│ ├── event-types-tools.ts # Event type tools
│ ├── scheduling-tools.ts # Scheduling & routing tools
│ ├── users-tools.ts # User management tools
│ ├── organizations-tools.ts # Organization tools
│ └── webhooks-tools.ts # Webhook tools
├── types/
│ └── index.ts # TypeScript definitions
├── ui/
│ └── react-app/ # 12 React MCP apps
├── server.ts # MCP server setup
└── main.ts # Entry point (stdio + HTTP)
```
## Development
### Build
```bash
npm run build
```
### Watch Mode
```bash
npm run dev
```
### Run React Apps
Each app is standalone:
```bash
cd src/ui/react-app/event-dashboard
npm install
npm run dev
```
## Resources
- Calendly API Documentation: https://developer.calendly.com/api-docs
- Model Context Protocol: https://modelcontextprotocol.io
- MCP SDK: https://github.com/modelcontextprotocol/typescript-sdk
## License
MIT

View File

@ -1,20 +1,35 @@
{ {
"name": "mcp-server-calendly", "name": "@mcpengine/calendly-server",
"version": "1.0.0", "version": "1.0.0",
"description": "Complete Calendly MCP server with 30+ tools and React UI apps",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "./dist/main.js",
"scripts": { "bin": {
"build": "tsc", "calendly-mcp": "./dist/main.js"
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
}, },
"scripts": {
"build": "tsc && chmod +x dist/main.js",
"dev": "tsc --watch",
"start": "node dist/main.js",
"start:http": "MCP_MODE=http node dist/main.js",
"test": "echo \"No tests yet\" && exit 0"
},
"keywords": [
"mcp",
"calendly",
"scheduling",
"model-context-protocol"
],
"author": "MCPEngine",
"license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0", "@modelcontextprotocol/sdk": "^1.0.4"
"zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.0", "@types/node": "^22.10.5",
"tsx": "^4.7.0", "typescript": "^5.7.3"
"typescript": "^5.3.0" },
"engines": {
"node": ">=18.0.0"
} }
} }

View File

@ -0,0 +1,322 @@
// Calendly API v2 Client
import type {
CalendlyConfig,
CalendlyUser,
CalendlyEvent,
CalendlyEventType,
CalendlyInvitee,
CalendlyOrganization,
CalendlyOrganizationMembership,
CalendlyOrganizationInvitation,
CalendlySchedulingLink,
CalendlyWebhookSubscription,
CalendlyAvailableTime,
CalendlyUserBusyTime,
CalendlyRoutingForm,
CalendlyNoShow,
PaginationParams,
PaginatedResponse,
CalendlyError,
} from '../types/index.js';
export class CalendlyClient {
private apiKey: string;
private baseUrl: string;
constructor(config: CalendlyConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || 'https://api.calendly.com';
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const headers = {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
...options.headers,
};
try {
const response = await fetch(url, {
...options,
headers,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const error: CalendlyError = errorData as CalendlyError;
throw new Error(
`Calendly API Error (${response.status}): ${error.title || 'Unknown error'} - ${error.message || response.statusText}`
);
}
// Handle 204 No Content
if (response.status === 204) {
return {} as T;
}
const data: unknown = await response.json();
return data as T;
} catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error(`Network error: ${String(error)}`);
}
}
private buildQueryString(params: Record<string, any>): string {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.append(key, String(value));
}
});
const query = searchParams.toString();
return query ? `?${query}` : '';
}
// Users
async getCurrentUser(): Promise<{ resource: CalendlyUser }> {
return this.request('/users/me');
}
async getUserByUri(uri: string): Promise<{ resource: CalendlyUser }> {
return this.request(`/users/${encodeURIComponent(uri)}`);
}
// Events
async listEvents(params: {
organization?: string;
user?: string;
invitee_email?: string;
status?: 'active' | 'canceled';
min_start_time?: string;
max_start_time?: string;
count?: number;
page_token?: string;
sort?: string;
}): Promise<PaginatedResponse<CalendlyEvent>> {
const query = this.buildQueryString(params);
return this.request(`/scheduled_events${query}`);
}
async getEvent(uuid: string): Promise<{ resource: CalendlyEvent }> {
return this.request(`/scheduled_events/${uuid}`);
}
async cancelEvent(uuid: string, reason?: string): Promise<{ resource: CalendlyEvent }> {
return this.request(`/scheduled_events/${uuid}/cancellation`, {
method: 'POST',
body: JSON.stringify({ reason: reason || 'Canceled' }),
});
}
// Event Invitees
async listEventInvitees(
eventUuid: string,
params?: PaginationParams
): Promise<PaginatedResponse<CalendlyInvitee>> {
const query = this.buildQueryString(params || {});
return this.request(`/scheduled_events/${eventUuid}/invitees${query}`);
}
async getInvitee(inviteeUuid: string): Promise<{ resource: CalendlyInvitee }> {
return this.request(`/scheduled_events/invitees/${inviteeUuid}`);
}
// No-shows
async listInviteeNoShows(inviteeUri: string): Promise<PaginatedResponse<CalendlyNoShow>> {
const query = this.buildQueryString({ invitee: inviteeUri });
return this.request(`/invitee_no_shows${query}`);
}
async createInviteeNoShow(inviteeUri: string): Promise<{ resource: CalendlyNoShow }> {
return this.request('/invitee_no_shows', {
method: 'POST',
body: JSON.stringify({ invitee: inviteeUri }),
});
}
async deleteInviteeNoShow(noShowUuid: string): Promise<void> {
return this.request(`/invitee_no_shows/${noShowUuid}`, {
method: 'DELETE',
});
}
// Event Types
async listEventTypes(params: {
organization?: string;
user?: string;
active?: boolean;
count?: number;
page_token?: string;
sort?: string;
}): Promise<PaginatedResponse<CalendlyEventType>> {
const query = this.buildQueryString(params);
return this.request(`/event_types${query}`);
}
async getEventType(uuid: string): Promise<{ resource: CalendlyEventType }> {
return this.request(`/event_types/${uuid}`);
}
async listAvailableTimes(
eventTypeUri: string,
params: {
start_time: string;
end_time: string;
}
): Promise<PaginatedResponse<CalendlyAvailableTime>> {
const query = this.buildQueryString({
event_type: eventTypeUri,
...params,
});
return this.request(`/event_type_available_times${query}`);
}
// User Availability & Busy Times
async getUserBusyTimes(
userUri: string,
params: {
start_time: string;
end_time: string;
}
): Promise<PaginatedResponse<CalendlyUserBusyTime>> {
const query = this.buildQueryString({
user: userUri,
...params,
});
return this.request(`/user_busy_times${query}`);
}
// Scheduling Links
async createSchedulingLink(params: {
max_event_count: number;
owner: string;
owner_type: 'EventType' | 'Group';
}): Promise<{ resource: CalendlySchedulingLink }> {
return this.request('/scheduling_links', {
method: 'POST',
body: JSON.stringify(params),
});
}
// Organizations
async getOrganization(uuid: string): Promise<{ resource: CalendlyOrganization }> {
return this.request(`/organizations/${uuid}`);
}
async listOrganizationMemberships(
organizationUri: string,
params?: {
count?: number;
page_token?: string;
email?: string;
}
): Promise<PaginatedResponse<CalendlyOrganizationMembership>> {
const query = this.buildQueryString({
organization: organizationUri,
...params,
});
return this.request(`/organization_memberships${query}`);
}
async listOrganizationInvitations(
organizationUri: string,
params?: {
count?: number;
page_token?: string;
email?: string;
status?: string;
}
): Promise<PaginatedResponse<CalendlyOrganizationInvitation>> {
const query = this.buildQueryString({
organization: organizationUri,
...params,
});
return this.request(`/organization_invitations${query}`);
}
async inviteUserToOrganization(
organizationUri: string,
email: string
): Promise<{ resource: CalendlyOrganizationInvitation }> {
return this.request('/organization_invitations', {
method: 'POST',
body: JSON.stringify({
organization: organizationUri,
email,
}),
});
}
async revokeOrganizationInvitation(uuid: string): Promise<{ resource: CalendlyOrganizationInvitation }> {
return this.request(`/organization_invitations/${uuid}`, {
method: 'DELETE',
});
}
async removeOrganizationMembership(uuid: string): Promise<void> {
return this.request(`/organization_memberships/${uuid}`, {
method: 'DELETE',
});
}
// Routing Forms
async listRoutingForms(
organizationUri: string,
params?: PaginationParams
): Promise<PaginatedResponse<CalendlyRoutingForm>> {
const query = this.buildQueryString({
organization: organizationUri,
...params,
});
return this.request(`/routing_forms${query}`);
}
async getRoutingForm(uuid: string): Promise<{ resource: CalendlyRoutingForm }> {
return this.request(`/routing_forms/${uuid}`);
}
// Webhooks
async listWebhookSubscriptions(
params: {
organization: string;
scope: 'user' | 'organization';
user?: string;
} & PaginationParams
): Promise<PaginatedResponse<CalendlyWebhookSubscription>> {
const query = this.buildQueryString(params);
return this.request(`/webhook_subscriptions${query}`);
}
async createWebhookSubscription(params: {
url: string;
events: string[];
organization: string;
user?: string;
scope: 'user' | 'organization';
signing_key?: string;
}): Promise<{ resource: CalendlyWebhookSubscription }> {
return this.request('/webhook_subscriptions', {
method: 'POST',
body: JSON.stringify(params),
});
}
async getWebhookSubscription(uuid: string): Promise<{ resource: CalendlyWebhookSubscription }> {
return this.request(`/webhook_subscriptions/${uuid}`);
}
async deleteWebhookSubscription(uuid: string): Promise<void> {
return this.request(`/webhook_subscriptions/${uuid}`, {
method: 'DELETE',
});
}
}

View File

@ -1,271 +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 = "calendly";
const MCP_VERSION = "1.0.0";
const API_BASE_URL = "https://api.calendly.com";
// ============================================
// API CLIENT - Calendly API v2
// ============================================
class CalendlyClient {
private apiKey: string;
private baseUrl: string;
private currentUserUri: string | null = null;
constructor(apiKey: string) {
this.apiKey = apiKey;
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.apiKey}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Calendly API error: ${response.status} ${response.statusText} - ${errorBody}`);
}
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 delete(endpoint: string) {
return this.request(endpoint, { method: "DELETE" });
}
async getCurrentUser(): Promise<string> {
if (!this.currentUserUri) {
const result = await this.get("/users/me");
this.currentUserUri = result.resource.uri;
}
return this.currentUserUri!;
}
}
// ============================================
// TOOL DEFINITIONS - Calendly API v2
// ============================================
const tools = [
{
name: "list_events",
description: "List scheduled events. Returns events for the authenticated user within the specified time range.",
inputSchema: {
type: "object" as const,
properties: {
count: { type: "number", description: "Number of events to return (max 100)" },
min_start_time: { type: "string", description: "Start of time range (ISO 8601 format)" },
max_start_time: { type: "string", description: "End of time range (ISO 8601 format)" },
status: { type: "string", enum: ["active", "canceled"], description: "Filter by event status" },
page_token: { type: "string", description: "Token for pagination" },
},
},
},
{
name: "get_event",
description: "Get details of a specific scheduled event by its UUID",
inputSchema: {
type: "object" as const,
properties: {
event_uuid: { type: "string", description: "The UUID of the scheduled event" },
},
required: ["event_uuid"],
},
},
{
name: "cancel_event",
description: "Cancel a scheduled event. Optionally provide a reason for cancellation.",
inputSchema: {
type: "object" as const,
properties: {
event_uuid: { type: "string", description: "The UUID of the scheduled event to cancel" },
reason: { type: "string", description: "Reason for cancellation (optional)" },
},
required: ["event_uuid"],
},
},
{
name: "list_event_types",
description: "List all event types available for the authenticated user",
inputSchema: {
type: "object" as const,
properties: {
count: { type: "number", description: "Number of event types to return (max 100)" },
active: { type: "boolean", description: "Filter by active status" },
page_token: { type: "string", description: "Token for pagination" },
},
},
},
{
name: "get_availability",
description: "Get available time slots for an event type",
inputSchema: {
type: "object" as const,
properties: {
event_type_uuid: { type: "string", description: "The UUID of the event type" },
start_time: { type: "string", description: "Start of availability window (ISO 8601)" },
end_time: { type: "string", description: "End of availability window (ISO 8601)" },
},
required: ["event_type_uuid", "start_time", "end_time"],
},
},
{
name: "list_invitees",
description: "List invitees for a scheduled event",
inputSchema: {
type: "object" as const,
properties: {
event_uuid: { type: "string", description: "The UUID of the scheduled event" },
count: { type: "number", description: "Number of invitees to return (max 100)" },
status: { type: "string", enum: ["active", "canceled"], description: "Filter by invitee status" },
page_token: { type: "string", description: "Token for pagination" },
},
required: ["event_uuid"],
},
},
{
name: "get_user",
description: "Get the current authenticated user's information",
inputSchema: {
type: "object" as const,
properties: {},
},
},
];
// ============================================
// TOOL HANDLERS
// ============================================
async function handleTool(client: CalendlyClient, name: string, args: any) {
switch (name) {
case "list_events": {
const userUri = await client.getCurrentUser();
const params = new URLSearchParams({ user: userUri });
if (args.count) params.append("count", String(args.count));
if (args.min_start_time) params.append("min_start_time", args.min_start_time);
if (args.max_start_time) params.append("max_start_time", args.max_start_time);
if (args.status) params.append("status", args.status);
if (args.page_token) params.append("page_token", args.page_token);
return await client.get(`/scheduled_events?${params.toString()}`);
}
case "get_event": {
const { event_uuid } = args;
return await client.get(`/scheduled_events/${event_uuid}`);
}
case "cancel_event": {
const { event_uuid, reason } = args;
const body: any = {};
if (reason) body.reason = reason;
return await client.post(`/scheduled_events/${event_uuid}/cancellation`, body);
}
case "list_event_types": {
const userUri = await client.getCurrentUser();
const params = new URLSearchParams({ user: userUri });
if (args.count) params.append("count", String(args.count));
if (args.active !== undefined) params.append("active", String(args.active));
if (args.page_token) params.append("page_token", args.page_token);
return await client.get(`/event_types?${params.toString()}`);
}
case "get_availability": {
const { event_type_uuid, start_time, end_time } = args;
const params = new URLSearchParams({
start_time,
end_time,
});
return await client.get(`/event_type_available_times?event_type=https://api.calendly.com/event_types/${event_type_uuid}&${params.toString()}`);
}
case "list_invitees": {
const { event_uuid, count, status, page_token } = args;
const params = new URLSearchParams();
if (count) params.append("count", String(count));
if (status) params.append("status", status);
if (page_token) params.append("page_token", page_token);
const queryString = params.toString();
return await client.get(`/scheduled_events/${event_uuid}/invitees${queryString ? '?' + queryString : ''}`);
}
case "get_user": {
return await client.get("/users/me");
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// ============================================
// SERVER SETUP
// ============================================
async function main() {
const apiKey = process.env.CALENDLY_API_KEY;
if (!apiKey) {
console.error("Error: CALENDLY_API_KEY environment variable required");
console.error("Get your Personal Access Token from: https://calendly.com/integrations/api_webhooks");
process.exit(1);
}
const client = new CalendlyClient(apiKey);
const server = new Server(
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(client, name, args || {});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${MCP_NAME} MCP server running on stdio`);
}
main().catch(console.error);

View File

@ -0,0 +1,101 @@
#!/usr/bin/env node
// Calendly MCP Server - Main Entry Point
// Supports both stdio and HTTP modes
import { createCalendlyServer, runStdioServer } from './server.js';
import { createServer } from 'http';
const API_KEY = process.env.CALENDLY_API_KEY;
const MODE = process.env.MCP_MODE || 'stdio'; // stdio or http
const PORT = parseInt(process.env.PORT || '3000', 10);
if (!API_KEY) {
console.error('Error: CALENDLY_API_KEY environment variable is required');
process.exit(1);
}
async function main() {
if (MODE === 'http') {
// HTTP mode - useful for web-based MCP apps
const mcpServer = createCalendlyServer(API_KEY!);
const httpServer = createServer(async (req, res) => {
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
if (req.method === 'GET' && req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', mode: 'http' }));
return;
}
if (req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', async () => {
try {
const request = JSON.parse(body);
// Handle MCP requests
if (request.method === 'tools/list') {
const tools = await (mcpServer as any).requestHandlers.get('tools/list')?.();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(tools));
} else if (request.method === 'tools/call') {
const result = await (mcpServer as any).requestHandlers.get('tools/call')?.(request);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
} else if (request.method === 'resources/list') {
const resources = await (mcpServer as any).requestHandlers.get('resources/list')?.();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(resources));
} else if (request.method === 'resources/read') {
const resource = await (mcpServer as any).requestHandlers.get('resources/read')?.(request);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(resource));
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unknown method' }));
}
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: error instanceof Error ? error.message : 'Unknown error',
}));
}
});
} else {
res.writeHead(404);
res.end();
}
});
httpServer.listen(PORT, () => {
console.error(`Calendly MCP Server running on http://localhost:${PORT}`);
console.error('Mode: HTTP');
console.error('Endpoints:');
console.error(' GET /health - Health check');
console.error(' POST / - MCP requests');
});
} else {
// Stdio mode - default for MCP
await runStdioServer(API_KEY!);
}
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

View File

@ -0,0 +1,129 @@
// Calendly MCP Server
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 { CalendlyClient } from './clients/calendly.js';
import { createEventsTools } from './tools/events-tools.js';
import { createEventTypesTools } from './tools/event-types-tools.js';
import { createSchedulingTools } from './tools/scheduling-tools.js';
import { createUsersTools } from './tools/users-tools.js';
import { createOrganizationsTools } from './tools/organizations-tools.js';
import { createWebhooksTools } from './tools/webhooks-tools.js';
export function createCalendlyServer(apiKey: string) {
const server = new Server(
{
name: 'calendly-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// Initialize Calendly client
const client = new CalendlyClient({
apiKey,
});
// Collect all tools
const allTools = {
...createEventsTools(client),
...createEventTypesTools(client),
...createSchedulingTools(client),
...createUsersTools(client),
...createOrganizationsTools(client),
...createWebhooksTools(client),
};
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: Object.entries(allTools).map(([name, tool]) => ({
name,
description: tool.description,
inputSchema: tool.parameters,
})),
};
});
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
const tool = allTools[toolName as keyof typeof allTools];
if (!tool) {
throw new Error(`Unknown tool: ${toolName}`);
}
try {
return await tool.handler(request.params.arguments || {});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: errorMessage,
tool: toolName,
}, null, 2),
},
],
isError: true,
};
}
});
// Resources - expose current user as a resource
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'calendly://user/me',
name: 'Current User',
description: 'Currently authenticated Calendly user',
mimeType: 'application/json',
},
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
if (uri === 'calendly://user/me') {
const user = await client.getCurrentUser();
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(user, null, 2),
},
],
};
}
throw new Error(`Unknown resource: ${uri}`);
});
return server;
}
export async function runStdioServer(apiKey: string) {
const server = createCalendlyServer(apiKey);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Calendly MCP Server running on stdio');
}

View File

@ -0,0 +1,113 @@
// Event Types Tools
import { CalendlyClient } from '../clients/calendly.js';
export function createEventTypesTools(client: CalendlyClient) {
return {
calendly_list_event_types: {
description: 'List event types for a user or organization',
parameters: {
type: 'object',
properties: {
organization: {
type: 'string',
description: 'Organization URI to filter by',
},
user: {
type: 'string',
description: 'User URI to filter by',
},
active: {
type: 'boolean',
description: 'Filter by active status',
},
count: {
type: 'number',
description: 'Number of results per page (max 100)',
default: 20,
},
page_token: {
type: 'string',
description: 'Pagination token',
},
sort: {
type: 'string',
description: 'Sort order (e.g., name:asc, name:desc)',
},
},
},
handler: async (args: any) => {
const result = await client.listEventTypes(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_get_event_type: {
description: 'Get details of a specific event type by UUID',
parameters: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Event type UUID',
},
},
required: ['uuid'],
},
handler: async (args: any) => {
const result = await client.getEventType(args.uuid);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_list_available_times: {
description: 'List available time slots for an event type within a date range',
parameters: {
type: 'object',
properties: {
event_type_uri: {
type: 'string',
description: 'Event type URI',
},
start_time: {
type: 'string',
description: 'Start of range (ISO 8601)',
},
end_time: {
type: 'string',
description: 'End of range (ISO 8601)',
},
},
required: ['event_type_uri', 'start_time', 'end_time'],
},
handler: async (args: any) => {
const result = await client.listAvailableTimes(args.event_type_uri, {
start_time: args.start_time,
end_time: args.end_time,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
};
}

View File

@ -0,0 +1,262 @@
// Events Tools
import { CalendlyClient } from '../clients/calendly.js';
export function createEventsTools(client: CalendlyClient) {
return {
calendly_list_scheduled_events: {
description: 'List scheduled events with filters (organization, user, status, date range)',
parameters: {
type: 'object',
properties: {
organization: {
type: 'string',
description: 'Organization URI to filter by',
},
user: {
type: 'string',
description: 'User URI to filter by',
},
invitee_email: {
type: 'string',
description: 'Filter by invitee email',
},
status: {
type: 'string',
enum: ['active', 'canceled'],
description: 'Event status filter',
},
min_start_time: {
type: 'string',
description: 'Minimum start time (ISO 8601)',
},
max_start_time: {
type: 'string',
description: 'Maximum start time (ISO 8601)',
},
count: {
type: 'number',
description: 'Number of results per page (max 100)',
default: 20,
},
page_token: {
type: 'string',
description: 'Pagination token',
},
sort: {
type: 'string',
description: 'Sort order (e.g., start_time:asc, start_time:desc)',
},
},
},
handler: async (args: any) => {
const result = await client.listEvents(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_get_event: {
description: 'Get details of a specific scheduled event by UUID',
parameters: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Event UUID (from event URI)',
},
},
required: ['uuid'],
},
handler: async (args: any) => {
const result = await client.getEvent(args.uuid);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_cancel_event: {
description: 'Cancel a scheduled event',
parameters: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Event UUID to cancel',
},
reason: {
type: 'string',
description: 'Cancellation reason',
default: 'Event canceled',
},
},
required: ['uuid'],
},
handler: async (args: any) => {
const result = await client.cancelEvent(args.uuid, args.reason);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_list_event_invitees: {
description: 'List invitees for a specific event',
parameters: {
type: 'object',
properties: {
event_uuid: {
type: 'string',
description: 'Event UUID',
},
count: {
type: 'number',
description: 'Number of results per page',
default: 20,
},
page_token: {
type: 'string',
description: 'Pagination token',
},
sort: {
type: 'string',
description: 'Sort order',
},
},
required: ['event_uuid'],
},
handler: async (args: any) => {
const result = await client.listEventInvitees(args.event_uuid, {
count: args.count,
page_token: args.page_token,
sort: args.sort,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_get_invitee: {
description: 'Get details of a specific invitee',
parameters: {
type: 'object',
properties: {
invitee_uuid: {
type: 'string',
description: 'Invitee UUID',
},
},
required: ['invitee_uuid'],
},
handler: async (args: any) => {
const result = await client.getInvitee(args.invitee_uuid);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_list_no_shows: {
description: 'List no-shows for a specific invitee',
parameters: {
type: 'object',
properties: {
invitee_uri: {
type: 'string',
description: 'Invitee URI',
},
},
required: ['invitee_uri'],
},
handler: async (args: any) => {
const result = await client.listInviteeNoShows(args.invitee_uri);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_mark_no_show: {
description: 'Mark an invitee as a no-show',
parameters: {
type: 'object',
properties: {
invitee_uri: {
type: 'string',
description: 'Invitee URI to mark as no-show',
},
},
required: ['invitee_uri'],
},
handler: async (args: any) => {
const result = await client.createInviteeNoShow(args.invitee_uri);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_unmark_no_show: {
description: 'Remove no-show status from an invitee',
parameters: {
type: 'object',
properties: {
no_show_uuid: {
type: 'string',
description: 'No-show UUID to delete',
},
},
required: ['no_show_uuid'],
},
handler: async (args: any) => {
await client.deleteInviteeNoShow(args.no_show_uuid);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, message: 'No-show removed' }),
},
],
};
},
},
};
}

View File

@ -0,0 +1,204 @@
// Organizations Tools
import { CalendlyClient } from '../clients/calendly.js';
export function createOrganizationsTools(client: CalendlyClient) {
return {
calendly_get_organization: {
description: 'Get organization details by UUID',
parameters: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Organization UUID',
},
},
required: ['uuid'],
},
handler: async (args: any) => {
const result = await client.getOrganization(args.uuid);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_list_organization_members: {
description: 'List members of an organization',
parameters: {
type: 'object',
properties: {
organization_uri: {
type: 'string',
description: 'Organization URI',
},
email: {
type: 'string',
description: 'Filter by email address',
},
count: {
type: 'number',
description: 'Number of results per page',
default: 20,
},
page_token: {
type: 'string',
description: 'Pagination token',
},
},
required: ['organization_uri'],
},
handler: async (args: any) => {
const result = await client.listOrganizationMemberships(args.organization_uri, {
email: args.email,
count: args.count,
page_token: args.page_token,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_list_organization_invitations: {
description: 'List pending invitations for an organization',
parameters: {
type: 'object',
properties: {
organization_uri: {
type: 'string',
description: 'Organization URI',
},
email: {
type: 'string',
description: 'Filter by email address',
},
status: {
type: 'string',
enum: ['pending', 'accepted', 'declined', 'revoked'],
description: 'Filter by invitation status',
},
count: {
type: 'number',
description: 'Number of results per page',
default: 20,
},
page_token: {
type: 'string',
description: 'Pagination token',
},
},
required: ['organization_uri'],
},
handler: async (args: any) => {
const result = await client.listOrganizationInvitations(args.organization_uri, {
email: args.email,
status: args.status,
count: args.count,
page_token: args.page_token,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_invite_user: {
description: 'Invite a user to an organization by email',
parameters: {
type: 'object',
properties: {
organization_uri: {
type: 'string',
description: 'Organization URI',
},
email: {
type: 'string',
description: 'Email address to invite',
},
},
required: ['organization_uri', 'email'],
},
handler: async (args: any) => {
const result = await client.inviteUserToOrganization(
args.organization_uri,
args.email
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_revoke_invitation: {
description: 'Revoke a pending organization invitation',
parameters: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Invitation UUID to revoke',
},
},
required: ['uuid'],
},
handler: async (args: any) => {
const result = await client.revokeOrganizationInvitation(args.uuid);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_remove_organization_member: {
description: 'Remove a member from an organization',
parameters: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Organization membership UUID to remove',
},
},
required: ['uuid'],
},
handler: async (args: any) => {
await client.removeOrganizationMembership(args.uuid);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, message: 'Member removed' }),
},
],
};
},
},
};
}

View File

@ -0,0 +1,113 @@
// Scheduling Tools
import { CalendlyClient } from '../clients/calendly.js';
export function createSchedulingTools(client: CalendlyClient) {
return {
calendly_create_scheduling_link: {
description: 'Create a single-use scheduling link for an event type or group',
parameters: {
type: 'object',
properties: {
owner: {
type: 'string',
description: 'Owner URI (event type or group)',
},
owner_type: {
type: 'string',
enum: ['EventType', 'Group'],
description: 'Type of owner',
},
max_event_count: {
type: 'number',
description: 'Maximum number of events that can be scheduled (1-1000)',
default: 1,
},
},
required: ['owner', 'owner_type'],
},
handler: async (args: any) => {
const result = await client.createSchedulingLink({
owner: args.owner,
owner_type: args.owner_type,
max_event_count: args.max_event_count || 1,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_list_routing_forms: {
description: 'List routing forms for an organization',
parameters: {
type: 'object',
properties: {
organization_uri: {
type: 'string',
description: 'Organization URI',
},
count: {
type: 'number',
description: 'Number of results per page',
default: 20,
},
page_token: {
type: 'string',
description: 'Pagination token',
},
sort: {
type: 'string',
description: 'Sort order',
},
},
required: ['organization_uri'],
},
handler: async (args: any) => {
const result = await client.listRoutingForms(args.organization_uri, {
count: args.count,
page_token: args.page_token,
sort: args.sort,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_get_routing_form: {
description: 'Get details of a specific routing form',
parameters: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Routing form UUID',
},
},
required: ['uuid'],
},
handler: async (args: any) => {
const result = await client.getRoutingForm(args.uuid);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
};
}

View File

@ -0,0 +1,87 @@
// Users Tools
import { CalendlyClient } from '../clients/calendly.js';
export function createUsersTools(client: CalendlyClient) {
return {
calendly_get_current_user: {
description: 'Get the currently authenticated user information',
parameters: {
type: 'object',
properties: {},
},
handler: async (args: any) => {
const result = await client.getCurrentUser();
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_get_user: {
description: 'Get user information by URI',
parameters: {
type: 'object',
properties: {
uri: {
type: 'string',
description: 'User URI',
},
},
required: ['uri'],
},
handler: async (args: any) => {
const result = await client.getUserByUri(args.uri);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_list_user_busy_times: {
description: 'List busy time blocks for a user within a date range',
parameters: {
type: 'object',
properties: {
user_uri: {
type: 'string',
description: 'User URI',
},
start_time: {
type: 'string',
description: 'Start of range (ISO 8601)',
},
end_time: {
type: 'string',
description: 'End of range (ISO 8601)',
},
},
required: ['user_uri', 'start_time', 'end_time'],
},
handler: async (args: any) => {
const result = await client.getUserBusyTimes(args.user_uri, {
start_time: args.start_time,
end_time: args.end_time,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
};
}

View File

@ -0,0 +1,162 @@
// Webhooks Tools
import { CalendlyClient } from '../clients/calendly.js';
export function createWebhooksTools(client: CalendlyClient) {
return {
calendly_list_webhook_subscriptions: {
description: 'List webhook subscriptions for an organization',
parameters: {
type: 'object',
properties: {
organization: {
type: 'string',
description: 'Organization URI',
},
scope: {
type: 'string',
enum: ['user', 'organization'],
description: 'Webhook scope',
},
user: {
type: 'string',
description: 'User URI (for user-scoped webhooks)',
},
count: {
type: 'number',
description: 'Number of results per page',
default: 20,
},
page_token: {
type: 'string',
description: 'Pagination token',
},
},
required: ['organization', 'scope'],
},
handler: async (args: any) => {
const result = await client.listWebhookSubscriptions({
organization: args.organization,
scope: args.scope,
user: args.user,
count: args.count,
page_token: args.page_token,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_create_webhook_subscription: {
description: 'Create a new webhook subscription',
parameters: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'Webhook callback URL',
},
events: {
type: 'array',
items: {
type: 'string',
},
description: 'Array of event types to subscribe to (e.g., ["invitee.created", "invitee.canceled"])',
},
organization: {
type: 'string',
description: 'Organization URI',
},
scope: {
type: 'string',
enum: ['user', 'organization'],
description: 'Webhook scope',
},
user: {
type: 'string',
description: 'User URI (for user-scoped webhooks)',
},
signing_key: {
type: 'string',
description: 'Optional signing key for webhook verification',
},
},
required: ['url', 'events', 'organization', 'scope'],
},
handler: async (args: any) => {
const result = await client.createWebhookSubscription({
url: args.url,
events: args.events,
organization: args.organization,
scope: args.scope,
user: args.user,
signing_key: args.signing_key,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_get_webhook_subscription: {
description: 'Get details of a specific webhook subscription',
parameters: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Webhook subscription UUID',
},
},
required: ['uuid'],
},
handler: async (args: any) => {
const result = await client.getWebhookSubscription(args.uuid);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
calendly_delete_webhook_subscription: {
description: 'Delete a webhook subscription',
parameters: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Webhook subscription UUID to delete',
},
},
required: ['uuid'],
},
handler: async (args: any) => {
await client.deleteWebhookSubscription(args.uuid);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, message: 'Webhook subscription deleted' }),
},
],
};
},
},
};
}

View File

@ -0,0 +1,279 @@
// Calendly API v2 Types
export interface CalendlyConfig {
apiKey: string;
baseUrl?: string;
}
export interface CalendlyUser {
uri: string;
name: string;
slug: string;
email: string;
scheduling_url: string;
timezone: string;
avatar_url: string;
created_at: string;
updated_at: string;
current_organization: string;
resource_type: string;
}
export interface CalendlyEvent {
uri: string;
name: string;
meeting_notes_plain: string;
meeting_notes_html: string;
status: 'active' | 'canceled';
start_time: string;
end_time: string;
event_type: string;
location: {
type: string;
location?: string;
join_url?: string;
};
invitees_counter: {
total: number;
active: number;
limit: number;
};
created_at: string;
updated_at: string;
event_memberships: Array<{
user: string;
}>;
event_guests: Array<{
email: string;
created_at: string;
}>;
cancellation?: {
canceled_by: string;
reason: string;
canceler_type: string;
};
}
export interface CalendlyEventType {
uri: string;
name: string;
active: boolean;
slug: string;
scheduling_url: string;
duration: number;
kind: 'solo' | 'group' | 'collective' | 'round_robin';
pooling_type?: string;
type: 'StandardEventType' | 'AdhocEventType';
color: string;
created_at: string;
updated_at: string;
internal_note: string;
description_plain: string;
description_html: string;
profile: {
type: string;
name: string;
owner: string;
};
secret: boolean;
booking_method: string;
custom_questions: Array<{
name: string;
type: string;
position: number;
enabled: boolean;
required: boolean;
answer_choices: string[];
include_other: boolean;
}>;
}
export interface CalendlyInvitee {
uri: string;
email: string;
name: string;
first_name: string;
last_name: string;
status: 'active' | 'canceled';
timezone: string;
event: string;
created_at: string;
updated_at: string;
tracking: {
utm_campaign?: string;
utm_source?: string;
utm_medium?: string;
utm_content?: string;
utm_term?: string;
salesforce_uuid?: string;
};
text_reminder_number: string;
rescheduled: boolean;
old_invitee: string;
new_invitee: string;
cancel_url: string;
reschedule_url: string;
questions_and_answers: Array<{
question: string;
answer: string;
position: number;
}>;
cancellation?: {
canceled_by: string;
reason: string;
};
payment?: {
id: string;
provider: string;
amount: number;
currency: string;
terms: string;
successful: boolean;
};
no_show?: {
created_at: string;
};
reconfirmation?: {
created_at: string;
confirmed_at: string;
};
}
export interface CalendlyOrganization {
uri: string;
name: string;
slug: string;
status: string;
timezone: string;
created_at: string;
updated_at: string;
resource_type: string;
}
export interface CalendlyOrganizationMembership {
uri: string;
role: 'owner' | 'admin' | 'user';
user: {
uri: string;
name: string;
slug: string;
email: string;
scheduling_url: string;
timezone: string;
avatar_url: string;
created_at: string;
updated_at: string;
};
organization: string;
created_at: string;
updated_at: string;
}
export interface CalendlyOrganizationInvitation {
uri: string;
organization: string;
email: string;
status: 'pending' | 'accepted' | 'declined' | 'revoked';
created_at: string;
updated_at: string;
last_sent_at: string;
}
export interface CalendlySchedulingLink {
booking_url: string;
owner: string;
owner_type: string;
resource_type: string;
}
export interface CalendlyWebhookSubscription {
uri: string;
callback_url: string;
created_at: string;
updated_at: string;
retry_started_at: string;
state: 'active' | 'disabled';
events: string[];
scope: 'user' | 'organization';
organization: string;
user: string;
creator: string;
}
export interface CalendlyAvailableTime {
status: 'available';
invitees_remaining: number;
start_time: string;
scheduling_url: string;
}
export interface CalendlyUserBusyTime {
start_time: string;
end_time: string;
type: 'calendly' | 'busy_calendar';
buffered: boolean;
}
export interface CalendlyRoutingForm {
uri: string;
name: string;
status: 'published' | 'draft';
published_version: number;
organization: string;
created_at: string;
updated_at: string;
questions: Array<{
uuid: string;
name: string;
type: string;
required: boolean;
answer_choices?: Array<{
uuid: string;
label: string;
position: number;
}>;
}>;
routing_configurations: Array<{
priority: number;
rules: Array<{
question_uuid: string;
type: string;
value: string;
}>;
routing_target: {
type: string;
target: string;
};
}>;
}
export interface CalendlyNoShow {
uri: string;
created_at: string;
}
export interface PaginationParams {
count?: number;
page_token?: string;
sort?: string;
}
export interface PaginatedResponse<T> {
collection: T[];
pagination: {
count: number;
next_page?: string;
previous_page?: string;
next_page_token?: string;
previous_page_token?: string;
};
}
export interface CalendlyError {
title: string;
message: string;
details?: Array<{
parameter: string;
message: string;
}>;
}

View File

@ -0,0 +1,48 @@
import React from 'react';
import './styles.css';
export default function AnalyticsDashboard() {
const stats = {
totalEvents: 245,
activeEvents: 32,
totalInvitees: 489,
noShows: 12,
cancelRate: '4.9%',
avgDuration: '45 min',
};
return (
<div className="app-container">
<header className="header">
<h1>📊 Analytics Dashboard</h1>
<p>Calendly metrics and insights</p>
</header>
<div className="detail-grid">
<div className="grid-item">
<h3>Total Events</h3>
<div style={{fontSize: '2rem', fontWeight: 'bold', color: '#58a6ff'}}>{stats.totalEvents}</div>
</div>
<div className="grid-item">
<h3>Active Events</h3>
<div style={{fontSize: '2rem', fontWeight: 'bold', color: '#58a6ff'}}>{stats.activeEvents}</div>
</div>
<div className="grid-item">
<h3>Total Invitees</h3>
<div style={{fontSize: '2rem', fontWeight: 'bold', color: '#58a6ff'}}>{stats.totalInvitees}</div>
</div>
<div className="grid-item">
<h3>No-Shows</h3>
<div style={{fontSize: '2rem', fontWeight: 'bold', color: '#f85149'}}>{stats.noShows}</div>
</div>
<div className="grid-item">
<h3>Cancel Rate</h3>
<div style={{fontSize: '2rem', fontWeight: 'bold', color: '#d29922'}}>{stats.cancelRate}</div>
</div>
<div className="grid-item">
<h3>Avg Duration</h3>
<div style={{fontSize: '2rem', fontWeight: 'bold', color: '#58a6ff'}}>{stats.avgDuration}</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendly MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18.3.1';
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
import App from './App.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
../shared-styles.css

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@ -0,0 +1,35 @@
import React, { useState } from 'react';
import './styles.css';
export default function AvailabilityCalendar() {
const [availableTimes] = useState([
{ time: '2024-02-15 09:00', available: true },
{ time: '2024-02-15 10:00', available: true },
{ time: '2024-02-15 11:00', available: false },
{ time: '2024-02-15 14:00', available: true },
]);
return (
<div className="app-container">
<header className="header">
<h1>📅 Availability Calendar</h1>
<p>View available time slots</p>
</header>
<div className="search-box">
<input type="date" placeholder="Start Date" />
<input type="date" placeholder="End Date" />
<button>Load Availability</button>
</div>
<div className="detail-grid">
{availableTimes.map((slot, i) => (
<div key={i} className="grid-item">
<strong>{new Date(slot.time).toLocaleString()}</strong>
<span className={`status-badge ${slot.available ? 'active' : 'canceled'}`}>
{slot.available ? 'Available' : 'Busy'}
</span>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendly MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18.3.1';
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
import App from './App.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
../shared-styles.css

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@ -0,0 +1,56 @@
import React, { useState } from 'react';
import './styles.css';
export default function BookingFlow() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({
name: '',
email: '',
eventType: '',
time: '',
});
return (
<div className="app-container">
<header className="header">
<h1>📝 Booking Flow</h1>
<p>Create a booking experience</p>
</header>
<div className="detail-card">
<h2>Step {step} of 3</h2>
{step === 1 && (
<div>
<h3>Select Event Type</h3>
<select onChange={(e) => setFormData({...formData, eventType: e.target.value})}>
<option value="">Choose...</option>
<option value="demo">Sales Demo (60 min)</option>
<option value="meeting">Quick Meeting (30 min)</option>
</select>
<button onClick={() => setStep(2)} style={{marginTop: '1rem'}}>Next</button>
</div>
)}
{step === 2 && (
<div>
<h3>Select Time</h3>
<input type="datetime-local" onChange={(e) => setFormData({...formData, time: e.target.value})} />
<div style={{marginTop: '1rem'}}>
<button onClick={() => setStep(1)}>Back</button>
<button onClick={() => setStep(3)} style={{marginLeft: '0.5rem'}}>Next</button>
</div>
</div>
)}
{step === 3 && (
<div>
<h3>Your Information</h3>
<input type="text" placeholder="Name" onChange={(e) => setFormData({...formData, name: e.target.value})} style={{marginBottom: '0.5rem'}} />
<input type="email" placeholder="Email" onChange={(e) => setFormData({...formData, email: e.target.value})} />
<div style={{marginTop: '1rem'}}>
<button onClick={() => setStep(2)}>Back</button>
<button style={{marginLeft: '0.5rem'}}>Book Event</button>
</div>
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendly MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18.3.1';
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
import App from './App.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
../shared-styles.css

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@ -0,0 +1,157 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
interface Event {
uri: string;
name: string;
status: string;
start_time: string;
end_time: string;
invitees_counter: {
total: number;
active: number;
};
}
export default function EventDashboard() {
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'canceled'>('active');
const [dateRange, setDateRange] = useState('week');
useEffect(() => {
loadEvents();
}, [statusFilter, dateRange]);
const loadEvents = async () => {
setLoading(true);
try {
// Call MCP tool via parent window
const params: any = {};
if (statusFilter !== 'all') {
params.status = statusFilter;
}
// Date range calculation
const now = new Date();
const minDate = new Date();
if (dateRange === 'week') {
minDate.setDate(now.getDate() - 7);
} else if (dateRange === 'month') {
minDate.setMonth(now.getMonth() - 1);
}
params.min_start_time = minDate.toISOString();
params.max_start_time = now.toISOString();
params.sort = 'start_time:desc';
const result = await window.parent.postMessage({
type: 'mcp_tool_call',
tool: 'calendly_list_scheduled_events',
params,
}, '*');
// In real implementation, would listen for response
// For now, mock data
setEvents([
{
uri: 'https://api.calendly.com/scheduled_events/001',
name: 'Sales Demo',
status: 'active',
start_time: new Date().toISOString(),
end_time: new Date(Date.now() + 3600000).toISOString(),
invitees_counter: { total: 1, active: 1 },
},
{
uri: 'https://api.calendly.com/scheduled_events/002',
name: 'Customer Onboarding',
status: 'active',
start_time: new Date(Date.now() - 86400000).toISOString(),
end_time: new Date(Date.now() - 82800000).toISOString(),
invitees_counter: { total: 2, active: 2 },
},
]);
} catch (error) {
console.error('Failed to load events:', error);
} finally {
setLoading(false);
}
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleString();
};
return (
<div className="app-container">
<header className="header">
<h1>📅 Event Dashboard</h1>
<p>Manage your Calendly scheduled events</p>
</header>
<div className="filters">
<div className="filter-group">
<label>Status:</label>
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value as any)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="canceled">Canceled</option>
</select>
</div>
<div className="filter-group">
<label>Date Range:</label>
<select value={dateRange} onChange={(e) => setDateRange(e.target.value)}>
<option value="week">Last Week</option>
<option value="month">Last Month</option>
<option value="all">All Time</option>
</select>
</div>
<button onClick={loadEvents} className="btn-refresh"> Refresh</button>
</div>
<div className="stats-grid">
<div className="stat-card">
<h3>Total Events</h3>
<div className="stat-value">{events.length}</div>
</div>
<div className="stat-card">
<h3>Active Events</h3>
<div className="stat-value">{events.filter(e => e.status === 'active').length}</div>
</div>
<div className="stat-card">
<h3>Total Invitees</h3>
<div className="stat-value">{events.reduce((acc, e) => acc + e.invitees_counter.total, 0)}</div>
</div>
</div>
{loading ? (
<div className="loading">Loading events...</div>
) : (
<div className="events-list">
{events.map((event) => (
<div key={event.uri} className="event-card">
<div className="event-header">
<h3>{event.name}</h3>
<span className={`status-badge ${event.status}`}>{event.status}</span>
</div>
<div className="event-details">
<p><strong>Start:</strong> {formatDate(event.start_time)}</p>
<p><strong>End:</strong> {formatDate(event.end_time)}</p>
<p><strong>Invitees:</strong> {event.invitees_counter.active} / {event.invitees_counter.total}</p>
</div>
<div className="event-actions">
<button className="btn-view">View Details</button>
{event.status === 'active' && (
<button className="btn-cancel">Cancel Event</button>
)}
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendly Event Dashboard - MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18.3.1';
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
import App from './App.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
../shared-styles.css

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@ -0,0 +1,83 @@
import React, { useState, useEffect } from 'react';
import './styles.css';
export default function EventDetail() {
const [event, setEvent] = useState<any>(null);
const [invitees, setInvitees] = useState<any[]>([]);
const [eventUuid, setEventUuid] = useState('');
const loadEvent = async () => {
if (!eventUuid) return;
// Call MCP tool calendly_get_event
setEvent({
name: 'Sales Demo',
status: 'active',
start_time: new Date().toISOString(),
end_time: new Date(Date.now() + 3600000).toISOString(),
location: { type: 'zoom', join_url: 'https://zoom.us/j/123' },
meeting_notes_plain: 'Please prepare your questions.',
});
};
const loadInvitees = async () => {
if (!eventUuid) return;
// Call MCP tool calendly_list_event_invitees
setInvitees([
{ name: 'John Doe', email: 'john@example.com', status: 'active' },
]);
};
const cancelEvent = async () => {
if (!confirm('Cancel this event?')) return;
// Call MCP tool calendly_cancel_event
};
return (
<div className="app-container">
<header className="header">
<h1>📋 Event Details</h1>
</header>
<div className="search-box">
<input
type="text"
placeholder="Enter Event UUID..."
value={eventUuid}
onChange={(e) => setEventUuid(e.target.value)}
/>
<button onClick={loadEvent}>Load Event</button>
</div>
{event && (
<div className="detail-card">
<h2>{event.name}</h2>
<div className="detail-grid">
<div><strong>Status:</strong> <span className={`status-badge ${event.status}`}>{event.status}</span></div>
<div><strong>Start:</strong> {new Date(event.start_time).toLocaleString()}</div>
<div><strong>End:</strong> {new Date(event.end_time).toLocaleString()}</div>
<div><strong>Location:</strong> {event.location.type}</div>
</div>
{event.meeting_notes_plain && (
<div className="notes">
<h3>Meeting Notes</h3>
<p>{event.meeting_notes_plain}</p>
</div>
)}
<div className="invitees-section">
<h3>Invitees</h3>
{invitees.map((inv, i) => (
<div key={i} className="invitee-row">
<span>{inv.name}</span>
<span>{inv.email}</span>
<span className={`status-badge ${inv.status}`}>{inv.status}</span>
</div>
))}
</div>
{event.status === 'active' && (
<button onClick={cancelEvent} className="btn-cancel">Cancel Event</button>
)}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendly MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18.3.1';
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
import App from './App.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
../shared-styles.css

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@ -0,0 +1,28 @@
import React, { useState } from 'react';
import './styles.css';
export default function EventGrid() {
const [events] = useState([
{ id: '1', name: 'Sales Demo', date: '2024-02-15 10:00', status: 'active' },
{ id: '2', name: 'Onboarding Call', date: '2024-02-16 14:00', status: 'active' },
{ id: '3', name: 'Team Sync', date: '2024-02-17 09:00', status: 'active' },
]);
return (
<div className="app-container">
<header className="header">
<h1>📊 Event Grid</h1>
<p>Calendar grid view of all events</p>
</header>
<div className="detail-grid">
{events.map(event => (
<div key={event.id} className="grid-item">
<h3>{event.name}</h3>
<p>{event.date}</p>
<span className={`status-badge ${event.status}`}>{event.status}</span>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendly MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18.3.1';
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
import App from './App.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
../shared-styles.css

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@ -0,0 +1,35 @@
import React, { useState } from 'react';
import './styles.css';
export default function EventTypeManager() {
const [eventTypes] = useState([
{ id: '1', name: '30 Minute Meeting', duration: 30, active: true },
{ id: '2', name: 'Sales Demo', duration: 60, active: true },
{ id: '3', name: 'Quick Chat', duration: 15, active: false },
]);
return (
<div className="app-container">
<header className="header">
<h1> Event Type Manager</h1>
<p>Manage your Calendly event types</p>
</header>
<button style={{marginBottom: '1rem'}}>+ Create New Event Type</button>
<div className="detail-grid">
{eventTypes.map(type => (
<div key={type.id} className="grid-item">
<h3>{type.name}</h3>
<p>Duration: {type.duration} minutes</p>
<span className={`status-badge ${type.active ? 'active' : 'canceled'}`}>
{type.active ? 'Active' : 'Inactive'}
</span>
<div style={{marginTop: '1rem'}}>
<button>Edit</button>
<button className="btn-cancel" style={{marginLeft: '0.5rem'}}>Delete</button>
</div>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendly MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18.3.1';
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
import App from './App.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
../shared-styles.css

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@ -0,0 +1,34 @@
import React, { useState } from 'react';
import './styles.css';
export default function InviteeList() {
const [invitees] = useState([
{ id: '1', name: 'John Doe', email: 'john@example.com', event: 'Sales Demo', status: 'active' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', event: 'Onboarding', status: 'active' },
]);
const markNoShow = (id: string) => {
// Call MCP tool calendly_mark_no_show
alert(`Marking invitee ${id} as no-show`);
};
return (
<div className="app-container">
<header className="header">
<h1>👥 Invitee List</h1>
<p>Manage event invitees</p>
</header>
{invitees.map(invitee => (
<div key={invitee.id} className="detail-card">
<h3>{invitee.name}</h3>
<p><strong>Email:</strong> {invitee.email}</p>
<p><strong>Event:</strong> {invitee.event}</p>
<span className={`status-badge ${invitee.status}`}>{invitee.status}</span>
<div style={{marginTop: '1rem'}}>
<button onClick={() => markNoShow(invitee.id)}>Mark No-Show</button>
</div>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendly MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18.3.1';
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
import App from './App.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
../shared-styles.css

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@ -0,0 +1,34 @@
import React, { useState } from 'react';
import './styles.css';
export default function NoShowTracker() {
const [noShows] = useState([
{ id: '1', invitee: 'John Doe', event: 'Sales Demo', date: '2024-02-10', email: 'john@example.com' },
{ id: '2', invitee: 'Jane Smith', event: 'Onboarding', date: '2024-02-12', email: 'jane@example.com' },
]);
const unmarkNoShow = (id: string) => {
// Call MCP tool calendly_unmark_no_show
alert(`Removing no-show status for ${id}`);
};
return (
<div className="app-container">
<header className="header">
<h1>🚫 No-Show Tracker</h1>
<p>Track and manage no-shows</p>
</header>
{noShows.map(noShow => (
<div key={noShow.id} className="detail-card">
<h3>{noShow.invitee}</h3>
<p><strong>Event:</strong> {noShow.event}</p>
<p><strong>Date:</strong> {noShow.date}</p>
<p><strong>Email:</strong> {noShow.email}</p>
<button onClick={() => unmarkNoShow(noShow.id)} style={{marginTop: '0.5rem'}}>
Remove No-Show Status
</button>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendly MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18.3.1';
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
import App from './App.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
../shared-styles.css

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@ -0,0 +1,41 @@
import React, { useState } from 'react';
import './styles.css';
export default function OrgMembers() {
const [members] = useState([
{ id: '1', name: 'Alice Johnson', email: 'alice@company.com', role: 'admin' },
{ id: '2', name: 'Bob Wilson', email: 'bob@company.com', role: 'user' },
]);
const [newEmail, setNewEmail] = useState('');
const inviteMember = async () => {
// Call MCP tool calendly_invite_user
alert(`Inviting ${newEmail}`);
};
return (
<div className="app-container">
<header className="header">
<h1>👨💼 Organization Members</h1>
<p>Manage team members</p>
</header>
<div className="search-box">
<input
type="email"
placeholder="Email to invite"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
/>
<button onClick={inviteMember}>Invite Member</button>
</div>
{members.map(member => (
<div key={member.id} className="detail-card">
<h3>{member.name}</h3>
<p><strong>Email:</strong> {member.email}</p>
<p><strong>Role:</strong> {member.role}</p>
<button className="btn-cancel" style={{marginTop: '0.5rem'}}>Remove</button>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendly MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18.3.1';
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
import App from './App.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
../shared-styles.css

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@ -0,0 +1,40 @@
import React, { useState } from 'react';
import './styles.css';
export default function SchedulingLinks() {
const [links, setLinks] = useState<any[]>([]);
const [eventTypeUri, setEventTypeUri] = useState('');
const createLink = async () => {
// Call MCP tool calendly_create_scheduling_link
const newLink = {
url: 'https://calendly.com/link/abc123',
created: new Date().toISOString(),
};
setLinks([...links, newLink]);
};
return (
<div className="app-container">
<header className="header">
<h1>🔗 Scheduling Links</h1>
<p>Create single-use scheduling links</p>
</header>
<div className="search-box">
<input
type="text"
placeholder="Event Type URI"
value={eventTypeUri}
onChange={(e) => setEventTypeUri(e.target.value)}
/>
<button onClick={createLink}>Create Link</button>
</div>
{links.map((link, i) => (
<div key={i} className="detail-card">
<p><strong>URL:</strong> <a href={link.url} target="_blank">{link.url}</a></p>
<p><strong>Created:</strong> {new Date(link.created).toLocaleString()}</p>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendly MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18.3.1';
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
import App from './App.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
../shared-styles.css

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@ -0,0 +1,106 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
background: #0f1419;
color: #e4e6eb;
line-height: 1.6;
}
.app-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #2a3441;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #fff;
}
.header p {
color: #8b949e;
}
button {
padding: 0.5rem 1rem;
background: #238636;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: background 0.2s;
}
button:hover {
background: #2ea043;
}
.btn-cancel {
background: #da3633 !important;
}
.btn-cancel:hover {
background: #f85149 !important;
}
input, select {
padding: 0.5rem;
background: #0d1117;
border: 1px solid #2a3441;
border-radius: 6px;
color: #e4e6eb;
font-size: 0.875rem;
width: 100%;
}
.detail-card, .grid-item {
background: #161b22;
border: 1px solid #2a3441;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1rem;
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.status-badge.active {
background: #238636;
color: white;
}
.status-badge.canceled {
background: #da3633;
color: white;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin: 1rem 0;
}
.search-box {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
}

View File

@ -0,0 +1,40 @@
import React, { useState } from 'react';
import './styles.css';
export default function WebhookManager() {
const [webhooks] = useState([
{ id: '1', url: 'https://api.example.com/webhook', events: ['invitee.created'], status: 'active' },
]);
const [newUrl, setNewUrl] = useState('');
const createWebhook = async () => {
// Call MCP tool calendly_create_webhook_subscription
alert(`Creating webhook for ${newUrl}`);
};
return (
<div className="app-container">
<header className="header">
<h1>🪝 Webhook Manager</h1>
<p>Manage webhook subscriptions</p>
</header>
<div className="search-box">
<input
type="url"
placeholder="Webhook URL"
value={newUrl}
onChange={(e) => setNewUrl(e.target.value)}
/>
<button onClick={createWebhook}>Create Webhook</button>
</div>
{webhooks.map(webhook => (
<div key={webhook.id} className="detail-card">
<p><strong>URL:</strong> {webhook.url}</p>
<p><strong>Events:</strong> {webhook.events.join(', ')}</p>
<span className={`status-badge ${webhook.status}`}>{webhook.status}</span>
<button className="btn-cancel" style={{marginTop: '1rem'}}>Delete</button>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendly MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18.3.1';
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
import App from './App.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
../shared-styles.css

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true,
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@ -1,15 +1,21 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "Node16",
"moduleResolution": "NodeNext", "moduleResolution": "Node16",
"lib": ["ES2022"],
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src",
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"declaration": true "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist", "src/ui"]
} }

236
servers/clickup/README.md Normal file
View File

@ -0,0 +1,236 @@
# ClickUp MCP Server
Complete Model Context Protocol server for ClickUp - the all-in-one productivity platform.
## Features
- **70+ Tools** across all ClickUp domains
- **25 React Apps** for rich interactive UIs
- **Full API Coverage**: Tasks, Spaces, Folders, Lists, Views, Comments, Docs, Goals, Tags, Time Tracking, Teams, Webhooks, Custom Fields, Templates, and Guests
- **Production Ready**: Rate limiting, pagination, error handling, comprehensive types
## Installation
```bash
npm install @mcpengine/clickup
```
## Configuration
Add to your MCP settings:
```json
{
"mcpServers": {
"clickup": {
"command": "node",
"args": ["/path/to/@mcpengine/clickup/dist/index.js"],
"env": {
"CLICKUP_API_TOKEN": "your-api-token"
}
}
}
}
```
### Authentication
ClickUp MCP supports two authentication methods:
1. **Personal API Token** (recommended for development):
- Get your token from: https://app.clickup.com/settings/apps
- Set `CLICKUP_API_TOKEN` environment variable
2. **OAuth2** (for production apps):
- Set `CLICKUP_CLIENT_ID`, `CLICKUP_CLIENT_SECRET`, and `CLICKUP_OAUTH_TOKEN`
## Available Tools
### Tasks (17 tools)
- `clickup_tasks_list` - List tasks with filtering
- `clickup_tasks_get` - Get task details
- `clickup_tasks_create` - Create new task
- `clickup_tasks_update` - Update task
- `clickup_tasks_delete` - Delete task
- `clickup_tasks_filter` - Advanced task filtering
- `clickup_tasks_bulk_update` - Bulk update tasks
- `clickup_tasks_get_time_entries` - Get time entries for task
- `clickup_tasks_add_time_entry` - Add time entry
- `clickup_tasks_get_custom_fields` - Get custom field values
- `clickup_tasks_set_custom_field` - Set custom field value
- `clickup_tasks_add_dependency` - Add task dependency
- `clickup_tasks_remove_dependency` - Remove dependency
- `clickup_tasks_list_members` - List task members
- `clickup_tasks_add_comment` - Add comment to task
- `clickup_tasks_get_comments` - Get task comments
- `clickup_tasks_search` - Search tasks
### Spaces (5 tools)
- `clickup_spaces_list` - List spaces
- `clickup_spaces_get` - Get space details
- `clickup_spaces_create` - Create space
- `clickup_spaces_update` - Update space
- `clickup_spaces_delete` - Delete space
### Folders (5 tools)
- `clickup_folders_list` - List folders
- `clickup_folders_get` - Get folder details
- `clickup_folders_create` - Create folder
- `clickup_folders_update` - Update folder
- `clickup_folders_delete` - Delete folder
### Lists (7 tools)
- `clickup_lists_list` - List lists
- `clickup_lists_get` - Get list details
- `clickup_lists_create` - Create list
- `clickup_lists_update` - Update list
- `clickup_lists_delete` - Delete list
- `clickup_lists_add_task` - Add task to list
- `clickup_lists_remove_task` - Remove task from list
### Views (5 tools)
- `clickup_views_list` - List views
- `clickup_views_get` - Get view details
- `clickup_views_create` - Create view
- `clickup_views_update` - Update view
- `clickup_views_delete` - Delete view
### Comments (5 tools)
- `clickup_comments_list` - List comments
- `clickup_comments_get` - Get comment
- `clickup_comments_create` - Create comment
- `clickup_comments_update` - Update comment
- `clickup_comments_delete` - Delete comment
### Docs (3 tools)
- `clickup_docs_list` - List docs
- `clickup_docs_get` - Get doc
- `clickup_docs_create` - Create doc
- `clickup_docs_search` - Search docs
### Goals (7 tools)
- `clickup_goals_list` - List goals
- `clickup_goals_get` - Get goal
- `clickup_goals_create` - Create goal
- `clickup_goals_update` - Update goal
- `clickup_goals_delete` - Delete goal
- `clickup_goals_add_key_result` - Add key result
- `clickup_goals_update_key_result` - Update key result
### Tags (5 tools)
- `clickup_tags_list` - List tags
- `clickup_tags_create` - Create tag
- `clickup_tags_update` - Update tag
- `clickup_tags_delete` - Delete tag
- `clickup_tags_add_to_task` - Add tag to task
### Checklists (6 tools)
- `clickup_checklists_create` - Create checklist
- `clickup_checklists_update` - Update checklist
- `clickup_checklists_delete` - Delete checklist
- `clickup_checklists_create_item` - Create checklist item
- `clickup_checklists_update_item` - Update item
- `clickup_checklists_delete_item` - Delete item
### Time Tracking (7 tools)
- `clickup_time_list_entries` - List time entries
- `clickup_time_get_entry` - Get time entry
- `clickup_time_create` - Create time entry
- `clickup_time_update` - Update time entry
- `clickup_time_delete` - Delete time entry
- `clickup_time_get_running` - Get running timer
- `clickup_time_start` - Start timer
- `clickup_time_stop` - Stop timer
### Teams (6 tools)
- `clickup_teams_list_workspaces` - List workspaces
- `clickup_teams_get_workspace` - Get workspace
- `clickup_teams_list_members` - List members
- `clickup_teams_get_member` - Get member
- `clickup_teams_list_groups` - List groups
- `clickup_teams_create_group` - Create group
### Webhooks (4 tools)
- `clickup_webhooks_list` - List webhooks
- `clickup_webhooks_create` - Create webhook
- `clickup_webhooks_update` - Update webhook
- `clickup_webhooks_delete` - Delete webhook
### Custom Fields (4 tools)
- `clickup_custom_fields_list` - List custom fields
- `clickup_custom_fields_get` - Get custom field
- `clickup_custom_fields_set_value` - Set field value
- `clickup_custom_fields_remove_value` - Remove value
### Templates (2 tools)
- `clickup_templates_list` - List templates
- `clickup_templates_apply` - Apply template
### Guests (6 tools)
- `clickup_guests_invite` - Invite guest
- `clickup_guests_get` - Get guest
- `clickup_guests_edit` - Edit guest
- `clickup_guests_remove` - Remove guest
- `clickup_guests_add_to_task` - Add guest to task
- `clickup_guests_add_to_list` - Add guest to list
## Available Apps
### Task Management
- **task-dashboard** - Overview with status counts, overdue, priority breakdown
- **task-detail** - Full task view with subtasks, comments, custom fields, time entries, dependencies
- **task-grid** - Sortable/filterable task list
- **task-board** - Kanban board by status (drag-drop)
### Workspace & Organization
- **space-overview** - Space with folders, lists, members
- **folder-overview** - Folder with lists and task summaries
- **list-view** - List detail with task table
- **workspace-overview** - High-level workspace stats
### Views & Visualization
- **calendar-view** - Tasks on calendar by due date
- **gantt-view** - Timeline/gantt of tasks with dependencies
- **sprint-board** - Sprint-style task board with velocity
### Goals & Tracking
- **goal-tracker** - Goals with key results progress bars
- **time-dashboard** - Time tracking overview, entries by date/member
- **time-entries** - Time entry list with task associations
- **member-workload** - Per-member task counts, time logged, overdue
### Content & Collaboration
- **doc-browser** - Document list with search
- **comment-thread** - Threaded comments for a task
- **checklist-manager** - Checklists with item completion
- **tag-manager** - Tag list with task counts
- **custom-fields-editor** - Custom field values on a task
### Utilities
- **template-gallery** - Available templates with preview
- **search-results** - Universal search across tasks/docs
- **activity-feed** - Recent changes across workspace
## API Coverage
This server implements the complete ClickUp API v2:
- https://clickup.com/api/clickupapiref/operation/GetTasks/
- Rate limiting and pagination handled automatically
- Comprehensive error handling and retries
## Development
```bash
# Install dependencies
npm install
# Build
npm run build
# Watch mode
npm run watch
```
## License
MIT

View File

@ -1,20 +1,38 @@
{ {
"name": "mcp-server-clickup", "name": "@mcpengine/clickup",
"version": "1.0.0", "version": "1.0.0",
"type": "module", "description": "ClickUp MCP Server - Complete task management, collaboration, and productivity platform integration",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "type": "module",
"build": "tsc", "bin": {
"start": "node dist/index.js", "clickup-mcp": "./dist/index.js"
"dev": "tsx src/index.ts"
}, },
"scripts": {
"build": "tsc && npm run chmod",
"chmod": "chmod +x dist/index.js",
"watch": "tsc --watch",
"prepare": "npm run build"
},
"keywords": [
"mcp",
"clickup",
"tasks",
"project-management",
"productivity",
"collaboration"
],
"author": "MCPEngine",
"license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0", "@modelcontextprotocol/sdk": "^1.0.6",
"zod": "^3.22.4" "axios": "^1.7.2",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.0", "@types/node": "^22.0.0",
"tsx": "^4.7.0", "typescript": "^5.5.4"
"typescript": "^5.3.0" },
"engines": {
"node": ">=18.0.0"
} }
} }

View File

@ -0,0 +1,570 @@
/**
* ClickUp API Client
* API v2: https://clickup.com/api
*/
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
import type { ClickUpConfig, ClickUpError } from '../types.js';
const BASE_URL = 'https://api.clickup.com/api/v2';
const RATE_LIMIT_DELAY = 100; // ms between requests
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // ms
export class ClickUpClient {
private client: AxiosInstance;
private lastRequestTime = 0;
private apiToken: string;
constructor(config: ClickUpConfig) {
this.apiToken = config.apiToken || config.oauthToken || '';
if (!this.apiToken) {
throw new Error('ClickUp API token is required. Set CLICKUP_API_TOKEN environment variable.');
}
this.client = axios.create({
baseURL: BASE_URL,
headers: {
'Authorization': this.apiToken,
'Content-Type': 'application/json',
},
timeout: 30000,
});
// Response interceptor for error handling
this.client.interceptors.response.use(
response => response,
error => this.handleError(error)
);
}
private async handleError(error: AxiosError): Promise<never> {
if (error.response) {
const clickupError = error.response.data as ClickUpError;
const status = error.response.status;
if (status === 429) {
throw new Error(`Rate limit exceeded. Please try again later. ${clickupError.err || ''}`);
} else if (status === 401) {
throw new Error('Unauthorized. Check your API token.');
} else if (status === 403) {
throw new Error(`Forbidden: ${clickupError.err || 'Access denied'}`);
} else if (status === 404) {
throw new Error(`Not found: ${clickupError.err || 'Resource does not exist'}`);
} else if (status === 400) {
throw new Error(`Bad request: ${clickupError.err || 'Invalid parameters'}`);
} else {
throw new Error(`ClickUp API error (${status}): ${clickupError.err || error.message}`);
}
} else if (error.request) {
throw new Error('No response from ClickUp API. Check your network connection.');
} else {
throw new Error(`Request error: ${error.message}`);
}
}
private async rateLimit(): Promise<void> {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < RATE_LIMIT_DELAY) {
await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY - timeSinceLastRequest));
}
this.lastRequestTime = Date.now();
}
private async retryRequest<T>(
fn: () => Promise<T>,
retries = MAX_RETRIES
): Promise<T> {
try {
return await fn();
} catch (error) {
if (retries > 0 && error instanceof Error && error.message.includes('Rate limit')) {
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
return this.retryRequest(fn, retries - 1);
}
throw error;
}
}
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
await this.rateLimit();
return this.retryRequest(async () => {
const response = await this.client.get<T>(endpoint, { params });
return response.data;
});
}
async post<T>(endpoint: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
await this.rateLimit();
return this.retryRequest(async () => {
const response = await this.client.post<T>(endpoint, data, config);
return response.data;
});
}
async put<T>(endpoint: string, data?: any): Promise<T> {
await this.rateLimit();
return this.retryRequest(async () => {
const response = await this.client.put<T>(endpoint, data);
return response.data;
});
}
async delete<T>(endpoint: string): Promise<T> {
await this.rateLimit();
return this.retryRequest(async () => {
const response = await this.client.delete<T>(endpoint);
return response.data;
});
}
// Pagination helper
async *paginate<T>(
endpoint: string,
params: Record<string, any> = {},
dataKey: string
): AsyncGenerator<T[], void, unknown> {
let page = 0;
let hasMore = true;
while (hasMore) {
const response: any = await this.get(endpoint, { ...params, page });
const items = response[dataKey] || [];
if (items.length > 0) {
yield items;
}
hasMore = !response.last_page && items.length > 0;
page++;
}
}
// ===== Team / Workspace =====
async getAuthorizedTeams() {
return this.get('/team');
}
async getTeam(teamId: string) {
return this.get(`/team/${teamId}`);
}
// ===== Spaces =====
async getSpaces(teamId: string, archived = false) {
return this.get(`/team/${teamId}/space`, { archived });
}
async getSpace(spaceId: string) {
return this.get(`/space/${spaceId}`);
}
async createSpace(teamId: string, data: any) {
return this.post(`/team/${teamId}/space`, data);
}
async updateSpace(spaceId: string, data: any) {
return this.put(`/space/${spaceId}`, data);
}
async deleteSpace(spaceId: string) {
return this.delete(`/space/${spaceId}`);
}
// ===== Folders =====
async getFolders(spaceId: string, archived = false) {
return this.get(`/space/${spaceId}/folder`, { archived });
}
async getFolder(folderId: string) {
return this.get(`/folder/${folderId}`);
}
async createFolder(spaceId: string, data: any) {
return this.post(`/space/${spaceId}/folder`, data);
}
async updateFolder(folderId: string, data: any) {
return this.put(`/folder/${folderId}`, data);
}
async deleteFolder(folderId: string) {
return this.delete(`/folder/${folderId}`);
}
// ===== Lists =====
async getFolderLists(folderId: string, archived = false) {
return this.get(`/folder/${folderId}/list`, { archived });
}
async getSpaceLists(spaceId: string, archived = false) {
return this.get(`/space/${spaceId}/list`, { archived });
}
async getList(listId: string) {
return this.get(`/list/${listId}`);
}
async createList(folderId: string, data: any) {
return this.post(`/folder/${folderId}/list`, data);
}
async createFolderlessList(spaceId: string, data: any) {
return this.post(`/space/${spaceId}/list`, data);
}
async updateList(listId: string, data: any) {
return this.put(`/list/${listId}`, data);
}
async deleteList(listId: string) {
return this.delete(`/list/${listId}`);
}
async addTaskToList(listId: string, taskId: string) {
return this.post(`/list/${listId}/task/${taskId}`);
}
async removeTaskFromList(listId: string, taskId: string) {
return this.delete(`/list/${listId}/task/${taskId}`);
}
// ===== Views =====
async getViews(teamId: string, spaceId: string, listId?: string, folderId?: string) {
const params: any = { space_id: spaceId };
if (listId) params.list_id = listId;
if (folderId) params.folder_id = folderId;
return this.get(`/team/${teamId}/view`, params);
}
async getView(viewId: string) {
return this.get(`/view/${viewId}`);
}
async getViewTasks(viewId: string, page = 0) {
return this.get(`/view/${viewId}/task`, { page });
}
async createView(teamId: string, spaceId: string, data: any) {
return this.post(`/team/${teamId}/view`, { ...data, space_id: spaceId });
}
async updateView(viewId: string, data: any) {
return this.put(`/view/${viewId}`, data);
}
async deleteView(viewId: string) {
return this.delete(`/view/${viewId}`);
}
// ===== Tasks =====
async getTasks(listId: string, params: any = {}) {
return this.get(`/list/${listId}/task`, params);
}
async getTask(taskId: string, params: any = {}) {
return this.get(`/task/${taskId}`, params);
}
async createTask(listId: string, data: any) {
return this.post(`/list/${listId}/task`, data);
}
async updateTask(taskId: string, data: any) {
return this.put(`/task/${taskId}`, data);
}
async deleteTask(taskId: string) {
return this.delete(`/task/${taskId}`);
}
async getFilteredTasks(teamId: string, params: any = {}) {
return this.get(`/team/${teamId}/task`, params);
}
async bulkUpdateTasks(taskIds: string[], data: any) {
return this.post('/task/bulk', { task_ids: taskIds, ...data });
}
// ===== Task Dependencies =====
async addDependency(taskId: string, dependsOn: string, dependencyOf?: string) {
return this.post(`/task/${taskId}/dependency`, {
depends_on: dependsOn,
dependency_of: dependencyOf
});
}
async deleteDependency(taskId: string, dependsOn: string, dependencyOf?: string) {
return this.delete(`/task/${taskId}/dependency?depends_on=${dependsOn}${dependencyOf ? `&dependency_of=${dependencyOf}` : ''}`);
}
// ===== Task Members =====
async getTaskMembers(taskId: string) {
return this.get(`/task/${taskId}/member`);
}
// ===== Comments =====
async getTaskComments(taskId: string) {
return this.get(`/task/${taskId}/comment`);
}
async getListComments(listId: string) {
return this.get(`/list/${listId}/comment`);
}
async getViewComments(viewId: string) {
return this.get(`/view/${viewId}/comment`);
}
async createComment(taskId: string, data: any) {
return this.post(`/task/${taskId}/comment`, data);
}
async updateComment(commentId: string, data: any) {
return this.put(`/comment/${commentId}`, data);
}
async deleteComment(commentId: string) {
return this.delete(`/comment/${commentId}`);
}
// ===== Checklists =====
async createChecklist(taskId: string, data: any) {
return this.post(`/task/${taskId}/checklist`, data);
}
async updateChecklist(checklistId: string, data: any) {
return this.put(`/checklist/${checklistId}`, data);
}
async deleteChecklist(checklistId: string) {
return this.delete(`/checklist/${checklistId}`);
}
async createChecklistItem(checklistId: string, data: any) {
return this.post(`/checklist/${checklistId}/checklist_item`, data);
}
async updateChecklistItem(checklistId: string, checklistItemId: string, data: any) {
return this.put(`/checklist/${checklistId}/checklist_item/${checklistItemId}`, data);
}
async deleteChecklistItem(checklistId: string, checklistItemId: string) {
return this.delete(`/checklist/${checklistId}/checklist_item/${checklistItemId}`);
}
// ===== Goals =====
async getGoals(teamId: string) {
return this.get(`/team/${teamId}/goal`);
}
async getGoal(goalId: string) {
return this.get(`/goal/${goalId}`);
}
async createGoal(teamId: string, data: any) {
return this.post(`/team/${teamId}/goal`, data);
}
async updateGoal(goalId: string, data: any) {
return this.put(`/goal/${goalId}`, data);
}
async deleteGoal(goalId: string) {
return this.delete(`/goal/${goalId}`);
}
async createKeyResult(goalId: string, data: any) {
return this.post(`/goal/${goalId}/key_result`, data);
}
async updateKeyResult(keyResultId: string, data: any) {
return this.put(`/key_result/${keyResultId}`, data);
}
async deleteKeyResult(keyResultId: string) {
return this.delete(`/key_result/${keyResultId}`);
}
// ===== Tags =====
async getSpaceTags(spaceId: string) {
return this.get(`/space/${spaceId}/tag`);
}
async createSpaceTag(spaceId: string, data: any) {
return this.post(`/space/${spaceId}/tag`, data);
}
async updateTag(spaceId: string, tagName: string, data: any) {
return this.put(`/space/${spaceId}/tag/${tagName}`, data);
}
async deleteTag(spaceId: string, tagName: string) {
return this.delete(`/space/${spaceId}/tag/${tagName}`);
}
async addTagToTask(taskId: string, tagName: string) {
return this.post(`/task/${taskId}/tag/${tagName}`);
}
async removeTagFromTask(taskId: string, tagName: string) {
return this.delete(`/task/${taskId}/tag/${tagName}`);
}
// ===== Time Tracking =====
async getTimeEntries(teamId: string, params: any = {}) {
return this.get(`/team/${teamId}/time_entries`, params);
}
async getTimeEntry(teamId: string, timerId: string) {
return this.get(`/team/${teamId}/time_entries/${timerId}`);
}
async createTimeEntry(teamId: string, data: any) {
return this.post(`/team/${teamId}/time_entries`, data);
}
async updateTimeEntry(teamId: string, timerId: string, data: any) {
return this.put(`/team/${teamId}/time_entries/${timerId}`, data);
}
async deleteTimeEntry(teamId: string, timerId: string) {
return this.delete(`/team/${teamId}/time_entries/${timerId}`);
}
async getRunningTimeEntry(teamId: string, assignee?: string) {
return this.get(`/team/${teamId}/time_entries/current`, assignee ? { assignee } : {});
}
async startTimer(teamId: string, taskId: string, data: any = {}) {
return this.post(`/team/${teamId}/time_entries/start/${taskId}`, data);
}
async stopTimer(teamId: string) {
return this.post(`/team/${teamId}/time_entries/stop`);
}
async getTaskTimeEntries(taskId: string) {
return this.get(`/task/${taskId}/time`);
}
// ===== Custom Fields =====
async getAccessibleCustomFields(listId: string) {
return this.get(`/list/${listId}/field`);
}
async setCustomFieldValue(taskId: string, fieldId: string, value: any) {
return this.post(`/task/${taskId}/field/${fieldId}`, { value });
}
async removeCustomFieldValue(taskId: string, fieldId: string) {
return this.delete(`/task/${taskId}/field/${fieldId}`);
}
// ===== Webhooks =====
async getWebhooks(teamId: string) {
return this.get(`/team/${teamId}/webhook`);
}
async createWebhook(teamId: string, data: any) {
return this.post(`/team/${teamId}/webhook`, data);
}
async updateWebhook(webhookId: string, data: any) {
return this.put(`/webhook/${webhookId}`, data);
}
async deleteWebhook(webhookId: string) {
return this.delete(`/webhook/${webhookId}`);
}
// ===== Templates =====
async getTemplates(teamId: string, page = 0) {
return this.get(`/team/${teamId}/taskTemplate`, { page });
}
async createTaskFromTemplate(listId: string, templateId: string, name: string) {
return this.post(`/list/${listId}/taskTemplate/${templateId}`, { name });
}
// ===== Members / Guests =====
async getListMembers(listId: string) {
return this.get(`/list/${listId}/member`);
}
async inviteGuestToWorkspace(teamId: string, email: string, canEditTags: boolean) {
return this.post(`/team/${teamId}/guest`, { email, can_edit_tags: canEditTags });
}
async getGuest(teamId: string, guestId: string) {
return this.get(`/team/${teamId}/guest/${guestId}`);
}
async editGuestOnWorkspace(teamId: string, guestId: string, data: any) {
return this.put(`/team/${teamId}/guest/${guestId}`, data);
}
async removeGuestFromWorkspace(teamId: string, guestId: string) {
return this.delete(`/team/${teamId}/guest/${guestId}`);
}
async addGuestToTask(taskId: string, guestId: string, permissionLevel?: string) {
return this.post(`/task/${taskId}/guest/${guestId}`,
permissionLevel ? { permission_level: permissionLevel } : {}
);
}
async removeGuestFromTask(taskId: string, guestId: string) {
return this.delete(`/task/${taskId}/guest/${guestId}`);
}
async addGuestToList(listId: string, guestId: string, permissionLevel?: string) {
return this.post(`/list/${listId}/guest/${guestId}`,
permissionLevel ? { permission_level: permissionLevel } : {}
);
}
async removeGuestFromList(listId: string, guestId: string) {
return this.delete(`/list/${listId}/guest/${guestId}`);
}
async addGuestToFolder(folderId: string, guestId: string, permissionLevel?: string) {
return this.post(`/folder/${folderId}/guest/${guestId}`,
permissionLevel ? { permission_level: permissionLevel } : {}
);
}
async removeGuestFromFolder(folderId: string, guestId: string) {
return this.delete(`/folder/${folderId}/guest/${guestId}`);
}
// ===== Docs =====
async getDocs(workspaceId: string) {
return this.get(`/team/${workspaceId}/docs`);
}
async searchDocs(workspaceId: string, search: string) {
return this.get(`/team/${workspaceId}/docs`, { search });
}
}

View File

@ -1,504 +1,164 @@
#!/usr/bin/env node #!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; /**
* ClickUp MCP Server
* Complete integration with ClickUp API v2
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { import {
CallToolRequestSchema, CallToolRequestSchema,
ListToolsRequestSchema, ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js"; Tool,
} from '@modelcontextprotocol/sdk/types.js';
// ============================================ import { ClickUpClient } from './clients/clickup.js';
// CONFIGURATION import type { ClickUpConfig } from './types.js';
// ============================================
const MCP_NAME = "clickup";
const MCP_VERSION = "1.0.0";
const API_BASE_URL = "https://api.clickup.com/api/v2";
// ============================================ // Import tool creators
// API CLIENT import { createTasksTools } from './tools/tasks-tools.js';
// ============================================ import { createSpacesTools } from './tools/spaces-tools.js';
class ClickUpClient { import { createFoldersTools } from './tools/folders-tools.js';
private apiKey: string; import { createListsTools } from './tools/lists-tools.js';
private baseUrl: string; import { createViewsTools } from './tools/views-tools.js';
import { createCommentsTools } from './tools/comments-tools.js';
import { createDocsTools } from './tools/docs-tools.js';
import { createGoalsTools } from './tools/goals-tools.js';
import { createTagsTools } from './tools/tags-tools.js';
import { createChecklistsTools } from './tools/checklists-tools.js';
import { createTimeTrackingTools } from './tools/time-tracking-tools.js';
import { createTeamsTools } from './tools/teams-tools.js';
import { createWebhooksTools } from './tools/webhooks-tools.js';
import { createCustomFieldsTools } from './tools/custom-fields-tools.js';
import { createTemplatesTools } from './tools/templates-tools.js';
import { createGuestsTools } from './tools/guests-tools.js';
constructor(apiKey: string) { class ClickUpServer {
this.apiKey = apiKey; private server: Server;
this.baseUrl = API_BASE_URL; private client: ClickUpClient;
} private tools: Map<string, any> = new Map();
async request(endpoint: string, options: RequestInit = {}) { constructor() {
const url = `${this.baseUrl}${endpoint}`; this.server = new Server(
const response = await fetch(url, {
...options,
headers: {
"Authorization": this.apiKey,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`ClickUp API error: ${response.status} ${response.statusText} - ${errorBody}`);
}
// Handle empty responses (like 204 No Content)
const text = await response.text();
return text ? JSON.parse(text) : { success: true };
}
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),
});
}
// Space endpoints
async listSpaces(teamId: string, archived?: boolean) {
const params = new URLSearchParams();
if (archived !== undefined) params.append("archived", archived.toString());
const query = params.toString() ? `?${params.toString()}` : "";
return this.get(`/team/${teamId}/space${query}`);
}
// List endpoints
async listLists(folderId: string, archived?: boolean) {
const params = new URLSearchParams();
if (archived !== undefined) params.append("archived", archived.toString());
const query = params.toString() ? `?${params.toString()}` : "";
return this.get(`/folder/${folderId}/list${query}`);
}
async listFolderlessLists(spaceId: string, archived?: boolean) {
const params = new URLSearchParams();
if (archived !== undefined) params.append("archived", archived.toString());
const query = params.toString() ? `?${params.toString()}` : "";
return this.get(`/space/${spaceId}/list${query}`);
}
// Task endpoints
async listTasks(listId: string, options?: {
archived?: boolean;
page?: number;
order_by?: string;
reverse?: boolean;
subtasks?: boolean;
statuses?: string[];
include_closed?: boolean;
assignees?: string[];
due_date_gt?: number;
due_date_lt?: number;
}) {
const params = new URLSearchParams();
if (options?.archived !== undefined) params.append("archived", options.archived.toString());
if (options?.page !== undefined) params.append("page", options.page.toString());
if (options?.order_by) params.append("order_by", options.order_by);
if (options?.reverse !== undefined) params.append("reverse", options.reverse.toString());
if (options?.subtasks !== undefined) params.append("subtasks", options.subtasks.toString());
if (options?.include_closed !== undefined) params.append("include_closed", options.include_closed.toString());
if (options?.statuses) options.statuses.forEach(s => params.append("statuses[]", s));
if (options?.assignees) options.assignees.forEach(a => params.append("assignees[]", a));
if (options?.due_date_gt) params.append("due_date_gt", options.due_date_gt.toString());
if (options?.due_date_lt) params.append("due_date_lt", options.due_date_lt.toString());
const query = params.toString() ? `?${params.toString()}` : "";
return this.get(`/list/${listId}/task${query}`);
}
async getTask(taskId: string, includeSubtasks?: boolean) {
const params = new URLSearchParams();
if (includeSubtasks !== undefined) params.append("include_subtasks", includeSubtasks.toString());
const query = params.toString() ? `?${params.toString()}` : "";
return this.get(`/task/${taskId}${query}`);
}
async createTask(listId: string, data: {
name: string;
description?: string;
assignees?: string[];
tags?: string[];
status?: string;
priority?: number;
due_date?: number;
due_date_time?: boolean;
time_estimate?: number;
start_date?: number;
start_date_time?: boolean;
notify_all?: boolean;
parent?: string;
links_to?: string;
custom_fields?: any[];
}) {
return this.post(`/list/${listId}/task`, data);
}
async updateTask(taskId: string, data: {
name?: string;
description?: string;
assignees?: { add?: string[]; rem?: string[] };
status?: string;
priority?: number;
due_date?: number;
due_date_time?: boolean;
time_estimate?: number;
start_date?: number;
start_date_time?: boolean;
parent?: string;
archived?: boolean;
}) {
return this.put(`/task/${taskId}`, data);
}
// Comment endpoints
async addComment(taskId: string, commentText: string, assignee?: string, notifyAll?: boolean) {
const payload: any = { comment_text: commentText };
if (assignee) payload.assignee = assignee;
if (notifyAll !== undefined) payload.notify_all = notifyAll;
return this.post(`/task/${taskId}/comment`, payload);
}
// Time tracking endpoints
async getTimeEntries(teamId: string, options?: {
start_date?: number;
end_date?: number;
assignee?: string;
include_task_tags?: boolean;
include_location_names?: boolean;
space_id?: string;
folder_id?: string;
list_id?: string;
task_id?: string;
}) {
const params = new URLSearchParams();
if (options?.start_date) params.append("start_date", options.start_date.toString());
if (options?.end_date) params.append("end_date", options.end_date.toString());
if (options?.assignee) params.append("assignee", options.assignee);
if (options?.include_task_tags !== undefined) params.append("include_task_tags", options.include_task_tags.toString());
if (options?.include_location_names !== undefined) params.append("include_location_names", options.include_location_names.toString());
if (options?.space_id) params.append("space_id", options.space_id);
if (options?.folder_id) params.append("folder_id", options.folder_id);
if (options?.list_id) params.append("list_id", options.list_id);
if (options?.task_id) params.append("task_id", options.task_id);
const query = params.toString() ? `?${params.toString()}` : "";
return this.get(`/team/${teamId}/time_entries${query}`);
}
}
// ============================================
// TOOL DEFINITIONS
// ============================================
const tools = [
{ {
name: "list_spaces", name: 'clickup',
description: "List all spaces in a ClickUp workspace/team", version: '1.0.0',
inputSchema: {
type: "object" as const,
properties: {
team_id: { type: "string", description: "The workspace/team ID" },
archived: { type: "boolean", description: "Include archived spaces" },
},
required: ["team_id"],
},
}, },
{ {
name: "list_lists", capabilities: {
description: "List all lists in a folder or space (folderless lists)", tools: {},
inputSchema: {
type: "object" as const,
properties: {
folder_id: { type: "string", description: "The folder ID (for lists in a folder)" },
space_id: { type: "string", description: "The space ID (for folderless lists)" },
archived: { type: "boolean", description: "Include archived lists" },
}, },
},
},
{
name: "list_tasks",
description: "List tasks in a list with optional filters",
inputSchema: {
type: "object" as const,
properties: {
list_id: { type: "string", description: "The list ID" },
archived: { type: "boolean", description: "Filter by archived status" },
page: { type: "number", description: "Page number (0-indexed)" },
order_by: {
type: "string",
description: "Order by field: id, created, updated, due_date",
enum: ["id", "created", "updated", "due_date"]
},
reverse: { type: "boolean", description: "Reverse order" },
subtasks: { type: "boolean", description: "Include subtasks" },
include_closed: { type: "boolean", description: "Include closed tasks" },
statuses: {
type: "array",
items: { type: "string" },
description: "Filter by status names"
},
assignees: {
type: "array",
items: { type: "string" },
description: "Filter by assignee user IDs"
},
},
required: ["list_id"],
},
},
{
name: "get_task",
description: "Get detailed information about a specific task",
inputSchema: {
type: "object" as const,
properties: {
task_id: { type: "string", description: "The task ID" },
include_subtasks: { type: "boolean", description: "Include subtask details" },
},
required: ["task_id"],
},
},
{
name: "create_task",
description: "Create a new task in a list",
inputSchema: {
type: "object" as const,
properties: {
list_id: { type: "string", description: "The list ID to create the task in" },
name: { type: "string", description: "Task name" },
description: { type: "string", description: "Task description (supports markdown)" },
assignees: {
type: "array",
items: { type: "string" },
description: "Array of user IDs to assign"
},
tags: {
type: "array",
items: { type: "string" },
description: "Array of tag names"
},
status: { type: "string", description: "Status name" },
priority: {
type: "number",
description: "Priority: 1=urgent, 2=high, 3=normal, 4=low",
enum: [1, 2, 3, 4]
},
due_date: { type: "number", description: "Due date as Unix timestamp in milliseconds" },
start_date: { type: "number", description: "Start date as Unix timestamp in milliseconds" },
time_estimate: { type: "number", description: "Time estimate in milliseconds" },
parent: { type: "string", description: "Parent task ID (to create as subtask)" },
},
required: ["list_id", "name"],
},
},
{
name: "update_task",
description: "Update an existing task",
inputSchema: {
type: "object" as const,
properties: {
task_id: { type: "string", description: "The task ID to update" },
name: { type: "string", description: "New task name" },
description: { type: "string", description: "New task description" },
status: { type: "string", description: "New status name" },
priority: {
type: "number",
description: "Priority: 1=urgent, 2=high, 3=normal, 4=low, null=none",
enum: [1, 2, 3, 4]
},
due_date: { type: "number", description: "Due date as Unix timestamp in milliseconds" },
start_date: { type: "number", description: "Start date as Unix timestamp in milliseconds" },
time_estimate: { type: "number", description: "Time estimate in milliseconds" },
assignees_add: {
type: "array",
items: { type: "string" },
description: "User IDs to add as assignees"
},
assignees_remove: {
type: "array",
items: { type: "string" },
description: "User IDs to remove from assignees"
},
archived: { type: "boolean", description: "Archive or unarchive the task" },
},
required: ["task_id"],
},
},
{
name: "add_comment",
description: "Add a comment to a task",
inputSchema: {
type: "object" as const,
properties: {
task_id: { type: "string", description: "The task ID" },
comment_text: { type: "string", description: "Comment text (supports markdown)" },
assignee: { type: "string", description: "User ID to assign the comment to" },
notify_all: { type: "boolean", description: "Notify all assignees" },
},
required: ["task_id", "comment_text"],
},
},
{
name: "get_time_entries",
description: "Get time tracking entries for a workspace",
inputSchema: {
type: "object" as const,
properties: {
team_id: { type: "string", description: "The workspace/team ID" },
start_date: { type: "number", description: "Start date as Unix timestamp in milliseconds" },
end_date: { type: "number", description: "End date as Unix timestamp in milliseconds" },
assignee: { type: "string", description: "Filter by user ID" },
task_id: { type: "string", description: "Filter by task ID" },
list_id: { type: "string", description: "Filter by list ID" },
space_id: { type: "string", description: "Filter by space ID" },
include_task_tags: { type: "boolean", description: "Include task tags in response" },
include_location_names: { type: "boolean", description: "Include location names in response" },
},
required: ["team_id"],
},
},
];
// ============================================
// TOOL HANDLERS
// ============================================
async function handleTool(client: ClickUpClient, name: string, args: any) {
switch (name) {
case "list_spaces": {
const { team_id, archived } = args;
return await client.listSpaces(team_id, archived);
} }
case "list_lists": {
const { folder_id, space_id, archived } = args;
if (folder_id) {
return await client.listLists(folder_id, archived);
} else if (space_id) {
return await client.listFolderlessLists(space_id, archived);
} else {
throw new Error("Either folder_id or space_id is required");
}
}
case "list_tasks": {
const { list_id, archived, page, order_by, reverse, subtasks, include_closed, statuses, assignees } = args;
return await client.listTasks(list_id, {
archived,
page,
order_by,
reverse,
subtasks,
include_closed,
statuses,
assignees,
});
}
case "get_task": {
const { task_id, include_subtasks } = args;
return await client.getTask(task_id, include_subtasks);
}
case "create_task": {
const { list_id, name, description, assignees, tags, status, priority, due_date, start_date, time_estimate, parent } = args;
return await client.createTask(list_id, {
name,
description,
assignees,
tags,
status,
priority,
due_date,
start_date,
time_estimate,
parent,
});
}
case "update_task": {
const { task_id, name, description, status, priority, due_date, start_date, time_estimate, assignees_add, assignees_remove, archived } = args;
const updateData: any = {};
if (name !== undefined) updateData.name = name;
if (description !== undefined) updateData.description = description;
if (status !== undefined) updateData.status = status;
if (priority !== undefined) updateData.priority = priority;
if (due_date !== undefined) updateData.due_date = due_date;
if (start_date !== undefined) updateData.start_date = start_date;
if (time_estimate !== undefined) updateData.time_estimate = time_estimate;
if (archived !== undefined) updateData.archived = archived;
if (assignees_add || assignees_remove) {
updateData.assignees = {};
if (assignees_add) updateData.assignees.add = assignees_add;
if (assignees_remove) updateData.assignees.rem = assignees_remove;
}
return await client.updateTask(task_id, updateData);
}
case "add_comment": {
const { task_id, comment_text, assignee, notify_all } = args;
return await client.addComment(task_id, comment_text, assignee, notify_all);
}
case "get_time_entries": {
const { team_id, start_date, end_date, assignee, task_id, list_id, space_id, include_task_tags, include_location_names } = args;
return await client.getTimeEntries(team_id, {
start_date,
end_date,
assignee,
task_id,
list_id,
space_id,
include_task_tags,
include_location_names,
});
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// ============================================
// SERVER SETUP
// ============================================
async function main() {
const apiKey = process.env.CLICKUP_API_KEY;
if (!apiKey) {
console.error("Error: CLICKUP_API_KEY environment variable required");
console.error("Get your API key from ClickUp Settings > Apps > API Token");
process.exit(1);
}
const client = new ClickUpClient(apiKey);
const server = new Server(
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
{ capabilities: { tools: {} } }
); );
server.setRequestHandler(ListToolsRequestSchema, async () => ({ // Initialize ClickUp client
tools, const config: ClickUpConfig = {
apiToken: process.env.CLICKUP_API_TOKEN,
oauthToken: process.env.CLICKUP_OAUTH_TOKEN,
clientId: process.env.CLICKUP_CLIENT_ID,
clientSecret: process.env.CLICKUP_CLIENT_SECRET,
};
this.client = new ClickUpClient(config);
// Register all tools
this.registerTools();
// Setup request handlers
this.setupHandlers();
// Error handling
this.server.onerror = (error) => {
console.error('[MCP Error]', error);
};
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private registerTools() {
const allTools = [
...createTasksTools(this.client),
...createSpacesTools(this.client),
...createFoldersTools(this.client),
...createListsTools(this.client),
...createViewsTools(this.client),
...createCommentsTools(this.client),
...createDocsTools(this.client),
...createGoalsTools(this.client),
...createTagsTools(this.client),
...createChecklistsTools(this.client),
...createTimeTrackingTools(this.client),
...createTeamsTools(this.client),
...createWebhooksTools(this.client),
...createCustomFieldsTools(this.client),
...createTemplatesTools(this.client),
...createGuestsTools(this.client),
];
for (const tool of allTools) {
this.tools.set(tool.name, tool);
}
}
private setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: Tool[] = Array.from(this.tools.values()).map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: {
type: 'object',
properties: tool.inputSchema.shape,
required: Object.keys(tool.inputSchema.shape).filter(
key => !tool.inputSchema.shape[key].isOptional()
),
},
})); }));
server.setRequestHandler(CallToolRequestSchema, async (request) => { return { tools };
const { name, arguments: args } = request.params; });
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const tool = this.tools.get(request.params.name);
if (!tool) {
throw new Error(`Unknown tool: ${request.params.name}`);
}
try { try {
const result = await handleTool(client, name, args || {}); // Validate input
return { const args = tool.inputSchema.parse(request.params.arguments);
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
}; // Execute tool
const result = await tool.handler(args);
return result;
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error); if (error instanceof Error) {
return { return {
content: [{ type: "text", text: `Error: ${message}` }], content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true, isError: true,
}; };
} }
throw error;
}
}); });
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${MCP_NAME} MCP server running on stdio`);
} }
main().catch(console.error); async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('ClickUp MCP server running on stdio');
}
}
const server = new ClickUpServer();
server.run().catch(console.error);

View File

@ -0,0 +1,97 @@
/**
* ClickUp Checklists Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createChecklistsTools(client: ClickUpClient) {
return [
{
name: 'clickup_checklists_create',
description: 'Create a checklist on a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
name: z.string().describe('Checklist name'),
}),
handler: async (args: any) => {
const { task_id, ...data } = args;
const response = await client.createChecklist(task_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_checklists_update',
description: 'Update a checklist',
inputSchema: z.object({
checklist_id: z.string().describe('Checklist ID'),
name: z.string().optional().describe('Checklist name'),
position: z.number().optional().describe('Position/order index'),
}),
handler: async (args: any) => {
const { checklist_id, ...data } = args;
const response = await client.updateChecklist(checklist_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_checklists_delete',
description: 'Delete a checklist',
inputSchema: z.object({
checklist_id: z.string().describe('Checklist ID'),
}),
handler: async (args: any) => {
await client.deleteChecklist(args.checklist_id);
return { content: [{ type: 'text', text: 'Checklist deleted successfully' }] };
}
},
{
name: 'clickup_checklists_create_item',
description: 'Create a checklist item',
inputSchema: z.object({
checklist_id: z.string().describe('Checklist ID'),
name: z.string().describe('Item name'),
assignee: z.number().optional().describe('Assignee user ID'),
}),
handler: async (args: any) => {
const { checklist_id, ...data } = args;
const response = await client.createChecklistItem(checklist_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_checklists_update_item',
description: 'Update a checklist item',
inputSchema: z.object({
checklist_id: z.string().describe('Checklist ID'),
checklist_item_id: z.string().describe('Checklist Item ID'),
name: z.string().optional().describe('Item name'),
assignee: z.number().optional().describe('Assignee user ID'),
resolved: z.boolean().optional().describe('Mark as resolved/completed'),
parent: z.string().optional().describe('Parent item ID (for nesting)'),
}),
handler: async (args: any) => {
const { checklist_id, checklist_item_id, ...data } = args;
const response = await client.updateChecklistItem(checklist_id, checklist_item_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_checklists_delete_item',
description: 'Delete a checklist item',
inputSchema: z.object({
checklist_id: z.string().describe('Checklist ID'),
checklist_item_id: z.string().describe('Checklist Item ID'),
}),
handler: async (args: any) => {
await client.deleteChecklistItem(args.checklist_id, args.checklist_item_id);
return { content: [{ type: 'text', text: 'Checklist item deleted successfully' }] };
}
},
];
}

View File

@ -0,0 +1,77 @@
/**
* ClickUp Comments Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createCommentsTools(client: ClickUpClient) {
return [
{
name: 'clickup_comments_list',
description: 'List comments on a task, list, or view',
inputSchema: z.object({
task_id: z.string().optional().describe('Task ID'),
list_id: z.string().optional().describe('List ID'),
view_id: z.string().optional().describe('View ID'),
}),
handler: async (args: any) => {
let response;
if (args.task_id) {
response = await client.getTaskComments(args.task_id);
} else if (args.list_id) {
response = await client.getListComments(args.list_id);
} else if (args.view_id) {
response = await client.getViewComments(args.view_id);
} else {
throw new Error('One of task_id, list_id, or view_id must be provided');
}
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_comments_create',
description: 'Create a comment on a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
comment_text: z.string().describe('Comment text'),
assignee: z.number().optional().describe('Assign comment to user ID'),
notify_all: z.boolean().optional().describe('Notify all task watchers'),
}),
handler: async (args: any) => {
const { task_id, ...data } = args;
const response = await client.createComment(task_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_comments_update',
description: 'Update a comment',
inputSchema: z.object({
comment_id: z.string().describe('Comment ID'),
comment_text: z.string().optional().describe('Comment text'),
assignee: z.number().optional().describe('Assign comment to user ID'),
resolved: z.boolean().optional().describe('Mark as resolved'),
}),
handler: async (args: any) => {
const { comment_id, ...data } = args;
const response = await client.updateComment(comment_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_comments_delete',
description: 'Delete a comment',
inputSchema: z.object({
comment_id: z.string().describe('Comment ID'),
}),
handler: async (args: any) => {
await client.deleteComment(args.comment_id);
return { content: [{ type: 'text', text: 'Comment deleted successfully' }] };
}
},
];
}

View File

@ -0,0 +1,62 @@
/**
* ClickUp Custom Fields Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createCustomFieldsTools(client: ClickUpClient) {
return [
{
name: 'clickup_custom_fields_list',
description: 'List accessible custom fields for a list',
inputSchema: z.object({
list_id: z.string().describe('List ID'),
}),
handler: async (args: any) => {
const response = await client.getAccessibleCustomFields(args.list_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_custom_fields_get',
description: 'Get custom field values for a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
}),
handler: async (args: any) => {
const task = await client.getTask(args.task_id);
const customFields = (task as any).custom_fields || [];
return { content: [{ type: 'text', text: JSON.stringify(customFields, null, 2) }] };
}
},
{
name: 'clickup_custom_fields_set_value',
description: 'Set a custom field value on a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
field_id: z.string().describe('Custom field ID'),
value: z.any().describe('Field value (type depends on field type)'),
}),
handler: async (args: any) => {
const response = await client.setCustomFieldValue(args.task_id, args.field_id, args.value);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_custom_fields_remove_value',
description: 'Remove a custom field value from a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
field_id: z.string().describe('Custom field ID'),
}),
handler: async (args: any) => {
await client.removeCustomFieldValue(args.task_id, args.field_id);
return { content: [{ type: 'text', text: 'Custom field value removed successfully' }] };
}
},
];
}

View File

@ -0,0 +1,35 @@
/**
* ClickUp Docs Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createDocsTools(client: ClickUpClient) {
return [
{
name: 'clickup_docs_list',
description: 'List all docs in a workspace',
inputSchema: z.object({
workspace_id: z.string().describe('Workspace/Team ID'),
}),
handler: async (args: any) => {
const response = await client.getDocs(args.workspace_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_docs_search',
description: 'Search docs in a workspace',
inputSchema: z.object({
workspace_id: z.string().describe('Workspace/Team ID'),
search: z.string().describe('Search query'),
}),
handler: async (args: any) => {
const response = await client.searchDocs(args.workspace_id, args.search);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
];
}

View File

@ -0,0 +1,75 @@
/**
* ClickUp Folders Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createFoldersTools(client: ClickUpClient) {
return [
{
name: 'clickup_folders_list',
description: 'List all folders in a space',
inputSchema: z.object({
space_id: z.string().describe('Space ID'),
archived: z.boolean().optional().describe('Include archived folders'),
}),
handler: async (args: any) => {
const response = await client.getFolders(args.space_id, args.archived);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_folders_get',
description: 'Get a specific folder by ID',
inputSchema: z.object({
folder_id: z.string().describe('Folder ID'),
}),
handler: async (args: any) => {
const response = await client.getFolder(args.folder_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_folders_create',
description: 'Create a new folder',
inputSchema: z.object({
space_id: z.string().describe('Space ID'),
name: z.string().describe('Folder name'),
}),
handler: async (args: any) => {
const { space_id, ...data } = args;
const response = await client.createFolder(space_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_folders_update',
description: 'Update a folder',
inputSchema: z.object({
folder_id: z.string().describe('Folder ID'),
name: z.string().optional().describe('Folder name'),
}),
handler: async (args: any) => {
const { folder_id, ...data } = args;
const response = await client.updateFolder(folder_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_folders_delete',
description: 'Delete a folder',
inputSchema: z.object({
folder_id: z.string().describe('Folder ID'),
}),
handler: async (args: any) => {
await client.deleteFolder(args.folder_id);
return { content: [{ type: 'text', text: 'Folder deleted successfully' }] };
}
},
];
}

View File

@ -0,0 +1,123 @@
/**
* ClickUp Goals Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createGoalsTools(client: ClickUpClient) {
return [
{
name: 'clickup_goals_list',
description: 'List all goals in a team',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
}),
handler: async (args: any) => {
const response = await client.getGoals(args.team_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_goals_get',
description: 'Get a specific goal by ID',
inputSchema: z.object({
goal_id: z.string().describe('Goal ID'),
}),
handler: async (args: any) => {
const response = await client.getGoal(args.goal_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_goals_create',
description: 'Create a new goal',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
name: z.string().describe('Goal name'),
due_date: z.number().optional().describe('Due date (Unix timestamp in milliseconds)'),
description: z.string().optional().describe('Goal description'),
multiple_owners: z.boolean().optional().describe('Allow multiple owners'),
owners: z.array(z.number()).optional().describe('Owner user IDs'),
color: z.string().optional().describe('Color hex code'),
}),
handler: async (args: any) => {
const { team_id, ...data } = args;
const response = await client.createGoal(team_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_goals_update',
description: 'Update a goal',
inputSchema: z.object({
goal_id: z.string().describe('Goal ID'),
name: z.string().optional().describe('Goal name'),
due_date: z.number().optional().describe('Due date (Unix timestamp in milliseconds)'),
description: z.string().optional().describe('Goal description'),
rem_owners: z.array(z.number()).optional().describe('Remove owner user IDs'),
add_owners: z.array(z.number()).optional().describe('Add owner user IDs'),
color: z.string().optional().describe('Color hex code'),
}),
handler: async (args: any) => {
const { goal_id, ...data } = args;
const response = await client.updateGoal(goal_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_goals_delete',
description: 'Delete a goal',
inputSchema: z.object({
goal_id: z.string().describe('Goal ID'),
}),
handler: async (args: any) => {
await client.deleteGoal(args.goal_id);
return { content: [{ type: 'text', text: 'Goal deleted successfully' }] };
}
},
{
name: 'clickup_goals_add_key_result',
description: 'Add a key result to a goal',
inputSchema: z.object({
goal_id: z.string().describe('Goal ID'),
name: z.string().describe('Key result name'),
owners: z.array(z.number()).optional().describe('Owner user IDs'),
type: z.string().describe('Type: number, currency, boolean, percentage, automatic'),
steps_start: z.number().optional().describe('Starting value (for number/currency/percentage)'),
steps_end: z.number().optional().describe('Target value'),
unit: z.string().optional().describe('Unit (for currency)'),
task_ids: z.array(z.string()).optional().describe('Task IDs (for automatic)'),
list_ids: z.array(z.string()).optional().describe('List IDs (for automatic)'),
}),
handler: async (args: any) => {
const { goal_id, ...data } = args;
const response = await client.createKeyResult(goal_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_goals_update_key_result',
description: 'Update a key result',
inputSchema: z.object({
key_result_id: z.string().describe('Key Result ID'),
name: z.string().optional().describe('Key result name'),
note: z.string().optional().describe('Note'),
steps_current: z.number().optional().describe('Current value'),
steps_start: z.number().optional().describe('Starting value'),
steps_end: z.number().optional().describe('Target value'),
}),
handler: async (args: any) => {
const { key_result_id, ...data } = args;
const response = await client.updateKeyResult(key_result_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
];
}

View File

@ -0,0 +1,114 @@
/**
* ClickUp Guests Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createGuestsTools(client: ClickUpClient) {
return [
{
name: 'clickup_guests_invite',
description: 'Invite a guest to a workspace',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
email: z.string().describe('Guest email address'),
can_edit_tags: z.boolean().optional().describe('Allow guest to edit tags'),
}),
handler: async (args: any) => {
const response = await client.inviteGuestToWorkspace(
args.team_id,
args.email,
args.can_edit_tags || false
);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_guests_get',
description: 'Get guest details',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
guest_id: z.string().describe('Guest ID'),
}),
handler: async (args: any) => {
const response = await client.getGuest(args.team_id, args.guest_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_guests_edit',
description: 'Edit guest permissions',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
guest_id: z.string().describe('Guest ID'),
username: z.string().optional().describe('Username'),
can_edit_tags: z.boolean().optional().describe('Allow guest to edit tags'),
can_see_time_spent: z.boolean().optional().describe('Allow guest to see time spent'),
can_see_time_estimated: z.boolean().optional().describe('Allow guest to see time estimated'),
}),
handler: async (args: any) => {
const { team_id, guest_id, ...data } = args;
const response = await client.editGuestOnWorkspace(team_id, guest_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_guests_remove',
description: 'Remove a guest from a workspace',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
guest_id: z.string().describe('Guest ID'),
}),
handler: async (args: any) => {
await client.removeGuestFromWorkspace(args.team_id, args.guest_id);
return { content: [{ type: 'text', text: 'Guest removed successfully' }] };
}
},
{
name: 'clickup_guests_add_to_task',
description: 'Add a guest to a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
guest_id: z.string().describe('Guest ID'),
permission_level: z.string().optional().describe('Permission level (read, comment, edit, create)'),
}),
handler: async (args: any) => {
const response = await client.addGuestToTask(args.task_id, args.guest_id, args.permission_level);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_guests_add_to_list',
description: 'Add a guest to a list',
inputSchema: z.object({
list_id: z.string().describe('List ID'),
guest_id: z.string().describe('Guest ID'),
permission_level: z.string().optional().describe('Permission level (read, comment, edit, create)'),
}),
handler: async (args: any) => {
const response = await client.addGuestToList(args.list_id, args.guest_id, args.permission_level);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_guests_add_to_folder',
description: 'Add a guest to a folder',
inputSchema: z.object({
folder_id: z.string().describe('Folder ID'),
guest_id: z.string().describe('Guest ID'),
permission_level: z.string().optional().describe('Permission level (read, comment, edit, create)'),
}),
handler: async (args: any) => {
const response = await client.addGuestToFolder(args.folder_id, args.guest_id, args.permission_level);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
];
}

View File

@ -0,0 +1,129 @@
/**
* ClickUp Lists Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createListsTools(client: ClickUpClient) {
return [
{
name: 'clickup_lists_list',
description: 'List all lists in a folder or space',
inputSchema: z.object({
folder_id: z.string().optional().describe('Folder ID'),
space_id: z.string().optional().describe('Space ID (for folderless lists)'),
archived: z.boolean().optional().describe('Include archived lists'),
}),
handler: async (args: any) => {
let response;
if (args.folder_id) {
response = await client.getFolderLists(args.folder_id, args.archived);
} else if (args.space_id) {
response = await client.getSpaceLists(args.space_id, args.archived);
} else {
throw new Error('Either folder_id or space_id must be provided');
}
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_lists_get',
description: 'Get a specific list by ID',
inputSchema: z.object({
list_id: z.string().describe('List ID'),
}),
handler: async (args: any) => {
const response = await client.getList(args.list_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_lists_create',
description: 'Create a new list',
inputSchema: z.object({
folder_id: z.string().optional().describe('Folder ID'),
space_id: z.string().optional().describe('Space ID (for folderless list)'),
name: z.string().describe('List name'),
content: z.string().optional().describe('List description'),
due_date: z.number().optional().describe('Due date (Unix timestamp)'),
due_date_time: z.boolean().optional().describe('Include time in due date'),
priority: z.number().optional().describe('Priority (1-4)'),
assignee: z.number().optional().describe('Default assignee user ID'),
status: z.string().optional().describe('Default status'),
}),
handler: async (args: any) => {
const { folder_id, space_id, ...data } = args;
let response;
if (folder_id) {
response = await client.createList(folder_id, data);
} else if (space_id) {
response = await client.createFolderlessList(space_id, data);
} else {
throw new Error('Either folder_id or space_id must be provided');
}
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_lists_update',
description: 'Update a list',
inputSchema: z.object({
list_id: z.string().describe('List ID'),
name: z.string().optional().describe('List name'),
content: z.string().optional().describe('List description'),
due_date: z.number().optional().describe('Due date (Unix timestamp)'),
due_date_time: z.boolean().optional().describe('Include time in due date'),
priority: z.number().optional().describe('Priority (1-4)'),
assignee: z.number().optional().describe('Default assignee user ID'),
unset_status: z.boolean().optional().describe('Remove default status'),
}),
handler: async (args: any) => {
const { list_id, ...data } = args;
const response = await client.updateList(list_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_lists_delete',
description: 'Delete a list',
inputSchema: z.object({
list_id: z.string().describe('List ID'),
}),
handler: async (args: any) => {
await client.deleteList(args.list_id);
return { content: [{ type: 'text', text: 'List deleted successfully' }] };
}
},
{
name: 'clickup_lists_add_task',
description: 'Add an existing task to a list',
inputSchema: z.object({
list_id: z.string().describe('List ID'),
task_id: z.string().describe('Task ID'),
}),
handler: async (args: any) => {
const response = await client.addTaskToList(args.list_id, args.task_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_lists_remove_task',
description: 'Remove a task from a list',
inputSchema: z.object({
list_id: z.string().describe('List ID'),
task_id: z.string().describe('Task ID'),
}),
handler: async (args: any) => {
await client.removeTaskFromList(args.list_id, args.task_id);
return { content: [{ type: 'text', text: 'Task removed from list successfully' }] };
}
},
];
}

View File

@ -0,0 +1,112 @@
/**
* ClickUp Spaces Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createSpacesTools(client: ClickUpClient) {
return [
{
name: 'clickup_spaces_list',
description: 'List all spaces in a workspace',
inputSchema: z.object({
team_id: z.string().describe('Team/Workspace ID'),
archived: z.boolean().optional().describe('Include archived spaces'),
}),
handler: async (args: any) => {
const response = await client.getSpaces(args.team_id, args.archived);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_spaces_get',
description: 'Get a specific space by ID',
inputSchema: z.object({
space_id: z.string().describe('Space ID'),
}),
handler: async (args: any) => {
const response = await client.getSpace(args.space_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_spaces_create',
description: 'Create a new space',
inputSchema: z.object({
team_id: z.string().describe('Team/Workspace ID'),
name: z.string().describe('Space name'),
multiple_assignees: z.boolean().optional().describe('Allow multiple assignees'),
features: z.object({
due_dates: z.object({
enabled: z.boolean().optional(),
start_date: z.boolean().optional(),
remap_due_dates: z.boolean().optional(),
remap_closed_due_date: z.boolean().optional(),
}).optional(),
time_tracking: z.object({ enabled: z.boolean().optional() }).optional(),
tags: z.object({ enabled: z.boolean().optional() }).optional(),
time_estimates: z.object({ enabled: z.boolean().optional() }).optional(),
checklists: z.object({ enabled: z.boolean().optional() }).optional(),
custom_fields: z.object({ enabled: z.boolean().optional() }).optional(),
remap_dependencies: z.object({ enabled: z.boolean().optional() }).optional(),
dependency_warning: z.object({ enabled: z.boolean().optional() }).optional(),
portfolios: z.object({ enabled: z.boolean().optional() }).optional(),
}).optional().describe('Space features configuration'),
}),
handler: async (args: any) => {
const { team_id, ...data } = args;
const response = await client.createSpace(team_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_spaces_update',
description: 'Update a space',
inputSchema: z.object({
space_id: z.string().describe('Space ID'),
name: z.string().optional().describe('Space name'),
color: z.string().optional().describe('Space color (hex)'),
private: z.boolean().optional().describe('Make space private'),
admin_can_manage: z.boolean().optional().describe('Allow admins to manage'),
multiple_assignees: z.boolean().optional().describe('Allow multiple assignees'),
features: z.object({
due_dates: z.object({
enabled: z.boolean().optional(),
start_date: z.boolean().optional(),
remap_due_dates: z.boolean().optional(),
remap_closed_due_date: z.boolean().optional(),
}).optional(),
time_tracking: z.object({ enabled: z.boolean().optional() }).optional(),
tags: z.object({ enabled: z.boolean().optional() }).optional(),
time_estimates: z.object({ enabled: z.boolean().optional() }).optional(),
checklists: z.object({ enabled: z.boolean().optional() }).optional(),
custom_fields: z.object({ enabled: z.boolean().optional() }).optional(),
remap_dependencies: z.object({ enabled: z.boolean().optional() }).optional(),
dependency_warning: z.object({ enabled: z.boolean().optional() }).optional(),
portfolios: z.object({ enabled: z.boolean().optional() }).optional(),
}).optional().describe('Space features configuration'),
}),
handler: async (args: any) => {
const { space_id, ...data } = args;
const response = await client.updateSpace(space_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_spaces_delete',
description: 'Delete a space',
inputSchema: z.object({
space_id: z.string().describe('Space ID'),
}),
handler: async (args: any) => {
await client.deleteSpace(args.space_id);
return { content: [{ type: 'text', text: 'Space deleted successfully' }] };
}
},
];
}

View File

@ -0,0 +1,96 @@
/**
* ClickUp Tags Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createTagsTools(client: ClickUpClient) {
return [
{
name: 'clickup_tags_list',
description: 'List all tags in a space',
inputSchema: z.object({
space_id: z.string().describe('Space ID'),
}),
handler: async (args: any) => {
const response = await client.getSpaceTags(args.space_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tags_create',
description: 'Create a new tag',
inputSchema: z.object({
space_id: z.string().describe('Space ID'),
name: z.string().describe('Tag name'),
tag_fg: z.string().optional().describe('Foreground color (hex)'),
tag_bg: z.string().optional().describe('Background color (hex)'),
}),
handler: async (args: any) => {
const { space_id, name, ...data } = args;
const response = await client.createSpaceTag(space_id, { tag: { name, ...data } });
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tags_update',
description: 'Update a tag',
inputSchema: z.object({
space_id: z.string().describe('Space ID'),
tag_name: z.string().describe('Current tag name'),
new_name: z.string().optional().describe('New tag name'),
tag_fg: z.string().optional().describe('Foreground color (hex)'),
tag_bg: z.string().optional().describe('Background color (hex)'),
}),
handler: async (args: any) => {
const { space_id, tag_name, new_name, ...data } = args;
const updateData: any = { ...data };
if (new_name) updateData.name = new_name;
const response = await client.updateTag(space_id, tag_name, { tag: updateData });
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tags_delete',
description: 'Delete a tag',
inputSchema: z.object({
space_id: z.string().describe('Space ID'),
tag_name: z.string().describe('Tag name'),
}),
handler: async (args: any) => {
await client.deleteTag(args.space_id, args.tag_name);
return { content: [{ type: 'text', text: 'Tag deleted successfully' }] };
}
},
{
name: 'clickup_tags_add_to_task',
description: 'Add a tag to a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
tag_name: z.string().describe('Tag name'),
}),
handler: async (args: any) => {
const response = await client.addTagToTask(args.task_id, args.tag_name);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tags_remove_from_task',
description: 'Remove a tag from a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
tag_name: z.string().describe('Tag name'),
}),
handler: async (args: any) => {
await client.removeTagFromTask(args.task_id, args.tag_name);
return { content: [{ type: 'text', text: 'Tag removed from task successfully' }] };
}
},
];
}

View File

@ -0,0 +1,325 @@
/**
* ClickUp Tasks Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createTasksTools(client: ClickUpClient) {
return [
{
name: 'clickup_tasks_list',
description: 'List tasks in a list with optional filtering',
inputSchema: z.object({
list_id: z.string().describe('List ID'),
archived: z.boolean().optional().describe('Include archived tasks'),
page: z.number().optional().describe('Page number for pagination'),
order_by: z.string().optional().describe('Order by field'),
reverse: z.boolean().optional().describe('Reverse order'),
subtasks: z.boolean().optional().describe('Include subtasks'),
statuses: z.array(z.string()).optional().describe('Filter by statuses'),
include_closed: z.boolean().optional().describe('Include closed tasks'),
assignees: z.array(z.string()).optional().describe('Filter by assignees'),
tags: z.array(z.string()).optional().describe('Filter by tags'),
due_date_gt: z.number().optional().describe('Due date greater than (Unix timestamp)'),
due_date_lt: z.number().optional().describe('Due date less than (Unix timestamp)'),
date_created_gt: z.number().optional().describe('Created after (Unix timestamp)'),
date_created_lt: z.number().optional().describe('Created before (Unix timestamp)'),
date_updated_gt: z.number().optional().describe('Updated after (Unix timestamp)'),
date_updated_lt: z.number().optional().describe('Updated before (Unix timestamp)'),
custom_fields: z.array(z.object({
field_id: z.string(),
operator: z.string(),
value: z.any()
})).optional().describe('Custom field filters'),
}),
handler: async (args: any) => {
const response = await client.getTasks(args.list_id, args);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tasks_get',
description: 'Get a specific task by ID',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
custom_task_ids: z.boolean().optional().describe('Use custom task IDs'),
team_id: z.string().optional().describe('Team ID (required if using custom task IDs)'),
include_subtasks: z.boolean().optional().describe('Include subtasks'),
}),
handler: async (args: any) => {
const response = await client.getTask(args.task_id, {
custom_task_ids: args.custom_task_ids,
team_id: args.team_id,
include_subtasks: args.include_subtasks
});
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tasks_create',
description: 'Create a new task',
inputSchema: z.object({
list_id: z.string().describe('List ID'),
name: z.string().describe('Task name'),
description: z.string().optional().describe('Task description'),
assignees: z.array(z.number()).optional().describe('Assignee user IDs'),
tags: z.array(z.string()).optional().describe('Tag names'),
status: z.string().optional().describe('Status name'),
priority: z.number().optional().describe('Priority (1=urgent, 2=high, 3=normal, 4=low)'),
due_date: z.number().optional().describe('Due date (Unix timestamp in milliseconds)'),
due_date_time: z.boolean().optional().describe('Include time in due date'),
time_estimate: z.number().optional().describe('Time estimate in milliseconds'),
start_date: z.number().optional().describe('Start date (Unix timestamp in milliseconds)'),
start_date_time: z.boolean().optional().describe('Include time in start date'),
notify_all: z.boolean().optional().describe('Notify all task watchers'),
parent: z.string().optional().describe('Parent task ID for subtasks'),
links_to: z.string().optional().describe('Link to another task ID'),
check_required_custom_fields: z.boolean().optional().describe('Validate required custom fields'),
custom_fields: z.array(z.object({
id: z.string(),
value: z.any()
})).optional().describe('Custom field values'),
}),
handler: async (args: any) => {
const { list_id, ...data } = args;
const response = await client.createTask(list_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tasks_update',
description: 'Update an existing task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
name: z.string().optional().describe('Task name'),
description: z.string().optional().describe('Task description'),
status: z.string().optional().describe('Status name'),
priority: z.number().optional().describe('Priority (1=urgent, 2=high, 3=normal, 4=low)'),
due_date: z.number().optional().describe('Due date (Unix timestamp in milliseconds)'),
due_date_time: z.boolean().optional().describe('Include time in due date'),
parent: z.string().optional().describe('Parent task ID'),
time_estimate: z.number().optional().describe('Time estimate in milliseconds'),
start_date: z.number().optional().describe('Start date (Unix timestamp in milliseconds)'),
start_date_time: z.boolean().optional().describe('Include time in start date'),
assignees_add: z.array(z.number()).optional().describe('Add assignees (user IDs)'),
assignees_rem: z.array(z.number()).optional().describe('Remove assignees (user IDs)'),
archived: z.boolean().optional().describe('Archive/unarchive task'),
}),
handler: async (args: any) => {
const { task_id, assignees_add, assignees_rem, ...data } = args;
if (assignees_add || assignees_rem) {
data.assignees = {
add: assignees_add,
rem: assignees_rem
};
}
const response = await client.updateTask(task_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tasks_delete',
description: 'Delete a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
custom_task_ids: z.boolean().optional().describe('Use custom task IDs'),
team_id: z.string().optional().describe('Team ID (required if using custom task IDs)'),
}),
handler: async (args: any) => {
const response = await client.deleteTask(args.task_id);
return { content: [{ type: 'text', text: 'Task deleted successfully' }] };
}
},
{
name: 'clickup_tasks_search',
description: 'Search/filter tasks across a team',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
page: z.number().optional().describe('Page number'),
order_by: z.string().optional().describe('Order by field'),
reverse: z.boolean().optional().describe('Reverse order'),
subtasks: z.boolean().optional().describe('Include subtasks'),
space_ids: z.array(z.string()).optional().describe('Filter by space IDs'),
project_ids: z.array(z.string()).optional().describe('Filter by project/folder IDs'),
list_ids: z.array(z.string()).optional().describe('Filter by list IDs'),
statuses: z.array(z.string()).optional().describe('Filter by statuses'),
include_closed: z.boolean().optional().describe('Include closed tasks'),
assignees: z.array(z.string()).optional().describe('Filter by assignee IDs'),
tags: z.array(z.string()).optional().describe('Filter by tags'),
due_date_gt: z.number().optional().describe('Due date greater than'),
due_date_lt: z.number().optional().describe('Due date less than'),
date_created_gt: z.number().optional().describe('Created after'),
date_created_lt: z.number().optional().describe('Created before'),
date_updated_gt: z.number().optional().describe('Updated after'),
date_updated_lt: z.number().optional().describe('Updated before'),
}),
handler: async (args: any) => {
const { team_id, ...params } = args;
const response = await client.getFilteredTasks(team_id, params);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tasks_bulk_update',
description: 'Bulk update multiple tasks',
inputSchema: z.object({
task_ids: z.array(z.string()).describe('Array of task IDs'),
status: z.string().optional().describe('Status to set'),
priority: z.number().optional().describe('Priority to set'),
assignees_add: z.array(z.number()).optional().describe('Add assignees'),
assignees_rem: z.array(z.number()).optional().describe('Remove assignees'),
archived: z.boolean().optional().describe('Archive/unarchive'),
}),
handler: async (args: any) => {
const { task_ids, assignees_add, assignees_rem, ...data } = args;
if (assignees_add || assignees_rem) {
data.assignees = {
add: assignees_add,
rem: assignees_rem
};
}
const response = await client.bulkUpdateTasks(task_ids, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tasks_get_time_entries',
description: 'Get time entries for a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
}),
handler: async (args: any) => {
const response = await client.getTaskTimeEntries(args.task_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tasks_add_time_entry',
description: 'Add a time entry to a task',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
task_id: z.string().describe('Task ID'),
duration: z.number().describe('Duration in milliseconds'),
start: z.number().describe('Start time (Unix timestamp in milliseconds)'),
description: z.string().optional().describe('Time entry description'),
billable: z.boolean().optional().describe('Is billable'),
assignee: z.number().optional().describe('Assignee user ID'),
tags: z.array(z.string()).optional().describe('Tag names'),
}),
handler: async (args: any) => {
const { team_id, task_id, ...data } = args;
data.tid = task_id;
const response = await client.createTimeEntry(team_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tasks_get_custom_fields',
description: 'Get custom field values for a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
}),
handler: async (args: any) => {
const task = await client.getTask(args.task_id);
const customFields = (task as any).custom_fields || [];
return { content: [{ type: 'text', text: JSON.stringify(customFields, null, 2) }] };
}
},
{
name: 'clickup_tasks_set_custom_field',
description: 'Set a custom field value on a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
field_id: z.string().describe('Custom field ID'),
value: z.any().describe('Field value'),
}),
handler: async (args: any) => {
const response = await client.setCustomFieldValue(args.task_id, args.field_id, args.value);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tasks_add_dependency',
description: 'Add a task dependency',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
depends_on: z.string().optional().describe('Task this depends on'),
dependency_of: z.string().optional().describe('Task that depends on this'),
}),
handler: async (args: any) => {
const response = await client.addDependency(args.task_id, args.depends_on, args.dependency_of);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tasks_remove_dependency',
description: 'Remove a task dependency',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
depends_on: z.string().optional().describe('Task this depends on'),
dependency_of: z.string().optional().describe('Task that depends on this'),
}),
handler: async (args: any) => {
const response = await client.deleteDependency(args.task_id, args.depends_on, args.dependency_of);
return { content: [{ type: 'text', text: 'Dependency removed' }] };
}
},
{
name: 'clickup_tasks_list_members',
description: 'List members assigned to a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
}),
handler: async (args: any) => {
const response = await client.getTaskMembers(args.task_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tasks_add_comment',
description: 'Add a comment to a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
comment_text: z.string().describe('Comment text'),
assignee: z.number().optional().describe('Assign comment to user ID'),
notify_all: z.boolean().optional().describe('Notify all task watchers'),
}),
handler: async (args: any) => {
const { task_id, ...data } = args;
const response = await client.createComment(task_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_tasks_get_comments',
description: 'Get comments for a task',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
}),
handler: async (args: any) => {
const response = await client.getTaskComments(args.task_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
];
}

View File

@ -0,0 +1,56 @@
/**
* ClickUp Teams/Workspaces Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createTeamsTools(client: ClickUpClient) {
return [
{
name: 'clickup_teams_list_workspaces',
description: 'List all authorized workspaces/teams',
inputSchema: z.object({}),
handler: async (args: any) => {
const response = await client.getAuthorizedTeams();
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_teams_get_workspace',
description: 'Get workspace/team details',
inputSchema: z.object({
team_id: z.string().describe('Team/Workspace ID'),
}),
handler: async (args: any) => {
const response = await client.getTeam(args.team_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_teams_list_members',
description: 'List members of a list',
inputSchema: z.object({
list_id: z.string().describe('List ID'),
}),
handler: async (args: any) => {
const response = await client.getListMembers(args.list_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_teams_get_member',
description: 'Get task member details',
inputSchema: z.object({
task_id: z.string().describe('Task ID'),
}),
handler: async (args: any) => {
const response = await client.getTaskMembers(args.task_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
];
}

View File

@ -0,0 +1,37 @@
/**
* ClickUp Templates Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createTemplatesTools(client: ClickUpClient) {
return [
{
name: 'clickup_templates_list',
description: 'List available task templates',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
page: z.number().optional().describe('Page number'),
}),
handler: async (args: any) => {
const response = await client.getTemplates(args.team_id, args.page);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_templates_apply',
description: 'Create a task from a template',
inputSchema: z.object({
list_id: z.string().describe('List ID'),
template_id: z.string().describe('Template ID'),
name: z.string().describe('Task name'),
}),
handler: async (args: any) => {
const response = await client.createTaskFromTemplate(args.list_id, args.template_id, args.name);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
];
}

View File

@ -0,0 +1,141 @@
/**
* ClickUp Time Tracking Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createTimeTrackingTools(client: ClickUpClient) {
return [
{
name: 'clickup_time_list_entries',
description: 'List time entries in a team',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
start_date: z.number().optional().describe('Start date filter (Unix timestamp in milliseconds)'),
end_date: z.number().optional().describe('End date filter (Unix timestamp in milliseconds)'),
assignee: z.number().optional().describe('Filter by assignee user ID'),
include_task_tags: z.boolean().optional().describe('Include task tags'),
include_location_names: z.boolean().optional().describe('Include location names'),
space_id: z.string().optional().describe('Filter by space ID'),
folder_id: z.string().optional().describe('Filter by folder ID'),
list_id: z.string().optional().describe('Filter by list ID'),
task_id: z.string().optional().describe('Filter by task ID'),
}),
handler: async (args: any) => {
const { team_id, ...params } = args;
const response = await client.getTimeEntries(team_id, params);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_time_get_entry',
description: 'Get a specific time entry',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
timer_id: z.string().describe('Timer/Time Entry ID'),
}),
handler: async (args: any) => {
const response = await client.getTimeEntry(args.team_id, args.timer_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_time_create',
description: 'Create a time entry',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
task_id: z.string().describe('Task ID'),
duration: z.number().describe('Duration in milliseconds'),
start: z.number().describe('Start time (Unix timestamp in milliseconds)'),
description: z.string().optional().describe('Time entry description'),
billable: z.boolean().optional().describe('Is billable'),
assignee: z.number().optional().describe('Assignee user ID'),
tags: z.array(z.string()).optional().describe('Tag names'),
}),
handler: async (args: any) => {
const { team_id, task_id, ...data } = args;
data.tid = task_id;
const response = await client.createTimeEntry(team_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_time_update',
description: 'Update a time entry',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
timer_id: z.string().describe('Timer/Time Entry ID'),
description: z.string().optional().describe('Time entry description'),
billable: z.boolean().optional().describe('Is billable'),
start: z.number().optional().describe('Start time (Unix timestamp in milliseconds)'),
end: z.number().optional().describe('End time (Unix timestamp in milliseconds)'),
duration: z.number().optional().describe('Duration in milliseconds'),
assignee: z.number().optional().describe('Assignee user ID'),
tags: z.array(z.string()).optional().describe('Tag names'),
}),
handler: async (args: any) => {
const { team_id, timer_id, ...data } = args;
const response = await client.updateTimeEntry(team_id, timer_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_time_delete',
description: 'Delete a time entry',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
timer_id: z.string().describe('Timer/Time Entry ID'),
}),
handler: async (args: any) => {
await client.deleteTimeEntry(args.team_id, args.timer_id);
return { content: [{ type: 'text', text: 'Time entry deleted successfully' }] };
}
},
{
name: 'clickup_time_get_running',
description: 'Get running timer for a user',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
assignee: z.string().optional().describe('Assignee user ID (defaults to current user)'),
}),
handler: async (args: any) => {
const response = await client.getRunningTimeEntry(args.team_id, args.assignee);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_time_start',
description: 'Start a timer for a task',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
task_id: z.string().describe('Task ID'),
description: z.string().optional().describe('Timer description'),
billable: z.boolean().optional().describe('Is billable'),
}),
handler: async (args: any) => {
const { team_id, task_id, ...data } = args;
const response = await client.startTimer(team_id, task_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_time_stop',
description: 'Stop the running timer',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
}),
handler: async (args: any) => {
const response = await client.stopTimer(args.team_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
];
}

View File

@ -0,0 +1,116 @@
/**
* ClickUp Views Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createViewsTools(client: ClickUpClient) {
return [
{
name: 'clickup_views_list',
description: 'List all views in a workspace/space',
inputSchema: z.object({
team_id: z.string().describe('Team/Workspace ID'),
space_id: z.string().describe('Space ID'),
list_id: z.string().optional().describe('List ID'),
folder_id: z.string().optional().describe('Folder ID'),
}),
handler: async (args: any) => {
const response = await client.getViews(args.team_id, args.space_id, args.list_id, args.folder_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_views_get',
description: 'Get a specific view by ID',
inputSchema: z.object({
view_id: z.string().describe('View ID'),
}),
handler: async (args: any) => {
const response = await client.getView(args.view_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_views_get_tasks',
description: 'Get tasks from a view',
inputSchema: z.object({
view_id: z.string().describe('View ID'),
page: z.number().optional().describe('Page number'),
}),
handler: async (args: any) => {
const response = await client.getViewTasks(args.view_id, args.page);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_views_create',
description: 'Create a new view',
inputSchema: z.object({
team_id: z.string().describe('Team/Workspace ID'),
space_id: z.string().describe('Space ID'),
name: z.string().describe('View name'),
type: z.string().describe('View type (list, board, calendar, gantt, etc.)'),
parent: z.object({
id: z.string(),
type: z.number()
}).optional().describe('Parent object (list, folder, space)'),
grouping: z.object({
field: z.string(),
dir: z.number()
}).optional().describe('Grouping configuration'),
sorting: z.object({
fields: z.array(z.object({
field: z.string(),
dir: z.number()
}))
}).optional().describe('Sorting configuration'),
}),
handler: async (args: any) => {
const { team_id, space_id, ...data } = args;
const response = await client.createView(team_id, space_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_views_update',
description: 'Update a view',
inputSchema: z.object({
view_id: z.string().describe('View ID'),
name: z.string().optional().describe('View name'),
grouping: z.object({
field: z.string(),
dir: z.number()
}).optional().describe('Grouping configuration'),
sorting: z.object({
fields: z.array(z.object({
field: z.string(),
dir: z.number()
}))
}).optional().describe('Sorting configuration'),
}),
handler: async (args: any) => {
const { view_id, ...data } = args;
const response = await client.updateView(view_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_views_delete',
description: 'Delete a view',
inputSchema: z.object({
view_id: z.string().describe('View ID'),
}),
handler: async (args: any) => {
await client.deleteView(args.view_id);
return { content: [{ type: 'text', text: 'View deleted successfully' }] };
}
},
];
}

View File

@ -0,0 +1,69 @@
/**
* ClickUp Webhooks Tools
*/
import { z } from 'zod';
import type { ClickUpClient } from '../clients/clickup.js';
export function createWebhooksTools(client: ClickUpClient) {
return [
{
name: 'clickup_webhooks_list',
description: 'List all webhooks in a team',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
}),
handler: async (args: any) => {
const response = await client.getWebhooks(args.team_id);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_webhooks_create',
description: 'Create a webhook',
inputSchema: z.object({
team_id: z.string().describe('Team ID'),
endpoint: z.string().describe('Webhook endpoint URL'),
events: z.array(z.string()).describe('Event types (e.g., taskCreated, taskUpdated, taskDeleted)'),
space_id: z.string().optional().describe('Filter to space ID'),
folder_id: z.string().optional().describe('Filter to folder ID'),
list_id: z.string().optional().describe('Filter to list ID'),
task_id: z.string().optional().describe('Filter to task ID'),
}),
handler: async (args: any) => {
const { team_id, ...data } = args;
const response = await client.createWebhook(team_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_webhooks_update',
description: 'Update a webhook',
inputSchema: z.object({
webhook_id: z.string().describe('Webhook ID'),
endpoint: z.string().optional().describe('Webhook endpoint URL'),
events: z.array(z.string()).optional().describe('Event types'),
status: z.string().optional().describe('Status (active, disabled)'),
}),
handler: async (args: any) => {
const { webhook_id, ...data } = args;
const response = await client.updateWebhook(webhook_id, data);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}
},
{
name: 'clickup_webhooks_delete',
description: 'Delete a webhook',
inputSchema: z.object({
webhook_id: z.string().describe('Webhook ID'),
}),
handler: async (args: any) => {
await client.deleteWebhook(args.webhook_id);
return { content: [{ type: 'text', text: 'Webhook deleted successfully' }] };
}
},
];
}

View File

@ -0,0 +1,596 @@
/**
* ClickUp API Types
* Based on ClickUp API v2: https://clickup.com/api
*/
// ===== Core Types =====
export interface ClickUpConfig {
apiToken?: string;
clientId?: string;
clientSecret?: string;
oauthToken?: string;
}
export interface ClickUpUser {
id: number;
username: string;
email: string;
color: string;
profilePicture: string | null;
initials: string;
role: number;
custom_role: number | null;
last_active: string;
date_joined: string;
date_invited: string;
}
export interface ClickUpTeam {
id: string;
name: string;
color: string;
avatar: string | null;
members: ClickUpUser[];
}
// ===== Task Types =====
export interface ClickUpTask {
id: string;
custom_id: string | null;
name: string;
text_content: string;
description: string;
status: ClickUpStatus;
orderindex: string;
date_created: string;
date_updated: string;
date_closed: string | null;
date_done: string | null;
archived: boolean;
creator: ClickUpUser;
assignees: ClickUpUser[];
watchers: ClickUpUser[];
checklists: ClickUpChecklist[];
tags: ClickUpTag[];
parent: string | null;
priority: ClickUpPriority | null;
due_date: string | null;
start_date: string | null;
points: number | null;
time_estimate: number | null;
time_spent: number | null;
custom_fields: ClickUpCustomFieldValue[];
dependencies: ClickUpTaskDependency[];
linked_tasks: ClickUpLinkedTask[];
team_id: string;
url: string;
permission_level: string;
list: ClickUpListReference;
project: ClickUpFolderReference;
folder: ClickUpFolderReference;
space: ClickUpSpaceReference;
}
export interface ClickUpStatus {
id: string;
status: string;
color: string;
orderindex: number;
type: string;
}
export interface ClickUpPriority {
id: string;
priority: string;
color: string;
orderindex: string;
}
export interface ClickUpTaskDependency {
task_id: string;
depends_on: string;
type: number;
date_created: string;
user: ClickUpUser;
}
export interface ClickUpLinkedTask {
task_id: string;
link_id: string;
date_created: string;
user: ClickUpUser;
}
// ===== Space Types =====
export interface ClickUpSpace {
id: string;
name: string;
private: boolean;
statuses: ClickUpStatus[];
multiple_assignees: boolean;
features: ClickUpSpaceFeatures;
archived: boolean;
}
export interface ClickUpSpaceFeatures {
due_dates: { enabled: boolean; start_date: boolean; remap_due_dates: boolean; remap_closed_due_date: boolean };
time_tracking: { enabled: boolean };
tags: { enabled: boolean };
time_estimates: { enabled: boolean };
checklists: { enabled: boolean };
custom_fields: { enabled: boolean };
remap_dependencies: { enabled: boolean };
dependency_warning: { enabled: boolean };
portfolios: { enabled: boolean };
}
export interface ClickUpSpaceReference {
id: string;
name: string;
access: boolean;
}
// ===== Folder Types =====
export interface ClickUpFolder {
id: string;
name: string;
orderindex: number;
override_statuses: boolean;
hidden: boolean;
space: ClickUpSpaceReference;
task_count: string;
archived: boolean;
statuses: ClickUpStatus[];
lists: ClickUpList[];
permission_level: string;
}
export interface ClickUpFolderReference {
id: string;
name: string;
hidden: boolean;
access: boolean;
}
// ===== List Types =====
export interface ClickUpList {
id: string;
name: string;
orderindex: number;
status: ClickUpStatus | null;
priority: ClickUpPriority | null;
assignee: ClickUpUser | null;
task_count: number;
due_date: string | null;
start_date: string | null;
folder: ClickUpFolderReference;
space: ClickUpSpaceReference;
archived: boolean;
override_statuses: boolean;
statuses: ClickUpStatus[];
permission_level: string;
}
export interface ClickUpListReference {
id: string;
name: string;
access: boolean;
}
// ===== View Types =====
export interface ClickUpView {
id: string;
name: string;
type: string;
parent: {
id: string;
type: number;
};
grouping: {
field: string;
dir: number;
};
divide: {
field: string | null;
dir: number | null;
};
sorting: {
fields: Array<{
field: string;
dir: number;
}>;
};
filters: {
op: string;
fields: any[];
};
columns: {
fields: any[];
};
team_sidebar: {
assignees: any[];
assigned_comments: boolean;
unassigned_tasks: boolean;
};
settings: {
show_task_locations: boolean;
show_subtasks: number;
show_subtask_parent_names: boolean;
show_closed_subtasks: boolean;
show_assignees: boolean;
show_images: boolean;
collapse_empty_columns: boolean | null;
me_comments: boolean;
me_subtasks: boolean;
me_checklists: boolean;
};
}
// ===== Comment Types =====
export interface ClickUpComment {
id: string;
comment: Array<{
text: string;
}>;
comment_text: string;
user: ClickUpUser;
resolved: boolean;
assignee: ClickUpUser | null;
assigned_by: ClickUpUser | null;
reactions: ClickUpReaction[];
date: string;
}
export interface ClickUpReaction {
reaction: string;
users: ClickUpUser[];
}
// ===== Doc Types =====
export interface ClickUpDoc {
id: string;
name: string;
type: string;
parent: {
id: string;
type: number;
};
date_created: string;
date_updated: string;
creator: ClickUpUser;
deleted: boolean;
content: string;
}
// ===== Goal Types =====
export interface ClickUpGoal {
id: string;
name: string;
team_id: string;
date_created: string;
start_date: string | null;
due_date: string | null;
description: string;
private: boolean;
archived: boolean;
creator: ClickUpUser;
color: string;
pretty_id: string;
multiple_owners: boolean;
folder_id: string | null;
members: ClickUpUser[];
owners: ClickUpUser[];
key_results: ClickUpKeyResult[];
percent_completed: number;
history: any[];
pretty_url: string;
}
export interface ClickUpKeyResult {
id: string;
goal_id: string;
name: string;
creator: ClickUpUser;
type: string;
unit: string | null;
steps_start: number;
steps_end: number;
steps_current: number;
percent_completed: number;
task_ids: string[];
list_ids: string[];
subcategory_ids: string[];
owners: ClickUpUser[];
}
// ===== Tag Types =====
export interface ClickUpTag {
name: string;
tag_fg: string;
tag_bg: string;
creator: number;
}
// ===== Checklist Types =====
export interface ClickUpChecklist {
id: string;
task_id: string;
name: string;
orderindex: number;
resolved: number;
unresolved: number;
items: ClickUpChecklistItem[];
}
export interface ClickUpChecklistItem {
id: string;
name: string;
orderindex: number;
assignee: ClickUpUser | null;
resolved: boolean;
parent: string | null;
date_created: string;
children: string[];
}
// ===== Time Tracking Types =====
export interface ClickUpTimeEntry {
id: string;
task: {
id: string;
name: string;
status: ClickUpStatus;
custom_type: any;
};
wid: string;
user: ClickUpUser;
billable: boolean;
start: string;
end: string | null;
duration: string;
description: string;
tags: ClickUpTag[];
source: string;
at: string;
}
// ===== Custom Field Types =====
export interface ClickUpCustomField {
id: string;
name: string;
type: string;
type_config: any;
date_created: string;
hide_from_guests: boolean;
required: boolean;
}
export interface ClickUpCustomFieldValue {
id: string;
name: string;
type: string;
type_config: any;
date_created: string;
hide_from_guests: boolean;
value: any;
required: boolean;
}
// ===== Webhook Types =====
export interface ClickUpWebhook {
id: string;
userid: number;
team_id: number;
endpoint: string;
client_id: string;
events: string[];
task_id: string | null;
list_id: string | null;
folder_id: string | null;
space_id: string | null;
health: {
status: string;
fail_count: number;
};
secret: string;
}
// ===== Template Types =====
export interface ClickUpTemplate {
id: string;
name: string;
}
// ===== Guest Types =====
export interface ClickUpGuest {
user: ClickUpUser;
invited_by: ClickUpUser;
}
// ===== Workspace Types =====
export interface ClickUpWorkspace {
id: string;
name: string;
color: string;
avatar: string | null;
members: ClickUpUser[];
}
// ===== API Response Types =====
export interface ClickUpListResponse<T> {
data: T[];
last_page?: boolean;
}
export interface ClickUpTasksResponse {
tasks: ClickUpTask[];
last_page?: boolean;
}
export interface ClickUpSpacesResponse {
spaces: ClickUpSpace[];
}
export interface ClickUpFoldersResponse {
folders: ClickUpFolder[];
}
export interface ClickUpListsResponse {
lists: ClickUpList[];
}
export interface ClickUpViewsResponse {
views: ClickUpView[];
}
export interface ClickUpCommentsResponse {
comments: ClickUpComment[];
}
export interface ClickUpGoalsResponse {
goals: ClickUpGoal[];
}
export interface ClickUpTimeEntriesResponse {
data: ClickUpTimeEntry[];
}
export interface ClickUpWebhooksResponse {
webhooks: ClickUpWebhook[];
}
// ===== Error Types =====
export interface ClickUpError {
err: string;
ECODE: string;
}
// ===== API Request Types =====
export interface CreateTaskRequest {
name: string;
description?: string;
assignees?: number[];
tags?: string[];
status?: string;
priority?: number;
due_date?: number;
due_date_time?: boolean;
time_estimate?: number;
start_date?: number;
start_date_time?: boolean;
notify_all?: boolean;
parent?: string;
links_to?: string;
check_required_custom_fields?: boolean;
custom_fields?: Array<{
id: string;
value: any;
}>;
}
export interface UpdateTaskRequest {
name?: string;
description?: string;
status?: string;
priority?: number;
due_date?: number;
due_date_time?: boolean;
time_estimate?: number;
start_date?: number;
start_date_time?: boolean;
assignees?: {
add?: number[];
rem?: number[];
};
archived?: boolean;
}
export interface CreateSpaceRequest {
name: string;
multiple_assignees?: boolean;
features?: Partial<ClickUpSpaceFeatures>;
}
export interface CreateFolderRequest {
name: string;
}
export interface CreateListRequest {
name: string;
content?: string;
due_date?: number;
due_date_time?: boolean;
priority?: number;
assignee?: number;
status?: string;
}
export interface CreateCommentRequest {
comment_text: string;
assignee?: number;
notify_all?: boolean;
}
export interface CreateGoalRequest {
name: string;
due_date?: number;
description?: string;
multiple_owners?: boolean;
owners?: number[];
color?: string;
}
export interface CreateKeyResultRequest {
name: string;
owners?: number[];
type: string;
steps_start?: number;
steps_end?: number;
unit?: string;
task_ids?: string[];
list_ids?: string[];
}
export interface CreateTimeEntryRequest {
description?: string;
tags?: string[];
start: number;
billable?: boolean;
duration: number;
assignee?: number;
tid?: string;
}
export interface CreateWebhookRequest {
endpoint: string;
events: string[];
}
export interface CreateChecklistRequest {
name: string;
}
export interface CreateChecklistItemRequest {
name: string;
assignee?: number;
}

View File

@ -1,14 +1,20 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "Node16",
"moduleResolution": "NodeNext", "moduleResolution": "Node16",
"lib": ["ES2022"],
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src",
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"declaration": true "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"jsx": "react-jsx"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]