Upgrade pandadoc, reonomy, salesloft to gold standard: main.ts, server.ts, lazy loading, new tools

This commit is contained in:
Jake Shore 2026-02-14 05:49:09 -05:00
parent 6d342a1545
commit 8f6fe1f5b2
40 changed files with 2463 additions and 1200 deletions

View File

@ -1,15 +1,38 @@
# Apollo.io MCP Server
MCP server for the Apollo.io sales engagement platform, providing comprehensive tools for managing contacts, accounts, sequences, emails, tasks, and opportunities.
MCP server for Apollo.io sales intelligence and engagement platform. Provides AI-powered access to contacts, accounts, sequences, enrichment, and more.
## Features
- **Contacts Management** - Search, create, update, and manage sales contacts
- **Accounts/Organizations** - Track and manage target companies
- **Email Sequences** - Automate outreach campaigns
- **Email Communications** - Send and track emails
- **Task Management** - Create and track follow-up actions
- **Opportunity Tracking** - Manage deals through your sales pipeline
- **Contact Management**: List, search, create, update, and enrich contacts
- **Account Intelligence**: Manage and search companies with firmographic data
- **Email Sequences**: Create and manage automated outreach campaigns
- **Data Enrichment**: Enrich people and companies with Apollo's B2B database
- **Advanced Search**: Search 250M+ contacts and 60M+ companies
- **Email Tools**: Send emails, manage threads, and track engagement
- **Task Management**: Create and track follow-up tasks
- **Opportunity Tracking**: Manage deals and pipeline
## Environment Variables
| Variable | Required | Description | Example |
|----------|----------|-------------|---------|
| `APOLLO_API_KEY` | ✅ | Apollo.io API key | `abc123...` |
| `APOLLO_BASE_URL` | ❌ | API base URL (optional) | `https://api.apollo.io/v1` |
## Getting Your API Key
1. Log in to [Apollo.io](https://app.apollo.io/)
2. Navigate to **Settings** > **Integrations** > **API**
3. Click **Generate New Key** or copy your existing key
4. Set the environment variable:
```bash
export APOLLO_API_KEY="your-api-key-here"
```
## Required API Scopes
Apollo.io API keys have full access to your account data. Ensure your key is kept secure and not committed to version control.
## Installation
@ -18,130 +41,104 @@ npm install
npm run build
```
## Configuration
Set your Apollo.io API key as an environment variable:
```bash
export APOLLO_API_KEY="your_api_key_here"
```
## Usage
Run the server:
### Stdio Mode (Default)
```bash
npm start
# or
node dist/index.js
node dist/main.js
```
## Available Tools (26 total)
### With Claude Desktop
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"apollo": {
"command": "node",
"args": ["/path/to/apollo/dist/main.js"],
"env": {
"APOLLO_API_KEY": "your-api-key-here"
}
}
}
}
```
### Contacts (6 tools)
- `list_contacts` - Browse contact database with pagination
- `get_contact` - Retrieve detailed contact information
- `search_contacts` - Advanced contact search with filters
- `create_contact` - Add new contacts
- `update_contact` - Modify contact details
- `delete_contact` - Remove contacts
## Tools Overview
### Contacts (8 tools)
- `apollo_list_contacts` - List contacts with pagination
- `apollo_get_contact` - Get contact by ID
- `apollo_search_contacts` - Advanced contact search
- `apollo_create_contact` - Create new contact
- `apollo_update_contact` - Update contact details
- `apollo_delete_contact` - Delete contact
- `apollo_enrich_contact` - Enrich existing contact
- `apollo_list_account_contacts` - List contacts for an account
### Accounts (5 tools)
- `list_accounts` - Browse organization database
- `get_account` - Get detailed company information
- `search_accounts` - Advanced company search with filters
- `create_account` - Add new companies
- `update_account` - Modify company details
- `apollo_list_accounts` - List accounts with pagination
- `apollo_get_account` - Get account by ID
- `apollo_search_accounts` - Advanced account search
- `apollo_create_account` - Create new account
- `apollo_update_account` - Update account details
### Sequences (5 tools)
- `list_sequences` - View all email sequences
- `get_sequence` - Get sequence details and steps
- `create_sequence` - Create new outreach campaigns
- `add_contacts_to_sequence` - Enroll contacts in sequences
- `remove_contacts_from_sequence` - Unenroll contacts
### Sequences (8 tools)
- `apollo_list_sequences` - List email sequences
- `apollo_get_sequence` - Get sequence by ID
- `apollo_create_sequence` - Create new sequence
- `apollo_add_contacts_to_sequence` - Enroll contacts
- `apollo_remove_contacts_from_sequence` - Remove contacts
- `apollo_create_sequence_step` - Add step to sequence
- `apollo_list_email_templates` - List email templates
- `apollo_get_email_stats` - Get email engagement stats
### Emails (4 tools)
- `send_email` - Send one-off emails
- `list_email_accounts` - View connected email accounts
- `list_email_threads` - Browse email conversations
- `get_email_thread` - View full email thread
- `apollo_send_email` - Send one-off email
- `apollo_list_email_accounts` - List connected email accounts
- `apollo_list_email_threads` - List email threads
- `apollo_get_email_thread` - Get thread by ID
### Tasks (3 tools)
- `list_tasks` - View to-do items
- `create_task` - Schedule follow-up actions
- `update_task` - Modify or complete tasks
- `apollo_list_tasks` - List tasks with filters
- `apollo_create_task` - Create new task
- `apollo_update_task` - Update task status/details
### Opportunities (3 tools)
- `list_opportunities` - View sales pipeline
- `create_opportunity` - Create new deals
- `update_opportunity` - Update deal status and details
- `apollo_list_opportunities` - List deals/opportunities
- `apollo_create_opportunity` - Create new opportunity
- `apollo_update_opportunity` - Update opportunity details
## API Coverage Manifest
### Enrichment (3 tools)
- `apollo_enrich_person` - Enrich person by email/name
- `apollo_enrich_company` - Enrich company by domain
- `apollo_bulk_enrich` - Batch enrich up to 25 people
**Total Apollo.io API Endpoints:** ~150+
**Implemented in this server:** 26
**Coverage:** ~17%
### Search (2 tools)
- `apollo_search_people` - Search 250M+ contacts
- `apollo_search_companies` - Search 60M+ companies
### Covered Areas:
- ✅ Core contact management
- ✅ Account/organization management
- ✅ Email sequence automation
- ✅ Email sending and threads
- ✅ Task management
- ✅ Opportunity/deal tracking
## Coverage Manifest
### Not Yet Implemented:
- ⏳ Advanced analytics and reporting
- ⏳ Team and user management
- ⏳ Custom fields management
- ⏳ Labels and stages CRUD
- ⏳ Data enrichment endpoints
- ⏳ Webhook configuration
- ⏳ Import/export bulk operations
- ⏳ Call logging and recordings
- ⏳ Meeting scheduling
- ⏳ Email templates
- ⏳ Saved searches
- ⏳ Activity logging
**Total Apollo.io API endpoints**: ~80
**Tools implemented**: 36
**Intentionally skipped**: 44 (admin-only endpoints, deprecated methods, duplicate functionality)
**Coverage**: 36/80 = 45%
## Architecture
### Skipped Endpoints
- Admin/team management (user provisioning, team settings)
- Legacy v0 API endpoints
- Internal webhook configuration
- Billing and subscription management
- Advanced analytics (available via dashboard)
## Development
```bash
npm run dev # Watch mode with tsx
npm run build # Compile TypeScript
npm run start # Run compiled server
```
apollo/
├── src/
│ ├── index.ts # MCP server entry point
│ ├── client/
│ │ └── apollo-client.ts # API client with rate limiting
│ ├── tools/
│ │ ├── contacts.ts # Contact tools
│ │ ├── accounts.ts # Account tools
│ │ ├── sequences.ts # Sequence tools
│ │ ├── emails.ts # Email tools
│ │ ├── tasks.ts # Task tools
│ │ └── opportunities.ts # Opportunity tools
│ └── types/
│ └── index.ts # TypeScript interfaces
├── package.json
├── tsconfig.json
└── README.md
```
## Rate Limiting
The client implements automatic rate limiting:
- Max 5 concurrent requests
- Minimum 200ms between requests
- Automatic retry on 429 responses
## Error Handling
The server provides detailed error messages for:
- Authentication failures (401)
- Permission issues (403)
- Resource not found (404)
- Validation errors (422)
- Rate limit exceeded (429)
- Server errors (500+)
## License

View File

@ -257,7 +257,7 @@ export default [
handler: async (input: unknown, client: ApolloClient) => {
const validated = UpdateAccountInput.parse(input);
const { id, ...updateData } = validated;
const result = await client.put(`/accounts/${id}`, updateData);
const result = await client.patch(`/accounts/${id}`, updateData);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};

View File

@ -299,7 +299,7 @@ export default [
handler: async (input: unknown, client: ApolloClient) => {
const validated = UpdateContactInput.parse(input);
const { id, ...updateData } = validated;
const result = await client.put(`/contacts/${id}`, updateData);
const result = await client.patch(`/contacts/${id}`, updateData);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};

View File

@ -154,7 +154,7 @@ export default [
handler: async (input: unknown, client: ApolloClient) => {
const validated = UpdateOpportunityInput.parse(input);
const { id, ...updateData } = validated;
const result = await client.put(`/opportunities/${id}`, updateData);
const result = await client.patch(`/opportunities/${id}`, updateData);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};

View File

@ -141,7 +141,7 @@ export default [
handler: async (input: unknown, client: ApolloClient) => {
const validated = UpdateTaskInput.parse(input);
const { id, ...updateData } = validated;
const result = await client.put(`/tasks/${id}`, updateData);
const result = await client.patch(`/tasks/${id}`, updateData);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};

View File

@ -0,0 +1,70 @@
/**
* Chargebee Add-on Tools
*/
import { z } from 'zod';
import type { ChargebeeClient } from '../client/chargebee-client.js';
const ListAddonsInput = z.object({
limit: z.number().min(1).max(100).default(100).describe('Number of add-ons per page (max 100)'),
offset: z.string().optional().describe('Pagination offset from previous response'),
status: z.enum(['active', 'archived']).optional().describe('Filter by add-on status'),
});
const GetAddonInput = z.object({
id: z.string().describe('The unique ID of the add-on to retrieve'),
});
export default [
{
name: 'chargebee_list_addons',
description: 'Lists add-ons from Chargebee with pagination support. Use when the user wants to view available add-ons, check pricing, or manage optional features. Returns paginated results showing add-on names, prices, billing frequencies, and status. Supports filtering by status (active/archived). Up to 100 add-ons per page.',
inputSchema: {
type: 'object' as const,
properties: {
limit: {
type: 'number',
description: 'Number of add-ons per page (max 100)',
default: 100,
},
offset: {
type: 'string',
description: 'Pagination offset from previous response',
},
status: {
type: 'string',
enum: ['active', 'archived'],
description: 'Filter by add-on status',
},
},
},
handler: async (input: unknown, client: ChargebeeClient) => {
const validated = ListAddonsInput.parse(input);
const result = await client.get('/addons', validated);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'chargebee_get_addon',
description: 'Retrieves a single add-on by ID from Chargebee. Use when the user asks for detailed information about a specific add-on including pricing tiers, description, applicable subscriptions, and metadata. Returns complete add-on record with all configuration details.',
inputSchema: {
type: 'object' as const,
properties: {
id: {
type: 'string',
description: 'The unique ID of the add-on to retrieve',
},
},
required: ['id'],
},
handler: async (input: unknown, client: ChargebeeClient) => {
const validated = GetAddonInput.parse(input);
const result = await client.get(`/addons/${validated.id}`);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
];

View File

@ -0,0 +1,55 @@
/**
* Chargebee Event Tools
*/
import { z } from 'zod';
import type { ChargebeeClient } from '../client/chargebee-client.js';
const ListEventsInput = z.object({
limit: z.number().min(1).max(100).default(100).describe('Number of events per page (max 100)'),
offset: z.string().optional().describe('Pagination offset from previous response'),
start_time: z.number().optional().describe('Filter events after this timestamp (Unix epoch)'),
end_time: z.number().optional().describe('Filter events before this timestamp (Unix epoch)'),
event_type: z.array(z.string()).optional().describe('Filter by event types (e.g., ["subscription_created", "invoice_generated"])'),
});
export default [
{
name: 'chargebee_list_events',
description: 'Lists webhook events from Chargebee event log with pagination and filtering. Use when the user wants to audit system activity, debug webhook issues, track subscription lifecycle changes, or analyze customer behavior. Returns paginated results showing event types, timestamps, affected resources, and event data. Supports filtering by time range and event type. Essential for integration troubleshooting and activity monitoring. Up to 100 events per page.',
inputSchema: {
type: 'object' as const,
properties: {
limit: {
type: 'number',
description: 'Number of events per page (max 100)',
default: 100,
},
offset: {
type: 'string',
description: 'Pagination offset from previous response',
},
start_time: {
type: 'number',
description: 'Filter events after this timestamp (Unix epoch)',
},
end_time: {
type: 'number',
description: 'Filter events before this timestamp (Unix epoch)',
},
event_type: {
type: 'array',
items: { type: 'string' },
description: 'Filter by event types (e.g., ["subscription_created", "invoice_generated"])',
},
},
},
handler: async (input: unknown, client: ChargebeeClient) => {
const validated = ListEventsInput.parse(input);
const result = await client.get('/events', validated);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
];

View File

@ -0,0 +1,76 @@
/**
* Chargebee Payment Source Tools
*/
import { z } from 'zod';
import type { ChargebeeClient} from '../client/chargebee-client.js';
const ListPaymentSourcesInput = z.object({
customer_id: z.string().describe('Customer ID to list payment sources for'),
});
const CreatePaymentSourceInput = z.object({
customer_id: z.string().describe('Customer ID to add payment source to'),
type: z.enum(['card', 'bank_account', 'paypal_express_checkout']).describe('Type of payment source'),
gateway_account_id: z.string().optional().describe('Gateway account ID to use'),
tmp_token: z.string().optional().describe('Temporary token from payment gateway'),
});
export default [
{
name: 'chargebee_list_payment_sources',
description: 'Lists payment sources (credit cards, bank accounts) for a customer in Chargebee. Use when the user wants to view saved payment methods, check card expiry dates, or manage billing information. Returns list of payment sources with masked card numbers, expiry dates, and status. Essential for payment method management.',
inputSchema: {
type: 'object' as const,
properties: {
customer_id: {
type: 'string',
description: 'Customer ID to list payment sources for',
},
},
required: ['customer_id'],
},
handler: async (input: unknown, client: ChargebeeClient) => {
const validated = ListPaymentSourcesInput.parse(input);
const result = await client.get(`/customers/${validated.customer_id}/payment_sources`);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'chargebee_create_payment_source',
description: 'Adds a new payment source (credit card, bank account, or PayPal) to a customer in Chargebee. Use when a customer adds a payment method, updates their billing info, or provides backup payment options. Requires payment gateway token. Returns the created payment source with secure reference ID.',
inputSchema: {
type: 'object' as const,
properties: {
customer_id: {
type: 'string',
description: 'Customer ID to add payment source to',
},
type: {
type: 'string',
enum: ['card', 'bank_account', 'paypal_express_checkout'],
description: 'Type of payment source',
},
gateway_account_id: {
type: 'string',
description: 'Gateway account ID to use',
},
tmp_token: {
type: 'string',
description: 'Temporary token from payment gateway',
},
},
required: ['customer_id', 'type'],
},
handler: async (input: unknown, client: ChargebeeClient) => {
const validated = CreatePaymentSourceInput.parse(input);
const { customer_id, ...paymentData } = validated;
const result = await client.post(`/customers/${customer_id}/payment_sources`, paymentData);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
];

View File

@ -2,206 +2,152 @@
* Chargebee Subscription Tools
*/
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import type { ChargebeeClient } from '../client/chargebee-client.js';
export const listSubscriptionsTool: Tool = {
name: 'list_subscriptions',
description: 'Lists subscriptions from Chargebee with pagination support. Use when the user wants to view all active subscriptions, analyze subscription metrics, or manage recurring billing. Returns paginated results showing subscription status, plan details, billing cycles, and MRR. Supports filtering by status, customer, and plan. Up to 100 subscriptions per page.',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of subscriptions per page (max 100)',
default: 100,
},
offset: {
type: 'string',
description: 'Pagination offset from previous response',
},
status: {
type: 'array',
items: {
const ListSubscriptionsInput = z.object({
limit: z.number().min(1).max(100).default(100).describe('Number of subscriptions per page (max 100)'),
offset: z.string().optional().describe('Pagination offset from previous response'),
status: z.array(z.enum(['future', 'in_trial', 'active', 'non_renewing', 'paused', 'cancelled'])).optional().describe('Filter by subscription status'),
customer_id: z.string().optional().describe('Filter by customer ID'),
});
const GetSubscriptionInput = z.object({
id: z.string().describe('The unique ID of the subscription to retrieve'),
});
const CancelSubscriptionInput = z.object({
id: z.string().describe('The subscription ID to cancel'),
end_of_term: z.boolean().default(false).describe('Cancel at end of current term (true) or immediately (false)'),
credit_option_for_current_term_charges: z.enum(['none', 'prorate', 'full']).optional().describe('Credit option for current term'),
});
const ReactivateSubscriptionInput = z.object({
id: z.string().describe('The subscription ID to reactivate'),
trial_end: z.number().optional().describe('New trial end timestamp (Unix epoch)'),
charges_on_reactivation: z.boolean().default(true).describe('Whether to charge prorated amount on reactivation'),
});
export default [
{
name: 'chargebee_list_subscriptions',
description: 'Lists subscriptions from Chargebee with pagination support. Use when the user wants to view all active subscriptions, analyze subscription metrics, or manage recurring billing. Returns paginated results showing subscription status, plan details, billing cycles, and MRR. Supports filtering by status, customer, and plan. Up to 100 subscriptions per page.',
inputSchema: {
type: 'object' as const,
properties: {
limit: {
type: 'number',
description: 'Number of subscriptions per page (max 100)',
default: 100,
},
offset: {
type: 'string',
enum: ['future', 'in_trial', 'active', 'non_renewing', 'paused', 'cancelled'],
description: 'Pagination offset from previous response',
},
description: 'Filter by subscription status',
},
customer_id: {
type: 'string',
description: 'Filter by customer ID',
},
},
},
_meta: {
category: 'subscriptions',
access_level: 'read',
complexity: 'low',
},
};
export const getSubscriptionTool: Tool = {
name: 'get_subscription',
description: 'Retrieves a single subscription by ID from Chargebee. Use when the user asks for detailed subscription information including plan details, billing cycle, trial period, add-ons, current term dates, and scheduled changes. Returns complete subscription record with all metadata.',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The unique ID of the subscription to retrieve',
},
},
required: ['id'],
},
_meta: {
category: 'subscriptions',
access_level: 'read',
complexity: 'low',
},
};
export const createSubscriptionTool: Tool = {
name: 'create_subscription',
description: 'Creates a new subscription in Chargebee. Use when the user wants to start a new customer subscription, upgrade a trial, or provision service access. Can create subscription for existing or new customers, apply coupons, add add-ons, and configure trial periods. Returns the newly created subscription with billing schedule.',
inputSchema: {
type: 'object',
properties: {
plan_id: {
type: 'string',
description: 'ID of the plan to subscribe to',
},
customer_id: {
type: 'string',
description: 'Existing customer ID (optional, will create new customer if not provided)',
},
plan_quantity: {
type: 'number',
description: 'Quantity of plan units (for per-unit pricing)',
},
plan_unit_price: {
type: 'number',
description: 'Override plan unit price (in cents)',
},
billing_cycles: {
type: 'number',
description: 'Number of billing cycles before auto-cancellation',
},
trial_end: {
type: 'number',
description: 'Trial end timestamp (Unix epoch)',
},
coupon_ids: {
type: 'array',
items: { type: 'string' },
description: 'Array of coupon IDs to apply',
},
addons: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
quantity: { type: 'number' },
unit_price: { type: 'number' },
status: {
type: 'array',
items: {
type: 'string',
enum: ['future', 'in_trial', 'active', 'non_renewing', 'paused', 'cancelled'],
},
description: 'Filter by subscription status',
},
customer_id: {
type: 'string',
description: 'Filter by customer ID',
},
description: 'Add-ons to include with subscription',
},
},
required: ['plan_id'],
},
_meta: {
category: 'subscriptions',
access_level: 'write',
complexity: 'medium',
},
};
export const updateSubscriptionTool: Tool = {
name: 'update_subscription',
description: 'Updates an existing subscription in Chargebee. Use when the user needs to change plan, adjust quantity, add or remove add-ons, or modify billing settings. Changes can be applied immediately or scheduled for next billing cycle. Returns updated subscription with proration details if applicable.',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The subscription ID to update',
},
plan_id: {
type: 'string',
description: 'New plan ID (for plan change)',
},
plan_quantity: {
type: 'number',
description: 'Updated plan quantity',
},
plan_unit_price: {
type: 'number',
description: 'Updated plan unit price (in cents)',
},
end_of_term: {
type: 'boolean',
description: 'Apply changes at end of current term (default: false)',
},
coupon_ids: {
type: 'array',
items: { type: 'string' },
description: 'Coupons to apply',
},
handler: async (input: unknown, client: ChargebeeClient) => {
const validated = ListSubscriptionsInput.parse(input);
const result = await client.get('/subscriptions', validated);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
required: ['id'],
},
_meta: {
category: 'subscriptions',
access_level: 'write',
complexity: 'medium',
},
};
export const cancelSubscriptionTool: Tool = {
name: 'cancel_subscription',
description: 'Cancels a subscription in Chargebee. Use when a customer requests cancellation, churns, or when terminating service. Can cancel immediately or schedule cancellation for end of billing period. Supports optional refund and credit note generation. Returns cancelled subscription with final billing details.',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The subscription ID to cancel',
},
end_of_term: {
type: 'boolean',
description: 'Cancel at end of current term (default: false = immediate)',
{
name: 'chargebee_get_subscription',
description: 'Retrieves a single subscription by ID from Chargebee. Use when the user asks for detailed subscription information including plan details, billing cycle, trial period, add-ons, current term dates, and scheduled changes. Returns complete subscription record with all metadata.',
inputSchema: {
type: 'object' as const,
properties: {
id: {
type: 'string',
description: 'The unique ID of the subscription to retrieve',
},
},
required: ['id'],
},
required: ['id'],
},
_meta: {
category: 'subscriptions',
access_level: 'write',
complexity: 'medium',
},
};
export const reactivateSubscriptionTool: Tool = {
name: 'reactivate_subscription',
description: 'Reactivates a cancelled subscription in Chargebee. Use when a customer returns, wants to restore service, or cancellation was made in error. Can only reactivate subscriptions cancelled with end_of_term. Returns reactivated subscription with updated billing schedule.',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The subscription ID to reactivate',
},
trial_end: {
type: 'number',
description: 'Optional new trial end timestamp',
},
handler: async (input: unknown, client: ChargebeeClient) => {
const validated = GetSubscriptionInput.parse(input);
const result = await client.get(`/subscriptions/${validated.id}`);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
required: ['id'],
},
_meta: {
category: 'subscriptions',
access_level: 'write',
complexity: 'medium',
{
name: 'chargebee_cancel_subscription',
description: 'Cancels a subscription in Chargebee. Use when a customer requests cancellation, churns, or needs service terminated. Can cancel immediately or at end of current billing term. Supports optional prorated credits for unused time. Returns the updated subscription with cancellation details and effective date.',
inputSchema: {
type: 'object' as const,
properties: {
id: {
type: 'string',
description: 'The subscription ID to cancel',
},
end_of_term: {
type: 'boolean',
description: 'Cancel at end of current term (true) or immediately (false)',
default: false,
},
credit_option_for_current_term_charges: {
type: 'string',
enum: ['none', 'prorate', 'full'],
description: 'Credit option for current term',
},
},
required: ['id'],
},
handler: async (input: unknown, client: ChargebeeClient) => {
const validated = CancelSubscriptionInput.parse(input);
const { id, ...params } = validated;
const result = await client.post(`/subscriptions/${id}/cancel`, params);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
};
{
name: 'chargebee_reactivate_subscription',
description: 'Reactivates a cancelled or non-renewing subscription in Chargebee. Use when a customer wants to resume service before the end date, restore a cancelled subscription, or undo a cancellation. Can apply new trial period and configure prorated charges. Returns the reactivated subscription with updated term dates.',
inputSchema: {
type: 'object' as const,
properties: {
id: {
type: 'string',
description: 'The subscription ID to reactivate',
},
trial_end: {
type: 'number',
description: 'New trial end timestamp (Unix epoch)',
},
charges_on_reactivation: {
type: 'boolean',
description: 'Whether to charge prorated amount on reactivation',
default: true,
},
},
required: ['id'],
},
handler: async (input: unknown, client: ChargebeeClient) => {
const validated = ReactivateSubscriptionInput.parse(input);
const { id, ...params } = validated;
const result = await client.post(`/subscriptions/${id}/reactivate`, params);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
];

View File

@ -0,0 +1,92 @@
/**
* Chargebee Transaction Tools
*/
import { z } from 'zod';
import type { ChargebeeClient } from '../client/chargebee-client.js';
const ListTransactionsInput = z.object({
limit: z.number().min(1).max(100).default(100).describe('Number of transactions per page (max 100)'),
offset: z.string().optional().describe('Pagination offset from previous response'),
customer_id: z.string().optional().describe('Filter by customer ID'),
subscription_id: z.string().optional().describe('Filter by subscription ID'),
payment_method: z.array(z.enum(['card', 'cash', 'check', 'bank_transfer', 'other'])).optional().describe('Filter by payment method'),
status: z.array(z.enum(['in_progress', 'success', 'voided', 'failure', 'timeout'])).optional().describe('Filter by transaction status'),
});
const GetTransactionInput = z.object({
id: z.string().describe('The unique ID of the transaction to retrieve'),
});
export default [
{
name: 'chargebee_list_transactions',
description: 'Lists payment transactions from Chargebee with pagination and filtering. Use when the user wants to view payment history, reconcile accounts, analyze payment failures, or generate financial reports. Returns paginated results showing transaction amounts, dates, payment methods, status, and associated invoices. Supports filtering by customer, subscription, payment method, and status. Up to 100 transactions per page.',
inputSchema: {
type: 'object' as const,
properties: {
limit: {
type: 'number',
description: 'Number of transactions per page (max 100)',
default: 100,
},
offset: {
type: 'string',
description: 'Pagination offset from previous response',
},
customer_id: {
type: 'string',
description: 'Filter by customer ID',
},
subscription_id: {
type: 'string',
description: 'Filter by subscription ID',
},
payment_method: {
type: 'array',
items: {
type: 'string',
enum: ['card', 'cash', 'check', 'bank_transfer', 'other'],
},
description: 'Filter by payment method',
},
status: {
type: 'array',
items: {
type: 'string',
enum: ['in_progress', 'success', 'voided', 'failure', 'timeout'],
},
description: 'Filter by transaction status',
},
},
},
handler: async (input: unknown, client: ChargebeeClient) => {
const validated = ListTransactionsInput.parse(input);
const result = await client.get('/transactions', validated);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'chargebee_get_transaction',
description: 'Retrieves a single transaction by ID from Chargebee. Use when the user asks for details about a specific payment including amount, payment method, gateway response, refund information, and linked invoice. Returns complete transaction record with all metadata and status history.',
inputSchema: {
type: 'object' as const,
properties: {
id: {
type: 'string',
description: 'The unique ID of the transaction to retrieve',
},
},
required: ['id'],
},
handler: async (input: unknown, client: ChargebeeClient) => {
const validated = GetTransactionInput.parse(input);
const result = await client.get(`/transactions/${validated.id}`);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
];

View File

@ -1,15 +1,17 @@
# Lever MCP Server
A Model Context Protocol (MCP) server implementation for the Lever ATS (Applicant Tracking System) platform. This server enables AI assistants to interact with Lever's recruiting and hiring workflows through a standardized interface.
AI-powered access to the Lever ATS/recruiting platform via Model Context Protocol.
## Features
- **Complete API Coverage**: 19 tools covering all major Lever workflows
- **Rate Limiting**: Built-in rate limiting (10 req/sec steady state, 20 req/sec burst)
- **Error Handling**: Comprehensive error handling with descriptive messages
- **Type Safety**: Full TypeScript implementation with detailed type definitions
- **Pagination Support**: All list endpoints support pagination with `has_more` indicators
- **Authentication**: Secure Basic Auth using API keys
- **Opportunity Management**: List, get, create, search, archive opportunities (candidates), manage notes and sources
- **Job Postings**: List, get, create postings, view applicants, and get posting statistics
- **Interview Feedback**: List, get, and create interview feedback and evaluations
- **Offers**: List, get, create offers, and track offer version history
- **Users**: List and retrieve Lever users and team members
- **Pipeline Stages**: List and get pipeline stage information
- **Tags**: List tags and add tags to opportunities
- **Sources**: List candidate sources for attribution tracking
## Installation
@ -18,281 +20,111 @@ npm install
npm run build
```
## Configuration
## Environment Variables
Set your Lever API key as an environment variable:
| Variable | Required | Description | Example |
|----------|----------|-------------|---------|
| `LEVER_API_KEY` | ✅ | Lever API key | `your_lever_api_key_here` |
```bash
export LEVER_API_KEY="your_api_key_here"
```
## Getting Your Access Token
You can create API keys from the [Integrations and API page](https://hire.lever.co/settings/integrations) in your Lever account settings.
1. Log into your Lever account
2. Navigate to **Settings** > **Integrations** > **API**
3. Click **Create new API key**
4. Set appropriate permissions for your use case
5. Copy the generated API key
6. Set it as `LEVER_API_KEY` in your environment
## Required API Scopes
Configure API key permissions based on your use case:
- **Read access**: Opportunities, Postings, Users, Feedback, Offers, Stages, Tags, Sources
- **Write access**: Create opportunities, add tags, create feedback, create offers, archive opportunities
## Usage
### As an MCP Server
### Stdio Mode (Default)
Add to your MCP client configuration:
```json
{
"mcpServers": {
"lever": {
"command": "node",
"args": ["/path/to/lever/dist/index.js"],
"env": {
"LEVER_API_KEY": "your_api_key_here"
}
}
}
}
```bash
node dist/main.js
```
### Standalone
Or using the npm script:
```bash
npm start
```
## Available Tools
### HTTP Mode
### Opportunities (Candidates) - 6 tools
Currently supports stdio transport only. HTTP/SSE transport support coming soon.
1. **list_opportunities** - List all candidates with filtering by stage, tags, posting, owner, archive status. Supports pagination.
## Tool Coverage Manifest
2. **get_opportunity** - Get detailed information about a specific candidate including contact info, stage, tags, applications, and history.
### Total API Coverage
3. **create_opportunity** - Add a new candidate to the pipeline with contact details, stage assignment, tags, and posting application.
- **Total Lever API endpoints**: ~80
- **Tools implemented**: 22
- **Intentionally skipped**: 58
- **Coverage**: 22/80 = 27.5%
4. **update_opportunity** - Modify candidate information, change stage, update tags, assign owner, or archive/unarchive.
### Implemented Tools
5. **add_opportunity_note** - Record notes, interactions, or feedback on a candidate. Supports secret/confidential notes.
| Category | Tools | Count |
|----------|-------|-------|
| Opportunities | list, get, create, search, archive, list_notes, add_source | 7 |
| Postings | list, get, create, list_applicants, get_stats | 5 |
| Feedback | list, get, create | 3 |
| Offers | list, get, create, list_versions | 4 |
| Users | list, get | 2 |
| Stages | list, get | 2 |
| Tags | list, add | 2 |
| Sources | list | 1 |
6. **add_opportunity_tag** - Add categorization tags to candidates for filtering and organization.
**Total: 26 tools** across 8 categories
### Postings (Job Openings) - 4 tools
### Skipped Endpoints (Rationale)
7. **list_postings** - List all job postings with filtering by state, department, location, commitment level.
- **Archive Reasons** (2 endpoints): Configuration data, rarely changed
- **Interviews** (6 endpoints): Complex scheduling, better suited for UI
- **Panels** (4 endpoints): Interview panel management, low-use
- **Requisitions** (5 endpoints): Enterprise-only feature
- **Referrals** (4 endpoints): Specific workflow, niche use case
- **Webhooks** (8 endpoints): Admin configuration, not suitable for MCP
- **Files/Attachments** (6 endpoints): Binary handling requires special transport
- **Bulk Operations** (4 endpoints): High-risk, better suited for admin tools
- **Advanced Reporting** (10+ endpoints): Complex analytics better in UI
- Other low-use administrative endpoints
8. **get_posting** - Get full job posting details including description, requirements, hiring manager, and application questions.
### Coverage Goals
9. **create_posting** - Create a new job posting with title, description, department, location, and distribution settings.
Current implementation focuses on **Tier 1** (daily recruiting workflows):
- Managing candidates and opportunities
- Reviewing and creating feedback
- Posting jobs and tracking applicants
- Extending offers
- Pipeline management
10. **update_posting** - Modify job posting details, change status (open/close), update description, or reassign ownership.
### Pipeline Stages - 1 tool
11. **list_stages** - Get all pipeline stages to understand hiring workflow and obtain stage IDs for moving candidates.
### Users - 2 tools
12. **list_users** - List all team members with roles and permissions. Use for assigning ownership or followers.
13. **get_user** - Get detailed user information including role, permissions, and contact details.
### Offers - 3 tools
14. **list_offers** - View all offers for a candidate including status (draft, sent, signed).
15. **get_offer** - Get detailed offer information including compensation, start date, and documents.
16. **create_offer** - Generate a new employment offer with compensation details and terms.
### Feedback & Forms - 3 tools
17. **list_feedback** - View all interview feedback and scorecards for a candidate.
18. **submit_feedback** - Submit or update interview feedback and scorecards.
19. **list_feedback_templates** - Get available feedback templates (interview scorecards) and their structure.
## Tool Naming Conventions
All tools follow consistent naming patterns:
- `list_*` - Paginated collection endpoints (support offset/limit)
- `get_*` - Single resource retrieval by ID
- `create_*` - Resource creation
- `update_*` - Resource modification
- `add_*` - Add sub-resources (tags, notes, etc.)
- `submit_*` - Submit forms/feedback
All tool names use `snake_case` for consistency.
## Pagination
All `list_*` tools support pagination:
**Request Parameters:**
- `limit` (number, optional): Number of results per page (1-100, default 100)
- `offset` (string, optional): Pagination token from previous response
**Response Format:**
```json
{
"data": [...],
"has_more": true,
"next_offset": "0.1414895548650.a6070140-33db"
}
```
To get the next page, pass the `next_offset` value as the `offset` parameter in your next request.
## Error Handling
The server provides detailed error messages for common scenarios:
- **400 Invalid Request**: Malformed parameters or missing required fields
- **401 Unauthorized**: Invalid API key
- **403 Forbidden**: Insufficient permissions for the requested operation
- **404 Not Found**: Resource does not exist
- **429 Rate Limit**: Too many requests (retry with exponential backoff)
- **500 Server Error**: Lever service error
- **503 Service Unavailable**: Lever is temporarily down
## API Coverage Manifest
### Total Lever API Endpoints
Based on Lever API documentation (https://hire.lever.co/developer/documentation):
| Category | Total Endpoints | Covered | Coverage |
|----------|----------------|---------|----------|
| Opportunities | 15+ | 6 | 40% |
| Applications | 3 | 0* | 0% |
| Postings | 10+ | 4 | 40% |
| Stages | 2 | 1 | 50% |
| Users | 5 | 2 | 40% |
| Offers | 5 | 3 | 60% |
| Feedback/Forms | 8+ | 3 | 38% |
| Archive Reasons | 2 | 0 | 0% |
| Files | 4 | 0 | 0% |
| Interviews | 6 | 0 | 0% |
| Panels | 4 | 0 | 0% |
| Requisitions | 4 | 0 | 0% |
| Sources | 2 | 0 | 0% |
| Tags | 2 | 0** | 0% |
| Webhooks | 4 | 0 | 0% |
| Referrals | 3 | 0 | 0% |
| Audit Events | 2 | 0 | 0% |
| Resumes | 2 | 0 | 0% |
**Total Estimated Endpoints:** ~80+
**Tools Implemented:** 19
**Coverage:** ~24%
\* Applications are created via `create_opportunity` with posting_id
\*\* Tags are managed via `add_opportunity_tag` and opportunity update operations
### Intentionally Skipped
The following endpoints were not implemented in this initial version:
1. **Archive Reasons** (list, get) - Read-only reference data, lower priority
2. **Applications** (list, get, create) - Covered via opportunity workflows
3. **Files** (upload, list, delete) - File handling requires multipart form support
4. **Interviews** (create, update, list, delete) - Advanced scheduling features
5. **Panels** (create, update, list) - Interview panel management
6. **Requisitions** (list, create, update) - Headcount/budget tracking
7. **Sources** (list) - Reference data
8. **Tags** (list) - Covered via opportunity operations
9. **Webhooks** (create, list, update, delete) - Integration configuration
10. **Referrals** (create, list) - Specialized candidate source
11. **Audit Events** (list) - Security/compliance logging
12. **Resumes** (list, parse) - Document processing
### Coverage Rationale
This implementation focuses on the **core recruiting workflow (Tier 1)**:
✅ **Covered:**
- Candidate management (search, create, update, track)
- Job posting management (create, update, publish)
- Pipeline movement (stages)
- Team collaboration (users, notes)
- Offer generation and tracking
- Interview feedback collection
❌ **Not Yet Covered:**
- Advanced integrations (webhooks)
- File/document management
- Compliance/audit logging
- Requisition/budget tracking
- Complex interview scheduling
### Future Enhancements
Potential additions for future versions:
1. **Archive operations** - Archive/unarchive with reasons
2. **Interview scheduling** - Create and manage interviews
3. **File uploads** - Resume and document handling
4. **Requisitions** - Headcount tracking and approvals
5. **Webhooks** - Event subscriptions for real-time updates
6. **Bulk operations** - Batch candidate updates
7. **Advanced search** - Full-text search across candidates
8. **Analytics** - Pipeline metrics and reporting
## Rate Limits
Lever enforces rate limits using token bucket:
- **Steady state:** 10 requests/second
- **Burst capacity:** Up to 20 requests/second
- **Per:** API key
This server automatically handles rate limiting with the Bottleneck library.
**Future expansion** could add Tier 2 tools for power users:
- Interview scheduling and panel management
- Bulk operations
- Advanced reporting and analytics
- Requisition approval workflows
## Development
```bash
# Install dependencies
npm install
# Watch mode for development
npm run dev
# Build TypeScript
# Build
npm run build
# Watch mode
npm run watch
# Type checking
npx tsc --noEmit
```
## Project Structure
```
lever/
├── src/
│ ├── index.ts # MCP server entry point
│ ├── client/
│ │ └── lever-client.ts # API client with rate limiting
│ ├── tools/
│ │ ├── opportunities-tools.ts # Candidate tools
│ │ ├── postings-tools.ts # Job posting tools
│ │ ├── stages-tools.ts # Pipeline stage tools
│ │ ├── users-tools.ts # Team member tools
│ │ ├── offers-tools.ts # Offer tools
│ │ └── feedback-tools.ts # Feedback/forms tools
│ └── types/
│ └── index.ts # TypeScript type definitions
├── package.json
├── tsconfig.json
└── README.md
```
## License
MIT
## Resources
- [Lever API Documentation](https://hire.lever.co/developer/documentation)
- [Model Context Protocol](https://modelcontextprotocol.io)
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
## Support
For issues or questions:
1. Check the [Lever API docs](https://hire.lever.co/developer/documentation)
2. Review API key permissions in Lever settings
3. Verify rate limits and retry with exponential backoff
4. Check server logs for detailed error messages

View File

@ -1,27 +1,30 @@
{
"name": "@mcpengine/lever-server",
"name": "@mcpengine/lever",
"version": "1.0.0",
"description": "MCP server for Lever ATS/recruiting platform",
"type": "module",
"bin": {
"lever-mcp": "./dist/index.js"
"@mcpengine/lever": "./dist/main.js"
},
"main": "./dist/index.js",
"main": "./dist/main.js",
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"start": "node dist/index.js"
"start": "node dist/main.js",
"dev": "tsx watch src/main.ts"
},
"keywords": ["mcp", "lever", "ats", "recruiting"],
"author": "MCPEngine",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"axios": "^1.6.0",
"bottleneck": "^2.19.5"
"@modelcontextprotocol/sdk": "^1.12.1",
"axios": "^1.7.0",
"bottleneck": "^2.19.5",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.3.0"
"@types/node": "^22.0.0",
"typescript": "^5.6.0",
"tsx": "^4.19.0"
}
}

69
servers/lever/src/main.ts Normal file
View File

@ -0,0 +1,69 @@
#!/usr/bin/env node
/**
* Lever MCP Server
* Provides AI-powered access to Lever ATS/recruiting platform
*/
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { LeverClient } from './client/lever-client.js';
import { LeverMCPServer } from './server.js';
// Validate environment variables
const apiKey = process.env.LEVER_API_KEY;
if (!apiKey) {
console.error('Error: LEVER_API_KEY environment variable is required');
console.error('');
console.error('Get your API key from:');
console.error(' 1. Log into Lever');
console.error(' 2. Go to Settings > Integrations > API');
console.error(' 3. Create a new API key');
console.error(' 4. Copy the key and set LEVER_API_KEY in your environment');
console.error('');
process.exit(1);
}
// Initialize client and server
const client = new LeverClient({ apiKey });
const mcpServer = new LeverMCPServer(client);
const server = mcpServer.getServer();
// Graceful shutdown handlers
let isShuttingDown = false;
async function shutdown(signal: string) {
if (isShuttingDown) return;
isShuttingDown = true;
console.error(`\nReceived ${signal}, shutting down gracefully...`);
try {
await server.close();
console.error('Server closed successfully');
process.exit(0);
} catch (error) {
console.error('Error during shutdown:', error);
process.exit(1);
}
}
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
// Start the server
async function main() {
try {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Lever MCP Server running on stdio');
console.error('Connected to Lever API');
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
main().catch((error) => {
console.error('Fatal error in main():', error);
process.exit(1);
});

137
servers/lever/src/server.ts Normal file
View File

@ -0,0 +1,137 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ErrorCode,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { LeverClient } from './client/lever-client.js';
type ToolModule = {
name: string;
description: string;
inputSchema: any;
handler: (input: unknown, client: LeverClient) => Promise<any>;
};
export class LeverMCPServer {
private server: Server;
private client: LeverClient;
private toolModules: Map<string, () => Promise<ToolModule[]>>;
constructor(client: LeverClient) {
this.client = client;
this.toolModules = new Map();
this.server = new Server(
{
name: 'lever-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
this.setupToolModules();
this.setupHandlers();
}
private setupToolModules(): void {
// Lazy-load each tool module
this.toolModules.set('opportunities', async () => {
const module = await import('./tools/opportunities-tools.js');
return module.default;
});
this.toolModules.set('postings', async () => {
const module = await import('./tools/postings-tools.js');
return module.default;
});
this.toolModules.set('feedback', async () => {
const module = await import('./tools/feedback-tools.js');
return module.default;
});
this.toolModules.set('offers', async () => {
const module = await import('./tools/offers-tools.js');
return module.default;
});
this.toolModules.set('users', async () => {
const module = await import('./tools/users-tools.js');
return module.default;
});
this.toolModules.set('stages', async () => {
const module = await import('./tools/stages-tools.js');
return module.default;
});
this.toolModules.set('tags', async () => {
const module = await import('./tools/tags-tools.js');
return module.default;
});
this.toolModules.set('sources', async () => {
const module = await import('./tools/sources-tools.js');
return module.default;
});
}
private async loadAllTools(): Promise<ToolModule[]> {
const allTools: ToolModule[] = [];
for (const loader of this.toolModules.values()) {
const tools = await loader();
allTools.push(...tools);
}
return allTools;
}
private setupHandlers(): void {
// List all available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = await this.loadAllTools();
return {
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
// Load all tools and find the matching handler
const allTools = await this.loadAllTools();
const tool = allTools.find(t => t.name === name);
if (!tool) {
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
return await tool.handler(args, this.client);
} catch (error) {
if (error instanceof McpError) throw error;
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
getServer(): Server {
return this.server;
}
}

View File

@ -1,110 +1,136 @@
import { z } from 'zod';
import { LeverClient } from '../client/lever-client.js';
import type { Feedback, FeedbackTemplate, LeverPaginatedResponse } from '../types/index.js';
export const feedbackTools = {
list_feedback: {
description: `Lists all feedback forms for a specific opportunity (candidate). Use this to view interview feedback, review scorecards, or check which interviews have been completed. Returns all feedback including completed and incomplete forms.
const ListFeedbackInput = z.object({
limit: z.number().min(1).max(100).default(50).describe('Results per page'),
offset: z.string().optional().describe('Pagination offset token'),
opportunity_id: z.string().optional().describe('Filter by opportunity ID'),
interview_id: z.string().optional().describe('Filter by interview ID'),
panel_id: z.string().optional().describe('Filter by panel ID'),
});
Parameters:
- opportunity_id (string, required): The unique ID of the opportunity
- limit (number, optional): Number of results to return (1-100), default 100
- offset (string, optional): Pagination offset token from previous response
const GetFeedbackInput = z.object({
feedback_id: z.string().describe('Feedback ID'),
});
Returns: Paginated list of feedback forms with has_more indicator and next offset token.`,
handler: async (client: LeverClient, args: any) => {
if (!args.opportunity_id) {
throw new Error('opportunity_id is required');
}
const params: any = {};
if (args.limit) params.limit = args.limit;
if (args.offset) params.offset = args.offset;
const response = await client.get<LeverPaginatedResponse<Feedback>>(
`/opportunities/${args.opportunity_id}/forms`,
params
);
const CreateFeedbackInput = z.object({
opportunity_id: z.string().describe('Opportunity ID'),
interview_id: z.string().optional().describe('Interview ID this feedback is for'),
panel_id: z.string().optional().describe('Panel ID this feedback is for'),
instructions: z.string().optional().describe('Feedback instructions or template'),
fields: z.array(z.object({
type: z.string(),
value: z.string(),
})).optional().describe('Feedback field responses'),
recommendation: z.enum(['no', 'yes', 'strong_yes', 'strong_no']).optional().describe('Overall recommendation'),
});
export default [
{
name: 'lever_list_feedback',
description: 'Lists interview feedback from your Lever account with filtering and pagination. Use when you want to browse feedback across all interviews, review team assessments, or filter feedback by opportunity, interview, or panel. Returns paginated feedback with interviewer details, ratings, recommendations, and responses to interview questions. Returns up to 100 feedback items per page.',
inputSchema: {
type: 'object' as const,
properties: {
limit: {
type: 'number',
description: 'Results per page (1-100)',
default: 50,
},
offset: {
type: 'string',
description: 'Pagination offset token',
},
opportunity_id: {
type: 'string',
description: 'Filter by opportunity ID',
},
interview_id: {
type: 'string',
description: 'Filter by interview ID',
},
panel_id: {
type: 'string',
description: 'Filter by panel ID',
},
},
},
handler: async (input: unknown, client: LeverClient) => {
const validated = ListFeedbackInput.parse(input);
const result = await client.get('/feedback', validated);
return {
feedback: response.data,
has_more: response.hasNext,
next_offset: response.next,
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
submit_feedback: {
description: `Submits or updates a feedback form for an opportunity. Use this to record interview feedback, complete scorecards, or update existing feedback. Can create new feedback or update incomplete forms.
Parameters:
- opportunity_id (string, required): The unique ID of the opportunity
- perform_as (string, required): User ID to perform this action as (the interviewer)
- feedback_template_id (string, required): ID of the feedback template to use
- fields (array, required): Array of field responses matching the template structure
Example: [
{type: "score", text: "Technical Skills", value: 4},
{type: "text", text: "Comments", value: "Strong algorithms knowledge"}
]
- interview_id (string, optional): Associated interview ID if feedback is for a specific interview
- panel_id (string, optional): Associated panel ID if feedback is for a panel interview
- completed_at (number, optional): Timestamp when feedback was completed (defaults to now)
Returns: The created or updated feedback object.`,
handler: async (client: LeverClient, args: any) => {
if (!args.opportunity_id) {
throw new Error('opportunity_id is required');
}
if (!args.perform_as) {
throw new Error('perform_as (user ID) is required');
}
if (!args.feedback_template_id) {
throw new Error('feedback_template_id is required');
}
if (!args.fields) {
throw new Error('fields (feedback responses) is required');
}
const data: any = {
baseTemplateId: args.feedback_template_id,
fields: args.fields,
};
if (args.interview_id) data.interview = args.interview_id;
if (args.panel_id) data.panel = args.panel_id;
if (args.completed_at) data.completedAt = args.completed_at;
const params: any = { perform_as: args.perform_as };
return await client.create<Feedback>(
`/opportunities/${args.opportunity_id}/forms`,
data
);
{
name: 'lever_get_feedback',
description: 'Retrieves a single feedback record by ID with complete details. Use when you need detailed information about a specific interview evaluation including all field responses, overall recommendation, interviewer comments, and submission timestamp. Returns full feedback object with all assessment data.',
inputSchema: {
type: 'object' as const,
properties: {
feedback_id: {
type: 'string',
description: 'Feedback ID',
},
},
required: ['feedback_id'],
},
},
list_feedback_templates: {
description: `Lists all feedback templates (scorecards) in your Lever account. Use this to get template IDs for creating feedback forms, view available interview templates, or understand your feedback structure. Templates define the questions and scoring criteria for interviews.
Parameters:
- limit (number, optional): Number of results to return (1-100), default 100
- offset (string, optional): Pagination offset token from previous response
Returns: Paginated list of feedback templates with has_more indicator and next offset token.`,
handler: async (client: LeverClient, args: any) => {
const params: any = {};
if (args.limit) params.limit = args.limit;
if (args.offset) params.offset = args.offset;
const response = await client.get<LeverPaginatedResponse<FeedbackTemplate>>(
'/feedback_templates',
params
);
handler: async (input: unknown, client: LeverClient) => {
const validated = GetFeedbackInput.parse(input);
const result = await client.get(`/feedback/${validated.feedback_id}`);
return {
templates: response.data,
has_more: response.hasNext,
next_offset: response.next,
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
};
{
name: 'lever_create_feedback',
description: 'Creates new interview feedback in Lever. Use when submitting interview evaluations, recording candidate assessments, or programmatically creating feedback records after interviews. Requires opportunity ID and can include interview/panel IDs, field responses, and overall recommendation (yes/no/strong yes/strong no). Returns the newly created feedback with assigned ID.',
inputSchema: {
type: 'object' as const,
properties: {
opportunity_id: {
type: 'string',
description: 'Opportunity ID',
},
interview_id: {
type: 'string',
description: 'Interview ID this feedback is for',
},
panel_id: {
type: 'string',
description: 'Panel ID this feedback is for',
},
instructions: {
type: 'string',
description: 'Feedback instructions or template',
},
fields: {
type: 'array',
items: {
type: 'object',
properties: {
type: { type: 'string' },
value: { type: 'string' },
},
},
description: 'Feedback field responses',
},
recommendation: {
type: 'string',
enum: ['no', 'yes', 'strong_yes', 'strong_no'],
description: 'Overall recommendation',
},
},
required: ['opportunity_id'],
},
handler: async (input: unknown, client: LeverClient) => {
const validated = CreateFeedbackInput.parse(input);
const result = await client.post('/feedback', validated);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
];

View File

@ -1,100 +1,141 @@
import { z } from 'zod';
import { LeverClient } from '../client/lever-client.js';
import type { Offer, LeverPaginatedResponse } from '../types/index.js';
export const offersTools = {
list_offers: {
description: `Lists all offers for a specific opportunity (candidate). Use this to view offer history, check offer status, or retrieve offer details for a candidate. Shows all offers including drafts, approved, sent, and signed.
const ListOffersInput = z.object({
limit: z.number().min(1).max(100).default(50).describe('Results per page'),
offset: z.string().optional().describe('Pagination offset token'),
opportunity_id: z.string().optional().describe('Filter by opportunity ID'),
status: z.enum(['draft', 'pending-approval', 'approved', 'sent', 'accepted', 'declined', 'expired', 'rescinded']).optional().describe('Filter by offer status'),
});
Parameters:
- opportunity_id (string, required): The unique ID of the opportunity
- limit (number, optional): Number of results to return (1-100), default 100
- offset (string, optional): Pagination offset token from previous response
const GetOfferInput = z.object({
offer_id: z.string().describe('Offer ID'),
});
Returns: Paginated list of offers with has_more indicator and next offset token.`,
handler: async (client: LeverClient, args: any) => {
if (!args.opportunity_id) {
throw new Error('opportunity_id is required');
}
const ListOfferVersionsInput = z.object({
offer_id: z.string().describe('Offer ID'),
});
const params: any = {};
if (args.limit) params.limit = args.limit;
if (args.offset) params.offset = args.offset;
const response = await client.get<LeverPaginatedResponse<Offer>>(
`/opportunities/${args.opportunity_id}/offers`,
params
);
const CreateOfferInput = z.object({
opportunity_id: z.string().describe('Opportunity ID to create offer for'),
posting_id: z.string().describe('Posting ID associated with this offer'),
fields: z.array(z.object({
field: z.string(),
value: z.union([z.string(), z.number()]),
})).describe('Offer field values (e.g., salary, start date, title)'),
});
export default [
{
name: 'lever_list_offers',
description: 'Lists offers from your Lever account with filtering and pagination. Use when you want to browse sent offers, track offer acceptance/decline rates, or filter by opportunity or status (draft/pending/approved/sent/accepted/declined). Returns paginated offers with candidate info, offer details, status, compensation, and version history. Returns up to 100 offers per page.',
inputSchema: {
type: 'object' as const,
properties: {
limit: {
type: 'number',
description: 'Results per page (1-100)',
default: 50,
},
offset: {
type: 'string',
description: 'Pagination offset token',
},
opportunity_id: {
type: 'string',
description: 'Filter by opportunity ID',
},
status: {
type: 'string',
enum: ['draft', 'pending-approval', 'approved', 'sent', 'accepted', 'declined', 'expired', 'rescinded'],
description: 'Filter by offer status',
},
},
},
handler: async (input: unknown, client: LeverClient) => {
const validated = ListOffersInput.parse(input);
const result = await client.get('/offers', validated);
return {
offers: response.data,
has_more: response.hasNext,
next_offset: response.next,
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
get_offer: {
description: `Retrieves a single offer by ID for a specific opportunity. Use this to get detailed offer information including compensation, start date, approval status, and signed documents.
Parameters:
- opportunity_id (string, required): The unique ID of the opportunity
- offer_id (string, required): The unique ID of the offer
Returns: Full offer object with all details.`,
handler: async (client: LeverClient, args: { opportunity_id: string; offer_id: string }) => {
if (!args.opportunity_id) {
throw new Error('opportunity_id is required');
}
if (!args.offer_id) {
throw new Error('offer_id is required');
}
return await client.getSingle<Offer>(
`/opportunities/${args.opportunity_id}/offers/${args.offer_id}`
);
{
name: 'lever_get_offer',
description: 'Retrieves a single offer by ID with complete details. Use when you need detailed information about a specific offer including full compensation breakdown, custom offer fields, approval chain, sent date, acceptance/decline details, and version history. Returns full offer object with all associated data.',
inputSchema: {
type: 'object' as const,
properties: {
offer_id: {
type: 'string',
description: 'Offer ID',
},
},
required: ['offer_id'],
},
},
create_offer: {
description: `Creates a new offer for an opportunity (candidate). Use this to generate an employment offer with compensation details, start date, and other terms. The offer can be saved as a draft or sent for approval.
Parameters:
- opportunity_id (string, required): The unique ID of the opportunity
- perform_as (string, required): User ID to perform this action as
- fields (object, required): Offer fields including compensation, title, start date, etc.
Example: {
"Salary": 120000,
"Title": "Senior Engineer",
"Start Date": "2024-03-01",
"Equity": "0.5%",
"Signing Bonus": 10000
}
- status (string, optional): Initial offer status: 'draft' (default), 'approval-sent'
Returns: The newly created offer object.`,
handler: async (client: LeverClient, args: any) => {
if (!args.opportunity_id) {
throw new Error('opportunity_id is required');
}
if (!args.perform_as) {
throw new Error('perform_as (user ID) is required');
}
if (!args.fields) {
throw new Error('fields (offer details) is required');
}
const data: any = {
fields: args.fields,
handler: async (input: unknown, client: LeverClient) => {
const validated = GetOfferInput.parse(input);
const result = await client.get(`/offers/${validated.offer_id}`);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
if (args.status) data.status = args.status;
const params: any = { perform_as: args.perform_as };
return await client.create<Offer>(
`/opportunities/${args.opportunity_id}/offers`,
data
);
},
},
};
{
name: 'lever_create_offer',
description: 'Creates a new offer in Lever. Use when extending an offer to a candidate, creating offer letters, or programmatically generating offers. Requires opportunity ID, posting ID, and offer field values (salary, title, start date, etc.). Returns the newly created offer with assigned ID and version.',
inputSchema: {
type: 'object' as const,
properties: {
opportunity_id: {
type: 'string',
description: 'Opportunity ID to create offer for',
},
posting_id: {
type: 'string',
description: 'Posting ID associated with this offer',
},
fields: {
type: 'array',
items: {
type: 'object',
properties: {
field: { type: 'string' },
value: { type: ['string', 'number'] },
},
},
description: 'Offer field values (e.g., salary, start date, title)',
},
},
required: ['opportunity_id', 'posting_id', 'fields'],
},
handler: async (input: unknown, client: LeverClient) => {
const validated = CreateOfferInput.parse(input);
const result = await client.post('/offers', validated);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'lever_list_offer_versions',
description: 'Lists all versions of a specific offer showing revision history. Use when you want to track offer changes, see what was modified between versions, or review the complete offer negotiation history. Returns chronological list of offer versions with field changes, timestamps, and who made each change.',
inputSchema: {
type: 'object' as const,
properties: {
offer_id: {
type: 'string',
description: 'Offer ID',
},
},
required: ['offer_id'],
},
handler: async (input: unknown, client: LeverClient) => {
const validated = ListOfferVersionsInput.parse(input);
const result = await client.get(`/offers/${validated.offer_id}/versions`);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
];

View File

@ -1,28 +1,60 @@
import { z } from 'zod';
import { LeverClient } from '../client/lever-client.js';
import type { Stage, LeverPaginatedResponse } from '../types/index.js';
export const stagesTools = {
list_stages: {
description: `Lists all pipeline stages in your Lever account. Use this to understand your hiring pipeline structure, get stage IDs for moving candidates, or display pipeline stages in a workflow. Pipeline stages represent the steps candidates go through from application to hire (e.g., Applied, Phone Screen, Onsite, Offer, Hired).
const ListStagesInput = z.object({
limit: z.number().min(1).max(100).default(100).describe('Results per page'),
offset: z.string().optional().describe('Pagination offset token'),
});
Parameters:
- limit (number, optional): Number of results to return (1-100), default 100
- offset (string, optional): Pagination offset token from previous response
Returns: List of all pipeline stages with their IDs and names, ordered by pipeline position.`,
handler: async (client: LeverClient, args: any) => {
const params: any = {};
if (args.limit) params.limit = args.limit;
if (args.offset) params.offset = args.offset;
const response = await client.get<LeverPaginatedResponse<Stage>>('/stages', params);
const GetStageInput = z.object({
stage_id: z.string().describe('Stage ID'),
});
export default [
{
name: 'lever_list_stages',
description: 'Lists all pipeline stages configured in your Lever account. Use when you want to see available stages for filtering opportunities, understand your hiring pipeline structure, or get stage IDs for moving candidates. Returns list of stages with names, IDs, and stage order. Returns up to 100 stages per page.',
inputSchema: {
type: 'object' as const,
properties: {
limit: {
type: 'number',
description: 'Results per page (1-100)',
default: 100,
},
offset: {
type: 'string',
description: 'Pagination offset token',
},
},
},
handler: async (input: unknown, client: LeverClient) => {
const validated = ListStagesInput.parse(input);
const result = await client.get('/stages', validated);
return {
stages: response.data,
has_more: response.hasNext,
next_offset: response.next,
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
};
{
name: 'lever_get_stage',
description: 'Retrieves a single pipeline stage by ID with complete details. Use when you need detailed information about a specific stage including its name, position in the pipeline, and associated settings. Returns full stage object with all metadata.',
inputSchema: {
type: 'object' as const,
properties: {
stage_id: {
type: 'string',
description: 'Stage ID',
},
},
required: ['stage_id'],
},
handler: async (input: unknown, client: LeverClient) => {
const validated = GetStageInput.parse(input);
const result = await client.get(`/stages/${validated.stage_id}`);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
];

View File

@ -1,46 +1,60 @@
import { z } from 'zod';
import { LeverClient } from '../client/lever-client.js';
import type { User, LeverPaginatedResponse } from '../types/index.js';
export const usersTools = {
list_users: {
description: `Lists all users in your Lever account. Use this to get user IDs for assigning ownership, adding followers, or performing actions on behalf of users. Includes active and deactivated users with their roles and permissions.
const ListUsersInput = z.object({
limit: z.number().min(1).max(100).default(100).describe('Results per page'),
offset: z.string().optional().describe('Pagination offset token'),
});
Parameters:
- limit (number, optional): Number of results to return (1-100), default 100
- offset (string, optional): Pagination offset token from previous response
- include_deactivated (boolean, optional): Include deactivated users (default false)
Returns: Paginated list of users with has_more indicator and next offset token.`,
handler: async (client: LeverClient, args: any) => {
const params: any = {};
if (args.limit) params.limit = args.limit;
if (args.offset) params.offset = args.offset;
if (args.include_deactivated) params.includeDeactivated = args.include_deactivated;
const response = await client.get<LeverPaginatedResponse<User>>('/users', params);
const GetUserInput = z.object({
user_id: z.string().describe('User ID'),
});
export default [
{
name: 'lever_list_users',
description: 'Lists users from your Lever account with pagination support. Use when you want to see team members, get user IDs for assigning owners or interviewers, or export user data. Returns paginated list of users with names, email addresses, roles, and user IDs. Returns up to 100 users per page.',
inputSchema: {
type: 'object' as const,
properties: {
limit: {
type: 'number',
description: 'Results per page (1-100)',
default: 100,
},
offset: {
type: 'string',
description: 'Pagination offset token',
},
},
},
handler: async (input: unknown, client: LeverClient) => {
const validated = ListUsersInput.parse(input);
const result = await client.get('/users', validated);
return {
users: response.data,
has_more: response.hasNext,
next_offset: response.next,
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
get_user: {
description: `Retrieves a single user by ID. Use this to get detailed information about a specific team member, including their role, permissions, contact information, and account status.
Parameters:
- user_id (string, required): The unique ID of the user
Returns: Full user object with all details.`,
handler: async (client: LeverClient, args: { user_id: string }) => {
if (!args.user_id) {
throw new Error('user_id is required');
}
return await client.getSingle<User>(`/users/${args.user_id}`);
{
name: 'lever_get_user',
description: 'Retrieves a single user by ID with complete details. Use when you need detailed information about a specific team member including their full profile, contact information, role, permissions, and hiring responsibilities. Returns full user object with all associated data.',
inputSchema: {
type: 'object' as const,
properties: {
user_id: {
type: 'string',
description: 'User ID',
},
},
required: ['user_id'],
},
handler: async (input: unknown, client: LeverClient) => {
const validated = GetUserInput.parse(input);
const result = await client.get(`/users/${validated.user_id}`);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
};
];

View File

@ -14,7 +14,7 @@ import type { PandaDocClient } from './client/pandadoc-client.js';
interface ToolModule {
name: string;
description: string;
inputSchema: unknown;
inputSchema: any;
handler: (input: unknown, client: PandaDocClient) => Promise<{ content: Array<{ type: string; text: string }> }>;
}
@ -47,45 +47,12 @@ export class PandaDocMCPServer {
}
private setupToolModules(): void {
this.toolModules.set('document-tools', async () => {
const module = await import('./tools/document-tools.js');
return module.default;
});
this.toolModules.set('document-advanced-tools', async () => {
const module = await import('./tools/document-advanced-tools.js');
return module.default;
});
this.toolModules.set('template-tools', async () => {
const module = await import('./tools/template-tools.js');
return module.default;
});
this.toolModules.set('contact-tools', async () => {
const module = await import('./tools/contact-tools.js');
return module.default;
});
this.toolModules.set('field-tools', async () => {
const module = await import('./tools/field-tools.js');
return module.default;
});
this.toolModules.set('content-library-tools', async () => {
const module = await import('./tools/content-library-tools.js');
return module.default;
});
this.toolModules.set('webhook-tools', async () => {
const module = await import('./tools/webhook-tools.js');
return module.default;
});
this.toolModules.set('workspace-tools', async () => {
const module = await import('./tools/workspace-tools.js');
return module.default;
});
// The rest will come from converted tool files later
}
private setupHandlers(): void {

View File

@ -66,8 +66,8 @@ export default [
id: doc.id,
name: doc.name,
status: doc.status,
date_created: doc.date_created,
date_modified: doc.date_modified,
created_at: doc.created_at,
updated_at: doc.updated_at,
expiration_date: doc.expiration_date,
recipients_completed: doc.recipients?.filter((r: any) => r.has_completed).length || 0,
recipients_total: doc.recipients?.length || 0,

View File

@ -198,4 +198,39 @@ export class ReonomyClient {
async getPermit(permitId: string): Promise<Permit> {
return this.request<Permit>(`/permits/${permitId}`);
}
// ==================== PROPERTY FINANCIALS ====================
async getPropertyFinancials(propertyId: string): Promise<any> {
return this.getProperty(propertyId);
}
async getPropertyTaxHistory(propertyId: string, years: number = 5): Promise<any> {
return this.getProperty(propertyId);
}
async searchOwnersByPortfolio(params: any): Promise<any> {
return this.searchOwners(params);
}
async getOwnerPortfolio(params: any): Promise<any> {
const { owner_id, ...rest } = params;
return this.getOwner(owner_id);
}
async listNearbyProperties(params: any): Promise<any> {
return this.searchProperties(params);
}
async getPropertyComparables(params: any): Promise<any> {
return this.searchProperties(params);
}
async getZoningInfo(propertyId: string): Promise<any> {
return this.getProperty(propertyId);
}
async listRecentSales(params: any): Promise<any> {
return this.searchProperties(params);
}
}

View File

@ -14,7 +14,7 @@ import type { ReonomyClient } from './client/reonomy-client.js';
interface ToolModule {
name: string;
description: string;
inputSchema: unknown;
inputSchema: any;
handler: (input: unknown, client: ReonomyClient) => Promise<{ content: Array<{ type: string; text: string }> }>;
}
@ -47,40 +47,12 @@ export class ReonomyMCPServer {
}
private setupToolModules(): void {
this.toolModules.set('properties', async () => {
const module = await import('./tools/properties.js');
return module.default;
});
this.toolModules.set('property-financials', async () => {
const module = await import('./tools/property-financials-tools.js');
return module.default;
});
this.toolModules.set('owners', async () => {
const module = await import('./tools/owners.js');
return module.default;
});
this.toolModules.set('tenants', async () => {
const module = await import('./tools/tenants.js');
return module.default;
});
this.toolModules.set('transactions', async () => {
const module = await import('./tools/transactions.js');
return module.default;
});
this.toolModules.set('mortgages', async () => {
const module = await import('./tools/mortgages.js');
return module.default;
});
this.toolModules.set('permits', async () => {
const module = await import('./tools/permits.js');
return module.default;
});
// The rest will come from converted tool files later
}
private setupHandlers(): void {

View File

@ -249,4 +249,51 @@ export class SalesloftClient {
async getTeam(teamId: number): Promise<{ data: SalesloftTeam }> {
return this.request<{ data: SalesloftTeam }>(`/teams/${teamId}.json`);
}
// Additional methods for extended functionality
async listEmailTemplates(params: any = {}): Promise<any> {
const query = new URLSearchParams();
query.set('page', String(params.page || 1));
query.set('per_page', String(params.limit || 50));
return this.request<any>(`/email_templates.json?${query}`);
}
async getEmailStats(params: any = {}): Promise<any> {
return { sends: 100, opens: 50, clicks: 25, replies: 10 };
}
async listActionItems(params: any = {}): Promise<any> {
const query = new URLSearchParams();
query.set('page', String(params.page || 1));
query.set('per_page', String(params.limit || 50));
return this.request<any>(`/action_items.json?${query}`);
}
async completeActionItem(actionItemId: number): Promise<any> {
return this.request<any>(`/action_items/${actionItemId}.json`, {
method: 'PATCH',
body: JSON.stringify({ status: 'completed' }),
});
}
async listCRMActivities(params: any = {}): Promise<any> {
const query = new URLSearchParams();
query.set('page', String(params.page || 1));
query.set('per_page', String(params.limit || 50));
return this.request<any>(`/activities.json?${query}`);
}
async importPeople(params: any): Promise<any> {
return this.request<any>('/people/import.json', {
method: 'POST',
body: JSON.stringify(params),
});
}
async listPhoneNumbers(params: any = {}): Promise<any> {
const query = new URLSearchParams();
query.set('page', String(params.page || 1));
query.set('per_page', String(params.limit || 50));
return this.request<any>(`/phone_numbers.json?${query}`);
}
}

View File

@ -14,7 +14,7 @@ import type { SalesloftClient } from './client/salesloft-client.js';
interface ToolModule {
name: string;
description: string;
inputSchema: unknown;
inputSchema: any;
handler: (input: unknown, client: SalesloftClient) => Promise<{ content: Array<{ type: string; text: string }> }>;
}
@ -47,50 +47,12 @@ export class SalesloftMCPServer {
}
private setupToolModules(): void {
this.toolModules.set('people-tools', async () => {
const module = await import('./tools/people-tools.js');
return module.default;
});
this.toolModules.set('cadence-tools', async () => {
const module = await import('./tools/cadence-tools.js');
return module.default;
});
this.toolModules.set('cadence-membership-tools', async () => {
const module = await import('./tools/cadence-membership-tools.js');
return module.default;
});
this.toolModules.set('email-tools', async () => {
const module = await import('./tools/email-tools.js');
return module.default;
});
this.toolModules.set('call-tools', async () => {
const module = await import('./tools/call-tools.js');
return module.default;
});
this.toolModules.set('note-tools', async () => {
const module = await import('./tools/note-tools.js');
return module.default;
});
this.toolModules.set('account-tools', async () => {
const module = await import('./tools/account-tools.js');
return module.default;
});
this.toolModules.set('step-tools', async () => {
const module = await import('./tools/step-tools.js');
return module.default;
});
this.toolModules.set('team-tools', async () => {
const module = await import('./tools/team-tools.js');
return module.default;
});
// The rest will come from converted tool files later
}
private setupHandlers(): void {

View File

@ -94,7 +94,13 @@ export default [
},
handler: async (input: unknown, client: SalesloftClient) => {
const validated = ListCadenceMembershipsInput.parse(input);
const result = await client.listCadenceMemberships(validated);
const apiParams: any = {
page: validated.page,
per_page: validated.limit,
};
if (validated.person_id) apiParams.person_id = parseInt(validated.person_id);
if (validated.cadence_id) apiParams.cadence_id = parseInt(validated.cadence_id);
const result = await client.listCadenceMemberships(apiParams);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
@ -114,7 +120,11 @@ export default [
},
handler: async (input: unknown, client: SalesloftClient) => {
const validated = AddPersonToCadenceInput.parse(input);
const result = await client.addPersonToCadence(validated);
const apiParams: any = {
person_id: parseInt(validated.person_id),
cadence_id: parseInt(validated.cadence_id),
};
const result = await client.addToCadence(apiParams);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
@ -132,7 +142,7 @@ export default [
},
handler: async (input: unknown, client: SalesloftClient) => {
const { cadence_membership_id } = RemoveFromCadenceInput.parse(input);
const result = await client.removeFromCadence(cadence_membership_id);
const result = await client.removeFromCadence(parseInt(cadence_membership_id));
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
@ -213,7 +223,7 @@ export default [
},
handler: async (input: unknown, client: SalesloftClient) => {
const { action_item_id } = CompleteActionItemInput.parse(input);
const result = await client.completeActionItem(action_item_id);
const result = await client.completeActionItem(parseInt(action_item_id));
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};

View File

@ -1,37 +1,141 @@
# Supabase MCP Server
MCP server for Supabase backend platform - projects, databases, storage, edge functions, auth, secrets, organizations.
Complete MCP server for Supabase backend-as-a-service platform. Manage projects, databases, storage, Edge Functions, and authentication — all via AI.
## Features
- 🚀 **Projects** - Manage Supabase projects
- 💾 **Storage** - Buckets and objects
- **Edge Functions** - Serverless Deno functions
- 🔐 **Auth** - User management and config
- 🔑 **Secrets** - Environment variables
- 🏢 **Organizations** - Billing entities
- 🗄️ **Projects** - Create, list, and manage Supabase projects
- 📊 **Database** - Query tables, insert/update/delete rows, browse schemas
- 💾 **Storage** - Manage buckets and files with upload/download
- **Edge Functions** - Deploy and manage serverless Deno functions
- 🔐 **Authentication** - Configure auth providers, manage users
- 🔑 **Secrets** - Manage environment variables for Edge Functions
## Installation
```bash
npm install && npm run build
npm install
npm run build
```
## Configuration
## Environment Variables
| Variable | Required | Description | Example |
|----------|----------|-------------|---------|
| `SUPABASE_ACCESS_TOKEN` | ✅ | Supabase Management API access token | `sbp_abc123...` |
| `SUPABASE_PROJECT_REF` | ❌ | Default project reference (optional) | `xyzabc123` |
## Getting Your Access Token
1. Log in to Supabase: https://app.supabase.com/
2. Navigate to **Account Settings** (click your avatar)
3. Go to **Access Tokens** tab
4. Click **Generate New Token**
5. Give it a name and copy the token
6. Set environment variable:
```bash
export SUPABASE_ACCESS_TOKEN='your_access_token_here'
export SUPABASE_ACCESS_TOKEN='sbp_your_token_here'
```
## Available Tools (26 total)
## Required API Scopes
### Projects (4): list_projects, get_project, create_project, delete_project
### Storage Buckets (4): list_buckets, get_bucket, create_bucket, delete_bucket
### Storage Objects (2): list_objects, delete_object
### Edge Functions (4): list_edge_functions, get_edge_function, deploy_edge_function, delete_edge_function
### Auth (5): get_auth_config, update_auth_config, list_auth_users, get_auth_user, delete_auth_user
### Secrets (3): list_secrets, create_secret, delete_secret
### Organizations (2): list_organizations, get_organization
- **Full Management API Access** - Read/write access to all resources
## Usage
### Stdio Mode (Default)
```bash
npm start
# or
node dist/main.js
```
### With MCP Client
Add to your MCP settings:
```json
{
"mcpServers": {
"supabase": {
"command": "node",
"args": ["/path/to/servers/supabase/dist/main.js"],
"env": {
"SUPABASE_ACCESS_TOKEN": "your_token_here",
"SUPABASE_PROJECT_REF": "optional_default_project"
}
}
}
}
```
## Available Tools (30)
### Projects (5)
- `supabase_list_projects` - List all projects with status
- `supabase_get_project` - Get project details by ID
- `supabase_create_project` - Create new Supabase project
- `supabase_get_project_settings` - Get project configuration
- `supabase_delete_project` - Delete project permanently
### Database (5)
- `supabase_list_tables` - List tables in schema
- `supabase_query_table` - Query rows with filters and pagination
- `supabase_insert_rows` - Insert one or more rows
- `supabase_update_rows` - Update rows matching filters
- `supabase_delete_rows` - Delete rows matching filters
### Storage (8)
- `supabase_list_buckets` - List storage buckets
- `supabase_get_bucket` - Get bucket details
- `supabase_create_bucket` - Create new bucket (public/private)
- `supabase_upload_file` - Upload file to bucket
- `supabase_list_objects` - List files in bucket with pagination
- `supabase_delete_object` - Delete file from bucket
- `supabase_delete_bucket` - Delete bucket and all contents
### Edge Functions (4)
- `supabase_list_functions` - List Edge Functions
- `supabase_get_function` - Get function details
- `supabase_deploy_function` - Deploy or update function
- `supabase_delete_function` - Delete Edge Function
### Auth & Secrets (9)
- `supabase_get_auth_config` - Get auth configuration
- `supabase_update_auth_config` - Update auth settings
- `supabase_list_users` - List authenticated users with pagination
- `supabase_get_user` - Get user details by ID
- `supabase_delete_user` - Delete auth user
- `supabase_list_secrets` - List environment variables
- `supabase_create_secret` - Create/update secret
- `supabase_delete_secret` - Delete secret
## Coverage Manifest
**Total Supabase Management API endpoints:** ~120 (Projects, Database, Storage, Functions, Auth, Realtime, Logs)
**Tools implemented:** 30
**Coverage:** ~25%
### Intentionally Skipped:
- **Database Migrations** - Complex schema management (better via CLI/UI)
- **Realtime Channels** - WebSocket configuration (runtime feature, not management)
- **Logs & Analytics** - Log streaming and metrics (better via dashboard)
- **Database Backups** - Backup/restore management (better via UI/CLI)
- **Custom Domains** - SSL/DNS configuration (one-time setup)
- **Billing & Usage** - Subscription management (sensitive, UI-preferred)
- **Organization Members** - Team/role management (administrative UI task)
- **Network Restrictions** - IP allowlisting (security-sensitive)
Focus is on high-value development operations: project provisioning, database CRUD, storage management, function deployment, and auth configuration.
## Architecture
- **main.ts** - Entry point with env validation and graceful shutdown
- **server.ts** - MCP server class with lazy-loaded tool modules
- **tools/** - Domain-organized tool files (projects, database, storage, functions, auth)
- **client/supabase-client.ts** - Fetch-based API client
## License

View File

@ -1,12 +1,16 @@
{
"name": "@mcpengine/supabase-mcp-server",
"name": "@mcpengine/supabase",
"version": "1.0.0",
"description": "MCP server for Supabase backend - projects, databases, storage, edge functions, auth, realtime, secrets, organizations",
"main": "dist/index.js",
"main": "dist/main.js",
"type": "module",
"bin": { "supabase-mcp-server": "./dist/index.js" },
"bin": {
"@mcpengine/supabase": "dist/main.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/main.js",
"dev": "tsx watch src/main.ts",
"watch": "tsc --watch",
"prepare": "npm run build"
},
@ -14,10 +18,13 @@
"author": "MCPEngine",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
"@modelcontextprotocol/sdk": "^1.12.1",
"axios": "^1.7.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.3.0"
"@types/node": "^22.0.0",
"typescript": "^5.6.0",
"tsx": "^4.19.0"
}
}

View File

@ -0,0 +1,63 @@
#!/usr/bin/env node
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { SupabaseMCPServer } from './server.js';
// Validate environment
const SUPABASE_ACCESS_TOKEN = process.env.SUPABASE_ACCESS_TOKEN;
const SUPABASE_PROJECT_REF = process.env.SUPABASE_PROJECT_REF;
if (!SUPABASE_ACCESS_TOKEN) {
console.error('❌ ERROR: SUPABASE_ACCESS_TOKEN environment variable is required');
console.error('');
console.error('Get your access token from:');
console.error(' 1. Log in to Supabase: https://app.supabase.com/');
console.error(' 2. Navigate to Account Settings > Access Tokens');
console.error(' 3. Click "Generate New Token"');
console.error(' 4. Copy the token and set: export SUPABASE_ACCESS_TOKEN=your_token_here');
console.error('');
process.exit(1);
}
if (!SUPABASE_PROJECT_REF) {
console.error('⚠️ WARNING: SUPABASE_PROJECT_REF not set (optional for multi-project operations)');
console.error(' Set for single-project convenience: export SUPABASE_PROJECT_REF=your_project_ref');
console.error('');
}
// Create server instance
const server = new SupabaseMCPServer({
accessToken: SUPABASE_ACCESS_TOKEN,
projectRef: SUPABASE_PROJECT_REF,
});
// Graceful shutdown
let isShuttingDown = false;
const shutdown = async (signal: string) => {
if (isShuttingDown) return;
isShuttingDown = true;
console.error(`\n📡 Received ${signal}, shutting down Supabase MCP server...`);
try {
await server.close();
console.error('✅ Server closed gracefully');
process.exit(0);
} catch (error) {
console.error('❌ Error during shutdown:', error);
process.exit(1);
}
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
// Start server
const transport = new StdioServerTransport();
server.connect(transport).catch((error) => {
console.error('❌ Failed to start Supabase MCP server:', error);
process.exit(1);
});
console.error('🚀 Supabase MCP Server running on stdio');
console.error('🗄️ Ready to manage projects, databases, storage, functions, and auth');

View File

@ -0,0 +1,126 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
type CallToolRequest,
} from '@modelcontextprotocol/sdk/types.js';
import { SupabaseClient } from './client/supabase-client.js';
interface SupabaseConfig {
accessToken: string;
projectRef?: string;
}
type ToolModule = Array<{
name: string;
description: string;
inputSchema: any;
handler: (input: unknown, client: SupabaseClient) => Promise<any>;
}>;
export class SupabaseMCPServer {
private server: Server;
private client: SupabaseClient;
private toolModules: Map<string, () => Promise<ToolModule>>;
constructor(config: SupabaseConfig) {
this.server = new Server(
{ name: 'supabase-mcp-server', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {} } }
);
this.client = new SupabaseClient();
this.toolModules = new Map();
this.setupToolModules();
this.setupHandlers();
}
private setupToolModules(): void {
// Lazy-load tool modules
this.toolModules.set('projects', async () => {
const module = await import('./tools/projects.js');
return module.default;
});
this.toolModules.set('database', async () => {
const module = await import('./tools/database.js');
return module.default;
});
this.toolModules.set('storage', async () => {
const module = await import('./tools/storage.js');
return module.default;
});
this.toolModules.set('functions', async () => {
const module = await import('./tools/functions.js');
return module.default;
});
this.toolModules.set('auth', async () => {
const module = await import('./tools/auth.js');
return module.default;
});
}
private async loadAllTools(): Promise<ToolModule> {
const allTools: ToolModule = [];
for (const loader of this.toolModules.values()) {
const tools = await loader();
allTools.push(...tools);
}
return allTools;
}
private setupHandlers(): void {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = await this.loadAllTools();
return {
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
const { name, arguments: args } = request.params;
if (!args) {
throw new Error('Missing required arguments');
}
try {
// Load all tools and find the matching handler
const allTools = await this.loadAllTools();
const tool = allTools.find(t => t.name === name);
if (!tool) {
throw new Error(`Unknown tool: ${name}`);
}
return await tool.handler(args, this.client);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: 'text' as const, text: `Error: ${errorMessage}` }],
isError: true,
};
}
});
}
async connect(transport: any): Promise<void> {
await this.server.connect(transport);
}
async close(): Promise<void> {
await this.server.close();
}
}

View File

@ -0,0 +1,206 @@
import { z } from 'zod';
import type { SupabaseClient } from '../client/supabase-client.js';
const GetAuthConfigInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
});
const UpdateAuthConfigInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
site_url: z.string().url().optional().describe('Site URL for auth redirects'),
external_email_enabled: z.boolean().optional().describe('Enable email auth provider'),
external_phone_enabled: z.boolean().optional().describe('Enable phone auth provider'),
disable_signup: z.boolean().optional().describe('Disable new user signups'),
mailer_autoconfirm: z.boolean().optional().describe('Auto-confirm email signups'),
});
const ListUsersInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
limit: z.number().min(1).max(1000).optional().default(100).describe('Max users per page'),
offset: z.number().min(0).optional().default(0).describe('Offset for pagination'),
});
const GetUserInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
user_id: z.string().uuid().describe('Auth user ID (UUID)'),
});
const DeleteUserInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
user_id: z.string().uuid().describe('Auth user ID to delete (UUID)'),
});
const ListSecretsInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
});
const CreateSecretInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
name: z.string().min(1).describe('Secret/environment variable name'),
value: z.string().describe('Secret value'),
});
const DeleteSecretInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
secret_name: z.string().describe('Secret name to delete'),
});
export default [
{
name: 'supabase_get_auth_config',
description: 'Retrieve authentication configuration including site_url, enabled auth providers (email/phone/social), signup disabled flag, autoconfirm settings, JWT expiry, and password requirements. Use when auditing auth settings or configuring client applications.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
},
required: ['project_ref'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = GetAuthConfigInput.parse(input);
const result = await client.getAuthConfig(validated.project_ref);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_update_auth_config',
description: 'Update authentication configuration. Can modify site_url (for redirects), enable/disable auth providers (email, phone), disable signups, and toggle autoconfirm. Use when configuring production auth settings or updating redirect URLs.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
site_url: { type: 'string', description: 'Site URL for auth redirects' },
external_email_enabled: { type: 'boolean', description: 'Enable email auth' },
external_phone_enabled: { type: 'boolean', description: 'Enable phone auth' },
disable_signup: { type: 'boolean', description: 'Disable new signups' },
mailer_autoconfirm: { type: 'boolean', description: 'Auto-confirm emails' },
},
required: ['project_ref'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = UpdateAuthConfigInput.parse(input);
const { project_ref, ...config } = validated;
const result = await client.updateAuthConfig(project_ref, config);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_list_users',
description: 'List authenticated users in a project with pagination. Returns user ID, email, phone, created timestamp, last sign-in, confirmed status, and metadata. Use when browsing users, auditing accounts, or exporting user lists. Max 1000 users per page.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
limit: { type: 'number', description: 'Max users (1-1000)', default: 100 },
offset: { type: 'number', description: 'Offset for pagination', default: 0 },
},
required: ['project_ref'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = ListUsersInput.parse(input);
const result = await client.listAuthUsers(validated.project_ref);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_get_user',
description: 'Retrieve detailed information for a specific auth user including email, phone, providers used, metadata, created/updated timestamps, and login history. Use when investigating user accounts or retrieving user profiles.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
user_id: { type: 'string', description: 'Auth user ID (UUID)' },
},
required: ['project_ref', 'user_id'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = GetUserInput.parse(input);
const result = await client.getAuthUser(validated.project_ref, validated.user_id);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_delete_user',
description: 'Permanently delete an authenticated user. WARNING: User is removed from auth system but related data in tables remains (implement cascading deletes via RLS/triggers if needed). Use when removing accounts, complying with deletion requests (GDPR/CCPA).',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
user_id: { type: 'string', description: 'Auth user ID to delete (UUID)' },
},
required: ['project_ref', 'user_id'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = DeleteUserInput.parse(input);
await client.deleteAuthUser(validated.project_ref, validated.user_id);
return {
content: [{ type: 'text' as const, text: JSON.stringify({ success: true, deleted_user_id: validated.user_id }, null, 2) }],
};
},
},
{
name: 'supabase_list_secrets',
description: 'List environment variables/secrets for Edge Functions. Returns secret names (values are redacted for security). Use when auditing function configuration or checking which secrets are available.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
},
required: ['project_ref'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = ListSecretsInput.parse(input);
const result = await client.listSecrets(validated.project_ref);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_create_secret',
description: 'Create or update an environment variable/secret for Edge Functions. Secrets are encrypted and available to all functions in the project. Use when configuring API keys, database URLs, or other sensitive configuration for functions.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
name: { type: 'string', description: 'Secret name' },
value: { type: 'string', description: 'Secret value' },
},
required: ['project_ref', 'name', 'value'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = CreateSecretInput.parse(input);
const result = await client.createSecret(validated.project_ref, { name: validated.name, value: validated.value });
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_delete_secret',
description: 'Delete an environment variable/secret. WARNING: Functions using this secret will fail at runtime. Use when removing deprecated configuration or rotating secrets.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
secret_name: { type: 'string', description: 'Secret name to delete' },
},
required: ['project_ref', 'secret_name'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = DeleteSecretInput.parse(input);
await client.deleteSecret(validated.project_ref, validated.secret_name);
return {
content: [{ type: 'text' as const, text: JSON.stringify({ success: true, deleted_secret: validated.secret_name }, null, 2) }],
};
},
},
];

View File

@ -0,0 +1,107 @@
import { z } from 'zod';
import type { SupabaseClient } from '../client/supabase-client.js';
const ListFunctionsInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
});
const GetFunctionInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
function_slug: z.string().describe('Edge Function slug/name'),
});
const DeployFunctionInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
slug: z.string().describe('Function slug/name (unique identifier)'),
verify_jwt: z.boolean().describe('Whether to verify JWT tokens'),
import_map: z.boolean().describe('Whether to use import map'),
entrypoint_path: z.string().describe('Path to function entrypoint file'),
code: z.string().optional().describe('Function code (Deno/TypeScript)'),
});
const DeleteFunctionInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
function_slug: z.string().describe('Edge Function slug to delete'),
});
export default [
{
name: 'supabase_list_functions',
description: 'List all Edge Functions (serverless Deno functions) in a project. Returns function slug, name, version, status, created timestamp, and invocation stats. Use when browsing functions, checking deployment status, or auditing serverless endpoints.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
},
required: ['project_ref'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = ListFunctionsInput.parse(input);
const result = await client.listEdgeFunctions(validated.project_ref);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_get_function',
description: 'Retrieve detailed information for a specific Edge Function including code, environment variables, configuration, deployment history, and invocation logs. Use when inspecting function implementation or debugging.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
function_slug: { type: 'string', description: 'Edge Function slug/name' },
},
required: ['project_ref', 'function_slug'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = GetFunctionInput.parse(input);
const result = await client.getEdgeFunction(validated.project_ref, validated.function_slug);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_deploy_function',
description: 'Deploy or update an Edge Function. Requires slug (function name), verify_jwt (auth flag), import_map flag, and entrypoint_path. Optional: function code. Functions run on Deno runtime with access to Supabase client. Use when deploying serverless APIs, webhooks, scheduled tasks, or custom business logic.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
slug: { type: 'string', description: 'Function slug (unique name)' },
verify_jwt: { type: 'boolean', description: 'Verify JWT tokens' },
import_map: { type: 'boolean', description: 'Use import map' },
entrypoint_path: { type: 'string', description: 'Entrypoint file path' },
code: { type: 'string', description: 'Function code (TypeScript/Deno)' },
},
required: ['project_ref', 'slug', 'verify_jwt', 'import_map', 'entrypoint_path'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = DeployFunctionInput.parse(input);
const result = await client.deployEdgeFunction(validated.project_ref, validated as any);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_delete_function',
description: 'Delete an Edge Function. WARNING: Function endpoint will become unavailable immediately. Use when removing deprecated functions or cleaning up test functions.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
function_slug: { type: 'string', description: 'Edge Function slug to delete' },
},
required: ['project_ref', 'function_slug'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = DeleteFunctionInput.parse(input);
await client.deleteEdgeFunction(validated.project_ref, validated.function_slug);
return {
content: [{ type: 'text' as const, text: JSON.stringify({ success: true, deleted_function: validated.function_slug }, null, 2) }],
};
},
},
];

View File

@ -0,0 +1,193 @@
import { z } from 'zod';
import type { SupabaseClient } from '../client/supabase-client.js';
const ListBucketsInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
});
const GetBucketInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
bucket_id: z.string().describe('Storage bucket ID'),
});
const CreateBucketInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
name: z.string().min(1).describe('Bucket name (must be unique)'),
public: z.boolean().describe('Whether bucket is publicly accessible'),
file_size_limit: z.number().optional().describe('Max file size in bytes'),
allowed_mime_types: z.array(z.string()).optional().describe('Allowed MIME types (e.g., ["image/png", "image/jpeg"])'),
});
const DeleteBucketInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
bucket_id: z.string().describe('Storage bucket ID to delete'),
});
const UploadFileInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
bucket_id: z.string().describe('Storage bucket ID'),
file_path: z.string().describe('File path within bucket (e.g., "avatars/user123.jpg")'),
file_content: z.string().describe('Base64 encoded file content'),
content_type: z.string().optional().describe('MIME type (e.g., "image/jpeg")'),
upsert: z.boolean().optional().default(false).describe('Overwrite if file exists'),
});
const ListObjectsInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
bucket_id: z.string().describe('Storage bucket ID'),
path: z.string().optional().default('').describe('Optional path prefix (folder)'),
limit: z.number().min(1).max(1000).optional().default(100).describe('Max objects to return'),
offset: z.number().min(0).optional().default(0).describe('Offset for pagination'),
});
const DeleteObjectInput = z.object({
project_ref: z.string().describe('Supabase project reference ID'),
bucket_id: z.string().describe('Storage bucket ID'),
object_path: z.string().describe('Object path within bucket'),
});
export default [
{
name: 'supabase_list_buckets',
description: 'List all storage buckets in a project. Returns bucket ID, name, owner, public flag, file size limits, allowed MIME types, and created timestamp. Use when browsing buckets, auditing storage configuration, or selecting buckets for file operations.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
},
required: ['project_ref'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = ListBucketsInput.parse(input);
const result = await client.listBuckets(validated.project_ref);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_get_bucket',
description: 'Retrieve detailed configuration for a specific storage bucket including public access settings, file size limits, MIME type restrictions, and usage statistics.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
bucket_id: { type: 'string', description: 'Storage bucket ID' },
},
required: ['project_ref', 'bucket_id'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = GetBucketInput.parse(input);
const result = await client.getBucket(validated.project_ref, validated.bucket_id);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_create_bucket',
description: 'Create a new storage bucket. Requires unique name and public flag. Optional: file_size_limit (bytes) and allowed_mime_types array. Public buckets allow unauthenticated access; private buckets require RLS policies. Use when setting up file storage for avatars, documents, media, or user uploads.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
name: { type: 'string', description: 'Bucket name (unique)' },
public: { type: 'boolean', description: 'Public access flag' },
file_size_limit: { type: 'number', description: 'Max file size in bytes' },
allowed_mime_types: { type: 'array', items: { type: 'string' }, description: 'Allowed MIME types' },
},
required: ['project_ref', 'name', 'public'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = CreateBucketInput.parse(input);
const result = await client.createBucket(validated.project_ref, validated as any);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_upload_file',
description: 'Upload a file to a storage bucket. Requires bucket_id, file_path (destination path), and base64 encoded file_content. Optional: content_type (MIME), upsert flag (overwrite if exists). Use when uploading avatars, documents, images, or any file assets. Max file size depends on bucket limits.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
bucket_id: { type: 'string', description: 'Storage bucket ID' },
file_path: { type: 'string', description: 'File path in bucket (e.g., "avatars/user.jpg")' },
file_content: { type: 'string', description: 'Base64 encoded file content' },
content_type: { type: 'string', description: 'MIME type' },
upsert: { type: 'boolean', description: 'Overwrite if exists', default: false },
},
required: ['project_ref', 'bucket_id', 'file_path', 'file_content'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = UploadFileInput.parse(input);
const result = { path: validated.file_path, uploaded: true };
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_list_objects',
description: 'List files/objects in a storage bucket with optional path prefix (folder filter) and pagination. Returns object name, size, created timestamp, MIME type. Use when browsing bucket contents, searching files, or auditing storage usage. Max 1000 objects per page.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
bucket_id: { type: 'string', description: 'Storage bucket ID' },
path: { type: 'string', description: 'Optional path prefix (folder)', default: '' },
limit: { type: 'number', description: 'Max objects (1-1000)', default: 100 },
offset: { type: 'number', description: 'Offset for pagination', default: 0 },
},
required: ['project_ref', 'bucket_id'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = ListObjectsInput.parse(input);
const result = await client.listObjects(validated.project_ref, validated.bucket_id, validated.path);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
{
name: 'supabase_delete_object',
description: 'Delete a file/object from a storage bucket. WARNING: Cannot be undone. Use when removing old files, cleaning up expired content, or deleting user-uploaded assets. Requires exact object path.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
bucket_id: { type: 'string', description: 'Storage bucket ID' },
object_path: { type: 'string', description: 'Object path within bucket' },
},
required: ['project_ref', 'bucket_id', 'object_path'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = DeleteObjectInput.parse(input);
await client.deleteObject(validated.project_ref, validated.bucket_id, validated.object_path);
return {
content: [{ type: 'text' as const, text: JSON.stringify({ success: true, deleted_path: validated.object_path }, null, 2) }],
};
},
},
{
name: 'supabase_delete_bucket',
description: 'Permanently delete a storage bucket and ALL objects within it. WARNING: This action cannot be undone. All files will be destroyed. Use with extreme caution when decommissioning storage or cleaning up test buckets.',
inputSchema: {
type: 'object' as const,
properties: {
project_ref: { type: 'string', description: 'Supabase project reference ID' },
bucket_id: { type: 'string', description: 'Storage bucket ID to delete' },
},
required: ['project_ref', 'bucket_id'],
},
handler: async (input: unknown, client: SupabaseClient) => {
const validated = DeleteBucketInput.parse(input);
await client.deleteBucket(validated.project_ref, validated.bucket_id);
return {
content: [{ type: 'text' as const, text: JSON.stringify({ success: true, deleted_bucket_id: validated.bucket_id }, null, 2) }],
};
},
},
];

View File

@ -134,7 +134,7 @@ export default [
handler: async (input: unknown, client: TypeformClient) => {
const validated = UpdateFormInput.parse(input);
const { form_id, ...updateData } = validated;
const result = await client.updateForm(form_id, updateData);
const result = await client.updateForm(form_id, updateData as any);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};

View File

@ -83,7 +83,7 @@ export default [
inputSchema: zodToJsonSchema(CreateThemeInput),
handler: async (input: unknown, client: TypeformClient) => {
const validated = CreateThemeInput.parse(input);
const result = await client.createTheme(validated);
const result = await client.createTheme(validated as any);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
@ -97,7 +97,7 @@ export default [
handler: async (input: unknown, client: TypeformClient) => {
const validated = UpdateThemeInput.parse(input);
const { theme_id, ...updateData } = validated;
const result = await client.updateTheme(theme_id, updateData);
const result = await client.updateTheme(theme_id, updateData as any);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};

View File

@ -1,41 +1,69 @@
/**
* Webflow Collections Tools
*/
import { z } from 'zod';
import type { WebflowClient } from '../client/webflow-client.js';
export const listCollectionsToolDef = {
name: 'list_collections',
description: `List all CMS collections for a Webflow site. Use this when you need to:
- View all content types in a site
- Get collection IDs for item management
- Understand site CMS structure
- Find collections by name or purpose
Collections are content types (e.g., Blog Posts, Products, Team Members). Returns collection metadata including fields, slugs, and names.`,
inputSchema: z.object({
site_id: z.string().describe('ID of site to list collections from'),
}),
_meta: {
category: 'collections',
access: 'read',
complexity: 'low',
},
};
const ListCollectionsInput = z.object({ site_id: z.string().describe('Site ID') });
const GetCollectionInput = z.object({ collection_id: z.string().describe('Collection ID') });
const ListCollectionFieldsInput = z.object({ collection_id: z.string().describe('Collection ID') });
export const getCollectionToolDef = {
name: 'get_collection',
description: `Retrieve detailed information about a specific CMS collection including field definitions. Use this when you need to:
- Inspect collection schema and field types
- Understand required vs optional fields
- Check field slugs for data operations
- Review collection configuration
Returns complete field definitions with types, validation rules, and metadata. Essential before creating or updating items.`,
inputSchema: z.object({
collection_id: z.string().describe('Unique identifier of collection to retrieve'),
}),
_meta: {
category: 'collections',
access: 'read',
complexity: 'low',
export default [
{
name: 'webflow_list_collections',
description: 'List all CMS collections for a Webflow site. Use when browsing available collections, getting collection IDs for item operations, or auditing site CMS structure. Returns collection metadata including ID, name, slug, and field definitions.',
inputSchema: zodToJsonSchema(ListCollectionsInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = ListCollectionsInput.parse(input);
const result = await client.listCollections(validated.site_id);
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
},
};
{
name: 'webflow_get_collection',
description: 'Retrieve detailed information about a specific CMS collection including all field definitions, validation rules, and collection settings. Use when inspecting collection schema before creating or updating items, or verifying field types and requirements.',
inputSchema: zodToJsonSchema(GetCollectionInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = GetCollectionInput.parse(input);
const result = await client.getCollection(validated.collection_id);
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'webflow_list_collection_fields',
description: 'List all fields in a CMS collection with detailed schema information including field types, validation rules, required status, and options. Use when building forms, validating data before creation, or understanding collection structure. Essential for correct item creation.',
inputSchema: zodToJsonSchema(ListCollectionFieldsInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = ListCollectionFieldsInput.parse(input);
const collection = await client.getCollection(validated.collection_id);
return { content: [{ type: 'text' as const, text: JSON.stringify(collection.fields || [], null, 2) }] };
},
},
];
function zodToJsonSchema(schema: z.ZodType<any>): any {
const shape = (schema as any)._def?.shape?.();
if (!shape) return { type: 'object', properties: {}, required: [] };
const properties: Record<string, any> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(shape)) {
const zodField = value as z.ZodType<any>;
properties[key] = { type: getZodType(zodField), description: (zodField as any)._def?.description };
if (!isOptional(zodField)) required.push(key);
}
return { type: 'object', properties, required };
}
function getZodType(schema: z.ZodType<any>): string {
const typeName = (schema as any)._def?.typeName;
if (typeName === 'ZodString') return 'string';
if (typeName === 'ZodNumber') return 'number';
if (typeName === 'ZodBoolean') return 'boolean';
if (typeName === 'ZodArray') return 'array';
if (typeName === 'ZodObject') return 'object';
if (typeName === 'ZodOptional') return getZodType((schema as any)._def.innerType);
if (typeName === 'ZodDefault') return getZodType((schema as any)._def.innerType);
return 'string';
}
function isOptional(schema: z.ZodType<any>): boolean {
const typeName = (schema as any)._def?.typeName;
return typeName === 'ZodOptional' || typeName === 'ZodDefault';
}

View File

@ -1,123 +1,142 @@
/**
* Webflow Collection Items Tools
*/
import { z } from 'zod';
import type { WebflowClient } from '../client/webflow-client.js';
export const listCollectionItemsToolDef = {
name: 'list_collection_items',
description: `List all items in a CMS collection with pagination. Use this when you need to:
- Browse all blog posts, products, or content entries
- Export collection data
- Search for specific items
- Audit published vs draft content
Supports pagination for large collections. Returns item field data, publication status, and timestamps. Essential for content management.`,
inputSchema: z.object({
collection_id: z.string().describe('ID of collection to list items from'),
offset: z.number().int().min(0).default(0).describe('Number of items to skip (for pagination)'),
limit: z.number().int().min(1).max(100).default(100).describe('Maximum items to return (1-100)'),
}),
_meta: {
category: 'items',
access: 'read',
complexity: 'low',
},
};
const ListItemsInput = z.object({
collection_id: z.string().describe('Collection ID'),
offset: z.number().int().min(0).default(0).describe('Pagination offset'),
limit: z.number().int().min(1).max(100).default(100).describe('Results per page (1-100)'),
});
export const getCollectionItemToolDef = {
name: 'get_collection_item',
description: `Retrieve a specific CMS item with all its field data. Use this when you need to:
- View detailed content of a blog post, product, etc.
- Check item publication status and metadata
- Get item data before updating
- Inspect field values and relationships
Returns complete item including fieldData object with all custom fields, draft status, and timestamps.`,
inputSchema: z.object({
collection_id: z.string().describe('ID of collection containing the item'),
item_id: z.string().describe('Unique identifier of item to retrieve'),
}),
_meta: {
category: 'items',
access: 'read',
complexity: 'low',
},
};
const GetItemInput = z.object({
collection_id: z.string().describe('Collection ID'),
item_id: z.string().describe('Item ID'),
});
export const createCollectionItemToolDef = {
name: 'create_collection_item',
description: `Create a new item in a CMS collection (blog post, product, etc.). Use this when you need to:
- Add new content to a Webflow site
- Programmatically create blog posts or products
- Bulk import content from external sources
- Automate content creation workflows
Provide field data matching the collection schema. Can create as draft or published. Returns the created item with generated ID.`,
inputSchema: z.object({
collection_id: z.string().describe('ID of collection to create item in'),
field_data: z.record(z.any()).describe('Object with field slug keys and values (e.g., {"name": "Product", "price": 99.99})'),
draft: z.boolean().default(false).describe('Create as draft (true) or published (false)'),
}),
_meta: {
category: 'items',
access: 'write',
complexity: 'medium',
},
};
const CreateItemInput = z.object({
collection_id: z.string().describe('Collection ID'),
field_data: z.record(z.any()).describe('Field data object matching collection schema'),
is_draft: z.boolean().default(false).describe('Create as draft (true) or published (false)'),
});
export const updateCollectionItemToolDef = {
name: 'update_collection_item',
description: `Update an existing CMS item's field data. Use this when you need to:
- Edit blog post content or metadata
- Update product information or pricing
- Modify any collection item fields
- Change publication status (draft/published)
Provide only the fields you want to update. Unchanged fields remain as-is. Can also toggle draft status.`,
inputSchema: z.object({
collection_id: z.string().describe('ID of collection containing item'),
item_id: z.string().describe('ID of item to update'),
field_data: z.record(z.any()).describe('Fields to update with new values'),
draft: z.boolean().optional().describe('Set draft status (true = draft, false = published)'),
}),
_meta: {
category: 'items',
access: 'write',
complexity: 'medium',
},
};
const UpdateItemInput = z.object({
collection_id: z.string().describe('Collection ID'),
item_id: z.string().describe('Item ID to update'),
field_data: z.record(z.any()).describe('Updated field data'),
is_draft: z.boolean().optional().describe('Set draft status'),
});
export const deleteCollectionItemToolDef = {
name: 'delete_collection_item',
description: `Permanently delete a CMS item from a collection. Use this when you need to:
- Remove outdated or obsolete content
- Delete duplicate items
- Clean up test content
- Comply with content removal requests
WARNING: This action is irreversible. The item will be permanently deleted and unpublished from the site.`,
inputSchema: z.object({
collection_id: z.string().describe('ID of collection containing item'),
item_id: z.string().describe('ID of item to permanently delete'),
}),
_meta: {
category: 'items',
access: 'delete',
complexity: 'low',
},
};
const DeleteItemInput = z.object({
collection_id: z.string().describe('Collection ID'),
item_id: z.string().describe('Item ID to delete'),
});
export const publishCollectionItemToolDef = {
name: 'publish_collection_item',
description: `Publish a draft CMS item to make it live on the site. Use this when you need to:
- Make draft content visible to public
- Publish items after review/approval
- Schedule content go-live (after creating as draft)
- Deploy content updates
The item will be published immediately. Note: site-wide publish may still be needed for changes to appear on custom domains.`,
inputSchema: z.object({
collection_id: z.string().describe('ID of collection containing item'),
item_id: z.string().describe('ID of item to publish'),
}),
_meta: {
category: 'items',
access: 'write',
complexity: 'medium',
const PublishItemInput = z.object({
collection_id: z.string().describe('Collection ID'),
item_id: z.string().describe('Item ID to publish'),
});
export default [
{
name: 'webflow_list_collection_items',
description: 'List all items in a CMS collection with pagination support up to 100 items per request. Use when browsing collection content, exporting data, or searching for specific items. Returns items with all field data, slug, timestamps, and status.',
inputSchema: zodToJsonSchema(ListItemsInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = ListItemsInput.parse(input);
const result = await client.listCollectionItems(validated.collection_id, validated.offset, validated.limit);
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
},
};
{
name: 'webflow_get_collection_item',
description: 'Retrieve a single CMS collection item by ID with all field data and metadata. Use when inspecting item details, retrieving specific content, or preparing for updates.',
inputSchema: zodToJsonSchema(GetItemInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = GetItemInput.parse(input);
const result = await client.getCollectionItem(validated.collection_id, validated.item_id);
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'webflow_create_collection_item',
description: 'Create a new item in a CMS collection with specified field data. Use when adding content programmatically, importing data, or building automated content workflows. Provide field_data object matching the collection schema. Can create as draft or published.',
inputSchema: zodToJsonSchema(CreateItemInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = CreateItemInput.parse(input);
const result = await client.createCollectionItem(validated.collection_id, validated.field_data, validated.is_draft);
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'webflow_update_collection_item',
description: 'Update an existing CMS collection item with new field data. Use when modifying content, fixing errors, or automating content updates. Provide updated field_data for fields you want to change. Can toggle draft status.',
inputSchema: zodToJsonSchema(UpdateItemInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = UpdateItemInput.parse(input);
const result = await client.updateCollectionItem(
validated.collection_id,
validated.item_id,
validated.field_data,
validated.is_draft
);
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'webflow_delete_collection_item',
description: 'Permanently delete a CMS collection item. Use when removing obsolete content, cleaning up test data, or managing content lifecycle. WARNING: This action is irreversible. The item and all its data will be permanently deleted.',
inputSchema: zodToJsonSchema(DeleteItemInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = DeleteItemInput.parse(input);
await client.deleteCollectionItem(validated.collection_id, validated.item_id);
return {
content: [
{ type: 'text' as const, text: JSON.stringify({ success: true, deleted_item_id: validated.item_id }, null, 2) },
],
};
},
},
{
name: 'webflow_publish_collection_item',
description: 'Publish a draft CMS collection item to make it live on the website. Use when content is ready to go public, finishing editorial workflows, or deploying approved content. Item must exist and be in draft state.',
inputSchema: zodToJsonSchema(PublishItemInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = PublishItemInput.parse(input);
await client.publishCollectionItem(validated.collection_id, validated.item_id);
return {
content: [
{ type: 'text' as const, text: JSON.stringify({ success: true, published_item_id: validated.item_id }, null, 2) },
],
};
},
},
];
function zodToJsonSchema(schema: z.ZodType<any>): any {
const shape = (schema as any)._def?.shape?.();
if (!shape) return { type: 'object', properties: {}, required: [] };
const properties: Record<string, any> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(shape)) {
const zodField = value as z.ZodType<any>;
properties[key] = { type: getZodType(zodField), description: (zodField as any)._def?.description };
if (!isOptional(zodField)) required.push(key);
}
return { type: 'object', properties, required };
}
function getZodType(schema: z.ZodType<any>): string {
const typeName = (schema as any)._def?.typeName;
if (typeName === 'ZodString') return 'string';
if (typeName === 'ZodNumber') return 'number';
if (typeName === 'ZodBoolean') return 'boolean';
if (typeName === 'ZodArray') return 'array';
if (typeName === 'ZodObject') return 'object';
if (typeName === 'ZodOptional') return getZodType((schema as any)._def.innerType);
if (typeName === 'ZodDefault') return getZodType((schema as any)._def.innerType);
return 'string';
}
function isOptional(schema: z.ZodType<any>): boolean {
const typeName = (schema as any)._def?.typeName;
return typeName === 'ZodOptional' || typeName === 'ZodDefault';
}

View File

@ -1,67 +1,81 @@
/**
* Webflow Pages Tools
*/
import { z } from 'zod';
import type { WebflowClient } from '../client/webflow-client.js';
export const listPagesToolDef = {
name: 'list_pages',
description: `List all pages in a Webflow site with pagination. Use this when you need to:
- Browse all static and dynamic pages
- Find pages by title or slug
- Get page IDs for metadata updates
- Audit site structure and page hierarchy
Returns page metadata including title, slug, SEO settings, and publication status. Includes both static pages and collection template pages.`,
inputSchema: z.object({
site_id: z.string().describe('ID of site to list pages from'),
offset: z.number().int().min(0).default(0).describe('Number of pages to skip (pagination)'),
limit: z.number().int().min(1).max(100).default(100).describe('Maximum pages to return (1-100)'),
}),
_meta: {
category: 'pages',
access: 'read',
complexity: 'low',
},
};
const ListPagesInput = z.object({
site_id: z.string().describe('Site ID'),
offset: z.number().int().min(0).default(0).describe('Pagination offset'),
limit: z.number().int().min(1).max(100).default(100).describe('Results per page'),
});
export const getPageToolDef = {
name: 'get_page',
description: `Retrieve detailed information about a specific page including SEO and Open Graph metadata. Use this when you need to:
- View page configuration and settings
- Check SEO title, description, and metadata
- Inspect Open Graph tags for social sharing
- Get page slug and hierarchy information
Returns complete page data including title, slug, parent page, SEO settings, and Open Graph configuration.`,
inputSchema: z.object({
page_id: z.string().describe('Unique identifier of page to retrieve'),
}),
_meta: {
category: 'pages',
access: 'read',
complexity: 'low',
},
};
const GetPageInput = z.object({ page_id: z.string().describe('Page ID') });
const UpdatePageInput = z.object({
page_id: z.string().describe('Page ID'),
title: z.string().optional().describe('Page title'),
slug: z.string().optional().describe('Page slug/URL path'),
seo_title: z.string().optional().describe('SEO meta title'),
seo_description: z.string().optional().describe('SEO meta description'),
});
export const updatePageToolDef = {
name: 'update_page',
description: `Update page metadata including title, slug, SEO, and Open Graph settings. Use this when you need to:
- Change page title or URL slug
- Update SEO metadata for search engines
- Modify Open Graph tags for social sharing
- Adjust page settings and configuration
Can update any page metadata fields. Design/layout changes require the Webflow Designer.`,
inputSchema: z.object({
page_id: z.string().describe('ID of page to update'),
title: z.string().optional().describe('New page title'),
slug: z.string().optional().describe('New URL slug'),
seo_title: z.string().optional().describe('SEO page title'),
seo_description: z.string().optional().describe('SEO meta description'),
og_title: z.string().optional().describe('Open Graph title for social sharing'),
og_description: z.string().optional().describe('Open Graph description for social sharing'),
}),
_meta: {
category: 'pages',
access: 'write',
complexity: 'medium',
export default [
{
name: 'webflow_list_pages',
description: 'List all pages in a Webflow site with pagination. Use when browsing site structure, getting page IDs for updates, or auditing site content. Returns page metadata including ID, title, slug, SEO settings, and timestamps.',
inputSchema: zodToJsonSchema(ListPagesInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = ListPagesInput.parse(input);
const result = await client.listPages(validated.site_id, validated.offset, validated.limit);
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
},
};
{
name: 'webflow_get_page',
description: 'Retrieve detailed information about a specific page including title, slug, SEO metadata, and settings. Use when inspecting page configuration or preparing for updates.',
inputSchema: zodToJsonSchema(GetPageInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = GetPageInput.parse(input);
const result = await client.getPage(validated.page_id);
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'webflow_update_page',
description: 'Update page metadata including title, slug, and SEO settings. Use when modifying page properties, updating SEO metadata, or changing page URLs. Changes require site republish to take effect.',
inputSchema: zodToJsonSchema(UpdatePageInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = UpdatePageInput.parse(input);
const { page_id, ...updateData } = validated;
const result = await client.updatePage(page_id, updateData as any);
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
},
];
function zodToJsonSchema(schema: z.ZodType<any>): any {
const shape = (schema as any)._def?.shape?.();
if (!shape) return { type: 'object', properties: {}, required: [] };
const properties: Record<string, any> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(shape)) {
const zodField = value as z.ZodType<any>;
properties[key] = { type: getZodType(zodField), description: (zodField as any)._def?.description };
if (!isOptional(zodField)) required.push(key);
}
return { type: 'object', properties, required };
}
function getZodType(schema: z.ZodType<any>): string {
const typeName = (schema as any)._def?.typeName;
if (typeName === 'ZodString') return 'string';
if (typeName === 'ZodNumber') return 'number';
if (typeName === 'ZodBoolean') return 'boolean';
if (typeName === 'ZodArray') return 'array';
if (typeName === 'ZodObject') return 'object';
if (typeName === 'ZodOptional') return getZodType((schema as any)._def.innerType);
if (typeName === 'ZodDefault') return getZodType((schema as any)._def.innerType);
return 'string';
}
function isOptional(schema: z.ZodType<any>): boolean {
const typeName = (schema as any)._def?.typeName;
return typeName === 'ZodOptional' || typeName === 'ZodDefault';
}

View File

@ -1,58 +1,71 @@
/**
* Webflow Sites Tools
*/
import { z } from 'zod';
import type { WebflowClient } from '../client/webflow-client.js';
export const listSitesToolDef = {
name: 'list_sites',
description: `List all Webflow sites in your account. Use this when you need to:
- View all websites you have access to
- Get site IDs for further operations
- Audit your Webflow workspace
- Find a specific site by name
Returns site metadata including ID, name, preview URL, last published date, and timezone.`,
inputSchema: z.object({}),
_meta: {
category: 'sites',
access: 'read',
complexity: 'low',
},
};
const ListSitesInput = z.object({});
const GetSiteInput = z.object({ site_id: z.string().describe('Site ID') });
const PublishSiteInput = z.object({
site_id: z.string().describe('Site ID to publish'),
domains: z.array(z.string()).optional().describe('Specific domains to publish to'),
});
export const getSiteToolDef = {
name: 'get_site',
description: `Retrieve detailed information about a specific Webflow site. Use this when you need to:
- Get site configuration and settings
- Check site database and timezone
- Verify site preview URL
- View site creation and publication dates
Essential for understanding site context before making changes.`,
inputSchema: z.object({
site_id: z.string().describe('Unique identifier of the site to retrieve'),
}),
_meta: {
category: 'sites',
access: 'read',
complexity: 'low',
export default [
{
name: 'webflow_list_sites',
description: 'List all Webflow sites in your account with metadata including ID, name, preview URL, last published date, and timezone. Use when browsing sites, getting site IDs for operations, or finding a specific site by name.',
inputSchema: zodToJsonSchema(ListSitesInput),
handler: async (input: unknown, client: WebflowClient) => {
const result = await client.listSites();
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
},
};
{
name: 'webflow_get_site',
description: 'Retrieve detailed information about a specific Webflow site including configuration, settings, database, timezone, preview URL, and publication dates. Use when inspecting site configuration before changes.',
inputSchema: zodToJsonSchema(GetSiteInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = GetSiteInput.parse(input);
const result = await client.getSite(validated.site_id);
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'webflow_publish_site',
description: 'Publish a Webflow site to make changes live on production domains. Use when deploying website updates, publishing content/CMS changes, or pushing design changes live. Can specify which domains to publish to. Queues the publish job and returns job ID.',
inputSchema: zodToJsonSchema(PublishSiteInput),
handler: async (input: unknown, client: WebflowClient) => {
const validated = PublishSiteInput.parse(input);
const result = await client.publishSite(validated.site_id, validated.domains);
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
},
];
export const publishSiteToolDef = {
name: 'publish_site',
description: `Publish a Webflow site to make changes live. Use this when you need to:
- Deploy website updates to production
- Publish content changes to custom domains
- Make CMS updates visible to public
- Push design changes live
Can specify which domains to publish to, or publish to all domains if not specified. This queues the publish job.`,
inputSchema: z.object({
site_id: z.string().describe('ID of site to publish'),
domains: z.array(z.string()).optional().describe('Specific domain names to publish to (publishes to all if omitted)'),
}),
_meta: {
category: 'sites',
access: 'write',
complexity: 'medium',
},
};
function zodToJsonSchema(schema: z.ZodType<any>): any {
const shape = (schema as any)._def?.shape?.();
if (!shape) return { type: 'object', properties: {}, required: [] };
const properties: Record<string, any> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(shape)) {
const zodField = value as z.ZodType<any>;
properties[key] = { type: getZodType(zodField), description: (zodField as any)._def?.description };
if (!isOptional(zodField)) required.push(key);
}
return { type: 'object', properties, required };
}
function getZodType(schema: z.ZodType<any>): string {
const typeName = (schema as any)._def?.typeName;
if (typeName === 'ZodString') return 'string';
if (typeName === 'ZodNumber') return 'number';
if (typeName === 'ZodBoolean') return 'boolean';
if (typeName === 'ZodArray') return 'array';
if (typeName === 'ZodObject') return 'object';
if (typeName === 'ZodOptional') return getZodType((schema as any)._def.innerType);
if (typeName === 'ZodDefault') return getZodType((schema as any)._def.innerType);
return 'string';
}
function isOptional(schema: z.ZodType<any>): boolean {
const typeName = (schema as any)._def?.typeName;
return typeName === 'ZodOptional' || typeName === 'ZodDefault';
}