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"] +}