diff --git a/servers/hubspot/TOOLS_SUMMARY.md b/servers/hubspot/TOOLS_SUMMARY.md new file mode 100644 index 0000000..d9062d6 --- /dev/null +++ b/servers/hubspot/TOOLS_SUMMARY.md @@ -0,0 +1,173 @@ +# HubSpot MCP Server - Tools Summary + +All tool files have been successfully created and verified with TypeScript compilation. + +## Total Tools: 108 (exceeds target of 60-80) + +## Breakdown by Category + +### 1. **contacts.ts** (8 tools) +- `hubspot_list_contacts` - List contacts with pagination +- `hubspot_get_contact` - Get single contact by ID +- `hubspot_create_contact` - Create new contact +- `hubspot_update_contact` - Update existing contact +- `hubspot_delete_contact` - Archive contact +- `hubspot_search_contacts` - Search contacts with filters +- `hubspot_batch_create_contacts` - Batch create contacts +- `hubspot_batch_update_contacts` - Batch update contacts + +### 2. **companies.ts** (8 tools) +- `hubspot_list_companies` - List companies with pagination +- `hubspot_get_company` - Get single company by ID +- `hubspot_create_company` - Create new company +- `hubspot_update_company` - Update existing company +- `hubspot_delete_company` - Archive company +- `hubspot_search_companies` - Search companies with filters +- `hubspot_batch_create_companies` - Batch create companies +- `hubspot_batch_update_companies` - Batch update companies + +### 3. **deals.ts** (9 tools) +- `hubspot_list_deals` - List deals with pagination +- `hubspot_get_deal` - Get single deal by ID +- `hubspot_create_deal` - Create new deal +- `hubspot_update_deal` - Update existing deal +- `hubspot_delete_deal` - Archive deal +- `hubspot_move_deal_stage` - Move deal to different stage +- `hubspot_search_deals` - Search deals with filters +- `hubspot_batch_create_deals` - Batch create deals +- `hubspot_batch_update_deals` - Batch update deals + +### 4. **tickets.ts** (6 tools) +- `hubspot_list_tickets` - List tickets with pagination +- `hubspot_get_ticket` - Get single ticket by ID +- `hubspot_create_ticket` - Create new ticket +- `hubspot_update_ticket` - Update existing ticket +- `hubspot_delete_ticket` - Archive ticket +- `hubspot_search_tickets` - Search tickets with filters + +### 5. **emails.ts** (7 tools) +- `hubspot_list_emails` - List marketing emails +- `hubspot_get_email` - Get single email by ID +- `hubspot_create_email` - Create new email +- `hubspot_update_email` - Update existing email +- `hubspot_send_email` - Send or schedule email +- `hubspot_clone_email` - Clone/duplicate email +- `hubspot_get_email_stats` - Get email statistics + +### 6. **engagements.ts** (15 tools) +**Notes:** +- `hubspot_list_notes` - List engagement notes +- `hubspot_get_note` - Get single note +- `hubspot_create_note` - Create new note +- `hubspot_update_note` - Update existing note +- `hubspot_delete_note` - Archive note + +**Calls:** +- `hubspot_list_calls` - List call engagements +- `hubspot_create_call` - Create new call +- `hubspot_delete_call` - Archive call + +**Meetings:** +- `hubspot_list_meetings` - List meeting engagements +- `hubspot_create_meeting` - Create new meeting +- `hubspot_delete_meeting` - Archive meeting + +**Tasks:** +- `hubspot_list_tasks` - List task engagements +- `hubspot_create_task` - Create new task +- `hubspot_update_task` - Update existing task +- `hubspot_delete_task` - Archive task + +### 7. **pipelines.ts** (11 tools) +**Deal Pipelines:** +- `hubspot_list_deal_pipelines` - List all deal pipelines +- `hubspot_get_deal_pipeline` - Get deal pipeline by ID +- `hubspot_create_deal_pipeline` - Create new deal pipeline +- `hubspot_update_deal_pipeline` - Update deal pipeline +- `hubspot_delete_deal_pipeline` - Archive deal pipeline +- `hubspot_create_deal_stage` - Create new stage in pipeline + +**Ticket Pipelines:** +- `hubspot_list_ticket_pipelines` - List all ticket pipelines +- `hubspot_get_ticket_pipeline` - Get ticket pipeline by ID +- `hubspot_create_ticket_pipeline` - Create new ticket pipeline +- `hubspot_update_ticket_pipeline` - Update ticket pipeline +- `hubspot_delete_ticket_pipeline` - Archive ticket pipeline + +### 8. **lists.ts** (8 tools) +- `hubspot_list_lists` - List all contact lists +- `hubspot_get_list` - Get single list by ID +- `hubspot_create_static_list` - Create static contact list +- `hubspot_create_active_list` - Create dynamic contact list +- `hubspot_update_list` - Update existing list +- `hubspot_delete_list` - Delete list +- `hubspot_add_contacts_to_list` - Add contacts to list +- `hubspot_remove_contacts_from_list` - Remove contacts from list + +### 9. **forms.ts** (6 tools) +- `hubspot_list_forms` - List all forms +- `hubspot_get_form` - Get single form by ID +- `hubspot_create_form` - Create new form +- `hubspot_update_form` - Update existing form +- `hubspot_delete_form` - Archive form +- `hubspot_get_form_submissions` - Get form submissions + +### 10. **campaigns.ts** (5 tools) +- `hubspot_list_campaigns` - List all campaigns +- `hubspot_get_campaign` - Get single campaign by ID +- `hubspot_create_campaign` - Create new campaign +- `hubspot_update_campaign` - Update existing campaign +- `hubspot_get_campaign_stats` - Get campaign statistics + +### 11. **blog.ts** (8 tools) +**Blog Posts:** +- `hubspot_list_blog_posts` - List blog posts +- `hubspot_get_blog_post` - Get blog post by ID +- `hubspot_create_blog_post` - Create new blog post +- `hubspot_update_blog_post` - Update blog post +- `hubspot_delete_blog_post` - Archive blog post +- `hubspot_publish_blog_post` - Publish/schedule blog post + +**Blog Authors:** +- `hubspot_list_blog_authors` - List all blog authors +- `hubspot_get_blog_author` - Get blog author by ID + +### 12. **analytics.ts** (6 tools) +- `hubspot_get_analytics_views` - Get page view analytics +- `hubspot_get_traffic_analytics` - Get traffic analytics +- `hubspot_get_sources_analytics` - Get traffic sources +- `hubspot_get_page_analytics` - Get page-specific analytics +- `hubspot_get_social_analytics` - Get social media analytics +- `hubspot_get_conversion_analytics` - Get conversion analytics + +### 13. **workflows.ts** (6 tools) +- `hubspot_list_workflows` - List all workflows +- `hubspot_get_workflow` - Get workflow by ID +- `hubspot_create_workflow` - Create new workflow +- `hubspot_activate_workflow` - Activate workflow +- `hubspot_deactivate_workflow` - Deactivate workflow +- `hubspot_enroll_contact_in_workflow` - Enroll contact in workflow + +### 14. **webhooks.ts** (5 tools) +- `hubspot_list_webhooks` - List all webhook subscriptions +- `hubspot_get_webhook` - Get webhook by ID +- `hubspot_create_webhook` - Create new webhook subscription +- `hubspot_update_webhook` - Update webhook subscription +- `hubspot_delete_webhook` - Delete webhook subscription + +## Quality Standards Met + +✅ All files export `getTools(client: HubSpotClient)` function +✅ Import client type from `'../clients/hubspot.js'` +✅ Use HubSpot CRM v3 API conventions +✅ Search API with filterGroups + sorts +✅ Batch operations with proper endpoints +✅ Pagination with `after` cursor + `limit` +✅ TypeScript compilation passes with `npx tsc --noEmit` +✅ Consistent naming: `hubspot_verb_noun` +✅ Proper error handling in responses +✅ Well-documented inputSchema for each tool + +## Next Steps + +The server foundation files (`src/server.ts`, `src/main.ts`) need to be updated to import and register all these tool modules for lazy loading. diff --git a/servers/hubspot/src/tools/analytics.ts b/servers/hubspot/src/tools/analytics.ts new file mode 100644 index 0000000..83a2cb4 --- /dev/null +++ b/servers/hubspot/src/tools/analytics.ts @@ -0,0 +1,256 @@ +/** + * HubSpot Analytics Tools + * Tools for accessing analytics data (traffic, sources, page views) + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; + +export function getTools(client: HubSpotClient) { + return [ + { + name: 'hubspot_get_analytics_views', + description: 'Get page view analytics for a specific time period', + inputSchema: { + type: 'object', + properties: { + startDate: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + frequency: { + type: 'string', + description: 'Data frequency (DAILY, WEEKLY, MONTHLY)', + default: 'DAILY', + }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/analytics/v2/reports/totals', + params: { + start: args.startDate, + end: args.endDate, + frequency: args.frequency || 'DAILY', + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_traffic_analytics', + description: 'Get website traffic analytics', + inputSchema: { + type: 'object', + properties: { + startDate: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + frequency: { + type: 'string', + description: 'Data frequency (DAILY, WEEKLY, MONTHLY)', + default: 'DAILY', + }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/analytics/v2/reports/traffic', + params: { + start: args.startDate, + end: args.endDate, + frequency: args.frequency || 'DAILY', + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_sources_analytics', + description: 'Get traffic sources analytics (where visitors come from)', + inputSchema: { + type: 'object', + properties: { + startDate: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/analytics/v2/reports/sources', + params: { + start: args.startDate, + end: args.endDate, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_page_analytics', + description: 'Get analytics for specific pages', + inputSchema: { + type: 'object', + properties: { + startDate: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + limit: { + type: 'number', + description: 'Number of top pages to return', + default: 20, + }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/analytics/v2/reports/pages', + params: { + start: args.startDate, + end: args.endDate, + limit: args.limit || 20, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_social_analytics', + description: 'Get social media analytics', + inputSchema: { + type: 'object', + properties: { + startDate: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/analytics/v2/reports/social', + params: { + start: args.startDate, + end: args.endDate, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_conversion_analytics', + description: 'Get conversion analytics (form submissions, landing pages)', + inputSchema: { + type: 'object', + properties: { + startDate: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/analytics/v2/reports/conversions', + params: { + start: args.startDate, + end: args.endDate, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/blog.ts b/servers/hubspot/src/tools/blog.ts new file mode 100644 index 0000000..cf01c02 --- /dev/null +++ b/servers/hubspot/src/tools/blog.ts @@ -0,0 +1,362 @@ +/** + * HubSpot Blog Tools + * Tools for managing blog posts and blog authors + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; + +export function getTools(client: HubSpotClient) { + return [ + // Blog Posts + { + name: 'hubspot_list_blog_posts', + description: 'List blog posts', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of posts to return', + default: 20, + }, + offset: { + type: 'number', + description: 'Offset for pagination', + default: 0, + }, + state: { + type: 'string', + description: 'Filter by state (DRAFT, PUBLISHED, SCHEDULED)', + }, + }, + }, + handler: async (args: any) => { + const params: any = { + limit: args.limit || 20, + offset: args.offset || 0, + }; + if (args.state) params.state = args.state; + + const response = await client.apiRequest({ + method: 'GET', + url: '/cms/v3/blogs/posts', + params, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_blog_post', + description: 'Get a specific blog post by ID', + inputSchema: { + type: 'object', + properties: { + postId: { + type: 'string', + description: 'Blog post ID', + }, + }, + required: ['postId'], + }, + handler: async (args: any) => { + const post = await client.apiRequest({ + method: 'GET', + url: `/cms/v3/blogs/posts/${args.postId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(post, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_blog_post', + description: 'Create a new blog post', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Post title/name', + }, + slug: { + type: 'string', + description: 'URL slug', + }, + contentGroupId: { + type: 'string', + description: 'Blog ID', + }, + htmlTitle: { + type: 'string', + description: 'HTML meta title', + }, + postBody: { + type: 'string', + description: 'Post body content (HTML)', + }, + metaDescription: { + type: 'string', + description: 'Meta description', + }, + }, + required: ['name'], + }, + handler: async (args: any) => { + const post = await client.apiRequest({ + method: 'POST', + url: '/cms/v3/blogs/posts', + data: { + name: args.name, + slug: args.slug, + contentGroupId: args.contentGroupId, + htmlTitle: args.htmlTitle, + postBody: args.postBody, + metaDescription: args.metaDescription, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + postId: post.id, + post, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_blog_post', + description: 'Update an existing blog post', + inputSchema: { + type: 'object', + properties: { + postId: { + type: 'string', + description: 'Blog post ID', + }, + name: { + type: 'string', + description: 'Updated post title', + }, + postBody: { + type: 'string', + description: 'Updated post body', + }, + metaDescription: { + type: 'string', + description: 'Updated meta description', + }, + }, + required: ['postId'], + }, + handler: async (args: any) => { + const updates: any = {}; + if (args.name) updates.name = args.name; + if (args.postBody) updates.postBody = args.postBody; + if (args.metaDescription) updates.metaDescription = args.metaDescription; + + const post = await client.apiRequest({ + method: 'PATCH', + url: `/cms/v3/blogs/posts/${args.postId}`, + data: updates, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + post, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_blog_post', + description: 'Archive a blog post', + inputSchema: { + type: 'object', + properties: { + postId: { + type: 'string', + description: 'Blog post ID to archive', + }, + }, + required: ['postId'], + }, + handler: async (args: any) => { + await client.apiRequest({ + method: 'DELETE', + url: `/cms/v3/blogs/posts/${args.postId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Blog post ${args.postId} archived successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_publish_blog_post', + description: 'Publish a blog post (schedule or publish immediately)', + inputSchema: { + type: 'object', + properties: { + postId: { + type: 'string', + description: 'Blog post ID', + }, + publishDate: { + type: 'string', + description: 'Publish date/time (ISO format). If not provided, publishes immediately.', + }, + }, + required: ['postId'], + }, + handler: async (args: any) => { + const data: any = { + state: 'PUBLISHED', + }; + if (args.publishDate) { + data.state = 'SCHEDULED'; + data.publishDate = args.publishDate; + } + + const post = await client.apiRequest({ + method: 'PATCH', + url: `/cms/v3/blogs/posts/${args.postId}`, + data, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: args.publishDate + ? `Post scheduled for ${args.publishDate}` + : 'Post published successfully', + post, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + // Blog Authors + { + name: 'hubspot_list_blog_authors', + description: 'List all blog authors', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of authors to return', + default: 20, + }, + }, + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/cms/v3/blogs/authors', + params: { + limit: args.limit || 20, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_blog_author', + description: 'Get a specific blog author by ID', + inputSchema: { + type: 'object', + properties: { + authorId: { + type: 'string', + description: 'Author ID', + }, + }, + required: ['authorId'], + }, + handler: async (args: any) => { + const author = await client.apiRequest({ + method: 'GET', + url: `/cms/v3/blogs/authors/${args.authorId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(author, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/campaigns.ts b/servers/hubspot/src/tools/campaigns.ts new file mode 100644 index 0000000..62564a8 --- /dev/null +++ b/servers/hubspot/src/tools/campaigns.ts @@ -0,0 +1,194 @@ +/** + * HubSpot Campaigns Tools + * Tools for managing marketing campaigns + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; + +export function getTools(client: HubSpotClient) { + return [ + { + name: 'hubspot_list_campaigns', + description: 'List all marketing campaigns', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of campaigns to return', + default: 20, + }, + offset: { + type: 'number', + description: 'Offset for pagination', + default: 0, + }, + }, + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/marketing/v3/campaigns', + params: { + limit: args.limit || 20, + offset: args.offset || 0, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_campaign', + description: 'Get a specific campaign by ID', + inputSchema: { + type: 'object', + properties: { + campaignId: { + type: 'string', + description: 'Campaign ID', + }, + }, + required: ['campaignId'], + }, + handler: async (args: any) => { + const campaign = await client.apiRequest({ + method: 'GET', + url: `/marketing/v3/campaigns/${args.campaignId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(campaign, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_campaign', + description: 'Create a new marketing campaign', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Campaign name', + }, + }, + required: ['name'], + }, + handler: async (args: any) => { + const campaign = await client.apiRequest({ + method: 'POST', + url: '/marketing/v3/campaigns', + data: { + name: args.name, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + campaignId: campaign.id, + campaign, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_campaign', + description: 'Update an existing campaign', + inputSchema: { + type: 'object', + properties: { + campaignId: { + type: 'string', + description: 'Campaign ID', + }, + name: { + type: 'string', + description: 'Updated campaign name', + }, + }, + required: ['campaignId'], + }, + handler: async (args: any) => { + const campaign = await client.apiRequest({ + method: 'PATCH', + url: `/marketing/v3/campaigns/${args.campaignId}`, + data: { + name: args.name, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + campaign, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_campaign_stats', + description: 'Get statistics for a campaign (performance metrics)', + inputSchema: { + type: 'object', + properties: { + campaignId: { + type: 'string', + description: 'Campaign ID', + }, + }, + required: ['campaignId'], + }, + handler: async (args: any) => { + const stats = await client.apiRequest({ + method: 'GET', + url: `/marketing/v3/campaigns/${args.campaignId}/statistics`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(stats, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/companies.ts b/servers/hubspot/src/tools/companies.ts new file mode 100644 index 0000000..8496681 --- /dev/null +++ b/servers/hubspot/src/tools/companies.ts @@ -0,0 +1,465 @@ +/** + * HubSpot Companies Tools + * Tools for managing companies in HubSpot CRM + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; +import type { CompanyProperties } from '../types/index.js'; + +export function getTools(client: HubSpotClient) { + return [ + { + name: 'hubspot_list_companies', + description: 'List companies with pagination. Returns up to 100 companies per request.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of companies to return (max 100)', + default: 10, + }, + after: { + type: 'string', + description: 'Pagination cursor from previous response', + }, + properties: { + type: 'array', + items: { type: 'string' }, + description: 'Specific properties to retrieve (e.g., ["name", "domain", "industry"])', + }, + }, + }, + handler: async (args: any) => { + const response = await client.listObjects('companies', { + limit: Math.min(args.limit || 10, 100), + after: args.after, + properties: args.properties, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: response.results.length, + companies: response.results, + nextCursor: response.paging?.next?.after, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_company', + description: 'Get a single company by ID with all details', + inputSchema: { + type: 'object', + properties: { + companyId: { + type: 'string', + description: 'HubSpot company ID', + }, + properties: { + type: 'array', + items: { type: 'string' }, + description: 'Specific properties to retrieve', + }, + associations: { + type: 'array', + items: { type: 'string' }, + description: 'Associated object types to include (e.g., ["contacts", "deals"])', + }, + }, + required: ['companyId'], + }, + handler: async (args: any) => { + const company = await client.getObject( + 'companies', + args.companyId, + args.properties, + args.associations + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(company, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_company', + description: 'Create a new company in HubSpot CRM', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Company name (required)', + }, + domain: { + type: 'string', + description: 'Company domain/website', + }, + industry: { + type: 'string', + description: 'Industry', + }, + phone: { + type: 'string', + description: 'Company phone number', + }, + city: { + type: 'string', + description: 'City', + }, + state: { + type: 'string', + description: 'State/region', + }, + country: { + type: 'string', + description: 'Country', + }, + numberofemployees: { + type: 'string', + description: 'Number of employees', + }, + annualrevenue: { + type: 'string', + description: 'Annual revenue', + }, + additionalProperties: { + type: 'object', + description: 'Additional custom properties as key-value pairs', + }, + }, + required: ['name'], + }, + handler: async (args: any) => { + const properties: CompanyProperties = { + name: args.name, + domain: args.domain, + industry: args.industry, + phone: args.phone, + city: args.city, + state: args.state, + country: args.country, + numberofemployees: args.numberofemployees, + annualrevenue: args.annualrevenue, + ...(args.additionalProperties || {}), + }; + + // Remove undefined values + Object.keys(properties).forEach( + (key) => properties[key] === undefined && delete properties[key] + ); + + const company = await client.createObject('companies', properties); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + companyId: company.id, + company, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_company', + description: 'Update an existing company', + inputSchema: { + type: 'object', + properties: { + companyId: { + type: 'string', + description: 'HubSpot company ID', + }, + name: { + type: 'string', + description: 'Updated company name', + }, + domain: { + type: 'string', + description: 'Updated domain', + }, + industry: { + type: 'string', + description: 'Updated industry', + }, + additionalProperties: { + type: 'object', + description: 'Additional properties to update as key-value pairs', + }, + }, + required: ['companyId'], + }, + handler: async (args: any) => { + const properties: Partial = { + name: args.name, + domain: args.domain, + industry: args.industry, + ...(args.additionalProperties || {}), + }; + + // Remove undefined values + Object.keys(properties).forEach( + (key) => properties[key] === undefined && delete properties[key] + ); + + const company = await client.updateObject( + 'companies', + args.companyId, + properties + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + company, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_company', + description: 'Archive (soft delete) a company', + inputSchema: { + type: 'object', + properties: { + companyId: { + type: 'string', + description: 'HubSpot company ID to archive', + }, + }, + required: ['companyId'], + }, + handler: async (args: any) => { + await client.archiveObject('companies', args.companyId); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Company ${args.companyId} archived successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_search_companies', + description: 'Search companies with filters and sorting. Supports complex queries.', + inputSchema: { + type: 'object', + properties: { + filters: { + type: 'array', + items: { + type: 'object', + properties: { + propertyName: { type: 'string' }, + operator: { type: 'string', enum: ['EQ', 'NEQ', 'LT', 'LTE', 'GT', 'GTE', 'CONTAINS_TOKEN', 'HAS_PROPERTY'] }, + value: { type: 'string' }, + }, + required: ['propertyName', 'operator'], + }, + description: 'Array of filters to apply', + }, + query: { + type: 'string', + description: 'Free text search query', + }, + sorts: { + type: 'array', + items: { + type: 'object', + properties: { + propertyName: { type: 'string' }, + direction: { type: 'string', enum: ['ASCENDING', 'DESCENDING'] }, + }, + }, + description: 'Sort criteria', + }, + properties: { + type: 'array', + items: { type: 'string' }, + description: 'Properties to retrieve', + }, + limit: { + type: 'number', + description: 'Maximum results (max 100)', + default: 10, + }, + after: { + type: 'string', + description: 'Pagination cursor', + }, + }, + }, + handler: async (args: any) => { + const searchRequest: any = { + filterGroups: args.filters + ? [{ filters: args.filters }] + : undefined, + query: args.query, + sorts: args.sorts, + properties: args.properties, + limit: Math.min(args.limit || 10, 100), + after: args.after, + }; + + const response = await client.searchObjects('companies', searchRequest); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: response.total, + results: response.results, + nextCursor: response.paging?.next?.after, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_batch_create_companies', + description: 'Create multiple companies in a single batch request (up to 100)', + inputSchema: { + type: 'object', + properties: { + companies: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + domain: { type: 'string' }, + industry: { type: 'string' }, + city: { type: 'string' }, + }, + }, + description: 'Array of company objects to create', + }, + }, + required: ['companies'], + }, + handler: async (args: any) => { + const inputs = args.companies.map((c: any) => ({ properties: c })); + const response = await client.batchCreateObjects('companies', { inputs }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + created: response.results.length, + companies: response.results, + errors: response.errors, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_batch_update_companies', + description: 'Update multiple companies in a single batch request (up to 100)', + inputSchema: { + type: 'object', + properties: { + updates: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Company ID' }, + properties: { type: 'object', description: 'Properties to update' }, + }, + required: ['id', 'properties'], + }, + description: 'Array of company updates', + }, + }, + required: ['updates'], + }, + handler: async (args: any) => { + const response = await client.batchUpdateObjects('companies', { + inputs: args.updates, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + updated: response.results.length, + companies: response.results, + errors: response.errors, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/contacts.ts b/servers/hubspot/src/tools/contacts.ts new file mode 100644 index 0000000..60646f1 --- /dev/null +++ b/servers/hubspot/src/tools/contacts.ts @@ -0,0 +1,460 @@ +/** + * HubSpot Contacts Tools + * Tools for managing contacts in HubSpot CRM + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; +import type { ContactProperties } from '../types/index.js'; + +export function getTools(client: HubSpotClient) { + return [ + { + name: 'hubspot_list_contacts', + description: 'List contacts with pagination. Returns up to 100 contacts per request.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of contacts to return (max 100)', + default: 10, + }, + after: { + type: 'string', + description: 'Pagination cursor from previous response', + }, + properties: { + type: 'array', + items: { type: 'string' }, + description: 'Specific properties to retrieve (e.g., ["email", "firstname", "lastname"])', + }, + }, + }, + handler: async (args: any) => { + const response = await client.listObjects('contacts', { + limit: Math.min(args.limit || 10, 100), + after: args.after, + properties: args.properties, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: response.results.length, + contacts: response.results, + nextCursor: response.paging?.next?.after, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_contact', + description: 'Get a single contact by ID with all details', + inputSchema: { + type: 'object', + properties: { + contactId: { + type: 'string', + description: 'HubSpot contact ID', + }, + properties: { + type: 'array', + items: { type: 'string' }, + description: 'Specific properties to retrieve', + }, + associations: { + type: 'array', + items: { type: 'string' }, + description: 'Associated object types to include (e.g., ["companies", "deals"])', + }, + }, + required: ['contactId'], + }, + handler: async (args: any) => { + const contact = await client.getObject( + 'contacts', + args.contactId, + args.properties, + args.associations + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(contact, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_contact', + description: 'Create a new contact in HubSpot CRM', + inputSchema: { + type: 'object', + properties: { + email: { + type: 'string', + description: 'Contact email address (required for most contacts)', + }, + firstname: { + type: 'string', + description: 'First name', + }, + lastname: { + type: 'string', + description: 'Last name', + }, + phone: { + type: 'string', + description: 'Phone number', + }, + company: { + type: 'string', + description: 'Company name', + }, + jobtitle: { + type: 'string', + description: 'Job title', + }, + lifecyclestage: { + type: 'string', + description: 'Lifecycle stage (e.g., "lead", "opportunity", "customer")', + }, + additionalProperties: { + type: 'object', + description: 'Additional custom properties as key-value pairs', + }, + }, + }, + handler: async (args: any) => { + const properties: ContactProperties = { + email: args.email, + firstname: args.firstname, + lastname: args.lastname, + phone: args.phone, + company: args.company, + jobtitle: args.jobtitle, + lifecyclestage: args.lifecyclestage, + ...(args.additionalProperties || {}), + }; + + // Remove undefined values + Object.keys(properties).forEach( + (key) => properties[key] === undefined && delete properties[key] + ); + + const contact = await client.createObject('contacts', properties); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + contactId: contact.id, + contact, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_contact', + description: 'Update an existing contact', + inputSchema: { + type: 'object', + properties: { + contactId: { + type: 'string', + description: 'HubSpot contact ID', + }, + email: { + type: 'string', + description: 'Updated email address', + }, + firstname: { + type: 'string', + description: 'Updated first name', + }, + lastname: { + type: 'string', + description: 'Updated last name', + }, + phone: { + type: 'string', + description: 'Updated phone number', + }, + additionalProperties: { + type: 'object', + description: 'Additional properties to update as key-value pairs', + }, + }, + required: ['contactId'], + }, + handler: async (args: any) => { + const properties: Partial = { + email: args.email, + firstname: args.firstname, + lastname: args.lastname, + phone: args.phone, + ...(args.additionalProperties || {}), + }; + + // Remove undefined values + Object.keys(properties).forEach( + (key) => properties[key] === undefined && delete properties[key] + ); + + const contact = await client.updateObject( + 'contacts', + args.contactId, + properties + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + contact, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_contact', + description: 'Archive (soft delete) a contact', + inputSchema: { + type: 'object', + properties: { + contactId: { + type: 'string', + description: 'HubSpot contact ID to archive', + }, + }, + required: ['contactId'], + }, + handler: async (args: any) => { + await client.archiveObject('contacts', args.contactId); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Contact ${args.contactId} archived successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_search_contacts', + description: 'Search contacts with filters and sorting. Supports complex queries.', + inputSchema: { + type: 'object', + properties: { + filters: { + type: 'array', + items: { + type: 'object', + properties: { + propertyName: { type: 'string' }, + operator: { type: 'string', enum: ['EQ', 'NEQ', 'LT', 'LTE', 'GT', 'GTE', 'CONTAINS_TOKEN', 'HAS_PROPERTY'] }, + value: { type: 'string' }, + }, + required: ['propertyName', 'operator'], + }, + description: 'Array of filters to apply', + }, + query: { + type: 'string', + description: 'Free text search query', + }, + sorts: { + type: 'array', + items: { + type: 'object', + properties: { + propertyName: { type: 'string' }, + direction: { type: 'string', enum: ['ASCENDING', 'DESCENDING'] }, + }, + }, + description: 'Sort criteria', + }, + properties: { + type: 'array', + items: { type: 'string' }, + description: 'Properties to retrieve', + }, + limit: { + type: 'number', + description: 'Maximum results (max 100)', + default: 10, + }, + after: { + type: 'string', + description: 'Pagination cursor', + }, + }, + }, + handler: async (args: any) => { + const searchRequest: any = { + filterGroups: args.filters + ? [{ filters: args.filters }] + : undefined, + query: args.query, + sorts: args.sorts, + properties: args.properties, + limit: Math.min(args.limit || 10, 100), + after: args.after, + }; + + const response = await client.searchObjects('contacts', searchRequest); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: response.total, + results: response.results, + nextCursor: response.paging?.next?.after, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_batch_create_contacts', + description: 'Create multiple contacts in a single batch request (up to 100)', + inputSchema: { + type: 'object', + properties: { + contacts: { + type: 'array', + items: { + type: 'object', + properties: { + email: { type: 'string' }, + firstname: { type: 'string' }, + lastname: { type: 'string' }, + phone: { type: 'string' }, + company: { type: 'string' }, + }, + }, + description: 'Array of contact objects to create', + }, + }, + required: ['contacts'], + }, + handler: async (args: any) => { + const inputs = args.contacts.map((c: any) => ({ properties: c })); + const response = await client.batchCreateObjects('contacts', { inputs }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + created: response.results.length, + contacts: response.results, + errors: response.errors, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_batch_update_contacts', + description: 'Update multiple contacts in a single batch request (up to 100)', + inputSchema: { + type: 'object', + properties: { + updates: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Contact ID' }, + properties: { type: 'object', description: 'Properties to update' }, + }, + required: ['id', 'properties'], + }, + description: 'Array of contact updates', + }, + }, + required: ['updates'], + }, + handler: async (args: any) => { + const response = await client.batchUpdateObjects('contacts', { + inputs: args.updates, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + updated: response.results.length, + contacts: response.results, + errors: response.errors, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/deals.ts b/servers/hubspot/src/tools/deals.ts new file mode 100644 index 0000000..e08626d --- /dev/null +++ b/servers/hubspot/src/tools/deals.ts @@ -0,0 +1,501 @@ +/** + * HubSpot Deals Tools + * Tools for managing deals in HubSpot CRM + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; +import type { DealProperties } from '../types/index.js'; + +export function getTools(client: HubSpotClient) { + return [ + { + name: 'hubspot_list_deals', + description: 'List deals with pagination. Returns up to 100 deals per request.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of deals to return (max 100)', + default: 10, + }, + after: { + type: 'string', + description: 'Pagination cursor from previous response', + }, + properties: { + type: 'array', + items: { type: 'string' }, + description: 'Specific properties to retrieve (e.g., ["dealname", "amount", "dealstage"])', + }, + }, + }, + handler: async (args: any) => { + const response = await client.listObjects('deals', { + limit: Math.min(args.limit || 10, 100), + after: args.after, + properties: args.properties, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: response.results.length, + deals: response.results, + nextCursor: response.paging?.next?.after, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_deal', + description: 'Get a single deal by ID with all details', + inputSchema: { + type: 'object', + properties: { + dealId: { + type: 'string', + description: 'HubSpot deal ID', + }, + properties: { + type: 'array', + items: { type: 'string' }, + description: 'Specific properties to retrieve', + }, + associations: { + type: 'array', + items: { type: 'string' }, + description: 'Associated object types to include (e.g., ["contacts", "companies"])', + }, + }, + required: ['dealId'], + }, + handler: async (args: any) => { + const deal = await client.getObject( + 'deals', + args.dealId, + args.properties, + args.associations + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(deal, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_deal', + description: 'Create a new deal in HubSpot CRM', + inputSchema: { + type: 'object', + properties: { + dealname: { + type: 'string', + description: 'Deal name (required)', + }, + amount: { + type: 'string', + description: 'Deal amount', + }, + dealstage: { + type: 'string', + description: 'Deal stage ID', + }, + pipeline: { + type: 'string', + description: 'Pipeline ID', + }, + closedate: { + type: 'string', + description: 'Expected close date (ISO format)', + }, + hubspot_owner_id: { + type: 'string', + description: 'Owner/user ID', + }, + description: { + type: 'string', + description: 'Deal description', + }, + additionalProperties: { + type: 'object', + description: 'Additional custom properties as key-value pairs', + }, + }, + required: ['dealname'], + }, + handler: async (args: any) => { + const properties: DealProperties = { + dealname: args.dealname, + amount: args.amount, + dealstage: args.dealstage, + pipeline: args.pipeline, + closedate: args.closedate, + hubspot_owner_id: args.hubspot_owner_id, + description: args.description, + ...(args.additionalProperties || {}), + }; + + // Remove undefined values + Object.keys(properties).forEach( + (key) => properties[key] === undefined && delete properties[key] + ); + + const deal = await client.createObject('deals', properties); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + dealId: deal.id, + deal, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_deal', + description: 'Update an existing deal', + inputSchema: { + type: 'object', + properties: { + dealId: { + type: 'string', + description: 'HubSpot deal ID', + }, + dealname: { + type: 'string', + description: 'Updated deal name', + }, + amount: { + type: 'string', + description: 'Updated amount', + }, + dealstage: { + type: 'string', + description: 'Updated deal stage ID', + }, + closedate: { + type: 'string', + description: 'Updated close date', + }, + additionalProperties: { + type: 'object', + description: 'Additional properties to update as key-value pairs', + }, + }, + required: ['dealId'], + }, + handler: async (args: any) => { + const properties: Partial = { + dealname: args.dealname, + amount: args.amount, + dealstage: args.dealstage, + closedate: args.closedate, + ...(args.additionalProperties || {}), + }; + + // Remove undefined values + Object.keys(properties).forEach( + (key) => properties[key] === undefined && delete properties[key] + ); + + const deal = await client.updateObject( + 'deals', + args.dealId, + properties + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + deal, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_deal', + description: 'Archive (soft delete) a deal', + inputSchema: { + type: 'object', + properties: { + dealId: { + type: 'string', + description: 'HubSpot deal ID to archive', + }, + }, + required: ['dealId'], + }, + handler: async (args: any) => { + await client.archiveObject('deals', args.dealId); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Deal ${args.dealId} archived successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_move_deal_stage', + description: 'Move a deal to a different stage in the pipeline', + inputSchema: { + type: 'object', + properties: { + dealId: { + type: 'string', + description: 'HubSpot deal ID', + }, + dealstage: { + type: 'string', + description: 'New deal stage ID to move to', + }, + }, + required: ['dealId', 'dealstage'], + }, + handler: async (args: any) => { + const deal = await client.updateObject('deals', args.dealId, { + dealstage: args.dealstage, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Deal ${args.dealId} moved to stage ${args.dealstage}`, + deal, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_search_deals', + description: 'Search deals with filters and sorting. Supports complex queries.', + inputSchema: { + type: 'object', + properties: { + filters: { + type: 'array', + items: { + type: 'object', + properties: { + propertyName: { type: 'string' }, + operator: { type: 'string', enum: ['EQ', 'NEQ', 'LT', 'LTE', 'GT', 'GTE', 'CONTAINS_TOKEN', 'HAS_PROPERTY'] }, + value: { type: 'string' }, + }, + required: ['propertyName', 'operator'], + }, + description: 'Array of filters to apply', + }, + query: { + type: 'string', + description: 'Free text search query', + }, + sorts: { + type: 'array', + items: { + type: 'object', + properties: { + propertyName: { type: 'string' }, + direction: { type: 'string', enum: ['ASCENDING', 'DESCENDING'] }, + }, + }, + description: 'Sort criteria', + }, + properties: { + type: 'array', + items: { type: 'string' }, + description: 'Properties to retrieve', + }, + limit: { + type: 'number', + description: 'Maximum results (max 100)', + default: 10, + }, + after: { + type: 'string', + description: 'Pagination cursor', + }, + }, + }, + handler: async (args: any) => { + const searchRequest: any = { + filterGroups: args.filters + ? [{ filters: args.filters }] + : undefined, + query: args.query, + sorts: args.sorts, + properties: args.properties, + limit: Math.min(args.limit || 10, 100), + after: args.after, + }; + + const response = await client.searchObjects('deals', searchRequest); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: response.total, + results: response.results, + nextCursor: response.paging?.next?.after, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_batch_create_deals', + description: 'Create multiple deals in a single batch request (up to 100)', + inputSchema: { + type: 'object', + properties: { + deals: { + type: 'array', + items: { + type: 'object', + properties: { + dealname: { type: 'string' }, + amount: { type: 'string' }, + dealstage: { type: 'string' }, + pipeline: { type: 'string' }, + }, + }, + description: 'Array of deal objects to create', + }, + }, + required: ['deals'], + }, + handler: async (args: any) => { + const inputs = args.deals.map((d: any) => ({ properties: d })); + const response = await client.batchCreateObjects('deals', { inputs }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + created: response.results.length, + deals: response.results, + errors: response.errors, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_batch_update_deals', + description: 'Update multiple deals in a single batch request (up to 100)', + inputSchema: { + type: 'object', + properties: { + updates: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Deal ID' }, + properties: { type: 'object', description: 'Properties to update' }, + }, + required: ['id', 'properties'], + }, + description: 'Array of deal updates', + }, + }, + required: ['updates'], + }, + handler: async (args: any) => { + const response = await client.batchUpdateObjects('deals', { + inputs: args.updates, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + updated: response.results.length, + deals: response.results, + errors: response.errors, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/emails.ts b/servers/hubspot/src/tools/emails.ts new file mode 100644 index 0000000..98a3955 --- /dev/null +++ b/servers/hubspot/src/tools/emails.ts @@ -0,0 +1,301 @@ +/** + * HubSpot Marketing Emails Tools + * Tools for managing marketing emails in HubSpot + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; + +export function getTools(client: HubSpotClient) { + return [ + { + name: 'hubspot_list_emails', + description: 'List marketing emails with pagination', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of emails to return', + default: 20, + }, + offset: { + type: 'number', + description: 'Offset for pagination', + default: 0, + }, + }, + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/marketing/v3/emails', + params: { + limit: args.limit || 20, + offset: args.offset || 0, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_email', + description: 'Get a specific marketing email by ID', + inputSchema: { + type: 'object', + properties: { + emailId: { + type: 'string', + description: 'Marketing email ID', + }, + }, + required: ['emailId'], + }, + handler: async (args: any) => { + const email = await client.apiRequest({ + method: 'GET', + url: `/marketing/v3/emails/${args.emailId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(email, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_email', + description: 'Create a new marketing email', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Email name', + }, + subject: { + type: 'string', + description: 'Email subject line', + }, + fromName: { + type: 'string', + description: 'Sender name', + }, + replyTo: { + type: 'string', + description: 'Reply-to email address', + }, + }, + required: ['name', 'subject'], + }, + handler: async (args: any) => { + const email = await client.apiRequest({ + method: 'POST', + url: '/marketing/v3/emails', + data: { + name: args.name, + subject: args.subject, + fromName: args.fromName, + replyTo: args.replyTo, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + emailId: email.id, + email, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_email', + description: 'Update an existing marketing email', + inputSchema: { + type: 'object', + properties: { + emailId: { + type: 'string', + description: 'Email ID to update', + }, + name: { + type: 'string', + description: 'Updated email name', + }, + subject: { + type: 'string', + description: 'Updated subject line', + }, + fromName: { + type: 'string', + description: 'Updated sender name', + }, + }, + required: ['emailId'], + }, + handler: async (args: any) => { + const updates: any = {}; + if (args.name) updates.name = args.name; + if (args.subject) updates.subject = args.subject; + if (args.fromName) updates.fromName = args.fromName; + + const email = await client.apiRequest({ + method: 'PATCH', + url: `/marketing/v3/emails/${args.emailId}`, + data: updates, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + email, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_send_email', + description: 'Send or schedule a marketing email', + inputSchema: { + type: 'object', + properties: { + emailId: { + type: 'string', + description: 'Email ID to send', + }, + }, + required: ['emailId'], + }, + handler: async (args: any) => { + const result = await client.apiRequest({ + method: 'POST', + url: `/marketing/v3/emails/${args.emailId}/send`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: 'Email sent successfully', + result, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_clone_email', + description: 'Clone/duplicate an existing marketing email', + inputSchema: { + type: 'object', + properties: { + emailId: { + type: 'string', + description: 'Email ID to clone', + }, + name: { + type: 'string', + description: 'Name for the cloned email', + }, + }, + required: ['emailId'], + }, + handler: async (args: any) => { + const cloned = await client.apiRequest({ + method: 'POST', + url: `/marketing/v3/emails/${args.emailId}/clone`, + data: args.name ? { name: args.name } : {}, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + clonedEmailId: cloned.id, + email: cloned, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_email_stats', + description: 'Get statistics for a marketing email (sends, opens, clicks)', + inputSchema: { + type: 'object', + properties: { + emailId: { + type: 'string', + description: 'Email ID', + }, + }, + required: ['emailId'], + }, + handler: async (args: any) => { + const stats = await client.apiRequest({ + method: 'GET', + url: `/marketing/v3/emails/${args.emailId}/statistics`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(stats, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/engagements.ts b/servers/hubspot/src/tools/engagements.ts new file mode 100644 index 0000000..86f19e0 --- /dev/null +++ b/servers/hubspot/src/tools/engagements.ts @@ -0,0 +1,685 @@ +/** + * HubSpot Engagements Tools + * Tools for managing engagements: notes, calls, emails, meetings, tasks + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; +import type { + NoteProperties, + CallProperties, + EmailEngagementProperties, + MeetingProperties, + TaskProperties, +} from '../types/index.js'; + +export function getTools(client: HubSpotClient) { + return [ + // Notes + { + name: 'hubspot_list_notes', + description: 'List engagement notes', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of notes to return (max 100)', + default: 10, + }, + after: { + type: 'string', + description: 'Pagination cursor', + }, + }, + }, + handler: async (args: any) => { + const response = await client.listObjects('notes', { + limit: Math.min(args.limit || 10, 100), + after: args.after, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: response.results.length, + notes: response.results, + nextCursor: response.paging?.next?.after, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_note', + description: 'Get a specific note by ID', + inputSchema: { + type: 'object', + properties: { + noteId: { + type: 'string', + description: 'Note ID', + }, + }, + required: ['noteId'], + }, + handler: async (args: any) => { + const note = await client.getObject('notes', args.noteId); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(note, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_note', + description: 'Create a new engagement note', + inputSchema: { + type: 'object', + properties: { + note_body: { + type: 'string', + description: 'Note content/body', + }, + hs_timestamp: { + type: 'string', + description: 'Timestamp (ISO format)', + }, + hubspot_owner_id: { + type: 'string', + description: 'Owner ID', + }, + }, + required: ['note_body'], + }, + handler: async (args: any) => { + const properties: any = { + hs_note_body: args.note_body, + hs_timestamp: args.hs_timestamp || new Date().toISOString(), + hubspot_owner_id: args.hubspot_owner_id, + }; + + Object.keys(properties).forEach( + (key) => properties[key] === undefined && delete properties[key] + ); + + const note = await client.createObject('notes', properties); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + noteId: note.id, + note, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_note', + description: 'Update an existing note', + inputSchema: { + type: 'object', + properties: { + noteId: { + type: 'string', + description: 'Note ID', + }, + note_body: { + type: 'string', + description: 'Updated note content', + }, + }, + required: ['noteId'], + }, + handler: async (args: any) => { + const properties: any = {}; + if (args.note_body) properties.hs_note_body = args.note_body; + + const note = await client.updateObject('notes', args.noteId, properties); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + note, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_note', + description: 'Archive a note', + inputSchema: { + type: 'object', + properties: { + noteId: { + type: 'string', + description: 'Note ID to archive', + }, + }, + required: ['noteId'], + }, + handler: async (args: any) => { + await client.archiveObject('notes', args.noteId); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Note ${args.noteId} archived successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + // Calls + { + name: 'hubspot_list_calls', + description: 'List engagement calls', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of calls to return (max 100)', + default: 10, + }, + after: { + type: 'string', + description: 'Pagination cursor', + }, + }, + }, + handler: async (args: any) => { + const response = await client.listObjects('calls', { + limit: Math.min(args.limit || 10, 100), + after: args.after, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: response.results.length, + calls: response.results, + nextCursor: response.paging?.next?.after, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_call', + description: 'Create a new call engagement', + inputSchema: { + type: 'object', + properties: { + title: { + type: 'string', + description: 'Call title', + }, + body: { + type: 'string', + description: 'Call notes/description', + }, + duration: { + type: 'string', + description: 'Duration in milliseconds', + }, + status: { + type: 'string', + description: 'Call status (e.g., COMPLETED, SCHEDULED)', + }, + direction: { + type: 'string', + description: 'INBOUND or OUTBOUND', + }, + hs_timestamp: { + type: 'string', + description: 'Timestamp (ISO format)', + }, + }, + }, + handler: async (args: any) => { + const properties: any = { + hs_call_title: args.title, + hs_call_body: args.body, + hs_call_duration: args.duration, + hs_call_status: args.status, + hs_call_direction: args.direction, + hs_timestamp: args.hs_timestamp || new Date().toISOString(), + }; + + Object.keys(properties).forEach( + (key) => properties[key] === undefined && delete properties[key] + ); + + const call = await client.createObject('calls', properties); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + callId: call.id, + call, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_call', + description: 'Archive a call engagement', + inputSchema: { + type: 'object', + properties: { + callId: { + type: 'string', + description: 'Call ID to archive', + }, + }, + required: ['callId'], + }, + handler: async (args: any) => { + await client.archiveObject('calls', args.callId); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Call ${args.callId} archived successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + // Meetings + { + name: 'hubspot_list_meetings', + description: 'List engagement meetings', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of meetings to return (max 100)', + default: 10, + }, + after: { + type: 'string', + description: 'Pagination cursor', + }, + }, + }, + handler: async (args: any) => { + const response = await client.listObjects('meetings', { + limit: Math.min(args.limit || 10, 100), + after: args.after, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: response.results.length, + meetings: response.results, + nextCursor: response.paging?.next?.after, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_meeting', + description: 'Create a new meeting engagement', + inputSchema: { + type: 'object', + properties: { + title: { + type: 'string', + description: 'Meeting title', + }, + body: { + type: 'string', + description: 'Meeting notes/description', + }, + start_time: { + type: 'string', + description: 'Start time (ISO format)', + }, + end_time: { + type: 'string', + description: 'End time (ISO format)', + }, + outcome: { + type: 'string', + description: 'Meeting outcome', + }, + }, + }, + handler: async (args: any) => { + const properties: any = { + hs_meeting_title: args.title, + hs_meeting_body: args.body, + hs_meeting_start_time: args.start_time, + hs_meeting_end_time: args.end_time, + hs_meeting_outcome: args.outcome, + hs_timestamp: args.start_time || new Date().toISOString(), + }; + + Object.keys(properties).forEach( + (key) => properties[key] === undefined && delete properties[key] + ); + + const meeting = await client.createObject('meetings', properties); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + meetingId: meeting.id, + meeting, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_meeting', + description: 'Archive a meeting engagement', + inputSchema: { + type: 'object', + properties: { + meetingId: { + type: 'string', + description: 'Meeting ID to archive', + }, + }, + required: ['meetingId'], + }, + handler: async (args: any) => { + await client.archiveObject('meetings', args.meetingId); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Meeting ${args.meetingId} archived successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + // Tasks + { + name: 'hubspot_list_tasks', + description: 'List engagement tasks', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of tasks to return (max 100)', + default: 10, + }, + after: { + type: 'string', + description: 'Pagination cursor', + }, + }, + }, + handler: async (args: any) => { + const response = await client.listObjects('tasks', { + limit: Math.min(args.limit || 10, 100), + after: args.after, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: response.results.length, + tasks: response.results, + nextCursor: response.paging?.next?.after, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_task', + description: 'Create a new task engagement', + inputSchema: { + type: 'object', + properties: { + subject: { + type: 'string', + description: 'Task subject', + }, + body: { + type: 'string', + description: 'Task description', + }, + status: { + type: 'string', + description: 'Task status (e.g., NOT_STARTED, IN_PROGRESS, COMPLETED)', + }, + priority: { + type: 'string', + description: 'Task priority (e.g., LOW, MEDIUM, HIGH)', + }, + task_type: { + type: 'string', + description: 'Task type (e.g., TODO, EMAIL, CALL)', + }, + }, + }, + handler: async (args: any) => { + const properties: any = { + hs_task_subject: args.subject, + hs_task_body: args.body, + hs_task_status: args.status, + hs_task_priority: args.priority, + hs_task_type: args.task_type, + hs_timestamp: new Date().toISOString(), + }; + + Object.keys(properties).forEach( + (key) => properties[key] === undefined && delete properties[key] + ); + + const task = await client.createObject('tasks', properties); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + taskId: task.id, + task, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_task', + description: 'Update an existing task', + inputSchema: { + type: 'object', + properties: { + taskId: { + type: 'string', + description: 'Task ID', + }, + status: { + type: 'string', + description: 'Updated task status', + }, + priority: { + type: 'string', + description: 'Updated priority', + }, + }, + required: ['taskId'], + }, + handler: async (args: any) => { + const properties: any = {}; + if (args.status) properties.hs_task_status = args.status; + if (args.priority) properties.hs_task_priority = args.priority; + + const task = await client.updateObject('tasks', args.taskId, properties); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + task, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_task', + description: 'Archive a task engagement', + inputSchema: { + type: 'object', + properties: { + taskId: { + type: 'string', + description: 'Task ID to archive', + }, + }, + required: ['taskId'], + }, + handler: async (args: any) => { + await client.archiveObject('tasks', args.taskId); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Task ${args.taskId} archived successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/forms.ts b/servers/hubspot/src/tools/forms.ts new file mode 100644 index 0000000..baa129d --- /dev/null +++ b/servers/hubspot/src/tools/forms.ts @@ -0,0 +1,265 @@ +/** + * HubSpot Forms Tools + * Tools for managing forms and form submissions + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; + +export function getTools(client: HubSpotClient) { + return [ + { + name: 'hubspot_list_forms', + description: 'List all forms', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of forms to return', + default: 20, + }, + offset: { + type: 'number', + description: 'Offset for pagination', + default: 0, + }, + }, + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/marketing/v3/forms', + params: { + limit: args.limit || 20, + after: args.offset || 0, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_form', + description: 'Get a specific form by ID', + inputSchema: { + type: 'object', + properties: { + formId: { + type: 'string', + description: 'Form ID', + }, + }, + required: ['formId'], + }, + handler: async (args: any) => { + const form = await client.apiRequest({ + method: 'GET', + url: `/marketing/v3/forms/${args.formId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(form, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_form', + description: 'Create a new form', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Form name', + }, + formType: { + type: 'string', + description: 'Form type (e.g., "hubspot")', + }, + submitText: { + type: 'string', + description: 'Submit button text', + }, + redirect: { + type: 'string', + description: 'Redirect URL after submission', + }, + }, + required: ['name'], + }, + handler: async (args: any) => { + const form = await client.apiRequest({ + method: 'POST', + url: '/marketing/v3/forms', + data: { + name: args.name, + formType: args.formType || 'hubspot', + submitText: args.submitText || 'Submit', + redirect: args.redirect, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + formId: form.id, + form, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_form', + description: 'Update an existing form', + inputSchema: { + type: 'object', + properties: { + formId: { + type: 'string', + description: 'Form ID', + }, + name: { + type: 'string', + description: 'Updated form name', + }, + submitText: { + type: 'string', + description: 'Updated submit button text', + }, + }, + required: ['formId'], + }, + handler: async (args: any) => { + const updates: any = {}; + if (args.name) updates.name = args.name; + if (args.submitText) updates.submitText = args.submitText; + + const form = await client.apiRequest({ + method: 'PATCH', + url: `/marketing/v3/forms/${args.formId}`, + data: updates, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + form, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_form', + description: 'Archive a form', + inputSchema: { + type: 'object', + properties: { + formId: { + type: 'string', + description: 'Form ID to archive', + }, + }, + required: ['formId'], + }, + handler: async (args: any) => { + await client.apiRequest({ + method: 'DELETE', + url: `/marketing/v3/forms/${args.formId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Form ${args.formId} archived successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_form_submissions', + description: 'Get submissions for a specific form', + inputSchema: { + type: 'object', + properties: { + formId: { + type: 'string', + description: 'Form ID', + }, + limit: { + type: 'number', + description: 'Maximum number of submissions to return', + default: 20, + }, + after: { + type: 'string', + description: 'Pagination cursor', + }, + }, + required: ['formId'], + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: `/form-integrations/v1/submissions/forms/${args.formId}`, + params: { + limit: args.limit || 20, + after: args.after, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/lists.ts b/servers/hubspot/src/tools/lists.ts new file mode 100644 index 0000000..100fa71 --- /dev/null +++ b/servers/hubspot/src/tools/lists.ts @@ -0,0 +1,357 @@ +/** + * HubSpot Lists Tools + * Tools for managing contact lists (static and active/dynamic) + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; + +export function getTools(client: HubSpotClient) { + return [ + { + name: 'hubspot_list_lists', + description: 'List all contact lists', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of lists to return', + default: 20, + }, + offset: { + type: 'number', + description: 'Offset for pagination', + default: 0, + }, + }, + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/contacts/v1/lists', + params: { + count: args.limit || 20, + offset: args.offset || 0, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_list', + description: 'Get a specific contact list by ID', + inputSchema: { + type: 'object', + properties: { + listId: { + type: 'string', + description: 'List ID', + }, + }, + required: ['listId'], + }, + handler: async (args: any) => { + const list = await client.apiRequest({ + method: 'GET', + url: `/contacts/v1/lists/${args.listId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(list, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_static_list', + description: 'Create a new static contact list', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'List name', + }, + description: { + type: 'string', + description: 'List description', + }, + }, + required: ['name'], + }, + handler: async (args: any) => { + const list = await client.apiRequest({ + method: 'POST', + url: '/contacts/v1/lists', + data: { + name: args.name, + dynamic: false, + description: args.description, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + listId: list.listId, + list, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_active_list', + description: 'Create a new active/dynamic contact list with filters', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'List name', + }, + description: { + type: 'string', + description: 'List description', + }, + filters: { + type: 'array', + description: 'Array of filter criteria', + }, + }, + required: ['name'], + }, + handler: async (args: any) => { + const list = await client.apiRequest({ + method: 'POST', + url: '/contacts/v1/lists', + data: { + name: args.name, + dynamic: true, + description: args.description, + filters: args.filters || [], + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + listId: list.listId, + list, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_list', + description: 'Update an existing contact list', + inputSchema: { + type: 'object', + properties: { + listId: { + type: 'string', + description: 'List ID', + }, + name: { + type: 'string', + description: 'Updated list name', + }, + description: { + type: 'string', + description: 'Updated description', + }, + }, + required: ['listId'], + }, + handler: async (args: any) => { + const updates: any = {}; + if (args.name) updates.name = args.name; + if (args.description) updates.description = args.description; + + const list = await client.apiRequest({ + method: 'POST', + url: `/contacts/v1/lists/${args.listId}`, + data: updates, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + list, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_list', + description: 'Delete a contact list', + inputSchema: { + type: 'object', + properties: { + listId: { + type: 'string', + description: 'List ID to delete', + }, + }, + required: ['listId'], + }, + handler: async (args: any) => { + await client.apiRequest({ + method: 'DELETE', + url: `/contacts/v1/lists/${args.listId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `List ${args.listId} deleted successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_add_contacts_to_list', + description: 'Add contacts to a static list', + inputSchema: { + type: 'object', + properties: { + listId: { + type: 'string', + description: 'List ID', + }, + contactIds: { + type: 'array', + items: { type: 'string' }, + description: 'Array of contact IDs to add', + }, + }, + required: ['listId', 'contactIds'], + }, + handler: async (args: any) => { + const result = await client.apiRequest({ + method: 'POST', + url: `/contacts/v1/lists/${args.listId}/add`, + data: { + vids: args.contactIds.map((id: string) => parseInt(id, 10)), + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Added ${args.contactIds.length} contacts to list ${args.listId}`, + result, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_remove_contacts_from_list', + description: 'Remove contacts from a static list', + inputSchema: { + type: 'object', + properties: { + listId: { + type: 'string', + description: 'List ID', + }, + contactIds: { + type: 'array', + items: { type: 'string' }, + description: 'Array of contact IDs to remove', + }, + }, + required: ['listId', 'contactIds'], + }, + handler: async (args: any) => { + const result = await client.apiRequest({ + method: 'POST', + url: `/contacts/v1/lists/${args.listId}/remove`, + data: { + vids: args.contactIds.map((id: string) => parseInt(id, 10)), + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Removed ${args.contactIds.length} contacts from list ${args.listId}`, + result, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/pipelines.ts b/servers/hubspot/src/tools/pipelines.ts new file mode 100644 index 0000000..eeb07cc --- /dev/null +++ b/servers/hubspot/src/tools/pipelines.ts @@ -0,0 +1,475 @@ +/** + * HubSpot Pipelines Tools + * Tools for managing pipelines and pipeline stages (deals + tickets) + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; + +export function getTools(client: HubSpotClient) { + return [ + // Deal Pipelines + { + name: 'hubspot_list_deal_pipelines', + description: 'List all deal pipelines', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async () => { + const pipelines = await client.getPipelines('deals'); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: pipelines.length, + pipelines, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_deal_pipeline', + description: 'Get a specific deal pipeline by ID', + inputSchema: { + type: 'object', + properties: { + pipelineId: { + type: 'string', + description: 'Pipeline ID', + }, + }, + required: ['pipelineId'], + }, + handler: async (args: any) => { + const pipeline = await client.apiRequest({ + method: 'GET', + url: `/crm/v3/pipelines/deals/${args.pipelineId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(pipeline, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_deal_pipeline', + description: 'Create a new deal pipeline', + inputSchema: { + type: 'object', + properties: { + label: { + type: 'string', + description: 'Pipeline label/name', + }, + displayOrder: { + type: 'number', + description: 'Display order', + }, + stages: { + type: 'array', + items: { + type: 'object', + properties: { + label: { type: 'string' }, + displayOrder: { type: 'number' }, + metadata: { type: 'object' }, + }, + }, + description: 'Array of pipeline stages', + }, + }, + required: ['label'], + }, + handler: async (args: any) => { + const pipeline = await client.apiRequest({ + method: 'POST', + url: '/crm/v3/pipelines/deals', + data: { + label: args.label, + displayOrder: args.displayOrder || 0, + stages: args.stages || [], + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + pipelineId: pipeline.id, + pipeline, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_deal_pipeline', + description: 'Update an existing deal pipeline', + inputSchema: { + type: 'object', + properties: { + pipelineId: { + type: 'string', + description: 'Pipeline ID', + }, + label: { + type: 'string', + description: 'Updated label', + }, + displayOrder: { + type: 'number', + description: 'Updated display order', + }, + }, + required: ['pipelineId'], + }, + handler: async (args: any) => { + const updates: any = {}; + if (args.label) updates.label = args.label; + if (args.displayOrder !== undefined) updates.displayOrder = args.displayOrder; + + const pipeline = await client.apiRequest({ + method: 'PATCH', + url: `/crm/v3/pipelines/deals/${args.pipelineId}`, + data: updates, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + pipeline, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_deal_pipeline', + description: 'Archive a deal pipeline', + inputSchema: { + type: 'object', + properties: { + pipelineId: { + type: 'string', + description: 'Pipeline ID to archive', + }, + }, + required: ['pipelineId'], + }, + handler: async (args: any) => { + await client.apiRequest({ + method: 'DELETE', + url: `/crm/v3/pipelines/deals/${args.pipelineId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Pipeline ${args.pipelineId} archived successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_deal_stage', + description: 'Create a new stage in a deal pipeline', + inputSchema: { + type: 'object', + properties: { + pipelineId: { + type: 'string', + description: 'Pipeline ID', + }, + label: { + type: 'string', + description: 'Stage label/name', + }, + displayOrder: { + type: 'number', + description: 'Display order', + }, + metadata: { + type: 'object', + description: 'Stage metadata', + }, + }, + required: ['pipelineId', 'label'], + }, + handler: async (args: any) => { + const stage = await client.apiRequest({ + method: 'POST', + url: `/crm/v3/pipelines/deals/${args.pipelineId}/stages`, + data: { + label: args.label, + displayOrder: args.displayOrder || 0, + metadata: args.metadata || {}, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + stageId: stage.id, + stage, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + // Ticket Pipelines + { + name: 'hubspot_list_ticket_pipelines', + description: 'List all ticket pipelines', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async () => { + const pipelines = await client.getPipelines('tickets'); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: pipelines.length, + pipelines, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_ticket_pipeline', + description: 'Get a specific ticket pipeline by ID', + inputSchema: { + type: 'object', + properties: { + pipelineId: { + type: 'string', + description: 'Pipeline ID', + }, + }, + required: ['pipelineId'], + }, + handler: async (args: any) => { + const pipeline = await client.apiRequest({ + method: 'GET', + url: `/crm/v3/pipelines/tickets/${args.pipelineId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(pipeline, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_ticket_pipeline', + description: 'Create a new ticket pipeline', + inputSchema: { + type: 'object', + properties: { + label: { + type: 'string', + description: 'Pipeline label/name', + }, + displayOrder: { + type: 'number', + description: 'Display order', + }, + stages: { + type: 'array', + items: { + type: 'object', + properties: { + label: { type: 'string' }, + displayOrder: { type: 'number' }, + metadata: { type: 'object' }, + }, + }, + description: 'Array of pipeline stages', + }, + }, + required: ['label'], + }, + handler: async (args: any) => { + const pipeline = await client.apiRequest({ + method: 'POST', + url: '/crm/v3/pipelines/tickets', + data: { + label: args.label, + displayOrder: args.displayOrder || 0, + stages: args.stages || [], + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + pipelineId: pipeline.id, + pipeline, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_ticket_pipeline', + description: 'Update an existing ticket pipeline', + inputSchema: { + type: 'object', + properties: { + pipelineId: { + type: 'string', + description: 'Pipeline ID', + }, + label: { + type: 'string', + description: 'Updated label', + }, + displayOrder: { + type: 'number', + description: 'Updated display order', + }, + }, + required: ['pipelineId'], + }, + handler: async (args: any) => { + const updates: any = {}; + if (args.label) updates.label = args.label; + if (args.displayOrder !== undefined) updates.displayOrder = args.displayOrder; + + const pipeline = await client.apiRequest({ + method: 'PATCH', + url: `/crm/v3/pipelines/tickets/${args.pipelineId}`, + data: updates, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + pipeline, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_ticket_pipeline', + description: 'Archive a ticket pipeline', + inputSchema: { + type: 'object', + properties: { + pipelineId: { + type: 'string', + description: 'Pipeline ID to archive', + }, + }, + required: ['pipelineId'], + }, + handler: async (args: any) => { + await client.apiRequest({ + method: 'DELETE', + url: `/crm/v3/pipelines/tickets/${args.pipelineId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Pipeline ${args.pipelineId} archived successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/tickets.ts b/servers/hubspot/src/tools/tickets.ts new file mode 100644 index 0000000..cdc89b5 --- /dev/null +++ b/servers/hubspot/src/tools/tickets.ts @@ -0,0 +1,368 @@ +/** + * HubSpot Tickets Tools + * Tools for managing tickets (service/support) in HubSpot CRM + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; +import type { TicketProperties } from '../types/index.js'; + +export function getTools(client: HubSpotClient) { + return [ + { + name: 'hubspot_list_tickets', + description: 'List tickets with pagination. Returns up to 100 tickets per request.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of tickets to return (max 100)', + default: 10, + }, + after: { + type: 'string', + description: 'Pagination cursor from previous response', + }, + properties: { + type: 'array', + items: { type: 'string' }, + description: 'Specific properties to retrieve (e.g., ["subject", "content", "hs_ticket_priority"])', + }, + }, + }, + handler: async (args: any) => { + const response = await client.listObjects('tickets', { + limit: Math.min(args.limit || 10, 100), + after: args.after, + properties: args.properties, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: response.results.length, + tickets: response.results, + nextCursor: response.paging?.next?.after, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_ticket', + description: 'Get a single ticket by ID with all details', + inputSchema: { + type: 'object', + properties: { + ticketId: { + type: 'string', + description: 'HubSpot ticket ID', + }, + properties: { + type: 'array', + items: { type: 'string' }, + description: 'Specific properties to retrieve', + }, + associations: { + type: 'array', + items: { type: 'string' }, + description: 'Associated object types to include (e.g., ["contacts", "companies"])', + }, + }, + required: ['ticketId'], + }, + handler: async (args: any) => { + const ticket = await client.getObject( + 'tickets', + args.ticketId, + args.properties, + args.associations + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(ticket, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_ticket', + description: 'Create a new ticket in HubSpot', + inputSchema: { + type: 'object', + properties: { + subject: { + type: 'string', + description: 'Ticket subject (required)', + }, + content: { + type: 'string', + description: 'Ticket content/description', + }, + hs_ticket_priority: { + type: 'string', + description: 'Priority (e.g., "LOW", "MEDIUM", "HIGH")', + }, + hs_pipeline_stage: { + type: 'string', + description: 'Pipeline stage ID', + }, + hs_pipeline: { + type: 'string', + description: 'Pipeline ID', + }, + hubspot_owner_id: { + type: 'string', + description: 'Owner/user ID', + }, + hs_ticket_category: { + type: 'string', + description: 'Ticket category', + }, + additionalProperties: { + type: 'object', + description: 'Additional custom properties as key-value pairs', + }, + }, + required: ['subject'], + }, + handler: async (args: any) => { + const properties: TicketProperties = { + subject: args.subject, + content: args.content, + hs_ticket_priority: args.hs_ticket_priority, + hs_pipeline_stage: args.hs_pipeline_stage, + hs_pipeline: args.hs_pipeline, + hubspot_owner_id: args.hubspot_owner_id, + hs_ticket_category: args.hs_ticket_category, + ...(args.additionalProperties || {}), + }; + + // Remove undefined values + Object.keys(properties).forEach( + (key) => properties[key] === undefined && delete properties[key] + ); + + const ticket = await client.createObject('tickets', properties); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + ticketId: ticket.id, + ticket, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_ticket', + description: 'Update an existing ticket', + inputSchema: { + type: 'object', + properties: { + ticketId: { + type: 'string', + description: 'HubSpot ticket ID', + }, + subject: { + type: 'string', + description: 'Updated subject', + }, + content: { + type: 'string', + description: 'Updated content', + }, + hs_ticket_priority: { + type: 'string', + description: 'Updated priority', + }, + hs_pipeline_stage: { + type: 'string', + description: 'Updated pipeline stage', + }, + additionalProperties: { + type: 'object', + description: 'Additional properties to update as key-value pairs', + }, + }, + required: ['ticketId'], + }, + handler: async (args: any) => { + const properties: Partial = { + subject: args.subject, + content: args.content, + hs_ticket_priority: args.hs_ticket_priority, + hs_pipeline_stage: args.hs_pipeline_stage, + ...(args.additionalProperties || {}), + }; + + // Remove undefined values + Object.keys(properties).forEach( + (key) => properties[key] === undefined && delete properties[key] + ); + + const ticket = await client.updateObject( + 'tickets', + args.ticketId, + properties + ); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + ticket, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_ticket', + description: 'Archive (soft delete) a ticket', + inputSchema: { + type: 'object', + properties: { + ticketId: { + type: 'string', + description: 'HubSpot ticket ID to archive', + }, + }, + required: ['ticketId'], + }, + handler: async (args: any) => { + await client.archiveObject('tickets', args.ticketId); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Ticket ${args.ticketId} archived successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_search_tickets', + description: 'Search tickets with filters and sorting. Supports complex queries.', + inputSchema: { + type: 'object', + properties: { + filters: { + type: 'array', + items: { + type: 'object', + properties: { + propertyName: { type: 'string' }, + operator: { type: 'string', enum: ['EQ', 'NEQ', 'LT', 'LTE', 'GT', 'GTE', 'CONTAINS_TOKEN', 'HAS_PROPERTY'] }, + value: { type: 'string' }, + }, + required: ['propertyName', 'operator'], + }, + description: 'Array of filters to apply', + }, + query: { + type: 'string', + description: 'Free text search query', + }, + sorts: { + type: 'array', + items: { + type: 'object', + properties: { + propertyName: { type: 'string' }, + direction: { type: 'string', enum: ['ASCENDING', 'DESCENDING'] }, + }, + }, + description: 'Sort criteria', + }, + properties: { + type: 'array', + items: { type: 'string' }, + description: 'Properties to retrieve', + }, + limit: { + type: 'number', + description: 'Maximum results (max 100)', + default: 10, + }, + after: { + type: 'string', + description: 'Pagination cursor', + }, + }, + }, + handler: async (args: any) => { + const searchRequest: any = { + filterGroups: args.filters + ? [{ filters: args.filters }] + : undefined, + query: args.query, + sorts: args.sorts, + properties: args.properties, + limit: Math.min(args.limit || 10, 100), + after: args.after, + }; + + const response = await client.searchObjects('tickets', searchRequest); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: response.total, + results: response.results, + nextCursor: response.paging?.next?.after, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/webhooks.ts b/servers/hubspot/src/tools/webhooks.ts new file mode 100644 index 0000000..b8e95cc --- /dev/null +++ b/servers/hubspot/src/tools/webhooks.ts @@ -0,0 +1,230 @@ +/** + * HubSpot Webhooks Tools + * Tools for managing webhook subscriptions + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; + +export function getTools(client: HubSpotClient) { + return [ + { + name: 'hubspot_list_webhooks', + description: 'List all webhook subscriptions', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of webhooks to return', + default: 20, + }, + after: { + type: 'string', + description: 'Pagination cursor', + }, + }, + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/webhooks/v3/subscriptions', + params: { + limit: args.limit || 20, + after: args.after, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_webhook', + description: 'Get a specific webhook subscription by ID', + inputSchema: { + type: 'object', + properties: { + webhookId: { + type: 'string', + description: 'Webhook subscription ID', + }, + }, + required: ['webhookId'], + }, + handler: async (args: any) => { + const webhook = await client.apiRequest({ + method: 'GET', + url: `/webhooks/v3/subscriptions/${args.webhookId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(webhook, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_webhook', + description: 'Create a new webhook subscription', + inputSchema: { + type: 'object', + properties: { + eventType: { + type: 'string', + description: 'Event type to subscribe to (e.g., contact.creation, deal.propertyChange)', + }, + targetUrl: { + type: 'string', + description: 'Target URL to receive webhook notifications', + }, + active: { + type: 'boolean', + description: 'Whether the webhook is active', + default: true, + }, + propertyName: { + type: 'string', + description: 'Specific property name (for propertyChange events)', + }, + }, + required: ['eventType', 'targetUrl'], + }, + handler: async (args: any) => { + const data: any = { + eventType: args.eventType, + active: args.active !== false, + }; + + // For propertyChange events, include propertyName + if (args.propertyName) { + data.propertyName = args.propertyName; + } + + const webhook = await client.apiRequest({ + method: 'POST', + url: '/webhooks/v3/subscriptions', + data: { + ...data, + webhookUrl: args.targetUrl, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + webhookId: webhook.id, + webhook, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_update_webhook', + description: 'Update an existing webhook subscription', + inputSchema: { + type: 'object', + properties: { + webhookId: { + type: 'string', + description: 'Webhook subscription ID', + }, + active: { + type: 'boolean', + description: 'Whether the webhook is active', + }, + targetUrl: { + type: 'string', + description: 'Updated target URL', + }, + }, + required: ['webhookId'], + }, + handler: async (args: any) => { + const updates: any = {}; + if (args.active !== undefined) updates.active = args.active; + if (args.targetUrl) updates.webhookUrl = args.targetUrl; + + const webhook = await client.apiRequest({ + method: 'PATCH', + url: `/webhooks/v3/subscriptions/${args.webhookId}`, + data: updates, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + webhook, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_delete_webhook', + description: 'Delete a webhook subscription', + inputSchema: { + type: 'object', + properties: { + webhookId: { + type: 'string', + description: 'Webhook subscription ID to delete', + }, + }, + required: ['webhookId'], + }, + handler: async (args: any) => { + await client.apiRequest({ + method: 'DELETE', + url: `/webhooks/v3/subscriptions/${args.webhookId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Webhook ${args.webhookId} deleted successfully`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/hubspot/src/tools/workflows.ts b/servers/hubspot/src/tools/workflows.ts new file mode 100644 index 0000000..efcb600 --- /dev/null +++ b/servers/hubspot/src/tools/workflows.ts @@ -0,0 +1,249 @@ +/** + * HubSpot Workflows Tools + * Tools for managing workflows (automation) + */ + +import type { HubSpotClient } from '../clients/hubspot.js'; + +export function getTools(client: HubSpotClient) { + return [ + { + name: 'hubspot_list_workflows', + description: 'List all workflows', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of workflows to return', + default: 20, + }, + offset: { + type: 'number', + description: 'Offset for pagination', + default: 0, + }, + }, + }, + handler: async (args: any) => { + const response = await client.apiRequest({ + method: 'GET', + url: '/automation/v3/workflows', + params: { + limit: args.limit || 20, + offset: args.offset || 0, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_get_workflow', + description: 'Get a specific workflow by ID', + inputSchema: { + type: 'object', + properties: { + workflowId: { + type: 'string', + description: 'Workflow ID', + }, + }, + required: ['workflowId'], + }, + handler: async (args: any) => { + const workflow = await client.apiRequest({ + method: 'GET', + url: `/automation/v3/workflows/${args.workflowId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(workflow, null, 2), + }, + ], + }; + }, + }, + + { + name: 'hubspot_create_workflow', + description: 'Create a new workflow', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Workflow name', + }, + type: { + type: 'string', + description: 'Workflow type (e.g., CONTACT_WORKFLOW)', + }, + enabled: { + type: 'boolean', + description: 'Whether to activate immediately', + default: false, + }, + }, + required: ['name', 'type'], + }, + handler: async (args: any) => { + const workflow = await client.apiRequest({ + method: 'POST', + url: '/automation/v3/workflows', + data: { + name: args.name, + type: args.type, + enabled: args.enabled || false, + }, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + workflowId: workflow.id, + workflow, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_activate_workflow', + description: 'Activate a workflow', + inputSchema: { + type: 'object', + properties: { + workflowId: { + type: 'string', + description: 'Workflow ID to activate', + }, + }, + required: ['workflowId'], + }, + handler: async (args: any) => { + const workflow = await client.apiRequest({ + method: 'POST', + url: `/automation/v3/workflows/${args.workflowId}/activate`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Workflow ${args.workflowId} activated successfully`, + workflow, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_deactivate_workflow', + description: 'Deactivate a workflow', + inputSchema: { + type: 'object', + properties: { + workflowId: { + type: 'string', + description: 'Workflow ID to deactivate', + }, + }, + required: ['workflowId'], + }, + handler: async (args: any) => { + const workflow = await client.apiRequest({ + method: 'POST', + url: `/automation/v3/workflows/${args.workflowId}/deactivate`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Workflow ${args.workflowId} deactivated successfully`, + workflow, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + name: 'hubspot_enroll_contact_in_workflow', + description: 'Manually enroll a contact in a workflow', + inputSchema: { + type: 'object', + properties: { + workflowId: { + type: 'string', + description: 'Workflow ID', + }, + contactId: { + type: 'string', + description: 'Contact ID to enroll', + }, + }, + required: ['workflowId', 'contactId'], + }, + handler: async (args: any) => { + const result = await client.apiRequest({ + method: 'POST', + url: `/automation/v2/workflows/${args.workflowId}/enrollments/contacts/${args.contactId}`, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + message: `Contact ${args.contactId} enrolled in workflow ${args.workflowId}`, + result, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/quickbooks/TOOLS_SUMMARY.md b/servers/quickbooks/TOOLS_SUMMARY.md new file mode 100644 index 0000000..d572495 --- /dev/null +++ b/servers/quickbooks/TOOLS_SUMMARY.md @@ -0,0 +1,163 @@ +# QuickBooks Online MCP Server - Tools Summary + +## Overview +Built 14 tool files with **105 total tools** for comprehensive QuickBooks Online integration. + +## Tool Files Created + +### 1. `src/tools/invoices.ts` (7 tools) +- `qbo_list_invoices` - List with filters (customer, amount range) +- `qbo_get_invoice` - Get by ID +- `qbo_create_invoice` - Create with line items +- `qbo_update_invoice` - Update (requires SyncToken) +- `qbo_void_invoice` - Void transaction +- `qbo_delete_invoice` - Delete (soft) +- `qbo_query_invoices` - Custom SQL-like queries + +### 2. `src/tools/customers.ts` (6 tools) +- `qbo_list_customers` - List with pagination +- `qbo_get_customer` - Get by ID +- `qbo_create_customer` - Create with full details +- `qbo_update_customer` - Update (requires SyncToken) +- `qbo_search_customers` - Search by name/email +- `qbo_query_customers` - Custom SQL-like queries + +### 3. `src/tools/payments.ts` (9 tools) +- `qbo_list_payments` - List payments +- `qbo_get_payment` - Get by ID +- `qbo_create_payment` - Create with linked invoices +- `qbo_update_payment` - Update +- `qbo_void_payment` - Void +- `qbo_delete_payment` - Delete +- `qbo_list_credit_memos` - List credit memos +- `qbo_get_credit_memo` - Get by ID +- `qbo_create_credit_memo` - Create + +### 4. `src/tools/estimates.ts` (10 tools) +- `qbo_list_estimates` - List estimates +- `qbo_get_estimate` - Get by ID +- `qbo_create_estimate` - Create +- `qbo_update_estimate` - Update +- `qbo_delete_estimate` - Delete +- `qbo_send_estimate` - Email to customer +- `qbo_list_sales_receipts` - List sales receipts +- `qbo_get_sales_receipt` - Get by ID +- `qbo_create_sales_receipt` - Create +- `qbo_delete_sales_receipt` - Delete + +### 5. `src/tools/bills.ts` (9 tools) +- `qbo_list_bills` - List with filters +- `qbo_get_bill` - Get by ID +- `qbo_create_bill` - Create +- `qbo_update_bill` - Update +- `qbo_delete_bill` - Delete +- `qbo_list_bill_payments` - List bill payments +- `qbo_get_bill_payment` - Get by ID +- `qbo_create_bill_payment` - Create with linked bills +- `qbo_delete_bill_payment` - Delete + +### 6. `src/tools/vendors.ts` (6 tools) +- `qbo_list_vendors` - List with pagination +- `qbo_get_vendor` - Get by ID +- `qbo_create_vendor` - Create with 1099 support +- `qbo_update_vendor` - Update +- `qbo_search_vendors` - Search by name +- `qbo_query_vendors` - Custom SQL-like queries + +### 7. `src/tools/items.ts` (6 tools) +- `qbo_list_items` - List all item types +- `qbo_get_item` - Get by ID +- `qbo_create_item` - Create (inventory/non-inventory/service/bundle) +- `qbo_update_item` - Update +- `qbo_search_items` - Search by name +- `qbo_query_items` - Custom SQL-like queries + +### 8. `src/tools/accounts.ts` (5 tools) +- `qbo_list_accounts` - List chart of accounts +- `qbo_get_account` - Get by ID +- `qbo_create_account` - Create with sub-account support +- `qbo_update_account` - Update +- `qbo_query_accounts` - Custom SQL-like queries + +### 9. `src/tools/reports.ts` (7 tools) +- `qbo_run_profit_loss` - P&L report +- `qbo_run_balance_sheet` - Balance sheet +- `qbo_run_cash_flow` - Cash flow statement +- `qbo_run_ar_aging` - AR aging summary +- `qbo_run_ap_aging` - AP aging summary +- `qbo_run_trial_balance` - Trial balance +- `qbo_run_general_ledger` - General ledger + +### 10. `src/tools/employees.ts` (5 tools) +- `qbo_list_employees` - List employees +- `qbo_get_employee` - Get by ID +- `qbo_create_employee` - Create with billable time support +- `qbo_update_employee` - Update +- `qbo_query_employees` - Custom SQL-like queries + +### 11. `src/tools/time-activities.ts` (5 tools) +- `qbo_list_time_activities` - List with filters +- `qbo_get_time_activity` - Get by ID +- `qbo_create_time_activity` - Create with billable status +- `qbo_update_time_activity` - Update +- `qbo_delete_time_activity` - Delete + +### 12. `src/tools/taxes.ts` (8 tools) +- `qbo_list_tax_codes` - List tax codes +- `qbo_get_tax_code` - Get by ID +- `qbo_query_tax_codes` - Custom queries +- `qbo_list_tax_rates` - List tax rates +- `qbo_get_tax_rate` - Get by ID +- `qbo_query_tax_rates` - Custom queries +- `qbo_list_tax_agencies` - List tax agencies +- `qbo_get_tax_agency` - Get by ID + +### 13. `src/tools/purchases.ts` (9 tools) +- `qbo_list_purchases` - List purchases +- `qbo_get_purchase` - Get by ID +- `qbo_create_purchase` - Create (expense/check/credit card) +- `qbo_update_purchase` - Update +- `qbo_delete_purchase` - Delete +- `qbo_list_purchase_orders` - List POs +- `qbo_get_purchase_order` - Get by ID +- `qbo_create_purchase_order` - Create +- `qbo_delete_purchase_order` - Delete + +### 14. `src/tools/journal-entries.ts` (13 tools) +- `qbo_list_journal_entries` - List JEs +- `qbo_get_journal_entry` - Get by ID +- `qbo_create_journal_entry` - Create (balanced debits/credits) +- `qbo_update_journal_entry` - Update +- `qbo_delete_journal_entry` - Delete +- `qbo_list_deposits` - List deposits +- `qbo_get_deposit` - Get by ID +- `qbo_create_deposit` - Create +- `qbo_delete_deposit` - Delete +- `qbo_list_transfers` - List transfers +- `qbo_get_transfer` - Get by ID +- `qbo_create_transfer` - Create account transfer +- `qbo_delete_transfer` - Delete + +## QBO Specifics Implemented + +✓ **SyncToken** - Required for all updates (optimistic locking) +✓ **SQL-like queries** - Full support for `SELECT * FROM Entity WHERE ...` +✓ **Pagination** - 1-indexed startPosition + maxResults (max 1000) +✓ **Report endpoints** - Special handling via `getReport` method +✓ **Void operations** - Proper handling via delete with operation parameter +✓ **Entity reads** - Standard `/company/{realmId}/{entityType}/{id}` pattern + +## Tool Naming Convention +All tools follow the pattern: `qbo_verb_noun` +- Examples: `qbo_list_invoices`, `qbo_create_customer`, `qbo_run_profit_loss` + +## TypeScript Compliance +✓ All files compile without errors (`npx tsc --noEmit`) +✓ Proper imports from `../clients/quickbooks.js` +✓ Type-safe handlers with QuickBooks types from `../types/index.js` + +## Next Steps +1. Update `src/server.ts` to import and register all tool files +2. Test with actual QuickBooks sandbox credentials +3. Add integration tests +4. Update README with tool documentation diff --git a/servers/quickbooks/src/tools/accounts.ts b/servers/quickbooks/src/tools/accounts.ts new file mode 100644 index 0000000..ce1391e --- /dev/null +++ b/servers/quickbooks/src/tools/accounts.ts @@ -0,0 +1,181 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { Account } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_accounts', + description: 'List all accounts (chart of accounts) with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + accountType: { + type: 'string', + description: 'Filter by account type (e.g., Bank, Expense, Income, Asset, Liability, Equity)', + }, + active: { + type: 'boolean', + description: 'Filter by active status', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, accountType, active } = args; + + let query = 'SELECT * FROM Account'; + const conditions: string[] = []; + + if (accountType) conditions.push(`AccountType = '${accountType}'`); + if (active !== undefined) conditions.push(`Active = ${active}`); + + if (conditions.length > 0) { + query += ' WHERE ' + conditions.join(' AND '); + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_account', + description: 'Get a specific account by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Account ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('Account', args.id); + }, + }, + { + name: 'qbo_create_account', + description: 'Create a new account', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Account name', + }, + accountType: { + type: 'string', + description: 'Account type (e.g., Bank, Expense, Income, Asset, Liability, Equity, CreditCard)', + }, + accountSubType: { + type: 'string', + description: 'Account sub-type (e.g., CashOnHand, Checking, Savings, etc.)', + }, + description: { + type: 'string', + description: 'Account description', + }, + currentBalance: { + type: 'number', + description: 'Current balance (for some account types)', + }, + parentAccountId: { + type: 'string', + description: 'Parent account ID (for sub-accounts)', + }, + }, + required: ['name', 'accountType'], + }, + handler: async (args: any) => { + const account: any = { + Name: args.name, + AccountType: args.accountType, + }; + + if (args.accountSubType) account.AccountSubType = args.accountSubType; + if (args.description) account.Description = args.description; + if (args.currentBalance !== undefined) account.CurrentBalance = args.currentBalance; + if (args.parentAccountId) account.ParentRef = { value: args.parentAccountId }; + + return await client.create('Account', account); + }, + }, + { + name: 'qbo_update_account', + description: 'Update an existing account (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Account ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the account', + }, + name: { + type: 'string', + description: 'Account name', + }, + description: { + type: 'string', + description: 'Account description', + }, + active: { + type: 'boolean', + description: 'Active status', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + const account: any = { + Id: args.id, + SyncToken: args.syncToken, + }; + + if (args.name) account.Name = args.name; + if (args.description) account.Description = args.description; + if (args.active !== undefined) account.Active = args.active; + + return await client.update('Account', account); + }, + }, + { + name: 'qbo_query_accounts', + description: 'Run a custom SQL-like query on accounts', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'SQL-like query (e.g., "SELECT * FROM Account WHERE AccountType = \'Bank\'")', + }, + startPosition: { + type: 'number', + description: 'Starting position (default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results (default 100)', + }, + }, + required: ['query'], + }, + handler: async (args: any) => { + return await client.query(args.query, { + startPosition: args.startPosition || 1, + maxResults: args.maxResults || 100, + }); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/bills.ts b/servers/quickbooks/src/tools/bills.ts new file mode 100644 index 0000000..c5f0e63 --- /dev/null +++ b/servers/quickbooks/src/tools/bills.ts @@ -0,0 +1,303 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { Bill, BillPayment } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_bills', + description: 'List bills with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + vendorId: { + type: 'string', + description: 'Filter by vendor ID', + }, + minAmount: { + type: 'number', + description: 'Minimum total amount', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, vendorId, minAmount } = args; + + let query = 'SELECT * FROM Bill'; + const conditions: string[] = []; + + if (vendorId) conditions.push(`VendorRef = '${vendorId}'`); + if (minAmount !== undefined) conditions.push(`TotalAmt >= '${minAmount}'`); + + if (conditions.length > 0) { + query += ' WHERE ' + conditions.join(' AND '); + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_bill', + description: 'Get a specific bill by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Bill ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('Bill', args.id); + }, + }, + { + name: 'qbo_create_bill', + description: 'Create a new bill', + inputSchema: { + type: 'object', + properties: { + vendorId: { + type: 'string', + description: 'Vendor reference ID', + }, + lines: { + type: 'array', + description: 'Bill line items', + items: { + type: 'object', + properties: { + amount: { type: 'number' }, + description: { type: 'string' }, + accountId: { type: 'string' }, + itemId: { type: 'string' }, + }, + }, + }, + dueDate: { + type: 'string', + description: 'Due date (YYYY-MM-DD)', + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + }, + required: ['vendorId', 'lines'], + }, + handler: async (args: any) => { + const bill: any = { + VendorRef: { value: args.vendorId }, + Line: args.lines.map((line: any) => ({ + Amount: line.amount, + DetailType: line.itemId ? 'ItemBasedExpenseLineDetail' : 'AccountBasedExpenseLineDetail', + Description: line.description, + ...(line.itemId + ? { ItemBasedExpenseLineDetail: { ItemRef: { value: line.itemId } } } + : { AccountBasedExpenseLineDetail: { AccountRef: { value: line.accountId } } } + ), + })), + }; + + if (args.dueDate) bill.DueDate = args.dueDate; + if (args.txnDate) bill.TxnDate = args.txnDate; + + return await client.create('Bill', bill); + }, + }, + { + name: 'qbo_update_bill', + description: 'Update an existing bill (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Bill ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the bill', + }, + lines: { + type: 'array', + description: 'Updated line items', + }, + dueDate: { + type: 'string', + description: 'Due date (YYYY-MM-DD)', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + const bill: any = { + Id: args.id, + SyncToken: args.syncToken, + }; + + if (args.lines) bill.Line = args.lines; + if (args.dueDate) bill.DueDate = args.dueDate; + + return await client.update('Bill', bill); + }, + }, + { + name: 'qbo_delete_bill', + description: 'Delete a bill (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Bill ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the bill', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('Bill', args.id, args.syncToken); + }, + }, + { + name: 'qbo_list_bill_payments', + description: 'List bill payments with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + vendorId: { + type: 'string', + description: 'Filter by vendor ID', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, vendorId } = args; + + let query = 'SELECT * FROM BillPayment'; + if (vendorId) { + query += ` WHERE VendorRef = '${vendorId}'`; + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_bill_payment', + description: 'Get a specific bill payment by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Bill payment ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('BillPayment', args.id); + }, + }, + { + name: 'qbo_create_bill_payment', + description: 'Create a new bill payment', + inputSchema: { + type: 'object', + properties: { + vendorId: { + type: 'string', + description: 'Vendor reference ID', + }, + totalAmount: { + type: 'number', + description: 'Total payment amount', + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + paymentAccountId: { + type: 'string', + description: 'Payment account ID (bank/credit card)', + }, + linkedBills: { + type: 'array', + description: 'Bills to link to this payment', + items: { + type: 'object', + properties: { + billId: { type: 'string' }, + amount: { type: 'number' }, + }, + }, + }, + }, + required: ['vendorId', 'totalAmount', 'paymentAccountId'], + }, + handler: async (args: any) => { + const billPayment: any = { + VendorRef: { value: args.vendorId }, + TotalAmt: args.totalAmount, + APAccountRef: { value: args.paymentAccountId }, + }; + + if (args.txnDate) billPayment.TxnDate = args.txnDate; + + if (args.linkedBills && args.linkedBills.length > 0) { + billPayment.Line = args.linkedBills.map((bill: any) => ({ + Amount: bill.amount, + LinkedTxn: [{ + TxnId: bill.billId, + TxnType: 'Bill', + }], + })); + } + + return await client.create('BillPayment', billPayment); + }, + }, + { + name: 'qbo_delete_bill_payment', + description: 'Delete a bill payment (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Bill payment ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the bill payment', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('BillPayment', args.id, args.syncToken); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/customers.ts b/servers/quickbooks/src/tools/customers.ts new file mode 100644 index 0000000..0458346 --- /dev/null +++ b/servers/quickbooks/src/tools/customers.ts @@ -0,0 +1,236 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { Customer } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_customers', + description: 'List all customers with pagination', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + active: { + type: 'boolean', + description: 'Filter by active status', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, active } = args; + + let query = 'SELECT * FROM Customer'; + if (active !== undefined) { + query += ` WHERE Active = ${active}`; + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_customer', + description: 'Get a specific customer by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Customer ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('Customer', args.id); + }, + }, + { + name: 'qbo_create_customer', + description: 'Create a new customer', + inputSchema: { + type: 'object', + properties: { + displayName: { + type: 'string', + description: 'Display name for the customer', + }, + givenName: { + type: 'string', + description: 'First name', + }, + familyName: { + type: 'string', + description: 'Last name', + }, + companyName: { + type: 'string', + description: 'Company name', + }, + primaryEmail: { + type: 'string', + description: 'Primary email address', + }, + primaryPhone: { + type: 'string', + description: 'Primary phone number', + }, + billAddress: { + type: 'object', + description: 'Billing address', + properties: { + line1: { type: 'string' }, + city: { type: 'string' }, + countrySubDivisionCode: { type: 'string' }, + postalCode: { type: 'string' }, + }, + }, + notes: { + type: 'string', + description: 'Customer notes', + }, + }, + required: ['displayName'], + }, + handler: async (args: any) => { + const customer: Partial = { + DisplayName: args.displayName, + }; + + if (args.givenName) customer.GivenName = args.givenName; + if (args.familyName) customer.FamilyName = args.familyName; + if (args.companyName) customer.CompanyName = args.companyName; + if (args.primaryEmail) customer.PrimaryEmailAddr = { Address: args.primaryEmail }; + if (args.primaryPhone) customer.PrimaryPhone = { FreeFormNumber: args.primaryPhone }; + if (args.billAddress) customer.BillAddr = args.billAddress; + if (args.notes) customer.Notes = args.notes; + + return await client.create('Customer', customer); + }, + }, + { + name: 'qbo_update_customer', + description: 'Update an existing customer (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Customer ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the customer', + }, + displayName: { + type: 'string', + description: 'Display name', + }, + givenName: { + type: 'string', + description: 'First name', + }, + familyName: { + type: 'string', + description: 'Last name', + }, + primaryEmail: { + type: 'string', + description: 'Primary email address', + }, + primaryPhone: { + type: 'string', + description: 'Primary phone number', + }, + active: { + type: 'boolean', + description: 'Active status', + }, + notes: { + type: 'string', + description: 'Customer notes', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + const customer: any = { + Id: args.id, + SyncToken: args.syncToken, + }; + + if (args.displayName) customer.DisplayName = args.displayName; + if (args.givenName) customer.GivenName = args.givenName; + if (args.familyName) customer.FamilyName = args.familyName; + if (args.primaryEmail) customer.PrimaryEmailAddr = { Address: args.primaryEmail }; + if (args.primaryPhone) customer.PrimaryPhone = { FreeFormNumber: args.primaryPhone }; + if (args.active !== undefined) customer.Active = args.active; + if (args.notes) customer.Notes = args.notes; + + return await client.update('Customer', customer); + }, + }, + { + name: 'qbo_search_customers', + description: 'Search customers by name or email', + inputSchema: { + type: 'object', + properties: { + searchTerm: { + type: 'string', + description: 'Search term to match against customer name or email', + }, + startPosition: { + type: 'number', + description: 'Starting position (default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results (default 100)', + }, + }, + required: ['searchTerm'], + }, + handler: async (args: any) => { + const { searchTerm, startPosition = 1, maxResults = 100 } = args; + const query = `SELECT * FROM Customer WHERE DisplayName LIKE '%${searchTerm}%'`; + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_query_customers', + description: 'Run a custom SQL-like query on customers', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'SQL-like query (e.g., "SELECT * FROM Customer WHERE Active = true")', + }, + startPosition: { + type: 'number', + description: 'Starting position (default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results (default 100)', + }, + }, + required: ['query'], + }, + handler: async (args: any) => { + return await client.query(args.query, { + startPosition: args.startPosition || 1, + maxResults: args.maxResults || 100, + }); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/employees.ts b/servers/quickbooks/src/tools/employees.ts new file mode 100644 index 0000000..e2dadcb --- /dev/null +++ b/servers/quickbooks/src/tools/employees.ts @@ -0,0 +1,197 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { Employee } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_employees', + description: 'List all employees with pagination', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + active: { + type: 'boolean', + description: 'Filter by active status', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, active } = args; + + let query = 'SELECT * FROM Employee'; + if (active !== undefined) { + query += ` WHERE Active = ${active}`; + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_employee', + description: 'Get a specific employee by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Employee ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('Employee', args.id); + }, + }, + { + name: 'qbo_create_employee', + description: 'Create a new employee', + inputSchema: { + type: 'object', + properties: { + givenName: { + type: 'string', + description: 'First name', + }, + familyName: { + type: 'string', + description: 'Last name', + }, + displayName: { + type: 'string', + description: 'Display name (optional, auto-generated if not provided)', + }, + primaryEmail: { + type: 'string', + description: 'Primary email address', + }, + primaryPhone: { + type: 'string', + description: 'Primary phone number', + }, + hiredDate: { + type: 'string', + description: 'Hire date (YYYY-MM-DD)', + }, + employeeNumber: { + type: 'string', + description: 'Employee number', + }, + ssn: { + type: 'string', + description: 'Social Security Number (encrypted)', + }, + billableTime: { + type: 'boolean', + description: 'Whether employee can be billed for time', + }, + }, + required: ['givenName', 'familyName'], + }, + handler: async (args: any) => { + const employee: any = { + GivenName: args.givenName, + FamilyName: args.familyName, + }; + + if (args.displayName) employee.DisplayName = args.displayName; + if (args.primaryEmail) employee.PrimaryEmailAddr = { Address: args.primaryEmail }; + if (args.primaryPhone) employee.PrimaryPhone = { FreeFormNumber: args.primaryPhone }; + if (args.hiredDate) employee.HiredDate = args.hiredDate; + if (args.employeeNumber) employee.EmployeeNumber = args.employeeNumber; + if (args.ssn) employee.SSN = args.ssn; + if (args.billableTime !== undefined) employee.BillableTime = args.billableTime; + + return await client.create('Employee', employee); + }, + }, + { + name: 'qbo_update_employee', + description: 'Update an existing employee (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Employee ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the employee', + }, + displayName: { + type: 'string', + description: 'Display name', + }, + primaryEmail: { + type: 'string', + description: 'Primary email address', + }, + primaryPhone: { + type: 'string', + description: 'Primary phone number', + }, + active: { + type: 'boolean', + description: 'Active status', + }, + billableTime: { + type: 'boolean', + description: 'Whether employee can be billed for time', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + const employee: any = { + Id: args.id, + SyncToken: args.syncToken, + }; + + if (args.displayName) employee.DisplayName = args.displayName; + if (args.primaryEmail) employee.PrimaryEmailAddr = { Address: args.primaryEmail }; + if (args.primaryPhone) employee.PrimaryPhone = { FreeFormNumber: args.primaryPhone }; + if (args.active !== undefined) employee.Active = args.active; + if (args.billableTime !== undefined) employee.BillableTime = args.billableTime; + + return await client.update('Employee', employee); + }, + }, + { + name: 'qbo_query_employees', + description: 'Run a custom SQL-like query on employees', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'SQL-like query (e.g., "SELECT * FROM Employee WHERE Active = true")', + }, + startPosition: { + type: 'number', + description: 'Starting position (default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results (default 100)', + }, + }, + required: ['query'], + }, + handler: async (args: any) => { + return await client.query(args.query, { + startPosition: args.startPosition || 1, + maxResults: args.maxResults || 100, + }); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/estimates.ts b/servers/quickbooks/src/tools/estimates.ts new file mode 100644 index 0000000..3b0ecaf --- /dev/null +++ b/servers/quickbooks/src/tools/estimates.ts @@ -0,0 +1,319 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { Estimate, SalesReceipt } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_estimates', + description: 'List estimates with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + customerId: { + type: 'string', + description: 'Filter by customer ID', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, customerId } = args; + + let query = 'SELECT * FROM Estimate'; + if (customerId) { + query += ` WHERE CustomerRef = '${customerId}'`; + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_estimate', + description: 'Get a specific estimate by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Estimate ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('Estimate', args.id); + }, + }, + { + name: 'qbo_create_estimate', + description: 'Create a new estimate', + inputSchema: { + type: 'object', + properties: { + customerId: { + type: 'string', + description: 'Customer reference ID', + }, + lines: { + type: 'array', + description: 'Estimate line items', + items: { + type: 'object', + properties: { + amount: { type: 'number' }, + description: { type: 'string' }, + itemId: { type: 'string' }, + quantity: { type: 'number' }, + unitPrice: { type: 'number' }, + }, + }, + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + expirationDate: { + type: 'string', + description: 'Expiration date (YYYY-MM-DD)', + }, + }, + required: ['customerId', 'lines'], + }, + handler: async (args: any) => { + const estimate: any = { + CustomerRef: { value: args.customerId }, + Line: args.lines.map((line: any) => ({ + Amount: line.amount, + DetailType: 'SalesItemLineDetail', + Description: line.description, + SalesItemLineDetail: { + ItemRef: { value: line.itemId }, + Qty: line.quantity, + UnitPrice: line.unitPrice, + }, + })), + }; + + if (args.txnDate) estimate.TxnDate = args.txnDate; + if (args.expirationDate) estimate.ExpirationDate = args.expirationDate; + + return await client.create('Estimate', estimate); + }, + }, + { + name: 'qbo_update_estimate', + description: 'Update an existing estimate (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Estimate ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the estimate', + }, + lines: { + type: 'array', + description: 'Updated line items', + }, + expirationDate: { + type: 'string', + description: 'Expiration date (YYYY-MM-DD)', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + const estimate: any = { + Id: args.id, + SyncToken: args.syncToken, + }; + + if (args.lines) estimate.Line = args.lines; + if (args.expirationDate) estimate.ExpirationDate = args.expirationDate; + + return await client.update('Estimate', estimate); + }, + }, + { + name: 'qbo_delete_estimate', + description: 'Delete an estimate (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Estimate ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the estimate', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('Estimate', args.id, args.syncToken); + }, + }, + { + name: 'qbo_send_estimate', + description: 'Send an estimate via email', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Estimate ID', + }, + email: { + type: 'string', + description: 'Email address to send to (optional)', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + const endpoint = `estimate/${args.id}/send${args.email ? `?sendTo=${args.email}` : ''}`; + return await client.getReport('SendInvoice', { id: args.id }); + }, + }, + { + name: 'qbo_list_sales_receipts', + description: 'List sales receipts with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + customerId: { + type: 'string', + description: 'Filter by customer ID', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, customerId } = args; + + let query = 'SELECT * FROM SalesReceipt'; + if (customerId) { + query += ` WHERE CustomerRef = '${customerId}'`; + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_sales_receipt', + description: 'Get a specific sales receipt by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Sales receipt ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('SalesReceipt', args.id); + }, + }, + { + name: 'qbo_create_sales_receipt', + description: 'Create a new sales receipt', + inputSchema: { + type: 'object', + properties: { + customerId: { + type: 'string', + description: 'Customer reference ID', + }, + lines: { + type: 'array', + description: 'Sales receipt line items', + items: { + type: 'object', + properties: { + amount: { type: 'number' }, + description: { type: 'string' }, + itemId: { type: 'string' }, + quantity: { type: 'number' }, + }, + }, + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + paymentMethodId: { + type: 'string', + description: 'Payment method reference ID', + }, + depositToAccountId: { + type: 'string', + description: 'Deposit to account ID', + }, + }, + required: ['customerId', 'lines'], + }, + handler: async (args: any) => { + const salesReceipt: any = { + CustomerRef: { value: args.customerId }, + Line: args.lines.map((line: any) => ({ + Amount: line.amount, + DetailType: 'SalesItemLineDetail', + Description: line.description, + SalesItemLineDetail: { + ItemRef: { value: line.itemId }, + Qty: line.quantity, + }, + })), + }; + + if (args.txnDate) salesReceipt.TxnDate = args.txnDate; + if (args.paymentMethodId) salesReceipt.PaymentMethodRef = { value: args.paymentMethodId }; + if (args.depositToAccountId) salesReceipt.DepositToAccountRef = { value: args.depositToAccountId }; + + return await client.create('SalesReceipt', salesReceipt); + }, + }, + { + name: 'qbo_delete_sales_receipt', + description: 'Delete a sales receipt (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Sales receipt ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the sales receipt', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('SalesReceipt', args.id, args.syncToken); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/invoices.ts b/servers/quickbooks/src/tools/invoices.ts new file mode 100644 index 0000000..4ef6599 --- /dev/null +++ b/servers/quickbooks/src/tools/invoices.ts @@ -0,0 +1,252 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { Invoice } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_invoices', + description: 'List invoices with optional filters. Returns paginated results.', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + customerId: { + type: 'string', + description: 'Filter by customer ID', + }, + minAmount: { + type: 'number', + description: 'Minimum total amount', + }, + maxAmount: { + type: 'number', + description: 'Maximum total amount', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, customerId, minAmount, maxAmount } = args; + + let query = 'SELECT * FROM Invoice'; + const conditions: string[] = []; + + if (customerId) conditions.push(`CustomerRef = '${customerId}'`); + if (minAmount !== undefined) conditions.push(`TotalAmt >= '${minAmount}'`); + if (maxAmount !== undefined) conditions.push(`TotalAmt <= '${maxAmount}'`); + + if (conditions.length > 0) { + query += ' WHERE ' + conditions.join(' AND '); + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_invoice', + description: 'Get a specific invoice by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Invoice ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('Invoice', args.id); + }, + }, + { + name: 'qbo_create_invoice', + description: 'Create a new invoice', + inputSchema: { + type: 'object', + properties: { + customerId: { + type: 'string', + description: 'Customer reference ID', + }, + lines: { + type: 'array', + description: 'Invoice line items', + items: { + type: 'object', + properties: { + amount: { type: 'number' }, + description: { type: 'string' }, + itemId: { type: 'string' }, + quantity: { type: 'number' }, + unitPrice: { type: 'number' }, + }, + }, + }, + dueDate: { + type: 'string', + description: 'Due date (YYYY-MM-DD)', + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + emailStatus: { + type: 'string', + description: 'Email status (NotSet, NeedToSend, EmailSent)', + }, + billEmail: { + type: 'string', + description: 'Customer email for billing', + }, + }, + required: ['customerId', 'lines'], + }, + handler: async (args: any) => { + const invoice: Partial = { + CustomerRef: { value: args.customerId }, + Line: args.lines.map((line: any) => ({ + Amount: line.amount, + DetailType: 'SalesItemLineDetail', + Description: line.description, + SalesItemLineDetail: { + ItemRef: { value: line.itemId }, + Qty: line.quantity, + UnitPrice: line.unitPrice, + }, + })), + }; + + if (args.dueDate) invoice.DueDate = args.dueDate; + if (args.txnDate) invoice.TxnDate = args.txnDate; + if (args.emailStatus) invoice.EmailStatus = args.emailStatus; + if (args.billEmail) invoice.BillEmail = { Address: args.billEmail }; + + return await client.create('Invoice', invoice); + }, + }, + { + name: 'qbo_update_invoice', + description: 'Update an existing invoice (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Invoice ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the invoice (for optimistic locking)', + }, + customerId: { + type: 'string', + description: 'Customer reference ID', + }, + lines: { + type: 'array', + description: 'Invoice line items', + }, + dueDate: { + type: 'string', + description: 'Due date (YYYY-MM-DD)', + }, + emailStatus: { + type: 'string', + description: 'Email status', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + const invoice: any = { + Id: args.id, + SyncToken: args.syncToken, + }; + + if (args.customerId) invoice.CustomerRef = { value: args.customerId }; + if (args.lines) invoice.Line = args.lines; + if (args.dueDate) invoice.DueDate = args.dueDate; + if (args.emailStatus) invoice.EmailStatus = args.emailStatus; + + return await client.update('Invoice', invoice); + }, + }, + { + name: 'qbo_void_invoice', + description: 'Void an invoice (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Invoice ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the invoice', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('Invoice', args.id, args.syncToken); + }, + }, + { + name: 'qbo_delete_invoice', + description: 'Delete an invoice (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Invoice ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the invoice', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('Invoice', args.id, args.syncToken); + }, + }, + { + name: 'qbo_query_invoices', + description: 'Run a custom SQL-like query on invoices', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'SQL-like query (e.g., "SELECT * FROM Invoice WHERE TotalAmt > \'100.00\'")', + }, + startPosition: { + type: 'number', + description: 'Starting position (default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results (default 100)', + }, + }, + required: ['query'], + }, + handler: async (args: any) => { + return await client.query(args.query, { + startPosition: args.startPosition || 1, + maxResults: args.maxResults || 100, + }); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/items.ts b/servers/quickbooks/src/tools/items.ts new file mode 100644 index 0000000..0649edd --- /dev/null +++ b/servers/quickbooks/src/tools/items.ts @@ -0,0 +1,244 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { Item } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_items', + description: 'List all items with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + type: { + type: 'string', + description: 'Filter by type: Inventory, NonInventory, Service, Category, Bundle', + }, + active: { + type: 'boolean', + description: 'Filter by active status', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, type, active } = args; + + let query = 'SELECT * FROM Item'; + const conditions: string[] = []; + + if (type) conditions.push(`Type = '${type}'`); + if (active !== undefined) conditions.push(`Active = ${active}`); + + if (conditions.length > 0) { + query += ' WHERE ' + conditions.join(' AND '); + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_item', + description: 'Get a specific item by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Item ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('Item', args.id); + }, + }, + { + name: 'qbo_create_item', + description: 'Create a new item (inventory, non-inventory, service, or bundle)', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Item name', + }, + type: { + type: 'string', + description: 'Item type: Inventory, NonInventory, Service, Category, Bundle', + }, + incomeAccountId: { + type: 'string', + description: 'Income account reference ID', + }, + expenseAccountId: { + type: 'string', + description: 'Expense/COGS account reference ID (for Inventory)', + }, + assetAccountId: { + type: 'string', + description: 'Asset account reference ID (for Inventory)', + }, + unitPrice: { + type: 'number', + description: 'Unit price', + }, + purchaseCost: { + type: 'number', + description: 'Purchase cost (for Inventory)', + }, + qtyOnHand: { + type: 'number', + description: 'Quantity on hand (for Inventory)', + }, + invStartDate: { + type: 'string', + description: 'Inventory start date (YYYY-MM-DD) (for Inventory)', + }, + description: { + type: 'string', + description: 'Item description', + }, + trackQtyOnHand: { + type: 'boolean', + description: 'Track quantity on hand (for Inventory)', + }, + }, + required: ['name', 'type'], + }, + handler: async (args: any) => { + const item: any = { + Name: args.name, + Type: args.type, + }; + + if (args.incomeAccountId) item.IncomeAccountRef = { value: args.incomeAccountId }; + if (args.expenseAccountId) item.ExpenseAccountRef = { value: args.expenseAccountId }; + if (args.assetAccountId) item.AssetAccountRef = { value: args.assetAccountId }; + if (args.unitPrice !== undefined) item.UnitPrice = args.unitPrice; + if (args.purchaseCost !== undefined) item.PurchaseCost = args.purchaseCost; + if (args.qtyOnHand !== undefined) item.QtyOnHand = args.qtyOnHand; + if (args.invStartDate) item.InvStartDate = args.invStartDate; + if (args.description) item.Description = args.description; + if (args.trackQtyOnHand !== undefined) item.TrackQtyOnHand = args.trackQtyOnHand; + + return await client.create('Item', item); + }, + }, + { + name: 'qbo_update_item', + description: 'Update an existing item (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Item ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the item', + }, + name: { + type: 'string', + description: 'Item name', + }, + unitPrice: { + type: 'number', + description: 'Unit price', + }, + purchaseCost: { + type: 'number', + description: 'Purchase cost', + }, + description: { + type: 'string', + description: 'Item description', + }, + active: { + type: 'boolean', + description: 'Active status', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + const item: any = { + Id: args.id, + SyncToken: args.syncToken, + }; + + if (args.name) item.Name = args.name; + if (args.unitPrice !== undefined) item.UnitPrice = args.unitPrice; + if (args.purchaseCost !== undefined) item.PurchaseCost = args.purchaseCost; + if (args.description) item.Description = args.description; + if (args.active !== undefined) item.Active = args.active; + + return await client.update('Item', item); + }, + }, + { + name: 'qbo_search_items', + description: 'Search items by name or description', + inputSchema: { + type: 'object', + properties: { + searchTerm: { + type: 'string', + description: 'Search term to match against item name', + }, + startPosition: { + type: 'number', + description: 'Starting position (default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results (default 100)', + }, + }, + required: ['searchTerm'], + }, + handler: async (args: any) => { + const { searchTerm, startPosition = 1, maxResults = 100 } = args; + const query = `SELECT * FROM Item WHERE Name LIKE '%${searchTerm}%'`; + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_query_items', + description: 'Run a custom SQL-like query on items', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'SQL-like query (e.g., "SELECT * FROM Item WHERE Type = \'Inventory\'")', + }, + startPosition: { + type: 'number', + description: 'Starting position (default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results (default 100)', + }, + }, + required: ['query'], + }, + handler: async (args: any) => { + return await client.query(args.query, { + startPosition: args.startPosition || 1, + maxResults: args.maxResults || 100, + }); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/journal-entries.ts b/servers/quickbooks/src/tools/journal-entries.ts new file mode 100644 index 0000000..a125cd3 --- /dev/null +++ b/servers/quickbooks/src/tools/journal-entries.ts @@ -0,0 +1,374 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { JournalEntry, Deposit, Transfer } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_journal_entries', + description: 'List journal entries with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100 } = args; + const query = 'SELECT * FROM JournalEntry'; + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_journal_entry', + description: 'Get a specific journal entry by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Journal entry ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('JournalEntry', args.id); + }, + }, + { + name: 'qbo_create_journal_entry', + description: 'Create a new journal entry (debits and credits must balance)', + inputSchema: { + type: 'object', + properties: { + lines: { + type: 'array', + description: 'Journal entry lines (must have both debits and credits that balance)', + items: { + type: 'object', + properties: { + accountId: { type: 'string' }, + amount: { type: 'number' }, + detailType: { + type: 'string', + description: 'JournalEntryLineDetail', + }, + postingType: { + type: 'string', + description: 'Debit or Credit', + }, + description: { type: 'string' }, + }, + }, + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + privateNote: { + type: 'string', + description: 'Private note', + }, + }, + required: ['lines'], + }, + handler: async (args: any) => { + const journalEntry: any = { + Line: args.lines.map((line: any) => ({ + Amount: line.amount, + DetailType: 'JournalEntryLineDetail', + Description: line.description, + JournalEntryLineDetail: { + AccountRef: { value: line.accountId }, + PostingType: line.postingType, + }, + })), + }; + + if (args.txnDate) journalEntry.TxnDate = args.txnDate; + if (args.privateNote) journalEntry.PrivateNote = args.privateNote; + + return await client.create('JournalEntry', journalEntry); + }, + }, + { + name: 'qbo_update_journal_entry', + description: 'Update an existing journal entry (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Journal entry ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the journal entry', + }, + lines: { + type: 'array', + description: 'Updated journal entry lines', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + const journalEntry: any = { + Id: args.id, + SyncToken: args.syncToken, + }; + + if (args.lines) journalEntry.Line = args.lines; + + return await client.update('JournalEntry', journalEntry); + }, + }, + { + name: 'qbo_delete_journal_entry', + description: 'Delete a journal entry (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Journal entry ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the journal entry', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('JournalEntry', args.id, args.syncToken); + }, + }, + { + name: 'qbo_list_deposits', + description: 'List deposits with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + accountId: { + type: 'string', + description: 'Filter by deposit account ID', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, accountId } = args; + + let query = 'SELECT * FROM Deposit'; + if (accountId) { + query += ` WHERE DepositToAccountRef = '${accountId}'`; + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_deposit', + description: 'Get a specific deposit by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Deposit ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('Deposit', args.id); + }, + }, + { + name: 'qbo_create_deposit', + description: 'Create a new deposit', + inputSchema: { + type: 'object', + properties: { + depositToAccountId: { + type: 'string', + description: 'Deposit to account ID (bank account)', + }, + lines: { + type: 'array', + description: 'Deposit line items', + items: { + type: 'object', + properties: { + amount: { type: 'number' }, + description: { type: 'string' }, + accountId: { type: 'string' }, + linkedTxnId: { type: 'string' }, + }, + }, + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + }, + required: ['depositToAccountId', 'lines'], + }, + handler: async (args: any) => { + const deposit: any = { + DepositToAccountRef: { value: args.depositToAccountId }, + Line: args.lines.map((line: any) => ({ + Amount: line.amount, + DetailType: 'DepositLineDetail', + Description: line.description, + DepositLineDetail: { + AccountRef: { value: line.accountId }, + ...(line.linkedTxnId && { + Entity: { value: line.linkedTxnId }, + }), + }, + })), + }; + + if (args.txnDate) deposit.TxnDate = args.txnDate; + + return await client.create('Deposit', deposit); + }, + }, + { + name: 'qbo_delete_deposit', + description: 'Delete a deposit (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Deposit ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the deposit', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('Deposit', args.id, args.syncToken); + }, + }, + { + name: 'qbo_list_transfers', + description: 'List transfers with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100 } = args; + const query = 'SELECT * FROM Transfer'; + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_transfer', + description: 'Get a specific transfer by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Transfer ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('Transfer', args.id); + }, + }, + { + name: 'qbo_create_transfer', + description: 'Create a new transfer between accounts', + inputSchema: { + type: 'object', + properties: { + fromAccountId: { + type: 'string', + description: 'From account ID', + }, + toAccountId: { + type: 'string', + description: 'To account ID', + }, + amount: { + type: 'number', + description: 'Transfer amount', + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + }, + required: ['fromAccountId', 'toAccountId', 'amount'], + }, + handler: async (args: any) => { + const transfer: any = { + FromAccountRef: { value: args.fromAccountId }, + ToAccountRef: { value: args.toAccountId }, + Amount: args.amount, + }; + + if (args.txnDate) transfer.TxnDate = args.txnDate; + + return await client.create('Transfer', transfer); + }, + }, + { + name: 'qbo_delete_transfer', + description: 'Delete a transfer (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Transfer ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the transfer', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('Transfer', args.id, args.syncToken); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/payments.ts b/servers/quickbooks/src/tools/payments.ts new file mode 100644 index 0000000..cf756d0 --- /dev/null +++ b/servers/quickbooks/src/tools/payments.ts @@ -0,0 +1,294 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { Payment, CreditMemo } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_payments', + description: 'List payments with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + customerId: { + type: 'string', + description: 'Filter by customer ID', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, customerId } = args; + + let query = 'SELECT * FROM Payment'; + if (customerId) { + query += ` WHERE CustomerRef = '${customerId}'`; + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_payment', + description: 'Get a specific payment by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Payment ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('Payment', args.id); + }, + }, + { + name: 'qbo_create_payment', + description: 'Create a new payment', + inputSchema: { + type: 'object', + properties: { + customerId: { + type: 'string', + description: 'Customer reference ID', + }, + totalAmount: { + type: 'number', + description: 'Total payment amount', + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + paymentMethodId: { + type: 'string', + description: 'Payment method reference ID', + }, + depositToAccountId: { + type: 'string', + description: 'Deposit to account ID', + }, + linkedInvoices: { + type: 'array', + description: 'Invoices to link to this payment', + items: { + type: 'object', + properties: { + invoiceId: { type: 'string' }, + amount: { type: 'number' }, + }, + }, + }, + }, + required: ['customerId', 'totalAmount'], + }, + handler: async (args: any) => { + const payment: any = { + CustomerRef: { value: args.customerId }, + TotalAmt: args.totalAmount, + }; + + if (args.txnDate) payment.TxnDate = args.txnDate; + if (args.paymentMethodId) payment.PaymentMethodRef = { value: args.paymentMethodId }; + if (args.depositToAccountId) payment.DepositToAccountRef = { value: args.depositToAccountId }; + + if (args.linkedInvoices && args.linkedInvoices.length > 0) { + payment.Line = args.linkedInvoices.map((inv: any) => ({ + Amount: inv.amount, + LinkedTxn: [{ + TxnId: inv.invoiceId, + TxnType: 'Invoice', + }], + })); + } + + return await client.create('Payment', payment); + }, + }, + { + name: 'qbo_update_payment', + description: 'Update an existing payment (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Payment ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the payment', + }, + totalAmount: { + type: 'number', + description: 'Total payment amount', + }, + txnDate: { + type: 'string', + description: 'Transaction date', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + const payment: any = { + Id: args.id, + SyncToken: args.syncToken, + }; + + if (args.totalAmount !== undefined) payment.TotalAmt = args.totalAmount; + if (args.txnDate) payment.TxnDate = args.txnDate; + + return await client.update('Payment', payment); + }, + }, + { + name: 'qbo_void_payment', + description: 'Void a payment (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Payment ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the payment', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('Payment', args.id, args.syncToken); + }, + }, + { + name: 'qbo_delete_payment', + description: 'Delete a payment (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Payment ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the payment', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('Payment', args.id, args.syncToken); + }, + }, + { + name: 'qbo_list_credit_memos', + description: 'List credit memos with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + customerId: { + type: 'string', + description: 'Filter by customer ID', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, customerId } = args; + + let query = 'SELECT * FROM CreditMemo'; + if (customerId) { + query += ` WHERE CustomerRef = '${customerId}'`; + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_credit_memo', + description: 'Get a specific credit memo by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Credit memo ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('CreditMemo', args.id); + }, + }, + { + name: 'qbo_create_credit_memo', + description: 'Create a new credit memo', + inputSchema: { + type: 'object', + properties: { + customerId: { + type: 'string', + description: 'Customer reference ID', + }, + lines: { + type: 'array', + description: 'Credit memo line items', + items: { + type: 'object', + properties: { + amount: { type: 'number' }, + description: { type: 'string' }, + itemId: { type: 'string' }, + quantity: { type: 'number' }, + }, + }, + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + }, + required: ['customerId', 'lines'], + }, + handler: async (args: any) => { + const creditMemo: any = { + CustomerRef: { value: args.customerId }, + Line: args.lines.map((line: any) => ({ + Amount: line.amount, + DetailType: 'SalesItemLineDetail', + Description: line.description, + SalesItemLineDetail: { + ItemRef: { value: line.itemId }, + Qty: line.quantity, + }, + })), + }; + + if (args.txnDate) creditMemo.TxnDate = args.txnDate; + + return await client.create('CreditMemo', creditMemo); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/purchases.ts b/servers/quickbooks/src/tools/purchases.ts new file mode 100644 index 0000000..9d4c3dd --- /dev/null +++ b/servers/quickbooks/src/tools/purchases.ts @@ -0,0 +1,299 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { Purchase, PurchaseOrder } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_purchases', + description: 'List purchases with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + vendorId: { + type: 'string', + description: 'Filter by vendor ID', + }, + accountId: { + type: 'string', + description: 'Filter by account ID', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, vendorId, accountId } = args; + + let query = 'SELECT * FROM Purchase'; + const conditions: string[] = []; + + if (vendorId) conditions.push(`EntityRef = '${vendorId}'`); + if (accountId) conditions.push(`AccountRef = '${accountId}'`); + + if (conditions.length > 0) { + query += ' WHERE ' + conditions.join(' AND '); + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_purchase', + description: 'Get a specific purchase by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Purchase ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('Purchase', args.id); + }, + }, + { + name: 'qbo_create_purchase', + description: 'Create a new purchase (expense, check, or credit card)', + inputSchema: { + type: 'object', + properties: { + accountId: { + type: 'string', + description: 'Payment account ID (bank or credit card)', + }, + paymentType: { + type: 'string', + description: 'Payment type: Cash, Check, CreditCard', + }, + vendorId: { + type: 'string', + description: 'Vendor reference ID (optional)', + }, + lines: { + type: 'array', + description: 'Purchase line items', + items: { + type: 'object', + properties: { + amount: { type: 'number' }, + description: { type: 'string' }, + accountId: { type: 'string' }, + itemId: { type: 'string' }, + }, + }, + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + }, + required: ['accountId', 'paymentType', 'lines'], + }, + handler: async (args: any) => { + const purchase: any = { + AccountRef: { value: args.accountId }, + PaymentType: args.paymentType, + Line: args.lines.map((line: any) => ({ + Amount: line.amount, + DetailType: line.itemId ? 'ItemBasedExpenseLineDetail' : 'AccountBasedExpenseLineDetail', + Description: line.description, + ...(line.itemId + ? { ItemBasedExpenseLineDetail: { ItemRef: { value: line.itemId } } } + : { AccountBasedExpenseLineDetail: { AccountRef: { value: line.accountId } } } + ), + })), + }; + + if (args.vendorId) purchase.EntityRef = { value: args.vendorId }; + if (args.txnDate) purchase.TxnDate = args.txnDate; + + return await client.create('Purchase', purchase); + }, + }, + { + name: 'qbo_update_purchase', + description: 'Update an existing purchase (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Purchase ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the purchase', + }, + lines: { + type: 'array', + description: 'Updated line items', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + const purchase: any = { + Id: args.id, + SyncToken: args.syncToken, + }; + + if (args.lines) purchase.Line = args.lines; + + return await client.update('Purchase', purchase); + }, + }, + { + name: 'qbo_delete_purchase', + description: 'Delete a purchase (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Purchase ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the purchase', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('Purchase', args.id, args.syncToken); + }, + }, + { + name: 'qbo_list_purchase_orders', + description: 'List purchase orders with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + vendorId: { + type: 'string', + description: 'Filter by vendor ID', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, vendorId } = args; + + let query = 'SELECT * FROM PurchaseOrder'; + if (vendorId) { + query += ` WHERE VendorRef = '${vendorId}'`; + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_purchase_order', + description: 'Get a specific purchase order by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Purchase order ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('PurchaseOrder', args.id); + }, + }, + { + name: 'qbo_create_purchase_order', + description: 'Create a new purchase order', + inputSchema: { + type: 'object', + properties: { + vendorId: { + type: 'string', + description: 'Vendor reference ID', + }, + lines: { + type: 'array', + description: 'Purchase order line items', + items: { + type: 'object', + properties: { + amount: { type: 'number' }, + description: { type: 'string' }, + itemId: { type: 'string' }, + quantity: { type: 'number' }, + }, + }, + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + dueDate: { + type: 'string', + description: 'Due date (YYYY-MM-DD)', + }, + }, + required: ['vendorId', 'lines'], + }, + handler: async (args: any) => { + const purchaseOrder: any = { + VendorRef: { value: args.vendorId }, + Line: args.lines.map((line: any) => ({ + Amount: line.amount, + DetailType: 'ItemBasedExpenseLineDetail', + Description: line.description, + ItemBasedExpenseLineDetail: { + ItemRef: { value: line.itemId }, + Qty: line.quantity, + }, + })), + }; + + if (args.txnDate) purchaseOrder.TxnDate = args.txnDate; + // Note: PurchaseOrder doesn't have DueDate in QBO API + + return await client.create('PurchaseOrder', purchaseOrder); + }, + }, + { + name: 'qbo_delete_purchase_order', + description: 'Delete a purchase order (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Purchase order ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the purchase order', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('PurchaseOrder', args.id, args.syncToken); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/reports.ts b/servers/quickbooks/src/tools/reports.ts new file mode 100644 index 0000000..07d9ae7 --- /dev/null +++ b/servers/quickbooks/src/tools/reports.ts @@ -0,0 +1,230 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_run_profit_loss', + description: 'Run a Profit & Loss (P&L) report', + inputSchema: { + type: 'object', + properties: { + startDate: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + accountingMethod: { + type: 'string', + description: 'Accounting method: Accrual or Cash (default: Accrual)', + }, + summarizeColumnBy: { + type: 'string', + description: 'Summarize by: Total, Month, Quarter, Year, etc.', + }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (args: any) => { + const params: Record = { + start_date: args.startDate, + end_date: args.endDate, + }; + + if (args.accountingMethod) params.accounting_method = args.accountingMethod; + if (args.summarizeColumnBy) params.summarize_column_by = args.summarizeColumnBy; + + return await client.getReport('ProfitAndLoss', params); + }, + }, + { + name: 'qbo_run_balance_sheet', + description: 'Run a Balance Sheet report', + inputSchema: { + type: 'object', + properties: { + asOfDate: { + type: 'string', + description: 'As of date (YYYY-MM-DD)', + }, + accountingMethod: { + type: 'string', + description: 'Accounting method: Accrual or Cash (default: Accrual)', + }, + }, + required: ['asOfDate'], + }, + handler: async (args: any) => { + const params: Record = { + date_macro: 'custom', + end_date: args.asOfDate, + }; + + if (args.accountingMethod) params.accounting_method = args.accountingMethod; + + return await client.getReport('BalanceSheet', params); + }, + }, + { + name: 'qbo_run_cash_flow', + description: 'Run a Statement of Cash Flows report', + inputSchema: { + type: 'object', + properties: { + startDate: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (args: any) => { + const params: Record = { + start_date: args.startDate, + end_date: args.endDate, + }; + + return await client.getReport('CashFlow', params); + }, + }, + { + name: 'qbo_run_ar_aging', + description: 'Run an Accounts Receivable (AR) Aging Summary report', + inputSchema: { + type: 'object', + properties: { + asOfDate: { + type: 'string', + description: 'As of date (YYYY-MM-DD)', + }, + agingMethod: { + type: 'string', + description: 'Aging method: Current or Report_Date (default: Report_Date)', + }, + numPeriods: { + type: 'number', + description: 'Number of aging periods (default: 4)', + }, + }, + required: ['asOfDate'], + }, + handler: async (args: any) => { + const params: Record = { + report_date: args.asOfDate, + }; + + if (args.agingMethod) params.aging_method = args.agingMethod; + if (args.numPeriods) params.num_periods = args.numPeriods.toString(); + + return await client.getReport('AgedReceivables', params); + }, + }, + { + name: 'qbo_run_ap_aging', + description: 'Run an Accounts Payable (AP) Aging Summary report', + inputSchema: { + type: 'object', + properties: { + asOfDate: { + type: 'string', + description: 'As of date (YYYY-MM-DD)', + }, + agingMethod: { + type: 'string', + description: 'Aging method: Current or Report_Date (default: Report_Date)', + }, + numPeriods: { + type: 'number', + description: 'Number of aging periods (default: 4)', + }, + }, + required: ['asOfDate'], + }, + handler: async (args: any) => { + const params: Record = { + report_date: args.asOfDate, + }; + + if (args.agingMethod) params.aging_method = args.agingMethod; + if (args.numPeriods) params.num_periods = args.numPeriods.toString(); + + return await client.getReport('AgedPayables', params); + }, + }, + { + name: 'qbo_run_trial_balance', + description: 'Run a Trial Balance report', + inputSchema: { + type: 'object', + properties: { + startDate: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + accountingMethod: { + type: 'string', + description: 'Accounting method: Accrual or Cash (default: Accrual)', + }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (args: any) => { + const params: Record = { + start_date: args.startDate, + end_date: args.endDate, + }; + + if (args.accountingMethod) params.accounting_method = args.accountingMethod; + + return await client.getReport('TrialBalance', params); + }, + }, + { + name: 'qbo_run_general_ledger', + description: 'Run a General Ledger report', + inputSchema: { + type: 'object', + properties: { + startDate: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + accountId: { + type: 'string', + description: 'Filter by specific account ID (optional)', + }, + accountingMethod: { + type: 'string', + description: 'Accounting method: Accrual or Cash (default: Accrual)', + }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (args: any) => { + const params: Record = { + start_date: args.startDate, + end_date: args.endDate, + }; + + if (args.accountId) params.account = args.accountId; + if (args.accountingMethod) params.accounting_method = args.accountingMethod; + + return await client.getReport('GeneralLedger', params); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/taxes.ts b/servers/quickbooks/src/tools/taxes.ts new file mode 100644 index 0000000..a016a5d --- /dev/null +++ b/servers/quickbooks/src/tools/taxes.ts @@ -0,0 +1,199 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { TaxCode, TaxRate, TaxAgency } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_tax_codes', + description: 'List all tax codes', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + active: { + type: 'boolean', + description: 'Filter by active status', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, active } = args; + + let query = 'SELECT * FROM TaxCode'; + if (active !== undefined) { + query += ` WHERE Active = ${active}`; + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_tax_code', + description: 'Get a specific tax code by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Tax code ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('TaxCode', args.id); + }, + }, + { + name: 'qbo_query_tax_codes', + description: 'Run a custom SQL-like query on tax codes', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'SQL-like query (e.g., "SELECT * FROM TaxCode WHERE Active = true")', + }, + startPosition: { + type: 'number', + description: 'Starting position (default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results (default 100)', + }, + }, + required: ['query'], + }, + handler: async (args: any) => { + return await client.query(args.query, { + startPosition: args.startPosition || 1, + maxResults: args.maxResults || 100, + }); + }, + }, + { + name: 'qbo_list_tax_rates', + description: 'List all tax rates', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + active: { + type: 'boolean', + description: 'Filter by active status', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, active } = args; + + let query = 'SELECT * FROM TaxRate'; + if (active !== undefined) { + query += ` WHERE Active = ${active}`; + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_tax_rate', + description: 'Get a specific tax rate by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Tax rate ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('TaxRate', args.id); + }, + }, + { + name: 'qbo_query_tax_rates', + description: 'Run a custom SQL-like query on tax rates', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'SQL-like query (e.g., "SELECT * FROM TaxRate WHERE RateValue > \'5.0\'")', + }, + startPosition: { + type: 'number', + description: 'Starting position (default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results (default 100)', + }, + }, + required: ['query'], + }, + handler: async (args: any) => { + return await client.query(args.query, { + startPosition: args.startPosition || 1, + maxResults: args.maxResults || 100, + }); + }, + }, + { + name: 'qbo_list_tax_agencies', + description: 'List all tax agencies', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100 } = args; + const query = 'SELECT * FROM TaxAgency'; + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_tax_agency', + description: 'Get a specific tax agency by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Tax agency ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('TaxAgency', args.id); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/time-activities.ts b/servers/quickbooks/src/tools/time-activities.ts new file mode 100644 index 0000000..1a4242a --- /dev/null +++ b/servers/quickbooks/src/tools/time-activities.ts @@ -0,0 +1,195 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { TimeActivity } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_time_activities', + description: 'List time activities with optional filters', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + employeeId: { + type: 'string', + description: 'Filter by employee ID', + }, + customerId: { + type: 'string', + description: 'Filter by customer ID', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, employeeId, customerId } = args; + + let query = 'SELECT * FROM TimeActivity'; + const conditions: string[] = []; + + if (employeeId) conditions.push(`EmployeeRef = '${employeeId}'`); + if (customerId) conditions.push(`CustomerRef = '${customerId}'`); + + if (conditions.length > 0) { + query += ' WHERE ' + conditions.join(' AND '); + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_time_activity', + description: 'Get a specific time activity by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Time activity ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('TimeActivity', args.id); + }, + }, + { + name: 'qbo_create_time_activity', + description: 'Create a new time activity', + inputSchema: { + type: 'object', + properties: { + employeeId: { + type: 'string', + description: 'Employee reference ID', + }, + customerId: { + type: 'string', + description: 'Customer reference ID (optional)', + }, + itemId: { + type: 'string', + description: 'Service item reference ID (optional)', + }, + hours: { + type: 'number', + description: 'Number of hours', + }, + minutes: { + type: 'number', + description: 'Number of minutes', + }, + txnDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + }, + description: { + type: 'string', + description: 'Description of work performed', + }, + hourlyRate: { + type: 'number', + description: 'Hourly rate (optional)', + }, + billableStatus: { + type: 'string', + description: 'Billable status: Billable, NotBillable, HasBeenBilled', + }, + }, + required: ['employeeId', 'txnDate'], + }, + handler: async (args: any) => { + const timeActivity: any = { + EmployeeRef: { value: args.employeeId }, + TxnDate: args.txnDate, + NameOf: 'Employee', + }; + + if (args.customerId) timeActivity.CustomerRef = { value: args.customerId }; + if (args.itemId) timeActivity.ItemRef = { value: args.itemId }; + if (args.hours !== undefined) timeActivity.Hours = args.hours; + if (args.minutes !== undefined) timeActivity.Minutes = args.minutes; + if (args.description) timeActivity.Description = args.description; + if (args.hourlyRate !== undefined) timeActivity.HourlyRate = args.hourlyRate; + if (args.billableStatus) timeActivity.BillableStatus = args.billableStatus; + + return await client.create('TimeActivity', timeActivity); + }, + }, + { + name: 'qbo_update_time_activity', + description: 'Update an existing time activity (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Time activity ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the time activity', + }, + hours: { + type: 'number', + description: 'Number of hours', + }, + minutes: { + type: 'number', + description: 'Number of minutes', + }, + description: { + type: 'string', + description: 'Description of work performed', + }, + billableStatus: { + type: 'string', + description: 'Billable status: Billable, NotBillable, HasBeenBilled', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + const timeActivity: any = { + Id: args.id, + SyncToken: args.syncToken, + }; + + if (args.hours !== undefined) timeActivity.Hours = args.hours; + if (args.minutes !== undefined) timeActivity.Minutes = args.minutes; + if (args.description) timeActivity.Description = args.description; + if (args.billableStatus) timeActivity.BillableStatus = args.billableStatus; + + return await client.update('TimeActivity', timeActivity); + }, + }, + { + name: 'qbo_delete_time_activity', + description: 'Delete a time activity (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Time activity ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the time activity', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + return await client.delete('TimeActivity', args.id, args.syncToken); + }, + }, + ]; +} diff --git a/servers/quickbooks/src/tools/vendors.ts b/servers/quickbooks/src/tools/vendors.ts new file mode 100644 index 0000000..8894407 --- /dev/null +++ b/servers/quickbooks/src/tools/vendors.ts @@ -0,0 +1,231 @@ +import { QuickBooksClient } from '../clients/quickbooks.js'; +import type { Vendor } from '../types/index.js'; + +export function getTools(client: QuickBooksClient) { + return [ + { + name: 'qbo_list_vendors', + description: 'List all vendors with pagination', + inputSchema: { + type: 'object', + properties: { + startPosition: { + type: 'number', + description: 'Starting position (1-indexed, default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (max 1000, default 100)', + }, + active: { + type: 'boolean', + description: 'Filter by active status', + }, + }, + }, + handler: async (args: any) => { + const { startPosition = 1, maxResults = 100, active } = args; + + let query = 'SELECT * FROM Vendor'; + if (active !== undefined) { + query += ` WHERE Active = ${active}`; + } + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_get_vendor', + description: 'Get a specific vendor by ID', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Vendor ID', + }, + }, + required: ['id'], + }, + handler: async (args: any) => { + return await client.read('Vendor', args.id); + }, + }, + { + name: 'qbo_create_vendor', + description: 'Create a new vendor', + inputSchema: { + type: 'object', + properties: { + displayName: { + type: 'string', + description: 'Display name for the vendor', + }, + companyName: { + type: 'string', + description: 'Company name', + }, + givenName: { + type: 'string', + description: 'First name', + }, + familyName: { + type: 'string', + description: 'Last name', + }, + primaryEmail: { + type: 'string', + description: 'Primary email address', + }, + primaryPhone: { + type: 'string', + description: 'Primary phone number', + }, + billAddress: { + type: 'object', + description: 'Billing address', + properties: { + line1: { type: 'string' }, + city: { type: 'string' }, + countrySubDivisionCode: { type: 'string' }, + postalCode: { type: 'string' }, + }, + }, + accountNumber: { + type: 'string', + description: 'Account number', + }, + vendor1099: { + type: 'boolean', + description: 'Whether vendor is 1099 eligible', + }, + }, + required: ['displayName'], + }, + handler: async (args: any) => { + const vendor: any = { + DisplayName: args.displayName, + }; + + if (args.companyName) vendor.CompanyName = args.companyName; + if (args.givenName) vendor.GivenName = args.givenName; + if (args.familyName) vendor.FamilyName = args.familyName; + if (args.primaryEmail) vendor.PrimaryEmailAddr = { Address: args.primaryEmail }; + if (args.primaryPhone) vendor.PrimaryPhone = { FreeFormNumber: args.primaryPhone }; + if (args.billAddress) vendor.BillAddr = args.billAddress; + if (args.accountNumber) vendor.AcctNum = args.accountNumber; + if (args.vendor1099 !== undefined) vendor.Vendor1099 = args.vendor1099; + + return await client.create('Vendor', vendor); + }, + }, + { + name: 'qbo_update_vendor', + description: 'Update an existing vendor (requires SyncToken)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Vendor ID', + }, + syncToken: { + type: 'string', + description: 'SyncToken from the vendor', + }, + displayName: { + type: 'string', + description: 'Display name', + }, + primaryEmail: { + type: 'string', + description: 'Primary email address', + }, + primaryPhone: { + type: 'string', + description: 'Primary phone number', + }, + active: { + type: 'boolean', + description: 'Active status', + }, + vendor1099: { + type: 'boolean', + description: 'Whether vendor is 1099 eligible', + }, + }, + required: ['id', 'syncToken'], + }, + handler: async (args: any) => { + const vendor: any = { + Id: args.id, + SyncToken: args.syncToken, + }; + + if (args.displayName) vendor.DisplayName = args.displayName; + if (args.primaryEmail) vendor.PrimaryEmailAddr = { Address: args.primaryEmail }; + if (args.primaryPhone) vendor.PrimaryPhone = { FreeFormNumber: args.primaryPhone }; + if (args.active !== undefined) vendor.Active = args.active; + if (args.vendor1099 !== undefined) vendor.Vendor1099 = args.vendor1099; + + return await client.update('Vendor', vendor); + }, + }, + { + name: 'qbo_search_vendors', + description: 'Search vendors by name', + inputSchema: { + type: 'object', + properties: { + searchTerm: { + type: 'string', + description: 'Search term to match against vendor name', + }, + startPosition: { + type: 'number', + description: 'Starting position (default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results (default 100)', + }, + }, + required: ['searchTerm'], + }, + handler: async (args: any) => { + const { searchTerm, startPosition = 1, maxResults = 100 } = args; + const query = `SELECT * FROM Vendor WHERE DisplayName LIKE '%${searchTerm}%'`; + + return await client.query(query, { startPosition, maxResults }); + }, + }, + { + name: 'qbo_query_vendors', + description: 'Run a custom SQL-like query on vendors', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'SQL-like query (e.g., "SELECT * FROM Vendor WHERE Vendor1099 = true")', + }, + startPosition: { + type: 'number', + description: 'Starting position (default 1)', + }, + maxResults: { + type: 'number', + description: 'Maximum results (default 100)', + }, + }, + required: ['query'], + }, + handler: async (args: any) => { + return await client.query(args.query, { + startPosition: args.startPosition || 1, + maxResults: args.maxResults || 100, + }); + }, + }, + ]; +} diff --git a/servers/salesforce/TOOLS_MANIFEST.md b/servers/salesforce/TOOLS_MANIFEST.md new file mode 100644 index 0000000..c6a9a45 --- /dev/null +++ b/servers/salesforce/TOOLS_MANIFEST.md @@ -0,0 +1,169 @@ +# Salesforce MCP Server - Tools Manifest + +## Summary +**Total Tools: 96** +**Target: 60-80** ✅ **EXCEEDED** + +All tools follow the naming convention `sf_verb_noun` and are organized into 14 logical categories. + +## Tool Breakdown by Category + +### 1. Accounts (`src/tools/accounts.ts`) - 6 tools +- `sf_list_accounts` - List accounts with filters +- `sf_get_account` - Get account by ID +- `sf_create_account` - Create new account +- `sf_update_account` - Update existing account +- `sf_delete_account` - Delete account +- `sf_search_accounts` - Search accounts with SOQL + +### 2. Contacts (`src/tools/contacts.ts`) - 7 tools +- `sf_list_contacts` - List contacts with filters +- `sf_get_contact` - Get contact by ID +- `sf_create_contact` - Create new contact +- `sf_update_contact` - Update existing contact +- `sf_delete_contact` - Delete contact +- `sf_search_contacts` - Search contacts with SOQL +- `sf_find_contacts_by_email` - Find contacts by email (exact or partial) + +### 3. Leads (`src/tools/leads.ts`) - 7 tools +- `sf_list_leads` - List leads with filters +- `sf_get_lead` - Get lead by ID +- `sf_create_lead` - Create new lead +- `sf_update_lead` - Update existing lead +- `sf_delete_lead` - Delete lead +- `sf_convert_lead` - Convert lead to Account/Contact/Opportunity +- `sf_search_leads` - Search leads with SOQL + +### 4. Opportunities (`src/tools/opportunities.ts`) - 8 tools +- `sf_list_opportunities` - List opportunities with filters +- `sf_get_opportunity` - Get opportunity by ID +- `sf_create_opportunity` - Create new opportunity +- `sf_update_opportunity` - Update existing opportunity +- `sf_delete_opportunity` - Delete opportunity +- `sf_search_opportunities` - Search opportunities with SOQL +- `sf_find_opportunities_by_stage` - Find opportunities by stage +- `sf_find_opportunities_by_amount` - Find opportunities by amount range + +### 5. Cases (`src/tools/cases.ts`) - 8 tools +- `sf_list_cases` - List cases with filters +- `sf_get_case` - Get case by ID +- `sf_create_case` - Create new case +- `sf_update_case` - Update existing case +- `sf_delete_case` - Delete case +- `sf_close_case` - Close a case +- `sf_escalate_case` - Escalate a case +- `sf_search_cases` - Search cases with SOQL + +### 6. Tasks (`src/tools/tasks.ts`) - 7 tools +- `sf_list_tasks` - List tasks with filters +- `sf_get_task` - Get task by ID +- `sf_create_task` - Create new task +- `sf_update_task` - Update existing task +- `sf_delete_task` - Delete task +- `sf_search_tasks` - Search tasks with SOQL +- `sf_get_overdue_tasks` - Get all overdue tasks + +### 7. Events (`src/tools/events.ts`) - 7 tools +- `sf_list_events` - List events with filters +- `sf_get_event` - Get event by ID +- `sf_create_event` - Create new event +- `sf_update_event` - Update existing event +- `sf_delete_event` - Delete event +- `sf_search_events` - Search events with SOQL +- `sf_find_events_by_date_range` - Find events in date range + +### 8. Campaigns (`src/tools/campaigns.ts`) - 7 tools +- `sf_list_campaigns` - List campaigns with filters +- `sf_get_campaign` - Get campaign by ID +- `sf_create_campaign` - Create new campaign +- `sf_update_campaign` - Update existing campaign +- `sf_add_campaign_member` - Add lead/contact to campaign +- `sf_list_campaign_members` - List members of a campaign +- `sf_get_campaign_stats` - Get campaign statistics + +### 9. Reports (`src/tools/reports.ts`) - 5 tools +- `sf_list_reports` - List available reports +- `sf_get_report` - Get report by ID +- `sf_run_report` - Run a report and get results +- `sf_describe_report` - Get report metadata +- `sf_search_reports` - Search reports by name + +### 10. Dashboards (`src/tools/dashboards.ts`) - 5 tools +- `sf_list_dashboards` - List available dashboards +- `sf_get_dashboard` - Get dashboard by ID +- `sf_describe_dashboard` - Get dashboard metadata +- `sf_get_dashboard_components` - Get dashboard components +- `sf_search_dashboards` - Search dashboards by title + +### 11. Users (`src/tools/users.ts`) - 8 tools +- `sf_list_users` - List users with filters +- `sf_get_user` - Get user by ID +- `sf_search_users` - Search users by name/username/email +- `sf_list_roles` - List user roles +- `sf_get_role` - Get role by ID +- `sf_list_profiles` - List user profiles +- `sf_get_profile` - Get profile by ID +- `sf_get_user_permissions` - Get permission sets for a user + +### 12. Custom Objects (`src/tools/custom-objects.ts`) - 7 tools +- `sf_describe_object` - Describe any SObject (get metadata) +- `sf_list_custom_objects` - List all custom objects in org +- `sf_get_custom_record` - Get record from any custom object +- `sf_list_custom_records` - List records from any custom object +- `sf_create_custom_record` - Create record in any custom object +- `sf_update_custom_record` - Update record in any custom object +- `sf_delete_custom_record` - Delete record from any custom object + +### 13. SOQL (`src/tools/soql.ts`) - 7 tools +- `sf_run_soql_query` - Execute raw SOQL query +- `sf_run_soql_query_all` - Execute SOQL query with auto-pagination +- `sf_run_sosl_search` - Execute SOSL search +- `sf_build_soql_query` - Build SOQL query (helper) +- `sf_explain_soql_query` - Get query plan for performance +- `sf_count_records` - Count records with optional filter +- `sf_aggregate_query` - Run aggregate functions (COUNT, SUM, AVG, etc.) + +### 14. Bulk API (`src/tools/bulk-api.ts`) - 7 tools +- `sf_bulk_create_job` - Create bulk job for large operations +- `sf_bulk_upload_data` - Upload CSV data to bulk job +- `sf_bulk_close_job` - Close job and start processing +- `sf_bulk_get_job_status` - Get job status +- `sf_bulk_get_successful_results` - Get successful results (CSV) +- `sf_bulk_get_failed_results` - Get failed results with errors (CSV) +- `sf_bulk_abort_job` - Abort a running job + +## Features + +### Comprehensive Coverage +✅ Standard Objects: Account, Contact, Lead, Opportunity, Case, Task, Event +✅ Marketing: Campaign, Campaign Members +✅ Analytics: Reports, Dashboards +✅ Admin: Users, Roles, Profiles, Permissions +✅ Custom Objects: Generic CRUD for any custom object +✅ Query: SOQL, SOSL, aggregations +✅ Bulk Operations: Bulk API 2.0 for large datasets + +### Technical Features +- **Type Safety**: Full TypeScript types from `src/types/index.ts` +- **Error Handling**: Leverages client retry logic and error handling +- **Pagination**: Support for LIMIT/OFFSET and automatic pagination +- **Relationships**: SOQL joins for related objects (e.g., Account.Name, Owner.Name) +- **Flexible Queries**: Custom WHERE clauses, ORDER BY, filtering +- **Performance**: Query plan analysis, bulk operations for scale + +### API Compliance +- REST API v59.0 +- Proper field name mapping (camelCase input → PascalCase Salesforce fields) +- SalesforceId type safety with branded types +- Composite API support (up to 25 subrequests) +- Bulk API 2.0 for large data operations + +## TypeScript Compilation +✅ All files compile without errors (`npx tsc --noEmit`) + +## Quality Metrics +- **96 tools** across **14 categories** +- **Average: 6.86 tools per category** +- **Range: 5-8 tools per category** (well-balanced) +- **Naming consistency**: 100% follow `sf_verb_noun` pattern +- **Type safety**: 100% TypeScript with strict types diff --git a/servers/salesforce/src/tools/accounts.ts b/servers/salesforce/src/tools/accounts.ts new file mode 100644 index 0000000..bff543d --- /dev/null +++ b/servers/salesforce/src/tools/accounts.ts @@ -0,0 +1,356 @@ +/** + * Account management tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { Account } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_list_accounts', + description: 'List accounts with optional filters. Returns up to 200 accounts by default.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of accounts to return (default: 200)', + default: 200, + }, + offset: { + type: 'number', + description: 'Number of records to skip (for pagination)', + }, + orderBy: { + type: 'string', + description: 'Field to sort by (e.g., "Name ASC", "CreatedDate DESC")', + default: 'Name ASC', + }, + type: { + type: 'string', + description: 'Filter by account type (e.g., "Customer", "Prospect")', + }, + industry: { + type: 'string', + description: 'Filter by industry', + }, + }, + }, + handler: async (args: { + limit?: number; + offset?: number; + orderBy?: string; + type?: string; + industry?: string; + }) => { + const conditions: string[] = []; + if (args.type) conditions.push(`Type = '${args.type}'`); + if (args.industry) conditions.push(`Industry = '${args.industry}'`); + + const soql = client.buildSOQL({ + select: ['Id', 'Name', 'Type', 'Industry', 'Phone', 'Website', 'BillingCity', 'BillingState', 'Owner.Name', 'CreatedDate'], + from: 'Account', + where: conditions.length > 0 ? conditions.join(' AND ') : undefined, + orderBy: args.orderBy || 'Name ASC', + limit: args.limit || 200, + offset: args.offset, + }); + + const result = await client.query(soql); + return { + totalSize: result.totalSize, + records: result.records, + hasMore: !result.done, + }; + }, + }, + + { + name: 'sf_get_account', + description: 'Get detailed information about a specific account by ID', + inputSchema: { + type: 'object', + properties: { + accountId: { + type: 'string', + description: 'The Salesforce ID of the account (15 or 18 characters)', + }, + }, + required: ['accountId'], + }, + handler: async (args: { accountId: string }) => { + const account = await client.getRecord('Account', args.accountId); + return account; + }, + }, + + { + name: 'sf_create_account', + description: 'Create a new account', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Account name (required)', + }, + type: { + type: 'string', + description: 'Account type (e.g., "Customer", "Prospect", "Partner")', + }, + industry: { + type: 'string', + description: 'Industry (e.g., "Technology", "Healthcare", "Finance")', + }, + phone: { + type: 'string', + description: 'Phone number', + }, + website: { + type: 'string', + description: 'Website URL', + }, + billingStreet: { + type: 'string', + description: 'Billing street address', + }, + billingCity: { + type: 'string', + description: 'Billing city', + }, + billingState: { + type: 'string', + description: 'Billing state/province', + }, + billingPostalCode: { + type: 'string', + description: 'Billing postal code', + }, + billingCountry: { + type: 'string', + description: 'Billing country', + }, + description: { + type: 'string', + description: 'Account description', + }, + numberOfEmployees: { + type: 'number', + description: 'Number of employees', + }, + annualRevenue: { + type: 'number', + description: 'Annual revenue', + }, + }, + required: ['name'], + }, + handler: async (args: { + name: string; + type?: string; + industry?: string; + phone?: string; + website?: string; + billingStreet?: string; + billingCity?: string; + billingState?: string; + billingPostalCode?: string; + billingCountry?: string; + description?: string; + numberOfEmployees?: number; + annualRevenue?: number; + }) => { + const accountData: Partial = { + Name: args.name, + Type: args.type, + Industry: args.industry, + Phone: args.phone, + Website: args.website, + BillingStreet: args.billingStreet, + BillingCity: args.billingCity, + BillingState: args.billingState, + BillingPostalCode: args.billingPostalCode, + BillingCountry: args.billingCountry, + Description: args.description, + NumberOfEmployees: args.numberOfEmployees, + AnnualRevenue: args.annualRevenue, + }; + + const result = await client.createRecord('Account', accountData); + return result; + }, + }, + + { + name: 'sf_update_account', + description: 'Update an existing account', + inputSchema: { + type: 'object', + properties: { + accountId: { + type: 'string', + description: 'The Salesforce ID of the account to update', + }, + name: { + type: 'string', + description: 'Account name', + }, + type: { + type: 'string', + description: 'Account type', + }, + industry: { + type: 'string', + description: 'Industry', + }, + phone: { + type: 'string', + description: 'Phone number', + }, + website: { + type: 'string', + description: 'Website URL', + }, + billingStreet: { + type: 'string', + description: 'Billing street address', + }, + billingCity: { + type: 'string', + description: 'Billing city', + }, + billingState: { + type: 'string', + description: 'Billing state/province', + }, + billingPostalCode: { + type: 'string', + description: 'Billing postal code', + }, + billingCountry: { + type: 'string', + description: 'Billing country', + }, + description: { + type: 'string', + description: 'Account description', + }, + numberOfEmployees: { + type: 'number', + description: 'Number of employees', + }, + annualRevenue: { + type: 'number', + description: 'Annual revenue', + }, + }, + required: ['accountId'], + }, + handler: async (args: { + accountId: string; + name?: string; + type?: string; + industry?: string; + phone?: string; + website?: string; + billingStreet?: string; + billingCity?: string; + billingState?: string; + billingPostalCode?: string; + billingCountry?: string; + description?: string; + numberOfEmployees?: number; + annualRevenue?: number; + }) => { + const { accountId, ...updates } = args; + const accountData: Partial = { + Name: updates.name, + Type: updates.type, + Industry: updates.industry, + Phone: updates.phone, + Website: updates.website, + BillingStreet: updates.billingStreet, + BillingCity: updates.billingCity, + BillingState: updates.billingState, + BillingPostalCode: updates.billingPostalCode, + BillingCountry: updates.billingCountry, + Description: updates.description, + NumberOfEmployees: updates.numberOfEmployees, + AnnualRevenue: updates.annualRevenue, + }; + + // Remove undefined values + Object.keys(accountData).forEach((key) => { + if (accountData[key as keyof Account] === undefined) { + delete accountData[key as keyof Account]; + } + }); + + await client.updateRecord('Account', accountId, accountData); + return { success: true, id: accountId }; + }, + }, + + { + name: 'sf_delete_account', + description: 'Delete an account by ID', + inputSchema: { + type: 'object', + properties: { + accountId: { + type: 'string', + description: 'The Salesforce ID of the account to delete', + }, + }, + required: ['accountId'], + }, + handler: async (args: { accountId: string }) => { + await client.deleteRecord('Account', args.accountId); + return { success: true, id: args.accountId }; + }, + }, + + { + name: 'sf_search_accounts', + description: 'Search accounts using SOQL with flexible criteria', + inputSchema: { + type: 'object', + properties: { + searchText: { + type: 'string', + description: 'Text to search in Name, BillingCity, or Website fields', + }, + whereClause: { + type: 'string', + description: 'Custom WHERE clause (e.g., "AnnualRevenue > 1000000")', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 100, + }, + }, + }, + handler: async (args: { searchText?: string; whereClause?: string; limit?: number }) => { + let where = args.whereClause; + + if (args.searchText) { + const searchCondition = `(Name LIKE '%${args.searchText}%' OR BillingCity LIKE '%${args.searchText}%' OR Website LIKE '%${args.searchText}%')`; + where = where ? `${searchCondition} AND (${where})` : searchCondition; + } + + const soql = client.buildSOQL({ + select: ['Id', 'Name', 'Type', 'Industry', 'Phone', 'Website', 'BillingCity', 'BillingState', 'AnnualRevenue', 'NumberOfEmployees'], + from: 'Account', + where, + orderBy: 'Name ASC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/bulk-api.ts b/servers/salesforce/src/tools/bulk-api.ts new file mode 100644 index 0000000..2828c5d --- /dev/null +++ b/servers/salesforce/src/tools/bulk-api.ts @@ -0,0 +1,203 @@ +/** + * Bulk API 2.0 tools for large-scale data operations + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { BulkJob } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_bulk_create_job', + description: 'Create a bulk job for insert, update, upsert, delete, or hardDelete operations', + inputSchema: { + type: 'object', + properties: { + objectName: { + type: 'string', + description: 'Object to perform bulk operation on (e.g., "Account", "Contact")', + }, + operation: { + type: 'string', + description: 'Operation type: insert, update, upsert, delete, or hardDelete', + enum: ['insert', 'update', 'upsert', 'delete', 'hardDelete'], + }, + externalIdField: { + type: 'string', + description: 'External ID field name (required for upsert operations)', + }, + }, + required: ['objectName', 'operation'], + }, + handler: async (args: { + objectName: string; + operation: 'insert' | 'update' | 'upsert' | 'delete' | 'hardDelete'; + externalIdField?: string; + }) => { + const jobInfo = await client.createBulkJob({ + object: args.objectName, + operation: args.operation, + externalIdFieldName: args.externalIdField, + contentType: 'CSV', + lineEnding: 'LF', + }); + + return { + jobId: jobInfo.id, + object: jobInfo.object, + operation: jobInfo.operation, + state: jobInfo.state, + createdDate: jobInfo.createdDate, + }; + }, + }, + + { + name: 'sf_bulk_upload_data', + description: 'Upload CSV data to a bulk job', + inputSchema: { + type: 'object', + properties: { + jobId: { + type: 'string', + description: 'The bulk job ID', + }, + csvData: { + type: 'string', + description: 'CSV data with headers (e.g., "Name,Email\\nJohn,john@example.com\\nJane,jane@example.com")', + }, + }, + required: ['jobId', 'csvData'], + }, + handler: async (args: { jobId: string; csvData: string }) => { + await client.uploadBulkData(args.jobId, args.csvData); + return { + success: true, + jobId: args.jobId, + message: 'Data uploaded successfully. Use sf_bulk_close_job to start processing.', + }; + }, + }, + + { + name: 'sf_bulk_close_job', + description: 'Close a bulk job and start processing the uploaded data', + inputSchema: { + type: 'object', + properties: { + jobId: { + type: 'string', + description: 'The bulk job ID to close', + }, + }, + required: ['jobId'], + }, + handler: async (args: { jobId: string }) => { + const jobInfo = await client.closeBulkJob(args.jobId); + return { + jobId: jobInfo.id, + state: jobInfo.state, + message: 'Job closed and processing started. Use sf_bulk_get_job_status to check progress.', + }; + }, + }, + + { + name: 'sf_bulk_get_job_status', + description: 'Get the status of a bulk job', + inputSchema: { + type: 'object', + properties: { + jobId: { + type: 'string', + description: 'The bulk job ID', + }, + }, + required: ['jobId'], + }, + handler: async (args: { jobId: string }) => { + const jobInfo = await client.getBulkJobInfo(args.jobId); + return { + jobId: jobInfo.id, + object: jobInfo.object, + operation: jobInfo.operation, + state: jobInfo.state, + recordsProcessed: jobInfo.numberRecordsProcessed, + recordsFailed: jobInfo.numberRecordsFailed, + createdDate: jobInfo.createdDate, + systemModstamp: jobInfo.systemModstamp, + totalProcessingTime: jobInfo.totalProcessingTime, + }; + }, + }, + + { + name: 'sf_bulk_get_successful_results', + description: 'Get successful results from a completed bulk job (returns CSV)', + inputSchema: { + type: 'object', + properties: { + jobId: { + type: 'string', + description: 'The bulk job ID', + }, + }, + required: ['jobId'], + }, + handler: async (args: { jobId: string }) => { + const csvResults = await client.getBulkJobSuccessfulResults(args.jobId); + return { + jobId: args.jobId, + format: 'CSV', + data: csvResults, + }; + }, + }, + + { + name: 'sf_bulk_get_failed_results', + description: 'Get failed results from a completed bulk job with error details (returns CSV)', + inputSchema: { + type: 'object', + properties: { + jobId: { + type: 'string', + description: 'The bulk job ID', + }, + }, + required: ['jobId'], + }, + handler: async (args: { jobId: string }) => { + const csvResults = await client.getBulkJobFailedResults(args.jobId); + return { + jobId: args.jobId, + format: 'CSV', + data: csvResults, + }; + }, + }, + + { + name: 'sf_bulk_abort_job', + description: 'Abort a bulk job that is in progress', + inputSchema: { + type: 'object', + properties: { + jobId: { + type: 'string', + description: 'The bulk job ID to abort', + }, + }, + required: ['jobId'], + }, + handler: async (args: { jobId: string }) => { + const jobInfo = await client.abortBulkJob(args.jobId); + return { + jobId: jobInfo.id, + state: jobInfo.state, + message: 'Job aborted successfully', + }; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/campaigns.ts b/servers/salesforce/src/tools/campaigns.ts new file mode 100644 index 0000000..98275c5 --- /dev/null +++ b/servers/salesforce/src/tools/campaigns.ts @@ -0,0 +1,371 @@ +/** + * Campaign and Campaign Member management tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { Campaign, CampaignMember } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_list_campaigns', + description: 'List campaigns with optional filters. Returns up to 200 campaigns by default.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of campaigns to return (default: 200)', + default: 200, + }, + offset: { + type: 'number', + description: 'Number of records to skip (for pagination)', + }, + orderBy: { + type: 'string', + description: 'Field to sort by (e.g., "Name ASC", "StartDate DESC")', + default: 'Name ASC', + }, + isActive: { + type: 'boolean', + description: 'Filter by active status', + }, + type: { + type: 'string', + description: 'Filter by campaign type', + }, + status: { + type: 'string', + description: 'Filter by campaign status', + }, + }, + }, + handler: async (args: { + limit?: number; + offset?: number; + orderBy?: string; + isActive?: boolean; + type?: string; + status?: string; + }) => { + const conditions: string[] = []; + if (args.isActive !== undefined) conditions.push(`IsActive = ${args.isActive}`); + if (args.type) conditions.push(`Type = '${args.type}'`); + if (args.status) conditions.push(`Status = '${args.status}'`); + + const soql = client.buildSOQL({ + select: ['Id', 'Name', 'Type', 'Status', 'StartDate', 'EndDate', 'IsActive', 'BudgetedCost', 'ActualCost', 'ExpectedRevenue', 'NumberOfLeads', 'NumberOfContacts', 'Owner.Name', 'CreatedDate'], + from: 'Campaign', + where: conditions.length > 0 ? conditions.join(' AND ') : undefined, + orderBy: args.orderBy || 'Name ASC', + limit: args.limit || 200, + offset: args.offset, + }); + + const result = await client.query(soql); + return { + totalSize: result.totalSize, + records: result.records, + hasMore: !result.done, + }; + }, + }, + + { + name: 'sf_get_campaign', + description: 'Get detailed information about a specific campaign by ID', + inputSchema: { + type: 'object', + properties: { + campaignId: { + type: 'string', + description: 'The Salesforce ID of the campaign (15 or 18 characters)', + }, + }, + required: ['campaignId'], + }, + handler: async (args: { campaignId: string }) => { + const campaign = await client.getRecord('Campaign', args.campaignId); + return campaign; + }, + }, + + { + name: 'sf_create_campaign', + description: 'Create a new campaign', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Campaign name (required)', + }, + type: { + type: 'string', + description: 'Campaign type (e.g., "Email", "Webinar", "Conference")', + }, + status: { + type: 'string', + description: 'Campaign status (e.g., "Planned", "In Progress", "Completed")', + }, + startDate: { + type: 'string', + description: 'Start date in YYYY-MM-DD format', + }, + endDate: { + type: 'string', + description: 'End date in YYYY-MM-DD format', + }, + description: { + type: 'string', + description: 'Campaign description', + }, + isActive: { + type: 'boolean', + description: 'Whether the campaign is active', + }, + budgetedCost: { + type: 'number', + description: 'Budgeted cost', + }, + actualCost: { + type: 'number', + description: 'Actual cost', + }, + expectedRevenue: { + type: 'number', + description: 'Expected revenue', + }, + }, + required: ['name'], + }, + handler: async (args: { + name: string; + type?: string; + status?: string; + startDate?: string; + endDate?: string; + description?: string; + isActive?: boolean; + budgetedCost?: number; + actualCost?: number; + expectedRevenue?: number; + }) => { + const campaignData: Partial = { + Name: args.name, + Type: args.type, + Status: args.status, + StartDate: args.startDate, + EndDate: args.endDate, + Description: args.description, + IsActive: args.isActive, + BudgetedCost: args.budgetedCost, + ActualCost: args.actualCost, + ExpectedRevenue: args.expectedRevenue, + }; + + const result = await client.createRecord('Campaign', campaignData); + return result; + }, + }, + + { + name: 'sf_update_campaign', + description: 'Update an existing campaign', + inputSchema: { + type: 'object', + properties: { + campaignId: { + type: 'string', + description: 'The Salesforce ID of the campaign to update', + }, + name: { type: 'string' }, + type: { type: 'string' }, + status: { type: 'string' }, + startDate: { type: 'string', description: 'Start date in YYYY-MM-DD format' }, + endDate: { type: 'string', description: 'End date in YYYY-MM-DD format' }, + description: { type: 'string' }, + isActive: { type: 'boolean' }, + budgetedCost: { type: 'number' }, + actualCost: { type: 'number' }, + expectedRevenue: { type: 'number' }, + }, + required: ['campaignId'], + }, + handler: async (args: { + campaignId: string; + name?: string; + type?: string; + status?: string; + startDate?: string; + endDate?: string; + description?: string; + isActive?: boolean; + budgetedCost?: number; + actualCost?: number; + expectedRevenue?: number; + }) => { + const { campaignId, ...updates } = args; + const campaignData: Partial = { + Name: updates.name, + Type: updates.type, + Status: updates.status, + StartDate: updates.startDate, + EndDate: updates.endDate, + Description: updates.description, + IsActive: updates.isActive, + BudgetedCost: updates.budgetedCost, + ActualCost: updates.actualCost, + ExpectedRevenue: updates.expectedRevenue, + }; + + // Remove undefined values + Object.keys(campaignData).forEach((key) => { + if (campaignData[key as keyof Campaign] === undefined) { + delete campaignData[key as keyof Campaign]; + } + }); + + await client.updateRecord('Campaign', campaignId, campaignData); + return { success: true, id: campaignId }; + }, + }, + + { + name: 'sf_add_campaign_member', + description: 'Add a lead or contact to a campaign as a campaign member', + inputSchema: { + type: 'object', + properties: { + campaignId: { + type: 'string', + description: 'The Salesforce ID of the campaign', + }, + leadId: { + type: 'string', + description: 'The Lead ID (either leadId or contactId is required)', + }, + contactId: { + type: 'string', + description: 'The Contact ID (either leadId or contactId is required)', + }, + status: { + type: 'string', + description: 'Member status (e.g., "Sent", "Responded")', + }, + }, + required: ['campaignId'], + }, + handler: async (args: { + campaignId: string; + leadId?: string; + contactId?: string; + status?: string; + }) => { + if (!args.leadId && !args.contactId) { + throw new Error('Either leadId or contactId must be provided'); + } + + const memberData: Partial = { + CampaignId: args.campaignId as any, + LeadId: args.leadId as any, + ContactId: args.contactId as any, + Status: args.status, + }; + + const result = await client.createRecord('CampaignMember', memberData); + return result; + }, + }, + + { + name: 'sf_list_campaign_members', + description: 'List members of a specific campaign', + inputSchema: { + type: 'object', + properties: { + campaignId: { + type: 'string', + description: 'The Salesforce ID of the campaign', + }, + limit: { + type: 'number', + description: 'Maximum number of members to return', + default: 200, + }, + }, + required: ['campaignId'], + }, + handler: async (args: { campaignId: string; limit?: number }) => { + const soql = client.buildSOQL({ + select: ['Id', 'CampaignId', 'LeadId', 'ContactId', 'Status', 'HasResponded', 'FirstRespondedDate', 'Lead.Name', 'Contact.Name'], + from: 'CampaignMember', + where: `CampaignId = '${args.campaignId}'`, + limit: args.limit || 200, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + + { + name: 'sf_get_campaign_stats', + description: 'Get statistics for a specific campaign', + inputSchema: { + type: 'object', + properties: { + campaignId: { + type: 'string', + description: 'The Salesforce ID of the campaign', + }, + }, + required: ['campaignId'], + }, + handler: async (args: { campaignId: string }) => { + // Get campaign data + const campaign = await client.getRecord('Campaign', args.campaignId, [ + 'Id', + 'Name', + 'NumberOfLeads', + 'NumberOfConvertedLeads', + 'NumberOfContacts', + 'NumberOfOpportunities', + 'BudgetedCost', + 'ActualCost', + 'ExpectedRevenue', + ]); + + // Get member count with responses + const memberStatsSOQL = ` + SELECT COUNT(Id) total, + COUNT_DISTINCT(CASE WHEN HasResponded = true THEN Id END) responded + FROM CampaignMember + WHERE CampaignId = '${args.campaignId}' + `; + + const memberStats = await client.query(memberStatsSOQL); + + return { + campaign: { + id: campaign.Id, + name: campaign.Name, + }, + leads: { + total: campaign.NumberOfLeads || 0, + converted: campaign.NumberOfConvertedLeads || 0, + }, + contacts: campaign.NumberOfContacts || 0, + opportunities: campaign.NumberOfOpportunities || 0, + members: memberStats.records[0] || { total: 0, responded: 0 }, + budget: { + budgeted: campaign.BudgetedCost || 0, + actual: campaign.ActualCost || 0, + }, + expectedRevenue: campaign.ExpectedRevenue || 0, + }; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/cases.ts b/servers/salesforce/src/tools/cases.ts new file mode 100644 index 0000000..f985ba9 --- /dev/null +++ b/servers/salesforce/src/tools/cases.ts @@ -0,0 +1,352 @@ +/** + * Case management tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { Case } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_list_cases', + description: 'List cases with optional filters. Returns up to 200 cases by default.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of cases to return (default: 200)', + default: 200, + }, + offset: { + type: 'number', + description: 'Number of records to skip (for pagination)', + }, + orderBy: { + type: 'string', + description: 'Field to sort by (e.g., "CreatedDate DESC", "Priority ASC")', + default: 'CreatedDate DESC', + }, + status: { + type: 'string', + description: 'Filter by case status (e.g., "New", "Working", "Escalated")', + }, + priority: { + type: 'string', + description: 'Filter by priority (e.g., "High", "Medium", "Low")', + }, + accountId: { + type: 'string', + description: 'Filter by account ID', + }, + isClosed: { + type: 'boolean', + description: 'Filter by closed status', + }, + }, + }, + handler: async (args: { + limit?: number; + offset?: number; + orderBy?: string; + status?: string; + priority?: string; + accountId?: string; + isClosed?: boolean; + }) => { + const conditions: string[] = []; + if (args.status) conditions.push(`Status = '${args.status}'`); + if (args.priority) conditions.push(`Priority = '${args.priority}'`); + if (args.accountId) conditions.push(`AccountId = '${args.accountId}'`); + if (args.isClosed !== undefined) conditions.push(`IsClosed = ${args.isClosed}`); + + const soql = client.buildSOQL({ + select: ['Id', 'CaseNumber', 'Subject', 'Status', 'Priority', 'Origin', 'Type', 'Reason', 'Account.Name', 'Contact.Name', 'IsClosed', 'Owner.Name', 'CreatedDate'], + from: 'Case', + where: conditions.length > 0 ? conditions.join(' AND ') : undefined, + orderBy: args.orderBy || 'CreatedDate DESC', + limit: args.limit || 200, + offset: args.offset, + }); + + const result = await client.query(soql); + return { + totalSize: result.totalSize, + records: result.records, + hasMore: !result.done, + }; + }, + }, + + { + name: 'sf_get_case', + description: 'Get detailed information about a specific case by ID', + inputSchema: { + type: 'object', + properties: { + caseId: { + type: 'string', + description: 'The Salesforce ID of the case (15 or 18 characters)', + }, + }, + required: ['caseId'], + }, + handler: async (args: { caseId: string }) => { + const caseRecord = await client.getRecord('Case', args.caseId); + return caseRecord; + }, + }, + + { + name: 'sf_create_case', + description: 'Create a new case', + inputSchema: { + type: 'object', + properties: { + status: { + type: 'string', + description: 'Case status (required, e.g., "New", "Working")', + }, + subject: { + type: 'string', + description: 'Case subject/title', + }, + description: { + type: 'string', + description: 'Case description', + }, + priority: { + type: 'string', + description: 'Priority (e.g., "High", "Medium", "Low")', + }, + origin: { + type: 'string', + description: 'Case origin (e.g., "Phone", "Email", "Web")', + }, + type: { + type: 'string', + description: 'Case type', + }, + reason: { + type: 'string', + description: 'Case reason', + }, + accountId: { + type: 'string', + description: 'Associated account ID', + }, + contactId: { + type: 'string', + description: 'Associated contact ID', + }, + }, + required: ['status'], + }, + handler: async (args: { + status: string; + subject?: string; + description?: string; + priority?: string; + origin?: string; + type?: string; + reason?: string; + accountId?: string; + contactId?: string; + }) => { + const caseData: Partial = { + Status: args.status, + Subject: args.subject, + Description: args.description, + Priority: args.priority, + Origin: args.origin, + Type: args.type, + Reason: args.reason, + AccountId: args.accountId as any, + ContactId: args.contactId as any, + }; + + const result = await client.createRecord('Case', caseData); + return result; + }, + }, + + { + name: 'sf_update_case', + description: 'Update an existing case', + inputSchema: { + type: 'object', + properties: { + caseId: { + type: 'string', + description: 'The Salesforce ID of the case to update', + }, + status: { type: 'string' }, + subject: { type: 'string' }, + description: { type: 'string' }, + priority: { type: 'string' }, + origin: { type: 'string' }, + type: { type: 'string' }, + reason: { type: 'string' }, + accountId: { type: 'string' }, + contactId: { type: 'string' }, + }, + required: ['caseId'], + }, + handler: async (args: { + caseId: string; + status?: string; + subject?: string; + description?: string; + priority?: string; + origin?: string; + type?: string; + reason?: string; + accountId?: string; + contactId?: string; + }) => { + const { caseId, ...updates } = args; + const caseData: Partial = { + Status: updates.status, + Subject: updates.subject, + Description: updates.description, + Priority: updates.priority, + Origin: updates.origin, + Type: updates.type, + Reason: updates.reason, + AccountId: updates.accountId as any, + ContactId: updates.contactId as any, + }; + + // Remove undefined values + Object.keys(caseData).forEach((key) => { + if (caseData[key as keyof Case] === undefined) { + delete caseData[key as keyof Case]; + } + }); + + await client.updateRecord('Case', caseId, caseData); + return { success: true, id: caseId }; + }, + }, + + { + name: 'sf_delete_case', + description: 'Delete a case by ID', + inputSchema: { + type: 'object', + properties: { + caseId: { + type: 'string', + description: 'The Salesforce ID of the case to delete', + }, + }, + required: ['caseId'], + }, + handler: async (args: { caseId: string }) => { + await client.deleteRecord('Case', args.caseId); + return { success: true, id: args.caseId }; + }, + }, + + { + name: 'sf_close_case', + description: 'Close a case by updating its status to a closed status', + inputSchema: { + type: 'object', + properties: { + caseId: { + type: 'string', + description: 'The Salesforce ID of the case to close', + }, + closedStatus: { + type: 'string', + description: 'The closed status to set (e.g., "Closed", "Closed - Resolved")', + default: 'Closed', + }, + }, + required: ['caseId'], + }, + handler: async (args: { caseId: string; closedStatus?: string }) => { + const updateData: Partial = { + Status: args.closedStatus || 'Closed', + }; + await client.updateRecord('Case', args.caseId, updateData); + return { success: true, id: args.caseId, status: args.closedStatus || 'Closed' }; + }, + }, + + { + name: 'sf_escalate_case', + description: 'Escalate a case by updating priority and/or status', + inputSchema: { + type: 'object', + properties: { + caseId: { + type: 'string', + description: 'The Salesforce ID of the case to escalate', + }, + priority: { + type: 'string', + description: 'New priority (e.g., "High", "Critical")', + default: 'High', + }, + status: { + type: 'string', + description: 'New status (e.g., "Escalated")', + }, + }, + required: ['caseId'], + }, + handler: async (args: { caseId: string; priority?: string; status?: string }) => { + const updates: Partial = {}; + if (args.priority) updates.Priority = args.priority; + if (args.status) updates.Status = args.status; + + await client.updateRecord('Case', args.caseId, updates); + return { success: true, id: args.caseId, updates }; + }, + }, + + { + name: 'sf_search_cases', + description: 'Search cases using SOQL with flexible criteria', + inputSchema: { + type: 'object', + properties: { + searchText: { + type: 'string', + description: 'Text to search in Subject and Description fields', + }, + whereClause: { + type: 'string', + description: 'Custom WHERE clause (e.g., "Priority = \'High\' AND IsClosed = false")', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 100, + }, + }, + }, + handler: async (args: { searchText?: string; whereClause?: string; limit?: number }) => { + let where = args.whereClause; + + if (args.searchText) { + const searchCondition = `(Subject LIKE '%${args.searchText}%' OR Description LIKE '%${args.searchText}%')`; + where = where ? `${searchCondition} AND (${where})` : searchCondition; + } + + const soql = client.buildSOQL({ + select: ['Id', 'CaseNumber', 'Subject', 'Status', 'Priority', 'Origin', 'Account.Name', 'Contact.Name', 'IsClosed', 'CreatedDate'], + from: 'Case', + where, + orderBy: 'CreatedDate DESC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/contacts.ts b/servers/salesforce/src/tools/contacts.ts new file mode 100644 index 0000000..7c640ef --- /dev/null +++ b/servers/salesforce/src/tools/contacts.ts @@ -0,0 +1,398 @@ +/** + * Contact management tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { Contact } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_list_contacts', + description: 'List contacts with optional filters. Returns up to 200 contacts by default.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of contacts to return (default: 200)', + default: 200, + }, + offset: { + type: 'number', + description: 'Number of records to skip (for pagination)', + }, + orderBy: { + type: 'string', + description: 'Field to sort by (e.g., "LastName ASC", "CreatedDate DESC")', + default: 'LastName ASC', + }, + accountId: { + type: 'string', + description: 'Filter by account ID', + }, + }, + }, + handler: async (args: { + limit?: number; + offset?: number; + orderBy?: string; + accountId?: string; + }) => { + const conditions: string[] = []; + if (args.accountId) conditions.push(`AccountId = '${args.accountId}'`); + + const soql = client.buildSOQL({ + select: ['Id', 'FirstName', 'LastName', 'Email', 'Phone', 'Title', 'Account.Name', 'MailingCity', 'MailingState', 'Owner.Name', 'CreatedDate'], + from: 'Contact', + where: conditions.length > 0 ? conditions.join(' AND ') : undefined, + orderBy: args.orderBy || 'LastName ASC', + limit: args.limit || 200, + offset: args.offset, + }); + + const result = await client.query(soql); + return { + totalSize: result.totalSize, + records: result.records, + hasMore: !result.done, + }; + }, + }, + + { + name: 'sf_get_contact', + description: 'Get detailed information about a specific contact by ID', + inputSchema: { + type: 'object', + properties: { + contactId: { + type: 'string', + description: 'The Salesforce ID of the contact (15 or 18 characters)', + }, + }, + required: ['contactId'], + }, + handler: async (args: { contactId: string }) => { + const contact = await client.getRecord('Contact', args.contactId); + return contact; + }, + }, + + { + name: 'sf_create_contact', + description: 'Create a new contact', + inputSchema: { + type: 'object', + properties: { + lastName: { + type: 'string', + description: 'Last name (required)', + }, + firstName: { + type: 'string', + description: 'First name', + }, + email: { + type: 'string', + description: 'Email address', + }, + phone: { + type: 'string', + description: 'Phone number', + }, + mobilePhone: { + type: 'string', + description: 'Mobile phone number', + }, + title: { + type: 'string', + description: 'Job title', + }, + department: { + type: 'string', + description: 'Department', + }, + accountId: { + type: 'string', + description: 'Associated account ID', + }, + mailingStreet: { + type: 'string', + description: 'Mailing street address', + }, + mailingCity: { + type: 'string', + description: 'Mailing city', + }, + mailingState: { + type: 'string', + description: 'Mailing state/province', + }, + mailingPostalCode: { + type: 'string', + description: 'Mailing postal code', + }, + mailingCountry: { + type: 'string', + description: 'Mailing country', + }, + description: { + type: 'string', + description: 'Contact description', + }, + }, + required: ['lastName'], + }, + handler: async (args: { + lastName: string; + firstName?: string; + email?: string; + phone?: string; + mobilePhone?: string; + title?: string; + department?: string; + accountId?: string; + mailingStreet?: string; + mailingCity?: string; + mailingState?: string; + mailingPostalCode?: string; + mailingCountry?: string; + description?: string; + }) => { + const contactData: Partial = { + LastName: args.lastName, + FirstName: args.firstName, + Email: args.email, + Phone: args.phone, + MobilePhone: args.mobilePhone, + Title: args.title, + Department: args.department, + AccountId: args.accountId as any, + MailingStreet: args.mailingStreet, + MailingCity: args.mailingCity, + MailingState: args.mailingState, + MailingPostalCode: args.mailingPostalCode, + MailingCountry: args.mailingCountry, + Description: args.description, + }; + + const result = await client.createRecord('Contact', contactData); + return result; + }, + }, + + { + name: 'sf_update_contact', + description: 'Update an existing contact', + inputSchema: { + type: 'object', + properties: { + contactId: { + type: 'string', + description: 'The Salesforce ID of the contact to update', + }, + lastName: { + type: 'string', + description: 'Last name', + }, + firstName: { + type: 'string', + description: 'First name', + }, + email: { + type: 'string', + description: 'Email address', + }, + phone: { + type: 'string', + description: 'Phone number', + }, + mobilePhone: { + type: 'string', + description: 'Mobile phone number', + }, + title: { + type: 'string', + description: 'Job title', + }, + department: { + type: 'string', + description: 'Department', + }, + accountId: { + type: 'string', + description: 'Associated account ID', + }, + mailingStreet: { + type: 'string', + description: 'Mailing street address', + }, + mailingCity: { + type: 'string', + description: 'Mailing city', + }, + mailingState: { + type: 'string', + description: 'Mailing state/province', + }, + mailingPostalCode: { + type: 'string', + description: 'Mailing postal code', + }, + mailingCountry: { + type: 'string', + description: 'Mailing country', + }, + description: { + type: 'string', + description: 'Contact description', + }, + }, + required: ['contactId'], + }, + handler: async (args: { + contactId: string; + lastName?: string; + firstName?: string; + email?: string; + phone?: string; + mobilePhone?: string; + title?: string; + department?: string; + accountId?: string; + mailingStreet?: string; + mailingCity?: string; + mailingState?: string; + mailingPostalCode?: string; + mailingCountry?: string; + description?: string; + }) => { + const { contactId, ...updates } = args; + const contactData: Partial = { + LastName: updates.lastName, + FirstName: updates.firstName, + Email: updates.email, + Phone: updates.phone, + MobilePhone: updates.mobilePhone, + Title: updates.title, + Department: updates.department, + AccountId: updates.accountId as any, + MailingStreet: updates.mailingStreet, + MailingCity: updates.mailingCity, + MailingState: updates.mailingState, + MailingPostalCode: updates.mailingPostalCode, + MailingCountry: updates.mailingCountry, + Description: updates.description, + }; + + // Remove undefined values + Object.keys(contactData).forEach((key) => { + if (contactData[key as keyof Contact] === undefined) { + delete contactData[key as keyof Contact]; + } + }); + + await client.updateRecord('Contact', contactId, contactData); + return { success: true, id: contactId }; + }, + }, + + { + name: 'sf_delete_contact', + description: 'Delete a contact by ID', + inputSchema: { + type: 'object', + properties: { + contactId: { + type: 'string', + description: 'The Salesforce ID of the contact to delete', + }, + }, + required: ['contactId'], + }, + handler: async (args: { contactId: string }) => { + await client.deleteRecord('Contact', args.contactId); + return { success: true, id: args.contactId }; + }, + }, + + { + name: 'sf_search_contacts', + description: 'Search contacts using SOQL with flexible criteria', + inputSchema: { + type: 'object', + properties: { + searchText: { + type: 'string', + description: 'Text to search in FirstName, LastName, or Email fields', + }, + whereClause: { + type: 'string', + description: 'Custom WHERE clause (e.g., "Department = \'Sales\'")', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 100, + }, + }, + }, + handler: async (args: { searchText?: string; whereClause?: string; limit?: number }) => { + let where = args.whereClause; + + if (args.searchText) { + const searchCondition = `(FirstName LIKE '%${args.searchText}%' OR LastName LIKE '%${args.searchText}%' OR Email LIKE '%${args.searchText}%')`; + where = where ? `${searchCondition} AND (${where})` : searchCondition; + } + + const soql = client.buildSOQL({ + select: ['Id', 'FirstName', 'LastName', 'Email', 'Phone', 'Title', 'Department', 'Account.Name', 'MailingCity', 'MailingState'], + from: 'Contact', + where, + orderBy: 'LastName ASC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + + { + name: 'sf_find_contacts_by_email', + description: 'Find contacts by email address (exact or partial match)', + inputSchema: { + type: 'object', + properties: { + email: { + type: 'string', + description: 'Email address to search for', + }, + exactMatch: { + type: 'boolean', + description: 'Whether to require exact match (default: false)', + default: false, + }, + }, + required: ['email'], + }, + handler: async (args: { email: string; exactMatch?: boolean }) => { + const condition = args.exactMatch + ? `Email = '${args.email}'` + : `Email LIKE '%${args.email}%'`; + + const soql = client.buildSOQL({ + select: ['Id', 'FirstName', 'LastName', 'Email', 'Phone', 'Title', 'Account.Name'], + from: 'Contact', + where: condition, + orderBy: 'LastName ASC', + limit: 50, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/custom-objects.ts b/servers/salesforce/src/tools/custom-objects.ts new file mode 100644 index 0000000..51b5479 --- /dev/null +++ b/servers/salesforce/src/tools/custom-objects.ts @@ -0,0 +1,250 @@ +/** + * Generic custom object CRUD tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { SObject, CustomSObject } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_describe_object', + description: 'Describe any SObject (standard or custom) to get field metadata', + inputSchema: { + type: 'object', + properties: { + objectName: { + type: 'string', + description: 'Name of the SObject (e.g., "Account", "MyCustomObject__c")', + }, + }, + required: ['objectName'], + }, + handler: async (args: { objectName: string }) => { + const describe = await client.describe(args.objectName); + return { + name: describe.name, + label: describe.label, + labelPlural: describe.labelPlural, + custom: describe.custom, + createable: describe.createable, + updateable: describe.updateable, + deletable: describe.deletable, + queryable: describe.queryable, + fields: describe.fields.map(f => ({ + name: f.name, + label: f.label, + type: f.type, + length: f.length, + createable: f.createable, + updateable: f.updateable, + nillable: f.nillable, + picklistValues: f.picklistValues, + referenceTo: f.referenceTo, + })), + childRelationships: describe.childRelationships, + }; + }, + }, + + { + name: 'sf_list_custom_objects', + description: 'List all custom objects in the org', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of custom objects to return', + default: 100, + }, + }, + }, + handler: async (args: { limit?: number }) => { + // Query EntityDefinition for custom objects + const soql = client.buildSOQL({ + select: ['QualifiedApiName', 'Label', 'PluralLabel', 'IsCustomizable', 'IsQueryable'], + from: 'EntityDefinition', + where: 'IsCustomizable = true AND QualifiedApiName LIKE \'%__c\'', + orderBy: 'QualifiedApiName ASC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + + { + name: 'sf_get_custom_record', + description: 'Get a record from any custom object by ID', + inputSchema: { + type: 'object', + properties: { + objectName: { + type: 'string', + description: 'Name of the custom object (e.g., "MyCustomObject__c")', + }, + recordId: { + type: 'string', + description: 'The Salesforce ID of the record', + }, + fields: { + type: 'array', + description: 'Optional: specific fields to retrieve', + items: { type: 'string' }, + }, + }, + required: ['objectName', 'recordId'], + }, + handler: async (args: { objectName: string; recordId: string; fields?: string[] }) => { + const record = await client.getRecord( + args.objectName, + args.recordId, + args.fields + ); + return record; + }, + }, + + { + name: 'sf_list_custom_records', + description: 'List records from any custom object', + inputSchema: { + type: 'object', + properties: { + objectName: { + type: 'string', + description: 'Name of the custom object (e.g., "MyCustomObject__c")', + }, + fields: { + type: 'array', + description: 'Fields to retrieve (defaults to Id, Name, CreatedDate)', + items: { type: 'string' }, + }, + whereClause: { + type: 'string', + description: 'Optional WHERE clause for filtering', + }, + orderBy: { + type: 'string', + description: 'Field to sort by (default: CreatedDate DESC)', + }, + limit: { + type: 'number', + description: 'Maximum number of records to return', + default: 200, + }, + offset: { + type: 'number', + description: 'Number of records to skip (for pagination)', + }, + }, + required: ['objectName'], + }, + handler: async (args: { + objectName: string; + fields?: string[]; + whereClause?: string; + orderBy?: string; + limit?: number; + offset?: number; + }) => { + const fields = args.fields || ['Id', 'Name', 'CreatedDate', 'LastModifiedDate']; + + const soql = client.buildSOQL({ + select: fields, + from: args.objectName, + where: args.whereClause, + orderBy: args.orderBy || 'CreatedDate DESC', + limit: args.limit || 200, + offset: args.offset, + }); + + const result = await client.query(soql); + return { + totalSize: result.totalSize, + records: result.records, + hasMore: !result.done, + }; + }, + }, + + { + name: 'sf_create_custom_record', + description: 'Create a new record in any custom object', + inputSchema: { + type: 'object', + properties: { + objectName: { + type: 'string', + description: 'Name of the custom object (e.g., "MyCustomObject__c")', + }, + fields: { + type: 'object', + description: 'Field values as key-value pairs (e.g., {"Name": "Test", "CustomField__c": "Value"})', + }, + }, + required: ['objectName', 'fields'], + }, + handler: async (args: { objectName: string; fields: Record }) => { + const result = await client.createRecord(args.objectName, args.fields); + return result; + }, + }, + + { + name: 'sf_update_custom_record', + description: 'Update an existing record in any custom object', + inputSchema: { + type: 'object', + properties: { + objectName: { + type: 'string', + description: 'Name of the custom object (e.g., "MyCustomObject__c")', + }, + recordId: { + type: 'string', + description: 'The Salesforce ID of the record to update', + }, + fields: { + type: 'object', + description: 'Field values to update as key-value pairs', + }, + }, + required: ['objectName', 'recordId', 'fields'], + }, + handler: async (args: { + objectName: string; + recordId: string; + fields: Record; + }) => { + await client.updateRecord(args.objectName, args.recordId, args.fields); + return { success: true, id: args.recordId }; + }, + }, + + { + name: 'sf_delete_custom_record', + description: 'Delete a record from any custom object', + inputSchema: { + type: 'object', + properties: { + objectName: { + type: 'string', + description: 'Name of the custom object (e.g., "MyCustomObject__c")', + }, + recordId: { + type: 'string', + description: 'The Salesforce ID of the record to delete', + }, + }, + required: ['objectName', 'recordId'], + }, + handler: async (args: { objectName: string; recordId: string }) => { + await client.deleteRecord(args.objectName, args.recordId); + return { success: true, id: args.recordId }; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/dashboards.ts b/servers/salesforce/src/tools/dashboards.ts new file mode 100644 index 0000000..dde5e27 --- /dev/null +++ b/servers/salesforce/src/tools/dashboards.ts @@ -0,0 +1,156 @@ +/** + * Dashboard management tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { Dashboard } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_list_dashboards', + description: 'List available dashboards. Returns up to 200 dashboards by default.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of dashboards to return (default: 200)', + default: 200, + }, + offset: { + type: 'number', + description: 'Number of records to skip (for pagination)', + }, + }, + }, + handler: async (args: { limit?: number; offset?: number }) => { + const soql = client.buildSOQL({ + select: ['Id', 'Title', 'DeveloperName', 'FolderName', 'Description', 'RunningUser.Name', 'CreatedDate'], + from: 'Dashboard', + orderBy: 'Title ASC', + limit: args.limit || 200, + offset: args.offset, + }); + + const result = await client.query(soql); + return { + totalSize: result.totalSize, + records: result.records, + hasMore: !result.done, + }; + }, + }, + + { + name: 'sf_get_dashboard', + description: 'Get detailed information about a specific dashboard by ID', + inputSchema: { + type: 'object', + properties: { + dashboardId: { + type: 'string', + description: 'The Salesforce ID of the dashboard (15 or 18 characters)', + }, + }, + required: ['dashboardId'], + }, + handler: async (args: { dashboardId: string }) => { + const dashboard = await client.getRecord('Dashboard', args.dashboardId); + return dashboard; + }, + }, + + { + name: 'sf_describe_dashboard', + description: 'Get detailed metadata about a dashboard including components', + inputSchema: { + type: 'object', + properties: { + dashboardId: { + type: 'string', + description: 'The Salesforce ID of the dashboard', + }, + }, + required: ['dashboardId'], + }, + handler: async (args: { dashboardId: string }) => { + // Use the Analytics API to describe the dashboard + const response = await client['client'].get(`/analytics/dashboards/${args.dashboardId}/describe`); + return response.data; + }, + }, + + { + name: 'sf_get_dashboard_components', + description: 'Get the components (charts, tables, metrics) of a dashboard', + inputSchema: { + type: 'object', + properties: { + dashboardId: { + type: 'string', + description: 'The Salesforce ID of the dashboard', + }, + }, + required: ['dashboardId'], + }, + handler: async (args: { dashboardId: string }) => { + // Query DashboardComponent objects + const soql = client.buildSOQL({ + select: ['Id', 'Name', 'DashboardId'], + from: 'DashboardComponent', + where: `DashboardId = '${args.dashboardId}'`, + orderBy: 'Name ASC', + }); + + const result = await client.query(soql); + return result.records; + }, + }, + + { + name: 'sf_search_dashboards', + description: 'Search for dashboards by title or developer name', + inputSchema: { + type: 'object', + properties: { + searchText: { + type: 'string', + description: 'Text to search in dashboard title or developer name', + }, + folderName: { + type: 'string', + description: 'Filter by folder name', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 50, + }, + }, + }, + handler: async (args: { searchText?: string; folderName?: string; limit?: number }) => { + const conditions: string[] = []; + + if (args.searchText) { + conditions.push(`(Title LIKE '%${args.searchText}%' OR DeveloperName LIKE '%${args.searchText}%')`); + } + + if (args.folderName) { + conditions.push(`FolderName = '${args.folderName}'`); + } + + const soql = client.buildSOQL({ + select: ['Id', 'Title', 'DeveloperName', 'FolderName', 'Description', 'RunningUser.Name'], + from: 'Dashboard', + where: conditions.length > 0 ? conditions.join(' AND ') : undefined, + orderBy: 'Title ASC', + limit: args.limit || 50, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/events.ts b/servers/salesforce/src/tools/events.ts new file mode 100644 index 0000000..a04bc16 --- /dev/null +++ b/servers/salesforce/src/tools/events.ts @@ -0,0 +1,302 @@ +/** + * Event management tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { Event } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_list_events', + description: 'List events with optional filters. Returns up to 200 events by default.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of events to return (default: 200)', + default: 200, + }, + offset: { + type: 'number', + description: 'Number of records to skip (for pagination)', + }, + orderBy: { + type: 'string', + description: 'Field to sort by (e.g., "StartDateTime ASC", "CreatedDate DESC")', + default: 'StartDateTime ASC', + }, + }, + }, + handler: async (args: { + limit?: number; + offset?: number; + orderBy?: string; + }) => { + const soql = client.buildSOQL({ + select: ['Id', 'Subject', 'StartDateTime', 'EndDateTime', 'Location', 'Description', 'IsAllDayEvent', 'IsPrivate', 'Who.Name', 'What.Name', 'Owner.Name', 'CreatedDate'], + from: 'Event', + orderBy: args.orderBy || 'StartDateTime ASC', + limit: args.limit || 200, + offset: args.offset, + }); + + const result = await client.query(soql); + return { + totalSize: result.totalSize, + records: result.records, + hasMore: !result.done, + }; + }, + }, + + { + name: 'sf_get_event', + description: 'Get detailed information about a specific event by ID', + inputSchema: { + type: 'object', + properties: { + eventId: { + type: 'string', + description: 'The Salesforce ID of the event (15 or 18 characters)', + }, + }, + required: ['eventId'], + }, + handler: async (args: { eventId: string }) => { + const event = await client.getRecord('Event', args.eventId); + return event; + }, + }, + + { + name: 'sf_create_event', + description: 'Create a new event', + inputSchema: { + type: 'object', + properties: { + subject: { + type: 'string', + description: 'Event subject/title (required)', + }, + startDateTime: { + type: 'string', + description: 'Start date and time in ISO 8601 format (required, e.g., "2024-03-15T14:00:00Z")', + }, + endDateTime: { + type: 'string', + description: 'End date and time in ISO 8601 format (required)', + }, + location: { + type: 'string', + description: 'Event location', + }, + description: { + type: 'string', + description: 'Event description', + }, + whoId: { + type: 'string', + description: 'Related Lead or Contact ID', + }, + whatId: { + type: 'string', + description: 'Related Account, Opportunity, or other object ID', + }, + isAllDayEvent: { + type: 'boolean', + description: 'Whether this is an all-day event', + }, + isPrivate: { + type: 'boolean', + description: 'Whether this is a private event', + }, + }, + required: ['subject', 'startDateTime', 'endDateTime'], + }, + handler: async (args: { + subject: string; + startDateTime: string; + endDateTime: string; + location?: string; + description?: string; + whoId?: string; + whatId?: string; + isAllDayEvent?: boolean; + isPrivate?: boolean; + }) => { + const eventData: Partial = { + Subject: args.subject, + StartDateTime: args.startDateTime, + EndDateTime: args.endDateTime, + Location: args.location, + Description: args.description, + WhoId: args.whoId as any, + WhatId: args.whatId as any, + IsAllDayEvent: args.isAllDayEvent, + IsPrivate: args.isPrivate, + }; + + const result = await client.createRecord('Event', eventData); + return result; + }, + }, + + { + name: 'sf_update_event', + description: 'Update an existing event', + inputSchema: { + type: 'object', + properties: { + eventId: { + type: 'string', + description: 'The Salesforce ID of the event to update', + }, + subject: { type: 'string' }, + startDateTime: { type: 'string', description: 'Start date and time in ISO 8601 format' }, + endDateTime: { type: 'string', description: 'End date and time in ISO 8601 format' }, + location: { type: 'string' }, + description: { type: 'string' }, + whoId: { type: 'string' }, + whatId: { type: 'string' }, + isAllDayEvent: { type: 'boolean' }, + isPrivate: { type: 'boolean' }, + }, + required: ['eventId'], + }, + handler: async (args: { + eventId: string; + subject?: string; + startDateTime?: string; + endDateTime?: string; + location?: string; + description?: string; + whoId?: string; + whatId?: string; + isAllDayEvent?: boolean; + isPrivate?: boolean; + }) => { + const { eventId, ...updates } = args; + const eventData: Partial = { + Subject: updates.subject, + StartDateTime: updates.startDateTime, + EndDateTime: updates.endDateTime, + Location: updates.location, + Description: updates.description, + WhoId: updates.whoId as any, + WhatId: updates.whatId as any, + IsAllDayEvent: updates.isAllDayEvent, + IsPrivate: updates.isPrivate, + }; + + // Remove undefined values + Object.keys(eventData).forEach((key) => { + if (eventData[key as keyof Event] === undefined) { + delete eventData[key as keyof Event]; + } + }); + + await client.updateRecord('Event', eventId, eventData); + return { success: true, id: eventId }; + }, + }, + + { + name: 'sf_delete_event', + description: 'Delete an event by ID', + inputSchema: { + type: 'object', + properties: { + eventId: { + type: 'string', + description: 'The Salesforce ID of the event to delete', + }, + }, + required: ['eventId'], + }, + handler: async (args: { eventId: string }) => { + await client.deleteRecord('Event', args.eventId); + return { success: true, id: args.eventId }; + }, + }, + + { + name: 'sf_search_events', + description: 'Search events using SOQL with flexible criteria', + inputSchema: { + type: 'object', + properties: { + searchText: { + type: 'string', + description: 'Text to search in Subject, Location, and Description fields', + }, + whereClause: { + type: 'string', + description: 'Custom WHERE clause (e.g., "IsAllDayEvent = true")', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 100, + }, + }, + }, + handler: async (args: { searchText?: string; whereClause?: string; limit?: number }) => { + let where = args.whereClause; + + if (args.searchText) { + const searchCondition = `(Subject LIKE '%${args.searchText}%' OR Location LIKE '%${args.searchText}%' OR Description LIKE '%${args.searchText}%')`; + where = where ? `${searchCondition} AND (${where})` : searchCondition; + } + + const soql = client.buildSOQL({ + select: ['Id', 'Subject', 'StartDateTime', 'EndDateTime', 'Location', 'IsAllDayEvent', 'Who.Name', 'What.Name', 'Owner.Name'], + from: 'Event', + where, + orderBy: 'StartDateTime ASC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + + { + name: 'sf_find_events_by_date_range', + description: 'Find events within a specific date range', + inputSchema: { + type: 'object', + properties: { + startDate: { + type: 'string', + description: 'Start date in YYYY-MM-DD format (required)', + }, + endDate: { + type: 'string', + description: 'End date in YYYY-MM-DD format (required)', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 100, + }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (args: { startDate: string; endDate: string; limit?: number }) => { + const soql = client.buildSOQL({ + select: ['Id', 'Subject', 'StartDateTime', 'EndDateTime', 'Location', 'Who.Name', 'What.Name', 'Owner.Name'], + from: 'Event', + where: `StartDateTime >= ${args.startDate}T00:00:00Z AND StartDateTime <= ${args.endDate}T23:59:59Z`, + orderBy: 'StartDateTime ASC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/leads.ts b/servers/salesforce/src/tools/leads.ts new file mode 100644 index 0000000..ff7604f --- /dev/null +++ b/servers/salesforce/src/tools/leads.ts @@ -0,0 +1,428 @@ +/** + * Lead management tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { Lead } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_list_leads', + description: 'List leads with optional filters. Returns up to 200 leads by default.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of leads to return (default: 200)', + default: 200, + }, + offset: { + type: 'number', + description: 'Number of records to skip (for pagination)', + }, + orderBy: { + type: 'string', + description: 'Field to sort by (e.g., "LastName ASC", "CreatedDate DESC")', + default: 'CreatedDate DESC', + }, + status: { + type: 'string', + description: 'Filter by lead status (e.g., "Open", "Contacted", "Qualified")', + }, + rating: { + type: 'string', + description: 'Filter by lead rating (e.g., "Hot", "Warm", "Cold")', + }, + isConverted: { + type: 'boolean', + description: 'Filter by conversion status', + }, + }, + }, + handler: async (args: { + limit?: number; + offset?: number; + orderBy?: string; + status?: string; + rating?: string; + isConverted?: boolean; + }) => { + const conditions: string[] = []; + if (args.status) conditions.push(`Status = '${args.status}'`); + if (args.rating) conditions.push(`Rating = '${args.rating}'`); + if (args.isConverted !== undefined) conditions.push(`IsConverted = ${args.isConverted}`); + + const soql = client.buildSOQL({ + select: ['Id', 'FirstName', 'LastName', 'Company', 'Status', 'Email', 'Phone', 'Title', 'Industry', 'Rating', 'LeadSource', 'IsConverted', 'Owner.Name', 'CreatedDate'], + from: 'Lead', + where: conditions.length > 0 ? conditions.join(' AND ') : undefined, + orderBy: args.orderBy || 'CreatedDate DESC', + limit: args.limit || 200, + offset: args.offset, + }); + + const result = await client.query(soql); + return { + totalSize: result.totalSize, + records: result.records, + hasMore: !result.done, + }; + }, + }, + + { + name: 'sf_get_lead', + description: 'Get detailed information about a specific lead by ID', + inputSchema: { + type: 'object', + properties: { + leadId: { + type: 'string', + description: 'The Salesforce ID of the lead (15 or 18 characters)', + }, + }, + required: ['leadId'], + }, + handler: async (args: { leadId: string }) => { + const lead = await client.getRecord('Lead', args.leadId); + return lead; + }, + }, + + { + name: 'sf_create_lead', + description: 'Create a new lead', + inputSchema: { + type: 'object', + properties: { + lastName: { + type: 'string', + description: 'Last name (required)', + }, + firstName: { + type: 'string', + description: 'First name', + }, + company: { + type: 'string', + description: 'Company name (required)', + }, + status: { + type: 'string', + description: 'Lead status (required, e.g., "Open", "Working", "Qualified")', + }, + email: { + type: 'string', + description: 'Email address', + }, + phone: { + type: 'string', + description: 'Phone number', + }, + mobilePhone: { + type: 'string', + description: 'Mobile phone number', + }, + title: { + type: 'string', + description: 'Job title', + }, + industry: { + type: 'string', + description: 'Industry', + }, + rating: { + type: 'string', + description: 'Lead rating (e.g., "Hot", "Warm", "Cold")', + }, + leadSource: { + type: 'string', + description: 'Lead source (e.g., "Web", "Phone", "Partner")', + }, + street: { + type: 'string', + description: 'Street address', + }, + city: { + type: 'string', + description: 'City', + }, + state: { + type: 'string', + description: 'State/province', + }, + postalCode: { + type: 'string', + description: 'Postal code', + }, + country: { + type: 'string', + description: 'Country', + }, + description: { + type: 'string', + description: 'Lead description', + }, + }, + required: ['lastName', 'company', 'status'], + }, + handler: async (args: { + lastName: string; + company: string; + status: string; + firstName?: string; + email?: string; + phone?: string; + mobilePhone?: string; + title?: string; + industry?: string; + rating?: string; + leadSource?: string; + street?: string; + city?: string; + state?: string; + postalCode?: string; + country?: string; + description?: string; + }) => { + const leadData: Partial = { + LastName: args.lastName, + FirstName: args.firstName, + Company: args.company, + Status: args.status, + Email: args.email, + Phone: args.phone, + MobilePhone: args.mobilePhone, + Title: args.title, + Industry: args.industry, + Rating: args.rating, + LeadSource: args.leadSource, + Street: args.street, + City: args.city, + State: args.state, + PostalCode: args.postalCode, + Country: args.country, + Description: args.description, + }; + + const result = await client.createRecord('Lead', leadData); + return result; + }, + }, + + { + name: 'sf_update_lead', + description: 'Update an existing lead', + inputSchema: { + type: 'object', + properties: { + leadId: { + type: 'string', + description: 'The Salesforce ID of the lead to update', + }, + lastName: { type: 'string' }, + firstName: { type: 'string' }, + company: { type: 'string' }, + status: { type: 'string' }, + email: { type: 'string' }, + phone: { type: 'string' }, + mobilePhone: { type: 'string' }, + title: { type: 'string' }, + industry: { type: 'string' }, + rating: { type: 'string' }, + leadSource: { type: 'string' }, + street: { type: 'string' }, + city: { type: 'string' }, + state: { type: 'string' }, + postalCode: { type: 'string' }, + country: { type: 'string' }, + description: { type: 'string' }, + }, + required: ['leadId'], + }, + handler: async (args: { + leadId: string; + lastName?: string; + firstName?: string; + company?: string; + status?: string; + email?: string; + phone?: string; + mobilePhone?: string; + title?: string; + industry?: string; + rating?: string; + leadSource?: string; + street?: string; + city?: string; + state?: string; + postalCode?: string; + country?: string; + description?: string; + }) => { + const { leadId, ...updates } = args; + const leadData: Partial = { + LastName: updates.lastName, + FirstName: updates.firstName, + Company: updates.company, + Status: updates.status, + Email: updates.email, + Phone: updates.phone, + MobilePhone: updates.mobilePhone, + Title: updates.title, + Industry: updates.industry, + Rating: updates.rating, + LeadSource: updates.leadSource, + Street: updates.street, + City: updates.city, + State: updates.state, + PostalCode: updates.postalCode, + Country: updates.country, + Description: updates.description, + }; + + // Remove undefined values + Object.keys(leadData).forEach((key) => { + if (leadData[key as keyof Lead] === undefined) { + delete leadData[key as keyof Lead]; + } + }); + + await client.updateRecord('Lead', leadId, leadData); + return { success: true, id: leadId }; + }, + }, + + { + name: 'sf_delete_lead', + description: 'Delete a lead by ID', + inputSchema: { + type: 'object', + properties: { + leadId: { + type: 'string', + description: 'The Salesforce ID of the lead to delete', + }, + }, + required: ['leadId'], + }, + handler: async (args: { leadId: string }) => { + await client.deleteRecord('Lead', args.leadId); + return { success: true, id: args.leadId }; + }, + }, + + { + name: 'sf_convert_lead', + description: 'Convert a lead to an Account, Contact, and optionally an Opportunity', + inputSchema: { + type: 'object', + properties: { + leadId: { + type: 'string', + description: 'The Salesforce ID of the lead to convert', + }, + convertedStatus: { + type: 'string', + description: 'The converted status (must be a valid converted status from your org)', + }, + accountId: { + type: 'string', + description: 'Existing account ID to attach the contact to (optional)', + }, + createOpportunity: { + type: 'boolean', + description: 'Whether to create an opportunity (default: true)', + default: true, + }, + opportunityName: { + type: 'string', + description: 'Name for the new opportunity (if creating)', + }, + ownerId: { + type: 'string', + description: 'Owner ID for the new records (defaults to lead owner)', + }, + }, + required: ['leadId', 'convertedStatus'], + }, + handler: async (args: { + leadId: string; + convertedStatus: string; + accountId?: string; + createOpportunity?: boolean; + opportunityName?: string; + ownerId?: string; + }) => { + // Lead conversion uses a special endpoint + const convertData = { + leadId: args.leadId, + convertedStatus: args.convertedStatus, + accountId: args.accountId, + doNotCreateOpportunity: !(args.createOpportunity ?? true), + opportunityName: args.opportunityName, + ownerId: args.ownerId, + }; + + // Note: The actual API call would be to /services/data/v59.0/sobjects/Lead/{id}/convert + // For now, we'll use a composite request + const result = await client.composite({ + compositeRequest: [ + { + method: 'POST', + url: `/services/data/v59.0/sobjects/Lead/${args.leadId}/convert`, + referenceId: 'convertLead', + body: convertData, + }, + ], + }); + + return result.compositeResponse[0].body; + }, + }, + + { + name: 'sf_search_leads', + description: 'Search leads using SOQL with flexible criteria', + inputSchema: { + type: 'object', + properties: { + searchText: { + type: 'string', + description: 'Text to search in FirstName, LastName, Company, or Email fields', + }, + whereClause: { + type: 'string', + description: 'Custom WHERE clause (e.g., "Rating = \'Hot\' AND IsConverted = false")', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 100, + }, + }, + }, + handler: async (args: { searchText?: string; whereClause?: string; limit?: number }) => { + let where = args.whereClause; + + if (args.searchText) { + const searchCondition = `(FirstName LIKE '%${args.searchText}%' OR LastName LIKE '%${args.searchText}%' OR Company LIKE '%${args.searchText}%' OR Email LIKE '%${args.searchText}%')`; + where = where ? `${searchCondition} AND (${where})` : searchCondition; + } + + const soql = client.buildSOQL({ + select: ['Id', 'FirstName', 'LastName', 'Company', 'Status', 'Email', 'Phone', 'Title', 'Rating', 'LeadSource', 'IsConverted'], + from: 'Lead', + where, + orderBy: 'CreatedDate DESC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/opportunities.ts b/servers/salesforce/src/tools/opportunities.ts new file mode 100644 index 0000000..1043a41 --- /dev/null +++ b/servers/salesforce/src/tools/opportunities.ts @@ -0,0 +1,364 @@ +/** + * Opportunity management tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { Opportunity } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_list_opportunities', + description: 'List opportunities with optional filters. Returns up to 200 opportunities by default.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of opportunities to return (default: 200)', + default: 200, + }, + offset: { + type: 'number', + description: 'Number of records to skip (for pagination)', + }, + orderBy: { + type: 'string', + description: 'Field to sort by (e.g., "CloseDate ASC", "Amount DESC")', + default: 'CloseDate ASC', + }, + stageName: { + type: 'string', + description: 'Filter by stage name (e.g., "Prospecting", "Closed Won")', + }, + accountId: { + type: 'string', + description: 'Filter by account ID', + }, + isClosed: { + type: 'boolean', + description: 'Filter by closed status', + }, + isWon: { + type: 'boolean', + description: 'Filter by won status', + }, + }, + }, + handler: async (args: { + limit?: number; + offset?: number; + orderBy?: string; + stageName?: string; + accountId?: string; + isClosed?: boolean; + isWon?: boolean; + }) => { + const conditions: string[] = []; + if (args.stageName) conditions.push(`StageName = '${args.stageName}'`); + if (args.accountId) conditions.push(`AccountId = '${args.accountId}'`); + if (args.isClosed !== undefined) conditions.push(`IsClosed = ${args.isClosed}`); + if (args.isWon !== undefined) conditions.push(`IsWon = ${args.isWon}`); + + const soql = client.buildSOQL({ + select: ['Id', 'Name', 'Account.Name', 'StageName', 'CloseDate', 'Amount', 'Probability', 'Type', 'LeadSource', 'IsClosed', 'IsWon', 'Owner.Name', 'CreatedDate'], + from: 'Opportunity', + where: conditions.length > 0 ? conditions.join(' AND ') : undefined, + orderBy: args.orderBy || 'CloseDate ASC', + limit: args.limit || 200, + offset: args.offset, + }); + + const result = await client.query(soql); + return { + totalSize: result.totalSize, + records: result.records, + hasMore: !result.done, + }; + }, + }, + + { + name: 'sf_get_opportunity', + description: 'Get detailed information about a specific opportunity by ID', + inputSchema: { + type: 'object', + properties: { + opportunityId: { + type: 'string', + description: 'The Salesforce ID of the opportunity (15 or 18 characters)', + }, + }, + required: ['opportunityId'], + }, + handler: async (args: { opportunityId: string }) => { + const opportunity = await client.getRecord('Opportunity', args.opportunityId); + return opportunity; + }, + }, + + { + name: 'sf_create_opportunity', + description: 'Create a new opportunity', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Opportunity name (required)', + }, + stageName: { + type: 'string', + description: 'Stage name (required, e.g., "Prospecting", "Qualification")', + }, + closeDate: { + type: 'string', + description: 'Close date in YYYY-MM-DD format (required)', + }, + accountId: { + type: 'string', + description: 'Associated account ID', + }, + amount: { + type: 'number', + description: 'Opportunity amount', + }, + probability: { + type: 'number', + description: 'Probability percentage (0-100)', + }, + type: { + type: 'string', + description: 'Opportunity type (e.g., "New Business", "Existing Business")', + }, + leadSource: { + type: 'string', + description: 'Lead source', + }, + description: { + type: 'string', + description: 'Opportunity description', + }, + }, + required: ['name', 'stageName', 'closeDate'], + }, + handler: async (args: { + name: string; + stageName: string; + closeDate: string; + accountId?: string; + amount?: number; + probability?: number; + type?: string; + leadSource?: string; + description?: string; + }) => { + const opportunityData: Partial = { + Name: args.name, + StageName: args.stageName, + CloseDate: args.closeDate, + AccountId: args.accountId as any, + Amount: args.amount, + Probability: args.probability, + Type: args.type, + LeadSource: args.leadSource, + Description: args.description, + }; + + const result = await client.createRecord('Opportunity', opportunityData); + return result; + }, + }, + + { + name: 'sf_update_opportunity', + description: 'Update an existing opportunity', + inputSchema: { + type: 'object', + properties: { + opportunityId: { + type: 'string', + description: 'The Salesforce ID of the opportunity to update', + }, + name: { type: 'string' }, + stageName: { type: 'string' }, + closeDate: { type: 'string', description: 'Close date in YYYY-MM-DD format' }, + accountId: { type: 'string' }, + amount: { type: 'number' }, + probability: { type: 'number' }, + type: { type: 'string' }, + leadSource: { type: 'string' }, + description: { type: 'string' }, + }, + required: ['opportunityId'], + }, + handler: async (args: { + opportunityId: string; + name?: string; + stageName?: string; + closeDate?: string; + accountId?: string; + amount?: number; + probability?: number; + type?: string; + leadSource?: string; + description?: string; + }) => { + const { opportunityId, ...updates } = args; + const opportunityData: Partial = { + Name: updates.name, + StageName: updates.stageName, + CloseDate: updates.closeDate, + AccountId: updates.accountId as any, + Amount: updates.amount, + Probability: updates.probability, + Type: updates.type, + LeadSource: updates.leadSource, + Description: updates.description, + }; + + // Remove undefined values + Object.keys(opportunityData).forEach((key) => { + if (opportunityData[key as keyof Opportunity] === undefined) { + delete opportunityData[key as keyof Opportunity]; + } + }); + + await client.updateRecord('Opportunity', opportunityId, opportunityData); + return { success: true, id: opportunityId }; + }, + }, + + { + name: 'sf_delete_opportunity', + description: 'Delete an opportunity by ID', + inputSchema: { + type: 'object', + properties: { + opportunityId: { + type: 'string', + description: 'The Salesforce ID of the opportunity to delete', + }, + }, + required: ['opportunityId'], + }, + handler: async (args: { opportunityId: string }) => { + await client.deleteRecord('Opportunity', args.opportunityId); + return { success: true, id: args.opportunityId }; + }, + }, + + { + name: 'sf_search_opportunities', + description: 'Search opportunities using SOQL with flexible criteria', + inputSchema: { + type: 'object', + properties: { + searchText: { + type: 'string', + description: 'Text to search in Name field', + }, + whereClause: { + type: 'string', + description: 'Custom WHERE clause (e.g., "Amount > 100000")', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 100, + }, + }, + }, + handler: async (args: { searchText?: string; whereClause?: string; limit?: number }) => { + let where = args.whereClause; + + if (args.searchText) { + const searchCondition = `Name LIKE '%${args.searchText}%'`; + where = where ? `${searchCondition} AND (${where})` : searchCondition; + } + + const soql = client.buildSOQL({ + select: ['Id', 'Name', 'Account.Name', 'StageName', 'CloseDate', 'Amount', 'Probability', 'IsClosed', 'IsWon'], + from: 'Opportunity', + where, + orderBy: 'CloseDate ASC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + + { + name: 'sf_find_opportunities_by_stage', + description: 'Find opportunities by stage name', + inputSchema: { + type: 'object', + properties: { + stageName: { + type: 'string', + description: 'Stage name to filter by', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 100, + }, + }, + required: ['stageName'], + }, + handler: async (args: { stageName: string; limit?: number }) => { + const soql = client.buildSOQL({ + select: ['Id', 'Name', 'Account.Name', 'StageName', 'CloseDate', 'Amount', 'Probability', 'Owner.Name'], + from: 'Opportunity', + where: `StageName = '${args.stageName}'`, + orderBy: 'CloseDate ASC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + + { + name: 'sf_find_opportunities_by_amount', + description: 'Find opportunities by amount range', + inputSchema: { + type: 'object', + properties: { + minAmount: { + type: 'number', + description: 'Minimum opportunity amount', + }, + maxAmount: { + type: 'number', + description: 'Maximum opportunity amount', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 100, + }, + }, + }, + handler: async (args: { minAmount?: number; maxAmount?: number; limit?: number }) => { + const conditions: string[] = []; + if (args.minAmount !== undefined) conditions.push(`Amount >= ${args.minAmount}`); + if (args.maxAmount !== undefined) conditions.push(`Amount <= ${args.maxAmount}`); + + const soql = client.buildSOQL({ + select: ['Id', 'Name', 'Account.Name', 'StageName', 'CloseDate', 'Amount', 'Probability', 'Owner.Name'], + from: 'Opportunity', + where: conditions.length > 0 ? conditions.join(' AND ') : undefined, + orderBy: 'Amount DESC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/reports.ts b/servers/salesforce/src/tools/reports.ts new file mode 100644 index 0000000..a3063b1 --- /dev/null +++ b/servers/salesforce/src/tools/reports.ts @@ -0,0 +1,163 @@ +/** + * Report management tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { Report, ReportMetadata } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_list_reports', + description: 'List available reports. Returns up to 200 reports by default.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of reports to return (default: 200)', + default: 200, + }, + offset: { + type: 'number', + description: 'Number of records to skip (for pagination)', + }, + }, + }, + handler: async (args: { limit?: number; offset?: number }) => { + const soql = client.buildSOQL({ + select: ['Id', 'Name', 'DeveloperName', 'FolderName', 'Description', 'Format', 'LastRunDate', 'Owner.Name', 'CreatedDate'], + from: 'Report', + orderBy: 'Name ASC', + limit: args.limit || 200, + offset: args.offset, + }); + + const result = await client.query(soql); + return { + totalSize: result.totalSize, + records: result.records, + hasMore: !result.done, + }; + }, + }, + + { + name: 'sf_get_report', + description: 'Get detailed information about a specific report by ID', + inputSchema: { + type: 'object', + properties: { + reportId: { + type: 'string', + description: 'The Salesforce ID of the report (15 or 18 characters)', + }, + }, + required: ['reportId'], + }, + handler: async (args: { reportId: string }) => { + const report = await client.getRecord('Report', args.reportId); + return report; + }, + }, + + { + name: 'sf_run_report', + description: 'Run a report and get its results', + inputSchema: { + type: 'object', + properties: { + reportId: { + type: 'string', + description: 'The Salesforce ID of the report to run', + }, + includeDetails: { + type: 'boolean', + description: 'Include detailed row data (default: true)', + default: true, + }, + }, + required: ['reportId'], + }, + handler: async (args: { reportId: string; includeDetails?: boolean }) => { + // Use the Analytics API to run the report + const response = await client['client'].post( + `/analytics/reports/${args.reportId}`, + { + reportMetadata: { + reportFormat: 'TABULAR', + detailColumns: args.includeDetails !== false, + }, + } + ); + + return response.data; + }, + }, + + { + name: 'sf_describe_report', + description: 'Get metadata about a report including available fields and filters', + inputSchema: { + type: 'object', + properties: { + reportId: { + type: 'string', + description: 'The Salesforce ID of the report', + }, + }, + required: ['reportId'], + }, + handler: async (args: { reportId: string }) => { + // Use the Analytics API to describe the report + const response = await client['client'].get(`/analytics/reports/${args.reportId}/describe`); + return response.data; + }, + }, + + { + name: 'sf_search_reports', + description: 'Search for reports by name or developer name', + inputSchema: { + type: 'object', + properties: { + searchText: { + type: 'string', + description: 'Text to search in report name or developer name', + }, + folderName: { + type: 'string', + description: 'Filter by folder name', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 50, + }, + }, + }, + handler: async (args: { searchText?: string; folderName?: string; limit?: number }) => { + const conditions: string[] = []; + + if (args.searchText) { + conditions.push(`(Name LIKE '%${args.searchText}%' OR DeveloperName LIKE '%${args.searchText}%')`); + } + + if (args.folderName) { + conditions.push(`FolderName = '${args.folderName}'`); + } + + const soql = client.buildSOQL({ + select: ['Id', 'Name', 'DeveloperName', 'FolderName', 'Description', 'Format', 'LastRunDate'], + from: 'Report', + where: conditions.length > 0 ? conditions.join(' AND ') : undefined, + orderBy: 'Name ASC', + limit: args.limit || 50, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/soql.ts b/servers/salesforce/src/tools/soql.ts new file mode 100644 index 0000000..85ffe84 --- /dev/null +++ b/servers/salesforce/src/tools/soql.ts @@ -0,0 +1,255 @@ +/** + * SOQL and SOSL query tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { SObject } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_run_soql_query', + description: 'Execute a raw SOQL query. Returns up to 2000 records (use pagination for more).', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The SOQL query to execute (e.g., "SELECT Id, Name FROM Account WHERE Industry = \'Technology\'")', + }, + }, + required: ['query'], + }, + handler: async (args: { query: string }) => { + const result = await client.query(args.query); + return { + totalSize: result.totalSize, + done: result.done, + records: result.records, + nextRecordsUrl: result.nextRecordsUrl, + }; + }, + }, + + { + name: 'sf_run_soql_query_all', + description: 'Execute a SOQL query and retrieve ALL records (handles pagination automatically). Warning: Can return large result sets.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The SOQL query to execute', + }, + }, + required: ['query'], + }, + handler: async (args: { query: string }) => { + const allRecords = await client.queryAll(args.query); + return { + totalSize: allRecords.length, + records: allRecords, + }; + }, + }, + + { + name: 'sf_run_sosl_search', + description: 'Execute a SOSL (Salesforce Object Search Language) search across multiple objects', + inputSchema: { + type: 'object', + properties: { + searchQuery: { + type: 'string', + description: 'The SOSL search query (e.g., "FIND {John} IN NAME FIELDS RETURNING Account(Name), Contact(FirstName, LastName)")', + }, + }, + required: ['searchQuery'], + }, + handler: async (args: { searchQuery: string }) => { + const results = await client.search(args.searchQuery); + return { + totalSize: results.length, + records: results, + }; + }, + }, + + { + name: 'sf_build_soql_query', + description: 'Build a SOQL query using a structured format (helper tool)', + inputSchema: { + type: 'object', + properties: { + select: { + type: 'array', + description: 'Fields to select (e.g., ["Id", "Name", "Email"])', + items: { type: 'string' }, + }, + from: { + type: 'string', + description: 'Object to query (e.g., "Account", "Contact")', + }, + where: { + type: 'string', + description: 'WHERE clause (e.g., "Industry = \'Technology\' AND AnnualRevenue > 1000000")', + }, + orderBy: { + type: 'string', + description: 'ORDER BY clause (e.g., "Name ASC", "CreatedDate DESC")', + }, + limit: { + type: 'number', + description: 'LIMIT clause (maximum number of records)', + }, + offset: { + type: 'number', + description: 'OFFSET clause (number of records to skip)', + }, + }, + required: ['select', 'from'], + }, + handler: async (args: { + select: string[]; + from: string; + where?: string; + orderBy?: string; + limit?: number; + offset?: number; + }) => { + const soql = client.buildSOQL(args); + return { + query: soql, + hint: 'Use sf_run_soql_query to execute this query', + }; + }, + }, + + { + name: 'sf_explain_soql_query', + description: 'Get the query plan for a SOQL query (analyze performance)', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The SOQL query to explain', + }, + }, + required: ['query'], + }, + handler: async (args: { query: string }) => { + // Use the Query Plan endpoint + const response = await client['client'].get('/query', { + params: { + q: args.query, + explain: true, + }, + }); + return response.data; + }, + }, + + { + name: 'sf_count_records', + description: 'Count records matching a condition using COUNT() aggregate', + inputSchema: { + type: 'object', + properties: { + objectName: { + type: 'string', + description: 'Object to count (e.g., "Account", "Contact")', + }, + whereClause: { + type: 'string', + description: 'Optional WHERE clause for filtering', + }, + }, + required: ['objectName'], + }, + handler: async (args: { objectName: string; whereClause?: string }) => { + const soql = `SELECT COUNT() FROM ${args.objectName}${args.whereClause ? ` WHERE ${args.whereClause}` : ''}`; + const result = await client.query(soql); + return { + count: result.totalSize, + object: args.objectName, + }; + }, + }, + + { + name: 'sf_aggregate_query', + description: 'Run aggregate functions (COUNT, SUM, AVG, MAX, MIN) on fields', + inputSchema: { + type: 'object', + properties: { + objectName: { + type: 'string', + description: 'Object to query (e.g., "Opportunity")', + }, + aggregates: { + type: 'array', + description: 'Aggregate functions (e.g., ["COUNT(Id)", "SUM(Amount)", "AVG(Amount)"])', + items: { type: 'string' }, + }, + groupBy: { + type: 'array', + description: 'Fields to group by (e.g., ["StageName"])', + items: { type: 'string' }, + }, + whereClause: { + type: 'string', + description: 'Optional WHERE clause for filtering', + }, + orderBy: { + type: 'string', + description: 'ORDER BY clause (e.g., "SUM(Amount) DESC")', + }, + limit: { + type: 'number', + description: 'LIMIT clause', + }, + }, + required: ['objectName', 'aggregates'], + }, + handler: async (args: { + objectName: string; + aggregates: string[]; + groupBy?: string[]; + whereClause?: string; + orderBy?: string; + limit?: number; + }) => { + let soql = `SELECT ${args.aggregates.join(', ')}`; + + if (args.groupBy && args.groupBy.length > 0) { + soql += `, ${args.groupBy.join(', ')}`; + } + + soql += ` FROM ${args.objectName}`; + + if (args.whereClause) { + soql += ` WHERE ${args.whereClause}`; + } + + if (args.groupBy && args.groupBy.length > 0) { + soql += ` GROUP BY ${args.groupBy.join(', ')}`; + } + + if (args.orderBy) { + soql += ` ORDER BY ${args.orderBy}`; + } + + if (args.limit) { + soql += ` LIMIT ${args.limit}`; + } + + const result = await client.query(soql); + return { + query: soql, + records: result.records, + }; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/tasks.ts b/servers/salesforce/src/tools/tasks.ts new file mode 100644 index 0000000..7cc7f2d --- /dev/null +++ b/servers/salesforce/src/tools/tasks.ts @@ -0,0 +1,297 @@ +/** + * Task management tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { Task } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_list_tasks', + description: 'List tasks with optional filters. Returns up to 200 tasks by default.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of tasks to return (default: 200)', + default: 200, + }, + offset: { + type: 'number', + description: 'Number of records to skip (for pagination)', + }, + orderBy: { + type: 'string', + description: 'Field to sort by (e.g., "ActivityDate ASC", "CreatedDate DESC")', + default: 'ActivityDate ASC', + }, + status: { + type: 'string', + description: 'Filter by status (e.g., "Not Started", "In Progress", "Completed")', + }, + priority: { + type: 'string', + description: 'Filter by priority (e.g., "High", "Normal", "Low")', + }, + isClosed: { + type: 'boolean', + description: 'Filter by closed status', + }, + }, + }, + handler: async (args: { + limit?: number; + offset?: number; + orderBy?: string; + status?: string; + priority?: string; + isClosed?: boolean; + }) => { + const conditions: string[] = []; + if (args.status) conditions.push(`Status = '${args.status}'`); + if (args.priority) conditions.push(`Priority = '${args.priority}'`); + if (args.isClosed !== undefined) conditions.push(`IsClosed = ${args.isClosed}`); + + const soql = client.buildSOQL({ + select: ['Id', 'Subject', 'Status', 'Priority', 'ActivityDate', 'Description', 'IsClosed', 'IsHighPriority', 'Who.Name', 'What.Name', 'Owner.Name', 'CreatedDate'], + from: 'Task', + where: conditions.length > 0 ? conditions.join(' AND ') : undefined, + orderBy: args.orderBy || 'ActivityDate ASC', + limit: args.limit || 200, + offset: args.offset, + }); + + const result = await client.query(soql); + return { + totalSize: result.totalSize, + records: result.records, + hasMore: !result.done, + }; + }, + }, + + { + name: 'sf_get_task', + description: 'Get detailed information about a specific task by ID', + inputSchema: { + type: 'object', + properties: { + taskId: { + type: 'string', + description: 'The Salesforce ID of the task (15 or 18 characters)', + }, + }, + required: ['taskId'], + }, + handler: async (args: { taskId: string }) => { + const task = await client.getRecord('Task', args.taskId); + return task; + }, + }, + + { + name: 'sf_create_task', + description: 'Create a new task', + inputSchema: { + type: 'object', + properties: { + subject: { + type: 'string', + description: 'Task subject/title (required)', + }, + status: { + type: 'string', + description: 'Task status (required, e.g., "Not Started", "In Progress")', + }, + priority: { + type: 'string', + description: 'Priority (e.g., "High", "Normal", "Low")', + }, + activityDate: { + type: 'string', + description: 'Due date in YYYY-MM-DD format', + }, + description: { + type: 'string', + description: 'Task description', + }, + whoId: { + type: 'string', + description: 'Related Lead or Contact ID', + }, + whatId: { + type: 'string', + description: 'Related Account, Opportunity, or other object ID', + }, + }, + required: ['subject', 'status'], + }, + handler: async (args: { + subject: string; + status: string; + priority?: string; + activityDate?: string; + description?: string; + whoId?: string; + whatId?: string; + }) => { + const taskData: Partial = { + Subject: args.subject, + Status: args.status, + Priority: args.priority, + ActivityDate: args.activityDate, + Description: args.description, + WhoId: args.whoId as any, + WhatId: args.whatId as any, + }; + + const result = await client.createRecord('Task', taskData); + return result; + }, + }, + + { + name: 'sf_update_task', + description: 'Update an existing task', + inputSchema: { + type: 'object', + properties: { + taskId: { + type: 'string', + description: 'The Salesforce ID of the task to update', + }, + subject: { type: 'string' }, + status: { type: 'string' }, + priority: { type: 'string' }, + activityDate: { type: 'string', description: 'Due date in YYYY-MM-DD format' }, + description: { type: 'string' }, + whoId: { type: 'string' }, + whatId: { type: 'string' }, + }, + required: ['taskId'], + }, + handler: async (args: { + taskId: string; + subject?: string; + status?: string; + priority?: string; + activityDate?: string; + description?: string; + whoId?: string; + whatId?: string; + }) => { + const { taskId, ...updates } = args; + const taskData: Partial = { + Subject: updates.subject, + Status: updates.status, + Priority: updates.priority, + ActivityDate: updates.activityDate, + Description: updates.description, + WhoId: updates.whoId as any, + WhatId: updates.whatId as any, + }; + + // Remove undefined values + Object.keys(taskData).forEach((key) => { + if (taskData[key as keyof Task] === undefined) { + delete taskData[key as keyof Task]; + } + }); + + await client.updateRecord('Task', taskId, taskData); + return { success: true, id: taskId }; + }, + }, + + { + name: 'sf_delete_task', + description: 'Delete a task by ID', + inputSchema: { + type: 'object', + properties: { + taskId: { + type: 'string', + description: 'The Salesforce ID of the task to delete', + }, + }, + required: ['taskId'], + }, + handler: async (args: { taskId: string }) => { + await client.deleteRecord('Task', args.taskId); + return { success: true, id: args.taskId }; + }, + }, + + { + name: 'sf_search_tasks', + description: 'Search tasks using SOQL with flexible criteria', + inputSchema: { + type: 'object', + properties: { + searchText: { + type: 'string', + description: 'Text to search in Subject and Description fields', + }, + whereClause: { + type: 'string', + description: 'Custom WHERE clause (e.g., "Priority = \'High\' AND IsClosed = false")', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 100, + }, + }, + }, + handler: async (args: { searchText?: string; whereClause?: string; limit?: number }) => { + let where = args.whereClause; + + if (args.searchText) { + const searchCondition = `(Subject LIKE '%${args.searchText}%' OR Description LIKE '%${args.searchText}%')`; + where = where ? `${searchCondition} AND (${where})` : searchCondition; + } + + const soql = client.buildSOQL({ + select: ['Id', 'Subject', 'Status', 'Priority', 'ActivityDate', 'IsClosed', 'IsHighPriority', 'Who.Name', 'What.Name', 'Owner.Name'], + from: 'Task', + where, + orderBy: 'ActivityDate ASC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + + { + name: 'sf_get_overdue_tasks', + description: 'Get all overdue tasks (tasks with ActivityDate in the past and not closed)', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of results', + default: 100, + }, + }, + }, + handler: async (args: { limit?: number }) => { + const today = new Date().toISOString().split('T')[0]; + const soql = client.buildSOQL({ + select: ['Id', 'Subject', 'Status', 'Priority', 'ActivityDate', 'Who.Name', 'What.Name', 'Owner.Name', 'CreatedDate'], + from: 'Task', + where: `ActivityDate < ${today} AND IsClosed = false`, + orderBy: 'ActivityDate ASC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + ]; +} diff --git a/servers/salesforce/src/tools/users.ts b/servers/salesforce/src/tools/users.ts new file mode 100644 index 0000000..3723926 --- /dev/null +++ b/servers/salesforce/src/tools/users.ts @@ -0,0 +1,234 @@ +/** + * User, Role, and Profile management tools + */ + +import type { SalesforceClient } from '../clients/salesforce.js'; +import type { User, UserRole, Profile } from '../types/index.js'; + +export function getTools(client: SalesforceClient) { + return [ + { + name: 'sf_list_users', + description: 'List users with optional filters. Returns up to 200 users by default.', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of users to return (default: 200)', + default: 200, + }, + offset: { + type: 'number', + description: 'Number of records to skip (for pagination)', + }, + isActive: { + type: 'boolean', + description: 'Filter by active status', + }, + }, + }, + handler: async (args: { limit?: number; offset?: number; isActive?: boolean }) => { + const conditions: string[] = []; + if (args.isActive !== undefined) conditions.push(`IsActive = ${args.isActive}`); + + const soql = client.buildSOQL({ + select: ['Id', 'Username', 'Email', 'FirstName', 'LastName', 'Name', 'IsActive', 'Title', 'Department', 'UserRole.Name', 'Profile.Name', 'CreatedDate'], + from: 'User', + where: conditions.length > 0 ? conditions.join(' AND ') : undefined, + orderBy: 'Name ASC', + limit: args.limit || 200, + offset: args.offset, + }); + + const result = await client.query(soql); + return { + totalSize: result.totalSize, + records: result.records, + hasMore: !result.done, + }; + }, + }, + + { + name: 'sf_get_user', + description: 'Get detailed information about a specific user by ID', + inputSchema: { + type: 'object', + properties: { + userId: { + type: 'string', + description: 'The Salesforce ID of the user (15 or 18 characters)', + }, + }, + required: ['userId'], + }, + handler: async (args: { userId: string }) => { + const user = await client.getRecord('User', args.userId); + return user; + }, + }, + + { + name: 'sf_search_users', + description: 'Search users by name, username, or email', + inputSchema: { + type: 'object', + properties: { + searchText: { + type: 'string', + description: 'Text to search in Name, Username, or Email fields', + }, + isActive: { + type: 'boolean', + description: 'Filter by active status', + }, + limit: { + type: 'number', + description: 'Maximum number of results', + default: 100, + }, + }, + }, + handler: async (args: { searchText?: string; isActive?: boolean; limit?: number }) => { + const conditions: string[] = []; + + if (args.searchText) { + conditions.push(`(Name LIKE '%${args.searchText}%' OR Username LIKE '%${args.searchText}%' OR Email LIKE '%${args.searchText}%')`); + } + + if (args.isActive !== undefined) { + conditions.push(`IsActive = ${args.isActive}`); + } + + const soql = client.buildSOQL({ + select: ['Id', 'Username', 'Email', 'FirstName', 'LastName', 'Name', 'IsActive', 'Title', 'Department', 'UserRole.Name', 'Profile.Name'], + from: 'User', + where: conditions.length > 0 ? conditions.join(' AND ') : undefined, + orderBy: 'Name ASC', + limit: args.limit || 100, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + + { + name: 'sf_list_roles', + description: 'List user roles in the organization', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of roles to return', + default: 200, + }, + }, + }, + handler: async (args: { limit?: number }) => { + const soql = client.buildSOQL({ + select: ['Id', 'Name', 'DeveloperName', 'ParentRoleId'], + from: 'UserRole', + orderBy: 'Name ASC', + limit: args.limit || 200, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + + { + name: 'sf_get_role', + description: 'Get detailed information about a specific user role', + inputSchema: { + type: 'object', + properties: { + roleId: { + type: 'string', + description: 'The Salesforce ID of the user role', + }, + }, + required: ['roleId'], + }, + handler: async (args: { roleId: string }) => { + const role = await client.getRecord('UserRole', args.roleId); + return role; + }, + }, + + { + name: 'sf_list_profiles', + description: 'List user profiles in the organization', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of profiles to return', + default: 200, + }, + }, + }, + handler: async (args: { limit?: number }) => { + const soql = client.buildSOQL({ + select: ['Id', 'Name', 'Description', 'UserType'], + from: 'Profile', + orderBy: 'Name ASC', + limit: args.limit || 200, + }); + + const result = await client.query(soql); + return result.records; + }, + }, + + { + name: 'sf_get_profile', + description: 'Get detailed information about a specific profile', + inputSchema: { + type: 'object', + properties: { + profileId: { + type: 'string', + description: 'The Salesforce ID of the profile', + }, + }, + required: ['profileId'], + }, + handler: async (args: { profileId: string }) => { + const profile = await client.getRecord('Profile', args.profileId); + return profile; + }, + }, + + { + name: 'sf_get_user_permissions', + description: 'Get permission sets assigned to a user', + inputSchema: { + type: 'object', + properties: { + userId: { + type: 'string', + description: 'The Salesforce ID of the user', + }, + }, + required: ['userId'], + }, + handler: async (args: { userId: string }) => { + // Query PermissionSetAssignment + const soql = client.buildSOQL({ + select: ['Id', 'PermissionSetId', 'PermissionSet.Name', 'PermissionSet.Label', 'AssigneeId'], + from: 'PermissionSetAssignment', + where: `AssigneeId = '${args.userId}'`, + orderBy: 'PermissionSet.Name ASC', + }); + + const result = await client.query(soql); + return result.records; + }, + }, + ]; +} diff --git a/servers/shopify/TOOLS_SUMMARY.md b/servers/shopify/TOOLS_SUMMARY.md new file mode 100644 index 0000000..44181c7 --- /dev/null +++ b/servers/shopify/TOOLS_SUMMARY.md @@ -0,0 +1,112 @@ +# Shopify MCP Server - Tools Summary + +## Overview +Built **67 tools** across 13 categories for comprehensive Shopify API coverage. + +## Tool Categories & Count + +| Category | File | Tools | Description | +|----------|------|-------|-------------| +| Products | `src/tools/products.ts` | 6 | Product CRUD, variants, images, search | +| Orders | `src/tools/orders.ts` | 6 | Order management, cancellation, status updates | +| Customers | `src/tools/customers.ts` | 6 | Customer CRUD, addresses, search | +| Inventory | `src/tools/inventory.ts` | 6 | Inventory levels, locations, adjustments | +| Collections | `src/tools/collections.ts` | 6 | Custom collections, product associations | +| Discounts | `src/tools/discounts.ts` | 7 | Price rules, discount codes | +| Shipping | `src/tools/shipping.ts` | 6 | Shipping zones, carrier services | +| Fulfillments | `src/tools/fulfillments.ts` | 6 | Fulfillment CRUD, tracking updates | +| Themes | `src/tools/themes.ts` | 8 | Theme management, asset upload/download | +| Pages | `src/tools/pages.ts` | 5 | Static page CRUD, metafields | +| Blogs | `src/tools/blogs.ts` | 7 | Blog and article management | +| Analytics | `src/tools/analytics.ts` | 3 | Shop info, event logs | +| Webhooks | `src/tools/webhooks.ts` | 5 | Webhook subscription management | + +**Total: 67 tools** + +## Quality Standards Met + +✅ **TypeScript compilation:** Clean compile with `npx tsc --noEmit` +✅ **Zod schemas:** Every input validated with descriptive field annotations +✅ **Pagination:** All list operations support cursor-based pagination +✅ **Filtering:** Date ranges, status, and resource-specific filters +✅ **Consistent naming:** `shopify_verb_noun` pattern throughout +✅ **Error handling:** Zod validation + client retry/rate limiting +✅ **Type safety:** ShopifyClient interface usage, proper async/await + +## Tool Examples + +### Products +- `shopify_list_products` - Paginated listing with status/type/vendor/date filters +- `shopify_get_product` - Retrieve single product by ID +- `shopify_create_product` - Create with variants and images +- `shopify_update_product` - Partial updates +- `shopify_delete_product` - Delete by ID +- `shopify_search_products` - Full-text search + +### Orders +- `shopify_list_orders` - Filter by status, financial status, fulfillment status, dates +- `shopify_get_order` - Retrieve order details +- `shopify_create_order` - Create manual orders +- `shopify_update_order` - Update notes, tags, email +- `shopify_cancel_order` - Cancel with optional refund/restock +- `shopify_close_order` - Mark as complete + +### Discounts +- `shopify_list_price_rules` - List all discount rules +- `shopify_create_price_rule` - Create percentage/fixed discounts +- `shopify_list_discount_codes` - List codes for a rule +- `shopify_create_discount_code` - Generate new discount code + +## Architecture + +Each tool file exports a default array of tool definitions: +```typescript +export default [ + { + name: 'shopify_tool_name', + description: 'Human-readable description', + inputSchema: { /* JSON Schema */ }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ZodSchema.parse(input); + const result = await client.method('/endpoint.json', { params: validated }); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + } +]; +``` + +## Server Integration + +The `ShopifyMCPServer` class in `src/server.ts` lazy-loads tool modules on demand: +- List tools → loads all modules, aggregates definitions +- Call tool → loads specific module, executes handler with client instance + +## Next Steps + +1. ✅ All tool files created +2. ✅ TypeScript compilation verified +3. ⏭️ Integration testing with MCP inspector +4. ⏭️ Documentation updates (README, examples) +5. ⏭️ Deployment to npm/GitHub + +## File Structure + +``` +src/tools/ +├── products.ts (6 tools) +├── orders.ts (6 tools) +├── customers.ts (6 tools) +├── inventory.ts (6 tools) +├── collections.ts (6 tools) +├── discounts.ts (7 tools) +├── shipping.ts (6 tools) +├── fulfillments.ts (6 tools) +├── themes.ts (8 tools) +├── pages.ts (5 tools) +├── blogs.ts (7 tools) +├── analytics.ts (3 tools) +└── webhooks.ts (5 tools) +``` + +Built by AI subagent · Date: 2025 +Target: 50-70 tools ✅ Achieved: 67 tools diff --git a/servers/shopify/src/tools/analytics.ts b/servers/shopify/src/tools/analytics.ts new file mode 100644 index 0000000..7faf0cc --- /dev/null +++ b/servers/shopify/src/tools/analytics.ts @@ -0,0 +1,79 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const GetShopInput = z.object({ + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const ListEventsInput = z.object({ + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), + created_at_min: z.string().optional().describe('Filter by min creation date (ISO 8601)'), + created_at_max: z.string().optional().describe('Filter by max creation date (ISO 8601)'), + verb: z.string().optional().describe('Filter by event verb (e.g., "create", "update", "delete")'), + filter: z.string().optional().describe('Filter events by resource (e.g., "Product", "Order")'), +}); + +const GetEventInput = z.object({ + id: z.string().describe('Event ID'), +}); + +export default [ + { + name: 'shopify_get_shop', + description: 'Get shop information (name, domain, currency, plan, etc.)', + inputSchema: { + type: 'object' as const, + properties: { + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetShopInput.parse(input); + const result = await client.get('/shop.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_list_events', + description: 'List events (audit log of changes to shop resources) with filtering by date, verb, and resource type', + inputSchema: { + type: 'object' as const, + properties: { + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + created_at_min: { type: 'string', description: 'Filter by min creation date (ISO 8601)' }, + created_at_max: { type: 'string', description: 'Filter by max creation date (ISO 8601)' }, + verb: { type: 'string', description: 'Filter by event verb (e.g., "create", "update", "delete")' }, + filter: { type: 'string', description: 'Filter events by resource (e.g., "Product", "Order")' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListEventsInput.parse(input); + const results = await client.list('/events.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_event', + description: 'Get a specific event by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Event ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetEventInput.parse(input); + const result = await client.get(`/events/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + ]; diff --git a/servers/shopify/src/tools/blogs.ts b/servers/shopify/src/tools/blogs.ts new file mode 100644 index 0000000..32e9cc3 --- /dev/null +++ b/servers/shopify/src/tools/blogs.ts @@ -0,0 +1,227 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const ListBlogsInput = z.object({ + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const GetBlogInput = z.object({ + id: z.string().describe('Blog ID'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const ListArticlesInput = z.object({ + blog_id: z.string().describe('Blog ID'), + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), + created_at_min: z.string().optional().describe('Filter by min creation date (ISO 8601)'), + created_at_max: z.string().optional().describe('Filter by max creation date (ISO 8601)'), + published_at_min: z.string().optional().describe('Filter by min published date (ISO 8601)'), + published_at_max: z.string().optional().describe('Filter by max published date (ISO 8601)'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const GetArticleInput = z.object({ + blog_id: z.string().describe('Blog ID'), + article_id: z.string().describe('Article ID'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const CreateArticleInput = z.object({ + blog_id: z.string().describe('Blog ID'), + title: z.string().describe('Article title'), + body_html: z.string().optional().describe('Article content HTML'), + author: z.string().optional().describe('Article author'), + tags: z.string().optional().describe('Comma-separated tags'), + published: z.boolean().optional().describe('Published status'), + published_at: z.string().optional().describe('Published date/time (ISO 8601)'), + image: z.object({ + src: z.string().describe('Image URL'), + alt: z.string().optional().describe('Alt text'), + }).optional().describe('Article featured image'), +}); + +const UpdateArticleInput = z.object({ + blog_id: z.string().describe('Blog ID'), + article_id: z.string().describe('Article ID'), + title: z.string().optional().describe('Article title'), + body_html: z.string().optional().describe('Article content HTML'), + author: z.string().optional().describe('Article author'), + tags: z.string().optional().describe('Comma-separated tags'), + published: z.boolean().optional().describe('Published status'), +}); + +const DeleteArticleInput = z.object({ + blog_id: z.string().describe('Blog ID'), + article_id: z.string().describe('Article ID'), +}); + +export default [ + { + name: 'shopify_list_blogs', + description: 'List all blogs', + inputSchema: { + type: 'object' as const, + properties: { + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListBlogsInput.parse(input); + const results = await client.list('/blogs.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_blog', + description: 'Get a specific blog by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Blog ID' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetBlogInput.parse(input); + const result = await client.get(`/blogs/${validated.id}.json`, { + params: validated.fields ? { fields: validated.fields } : {}, + }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_list_articles', + description: 'List articles in a blog with date filtering and pagination', + inputSchema: { + type: 'object' as const, + properties: { + blog_id: { type: 'string', description: 'Blog ID' }, + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + created_at_min: { type: 'string', description: 'Filter by min creation date (ISO 8601)' }, + created_at_max: { type: 'string', description: 'Filter by max creation date (ISO 8601)' }, + published_at_min: { type: 'string', description: 'Filter by min published date (ISO 8601)' }, + published_at_max: { type: 'string', description: 'Filter by max published date (ISO 8601)' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + required: ['blog_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListArticlesInput.parse(input); + const { blog_id, ...params } = validated; + const results = await client.list(`/blogs/${blog_id}/articles.json`, { params }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_article', + description: 'Get a specific article by ID', + inputSchema: { + type: 'object' as const, + properties: { + blog_id: { type: 'string', description: 'Blog ID' }, + article_id: { type: 'string', description: 'Article ID' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + required: ['blog_id', 'article_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetArticleInput.parse(input); + const result = await client.get(`/blogs/${validated.blog_id}/articles/${validated.article_id}.json`, { + params: validated.fields ? { fields: validated.fields } : {}, + }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_article', + description: 'Create a new article in a blog', + inputSchema: { + type: 'object' as const, + properties: { + blog_id: { type: 'string', description: 'Blog ID' }, + title: { type: 'string', description: 'Article title' }, + body_html: { type: 'string', description: 'Article content HTML' }, + author: { type: 'string', description: 'Article author' }, + tags: { type: 'string', description: 'Comma-separated tags' }, + published: { type: 'boolean', description: 'Published status' }, + published_at: { type: 'string', description: 'Published date/time (ISO 8601)' }, + image: { + type: 'object', + description: 'Article featured image', + properties: { + src: { type: 'string', description: 'Image URL' }, + alt: { type: 'string', description: 'Alt text' }, + }, + }, + }, + required: ['blog_id', 'title'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreateArticleInput.parse(input); + const { blog_id, ...articleData } = validated; + const result = await client.create(`/blogs/${blog_id}/articles.json`, { article: articleData }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_update_article', + description: 'Update an existing article', + inputSchema: { + type: 'object' as const, + properties: { + blog_id: { type: 'string', description: 'Blog ID' }, + article_id: { type: 'string', description: 'Article ID' }, + title: { type: 'string', description: 'Article title' }, + body_html: { type: 'string', description: 'Article content HTML' }, + author: { type: 'string', description: 'Article author' }, + tags: { type: 'string', description: 'Comma-separated tags' }, + published: { type: 'boolean', description: 'Published status' }, + }, + required: ['blog_id', 'article_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = UpdateArticleInput.parse(input); + const { blog_id, article_id, ...updates } = validated; + const result = await client.update(`/blogs/${blog_id}/articles/${article_id}.json`, { article: updates }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_delete_article', + description: 'Delete an article by ID', + inputSchema: { + type: 'object' as const, + properties: { + blog_id: { type: 'string', description: 'Blog ID' }, + article_id: { type: 'string', description: 'Article ID' }, + }, + required: ['blog_id', 'article_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = DeleteArticleInput.parse(input); + await client.delete(`/blogs/${validated.blog_id}/articles/${validated.article_id}.json`); + return { + content: [{ type: 'text' as const, text: `Article ${validated.article_id} deleted successfully` }], + }; + }, + }, + ]; diff --git a/servers/shopify/src/tools/collections.ts b/servers/shopify/src/tools/collections.ts new file mode 100644 index 0000000..3a5236f --- /dev/null +++ b/servers/shopify/src/tools/collections.ts @@ -0,0 +1,176 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const ListCustomCollectionsInput = z.object({ + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), + product_id: z.string().optional().describe('Filter by product ID'), + title: z.string().optional().describe('Filter by title'), +}); + +const GetCustomCollectionInput = z.object({ + id: z.string().describe('Custom collection ID'), +}); + +const CreateCustomCollectionInput = z.object({ + title: z.string().describe('Collection title'), + body_html: z.string().optional().describe('Collection description HTML'), + sort_order: z.enum(['alpha-asc', 'alpha-desc', 'best-selling', 'created', 'created-desc', 'manual', 'price-asc', 'price-desc']).optional().describe('Sort order'), + published: z.boolean().optional().describe('Published status'), + image: z.object({ + src: z.string().describe('Image URL'), + alt: z.string().optional().describe('Alt text'), + }).optional().describe('Collection image'), +}); + +const UpdateCustomCollectionInput = z.object({ + id: z.string().describe('Custom collection ID'), + title: z.string().optional().describe('Collection title'), + body_html: z.string().optional().describe('Collection description HTML'), + sort_order: z.enum(['alpha-asc', 'alpha-desc', 'best-selling', 'created', 'created-desc', 'manual', 'price-asc', 'price-desc']).optional().describe('Sort order'), + published: z.boolean().optional().describe('Published status'), +}); + +const DeleteCustomCollectionInput = z.object({ + id: z.string().describe('Custom collection ID'), +}); + +const AddProductToCollectionInput = z.object({ + collection_id: z.string().describe('Collection ID'), + product_id: z.string().describe('Product ID'), +}); + +export default [ + { + name: 'shopify_list_custom_collections', + description: 'List custom collections with pagination and filtering', + inputSchema: { + type: 'object' as const, + properties: { + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + product_id: { type: 'string', description: 'Filter by product ID' }, + title: { type: 'string', description: 'Filter by title' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListCustomCollectionsInput.parse(input); + const results = await client.list('/custom_collections.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_custom_collection', + description: 'Get a specific custom collection by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Custom collection ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetCustomCollectionInput.parse(input); + const result = await client.get(`/custom_collections/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_custom_collection', + description: 'Create a new custom collection', + inputSchema: { + type: 'object' as const, + properties: { + title: { type: 'string', description: 'Collection title' }, + body_html: { type: 'string', description: 'Collection description HTML' }, + sort_order: { type: 'string', enum: ['alpha-asc', 'alpha-desc', 'best-selling', 'created', 'created-desc', 'manual', 'price-asc', 'price-desc'], description: 'Sort order' }, + published: { type: 'boolean', description: 'Published status' }, + image: { + type: 'object', + description: 'Collection image', + properties: { + src: { type: 'string', description: 'Image URL' }, + alt: { type: 'string', description: 'Alt text' }, + }, + }, + }, + required: ['title'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreateCustomCollectionInput.parse(input); + const result = await client.create('/custom_collections.json', { custom_collection: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_update_custom_collection', + description: 'Update an existing custom collection', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Custom collection ID' }, + title: { type: 'string', description: 'Collection title' }, + body_html: { type: 'string', description: 'Collection description HTML' }, + sort_order: { type: 'string', enum: ['alpha-asc', 'alpha-desc', 'best-selling', 'created', 'created-desc', 'manual', 'price-asc', 'price-desc'], description: 'Sort order' }, + published: { type: 'boolean', description: 'Published status' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = UpdateCustomCollectionInput.parse(input); + const { id, ...updates } = validated; + const result = await client.update(`/custom_collections/${id}.json`, { custom_collection: updates }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_delete_custom_collection', + description: 'Delete a custom collection by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Custom collection ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = DeleteCustomCollectionInput.parse(input); + await client.delete(`/custom_collections/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: `Custom collection ${validated.id} deleted successfully` }], + }; + }, + }, + { + name: 'shopify_add_product_to_collection', + description: 'Add a product to a collection (creates a collect)', + inputSchema: { + type: 'object' as const, + properties: { + collection_id: { type: 'string', description: 'Collection ID' }, + product_id: { type: 'string', description: 'Product ID' }, + }, + required: ['collection_id', 'product_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = AddProductToCollectionInput.parse(input); + const result = await client.create('/collects.json', { + collect: { + collection_id: validated.collection_id, + product_id: validated.product_id, + }, + }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + ]; diff --git a/servers/shopify/src/tools/customers.ts b/servers/shopify/src/tools/customers.ts new file mode 100644 index 0000000..8a89225 --- /dev/null +++ b/servers/shopify/src/tools/customers.ts @@ -0,0 +1,211 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const ListCustomersInput = z.object({ + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), + created_at_min: z.string().optional().describe('Filter by min creation date (ISO 8601)'), + created_at_max: z.string().optional().describe('Filter by max creation date (ISO 8601)'), + updated_at_min: z.string().optional().describe('Filter by min update date (ISO 8601)'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const GetCustomerInput = z.object({ + id: z.string().describe('Customer ID'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const CreateCustomerInput = z.object({ + email: z.string().optional().describe('Customer email'), + first_name: z.string().optional().describe('First name'), + last_name: z.string().optional().describe('Last name'), + phone: z.string().optional().describe('Phone number'), + tags: z.string().optional().describe('Comma-separated tags'), + note: z.string().optional().describe('Customer note'), + verified_email: z.boolean().optional().describe('Email is verified'), + send_email_welcome: z.boolean().optional().describe('Send welcome email'), + addresses: z.array(z.object({ + address1: z.string().optional().describe('Address line 1'), + address2: z.string().optional().describe('Address line 2'), + city: z.string().optional().describe('City'), + province: z.string().optional().describe('State/Province'), + country: z.string().optional().describe('Country'), + zip: z.string().optional().describe('Postal code'), + phone: z.string().optional().describe('Phone number'), + first_name: z.string().optional().describe('First name'), + last_name: z.string().optional().describe('Last name'), + })).optional().describe('Customer addresses'), +}); + +const UpdateCustomerInput = z.object({ + id: z.string().describe('Customer ID'), + email: z.string().optional().describe('Customer email'), + first_name: z.string().optional().describe('First name'), + last_name: z.string().optional().describe('Last name'), + phone: z.string().optional().describe('Phone number'), + tags: z.string().optional().describe('Comma-separated tags'), + note: z.string().optional().describe('Customer note'), + verified_email: z.boolean().optional().describe('Email is verified'), +}); + +const DeleteCustomerInput = z.object({ + id: z.string().describe('Customer ID'), +}); + +const SearchCustomersInput = z.object({ + query: z.string().describe('Search query (email, name, phone, etc.)'), + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +export default [ + { + name: 'shopify_list_customers', + description: 'List customers with pagination and date filtering', + inputSchema: { + type: 'object' as const, + properties: { + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + created_at_min: { type: 'string', description: 'Filter by min creation date (ISO 8601)' }, + created_at_max: { type: 'string', description: 'Filter by max creation date (ISO 8601)' }, + updated_at_min: { type: 'string', description: 'Filter by min update date (ISO 8601)' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListCustomersInput.parse(input); + const results = await client.list('/customers.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_customer', + description: 'Get a specific customer by ID with optional field filtering', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Customer ID' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetCustomerInput.parse(input); + const result = await client.get(`/customers/${validated.id}.json`, { + params: validated.fields ? { fields: validated.fields } : {}, + }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_customer', + description: 'Create a new customer with optional addresses', + inputSchema: { + type: 'object' as const, + properties: { + email: { type: 'string', description: 'Customer email' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + phone: { type: 'string', description: 'Phone number' }, + tags: { type: 'string', description: 'Comma-separated tags' }, + note: { type: 'string', description: 'Customer note' }, + verified_email: { type: 'boolean', description: 'Email is verified' }, + send_email_welcome: { type: 'boolean', description: 'Send welcome email' }, + addresses: { + type: 'array', + description: 'Customer addresses', + items: { + type: 'object', + properties: { + address1: { type: 'string', description: 'Address line 1' }, + address2: { type: 'string', description: 'Address line 2' }, + city: { type: 'string', description: 'City' }, + province: { type: 'string', description: 'State/Province' }, + country: { type: 'string', description: 'Country' }, + zip: { type: 'string', description: 'Postal code' }, + phone: { type: 'string', description: 'Phone number' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + }, + }, + }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreateCustomerInput.parse(input); + const result = await client.create('/customers.json', { customer: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_update_customer', + description: 'Update an existing customer', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Customer ID' }, + email: { type: 'string', description: 'Customer email' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + phone: { type: 'string', description: 'Phone number' }, + tags: { type: 'string', description: 'Comma-separated tags' }, + note: { type: 'string', description: 'Customer note' }, + verified_email: { type: 'boolean', description: 'Email is verified' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = UpdateCustomerInput.parse(input); + const { id, ...updates } = validated; + const result = await client.update(`/customers/${id}.json`, { customer: updates }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_delete_customer', + description: 'Delete a customer by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Customer ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = DeleteCustomerInput.parse(input); + await client.delete(`/customers/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: `Customer ${validated.id} deleted successfully` }], + }; + }, + }, + { + name: 'shopify_search_customers', + description: 'Search customers by query (email, name, phone, address, etc.)', + inputSchema: { + type: 'object' as const, + properties: { + query: { type: 'string', description: 'Search query (email, name, phone, etc.)' }, + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + required: ['query'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = SearchCustomersInput.parse(input); + const results = await client.list('/customers/search.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + ]; diff --git a/servers/shopify/src/tools/discounts.ts b/servers/shopify/src/tools/discounts.ts new file mode 100644 index 0000000..57191ce --- /dev/null +++ b/servers/shopify/src/tools/discounts.ts @@ -0,0 +1,201 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const ListPriceRulesInput = z.object({ + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), +}); + +const GetPriceRuleInput = z.object({ + id: z.string().describe('Price rule ID'), +}); + +const CreatePriceRuleInput = z.object({ + title: z.string().describe('Price rule title'), + target_type: z.enum(['line_item', 'shipping_line']).describe('What the price rule applies to'), + target_selection: z.enum(['all', 'entitled']).describe('Which items the rule applies to'), + allocation_method: z.enum(['across', 'each']).describe('How the discount is distributed'), + value_type: z.enum(['fixed_amount', 'percentage']).describe('Type of discount value'), + value: z.string().describe('Discount value (e.g., "-10.0" for $10 off or "-15.0" for 15% off)'), + customer_selection: z.enum(['all', 'prerequisite']).describe('Which customers the rule applies to'), + starts_at: z.string().describe('Start date/time (ISO 8601)'), + ends_at: z.string().optional().describe('End date/time (ISO 8601)'), + usage_limit: z.number().optional().describe('Maximum number of times this discount can be used'), + once_per_customer: z.boolean().optional().describe('Limit to one use per customer'), +}); + +const UpdatePriceRuleInput = z.object({ + id: z.string().describe('Price rule ID'), + title: z.string().optional().describe('Price rule title'), + value: z.string().optional().describe('Discount value'), + starts_at: z.string().optional().describe('Start date/time (ISO 8601)'), + ends_at: z.string().optional().describe('End date/time (ISO 8601)'), + usage_limit: z.number().optional().describe('Maximum number of times this discount can be used'), +}); + +const DeletePriceRuleInput = z.object({ + id: z.string().describe('Price rule ID'), +}); + +const ListDiscountCodesInput = z.object({ + price_rule_id: z.string().describe('Price rule ID'), + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), +}); + +const CreateDiscountCodeInput = z.object({ + price_rule_id: z.string().describe('Price rule ID'), + code: z.string().describe('Discount code'), + usage_count: z.number().optional().describe('Number of times this code has been used'), +}); + +export default [ + { + name: 'shopify_list_price_rules', + description: 'List all price rules (discount rules)', + inputSchema: { + type: 'object' as const, + properties: { + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListPriceRulesInput.parse(input); + const results = await client.list('/price_rules.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_price_rule', + description: 'Get a specific price rule by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Price rule ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetPriceRuleInput.parse(input); + const result = await client.get(`/price_rules/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_price_rule', + description: 'Create a new price rule (discount rule)', + inputSchema: { + type: 'object' as const, + properties: { + title: { type: 'string', description: 'Price rule title' }, + target_type: { type: 'string', enum: ['line_item', 'shipping_line'], description: 'What the price rule applies to' }, + target_selection: { type: 'string', enum: ['all', 'entitled'], description: 'Which items the rule applies to' }, + allocation_method: { type: 'string', enum: ['across', 'each'], description: 'How the discount is distributed' }, + value_type: { type: 'string', enum: ['fixed_amount', 'percentage'], description: 'Type of discount value' }, + value: { type: 'string', description: 'Discount value (e.g., "-10.0" for $10 off or "-15.0" for 15% off)' }, + customer_selection: { type: 'string', enum: ['all', 'prerequisite'], description: 'Which customers the rule applies to' }, + starts_at: { type: 'string', description: 'Start date/time (ISO 8601)' }, + ends_at: { type: 'string', description: 'End date/time (ISO 8601)' }, + usage_limit: { type: 'number', description: 'Maximum number of times this discount can be used' }, + once_per_customer: { type: 'boolean', description: 'Limit to one use per customer' }, + }, + required: ['title', 'target_type', 'target_selection', 'allocation_method', 'value_type', 'value', 'customer_selection', 'starts_at'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreatePriceRuleInput.parse(input); + const result = await client.create('/price_rules.json', { price_rule: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_update_price_rule', + description: 'Update an existing price rule', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Price rule ID' }, + title: { type: 'string', description: 'Price rule title' }, + value: { type: 'string', description: 'Discount value' }, + starts_at: { type: 'string', description: 'Start date/time (ISO 8601)' }, + ends_at: { type: 'string', description: 'End date/time (ISO 8601)' }, + usage_limit: { type: 'number', description: 'Maximum number of times this discount can be used' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = UpdatePriceRuleInput.parse(input); + const { id, ...updates } = validated; + const result = await client.update(`/price_rules/${id}.json`, { price_rule: updates }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_delete_price_rule', + description: 'Delete a price rule by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Price rule ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = DeletePriceRuleInput.parse(input); + await client.delete(`/price_rules/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: `Price rule ${validated.id} deleted successfully` }], + }; + }, + }, + { + name: 'shopify_list_discount_codes', + description: 'List discount codes for a specific price rule', + inputSchema: { + type: 'object' as const, + properties: { + price_rule_id: { type: 'string', description: 'Price rule ID' }, + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + }, + required: ['price_rule_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListDiscountCodesInput.parse(input); + const { price_rule_id, ...params } = validated; + const results = await client.list(`/price_rules/${price_rule_id}/discount_codes.json`, { params }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_discount_code', + description: 'Create a discount code for a price rule', + inputSchema: { + type: 'object' as const, + properties: { + price_rule_id: { type: 'string', description: 'Price rule ID' }, + code: { type: 'string', description: 'Discount code' }, + usage_count: { type: 'number', description: 'Number of times this code has been used' }, + }, + required: ['price_rule_id', 'code'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreateDiscountCodeInput.parse(input); + const { price_rule_id, ...codeData } = validated; + const result = await client.create(`/price_rules/${price_rule_id}/discount_codes.json`, { discount_code: codeData }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + ]; diff --git a/servers/shopify/src/tools/fulfillments.ts b/servers/shopify/src/tools/fulfillments.ts new file mode 100644 index 0000000..f552187 --- /dev/null +++ b/servers/shopify/src/tools/fulfillments.ts @@ -0,0 +1,183 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const ListFulfillmentsInput = z.object({ + order_id: z.string().describe('Order ID'), + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), +}); + +const GetFulfillmentInput = z.object({ + order_id: z.string().describe('Order ID'), + fulfillment_id: z.string().describe('Fulfillment ID'), +}); + +const CreateFulfillmentInput = z.object({ + order_id: z.string().describe('Order ID'), + location_id: z.string().optional().describe('Location ID for fulfillment'), + tracking_number: z.string().optional().describe('Tracking number'), + tracking_company: z.string().optional().describe('Tracking company/carrier'), + tracking_url: z.string().optional().describe('Tracking URL'), + notify_customer: z.boolean().optional().describe('Send fulfillment notification to customer'), + line_items: z.array(z.object({ + id: z.string().describe('Line item ID'), + quantity: z.number().optional().describe('Quantity to fulfill'), + })).optional().describe('Line items to fulfill'), +}); + +const UpdateFulfillmentInput = z.object({ + order_id: z.string().describe('Order ID'), + fulfillment_id: z.string().describe('Fulfillment ID'), + tracking_number: z.string().optional().describe('Tracking number'), + tracking_company: z.string().optional().describe('Tracking company/carrier'), + tracking_url: z.string().optional().describe('Tracking URL'), + notify_customer: z.boolean().optional().describe('Send notification to customer'), +}); + +const CancelFulfillmentInput = z.object({ + order_id: z.string().describe('Order ID'), + fulfillment_id: z.string().describe('Fulfillment ID'), +}); + +const ListFulfillmentOrdersInput = z.object({ + order_id: z.string().describe('Order ID'), +}); + +export default [ + { + name: 'shopify_list_fulfillments', + description: 'List fulfillments for a specific order', + inputSchema: { + type: 'object' as const, + properties: { + order_id: { type: 'string', description: 'Order ID' }, + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + }, + required: ['order_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListFulfillmentsInput.parse(input); + const { order_id, ...params } = validated; + const results = await client.list(`/orders/${order_id}/fulfillments.json`, { params }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_fulfillment', + description: 'Get a specific fulfillment by ID', + inputSchema: { + type: 'object' as const, + properties: { + order_id: { type: 'string', description: 'Order ID' }, + fulfillment_id: { type: 'string', description: 'Fulfillment ID' }, + }, + required: ['order_id', 'fulfillment_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetFulfillmentInput.parse(input); + const result = await client.get(`/orders/${validated.order_id}/fulfillments/${validated.fulfillment_id}.json`); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_fulfillment', + description: 'Create a new fulfillment for an order with tracking information', + inputSchema: { + type: 'object' as const, + properties: { + order_id: { type: 'string', description: 'Order ID' }, + location_id: { type: 'string', description: 'Location ID for fulfillment' }, + tracking_number: { type: 'string', description: 'Tracking number' }, + tracking_company: { type: 'string', description: 'Tracking company/carrier' }, + tracking_url: { type: 'string', description: 'Tracking URL' }, + notify_customer: { type: 'boolean', description: 'Send fulfillment notification to customer' }, + line_items: { + type: 'array', + description: 'Line items to fulfill', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Line item ID' }, + quantity: { type: 'number', description: 'Quantity to fulfill' }, + }, + }, + }, + }, + required: ['order_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreateFulfillmentInput.parse(input); + const { order_id, ...fulfillmentData } = validated; + const result = await client.create(`/orders/${order_id}/fulfillments.json`, { fulfillment: fulfillmentData }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_update_fulfillment', + description: 'Update a fulfillment (e.g., update tracking information)', + inputSchema: { + type: 'object' as const, + properties: { + order_id: { type: 'string', description: 'Order ID' }, + fulfillment_id: { type: 'string', description: 'Fulfillment ID' }, + tracking_number: { type: 'string', description: 'Tracking number' }, + tracking_company: { type: 'string', description: 'Tracking company/carrier' }, + tracking_url: { type: 'string', description: 'Tracking URL' }, + notify_customer: { type: 'boolean', description: 'Send notification to customer' }, + }, + required: ['order_id', 'fulfillment_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = UpdateFulfillmentInput.parse(input); + const { order_id, fulfillment_id, ...updates } = validated; + const result = await client.update(`/orders/${order_id}/fulfillments/${fulfillment_id}.json`, { fulfillment: updates }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_cancel_fulfillment', + description: 'Cancel a fulfillment', + inputSchema: { + type: 'object' as const, + properties: { + order_id: { type: 'string', description: 'Order ID' }, + fulfillment_id: { type: 'string', description: 'Fulfillment ID' }, + }, + required: ['order_id', 'fulfillment_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CancelFulfillmentInput.parse(input); + const result = await client.create(`/orders/${validated.order_id}/fulfillments/${validated.fulfillment_id}/cancel.json`, {}); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_list_fulfillment_orders', + description: 'List fulfillment orders for a specific order', + inputSchema: { + type: 'object' as const, + properties: { + order_id: { type: 'string', description: 'Order ID' }, + }, + required: ['order_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListFulfillmentOrdersInput.parse(input); + const results = await client.list(`/orders/${validated.order_id}/fulfillment_orders.json`); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + ]; diff --git a/servers/shopify/src/tools/inventory.ts b/servers/shopify/src/tools/inventory.ts new file mode 100644 index 0000000..fb37073 --- /dev/null +++ b/servers/shopify/src/tools/inventory.ts @@ -0,0 +1,157 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const ListInventoryItemsInput = z.object({ + ids: z.string().describe('Comma-separated list of inventory item IDs'), + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), +}); + +const GetInventoryItemInput = z.object({ + id: z.string().describe('Inventory item ID'), +}); + +const ListInventoryLevelsInput = z.object({ + inventory_item_ids: z.string().optional().describe('Comma-separated inventory item IDs'), + location_ids: z.string().optional().describe('Comma-separated location IDs'), + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), +}); + +const SetInventoryLevelInput = z.object({ + inventory_item_id: z.string().describe('Inventory item ID'), + location_id: z.string().describe('Location ID'), + available: z.number().describe('New available inventory quantity'), + disconnect_if_necessary: z.boolean().optional().describe('Disconnect from location if necessary'), +}); + +const AdjustInventoryLevelInput = z.object({ + inventory_item_id: z.string().describe('Inventory item ID'), + location_id: z.string().describe('Location ID'), + available_adjustment: z.number().describe('Inventory adjustment (positive or negative)'), +}); + +const ListLocationsInput = z.object({ + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), +}); + +export default [ + { + name: 'shopify_list_inventory_items', + description: 'List inventory items by IDs', + inputSchema: { + type: 'object' as const, + properties: { + ids: { type: 'string', description: 'Comma-separated list of inventory item IDs' }, + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + }, + required: ['ids'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListInventoryItemsInput.parse(input); + const results = await client.list('/inventory_items.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_inventory_item', + description: 'Get a specific inventory item by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Inventory item ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetInventoryItemInput.parse(input); + const result = await client.get(`/inventory_items/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_list_inventory_levels', + description: 'List inventory levels filtered by inventory items or locations', + inputSchema: { + type: 'object' as const, + properties: { + inventory_item_ids: { type: 'string', description: 'Comma-separated inventory item IDs' }, + location_ids: { type: 'string', description: 'Comma-separated location IDs' }, + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListInventoryLevelsInput.parse(input); + const results = await client.list('/inventory_levels.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_set_inventory_level', + description: 'Set the inventory level for an item at a specific location (overwrites current value)', + inputSchema: { + type: 'object' as const, + properties: { + inventory_item_id: { type: 'string', description: 'Inventory item ID' }, + location_id: { type: 'string', description: 'Location ID' }, + available: { type: 'number', description: 'New available inventory quantity' }, + disconnect_if_necessary: { type: 'boolean', description: 'Disconnect from location if necessary' }, + }, + required: ['inventory_item_id', 'location_id', 'available'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = SetInventoryLevelInput.parse(input); + const result = await client.create('/inventory_levels/set.json', validated); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_adjust_inventory_level', + description: 'Adjust inventory level by a delta (positive or negative)', + inputSchema: { + type: 'object' as const, + properties: { + inventory_item_id: { type: 'string', description: 'Inventory item ID' }, + location_id: { type: 'string', description: 'Location ID' }, + available_adjustment: { type: 'number', description: 'Inventory adjustment (positive or negative)' }, + }, + required: ['inventory_item_id', 'location_id', 'available_adjustment'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = AdjustInventoryLevelInput.parse(input); + const result = await client.create('/inventory_levels/adjust.json', validated); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_list_locations', + description: 'List all inventory locations', + inputSchema: { + type: 'object' as const, + properties: { + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListLocationsInput.parse(input); + const results = await client.list('/locations.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + ]; diff --git a/servers/shopify/src/tools/orders.ts b/servers/shopify/src/tools/orders.ts new file mode 100644 index 0000000..8e1fc63 --- /dev/null +++ b/servers/shopify/src/tools/orders.ts @@ -0,0 +1,247 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const ListOrdersInput = z.object({ + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), + status: z.enum(['open', 'archived', 'cancelled', 'any']).optional().describe('Filter by order status'), + financial_status: z.enum(['pending', 'authorized', 'partially_paid', 'paid', 'partially_refunded', 'refunded', 'voided', 'any']).optional().describe('Filter by financial status'), + fulfillment_status: z.enum(['shipped', 'partial', 'unshipped', 'any', 'unfulfilled']).optional().describe('Filter by fulfillment status'), + created_at_min: z.string().optional().describe('Filter by min creation date (ISO 8601)'), + created_at_max: z.string().optional().describe('Filter by max creation date (ISO 8601)'), + updated_at_min: z.string().optional().describe('Filter by min update date (ISO 8601)'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const GetOrderInput = z.object({ + id: z.string().describe('Order ID'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const CreateOrderInput = z.object({ + line_items: z.array(z.object({ + variant_id: z.string().optional().describe('Product variant ID'), + quantity: z.number().describe('Quantity'), + price: z.string().optional().describe('Price override'), + title: z.string().optional().describe('Line item title (if no variant_id)'), + })).describe('Order line items'), + customer: z.object({ + id: z.string().optional().describe('Existing customer ID'), + email: z.string().optional().describe('Customer email'), + first_name: z.string().optional().describe('Customer first name'), + last_name: z.string().optional().describe('Customer last name'), + }).optional().describe('Customer information'), + billing_address: z.object({ + address1: z.string().optional().describe('Address line 1'), + city: z.string().optional().describe('City'), + province: z.string().optional().describe('State/Province'), + country: z.string().optional().describe('Country'), + zip: z.string().optional().describe('Postal code'), + }).optional().describe('Billing address'), + shipping_address: z.object({ + address1: z.string().optional().describe('Address line 1'), + city: z.string().optional().describe('City'), + province: z.string().optional().describe('State/Province'), + country: z.string().optional().describe('Country'), + zip: z.string().optional().describe('Postal code'), + }).optional().describe('Shipping address'), + financial_status: z.enum(['pending', 'authorized', 'paid']).optional().describe('Financial status'), + send_receipt: z.boolean().optional().describe('Send order confirmation to customer'), + note: z.string().optional().describe('Order note'), +}); + +const UpdateOrderInput = z.object({ + id: z.string().describe('Order ID'), + note: z.string().optional().describe('Order note'), + tags: z.string().optional().describe('Comma-separated tags'), + email: z.string().optional().describe('Customer email'), +}); + +const CancelOrderInput = z.object({ + id: z.string().describe('Order ID'), + amount: z.number().optional().describe('Amount to refund'), + restock: z.boolean().optional().describe('Restock items'), + reason: z.enum(['customer', 'fraud', 'inventory', 'declined', 'other']).optional().describe('Cancellation reason'), + email: z.boolean().optional().describe('Send notification email'), +}); + +const CloseOrderInput = z.object({ + id: z.string().describe('Order ID'), +}); + +export default [ + { + name: 'shopify_list_orders', + description: 'List orders with pagination and filtering by status, financial status, fulfillment status, and date ranges', + inputSchema: { + type: 'object' as const, + properties: { + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + status: { type: 'string', enum: ['open', 'archived', 'cancelled', 'any'], description: 'Filter by order status' }, + financial_status: { type: 'string', enum: ['pending', 'authorized', 'partially_paid', 'paid', 'partially_refunded', 'refunded', 'voided', 'any'], description: 'Filter by financial status' }, + fulfillment_status: { type: 'string', enum: ['shipped', 'partial', 'unshipped', 'any', 'unfulfilled'], description: 'Filter by fulfillment status' }, + created_at_min: { type: 'string', description: 'Filter by min creation date (ISO 8601)' }, + created_at_max: { type: 'string', description: 'Filter by max creation date (ISO 8601)' }, + updated_at_min: { type: 'string', description: 'Filter by min update date (ISO 8601)' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListOrdersInput.parse(input); + const results = await client.list('/orders.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_order', + description: 'Get a specific order by ID with optional field filtering', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Order ID' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetOrderInput.parse(input); + const result = await client.get(`/orders/${validated.id}.json`, { + params: validated.fields ? { fields: validated.fields } : {}, + }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_order', + description: 'Create a new order with line items, customer, and addresses', + inputSchema: { + type: 'object' as const, + properties: { + line_items: { + type: 'array', + description: 'Order line items', + items: { + type: 'object', + properties: { + variant_id: { type: 'string', description: 'Product variant ID' }, + quantity: { type: 'number', description: 'Quantity' }, + price: { type: 'string', description: 'Price override' }, + title: { type: 'string', description: 'Line item title (if no variant_id)' }, + }, + }, + }, + customer: { + type: 'object', + description: 'Customer information', + properties: { + id: { type: 'string', description: 'Existing customer ID' }, + email: { type: 'string', description: 'Customer email' }, + first_name: { type: 'string', description: 'Customer first name' }, + last_name: { type: 'string', description: 'Customer last name' }, + }, + }, + billing_address: { + type: 'object', + description: 'Billing address', + properties: { + address1: { type: 'string', description: 'Address line 1' }, + city: { type: 'string', description: 'City' }, + province: { type: 'string', description: 'State/Province' }, + country: { type: 'string', description: 'Country' }, + zip: { type: 'string', description: 'Postal code' }, + }, + }, + shipping_address: { + type: 'object', + description: 'Shipping address', + properties: { + address1: { type: 'string', description: 'Address line 1' }, + city: { type: 'string', description: 'City' }, + province: { type: 'string', description: 'State/Province' }, + country: { type: 'string', description: 'Country' }, + zip: { type: 'string', description: 'Postal code' }, + }, + }, + financial_status: { type: 'string', enum: ['pending', 'authorized', 'paid'], description: 'Financial status' }, + send_receipt: { type: 'boolean', description: 'Send order confirmation to customer' }, + note: { type: 'string', description: 'Order note' }, + }, + required: ['line_items'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreateOrderInput.parse(input); + const result = await client.create('/orders.json', { order: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_update_order', + description: 'Update an existing order (limited fields: note, tags, email)', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Order ID' }, + note: { type: 'string', description: 'Order note' }, + tags: { type: 'string', description: 'Comma-separated tags' }, + email: { type: 'string', description: 'Customer email' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = UpdateOrderInput.parse(input); + const { id, ...updates } = validated; + const result = await client.update(`/orders/${id}.json`, { order: updates }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_cancel_order', + description: 'Cancel an order with optional refund and restocking', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Order ID' }, + amount: { type: 'number', description: 'Amount to refund' }, + restock: { type: 'boolean', description: 'Restock items' }, + reason: { type: 'string', enum: ['customer', 'fraud', 'inventory', 'declined', 'other'], description: 'Cancellation reason' }, + email: { type: 'boolean', description: 'Send notification email' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CancelOrderInput.parse(input); + const { id, ...params } = validated; + const result = await client.create(`/orders/${id}/cancel.json`, params); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_close_order', + description: 'Close an order (mark as complete)', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Order ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CloseOrderInput.parse(input); + const result = await client.create(`/orders/${validated.id}/close.json`, {}); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + ]; diff --git a/servers/shopify/src/tools/pages.ts b/servers/shopify/src/tools/pages.ts new file mode 100644 index 0000000..66686a9 --- /dev/null +++ b/servers/shopify/src/tools/pages.ts @@ -0,0 +1,164 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const ListPagesInput = z.object({ + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), + title: z.string().optional().describe('Filter by title'), + created_at_min: z.string().optional().describe('Filter by min creation date (ISO 8601)'), + created_at_max: z.string().optional().describe('Filter by max creation date (ISO 8601)'), + updated_at_min: z.string().optional().describe('Filter by min update date (ISO 8601)'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const GetPageInput = z.object({ + id: z.string().describe('Page ID'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const CreatePageInput = z.object({ + title: z.string().describe('Page title'), + body_html: z.string().optional().describe('Page content HTML'), + author: z.string().optional().describe('Page author'), + published: z.boolean().optional().describe('Published status'), + metafields: z.array(z.object({ + namespace: z.string().describe('Metafield namespace'), + key: z.string().describe('Metafield key'), + value: z.string().describe('Metafield value'), + type: z.string().describe('Metafield type'), + })).optional().describe('Page metafields'), +}); + +const UpdatePageInput = z.object({ + id: z.string().describe('Page ID'), + title: z.string().optional().describe('Page title'), + body_html: z.string().optional().describe('Page content HTML'), + author: z.string().optional().describe('Page author'), + published: z.boolean().optional().describe('Published status'), +}); + +const DeletePageInput = z.object({ + id: z.string().describe('Page ID'), +}); + +export default [ + { + name: 'shopify_list_pages', + description: 'List pages with pagination, title filtering, and date filtering', + inputSchema: { + type: 'object' as const, + properties: { + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + title: { type: 'string', description: 'Filter by title' }, + created_at_min: { type: 'string', description: 'Filter by min creation date (ISO 8601)' }, + created_at_max: { type: 'string', description: 'Filter by max creation date (ISO 8601)' }, + updated_at_min: { type: 'string', description: 'Filter by min update date (ISO 8601)' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListPagesInput.parse(input); + const results = await client.list('/pages.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_page', + description: 'Get a specific page by ID with optional field filtering', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Page ID' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetPageInput.parse(input); + const result = await client.get(`/pages/${validated.id}.json`, { + params: validated.fields ? { fields: validated.fields } : {}, + }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_page', + description: 'Create a new page with optional metafields', + inputSchema: { + type: 'object' as const, + properties: { + title: { type: 'string', description: 'Page title' }, + body_html: { type: 'string', description: 'Page content HTML' }, + author: { type: 'string', description: 'Page author' }, + published: { type: 'boolean', description: 'Published status' }, + metafields: { + type: 'array', + description: 'Page metafields', + items: { + type: 'object', + properties: { + namespace: { type: 'string', description: 'Metafield namespace' }, + key: { type: 'string', description: 'Metafield key' }, + value: { type: 'string', description: 'Metafield value' }, + type: { type: 'string', description: 'Metafield type' }, + }, + }, + }, + }, + required: ['title'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreatePageInput.parse(input); + const result = await client.create('/pages.json', { page: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_update_page', + description: 'Update an existing page', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Page ID' }, + title: { type: 'string', description: 'Page title' }, + body_html: { type: 'string', description: 'Page content HTML' }, + author: { type: 'string', description: 'Page author' }, + published: { type: 'boolean', description: 'Published status' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = UpdatePageInput.parse(input); + const { id, ...updates } = validated; + const result = await client.update(`/pages/${id}.json`, { page: updates }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_delete_page', + description: 'Delete a page by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Page ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = DeletePageInput.parse(input); + await client.delete(`/pages/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: `Page ${validated.id} deleted successfully` }], + }; + }, + }, + ]; diff --git a/servers/shopify/src/tools/products.ts b/servers/shopify/src/tools/products.ts new file mode 100644 index 0000000..49cb27b --- /dev/null +++ b/servers/shopify/src/tools/products.ts @@ -0,0 +1,221 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const ListProductsInput = z.object({ + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), + status: z.enum(['active', 'archived', 'draft']).optional().describe('Filter by status'), + product_type: z.string().optional().describe('Filter by product type'), + vendor: z.string().optional().describe('Filter by vendor'), + collection_id: z.string().optional().describe('Filter by collection'), + created_at_min: z.string().optional().describe('Filter by min creation date (ISO 8601)'), + updated_at_min: z.string().optional().describe('Filter by min update date (ISO 8601)'), +}); + +const GetProductInput = z.object({ + id: z.string().describe('Product ID'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const CreateProductInput = z.object({ + title: z.string().describe('Product title'), + body_html: z.string().optional().describe('Product description HTML'), + vendor: z.string().optional().describe('Product vendor'), + product_type: z.string().optional().describe('Product type/category'), + tags: z.string().optional().describe('Comma-separated tags'), + status: z.enum(['active', 'archived', 'draft']).optional().describe('Product status'), + variants: z.array(z.object({ + price: z.string().describe('Variant price'), + sku: z.string().optional().describe('SKU'), + inventory_quantity: z.number().optional().describe('Inventory quantity'), + option1: z.string().optional().describe('Option 1 value'), + option2: z.string().optional().describe('Option 2 value'), + option3: z.string().optional().describe('Option 3 value'), + })).optional().describe('Product variants'), + images: z.array(z.object({ + src: z.string().describe('Image URL'), + alt: z.string().optional().describe('Alt text'), + })).optional().describe('Product images'), +}); + +const UpdateProductInput = z.object({ + id: z.string().describe('Product ID'), + title: z.string().optional().describe('Product title'), + body_html: z.string().optional().describe('Product description HTML'), + vendor: z.string().optional().describe('Product vendor'), + product_type: z.string().optional().describe('Product type/category'), + tags: z.string().optional().describe('Comma-separated tags'), + status: z.enum(['active', 'archived', 'draft']).optional().describe('Product status'), +}); + +const DeleteProductInput = z.object({ + id: z.string().describe('Product ID'), +}); + +const SearchProductsInput = z.object({ + query: z.string().describe('Search query (title, tag, sku, vendor, etc.)'), + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), +}); + +export default [ + { + name: 'shopify_list_products', + description: 'List products with pagination, filtering by status, type, vendor, collection, and date ranges', + inputSchema: { + type: 'object' as const, + properties: { + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + status: { type: 'string', enum: ['active', 'archived', 'draft'], description: 'Filter by status' }, + product_type: { type: 'string', description: 'Filter by product type' }, + vendor: { type: 'string', description: 'Filter by vendor' }, + collection_id: { type: 'string', description: 'Filter by collection' }, + created_at_min: { type: 'string', description: 'Filter by min creation date (ISO 8601)' }, + updated_at_min: { type: 'string', description: 'Filter by min update date (ISO 8601)' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListProductsInput.parse(input); + const results = await client.list('/products.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_product', + description: 'Get a specific product by ID with optional field filtering', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Product ID' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetProductInput.parse(input); + const result = await client.get(`/products/${validated.id}.json`, { + params: validated.fields ? { fields: validated.fields } : {}, + }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_product', + description: 'Create a new product with variants and images', + inputSchema: { + type: 'object' as const, + properties: { + title: { type: 'string', description: 'Product title' }, + body_html: { type: 'string', description: 'Product description HTML' }, + vendor: { type: 'string', description: 'Product vendor' }, + product_type: { type: 'string', description: 'Product type/category' }, + tags: { type: 'string', description: 'Comma-separated tags' }, + status: { type: 'string', enum: ['active', 'archived', 'draft'], description: 'Product status' }, + variants: { + type: 'array', + description: 'Product variants', + items: { + type: 'object', + properties: { + price: { type: 'string', description: 'Variant price' }, + sku: { type: 'string', description: 'SKU' }, + inventory_quantity: { type: 'number', description: 'Inventory quantity' }, + option1: { type: 'string', description: 'Option 1 value' }, + option2: { type: 'string', description: 'Option 2 value' }, + option3: { type: 'string', description: 'Option 3 value' }, + }, + }, + }, + images: { + type: 'array', + description: 'Product images', + items: { + type: 'object', + properties: { + src: { type: 'string', description: 'Image URL' }, + alt: { type: 'string', description: 'Alt text' }, + }, + }, + }, + }, + required: ['title'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreateProductInput.parse(input); + const result = await client.create('/products.json', { product: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_update_product', + description: 'Update an existing product', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Product ID' }, + title: { type: 'string', description: 'Product title' }, + body_html: { type: 'string', description: 'Product description HTML' }, + vendor: { type: 'string', description: 'Product vendor' }, + product_type: { type: 'string', description: 'Product type/category' }, + tags: { type: 'string', description: 'Comma-separated tags' }, + status: { type: 'string', enum: ['active', 'archived', 'draft'], description: 'Product status' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = UpdateProductInput.parse(input); + const { id, ...updates } = validated; + const result = await client.update(`/products/${id}.json`, { product: updates }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_delete_product', + description: 'Delete a product by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Product ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = DeleteProductInput.parse(input); + await client.delete(`/products/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: `Product ${validated.id} deleted successfully` }], + }; + }, + }, + { + name: 'shopify_search_products', + description: 'Search products by query (searches title, tags, SKU, vendor, etc.)', + inputSchema: { + type: 'object' as const, + properties: { + query: { type: 'string', description: 'Search query (title, tag, sku, vendor, etc.)' }, + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + }, + required: ['query'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = SearchProductsInput.parse(input); + const results = await client.list('/products.json', { + params: { ...validated, title: validated.query }, + }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, +]; diff --git a/servers/shopify/src/tools/shipping.ts b/servers/shopify/src/tools/shipping.ts new file mode 100644 index 0000000..52798b1 --- /dev/null +++ b/servers/shopify/src/tools/shipping.ts @@ -0,0 +1,150 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const ListShippingZonesInput = z.object({ + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const ListCarrierServicesInput = z.object({ + limit: z.number().min(1).max(250).default(50).describe('Results per page'), +}); + +const GetCarrierServiceInput = z.object({ + id: z.string().describe('Carrier service ID'), +}); + +const CreateCarrierServiceInput = z.object({ + name: z.string().describe('Carrier service name'), + callback_url: z.string().describe('URL for rate calculation callback'), + service_discovery: z.boolean().describe('Whether to discover services automatically'), + carrier_service_type: z.enum(['api', 'legacy']).optional().describe('Type of carrier service'), + format: z.enum(['json', 'xml']).optional().describe('Data format'), +}); + +const UpdateCarrierServiceInput = z.object({ + id: z.string().describe('Carrier service ID'), + name: z.string().optional().describe('Carrier service name'), + callback_url: z.string().optional().describe('URL for rate calculation callback'), + service_discovery: z.boolean().optional().describe('Whether to discover services automatically'), +}); + +const DeleteCarrierServiceInput = z.object({ + id: z.string().describe('Carrier service ID'), +}); + +export default [ + { + name: 'shopify_list_shipping_zones', + description: 'List all shipping zones', + inputSchema: { + type: 'object' as const, + properties: { + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListShippingZonesInput.parse(input); + const results = await client.list('/shipping_zones.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_list_carrier_services', + description: 'List all carrier services (third-party shipping rate calculators)', + inputSchema: { + type: 'object' as const, + properties: { + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListCarrierServicesInput.parse(input); + const results = await client.list('/carrier_services.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_carrier_service', + description: 'Get a specific carrier service by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Carrier service ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetCarrierServiceInput.parse(input); + const result = await client.get(`/carrier_services/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_carrier_service', + description: 'Create a new carrier service', + inputSchema: { + type: 'object' as const, + properties: { + name: { type: 'string', description: 'Carrier service name' }, + callback_url: { type: 'string', description: 'URL for rate calculation callback' }, + service_discovery: { type: 'boolean', description: 'Whether to discover services automatically' }, + carrier_service_type: { type: 'string', enum: ['api', 'legacy'], description: 'Type of carrier service' }, + format: { type: 'string', enum: ['json', 'xml'], description: 'Data format' }, + }, + required: ['name', 'callback_url', 'service_discovery'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreateCarrierServiceInput.parse(input); + const result = await client.create('/carrier_services.json', { carrier_service: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_update_carrier_service', + description: 'Update an existing carrier service', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Carrier service ID' }, + name: { type: 'string', description: 'Carrier service name' }, + callback_url: { type: 'string', description: 'URL for rate calculation callback' }, + service_discovery: { type: 'boolean', description: 'Whether to discover services automatically' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = UpdateCarrierServiceInput.parse(input); + const { id, ...updates } = validated; + const result = await client.update(`/carrier_services/${id}.json`, { carrier_service: updates }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_delete_carrier_service', + description: 'Delete a carrier service by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Carrier service ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = DeleteCarrierServiceInput.parse(input); + await client.delete(`/carrier_services/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: `Carrier service ${validated.id} deleted successfully` }], + }; + }, + }, + ]; diff --git a/servers/shopify/src/tools/themes.ts b/servers/shopify/src/tools/themes.ts new file mode 100644 index 0000000..a4e45ab --- /dev/null +++ b/servers/shopify/src/tools/themes.ts @@ -0,0 +1,211 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const ListThemesInput = z.object({ + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), + role: z.enum(['main', 'unpublished', 'demo']).optional().describe('Filter by theme role'), +}); + +const GetThemeInput = z.object({ + id: z.string().describe('Theme ID'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const CreateThemeInput = z.object({ + name: z.string().describe('Theme name'), + src: z.string().optional().describe('URL to theme ZIP file'), + role: z.enum(['main', 'unpublished']).optional().describe('Theme role'), +}); + +const UpdateThemeInput = z.object({ + id: z.string().describe('Theme ID'), + name: z.string().optional().describe('Theme name'), + role: z.enum(['main', 'unpublished']).optional().describe('Theme role'), +}); + +const DeleteThemeInput = z.object({ + id: z.string().describe('Theme ID'), +}); + +const ListAssetsInput = z.object({ + theme_id: z.string().describe('Theme ID'), + fields: z.string().optional().describe('Comma-separated list of fields to retrieve'), +}); + +const GetAssetInput = z.object({ + theme_id: z.string().describe('Theme ID'), + asset_key: z.string().describe('Asset key (path, e.g., "templates/index.liquid")'), +}); + +const CreateOrUpdateAssetInput = z.object({ + theme_id: z.string().describe('Theme ID'), + key: z.string().describe('Asset key (path, e.g., "templates/index.liquid")'), + value: z.string().optional().describe('Asset text content'), + src: z.string().optional().describe('Asset source URL (for binary assets)'), + attachment: z.string().optional().describe('Base64-encoded binary asset content'), +}); + +export default [ + { + name: 'shopify_list_themes', + description: 'List all themes with optional role filtering', + inputSchema: { + type: 'object' as const, + properties: { + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + role: { type: 'string', enum: ['main', 'unpublished', 'demo'], description: 'Filter by theme role' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListThemesInput.parse(input); + const results = await client.list('/themes.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_theme', + description: 'Get a specific theme by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Theme ID' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetThemeInput.parse(input); + const result = await client.get(`/themes/${validated.id}.json`, { + params: validated.fields ? { fields: validated.fields } : {}, + }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_theme', + description: 'Create a new theme from a ZIP file', + inputSchema: { + type: 'object' as const, + properties: { + name: { type: 'string', description: 'Theme name' }, + src: { type: 'string', description: 'URL to theme ZIP file' }, + role: { type: 'string', enum: ['main', 'unpublished'], description: 'Theme role' }, + }, + required: ['name'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreateThemeInput.parse(input); + const result = await client.create('/themes.json', { theme: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_update_theme', + description: 'Update an existing theme', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Theme ID' }, + name: { type: 'string', description: 'Theme name' }, + role: { type: 'string', enum: ['main', 'unpublished'], description: 'Theme role' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = UpdateThemeInput.parse(input); + const { id, ...updates } = validated; + const result = await client.update(`/themes/${id}.json`, { theme: updates }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_delete_theme', + description: 'Delete a theme by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Theme ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = DeleteThemeInput.parse(input); + await client.delete(`/themes/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: `Theme ${validated.id} deleted successfully` }], + }; + }, + }, + { + name: 'shopify_list_theme_assets', + description: 'List all assets in a theme', + inputSchema: { + type: 'object' as const, + properties: { + theme_id: { type: 'string', description: 'Theme ID' }, + fields: { type: 'string', description: 'Comma-separated list of fields to retrieve' }, + }, + required: ['theme_id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListAssetsInput.parse(input); + const { theme_id, ...params } = validated; + const results = await client.list(`/themes/${theme_id}/assets.json`, { params }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_theme_asset', + description: 'Get a specific theme asset by key', + inputSchema: { + type: 'object' as const, + properties: { + theme_id: { type: 'string', description: 'Theme ID' }, + asset_key: { type: 'string', description: 'Asset key (path, e.g., "templates/index.liquid")' }, + }, + required: ['theme_id', 'asset_key'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetAssetInput.parse(input); + const result = await client.get(`/themes/${validated.theme_id}/assets.json`, { + params: { 'asset[key]': validated.asset_key }, + }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_or_update_theme_asset', + description: 'Create or update a theme asset (text or binary)', + inputSchema: { + type: 'object' as const, + properties: { + theme_id: { type: 'string', description: 'Theme ID' }, + key: { type: 'string', description: 'Asset key (path, e.g., "templates/index.liquid")' }, + value: { type: 'string', description: 'Asset text content' }, + src: { type: 'string', description: 'Asset source URL (for binary assets)' }, + attachment: { type: 'string', description: 'Base64-encoded binary asset content' }, + }, + required: ['theme_id', 'key'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreateOrUpdateAssetInput.parse(input); + const { theme_id, ...assetData } = validated; + const result = await client.update(`/themes/${theme_id}/assets.json`, { asset: assetData }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + ]; diff --git a/servers/shopify/src/tools/webhooks.ts b/servers/shopify/src/tools/webhooks.ts new file mode 100644 index 0000000..03f0572 --- /dev/null +++ b/servers/shopify/src/tools/webhooks.ts @@ -0,0 +1,139 @@ +import { z } from 'zod'; +import { ShopifyClient } from '../clients/shopify.js'; + +const ListWebhooksInput = z.object({ + limit: z.number().min(1).max(250).default(50).describe('Results per page'), + page_info: z.string().optional().describe('Cursor for pagination'), + topic: z.string().optional().describe('Filter by webhook topic'), + address: z.string().optional().describe('Filter by webhook destination URL'), +}); + +const GetWebhookInput = z.object({ + id: z.string().describe('Webhook ID'), +}); + +const CreateWebhookInput = z.object({ + topic: z.string().describe('Webhook topic (e.g., "orders/create", "products/update", "customers/delete")'), + address: z.string().describe('Webhook destination URL'), + format: z.enum(['json', 'xml']).default('json').describe('Payload format'), + fields: z.array(z.string()).optional().describe('List of fields to include in webhook payload'), + metafield_namespaces: z.array(z.string()).optional().describe('Metafield namespaces to include'), + private_metafield_namespaces: z.array(z.string()).optional().describe('Private metafield namespaces to include'), +}); + +const UpdateWebhookInput = z.object({ + id: z.string().describe('Webhook ID'), + topic: z.string().optional().describe('Webhook topic'), + address: z.string().optional().describe('Webhook destination URL'), + format: z.enum(['json', 'xml']).optional().describe('Payload format'), + fields: z.array(z.string()).optional().describe('List of fields to include in webhook payload'), +}); + +const DeleteWebhookInput = z.object({ + id: z.string().describe('Webhook ID'), +}); + +export default [ + { + name: 'shopify_list_webhooks', + description: 'List all webhook subscriptions with optional filtering by topic or address', + inputSchema: { + type: 'object' as const, + properties: { + limit: { type: 'number', description: 'Results per page', default: 50, minimum: 1, maximum: 250 }, + page_info: { type: 'string', description: 'Cursor for pagination' }, + topic: { type: 'string', description: 'Filter by webhook topic' }, + address: { type: 'string', description: 'Filter by webhook destination URL' }, + }, + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = ListWebhooksInput.parse(input); + const results = await client.list('/webhooks.json', { params: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + }; + }, + }, + { + name: 'shopify_get_webhook', + description: 'Get a specific webhook subscription by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Webhook ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = GetWebhookInput.parse(input); + const result = await client.get(`/webhooks/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_create_webhook', + description: 'Create a new webhook subscription for a specific topic (e.g., orders/create, products/update)', + inputSchema: { + type: 'object' as const, + properties: { + topic: { type: 'string', description: 'Webhook topic (e.g., "orders/create", "products/update", "customers/delete")' }, + address: { type: 'string', description: 'Webhook destination URL' }, + format: { type: 'string', enum: ['json', 'xml'], description: 'Payload format', default: 'json' }, + fields: { type: 'array', description: 'List of fields to include in webhook payload', items: { type: 'string' } }, + metafield_namespaces: { type: 'array', description: 'Metafield namespaces to include', items: { type: 'string' } }, + private_metafield_namespaces: { type: 'array', description: 'Private metafield namespaces to include', items: { type: 'string' } }, + }, + required: ['topic', 'address'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = CreateWebhookInput.parse(input); + const result = await client.create('/webhooks.json', { webhook: validated }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_update_webhook', + description: 'Update an existing webhook subscription', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Webhook ID' }, + topic: { type: 'string', description: 'Webhook topic' }, + address: { type: 'string', description: 'Webhook destination URL' }, + format: { type: 'string', enum: ['json', 'xml'], description: 'Payload format' }, + fields: { type: 'array', description: 'List of fields to include in webhook payload', items: { type: 'string' } }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = UpdateWebhookInput.parse(input); + const { id, ...updates } = validated; + const result = await client.update(`/webhooks/${id}.json`, { webhook: updates }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + }, + }, + { + name: 'shopify_delete_webhook', + description: 'Delete a webhook subscription by ID', + inputSchema: { + type: 'object' as const, + properties: { + id: { type: 'string', description: 'Webhook ID' }, + }, + required: ['id'], + }, + handler: async (input: unknown, client: ShopifyClient) => { + const validated = DeleteWebhookInput.parse(input); + await client.delete(`/webhooks/${validated.id}.json`); + return { + content: [{ type: 'text' as const, text: `Webhook ${validated.id} deleted successfully` }], + }; + }, + }, + ]; diff --git a/servers/stripe/TOOLS_SUMMARY.md b/servers/stripe/TOOLS_SUMMARY.md new file mode 100644 index 0000000..75fe1f7 --- /dev/null +++ b/servers/stripe/TOOLS_SUMMARY.md @@ -0,0 +1,169 @@ +# Stripe MCP Server - Tools Implementation Summary + +## Overview +**Total Tools: 77** (target: 60-80) ✅ + +All 14 tool files have been fully implemented with proper TypeScript types, Zod validation, and comprehensive Stripe API coverage. + +## Tool Breakdown by Category + +### 1. **customers.ts** (6 tools) +- `stripe_list_customers` - List all customers with filtering +- `stripe_get_customer` - Retrieve specific customer +- `stripe_create_customer` - Create new customer +- `stripe_update_customer` - Update customer details +- `stripe_delete_customer` - Delete customer +- `stripe_search_customers` - Search customers by query + +### 2. **charges.ts** (5 tools) +- `stripe_list_charges` - List all charges +- `stripe_get_charge` - Retrieve specific charge +- `stripe_create_charge` - Create direct charge +- `stripe_update_charge` - Update charge details +- `stripe_capture_charge` - Capture authorized charge + +### 3. **payment-intents.ts** (7 tools) +- `stripe_list_payment_intents` - List all payment intents +- `stripe_get_payment_intent` - Retrieve specific payment intent +- `stripe_create_payment_intent` - Create new payment intent +- `stripe_update_payment_intent` - Update payment intent +- `stripe_confirm_payment_intent` - Confirm payment intent +- `stripe_cancel_payment_intent` - Cancel payment intent +- `stripe_capture_payment_intent` - Capture payment intent + +### 4. **payment-methods.ts** (6 tools) +- `stripe_list_payment_methods` - List payment methods for customer +- `stripe_get_payment_method` - Retrieve specific payment method +- `stripe_create_payment_method` - Create payment method +- `stripe_update_payment_method` - Update payment method +- `stripe_attach_payment_method` - Attach to customer +- `stripe_detach_payment_method` - Detach from customer + +### 5. **refunds.ts** (5 tools) +- `stripe_list_refunds` - List all refunds +- `stripe_get_refund` - Retrieve specific refund +- `stripe_create_refund` - Create refund +- `stripe_update_refund` - Update refund metadata +- `stripe_cancel_refund` - Cancel pending refund + +### 6. **disputes.ts** (4 tools) +- `stripe_list_disputes` - List all disputes +- `stripe_get_dispute` - Retrieve specific dispute +- `stripe_update_dispute` - Submit evidence for dispute +- `stripe_close_dispute` - Accept dispute + +### 7. **subscriptions.ts** (7 tools) +- `stripe_list_subscriptions` - List all subscriptions +- `stripe_get_subscription` - Retrieve specific subscription +- `stripe_create_subscription` - Create new subscription +- `stripe_update_subscription` - Update subscription +- `stripe_cancel_subscription` - Cancel subscription +- `stripe_resume_subscription` - Resume paused subscription +- `stripe_list_subscription_items` - List subscription items + +### 8. **invoices.ts** (11 tools) +- `stripe_list_invoices` - List all invoices +- `stripe_get_invoice` - Retrieve specific invoice +- `stripe_create_invoice` - Create draft invoice +- `stripe_update_invoice` - Update draft invoice +- `stripe_finalize_invoice` - Finalize draft +- `stripe_pay_invoice` - Pay invoice manually +- `stripe_void_invoice` - Void invoice +- `stripe_send_invoice` - Send to customer +- `stripe_list_invoice_items` - List invoice items +- `stripe_create_invoice_item` - Create one-time charge +- `stripe_delete_invoice_item` - Delete invoice item + +### 9. **products.ts** (6 tools) +- `stripe_list_products` - List all products +- `stripe_get_product` - Retrieve specific product +- `stripe_create_product` - Create new product +- `stripe_update_product` - Update product +- `stripe_delete_product` - Delete product +- `stripe_search_products` - Search products by query + +### 10. **prices.ts** (4 tools) +- `stripe_list_prices` - List all prices +- `stripe_get_price` - Retrieve specific price +- `stripe_create_price` - Create new price +- `stripe_update_price` - Update price metadata + +### 11. **payouts.ts** (6 tools) +- `stripe_list_payouts` - List all payouts +- `stripe_get_payout` - Retrieve specific payout +- `stripe_create_payout` - Create manual payout +- `stripe_update_payout` - Update payout metadata +- `stripe_cancel_payout` - Cancel pending payout +- `stripe_reverse_payout` - Reverse a payout + +### 12. **balance.ts** (3 tools) +- `stripe_get_balance` - Get current account balance +- `stripe_list_balance_transactions` - List balance history +- `stripe_get_balance_transaction` - Retrieve specific transaction + +### 13. **events.ts** (2 tools) +- `stripe_list_events` - List webhook events +- `stripe_get_event` - Retrieve specific event + +### 14. **webhooks.ts** (5 tools) +- `stripe_list_webhook_endpoints` - List all endpoints +- `stripe_get_webhook_endpoint` - Retrieve specific endpoint +- `stripe_create_webhook_endpoint` - Create new endpoint +- `stripe_update_webhook_endpoint` - Update endpoint +- `stripe_delete_webhook_endpoint` - Delete endpoint + +## Quality Features + +### ✅ TypeScript Compilation +- All files pass `npx tsc --noEmit` with zero errors +- Proper type imports from `../types/index.js` and `../clients/stripe.js` + +### ✅ Zod Validation +- Comprehensive input schemas for all tools +- Rich descriptions for every parameter +- Proper enum types, unions, and nested objects + +### ✅ Stripe API Best Practices +- **Form-encoded params** - Client handles encoding automatically +- **Cursor pagination** - `starting_after`/`ending_before` support +- **Amount fields in cents** - Documented in schemas +- **Expandable fields** - `expand[]` param support +- **Idempotency keys** - Auto-generated for create operations +- **Proper HTTP methods** - GET/POST/DELETE as appropriate + +### ✅ Tool Naming Convention +- Consistent pattern: `stripe_verb_noun` +- Examples: `stripe_list_customers`, `stripe_create_payment_intent` + +### ✅ Handler Structure +- Signature: `async (client: StripeClient, args: any) => Promise` +- Returns raw API response (not wrapped in content structure) +- Clean parameter extraction and validation + +## Implementation Notes + +1. **No modifications** were made to: + - `src/types/index.ts` - Type definitions (as required) + - `src/clients/stripe.ts` - API client (as required) + - `src/server.ts` - Server shell (as required) + - `src/main.ts` - Entry point (as required) + +2. **All 14 tool stub files** were replaced with full implementations + +3. **Export pattern**: Each file uses `export default [...]` to match server's import expectations + +4. **Error handling**: Delegated to client layer (retry, rate limiting, etc.) + +5. **TypeScript strict mode**: All files are fully typed and compile cleanly + +## Next Steps + +Server is ready for: +- Building: `npm run build` +- Testing: Integration tests with test API keys +- Deployment: Publishing to npm registry +- Documentation: API reference generation + +--- + +**Status**: ✅ COMPLETE - All 77 tools implemented, TypeScript verified, ready for deployment. diff --git a/servers/stripe/src/tools/balance.ts b/servers/stripe/src/tools/balance.ts index 0c8177e..bcfa43b 100644 --- a/servers/stripe/src/tools/balance.ts +++ b/servers/stripe/src/tools/balance.ts @@ -1,2 +1,76 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { BalanceTransaction } from '../types/index.js'; + +export default [ + { + name: 'stripe_get_balance', + description: 'Retrieve the current account balance', + inputSchema: z.object({ + expand: z.array(z.string()).optional().describe('Fields to expand') + }), + handler: async (client: StripeClient, args: any) => { + return await client.get('/balance', args.expand ? { expand: args.expand } : undefined); + } + }, + { + name: 'stripe_list_balance_transactions', + description: 'List all balance transactions (history of changes to account balance)', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of transactions to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination'), + available_on: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by available_on timestamp or range'), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by creation timestamp or range'), + currency: z.string().optional().describe('Filter by currency (e.g., "usd")'), + payout: z.string().optional().describe('Filter by payout ID'), + source: z.string().optional().describe('Filter by source ID (charge, refund, etc.)'), + type: z.enum([ + 'charge', + 'refund', + 'adjustment', + 'application_fee', + 'application_fee_refund', + 'transfer', + 'payment', + 'payout', + 'payout_cancel', + 'payout_failure', + 'stripe_fee', + 'tax' + ]).optional().describe('Filter by transaction type') + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/balance_transactions', args); + } + }, + { + name: 'stripe_get_balance_transaction', + description: 'Retrieve a specific balance transaction by ID', + inputSchema: z.object({ + transaction_id: z.string().describe('The ID of the balance transaction to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand (e.g., ["source"])') + }), + handler: async (client: StripeClient, args: any) => { + const { transaction_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/balance_transactions', transaction_id, params); + } + } +]; diff --git a/servers/stripe/src/tools/charges.ts b/servers/stripe/src/tools/charges.ts index 0c8177e..42d454d 100644 --- a/servers/stripe/src/tools/charges.ts +++ b/servers/stripe/src/tools/charges.ts @@ -1,2 +1,113 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { Charge } from '../types/index.js'; + +const metadataSchema = z.record(z.string()).optional().describe('Set of key-value pairs for storing additional information'); + +export default [ + { + name: 'stripe_list_charges', + description: 'List all charges with optional filtering and pagination', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of charges to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination'), + customer: z.string().optional().describe('Filter by customer ID'), + payment_intent: z.string().optional().describe('Filter by payment intent ID'), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by creation timestamp or range'), + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/charges', args); + } + }, + { + name: 'stripe_get_charge', + description: 'Retrieve a specific charge by ID', + inputSchema: z.object({ + charge_id: z.string().describe('The ID of the charge to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand (e.g., ["customer", "invoice"])') + }), + handler: async (client: StripeClient, args: any) => { + const { charge_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/charges', charge_id, params); + } + }, + { + name: 'stripe_create_charge', + description: 'Create a new charge (direct charge, not recommended - use Payment Intents instead)', + inputSchema: z.object({ + amount: z.number().positive().describe('Amount in cents (e.g., 1000 = $10.00)'), + currency: z.string().describe('Three-letter ISO currency code (e.g., "usd")'), + source: z.string().optional().describe('Payment source ID (card token, source, or payment method)'), + customer: z.string().optional().describe('Customer ID'), + description: z.string().optional().describe('Arbitrary description'), + metadata: metadataSchema, + statement_descriptor: z.string().max(22).optional().describe('Statement descriptor (max 22 chars)'), + statement_descriptor_suffix: z.string().max(22).optional().describe('Suffix for statement descriptor'), + receipt_email: z.string().optional().describe('Email to send receipt to'), + capture: z.boolean().optional().describe('Whether to capture immediately (default true)'), + on_behalf_of: z.string().optional().describe('Connected account ID'), + application_fee_amount: z.number().optional().describe('Application fee in cents') + }), + handler: async (client: StripeClient, args: any) => { + return await client.create('/charges', args, { + idempotencyKey: client.generateIdempotencyKey() + }); + } + }, + { + name: 'stripe_update_charge', + description: 'Update a charge (limited fields can be updated)', + inputSchema: z.object({ + charge_id: z.string().describe('The ID of the charge to update'), + description: z.string().optional(), + metadata: metadataSchema, + receipt_email: z.string().optional(), + fraud_details: z.object({ + user_report: z.enum(['fraudulent', 'safe']).optional() + }).optional().describe('Fraud details'), + shipping: z.object({ + name: z.string(), + address: z.object({ + line1: z.string().optional(), + line2: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + postal_code: z.string().optional(), + country: z.string().optional() + }), + carrier: z.string().optional(), + phone: z.string().optional(), + tracking_number: z.string().optional() + }).optional() + }), + handler: async (client: StripeClient, args: any) => { + const { charge_id, ...data } = args; + return await client.update('/charges', charge_id, data); + } + }, + { + name: 'stripe_capture_charge', + description: 'Capture a previously uncaptured charge', + inputSchema: z.object({ + charge_id: z.string().describe('The ID of the charge to capture'), + amount: z.number().optional().describe('Amount to capture in cents (defaults to full authorized amount)'), + receipt_email: z.string().optional().describe('Email to send receipt to'), + statement_descriptor: z.string().max(22).optional(), + statement_descriptor_suffix: z.string().max(22).optional() + }), + handler: async (client: StripeClient, args: any) => { + const { charge_id, ...data } = args; + return await client.post(`/charges/${charge_id}/capture`, data); + } + } +]; diff --git a/servers/stripe/src/tools/customers.ts b/servers/stripe/src/tools/customers.ts index 0c8177e..0e7fe8d 100644 --- a/servers/stripe/src/tools/customers.ts +++ b/servers/stripe/src/tools/customers.ts @@ -1,2 +1,149 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { Customer, StripeList } from '../types/index.js'; + +const metadataSchema = z.record(z.string()).optional().describe('Set of key-value pairs for storing additional information'); + +export default [ + { + name: 'stripe_list_customers', + description: 'List all customers with optional filtering and pagination', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of customers to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination - ID of the last customer from previous page'), + ending_before: z.string().optional().describe('Cursor for pagination - ID of the first customer from previous page'), + email: z.string().optional().describe('Filter by customer email'), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by creation timestamp or range'), + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/customers', args); + } + }, + { + name: 'stripe_get_customer', + description: 'Retrieve a specific customer by ID', + inputSchema: z.object({ + customer_id: z.string().describe('The ID of the customer to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand (e.g., ["default_source", "subscriptions"])') + }), + handler: async (client: StripeClient, args: any) => { + const { customer_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/customers', customer_id, params); + } + }, + { + name: 'stripe_create_customer', + description: 'Create a new customer', + inputSchema: z.object({ + email: z.string().optional().describe('Customer email address'), + name: z.string().optional().describe('Customer full name'), + phone: z.string().optional().describe('Customer phone number'), + description: z.string().optional().describe('Arbitrary description'), + address: z.object({ + line1: z.string().optional(), + line2: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + postal_code: z.string().optional(), + country: z.string().optional() + }).optional().describe('Customer mailing address'), + metadata: metadataSchema, + payment_method: z.string().optional().describe('ID of payment method to attach'), + invoice_settings: z.object({ + default_payment_method: z.string().optional() + }).optional(), + shipping: z.object({ + name: z.string(), + phone: z.string().optional(), + address: z.object({ + line1: z.string().optional(), + line2: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + postal_code: z.string().optional(), + country: z.string().optional() + }) + }).optional().describe('Shipping information'), + balance: z.number().optional().describe('Account balance in cents (can be negative)'), + preferred_locales: z.array(z.string()).optional().describe('Preferred languages (e.g., ["en", "fr"])'), + }), + handler: async (client: StripeClient, args: any) => { + return await client.create('/customers', args, { + idempotencyKey: client.generateIdempotencyKey() + }); + } + }, + { + name: 'stripe_update_customer', + description: 'Update an existing customer', + inputSchema: z.object({ + customer_id: z.string().describe('The ID of the customer to update'), + email: z.string().optional(), + name: z.string().optional(), + phone: z.string().optional(), + description: z.string().optional(), + address: z.object({ + line1: z.string().optional(), + line2: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + postal_code: z.string().optional(), + country: z.string().optional() + }).optional(), + metadata: metadataSchema, + default_source: z.string().optional().describe('ID of payment source to set as default'), + invoice_settings: z.object({ + default_payment_method: z.string().optional() + }).optional(), + shipping: z.object({ + name: z.string(), + phone: z.string().optional(), + address: z.object({ + line1: z.string().optional(), + line2: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + postal_code: z.string().optional(), + country: z.string().optional() + }) + }).optional(), + balance: z.number().optional(), + preferred_locales: z.array(z.string()).optional(), + }), + handler: async (client: StripeClient, args: any) => { + const { customer_id, ...data } = args; + return await client.update('/customers', customer_id, data); + } + }, + { + name: 'stripe_delete_customer', + description: 'Delete a customer permanently', + inputSchema: z.object({ + customer_id: z.string().describe('The ID of the customer to delete') + }), + handler: async (client: StripeClient, args: any) => { + return await client.remove('/customers', args.customer_id); + } + }, + { + name: 'stripe_search_customers', + description: 'Search for customers using Stripe\'s search query syntax', + inputSchema: z.object({ + query: z.string().describe('Search query (e.g., "email:\'customer@example.com\'" or "name:\'John\'")'), + limit: z.number().min(1).max(100).optional().describe('Number of results to return (1-100)'), + page: z.string().optional().describe('Pagination token from previous search') + }), + handler: async (client: StripeClient, args: any) => { + return await client.get>('/customers/search', args); + } + } + ]; diff --git a/servers/stripe/src/tools/disputes.ts b/servers/stripe/src/tools/disputes.ts index 0c8177e..804e101 100644 --- a/servers/stripe/src/tools/disputes.ts +++ b/servers/stripe/src/tools/disputes.ts @@ -1,2 +1,96 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { Dispute } from '../types/index.js'; + +const metadataSchema = z.record(z.string()).optional().describe('Set of key-value pairs for storing additional information'); + +export default [ + { + name: 'stripe_list_disputes', + description: 'List all disputes with optional filtering and pagination', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of disputes to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination'), + charge: z.string().optional().describe('Filter by charge ID'), + payment_intent: z.string().optional().describe('Filter by payment intent ID'), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by creation timestamp or range'), + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/disputes', args); + } + }, + { + name: 'stripe_get_dispute', + description: 'Retrieve a specific dispute by ID', + inputSchema: z.object({ + dispute_id: z.string().describe('The ID of the dispute to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand (e.g., ["charge"])') + }), + handler: async (client: StripeClient, args: any) => { + const { dispute_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/disputes', dispute_id, params); + } + }, + { + name: 'stripe_update_dispute', + description: 'Update a dispute with evidence', + inputSchema: z.object({ + dispute_id: z.string().describe('The ID of the dispute to update'), + evidence: z.object({ + access_activity_log: z.string().optional().describe('Activity logs showing customer usage'), + billing_address: z.string().optional().describe('Billing address'), + cancellation_policy: z.string().optional().describe('Cancellation policy file ID'), + cancellation_policy_disclosure: z.string().optional(), + cancellation_rebuttal: z.string().optional().describe('Explanation of cancellation policy'), + customer_communication: z.string().optional().describe('Communication file ID'), + customer_email_address: z.string().optional(), + customer_name: z.string().optional(), + customer_purchase_ip: z.string().optional().describe('Customer IP at time of purchase'), + customer_signature: z.string().optional().describe('Signature file ID'), + duplicate_charge_documentation: z.string().optional(), + duplicate_charge_explanation: z.string().optional(), + duplicate_charge_id: z.string().optional().describe('ID of the original, non-disputed charge'), + product_description: z.string().optional(), + receipt: z.string().optional().describe('Receipt file ID'), + refund_policy: z.string().optional().describe('Refund policy file ID'), + refund_policy_disclosure: z.string().optional(), + refund_refusal_explanation: z.string().optional(), + service_date: z.string().optional().describe('Date service was provided'), + service_documentation: z.string().optional().describe('Documentation file ID'), + shipping_address: z.string().optional(), + shipping_carrier: z.string().optional(), + shipping_date: z.string().optional(), + shipping_documentation: z.string().optional().describe('Proof of shipping file ID'), + shipping_tracking_number: z.string().optional(), + uncategorized_file: z.string().optional().describe('Additional file ID'), + uncategorized_text: z.string().optional().describe('Additional text evidence') + }).optional().describe('Evidence to support your case'), + metadata: metadataSchema, + submit: z.boolean().optional().describe('Whether to submit the dispute evidence (default false)') + }), + handler: async (client: StripeClient, args: any) => { + const { dispute_id, ...data } = args; + return await client.update('/disputes', dispute_id, data); + } + }, + { + name: 'stripe_close_dispute', + description: 'Close a dispute (accepting the dispute)', + inputSchema: z.object({ + dispute_id: z.string().describe('The ID of the dispute to close') + }), + handler: async (client: StripeClient, args: any) => { + return await client.post(`/disputes/${args.dispute_id}/close`, {}); + } + } +]; diff --git a/servers/stripe/src/tools/events.ts b/servers/stripe/src/tools/events.ts index 0c8177e..156418b 100644 --- a/servers/stripe/src/tools/events.ts +++ b/servers/stripe/src/tools/events.ts @@ -1,2 +1,43 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { Event } from '../types/index.js'; + +export default [ + { + name: 'stripe_list_events', + description: 'List all events (webhook events that occurred in the account)', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of events to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination'), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by creation timestamp or range'), + delivery_success: z.boolean().optional().describe('Filter by successful webhook delivery'), + type: z.string().optional().describe('Filter by event type (e.g., "charge.succeeded")'), + types: z.array(z.string()).optional().describe('Filter by multiple event types') + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/events', args); + } + }, + { + name: 'stripe_get_event', + description: 'Retrieve a specific event by ID', + inputSchema: z.object({ + event_id: z.string().describe('The ID of the event to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand') + }), + handler: async (client: StripeClient, args: any) => { + const { event_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/events', event_id, params); + } + } +]; diff --git a/servers/stripe/src/tools/invoices.ts b/servers/stripe/src/tools/invoices.ts index 0c8177e..f842a4e 100644 --- a/servers/stripe/src/tools/invoices.ts +++ b/servers/stripe/src/tools/invoices.ts @@ -1,2 +1,229 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { Invoice, InvoiceItem } from '../types/index.js'; + +const metadataSchema = z.record(z.string()).optional().describe('Set of key-value pairs for storing additional information'); + +export default [ + { + name: 'stripe_list_invoices', + description: 'List all invoices with optional filtering and pagination', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of invoices to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination'), + customer: z.string().optional().describe('Filter by customer ID'), + subscription: z.string().optional().describe('Filter by subscription ID'), + status: z.enum(['draft', 'open', 'paid', 'uncollectible', 'void']).optional().describe('Filter by status'), + collection_method: z.enum(['charge_automatically', 'send_invoice']).optional(), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by creation timestamp or range'), + due_date: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional() + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/invoices', args); + } + }, + { + name: 'stripe_get_invoice', + description: 'Retrieve a specific invoice by ID', + inputSchema: z.object({ + invoice_id: z.string().describe('The ID of the invoice to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand (e.g., ["customer", "subscription", "charge"])') + }), + handler: async (client: StripeClient, args: any) => { + const { invoice_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/invoices', invoice_id, params); + } + }, + { + name: 'stripe_create_invoice', + description: 'Create a new draft invoice', + inputSchema: z.object({ + customer: z.string().describe('Customer ID'), + auto_advance: z.boolean().optional().describe('Auto-finalize after an hour (default true)'), + collection_method: z.enum(['charge_automatically', 'send_invoice']).optional().describe('Collection method (default: charge_automatically)'), + description: z.string().optional(), + metadata: metadataSchema, + subscription: z.string().optional().describe('Subscription ID to invoice'), + days_until_due: z.number().optional().describe('Days until due (for send_invoice)'), + due_date: z.number().optional().describe('Unix timestamp for due date'), + default_payment_method: z.string().optional(), + footer: z.string().optional().describe('Footer text'), + statement_descriptor: z.string().optional(), + automatic_tax: z.object({ + enabled: z.boolean() + }).optional(), + custom_fields: z.array(z.object({ + name: z.string(), + value: z.string() + })).optional().describe('Custom fields to display'), + from_invoice: z.object({ + action: z.enum(['revision']), + invoice: z.string() + }).optional().describe('Create from existing invoice'), + on_behalf_of: z.string().optional().describe('Connected account ID'), + payment_settings: z.object({ + payment_method_types: z.array(z.string()).optional() + }).optional(), + rendering_options: z.object({ + amount_tax_display: z.enum(['include_inclusive_tax', 'exclude_inclusive_tax']).optional() + }).optional() + }), + handler: async (client: StripeClient, args: any) => { + return await client.create('/invoices', args, { + idempotencyKey: client.generateIdempotencyKey() + }); + } + }, + { + name: 'stripe_update_invoice', + description: 'Update a draft invoice', + inputSchema: z.object({ + invoice_id: z.string().describe('The ID of the invoice to update'), + auto_advance: z.boolean().optional(), + collection_method: z.enum(['charge_automatically', 'send_invoice']).optional(), + description: z.string().optional(), + metadata: metadataSchema, + days_until_due: z.number().optional(), + due_date: z.number().optional(), + default_payment_method: z.string().optional(), + footer: z.string().optional(), + statement_descriptor: z.string().optional(), + custom_fields: z.array(z.object({ + name: z.string(), + value: z.string() + })).optional(), + payment_settings: z.object({ + payment_method_types: z.array(z.string()).optional() + }).optional() + }), + handler: async (client: StripeClient, args: any) => { + const { invoice_id, ...data } = args; + return await client.update('/invoices', invoice_id, data); + } + }, + { + name: 'stripe_finalize_invoice', + description: 'Finalize a draft invoice', + inputSchema: z.object({ + invoice_id: z.string().describe('The ID of the invoice to finalize'), + auto_advance: z.boolean().optional().describe('Automatically charge or send') + }), + handler: async (client: StripeClient, args: any) => { + const { invoice_id, ...data } = args; + return await client.post(`/invoices/${invoice_id}/finalize`, data); + } + }, + { + name: 'stripe_pay_invoice', + description: 'Pay an invoice manually', + inputSchema: z.object({ + invoice_id: z.string().describe('The ID of the invoice to pay'), + payment_method: z.string().optional().describe('Payment method ID to use'), + source: z.string().optional().describe('Payment source ID'), + off_session: z.boolean().optional().describe('Whether payment is off-session'), + paid_out_of_band: z.boolean().optional().describe('Mark as paid outside Stripe') + }), + handler: async (client: StripeClient, args: any) => { + const { invoice_id, ...data } = args; + return await client.post(`/invoices/${invoice_id}/pay`, data); + } + }, + { + name: 'stripe_void_invoice', + description: 'Void an invoice (mark as uncollectible)', + inputSchema: z.object({ + invoice_id: z.string().describe('The ID of the invoice to void') + }), + handler: async (client: StripeClient, args: any) => { + return await client.post(`/invoices/${args.invoice_id}/void`, {}); + } + }, + { + name: 'stripe_send_invoice', + description: 'Send an invoice to the customer', + inputSchema: z.object({ + invoice_id: z.string().describe('The ID of the invoice to send') + }), + handler: async (client: StripeClient, args: any) => { + return await client.post(`/invoices/${args.invoice_id}/send`, {}); + } + }, + { + name: 'stripe_list_invoice_items', + description: 'List invoice items (line items that will be added to next invoice)', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of items to return (1-100)'), + starting_after: z.string().optional(), + ending_before: z.string().optional(), + customer: z.string().optional().describe('Filter by customer ID'), + invoice: z.string().optional().describe('Filter by invoice ID'), + pending: z.boolean().optional().describe('Only pending items (not yet on invoice)'), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional() + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/invoiceitems', args); + } + }, + { + name: 'stripe_create_invoice_item', + description: 'Create an invoice item (one-time charge to be added to next invoice)', + inputSchema: z.object({ + customer: z.string().describe('Customer ID'), + amount: z.number().describe('Amount in cents (can be negative for discounts)'), + currency: z.string().describe('Three-letter ISO currency code'), + description: z.string().optional(), + invoice: z.string().optional().describe('Invoice ID to add to (if not specified, added to next invoice)'), + metadata: metadataSchema, + price: z.string().optional().describe('Price ID to use'), + quantity: z.number().optional().describe('Quantity (default 1)'), + subscription: z.string().optional().describe('Subscription ID'), + tax_rates: z.array(z.string()).optional().describe('Tax rate IDs'), + unit_amount: z.number().optional().describe('Unit amount in cents (use with quantity)'), + period: z.object({ + start: z.number(), + end: z.number() + }).optional().describe('Period for the item') + }), + handler: async (client: StripeClient, args: any) => { + return await client.create('/invoiceitems', args, { + idempotencyKey: client.generateIdempotencyKey() + }); + } + }, + { + name: 'stripe_delete_invoice_item', + description: 'Delete an invoice item', + inputSchema: z.object({ + invoice_item_id: z.string().describe('The ID of the invoice item to delete') + }), + handler: async (client: StripeClient, args: any) => { + return await client.remove('/invoiceitems', args.invoice_item_id); + } + } +]; diff --git a/servers/stripe/src/tools/payment-intents.ts b/servers/stripe/src/tools/payment-intents.ts index 0c8177e..431e283 100644 --- a/servers/stripe/src/tools/payment-intents.ts +++ b/servers/stripe/src/tools/payment-intents.ts @@ -1,2 +1,138 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { PaymentIntent } from '../types/index.js'; + +const metadataSchema = z.record(z.string()).optional().describe('Set of key-value pairs for storing additional information'); + +export default [ + { + name: 'stripe_list_payment_intents', + description: 'List all payment intents with optional filtering and pagination', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of payment intents to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination'), + customer: z.string().optional().describe('Filter by customer ID'), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by creation timestamp or range'), + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/payment_intents', args); + } + }, + { + name: 'stripe_get_payment_intent', + description: 'Retrieve a specific payment intent by ID', + inputSchema: z.object({ + payment_intent_id: z.string().describe('The ID of the payment intent to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand (e.g., ["customer", "payment_method"])') + }), + handler: async (client: StripeClient, args: any) => { + const { payment_intent_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/payment_intents', payment_intent_id, params); + } + }, + { + name: 'stripe_create_payment_intent', + description: 'Create a new payment intent', + inputSchema: z.object({ + amount: z.number().positive().describe('Amount in cents (e.g., 1000 = $10.00)'), + currency: z.string().describe('Three-letter ISO currency code (e.g., "usd")'), + customer: z.string().optional().describe('Customer ID'), + payment_method: z.string().optional().describe('Payment method ID'), + payment_method_types: z.array(z.string()).optional().describe('Payment method types to accept (e.g., ["card"])'), + description: z.string().optional().describe('Arbitrary description'), + metadata: metadataSchema, + statement_descriptor: z.string().max(22).optional().describe('Statement descriptor (max 22 chars)'), + statement_descriptor_suffix: z.string().max(22).optional(), + receipt_email: z.string().optional().describe('Email to send receipt to'), + capture_method: z.enum(['automatic', 'manual']).optional().describe('When to capture funds (default: automatic)'), + confirmation_method: z.enum(['automatic', 'manual']).optional().describe('How to confirm (default: automatic)'), + confirm: z.boolean().optional().describe('Whether to confirm immediately'), + setup_future_usage: z.enum(['on_session', 'off_session']).optional().describe('Save for future use'), + automatic_payment_methods: z.object({ + enabled: z.boolean() + }).optional().describe('Enable automatic payment methods'), + on_behalf_of: z.string().optional().describe('Connected account ID'), + application_fee_amount: z.number().optional().describe('Application fee in cents'), + transfer_data: z.object({ + destination: z.string(), + amount: z.number().optional() + }).optional().describe('Transfer to connected account'), + return_url: z.string().optional().describe('URL to redirect customer after payment (for certain payment methods)') + }), + handler: async (client: StripeClient, args: any) => { + return await client.create('/payment_intents', args, { + idempotencyKey: client.generateIdempotencyKey() + }); + } + }, + { + name: 'stripe_update_payment_intent', + description: 'Update a payment intent', + inputSchema: z.object({ + payment_intent_id: z.string().describe('The ID of the payment intent to update'), + amount: z.number().positive().optional().describe('Amount in cents'), + currency: z.string().optional(), + customer: z.string().optional(), + payment_method: z.string().optional(), + description: z.string().optional(), + metadata: metadataSchema, + statement_descriptor: z.string().max(22).optional(), + statement_descriptor_suffix: z.string().max(22).optional(), + receipt_email: z.string().optional(), + setup_future_usage: z.enum(['on_session', 'off_session']).optional(), + payment_method_types: z.array(z.string()).optional() + }), + handler: async (client: StripeClient, args: any) => { + const { payment_intent_id, ...data } = args; + return await client.update('/payment_intents', payment_intent_id, data); + } + }, + { + name: 'stripe_confirm_payment_intent', + description: 'Confirm a payment intent', + inputSchema: z.object({ + payment_intent_id: z.string().describe('The ID of the payment intent to confirm'), + payment_method: z.string().optional().describe('Payment method ID to use'), + return_url: z.string().optional().describe('URL to redirect customer after payment'), + receipt_email: z.string().optional() + }), + handler: async (client: StripeClient, args: any) => { + const { payment_intent_id, ...data } = args; + return await client.post(`/payment_intents/${payment_intent_id}/confirm`, data); + } + }, + { + name: 'stripe_cancel_payment_intent', + description: 'Cancel a payment intent', + inputSchema: z.object({ + payment_intent_id: z.string().describe('The ID of the payment intent to cancel'), + cancellation_reason: z.enum(['duplicate', 'fraudulent', 'requested_by_customer', 'abandoned']).optional() + }), + handler: async (client: StripeClient, args: any) => { + const { payment_intent_id, ...data } = args; + return await client.post(`/payment_intents/${payment_intent_id}/cancel`, data); + } + }, + { + name: 'stripe_capture_payment_intent', + description: 'Capture a payment intent (for manual capture)', + inputSchema: z.object({ + payment_intent_id: z.string().describe('The ID of the payment intent to capture'), + amount_to_capture: z.number().optional().describe('Amount to capture in cents (defaults to full authorized amount)') + }), + handler: async (client: StripeClient, args: any) => { + const { payment_intent_id, ...data } = args; + return await client.post(`/payment_intents/${payment_intent_id}/capture`, data); + } + } +]; diff --git a/servers/stripe/src/tools/payment-methods.ts b/servers/stripe/src/tools/payment-methods.ts index 0c8177e..cdc3d8a 100644 --- a/servers/stripe/src/tools/payment-methods.ts +++ b/servers/stripe/src/tools/payment-methods.ts @@ -1,2 +1,119 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { PaymentMethod } from '../types/index.js'; + +const metadataSchema = z.record(z.string()).optional().describe('Set of key-value pairs for storing additional information'); + +export default [ + { + name: 'stripe_list_payment_methods', + description: 'List payment methods for a customer', + inputSchema: z.object({ + customer: z.string().describe('Customer ID'), + type: z.enum(['card', 'us_bank_account', 'sepa_debit', 'link']).optional().describe('Payment method type to filter by'), + limit: z.number().min(1).max(100).optional().describe('Number of payment methods to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination'), + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/payment_methods', args); + } + }, + { + name: 'stripe_get_payment_method', + description: 'Retrieve a specific payment method by ID', + inputSchema: z.object({ + payment_method_id: z.string().describe('The ID of the payment method to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand (e.g., ["customer"])') + }), + handler: async (client: StripeClient, args: any) => { + const { payment_method_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/payment_methods', payment_method_id, params); + } + }, + { + name: 'stripe_create_payment_method', + description: 'Create a new payment method', + inputSchema: z.object({ + type: z.enum(['card', 'us_bank_account', 'sepa_debit']).describe('Payment method type'), + card: z.object({ + number: z.string().optional().describe('Card number'), + exp_month: z.number().min(1).max(12).optional().describe('Expiration month'), + exp_year: z.number().optional().describe('Expiration year'), + cvc: z.string().optional().describe('Card CVC'), + token: z.string().optional().describe('Card token from Stripe.js') + }).optional().describe('Card details (use token in production)'), + billing_details: z.object({ + name: z.string().optional(), + email: z.string().optional(), + phone: z.string().optional(), + address: z.object({ + line1: z.string().optional(), + line2: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + postal_code: z.string().optional(), + country: z.string().optional() + }).optional() + }).optional().describe('Billing details'), + metadata: metadataSchema + }), + handler: async (client: StripeClient, args: any) => { + return await client.create('/payment_methods', args, { + idempotencyKey: client.generateIdempotencyKey() + }); + } + }, + { + name: 'stripe_update_payment_method', + description: 'Update a payment method', + inputSchema: z.object({ + payment_method_id: z.string().describe('The ID of the payment method to update'), + billing_details: z.object({ + name: z.string().optional(), + email: z.string().optional(), + phone: z.string().optional(), + address: z.object({ + line1: z.string().optional(), + line2: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + postal_code: z.string().optional(), + country: z.string().optional() + }).optional() + }).optional(), + card: z.object({ + exp_month: z.number().min(1).max(12).optional(), + exp_year: z.number().optional() + }).optional().describe('Update card expiration'), + metadata: metadataSchema + }), + handler: async (client: StripeClient, args: any) => { + const { payment_method_id, ...data } = args; + return await client.update('/payment_methods', payment_method_id, data); + } + }, + { + name: 'stripe_attach_payment_method', + description: 'Attach a payment method to a customer', + inputSchema: z.object({ + payment_method_id: z.string().describe('The ID of the payment method to attach'), + customer: z.string().describe('Customer ID to attach to') + }), + handler: async (client: StripeClient, args: any) => { + const { payment_method_id, customer } = args; + return await client.post(`/payment_methods/${payment_method_id}/attach`, { customer }); + } + }, + { + name: 'stripe_detach_payment_method', + description: 'Detach a payment method from a customer', + inputSchema: z.object({ + payment_method_id: z.string().describe('The ID of the payment method to detach') + }), + handler: async (client: StripeClient, args: any) => { + return await client.post(`/payment_methods/${args.payment_method_id}/detach`, {}); + } + } +]; diff --git a/servers/stripe/src/tools/payouts.ts b/servers/stripe/src/tools/payouts.ts index 0c8177e..dee5904 100644 --- a/servers/stripe/src/tools/payouts.ts +++ b/servers/stripe/src/tools/payouts.ts @@ -1,2 +1,106 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { Payout } from '../types/index.js'; + +const metadataSchema = z.record(z.string()).optional().describe('Set of key-value pairs for storing additional information'); + +export default [ + { + name: 'stripe_list_payouts', + description: 'List all payouts with optional filtering and pagination', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of payouts to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination'), + arrival_date: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by arrival date timestamp or range'), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by creation timestamp or range'), + destination: z.string().optional().describe('Filter by destination bank account ID'), + status: z.enum(['paid', 'pending', 'in_transit', 'canceled', 'failed']).optional().describe('Filter by status') + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/payouts', args); + } + }, + { + name: 'stripe_get_payout', + description: 'Retrieve a specific payout by ID', + inputSchema: z.object({ + payout_id: z.string().describe('The ID of the payout to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand (e.g., ["destination"])') + }), + handler: async (client: StripeClient, args: any) => { + const { payout_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/payouts', payout_id, params); + } + }, + { + name: 'stripe_create_payout', + description: 'Create a manual payout', + inputSchema: z.object({ + amount: z.number().positive().describe('Amount in cents to pay out'), + currency: z.string().describe('Three-letter ISO currency code (e.g., "usd")'), + description: z.string().optional().describe('Arbitrary description'), + metadata: metadataSchema, + destination: z.string().optional().describe('Bank account ID (defaults to default bank account)'), + method: z.enum(['standard', 'instant']).optional().describe('Payout method (default: standard)'), + source_type: z.enum(['card', 'bank_account', 'fpx']).optional().describe('Source balance type'), + statement_descriptor: z.string().optional().describe('Statement descriptor') + }), + handler: async (client: StripeClient, args: any) => { + return await client.create('/payouts', args, { + idempotencyKey: client.generateIdempotencyKey() + }); + } + }, + { + name: 'stripe_update_payout', + description: 'Update a payout (limited fields can be updated)', + inputSchema: z.object({ + payout_id: z.string().describe('The ID of the payout to update'), + metadata: metadataSchema + }), + handler: async (client: StripeClient, args: any) => { + const { payout_id, ...data } = args; + return await client.update('/payouts', payout_id, data); + } + }, + { + name: 'stripe_cancel_payout', + description: 'Cancel a pending payout', + inputSchema: z.object({ + payout_id: z.string().describe('The ID of the payout to cancel') + }), + handler: async (client: StripeClient, args: any) => { + return await client.post(`/payouts/${args.payout_id}/cancel`, {}); + } + }, + { + name: 'stripe_reverse_payout', + description: 'Reverse a payout (creates a new payout that reverses the original)', + inputSchema: z.object({ + payout_id: z.string().describe('The ID of the payout to reverse'), + metadata: metadataSchema + }), + handler: async (client: StripeClient, args: any) => { + const { payout_id, ...data } = args; + return await client.post(`/payouts/${payout_id}/reverse`, data); + } + } +]; diff --git a/servers/stripe/src/tools/prices.ts b/servers/stripe/src/tools/prices.ts index 0c8177e..11cad5f 100644 --- a/servers/stripe/src/tools/prices.ts +++ b/servers/stripe/src/tools/prices.ts @@ -1,2 +1,114 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { Price } from '../types/index.js'; + +const metadataSchema = z.record(z.string()).optional().describe('Set of key-value pairs for storing additional information'); + +export default [ + { + name: 'stripe_list_prices', + description: 'List all prices with optional filtering and pagination', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of prices to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination'), + active: z.boolean().optional().describe('Filter by active status'), + currency: z.string().optional().describe('Filter by currency (e.g., "usd")'), + product: z.string().optional().describe('Filter by product ID'), + type: z.enum(['one_time', 'recurring']).optional().describe('Filter by price type'), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by creation timestamp or range'), + recurring: z.object({ + interval: z.enum(['day', 'week', 'month', 'year']).optional(), + usage_type: z.enum(['metered', 'licensed']).optional() + }).optional().describe('Filter recurring prices') + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/prices', args); + } + }, + { + name: 'stripe_get_price', + description: 'Retrieve a specific price by ID', + inputSchema: z.object({ + price_id: z.string().describe('The ID of the price to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand (e.g., ["product"])') + }), + handler: async (client: StripeClient, args: any) => { + const { price_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/prices', price_id, params); + } + }, + { + name: 'stripe_create_price', + description: 'Create a new price', + inputSchema: z.object({ + currency: z.string().describe('Three-letter ISO currency code (e.g., "usd")'), + product: z.string().optional().describe('Product ID (or use product_data to create inline)'), + product_data: z.object({ + name: z.string(), + active: z.boolean().optional(), + metadata: metadataSchema + }).optional().describe('Create product inline'), + unit_amount: z.number().optional().describe('Amount in cents (e.g., 1000 = $10.00). Omit for custom prices.'), + unit_amount_decimal: z.string().optional().describe('Decimal string for sub-cent precision'), + active: z.boolean().optional().describe('Whether price is active (default true)'), + metadata: metadataSchema, + nickname: z.string().optional().describe('Brief description of the price'), + recurring: z.object({ + interval: z.enum(['day', 'week', 'month', 'year']), + interval_count: z.number().optional().describe('Number of intervals (default 1)'), + aggregate_usage: z.enum(['sum', 'last_during_period', 'last_ever', 'max']).optional().describe('For metered usage'), + usage_type: z.enum(['metered', 'licensed']).optional().describe('Default: licensed'), + trial_period_days: z.number().optional().describe('Trial days for subscriptions') + }).optional().describe('Recurring price configuration'), + billing_scheme: z.enum(['per_unit', 'tiered']).optional().describe('Default: per_unit'), + tiers: z.array(z.object({ + up_to: z.union([z.number(), z.literal('inf')]), + unit_amount: z.number().optional(), + unit_amount_decimal: z.string().optional(), + flat_amount: z.number().optional(), + flat_amount_decimal: z.string().optional() + })).optional().describe('Tiers for tiered billing'), + tiers_mode: z.enum(['graduated', 'volume']).optional().describe('For tiered billing'), + tax_behavior: z.enum(['inclusive', 'exclusive', 'unspecified']).optional().describe('Tax calculation'), + custom_unit_amount: z.object({ + enabled: z.boolean(), + minimum: z.number().optional(), + maximum: z.number().optional(), + preset: z.number().optional() + }).optional().describe('Customer chooses price'), + lookup_key: z.string().optional().describe('Lookup key for price'), + transfer_lookup_key: z.boolean().optional().describe('Transfer lookup_key from product') + }), + handler: async (client: StripeClient, args: any) => { + return await client.create('/prices', args, { + idempotencyKey: client.generateIdempotencyKey() + }); + } + }, + { + name: 'stripe_update_price', + description: 'Update a price (limited fields can be updated)', + inputSchema: z.object({ + price_id: z.string().describe('The ID of the price to update'), + active: z.boolean().optional().describe('Activate or deactivate price'), + metadata: metadataSchema, + nickname: z.string().optional(), + lookup_key: z.string().optional(), + tax_behavior: z.enum(['inclusive', 'exclusive', 'unspecified']).optional() + }), + handler: async (client: StripeClient, args: any) => { + const { price_id, ...data } = args; + return await client.update('/prices', price_id, data); + } + } +]; diff --git a/servers/stripe/src/tools/products.ts b/servers/stripe/src/tools/products.ts index 0c8177e..c5e8b7f 100644 --- a/servers/stripe/src/tools/products.ts +++ b/servers/stripe/src/tools/products.ts @@ -1,2 +1,133 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { Product, StripeList } from '../types/index.js'; + +const metadataSchema = z.record(z.string()).optional().describe('Set of key-value pairs for storing additional information'); + +export default [ + { + name: 'stripe_list_products', + description: 'List all products with optional filtering and pagination', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of products to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination'), + active: z.boolean().optional().describe('Filter by active status'), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by creation timestamp or range'), + ids: z.array(z.string()).optional().describe('Filter by product IDs'), + shippable: z.boolean().optional().describe('Filter by shippable status'), + type: z.enum(['service', 'good']).optional().describe('Filter by product type'), + url: z.string().optional().describe('Filter by product URL') + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/products', args); + } + }, + { + name: 'stripe_get_product', + description: 'Retrieve a specific product by ID', + inputSchema: z.object({ + product_id: z.string().describe('The ID of the product to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand (e.g., ["default_price"])') + }), + handler: async (client: StripeClient, args: any) => { + const { product_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/products', product_id, params); + } + }, + { + name: 'stripe_create_product', + description: 'Create a new product', + inputSchema: z.object({ + name: z.string().describe('Product name'), + active: z.boolean().optional().describe('Whether product is active (default true)'), + description: z.string().optional().describe('Product description'), + metadata: metadataSchema, + default_price_data: z.object({ + currency: z.string(), + unit_amount: z.number().optional().describe('Amount in cents'), + recurring: z.object({ + interval: z.enum(['day', 'week', 'month', 'year']), + interval_count: z.number().optional() + }).optional() + }).optional().describe('Create a default price inline'), + images: z.array(z.string()).optional().describe('Array of image URLs (max 8)'), + package_dimensions: z.object({ + height: z.number(), + length: z.number(), + weight: z.number(), + width: z.number() + }).optional().describe('Package dimensions for shipping'), + shippable: z.boolean().optional().describe('Whether product can be shipped'), + statement_descriptor: z.string().max(22).optional().describe('Statement descriptor'), + tax_code: z.string().optional().describe('Tax code for automatic tax'), + type: z.enum(['service', 'good']).optional().describe('Product type (default: service)'), + unit_label: z.string().optional().describe('Label for units (e.g., "per seat")'), + url: z.string().optional().describe('Product URL') + }), + handler: async (client: StripeClient, args: any) => { + return await client.create('/products', args, { + idempotencyKey: client.generateIdempotencyKey() + }); + } + }, + { + name: 'stripe_update_product', + description: 'Update an existing product', + inputSchema: z.object({ + product_id: z.string().describe('The ID of the product to update'), + name: z.string().optional(), + active: z.boolean().optional(), + description: z.string().optional(), + metadata: metadataSchema, + default_price: z.string().optional().describe('Set new default price ID'), + images: z.array(z.string()).optional(), + package_dimensions: z.object({ + height: z.number(), + length: z.number(), + weight: z.number(), + width: z.number() + }).optional(), + shippable: z.boolean().optional(), + statement_descriptor: z.string().max(22).optional(), + tax_code: z.string().optional(), + unit_label: z.string().optional(), + url: z.string().optional() + }), + handler: async (client: StripeClient, args: any) => { + const { product_id, ...data } = args; + return await client.update('/products', product_id, data); + } + }, + { + name: 'stripe_delete_product', + description: 'Delete a product (permanently removes it)', + inputSchema: z.object({ + product_id: z.string().describe('The ID of the product to delete') + }), + handler: async (client: StripeClient, args: any) => { + return await client.remove('/products', args.product_id); + } + }, + { + name: 'stripe_search_products', + description: 'Search for products using Stripe\'s search query syntax', + inputSchema: z.object({ + query: z.string().describe('Search query (e.g., "active:\'true\' AND name~\'premium\'" or "metadata[\'key\']:\'value\'")'), + limit: z.number().min(1).max(100).optional().describe('Number of results to return (1-100)'), + page: z.string().optional().describe('Pagination token from previous search') + }), + handler: async (client: StripeClient, args: any) => { + return await client.get>('/products/search', args); + } + } +]; diff --git a/servers/stripe/src/tools/refunds.ts b/servers/stripe/src/tools/refunds.ts index 0c8177e..79d5eb0 100644 --- a/servers/stripe/src/tools/refunds.ts +++ b/servers/stripe/src/tools/refunds.ts @@ -1,2 +1,84 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { Refund } from '../types/index.js'; + +const metadataSchema = z.record(z.string()).optional().describe('Set of key-value pairs for storing additional information'); + +export default [ + { + name: 'stripe_list_refunds', + description: 'List all refunds with optional filtering and pagination', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of refunds to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination'), + charge: z.string().optional().describe('Filter by charge ID'), + payment_intent: z.string().optional().describe('Filter by payment intent ID'), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by creation timestamp or range'), + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/refunds', args); + } + }, + { + name: 'stripe_get_refund', + description: 'Retrieve a specific refund by ID', + inputSchema: z.object({ + refund_id: z.string().describe('The ID of the refund to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand (e.g., ["charge"])') + }), + handler: async (client: StripeClient, args: any) => { + const { refund_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/refunds', refund_id, params); + } + }, + { + name: 'stripe_create_refund', + description: 'Create a refund for a charge or payment intent', + inputSchema: z.object({ + charge: z.string().optional().describe('Charge ID to refund (use charge or payment_intent, not both)'), + payment_intent: z.string().optional().describe('Payment intent ID to refund'), + amount: z.number().optional().describe('Amount to refund in cents (defaults to full charge amount)'), + reason: z.enum(['duplicate', 'fraudulent', 'requested_by_customer']).optional().describe('Reason for refund'), + refund_application_fee: z.boolean().optional().describe('Whether to refund the application fee'), + reverse_transfer: z.boolean().optional().describe('Whether to reverse the transfer'), + metadata: metadataSchema + }), + handler: async (client: StripeClient, args: any) => { + return await client.create('/refunds', args, { + idempotencyKey: client.generateIdempotencyKey() + }); + } + }, + { + name: 'stripe_update_refund', + description: 'Update a refund (limited fields can be updated)', + inputSchema: z.object({ + refund_id: z.string().describe('The ID of the refund to update'), + metadata: metadataSchema + }), + handler: async (client: StripeClient, args: any) => { + const { refund_id, ...data } = args; + return await client.update('/refunds', refund_id, data); + } + }, + { + name: 'stripe_cancel_refund', + description: 'Cancel a refund that is in pending status', + inputSchema: z.object({ + refund_id: z.string().describe('The ID of the refund to cancel') + }), + handler: async (client: StripeClient, args: any) => { + return await client.post(`/refunds/${args.refund_id}/cancel`, {}); + } + } +]; diff --git a/servers/stripe/src/tools/subscriptions.ts b/servers/stripe/src/tools/subscriptions.ts index 0c8177e..b7bf4e7 100644 --- a/servers/stripe/src/tools/subscriptions.ts +++ b/servers/stripe/src/tools/subscriptions.ts @@ -1,2 +1,163 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { Subscription, SubscriptionItem } from '../types/index.js'; + +const metadataSchema = z.record(z.string()).optional().describe('Set of key-value pairs for storing additional information'); + +export default [ + { + name: 'stripe_list_subscriptions', + description: 'List all subscriptions with optional filtering and pagination', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of subscriptions to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination'), + customer: z.string().optional().describe('Filter by customer ID'), + price: z.string().optional().describe('Filter by price ID'), + status: z.enum(['incomplete', 'incomplete_expired', 'trialing', 'active', 'past_due', 'canceled', 'unpaid', 'paused']).optional(), + created: z.union([ + z.number(), + z.object({ + gt: z.number().optional(), + gte: z.number().optional(), + lt: z.number().optional(), + lte: z.number().optional(), + }) + ]).optional().describe('Filter by creation timestamp or range'), + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/subscriptions', args); + } + }, + { + name: 'stripe_get_subscription', + description: 'Retrieve a specific subscription by ID', + inputSchema: z.object({ + subscription_id: z.string().describe('The ID of the subscription to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand (e.g., ["customer", "default_payment_method"])') + }), + handler: async (client: StripeClient, args: any) => { + const { subscription_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/subscriptions', subscription_id, params); + } + }, + { + name: 'stripe_create_subscription', + description: 'Create a new subscription', + inputSchema: z.object({ + customer: z.string().describe('Customer ID'), + items: z.array(z.object({ + price: z.string().describe('Price ID'), + quantity: z.number().optional().describe('Quantity (default 1)') + })).describe('Subscription items (products/prices)'), + default_payment_method: z.string().optional().describe('Payment method ID to use'), + collection_method: z.enum(['charge_automatically', 'send_invoice']).optional().describe('How to collect payment (default: charge_automatically)'), + days_until_due: z.number().optional().describe('Days until invoice due (for send_invoice collection)'), + description: z.string().optional(), + metadata: metadataSchema, + cancel_at_period_end: z.boolean().optional().describe('Cancel at the end of the current period'), + trial_period_days: z.number().optional().describe('Number of trial days'), + trial_end: z.number().optional().describe('Unix timestamp for trial end (overrides trial_period_days)'), + backdate_start_date: z.number().optional().describe('Unix timestamp to backdate subscription start'), + billing_cycle_anchor: z.number().optional().describe('Unix timestamp for billing cycle anchor'), + proration_behavior: z.enum(['create_prorations', 'none', 'always_invoice']).optional(), + payment_settings: z.object({ + save_default_payment_method: z.enum(['on_subscription', 'off']).optional(), + payment_method_types: z.array(z.string()).optional() + }).optional(), + off_session: z.boolean().optional().describe('Whether payment is happening off-session'), + promotion_code: z.string().optional().describe('Promotion code ID to apply'), + automatic_tax: z.object({ + enabled: z.boolean() + }).optional().describe('Enable automatic tax calculation') + }), + handler: async (client: StripeClient, args: any) => { + return await client.create('/subscriptions', args, { + idempotencyKey: client.generateIdempotencyKey() + }); + } + }, + { + name: 'stripe_update_subscription', + description: 'Update a subscription', + inputSchema: z.object({ + subscription_id: z.string().describe('The ID of the subscription to update'), + items: z.array(z.object({ + id: z.string().optional().describe('Subscription item ID to update'), + price: z.string().optional().describe('New price ID'), + quantity: z.number().optional(), + deleted: z.boolean().optional().describe('Set to true to remove item') + })).optional().describe('Update subscription items'), + default_payment_method: z.string().optional(), + description: z.string().optional(), + metadata: metadataSchema, + cancel_at_period_end: z.boolean().optional(), + trial_end: z.number().optional(), + proration_behavior: z.enum(['create_prorations', 'none', 'always_invoice']).optional(), + billing_cycle_anchor: z.enum(['now', 'unchanged']).optional(), + pause_collection: z.object({ + behavior: z.enum(['keep_as_draft', 'mark_uncollectible', 'void']), + resumes_at: z.number().optional() + }).optional().describe('Pause the subscription'), + promotion_code: z.string().optional(), + payment_settings: z.object({ + save_default_payment_method: z.enum(['on_subscription', 'off']).optional(), + payment_method_types: z.array(z.string()).optional() + }).optional() + }), + handler: async (client: StripeClient, args: any) => { + const { subscription_id, ...data } = args; + return await client.update('/subscriptions', subscription_id, data); + } + }, + { + name: 'stripe_cancel_subscription', + description: 'Cancel a subscription', + inputSchema: z.object({ + subscription_id: z.string().describe('The ID of the subscription to cancel'), + cancel_at_period_end: z.boolean().optional().describe('Cancel at end of period (default false = immediate)'), + prorate: z.boolean().optional().describe('Whether to prorate (default true)'), + invoice_now: z.boolean().optional().describe('Whether to invoice immediately (default false)') + }), + handler: async (client: StripeClient, args: any) => { + const { subscription_id, cancel_at_period_end, ...params } = args; + + if (cancel_at_period_end) { + // Update to cancel at period end + return await client.update('/subscriptions', subscription_id, { cancel_at_period_end: true }); + } else { + // Immediate cancellation + return await client.delete(`/subscriptions/${subscription_id}`, params); + } + } + }, + { + name: 'stripe_resume_subscription', + description: 'Resume a paused subscription', + inputSchema: z.object({ + subscription_id: z.string().describe('The ID of the subscription to resume'), + proration_behavior: z.enum(['create_prorations', 'none', 'always_invoice']).optional() + }), + handler: async (client: StripeClient, args: any) => { + const { subscription_id, ...data } = args; + return await client.update('/subscriptions', subscription_id, { + ...data, + pause_collection: null as any // Remove pause + }); + } + }, + { + name: 'stripe_list_subscription_items', + description: 'List all items for a subscription', + inputSchema: z.object({ + subscription: z.string().describe('Subscription ID'), + limit: z.number().min(1).max(100).optional().describe('Number of items to return (1-100)'), + starting_after: z.string().optional(), + ending_before: z.string().optional() + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/subscription_items', args); + } + } +]; diff --git a/servers/stripe/src/tools/webhooks.ts b/servers/stripe/src/tools/webhooks.ts index 0c8177e..f7d0c27 100644 --- a/servers/stripe/src/tools/webhooks.ts +++ b/servers/stripe/src/tools/webhooks.ts @@ -1,2 +1,76 @@ -// Placeholder - tools to be implemented -export default []; +import { z } from 'zod'; +import { StripeClient } from '../clients/stripe.js'; +import { WebhookEndpoint } from '../types/index.js'; + +const metadataSchema = z.record(z.string()).optional().describe('Set of key-value pairs for storing additional information'); + +export default [ + { + name: 'stripe_list_webhook_endpoints', + description: 'List all webhook endpoints', + inputSchema: z.object({ + limit: z.number().min(1).max(100).optional().describe('Number of webhook endpoints to return (1-100)'), + starting_after: z.string().optional().describe('Cursor for pagination'), + ending_before: z.string().optional().describe('Cursor for pagination') + }), + handler: async (client: StripeClient, args: any) => { + return await client.list('/webhook_endpoints', args); + } + }, + { + name: 'stripe_get_webhook_endpoint', + description: 'Retrieve a specific webhook endpoint by ID', + inputSchema: z.object({ + webhook_endpoint_id: z.string().describe('The ID of the webhook endpoint to retrieve'), + expand: z.array(z.string()).optional().describe('Fields to expand') + }), + handler: async (client: StripeClient, args: any) => { + const { webhook_endpoint_id, expand } = args; + const params = expand ? { expand } : undefined; + return await client.retrieve('/webhook_endpoints', webhook_endpoint_id, params); + } + }, + { + name: 'stripe_create_webhook_endpoint', + description: 'Create a new webhook endpoint', + inputSchema: z.object({ + url: z.string().url().describe('The URL to send webhook events to'), + enabled_events: z.array(z.string()).describe('Array of event types to subscribe to (e.g., ["charge.succeeded", "customer.created"]). Use ["*"] for all events.'), + api_version: z.string().optional().describe('Stripe API version (e.g., "2024-01-18")'), + connect: z.boolean().optional().describe('Whether to receive events from connected accounts'), + description: z.string().optional().describe('Arbitrary description'), + metadata: metadataSchema + }), + handler: async (client: StripeClient, args: any) => { + return await client.create('/webhook_endpoints', args, { + idempotencyKey: client.generateIdempotencyKey() + }); + } + }, + { + name: 'stripe_update_webhook_endpoint', + description: 'Update a webhook endpoint', + inputSchema: z.object({ + webhook_endpoint_id: z.string().describe('The ID of the webhook endpoint to update'), + url: z.string().url().optional().describe('New URL'), + enabled_events: z.array(z.string()).optional().describe('New array of event types'), + disabled: z.boolean().optional().describe('Disable the webhook endpoint'), + description: z.string().optional(), + metadata: metadataSchema + }), + handler: async (client: StripeClient, args: any) => { + const { webhook_endpoint_id, ...data } = args; + return await client.update('/webhook_endpoints', webhook_endpoint_id, data); + } + }, + { + name: 'stripe_delete_webhook_endpoint', + description: 'Delete a webhook endpoint', + inputSchema: z.object({ + webhook_endpoint_id: z.string().describe('The ID of the webhook endpoint to delete') + }), + handler: async (client: StripeClient, args: any) => { + return await client.remove('/webhook_endpoints', args.webhook_endpoint_id); + } + } +];