V3 Batch 1 Foundation: Shopify, Stripe, QuickBooks, HubSpot, Salesforce - types + clients + server + main, zero TSC errors
This commit is contained in:
parent
6741068aef
commit
6ff76669a9
3
servers/hubspot/.env.example
Normal file
3
servers/hubspot/.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# HubSpot Private App Access Token
|
||||||
|
# Create at: https://app.hubspot.com/private-apps/{your-account-id}
|
||||||
|
HUBSPOT_ACCESS_TOKEN=your-token-here
|
||||||
8
servers/hubspot/.gitignore
vendored
Normal file
8
servers/hubspot/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
coverage/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
203
servers/hubspot/README.md
Normal file
203
servers/hubspot/README.md
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
# HubSpot MCP Server
|
||||||
|
|
||||||
|
Model Context Protocol (MCP) server for HubSpot CRM API v3. Provides comprehensive access to HubSpot CRM objects, marketing tools, CMS, and analytics.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **CRM Objects**: Contacts, Companies, Deals, Tickets, Line Items, Products, Quotes
|
||||||
|
- **Associations**: Manage relationships between objects
|
||||||
|
- **Marketing**: Email campaigns, forms, lists, workflows
|
||||||
|
- **CMS**: Blog posts, pages, HubDB tables
|
||||||
|
- **Engagements**: Notes, calls, emails, meetings, tasks
|
||||||
|
- **Pipelines & Stages**: Deal and ticket pipeline management
|
||||||
|
- **Search API**: Advanced filtering and search across all objects
|
||||||
|
- **Batch Operations**: Efficient bulk create/update/archive
|
||||||
|
- **Rate Limiting**: Automatic retry with exponential backoff
|
||||||
|
- **Pagination**: Automatic handling of cursor-based pagination
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create a `.env` file (see `.env.example`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HUBSPOT_ACCESS_TOKEN=your-private-app-token-here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting a HubSpot Access Token
|
||||||
|
|
||||||
|
1. Go to your HubSpot account settings
|
||||||
|
2. Navigate to **Integrations** → **Private Apps**
|
||||||
|
3. Click **Create a private app**
|
||||||
|
4. Configure the required scopes:
|
||||||
|
- `crm.objects.contacts.read` / `crm.objects.contacts.write`
|
||||||
|
- `crm.objects.companies.read` / `crm.objects.companies.write`
|
||||||
|
- `crm.objects.deals.read` / `crm.objects.deals.write`
|
||||||
|
- `crm.objects.owners.read`
|
||||||
|
- Additional scopes based on your needs
|
||||||
|
5. Copy the generated access token
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### MCP Configuration
|
||||||
|
|
||||||
|
Add to your MCP client configuration (e.g., Claude Desktop):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"hubspot": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/path/to/hubspot/dist/main.js"],
|
||||||
|
"env": {
|
||||||
|
"HUBSPOT_ACCESS_TOKEN": "your-token-here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
### Contacts
|
||||||
|
|
||||||
|
- `hubspot_contacts_list` - List all contacts with pagination
|
||||||
|
- `hubspot_contacts_get` - Get a contact by ID
|
||||||
|
- `hubspot_contacts_create` - Create a new contact
|
||||||
|
- `hubspot_contacts_update` - Update contact properties
|
||||||
|
- `hubspot_contacts_search` - Search contacts with filters
|
||||||
|
|
||||||
|
### Companies
|
||||||
|
|
||||||
|
- `hubspot_companies_list` - List all companies
|
||||||
|
- `hubspot_companies_get` - Get a company by ID
|
||||||
|
- `hubspot_companies_create` - Create a new company
|
||||||
|
|
||||||
|
### Deals
|
||||||
|
|
||||||
|
- `hubspot_deals_list` - List all deals
|
||||||
|
- `hubspot_deals_get` - Get a deal by ID
|
||||||
|
- `hubspot_deals_create` - Create a new deal
|
||||||
|
|
||||||
|
### Tickets
|
||||||
|
|
||||||
|
- `hubspot_tickets_list` - List all tickets
|
||||||
|
- `hubspot_tickets_create` - Create a new ticket
|
||||||
|
|
||||||
|
### Associations
|
||||||
|
|
||||||
|
- `hubspot_associations_get` - Get associations for an object
|
||||||
|
- `hubspot_associations_create` - Create an association between objects
|
||||||
|
|
||||||
|
### Pipelines
|
||||||
|
|
||||||
|
- `hubspot_pipelines_list` - List pipelines for an object type
|
||||||
|
|
||||||
|
### Owners
|
||||||
|
|
||||||
|
- `hubspot_owners_list` - List all owners in the account
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
- `hubspot_properties_list` - List properties for an object type
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **`src/types/index.ts`** - Comprehensive TypeScript type definitions for all HubSpot entities
|
||||||
|
2. **`src/clients/hubspot.ts`** - API client with retry logic, rate limiting, and pagination
|
||||||
|
3. **`src/server.ts`** - MCP server implementation with lazy-loaded tool modules
|
||||||
|
4. **`src/main.ts`** - Entry point with environment validation and graceful shutdown
|
||||||
|
|
||||||
|
### API Client Features
|
||||||
|
|
||||||
|
- **Authentication**: Bearer token (Private App or OAuth)
|
||||||
|
- **Rate Limiting**: Automatic handling of 429 responses with retry-after headers
|
||||||
|
- **Retry Logic**: Exponential backoff for transient failures (max 3 retries)
|
||||||
|
- **Pagination**: Automatic cursor-based pagination (HubSpot uses `after` cursor)
|
||||||
|
- **Batch Operations**: Efficient bulk operations for create/update/archive
|
||||||
|
- **Type Safety**: Full TypeScript type definitions for all API entities
|
||||||
|
|
||||||
|
### HubSpot API Limits
|
||||||
|
|
||||||
|
- **Free/Starter**: 10 requests/second
|
||||||
|
- **Professional/Enterprise**: Higher limits based on tier
|
||||||
|
- **Search API**: Up to 10,000 results per search
|
||||||
|
- **Batch API**: Up to 100 objects per batch request
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The server provides structured error responses:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Error: HubSpot API Error: {message} ({status}) [{correlationId}]'
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Type Safety
|
||||||
|
|
||||||
|
All HubSpot entities use branded types for IDs:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type ContactId = string & { readonly __brand: 'ContactId' };
|
||||||
|
type CompanyId = string & { readonly __brand: 'CompanyId' };
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding New Tools
|
||||||
|
|
||||||
|
1. Add tool definition in `src/server.ts` → `ListToolsRequestSchema` handler
|
||||||
|
2. Add routing in `handleToolCall` method
|
||||||
|
3. Implement handler method (or lazy-load from separate module)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test # (tests not yet implemented)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Checking
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [HubSpot API Documentation](https://developers.hubspot.com/docs/api/overview)
|
||||||
|
- [HubSpot CRM API v3](https://developers.hubspot.com/docs/api/crm/understanding-the-crm)
|
||||||
|
- [Model Context Protocol](https://modelcontextprotocol.io)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and feature requests, please open an issue on GitHub.
|
||||||
21
servers/hubspot/package.json
Normal file
21
servers/hubspot/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@mcpengine/hubspot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"dev": "tsx watch src/main.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
|
"axios": "^1.7.0",
|
||||||
|
"zod": "^3.23.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"tsx": "^4.19.0",
|
||||||
|
"@types/node": "^22.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
457
servers/hubspot/src/clients/hubspot.ts
Normal file
457
servers/hubspot/src/clients/hubspot.ts
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
/**
|
||||||
|
* HubSpot API Client
|
||||||
|
* Handles authentication, pagination, rate limiting, and batch operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
|
||||||
|
import type {
|
||||||
|
HubSpotObject,
|
||||||
|
PagedResponse,
|
||||||
|
SearchRequest,
|
||||||
|
SearchResponse,
|
||||||
|
BatchReadRequest,
|
||||||
|
BatchCreateRequest,
|
||||||
|
BatchUpdateRequest,
|
||||||
|
BatchArchiveRequest,
|
||||||
|
BatchResponse,
|
||||||
|
HubSpotError,
|
||||||
|
AssociationResult,
|
||||||
|
BatchAssociation,
|
||||||
|
} from '../types/index.js';
|
||||||
|
|
||||||
|
export interface HubSpotClientConfig {
|
||||||
|
accessToken: string;
|
||||||
|
baseURL?: string;
|
||||||
|
maxRetries?: number;
|
||||||
|
retryDelay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HubSpotClient {
|
||||||
|
private client: AxiosInstance;
|
||||||
|
private maxRetries: number;
|
||||||
|
private retryDelay: number;
|
||||||
|
|
||||||
|
constructor(config: HubSpotClientConfig) {
|
||||||
|
this.maxRetries = config.maxRetries ?? 3;
|
||||||
|
this.retryDelay = config.retryDelay ?? 1000;
|
||||||
|
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: config.baseURL ?? 'https://api.hubapi.com',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response interceptor for error handling
|
||||||
|
this.client.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
async (error: AxiosError<HubSpotError>) => {
|
||||||
|
if (error.response?.status === 429) {
|
||||||
|
// Rate limit hit - implement exponential backoff
|
||||||
|
const retryAfter = parseInt(error.response.headers['retry-after'] || '1', 10);
|
||||||
|
await this.sleep(retryAfter * 1000);
|
||||||
|
return this.client.request(error.config!);
|
||||||
|
}
|
||||||
|
throw this.handleError(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic request with retry logic
|
||||||
|
*/
|
||||||
|
private async request<T>(
|
||||||
|
config: AxiosRequestConfig,
|
||||||
|
retries = 0
|
||||||
|
): Promise<T> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.request<T>(config);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (retries < this.maxRetries && this.isRetryableError(error)) {
|
||||||
|
await this.sleep(this.retryDelay * Math.pow(2, retries));
|
||||||
|
return this.request<T>(config, retries + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if error is retryable
|
||||||
|
*/
|
||||||
|
private isRetryableError(error: any): boolean {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const status = error.response?.status;
|
||||||
|
return (
|
||||||
|
status === 429 || // Rate limit
|
||||||
|
status === 500 || // Server error
|
||||||
|
status === 502 || // Bad gateway
|
||||||
|
status === 503 || // Service unavailable
|
||||||
|
status === 504 // Gateway timeout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle and format errors
|
||||||
|
*/
|
||||||
|
private handleError(error: AxiosError<HubSpotError>): Error {
|
||||||
|
if (error.response?.data) {
|
||||||
|
const hubspotError = error.response.data;
|
||||||
|
return new Error(
|
||||||
|
`HubSpot API Error: ${hubspotError.message} (${hubspotError.status}) [${hubspotError.correlationId}]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Error(`HubSpot API Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sleep utility for retry delays
|
||||||
|
*/
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== CRM Objects API =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single object by ID
|
||||||
|
*/
|
||||||
|
async getObject<T extends Record<string, any>>(
|
||||||
|
objectType: string,
|
||||||
|
objectId: string,
|
||||||
|
properties?: string[],
|
||||||
|
associations?: string[]
|
||||||
|
): Promise<HubSpotObject<T>> {
|
||||||
|
const params: Record<string, any> = {};
|
||||||
|
if (properties?.length) {
|
||||||
|
params.properties = properties.join(',');
|
||||||
|
}
|
||||||
|
if (associations?.length) {
|
||||||
|
params.associations = associations.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.request<HubSpotObject<T>>({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/crm/v3/objects/${objectType}/${objectId}`,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List objects with pagination
|
||||||
|
*/
|
||||||
|
async listObjects<T extends Record<string, any>>(
|
||||||
|
objectType: string,
|
||||||
|
options?: {
|
||||||
|
limit?: number;
|
||||||
|
after?: string;
|
||||||
|
properties?: string[];
|
||||||
|
associations?: string[];
|
||||||
|
}
|
||||||
|
): Promise<PagedResponse<HubSpotObject<T>>> {
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
limit: Math.min(options?.limit ?? 100, 100),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options?.after) {
|
||||||
|
params.after = options.after;
|
||||||
|
}
|
||||||
|
if (options?.properties?.length) {
|
||||||
|
params.properties = options.properties.join(',');
|
||||||
|
}
|
||||||
|
if (options?.associations?.length) {
|
||||||
|
params.associations = options.associations.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.request<PagedResponse<HubSpotObject<T>>>({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/crm/v3/objects/${objectType}`,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new object
|
||||||
|
*/
|
||||||
|
async createObject<T extends Record<string, any>>(
|
||||||
|
objectType: string,
|
||||||
|
properties: T,
|
||||||
|
associations?: Array<{ to: { id: string }; types: Array<{ associationCategory: string; associationTypeId: number }> }>
|
||||||
|
): Promise<HubSpotObject<T>> {
|
||||||
|
return this.request<HubSpotObject<T>>({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/crm/v3/objects/${objectType}`,
|
||||||
|
data: {
|
||||||
|
properties,
|
||||||
|
associations,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing object
|
||||||
|
*/
|
||||||
|
async updateObject<T extends Record<string, any>>(
|
||||||
|
objectType: string,
|
||||||
|
objectId: string,
|
||||||
|
properties: Partial<T>
|
||||||
|
): Promise<HubSpotObject<T>> {
|
||||||
|
return this.request<HubSpotObject<T>>({
|
||||||
|
method: 'PATCH',
|
||||||
|
url: `/crm/v3/objects/${objectType}/${objectId}`,
|
||||||
|
data: { properties },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archive (soft delete) an object
|
||||||
|
*/
|
||||||
|
async archiveObject(objectType: string, objectId: string): Promise<void> {
|
||||||
|
await this.request({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: `/crm/v3/objects/${objectType}/${objectId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Search API =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search objects with filters and pagination
|
||||||
|
*/
|
||||||
|
async searchObjects<T extends Record<string, any>>(
|
||||||
|
objectType: string,
|
||||||
|
searchRequest: SearchRequest
|
||||||
|
): Promise<SearchResponse<HubSpotObject<T>>> {
|
||||||
|
return this.request<SearchResponse<HubSpotObject<T>>>({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/crm/v3/objects/${objectType}/search`,
|
||||||
|
data: searchRequest,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search all results with automatic pagination (up to 10,000 limit)
|
||||||
|
*/
|
||||||
|
async searchAllObjects<T extends Record<string, any>>(
|
||||||
|
objectType: string,
|
||||||
|
searchRequest: SearchRequest,
|
||||||
|
maxResults = 10000
|
||||||
|
): Promise<HubSpotObject<T>[]> {
|
||||||
|
const allResults: HubSpotObject<T>[] = [];
|
||||||
|
let after: string | undefined;
|
||||||
|
const limit = Math.min(searchRequest.limit ?? 100, 100);
|
||||||
|
|
||||||
|
do {
|
||||||
|
const response = await this.searchObjects<T>(objectType, {
|
||||||
|
...searchRequest,
|
||||||
|
limit,
|
||||||
|
after,
|
||||||
|
});
|
||||||
|
|
||||||
|
allResults.push(...response.results);
|
||||||
|
after = response.paging?.next?.after;
|
||||||
|
|
||||||
|
if (allResults.length >= maxResults) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (after);
|
||||||
|
|
||||||
|
return allResults.slice(0, maxResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Batch API =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch read objects
|
||||||
|
*/
|
||||||
|
async batchReadObjects<T extends Record<string, any>>(
|
||||||
|
objectType: string,
|
||||||
|
request: BatchReadRequest
|
||||||
|
): Promise<BatchResponse<HubSpotObject<T>>> {
|
||||||
|
return this.request<BatchResponse<HubSpotObject<T>>>({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/crm/v3/objects/${objectType}/batch/read`,
|
||||||
|
data: request,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch create objects
|
||||||
|
*/
|
||||||
|
async batchCreateObjects<T extends Record<string, any>>(
|
||||||
|
objectType: string,
|
||||||
|
request: BatchCreateRequest<T>
|
||||||
|
): Promise<BatchResponse<HubSpotObject<T>>> {
|
||||||
|
return this.request<BatchResponse<HubSpotObject<T>>>({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/crm/v3/objects/${objectType}/batch/create`,
|
||||||
|
data: request,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch update objects
|
||||||
|
*/
|
||||||
|
async batchUpdateObjects<T extends Record<string, any>>(
|
||||||
|
objectType: string,
|
||||||
|
request: BatchUpdateRequest<T>
|
||||||
|
): Promise<BatchResponse<HubSpotObject<T>>> {
|
||||||
|
return this.request<BatchResponse<HubSpotObject<T>>>({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/crm/v3/objects/${objectType}/batch/update`,
|
||||||
|
data: request,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch archive objects
|
||||||
|
*/
|
||||||
|
async batchArchiveObjects(
|
||||||
|
objectType: string,
|
||||||
|
request: BatchArchiveRequest
|
||||||
|
): Promise<void> {
|
||||||
|
await this.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/crm/v3/objects/${objectType}/batch/archive`,
|
||||||
|
data: request,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Associations API =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get associations for an object
|
||||||
|
*/
|
||||||
|
async getAssociations(
|
||||||
|
fromObjectType: string,
|
||||||
|
fromObjectId: string,
|
||||||
|
toObjectType: string
|
||||||
|
): Promise<AssociationResult> {
|
||||||
|
return this.request<AssociationResult>({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/crm/v4/objects/${fromObjectType}/${fromObjectId}/associations/${toObjectType}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an association between objects
|
||||||
|
*/
|
||||||
|
async createAssociation(
|
||||||
|
fromObjectType: string,
|
||||||
|
fromObjectId: string,
|
||||||
|
toObjectType: string,
|
||||||
|
toObjectId: string,
|
||||||
|
associationTypeId: number
|
||||||
|
): Promise<void> {
|
||||||
|
await this.request({
|
||||||
|
method: 'PUT',
|
||||||
|
url: `/crm/v4/objects/${fromObjectType}/${fromObjectId}/associations/${toObjectType}/${toObjectId}`,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
associationCategory: 'HUBSPOT_DEFINED',
|
||||||
|
associationTypeId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an association between objects
|
||||||
|
*/
|
||||||
|
async removeAssociation(
|
||||||
|
fromObjectType: string,
|
||||||
|
fromObjectId: string,
|
||||||
|
toObjectType: string,
|
||||||
|
toObjectId: string,
|
||||||
|
associationTypeId: number
|
||||||
|
): Promise<void> {
|
||||||
|
await this.request({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: `/crm/v4/objects/${fromObjectType}/${fromObjectId}/associations/${toObjectType}/${toObjectId}`,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
associationCategory: 'HUBSPOT_DEFINED',
|
||||||
|
associationTypeId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch create associations
|
||||||
|
*/
|
||||||
|
async batchCreateAssociations(
|
||||||
|
fromObjectType: string,
|
||||||
|
toObjectType: string,
|
||||||
|
associations: BatchAssociation[]
|
||||||
|
): Promise<void> {
|
||||||
|
await this.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/crm/v4/associations/${fromObjectType}/${toObjectType}/batch/create`,
|
||||||
|
data: { inputs: associations },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Properties API =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all properties for an object type
|
||||||
|
*/
|
||||||
|
async getProperties(objectType: string): Promise<any[]> {
|
||||||
|
const response = await this.request<{ results: any[] }>({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/crm/v3/properties/${objectType}`,
|
||||||
|
});
|
||||||
|
return response.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a custom property
|
||||||
|
*/
|
||||||
|
async createProperty(objectType: string, property: any): Promise<any> {
|
||||||
|
return this.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/crm/v3/properties/${objectType}`,
|
||||||
|
data: property,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Pipelines API =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all pipelines for an object type
|
||||||
|
*/
|
||||||
|
async getPipelines(objectType: string): Promise<any[]> {
|
||||||
|
const response = await this.request<{ results: any[] }>({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/crm/v3/pipelines/${objectType}`,
|
||||||
|
});
|
||||||
|
return response.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Owners API =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all owners
|
||||||
|
*/
|
||||||
|
async getOwners(): Promise<any[]> {
|
||||||
|
const response = await this.request<{ results: any[] }>({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/crm/v3/owners',
|
||||||
|
});
|
||||||
|
return response.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Generic API request =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a generic API request (for endpoints not covered above)
|
||||||
|
*/
|
||||||
|
async apiRequest<T>(config: AxiosRequestConfig): Promise<T> {
|
||||||
|
return this.request<T>(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
131
servers/hubspot/src/main.ts
Normal file
131
servers/hubspot/src/main.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HubSpot MCP Server - Entry Point
|
||||||
|
* Handles environment validation, dual transport, graceful shutdown, and health checks
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { HubSpotServer } from './server.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Environment validation schema
|
||||||
|
const envSchema = z.object({
|
||||||
|
HUBSPOT_ACCESS_TOKEN: z.string().min(1, 'HUBSPOT_ACCESS_TOKEN is required'),
|
||||||
|
SERVER_NAME: z.string().optional().default('@mcpengine/hubspot'),
|
||||||
|
SERVER_VERSION: z.string().optional().default('1.0.0'),
|
||||||
|
NODE_ENV: z.enum(['development', 'production', 'test']).optional().default('production'),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate environment variables
|
||||||
|
*/
|
||||||
|
function validateEnvironment() {
|
||||||
|
try {
|
||||||
|
return envSchema.parse(process.env);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
console.error('Environment validation failed:');
|
||||||
|
error.errors.forEach((err) => {
|
||||||
|
console.error(` - ${err.path.join('.')}: ${err.message}`);
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check endpoint (for monitoring)
|
||||||
|
*/
|
||||||
|
function setupHealthCheck() {
|
||||||
|
// Simple health check that responds to SIGUSR1
|
||||||
|
process.on('SIGUSR1', () => {
|
||||||
|
console.error(JSON.stringify({
|
||||||
|
status: 'healthy',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
uptime: process.uptime(),
|
||||||
|
memory: process.memoryUsage(),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup graceful shutdown handlers
|
||||||
|
*/
|
||||||
|
function setupGracefulShutdown(server: HubSpotServer) {
|
||||||
|
let isShuttingDown = false;
|
||||||
|
|
||||||
|
const shutdown = async (signal: string) => {
|
||||||
|
if (isShuttingDown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isShuttingDown = true;
|
||||||
|
console.error(`Received ${signal}, shutting down gracefully...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Give pending operations time to complete
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
console.error('Server shut down successfully');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during shutdown:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle termination signals
|
||||||
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||||
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||||
|
|
||||||
|
// Handle uncaught errors
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.error('Uncaught exception:', error);
|
||||||
|
shutdown('uncaughtException');
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('Unhandled rejection at:', promise, 'reason:', reason);
|
||||||
|
shutdown('unhandledRejection');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
// Validate environment
|
||||||
|
const env = validateEnvironment();
|
||||||
|
|
||||||
|
console.error('Starting HubSpot MCP Server...');
|
||||||
|
console.error(`Environment: ${env.NODE_ENV}`);
|
||||||
|
console.error(`Server: ${env.SERVER_NAME} v${env.SERVER_VERSION}`);
|
||||||
|
|
||||||
|
// Create and configure server
|
||||||
|
const server = new HubSpotServer({
|
||||||
|
accessToken: env.HUBSPOT_ACCESS_TOKEN,
|
||||||
|
serverName: env.SERVER_NAME,
|
||||||
|
serverVersion: env.SERVER_VERSION,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup health check
|
||||||
|
setupHealthCheck();
|
||||||
|
|
||||||
|
// Setup graceful shutdown
|
||||||
|
setupGracefulShutdown(server);
|
||||||
|
|
||||||
|
// Start server (uses stdio transport by default)
|
||||||
|
console.error('Server starting with stdio transport...');
|
||||||
|
await server.start();
|
||||||
|
|
||||||
|
console.error('HubSpot MCP Server is running');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the server
|
||||||
|
main();
|
||||||
559
servers/hubspot/src/server.ts
Normal file
559
servers/hubspot/src/server.ts
Normal file
@ -0,0 +1,559 @@
|
|||||||
|
/**
|
||||||
|
* HubSpot MCP Server
|
||||||
|
* Main server implementation with lazy-loaded tool modules
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { HubSpotClient } from './clients/hubspot.js';
|
||||||
|
|
||||||
|
export interface HubSpotServerConfig {
|
||||||
|
accessToken: string;
|
||||||
|
serverName?: string;
|
||||||
|
serverVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HubSpotServer {
|
||||||
|
private server: Server;
|
||||||
|
private client: HubSpotClient;
|
||||||
|
private toolModules: Map<string, any> = new Map();
|
||||||
|
|
||||||
|
constructor(config: HubSpotServerConfig) {
|
||||||
|
this.client = new HubSpotClient({
|
||||||
|
accessToken: config.accessToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server = new Server(
|
||||||
|
{
|
||||||
|
name: config.serverName ?? '@mcpengine/hubspot',
|
||||||
|
version: config.serverVersion ?? '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
resources: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setupHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup MCP protocol handlers
|
||||||
|
*/
|
||||||
|
private setupHandlers(): void {
|
||||||
|
// List available tools
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
// CRM Objects
|
||||||
|
{
|
||||||
|
name: 'hubspot_contacts_list',
|
||||||
|
description: 'List contacts with optional filtering and pagination',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: { type: 'number', description: 'Max results (1-100)' },
|
||||||
|
after: { type: 'string', description: 'Pagination cursor' },
|
||||||
|
properties: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Properties to return',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hubspot_contacts_get',
|
||||||
|
description: 'Get a contact by ID',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
contactId: { type: 'string', description: 'Contact ID' },
|
||||||
|
properties: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Properties to return',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['contactId'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hubspot_contacts_create',
|
||||||
|
description: 'Create a new contact',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
properties: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Contact properties (email, firstname, lastname, etc.)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['properties'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hubspot_contacts_update',
|
||||||
|
description: 'Update a contact',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
contactId: { type: 'string', description: 'Contact ID' },
|
||||||
|
properties: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Properties to update',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['contactId', 'properties'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hubspot_contacts_search',
|
||||||
|
description: 'Search contacts with filters',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
filterGroups: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Filter groups for search',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Properties to return',
|
||||||
|
},
|
||||||
|
limit: { type: 'number', description: 'Max results' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Companies
|
||||||
|
{
|
||||||
|
name: 'hubspot_companies_list',
|
||||||
|
description: 'List companies',
|
||||||
|
inputSchema: { type: 'object', properties: {} },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hubspot_companies_get',
|
||||||
|
description: 'Get a company by ID',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
companyId: { type: 'string', description: 'Company ID' },
|
||||||
|
},
|
||||||
|
required: ['companyId'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hubspot_companies_create',
|
||||||
|
description: 'Create a new company',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
properties: { type: 'object', description: 'Company properties' },
|
||||||
|
},
|
||||||
|
required: ['properties'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Deals
|
||||||
|
{
|
||||||
|
name: 'hubspot_deals_list',
|
||||||
|
description: 'List deals',
|
||||||
|
inputSchema: { type: 'object', properties: {} },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hubspot_deals_get',
|
||||||
|
description: 'Get a deal by ID',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
dealId: { type: 'string', description: 'Deal ID' },
|
||||||
|
},
|
||||||
|
required: ['dealId'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hubspot_deals_create',
|
||||||
|
description: 'Create a new deal',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
properties: { type: 'object', description: 'Deal properties' },
|
||||||
|
},
|
||||||
|
required: ['properties'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Tickets
|
||||||
|
{
|
||||||
|
name: 'hubspot_tickets_list',
|
||||||
|
description: 'List tickets',
|
||||||
|
inputSchema: { type: 'object', properties: {} },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hubspot_tickets_create',
|
||||||
|
description: 'Create a new ticket',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
properties: { type: 'object', description: 'Ticket properties' },
|
||||||
|
},
|
||||||
|
required: ['properties'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Associations
|
||||||
|
{
|
||||||
|
name: 'hubspot_associations_get',
|
||||||
|
description: 'Get associations for an object',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
fromObjectType: { type: 'string' },
|
||||||
|
fromObjectId: { type: 'string' },
|
||||||
|
toObjectType: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['fromObjectType', 'fromObjectId', 'toObjectType'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hubspot_associations_create',
|
||||||
|
description: 'Create an association between objects',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
fromObjectType: { type: 'string' },
|
||||||
|
fromObjectId: { type: 'string' },
|
||||||
|
toObjectType: { type: 'string' },
|
||||||
|
toObjectId: { type: 'string' },
|
||||||
|
associationTypeId: { type: 'number' },
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
'fromObjectType',
|
||||||
|
'fromObjectId',
|
||||||
|
'toObjectType',
|
||||||
|
'toObjectId',
|
||||||
|
'associationTypeId',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Pipelines
|
||||||
|
{
|
||||||
|
name: 'hubspot_pipelines_list',
|
||||||
|
description: 'List pipelines for an object type',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
objectType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Object type (deals, tickets, etc.)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['objectType'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Owners
|
||||||
|
{
|
||||||
|
name: 'hubspot_owners_list',
|
||||||
|
description: 'List all owners',
|
||||||
|
inputSchema: { type: 'object', properties: {} },
|
||||||
|
},
|
||||||
|
// Properties
|
||||||
|
{
|
||||||
|
name: 'hubspot_properties_list',
|
||||||
|
description: 'List properties for an object type',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
objectType: { type: 'string', description: 'Object type' },
|
||||||
|
},
|
||||||
|
required: ['objectType'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle tool calls
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
try {
|
||||||
|
return await this.handleToolCall(request.params.name, request.params.arguments ?? {});
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Error: ${error.message}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// List resources (for UI apps)
|
||||||
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
uri: 'hubspot://contacts',
|
||||||
|
name: 'HubSpot Contacts',
|
||||||
|
description: 'Access to HubSpot contacts data',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'hubspot://companies',
|
||||||
|
name: 'HubSpot Companies',
|
||||||
|
description: 'Access to HubSpot companies data',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'hubspot://deals',
|
||||||
|
name: 'HubSpot Deals',
|
||||||
|
description: 'Access to HubSpot deals data',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read resources
|
||||||
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
const uri = request.params.uri;
|
||||||
|
|
||||||
|
// Parse URI and return appropriate data
|
||||||
|
// Implementation will be in tool modules
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify({ message: 'Resource handler not yet implemented' }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle tool calls with lazy loading
|
||||||
|
*/
|
||||||
|
private async handleToolCall(name: string, args: any): Promise<any> {
|
||||||
|
// Tool routing - this is where lazy-loaded modules will be imported
|
||||||
|
// For now, we'll implement basic routing structure
|
||||||
|
|
||||||
|
const [prefix, module, action] = name.split('_');
|
||||||
|
|
||||||
|
if (prefix !== 'hubspot') {
|
||||||
|
throw new Error(`Unknown tool prefix: ${prefix}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route to appropriate handler based on module
|
||||||
|
switch (module) {
|
||||||
|
case 'contacts':
|
||||||
|
return this.handleContactsTool(action, args);
|
||||||
|
case 'companies':
|
||||||
|
return this.handleCompaniesTool(action, args);
|
||||||
|
case 'deals':
|
||||||
|
return this.handleDealsTool(action, args);
|
||||||
|
case 'tickets':
|
||||||
|
return this.handleTicketsTool(action, args);
|
||||||
|
case 'associations':
|
||||||
|
return this.handleAssociationsTool(action, args);
|
||||||
|
case 'pipelines':
|
||||||
|
return this.handlePipelinesTool(action, args);
|
||||||
|
case 'owners':
|
||||||
|
return this.handleOwnersTool(action, args);
|
||||||
|
case 'properties':
|
||||||
|
return this.handlePropertiesTool(action, args);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown module: ${module}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder tool handlers - will be replaced with lazy-loaded modules
|
||||||
|
*/
|
||||||
|
private async handleContactsTool(action: string, args: any): Promise<any> {
|
||||||
|
switch (action) {
|
||||||
|
case 'list':
|
||||||
|
const contacts = await this.client.listObjects('contacts', args);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(contacts, null, 2) }],
|
||||||
|
};
|
||||||
|
case 'get':
|
||||||
|
const contact = await this.client.getObject('contacts', args.contactId, args.properties);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(contact, null, 2) }],
|
||||||
|
};
|
||||||
|
case 'create':
|
||||||
|
const newContact = await this.client.createObject('contacts', args.properties);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(newContact, null, 2) }],
|
||||||
|
};
|
||||||
|
case 'update':
|
||||||
|
const updated = await this.client.updateObject('contacts', args.contactId, args.properties);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(updated, null, 2) }],
|
||||||
|
};
|
||||||
|
case 'search':
|
||||||
|
const results = await this.client.searchObjects('contacts', args);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown contacts action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCompaniesTool(action: string, args: any): Promise<any> {
|
||||||
|
switch (action) {
|
||||||
|
case 'list':
|
||||||
|
const companies = await this.client.listObjects('companies', args);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(companies, null, 2) }],
|
||||||
|
};
|
||||||
|
case 'get':
|
||||||
|
const company = await this.client.getObject('companies', args.companyId);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(company, null, 2) }],
|
||||||
|
};
|
||||||
|
case 'create':
|
||||||
|
const newCompany = await this.client.createObject('companies', args.properties);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(newCompany, null, 2) }],
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown companies action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleDealsTool(action: string, args: any): Promise<any> {
|
||||||
|
switch (action) {
|
||||||
|
case 'list':
|
||||||
|
const deals = await this.client.listObjects('deals', args);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(deals, null, 2) }],
|
||||||
|
};
|
||||||
|
case 'get':
|
||||||
|
const deal = await this.client.getObject('deals', args.dealId);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(deal, null, 2) }],
|
||||||
|
};
|
||||||
|
case 'create':
|
||||||
|
const newDeal = await this.client.createObject('deals', args.properties);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(newDeal, null, 2) }],
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown deals action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleTicketsTool(action: string, args: any): Promise<any> {
|
||||||
|
switch (action) {
|
||||||
|
case 'list':
|
||||||
|
const tickets = await this.client.listObjects('tickets', args);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(tickets, null, 2) }],
|
||||||
|
};
|
||||||
|
case 'create':
|
||||||
|
const newTicket = await this.client.createObject('tickets', args.properties);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(newTicket, null, 2) }],
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tickets action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleAssociationsTool(action: string, args: any): Promise<any> {
|
||||||
|
switch (action) {
|
||||||
|
case 'get':
|
||||||
|
const associations = await this.client.getAssociations(
|
||||||
|
args.fromObjectType,
|
||||||
|
args.fromObjectId,
|
||||||
|
args.toObjectType
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(associations, null, 2) }],
|
||||||
|
};
|
||||||
|
case 'create':
|
||||||
|
await this.client.createAssociation(
|
||||||
|
args.fromObjectType,
|
||||||
|
args.fromObjectId,
|
||||||
|
args.toObjectType,
|
||||||
|
args.toObjectId,
|
||||||
|
args.associationTypeId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: 'Association created successfully' }],
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown associations action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handlePipelinesTool(action: string, args: any): Promise<any> {
|
||||||
|
switch (action) {
|
||||||
|
case 'list':
|
||||||
|
const pipelines = await this.client.getPipelines(args.objectType);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(pipelines, null, 2) }],
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown pipelines action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleOwnersTool(action: string, args: any): Promise<any> {
|
||||||
|
switch (action) {
|
||||||
|
case 'list':
|
||||||
|
const owners = await this.client.getOwners();
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(owners, null, 2) }],
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown owners action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handlePropertiesTool(action: string, args: any): Promise<any> {
|
||||||
|
switch (action) {
|
||||||
|
case 'list':
|
||||||
|
const properties = await this.client.getProperties(args.objectType);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(properties, null, 2) }],
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown properties action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the server
|
||||||
|
*/
|
||||||
|
async start(): Promise<void> {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await this.server.connect(transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the underlying Server instance
|
||||||
|
*/
|
||||||
|
getServer(): Server {
|
||||||
|
return this.server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the HubSpot client
|
||||||
|
*/
|
||||||
|
getClient(): HubSpotClient {
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
608
servers/hubspot/src/types/index.ts
Normal file
608
servers/hubspot/src/types/index.ts
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
/**
|
||||||
|
* HubSpot MCP Server - TypeScript Type Definitions
|
||||||
|
* Based on HubSpot CRM API v3
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Branded ID types (HubSpot uses numeric string IDs)
|
||||||
|
export type ContactId = string & { readonly __brand: 'ContactId' };
|
||||||
|
export type CompanyId = string & { readonly __brand: 'CompanyId' };
|
||||||
|
export type DealId = string & { readonly __brand: 'DealId' };
|
||||||
|
export type TicketId = string & { readonly __brand: 'TicketId' };
|
||||||
|
export type LineItemId = string & { readonly __brand: 'LineItemId' };
|
||||||
|
export type ProductId = string & { readonly __brand: 'ProductId' };
|
||||||
|
export type QuoteId = string & { readonly __brand: 'QuoteId' };
|
||||||
|
export type EngagementId = string & { readonly __brand: 'EngagementId' };
|
||||||
|
export type OwnerId = string & { readonly __brand: 'OwnerId' };
|
||||||
|
export type TeamId = string & { readonly __brand: 'TeamId' };
|
||||||
|
export type PipelineId = string & { readonly __brand: 'PipelineId' };
|
||||||
|
export type StageId = string & { readonly __brand: 'StageId' };
|
||||||
|
export type EmailId = string & { readonly __brand: 'EmailId' };
|
||||||
|
export type CampaignId = string & { readonly __brand: 'CampaignId' };
|
||||||
|
export type FormId = string & { readonly __brand: 'FormId' };
|
||||||
|
export type ListId = string & { readonly __brand: 'ListId' };
|
||||||
|
export type WorkflowId = string & { readonly __brand: 'WorkflowId' };
|
||||||
|
export type BlogId = string & { readonly __brand: 'BlogId' };
|
||||||
|
export type BlogPostId = string & { readonly __brand: 'BlogPostId' };
|
||||||
|
export type PageId = string & { readonly __brand: 'PageId' };
|
||||||
|
export type HubDbTableId = string & { readonly __brand: 'HubDbTableId' };
|
||||||
|
export type HubDbRowId = string & { readonly __brand: 'HubDbRowId' };
|
||||||
|
export type TimelineEventTypeId = string & { readonly __brand: 'TimelineEventTypeId' };
|
||||||
|
export type WebhookId = string & { readonly __brand: 'WebhookId' };
|
||||||
|
|
||||||
|
// Generic object ID
|
||||||
|
export type HubSpotObjectId = string;
|
||||||
|
|
||||||
|
// Standard HubSpot object format
|
||||||
|
export interface HubSpotObject<T extends Record<string, any> = Record<string, any>> {
|
||||||
|
id: HubSpotObjectId;
|
||||||
|
properties: T;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
archived: boolean;
|
||||||
|
archivedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRM Object Types
|
||||||
|
export type CRMObjectType =
|
||||||
|
| 'contacts'
|
||||||
|
| 'companies'
|
||||||
|
| 'deals'
|
||||||
|
| 'tickets'
|
||||||
|
| 'line_items'
|
||||||
|
| 'products'
|
||||||
|
| 'quotes';
|
||||||
|
|
||||||
|
// Contact properties
|
||||||
|
export interface ContactProperties {
|
||||||
|
email?: string;
|
||||||
|
firstname?: string;
|
||||||
|
lastname?: string;
|
||||||
|
phone?: string;
|
||||||
|
company?: string;
|
||||||
|
website?: string;
|
||||||
|
lifecyclestage?: string;
|
||||||
|
hs_lead_status?: string;
|
||||||
|
jobtitle?: string;
|
||||||
|
address?: string;
|
||||||
|
city?: string;
|
||||||
|
state?: string;
|
||||||
|
zip?: string;
|
||||||
|
country?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Contact extends HubSpotObject<ContactProperties> {
|
||||||
|
id: ContactId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Company properties
|
||||||
|
export interface CompanyProperties {
|
||||||
|
name?: string;
|
||||||
|
domain?: string;
|
||||||
|
industry?: string;
|
||||||
|
phone?: string;
|
||||||
|
city?: string;
|
||||||
|
state?: string;
|
||||||
|
country?: string;
|
||||||
|
website?: string;
|
||||||
|
numberofemployees?: string;
|
||||||
|
annualrevenue?: string;
|
||||||
|
description?: string;
|
||||||
|
type?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Company extends HubSpotObject<CompanyProperties> {
|
||||||
|
id: CompanyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deal properties
|
||||||
|
export interface DealProperties {
|
||||||
|
dealname?: string;
|
||||||
|
amount?: string;
|
||||||
|
closedate?: string;
|
||||||
|
dealstage?: string;
|
||||||
|
pipeline?: string;
|
||||||
|
hubspot_owner_id?: string;
|
||||||
|
description?: string;
|
||||||
|
deal_currency_code?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Deal extends HubSpotObject<DealProperties> {
|
||||||
|
id: DealId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ticket properties
|
||||||
|
export interface TicketProperties {
|
||||||
|
subject?: string;
|
||||||
|
content?: string;
|
||||||
|
hs_ticket_priority?: string;
|
||||||
|
hs_pipeline_stage?: string;
|
||||||
|
hs_pipeline?: string;
|
||||||
|
hubspot_owner_id?: string;
|
||||||
|
hs_ticket_category?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ticket extends HubSpotObject<TicketProperties> {
|
||||||
|
id: TicketId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line Item properties
|
||||||
|
export interface LineItemProperties {
|
||||||
|
name?: string;
|
||||||
|
price?: string;
|
||||||
|
quantity?: string;
|
||||||
|
amount?: string;
|
||||||
|
hs_sku?: string;
|
||||||
|
description?: string;
|
||||||
|
discount?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LineItem extends HubSpotObject<LineItemProperties> {
|
||||||
|
id: LineItemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product properties
|
||||||
|
export interface ProductProperties {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
price?: string;
|
||||||
|
hs_sku?: string;
|
||||||
|
hs_cost_of_goods_sold?: string;
|
||||||
|
hs_recurring_billing_period?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Product extends HubSpotObject<ProductProperties> {
|
||||||
|
id: ProductId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quote properties
|
||||||
|
export interface QuoteProperties {
|
||||||
|
hs_title?: string;
|
||||||
|
hs_expiration_date?: string;
|
||||||
|
hs_status?: string;
|
||||||
|
hs_public_url_key?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Quote extends HubSpotObject<QuoteProperties> {
|
||||||
|
id: QuoteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Associations
|
||||||
|
export interface AssociationType {
|
||||||
|
associationCategory: 'HUBSPOT_DEFINED' | 'USER_DEFINED' | 'INTEGRATOR_DEFINED';
|
||||||
|
associationTypeId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Association {
|
||||||
|
id: HubSpotObjectId;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssociationResult {
|
||||||
|
from: {
|
||||||
|
id: HubSpotObjectId;
|
||||||
|
};
|
||||||
|
to: Array<{
|
||||||
|
id: HubSpotObjectId;
|
||||||
|
type: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchAssociation {
|
||||||
|
from: {
|
||||||
|
id: HubSpotObjectId;
|
||||||
|
};
|
||||||
|
to: {
|
||||||
|
id: HubSpotObjectId;
|
||||||
|
};
|
||||||
|
type: AssociationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Engagement types
|
||||||
|
export type EngagementType = 'NOTE' | 'CALL' | 'EMAIL' | 'MEETING' | 'TASK';
|
||||||
|
|
||||||
|
export interface EngagementProperties {
|
||||||
|
hs_timestamp?: string;
|
||||||
|
hs_engagement_type?: EngagementType;
|
||||||
|
hs_body_preview?: string;
|
||||||
|
hubspot_owner_id?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoteProperties extends EngagementProperties {
|
||||||
|
hs_note_body?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallProperties extends EngagementProperties {
|
||||||
|
hs_call_title?: string;
|
||||||
|
hs_call_body?: string;
|
||||||
|
hs_call_duration?: string;
|
||||||
|
hs_call_status?: string;
|
||||||
|
hs_call_direction?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailEngagementProperties extends EngagementProperties {
|
||||||
|
hs_email_subject?: string;
|
||||||
|
hs_email_text?: string;
|
||||||
|
hs_email_html?: string;
|
||||||
|
hs_email_status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MeetingProperties extends EngagementProperties {
|
||||||
|
hs_meeting_title?: string;
|
||||||
|
hs_meeting_body?: string;
|
||||||
|
hs_meeting_start_time?: string;
|
||||||
|
hs_meeting_end_time?: string;
|
||||||
|
hs_meeting_outcome?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskProperties extends EngagementProperties {
|
||||||
|
hs_task_subject?: string;
|
||||||
|
hs_task_body?: string;
|
||||||
|
hs_task_status?: string;
|
||||||
|
hs_task_priority?: string;
|
||||||
|
hs_task_type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Engagement extends HubSpotObject<EngagementProperties> {
|
||||||
|
id: EngagementId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pipeline & Stages
|
||||||
|
export interface PipelineStage {
|
||||||
|
id: StageId;
|
||||||
|
label: string;
|
||||||
|
displayOrder: number;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
archived: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Pipeline {
|
||||||
|
id: PipelineId;
|
||||||
|
label: string;
|
||||||
|
displayOrder: number;
|
||||||
|
stages: PipelineStage[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
archived: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner & Team
|
||||||
|
export interface Owner {
|
||||||
|
id: OwnerId;
|
||||||
|
email: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
userId?: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
archived: boolean;
|
||||||
|
teams?: Array<{ id: TeamId; name: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Team {
|
||||||
|
id: TeamId;
|
||||||
|
name: string;
|
||||||
|
userIds: number[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
export interface PropertyDefinition {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
type: 'string' | 'number' | 'date' | 'datetime' | 'enumeration' | 'bool';
|
||||||
|
fieldType: string;
|
||||||
|
description?: string;
|
||||||
|
groupName?: string;
|
||||||
|
options?: Array<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
displayOrder: number;
|
||||||
|
hidden: boolean;
|
||||||
|
}>;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
hidden: boolean;
|
||||||
|
displayOrder?: number;
|
||||||
|
calculated: boolean;
|
||||||
|
externalOptions: boolean;
|
||||||
|
hasUniqueValue: boolean;
|
||||||
|
formField: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PropertyGroup {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
displayOrder: number;
|
||||||
|
properties: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marketing - Email
|
||||||
|
export interface MarketingEmail {
|
||||||
|
id: EmailId;
|
||||||
|
name: string;
|
||||||
|
subject: string;
|
||||||
|
fromName?: string;
|
||||||
|
replyTo?: string;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
state?: string;
|
||||||
|
campaignGuid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marketing - Campaign
|
||||||
|
export interface Campaign {
|
||||||
|
id: CampaignId;
|
||||||
|
name: string;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
counters?: {
|
||||||
|
sent?: number;
|
||||||
|
delivered?: number;
|
||||||
|
open?: number;
|
||||||
|
click?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marketing - Form
|
||||||
|
export interface Form {
|
||||||
|
guid: FormId;
|
||||||
|
name: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
archived: boolean;
|
||||||
|
formType: string;
|
||||||
|
submitText?: string;
|
||||||
|
redirect?: string;
|
||||||
|
formFieldGroups?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marketing - List
|
||||||
|
export interface List {
|
||||||
|
listId: ListId;
|
||||||
|
name: string;
|
||||||
|
dynamic: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
filters?: any[];
|
||||||
|
metaData?: {
|
||||||
|
size?: number;
|
||||||
|
processing?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marketing - Workflow
|
||||||
|
export interface Workflow {
|
||||||
|
id: WorkflowId;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
enabled: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
contactListIds?: {
|
||||||
|
enrolled?: number[];
|
||||||
|
active?: number[];
|
||||||
|
completed?: number[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// CMS - Blog
|
||||||
|
export interface Blog {
|
||||||
|
id: BlogId;
|
||||||
|
name: string;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
domain?: string;
|
||||||
|
language?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogPost {
|
||||||
|
id: BlogPostId;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
contentGroupId: BlogId;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
published: string;
|
||||||
|
authorName?: string;
|
||||||
|
state: string;
|
||||||
|
htmlTitle?: string;
|
||||||
|
metaDescription?: string;
|
||||||
|
postBody?: string;
|
||||||
|
postSummary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CMS - Page
|
||||||
|
export interface Page {
|
||||||
|
id: PageId;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
published?: string;
|
||||||
|
domain?: string;
|
||||||
|
htmlTitle?: string;
|
||||||
|
metaDescription?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CMS - HubDB
|
||||||
|
export interface HubDbTable {
|
||||||
|
id: HubDbTableId;
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
columns: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
}>;
|
||||||
|
rowCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
published: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HubDbRow {
|
||||||
|
id: HubDbRowId;
|
||||||
|
path?: string;
|
||||||
|
name?: string;
|
||||||
|
values: Record<string, any>;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search API
|
||||||
|
export interface FilterGroup {
|
||||||
|
filters: Filter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Filter {
|
||||||
|
propertyName: string;
|
||||||
|
operator: FilterOperator;
|
||||||
|
value?: string | number | boolean;
|
||||||
|
values?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FilterOperator =
|
||||||
|
| 'EQ'
|
||||||
|
| 'NEQ'
|
||||||
|
| 'LT'
|
||||||
|
| 'LTE'
|
||||||
|
| 'GT'
|
||||||
|
| 'GTE'
|
||||||
|
| 'BETWEEN'
|
||||||
|
| 'IN'
|
||||||
|
| 'NOT_IN'
|
||||||
|
| 'HAS_PROPERTY'
|
||||||
|
| 'NOT_HAS_PROPERTY'
|
||||||
|
| 'CONTAINS_TOKEN'
|
||||||
|
| 'NOT_CONTAINS_TOKEN';
|
||||||
|
|
||||||
|
export interface Sort {
|
||||||
|
propertyName: string;
|
||||||
|
direction: 'ASCENDING' | 'DESCENDING';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchRequest {
|
||||||
|
filterGroups?: FilterGroup[];
|
||||||
|
sorts?: Sort[];
|
||||||
|
query?: string;
|
||||||
|
properties?: string[];
|
||||||
|
limit?: number;
|
||||||
|
after?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResponse<T> {
|
||||||
|
total: number;
|
||||||
|
results: T[];
|
||||||
|
paging?: {
|
||||||
|
next?: {
|
||||||
|
after: string;
|
||||||
|
link?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhooks
|
||||||
|
export interface Webhook {
|
||||||
|
id: WebhookId;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
active: boolean;
|
||||||
|
eventType: string;
|
||||||
|
targetUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeline Events
|
||||||
|
export interface TimelineEventType {
|
||||||
|
id: TimelineEventTypeId;
|
||||||
|
name: string;
|
||||||
|
headerTemplate?: string;
|
||||||
|
detailTemplate?: string;
|
||||||
|
objectType: string;
|
||||||
|
applicationId: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimelineEvent {
|
||||||
|
id: string;
|
||||||
|
eventTemplateId: TimelineEventTypeId;
|
||||||
|
objectId: HubSpotObjectId;
|
||||||
|
timestamp: string;
|
||||||
|
tokens?: Record<string, string>;
|
||||||
|
extraData?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
export interface PaginationInfo {
|
||||||
|
next?: {
|
||||||
|
after: string;
|
||||||
|
link?: string;
|
||||||
|
};
|
||||||
|
prev?: {
|
||||||
|
before: string;
|
||||||
|
link?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PagedResponse<T> {
|
||||||
|
results: T[];
|
||||||
|
paging?: PaginationInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch operations
|
||||||
|
export interface BatchReadRequest {
|
||||||
|
properties?: string[];
|
||||||
|
propertiesWithHistory?: string[];
|
||||||
|
idProperty?: string;
|
||||||
|
inputs: Array<{ id: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchCreateRequest<T> {
|
||||||
|
inputs: Array<{ properties: T }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchUpdateRequest<T> {
|
||||||
|
inputs: Array<{ id: string; properties: T }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchArchiveRequest {
|
||||||
|
inputs: Array<{ id: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchResponse<T> {
|
||||||
|
status: 'PENDING' | 'PROCESSING' | 'CANCELED' | 'COMPLETE';
|
||||||
|
results: T[];
|
||||||
|
numErrors?: number;
|
||||||
|
errors?: Array<{
|
||||||
|
status: string;
|
||||||
|
category: string;
|
||||||
|
message: string;
|
||||||
|
context?: Record<string, any>;
|
||||||
|
}>;
|
||||||
|
startedAt: string;
|
||||||
|
completedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Error Response
|
||||||
|
export interface HubSpotError {
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
correlationId: string;
|
||||||
|
category: string;
|
||||||
|
errors?: Array<{
|
||||||
|
message: string;
|
||||||
|
in?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
21
servers/hubspot/tsconfig.json
Normal file
21
servers/hubspot/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
16
servers/quickbooks/.env.example
Normal file
16
servers/quickbooks/.env.example
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# QuickBooks Online Configuration
|
||||||
|
|
||||||
|
# Required: OAuth2 Access Token
|
||||||
|
QBO_ACCESS_TOKEN=your_access_token_here
|
||||||
|
|
||||||
|
# Required: Company/Realm ID
|
||||||
|
QBO_REALM_ID=your_realm_id_here
|
||||||
|
|
||||||
|
# Optional: Refresh Token (for token renewal)
|
||||||
|
QBO_REFRESH_TOKEN=your_refresh_token_here
|
||||||
|
|
||||||
|
# Optional: Use sandbox environment (true/false, default: false)
|
||||||
|
QBO_SANDBOX=false
|
||||||
|
|
||||||
|
# Optional: API minor version (default: 73)
|
||||||
|
QBO_MINOR_VERSION=73
|
||||||
26
servers/quickbooks/.gitignore
vendored
Normal file
26
servers/quickbooks/.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
174
servers/quickbooks/README.md
Normal file
174
servers/quickbooks/README.md
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# QuickBooks Online MCP Server
|
||||||
|
|
||||||
|
Model Context Protocol (MCP) server for QuickBooks Online integration.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Full QBO API Coverage**: Invoices, Customers, Vendors, Bills, Payments, Estimates, Items, Accounts, Reports, and more
|
||||||
|
- **SQL-like Queries**: Native support for QBO's query language
|
||||||
|
- **Rate Limiting**: Respects QBO's 500 req/min throttle with automatic retry
|
||||||
|
- **Optimistic Locking**: SyncToken support for safe updates
|
||||||
|
- **Sandbox Support**: Test against QBO Sandbox environment
|
||||||
|
- **Type Safety**: Comprehensive TypeScript types for all QBO entities
|
||||||
|
- **Pagination**: Automatic handling of large result sets
|
||||||
|
- **Token Refresh**: Built-in OAuth2 token refresh support
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create a `.env` file (see `.env.example`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required
|
||||||
|
QBO_ACCESS_TOKEN=your_access_token
|
||||||
|
QBO_REALM_ID=your_realm_id
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
QBO_REFRESH_TOKEN=your_refresh_token
|
||||||
|
QBO_SANDBOX=false
|
||||||
|
QBO_MINOR_VERSION=73
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting OAuth Credentials
|
||||||
|
|
||||||
|
1. Create an app at [Intuit Developer Portal](https://developer.intuit.com)
|
||||||
|
2. Configure OAuth2 redirect URIs
|
||||||
|
3. Obtain access token and realm ID via OAuth flow
|
||||||
|
4. Store refresh token for automatic renewal
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Foundation Components
|
||||||
|
|
||||||
|
- **`src/types/index.ts`**: Comprehensive TypeScript interfaces for all QBO entities
|
||||||
|
- **`src/clients/quickbooks.ts`**: Full-featured QBO API client with retry, rate limiting, and pagination
|
||||||
|
- **`src/server.ts`**: MCP server with lazy-loaded tool modules
|
||||||
|
- **`src/main.ts`**: Entry point with environment validation and dual transport
|
||||||
|
|
||||||
|
### QuickBooks Client Features
|
||||||
|
|
||||||
|
- OAuth2 Bearer authentication
|
||||||
|
- Automatic retry with exponential backoff (3 retries)
|
||||||
|
- Rate limit handling (500 req/min)
|
||||||
|
- Pagination support (startPosition + maxResults)
|
||||||
|
- SQL-like query execution
|
||||||
|
- Batch operations (up to 30 per batch)
|
||||||
|
- Token refresh helper
|
||||||
|
- Sandbox/production environment switching
|
||||||
|
|
||||||
|
### Tool Categories (Lazy-Loaded)
|
||||||
|
|
||||||
|
Tool implementations will be added in:
|
||||||
|
- `src/tools/invoices.ts` - Create, read, update, send invoices
|
||||||
|
- `src/tools/customers.ts` - Customer management
|
||||||
|
- `src/tools/payments.ts` - Payment processing
|
||||||
|
- `src/tools/estimates.ts` - Estimate creation and conversion
|
||||||
|
- `src/tools/bills.ts` - Bill management
|
||||||
|
- `src/tools/vendors.ts` - Vendor operations
|
||||||
|
- `src/tools/items.ts` - Inventory and service items
|
||||||
|
- `src/tools/accounts.ts` - Chart of accounts
|
||||||
|
- `src/tools/reports.ts` - P&L, Balance Sheet, Cash Flow, etc.
|
||||||
|
- `src/tools/employees.ts` - Employee management
|
||||||
|
- `src/tools/time-activities.ts` - Time tracking
|
||||||
|
- `src/tools/taxes.ts` - Tax codes and rates
|
||||||
|
- `src/tools/purchases.ts` - Purchase orders and expenses
|
||||||
|
- `src/tools/journal-entries.ts` - Manual journal entries
|
||||||
|
|
||||||
|
## API Examples
|
||||||
|
|
||||||
|
### Query Invoices
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await client.query(
|
||||||
|
'SELECT * FROM Invoice WHERE TotalAmt > 1000',
|
||||||
|
{ startPosition: 1, maxResults: 100 }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Customer
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const customer = await client.create('Customer', {
|
||||||
|
DisplayName: 'Acme Corp',
|
||||||
|
PrimaryEmailAddr: { Address: 'billing@acme.com' },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Invoice (with SyncToken)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const invoice = await client.update('Invoice', {
|
||||||
|
Id: '123',
|
||||||
|
SyncToken: '0',
|
||||||
|
TotalAmt: 5000,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Reports
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const profitLoss = await client.getReport('ProfitAndLoss', {
|
||||||
|
start_date: '2024-01-01',
|
||||||
|
end_date: '2024-12-31',
|
||||||
|
accounting_method: 'Accrual',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## QuickBooks Online API Reference
|
||||||
|
|
||||||
|
- [API Documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice)
|
||||||
|
- [Query Language](https://developer.intuit.com/app/developer/qbo/docs/develop/explore-the-quickbooks-online-api/data-queries)
|
||||||
|
- [OAuth Guide](https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization)
|
||||||
|
|
||||||
|
## Type Safety
|
||||||
|
|
||||||
|
All QBO entities use:
|
||||||
|
- Branded types for IDs (prevents mixing Customer/Vendor/etc. IDs)
|
||||||
|
- Discriminated unions for entity types and statuses
|
||||||
|
- SyncToken for optimistic locking on all entities
|
||||||
|
- Strict TypeScript mode for compile-time safety
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
QBO enforces 500 requests/minute:
|
||||||
|
- Client tracks requests per rolling window
|
||||||
|
- Automatically throttles when approaching limit
|
||||||
|
- Retries 429 responses with exponential backoff
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
QBO errors include:
|
||||||
|
- Error code
|
||||||
|
- Message
|
||||||
|
- Detail
|
||||||
|
- Element (field causing error)
|
||||||
|
|
||||||
|
The client wraps these in structured MCP errors.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
This is the foundation layer. Tool implementations and UI apps will be added separately.
|
||||||
21
servers/quickbooks/package.json
Normal file
21
servers/quickbooks/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@mcpengine/quickbooks",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"dev": "tsx watch src/main.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
|
"axios": "^1.7.0",
|
||||||
|
"zod": "^3.23.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"tsx": "^4.19.0",
|
||||||
|
"@types/node": "^22.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
312
servers/quickbooks/src/clients/quickbooks.ts
Normal file
312
servers/quickbooks/src/clients/quickbooks.ts
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||||
|
import type { QBOEntity, QueryResponse, QBOError } from '../types/index.js';
|
||||||
|
|
||||||
|
export interface QuickBooksConfig {
|
||||||
|
accessToken: string;
|
||||||
|
realmId: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
sandbox?: boolean;
|
||||||
|
minorVersion?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationOptions {
|
||||||
|
startPosition?: number;
|
||||||
|
maxResults?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryOptions extends PaginationOptions {
|
||||||
|
sql: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_MINOR_VERSION = 73;
|
||||||
|
const MAX_RESULTS = 1000;
|
||||||
|
const RATE_LIMIT_REQUESTS = 500;
|
||||||
|
const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
const INITIAL_RETRY_DELAY_MS = 1000;
|
||||||
|
|
||||||
|
export class QuickBooksClient {
|
||||||
|
private client: AxiosInstance;
|
||||||
|
private realmId: string;
|
||||||
|
private refreshToken?: string;
|
||||||
|
private minorVersion: number;
|
||||||
|
private requestCount = 0;
|
||||||
|
private windowStart = Date.now();
|
||||||
|
|
||||||
|
constructor(config: QuickBooksConfig) {
|
||||||
|
this.realmId = config.realmId;
|
||||||
|
this.refreshToken = config.refreshToken;
|
||||||
|
this.minorVersion = config.minorVersion ?? DEFAULT_MINOR_VERSION;
|
||||||
|
|
||||||
|
const baseURL = config.sandbox
|
||||||
|
? 'https://sandbox-quickbooks.api.intuit.com'
|
||||||
|
: 'https://quickbooks.api.intuit.com';
|
||||||
|
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${config.accessToken}`,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limiting check - respects QBO 500 req/min throttle
|
||||||
|
*/
|
||||||
|
private async checkRateLimit(): Promise<void> {
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = now - this.windowStart;
|
||||||
|
|
||||||
|
if (elapsed >= RATE_LIMIT_WINDOW_MS) {
|
||||||
|
// Reset window
|
||||||
|
this.requestCount = 0;
|
||||||
|
this.windowStart = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.requestCount >= RATE_LIMIT_REQUESTS) {
|
||||||
|
// Wait until window resets
|
||||||
|
const waitMs = RATE_LIMIT_WINDOW_MS - elapsed;
|
||||||
|
await this.sleep(waitMs);
|
||||||
|
this.requestCount = 0;
|
||||||
|
this.windowStart = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exponential backoff retry logic
|
||||||
|
*/
|
||||||
|
private async retry<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
retries = MAX_RETRIES,
|
||||||
|
delay = INITIAL_RETRY_DELAY_MS
|
||||||
|
): Promise<T> {
|
||||||
|
try {
|
||||||
|
await this.checkRateLimit();
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
if (retries === 0) throw error;
|
||||||
|
|
||||||
|
const axiosError = error as AxiosError;
|
||||||
|
|
||||||
|
// Retry on rate limit (429) or server errors (5xx)
|
||||||
|
if (
|
||||||
|
axiosError.response?.status === 429 ||
|
||||||
|
(axiosError.response?.status && axiosError.response.status >= 500)
|
||||||
|
) {
|
||||||
|
await this.sleep(delay);
|
||||||
|
return this.retry(fn, retries - 1, delay * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build URL with minor version parameter
|
||||||
|
*/
|
||||||
|
private buildUrl(path: string, params?: Record<string, string | number>): string {
|
||||||
|
const url = `/v3/company/${this.realmId}${path}`;
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
minorversion: this.minorVersion.toString(),
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(params || {}).map(([k, v]) => [k, String(v)])
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return `${url}?${queryParams.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute SQL-like query
|
||||||
|
*/
|
||||||
|
async query<T extends QBOEntity>(
|
||||||
|
sql: string,
|
||||||
|
options: PaginationOptions = {}
|
||||||
|
): Promise<QueryResponse<T>> {
|
||||||
|
const { startPosition = 1, maxResults = MAX_RESULTS } = options;
|
||||||
|
|
||||||
|
// QBO uses STARTPOSITION and MAXRESULTS in the SQL query itself
|
||||||
|
const paginatedSql = `${sql} STARTPOSITION ${startPosition} MAXRESULTS ${Math.min(maxResults, MAX_RESULTS)}`;
|
||||||
|
|
||||||
|
return this.retry(async () => {
|
||||||
|
const url = this.buildUrl('/query', { query: paginatedSql });
|
||||||
|
const response = await this.client.get<QueryResponse<T>>(url);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create entity
|
||||||
|
*/
|
||||||
|
async create<T extends QBOEntity>(
|
||||||
|
entityType: string,
|
||||||
|
data: Partial<T>
|
||||||
|
): Promise<T> {
|
||||||
|
return this.retry(async () => {
|
||||||
|
const url = this.buildUrl(`/${entityType.toLowerCase()}`);
|
||||||
|
const response = await this.client.post<{ [key: string]: T }>(url, data);
|
||||||
|
return response.data[entityType];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read entity by ID
|
||||||
|
*/
|
||||||
|
async read<T extends QBOEntity>(
|
||||||
|
entityType: string,
|
||||||
|
id: string
|
||||||
|
): Promise<T> {
|
||||||
|
return this.retry(async () => {
|
||||||
|
const url = this.buildUrl(`/${entityType.toLowerCase()}/${id}`);
|
||||||
|
const response = await this.client.get<{ [key: string]: T }>(url);
|
||||||
|
return response.data[entityType];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update entity (requires SyncToken for optimistic locking)
|
||||||
|
*/
|
||||||
|
async update<T extends QBOEntity>(
|
||||||
|
entityType: string,
|
||||||
|
data: Partial<T> & { Id: string; SyncToken: string }
|
||||||
|
): Promise<T> {
|
||||||
|
return this.retry(async () => {
|
||||||
|
const url = this.buildUrl(`/${entityType.toLowerCase()}`);
|
||||||
|
const response = await this.client.post<{ [key: string]: T }>(url, data);
|
||||||
|
return response.data[entityType];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete entity (soft delete - requires SyncToken)
|
||||||
|
*/
|
||||||
|
async delete<T extends QBOEntity>(
|
||||||
|
entityType: string,
|
||||||
|
id: string,
|
||||||
|
syncToken: string
|
||||||
|
): Promise<T> {
|
||||||
|
return this.retry(async () => {
|
||||||
|
const url = this.buildUrl(`/${entityType.toLowerCase()}`, {
|
||||||
|
operation: 'delete',
|
||||||
|
});
|
||||||
|
const response = await this.client.post<{ [key: string]: T }>(url, {
|
||||||
|
Id: id,
|
||||||
|
SyncToken: syncToken,
|
||||||
|
});
|
||||||
|
return response.data[entityType];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get report
|
||||||
|
*/
|
||||||
|
async getReport<T = any>(
|
||||||
|
reportName: string,
|
||||||
|
params: Record<string, string | number> = {}
|
||||||
|
): Promise<T> {
|
||||||
|
return this.retry(async () => {
|
||||||
|
const url = this.buildUrl(`/reports/${reportName}`, params);
|
||||||
|
const response = await this.client.get<T>(url);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh access token using refresh token
|
||||||
|
*/
|
||||||
|
async refreshAccessToken(
|
||||||
|
clientId: string,
|
||||||
|
clientSecret: string
|
||||||
|
): Promise<{ access_token: string; refresh_token: string; expires_in: number }> {
|
||||||
|
if (!this.refreshToken) {
|
||||||
|
throw new Error('No refresh token available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenUrl = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer';
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
tokenUrl,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: this.refreshToken,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
username: clientId,
|
||||||
|
password: clientSecret,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update client with new access token
|
||||||
|
this.client.defaults.headers['Authorization'] = `Bearer ${response.data.access_token}`;
|
||||||
|
this.refreshToken = response.data.refresh_token;
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get company info
|
||||||
|
*/
|
||||||
|
async getCompanyInfo(): Promise<any> {
|
||||||
|
return this.retry(async () => {
|
||||||
|
const url = this.buildUrl('/companyinfo', { id: this.realmId });
|
||||||
|
const response = await this.client.get(url);
|
||||||
|
return response.data.CompanyInfo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get preferences
|
||||||
|
*/
|
||||||
|
async getPreferences(): Promise<any> {
|
||||||
|
return this.retry(async () => {
|
||||||
|
const url = this.buildUrl('/preferences');
|
||||||
|
const response = await this.client.get(url);
|
||||||
|
return response.data.Preferences;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch operations (up to 30 operations per batch)
|
||||||
|
*/
|
||||||
|
async batch(operations: Array<{
|
||||||
|
bId: string;
|
||||||
|
operation: 'create' | 'update' | 'delete' | 'query';
|
||||||
|
entity?: string;
|
||||||
|
data?: any;
|
||||||
|
query?: string;
|
||||||
|
}>): Promise<any> {
|
||||||
|
return this.retry(async () => {
|
||||||
|
const url = this.buildUrl('/batch');
|
||||||
|
const response = await this.client.post(url, {
|
||||||
|
BatchItemRequest: operations.map(op => {
|
||||||
|
if (op.operation === 'query') {
|
||||||
|
return {
|
||||||
|
bId: op.bId,
|
||||||
|
Query: op.query,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
bId: op.bId,
|
||||||
|
operation: op.operation,
|
||||||
|
[op.entity!]: op.data,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return response.data.BatchItemResponse;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
149
servers/quickbooks/src/main.ts
Normal file
149
servers/quickbooks/src/main.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { QuickBooksClient } from './clients/quickbooks.js';
|
||||||
|
import { createMCPServer, startServer } from './server.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variable schema
|
||||||
|
*/
|
||||||
|
const envSchema = z.object({
|
||||||
|
QBO_ACCESS_TOKEN: z.string().min(1, 'QBO_ACCESS_TOKEN is required'),
|
||||||
|
QBO_REALM_ID: z.string().min(1, 'QBO_REALM_ID is required'),
|
||||||
|
QBO_REFRESH_TOKEN: z.string().optional(),
|
||||||
|
QBO_SANDBOX: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform(val => val === 'true' || val === '1'),
|
||||||
|
QBO_MINOR_VERSION: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform(val => (val ? parseInt(val, 10) : undefined)),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate environment variables
|
||||||
|
*/
|
||||||
|
function validateEnvironment() {
|
||||||
|
try {
|
||||||
|
return envSchema.parse(process.env);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
console.error('❌ Environment validation failed:');
|
||||||
|
error.errors.forEach(err => {
|
||||||
|
console.error(` - ${err.path.join('.')}: ${err.message}`);
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check endpoint (optional)
|
||||||
|
*/
|
||||||
|
function setupHealthCheck() {
|
||||||
|
// Simple health indicator - write to stderr so it doesn't interfere with stdio transport
|
||||||
|
const logHealth = () => {
|
||||||
|
process.stderr.write(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 'healthy',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
uptime: process.uptime(),
|
||||||
|
}) + '\n'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log health every 5 minutes
|
||||||
|
const healthInterval = setInterval(logHealth, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(healthInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graceful shutdown handler
|
||||||
|
*/
|
||||||
|
function setupGracefulShutdown(cleanup: () => void) {
|
||||||
|
const shutdown = (signal: string) => {
|
||||||
|
process.stderr.write(`\n⚠️ Received ${signal}, shutting down gracefully...\n`);
|
||||||
|
cleanup();
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||||
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
// Validate environment
|
||||||
|
const env = validateEnvironment();
|
||||||
|
|
||||||
|
process.stderr.write('🚀 Starting QuickBooks Online MCP Server...\n');
|
||||||
|
process.stderr.write(` Realm ID: ${env.QBO_REALM_ID}\n`);
|
||||||
|
process.stderr.write(` Sandbox: ${env.QBO_SANDBOX ? 'Yes' : 'No'}\n`);
|
||||||
|
process.stderr.write(` Minor Version: ${env.QBO_MINOR_VERSION || 73}\n\n`);
|
||||||
|
|
||||||
|
// Create QuickBooks client
|
||||||
|
const qboClient = new QuickBooksClient({
|
||||||
|
accessToken: env.QBO_ACCESS_TOKEN,
|
||||||
|
realmId: env.QBO_REALM_ID,
|
||||||
|
refreshToken: env.QBO_REFRESH_TOKEN,
|
||||||
|
sandbox: env.QBO_SANDBOX,
|
||||||
|
minorVersion: env.QBO_MINOR_VERSION,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test connection
|
||||||
|
try {
|
||||||
|
const companyInfo = await qboClient.getCompanyInfo();
|
||||||
|
process.stderr.write(`✅ Connected to: ${companyInfo.CompanyName}\n\n`);
|
||||||
|
} catch (error: any) {
|
||||||
|
process.stderr.write('❌ Failed to connect to QuickBooks Online:\n');
|
||||||
|
process.stderr.write(` ${error.message}\n\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create MCP server
|
||||||
|
const server = createMCPServer({
|
||||||
|
name: '@mcpengine/quickbooks',
|
||||||
|
version: '1.0.0',
|
||||||
|
qboClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup health check
|
||||||
|
const cleanupHealth = setupHealthCheck();
|
||||||
|
|
||||||
|
// Setup graceful shutdown
|
||||||
|
setupGracefulShutdown(() => {
|
||||||
|
cleanupHealth();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server with stdio transport
|
||||||
|
process.stderr.write('📡 MCP Server ready (stdio transport)\n');
|
||||||
|
process.stderr.write(' Waiting for requests...\n\n');
|
||||||
|
|
||||||
|
await startServer(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unhandled errors
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
process.stderr.write('❌ Unhandled Rejection:\n');
|
||||||
|
process.stderr.write(` ${reason}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
process.stderr.write('❌ Uncaught Exception:\n');
|
||||||
|
process.stderr.write(` ${error.message}\n`);
|
||||||
|
process.stderr.write(` ${error.stack}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run
|
||||||
|
main().catch((error) => {
|
||||||
|
process.stderr.write('❌ Fatal error:\n');
|
||||||
|
process.stderr.write(` ${error.message}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
282
servers/quickbooks/src/server.ts
Normal file
282
servers/quickbooks/src/server.ts
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
|
ErrorCode,
|
||||||
|
McpError,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { QuickBooksClient } from './clients/quickbooks.js';
|
||||||
|
|
||||||
|
export interface MCPServerConfig {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
qboClient: QuickBooksClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool module definition for lazy loading
|
||||||
|
*/
|
||||||
|
interface ToolModule {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: any;
|
||||||
|
handler: (client: QuickBooksClient, args: any) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create MCP server for QuickBooks Online
|
||||||
|
*/
|
||||||
|
export function createMCPServer(config: MCPServerConfig): Server {
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: config.name,
|
||||||
|
version: config.version,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
resources: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tool categories for lazy loading
|
||||||
|
const toolCategories = [
|
||||||
|
'invoices',
|
||||||
|
'customers',
|
||||||
|
'payments',
|
||||||
|
'estimates',
|
||||||
|
'bills',
|
||||||
|
'vendors',
|
||||||
|
'items',
|
||||||
|
'accounts',
|
||||||
|
'reports',
|
||||||
|
'employees',
|
||||||
|
'time-activities',
|
||||||
|
'taxes',
|
||||||
|
'purchases',
|
||||||
|
'journal-entries',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Tool registry - will be populated lazily
|
||||||
|
const toolRegistry = new Map<string, ToolModule>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a tool module
|
||||||
|
*/
|
||||||
|
function registerTool(tool: ToolModule): void {
|
||||||
|
toolRegistry.set(tool.name, tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy load tool modules
|
||||||
|
*/
|
||||||
|
async function loadToolModules(): Promise<void> {
|
||||||
|
// Tool modules will be created later - for now we register placeholders
|
||||||
|
// to demonstrate the lazy-loading pattern
|
||||||
|
|
||||||
|
// Example tool structure - actual implementations will be in separate files
|
||||||
|
registerTool({
|
||||||
|
name: 'qbo_query',
|
||||||
|
description: 'Execute SQL-like query against QuickBooks Online',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
sql: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'SQL-like query (e.g., "SELECT * FROM Invoice WHERE TotalAmt > 1000")',
|
||||||
|
},
|
||||||
|
startPosition: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Start position for pagination (1-indexed)',
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
maxResults: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Maximum results to return (max 1000)',
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['sql'],
|
||||||
|
},
|
||||||
|
handler: async (client, args) => {
|
||||||
|
return await client.query(args.sql, {
|
||||||
|
startPosition: args.startPosition,
|
||||||
|
maxResults: args.maxResults,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registerTool({
|
||||||
|
name: 'qbo_get_company_info',
|
||||||
|
description: 'Get company information',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
handler: async (client, _args) => {
|
||||||
|
return await client.getCompanyInfo();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registerTool({
|
||||||
|
name: 'qbo_get_preferences',
|
||||||
|
description: 'Get company preferences',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
handler: async (client, _args) => {
|
||||||
|
return await client.getPreferences();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Placeholder for future tool modules
|
||||||
|
// These will be implemented in separate files like:
|
||||||
|
// - src/tools/invoices.ts
|
||||||
|
// - src/tools/customers.ts
|
||||||
|
// - etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
// List available tools
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
await loadToolModules();
|
||||||
|
|
||||||
|
return {
|
||||||
|
tools: Array.from(toolRegistry.values()).map(tool => ({
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: tool.inputSchema,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle tool calls
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
await loadToolModules();
|
||||||
|
|
||||||
|
const tool = toolRegistry.get(request.params.name);
|
||||||
|
|
||||||
|
if (!tool) {
|
||||||
|
throw new McpError(
|
||||||
|
ErrorCode.MethodNotFound,
|
||||||
|
`Unknown tool: ${request.params.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await tool.handler(config.qboClient, request.params.arguments || {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
// Structured error response
|
||||||
|
const errorMessage = error.response?.data?.Fault?.Error?.[0]?.Message || error.message;
|
||||||
|
const errorDetail = error.response?.data?.Fault?.Error?.[0]?.Detail || '';
|
||||||
|
const errorCode = error.response?.data?.Fault?.Error?.[0]?.code || 'UNKNOWN';
|
||||||
|
|
||||||
|
throw new McpError(
|
||||||
|
ErrorCode.InternalError,
|
||||||
|
`QuickBooks API error (${errorCode}): ${errorMessage}${errorDetail ? ` - ${errorDetail}` : ''}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resource handlers for UI apps
|
||||||
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
uri: 'qbo://company/info',
|
||||||
|
name: 'Company Information',
|
||||||
|
description: 'Current company information',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'qbo://company/preferences',
|
||||||
|
name: 'Company Preferences',
|
||||||
|
description: 'Company preferences and settings',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'qbo://invoices/recent',
|
||||||
|
name: 'Recent Invoices',
|
||||||
|
description: 'Recent invoices (last 30 days)',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'qbo://customers/list',
|
||||||
|
name: 'Customer List',
|
||||||
|
description: 'All active customers',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
const uri = request.params.uri;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data: any;
|
||||||
|
|
||||||
|
if (uri === 'qbo://company/info') {
|
||||||
|
data = await config.qboClient.getCompanyInfo();
|
||||||
|
} else if (uri === 'qbo://company/preferences') {
|
||||||
|
data = await config.qboClient.getPreferences();
|
||||||
|
} else if (uri === 'qbo://invoices/recent') {
|
||||||
|
const thirtyDaysAgo = new Date();
|
||||||
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||||
|
const dateStr = thirtyDaysAgo.toISOString().split('T')[0];
|
||||||
|
data = await config.qboClient.query(
|
||||||
|
`SELECT * FROM Invoice WHERE TxnDate >= '${dateStr}'`,
|
||||||
|
{ maxResults: 100 }
|
||||||
|
);
|
||||||
|
} else if (uri === 'qbo://customers/list') {
|
||||||
|
data = await config.qboClient.query(
|
||||||
|
'SELECT * FROM Customer WHERE Active = true',
|
||||||
|
{ maxResults: 1000 }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new McpError(
|
||||||
|
ErrorCode.InvalidRequest,
|
||||||
|
`Unknown resource: ${uri}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(data, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new McpError(
|
||||||
|
ErrorCode.InternalError,
|
||||||
|
`Failed to read resource: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the MCP server with stdio transport
|
||||||
|
*/
|
||||||
|
export async function startServer(server: Server): Promise<void> {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
}
|
||||||
791
servers/quickbooks/src/types/index.ts
Normal file
791
servers/quickbooks/src/types/index.ts
Normal file
@ -0,0 +1,791 @@
|
|||||||
|
// Branded types for IDs
|
||||||
|
export type CustomerId = string & { readonly __brand: 'CustomerId' };
|
||||||
|
export type VendorId = string & { readonly __brand: 'VendorId' };
|
||||||
|
export type EmployeeId = string & { readonly __brand: 'EmployeeId' };
|
||||||
|
export type InvoiceId = string & { readonly __brand: 'InvoiceId' };
|
||||||
|
export type PaymentId = string & { readonly __brand: 'PaymentId' };
|
||||||
|
export type BillId = string & { readonly __brand: 'BillId' };
|
||||||
|
export type EstimateId = string & { readonly __brand: 'EstimateId' };
|
||||||
|
export type ItemId = string & { readonly __brand: 'ItemId' };
|
||||||
|
export type AccountId = string & { readonly __brand: 'AccountId' };
|
||||||
|
export type TaxCodeId = string & { readonly __brand: 'TaxCodeId' };
|
||||||
|
export type ClassId = string & { readonly __brand: 'ClassId' };
|
||||||
|
export type DepartmentId = string & { readonly __brand: 'DepartmentId' };
|
||||||
|
export type TimeActivityId = string & { readonly __brand: 'TimeActivityId' };
|
||||||
|
|
||||||
|
// Base entity with SyncToken for optimistic locking
|
||||||
|
export interface QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
SyncToken: string;
|
||||||
|
MetaData: {
|
||||||
|
CreateTime: string;
|
||||||
|
LastUpdatedTime: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference types
|
||||||
|
export interface Ref {
|
||||||
|
value: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerRef extends Ref {
|
||||||
|
value: CustomerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VendorRef extends Ref {
|
||||||
|
value: VendorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmployeeRef extends Ref {
|
||||||
|
value: EmployeeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemRef extends Ref {
|
||||||
|
value: ItemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountRef extends Ref {
|
||||||
|
value: AccountId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClassRef extends Ref {
|
||||||
|
value: ClassId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DepartmentRef extends Ref {
|
||||||
|
value: DepartmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaxCodeRef extends Ref {
|
||||||
|
value: TaxCodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address
|
||||||
|
export interface PhysicalAddress {
|
||||||
|
Line1?: string;
|
||||||
|
Line2?: string;
|
||||||
|
Line3?: string;
|
||||||
|
Line4?: string;
|
||||||
|
Line5?: string;
|
||||||
|
City?: string;
|
||||||
|
Country?: string;
|
||||||
|
CountrySubDivisionCode?: string;
|
||||||
|
PostalCode?: string;
|
||||||
|
Lat?: string;
|
||||||
|
Long?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customer
|
||||||
|
export interface Customer extends QBOEntity {
|
||||||
|
Id: CustomerId;
|
||||||
|
DisplayName: string;
|
||||||
|
Title?: string;
|
||||||
|
GivenName?: string;
|
||||||
|
MiddleName?: string;
|
||||||
|
FamilyName?: string;
|
||||||
|
Suffix?: string;
|
||||||
|
FullyQualifiedName?: string;
|
||||||
|
CompanyName?: string;
|
||||||
|
PrintOnCheckName?: string;
|
||||||
|
Active?: boolean;
|
||||||
|
PrimaryPhone?: { FreeFormNumber: string };
|
||||||
|
AlternatePhone?: { FreeFormNumber: string };
|
||||||
|
Mobile?: { FreeFormNumber: string };
|
||||||
|
Fax?: { FreeFormNumber: string };
|
||||||
|
PrimaryEmailAddr?: { Address: string };
|
||||||
|
WebAddr?: { URI: string };
|
||||||
|
BillAddr?: PhysicalAddress;
|
||||||
|
ShipAddr?: PhysicalAddress;
|
||||||
|
Notes?: string;
|
||||||
|
Taxable?: boolean;
|
||||||
|
Balance?: number;
|
||||||
|
BalanceWithJobs?: number;
|
||||||
|
CurrencyRef?: Ref;
|
||||||
|
PreferredDeliveryMethod?: string;
|
||||||
|
ResaleNum?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vendor
|
||||||
|
export interface Vendor extends QBOEntity {
|
||||||
|
Id: VendorId;
|
||||||
|
DisplayName: string;
|
||||||
|
Title?: string;
|
||||||
|
GivenName?: string;
|
||||||
|
MiddleName?: string;
|
||||||
|
FamilyName?: string;
|
||||||
|
Suffix?: string;
|
||||||
|
CompanyName?: string;
|
||||||
|
PrintOnCheckName?: string;
|
||||||
|
Active?: boolean;
|
||||||
|
PrimaryPhone?: { FreeFormNumber: string };
|
||||||
|
AlternatePhone?: { FreeFormNumber: string };
|
||||||
|
Mobile?: { FreeFormNumber: string };
|
||||||
|
Fax?: { FreeFormNumber: string };
|
||||||
|
PrimaryEmailAddr?: { Address: string };
|
||||||
|
WebAddr?: { URI: string };
|
||||||
|
BillAddr?: PhysicalAddress;
|
||||||
|
Balance?: number;
|
||||||
|
AcctNum?: string;
|
||||||
|
Vendor1099?: boolean;
|
||||||
|
CurrencyRef?: Ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Employee
|
||||||
|
export interface Employee extends QBOEntity {
|
||||||
|
Id: EmployeeId;
|
||||||
|
DisplayName: string;
|
||||||
|
Title?: string;
|
||||||
|
GivenName?: string;
|
||||||
|
MiddleName?: string;
|
||||||
|
FamilyName?: string;
|
||||||
|
Suffix?: string;
|
||||||
|
PrintOnCheckName?: string;
|
||||||
|
Active?: boolean;
|
||||||
|
PrimaryPhone?: { FreeFormNumber: string };
|
||||||
|
Mobile?: { FreeFormNumber: string };
|
||||||
|
PrimaryEmailAddr?: { Address: string };
|
||||||
|
PrimaryAddr?: PhysicalAddress;
|
||||||
|
EmployeeNumber?: string;
|
||||||
|
SSN?: string;
|
||||||
|
HiredDate?: string;
|
||||||
|
ReleasedDate?: string;
|
||||||
|
BillableTime?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item types
|
||||||
|
export type ItemType = 'Inventory' | 'NonInventory' | 'Service' | 'Category' | 'Group';
|
||||||
|
|
||||||
|
export interface Item extends QBOEntity {
|
||||||
|
Id: ItemId;
|
||||||
|
Name: string;
|
||||||
|
Type: ItemType;
|
||||||
|
Active?: boolean;
|
||||||
|
FullyQualifiedName?: string;
|
||||||
|
Taxable?: boolean;
|
||||||
|
UnitPrice?: number;
|
||||||
|
Description?: string;
|
||||||
|
PurchaseDesc?: string;
|
||||||
|
PurchaseCost?: number;
|
||||||
|
QtyOnHand?: number;
|
||||||
|
IncomeAccountRef?: AccountRef;
|
||||||
|
ExpenseAccountRef?: AccountRef;
|
||||||
|
AssetAccountRef?: AccountRef;
|
||||||
|
InvStartDate?: string;
|
||||||
|
TrackQtyOnHand?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoice Line
|
||||||
|
export interface InvoiceLine {
|
||||||
|
Id?: string;
|
||||||
|
LineNum?: number;
|
||||||
|
Description?: string;
|
||||||
|
Amount: number;
|
||||||
|
DetailType: 'SalesItemLineDetail' | 'SubTotalLineDetail' | 'DiscountLineDetail' | 'DescriptionOnly';
|
||||||
|
SalesItemLineDetail?: {
|
||||||
|
ItemRef?: ItemRef;
|
||||||
|
Qty?: number;
|
||||||
|
UnitPrice?: number;
|
||||||
|
TaxCodeRef?: TaxCodeRef;
|
||||||
|
ClassRef?: ClassRef;
|
||||||
|
ServiceDate?: string;
|
||||||
|
};
|
||||||
|
DiscountLineDetail?: {
|
||||||
|
PercentBased?: boolean;
|
||||||
|
DiscountPercent?: number;
|
||||||
|
DiscountAccountRef?: AccountRef;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoice
|
||||||
|
export type InvoiceStatus = 'Draft' | 'Pending' | 'Sent' | 'Paid' | 'Overdue' | 'Void';
|
||||||
|
|
||||||
|
export interface Invoice extends QBOEntity {
|
||||||
|
Id: InvoiceId;
|
||||||
|
DocNumber?: string;
|
||||||
|
TxnDate: string;
|
||||||
|
DueDate?: string;
|
||||||
|
CustomerRef: CustomerRef;
|
||||||
|
Line: InvoiceLine[];
|
||||||
|
TotalAmt: number;
|
||||||
|
Balance: number;
|
||||||
|
BillAddr?: PhysicalAddress;
|
||||||
|
ShipAddr?: PhysicalAddress;
|
||||||
|
EmailStatus?: 'NotSet' | 'NeedToSend' | 'EmailSent';
|
||||||
|
PrintStatus?: 'NotSet' | 'NeedToPrint' | 'PrintComplete';
|
||||||
|
TxnStatus?: InvoiceStatus;
|
||||||
|
PrivateNote?: string;
|
||||||
|
CustomerMemo?: { value: string };
|
||||||
|
BillEmail?: { Address: string };
|
||||||
|
AllowOnlineCreditCardPayment?: boolean;
|
||||||
|
AllowOnlineACHPayment?: boolean;
|
||||||
|
CurrencyRef?: Ref;
|
||||||
|
ExchangeRate?: number;
|
||||||
|
Deposit?: number;
|
||||||
|
DepositToAccountRef?: AccountRef;
|
||||||
|
ClassRef?: ClassRef;
|
||||||
|
DepartmentRef?: DepartmentRef;
|
||||||
|
SalesTermRef?: Ref;
|
||||||
|
ShipMethodRef?: Ref;
|
||||||
|
ShipDate?: string;
|
||||||
|
TrackingNum?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payment
|
||||||
|
export interface PaymentLine {
|
||||||
|
Amount: number;
|
||||||
|
LinkedTxn?: Array<{
|
||||||
|
TxnId: string;
|
||||||
|
TxnType: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Payment extends QBOEntity {
|
||||||
|
Id: PaymentId;
|
||||||
|
TxnDate: string;
|
||||||
|
CustomerRef: CustomerRef;
|
||||||
|
TotalAmt: number;
|
||||||
|
UnappliedAmt?: number;
|
||||||
|
DepositToAccountRef?: AccountRef;
|
||||||
|
Line?: PaymentLine[];
|
||||||
|
PaymentMethodRef?: Ref;
|
||||||
|
PaymentRefNum?: string;
|
||||||
|
PrivateNote?: string;
|
||||||
|
CurrencyRef?: Ref;
|
||||||
|
ExchangeRate?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credit Memo
|
||||||
|
export interface CreditMemo extends QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
DocNumber?: string;
|
||||||
|
TxnDate: string;
|
||||||
|
CustomerRef: CustomerRef;
|
||||||
|
Line: InvoiceLine[];
|
||||||
|
TotalAmt: number;
|
||||||
|
Balance: number;
|
||||||
|
BillAddr?: PhysicalAddress;
|
||||||
|
PrivateNote?: string;
|
||||||
|
CustomerMemo?: { value: string };
|
||||||
|
EmailStatus?: 'NotSet' | 'NeedToSend' | 'EmailSent';
|
||||||
|
PrintStatus?: 'NotSet' | 'NeedToPrint' | 'PrintComplete';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bill
|
||||||
|
export interface BillLine {
|
||||||
|
Id?: string;
|
||||||
|
LineNum?: number;
|
||||||
|
Description?: string;
|
||||||
|
Amount: number;
|
||||||
|
DetailType: 'AccountBasedExpenseLineDetail' | 'ItemBasedExpenseLineDetail';
|
||||||
|
AccountBasedExpenseLineDetail?: {
|
||||||
|
AccountRef: AccountRef;
|
||||||
|
TaxCodeRef?: TaxCodeRef;
|
||||||
|
ClassRef?: ClassRef;
|
||||||
|
BillableStatus?: 'Billable' | 'NotBillable' | 'HasBeenBilled';
|
||||||
|
CustomerRef?: CustomerRef;
|
||||||
|
};
|
||||||
|
ItemBasedExpenseLineDetail?: {
|
||||||
|
ItemRef: ItemRef;
|
||||||
|
Qty?: number;
|
||||||
|
UnitPrice?: number;
|
||||||
|
TaxCodeRef?: TaxCodeRef;
|
||||||
|
ClassRef?: ClassRef;
|
||||||
|
BillableStatus?: 'Billable' | 'NotBillable' | 'HasBeenBilled';
|
||||||
|
CustomerRef?: CustomerRef;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Bill extends QBOEntity {
|
||||||
|
Id: BillId;
|
||||||
|
DocNumber?: string;
|
||||||
|
TxnDate: string;
|
||||||
|
DueDate?: string;
|
||||||
|
VendorRef: VendorRef;
|
||||||
|
Line: BillLine[];
|
||||||
|
TotalAmt: number;
|
||||||
|
Balance: number;
|
||||||
|
APAccountRef?: AccountRef;
|
||||||
|
PrivateNote?: string;
|
||||||
|
CurrencyRef?: Ref;
|
||||||
|
ExchangeRate?: number;
|
||||||
|
SalesTermRef?: Ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bill Payment
|
||||||
|
export interface BillPayment extends QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
TxnDate: string;
|
||||||
|
VendorRef: VendorRef;
|
||||||
|
TotalAmt: number;
|
||||||
|
APAccountRef?: AccountRef;
|
||||||
|
PayType: 'Check' | 'CreditCard';
|
||||||
|
CheckPayment?: {
|
||||||
|
BankAccountRef: AccountRef;
|
||||||
|
PrintStatus?: 'NotSet' | 'NeedToPrint' | 'PrintComplete';
|
||||||
|
};
|
||||||
|
CreditCardPayment?: {
|
||||||
|
CCAccountRef: AccountRef;
|
||||||
|
};
|
||||||
|
Line?: Array<{
|
||||||
|
Amount: number;
|
||||||
|
LinkedTxn?: Array<{
|
||||||
|
TxnId: string;
|
||||||
|
TxnType: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
PrivateNote?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purchase
|
||||||
|
export interface Purchase extends QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
DocNumber?: string;
|
||||||
|
TxnDate: string;
|
||||||
|
AccountRef?: AccountRef;
|
||||||
|
PaymentType: 'Cash' | 'Check' | 'CreditCard';
|
||||||
|
EntityRef?: VendorRef | CustomerRef | EmployeeRef;
|
||||||
|
Line: BillLine[];
|
||||||
|
TotalAmt: number;
|
||||||
|
PrivateNote?: string;
|
||||||
|
CurrencyRef?: Ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purchase Order
|
||||||
|
export interface PurchaseOrder extends QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
DocNumber?: string;
|
||||||
|
TxnDate: string;
|
||||||
|
VendorRef: VendorRef;
|
||||||
|
Line: Array<{
|
||||||
|
Id?: string;
|
||||||
|
LineNum?: number;
|
||||||
|
Description?: string;
|
||||||
|
Amount: number;
|
||||||
|
DetailType: 'ItemBasedExpenseLineDetail';
|
||||||
|
ItemBasedExpenseLineDetail: {
|
||||||
|
ItemRef: ItemRef;
|
||||||
|
Qty?: number;
|
||||||
|
UnitPrice?: number;
|
||||||
|
ClassRef?: ClassRef;
|
||||||
|
CustomerRef?: CustomerRef;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
TotalAmt: number;
|
||||||
|
POStatus?: 'Open' | 'Closed';
|
||||||
|
POEmail?: { Address: string };
|
||||||
|
ShipAddr?: PhysicalAddress;
|
||||||
|
ShipMethodRef?: Ref;
|
||||||
|
PrivateNote?: string;
|
||||||
|
Memo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimate
|
||||||
|
export interface Estimate extends QBOEntity {
|
||||||
|
Id: EstimateId;
|
||||||
|
DocNumber?: string;
|
||||||
|
TxnDate: string;
|
||||||
|
CustomerRef: CustomerRef;
|
||||||
|
Line: InvoiceLine[];
|
||||||
|
TotalAmt: number;
|
||||||
|
BillAddr?: PhysicalAddress;
|
||||||
|
ShipAddr?: PhysicalAddress;
|
||||||
|
EmailStatus?: 'NotSet' | 'NeedToSend' | 'EmailSent';
|
||||||
|
PrintStatus?: 'NotSet' | 'NeedToPrint' | 'PrintComplete';
|
||||||
|
TxnStatus?: 'Accepted' | 'Closed' | 'Pending' | 'Rejected';
|
||||||
|
ExpirationDate?: string;
|
||||||
|
AcceptedBy?: string;
|
||||||
|
AcceptedDate?: string;
|
||||||
|
PrivateNote?: string;
|
||||||
|
CustomerMemo?: { value: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sales Receipt
|
||||||
|
export interface SalesReceipt extends QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
DocNumber?: string;
|
||||||
|
TxnDate: string;
|
||||||
|
CustomerRef: CustomerRef;
|
||||||
|
Line: InvoiceLine[];
|
||||||
|
TotalAmt: number;
|
||||||
|
Balance: number;
|
||||||
|
DepositToAccountRef?: AccountRef;
|
||||||
|
PaymentMethodRef?: Ref;
|
||||||
|
PaymentRefNum?: string;
|
||||||
|
BillAddr?: PhysicalAddress;
|
||||||
|
ShipAddr?: PhysicalAddress;
|
||||||
|
EmailStatus?: 'NotSet' | 'NeedToSend' | 'EmailSent';
|
||||||
|
PrintStatus?: 'NotSet' | 'NeedToPrint' | 'PrintComplete';
|
||||||
|
PrivateNote?: string;
|
||||||
|
CustomerMemo?: { value: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account
|
||||||
|
export type AccountType =
|
||||||
|
| 'Bank' | 'Other Current Asset' | 'Fixed Asset' | 'Other Asset'
|
||||||
|
| 'Accounts Receivable' | 'Equity' | 'Expense' | 'Other Expense'
|
||||||
|
| 'Cost of Goods Sold' | 'Accounts Payable' | 'Credit Card'
|
||||||
|
| 'Long Term Liability' | 'Other Current Liability' | 'Income'
|
||||||
|
| 'Other Income';
|
||||||
|
|
||||||
|
export type AccountSubType = string;
|
||||||
|
|
||||||
|
export interface Account extends QBOEntity {
|
||||||
|
Id: AccountId;
|
||||||
|
Name: string;
|
||||||
|
FullyQualifiedName?: string;
|
||||||
|
Active?: boolean;
|
||||||
|
AccountType: AccountType;
|
||||||
|
AccountSubType?: AccountSubType;
|
||||||
|
AcctNum?: string;
|
||||||
|
CurrentBalance?: number;
|
||||||
|
CurrentBalanceWithSubAccounts?: number;
|
||||||
|
CurrencyRef?: Ref;
|
||||||
|
ParentRef?: AccountRef;
|
||||||
|
Description?: string;
|
||||||
|
SubAccount?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Journal Entry
|
||||||
|
export interface JournalEntryLine {
|
||||||
|
Id?: string;
|
||||||
|
LineNum?: number;
|
||||||
|
Description?: string;
|
||||||
|
Amount: number;
|
||||||
|
DetailType: 'JournalEntryLineDetail';
|
||||||
|
JournalEntryLineDetail: {
|
||||||
|
PostingType: 'Debit' | 'Credit';
|
||||||
|
AccountRef: AccountRef;
|
||||||
|
Entity?: CustomerRef | VendorRef | EmployeeRef;
|
||||||
|
ClassRef?: ClassRef;
|
||||||
|
DepartmentRef?: DepartmentRef;
|
||||||
|
TaxCodeRef?: TaxCodeRef;
|
||||||
|
BillableStatus?: 'Billable' | 'NotBillable' | 'HasBeenBilled';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JournalEntry extends QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
DocNumber?: string;
|
||||||
|
TxnDate: string;
|
||||||
|
Line: JournalEntryLine[];
|
||||||
|
TotalAmt: number;
|
||||||
|
Adjustment?: boolean;
|
||||||
|
PrivateNote?: string;
|
||||||
|
CurrencyRef?: Ref;
|
||||||
|
ExchangeRate?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deposit
|
||||||
|
export interface Deposit extends QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
TxnDate: string;
|
||||||
|
DepositToAccountRef: AccountRef;
|
||||||
|
Line: Array<{
|
||||||
|
Id?: string;
|
||||||
|
LineNum?: number;
|
||||||
|
Description?: string;
|
||||||
|
Amount: number;
|
||||||
|
DetailType: 'DepositLineDetail';
|
||||||
|
DepositLineDetail: {
|
||||||
|
Entity?: CustomerRef | VendorRef | EmployeeRef;
|
||||||
|
AccountRef: AccountRef;
|
||||||
|
ClassRef?: ClassRef;
|
||||||
|
PaymentMethodRef?: Ref;
|
||||||
|
CheckNum?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
TotalAmt: number;
|
||||||
|
PrivateNote?: string;
|
||||||
|
CurrencyRef?: Ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer
|
||||||
|
export interface Transfer extends QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
TxnDate: string;
|
||||||
|
FromAccountRef: AccountRef;
|
||||||
|
ToAccountRef: AccountRef;
|
||||||
|
Amount: number;
|
||||||
|
PrivateNote?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tax Code
|
||||||
|
export interface TaxCode extends QBOEntity {
|
||||||
|
Id: TaxCodeId;
|
||||||
|
Name: string;
|
||||||
|
Description?: string;
|
||||||
|
Active?: boolean;
|
||||||
|
Taxable?: boolean;
|
||||||
|
TaxGroup?: boolean;
|
||||||
|
SalesTaxRateList?: {
|
||||||
|
TaxRateDetail: Array<{
|
||||||
|
TaxRateRef: Ref;
|
||||||
|
TaxTypeApplicable?: string;
|
||||||
|
TaxOrder?: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
PurchaseTaxRateList?: {
|
||||||
|
TaxRateDetail: Array<{
|
||||||
|
TaxRateRef: Ref;
|
||||||
|
TaxTypeApplicable?: string;
|
||||||
|
TaxOrder?: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tax Rate
|
||||||
|
export interface TaxRate extends QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
Name: string;
|
||||||
|
Description?: string;
|
||||||
|
Active?: boolean;
|
||||||
|
RateValue?: number;
|
||||||
|
AgencyRef?: Ref;
|
||||||
|
SpecialTaxType?: string;
|
||||||
|
DisplayType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tax Agency
|
||||||
|
export interface TaxAgency extends QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
DisplayName: string;
|
||||||
|
TaxTrackedOnPurchases?: boolean;
|
||||||
|
TaxTrackedOnSales?: boolean;
|
||||||
|
TaxTrackedOnSalesReceipts?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time Activity
|
||||||
|
export interface TimeActivity extends QBOEntity {
|
||||||
|
Id: TimeActivityId;
|
||||||
|
TxnDate: string;
|
||||||
|
EmployeeRef?: EmployeeRef;
|
||||||
|
VendorRef?: VendorRef;
|
||||||
|
CustomerRef?: CustomerRef;
|
||||||
|
ItemRef?: ItemRef;
|
||||||
|
ClassRef?: ClassRef;
|
||||||
|
NameOf: 'Employee' | 'Vendor';
|
||||||
|
Hours?: number;
|
||||||
|
Minutes?: number;
|
||||||
|
StartTime?: string;
|
||||||
|
EndTime?: string;
|
||||||
|
Description?: string;
|
||||||
|
HourlyRate?: number;
|
||||||
|
BillableStatus?: 'Billable' | 'NotBillable' | 'HasBeenBilled';
|
||||||
|
Taxable?: boolean;
|
||||||
|
BreakHours?: number;
|
||||||
|
BreakMinutes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Class
|
||||||
|
export interface Class extends QBOEntity {
|
||||||
|
Id: ClassId;
|
||||||
|
Name: string;
|
||||||
|
FullyQualifiedName?: string;
|
||||||
|
Active?: boolean;
|
||||||
|
SubClass?: boolean;
|
||||||
|
ParentRef?: ClassRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Department
|
||||||
|
export interface Department extends QBOEntity {
|
||||||
|
Id: DepartmentId;
|
||||||
|
Name: string;
|
||||||
|
FullyQualifiedName?: string;
|
||||||
|
Active?: boolean;
|
||||||
|
SubDepartment?: boolean;
|
||||||
|
ParentRef?: DepartmentRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Company Info
|
||||||
|
export interface CompanyInfo extends QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
CompanyName: string;
|
||||||
|
LegalName?: string;
|
||||||
|
CompanyAddr?: PhysicalAddress;
|
||||||
|
CustomerCommunicationAddr?: PhysicalAddress;
|
||||||
|
LegalAddr?: PhysicalAddress;
|
||||||
|
PrimaryPhone?: { FreeFormNumber: string };
|
||||||
|
CompanyStartDate?: string;
|
||||||
|
FiscalYearStartMonth?: string;
|
||||||
|
Country?: string;
|
||||||
|
Email?: { Address: string };
|
||||||
|
WebAddr?: { URI: string };
|
||||||
|
SupportedLanguages?: string;
|
||||||
|
NameValue?: Array<{
|
||||||
|
Name: string;
|
||||||
|
Value: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
export interface Preferences extends QBOEntity {
|
||||||
|
Id: string;
|
||||||
|
AccountingInfoPrefs?: {
|
||||||
|
FirstMonthOfFiscalYear?: string;
|
||||||
|
UseAccountNumbers?: boolean;
|
||||||
|
TaxYearMonth?: string;
|
||||||
|
ClassTrackingPerTxn?: boolean;
|
||||||
|
TrackDepartments?: boolean;
|
||||||
|
DepartmentTerminology?: string;
|
||||||
|
ClassTrackingPerTxnLine?: boolean;
|
||||||
|
};
|
||||||
|
ProductAndServicesPrefs?: {
|
||||||
|
ForSales?: boolean;
|
||||||
|
ForPurchase?: boolean;
|
||||||
|
QuantityWithPriceAndRate?: boolean;
|
||||||
|
QuantityOnHand?: boolean;
|
||||||
|
};
|
||||||
|
SalesFormsPrefs?: {
|
||||||
|
CustomField?: Array<{
|
||||||
|
CustomField: Array<{
|
||||||
|
Name: string;
|
||||||
|
Type: string;
|
||||||
|
StringValue?: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
AllowDeposit?: boolean;
|
||||||
|
AllowDiscount?: boolean;
|
||||||
|
DefaultDiscountAccount?: string;
|
||||||
|
AllowEstimates?: boolean;
|
||||||
|
AllowShipping?: boolean;
|
||||||
|
DefaultShippingAccount?: string;
|
||||||
|
IPNSupportEnabled?: boolean;
|
||||||
|
UsingPriceLevels?: boolean;
|
||||||
|
UsingProgressInvoicing?: boolean;
|
||||||
|
ETransactionEnabledStatus?: string;
|
||||||
|
ETransactionPaymentEnabled?: boolean;
|
||||||
|
ETransactionAttachPDF?: boolean;
|
||||||
|
};
|
||||||
|
EmailMessagesPrefs?: {
|
||||||
|
InvoiceMessage?: { Subject?: string; Message?: string };
|
||||||
|
EstimateMessage?: { Subject?: string; Message?: string };
|
||||||
|
SalesReceiptMessage?: { Subject?: string; Message?: string };
|
||||||
|
};
|
||||||
|
VendorAndPurchasesPrefs?: {
|
||||||
|
TrackingByCustomer?: boolean;
|
||||||
|
BillableExpenseTracking?: boolean;
|
||||||
|
POCustomField?: Array<{
|
||||||
|
CustomField: Array<{
|
||||||
|
Name: string;
|
||||||
|
Type: string;
|
||||||
|
StringValue?: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
TimeTrackingPrefs?: {
|
||||||
|
UseServices?: boolean;
|
||||||
|
BillCustomers?: boolean;
|
||||||
|
ShowBillRateToAll?: boolean;
|
||||||
|
WorkWeekStartDate?: string;
|
||||||
|
MarkTimeEntriesBillable?: boolean;
|
||||||
|
};
|
||||||
|
TaxPrefs?: {
|
||||||
|
UsingSalesTax?: boolean;
|
||||||
|
TaxGroupCodeRef?: Ref;
|
||||||
|
};
|
||||||
|
CurrencyPrefs?: {
|
||||||
|
MultiCurrencyEnabled?: boolean;
|
||||||
|
HomeCurrency?: Ref;
|
||||||
|
};
|
||||||
|
ReportPrefs?: {
|
||||||
|
ReportBasis?: 'Accrual' | 'Cash';
|
||||||
|
CalcAgingReportFromTxnDate?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report types
|
||||||
|
export interface ReportColumn {
|
||||||
|
ColTitle?: string;
|
||||||
|
ColType?: string;
|
||||||
|
MetaData?: Array<{
|
||||||
|
Name: string;
|
||||||
|
Value: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportRow {
|
||||||
|
type: string;
|
||||||
|
Summary?: {
|
||||||
|
ColData: Array<{
|
||||||
|
value: string;
|
||||||
|
id?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
ColData?: Array<{
|
||||||
|
value: string;
|
||||||
|
id?: string;
|
||||||
|
}>;
|
||||||
|
Rows?: {
|
||||||
|
Row: ReportRow[];
|
||||||
|
};
|
||||||
|
group?: string;
|
||||||
|
Header?: {
|
||||||
|
ColData: Array<{
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Report {
|
||||||
|
Header: {
|
||||||
|
Time: string;
|
||||||
|
ReportName: string;
|
||||||
|
DateMacro?: string;
|
||||||
|
StartPeriod?: string;
|
||||||
|
EndPeriod?: string;
|
||||||
|
Currency?: string;
|
||||||
|
Option?: Array<{
|
||||||
|
Name: string;
|
||||||
|
Value: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
Columns: {
|
||||||
|
Column: ReportColumn[];
|
||||||
|
};
|
||||||
|
Rows: {
|
||||||
|
Row: ReportRow[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfitAndLoss extends Report {
|
||||||
|
Header: Report['Header'] & {
|
||||||
|
ReportName: 'ProfitAndLoss';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BalanceSheet extends Report {
|
||||||
|
Header: Report['Header'] & {
|
||||||
|
ReportName: 'BalanceSheet';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CashFlow extends Report {
|
||||||
|
Header: Report['Header'] & {
|
||||||
|
ReportName: 'CashFlow';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query Response wrapper
|
||||||
|
export interface QueryResponse<T> {
|
||||||
|
QueryResponse: {
|
||||||
|
[key: string]: T[] | number | undefined;
|
||||||
|
startPosition: number;
|
||||||
|
maxResults: number;
|
||||||
|
totalCount?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error response
|
||||||
|
export interface QBOError {
|
||||||
|
Fault: {
|
||||||
|
Error: Array<{
|
||||||
|
Message: string;
|
||||||
|
Detail: string;
|
||||||
|
code: string;
|
||||||
|
element?: string;
|
||||||
|
}>;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
21
servers/quickbooks/tsconfig.json
Normal file
21
servers/quickbooks/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
4
servers/salesforce/.env.example
Normal file
4
servers/salesforce/.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Salesforce credentials
|
||||||
|
SF_ACCESS_TOKEN=your_oauth2_access_token_here
|
||||||
|
SF_INSTANCE_URL=https://yourinstance.my.salesforce.com
|
||||||
|
SF_API_VERSION=v59.0
|
||||||
7
servers/salesforce/.gitignore
vendored
Normal file
7
servers/salesforce/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
165
servers/salesforce/README.md
Normal file
165
servers/salesforce/README.md
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
# Salesforce MCP Server
|
||||||
|
|
||||||
|
Model Context Protocol (MCP) server for Salesforce integration. Provides tools for querying and managing Salesforce data through the REST API.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **SOQL Queries**: Execute powerful SOQL queries with automatic pagination
|
||||||
|
- **CRUD Operations**: Create, read, update, and delete records across all Salesforce objects
|
||||||
|
- **Bulk API**: Handle large data operations efficiently with Bulk API 2.0
|
||||||
|
- **Composite Requests**: Batch multiple operations in a single API call
|
||||||
|
- **Object Metadata**: Describe Salesforce objects and fields
|
||||||
|
- **Rate Limiting**: Automatic retry with exponential backoff and API limit tracking
|
||||||
|
- **Type Safety**: Comprehensive TypeScript types for all Salesforce entities
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create a `.env` file based on `.env.example`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SF_ACCESS_TOKEN=your_oauth2_access_token_here
|
||||||
|
SF_INSTANCE_URL=https://yourinstance.my.salesforce.com
|
||||||
|
SF_API_VERSION=v59.0 # Optional, defaults to v59.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting an Access Token
|
||||||
|
|
||||||
|
1. **Connected App**: Create a Connected App in Salesforce Setup
|
||||||
|
2. **OAuth Flow**: Use OAuth 2.0 to obtain an access token
|
||||||
|
3. **Refresh Token**: Implement token refresh logic for long-running servers
|
||||||
|
|
||||||
|
For development, you can use Salesforce CLI:
|
||||||
|
```bash
|
||||||
|
sfdx force:org:display --verbose -u your_org_alias
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
### Query Tools
|
||||||
|
|
||||||
|
- `salesforce_query`: Execute SOQL queries
|
||||||
|
- `salesforce_describe_object`: Get object metadata
|
||||||
|
|
||||||
|
### CRUD Tools
|
||||||
|
|
||||||
|
- `salesforce_create_record`: Create new records
|
||||||
|
- `salesforce_update_record`: Update existing records
|
||||||
|
- `salesforce_delete_record`: Delete records
|
||||||
|
|
||||||
|
### Bulk Operations
|
||||||
|
|
||||||
|
- Bulk API 2.0 support for large data operations
|
||||||
|
- Automatic CSV upload and job monitoring
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
- **`src/types/index.ts`**: TypeScript definitions for all Salesforce entities
|
||||||
|
- **`src/clients/salesforce.ts`**: REST API client with retry logic and caching
|
||||||
|
- **`src/server.ts`**: MCP server with lazy-loaded tool modules
|
||||||
|
- **`src/main.ts`**: Entry point with environment validation
|
||||||
|
|
||||||
|
### Supported Objects
|
||||||
|
|
||||||
|
- Standard Objects: Account, Contact, Lead, Opportunity, Case, Task, Event
|
||||||
|
- Marketing: Campaign, CampaignMember
|
||||||
|
- Admin: User, UserRole, Profile, PermissionSet
|
||||||
|
- Reports: Report, Dashboard
|
||||||
|
- Files: ContentDocument, ContentVersion, Attachment
|
||||||
|
- Custom Objects: Generic support for any custom object
|
||||||
|
|
||||||
|
## API Patterns
|
||||||
|
|
||||||
|
### SOQL Queries
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Simple query
|
||||||
|
const result = await client.query('SELECT Id, Name FROM Account LIMIT 10');
|
||||||
|
|
||||||
|
// Query builder
|
||||||
|
const soql = client.buildSOQL({
|
||||||
|
select: ['Id', 'Name', 'Industry'],
|
||||||
|
from: 'Account',
|
||||||
|
where: "Industry = 'Technology'",
|
||||||
|
orderBy: 'Name ASC',
|
||||||
|
limit: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query all (auto-pagination)
|
||||||
|
const allAccounts = await client.queryAll('SELECT Id, Name FROM Account');
|
||||||
|
```
|
||||||
|
|
||||||
|
### CRUD Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create
|
||||||
|
const result = await client.createRecord('Account', {
|
||||||
|
Name: 'Acme Corp',
|
||||||
|
Industry: 'Technology'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update
|
||||||
|
await client.updateRecord('Account', accountId, {
|
||||||
|
Phone: '555-1234'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
await client.deleteRecord('Account', accountId);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bulk API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create bulk job
|
||||||
|
const job = await client.createBulkJob({
|
||||||
|
object: 'Account',
|
||||||
|
operation: 'insert'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload CSV data
|
||||||
|
await client.uploadBulkData(job.id, csvData);
|
||||||
|
|
||||||
|
// Close job
|
||||||
|
await client.closeBulkJob(job.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The client includes:
|
||||||
|
- Automatic retry with exponential backoff (3 retries)
|
||||||
|
- Rate limit detection and handling
|
||||||
|
- Structured error responses
|
||||||
|
- API limit tracking via `Sforce-Limit-Info` header
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Salesforce REST API Documentation](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/)
|
||||||
|
- [SOQL Reference](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/)
|
||||||
|
- [Bulk API 2.0](https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
21
servers/salesforce/package.json
Normal file
21
servers/salesforce/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@mcpengine/salesforce",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"dev": "tsx watch src/main.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
|
"axios": "^1.7.0",
|
||||||
|
"zod": "^3.23.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"tsx": "^4.19.0",
|
||||||
|
"@types/node": "^22.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
404
servers/salesforce/src/clients/salesforce.ts
Normal file
404
servers/salesforce/src/clients/salesforce.ts
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
/**
|
||||||
|
* Salesforce REST API Client
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||||
|
import type {
|
||||||
|
SObject,
|
||||||
|
QueryResult,
|
||||||
|
SObjectDescribe,
|
||||||
|
BulkJob,
|
||||||
|
BulkJobInfo,
|
||||||
|
CompositeRequest,
|
||||||
|
CompositeResponse,
|
||||||
|
SalesforceErrorResponse,
|
||||||
|
ApiLimits,
|
||||||
|
} from '../types/index.js';
|
||||||
|
|
||||||
|
export interface SalesforceClientConfig {
|
||||||
|
accessToken: string;
|
||||||
|
instanceUrl: string;
|
||||||
|
apiVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SalesforceClient {
|
||||||
|
private client: AxiosInstance;
|
||||||
|
private instanceUrl: string;
|
||||||
|
private apiVersion: string;
|
||||||
|
private describeCache: Map<string, SObjectDescribe> = new Map();
|
||||||
|
private apiLimits: ApiLimits | null = null;
|
||||||
|
|
||||||
|
constructor(config: SalesforceClientConfig) {
|
||||||
|
this.instanceUrl = config.instanceUrl.replace(/\/$/, '');
|
||||||
|
this.apiVersion = config.apiVersion || 'v59.0';
|
||||||
|
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: `${this.instanceUrl}/services/data/${this.apiVersion}`,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${config.accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Intercept responses to extract API limit info
|
||||||
|
this.client.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
const limitInfo = response.headers['sforce-limit-info'];
|
||||||
|
if (limitInfo) {
|
||||||
|
this.parseApiLimits(limitInfo);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseApiLimits(limitInfo: string): void {
|
||||||
|
// Format: "api-usage=123/15000"
|
||||||
|
const match = limitInfo.match(/api-usage=(\d+)\/(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
this.apiLimits = {
|
||||||
|
used: parseInt(match[1], 10),
|
||||||
|
remaining: parseInt(match[2], 10) - parseInt(match[1], 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getApiLimits(): ApiLimits | null {
|
||||||
|
return this.apiLimits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry wrapper with exponential backoff
|
||||||
|
*/
|
||||||
|
private async retryRequest<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
retries = 3,
|
||||||
|
delay = 1000
|
||||||
|
): Promise<T> {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
if (retries === 0) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const axiosError = error as AxiosError;
|
||||||
|
const shouldRetry =
|
||||||
|
axiosError.response?.status === 429 || // Rate limit
|
||||||
|
axiosError.response?.status === 503 || // Service unavailable
|
||||||
|
axiosError.code === 'ECONNRESET' ||
|
||||||
|
axiosError.code === 'ETIMEDOUT';
|
||||||
|
|
||||||
|
if (!shouldRetry) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
return this.retryRequest(fn, retries - 1, delay * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute SOQL query
|
||||||
|
*/
|
||||||
|
public async query<T extends SObject = SObject>(
|
||||||
|
soql: string
|
||||||
|
): Promise<QueryResult<T>> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.get<QueryResult<T>>('/query', {
|
||||||
|
params: { q: soql },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query more records using nextRecordsUrl
|
||||||
|
*/
|
||||||
|
public async queryMore<T extends SObject = SObject>(
|
||||||
|
nextRecordsUrl: string
|
||||||
|
): Promise<QueryResult<T>> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.get<QueryResult<T>>(nextRecordsUrl);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute SOSL search
|
||||||
|
*/
|
||||||
|
public async search<T extends SObject = SObject>(
|
||||||
|
sosl: string
|
||||||
|
): Promise<T[]> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.get<{ searchRecords: T[] }>('/search', {
|
||||||
|
params: { q: sosl },
|
||||||
|
});
|
||||||
|
return response.data.searchRecords;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a record by ID
|
||||||
|
*/
|
||||||
|
public async getRecord<T extends SObject = SObject>(
|
||||||
|
objectType: string,
|
||||||
|
id: string,
|
||||||
|
fields?: string[]
|
||||||
|
): Promise<T> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const params = fields ? { fields: fields.join(',') } : undefined;
|
||||||
|
const response = await this.client.get<T>(`/sobjects/${objectType}/${id}`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a record
|
||||||
|
*/
|
||||||
|
public async createRecord<T extends SObject = SObject>(
|
||||||
|
objectType: string,
|
||||||
|
data: Partial<T>
|
||||||
|
): Promise<{ id: string; success: boolean; errors: SalesforceErrorResponse[] }> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.post(`/sobjects/${objectType}`, data);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a record
|
||||||
|
*/
|
||||||
|
public async updateRecord<T extends SObject = SObject>(
|
||||||
|
objectType: string,
|
||||||
|
id: string,
|
||||||
|
data: Partial<T>
|
||||||
|
): Promise<void> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
await this.client.patch(`/sobjects/${objectType}/${id}`, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert a record (insert or update based on external ID)
|
||||||
|
*/
|
||||||
|
public async upsertRecord<T extends SObject = SObject>(
|
||||||
|
objectType: string,
|
||||||
|
externalIdField: string,
|
||||||
|
externalIdValue: string,
|
||||||
|
data: Partial<T>
|
||||||
|
): Promise<{ id: string; success: boolean; created: boolean }> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.patch(
|
||||||
|
`/sobjects/${objectType}/${externalIdField}/${externalIdValue}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a record
|
||||||
|
*/
|
||||||
|
public async deleteRecord(objectType: string, id: string): Promise<void> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
await this.client.delete(`/sobjects/${objectType}/${id}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describe an SObject (with caching)
|
||||||
|
*/
|
||||||
|
public async describe(objectType: string, skipCache = false): Promise<SObjectDescribe> {
|
||||||
|
if (!skipCache && this.describeCache.has(objectType)) {
|
||||||
|
return this.describeCache.get(objectType)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.get<SObjectDescribe>(
|
||||||
|
`/sobjects/${objectType}/describe`
|
||||||
|
);
|
||||||
|
this.describeCache.set(objectType, response.data);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute composite request (up to 25 subrequests)
|
||||||
|
*/
|
||||||
|
public async composite(request: CompositeRequest): Promise<CompositeResponse> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.post<CompositeResponse>(
|
||||||
|
'/composite',
|
||||||
|
request
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute batch request
|
||||||
|
*/
|
||||||
|
public async batch(
|
||||||
|
requests: Array<{
|
||||||
|
method: string;
|
||||||
|
url: string;
|
||||||
|
richInput?: Record<string, unknown>;
|
||||||
|
}>
|
||||||
|
): Promise<{ hasErrors: boolean; results: unknown[] }> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.post('/composite/batch', {
|
||||||
|
batchRequests: requests,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Bulk API 2.0 job
|
||||||
|
*/
|
||||||
|
public async createBulkJob(job: BulkJob): Promise<BulkJobInfo> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.post<BulkJobInfo>('/jobs/ingest', {
|
||||||
|
object: job.object,
|
||||||
|
operation: job.operation,
|
||||||
|
externalIdFieldName: job.externalIdFieldName,
|
||||||
|
contentType: job.contentType || 'CSV',
|
||||||
|
lineEnding: job.lineEnding || 'LF',
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload data to a bulk job
|
||||||
|
*/
|
||||||
|
public async uploadBulkData(jobId: string, data: string): Promise<void> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
await this.client.put(`/jobs/ingest/${jobId}/batches`, data, {
|
||||||
|
headers: { 'Content-Type': 'text/csv' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close/complete a bulk job
|
||||||
|
*/
|
||||||
|
public async closeBulkJob(jobId: string): Promise<BulkJobInfo> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.patch<BulkJobInfo>(`/jobs/ingest/${jobId}`, {
|
||||||
|
state: 'UploadComplete',
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abort a bulk job
|
||||||
|
*/
|
||||||
|
public async abortBulkJob(jobId: string): Promise<BulkJobInfo> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.patch<BulkJobInfo>(`/jobs/ingest/${jobId}`, {
|
||||||
|
state: 'Aborted',
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bulk job info
|
||||||
|
*/
|
||||||
|
public async getBulkJobInfo(jobId: string): Promise<BulkJobInfo> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.get<BulkJobInfo>(`/jobs/ingest/${jobId}`);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get successful results from a bulk job
|
||||||
|
*/
|
||||||
|
public async getBulkJobSuccessfulResults(jobId: string): Promise<string> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.get<string>(
|
||||||
|
`/jobs/ingest/${jobId}/successfulResults`,
|
||||||
|
{ headers: { Accept: 'text/csv' } }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get failed results from a bulk job
|
||||||
|
*/
|
||||||
|
public async getBulkJobFailedResults(jobId: string): Promise<string> {
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.get<string>(
|
||||||
|
`/jobs/ingest/${jobId}/failedResults`,
|
||||||
|
{ headers: { Accept: 'text/csv' } }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SOQL query builder helper
|
||||||
|
*/
|
||||||
|
public buildSOQL(options: {
|
||||||
|
select: string[];
|
||||||
|
from: string;
|
||||||
|
where?: string;
|
||||||
|
orderBy?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}): string {
|
||||||
|
let soql = `SELECT ${options.select.join(', ')} FROM ${options.from}`;
|
||||||
|
|
||||||
|
if (options.where) {
|
||||||
|
soql += ` WHERE ${options.where}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.orderBy) {
|
||||||
|
soql += ` ORDER BY ${options.orderBy}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.limit) {
|
||||||
|
soql += ` LIMIT ${options.limit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.offset) {
|
||||||
|
soql += ` OFFSET ${options.offset}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return soql;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: Query all records (handles pagination automatically)
|
||||||
|
*/
|
||||||
|
public async queryAll<T extends SObject = SObject>(
|
||||||
|
soql: string
|
||||||
|
): Promise<T[]> {
|
||||||
|
const allRecords: T[] = [];
|
||||||
|
let result = await this.query<T>(soql);
|
||||||
|
allRecords.push(...result.records);
|
||||||
|
|
||||||
|
while (!result.done && result.nextRecordsUrl) {
|
||||||
|
result = await this.queryMore<T>(result.nextRecordsUrl);
|
||||||
|
allRecords.push(...result.records);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear describe cache
|
||||||
|
*/
|
||||||
|
public clearDescribeCache(): void {
|
||||||
|
this.describeCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
105
servers/salesforce/src/main.ts
Normal file
105
servers/salesforce/src/main.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Salesforce MCP Server Entry Point
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SalesforceServer } from './server.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Environment variable schema
|
||||||
|
const EnvSchema = z.object({
|
||||||
|
SF_ACCESS_TOKEN: z.string().min(1, 'SF_ACCESS_TOKEN is required'),
|
||||||
|
SF_INSTANCE_URL: z.string().url('SF_INSTANCE_URL must be a valid URL'),
|
||||||
|
SF_API_VERSION: z.string().optional().default('v59.0'),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate environment variables
|
||||||
|
*/
|
||||||
|
function validateEnv() {
|
||||||
|
try {
|
||||||
|
return EnvSchema.parse({
|
||||||
|
SF_ACCESS_TOKEN: process.env.SF_ACCESS_TOKEN,
|
||||||
|
SF_INSTANCE_URL: process.env.SF_INSTANCE_URL,
|
||||||
|
SF_API_VERSION: process.env.SF_API_VERSION,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
console.error('Environment validation failed:');
|
||||||
|
error.errors.forEach((err) => {
|
||||||
|
console.error(` - ${err.path.join('.')}: ${err.message}`);
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check endpoint (if needed for HTTP transport)
|
||||||
|
*/
|
||||||
|
function healthCheck(): { status: string; timestamp: string } {
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
// Validate environment
|
||||||
|
const env = validateEnv();
|
||||||
|
|
||||||
|
console.error('Starting Salesforce MCP Server...');
|
||||||
|
console.error(`Instance URL: ${env.SF_INSTANCE_URL}`);
|
||||||
|
console.error(`API Version: ${env.SF_API_VERSION}`);
|
||||||
|
|
||||||
|
// Create and start server
|
||||||
|
const server = new SalesforceServer({
|
||||||
|
accessToken: env.SF_ACCESS_TOKEN,
|
||||||
|
instanceUrl: env.SF_INSTANCE_URL,
|
||||||
|
apiVersion: env.SF_API_VERSION,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown handlers
|
||||||
|
const shutdown = async (signal: string) => {
|
||||||
|
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'));
|
||||||
|
|
||||||
|
// Handle uncaught errors
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.error('Uncaught exception:', error);
|
||||||
|
shutdown('uncaughtException');
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('Unhandled rejection at:', promise, 'reason:', reason);
|
||||||
|
shutdown('unhandledRejection');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server (stdio transport by default)
|
||||||
|
try {
|
||||||
|
await server.start();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run main function
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
362
servers/salesforce/src/server.ts
Normal file
362
servers/salesforce/src/server.ts
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
/**
|
||||||
|
* Salesforce MCP Server
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { SalesforceClient } from './clients/salesforce.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export interface SalesforceServerConfig {
|
||||||
|
accessToken: string;
|
||||||
|
instanceUrl: string;
|
||||||
|
apiVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SalesforceServer {
|
||||||
|
private server: Server;
|
||||||
|
private client: SalesforceClient;
|
||||||
|
|
||||||
|
constructor(config: SalesforceServerConfig) {
|
||||||
|
this.server = new Server(
|
||||||
|
{
|
||||||
|
name: 'salesforce-mcp-server',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
resources: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.client = new SalesforceClient({
|
||||||
|
accessToken: config.accessToken,
|
||||||
|
instanceUrl: config.instanceUrl,
|
||||||
|
apiVersion: config.apiVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup MCP handlers
|
||||||
|
*/
|
||||||
|
private setupHandlers(): void {
|
||||||
|
// List available tools
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
// Foundation tools - additional tools can be added via tool modules later
|
||||||
|
{
|
||||||
|
name: 'salesforce_query',
|
||||||
|
description: 'Execute a SOQL query against Salesforce',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'SOQL query string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['query'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'salesforce_create_record',
|
||||||
|
description: 'Create a new Salesforce record',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
objectType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Salesforce object type (e.g., Account, Contact)',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Record data',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['objectType', 'data'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'salesforce_update_record',
|
||||||
|
description: 'Update an existing Salesforce record',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
objectType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Salesforce object type',
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Record ID',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Fields to update',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['objectType', 'id', 'data'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'salesforce_delete_record',
|
||||||
|
description: 'Delete a Salesforce record',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
objectType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Salesforce object type',
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Record ID',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['objectType', 'id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'salesforce_describe_object',
|
||||||
|
description: 'Get metadata for a Salesforce object',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
objectType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Salesforce object type',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['objectType'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle tool calls
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Route to appropriate handler
|
||||||
|
switch (name) {
|
||||||
|
case 'salesforce_query':
|
||||||
|
return await this.handleQuery(args as { query: string });
|
||||||
|
|
||||||
|
case 'salesforce_create_record':
|
||||||
|
return await this.handleCreateRecord(
|
||||||
|
args as { objectType: string; data: Record<string, unknown> }
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'salesforce_update_record':
|
||||||
|
return await this.handleUpdateRecord(
|
||||||
|
args as { objectType: string; id: string; data: Record<string, unknown> }
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'salesforce_delete_record':
|
||||||
|
return await this.handleDeleteRecord(
|
||||||
|
args as { objectType: string; id: string }
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'salesforce_describe_object':
|
||||||
|
return await this.handleDescribeObject(
|
||||||
|
args as { objectType: string }
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${name}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return this.formatError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// List resources (for UI apps)
|
||||||
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
uri: 'salesforce://dashboard',
|
||||||
|
name: 'Salesforce Dashboard',
|
||||||
|
description: 'Overview of Salesforce data and metrics',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read resource
|
||||||
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
const { uri } = request.params;
|
||||||
|
|
||||||
|
if (uri === 'salesforce://dashboard') {
|
||||||
|
const limits = this.client.getApiLimits();
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(
|
||||||
|
{
|
||||||
|
apiLimits: limits,
|
||||||
|
instanceUrl: this.client['instanceUrl'],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown resource: ${uri}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle SOQL query
|
||||||
|
*/
|
||||||
|
private async handleQuery(args: { query: string }) {
|
||||||
|
const result = await this.client.query(args.query);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle create record
|
||||||
|
*/
|
||||||
|
private async handleCreateRecord(args: {
|
||||||
|
objectType: string;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
const result = await this.client.createRecord(args.objectType, args.data);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle update record
|
||||||
|
*/
|
||||||
|
private async handleUpdateRecord(args: {
|
||||||
|
objectType: string;
|
||||||
|
id: string;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
await this.client.updateRecord(args.objectType, args.id, args.data);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Record updated successfully',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle delete record
|
||||||
|
*/
|
||||||
|
private async handleDeleteRecord(args: { objectType: string; id: string }) {
|
||||||
|
await this.client.deleteRecord(args.objectType, args.id);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Record deleted successfully',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle describe object
|
||||||
|
*/
|
||||||
|
private async handleDescribeObject(args: { objectType: string }) {
|
||||||
|
const result = await this.client.describe(args.objectType);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format error response
|
||||||
|
*/
|
||||||
|
private formatError(error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the server
|
||||||
|
*/
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await this.server.connect(transport);
|
||||||
|
console.error('Salesforce MCP Server running on stdio');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graceful shutdown
|
||||||
|
*/
|
||||||
|
public async close(): Promise<void> {
|
||||||
|
await this.server.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the underlying Server instance
|
||||||
|
*/
|
||||||
|
public getServer(): Server {
|
||||||
|
return this.server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Salesforce client
|
||||||
|
*/
|
||||||
|
public getClient(): SalesforceClient {
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
467
servers/salesforce/src/types/index.ts
Normal file
467
servers/salesforce/src/types/index.ts
Normal file
@ -0,0 +1,467 @@
|
|||||||
|
/**
|
||||||
|
* Salesforce REST API TypeScript Definitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Branded type for Salesforce IDs (15 or 18 characters)
|
||||||
|
export type SalesforceId = string & { readonly __brand: 'SalesforceId' };
|
||||||
|
|
||||||
|
// Helper to create branded IDs
|
||||||
|
export const salesforceId = (id: string): SalesforceId => id as SalesforceId;
|
||||||
|
|
||||||
|
// Base SObject interface - all Salesforce objects extend this
|
||||||
|
export interface SObject {
|
||||||
|
Id?: SalesforceId;
|
||||||
|
IsDeleted?: boolean;
|
||||||
|
CreatedDate?: string;
|
||||||
|
CreatedById?: SalesforceId;
|
||||||
|
LastModifiedDate?: string;
|
||||||
|
LastModifiedById?: SalesforceId;
|
||||||
|
SystemModstamp?: string;
|
||||||
|
attributes?: {
|
||||||
|
type: string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard Objects
|
||||||
|
export interface Account extends SObject {
|
||||||
|
Name: string;
|
||||||
|
Type?: string;
|
||||||
|
Industry?: string;
|
||||||
|
BillingStreet?: string;
|
||||||
|
BillingCity?: string;
|
||||||
|
BillingState?: string;
|
||||||
|
BillingPostalCode?: string;
|
||||||
|
BillingCountry?: string;
|
||||||
|
ShippingStreet?: string;
|
||||||
|
ShippingCity?: string;
|
||||||
|
ShippingState?: string;
|
||||||
|
ShippingPostalCode?: string;
|
||||||
|
ShippingCountry?: string;
|
||||||
|
Phone?: string;
|
||||||
|
Fax?: string;
|
||||||
|
Website?: string;
|
||||||
|
Description?: string;
|
||||||
|
NumberOfEmployees?: number;
|
||||||
|
AnnualRevenue?: number;
|
||||||
|
OwnerId?: SalesforceId;
|
||||||
|
ParentId?: SalesforceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Contact extends SObject {
|
||||||
|
FirstName?: string;
|
||||||
|
LastName: string;
|
||||||
|
AccountId?: SalesforceId;
|
||||||
|
Email?: string;
|
||||||
|
Phone?: string;
|
||||||
|
MobilePhone?: string;
|
||||||
|
Title?: string;
|
||||||
|
Department?: string;
|
||||||
|
MailingStreet?: string;
|
||||||
|
MailingCity?: string;
|
||||||
|
MailingState?: string;
|
||||||
|
MailingPostalCode?: string;
|
||||||
|
MailingCountry?: string;
|
||||||
|
Description?: string;
|
||||||
|
OwnerId?: SalesforceId;
|
||||||
|
ReportsToId?: SalesforceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Lead extends SObject {
|
||||||
|
FirstName?: string;
|
||||||
|
LastName: string;
|
||||||
|
Company: string;
|
||||||
|
Status: string;
|
||||||
|
Email?: string;
|
||||||
|
Phone?: string;
|
||||||
|
MobilePhone?: string;
|
||||||
|
Title?: string;
|
||||||
|
Industry?: string;
|
||||||
|
Street?: string;
|
||||||
|
City?: string;
|
||||||
|
State?: string;
|
||||||
|
PostalCode?: string;
|
||||||
|
Country?: string;
|
||||||
|
Description?: string;
|
||||||
|
OwnerId?: SalesforceId;
|
||||||
|
Rating?: string;
|
||||||
|
LeadSource?: string;
|
||||||
|
ConvertedAccountId?: SalesforceId;
|
||||||
|
ConvertedContactId?: SalesforceId;
|
||||||
|
ConvertedOpportunityId?: SalesforceId;
|
||||||
|
ConvertedDate?: string;
|
||||||
|
IsConverted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Opportunity extends SObject {
|
||||||
|
Name: string;
|
||||||
|
AccountId?: SalesforceId;
|
||||||
|
StageName: string;
|
||||||
|
CloseDate: string;
|
||||||
|
Amount?: number;
|
||||||
|
Probability?: number;
|
||||||
|
Type?: string;
|
||||||
|
LeadSource?: string;
|
||||||
|
Description?: string;
|
||||||
|
OwnerId?: SalesforceId;
|
||||||
|
IsClosed?: boolean;
|
||||||
|
IsWon?: boolean;
|
||||||
|
ForecastCategoryName?: string;
|
||||||
|
CampaignId?: SalesforceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Case extends SObject {
|
||||||
|
AccountId?: SalesforceId;
|
||||||
|
ContactId?: SalesforceId;
|
||||||
|
Status: string;
|
||||||
|
Priority?: string;
|
||||||
|
Origin?: string;
|
||||||
|
Subject?: string;
|
||||||
|
Description?: string;
|
||||||
|
Type?: string;
|
||||||
|
Reason?: string;
|
||||||
|
OwnerId?: SalesforceId;
|
||||||
|
IsClosed?: boolean;
|
||||||
|
ClosedDate?: string;
|
||||||
|
ParentId?: SalesforceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Task extends SObject {
|
||||||
|
Subject: string;
|
||||||
|
Status: string;
|
||||||
|
Priority?: string;
|
||||||
|
ActivityDate?: string;
|
||||||
|
Description?: string;
|
||||||
|
OwnerId?: SalesforceId;
|
||||||
|
WhoId?: SalesforceId; // Lead or Contact
|
||||||
|
WhatId?: SalesforceId; // Account, Opportunity, etc.
|
||||||
|
IsClosed?: boolean;
|
||||||
|
IsHighPriority?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Event extends SObject {
|
||||||
|
Subject: string;
|
||||||
|
StartDateTime: string;
|
||||||
|
EndDateTime: string;
|
||||||
|
Location?: string;
|
||||||
|
Description?: string;
|
||||||
|
OwnerId?: SalesforceId;
|
||||||
|
WhoId?: SalesforceId; // Lead or Contact
|
||||||
|
WhatId?: SalesforceId; // Account, Opportunity, etc.
|
||||||
|
IsAllDayEvent?: boolean;
|
||||||
|
IsPrivate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Campaign extends SObject {
|
||||||
|
Name: string;
|
||||||
|
Type?: string;
|
||||||
|
Status?: string;
|
||||||
|
StartDate?: string;
|
||||||
|
EndDate?: string;
|
||||||
|
Description?: string;
|
||||||
|
IsActive?: boolean;
|
||||||
|
BudgetedCost?: number;
|
||||||
|
ActualCost?: number;
|
||||||
|
ExpectedRevenue?: number;
|
||||||
|
NumberOfLeads?: number;
|
||||||
|
NumberOfConvertedLeads?: number;
|
||||||
|
NumberOfContacts?: number;
|
||||||
|
NumberOfOpportunities?: number;
|
||||||
|
OwnerId?: SalesforceId;
|
||||||
|
ParentId?: SalesforceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CampaignMember extends SObject {
|
||||||
|
CampaignId: SalesforceId;
|
||||||
|
LeadId?: SalesforceId;
|
||||||
|
ContactId?: SalesforceId;
|
||||||
|
Status?: string;
|
||||||
|
HasResponded?: boolean;
|
||||||
|
FirstRespondedDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User extends SObject {
|
||||||
|
Username: string;
|
||||||
|
Email: string;
|
||||||
|
FirstName?: string;
|
||||||
|
LastName: string;
|
||||||
|
Name?: string;
|
||||||
|
IsActive?: boolean;
|
||||||
|
UserRoleId?: SalesforceId;
|
||||||
|
ProfileId?: SalesforceId;
|
||||||
|
Title?: string;
|
||||||
|
Department?: string;
|
||||||
|
Phone?: string;
|
||||||
|
MobilePhone?: string;
|
||||||
|
TimeZoneSidKey?: string;
|
||||||
|
LocaleSidKey?: string;
|
||||||
|
EmailEncodingKey?: string;
|
||||||
|
LanguageLocaleKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserRole extends SObject {
|
||||||
|
Name: string;
|
||||||
|
ParentRoleId?: SalesforceId;
|
||||||
|
DeveloperName?: string;
|
||||||
|
PortalType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Profile extends SObject {
|
||||||
|
Name: string;
|
||||||
|
Description?: string;
|
||||||
|
UserType?: string;
|
||||||
|
UserLicenseId?: SalesforceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionSet extends SObject {
|
||||||
|
Name: string;
|
||||||
|
Label: string;
|
||||||
|
Description?: string;
|
||||||
|
IsOwnedByProfile?: boolean;
|
||||||
|
ProfileId?: SalesforceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Report extends SObject {
|
||||||
|
Name: string;
|
||||||
|
DeveloperName: string;
|
||||||
|
FolderName?: string;
|
||||||
|
Description?: string;
|
||||||
|
Format?: string;
|
||||||
|
LastRunDate?: string;
|
||||||
|
OwnerId?: SalesforceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Dashboard extends SObject {
|
||||||
|
Title: string;
|
||||||
|
DeveloperName: string;
|
||||||
|
FolderName?: string;
|
||||||
|
Description?: string;
|
||||||
|
LeftSize?: string;
|
||||||
|
MiddleSize?: string;
|
||||||
|
RightSize?: string;
|
||||||
|
RunningUserId?: SalesforceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportMetadata {
|
||||||
|
name: string;
|
||||||
|
id?: string;
|
||||||
|
developerName?: string;
|
||||||
|
reportType?: string;
|
||||||
|
reportFormat?: 'TABULAR' | 'SUMMARY' | 'MATRIX';
|
||||||
|
aggregates?: Array<{
|
||||||
|
acrossGroupingContext?: string;
|
||||||
|
calculatedFormula?: string;
|
||||||
|
datatype?: string;
|
||||||
|
description?: string;
|
||||||
|
developerName?: string;
|
||||||
|
downGroupingContext?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
isCrossBlock?: boolean;
|
||||||
|
masterLabel?: string;
|
||||||
|
reportType?: string;
|
||||||
|
scale?: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentDocument extends SObject {
|
||||||
|
Title: string;
|
||||||
|
FileType?: string;
|
||||||
|
FileExtension?: string;
|
||||||
|
ContentSize?: number;
|
||||||
|
OwnerId?: SalesforceId;
|
||||||
|
ParentId?: SalesforceId;
|
||||||
|
PublishStatus?: string;
|
||||||
|
LatestPublishedVersionId?: SalesforceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentVersion extends SObject {
|
||||||
|
Title: string;
|
||||||
|
PathOnClient?: string;
|
||||||
|
VersionData?: string; // Base64 encoded
|
||||||
|
ContentDocumentId?: SalesforceId;
|
||||||
|
ReasonForChange?: string;
|
||||||
|
ContentSize?: number;
|
||||||
|
FileType?: string;
|
||||||
|
FileExtension?: string;
|
||||||
|
OwnerId?: SalesforceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Attachment extends SObject {
|
||||||
|
Name: string;
|
||||||
|
Body: string; // Base64 encoded
|
||||||
|
ContentType?: string;
|
||||||
|
ParentId?: SalesforceId;
|
||||||
|
OwnerId?: SalesforceId;
|
||||||
|
IsPrivate?: boolean;
|
||||||
|
Description?: string;
|
||||||
|
BodyLength?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SOQL Query Result
|
||||||
|
export interface QueryResult<T = SObject> {
|
||||||
|
totalSize: number;
|
||||||
|
done: boolean;
|
||||||
|
records: T[];
|
||||||
|
nextRecordsUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Describe results
|
||||||
|
export interface FieldDescribe {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
length?: number;
|
||||||
|
byteLength?: number;
|
||||||
|
precision?: number;
|
||||||
|
scale?: number;
|
||||||
|
digits?: number;
|
||||||
|
picklistValues?: Array<{
|
||||||
|
active: boolean;
|
||||||
|
defaultValue: boolean;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
referenceTo?: string[];
|
||||||
|
relationshipName?: string;
|
||||||
|
calculatedFormula?: string;
|
||||||
|
defaultValue?: unknown;
|
||||||
|
defaultValueFormula?: string;
|
||||||
|
defaultedOnCreate?: boolean;
|
||||||
|
dependentPicklist?: boolean;
|
||||||
|
externalId?: boolean;
|
||||||
|
htmlFormatted?: boolean;
|
||||||
|
idLookup?: boolean;
|
||||||
|
inlineHelpText?: string;
|
||||||
|
autoNumber?: boolean;
|
||||||
|
calculated?: boolean;
|
||||||
|
cascadeDelete?: boolean;
|
||||||
|
caseSensitive?: boolean;
|
||||||
|
createable?: boolean;
|
||||||
|
custom?: boolean;
|
||||||
|
updateable?: boolean;
|
||||||
|
nillable?: boolean;
|
||||||
|
nameField?: boolean;
|
||||||
|
unique?: boolean;
|
||||||
|
sortable?: boolean;
|
||||||
|
filterable?: boolean;
|
||||||
|
groupable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SObjectDescribe {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
labelPlural: string;
|
||||||
|
keyPrefix?: string;
|
||||||
|
custom: boolean;
|
||||||
|
fields: FieldDescribe[];
|
||||||
|
childRelationships?: Array<{
|
||||||
|
field: string;
|
||||||
|
childSObject: string;
|
||||||
|
relationshipName?: string;
|
||||||
|
cascadeDelete: boolean;
|
||||||
|
}>;
|
||||||
|
recordTypeInfos?: Array<{
|
||||||
|
name: string;
|
||||||
|
recordTypeId: string;
|
||||||
|
developerName: string;
|
||||||
|
active: boolean;
|
||||||
|
defaultRecordTypeMapping: boolean;
|
||||||
|
master: boolean;
|
||||||
|
}>;
|
||||||
|
createable?: boolean;
|
||||||
|
updateable?: boolean;
|
||||||
|
deletable?: boolean;
|
||||||
|
queryable?: boolean;
|
||||||
|
searchable?: boolean;
|
||||||
|
undeletable?: boolean;
|
||||||
|
mergeable?: boolean;
|
||||||
|
triggerable?: boolean;
|
||||||
|
feedEnabled?: boolean;
|
||||||
|
activateable?: boolean;
|
||||||
|
urls?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk API 2.0 types
|
||||||
|
export interface BulkJob {
|
||||||
|
id?: string;
|
||||||
|
object: string;
|
||||||
|
operation: 'insert' | 'update' | 'upsert' | 'delete' | 'hardDelete';
|
||||||
|
externalIdFieldName?: string;
|
||||||
|
contentType?: 'CSV';
|
||||||
|
lineEnding?: 'LF' | 'CRLF';
|
||||||
|
state?: 'Open' | 'UploadComplete' | 'InProgress' | 'Aborted' | 'JobComplete' | 'Failed';
|
||||||
|
createdDate?: string;
|
||||||
|
systemModstamp?: string;
|
||||||
|
createdById?: string;
|
||||||
|
numberRecordsProcessed?: number;
|
||||||
|
numberRecordsFailed?: number;
|
||||||
|
retries?: number;
|
||||||
|
totalProcessingTime?: number;
|
||||||
|
apiVersion?: string;
|
||||||
|
jobType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BulkJobInfo extends BulkJob {
|
||||||
|
state: 'Open' | 'UploadComplete' | 'InProgress' | 'Aborted' | 'JobComplete' | 'Failed';
|
||||||
|
numberRecordsProcessed: number;
|
||||||
|
numberRecordsFailed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BulkResult {
|
||||||
|
id?: string;
|
||||||
|
success: boolean;
|
||||||
|
created: boolean;
|
||||||
|
errors?: Array<{
|
||||||
|
statusCode: string;
|
||||||
|
message: string;
|
||||||
|
fields: string[];
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composite request/response types
|
||||||
|
export interface CompositeSubrequest {
|
||||||
|
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
||||||
|
url: string;
|
||||||
|
referenceId: string;
|
||||||
|
body?: Record<string, unknown>;
|
||||||
|
httpHeaders?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompositeRequest {
|
||||||
|
allOrNone?: boolean;
|
||||||
|
collateSubrequests?: boolean;
|
||||||
|
compositeRequest: CompositeSubrequest[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompositeSubresponse {
|
||||||
|
body: unknown;
|
||||||
|
httpHeaders: Record<string, string>;
|
||||||
|
httpStatusCode: number;
|
||||||
|
referenceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompositeResponse {
|
||||||
|
compositeResponse: CompositeSubresponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error response
|
||||||
|
export interface SalesforceError {
|
||||||
|
message: string;
|
||||||
|
errorCode: string;
|
||||||
|
fields?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SalesforceErrorResponse {
|
||||||
|
errors?: SalesforceError[];
|
||||||
|
message?: string;
|
||||||
|
errorCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API limits
|
||||||
|
export interface ApiLimits {
|
||||||
|
used: number;
|
||||||
|
remaining: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic custom object support
|
||||||
|
export type CustomSObject<T extends Record<string, unknown> = Record<string, unknown>> = SObject & T;
|
||||||
20
servers/salesforce/tsconfig.json
Normal file
20
servers/salesforce/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
13
servers/shopify/.env.example
Normal file
13
servers/shopify/.env.example
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Shopify MCP Server Configuration
|
||||||
|
|
||||||
|
# Your Shopify store name (without .myshopify.com)
|
||||||
|
SHOPIFY_STORE=your-store-name
|
||||||
|
|
||||||
|
# Shopify Admin API access token (create in Settings > Apps and sales channels > Develop apps)
|
||||||
|
SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
|
# Optional: API version (defaults to 2024-01)
|
||||||
|
# SHOPIFY_API_VERSION=2024-01
|
||||||
|
|
||||||
|
# Optional: Enable debug logging
|
||||||
|
# DEBUG=true
|
||||||
4
servers/shopify/.gitignore
vendored
Normal file
4
servers/shopify/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
277
servers/shopify/README.md
Normal file
277
servers/shopify/README.md
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
# Shopify MCP Server
|
||||||
|
|
||||||
|
Model Context Protocol (MCP) server for Shopify Admin API integration.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Full Shopify Admin API Coverage**: Products, Orders, Customers, Inventory, Collections, and more
|
||||||
|
- **GraphQL Support**: Execute GraphQL Admin API queries
|
||||||
|
- **Automatic Rate Limiting**: Intelligent retry with exponential backoff
|
||||||
|
- **Type-Safe**: Comprehensive TypeScript types for all Shopify entities
|
||||||
|
- **Cursor-Based Pagination**: Automatic handling of paginated responses
|
||||||
|
- **MCP Resources**: UI-friendly resource endpoints for apps
|
||||||
|
- **Dual Transport**: Stdio (default) or HTTP/SSE modes
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Required | Description | Example |
|
||||||
|
|----------|----------|-------------|---------|
|
||||||
|
| `SHOPIFY_ACCESS_TOKEN` | ✅ | Admin API access token | `shpat_xxxxxxxxxxxxx` |
|
||||||
|
| `SHOPIFY_STORE` | ✅ | Store name (without .myshopify.com) | `my-store` |
|
||||||
|
| `SHOPIFY_API_VERSION` | ❌ | API version (default: 2024-01) | `2024-01` |
|
||||||
|
| `DEBUG` | ❌ | Enable debug logging | `true` |
|
||||||
|
|
||||||
|
### Getting Your Access Token
|
||||||
|
|
||||||
|
1. Go to your Shopify admin: `https://your-store.myshopify.com/admin`
|
||||||
|
2. Navigate to **Settings** → **Apps and sales channels** → **Develop apps**
|
||||||
|
3. Click **Create an app** or select an existing one
|
||||||
|
4. Go to **API credentials** tab
|
||||||
|
5. Click **Install app** (if not already installed)
|
||||||
|
6. Copy the **Admin API access token** (starts with `shpat_`)
|
||||||
|
|
||||||
|
### Required API Scopes
|
||||||
|
|
||||||
|
Grant the following scopes to your app (based on features needed):
|
||||||
|
|
||||||
|
- `read_products`, `write_products` - Product management
|
||||||
|
- `read_orders`, `write_orders` - Order management
|
||||||
|
- `read_customers`, `write_customers` - Customer management
|
||||||
|
- `read_inventory`, `write_inventory` - Inventory tracking
|
||||||
|
- `read_content`, `write_content` - Pages, blogs, themes
|
||||||
|
- `read_discounts`, `write_discounts` - Price rules and discount codes
|
||||||
|
- `read_shipping`, `write_shipping` - Shipping zones and carriers
|
||||||
|
- `read_analytics` - Analytics and reports
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Stdio Mode (Default)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy example env file
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit .env with your credentials
|
||||||
|
# SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxxx
|
||||||
|
# SHOPIFY_STORE=your-store-name
|
||||||
|
|
||||||
|
# Start the server
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Or in development mode with auto-reload
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start with HTTP transport
|
||||||
|
npm start -- --http --port=3000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
The server provides the following tool categories (lazy-loaded on demand):
|
||||||
|
|
||||||
|
### Products
|
||||||
|
- Create, read, update, delete products
|
||||||
|
- Manage variants and images
|
||||||
|
- Bulk operations
|
||||||
|
|
||||||
|
### Orders
|
||||||
|
- List and search orders
|
||||||
|
- Update order status
|
||||||
|
- Create refunds
|
||||||
|
- Manage fulfillments
|
||||||
|
|
||||||
|
### Customers
|
||||||
|
- Customer CRUD operations
|
||||||
|
- Customer search
|
||||||
|
- Manage addresses
|
||||||
|
|
||||||
|
### Inventory
|
||||||
|
- Track inventory levels
|
||||||
|
- Manage locations
|
||||||
|
- Adjust stock quantities
|
||||||
|
|
||||||
|
### Collections
|
||||||
|
- Smart and custom collections
|
||||||
|
- Add/remove products from collections
|
||||||
|
|
||||||
|
### Discounts
|
||||||
|
- Create price rules
|
||||||
|
- Generate discount codes
|
||||||
|
- Manage promotions
|
||||||
|
|
||||||
|
### Fulfillments
|
||||||
|
- Create fulfillments
|
||||||
|
- Update tracking information
|
||||||
|
- Cancel fulfillments
|
||||||
|
|
||||||
|
### Shipping
|
||||||
|
- Shipping zones
|
||||||
|
- Carrier services
|
||||||
|
- Delivery profiles
|
||||||
|
|
||||||
|
### Themes
|
||||||
|
- Theme management
|
||||||
|
- Asset upload/download
|
||||||
|
|
||||||
|
### Content
|
||||||
|
- Pages
|
||||||
|
- Blogs and articles
|
||||||
|
- Redirects
|
||||||
|
|
||||||
|
### Webhooks
|
||||||
|
- Register webhooks
|
||||||
|
- Manage subscriptions
|
||||||
|
|
||||||
|
### Analytics
|
||||||
|
- Sales reports
|
||||||
|
- Product analytics
|
||||||
|
- Customer insights
|
||||||
|
|
||||||
|
## MCP Resources
|
||||||
|
|
||||||
|
Resources are available for UI applications:
|
||||||
|
|
||||||
|
- `shopify://products` - List all products
|
||||||
|
- `shopify://orders` - List all orders
|
||||||
|
- `shopify://customers` - List all customers
|
||||||
|
- `shopify://inventory` - View inventory levels
|
||||||
|
|
||||||
|
## API Client Features
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
The client automatically handles Shopify's rate limits:
|
||||||
|
|
||||||
|
- Parses `X-Shopify-Shop-Api-Call-Limit` header
|
||||||
|
- Warns when approaching limit (80%+ used)
|
||||||
|
- Automatic retry with exponential backoff (1s, 2s, 4s)
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get single page
|
||||||
|
const { data, pageInfo } = await client.list('/products.json');
|
||||||
|
|
||||||
|
// Get all pages (with safety limit)
|
||||||
|
const allProducts = await client.listAll('/products.json', {}, 10);
|
||||||
|
```
|
||||||
|
|
||||||
|
### GraphQL
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await client.graphql(`
|
||||||
|
query {
|
||||||
|
products(first: 10) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
All errors are normalized with structured responses:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "RATE_LIMIT_EXCEEDED",
|
||||||
|
"message": "Shopify API rate limit exceeded. Please retry after a delay."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Error codes:
|
||||||
|
- `TOOL_NOT_FOUND` - Requested tool doesn't exist
|
||||||
|
- `RESOURCE_READ_ERROR` - Failed to read resource
|
||||||
|
- `AUTHENTICATION_ERROR` - Invalid access token
|
||||||
|
- `RATE_LIMIT_EXCEEDED` - Too many requests
|
||||||
|
- `UNKNOWN_ERROR` - Unexpected error
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Run TypeScript compiler in watch mode
|
||||||
|
npx tsc --watch
|
||||||
|
|
||||||
|
# Run development server with auto-reload
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Type check only (no build)
|
||||||
|
npx tsc --noEmit
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.ts # Entry point, env validation, transports
|
||||||
|
├── server.ts # MCP server setup, handlers
|
||||||
|
├── clients/
|
||||||
|
│ └── shopify.ts # Shopify API client (REST + GraphQL)
|
||||||
|
├── types/
|
||||||
|
│ └── index.ts # TypeScript interfaces for Shopify entities
|
||||||
|
├── tools/ # Tool modules (created separately)
|
||||||
|
│ ├── products.ts
|
||||||
|
│ ├── orders.ts
|
||||||
|
│ ├── customers.ts
|
||||||
|
│ └── ...
|
||||||
|
└── apps/ # UI applications (created separately)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Authentication failed"
|
||||||
|
- Verify `SHOPIFY_ACCESS_TOKEN` is correct
|
||||||
|
- Ensure the app is installed on your store
|
||||||
|
- Check that the token hasn't been revoked
|
||||||
|
|
||||||
|
### "Resource not found"
|
||||||
|
- Check that `SHOPIFY_STORE` is correct (without `.myshopify.com`)
|
||||||
|
- Verify the resource ID exists in your store
|
||||||
|
|
||||||
|
### "Rate limit exceeded"
|
||||||
|
- The client will automatically retry with backoff
|
||||||
|
- Consider implementing request batching for bulk operations
|
||||||
|
- Monitor rate limit info with `client.getRateLimitInfo()`
|
||||||
|
|
||||||
|
### TypeScript errors
|
||||||
|
- Run `npm install` to ensure all dependencies are installed
|
||||||
|
- Check that `@types/node` version matches your Node.js version
|
||||||
|
- Verify `tsconfig.json` settings
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and questions:
|
||||||
|
- GitHub Issues: [mcpengine repository](https://github.com/BusyBee3333/mcpengine)
|
||||||
|
- Shopify API Docs: https://shopify.dev/docs/api/admin-rest
|
||||||
21
servers/shopify/package.json
Normal file
21
servers/shopify/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@mcpengine/shopify",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"dev": "tsx watch src/main.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
|
"axios": "^1.7.0",
|
||||||
|
"zod": "^3.23.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"tsx": "^4.19.0",
|
||||||
|
"@types/node": "^22.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
315
servers/shopify/src/clients/shopify.ts
Normal file
315
servers/shopify/src/clients/shopify.ts
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
|
||||||
|
import { GraphQLRequest, GraphQLResponse } from '../types/index.js';
|
||||||
|
|
||||||
|
interface ShopifyClientConfig {
|
||||||
|
accessToken: string;
|
||||||
|
store: string;
|
||||||
|
apiVersion?: string;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RateLimitInfo {
|
||||||
|
current: number;
|
||||||
|
max: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaginatedResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
pageInfo?: {
|
||||||
|
nextPageUrl?: string;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShopifyClient {
|
||||||
|
private client: AxiosInstance;
|
||||||
|
private store: string;
|
||||||
|
private apiVersion: string;
|
||||||
|
private rateLimitInfo: RateLimitInfo = { current: 0, max: 40 };
|
||||||
|
|
||||||
|
constructor(config: ShopifyClientConfig) {
|
||||||
|
this.store = config.store;
|
||||||
|
this.apiVersion = config.apiVersion || '2024-01';
|
||||||
|
|
||||||
|
const baseURL = `https://${this.store}.myshopify.com/admin/api/${this.apiVersion}`;
|
||||||
|
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL,
|
||||||
|
timeout: config.timeout || 30000,
|
||||||
|
headers: {
|
||||||
|
'X-Shopify-Access-Token': config.accessToken,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupInterceptors();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupInterceptors(): void {
|
||||||
|
// Request interceptor for logging
|
||||||
|
this.client.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
console.log(`[${timestamp}] ${config.method?.toUpperCase()} ${config.url}`);
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('[Request Error]', error.message);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Response interceptor for rate limit tracking and error normalization
|
||||||
|
this.client.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
// Parse rate limit header: "12/40" means 12 calls made out of 40 max
|
||||||
|
const rateLimitHeader = response.headers['x-shopify-shop-api-call-limit'];
|
||||||
|
if (rateLimitHeader) {
|
||||||
|
const [current, max] = rateLimitHeader.split('/').map(Number);
|
||||||
|
this.rateLimitInfo = { current: current || 0, max: max || 40 };
|
||||||
|
|
||||||
|
// Log warning if approaching rate limit
|
||||||
|
if (this.rateLimitInfo.current >= this.rateLimitInfo.max * 0.8) {
|
||||||
|
console.warn(
|
||||||
|
`[Rate Limit Warning] ${this.rateLimitInfo.current}/${this.rateLimitInfo.max} calls used`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(this.normalizeError(error));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeError(error: unknown): Error {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const axiosError = error as AxiosError;
|
||||||
|
const status = axiosError.response?.status;
|
||||||
|
const data = axiosError.response?.data as { errors?: unknown };
|
||||||
|
|
||||||
|
let message = axiosError.message;
|
||||||
|
|
||||||
|
if (status === 429) {
|
||||||
|
message = 'Shopify API rate limit exceeded. Please retry after a delay.';
|
||||||
|
} else if (status === 401) {
|
||||||
|
message = 'Authentication failed. Check your access token.';
|
||||||
|
} else if (status === 404) {
|
||||||
|
message = 'Resource not found.';
|
||||||
|
} else if (data?.errors) {
|
||||||
|
message = typeof data.errors === 'string'
|
||||||
|
? data.errors
|
||||||
|
: JSON.stringify(data.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedError = new Error(message);
|
||||||
|
(normalizedError as Error & { status?: number; code?: string }).status = status;
|
||||||
|
(normalizedError as Error & { status?: number; code?: string }).code = axiosError.code;
|
||||||
|
return normalizedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error instanceof Error ? error : new Error(String(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async retryWithBackoff<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
retries = 3,
|
||||||
|
baseDelay = 1000
|
||||||
|
): Promise<T> {
|
||||||
|
let lastError: Error | undefined;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
|
||||||
|
// Don't retry on client errors (4xx except 429)
|
||||||
|
const status = (lastError as Error & { status?: number }).status;
|
||||||
|
if (status && status >= 400 && status < 500 && status !== 429) {
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < retries) {
|
||||||
|
const delay = baseDelay * Math.pow(2, attempt); // Exponential: 1s, 2s, 4s
|
||||||
|
console.log(`[Retry ${attempt + 1}/${retries}] Waiting ${delay}ms...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse Link header for cursor-based pagination
|
||||||
|
*/
|
||||||
|
private parseLinkHeader(linkHeader?: string): { nextPageUrl?: string } {
|
||||||
|
if (!linkHeader) return {};
|
||||||
|
|
||||||
|
const links = linkHeader.split(',').map(link => {
|
||||||
|
const parts = link.split(';').map(s => s.trim());
|
||||||
|
const url = parts[0];
|
||||||
|
const rel = parts[1];
|
||||||
|
|
||||||
|
if (!url || !rel) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: url.slice(1, -1), // Remove angle brackets
|
||||||
|
rel: rel.split('=')[1]?.replace(/"/g, ''),
|
||||||
|
};
|
||||||
|
}).filter((link): link is { url: string; rel: string | undefined } => link !== null);
|
||||||
|
|
||||||
|
const nextLink = links.find(link => link.rel === 'next');
|
||||||
|
return { nextPageUrl: nextLink?.url };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET request with automatic retry
|
||||||
|
*/
|
||||||
|
async get<T>(path: string, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return this.retryWithBackoff(async () => {
|
||||||
|
const response = await this.client.get<T>(path, config);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET request with pagination support
|
||||||
|
*/
|
||||||
|
async list<T>(
|
||||||
|
path: string,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<PaginatedResponse<T>> {
|
||||||
|
return this.retryWithBackoff(async () => {
|
||||||
|
const response = await this.client.get<T[]>(path, config);
|
||||||
|
const linkHeader = response.headers['link'];
|
||||||
|
const { nextPageUrl } = this.parseLinkHeader(linkHeader);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: response.data,
|
||||||
|
pageInfo: {
|
||||||
|
nextPageUrl,
|
||||||
|
hasNextPage: !!nextPageUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET all pages automatically (use with caution on large datasets)
|
||||||
|
*/
|
||||||
|
async listAll<T>(
|
||||||
|
path: string,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
maxPages = 10
|
||||||
|
): Promise<T[]> {
|
||||||
|
const allData: T[] = [];
|
||||||
|
let currentPath = path;
|
||||||
|
let pageCount = 0;
|
||||||
|
|
||||||
|
while (currentPath && pageCount < maxPages) {
|
||||||
|
const response = await this.retryWithBackoff(async () => {
|
||||||
|
const url = currentPath.startsWith('http')
|
||||||
|
? currentPath
|
||||||
|
: currentPath;
|
||||||
|
const res = await this.client.get<T[]>(url, config);
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
allData.push(...response.data);
|
||||||
|
|
||||||
|
const linkHeader = response.headers['link'];
|
||||||
|
const { nextPageUrl } = this.parseLinkHeader(linkHeader);
|
||||||
|
|
||||||
|
if (!nextPageUrl) break;
|
||||||
|
|
||||||
|
// Extract path from full URL
|
||||||
|
currentPath = nextPageUrl.replace(this.client.defaults.baseURL || '', '');
|
||||||
|
pageCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST request with automatic retry
|
||||||
|
*/
|
||||||
|
async create<T, D = unknown>(
|
||||||
|
path: string,
|
||||||
|
data: D,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> {
|
||||||
|
return this.retryWithBackoff(async () => {
|
||||||
|
const response = await this.client.post<T>(path, data, config);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT request with automatic retry
|
||||||
|
*/
|
||||||
|
async update<T, D = unknown>(
|
||||||
|
path: string,
|
||||||
|
data: D,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<T> {
|
||||||
|
return this.retryWithBackoff(async () => {
|
||||||
|
const response = await this.client.put<T>(path, data, config);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE request with automatic retry
|
||||||
|
*/
|
||||||
|
async delete<T = void>(path: string, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return this.retryWithBackoff(async () => {
|
||||||
|
const response = await this.client.delete<T>(path, config);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GraphQL Admin API request
|
||||||
|
*/
|
||||||
|
async graphql<T = unknown>(
|
||||||
|
query: string,
|
||||||
|
variables?: Record<string, unknown>
|
||||||
|
): Promise<GraphQLResponse<T>> {
|
||||||
|
const graphqlUrl = `https://${this.store}.myshopify.com/admin/api/${this.apiVersion}/graphql.json`;
|
||||||
|
|
||||||
|
return this.retryWithBackoff(async () => {
|
||||||
|
const response = await this.client.post<GraphQLResponse<T>>(
|
||||||
|
graphqlUrl,
|
||||||
|
{ query, variables } as GraphQLRequest,
|
||||||
|
{
|
||||||
|
baseURL: '', // Override baseURL for GraphQL endpoint
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.errors && response.data.errors.length > 0) {
|
||||||
|
const errorMessages = response.data.errors.map(e => e.message).join(', ');
|
||||||
|
throw new Error(`GraphQL Error: ${errorMessages}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current rate limit info
|
||||||
|
*/
|
||||||
|
getRateLimitInfo(): RateLimitInfo {
|
||||||
|
return { ...this.rateLimitInfo };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if approaching rate limit (>80% used)
|
||||||
|
*/
|
||||||
|
isApproachingRateLimit(): boolean {
|
||||||
|
return this.rateLimitInfo.current >= this.rateLimitInfo.max * 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
170
servers/shopify/src/main.ts
Normal file
170
servers/shopify/src/main.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { ShopifyClient } from './clients/shopify.js';
|
||||||
|
import { ShopifyMCPServer } from './server.js';
|
||||||
|
|
||||||
|
interface EnvironmentConfig {
|
||||||
|
accessToken: string;
|
||||||
|
store: string;
|
||||||
|
apiVersion?: string;
|
||||||
|
httpMode: boolean;
|
||||||
|
port?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and load environment configuration
|
||||||
|
*/
|
||||||
|
function loadConfig(): EnvironmentConfig {
|
||||||
|
const accessToken = process.env.SHOPIFY_ACCESS_TOKEN;
|
||||||
|
const store = process.env.SHOPIFY_STORE;
|
||||||
|
const apiVersion = process.env.SHOPIFY_API_VERSION;
|
||||||
|
const httpMode = process.argv.includes('--http');
|
||||||
|
const portArg = process.argv.find(arg => arg.startsWith('--port='));
|
||||||
|
const port = portArg ? parseInt(portArg.split('=')[1] || '3000', 10) : 3000;
|
||||||
|
|
||||||
|
// Validate required environment variables
|
||||||
|
if (!accessToken) {
|
||||||
|
console.error('Error: SHOPIFY_ACCESS_TOKEN environment variable is required');
|
||||||
|
console.error('Get your access token from: Settings > Apps and sales channels > Develop apps');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!store) {
|
||||||
|
console.error('Error: SHOPIFY_STORE environment variable is required');
|
||||||
|
console.error('Example: your-store-name (without .myshopify.com)');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate store format (should not include .myshopify.com)
|
||||||
|
if (store.includes('.myshopify.com')) {
|
||||||
|
console.error('Error: SHOPIFY_STORE should not include .myshopify.com');
|
||||||
|
console.error('Example: Use "your-store-name" instead of "your-store-name.myshopify.com"');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
store,
|
||||||
|
apiVersion,
|
||||||
|
httpMode,
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup graceful shutdown handlers
|
||||||
|
*/
|
||||||
|
function setupGracefulShutdown(cleanup: () => Promise<void>): void {
|
||||||
|
const handleShutdown = async (signal: string) => {
|
||||||
|
console.log(`\n[${signal}] Shutting down gracefully...`);
|
||||||
|
try {
|
||||||
|
await cleanup();
|
||||||
|
console.log('Shutdown complete');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during shutdown:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
||||||
|
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start HTTP/SSE server (for --http mode)
|
||||||
|
*/
|
||||||
|
async function startHttpServer(
|
||||||
|
server: ShopifyMCPServer,
|
||||||
|
port: number
|
||||||
|
): Promise<() => Promise<void>> {
|
||||||
|
// Note: HTTP/SSE transport requires additional setup
|
||||||
|
// For now, this is a placeholder for future implementation
|
||||||
|
console.log(`HTTP mode requested on port ${port}`);
|
||||||
|
console.log('Note: HTTP/SSE transport not yet implemented');
|
||||||
|
console.log('Falling back to stdio mode...');
|
||||||
|
|
||||||
|
await server.start();
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
console.log('HTTP server cleanup');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start stdio server (default mode)
|
||||||
|
*/
|
||||||
|
async function startStdioServer(
|
||||||
|
server: ShopifyMCPServer
|
||||||
|
): Promise<() => Promise<void>> {
|
||||||
|
await server.start();
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
console.log('Stdio server cleanup');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check endpoint (when in HTTP mode)
|
||||||
|
*/
|
||||||
|
function createHealthCheckHandler() {
|
||||||
|
return async (req: unknown, res: unknown) => {
|
||||||
|
// Placeholder for HTTP health check
|
||||||
|
console.log('Health check called');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
console.log('Shopify MCP Server v1.0.0');
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
console.log(`Store: ${config.store}`);
|
||||||
|
console.log(`API Version: ${config.apiVersion || '2024-01'}`);
|
||||||
|
console.log(`Mode: ${config.httpMode ? `HTTP (port ${config.port})` : 'stdio'}`);
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
|
||||||
|
// Initialize Shopify client
|
||||||
|
const shopifyClient = new ShopifyClient({
|
||||||
|
accessToken: config.accessToken,
|
||||||
|
store: config.store,
|
||||||
|
apiVersion: config.apiVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize MCP server
|
||||||
|
const mcpServer = new ShopifyMCPServer({
|
||||||
|
name: '@mcpengine/shopify',
|
||||||
|
version: '1.0.0',
|
||||||
|
shopifyClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server in appropriate mode
|
||||||
|
let cleanup: () => Promise<void>;
|
||||||
|
|
||||||
|
if (config.httpMode) {
|
||||||
|
cleanup = await startHttpServer(mcpServer, config.port || 3000);
|
||||||
|
} else {
|
||||||
|
cleanup = await startStdioServer(mcpServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup graceful shutdown
|
||||||
|
setupGracefulShutdown(cleanup);
|
||||||
|
|
||||||
|
// Keep process alive
|
||||||
|
if (config.httpMode) {
|
||||||
|
// In HTTP mode, the server keeps the process alive
|
||||||
|
console.log(`Server listening on http://localhost:${config.port}`);
|
||||||
|
console.log(`Health check available at: http://localhost:${config.port}/health`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
337
servers/shopify/src/server.ts
Normal file
337
servers/shopify/src/server.ts
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { ShopifyClient } from './clients/shopify.js';
|
||||||
|
|
||||||
|
interface ToolModule {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object';
|
||||||
|
properties: Record<string, unknown>;
|
||||||
|
required?: string[];
|
||||||
|
};
|
||||||
|
handler: (args: Record<string, unknown>, client: ShopifyClient) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServerConfig {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
shopifyClient: ShopifyClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShopifyMCPServer {
|
||||||
|
private server: Server;
|
||||||
|
private client: ShopifyClient;
|
||||||
|
private toolModules: Map<string, () => Promise<ToolModule[]>> = new Map();
|
||||||
|
|
||||||
|
constructor(config: ServerConfig) {
|
||||||
|
this.client = config.shopifyClient;
|
||||||
|
|
||||||
|
this.server = new Server(
|
||||||
|
{
|
||||||
|
name: config.name,
|
||||||
|
version: config.version,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
resources: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setupToolModules();
|
||||||
|
this.setupHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register lazy-loaded tool modules
|
||||||
|
*/
|
||||||
|
private setupToolModules(): void {
|
||||||
|
// Tool modules will be dynamically imported when needed
|
||||||
|
this.toolModules.set('products', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/products.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolModules.set('orders', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/orders.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolModules.set('customers', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/customers.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolModules.set('inventory', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/inventory.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolModules.set('collections', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/collections.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolModules.set('discounts', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/discounts.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolModules.set('shipping', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/shipping.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolModules.set('fulfillments', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/fulfillments.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolModules.set('themes', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/themes.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolModules.set('pages', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/pages.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolModules.set('blogs', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/blogs.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolModules.set('analytics', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/analytics.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolModules.set('webhooks', async () => {
|
||||||
|
// @ts-ignore - Tool modules not created yet (foundation only)
|
||||||
|
const module = await import('./tools/webhooks.js');
|
||||||
|
return module.default;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all tool modules (called on list_tools)
|
||||||
|
*/
|
||||||
|
private async loadAllTools(): Promise<ToolModule[]> {
|
||||||
|
const allTools: ToolModule[] = [];
|
||||||
|
|
||||||
|
// Only load modules that exist (we haven't created tool files yet)
|
||||||
|
// This prevents errors during foundation setup
|
||||||
|
for (const [moduleName, loader] of this.toolModules.entries()) {
|
||||||
|
try {
|
||||||
|
const tools = await loader();
|
||||||
|
allTools.push(...tools);
|
||||||
|
} catch (error) {
|
||||||
|
// Tool module doesn't exist yet - this is expected during foundation setup
|
||||||
|
console.log(`[Info] Tool module '${moduleName}' not loaded (not created yet)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup MCP protocol handlers
|
||||||
|
*/
|
||||||
|
private setupHandlers(): void {
|
||||||
|
// List available tools
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
console.log(`[${timestamp}] Listing tools`);
|
||||||
|
|
||||||
|
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 timestamp = new Date().toISOString();
|
||||||
|
console.log(`[${timestamp}] Tool call: ${request.params.name}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tools = await this.loadAllTools();
|
||||||
|
const tool = tools.find(t => t.name === request.params.name);
|
||||||
|
|
||||||
|
if (!tool) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({
|
||||||
|
error: 'TOOL_NOT_FOUND',
|
||||||
|
message: `Tool '${request.params.name}' not found`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await tool.handler(
|
||||||
|
request.params.arguments as Record<string, unknown>,
|
||||||
|
this.client
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
const errorCode = (error as Error & { code?: string }).code || 'UNKNOWN_ERROR';
|
||||||
|
|
||||||
|
console.error(`[${timestamp}] Tool error:`, errorMessage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({
|
||||||
|
error: errorCode,
|
||||||
|
message: errorMessage,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// List resources (for UI apps)
|
||||||
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
console.log(`[${timestamp}] Listing resources`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
uri: 'shopify://products',
|
||||||
|
name: 'Products',
|
||||||
|
description: 'List all products in the store',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'shopify://orders',
|
||||||
|
name: 'Orders',
|
||||||
|
description: 'List all orders in the store',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'shopify://customers',
|
||||||
|
name: 'Customers',
|
||||||
|
description: 'List all customers in the store',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'shopify://inventory',
|
||||||
|
name: 'Inventory',
|
||||||
|
description: 'View inventory levels across locations',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read resource (for UI apps)
|
||||||
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
console.log(`[${timestamp}] Reading resource: ${request.params.uri}`);
|
||||||
|
|
||||||
|
const uri = request.params.uri;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data: unknown;
|
||||||
|
|
||||||
|
if (uri === 'shopify://products') {
|
||||||
|
data = await this.client.list('/products.json');
|
||||||
|
} else if (uri === 'shopify://orders') {
|
||||||
|
data = await this.client.list('/orders.json');
|
||||||
|
} else if (uri === 'shopify://customers') {
|
||||||
|
data = await this.client.list('/customers.json');
|
||||||
|
} else if (uri === 'shopify://inventory') {
|
||||||
|
data = await this.client.list('/inventory_levels.json');
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown resource URI: ${uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(data, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`[${timestamp}] Resource read error:`, errorMessage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify({
|
||||||
|
error: 'RESOURCE_READ_ERROR',
|
||||||
|
message: errorMessage,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the server with stdio transport
|
||||||
|
*/
|
||||||
|
async start(): Promise<void> {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await this.server.connect(transport);
|
||||||
|
console.log('Shopify MCP server running');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the underlying MCP server instance (for custom transports)
|
||||||
|
*/
|
||||||
|
getServer(): Server {
|
||||||
|
return this.server;
|
||||||
|
}
|
||||||
|
}
|
||||||
762
servers/shopify/src/types/index.ts
Normal file
762
servers/shopify/src/types/index.ts
Normal file
@ -0,0 +1,762 @@
|
|||||||
|
// Branded ID types for type safety
|
||||||
|
export type ProductId = string & { __brand: 'ProductId' };
|
||||||
|
export type VariantId = string & { __brand: 'VariantId' };
|
||||||
|
export type CollectionId = string & { __brand: 'CollectionId' };
|
||||||
|
export type OrderId = string & { __brand: 'OrderId' };
|
||||||
|
export type CustomerId = string & { __brand: 'CustomerId' };
|
||||||
|
export type FulfillmentId = string & { __brand: 'FulfillmentId' };
|
||||||
|
export type FulfillmentOrderId = string & { __brand: 'FulfillmentOrderId' };
|
||||||
|
export type LocationId = string & { __brand: 'LocationId' };
|
||||||
|
export type InventoryItemId = string & { __brand: 'InventoryItemId' };
|
||||||
|
export type PriceRuleId = string & { __brand: 'PriceRuleId' };
|
||||||
|
export type DiscountCodeId = string & { __brand: 'DiscountCodeId' };
|
||||||
|
export type ThemeId = string & { __brand: 'ThemeId' };
|
||||||
|
export type PageId = string & { __brand: 'PageId' };
|
||||||
|
export type BlogId = string & { __brand: 'BlogId' };
|
||||||
|
export type ArticleId = string & { __brand: 'ArticleId' };
|
||||||
|
export type WebhookId = string & { __brand: 'WebhookId' };
|
||||||
|
export type MetafieldId = string & { __brand: 'MetafieldId' };
|
||||||
|
export type TransactionId = string & { __brand: 'TransactionId' };
|
||||||
|
export type RefundId = string & { __brand: 'RefundId' };
|
||||||
|
|
||||||
|
// Order status discriminated unions
|
||||||
|
export type OrderFinancialStatus =
|
||||||
|
| 'pending'
|
||||||
|
| 'authorized'
|
||||||
|
| 'partially_paid'
|
||||||
|
| 'paid'
|
||||||
|
| 'partially_refunded'
|
||||||
|
| 'refunded'
|
||||||
|
| 'voided';
|
||||||
|
|
||||||
|
export type OrderFulfillmentStatus =
|
||||||
|
| 'fulfilled'
|
||||||
|
| 'null'
|
||||||
|
| 'partial'
|
||||||
|
| 'restocked';
|
||||||
|
|
||||||
|
export type OrderStatus = 'open' | 'archived' | 'cancelled';
|
||||||
|
|
||||||
|
// Product types
|
||||||
|
export interface ProductImage {
|
||||||
|
id?: number;
|
||||||
|
product_id?: ProductId;
|
||||||
|
position?: number;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
alt?: string | null;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
src: string;
|
||||||
|
variant_ids?: VariantId[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductVariant {
|
||||||
|
id: VariantId;
|
||||||
|
product_id: ProductId;
|
||||||
|
title: string;
|
||||||
|
price: string;
|
||||||
|
sku?: string | null;
|
||||||
|
position?: number;
|
||||||
|
inventory_policy?: 'deny' | 'continue';
|
||||||
|
compare_at_price?: string | null;
|
||||||
|
fulfillment_service?: string;
|
||||||
|
inventory_management?: string | null;
|
||||||
|
option1?: string | null;
|
||||||
|
option2?: string | null;
|
||||||
|
option3?: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
taxable?: boolean;
|
||||||
|
barcode?: string | null;
|
||||||
|
grams?: number;
|
||||||
|
image_id?: number | null;
|
||||||
|
weight?: number;
|
||||||
|
weight_unit?: string;
|
||||||
|
inventory_item_id?: InventoryItemId;
|
||||||
|
inventory_quantity?: number;
|
||||||
|
old_inventory_quantity?: number;
|
||||||
|
requires_shipping?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductOption {
|
||||||
|
id?: number;
|
||||||
|
product_id?: ProductId;
|
||||||
|
name: string;
|
||||||
|
position?: number;
|
||||||
|
values: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Product {
|
||||||
|
id: ProductId;
|
||||||
|
title: string;
|
||||||
|
body_html?: string | null;
|
||||||
|
vendor?: string;
|
||||||
|
product_type?: string;
|
||||||
|
created_at?: string;
|
||||||
|
handle?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
published_at?: string | null;
|
||||||
|
template_suffix?: string | null;
|
||||||
|
status?: 'active' | 'archived' | 'draft';
|
||||||
|
published_scope?: string;
|
||||||
|
tags?: string;
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
variants?: ProductVariant[];
|
||||||
|
options?: ProductOption[];
|
||||||
|
images?: ProductImage[];
|
||||||
|
image?: ProductImage | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collection types
|
||||||
|
export interface CollectionRule {
|
||||||
|
column: string;
|
||||||
|
relation: string;
|
||||||
|
condition: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SmartCollection {
|
||||||
|
id: CollectionId;
|
||||||
|
handle?: string;
|
||||||
|
title: string;
|
||||||
|
updated_at?: string;
|
||||||
|
body_html?: string | null;
|
||||||
|
published_at?: string | null;
|
||||||
|
sort_order?: string;
|
||||||
|
template_suffix?: string | null;
|
||||||
|
published_scope?: string;
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
rules?: CollectionRule[];
|
||||||
|
disjunctive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomCollection {
|
||||||
|
id: CollectionId;
|
||||||
|
handle?: string;
|
||||||
|
title: string;
|
||||||
|
updated_at?: string;
|
||||||
|
body_html?: string | null;
|
||||||
|
published_at?: string | null;
|
||||||
|
sort_order?: string;
|
||||||
|
template_suffix?: string | null;
|
||||||
|
published_scope?: string;
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Collection = SmartCollection | CustomCollection;
|
||||||
|
|
||||||
|
// Order types
|
||||||
|
export interface OrderLineItem {
|
||||||
|
id: number;
|
||||||
|
variant_id?: VariantId | null;
|
||||||
|
title: string;
|
||||||
|
quantity: number;
|
||||||
|
price: string;
|
||||||
|
grams?: number;
|
||||||
|
sku?: string | null;
|
||||||
|
variant_title?: string | null;
|
||||||
|
vendor?: string | null;
|
||||||
|
fulfillment_service?: string;
|
||||||
|
product_id?: ProductId | null;
|
||||||
|
requires_shipping?: boolean;
|
||||||
|
taxable?: boolean;
|
||||||
|
gift_card?: boolean;
|
||||||
|
name?: string;
|
||||||
|
properties?: Array<{ name: string; value: string }>;
|
||||||
|
product_exists?: boolean;
|
||||||
|
fulfillable_quantity?: number;
|
||||||
|
total_discount?: string;
|
||||||
|
fulfillment_status?: 'fulfilled' | 'partial' | 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShippingAddress {
|
||||||
|
first_name?: string | null;
|
||||||
|
address1?: string | null;
|
||||||
|
phone?: string | null;
|
||||||
|
city?: string | null;
|
||||||
|
zip?: string | null;
|
||||||
|
province?: string | null;
|
||||||
|
country?: string | null;
|
||||||
|
last_name?: string | null;
|
||||||
|
address2?: string | null;
|
||||||
|
company?: string | null;
|
||||||
|
latitude?: number | null;
|
||||||
|
longitude?: number | null;
|
||||||
|
name?: string;
|
||||||
|
country_code?: string | null;
|
||||||
|
province_code?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Transaction {
|
||||||
|
id: TransactionId;
|
||||||
|
order_id: OrderId;
|
||||||
|
kind: 'authorization' | 'capture' | 'sale' | 'void' | 'refund';
|
||||||
|
gateway: string;
|
||||||
|
status: 'pending' | 'failure' | 'success' | 'error';
|
||||||
|
message?: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
test?: boolean;
|
||||||
|
authorization?: string | null;
|
||||||
|
currency?: string;
|
||||||
|
amount?: string;
|
||||||
|
parent_id?: TransactionId | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Refund {
|
||||||
|
id: RefundId;
|
||||||
|
order_id: OrderId;
|
||||||
|
created_at?: string;
|
||||||
|
note?: string | null;
|
||||||
|
user_id?: number | null;
|
||||||
|
processed_at?: string | null;
|
||||||
|
restock?: boolean;
|
||||||
|
transactions?: Transaction[];
|
||||||
|
refund_line_items?: Array<{
|
||||||
|
id: number;
|
||||||
|
quantity: number;
|
||||||
|
line_item_id: number;
|
||||||
|
location_id?: LocationId | null;
|
||||||
|
restock_type?: string;
|
||||||
|
subtotal?: string;
|
||||||
|
total_tax?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Fulfillment {
|
||||||
|
id: FulfillmentId;
|
||||||
|
order_id: OrderId;
|
||||||
|
status: 'pending' | 'open' | 'success' | 'cancelled' | 'error' | 'failure';
|
||||||
|
created_at?: string;
|
||||||
|
service?: string | null;
|
||||||
|
updated_at?: string;
|
||||||
|
tracking_company?: string | null;
|
||||||
|
shipment_status?: string | null;
|
||||||
|
location_id?: LocationId | null;
|
||||||
|
tracking_number?: string | null;
|
||||||
|
tracking_numbers?: string[];
|
||||||
|
tracking_url?: string | null;
|
||||||
|
tracking_urls?: string[];
|
||||||
|
receipt?: Record<string, unknown>;
|
||||||
|
line_items?: OrderLineItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FulfillmentOrder {
|
||||||
|
id: FulfillmentOrderId;
|
||||||
|
shop_id: number;
|
||||||
|
order_id: OrderId;
|
||||||
|
assigned_location_id?: LocationId | null;
|
||||||
|
request_status: string;
|
||||||
|
status: 'open' | 'in_progress' | 'cancelled' | 'incomplete' | 'closed';
|
||||||
|
supported_actions?: string[];
|
||||||
|
destination?: ShippingAddress;
|
||||||
|
line_items?: Array<{
|
||||||
|
id: number;
|
||||||
|
shop_id: number;
|
||||||
|
fulfillment_order_id: FulfillmentOrderId;
|
||||||
|
quantity: number;
|
||||||
|
line_item_id: number;
|
||||||
|
inventory_item_id: InventoryItemId;
|
||||||
|
fulfillable_quantity: number;
|
||||||
|
variant_id: VariantId;
|
||||||
|
}>;
|
||||||
|
fulfillment_service_handle?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Order {
|
||||||
|
id: OrderId;
|
||||||
|
email?: string | null;
|
||||||
|
closed_at?: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
number?: number;
|
||||||
|
note?: string | null;
|
||||||
|
token?: string;
|
||||||
|
gateway?: string | null;
|
||||||
|
test?: boolean;
|
||||||
|
total_price?: string;
|
||||||
|
subtotal_price?: string;
|
||||||
|
total_weight?: number;
|
||||||
|
total_tax?: string;
|
||||||
|
taxes_included?: boolean;
|
||||||
|
currency?: string;
|
||||||
|
financial_status?: OrderFinancialStatus;
|
||||||
|
confirmed?: boolean;
|
||||||
|
total_discounts?: string;
|
||||||
|
total_line_items_price?: string;
|
||||||
|
cart_token?: string | null;
|
||||||
|
buyer_accepts_marketing?: boolean;
|
||||||
|
name?: string;
|
||||||
|
referring_site?: string | null;
|
||||||
|
landing_site?: string | null;
|
||||||
|
cancelled_at?: string | null;
|
||||||
|
cancel_reason?: string | null;
|
||||||
|
user_id?: number | null;
|
||||||
|
location_id?: LocationId | null;
|
||||||
|
processed_at?: string | null;
|
||||||
|
device_id?: number | null;
|
||||||
|
phone?: string | null;
|
||||||
|
customer_locale?: string | null;
|
||||||
|
app_id?: number | null;
|
||||||
|
browser_ip?: string | null;
|
||||||
|
landing_site_ref?: string | null;
|
||||||
|
order_number?: number;
|
||||||
|
discount_codes?: Array<{ code: string; amount: string; type: string }>;
|
||||||
|
note_attributes?: Array<{ name: string; value: string }>;
|
||||||
|
payment_gateway_names?: string[];
|
||||||
|
processing_method?: string;
|
||||||
|
source_name?: string;
|
||||||
|
fulfillment_status?: OrderFulfillmentStatus;
|
||||||
|
tax_lines?: Array<{ price: string; rate: number; title: string }>;
|
||||||
|
tags?: string;
|
||||||
|
contact_email?: string | null;
|
||||||
|
order_status_url?: string;
|
||||||
|
presentment_currency?: string;
|
||||||
|
total_line_items_price_set?: Record<string, unknown>;
|
||||||
|
total_discounts_set?: Record<string, unknown>;
|
||||||
|
total_shipping_price_set?: Record<string, unknown>;
|
||||||
|
subtotal_price_set?: Record<string, unknown>;
|
||||||
|
total_price_set?: Record<string, unknown>;
|
||||||
|
total_tax_set?: Record<string, unknown>;
|
||||||
|
line_items?: OrderLineItem[];
|
||||||
|
shipping_lines?: Array<{
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
price: string;
|
||||||
|
code: string;
|
||||||
|
source: string;
|
||||||
|
}>;
|
||||||
|
shipping_address?: ShippingAddress | null;
|
||||||
|
billing_address?: ShippingAddress | null;
|
||||||
|
fulfillments?: Fulfillment[];
|
||||||
|
refunds?: Refund[];
|
||||||
|
customer?: Customer | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customer types
|
||||||
|
export interface CustomerAddress {
|
||||||
|
id: number;
|
||||||
|
customer_id: CustomerId;
|
||||||
|
first_name?: string | null;
|
||||||
|
last_name?: string | null;
|
||||||
|
company?: string | null;
|
||||||
|
address1?: string | null;
|
||||||
|
address2?: string | null;
|
||||||
|
city?: string | null;
|
||||||
|
province?: string | null;
|
||||||
|
country?: string | null;
|
||||||
|
zip?: string | null;
|
||||||
|
phone?: string | null;
|
||||||
|
name?: string;
|
||||||
|
province_code?: string | null;
|
||||||
|
country_code?: string | null;
|
||||||
|
country_name?: string;
|
||||||
|
default?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Customer {
|
||||||
|
id: CustomerId;
|
||||||
|
email?: string | null;
|
||||||
|
accepts_marketing?: boolean;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
first_name?: string | null;
|
||||||
|
last_name?: string | null;
|
||||||
|
orders_count?: number;
|
||||||
|
state?: 'disabled' | 'invited' | 'enabled' | 'declined';
|
||||||
|
total_spent?: string;
|
||||||
|
last_order_id?: OrderId | null;
|
||||||
|
note?: string | null;
|
||||||
|
verified_email?: boolean;
|
||||||
|
multipass_identifier?: string | null;
|
||||||
|
tax_exempt?: boolean;
|
||||||
|
phone?: string | null;
|
||||||
|
tags?: string;
|
||||||
|
last_order_name?: string | null;
|
||||||
|
currency?: string;
|
||||||
|
addresses?: CustomerAddress[];
|
||||||
|
accepts_marketing_updated_at?: string;
|
||||||
|
marketing_opt_in_level?: string | null;
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
default_address?: CustomerAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerSavedSearch {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inventory types
|
||||||
|
export interface InventoryItem {
|
||||||
|
id: InventoryItemId;
|
||||||
|
sku?: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
requires_shipping?: boolean;
|
||||||
|
cost?: string | null;
|
||||||
|
country_code_of_origin?: string | null;
|
||||||
|
province_code_of_origin?: string | null;
|
||||||
|
harmonized_system_code?: string | null;
|
||||||
|
tracked?: boolean;
|
||||||
|
country_harmonized_system_codes?: Array<{
|
||||||
|
country_code: string;
|
||||||
|
harmonized_system_code: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InventoryLevel {
|
||||||
|
inventory_item_id: InventoryItemId;
|
||||||
|
location_id: LocationId;
|
||||||
|
available?: number | null;
|
||||||
|
updated_at?: string;
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Location {
|
||||||
|
id: LocationId;
|
||||||
|
name: string;
|
||||||
|
address1?: string | null;
|
||||||
|
address2?: string | null;
|
||||||
|
city?: string | null;
|
||||||
|
zip?: string | null;
|
||||||
|
province?: string | null;
|
||||||
|
country?: string | null;
|
||||||
|
phone?: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
country_code?: string;
|
||||||
|
country_name?: string;
|
||||||
|
province_code?: string | null;
|
||||||
|
legacy?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discount types
|
||||||
|
export interface PriceRule {
|
||||||
|
id: PriceRuleId;
|
||||||
|
title: string;
|
||||||
|
target_type: 'line_item' | 'shipping_line';
|
||||||
|
target_selection: 'all' | 'entitled';
|
||||||
|
allocation_method: 'each' | 'across';
|
||||||
|
value_type: 'fixed_amount' | 'percentage';
|
||||||
|
value: string;
|
||||||
|
customer_selection: 'all' | 'prerequisite';
|
||||||
|
prerequisite_customer_ids?: CustomerId[];
|
||||||
|
prerequisite_saved_search_ids?: number[];
|
||||||
|
prerequisite_subtotal_range?: { greater_than_or_equal_to?: string };
|
||||||
|
prerequisite_quantity_range?: { greater_than_or_equal_to?: number };
|
||||||
|
prerequisite_shipping_price_range?: { less_than_or_equal_to?: string };
|
||||||
|
entitled_product_ids?: ProductId[];
|
||||||
|
entitled_variant_ids?: VariantId[];
|
||||||
|
entitled_collection_ids?: CollectionId[];
|
||||||
|
entitled_country_ids?: number[];
|
||||||
|
starts_at: string;
|
||||||
|
ends_at?: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
once_per_customer?: boolean;
|
||||||
|
usage_limit?: number | null;
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiscountCode {
|
||||||
|
id: DiscountCodeId;
|
||||||
|
price_rule_id: PriceRuleId;
|
||||||
|
code: string;
|
||||||
|
usage_count?: number;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shipping types
|
||||||
|
export interface ShippingZone {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
countries?: Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
tax: number;
|
||||||
|
code: string;
|
||||||
|
tax_name: string;
|
||||||
|
provinces: Array<{
|
||||||
|
id: number;
|
||||||
|
country_id: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
tax: number;
|
||||||
|
tax_name: string;
|
||||||
|
tax_type: string | null;
|
||||||
|
shipping_zone_id: number;
|
||||||
|
tax_percentage: number;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
weight_based_shipping_rates?: Array<{
|
||||||
|
id: number;
|
||||||
|
weight_low: number;
|
||||||
|
weight_high: number;
|
||||||
|
price: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
price_based_shipping_rates?: Array<{
|
||||||
|
id: number;
|
||||||
|
min_order_subtotal: string;
|
||||||
|
max_order_subtotal: string | null;
|
||||||
|
price: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
carrier_shipping_rate_providers?: Array<{
|
||||||
|
id: number;
|
||||||
|
carrier_service_id: number;
|
||||||
|
flat_modifier: string;
|
||||||
|
percent_modifier: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarrierService {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
active: boolean;
|
||||||
|
service_discovery: boolean;
|
||||||
|
carrier_service_type: 'api' | 'legacy';
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
format: 'json' | 'xml';
|
||||||
|
callback_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeliveryProfile {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
default: boolean;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme types
|
||||||
|
export interface Theme {
|
||||||
|
id: ThemeId;
|
||||||
|
name: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
role: 'main' | 'unpublished' | 'demo' | 'development';
|
||||||
|
theme_store_id?: number | null;
|
||||||
|
previewable?: boolean;
|
||||||
|
processing?: boolean;
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Asset {
|
||||||
|
key: string;
|
||||||
|
public_url?: string | null;
|
||||||
|
value?: string;
|
||||||
|
attachment?: string;
|
||||||
|
content_type?: string;
|
||||||
|
size?: number;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
checksum?: string | null;
|
||||||
|
theme_id?: ThemeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content types
|
||||||
|
export interface Page {
|
||||||
|
id: PageId;
|
||||||
|
title: string;
|
||||||
|
shop_id?: number;
|
||||||
|
handle?: string;
|
||||||
|
body_html?: string | null;
|
||||||
|
author?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
published_at?: string | null;
|
||||||
|
template_suffix?: string | null;
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Blog {
|
||||||
|
id: BlogId;
|
||||||
|
handle?: string;
|
||||||
|
title: string;
|
||||||
|
updated_at?: string;
|
||||||
|
commentable?: string;
|
||||||
|
feedburner?: string | null;
|
||||||
|
feedburner_location?: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
template_suffix?: string | null;
|
||||||
|
tags?: string;
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Article {
|
||||||
|
id: ArticleId;
|
||||||
|
title: string;
|
||||||
|
created_at?: string;
|
||||||
|
body_html?: string | null;
|
||||||
|
blog_id: BlogId;
|
||||||
|
author?: string;
|
||||||
|
user_id?: number;
|
||||||
|
published_at?: string | null;
|
||||||
|
updated_at?: string;
|
||||||
|
summary_html?: string | null;
|
||||||
|
template_suffix?: string | null;
|
||||||
|
handle?: string;
|
||||||
|
tags?: string;
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
image?: {
|
||||||
|
created_at?: string;
|
||||||
|
alt?: string | null;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
src: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Redirect {
|
||||||
|
id: number;
|
||||||
|
path: string;
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptTag {
|
||||||
|
id: number;
|
||||||
|
src: string;
|
||||||
|
event: 'onload';
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
display_scope: 'online_store' | 'order_status' | 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhook types
|
||||||
|
export interface Webhook {
|
||||||
|
id: WebhookId;
|
||||||
|
address: string;
|
||||||
|
topic: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
format: 'json' | 'xml';
|
||||||
|
fields?: string[];
|
||||||
|
metafield_namespaces?: string[];
|
||||||
|
api_version?: string;
|
||||||
|
private_metafield_namespaces?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event types
|
||||||
|
export interface Event {
|
||||||
|
id: number;
|
||||||
|
subject_id: number;
|
||||||
|
created_at?: string;
|
||||||
|
subject_type: string;
|
||||||
|
verb: string;
|
||||||
|
arguments?: unknown[];
|
||||||
|
body?: string | null;
|
||||||
|
message?: string;
|
||||||
|
author?: string;
|
||||||
|
description?: string;
|
||||||
|
path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metafield types
|
||||||
|
export interface Metafield {
|
||||||
|
id: MetafieldId;
|
||||||
|
namespace: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
description?: string | null;
|
||||||
|
owner_id?: number;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
owner_resource?: string;
|
||||||
|
admin_graphql_api_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shop types
|
||||||
|
export interface Shop {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
domain: string;
|
||||||
|
province?: string;
|
||||||
|
country?: string;
|
||||||
|
address1?: string;
|
||||||
|
zip?: string;
|
||||||
|
city?: string;
|
||||||
|
source?: string | null;
|
||||||
|
phone?: string;
|
||||||
|
latitude?: number | null;
|
||||||
|
longitude?: number | null;
|
||||||
|
primary_locale?: string;
|
||||||
|
address2?: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
country_code?: string;
|
||||||
|
country_name?: string;
|
||||||
|
currency?: string;
|
||||||
|
customer_email?: string;
|
||||||
|
timezone?: string;
|
||||||
|
iana_timezone?: string;
|
||||||
|
shop_owner?: string;
|
||||||
|
money_format?: string;
|
||||||
|
money_with_currency_format?: string;
|
||||||
|
weight_unit?: string;
|
||||||
|
province_code?: string | null;
|
||||||
|
taxes_included?: boolean | null;
|
||||||
|
auto_configure_tax_inclusivity?: boolean | null;
|
||||||
|
tax_shipping?: boolean | null;
|
||||||
|
county_taxes?: boolean;
|
||||||
|
plan_display_name?: string;
|
||||||
|
plan_name?: string;
|
||||||
|
has_discounts?: boolean;
|
||||||
|
has_gift_cards?: boolean;
|
||||||
|
myshopify_domain?: string;
|
||||||
|
google_apps_domain?: string | null;
|
||||||
|
google_apps_login_enabled?: boolean | null;
|
||||||
|
money_in_emails_format?: string;
|
||||||
|
money_with_currency_in_emails_format?: string;
|
||||||
|
eligible_for_payments?: boolean;
|
||||||
|
requires_extra_payments_agreement?: boolean;
|
||||||
|
password_enabled?: boolean;
|
||||||
|
has_storefront?: boolean;
|
||||||
|
finances?: boolean;
|
||||||
|
primary_location_id?: LocationId;
|
||||||
|
checkout_api_supported?: boolean;
|
||||||
|
multi_location_enabled?: boolean;
|
||||||
|
setup_required?: boolean;
|
||||||
|
pre_launch_enabled?: boolean;
|
||||||
|
enabled_presentment_currencies?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Response wrappers
|
||||||
|
export interface ShopifyListResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
pageInfo?: {
|
||||||
|
hasNextPage: boolean;
|
||||||
|
hasPreviousPage: boolean;
|
||||||
|
nextCursor?: string;
|
||||||
|
previousCursor?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShopifyError {
|
||||||
|
errors: string | Record<string, string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQL types
|
||||||
|
export interface GraphQLRequest {
|
||||||
|
query: string;
|
||||||
|
variables?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphQLResponse<T = unknown> {
|
||||||
|
data?: T;
|
||||||
|
errors?: Array<{
|
||||||
|
message: string;
|
||||||
|
locations?: Array<{ line: number; column: number }>;
|
||||||
|
path?: string[];
|
||||||
|
extensions?: Record<string, unknown>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
19
servers/shopify/tsconfig.json
Normal file
19
servers/shopify/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
8
servers/stripe/.env.example
Normal file
8
servers/stripe/.env.example
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Required: Your Stripe secret key (starts with sk_test_ or sk_live_)
|
||||||
|
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
|
# Optional: Webhook signing secret for webhook verification
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
|
# Optional: HTTP server port for SSE transport
|
||||||
|
PORT=3000
|
||||||
6
servers/stripe/.gitignore
vendored
Normal file
6
servers/stripe/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
*.tsbuildinfo
|
||||||
290
servers/stripe/README.md
Normal file
290
servers/stripe/README.md
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
# Stripe MCP Server
|
||||||
|
|
||||||
|
Model Context Protocol (MCP) server for Stripe API integration. Provides comprehensive access to Stripe's payment processing, subscription management, and customer data.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔐 **Secure Authentication** - Bearer token authentication with API key
|
||||||
|
- 🔄 **Automatic Retries** - Exponential backoff with configurable retry logic
|
||||||
|
- 📊 **Pagination Support** - Cursor-based pagination with automatic iteration
|
||||||
|
- 🎯 **Idempotency** - Built-in idempotency key support for safe retries
|
||||||
|
- 🛡️ **Type Safety** - Comprehensive TypeScript types for all Stripe entities
|
||||||
|
- 🚀 **Lazy Loading** - Tool modules loaded on-demand for better performance
|
||||||
|
- 🔌 **Dual Transport** - Supports both stdio and HTTP/SSE transports
|
||||||
|
- 📦 **Resource Handlers** - Pre-built resources for common UI needs
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create a `.env` file based on `.env.example`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required
|
||||||
|
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxx
|
||||||
|
PORT=3000
|
||||||
|
TRANSPORT_MODE=stdio # stdio | http | dual
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| `STRIPE_SECRET_KEY` | ✅ Yes | Your Stripe secret API key (starts with `sk_test_` or `sk_live_`) |
|
||||||
|
| `STRIPE_WEBHOOK_SECRET` | ❌ No | Webhook signing secret for webhook verification (starts with `whsec_`) |
|
||||||
|
| `PORT` | ❌ No | HTTP server port for SSE transport (default: 3000) |
|
||||||
|
| `TRANSPORT_MODE` | ❌ No | Transport mode: `stdio`, `http`, or `dual` (default: stdio) |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transport Modes
|
||||||
|
|
||||||
|
#### stdio (Default)
|
||||||
|
Used by MCP clients like Claude Desktop:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
# or
|
||||||
|
TRANSPORT_MODE=stdio npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
#### HTTP/SSE
|
||||||
|
For web-based integrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TRANSPORT_MODE=http npm start
|
||||||
|
# Server available at http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dual Transport
|
||||||
|
Run both simultaneously:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TRANSPORT_MODE=dual npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tool Categories
|
||||||
|
|
||||||
|
The server provides lazy-loaded tool modules organized by functionality:
|
||||||
|
|
||||||
|
### 💳 Payments
|
||||||
|
- `charges` - Create, retrieve, update, and list charges
|
||||||
|
- `payment-intents` - Manage payment intents
|
||||||
|
- `payment-methods` - Handle payment methods
|
||||||
|
- `refunds` - Process and manage refunds
|
||||||
|
- `disputes` - Handle payment disputes
|
||||||
|
|
||||||
|
### 👥 Customers
|
||||||
|
- `customers` - Create and manage customers
|
||||||
|
|
||||||
|
### 📅 Subscriptions
|
||||||
|
- `subscriptions` - Manage recurring subscriptions
|
||||||
|
- `invoices` - Handle invoices and billing
|
||||||
|
|
||||||
|
### 🏷️ Products & Pricing
|
||||||
|
- `products` - Manage products
|
||||||
|
- `prices` - Handle pricing models
|
||||||
|
|
||||||
|
### 💰 Payouts & Balance
|
||||||
|
- `payouts` - Manage payouts to bank accounts
|
||||||
|
- `balance` - Check account balance and transactions
|
||||||
|
|
||||||
|
### 🔔 Events & Webhooks
|
||||||
|
- `events` - Retrieve and search events
|
||||||
|
- `webhooks` - Manage webhook endpoints
|
||||||
|
|
||||||
|
## Resource URIs
|
||||||
|
|
||||||
|
Pre-built resources for UI applications:
|
||||||
|
|
||||||
|
| URI | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| `stripe://dashboard` | Account overview and key metrics |
|
||||||
|
| `stripe://customers` | Customer list |
|
||||||
|
| `stripe://payments/recent` | Recent payments and charges |
|
||||||
|
| `stripe://subscriptions/active` | Active subscriptions |
|
||||||
|
| `stripe://invoices/unpaid` | Unpaid invoices |
|
||||||
|
|
||||||
|
## API Client Features
|
||||||
|
|
||||||
|
### Automatic Pagination
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Iterate through all customers
|
||||||
|
for await (const customer of client.paginate('customers')) {
|
||||||
|
console.log(customer.email);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Idempotency Keys
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Safe retry with idempotency
|
||||||
|
const result = await client.create('charges', data, {
|
||||||
|
idempotencyKey: client.generateIdempotencyKey()
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retry Logic
|
||||||
|
|
||||||
|
- Automatic retry on 429 (rate limit), 409 (conflict), and 5xx errors
|
||||||
|
- Exponential backoff with jitter
|
||||||
|
- Respects `Stripe-Should-Retry` and `Retry-After` headers
|
||||||
|
- Configurable retry count (default: 3)
|
||||||
|
|
||||||
|
### Form Encoding
|
||||||
|
|
||||||
|
Stripe requires `application/x-www-form-urlencoded` for POST requests. The client automatically handles:
|
||||||
|
|
||||||
|
- Nested object encoding with dot notation
|
||||||
|
- Array parameters
|
||||||
|
- Metadata fields
|
||||||
|
- Null/undefined value filtering
|
||||||
|
|
||||||
|
## Type Safety
|
||||||
|
|
||||||
|
All Stripe entities use branded ID types for additional type safety:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type CustomerId = string & { __brand: 'CustomerId' };
|
||||||
|
type PaymentIntentId = string & { __brand: 'PaymentIntentId' };
|
||||||
|
```
|
||||||
|
|
||||||
|
This prevents accidentally mixing different ID types.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The server provides structured error responses:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"type": "api_error",
|
||||||
|
"message": "The API key provided is invalid.",
|
||||||
|
"code": "invalid_api_key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Common error types:
|
||||||
|
- `api_error` - Stripe API errors
|
||||||
|
- `card_error` - Card-related errors
|
||||||
|
- `rate_limit_error` - Too many requests
|
||||||
|
- `invalid_request_error` - Invalid parameters
|
||||||
|
- `authentication_error` - Invalid API key
|
||||||
|
|
||||||
|
## Health Check
|
||||||
|
|
||||||
|
When running in HTTP mode, health check available at:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "@mcpengine/stripe",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"timestamp": "2024-01-18T12:00:00.000Z",
|
||||||
|
"uptime": 123.45,
|
||||||
|
"memory": { "rss": 45678, "heapTotal": 23456, "heapUsed": 12345 },
|
||||||
|
"node": "v22.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Graceful Shutdown
|
||||||
|
|
||||||
|
The server handles shutdown signals gracefully:
|
||||||
|
|
||||||
|
- `SIGTERM` - Kubernetes/Docker shutdown
|
||||||
|
- `SIGINT` - Ctrl+C in terminal
|
||||||
|
- `SIGUSR2` - Nodemon restart
|
||||||
|
|
||||||
|
Cleanup process:
|
||||||
|
1. Stop accepting new connections
|
||||||
|
2. Finish processing active requests
|
||||||
|
3. Close MCP server connection
|
||||||
|
4. Exit with code 0
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
stripe/
|
||||||
|
├── src/
|
||||||
|
│ ├── clients/
|
||||||
|
│ │ └── stripe.ts # Stripe API client
|
||||||
|
│ ├── types/
|
||||||
|
│ │ └── index.ts # TypeScript type definitions
|
||||||
|
│ ├── tools/ # Tool modules (lazy-loaded)
|
||||||
|
│ │ ├── customers.ts
|
||||||
|
│ │ ├── charges.ts
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── server.ts # MCP server implementation
|
||||||
|
│ └── main.ts # Entry point
|
||||||
|
├── package.json
|
||||||
|
├── tsconfig.json
|
||||||
|
├── .env.example
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding New Tools
|
||||||
|
|
||||||
|
1. Create a new file in `src/tools/`
|
||||||
|
2. Export an array of `ToolModule` objects
|
||||||
|
3. Register in `TOOL_MODULES` in `src/server.ts`
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Type check
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Run in dev mode
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- **Never commit `.env` file** - Contains sensitive API keys
|
||||||
|
- **Use test keys in development** - Keys starting with `sk_test_`
|
||||||
|
- **Rotate keys regularly** - Especially after security incidents
|
||||||
|
- **Webhook signature verification** - Use `STRIPE_WEBHOOK_SECRET` for webhooks
|
||||||
|
- **Idempotency keys** - Prevent duplicate charges on retries
|
||||||
|
|
||||||
|
## Stripe API Version
|
||||||
|
|
||||||
|
This server uses Stripe API version `2024-01-18`. The version is set in the `Stripe-Version` header for all requests.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and questions:
|
||||||
|
- Stripe API Docs: https://stripe.com/docs/api
|
||||||
|
- MCP Protocol: https://github.com/anthropics/model-context-protocol
|
||||||
21
servers/stripe/package.json
Normal file
21
servers/stripe/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@mcpengine/stripe",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"dev": "tsx watch src/main.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
|
"axios": "^1.7.0",
|
||||||
|
"zod": "^3.23.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"tsx": "^4.19.0",
|
||||||
|
"@types/node": "^22.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
352
servers/stripe/src/clients/stripe.ts
Normal file
352
servers/stripe/src/clients/stripe.ts
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
|
||||||
|
import { StripeList, StripeError } from '../types/index.js';
|
||||||
|
|
||||||
|
const STRIPE_API_BASE = 'https://api.stripe.com/v1';
|
||||||
|
const STRIPE_API_VERSION = '2024-01-18';
|
||||||
|
const DEFAULT_TIMEOUT = 30000;
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
const INITIAL_RETRY_DELAY = 1000;
|
||||||
|
|
||||||
|
export interface StripeClientConfig {
|
||||||
|
apiKey: string;
|
||||||
|
apiVersion?: string;
|
||||||
|
timeout?: number;
|
||||||
|
maxRetries?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationParams {
|
||||||
|
limit?: number;
|
||||||
|
starting_after?: string;
|
||||||
|
ending_before?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestOptions {
|
||||||
|
idempotencyKey?: string;
|
||||||
|
stripeAccount?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert object to application/x-www-form-urlencoded format
|
||||||
|
* Handles nested objects with dot notation (e.g., metadata.key=value)
|
||||||
|
*/
|
||||||
|
function encodeFormData(data: Record<string, any>, prefix = ''): string {
|
||||||
|
const pairs: string[] = [];
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(data, key)) continue;
|
||||||
|
|
||||||
|
const value = data[key];
|
||||||
|
const encodedKey = prefix ? `${prefix}[${key}]` : key;
|
||||||
|
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
continue;
|
||||||
|
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
// Recursively handle nested objects
|
||||||
|
pairs.push(encodeFormData(value, encodedKey));
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
// Handle arrays
|
||||||
|
value.forEach((item, index) => {
|
||||||
|
if (typeof item === 'object') {
|
||||||
|
pairs.push(encodeFormData(item, `${encodedKey}[${index}]`));
|
||||||
|
} else {
|
||||||
|
pairs.push(`${encodeURIComponent(`${encodedKey}[]`)}=${encodeURIComponent(String(item))}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pairs.push(`${encodeURIComponent(encodedKey)}=${encodeURIComponent(String(value))}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pairs.filter(p => p).join('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate exponential backoff delay
|
||||||
|
*/
|
||||||
|
function getRetryDelay(attempt: number, baseDelay = INITIAL_RETRY_DELAY): number {
|
||||||
|
return Math.min(baseDelay * Math.pow(2, attempt - 1), 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if error is retryable
|
||||||
|
*/
|
||||||
|
function isRetryableError(error: AxiosError): boolean {
|
||||||
|
if (!error.response) return true; // Network errors are retryable
|
||||||
|
|
||||||
|
const status = error.response.status;
|
||||||
|
const shouldRetry = error.response.headers['stripe-should-retry'];
|
||||||
|
|
||||||
|
// Respect Stripe-Should-Retry header if present
|
||||||
|
if (shouldRetry !== undefined) {
|
||||||
|
return shouldRetry === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry on 429 (rate limit), 500s, and 409 (conflict)
|
||||||
|
return status === 429 || status === 409 || (status >= 500 && status < 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StripeClient {
|
||||||
|
private client: AxiosInstance;
|
||||||
|
private apiKey: string;
|
||||||
|
private maxRetries: number;
|
||||||
|
|
||||||
|
constructor(config: StripeClientConfig) {
|
||||||
|
this.apiKey = config.apiKey;
|
||||||
|
this.maxRetries = config.maxRetries ?? MAX_RETRIES;
|
||||||
|
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: STRIPE_API_BASE,
|
||||||
|
timeout: config.timeout ?? DEFAULT_TIMEOUT,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`,
|
||||||
|
'Stripe-Version': config.apiVersion ?? STRIPE_API_VERSION,
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupInterceptors();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupInterceptors(): void {
|
||||||
|
// Request interceptor
|
||||||
|
this.client.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// Log request (optional - can be removed in production)
|
||||||
|
console.log(`[Stripe API] ${config.method?.toUpperCase()} ${config.url}`);
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Response interceptor
|
||||||
|
this.client.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
// Log successful response
|
||||||
|
console.log(`[Stripe API] ${response.status} ${response.config.url}`);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
async (error: AxiosError) => {
|
||||||
|
const config = error.config as AxiosRequestConfig & { _retryCount?: number };
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize retry count
|
||||||
|
config._retryCount = config._retryCount ?? 0;
|
||||||
|
|
||||||
|
// Check if we should retry
|
||||||
|
if (config._retryCount < this.maxRetries && isRetryableError(error)) {
|
||||||
|
config._retryCount++;
|
||||||
|
|
||||||
|
// Calculate delay
|
||||||
|
let delay = getRetryDelay(config._retryCount);
|
||||||
|
|
||||||
|
// Respect Retry-After header if present
|
||||||
|
if (error.response?.headers['retry-after']) {
|
||||||
|
const retryAfter = parseInt(error.response.headers['retry-after'], 10);
|
||||||
|
delay = retryAfter * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Stripe API] Retrying request (${config._retryCount}/${this.maxRetries}) after ${delay}ms`);
|
||||||
|
|
||||||
|
// Wait before retrying
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
|
||||||
|
return this.client.request(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format error
|
||||||
|
const stripeError = this.formatError(error);
|
||||||
|
return Promise.reject(stripeError);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatError(error: AxiosError): Error {
|
||||||
|
if (error.response?.data) {
|
||||||
|
const data = error.response.data as any;
|
||||||
|
if (data.error) {
|
||||||
|
const stripeError = data.error as StripeError;
|
||||||
|
return new Error(`Stripe API Error (${stripeError.type}): ${stripeError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Error(error.message || 'Unknown Stripe API error');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET request
|
||||||
|
*/
|
||||||
|
async get<T>(
|
||||||
|
path: string,
|
||||||
|
params?: Record<string, any>,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<T> {
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
params,
|
||||||
|
headers: this.buildHeaders(options),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.client.get<T>(path, config);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST request (form-encoded)
|
||||||
|
*/
|
||||||
|
async post<T>(
|
||||||
|
path: string,
|
||||||
|
data?: Record<string, any>,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<T> {
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
headers: this.buildHeaders(options),
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = data ? encodeFormData(data) : '';
|
||||||
|
const response = await this.client.post<T>(path, formData, config);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE request
|
||||||
|
*/
|
||||||
|
async delete<T>(
|
||||||
|
path: string,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<T> {
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
headers: this.buildHeaders(options),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.client.delete<T>(path, config);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginate through all results automatically
|
||||||
|
*/
|
||||||
|
async *paginate<T>(
|
||||||
|
path: string,
|
||||||
|
params?: Record<string, any>,
|
||||||
|
options?: RequestOptions
|
||||||
|
): AsyncGenerator<T, void, unknown> {
|
||||||
|
let hasMore = true;
|
||||||
|
let startingAfter: string | undefined;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const response = await this.get<StripeList<T>>(
|
||||||
|
path,
|
||||||
|
{
|
||||||
|
...params,
|
||||||
|
starting_after: startingAfter,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const item of response.data) {
|
||||||
|
yield item;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMore = response.has_more;
|
||||||
|
if (hasMore && response.data.length > 0) {
|
||||||
|
// Get the ID of the last item for the next request
|
||||||
|
const lastItem = response.data[response.data.length - 1] as any;
|
||||||
|
startingAfter = lastItem.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List resources with pagination support
|
||||||
|
*/
|
||||||
|
async list<T>(
|
||||||
|
path: string,
|
||||||
|
params?: PaginationParams & Record<string, any>,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<StripeList<T>> {
|
||||||
|
return this.get<StripeList<T>>(path, params, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a single resource
|
||||||
|
*/
|
||||||
|
async retrieve<T>(
|
||||||
|
path: string,
|
||||||
|
id: string,
|
||||||
|
params?: Record<string, any>,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<T> {
|
||||||
|
return this.get<T>(`${path}/${id}`, params, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a resource
|
||||||
|
*/
|
||||||
|
async create<T>(
|
||||||
|
path: string,
|
||||||
|
data: Record<string, any>,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<T> {
|
||||||
|
return this.post<T>(path, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a resource
|
||||||
|
*/
|
||||||
|
async update<T>(
|
||||||
|
path: string,
|
||||||
|
id: string,
|
||||||
|
data: Record<string, any>,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<T> {
|
||||||
|
return this.post<T>(`${path}/${id}`, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete/cancel a resource
|
||||||
|
*/
|
||||||
|
async remove<T>(
|
||||||
|
path: string,
|
||||||
|
id: string,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<T> {
|
||||||
|
return this.delete<T>(`${path}/${id}`, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build headers with optional idempotency key and Stripe-Account
|
||||||
|
*/
|
||||||
|
private buildHeaders(options?: RequestOptions): Record<string, string> {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (options?.idempotencyKey) {
|
||||||
|
headers['Idempotency-Key'] = options.idempotencyKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.stripeAccount) {
|
||||||
|
headers['Stripe-Account'] = options.stripeAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate idempotency key (UUID v4)
|
||||||
|
*/
|
||||||
|
generateIdempotencyKey(): string {
|
||||||
|
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Stripe client instance
|
||||||
|
*/
|
||||||
|
export function createStripeClient(apiKey: string, config?: Partial<StripeClientConfig>): StripeClient {
|
||||||
|
return new StripeClient({
|
||||||
|
apiKey,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
}
|
||||||
185
servers/stripe/src/main.ts
Normal file
185
servers/stripe/src/main.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { createStripeServer } from './server.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variable schema
|
||||||
|
*/
|
||||||
|
const EnvSchema = z.object({
|
||||||
|
STRIPE_SECRET_KEY: z.string().min(1, 'STRIPE_SECRET_KEY is required'),
|
||||||
|
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
||||||
|
PORT: z.string().optional().default('3000'),
|
||||||
|
NODE_ENV: z.enum(['development', 'production', 'test']).optional().default('production'),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Env = z.infer<typeof EnvSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate environment variables
|
||||||
|
*/
|
||||||
|
function validateEnv(): Env {
|
||||||
|
try {
|
||||||
|
return EnvSchema.parse(process.env);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
console.error('❌ Environment validation failed:');
|
||||||
|
for (const issue of error.issues) {
|
||||||
|
console.error(` - ${issue.path.join('.')}: ${issue.message}`);
|
||||||
|
}
|
||||||
|
console.error('\nRequired environment variables:');
|
||||||
|
console.error(' STRIPE_SECRET_KEY - Your Stripe secret key (starts with sk_test_ or sk_live_)');
|
||||||
|
console.error('\nOptional environment variables:');
|
||||||
|
console.error(' STRIPE_WEBHOOK_SECRET - Webhook signing secret (starts with whsec_)');
|
||||||
|
console.error(' PORT - HTTP server port for SSE transport (default: 3000)');
|
||||||
|
console.error('\nExample:');
|
||||||
|
console.error(' export STRIPE_SECRET_KEY=sk_test_xxxxx');
|
||||||
|
console.error(' npm start');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check endpoint data
|
||||||
|
*/
|
||||||
|
function getHealthInfo() {
|
||||||
|
return {
|
||||||
|
status: 'healthy',
|
||||||
|
service: '@mcpengine/stripe',
|
||||||
|
version: '1.0.0',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
uptime: process.uptime(),
|
||||||
|
memory: process.memoryUsage(),
|
||||||
|
node: process.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup graceful shutdown
|
||||||
|
*/
|
||||||
|
function setupGracefulShutdown(cleanup: () => Promise<void>): void {
|
||||||
|
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
|
||||||
|
|
||||||
|
signals.forEach(signal => {
|
||||||
|
process.on(signal, async () => {
|
||||||
|
console.error(`\n📡 Received ${signal}, shutting down gracefully...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cleanup();
|
||||||
|
console.error('✅ Cleanup complete');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during cleanup:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle uncaught errors
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.error('❌ Uncaught exception:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('❌ Unhandled rejection at:', promise, 'reason:', reason);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start HTTP/SSE server for dual transport support
|
||||||
|
*/
|
||||||
|
async function startHttpServer(server: ReturnType<typeof createStripeServer>, port: number): Promise<void> {
|
||||||
|
const { createServer } = await import('http');
|
||||||
|
const { SSEServerTransport } = await import('@modelcontextprotocol/sdk/server/sse.js');
|
||||||
|
|
||||||
|
const httpServer = createServer(async (req, res) => {
|
||||||
|
// Health check endpoint
|
||||||
|
if (req.url === '/health' || req.url === '/') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(getHealthInfo(), null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSE endpoint
|
||||||
|
if (req.url === '/sse') {
|
||||||
|
console.error('📡 SSE client connected');
|
||||||
|
|
||||||
|
const transport = new SSEServerTransport('/message', res);
|
||||||
|
await server.getServer().connect(transport);
|
||||||
|
|
||||||
|
// Handle client disconnect
|
||||||
|
req.on('close', () => {
|
||||||
|
console.error('📡 SSE client disconnected');
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 404 for other routes
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Not found' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
httpServer.listen(port, () => {
|
||||||
|
console.error(`🌐 HTTP/SSE server listening on http://localhost:${port}`);
|
||||||
|
console.error(` Health: http://localhost:${port}/health`);
|
||||||
|
console.error(` SSE: http://localhost:${port}/sse`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point
|
||||||
|
*/
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
console.error('🚀 Starting Stripe MCP Server...\n');
|
||||||
|
|
||||||
|
// Validate environment
|
||||||
|
const env = validateEnv();
|
||||||
|
|
||||||
|
console.error('✅ Environment validated');
|
||||||
|
console.error(` API Key: ${env.STRIPE_SECRET_KEY.substring(0, 12)}...`);
|
||||||
|
if (env.STRIPE_WEBHOOK_SECRET) {
|
||||||
|
console.error(` Webhook Secret: ${env.STRIPE_WEBHOOK_SECRET.substring(0, 12)}...`);
|
||||||
|
}
|
||||||
|
console.error('');
|
||||||
|
|
||||||
|
// Create server
|
||||||
|
const server = createStripeServer({
|
||||||
|
apiKey: env.STRIPE_SECRET_KEY,
|
||||||
|
webhookSecret: env.STRIPE_WEBHOOK_SECRET,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup graceful shutdown
|
||||||
|
setupGracefulShutdown(async () => {
|
||||||
|
await server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determine transport mode
|
||||||
|
const transportMode = process.env.TRANSPORT_MODE || 'stdio';
|
||||||
|
|
||||||
|
if (transportMode === 'http' || transportMode === 'dual') {
|
||||||
|
const port = parseInt(env.PORT, 10);
|
||||||
|
await startHttpServer(server, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transportMode === 'stdio' || transportMode === 'dual') {
|
||||||
|
// Connect to stdio (this is the default for MCP)
|
||||||
|
await server.connectStdio();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transportMode !== 'http' && transportMode !== 'stdio' && transportMode !== 'dual') {
|
||||||
|
console.error(`❌ Unknown transport mode: ${transportMode}`);
|
||||||
|
console.error(' Valid modes: stdio, http, dual');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('✅ Server ready\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the server
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('❌ Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
324
servers/stripe/src/server.ts
Normal file
324
servers/stripe/src/server.ts
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
|
ErrorCode,
|
||||||
|
McpError,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { StripeClient } from './clients/stripe.js';
|
||||||
|
|
||||||
|
export interface StripeServerConfig {
|
||||||
|
apiKey: string;
|
||||||
|
webhookSecret?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool module interface for lazy loading
|
||||||
|
*/
|
||||||
|
interface ToolModule {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: any;
|
||||||
|
handler: (client: StripeClient, args: any) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy-loaded tool registry
|
||||||
|
* Tools are loaded on-demand to reduce startup time and memory footprint
|
||||||
|
*/
|
||||||
|
const TOOL_MODULES: Record<string, () => Promise<ToolModule[]>> = {
|
||||||
|
'customers': async () => (await import('./tools/customers.js')).default,
|
||||||
|
'charges': async () => (await import('./tools/charges.js')).default,
|
||||||
|
'payment-intents': async () => (await import('./tools/payment-intents.js')).default,
|
||||||
|
'payment-methods': async () => (await import('./tools/payment-methods.js')).default,
|
||||||
|
'refunds': async () => (await import('./tools/refunds.js')).default,
|
||||||
|
'disputes': async () => (await import('./tools/disputes.js')).default,
|
||||||
|
'subscriptions': async () => (await import('./tools/subscriptions.js')).default,
|
||||||
|
'invoices': async () => (await import('./tools/invoices.js')).default,
|
||||||
|
'products': async () => (await import('./tools/products.js')).default,
|
||||||
|
'prices': async () => (await import('./tools/prices.js')).default,
|
||||||
|
'payouts': async () => (await import('./tools/payouts.js')).default,
|
||||||
|
'balance': async () => (await import('./tools/balance.js')).default,
|
||||||
|
'events': async () => (await import('./tools/events.js')).default,
|
||||||
|
'webhooks': async () => (await import('./tools/webhooks.js')).default,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource definitions for UI apps
|
||||||
|
*/
|
||||||
|
const RESOURCES = [
|
||||||
|
{
|
||||||
|
uri: 'stripe://dashboard',
|
||||||
|
name: 'Stripe Dashboard',
|
||||||
|
description: 'Overview of account activity and key metrics',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'stripe://customers',
|
||||||
|
name: 'Customer List',
|
||||||
|
description: 'List of all customers',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'stripe://payments/recent',
|
||||||
|
name: 'Recent Payments',
|
||||||
|
description: 'Recent payment intents and charges',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'stripe://subscriptions/active',
|
||||||
|
name: 'Active Subscriptions',
|
||||||
|
description: 'Currently active subscriptions',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'stripe://invoices/unpaid',
|
||||||
|
name: 'Unpaid Invoices',
|
||||||
|
description: 'Invoices that are unpaid or overdue',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export class StripeServer {
|
||||||
|
private server: Server;
|
||||||
|
private client: StripeClient;
|
||||||
|
private toolCache: Map<string, ToolModule[]> = new Map();
|
||||||
|
|
||||||
|
constructor(config: StripeServerConfig) {
|
||||||
|
this.client = new StripeClient({ apiKey: config.apiKey });
|
||||||
|
|
||||||
|
this.server = new Server(
|
||||||
|
{
|
||||||
|
name: '@mcpengine/stripe',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
resources: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setupHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupHandlers(): void {
|
||||||
|
// List available tools
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
const tools: any[] = [];
|
||||||
|
|
||||||
|
// Load all tool modules to get their definitions
|
||||||
|
for (const [moduleName, loader] of Object.entries(TOOL_MODULES)) {
|
||||||
|
try {
|
||||||
|
const moduleTools = await this.getToolModule(moduleName);
|
||||||
|
for (const tool of moduleTools) {
|
||||||
|
tools.push({
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: tool.inputSchema,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load tool module ${moduleName}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tools };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle tool calls
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the tool handler
|
||||||
|
const handler = await this.findToolHandler(name);
|
||||||
|
|
||||||
|
if (!handler) {
|
||||||
|
throw new McpError(
|
||||||
|
ErrorCode.MethodNotFound,
|
||||||
|
`Tool not found: ${name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the tool
|
||||||
|
const result = await handler(this.client, args || {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof McpError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
throw new McpError(
|
||||||
|
ErrorCode.InternalError,
|
||||||
|
`Tool execution failed: ${errorMessage}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// List available resources
|
||||||
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||||
|
return { resources: RESOURCES };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read resource data
|
||||||
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
const { uri } = request.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await this.handleResourceRead(uri);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(content, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
throw new McpError(
|
||||||
|
ErrorCode.InternalError,
|
||||||
|
`Failed to read resource: ${errorMessage}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tool module (with caching)
|
||||||
|
*/
|
||||||
|
private async getToolModule(moduleName: string): Promise<ToolModule[]> {
|
||||||
|
if (this.toolCache.has(moduleName)) {
|
||||||
|
return this.toolCache.get(moduleName)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loader = TOOL_MODULES[moduleName];
|
||||||
|
if (!loader) {
|
||||||
|
throw new Error(`Tool module not found: ${moduleName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tools = await loader();
|
||||||
|
this.toolCache.set(moduleName, tools);
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find tool handler by name
|
||||||
|
*/
|
||||||
|
private async findToolHandler(toolName: string): Promise<((client: StripeClient, args: any) => Promise<any>) | null> {
|
||||||
|
for (const moduleName of Object.keys(TOOL_MODULES)) {
|
||||||
|
const tools = await this.getToolModule(moduleName);
|
||||||
|
const tool = tools.find(t => t.name === toolName);
|
||||||
|
if (tool) {
|
||||||
|
return tool.handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle resource read requests
|
||||||
|
*/
|
||||||
|
private async handleResourceRead(uri: string): Promise<any> {
|
||||||
|
// Parse the URI
|
||||||
|
const url = new URL(uri);
|
||||||
|
const path = url.pathname;
|
||||||
|
|
||||||
|
switch (path) {
|
||||||
|
case '/dashboard':
|
||||||
|
return this.getDashboardData();
|
||||||
|
|
||||||
|
case '/customers':
|
||||||
|
return this.client.list('customers', { limit: 100 });
|
||||||
|
|
||||||
|
case '/payments/recent':
|
||||||
|
return this.getRecentPayments();
|
||||||
|
|
||||||
|
case '/subscriptions/active':
|
||||||
|
return this.client.list('subscriptions', { status: 'active', limit: 100 });
|
||||||
|
|
||||||
|
case '/invoices/unpaid':
|
||||||
|
return this.client.list('invoices', { status: 'open', limit: 100 });
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown resource path: ${path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dashboard data (overview metrics)
|
||||||
|
*/
|
||||||
|
private async getDashboardData(): Promise<any> {
|
||||||
|
const [balance, recentCharges, recentCustomers] = await Promise.all([
|
||||||
|
this.client.get('balance'),
|
||||||
|
this.client.list('charges', { limit: 10 }),
|
||||||
|
this.client.list('customers', { limit: 10 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
balance,
|
||||||
|
recent_charges: recentCharges,
|
||||||
|
recent_customers: recentCustomers,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent payments
|
||||||
|
*/
|
||||||
|
private async getRecentPayments(): Promise<any> {
|
||||||
|
const [paymentIntents, charges] = await Promise.all([
|
||||||
|
this.client.list('payment_intents', { limit: 50 }),
|
||||||
|
this.client.list('charges', { limit: 50 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
payment_intents: paymentIntents,
|
||||||
|
charges,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to stdio transport
|
||||||
|
*/
|
||||||
|
async connectStdio(): Promise<void> {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await this.server.connect(transport);
|
||||||
|
console.error('Stripe MCP server running on stdio');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the underlying MCP server instance
|
||||||
|
*/
|
||||||
|
getServer(): Server {
|
||||||
|
return this.server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the server
|
||||||
|
*/
|
||||||
|
async close(): Promise<void> {
|
||||||
|
await this.server.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createStripeServer(config: StripeServerConfig): StripeServer {
|
||||||
|
return new StripeServer(config);
|
||||||
|
}
|
||||||
2
servers/stripe/src/tools/balance.ts
Normal file
2
servers/stripe/src/tools/balance.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/charges.ts
Normal file
2
servers/stripe/src/tools/charges.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/customers.ts
Normal file
2
servers/stripe/src/tools/customers.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/disputes.ts
Normal file
2
servers/stripe/src/tools/disputes.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/events.ts
Normal file
2
servers/stripe/src/tools/events.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/invoices.ts
Normal file
2
servers/stripe/src/tools/invoices.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/payment-intents.ts
Normal file
2
servers/stripe/src/tools/payment-intents.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/payment-methods.ts
Normal file
2
servers/stripe/src/tools/payment-methods.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/payouts.ts
Normal file
2
servers/stripe/src/tools/payouts.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/prices.ts
Normal file
2
servers/stripe/src/tools/prices.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/products.ts
Normal file
2
servers/stripe/src/tools/products.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/refunds.ts
Normal file
2
servers/stripe/src/tools/refunds.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/subscriptions.ts
Normal file
2
servers/stripe/src/tools/subscriptions.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
2
servers/stripe/src/tools/webhooks.ts
Normal file
2
servers/stripe/src/tools/webhooks.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder - tools to be implemented
|
||||||
|
export default [];
|
||||||
1132
servers/stripe/src/types/index.ts
Normal file
1132
servers/stripe/src/types/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
21
servers/stripe/tsconfig.json
Normal file
21
servers/stripe/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user