V3 Batch 1 Foundation: Shopify, Stripe, QuickBooks, HubSpot, Salesforce - types + clients + server + main, zero TSC errors

This commit is contained in:
Jake Shore 2026-02-13 02:33:45 -05:00
parent 6741068aef
commit 6ff76669a9
59 changed files with 9643 additions and 0 deletions

View 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
View File

@ -0,0 +1,8 @@
node_modules/
dist/
.env
*.log
.DS_Store
coverage/
.vscode/
.idea/

203
servers/hubspot/README.md Normal file
View 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.

View 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"
}
}

View 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
View 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();

View 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;
}
}

View 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;
}>;
}

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

View 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
View 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

View 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.

View 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"
}
}

View 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;
});
}
}

View 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);
});

View 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);
}

View 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;
}

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

View 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
View File

@ -0,0 +1,7 @@
node_modules/
dist/
.env
*.log
.DS_Store
coverage/
.nyc_output/

View 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

View 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"
}
}

View 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();
}
}

View 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);
});

View 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;
}
}

View 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;

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

View 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
View File

@ -0,0 +1,4 @@
node_modules
dist
.env
*.log

277
servers/shopify/README.md Normal file
View 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

View 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"
}
}

View 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
View 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);
});

View 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;
}
}

View 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>;
}>;
}

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

View 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
View File

@ -0,0 +1,6 @@
node_modules/
dist/
.env
*.log
.DS_Store
*.tsbuildinfo

290
servers/stripe/README.md Normal file
View 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

View 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"
}
}

View 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
View 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);
});

View 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);
}

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

View File

@ -0,0 +1,2 @@
// Placeholder - tools to be implemented
export default [];

File diff suppressed because it is too large Load Diff

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