From 00542733366aa460ee14bf8edd5b42ac749d72b9 Mon Sep 17 00:00:00 2001 From: Manik Date: Mon, 23 Mar 2026 16:23:09 +0530 Subject: [PATCH] feat: add Calendly integration Adds full Calendly integration with OAuth2 auth and 31 MCP tools covering identity, event types, scheduled events, invitees, scheduling links, webhooks, routing forms, groups, org invitations, and availability schedules. Co-Authored-By: Claude Sonnet 4.6 --- integrations/calendly/.prettierrc | 10 + integrations/calendly/package.json | 42 ++ integrations/calendly/scripts/register.ts | 75 ++ integrations/calendly/src/account-create.ts | 45 ++ integrations/calendly/src/create-activity.ts | 85 +++ integrations/calendly/src/index.ts | 84 +++ integrations/calendly/src/mcp/index.ts | 702 +++++++++++++++++++ integrations/calendly/src/utils.ts | 19 + integrations/calendly/tsconfig.json | 31 + 9 files changed, 1093 insertions(+) create mode 100644 integrations/calendly/.prettierrc create mode 100644 integrations/calendly/package.json create mode 100644 integrations/calendly/scripts/register.ts create mode 100644 integrations/calendly/src/account-create.ts create mode 100644 integrations/calendly/src/create-activity.ts create mode 100644 integrations/calendly/src/index.ts create mode 100644 integrations/calendly/src/mcp/index.ts create mode 100644 integrations/calendly/src/utils.ts create mode 100644 integrations/calendly/tsconfig.json diff --git a/integrations/calendly/.prettierrc b/integrations/calendly/.prettierrc new file mode 100644 index 000000000..f063878d5 --- /dev/null +++ b/integrations/calendly/.prettierrc @@ -0,0 +1,10 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "always" +} diff --git a/integrations/calendly/package.json b/integrations/calendly/package.json new file mode 100644 index 000000000..1b1137b15 --- /dev/null +++ b/integrations/calendly/package.json @@ -0,0 +1,42 @@ +{ + "name": "@core/calendly", + "version": "0.1.0", + "description": "Calendly integration for CORE", + "main": "./bin/index.js", + "type": "module", + "files": [ + "calendly", + "bin" + ], + "bin": { + "calendly": "./bin/index.js" + }, + "scripts": { + "build": "rimraf bin && bun build src/index.ts --outfile dist/index.js --target node --minify", + "lint": "eslint --ext js,ts,tsx src/ --fix", + "prettier": "prettier --config .prettierrc --write ." + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "axios": "^1.7.9", + "commander": "^12.0.0", + "@redplanethq/sdk": "0.1.9", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.1" + }, + "devDependencies": { + "@types/node": "^18.0.20", + "eslint": "^9.24.0", + "eslint-config-prettier": "^10.1.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-unused-imports": "^2.0.0", + "prettier": "^3.4.2", + "rimraf": "^3.0.2", + "tslib": "^2.8.1", + "tsx": "4.20.6", + "typescript": "^4.7.2" + } +} diff --git a/integrations/calendly/scripts/register.ts b/integrations/calendly/scripts/register.ts new file mode 100644 index 000000000..5866de2d0 --- /dev/null +++ b/integrations/calendly/scripts/register.ts @@ -0,0 +1,75 @@ +import pg from 'pg'; + +const { Client } = pg; + +async function main() { + const connectionString = process.env.DATABASE_URL; + if (!connectionString) { + console.error('DATABASE_URL environment variable is required'); + process.exit(1); + } + + const clientId = process.env.CALENDLY_CLIENT_ID; + const clientSecret = process.env.CALENDLY_CLIENT_SECRET; + + if (!clientId || !clientSecret) { + console.error( + 'CALENDLY_CLIENT_ID and CALENDLY_CLIENT_SECRET environment variables are required', + ); + process.exit(1); + } + + const client = new Client({ connectionString }); + + const spec = { + name: 'Calendly', + key: 'calendly', + description: + 'Connect your Calendly account to view and manage event types, scheduled meetings, invitees, availability, routing forms, and webhook subscriptions in CORE.', + icon: 'calendly', + auth: { + OAuth2: { + authorization_url: 'https://auth.calendly.com/oauth/authorize', + token_url: 'https://auth.calendly.com/oauth/token', + scopes: ['default'], + scope_separator: ' ', + token_request_auth_method: 'basic', + }, + }, + }; + + try { + await client.connect(); + + await client.query( + ` + INSERT INTO core."IntegrationDefinitionV2" ("id", "name", "slug", "description", "icon", "spec", "config", "version", "url", "updatedAt", "createdAt") + VALUES (gen_random_uuid(), 'Calendly', 'calendly', $4, 'calendly', $1, $2, '0.1.0', $3, NOW(), NOW()) + ON CONFLICT (name) DO UPDATE SET + "slug" = EXCLUDED."slug", + "description" = EXCLUDED."description", + "icon" = EXCLUDED."icon", + "spec" = EXCLUDED."spec", + "config" = EXCLUDED."config", + "version" = EXCLUDED."version", + "url" = EXCLUDED."url", + "updatedAt" = NOW() + RETURNING *; + `, + [ + JSON.stringify(spec), + JSON.stringify({ clientId, clientSecret }), + '../../integrations/calendly/dist/index.js', + spec.description, + ], + ); + + console.log('Calendly integration registered successfully in the database.'); + } catch (error) { + console.error('Error registering Calendly integration:', error); + } finally { + await client.end(); + } +} + +main().catch(console.error); diff --git a/integrations/calendly/src/account-create.ts b/integrations/calendly/src/account-create.ts new file mode 100644 index 000000000..4abf83fa7 --- /dev/null +++ b/integrations/calendly/src/account-create.ts @@ -0,0 +1,45 @@ +import { getCurrentUser } from './utils'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function integrationCreate(data: any) { + const { oauthResponse } = data; + + let userUri: string | null = null; + let orgUri: string | null = null; + let email: string | null = null; + let name: string | null = null; + + try { + const user = await getCurrentUser(oauthResponse.access_token); + userUri = user.uri; + orgUri = user.current_organization; + email = user.email; + name = user.name; + } catch (error) { + console.error('Error fetching Calendly user profile:', error); + } + + return [ + { + type: 'account', + data: { + settings: { + userUri, + orgUri, + name, + }, + accountId: email || userUri || 'calendly', + config: { + access_token: oauthResponse.access_token, + refresh_token: oauthResponse.refresh_token, + expires_in: oauthResponse.expires_in, + expires_at: oauthResponse.expires_at, + userUri, + orgUri, + email, + name, + }, + }, + }, + ]; +} diff --git a/integrations/calendly/src/create-activity.ts b/integrations/calendly/src/create-activity.ts new file mode 100644 index 000000000..45136bad7 --- /dev/null +++ b/integrations/calendly/src/create-activity.ts @@ -0,0 +1,85 @@ +interface CalendlyWebhookEvent { + event: string; + payload: { + event_type?: { name?: string }; + invitee?: { + name?: string; + email?: string; + cancel_url?: string; + reschedule_url?: string; + }; + scheduled_event?: { + name?: string; + start_time?: string; + end_time?: string; + uri?: string; + }; + form_submission?: { + questions_and_answers?: Array<{ question: string; answer: string }>; + }; + routing_form?: { name?: string }; + cancel_url?: string; + reschedule_url?: string; + }; +} + +function formatTime(iso: string | undefined): string { + if (!iso) return ''; + return new Date(iso).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short', + }); +} + +function describeEvent(webhookEvent: CalendlyWebhookEvent): string | null { + const { event, payload } = webhookEvent; + const invitee = payload.invitee; + const scheduled = payload.scheduled_event; + const eventTypeName = payload.event_type?.name || scheduled?.name || 'meeting'; + const inviteeName = invitee?.name || invitee?.email || 'someone'; + const startTime = scheduled?.start_time; + + switch (event) { + case 'invitee.created': + return `${inviteeName} booked "${eventTypeName}"${startTime ? ` on ${formatTime(startTime)}` : ''}`; + + case 'invitee.canceled': + return `${inviteeName} canceled "${eventTypeName}"${startTime ? ` (was ${formatTime(startTime)})` : ''}`; + + case 'routing_form_submission.created': { + const formName = payload.routing_form?.name || 'routing form'; + return `New submission on "${formName}"`; + } + + default: + return null; + } +} + +function getSourceUrl(webhookEvent: CalendlyWebhookEvent): string { + const { payload } = webhookEvent; + const uri = payload.scheduled_event?.uri; + if (uri) { + // Extract UUID from URI like https://api.calendly.com/scheduled_events/ + const uuid = uri.split('/').pop(); + return `https://calendly.com/scheduled_events/${uuid}`; + } + return 'https://calendly.com/app/scheduled_events'; +} + +export function createActivity(webhookEvent: CalendlyWebhookEvent) { + const text = describeEvent(webhookEvent); + if (!text) return null; + + return { + type: 'activity', + data: { + text, + sourceURL: getSourceUrl(webhookEvent), + }, + }; +} diff --git a/integrations/calendly/src/index.ts b/integrations/calendly/src/index.ts new file mode 100644 index 000000000..885a98785 --- /dev/null +++ b/integrations/calendly/src/index.ts @@ -0,0 +1,84 @@ +import { fileURLToPath } from 'url'; + +import { + IntegrationCLI, + IntegrationEventPayload, + IntegrationEventType, + Spec, +} from '@redplanethq/sdk'; + +import { integrationCreate } from './account-create'; +import { createActivity } from './create-activity'; +import { callTool, getTools } from './mcp'; + +export async function run(eventPayload: IntegrationEventPayload) { + switch (eventPayload.event) { + case IntegrationEventType.SETUP: + return await integrationCreate(eventPayload.eventBody); + + case IntegrationEventType.PROCESS: { + const event = eventPayload.eventBody?.eventData; + if (!event) return []; + const activity = createActivity(event); + return activity ? [activity] : []; + } + + case IntegrationEventType.GET_TOOLS: { + try { + return getTools(); + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e); + return { message: `Error ${message}` }; + } + } + + case IntegrationEventType.CALL_TOOL: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const config = eventPayload.config as any; + const { name, arguments: args } = eventPayload.eventBody; + return await callTool(name, args, config); + } + + default: + return { message: `The event payload type is ${eventPayload.event}` }; + } +} + +class CalendlyCLI extends IntegrationCLI { + constructor() { + super('calendly', '1.0.0'); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected async handleEvent(eventPayload: IntegrationEventPayload): Promise { + return await run(eventPayload); + } + + protected async getSpec(): Promise { + return { + name: 'Calendly', + key: 'calendly', + description: + 'Connect your Calendly account to view and manage event types, scheduled meetings, invitees, availability, routing forms, and webhook subscriptions in CORE.', + icon: 'calendly', + auth: { + OAuth2: { + authorization_url: 'https://auth.calendly.com/oauth/authorize', + token_url: 'https://auth.calendly.com/oauth/token', + scopes: ['default'], + scope_separator: ' ', + token_request_auth_method: 'basic', + }, + }, + }; + } +} + +function main() { + const calendlyCLI = new CalendlyCLI(); + calendlyCLI.parse(); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + main(); +} diff --git a/integrations/calendly/src/mcp/index.ts b/integrations/calendly/src/mcp/index.ts new file mode 100644 index 000000000..ace48a9ad --- /dev/null +++ b/integrations/calendly/src/mcp/index.ts @@ -0,0 +1,702 @@ +import axios from 'axios'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + +const CALENDLY_API_BASE = 'https://api.calendly.com'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function createClient(accessToken: string): any { + return axios.create({ + baseURL: CALENDLY_API_BASE, + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); +} + +function text(data: unknown) { + return [{ type: 'text', text: JSON.stringify(data, null, 2) }]; +} + +// ─── Common pagination + filter schemas ───────────────────────────────────── + +const PageSchema = z.object({ + count: z.number().optional().default(20).describe('Number of results per page (max 100)'), + page_token: z.string().optional().describe('Token for the next page of results'), +}); + +const UriSchema = z.object({ + uri: z.string().describe('Full Calendly resource URI (e.g. https://api.calendly.com/users/xxx)'), +}); + +// ─── Identity + Org ────────────────────────────────────────────────────────── + +const GetUserSchema = UriSchema; + +const GetOrganizationSchema = UriSchema; + +const ListOrgMembershipsSchema = PageSchema.extend({ + organization: z.string().describe('Organization URI'), + email: z.string().optional().describe('Filter by member email'), +}); + +// ─── Event Types ───────────────────────────────────────────────────────────── + +const ListEventTypesSchema = PageSchema.extend({ + user: z.string().optional().describe('User URI — required if organization is not set'), + organization: z.string().optional().describe('Organization URI — required if user is not set'), + active: z.boolean().optional().describe('Filter by active status'), + sort: z + .enum(['created_at:asc', 'created_at:desc', 'name:asc', 'name:desc']) + .optional() + .describe('Sort order'), +}); + +const GetEventTypeSchema = UriSchema; + +const GetEventTypeAvailabilitySchema = z.object({ + event_type_uri: z + .string() + .describe('Event type URI (e.g. https://api.calendly.com/event_types/xxx)'), + start_time: z.string().describe('Start of availability window (ISO 8601)'), + end_time: z.string().describe('End of availability window (ISO 8601)'), +}); + +const ListEventTypeAvailableTimesSchema = z.object({ + event_type: z.string().describe('Event type URI'), + start_time: z.string().describe('Start time (ISO 8601)'), + end_time: z.string().describe('End time (ISO 8601)'), +}); + +// ─── Scheduled Events ──────────────────────────────────────────────────────── + +const ListScheduledEventsSchema = PageSchema.extend({ + user: z.string().optional().describe('User URI — use this or organization, not both'), + organization: z + .string() + .optional() + .describe('Organization URI — use this or user, not both'), + status: z.enum(['active', 'canceled']).optional().describe('Filter by event status'), + min_start_time: z.string().optional().describe('Earliest start time filter (ISO 8601)'), + max_start_time: z.string().optional().describe('Latest start time filter (ISO 8601)'), + sort: z + .enum(['start_time:asc', 'start_time:desc']) + .optional() + .describe('Sort order (default: start_time:asc)'), + invitee_email: z.string().optional().describe('Filter by invitee email'), +}); + +const GetScheduledEventSchema = UriSchema; + +const CancelScheduledEventSchema = z.object({ + uuid: z.string().describe('UUID of the scheduled event to cancel'), + reason: z.string().optional().describe('Cancellation reason'), +}); + +// ─── Invitees ───────────────────────────────────────────────────────────────── + +const ListEventInviteesSchema = PageSchema.extend({ + uuid: z.string().describe('UUID of the scheduled event'), + status: z.enum(['active', 'canceled']).optional().describe('Filter by invitee status'), + sort: z + .enum(['created_at:asc', 'created_at:desc']) + .optional() + .describe('Sort order'), + email: z.string().optional().describe('Filter by invitee email'), +}); + +const GetEventInviteeSchema = z.object({ + event_uuid: z.string().describe('UUID of the scheduled event'), + invitee_uuid: z.string().describe('UUID of the invitee'), +}); + +// ─── Scheduling Links ───────────────────────────────────────────────────────── + +const CreateSchedulingLinkSchema = z.object({ + max_event_count: z.number().describe('Maximum number of events for this link'), + owner: z.string().describe('Owner URI (event type URI or user URI)'), + owner_type: z.enum(['EventType', 'User']).describe('Type of owner resource'), +}); + +const CreateSingleUseSchedulingLinkSchema = z.object({ + owner: z.string().describe('Event type URI this link is for'), + owner_type: z.enum(['EventType']).optional().default('EventType'), + max_event_count: z.number().optional().default(1).describe('Max events (default 1)'), +}); + +// ─── Webhooks ───────────────────────────────────────────────────────────────── + +const CalendlyWebhookEvents = [ + 'invitee.created', + 'invitee.canceled', + 'invitee_no_show.created', + 'invitee_no_show.deleted', + 'routing_form_submission.created', +] as const; + +const CreateWebhookSchema = z.object({ + url: z.string().url().describe('HTTPS URL to receive webhook POST requests'), + events: z + .array(z.enum(CalendlyWebhookEvents)) + .min(1) + .describe('List of events to subscribe to'), + organization: z.string().describe('Organization URI'), + user: z.string().optional().describe('User URI (for user-scoped webhooks)'), + scope: z.enum(['user', 'organization']).describe('Scope of the webhook subscription'), + signing_key: z + .string() + .optional() + .describe('Optional signing key to verify webhook payloads'), +}); + +const ListWebhooksSchema = PageSchema.extend({ + organization: z.string().describe('Organization URI'), + scope: z.enum(['user', 'organization']).describe('Webhook scope'), + user: z.string().optional().describe('User URI (required when scope is user)'), +}); + +const GetWebhookSchema = z.object({ + webhook_uuid: z.string().describe('UUID of the webhook subscription'), +}); + +const DeleteWebhookSchema = z.object({ + webhook_uuid: z.string().describe('UUID of the webhook subscription to delete'), +}); + +// ─── Routing Forms ──────────────────────────────────────────────────────────── + +const ListRoutingFormsSchema = PageSchema.extend({ + organization: z.string().describe('Organization URI'), + sort: z + .enum(['created_at:asc', 'created_at:desc']) + .optional() + .describe('Sort order'), +}); + +const GetRoutingFormSchema = z.object({ + form_uuid: z.string().describe('UUID of the routing form'), +}); + +const GetRoutingFormSubmissionSchema = z.object({ + submission_uuid: z.string().describe('UUID of the routing form submission'), +}); + +// ─── Groups ─────────────────────────────────────────────────────────────────── + +const ListGroupsSchema = PageSchema.extend({ + organization: z.string().describe('Organization URI'), +}); + +const GetGroupSchema = z.object({ + group_uuid: z.string().describe('UUID of the group'), +}); + +// ─── Org Invitations ────────────────────────────────────────────────────────── + +const ListOrgInvitationsSchema = PageSchema.extend({ + organization_uuid: z.string().describe('Organization UUID'), + status: z + .enum(['pending', 'accepted', 'declined', 'revoked']) + .optional() + .describe('Filter by invitation status'), + email: z.string().optional().describe('Filter by invitee email'), +}); + +const CreateOrgInvitationSchema = z.object({ + organization_uuid: z.string().describe('Organization UUID'), + email: z.string().email().describe('Email address of the person to invite'), +}); + +const RevokeOrgInvitationSchema = z.object({ + organization_uuid: z.string().describe('Organization UUID'), + invitation_uuid: z.string().describe('UUID of the invitation to revoke'), +}); + +// ─── Availability Schedules ─────────────────────────────────────────────────── + +const ListAvailabilitySchedulesSchema = z.object({ + user: z.string().describe('User URI'), +}); + +const GetAvailabilityScheduleSchema = z.object({ + uuid: z.string().describe('UUID of the availability schedule'), +}); + +// ─── User Busy Times ────────────────────────────────────────────────────────── + +const ListUserBusyTimesSchema = z.object({ + user: z.string().describe('User URI'), + start_time: z.string().describe('Start of the time range (ISO 8601)'), + end_time: z.string().describe('End of the time range (ISO 8601)'), +}); + +// ─── Tool definitions ──────────────────────────────────────────────────────── + +export function getTools() { + return [ + // Identity + Org + { + name: 'get_current_user', + description: 'Get the authenticated Calendly user profile', + inputSchema: zodToJsonSchema(z.object({})), + }, + { + name: 'get_user', + description: 'Get a Calendly user by URI', + inputSchema: zodToJsonSchema(GetUserSchema), + }, + { + name: 'get_organization', + description: 'Get a Calendly organization by URI', + inputSchema: zodToJsonSchema(GetOrganizationSchema), + }, + { + name: 'list_organization_memberships', + description: 'List members of a Calendly organization', + inputSchema: zodToJsonSchema(ListOrgMembershipsSchema), + }, + // Event Types + { + name: 'list_event_types', + description: + 'List event types for a user or organization. Provide exactly one of user or organization.', + inputSchema: zodToJsonSchema(ListEventTypesSchema), + }, + { + name: 'get_event_type', + description: 'Get a specific event type by URI', + inputSchema: zodToJsonSchema(GetEventTypeSchema), + }, + { + name: 'get_event_type_availability', + description: 'Get available times for a specific event type within a date range', + inputSchema: zodToJsonSchema(GetEventTypeAvailabilitySchema), + }, + { + name: 'list_event_type_available_times', + description: 'List available meeting slots for an event type within a time range', + inputSchema: zodToJsonSchema(ListEventTypeAvailableTimesSchema), + }, + // Scheduled Events + { + name: 'list_scheduled_events', + description: + 'List scheduled events for a user or organization. Provide exactly one of user or organization.', + inputSchema: zodToJsonSchema(ListScheduledEventsSchema), + }, + { + name: 'get_scheduled_event', + description: 'Get a specific scheduled event by URI', + inputSchema: zodToJsonSchema(GetScheduledEventSchema), + }, + { + name: 'cancel_scheduled_event', + description: 'Cancel a scheduled event by UUID', + inputSchema: zodToJsonSchema(CancelScheduledEventSchema), + }, + // Invitees + { + name: 'list_event_invitees', + description: 'List invitees for a scheduled event', + inputSchema: zodToJsonSchema(ListEventInviteesSchema), + }, + { + name: 'get_event_invitee', + description: 'Get a specific invitee for a scheduled event', + inputSchema: zodToJsonSchema(GetEventInviteeSchema), + }, + // Scheduling Links + { + name: 'create_scheduling_link', + description: 'Create a scheduling link for an event type or user', + inputSchema: zodToJsonSchema(CreateSchedulingLinkSchema), + }, + { + name: 'create_single_use_scheduling_link', + description: 'Create a single-use scheduling link for an event type', + inputSchema: zodToJsonSchema(CreateSingleUseSchedulingLinkSchema), + }, + // Webhooks + { + name: 'create_webhook_subscription', + description: 'Create a webhook subscription to receive Calendly events', + inputSchema: zodToJsonSchema(CreateWebhookSchema), + }, + { + name: 'list_webhook_subscriptions', + description: 'List webhook subscriptions for an organization', + inputSchema: zodToJsonSchema(ListWebhooksSchema), + }, + { + name: 'get_webhook_subscription', + description: 'Get a specific webhook subscription by UUID', + inputSchema: zodToJsonSchema(GetWebhookSchema), + }, + { + name: 'delete_webhook_subscription', + description: 'Delete a webhook subscription by UUID', + inputSchema: zodToJsonSchema(DeleteWebhookSchema), + }, + // Routing Forms + { + name: 'list_routing_forms', + description: 'List routing forms for an organization', + inputSchema: zodToJsonSchema(ListRoutingFormsSchema), + }, + { + name: 'get_routing_form', + description: 'Get a specific routing form by UUID', + inputSchema: zodToJsonSchema(GetRoutingFormSchema), + }, + { + name: 'get_routing_form_submission', + description: 'Get a specific routing form submission by UUID', + inputSchema: zodToJsonSchema(GetRoutingFormSubmissionSchema), + }, + // Groups + { + name: 'list_groups', + description: 'List groups in a Calendly organization', + inputSchema: zodToJsonSchema(ListGroupsSchema), + }, + { + name: 'get_group', + description: 'Get a specific group by UUID', + inputSchema: zodToJsonSchema(GetGroupSchema), + }, + // Org Invitations + { + name: 'list_organization_invitations', + description: 'List invitations sent for a Calendly organization', + inputSchema: zodToJsonSchema(ListOrgInvitationsSchema), + }, + { + name: 'create_organization_invitation', + description: 'Invite someone to join a Calendly organization by email', + inputSchema: zodToJsonSchema(CreateOrgInvitationSchema), + }, + { + name: 'revoke_organization_invitation', + description: 'Revoke a pending organization invitation', + inputSchema: zodToJsonSchema(RevokeOrgInvitationSchema), + }, + // Availability + { + name: 'list_user_availability_schedules', + description: "List a user's availability schedules", + inputSchema: zodToJsonSchema(ListAvailabilitySchedulesSchema), + }, + { + name: 'get_user_availability_schedule', + description: 'Get a specific availability schedule by UUID', + inputSchema: zodToJsonSchema(GetAvailabilityScheduleSchema), + }, + { + name: 'list_user_busy_times', + description: "List a user's busy time blocks within a given time range", + inputSchema: zodToJsonSchema(ListUserBusyTimesSchema), + }, + ]; +} + +// ─── Tool execution ────────────────────────────────────────────────────────── + +export async function callTool( + name: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args: Record, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: Record, +) { + const accessToken = config?.access_token as string; + if (!accessToken) throw new Error('No access_token in config'); + + const client = createClient(accessToken); + + switch (name) { + // ── Identity + Org ─────────────────────────────────────────────────────── + case 'get_current_user': { + const res = await client.get('/users/me'); + return text(res.data.resource); + } + + case 'get_user': { + const { uri } = GetUserSchema.parse(args); + const uuid = uri.split('/').pop(); + const res = await client.get(`/users/${uuid}`); + return text(res.data.resource); + } + + case 'get_organization': { + const { uri } = GetOrganizationSchema.parse(args); + const uuid = uri.split('/').pop(); + const res = await client.get(`/organizations/${uuid}`); + return text(res.data.resource); + } + + case 'list_organization_memberships': { + const { organization, email, count, page_token } = + ListOrgMembershipsSchema.parse(args); + const params: Record = { organization, count }; + if (email) params.email = email; + if (page_token) params.page_token = page_token; + const res = await client.get('/organization_memberships', { params }); + return text(res.data); + } + + // ── Event Types ────────────────────────────────────────────────────────── + case 'list_event_types': { + const { user, organization, active, sort, count, page_token } = + ListEventTypesSchema.parse(args); + const params: Record = { count }; + if (user) params.user = user; + if (organization) params.organization = organization; + if (active !== undefined) params.active = active; + if (sort) params.sort = sort; + if (page_token) params.page_token = page_token; + const res = await client.get('/event_types', { params }); + return text(res.data); + } + + case 'get_event_type': { + const { uri } = GetEventTypeSchema.parse(args); + const uuid = uri.split('/').pop(); + const res = await client.get(`/event_types/${uuid}`); + return text(res.data.resource); + } + + case 'get_event_type_availability': { + const { event_type_uri, start_time, end_time } = + GetEventTypeAvailabilitySchema.parse(args); + const uuid = event_type_uri.split('/').pop(); + const res = await client.get(`/event_type_available_times`, { + params: { event_type: `https://api.calendly.com/event_types/${uuid}`, start_time, end_time }, + }); + return text(res.data); + } + + case 'list_event_type_available_times': { + const { event_type, start_time, end_time } = + ListEventTypeAvailableTimesSchema.parse(args); + const res = await client.get('/event_type_available_times', { + params: { event_type, start_time, end_time }, + }); + return text(res.data); + } + + // ── Scheduled Events ───────────────────────────────────────────────────── + case 'list_scheduled_events': { + const { + user, + organization, + status, + min_start_time, + max_start_time, + sort, + invitee_email, + count, + page_token, + } = ListScheduledEventsSchema.parse(args); + const params: Record = { count }; + if (user) params.user = user; + if (organization) params.organization = organization; + if (status) params.status = status; + if (min_start_time) params.min_start_time = min_start_time; + if (max_start_time) params.max_start_time = max_start_time; + if (sort) params.sort = sort; + if (invitee_email) params.invitee_email = invitee_email; + if (page_token) params.page_token = page_token; + const res = await client.get('/scheduled_events', { params }); + return text(res.data); + } + + case 'get_scheduled_event': { + const { uri } = GetScheduledEventSchema.parse(args); + const uuid = uri.split('/').pop(); + const res = await client.get(`/scheduled_events/${uuid}`); + return text(res.data.resource); + } + + case 'cancel_scheduled_event': { + const { uuid, reason } = CancelScheduledEventSchema.parse(args); + const body: Record = {}; + if (reason) body.reason = reason; + const res = await client.post(`/scheduled_events/${uuid}/cancellation`, body); + return text(res.data); + } + + // ── Invitees ───────────────────────────────────────────────────────────── + case 'list_event_invitees': { + const { uuid, status, sort, email, count, page_token } = + ListEventInviteesSchema.parse(args); + const params: Record = { count }; + if (status) params.status = status; + if (sort) params.sort = sort; + if (email) params.email = email; + if (page_token) params.page_token = page_token; + const res = await client.get(`/scheduled_events/${uuid}/invitees`, { params }); + return text(res.data); + } + + case 'get_event_invitee': { + const { event_uuid, invitee_uuid } = GetEventInviteeSchema.parse(args); + const res = await client.get( + `/scheduled_events/${event_uuid}/invitees/${invitee_uuid}`, + ); + return text(res.data.resource); + } + + // ── Scheduling Links ───────────────────────────────────────────────────── + case 'create_scheduling_link': { + const { max_event_count, owner, owner_type } = + CreateSchedulingLinkSchema.parse(args); + const res = await client.post('/scheduling_links', { + max_event_count, + owner, + owner_type, + }); + return text(res.data.resource); + } + + case 'create_single_use_scheduling_link': { + const { owner, owner_type, max_event_count } = + CreateSingleUseSchedulingLinkSchema.parse(args); + const res = await client.post('/scheduling_links', { + max_event_count, + owner, + owner_type, + }); + return text(res.data.resource); + } + + // ── Webhooks ───────────────────────────────────────────────────────────── + case 'create_webhook_subscription': { + const { url, events, organization, user, scope, signing_key } = + CreateWebhookSchema.parse(args); + const body: Record = { url, events, organization, scope }; + if (user) body.user = user; + if (signing_key) body.signing_key = signing_key; + const res = await client.post('/webhook_subscriptions', body); + return text(res.data.resource); + } + + case 'list_webhook_subscriptions': { + const { organization, scope, user, count, page_token } = + ListWebhooksSchema.parse(args); + const params: Record = { organization, scope, count }; + if (user) params.user = user; + if (page_token) params.page_token = page_token; + const res = await client.get('/webhook_subscriptions', { params }); + return text(res.data); + } + + case 'get_webhook_subscription': { + const { webhook_uuid } = GetWebhookSchema.parse(args); + const res = await client.get(`/webhook_subscriptions/${webhook_uuid}`); + return text(res.data.resource); + } + + case 'delete_webhook_subscription': { + const { webhook_uuid } = DeleteWebhookSchema.parse(args); + await client.delete(`/webhook_subscriptions/${webhook_uuid}`); + return text({ deleted: true, webhook_uuid }); + } + + // ── Routing Forms ──────────────────────────────────────────────────────── + case 'list_routing_forms': { + const { organization, sort, count, page_token } = + ListRoutingFormsSchema.parse(args); + const params: Record = { organization, count }; + if (sort) params.sort = sort; + if (page_token) params.page_token = page_token; + const res = await client.get('/routing_forms', { params }); + return text(res.data); + } + + case 'get_routing_form': { + const { form_uuid } = GetRoutingFormSchema.parse(args); + const res = await client.get(`/routing_forms/${form_uuid}`); + return text(res.data.resource); + } + + case 'get_routing_form_submission': { + const { submission_uuid } = GetRoutingFormSubmissionSchema.parse(args); + const res = await client.get(`/routing_form_submissions/${submission_uuid}`); + return text(res.data.resource); + } + + // ── Groups ─────────────────────────────────────────────────────────────── + case 'list_groups': { + const { organization, count, page_token } = ListGroupsSchema.parse(args); + const params: Record = { organization, count }; + if (page_token) params.page_token = page_token; + const res = await client.get('/groups', { params }); + return text(res.data); + } + + case 'get_group': { + const { group_uuid } = GetGroupSchema.parse(args); + const res = await client.get(`/groups/${group_uuid}`); + return text(res.data.resource); + } + + // ── Org Invitations ────────────────────────────────────────────────────── + case 'list_organization_invitations': { + const { organization_uuid, status, email, count, page_token } = + ListOrgInvitationsSchema.parse(args); + const params: Record = { count }; + if (status) params.status = status; + if (email) params.email = email; + if (page_token) params.page_token = page_token; + const res = await client.get( + `/organizations/${organization_uuid}/invitations`, + { params }, + ); + return text(res.data); + } + + case 'create_organization_invitation': { + const { organization_uuid, email } = CreateOrgInvitationSchema.parse(args); + const res = await client.post(`/organizations/${organization_uuid}/invitations`, { + email, + }); + return text(res.data.resource); + } + + case 'revoke_organization_invitation': { + const { organization_uuid, invitation_uuid } = + RevokeOrgInvitationSchema.parse(args); + await client.delete( + `/organizations/${organization_uuid}/invitations/${invitation_uuid}`, + ); + return text({ revoked: true, invitation_uuid }); + } + + // ── Availability ───────────────────────────────────────────────────────── + case 'list_user_availability_schedules': { + const { user } = ListAvailabilitySchedulesSchema.parse(args); + const res = await client.get('/user_availability_schedules', { + params: { user }, + }); + return text(res.data); + } + + case 'get_user_availability_schedule': { + const { uuid } = GetAvailabilityScheduleSchema.parse(args); + const res = await client.get(`/user_availability_schedules/${uuid}`); + return text(res.data.resource); + } + + case 'list_user_busy_times': { + const { user, start_time, end_time } = ListUserBusyTimesSchema.parse(args); + const res = await client.get('/user_busy_times', { + params: { user, start_time, end_time }, + }); + return text(res.data); + } + + default: + throw new Error(`Unknown tool: ${name}`); + } +} diff --git a/integrations/calendly/src/utils.ts b/integrations/calendly/src/utils.ts new file mode 100644 index 000000000..de37ac929 --- /dev/null +++ b/integrations/calendly/src/utils.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; + +export const CALENDLY_API_BASE = 'https://api.calendly.com'; + +export function createCalendlyClient(accessToken: string) { + return axios.create({ + baseURL: CALENDLY_API_BASE, + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); +} + +export async function getCurrentUser(accessToken: string) { + const client = createCalendlyClient(accessToken); + const res = await client.get('/users/me'); + return res.data.resource; +} diff --git a/integrations/calendly/tsconfig.json b/integrations/calendly/tsconfig.json new file mode 100644 index 000000000..830dcf58d --- /dev/null +++ b/integrations/calendly/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "outDir": "./dist", + "rootDir": "./src", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "allowSyntheticDefaultImports": true, + "module": "esnext", + "moduleResolution": "node", + "isolatedModules": true, + "strictNullChecks": true, + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "noFallthroughCasesInSwitch": true, + "useUnknownInCatchVariables": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "build", "dist", "bin"] +}