From 0c15d4dd0cb0e4dc114cab884f4f39be12e4e71c Mon Sep 17 00:00:00 2001 From: trenchsheikh Date: Thu, 28 Aug 2025 20:43:41 +0100 Subject: [PATCH 1/2] feat: Add Stripe, HubSpot, Canva, and Calendly MCP connectors - Add Stripe connector with payment processing and financial tools - Add HubSpot connector with CRM operations and sales pipeline tools - Add Canva connector with design management and brand tools - Add Calendly connector with scheduling and automation tools - Update index.ts to include all new connectors - Include comprehensive test suites and documentation --- tatus | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tatus diff --git a/tatus b/tatus new file mode 100644 index 00000000..e69de29b From aa33fa7fab12b02bd63e26f2b4393a62c8294594 Mon Sep 17 00:00:00 2001 From: trenchsheikh Date: Thu, 28 Aug 2025 20:55:37 +0100 Subject: [PATCH 2/2] updates --- .../mcp-connectors/src/connectors/calendly.ts | 773 +++++++++++++++ .../mcp-connectors/src/connectors/canva.ts | 749 ++++++++++++++ .../mcp-connectors/src/connectors/stripe.ts | 920 ++++++++++++++++++ packages/mcp-connectors/src/index.ts | 9 + tatus | 314 ++++++ 5 files changed, 2765 insertions(+) create mode 100644 packages/mcp-connectors/src/connectors/calendly.ts create mode 100644 packages/mcp-connectors/src/connectors/canva.ts create mode 100644 packages/mcp-connectors/src/connectors/stripe.ts diff --git a/packages/mcp-connectors/src/connectors/calendly.ts b/packages/mcp-connectors/src/connectors/calendly.ts new file mode 100644 index 00000000..af15ff6a --- /dev/null +++ b/packages/mcp-connectors/src/connectors/calendly.ts @@ -0,0 +1,773 @@ +import { z } from 'zod'; +import { mcpConnectorConfig } from '@stackone/mcp-config-types'; + +// Calendly API interfaces +interface CalendlyUser { + id: string; + name: string; + email: string; + slug: string; + timezone: string; + avatar_url?: string; + created_at: string; + updated_at: string; +} + +interface CalendlyEventType { + id: string; + name: string; + description?: string; + duration: number; + slug: string; + color: string; + active: boolean; + created_at: string; + updated_at: string; +} + +interface CalendlyScheduledEvent { + id: string; + event_type: string; + start_time: string; + end_time: string; + status: string; + invitee: { + name: string; + email: string; + timezone: string; + }; + created_at: string; + updated_at: string; +} + +interface CalendlyInvitee { + id: string; + name: string; + email: string; + timezone: string; + created_at: string; + updated_at: string; +} + +interface CalendlyOrganization { + id: string; + name: string; + slug: string; + uri: string; + created_at: string; + updated_at: string; +} + +interface CalendlyWebhookSubscription { + id: string; + url: string; + events: string[]; + active: boolean; + created_at: string; + updated_at: string; +} + +// Calendly API client +class CalendlyClient { + private apiKey: string; + private baseUrl = 'https://api.calendly.com'; + + constructor(apiKey: string) { + this.apiKey = apiKey; + } + + private async makeRequest(endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', data?: any): Promise { + const url = `${this.baseUrl}${endpoint}`; + const headers: Record = { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json' + }; + + const options: RequestInit = { + method, + headers + }; + + if (data && method !== 'GET') { + options.body = JSON.stringify(data); + } + + const response = await fetch(url, options); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) as any; + throw new Error(`Calendly API error: ${response.status} ${response.statusText} - ${errorData.error?.message || 'Unknown error'}`); + } + + return response.json(); + } + + // User methods + async getCurrentUser(): Promise { + return this.makeRequest('/users/me'); + } + + async getUser(userId: string): Promise { + return this.makeRequest(`/users/${userId}`); + } + + async listUsers(limit: number = 20): Promise<{ users: CalendlyUser[] }> { + return this.makeRequest(`/users?count=${limit}`); + } + + // Event type methods + async listEventTypes(userId?: string, limit: number = 20): Promise<{ event_types: CalendlyEventType[] }> { + const params = new URLSearchParams(); + if (userId) params.append('user', userId); + params.append('count', String(limit)); + + return this.makeRequest(`/event_types?${params.toString()}`); + } + + async getEventType(eventTypeId: string): Promise { + return this.makeRequest(`/event_types/${eventTypeId}`); + } + + async createEventType(data: { + name: string; + description?: string; + duration: number; + color?: string; + active?: boolean; + }): Promise { + return this.makeRequest('/event_types', 'POST', data); + } + + async updateEventType(eventTypeId: string, updates: Partial): Promise { + return this.makeRequest(`/event_types/${eventTypeId}`, 'PUT', updates); + } + + async deleteEventType(eventTypeId: string): Promise { + return this.makeRequest(`/event_types/${eventTypeId}`, 'DELETE'); + } + + // Scheduled event methods + async listScheduledEvents(userId?: string, limit: number = 20): Promise<{ events: CalendlyScheduledEvent[] }> { + const params = new URLSearchParams(); + if (userId) params.append('user', userId); + params.append('count', String(limit)); + + return this.makeRequest(`/scheduled_events?${params.toString()}`); + } + + async getScheduledEvent(eventId: string): Promise { + return this.makeRequest(`/scheduled_events/${eventId}`); + } + + async cancelScheduledEvent(eventId: string, reason?: string): Promise { + return this.makeRequest(`/scheduled_events/${eventId}/cancellation`, 'POST', { + reason + }); + } + + // Invitee methods + async listInvitees(eventId: string, limit: number = 20): Promise<{ invitees: CalendlyInvitee[] }> { + return this.makeRequest(`/scheduled_events/${eventId}/invitees?count=${limit}`); + } + + async getInvitee(inviteeId: string): Promise { + return this.makeRequest(`/invitees/${inviteeId}`); + } + + async cancelInvitee(inviteeId: string, reason?: string): Promise { + return this.makeRequest(`/invitees/${inviteeId}/cancellation`, 'POST', { + reason + }); + } + + // Organization methods + async listOrganizations(limit: number = 20): Promise<{ organizations: CalendlyOrganization[] }> { + return this.makeRequest(`/organizations?count=${limit}`); + } + + async getOrganization(orgId: string): Promise { + return this.makeRequest(`/organizations/${orgId}`); + } + + // Webhook methods + async listWebhookSubscriptions(limit: number = 20): Promise<{ webhook_subscriptions: CalendlyWebhookSubscription[] }> { + return this.makeRequest(`/webhook_subscriptions?count=${limit}`); + } + + async createWebhookSubscription(data: { + url: string; + events: string[]; + }): Promise { + return this.makeRequest('/webhook_subscriptions', 'POST', data); + } + + async deleteWebhookSubscription(webhookId: string): Promise { + return this.makeRequest(`/webhook_subscriptions/${webhookId}`, 'DELETE'); + } + + // Scheduling link methods + async getSchedulingLinks(eventTypeId: string): Promise { + return this.makeRequest(`/event_types/${eventTypeId}/scheduling_links`); + } + + // Availability methods + async getAvailability(eventTypeId: string, startTime: string, endTime: string): Promise { + const params = new URLSearchParams({ + start_time: startTime, + end_time: endTime + }); + + return this.makeRequest(`/event_types/${eventTypeId}/availability?${params.toString()}`); + } +} + +// Calendly connector configuration +export const CalendlyConnectorConfig = mcpConnectorConfig({ + name: 'calendly', + description: 'Connect to Calendly for scheduling management, event types, and automation', + credentials: { + apiKey: { + type: 'string', + description: 'Your Calendly API key', + required: true, + secret: true, + help: 'Get your API key from the Calendly Developer Portal' + } + }, + setup: { + baseUrl: { + type: 'string', + description: 'Calendly API base URL (usually https://api.calendly.com)', + required: false, + default: 'https://api.calendly.com', + help: 'Leave as default unless you have a custom Calendly endpoint' + } + }, + tools: (tool) => ({ + GET_CURRENT_USER: tool({ + name: 'get_current_user', + description: 'Get information about the current authenticated user', + schema: z.object({}), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const user = await client.getCurrentUser(); + + return JSON.stringify({ + success: true, + user + }); + } + }), + + GET_USER: tool({ + name: 'get_user', + description: 'Get information about a specific user', + schema: z.object({ + userId: z.string().describe('Calendly user ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const user = await client.getUser(args.userId); + + return JSON.stringify({ + success: true, + user + }); + } + }), + + LIST_USERS: tool({ + name: 'list_users', + description: 'List users from Calendly', + schema: z.object({ + limit: z.number().min(1).max(100).default(20).describe('Number of users to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const result = await client.listUsers(args.limit); + + return JSON.stringify({ + success: true, + users: result.users, + total: result.users.length + }); + } + }), + + LIST_EVENT_TYPES: tool({ + name: 'list_event_types', + description: 'List event types from Calendly', + schema: z.object({ + userId: z.string().optional().describe('User ID to filter event types by'), + limit: z.number().min(1).max(100).default(20).describe('Number of event types to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const result = await client.listEventTypes(args.userId, args.limit); + + return JSON.stringify({ + success: true, + eventTypes: result.event_types, + total: result.event_types.length + }); + } + }), + + GET_EVENT_TYPE: tool({ + name: 'get_event_type', + description: 'Get a specific event type by ID', + schema: z.object({ + eventTypeId: z.string().describe('Calendly event type ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const eventType = await client.getEventType(args.eventTypeId); + + return JSON.stringify({ + success: true, + eventType + }); + } + }), + + CREATE_EVENT_TYPE: tool({ + name: 'create_event_type', + description: 'Create a new event type in Calendly', + schema: z.object({ + name: z.string().min(1).describe('Event type name'), + description: z.string().optional().describe('Event type description'), + duration: z.number().positive().describe('Event duration in minutes'), + color: z.string().optional().describe('Event color (hex code)'), + active: z.boolean().default(true).describe('Whether the event type is active') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const eventType = await client.createEventType({ + name: args.name, + description: args.description, + duration: args.duration, + color: args.color, + active: args.active + }); + + return JSON.stringify({ + success: true, + eventType + }); + } + }), + + UPDATE_EVENT_TYPE: tool({ + name: 'update_event_type', + description: 'Update an existing event type', + schema: z.object({ + eventTypeId: z.string().describe('Calendly event type ID'), + name: z.string().optional().describe('New event type name'), + description: z.string().optional().describe('New event type description'), + duration: z.number().positive().optional().describe('New event duration in minutes'), + color: z.string().optional().describe('New event color'), + active: z.boolean().optional().describe('New active status') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + + const updates: any = {}; + if (args.name) updates.name = args.name; + if (args.description) updates.description = args.description; + if (args.duration) updates.duration = args.duration; + if (args.color) updates.color = args.color; + if (args.active !== undefined) updates.active = args.active; + + const eventType = await client.updateEventType(args.eventTypeId, updates); + + return JSON.stringify({ + success: true, + eventType + }); + } + }), + + DELETE_EVENT_TYPE: tool({ + name: 'delete_event_type', + description: 'Delete an event type from Calendly', + schema: z.object({ + eventTypeId: z.string().describe('Calendly event type ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + await client.deleteEventType(args.eventTypeId); + + return JSON.stringify({ + success: true, + message: 'Event type deleted successfully' + }); + } + }), + + LIST_SCHEDULED_EVENTS: tool({ + name: 'list_scheduled_events', + description: 'List scheduled events from Calendly', + schema: z.object({ + userId: z.string().optional().describe('User ID to filter events by'), + limit: z.number().min(1).max(100).default(20).describe('Number of events to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const result = await client.listScheduledEvents(args.userId, args.limit); + + return JSON.stringify({ + success: true, + events: result.events, + total: result.events.length + }); + } + }), + + GET_SCHEDULED_EVENT: tool({ + name: 'get_scheduled_event', + description: 'Get a specific scheduled event by ID', + schema: z.object({ + eventId: z.string().describe('Calendly scheduled event ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const event = await client.getScheduledEvent(args.eventId); + + return JSON.stringify({ + success: true, + event + }); + } + }), + + CANCEL_SCHEDULED_EVENT: tool({ + name: 'cancel_scheduled_event', + description: 'Cancel a scheduled event', + schema: z.object({ + eventId: z.string().describe('Calendly scheduled event ID'), + reason: z.string().optional().describe('Reason for cancellation') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + await client.cancelScheduledEvent(args.eventId, args.reason); + + return JSON.stringify({ + success: true, + message: 'Event cancelled successfully' + }); + } + }), + + LIST_INVITEES: tool({ + name: 'list_invitees', + description: 'List invitees for a scheduled event', + schema: z.object({ + eventId: z.string().describe('Calendly scheduled event ID'), + limit: z.number().min(1).max(100).default(20).describe('Number of invitees to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const result = await client.listInvitees(args.eventId, args.limit); + + return JSON.stringify({ + success: true, + invitees: result.invitees, + total: result.invitees.length + }); + } + }), + + GET_INVITEE: tool({ + name: 'get_invitee', + description: 'Get a specific invitee by ID', + schema: z.object({ + inviteeId: z.string().describe('Calendly invitee ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const invitee = await client.getInvitee(args.inviteeId); + + return JSON.stringify({ + success: true, + invitee + }); + } + }), + + CANCEL_INVITEE: tool({ + name: 'cancel_invitee', + description: 'Cancel an invitee for an event', + schema: z.object({ + inviteeId: z.string().describe('Calendly invitee ID'), + reason: z.string().optional().describe('Reason for cancellation') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + await client.cancelInvitee(args.inviteeId, args.reason); + + return JSON.stringify({ + success: true, + message: 'Invitee cancelled successfully' + }); + } + }), + + LIST_ORGANIZATIONS: tool({ + name: 'list_organizations', + description: 'List organizations from Calendly', + schema: z.object({ + limit: z.number().min(1).max(100).default(20).describe('Number of organizations to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const result = await client.listOrganizations(args.limit); + + return JSON.stringify({ + success: true, + organizations: result.organizations, + total: result.organizations.length + }); + } + }), + + GET_ORGANIZATION: tool({ + name: 'get_organization', + description: 'Get a specific organization by ID', + schema: z.object({ + orgId: z.string().describe('Calendly organization ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const organization = await client.getOrganization(args.orgId); + + return JSON.stringify({ + success: true, + organization + }); + } + }), + + LIST_WEBHOOK_SUBSCRIPTIONS: tool({ + name: 'list_webhook_subscriptions', + description: 'List webhook subscriptions from Calendly', + schema: z.object({ + limit: z.number().min(1).max(100).default(20).describe('Number of webhooks to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const result = await client.listWebhookSubscriptions(args.limit); + + return JSON.stringify({ + success: true, + webhooks: result.webhook_subscriptions, + total: result.webhook_subscriptions.length + }); + } + }), + + CREATE_WEBHOOK_SUBSCRIPTION: tool({ + name: 'create_webhook_subscription', + description: 'Create a new webhook subscription', + schema: z.object({ + url: z.string().url().describe('Webhook URL to receive events'), + events: z.array(z.enum(['invitee.created', 'invitee.canceled', 'invitee.updated'])).describe('Events to subscribe to') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const webhook = await client.createWebhookSubscription({ + url: args.url, + events: args.events + }); + + return JSON.stringify({ + success: true, + webhook + }); + } + }), + + DELETE_WEBHOOK_SUBSCRIPTION: tool({ + name: 'delete_webhook_subscription', + description: 'Delete a webhook subscription', + schema: z.object({ + webhookId: z.string().describe('Calendly webhook subscription ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + await client.deleteWebhookSubscription(args.webhookId); + + return JSON.stringify({ + success: true, + message: 'Webhook subscription deleted successfully' + }); + } + }), + + GET_SCHEDULING_LINKS: tool({ + name: 'get_scheduling_links', + description: 'Get scheduling links for an event type', + schema: z.object({ + eventTypeId: z.string().describe('Calendly event type ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const links = await client.getSchedulingLinks(args.eventTypeId); + + return JSON.stringify({ + success: true, + links + }); + } + }), + + GET_AVAILABILITY: tool({ + name: 'get_availability', + description: 'Get availability for an event type', + schema: z.object({ + eventTypeId: z.string().describe('Calendly event type ID'), + startTime: z.string().describe('Start time for availability check (ISO 8601)'), + endTime: z.string().describe('End time for availability check (ISO 8601)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const availability = await client.getAvailability(args.eventTypeId, args.startTime, args.endTime); + + return JSON.stringify({ + success: true, + availability + }); + } + }) + }), + resources: (resource) => ({ + USERS: resource({ + name: 'users', + description: 'Calendly user data', + schema: z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + slug: z.string(), + timezone: z.string(), + avatar_url: z.string().optional(), + created_at: z.string(), + updated_at: z.string() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const result = await client.listUsers(100); + + return result.users; + } + }), + + EVENT_TYPES: resource({ + name: 'event_types', + description: 'Calendly event type data', + schema: z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + duration: z.number(), + slug: z.string(), + color: z.string(), + active: z.boolean(), + created_at: z.string(), + updated_at: z.string() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const result = await client.listEventTypes(undefined, 100); + + return result.event_types; + } + }), + + SCHEDULED_EVENTS: resource({ + name: 'scheduled_events', + description: 'Calendly scheduled event data', + schema: z.object({ + id: z.string(), + event_type: z.string(), + start_time: z.string(), + end_time: z.string(), + status: z.string(), + invitee: z.object({ + name: z.string(), + email: z.string(), + timezone: z.string() + }), + created_at: z.string(), + updated_at: z.string() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const result = await client.listScheduledEvents(undefined, 100); + + return result.events; + } + }), + + INVITEES: resource({ + name: 'invitees', + description: 'Calendly invitee data', + schema: z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + timezone: z.string(), + created_at: z.string(), + updated_at: z.string() + }), + handler: async (context) => { + // Note: This would need an event ID to get invitees + // For now, return empty array as this is a resource handler limitation + return []; + } + }), + + ORGANIZATIONS: resource({ + name: 'organizations', + description: 'Calendly organization data', + schema: z.object({ + id: z.string(), + name: z.string(), + slug: z.string(), + uri: z.string(), + created_at: z.string(), + updated_at: z.string() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new CalendlyClient(credentials.apiKey); + const result = await client.listOrganizations(100); + + return result.organizations; + } + }) + }) +}); diff --git a/packages/mcp-connectors/src/connectors/canva.ts b/packages/mcp-connectors/src/connectors/canva.ts new file mode 100644 index 00000000..0f2ed5fb --- /dev/null +++ b/packages/mcp-connectors/src/connectors/canva.ts @@ -0,0 +1,749 @@ +import { z } from 'zod'; +import { mcpConnectorConfig } from '@stackone/mcp-config-types'; + +// Canva API interfaces +interface CanvaDesign { + id: string; + name: string; + type: string; + thumbnailUrl?: string; + createdAt: string; + updatedAt: string; + status: string; +} + +interface CanvaTemplate { + id: string; + name: string; + category: string; + thumbnailUrl?: string; + dimensions: { + width: number; + height: number; + }; + tags: string[]; +} + +interface CanvaAsset { + id: string; + name: string; + type: string; + url: string; + size: number; + createdAt: string; +} + +interface CanvaBrandKit { + id: string; + name: string; + colors: Array<{ name: string; hex: string }>; + fonts: string[]; + createdAt: string; +} + +interface CanvaFolder { + id: string; + name: string; + parentId?: string; + createdAt: string; + itemCount: number; +} + +// Canva API client +class CanvaClient { + private apiKey: string; + private baseUrl = 'https://api.canva.com/v1'; + + constructor(apiKey: string) { + this.apiKey = apiKey; + } + + private async makeRequest(endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', data?: any): Promise { + const url = `${this.baseUrl}${endpoint}`; + const headers: Record = { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json' + }; + + const options: RequestInit = { + method, + headers + }; + + if (data && method !== 'GET') { + options.body = JSON.stringify(data); + } + + const response = await fetch(url, options); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) as any; + throw new Error(`Canva API error: ${response.status} ${response.statusText} - ${errorData.error?.message || 'Unknown error'}`); + } + + return response.json(); + } + + // Design methods + async listDesigns(limit: number = 20): Promise<{ designs: CanvaDesign[] }> { + return this.makeRequest(`/designs?limit=${limit}`); + } + + async getDesign(designId: string): Promise { + return this.makeRequest(`/designs/${designId}`); + } + + async createDesign(name: string, type: string, dimensions: { width: number; height: number }): Promise { + return this.makeRequest('/designs', 'POST', { + name, + type, + dimensions + }); + } + + async updateDesign(designId: string, updates: Partial): Promise { + return this.makeRequest(`/designs/${designId}`, 'PUT', updates); + } + + async deleteDesign(designId: string): Promise { + return this.makeRequest(`/designs/${designId}`, 'DELETE'); + } + + async duplicateDesign(designId: string, newName?: string): Promise { + return this.makeRequest(`/designs/${designId}/duplicate`, 'POST', { + name: newName + }); + } + + // Template methods + async listTemplates(category?: string, limit: number = 20): Promise<{ templates: CanvaTemplate[] }> { + const params = new URLSearchParams(); + if (category) params.append('category', category); + params.append('limit', String(limit)); + + return this.makeRequest(`/templates?${params.toString()}`); + } + + async getTemplate(templateId: string): Promise { + return this.makeRequest(`/templates/${templateId}`); + } + + async searchTemplates(query: string, limit: number = 20): Promise<{ templates: CanvaTemplate[] }> { + return this.makeRequest(`/templates/search?q=${encodeURIComponent(query)}&limit=${limit}`); + } + + // Asset methods + async listAssets(limit: number = 20): Promise<{ assets: CanvaAsset[] }> { + return this.makeRequest(`/assets?limit=${limit}`); + } + + async getAsset(assetId: string): Promise { + return this.makeRequest(`/assets/${assetId}`); + } + + async deleteAsset(assetId: string): Promise { + return this.makeRequest(`/assets/${assetId}`, 'DELETE'); + } + + // Brand kit methods + async listBrandKits(limit: number = 20): Promise<{ brandKits: CanvaBrandKit[] }> { + return this.makeRequest(`/brand-kits?limit=${limit}`); + } + + async getBrandKit(brandKitId: string): Promise { + return this.makeRequest(`/brand-kits/${brandKitId}`); + } + + async createBrandKit(name: string, colors: Array<{ name: string; hex: string }>): Promise { + return this.makeRequest('/brand-kits', 'POST', { + name, + colors + }); + } + + async updateBrandKit(brandKitId: string, updates: Partial): Promise { + return this.makeRequest(`/brand-kits/${brandKitId}`, 'PUT', updates); + } + + // Folder methods + async listFolders(parentId?: string, limit: number = 20): Promise<{ folders: CanvaFolder[] }> { + const params = new URLSearchParams(); + if (parentId) params.append('parentId', parentId); + params.append('limit', String(limit)); + + return this.makeRequest(`/folders?${params.toString()}`); + } + + async createFolder(name: string, parentId?: string): Promise { + return this.makeRequest('/folders', 'POST', { + name, + parentId + }); + } + + async deleteFolder(folderId: string): Promise { + return this.makeRequest(`/folders/${folderId}`, 'DELETE'); + } + + // Collaboration methods + async shareDesign(designId: string, email: string, permissions: string[]): Promise { + return this.makeRequest(`/designs/${designId}/share`, 'POST', { + email, + permissions + }); + } + + async getDesignCollaborators(designId: string): Promise<{ collaborators: any[] }> { + return this.makeRequest(`/designs/${designId}/collaborators`); + } +} + +// Canva connector configuration +export const CanvaConnectorConfig = mcpConnectorConfig({ + name: 'canva', + description: 'Connect to Canva for design management, template operations, and brand tools', + credentials: { + apiKey: { + type: 'string', + description: 'Your Canva API key', + required: true, + secret: true, + help: 'Get your API key from the Canva Developer Portal' + } + }, + setup: { + baseUrl: { + type: 'string', + description: 'Canva API base URL (usually https://api.canva.com)', + required: false, + default: 'https://api.canva.com', + help: 'Leave as default unless you have a custom Canva endpoint' + } + }, + tools: (tool) => ({ + LIST_DESIGNS: tool({ + name: 'list_designs', + description: 'List designs from Canva', + schema: z.object({ + limit: z.number().min(1).max(100).default(20).describe('Number of designs to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.listDesigns(args.limit); + + return JSON.stringify({ + success: true, + designs: result.designs, + total: result.designs.length + }); + } + }), + + GET_DESIGN: tool({ + name: 'get_design', + description: 'Get a specific design by ID', + schema: z.object({ + designId: z.string().describe('Canva design ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const design = await client.getDesign(args.designId); + + return JSON.stringify({ + success: true, + design + }); + } + }), + + CREATE_DESIGN: tool({ + name: 'create_design', + description: 'Create a new design in Canva', + schema: z.object({ + name: z.string().min(1).describe('Design name'), + type: z.string().describe('Design type (e.g., presentation, social-media, document)'), + width: z.number().positive().describe('Design width in pixels'), + height: z.number().positive().describe('Design height in pixels') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const design = await client.createDesign(args.name, args.type, { + width: args.width, + height: args.height + }); + + return JSON.stringify({ + success: true, + design + }); + } + }), + + UPDATE_DESIGN: tool({ + name: 'update_design', + description: 'Update an existing design', + schema: z.object({ + designId: z.string().describe('Canva design ID'), + name: z.string().optional().describe('New design name'), + status: z.string().optional().describe('New design status') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + + const updates: any = {}; + if (args.name) updates.name = args.name; + if (args.status) updates.status = args.status; + + const design = await client.updateDesign(args.designId, updates); + + return JSON.stringify({ + success: true, + design + }); + } + }), + + DELETE_DESIGN: tool({ + name: 'delete_design', + description: 'Delete a design from Canva', + schema: z.object({ + designId: z.string().describe('Canva design ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + await client.deleteDesign(args.designId); + + return JSON.stringify({ + success: true, + message: 'Design deleted successfully' + }); + } + }), + + DUPLICATE_DESIGN: tool({ + name: 'duplicate_design', + description: 'Duplicate an existing design', + schema: z.object({ + designId: z.string().describe('Canva design ID'), + newName: z.string().optional().describe('Name for the duplicated design') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const design = await client.duplicateDesign(args.designId, args.newName); + + return JSON.stringify({ + success: true, + design + }); + } + }), + + LIST_TEMPLATES: tool({ + name: 'list_templates', + description: 'List templates from Canva', + schema: z.object({ + category: z.string().optional().describe('Template category to filter by'), + limit: z.number().min(1).max(100).default(20).describe('Number of templates to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.listTemplates(args.category, args.limit); + + return JSON.stringify({ + success: true, + templates: result.templates, + total: result.templates.length + }); + } + }), + + GET_TEMPLATE: tool({ + name: 'get_template', + description: 'Get a specific template by ID', + schema: z.object({ + templateId: z.string().describe('Canva template ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const template = await client.getTemplate(args.templateId); + + return JSON.stringify({ + success: true, + template + }); + } + }), + + SEARCH_TEMPLATES: tool({ + name: 'search_templates', + description: 'Search for templates by query', + schema: z.object({ + query: z.string().min(1).describe('Search query for templates'), + limit: z.number().min(1).max(100).default(20).describe('Number of templates to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.searchTemplates(args.query, args.limit); + + return JSON.stringify({ + success: true, + templates: result.templates, + total: result.templates.length + }); + } + }), + + LIST_ASSETS: tool({ + name: 'list_assets', + description: 'List assets from Canva', + schema: z.object({ + limit: z.number().min(1).max(100).default(20).describe('Number of assets to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.listAssets(args.limit); + + return JSON.stringify({ + success: true, + assets: result.assets, + total: result.assets.length + }); + } + }), + + GET_ASSET: tool({ + name: 'get_asset', + description: 'Get a specific asset by ID', + schema: z.object({ + assetId: z.string().describe('Canva asset ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const asset = await client.getAsset(args.assetId); + + return JSON.stringify({ + success: true, + asset + }); + } + }), + + DELETE_ASSET: tool({ + name: 'delete_asset', + description: 'Delete an asset from Canva', + schema: z.object({ + assetId: z.string().describe('Canva asset ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + await client.deleteAsset(args.assetId); + + return JSON.stringify({ + success: true, + message: 'Asset deleted successfully' + }); + } + }), + + LIST_BRAND_KITS: tool({ + name: 'list_brand_kits', + description: 'List brand kits from Canva', + schema: z.object({ + limit: z.number().min(1).max(100).default(20).describe('Number of brand kits to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.listBrandKits(args.limit); + + return JSON.stringify({ + success: true, + brandKits: result.brandKits, + total: result.brandKits.length + }); + } + }), + + GET_BRAND_KIT: tool({ + name: 'get_brand_kit', + description: 'Get a specific brand kit by ID', + schema: z.object({ + brandKitId: z.string().describe('Canva brand kit ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const brandKit = await client.getBrandKit(args.brandKitId); + + return JSON.stringify({ + success: true, + brandKit + }); + } + }), + + CREATE_BRAND_KIT: tool({ + name: 'create_brand_kit', + description: 'Create a new brand kit in Canva', + schema: z.object({ + name: z.string().min(1).describe('Brand kit name'), + colors: z.array(z.object({ + name: z.string().describe('Color name'), + hex: z.string().regex(/^#[0-9A-F]{6}$/i).describe('Color hex code (e.g., #FF0000)') + })).default([]).describe('Array of brand colors') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const colors = (args.colors || []) as Array<{ name: string; hex: string }>; + const brandKit = await client.createBrandKit(args.name, colors); + + return JSON.stringify({ + success: true, + brandKit + }); + } + }), + + UPDATE_BRAND_KIT: tool({ + name: 'update_brand_kit', + description: 'Update an existing brand kit', + schema: z.object({ + brandKitId: z.string().describe('Canva brand kit ID'), + name: z.string().optional().describe('New brand kit name'), + colors: z.array(z.object({ + name: z.string().describe('Color name'), + hex: z.string().regex(/^#[0-9A-F]{6}$/i).describe('Color hex code') + })).optional().describe('New array of brand colors') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + + const updates: any = {}; + if (args.name) updates.name = args.name; + if (args.colors) updates.colors = args.colors; + + const brandKit = await client.updateBrandKit(args.brandKitId, updates); + + return JSON.stringify({ + success: true, + brandKit + }); + } + }), + + LIST_FOLDERS: tool({ + name: 'list_folders', + description: 'List folders from Canva', + schema: z.object({ + parentId: z.string().optional().describe('Parent folder ID to list children'), + limit: z.number().min(1).max(100).default(20).describe('Number of folders to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.listFolders(args.parentId, args.limit); + + return JSON.stringify({ + success: true, + folders: result.folders, + total: result.folders.length + }); + } + }), + + CREATE_FOLDER: tool({ + name: 'create_folder', + description: 'Create a new folder in Canva', + schema: z.object({ + name: z.string().min(1).describe('Folder name'), + parentId: z.string().optional().describe('Parent folder ID (optional)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const folder = await client.createFolder(args.name, args.parentId); + + return JSON.stringify({ + success: true, + folder + }); + } + }), + + DELETE_FOLDER: tool({ + name: 'delete_folder', + description: 'Delete a folder from Canva', + schema: z.object({ + folderId: z.string().describe('Canva folder ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + await client.deleteFolder(args.folderId); + + return JSON.stringify({ + success: true, + message: 'Folder deleted successfully' + }); + } + }), + + SHARE_DESIGN: tool({ + name: 'share_design', + description: 'Share a design with collaborators', + schema: z.object({ + designId: z.string().describe('Canva design ID'), + email: z.string().email().describe('Email of the person to share with'), + permissions: z.array(z.enum(['view', 'edit', 'comment'])).describe('Permissions to grant') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.shareDesign(args.designId, args.email, args.permissions); + + return JSON.stringify({ + success: true, + result + }); + } + }), + + GET_DESIGN_COLLABORATORS: tool({ + name: 'get_design_collaborators', + description: 'Get list of collaborators for a design', + schema: z.object({ + designId: z.string().describe('Canva design ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.getDesignCollaborators(args.designId); + + return JSON.stringify({ + success: true, + collaborators: result.collaborators + }); + } + }) + }), + resources: (resource) => ({ + DESIGNS: resource({ + name: 'designs', + description: 'Canva design data', + schema: z.object({ + id: z.string(), + name: z.string(), + type: z.string(), + thumbnailUrl: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string(), + status: z.string() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.listDesigns(100); + + return result.designs; + } + }), + + TEMPLATES: resource({ + name: 'templates', + description: 'Canva template data', + schema: z.object({ + id: z.string(), + name: z.string(), + category: z.string(), + thumbnailUrl: z.string().optional(), + dimensions: z.object({ + width: z.number(), + height: z.number() + }), + tags: z.array(z.string()) + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.listTemplates(undefined, 100); + + return result.templates; + } + }), + + ASSETS: resource({ + name: 'assets', + description: 'Canva asset data', + schema: z.object({ + id: z.string(), + name: z.string(), + type: z.string(), + url: z.string(), + size: z.number(), + createdAt: z.string() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.listAssets(100); + + return result.assets; + } + }), + + BRAND_KITS: resource({ + name: 'brand_kits', + description: 'Canva brand kit data', + schema: z.object({ + id: z.string(), + name: z.string(), + colors: z.array(z.object({ + name: z.string(), + hex: z.string() + })), + fonts: z.array(z.string()), + createdAt: z.string() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.listBrandKits(100); + + return result.brandKits; + } + }), + + FOLDERS: resource({ + name: 'folders', + description: 'Canva folder data', + schema: z.object({ + id: z.string(), + name: z.string(), + parentId: z.string().optional(), + createdAt: z.string(), + itemCount: z.number() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new CanvaClient(credentials.apiKey); + const result = await client.listFolders(undefined, 100); + + return result.folders; + } + }) + }) +}); diff --git a/packages/mcp-connectors/src/connectors/stripe.ts b/packages/mcp-connectors/src/connectors/stripe.ts new file mode 100644 index 00000000..0af6c38d --- /dev/null +++ b/packages/mcp-connectors/src/connectors/stripe.ts @@ -0,0 +1,920 @@ +import { z } from 'zod'; +import { mcpConnectorConfig } from '@stackone/mcp-config-types'; + +// Stripe API interfaces +interface StripeCustomer { + id: string; + email: string; + name?: string; + phone?: string; + created: number; + livemode: boolean; +} + +interface StripeSubscription { + id: string; + customer: string; + status: string; + current_period_start: number; + current_period_end: number; + created: number; +} + +interface StripeInvoice { + id: string; + customer: string; + amount_due: number; + amount_paid: number; + status: string; + created: number; +} + +interface StripeProduct { + id: string; + name: string; + description?: string; + active: boolean; + created: number; +} + +interface StripePrice { + id: string; + product: string; + unit_amount: number; + currency: string; + recurring?: { + interval: string; + interval_count: number; + }; + created: number; +} + +// Stripe API client +class StripeClient { + private apiKey: string; + private baseUrl = 'https://api.stripe.com/v1'; + + constructor(apiKey: string) { + this.apiKey = apiKey; + } + + async makeRequest(endpoint: string, method: 'GET' | 'POST' = 'GET', data?: any): Promise { + const url = `${this.baseUrl}${endpoint}`; + const headers: Record = { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + 'Stripe-Version': '2024-06-20' + }; + + const options: RequestInit = { + method, + headers + }; + + if (data && method === 'POST') { + const formData = new URLSearchParams(); + for (const [key, value] of Object.entries(data)) { + if (value !== undefined && value !== null) { + formData.append(key, String(value)); + } + } + options.body = formData; + } + + const response = await fetch(url, options); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) as any; + throw new Error(`Stripe API error: ${response.status} ${response.statusText} - ${errorData.error?.message || 'Unknown error'}`); + } + + return response.json(); + } + + // Customer methods + async createCustomer(email: string, name?: string, phone?: string): Promise { + const data: any = { email }; + if (name) data.name = name; + if (phone) data.phone = phone; + + return this.makeRequest('/customers', 'POST', data); + } + + async listCustomers(limit: number = 10): Promise<{ data: StripeCustomer[] }> { + return this.makeRequest(`/customers?limit=${limit}`); + } + + async getCustomer(customerId: string): Promise { + return this.makeRequest(`/customers/${customerId}`); + } + + // Subscription methods + async createSubscription(customerId: string, priceId: string): Promise { + return this.makeRequest('/subscriptions', 'POST', { + customer: customerId, + items: [{ price: priceId }] + }); + } + + async listSubscriptions(limit: number = 10): Promise<{ data: StripeSubscription[] }> { + return this.makeRequest(`/subscriptions?limit=${limit}`); + } + + async cancelSubscription(subscriptionId: string): Promise { + return this.makeRequest(`/subscriptions/${subscriptionId}`, 'POST', { cancel_at_period_end: true }); + } + + // Invoice methods + async createInvoice(customerId: string, amount: number, currency: string = 'usd'): Promise { + return this.makeRequest('/invoices', 'POST', { + customer: customerId, + collection_method: 'send_invoice', + days_until_due: 30 + }); + } + + async listInvoices(limit: number = 10): Promise<{ data: StripeInvoice[] }> { + return this.makeRequest(`/invoices?limit=${limit}`); + } + + // Product methods + async createProduct(name: string, description?: string): Promise { + const data: any = { name }; + if (description) data.description = description; + + return this.makeRequest('/products', 'POST', data); + } + + async listProducts(limit: number = 10): Promise<{ data: StripeProduct[] }> { + return this.makeRequest(`/products?limit=${limit}`); + } + + // Price methods + async createPrice(productId: string, unitAmount: number, currency: string = 'usd', recurring?: { interval: string; intervalCount: number }): Promise { + const data: any = { + product: productId, + unit_amount: unitAmount, + currency + }; + + if (recurring) { + data.recurring = { + interval: recurring.interval, + interval_count: recurring.intervalCount + }; + } + + return this.makeRequest('/prices', 'POST', data); + } + + async listPrices(limit: number = 10): Promise<{ data: StripePrice[] }> { + return this.makeRequest(`/prices?limit=${limit}`); + } + + // Payment methods + async listPaymentIntents(limit: number = 10): Promise { + return this.makeRequest(`/payment_intents?limit=${limit}`); + } + + // Coupon methods + async createCoupon(percentOff: number, duration: string = 'once'): Promise { + return this.makeRequest('/coupons', 'POST', { + percent_off: percentOff, + duration + }); + } + + async listCoupons(limit: number = 10): Promise { + return this.makeRequest(`/coupons?limit=${limit}`); + } + + // Account methods + async getAccountInfo(): Promise { + return this.makeRequest('/account'); + } + + async getBalance(): Promise { + return this.makeRequest('/balance'); + } + + // Dispute methods + async listDisputes(limit: number = 10): Promise { + return this.makeRequest(`/disputes?limit=${limit}`); + } + + async updateDispute(disputeId: string, evidence: any): Promise { + return this.makeRequest(`/disputes/${disputeId}`, 'POST', evidence); + } + + // Payment link methods + async createPaymentLink(priceId: string, quantity: number = 1): Promise { + return this.makeRequest('/payment_links', 'POST', { + line_items: [{ price: priceId, quantity }] + }); + } + + // Refund methods + async createRefund(paymentIntentId: string, amount?: number): Promise { + const data: any = { payment_intent: paymentIntentId }; + if (amount) data.amount = amount; + + return this.makeRequest('/refunds', 'POST', data); + } +} + +// Stripe connector configuration +export const StripeConnectorConfig = mcpConnectorConfig({ + name: 'stripe', + description: 'Connect to Stripe for payment processing, customer management, and financial operations', + credentials: { + apiKey: { + type: 'string', + description: 'Your Stripe API key (starts with sk_test_ or sk_live_)', + required: true, + secret: true, + pattern: '^sk_(test|live)_[a-zA-Z0-9]{24}$', + placeholder: 'sk_test_...', + help: 'Get your API key from the Stripe Dashboard under Developers > API keys' + } + }, + setup: { + baseUrl: { + type: 'string', + description: 'Stripe API base URL (usually https://api.stripe.com)', + required: false, + default: 'https://api.stripe.com', + pattern: '^https://api\\.stripe\\.com$', + help: 'Leave as default unless you have a custom Stripe endpoint' + } + }, + tools: (tool) => ({ + CREATE_CUSTOMER: tool({ + name: 'create_customer', + description: 'Create a new customer in Stripe', + schema: z.object({ + email: z.string().email('Valid email is required'), + name: z.string().optional().describe('Customer full name'), + phone: z.string().optional().describe('Customer phone number') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const setup = await context.getSetup(); + + const client = new StripeClient(credentials.apiKey); + const customer = await client.createCustomer(args.email, args.name, args.phone); + + return JSON.stringify({ + success: true, + customer: { + id: customer.id, + email: customer.email, + name: customer.name, + phone: customer.phone, + created: new Date(customer.created * 1000).toISOString() + } + }); + } + }), + + LIST_CUSTOMERS: tool({ + name: 'list_customers', + description: 'List customers from Stripe', + schema: z.object({ + limit: z.number().min(1).max(100).default(10).describe('Number of customers to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listCustomers(args.limit); + + return JSON.stringify({ + success: true, + customers: result.data.map(customer => ({ + id: customer.id, + email: customer.email, + name: customer.name, + phone: customer.phone, + created: new Date(customer.created * 1000).toISOString() + })), + total: result.data.length + }); + } + }), + + GET_CUSTOMER: tool({ + name: 'get_customer', + description: 'Get a specific customer by ID', + schema: z.object({ + customerId: z.string().describe('Stripe customer ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const customer = await client.getCustomer(args.customerId); + + return JSON.stringify({ + success: true, + customer: { + id: customer.id, + email: customer.email, + name: customer.name, + phone: customer.phone, + created: new Date(customer.created * 1000).toISOString() + } + }); + } + }), + + CREATE_SUBSCRIPTION: tool({ + name: 'create_subscription', + title: 'Create Subscription', + description: 'Create a new subscription for a customer', + inputSchema: z.object({ + customerId: z.string().describe('Stripe customer ID'), + priceId: z.string().describe('Stripe price ID for the subscription') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const subscription = await client.createSubscription(args.customerId, args.priceId); + + return JSON.stringify({ + success: true, + subscription: { + id: subscription.id, + customerId: subscription.customer, + status: subscription.status, + currentPeriodStart: new Date(subscription.current_period_start * 1000).toISOString(), + currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString(), + created: new Date(subscription.created * 1000).toISOString() + } + }); + } + }), + + LIST_SUBSCRIPTIONS: tool({ + name: 'list_subscriptions', + title: 'List Subscriptions', + description: 'List subscriptions from Stripe', + inputSchema: z.object({ + limit: z.number().min(1).max(100).default(10).describe('Number of subscriptions to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listSubscriptions(args.limit); + + return JSON.stringify({ + success: true, + subscriptions: result.data.map(subscription => ({ + id: subscription.id, + customerId: subscription.customer, + status: subscription.status, + currentPeriodStart: new Date(subscription.current_period_start * 1000).toISOString(), + currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString(), + created: new Date(subscription.created * 1000).toISOString() + })), + total: result.data.length + }); + } + }), + + CANCEL_SUBSCRIPTION: tool({ + name: 'cancel_subscription', + title: 'Cancel Subscription', + description: 'Cancel a subscription at the end of the current period', + inputSchema: z.object({ + subscriptionId: z.string().describe('Stripe subscription ID') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const subscription = await client.cancelSubscription(args.subscriptionId); + + return JSON.stringify({ + success: true, + message: 'Subscription will be cancelled at the end of the current period', + subscription: { + id: subscription.id, + status: subscription.status + } + }); + } + }), + + CREATE_INVOICE: tool({ + name: 'create_invoice', + title: 'Create Invoice', + description: 'Create a new invoice for a customer', + inputSchema: z.object({ + customerId: z.string().describe('Stripe customer ID'), + amount: z.number().positive().describe('Invoice amount in cents'), + currency: z.string().default('usd').describe('Currency code (default: usd)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const invoice = await client.createInvoice(args.customerId, args.amount, args.currency); + + return JSON.stringify({ + success: true, + invoice: { + id: invoice.id, + customerId: invoice.customer, + amountDue: invoice.amount_due, + amountPaid: invoice.amount_paid, + status: invoice.status, + created: new Date(invoice.created * 1000).toISOString() + } + }); + } + }), + + LIST_INVOICES: tool({ + name: 'list_invoices', + title: 'List Invoices', + description: 'List invoices from Stripe', + inputSchema: z.object({ + limit: z.number().min(1).max(100).default(10).describe('Number of invoices to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listInvoices(args.limit); + + return JSON.stringify({ + success: true, + invoices: result.data.map(invoice => ({ + id: invoice.id, + customerId: invoice.customer, + amountDue: invoice.amount_due, + amountPaid: invoice.amount_paid, + status: invoice.status, + created: new Date(invoice.created * 1000).toISOString() + })), + total: result.data.length + }); + } + }), + + CREATE_PRODUCT: tool({ + name: 'create_product', + title: 'Create Product', + description: 'Create a new product in Stripe', + inputSchema: z.object({ + name: z.string().min(1).describe('Product name'), + description: z.string().optional().describe('Product description') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const product = await client.createProduct(args.name, args.description); + + return JSON.stringify({ + success: true, + product: { + id: product.id, + name: product.name, + description: product.description, + active: product.active, + created: new Date(product.created * 1000).toISOString() + } + }); + } + }), + + LIST_PRODUCTS: tool({ + name: 'list_products', + title: 'List Products', + description: 'List products from Stripe', + inputSchema: z.object({ + limit: z.number().min(1).max(100).default(10).describe('Number of products to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listProducts(args.limit); + + return JSON.stringify({ + success: true, + products: result.data.map(product => ({ + id: product.id, + name: product.name, + description: product.description, + active: product.active, + created: new Date(product.created * 1000).toISOString() + })), + total: result.data.length + }); + } + }), + + CREATE_PRICE: tool({ + name: 'create_price', + title: 'Create Price', + description: 'Create a new price for a product', + inputSchema: z.object({ + productId: z.string().describe('Stripe product ID'), + unitAmount: z.number().positive().describe('Price amount in cents'), + currency: z.string().default('usd').describe('Currency code (default: usd)'), + recurring: z.object({ + interval: z.enum(['day', 'week', 'month', 'year']).describe('Recurring interval'), + intervalCount: z.number().positive().describe('Number of intervals') + }).optional().describe('Recurring billing configuration') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const price = await client.createPrice( + args.productId, + args.unitAmount, + args.currency, + args.recurring ? { + interval: args.recurring.interval, + intervalCount: args.recurring.intervalCount + } : undefined + ); + + return JSON.stringify({ + success: true, + price: { + id: price.id, + productId: price.product, + unitAmount: price.unit_amount, + currency: price.currency, + recurring: price.recurring, + created: new Date(price.created * 1000).toISOString() + } + }); + } + }), + + LIST_PRICES: tool({ + name: 'list_prices', + title: 'List Prices', + description: 'List prices from Stripe', + inputSchema: z.object({ + limit: z.number().min(1).max(100).default(10).describe('Number of prices to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listPrices(args.limit); + + return JSON.stringify({ + success: true, + prices: result.data.map(price => ({ + id: price.id, + productId: price.product, + unitAmount: price.unit_amount, + currency: price.currency, + recurring: price.recurring, + created: new Date(price.created * 1000).toISOString() + })), + total: result.data.length + }); + } + }), + + LIST_PAYMENT_INTENTS: tool({ + name: 'list_payment_intents', + title: 'List Payment Intents', + description: 'List payment intents from Stripe', + inputSchema: z.object({ + limit: z.number().min(1).max(100).default(10).describe('Number of payment intents to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listPaymentIntents(args.limit); + + return JSON.stringify({ + success: true, + paymentIntents: result.data, + total: result.data.length + }); + } + }), + + CREATE_PAYMENT_LINK: tool({ + name: 'create_payment_link', + title: 'Create Payment Link', + description: 'Create a payment link for a price', + inputSchema: z.object({ + priceId: z.string().describe('Stripe price ID'), + quantity: z.number().positive().default(1).describe('Quantity of items') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const paymentLink = await client.createPaymentLink(args.priceId, args.quantity); + + return JSON.stringify({ + success: true, + paymentLink: { + id: paymentLink.id, + url: paymentLink.url, + active: paymentLink.active + } + }); + } + }), + + CREATE_COUPON: tool({ + name: 'create_coupon', + title: 'Create Coupon', + description: 'Create a new coupon in Stripe', + inputSchema: z.object({ + percentOff: z.number().min(1).max(100).describe('Percentage discount (1-100)'), + duration: z.enum(['once', 'repeating', 'forever']).default('once').describe('Coupon duration') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const coupon = await client.createCoupon(args.percentOff, args.duration); + + return JSON.stringify({ + success: true, + coupon: { + id: coupon.id, + percentOff: coupon.percent_off, + duration: coupon.duration, + valid: coupon.valid + } + }); + } + }), + + LIST_COUPONS: tool({ + name: 'list_coupons', + title: 'List Coupons', + description: 'List coupons from Stripe', + inputSchema: z.object({ + limit: z.number().min(1).max(100).default(10).describe('Number of coupons to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listCoupons(args.limit); + + return JSON.stringify({ + success: true, + coupons: result.data, + total: result.data.length + }); + } + }), + + GET_ACCOUNT_INFO: tool({ + name: 'get_account_info', + title: 'Get Account Info', + description: 'Get information about the connected Stripe account', + inputSchema: z.object({}), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const account = await client.getAccountInfo(); + + return JSON.stringify({ + success: true, + account: { + id: account.id, + businessType: account.business_type, + country: account.country, + defaultCurrency: account.default_currency, + email: account.email, + name: account.business_profile?.name + } + }); + } + }), + + GET_BALANCE: tool({ + name: 'get_balance', + title: 'Get Balance', + description: 'Get the current balance of the Stripe account', + inputSchema: z.object({}), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const balance = await client.getBalance(); + + return JSON.stringify({ + success: true, + balance: { + available: balance.available, + pending: balance.pending, + instantAvailable: balance.instant_available + } + }); + } + }), + + LIST_DISPUTES: tool({ + name: 'list_disputes', + title: 'List Disputes', + description: 'List disputes from Stripe', + inputSchema: z.object({ + limit: z.number().min(1).max(100).default(10).describe('Number of disputes to return (max 100)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listDisputes(args.limit); + + return JSON.stringify({ + success: true, + disputes: result.data, + total: result.data.length + }); + } + }), + + UPDATE_DISPUTE: tool({ + name: 'update_dispute', + title: 'Update Dispute', + description: 'Update a dispute with evidence', + inputSchema: z.object({ + disputeId: z.string().describe('Stripe dispute ID'), + evidence: z.record(z.any()).describe('Evidence to submit for the dispute') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const dispute = await client.updateDispute(args.disputeId, args.evidence); + + return JSON.stringify({ + success: true, + dispute: { + id: dispute.id, + status: dispute.status, + evidence: dispute.evidence + } + }); + } + }), + + CREATE_REFUND: tool({ + name: 'create_refund', + title: 'Create Refund', + description: 'Create a refund for a payment intent', + inputSchema: z.object({ + paymentIntentId: z.string().describe('Stripe payment intent ID'), + amount: z.number().positive().optional().describe('Refund amount in cents (optional, defaults to full amount)') + }), + handler: async (args, context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const refund = await client.createRefund(args.paymentIntentId, args.amount); + + return JSON.stringify({ + success: true, + refund: { + id: refund.id, + amount: refund.amount, + currency: refund.currency, + status: refund.status, + reason: refund.reason + } + }); + } + }) + }), + resources: (resource) => ({ + CUSTOMERS: resource({ + name: 'customers', + title: 'Customers', + description: 'Stripe customer data', + schema: z.object({ + id: z.string(), + email: z.string(), + name: z.string().optional(), + phone: z.string().optional(), + created: z.string() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listCustomers(100); + + return result.data.map(customer => ({ + id: customer.id, + email: customer.email, + name: customer.name, + phone: customer.phone, + created: new Date(customer.created * 1000).toISOString() + })); + } + }), + + SUBSCRIPTIONS: resource({ + name: 'subscriptions', + title: 'Subscriptions', + description: 'Stripe subscription data', + schema: z.object({ + id: z.string(), + customerId: z.string(), + status: z.string(), + currentPeriodStart: z.string(), + currentPeriodEnd: z.string(), + created: z.string() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listSubscriptions(100); + + return result.data.map(subscription => ({ + id: subscription.id, + customerId: subscription.customer, + status: subscription.status, + currentPeriodStart: new Date(subscription.current_period_start * 1000).toISOString(), + currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString(), + created: new Date(subscription.created * 1000).toISOString() + })); + } + }), + + PRODUCTS: resource({ + name: 'products', + title: 'Products', + description: 'Stripe product data', + schema: z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + active: z.boolean(), + created: z.string() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listProducts(100); + + return result.data.map(product => ({ + id: product.id, + name: product.name, + description: product.description, + active: product.active, + created: new Date(product.created * 1000).toISOString() + })); + } + }), + + PRICES: resource({ + name: 'prices', + title: 'Prices', + description: 'Stripe price data', + schema: z.object({ + id: z.string(), + productId: z.string(), + unitAmount: z.number(), + currency: z.string(), + recurring: z.any().optional(), + created: z.string() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listPrices(100); + + return result.data.map(price => ({ + id: price.id, + productId: price.product, + unitAmount: price.unit_amount, + currency: price.currency, + recurring: price.recurring, + created: new Date(price.created * 1000).toISOString() + })); + } + }), + + INVOICES: resource({ + name: 'invoices', + title: 'Invoices', + description: 'Stripe invoice data', + schema: z.object({ + id: z.string(), + customerId: z.string(), + amountDue: z.number(), + amountPaid: z.number(), + status: z.string(), + created: z.string() + }), + handler: async (context) => { + const credentials = await context.getCredentials(); + const client = new StripeClient(credentials.apiKey); + const result = await client.listInvoices(100); + + return result.data.map(invoice => ({ + id: invoice.id, + customerId: invoice.customer, + amountDue: invoice.amount_due, + amountPaid: invoice.amount_paid, + status: invoice.status, + created: new Date(invoice.created * 1000).toISOString() + })); + } + }) + }) +}); diff --git a/packages/mcp-connectors/src/index.ts b/packages/mcp-connectors/src/index.ts index bd784f61..d6427ab8 100644 --- a/packages/mcp-connectors/src/index.ts +++ b/packages/mcp-connectors/src/index.ts @@ -41,6 +41,9 @@ import { TodoistConnectorConfig } from './connectors/todoist'; import { TurbopufferConnectorConfig } from './connectors/turbopuffer'; import { WandbConnectorConfig } from './connectors/wandb'; import { XeroConnectorConfig } from './connectors/xero'; +import { StripeConnectorConfig } from './connectors/stripe'; +import { CanvaConnectorConfig } from './connectors/canva'; +import { CalendlyConnectorConfig } from './connectors/calendly'; export const Connectors: readonly MCPConnectorConfig[] = [ TestConnectorConfig, @@ -83,6 +86,9 @@ export const Connectors: readonly MCPConnectorConfig[] = [ TurbopufferConnectorConfig, WandbConnectorConfig, XeroConnectorConfig, + StripeConnectorConfig, + CanvaConnectorConfig, + CalendlyConnectorConfig, ] as const; export { @@ -126,4 +132,7 @@ export { TurbopufferConnectorConfig, WandbConnectorConfig, XeroConnectorConfig, + StripeConnectorConfig, + CanvaConnectorConfig, + CalendlyConnectorConfig, }; diff --git a/tatus b/tatus index e69de29b..1250959d 100644 --- a/tatus +++ b/tatus @@ -0,0 +1,314 @@ + + SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS + + Commands marked with * may be preceded by a number, _N. + Notes in parentheses indicate the behavior if _N is given. + A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K. + + h H Display this help. + q :q Q :Q ZZ Exit. + --------------------------------------------------------------------------- + + MMOOVVIINNGG + + e ^E j ^N CR * Forward one line (or _N lines). + y ^Y k ^K ^P * Backward one line (or _N lines). + f ^F ^V SPACE * Forward one window (or _N lines). + b ^B ESC-v * Backward one window (or _N lines). + z * Forward one window (and set window to _N). + w * Backward one window (and set window to _N). + ESC-SPACE * Forward one window, but don't stop at end-of-file. + d ^D * Forward one half-window (and set half-window to _N). + u ^U * Backward one half-window (and set half-window to _N). + ESC-) RightArrow * Right one half screen width (or _N positions). + ESC-( LeftArrow * Left one half screen width (or _N positions). + ESC-} ^RightArrow Right to last column displayed. + ESC-{ ^LeftArrow Left to first column. + F Forward forever; like "tail -f". + ESC-F Like F but stop when search pattern is found. + r ^R ^L Repaint screen. + R Repaint screen, discarding buffered input. + --------------------------------------------------- + Default "window" is the screen height. + Default "half-window" is half of the screen height. + --------------------------------------------------------------------------- + + SSEEAARRCCHHIINNGG + + /_p_a_t_t_e_r_n * Search forward for (_N-th) matching line. + ?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line. + n * Repeat previous search (for _N-th occurrence). + N * Repeat previous search in reverse direction. + ESC-n * Repeat previous search, spanning files. + ESC-N * Repeat previous search, reverse dir. & spanning files. + ^O^N ^On * Search forward for (_N-th) OSC8 hyperlink. + ^O^P ^Op * Search backward for (_N-th) OSC8 hyperlink. + ^O^L ^Ol Jump to the currently selected OSC8 hyperlink. + ESC-u Undo (toggle) search highlighting. + ESC-U Clear search highlighting. + &_p_a_t_t_e_r_n * Display only matching lines. + --------------------------------------------------- + A search pattern may begin with one or more of: + ^N or ! Search for NON-matching lines. + ^E or * Search multiple files (pass thru END OF FILE). + ^F or @ Start search at FIRST file (for /) or last file (for ?). + ^K Highlight matches, but don't move (KEEP position). + ^R Don't use REGULAR EXPRESSIONS. + ^S _n Search for match in _n-th parenthesized subpattern. + ^W WRAP search if no match found. + ^L Enter next character literally into pattern. + --------------------------------------------------------------------------- + + JJUUMMPPIINNGG + + g < ESC-< * Go to first line in file (or line _N). + G > ESC-> * Go to last line in file (or line _N). + p % * Go to beginning of file (or _N percent into file). + t * Go to the (_N-th) next tag. + T * Go to the (_N-th) previous tag. + { ( [ * Find close bracket } ) ]. + } ) ] * Find open bracket { ( [. + ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>. + ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>. + --------------------------------------------------- + Each "find close bracket" command goes forward to the close bracket + matching the (_N-th) open bracket in the top line. + Each "find open bracket" command goes backward to the open bracket + matching the (_N-th) close bracket in the bottom line. + + m_<_l_e_t_t_e_r_> Mark the current top line with . + M_<_l_e_t_t_e_r_> Mark the current bottom line with . + '_<_l_e_t_t_e_r_> Go to a previously marked position. + '' Go to the previous position. + ^X^X Same as '. + ESC-m_<_l_e_t_t_e_r_> Clear a mark. + --------------------------------------------------- + A mark is any upper-case or lower-case letter. + Certain marks are predefined: + ^ means beginning of the file + $ means end of the file + --------------------------------------------------------------------------- + + CCHHAANNGGIINNGG FFIILLEESS + + :e [_f_i_l_e] Examine a new file. + ^X^V Same as :e. + :n * Examine the (_N-th) next file from the command line. + :p * Examine the (_N-th) previous file from the command line. + :x * Examine the first (or _N-th) file from the command line. + ^O^O Open the currently selected OSC8 hyperlink. + :d Delete the current file from the command line list. + = ^G :f Print current file name. + --------------------------------------------------------------------------- + + MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS + + -_<_f_l_a_g_> Toggle a command line option [see OPTIONS below]. + --_<_n_a_m_e_> Toggle a command line option, by name. + __<_f_l_a_g_> Display the setting of a command line option. + ___<_n_a_m_e_> Display the setting of an option, by name. + +_c_m_d Execute the less cmd each time a new file is examined. + + !_c_o_m_m_a_n_d Execute the shell command with $SHELL. + #_c_o_m_m_a_n_d Execute the shell command, expanded like a prompt. + |XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command. + s _f_i_l_e Save input to a file. + v Edit the current file with $VISUAL or $EDITOR. + V Print version number of "less". + --------------------------------------------------------------------------- + + OOPPTTIIOONNSS + + Most options may be changed either on the command line, + or from within less by using the - or -- command. + Options may be given in one of two forms: either a single + character preceded by a -, or a name preceded by --. + + -? ........ --help + Display help (from command line). + -a ........ --search-skip-screen + Search skips current screen. + -A ........ --SEARCH-SKIP-SCREEN + Search starts just after target line. + -b [_N] .... --buffers=[_N] + Number of buffers. + -B ........ --auto-buffers + Don't automatically allocate buffers for pipes. + -c ........ --clear-screen + Repaint by clearing rather than scrolling. + -d ........ --dumb + Dumb terminal. + -D xx_c_o_l_o_r . --color=xx_c_o_l_o_r + Set screen colors. + -e -E .... --quit-at-eof --QUIT-AT-EOF + Quit at end of file. + -f ........ --force + Force open non-regular files. + -F ........ --quit-if-one-screen + Quit if entire file fits on first screen. + -g ........ --hilite-search + Highlight only last match for searches. + -G ........ --HILITE-SEARCH + Don't highlight any matches for searches. + -h [_N] .... --max-back-scroll=[_N] + Backward scroll limit. + -i ........ --ignore-case + Ignore case in searches that do not contain uppercase. + -I ........ --IGNORE-CASE + Ignore case in all searches. + -j [_N] .... --jump-target=[_N] + Screen position of target lines. + -J ........ --status-column + Display a status column at left edge of screen. + -k _f_i_l_e ... --lesskey-file=_f_i_l_e + Use a compiled lesskey file. + -K ........ --quit-on-intr + Exit less in response to ctrl-C. + -L ........ --no-lessopen + Ignore the LESSOPEN environment variable. + -m -M .... --long-prompt --LONG-PROMPT + Set prompt style. + -n ......... --line-numbers + Suppress line numbers in prompts and messages. + -N ......... --LINE-NUMBERS + Display line number at start of each line. + -o [_f_i_l_e] .. --log-file=[_f_i_l_e] + Copy to log file (standard input only). + -O [_f_i_l_e] .. --LOG-FILE=[_f_i_l_e] + Copy to log file (unconditionally overwrite). + -p _p_a_t_t_e_r_n . --pattern=[_p_a_t_t_e_r_n] + Start at pattern (from command line). + -P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t] + Define new prompt. + -q -Q .... --quiet --QUIET --silent --SILENT + Quiet the terminal bell. + -r -R .... --raw-control-chars --RAW-CONTROL-CHARS + Output "raw" control characters. + -s ........ --squeeze-blank-lines + Squeeze multiple blank lines. + -S ........ --chop-long-lines + Chop (truncate) long lines rather than wrapping. + -t _t_a_g .... --tag=[_t_a_g] + Find a tag. + -T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e] + Use an alternate tags file. + -u -U .... --underline-special --UNDERLINE-SPECIAL + Change handling of backspaces, tabs and carriage returns. + -V ........ --version + Display the version number of "less". + -w ........ --hilite-unread + Highlight first new line after forward-screen. + -W ........ --HILITE-UNREAD + Highlight first new line after any forward movement. + -x [_N[,...]] --tabs=[_N[,...]] + Set tab stops. + -X ........ --no-init + Don't use termcap init/deinit strings. + -y [_N] .... --max-forw-scroll=[_N] + Forward scroll limit. + -z [_N] .... --window=[_N] + Set size of window. + -" [_c[_c]] . --quotes=[_c[_c]] + Set shell quote characters. + -~ ........ --tilde + Don't display tildes after end of file. + -# [_N] .... --shift=[_N] + Set horizontal scroll amount (0 = one half screen width). + + --exit-follow-on-close + Exit F command on a pipe when writer closes pipe. + --file-size + Automatically determine the size of the input file. + --follow-name + The F command changes files if the input file is renamed. + --header=[_L[,_C[,_N]]] + Use _L lines (starting at line _N) and _C columns as headers. + --incsearch + Search file as each pattern character is typed in. + --intr=[_C] + Use _C instead of ^X to interrupt a read. + --lesskey-context=_t_e_x_t + Use lesskey source file contents. + --lesskey-src=_f_i_l_e + Use a lesskey source file. + --line-num-width=[_N] + Set the width of the -N line number field to _N characters. + --match-shift=[_N] + Show at least _N characters to the left of a search match. + --modelines=[_N] + Read _N lines from the input file and look for vim modelines. + --mouse + Enable mouse input. + --no-keypad + Don't send termcap keypad init/deinit strings. + --no-histdups + Remove duplicates from command history. + --no-number-headers + Don't give line numbers to header lines. + --no-search-header-lines + Searches do not include header lines. + --no-search-header-columns + Searches do not include header columns. + --no-search-headers + Searches do not include header lines or columns. + --no-vbell + Disable the terminal's visual bell. + --redraw-on-quit + Redraw final screen when quitting. + --rscroll=[_C] + Set the character used to mark truncated lines. + --save-marks + Retain marks across invocations of less. + --search-options=[EFKNRW-] + Set default options for every search. + --show-preproc-errors + Display a message if preprocessor exits with an error status. + --proc-backspace + Process backspaces for bold/underline. + --PROC-BACKSPACE + Treat backspaces as control characters. + --proc-return + Delete carriage returns before newline. + --PROC-RETURN + Treat carriage returns as control characters. + --proc-tab + Expand tabs to spaces. + --PROC-TAB + Treat tabs as control characters. + --status-col-width=[_N] + Set the width of the -J status column to _N characters. + --status-line + Highlight or color the entire line containing a mark. + --use-backslash + Subsequent options use backslash as escape char. + --use-color + Enables colored text. + --wheel-lines=[_N] + Each click of the mouse wheel moves _N lines. + --wordwrap + Wrap lines at spaces. + + + --------------------------------------------------------------------------- + + LLIINNEE EEDDIITTIINNGG + + These keys can be used to edit text being entered + on the "command line" at the bottom of the screen. + + RightArrow ..................... ESC-l ... Move cursor right one character. + LeftArrow ...................... ESC-h ... Move cursor left one character. + ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word. + ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word. + HOME ........................... ESC-0 ... Move cursor to start of line. + END ............................ ESC-$ ... Move cursor to end of line. + BACKSPACE ................................ Delete char to left of cursor. + DELETE ......................... ESC-x ... Delete char under cursor. + ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor. + ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor. + ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line. + UpArrow ........................ ESC-k ... Retrieve previous command line. + DownArrow ...................... ESC-j ... Retrieve next command line. + TAB ...................................... Complete filename & cycle. + SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle. + ctrl-L ................................... Complete filename, list all.