V3 Batch 2 Foundation: Notion, Airtable, Intercom, Monday.com, Xero - types + clients + server + main, zero TSC errors

This commit is contained in:
Jake Shore 2026-02-13 03:10:06 -05:00
parent 062e0f281a
commit 6763409b5e
45 changed files with 11122 additions and 0 deletions

View File

@ -0,0 +1,4 @@
# Airtable API Configuration
# Get your API key from: https://airtable.com/create/tokens
AIRTABLE_API_KEY=your_api_key_here

42
servers/airtable/.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# Dependencies
node_modules/
package-lock.json
yarn.lock
pnpm-lock.yaml
# Build output
dist/
build/
*.tsbuildinfo
# Environment
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Testing
coverage/
.nyc_output/
# Misc
.cache/
temp/
tmp/

220
servers/airtable/README.md Normal file
View File

@ -0,0 +1,220 @@
# @mcpengine/airtable
Complete Airtable MCP Server providing full API integration for bases, tables, records, fields, views, webhooks, automations, and comments.
## Features
- **Bases**: List and get base information
- **Tables**: List tables, get table schema with all fields and views
- **Records**: Full CRUD operations with filtering, sorting, and pagination
- **Fields**: Access field definitions and types (all 34 field types supported)
- **Views**: List and access views (grid, form, calendar, gallery, kanban, timeline, gantt)
- **Webhooks**: Create, list, refresh, and delete webhooks
- **Comments**: Full comment management on records
- **Rate Limiting**: Automatic retry with exponential backoff (5 req/sec per base)
- **Pagination**: Offset-based for records, cursor-based for metadata
- **Batch Operations**: Create/update/delete up to 10 records per request
## Installation
```bash
npm install @mcpengine/airtable
```
## Configuration
### Environment Variables
Create a `.env` file:
```bash
AIRTABLE_API_KEY=your_api_key_here
```
Get your API key from: https://airtable.com/create/tokens
### MCP Settings
Add to your MCP settings file (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
```json
{
"mcpServers": {
"airtable": {
"command": "node",
"args": ["/path/to/@mcpengine/airtable/dist/main.js"],
"env": {
"AIRTABLE_API_KEY": "your_api_key_here"
}
}
}
}
```
## Usage
### List Bases
```typescript
// Returns all bases accessible with the API key
airtable_list_bases({ offset?: string })
```
### List Tables
```typescript
// Get all tables in a base with schema
airtable_list_tables({ baseId: "appXXXXXXXXXXXXXX" })
```
### List Records
```typescript
// List records with filtering, sorting, and pagination
airtable_list_records({
baseId: "appXXXXXXXXXXXXXX",
tableIdOrName: "tblXXXXXXXXXXXXXX" || "Table Name",
fields?: ["Field1", "Field2"],
filterByFormula?: "{Status} = 'Done'",
sort?: [{ field: "Name", direction: "asc" }],
maxRecords?: 100,
pageSize?: 100,
view?: "Grid view",
offset?: "itrXXXXXXXXXXXXXX/recXXXXXXXXXXXXXX"
})
```
### Create Records
```typescript
// Create up to 10 records at once
airtable_create_records({
baseId: "appXXXXXXXXXXXXXX",
tableIdOrName: "tblXXXXXXXXXXXXXX",
records: [
{ fields: { Name: "John Doe", Email: "john@example.com" } },
{ fields: { Name: "Jane Smith", Email: "jane@example.com" } }
],
typecast?: true // Auto-convert types
})
```
### Update Records
```typescript
// Update up to 10 records at once
airtable_update_records({
baseId: "appXXXXXXXXXXXXXX",
tableIdOrName: "tblXXXXXXXXXXXXXX",
records: [
{ id: "recXXXXXXXXXXXXXX", fields: { Status: "Complete" } }
],
typecast?: true
})
```
### Delete Records
```typescript
// Delete up to 10 records at once
airtable_delete_records({
baseId: "appXXXXXXXXXXXXXX",
tableIdOrName: "tblXXXXXXXXXXXXXX",
recordIds: ["recXXXXXXXXXXXXXX", "recYYYYYYYYYYYYYY"]
})
```
### Webhooks
```typescript
// Create a webhook
airtable_create_webhook({
baseId: "appXXXXXXXXXXXXXX",
notificationUrl: "https://your-server.com/webhook",
specification: {
options: {
filters: {
dataTypes: ["tableData"],
recordChangeScope: "tblXXXXXXXXXXXXXX"
}
}
}
})
// Refresh webhook (extends expiration)
airtable_refresh_webhook({
baseId: "appXXXXXXXXXXXXXX",
webhookId: "achXXXXXXXXXXXXXX"
})
```
### Comments
```typescript
// Create a comment on a record
airtable_create_comment({
baseId: "appXXXXXXXXXXXXXX",
tableIdOrName: "tblXXXXXXXXXXXXXX",
recordId: "recXXXXXXXXXXXXXX",
text: "This is a comment"
})
```
## Supported Field Types
All 34 Airtable field types are fully supported:
- **Text**: singleLineText, email, url, multilineText, richText, phoneNumber
- **Number**: number, percent, currency, rating, duration, autoNumber
- **Select**: singleSelect, multipleSelects
- **Date**: date, dateTime, createdTime, lastModifiedTime
- **Attachment**: multipleAttachments
- **Link**: multipleRecordLinks
- **User**: singleCollaborator, multipleCollaborators, createdBy, lastModifiedBy
- **Computed**: formula, rollup, count, lookup, multipleLookupValues
- **Other**: checkbox, barcode, button, externalSyncSource, aiText
## API Limits
- **Rate Limit**: 5 requests per second per base
- **Batch Create/Update**: Max 10 records per request
- **Batch Delete**: Max 10 record IDs per request
- **Page Size**: Max 100 records per page
Rate limiting is handled automatically with exponential backoff.
## Development
```bash
# Install dependencies
npm install
# Type check
npm run typecheck
# Build
npm run build
# Run
npm start
```
## Architecture
```
src/
├── types/index.ts # All Airtable API types (bases, tables, records, fields, etc.)
├── clients/airtable.ts # API client with rate limiting and pagination
├── server.ts # MCP server with lazy-loaded tools
└── main.ts # Entry point with dual transport support
```
## License
MIT
## Resources
- [Airtable API Documentation](https://airtable.com/developers/web/api/introduction)
- [Model Context Protocol](https://modelcontextprotocol.io)
- [Create API Token](https://airtable.com/create/tokens)

View File

@ -0,0 +1,32 @@
{
"name": "@mcpengine/airtable",
"version": "1.0.0",
"description": "Airtable MCP Server - Complete API integration for bases, tables, records, fields, views, webhooks, and automations",
"type": "module",
"main": "dist/main.js",
"bin": {
"mcp-airtable": "./dist/main.js"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/main.js",
"typecheck": "tsc --noEmit"
},
"keywords": [
"mcp",
"airtable",
"model-context-protocol"
],
"author": "MCPEngine",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"axios": "^1.7.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.6.0"
}
}

View File

@ -0,0 +1,438 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
import type {
Base,
BaseId,
Table,
TableId,
AirtableRecord,
RecordId,
RecordFields,
Field,
View,
ViewId,
Webhook,
WebhookId,
Automation,
Comment,
FilterFormula,
SortConfig,
ListBasesResponse,
ListTablesResponse,
ListRecordsResponse,
CreateRecordsResponse,
UpdateRecordsResponse,
DeleteRecordsResponse,
} from '../types/index.js';
export interface AirtableClientConfig {
apiKey: string;
baseUrl?: string;
metaBaseUrl?: string;
maxRetries?: number;
retryDelayMs?: number;
}
export interface ListRecordsOptions {
fields?: string[];
filterByFormula?: FilterFormula;
maxRecords?: number;
pageSize?: number;
sort?: SortConfig[];
view?: string;
cellFormat?: 'json' | 'string';
timeZone?: string;
userLocale?: string;
offset?: string;
returnFieldsByFieldId?: boolean;
}
export interface CreateRecordOptions {
fields: RecordFields;
typecast?: boolean;
}
export interface UpdateRecordOptions {
fields: RecordFields;
typecast?: boolean;
}
export interface DeleteRecordsResult {
records: Array<{ id: RecordId; deleted: boolean }>;
}
export class AirtableError extends Error {
constructor(
message: string,
public statusCode?: number,
public response?: unknown
) {
super(message);
this.name = 'AirtableError';
}
}
/**
* Airtable API Client
*
* Official API Documentation: https://airtable.com/developers/web/api/introduction
*
* Rate Limits:
* - 5 requests per second per base
* - Implement exponential backoff for 429 responses
*
* Base URLs:
* - Records API: https://api.airtable.com/v0
* - Meta API: https://api.airtable.com/v0/meta
*
* Pagination:
* - Records: offset-based (include `offset` from response in next request)
* - Meta endpoints: cursor-based (vary by endpoint)
*
* Batch Limits:
* - Create/Update: max 10 records per request
* - Delete: max 10 record IDs per request
*/
export class AirtableClient {
private client: AxiosInstance;
private metaClient: AxiosInstance;
private maxRetries: number;
private retryDelayMs: number;
constructor(config: AirtableClientConfig) {
const baseUrl = config.baseUrl || 'https://api.airtable.com/v0';
const metaBaseUrl = config.metaBaseUrl || 'https://api.airtable.com/v0/meta';
this.maxRetries = config.maxRetries || 3;
this.retryDelayMs = config.retryDelayMs || 1000;
this.client = axios.create({
baseURL: baseUrl,
headers: {
Authorization: `Bearer ${config.apiKey}`,
'Content-Type': 'application/json',
},
timeout: 30000,
});
this.metaClient = axios.create({
baseURL: metaBaseUrl,
headers: {
Authorization: `Bearer ${config.apiKey}`,
'Content-Type': 'application/json',
},
timeout: 30000,
});
this.setupInterceptors();
}
private setupInterceptors(): void {
const retryInterceptor = async (error: AxiosError) => {
const config = error.config as any;
if (!config || !error.response) {
throw error;
}
// Handle rate limiting (429)
if (error.response.status === 429) {
const retryCount = config.__retryCount || 0;
if (retryCount < this.maxRetries) {
config.__retryCount = retryCount + 1;
const delay = this.retryDelayMs * Math.pow(2, retryCount);
await new Promise((resolve) => setTimeout(resolve, delay));
return this.client.request(config);
}
}
throw this.handleError(error);
};
this.client.interceptors.response.use(
(response) => response,
retryInterceptor
);
this.metaClient.interceptors.response.use(
(response) => response,
retryInterceptor
);
}
private handleError(error: AxiosError): AirtableError {
if (error.response) {
const data = error.response.data as any;
const message = data?.error?.message || data?.message || 'Airtable API error';
return new AirtableError(message, error.response.status, data);
}
return new AirtableError(error.message);
}
// ============================================================================
// Bases API
// ============================================================================
async listBases(offset?: string): Promise<ListBasesResponse> {
const params: Record<string, string> = {};
if (offset) params.offset = offset;
const response = await this.metaClient.get<ListBasesResponse>('/bases', { params });
return response.data;
}
async getBase(baseId: BaseId): Promise<Base> {
const response = await this.metaClient.get<{ id: string; name: string; permissionLevel: string }>(
`/bases/${baseId}`
);
return response.data as Base;
}
// ============================================================================
// Tables API (Meta)
// ============================================================================
async listTables(baseId: BaseId): Promise<ListTablesResponse> {
const response = await this.metaClient.get<ListTablesResponse>(`/bases/${baseId}/tables`);
return response.data;
}
async getTable(baseId: BaseId, tableId: TableId): Promise<Table> {
const response = await this.metaClient.get<Table>(`/bases/${baseId}/tables/${tableId}`);
return response.data;
}
// ============================================================================
// Fields API (Meta)
// ============================================================================
async listFields(baseId: BaseId, tableId: TableId): Promise<Field[]> {
const table = await this.getTable(baseId, tableId);
return table.fields;
}
async getField(baseId: BaseId, tableId: TableId, fieldId: string): Promise<Field> {
const fields = await this.listFields(baseId, tableId);
const field = fields.find((f) => f.id === fieldId);
if (!field) {
throw new AirtableError(`Field ${fieldId} not found in table ${tableId}`);
}
return field;
}
// ============================================================================
// Views API (Meta)
// ============================================================================
async listViews(baseId: BaseId, tableId: TableId): Promise<View[]> {
const table = await this.getTable(baseId, tableId);
return table.views;
}
async getView(baseId: BaseId, tableId: TableId, viewId: ViewId): Promise<View> {
const views = await this.listViews(baseId, tableId);
const view = views.find((v) => v.id === viewId);
if (!view) {
throw new AirtableError(`View ${viewId} not found in table ${tableId}`);
}
return view;
}
// ============================================================================
// Records API
// ============================================================================
async listRecords(
baseId: BaseId,
tableIdOrName: string,
options?: ListRecordsOptions
): Promise<ListRecordsResponse> {
const params: Record<string, unknown> = {};
if (options?.fields) params.fields = options.fields;
if (options?.filterByFormula) params.filterByFormula = options.filterByFormula;
if (options?.maxRecords) params.maxRecords = options.maxRecords;
if (options?.pageSize) params.pageSize = options.pageSize;
if (options?.view) params.view = options.view;
if (options?.cellFormat) params.cellFormat = options.cellFormat;
if (options?.timeZone) params.timeZone = options.timeZone;
if (options?.userLocale) params.userLocale = options.userLocale;
if (options?.offset) params.offset = options.offset;
if (options?.returnFieldsByFieldId) params.returnFieldsByFieldId = options.returnFieldsByFieldId;
if (options?.sort) {
options.sort.forEach((sortItem, index) => {
params[`sort[${index}][field]`] = sortItem.field;
params[`sort[${index}][direction]`] = sortItem.direction;
});
}
const response = await this.client.get<ListRecordsResponse>(`/${baseId}/${encodeURIComponent(tableIdOrName)}`, {
params,
});
return response.data;
}
async getRecord(baseId: BaseId, tableIdOrName: string, recordId: RecordId): Promise<AirtableRecord> {
const response = await this.client.get<AirtableRecord>(
`/${baseId}/${encodeURIComponent(tableIdOrName)}/${recordId}`
);
return response.data;
}
/**
* Create records (max 10 per request)
*/
async createRecords(
baseId: BaseId,
tableIdOrName: string,
records: CreateRecordOptions[],
typecast?: boolean
): Promise<CreateRecordsResponse> {
if (records.length > 10) {
throw new AirtableError('Cannot create more than 10 records per request');
}
const payload: Record<string, unknown> = {
records: records.map((r) => ({ fields: r.fields })),
};
if (typecast !== undefined) {
payload.typecast = typecast;
}
const response = await this.client.post<CreateRecordsResponse>(
`/${baseId}/${encodeURIComponent(tableIdOrName)}`,
payload
);
return response.data;
}
/**
* Update records (max 10 per request)
*/
async updateRecords(
baseId: BaseId,
tableIdOrName: string,
records: Array<{ id: RecordId } & UpdateRecordOptions>,
typecast?: boolean
): Promise<UpdateRecordsResponse> {
if (records.length > 10) {
throw new AirtableError('Cannot update more than 10 records per request');
}
const payload: Record<string, unknown> = {
records: records.map((r) => ({ id: r.id, fields: r.fields })),
};
if (typecast !== undefined) {
payload.typecast = typecast;
}
const response = await this.client.patch<UpdateRecordsResponse>(
`/${baseId}/${encodeURIComponent(tableIdOrName)}`,
payload
);
return response.data;
}
/**
* Delete records (max 10 per request)
*/
async deleteRecords(
baseId: BaseId,
tableIdOrName: string,
recordIds: RecordId[]
): Promise<DeleteRecordsResult> {
if (recordIds.length > 10) {
throw new AirtableError('Cannot delete more than 10 records per request');
}
const params = recordIds.map((id) => `records[]=${id}`).join('&');
const response = await this.client.delete<DeleteRecordsResult>(
`/${baseId}/${encodeURIComponent(tableIdOrName)}?${params}`
);
return response.data;
}
// ============================================================================
// Webhooks API
// ============================================================================
async listWebhooks(baseId: BaseId): Promise<Webhook[]> {
const response = await this.client.get<{ webhooks: Webhook[] }>(`/${baseId}/webhooks`);
return response.data.webhooks;
}
async createWebhook(baseId: BaseId, notificationUrl: string, specification: unknown): Promise<Webhook> {
const response = await this.client.post<Webhook>(`/${baseId}/webhooks`, {
notificationUrl,
specification,
});
return response.data;
}
async deleteWebhook(baseId: BaseId, webhookId: WebhookId): Promise<void> {
await this.client.delete(`/${baseId}/webhooks/${webhookId}`);
}
async refreshWebhook(baseId: BaseId, webhookId: WebhookId): Promise<Webhook> {
const response = await this.client.post<Webhook>(`/${baseId}/webhooks/${webhookId}/refresh`);
return response.data;
}
// ============================================================================
// Comments API
// ============================================================================
async listComments(baseId: BaseId, tableIdOrName: string, recordId: RecordId): Promise<Comment[]> {
const response = await this.client.get<{ comments: Comment[] }>(
`/${baseId}/${encodeURIComponent(tableIdOrName)}/${recordId}/comments`
);
return response.data.comments;
}
async createComment(baseId: BaseId, tableIdOrName: string, recordId: RecordId, text: string): Promise<Comment> {
const response = await this.client.post<Comment>(
`/${baseId}/${encodeURIComponent(tableIdOrName)}/${recordId}/comments`,
{ text }
);
return response.data;
}
async updateComment(
baseId: BaseId,
tableIdOrName: string,
recordId: RecordId,
commentId: string,
text: string
): Promise<Comment> {
const response = await this.client.patch<Comment>(
`/${baseId}/${encodeURIComponent(tableIdOrName)}/${recordId}/comments/${commentId}`,
{ text }
);
return response.data;
}
async deleteComment(
baseId: BaseId,
tableIdOrName: string,
recordId: RecordId,
commentId: string
): Promise<void> {
await this.client.delete(`/${baseId}/${encodeURIComponent(tableIdOrName)}/${recordId}/comments/${commentId}`);
}
// ============================================================================
// Automations API (Read-only for now)
// ============================================================================
/**
* Note: Airtable's API does not currently provide direct automation management.
* This is a placeholder for future functionality or webhook-based automation triggers.
*/
async listAutomations(baseId: BaseId): Promise<Automation[]> {
throw new AirtableError('Automations API not yet supported by Airtable');
}
}

View File

@ -0,0 +1,50 @@
#!/usr/bin/env node
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { AirtableServer } from './server.js';
async function main() {
const apiKey = process.env.AIRTABLE_API_KEY;
if (!apiKey) {
console.error('Error: AIRTABLE_API_KEY environment variable is required');
console.error('Please set your Airtable API key:');
console.error(' export AIRTABLE_API_KEY=your_api_key_here');
process.exit(1);
}
const server = new AirtableServer({
apiKey,
serverName: '@mcpengine/airtable',
serverVersion: '1.0.0',
});
const transport = new StdioServerTransport();
// Graceful shutdown
const cleanup = async () => {
console.error('Shutting down Airtable MCP server...');
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
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);
});
await server.connect(transport);
console.error('Airtable MCP server running on stdio');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

View File

@ -0,0 +1,690 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { AirtableClient } from './clients/airtable.js';
import { z } from 'zod';
export interface AirtableServerConfig {
apiKey: string;
serverName?: string;
serverVersion?: string;
}
export class AirtableServer {
private server: Server;
private client: AirtableClient;
constructor(config: AirtableServerConfig) {
this.client = new AirtableClient({
apiKey: config.apiKey,
});
this.server = new Server(
{
name: config.serverName || '@mcpengine/airtable',
version: config.serverVersion || '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: this.getTools(),
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await this.handleToolCall(name, args || {});
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({ error: errorMessage }, null, 2),
},
],
isError: true,
};
}
});
}
private getTools(): Tool[] {
return [
// ========================================================================
// Bases
// ========================================================================
{
name: 'airtable_list_bases',
description: 'List all bases accessible with the API key. Supports pagination with offset.',
inputSchema: {
type: 'object',
properties: {
offset: {
type: 'string',
description: 'Pagination offset from previous response',
},
},
},
},
{
name: 'airtable_get_base',
description: 'Get details of a specific base by ID',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
},
required: ['baseId'],
},
},
// ========================================================================
// Tables
// ========================================================================
{
name: 'airtable_list_tables',
description: 'List all tables in a base with their schema (fields, views)',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
},
required: ['baseId'],
},
},
{
name: 'airtable_get_table',
description: 'Get detailed information about a specific table including all fields and views',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableId: {
type: 'string',
description: 'The table ID (starts with tbl)',
},
},
required: ['baseId', 'tableId'],
},
},
// ========================================================================
// Records
// ========================================================================
{
name: 'airtable_list_records',
description:
'List records from a table. Supports filtering, sorting, pagination, and field selection. Use filterByFormula for advanced filtering.',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableIdOrName: {
type: 'string',
description: 'Table ID (starts with tbl) or table name',
},
fields: {
type: 'array',
items: { type: 'string' },
description: 'Only return specific fields',
},
filterByFormula: {
type: 'string',
description: 'Airtable formula to filter records (e.g., "{Status} = \'Done\'")',
},
maxRecords: {
type: 'number',
description: 'Maximum number of records to return (default: all)',
},
pageSize: {
type: 'number',
description: 'Number of records per page (max 100, default 100)',
},
sort: {
type: 'array',
items: {
type: 'object',
properties: {
field: { type: 'string' },
direction: { type: 'string', enum: ['asc', 'desc'] },
},
required: ['field', 'direction'],
},
description: 'Sort configuration',
},
view: {
type: 'string',
description: 'View name or ID to use for filtering/sorting',
},
offset: {
type: 'string',
description: 'Pagination offset from previous response',
},
},
required: ['baseId', 'tableIdOrName'],
},
},
{
name: 'airtable_get_record',
description: 'Get a specific record by ID',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableIdOrName: {
type: 'string',
description: 'Table ID (starts with tbl) or table name',
},
recordId: {
type: 'string',
description: 'The record ID (starts with rec)',
},
},
required: ['baseId', 'tableIdOrName', 'recordId'],
},
},
{
name: 'airtable_create_records',
description: 'Create new records (max 10 per request). Use typecast to enable automatic type conversion.',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableIdOrName: {
type: 'string',
description: 'Table ID (starts with tbl) or table name',
},
records: {
type: 'array',
items: {
type: 'object',
properties: {
fields: {
type: 'object',
description: 'Field name to value mapping',
},
},
required: ['fields'],
},
description: 'Records to create (max 10)',
},
typecast: {
type: 'boolean',
description: 'Enable automatic type conversion (default: false)',
},
},
required: ['baseId', 'tableIdOrName', 'records'],
},
},
{
name: 'airtable_update_records',
description: 'Update existing records (max 10 per request). Use typecast to enable automatic type conversion.',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableIdOrName: {
type: 'string',
description: 'Table ID (starts with tbl) or table name',
},
records: {
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Record ID (starts with rec)',
},
fields: {
type: 'object',
description: 'Field name to value mapping',
},
},
required: ['id', 'fields'],
},
description: 'Records to update (max 10)',
},
typecast: {
type: 'boolean',
description: 'Enable automatic type conversion (default: false)',
},
},
required: ['baseId', 'tableIdOrName', 'records'],
},
},
{
name: 'airtable_delete_records',
description: 'Delete records (max 10 per request)',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableIdOrName: {
type: 'string',
description: 'Table ID (starts with tbl) or table name',
},
recordIds: {
type: 'array',
items: { type: 'string' },
description: 'Record IDs to delete (max 10)',
},
},
required: ['baseId', 'tableIdOrName', 'recordIds'],
},
},
// ========================================================================
// Fields
// ========================================================================
{
name: 'airtable_list_fields',
description: 'List all fields in a table with their types and configuration',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableId: {
type: 'string',
description: 'The table ID (starts with tbl)',
},
},
required: ['baseId', 'tableId'],
},
},
{
name: 'airtable_get_field',
description: 'Get details of a specific field',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableId: {
type: 'string',
description: 'The table ID (starts with tbl)',
},
fieldId: {
type: 'string',
description: 'The field ID (starts with fld)',
},
},
required: ['baseId', 'tableId', 'fieldId'],
},
},
// ========================================================================
// Views
// ========================================================================
{
name: 'airtable_list_views',
description: 'List all views in a table',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableId: {
type: 'string',
description: 'The table ID (starts with tbl)',
},
},
required: ['baseId', 'tableId'],
},
},
{
name: 'airtable_get_view',
description: 'Get details of a specific view',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableId: {
type: 'string',
description: 'The table ID (starts with tbl)',
},
viewId: {
type: 'string',
description: 'The view ID (starts with viw)',
},
},
required: ['baseId', 'tableId', 'viewId'],
},
},
// ========================================================================
// Webhooks
// ========================================================================
{
name: 'airtable_list_webhooks',
description: 'List all webhooks configured for a base',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
},
required: ['baseId'],
},
},
{
name: 'airtable_create_webhook',
description: 'Create a new webhook for a base',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
notificationUrl: {
type: 'string',
description: 'URL to receive webhook notifications',
},
specification: {
type: 'object',
description: 'Webhook specification (filters, includes)',
},
},
required: ['baseId', 'notificationUrl', 'specification'],
},
},
{
name: 'airtable_delete_webhook',
description: 'Delete a webhook',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
webhookId: {
type: 'string',
description: 'The webhook ID',
},
},
required: ['baseId', 'webhookId'],
},
},
{
name: 'airtable_refresh_webhook',
description: 'Refresh a webhook to extend its expiration time',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
webhookId: {
type: 'string',
description: 'The webhook ID',
},
},
required: ['baseId', 'webhookId'],
},
},
// ========================================================================
// Comments
// ========================================================================
{
name: 'airtable_list_comments',
description: 'List all comments on a record',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableIdOrName: {
type: 'string',
description: 'Table ID (starts with tbl) or table name',
},
recordId: {
type: 'string',
description: 'The record ID (starts with rec)',
},
},
required: ['baseId', 'tableIdOrName', 'recordId'],
},
},
{
name: 'airtable_create_comment',
description: 'Create a comment on a record',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableIdOrName: {
type: 'string',
description: 'Table ID (starts with tbl) or table name',
},
recordId: {
type: 'string',
description: 'The record ID (starts with rec)',
},
text: {
type: 'string',
description: 'Comment text',
},
},
required: ['baseId', 'tableIdOrName', 'recordId', 'text'],
},
},
{
name: 'airtable_update_comment',
description: 'Update an existing comment',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableIdOrName: {
type: 'string',
description: 'Table ID (starts with tbl) or table name',
},
recordId: {
type: 'string',
description: 'The record ID (starts with rec)',
},
commentId: {
type: 'string',
description: 'The comment ID',
},
text: {
type: 'string',
description: 'New comment text',
},
},
required: ['baseId', 'tableIdOrName', 'recordId', 'commentId', 'text'],
},
},
{
name: 'airtable_delete_comment',
description: 'Delete a comment',
inputSchema: {
type: 'object',
properties: {
baseId: {
type: 'string',
description: 'The base ID (starts with app)',
},
tableIdOrName: {
type: 'string',
description: 'Table ID (starts with tbl) or table name',
},
recordId: {
type: 'string',
description: 'The record ID (starts with rec)',
},
commentId: {
type: 'string',
description: 'The comment ID',
},
},
required: ['baseId', 'tableIdOrName', 'recordId', 'commentId'],
},
},
];
}
private async handleToolCall(name: string, args: Record<string, unknown>): Promise<unknown> {
switch (name) {
// Bases
case 'airtable_list_bases':
return this.client.listBases(args.offset as string | undefined);
case 'airtable_get_base':
return this.client.getBase(args.baseId as any);
// Tables
case 'airtable_list_tables':
return this.client.listTables(args.baseId as any);
case 'airtable_get_table':
return this.client.getTable(args.baseId as any, args.tableId as any);
// Records
case 'airtable_list_records':
return this.client.listRecords(args.baseId as any, args.tableIdOrName as string, args as any);
case 'airtable_get_record':
return this.client.getRecord(args.baseId as any, args.tableIdOrName as string, args.recordId as any);
case 'airtable_create_records':
return this.client.createRecords(
args.baseId as any,
args.tableIdOrName as string,
args.records as any[],
args.typecast as boolean | undefined
);
case 'airtable_update_records':
return this.client.updateRecords(
args.baseId as any,
args.tableIdOrName as string,
args.records as any[],
args.typecast as boolean | undefined
);
case 'airtable_delete_records':
return this.client.deleteRecords(args.baseId as any, args.tableIdOrName as string, args.recordIds as any[]);
// Fields
case 'airtable_list_fields':
return this.client.listFields(args.baseId as any, args.tableId as any);
case 'airtable_get_field':
return this.client.getField(args.baseId as any, args.tableId as any, args.fieldId as string);
// Views
case 'airtable_list_views':
return this.client.listViews(args.baseId as any, args.tableId as any);
case 'airtable_get_view':
return this.client.getView(args.baseId as any, args.tableId as any, args.viewId as any);
// Webhooks
case 'airtable_list_webhooks':
return this.client.listWebhooks(args.baseId as any);
case 'airtable_create_webhook':
return this.client.createWebhook(args.baseId as any, args.notificationUrl as string, args.specification);
case 'airtable_delete_webhook':
await this.client.deleteWebhook(args.baseId as any, args.webhookId as any);
return { success: true };
case 'airtable_refresh_webhook':
return this.client.refreshWebhook(args.baseId as any, args.webhookId as any);
// Comments
case 'airtable_list_comments':
return this.client.listComments(args.baseId as any, args.tableIdOrName as string, args.recordId as any);
case 'airtable_create_comment':
return this.client.createComment(
args.baseId as any,
args.tableIdOrName as string,
args.recordId as any,
args.text as string
);
case 'airtable_update_comment':
return this.client.updateComment(
args.baseId as any,
args.tableIdOrName as string,
args.recordId as any,
args.commentId as string,
args.text as string
);
case 'airtable_delete_comment':
await this.client.deleteComment(
args.baseId as any,
args.tableIdOrName as string,
args.recordId as any,
args.commentId as string
);
return { success: true };
default:
throw new Error(`Unknown tool: ${name}`);
}
}
async connect(transport: StdioServerTransport): Promise<void> {
await this.server.connect(transport);
}
getServer(): Server {
return this.server;
}
}

View File

@ -0,0 +1,563 @@
import { z } from 'zod';
// ============================================================================
// Branded ID Types
// ============================================================================
export type BaseId = string & { readonly __brand: 'BaseId' };
export type TableId = string & { readonly __brand: 'TableId' };
export type RecordId = string & { readonly __brand: 'RecordId' };
export type FieldId = string & { readonly __brand: 'FieldId' };
export type ViewId = string & { readonly __brand: 'ViewId' };
export type WebhookId = string & { readonly __brand: 'WebhookId' };
export type AutomationId = string & { readonly __brand: 'AutomationId' };
export const BaseIdSchema = z.string().transform((v) => v as BaseId);
export const TableIdSchema = z.string().transform((v) => v as TableId);
export const RecordIdSchema = z.string().transform((v) => v as RecordId);
export const FieldIdSchema = z.string().transform((v) => v as FieldId);
export const ViewIdSchema = z.string().transform((v) => v as ViewId);
export const WebhookIdSchema = z.string().transform((v) => v as WebhookId);
export const AutomationIdSchema = z.string().transform((v) => v as AutomationId);
// ============================================================================
// Field Types
// ============================================================================
export type FieldType =
| 'singleLineText'
| 'email'
| 'url'
| 'multilineText'
| 'number'
| 'percent'
| 'currency'
| 'singleSelect'
| 'multipleSelects'
| 'singleCollaborator'
| 'multipleCollaborators'
| 'multipleRecordLinks'
| 'date'
| 'dateTime'
| 'phoneNumber'
| 'multipleAttachments'
| 'checkbox'
| 'formula'
| 'createdTime'
| 'rollup'
| 'count'
| 'lookup'
| 'multipleLookupValues'
| 'autoNumber'
| 'barcode'
| 'rating'
| 'richText'
| 'duration'
| 'lastModifiedTime'
| 'button'
| 'createdBy'
| 'lastModifiedBy'
| 'externalSyncSource'
| 'aiText';
export const FieldTypeSchema = z.enum([
'singleLineText',
'email',
'url',
'multilineText',
'number',
'percent',
'currency',
'singleSelect',
'multipleSelects',
'singleCollaborator',
'multipleCollaborators',
'multipleRecordLinks',
'date',
'dateTime',
'phoneNumber',
'multipleAttachments',
'checkbox',
'formula',
'createdTime',
'rollup',
'count',
'lookup',
'multipleLookupValues',
'autoNumber',
'barcode',
'rating',
'richText',
'duration',
'lastModifiedTime',
'button',
'createdBy',
'lastModifiedBy',
'externalSyncSource',
'aiText',
]);
// ============================================================================
// Field Configuration
// ============================================================================
export interface SelectOption {
id: string;
name: string;
color?: string;
}
export interface Collaborator {
id: string;
email: string;
name?: string;
}
export interface CurrencyOptions {
precision: number;
symbol: string;
}
export interface NumberOptions {
precision: number;
}
export interface PercentOptions {
precision: number;
}
export interface RatingOptions {
icon: string;
max: number;
color?: string;
}
export interface DurationOptions {
durationFormat: 'h:mm' | 'h:mm:ss' | 'h:mm:ss.S' | 'h:mm:ss.SS' | 'h:mm:ss.SSS';
}
export interface DateOptions {
dateFormat: {
name: 'local' | 'friendly' | 'us' | 'european' | 'iso';
format: string;
};
}
export interface DateTimeOptions {
dateFormat: {
name: 'local' | 'friendly' | 'us' | 'european' | 'iso';
format: string;
};
timeFormat: {
name: '12hour' | '24hour';
format: string;
};
timeZone: string;
}
export interface Field {
id: FieldId;
name: string;
type: FieldType;
description?: string;
options?: {
choices?: SelectOption[];
linkedTableId?: TableId;
prefersSingleRecordLink?: boolean;
inverseLinkFieldId?: FieldId;
isReversed?: boolean;
precision?: number;
symbol?: string;
icon?: string;
max?: number;
color?: string;
durationFormat?: string;
dateFormat?: unknown;
timeFormat?: unknown;
timeZone?: string;
result?: unknown;
formula?: string;
recordLinkFieldId?: FieldId;
fieldIdInLinkedTable?: FieldId;
referencedFieldIds?: FieldId[];
};
}
export const FieldSchema = z.object({
id: FieldIdSchema,
name: z.string(),
type: FieldTypeSchema,
description: z.string().optional(),
options: z.record(z.unknown()).optional(),
});
// ============================================================================
// Attachments and Thumbnails
// ============================================================================
export interface Thumbnail {
url: string;
width: number;
height: number;
}
export interface Attachment {
id: string;
url: string;
filename: string;
size: number;
type: string;
width?: number;
height?: number;
thumbnails?: {
small?: Thumbnail;
large?: Thumbnail;
full?: Thumbnail;
};
}
export const ThumbnailSchema = z.object({
url: z.string(),
width: z.number(),
height: z.number(),
});
export const AttachmentSchema = z.object({
id: z.string(),
url: z.string(),
filename: z.string(),
size: z.number(),
type: z.string(),
width: z.number().optional(),
height: z.number().optional(),
thumbnails: z
.object({
small: ThumbnailSchema.optional(),
large: ThumbnailSchema.optional(),
full: ThumbnailSchema.optional(),
})
.optional(),
});
// ============================================================================
// Records
// ============================================================================
export type RecordFields = globalThis.Record<string, unknown>;
export interface AirtableRecord {
id: RecordId;
createdTime: string;
fields: RecordFields;
}
export const AirtableRecordSchema = z.object({
id: RecordIdSchema,
createdTime: z.string(),
fields: z.record(z.unknown()),
});
// ============================================================================
// Tables
// ============================================================================
export interface Table {
id: TableId;
name: string;
description?: string;
primaryFieldId: FieldId;
fields: Field[];
views: View[];
}
export const TableSchema = z.object({
id: TableIdSchema,
name: z.string(),
description: z.string().optional(),
primaryFieldId: FieldIdSchema,
fields: z.array(FieldSchema),
views: z.array(z.lazy(() => ViewSchema)),
});
// ============================================================================
// Views
// ============================================================================
export type ViewType = 'grid' | 'form' | 'calendar' | 'gallery' | 'kanban' | 'timeline' | 'gantt';
export const ViewTypeSchema = z.enum(['grid', 'form', 'calendar', 'gallery', 'kanban', 'timeline', 'gantt']);
export interface View {
id: ViewId;
name: string;
type: ViewType;
}
export const ViewSchema = z.object({
id: ViewIdSchema,
name: z.string(),
type: ViewTypeSchema,
});
// ============================================================================
// Bases
// ============================================================================
export interface Base {
id: BaseId;
name: string;
permissionLevel: 'none' | 'read' | 'comment' | 'edit' | 'create';
}
export const BaseSchema = z.object({
id: BaseIdSchema,
name: z.string(),
permissionLevel: z.enum(['none', 'read', 'comment', 'edit', 'create']),
});
// ============================================================================
// Webhooks
// ============================================================================
export interface WebhookPayload {
baseTransactionNumber?: number;
timestamp?: string;
actionMetadata?: {
source: string;
sourceMetadata?: unknown;
};
changedTablesById?: globalThis.Record<
string,
{
createdRecordsById?: globalThis.Record<string, unknown>;
changedRecordsById?: globalThis.Record<string, unknown>;
destroyedRecordIds?: string[];
createdFieldsById?: globalThis.Record<string, unknown>;
changedFieldsById?: globalThis.Record<string, unknown>;
destroyedFieldIds?: string[];
changedViewsById?: globalThis.Record<string, unknown>;
createdViewsById?: globalThis.Record<string, unknown>;
destroyedViewIds?: string[];
changedMetadata?: unknown;
}
>;
}
export interface Webhook {
id: WebhookId;
macSecretBase64?: string;
expirationTime?: string;
specification?: {
options: {
filters: {
dataTypes: Array<'tableData' | 'tableFields' | 'tableMetadata'>;
recordChangeScope?: string;
watchDataInFieldIds?: string[];
watchSchemasOfFieldIds?: string[];
sourceOptions?: unknown;
};
includes?: {
includeCellValuesInFieldIds?: string[] | 'all';
includePreviousCellValues?: boolean;
includePreviousFieldDefinitions?: boolean;
};
};
};
}
export const WebhookSchema = z.object({
id: WebhookIdSchema,
macSecretBase64: z.string().optional(),
expirationTime: z.string().optional(),
specification: z
.object({
options: z.object({
filters: z.object({
dataTypes: z.array(z.enum(['tableData', 'tableFields', 'tableMetadata'])),
recordChangeScope: z.string().optional(),
watchDataInFieldIds: z.array(z.string()).optional(),
watchSchemasOfFieldIds: z.array(z.string()).optional(),
sourceOptions: z.unknown().optional(),
}),
includes: z
.object({
includeCellValuesInFieldIds: z.union([z.array(z.string()), z.literal('all')]).optional(),
includePreviousCellValues: z.boolean().optional(),
includePreviousFieldDefinitions: z.boolean().optional(),
})
.optional(),
}),
})
.optional(),
});
// ============================================================================
// Automations
// ============================================================================
export interface AutomationTrigger {
type: string;
config: unknown;
}
export interface AutomationAction {
type: string;
config: unknown;
}
export interface Automation {
id: AutomationId;
name: string;
state: 'active' | 'disabled';
trigger: AutomationTrigger;
actions: AutomationAction[];
createdTime: string;
}
export const AutomationSchema = z.object({
id: AutomationIdSchema,
name: z.string(),
state: z.enum(['active', 'disabled']),
trigger: z.object({
type: z.string(),
config: z.unknown(),
}),
actions: z.array(
z.object({
type: z.string(),
config: z.unknown(),
})
),
createdTime: z.string(),
});
// ============================================================================
// Comments
// ============================================================================
export interface Comment {
id: string;
text: string;
createdTime: string;
author: {
id: string;
email: string;
name?: string;
};
lastUpdatedTime?: string;
mentioned?: globalThis.Record<string, unknown>;
}
export const CommentSchema = z.object({
id: z.string(),
text: z.string(),
createdTime: z.string(),
author: z.object({
id: z.string(),
email: z.string(),
name: z.string().optional(),
}),
lastUpdatedTime: z.string().optional(),
mentioned: z.record(z.unknown()).optional(),
});
// ============================================================================
// Pagination
// ============================================================================
export interface RecordsPagination {
offset?: string;
}
export interface MetaPagination {
cursor?: string;
}
export const RecordsPaginationSchema = z.object({
offset: z.string().optional(),
});
export const MetaPaginationSchema = z.object({
cursor: z.string().optional(),
});
// ============================================================================
// Filtering and Sorting
// ============================================================================
export type FilterFormula = string;
export interface SortConfig {
field: string;
direction: 'asc' | 'desc';
}
export const SortConfigSchema = z.object({
field: z.string(),
direction: z.enum(['asc', 'desc']),
});
// ============================================================================
// API Response Types
// ============================================================================
export interface ListBasesResponse {
bases: Base[];
offset?: string;
}
export interface ListTablesResponse {
tables: Table[];
}
export interface ListRecordsResponse {
records: AirtableRecord[];
offset?: string;
}
export interface CreateRecordsResponse {
records: AirtableRecord[];
createdRecords?: AirtableRecord[];
}
export interface UpdateRecordsResponse {
records: AirtableRecord[];
updatedRecords?: AirtableRecord[];
}
export interface DeleteRecordsResponse {
records: Array<{ id: RecordId; deleted: boolean }>;
}
export const ListBasesResponseSchema = z.object({
bases: z.array(BaseSchema),
offset: z.string().optional(),
});
export const ListTablesResponseSchema = z.object({
tables: z.array(TableSchema),
});
export const ListRecordsResponseSchema = z.object({
records: z.array(AirtableRecordSchema),
offset: z.string().optional(),
});
export const CreateRecordsResponseSchema = z.object({
records: z.array(AirtableRecordSchema),
createdRecords: z.array(AirtableRecordSchema).optional(),
});
export const UpdateRecordsResponseSchema = z.object({
records: z.array(AirtableRecordSchema),
updatedRecords: z.array(AirtableRecordSchema).optional(),
});
export const DeleteRecordsResponseSchema = z.object({
records: z.array(
z.object({
id: RecordIdSchema,
deleted: z.boolean(),
})
),
});

View File

@ -0,0 +1,22 @@
{
"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",
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,3 @@
# Intercom Access Token (required)
# Get your access token from: https://app.intercom.com/a/apps/_/settings/developer-hub
INTERCOM_ACCESS_TOKEN=your_access_token_here

29
servers/intercom/.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
# Dependencies
node_modules/
# Build output
dist/
# Environment variables
.env
.env.local
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# TypeScript
*.tsbuildinfo

168
servers/intercom/README.md Normal file
View File

@ -0,0 +1,168 @@
# @mcpengine/intercom
Model Context Protocol (MCP) server for Intercom API integration.
## Features
- ✅ **Contacts** - Create, read, update, delete, search, and list contacts
- ✅ **Conversations** - Create, reply, assign, close, search conversations
- ✅ **Companies** - Manage companies and their relationships with contacts
- ✅ **Articles** - Create and manage help center articles
- ✅ **Help Center** - Collections, sections, and help center management
- ✅ **Tickets** - Create, update, search tickets and ticket types
- ✅ **Tags** - Create, list, and delete tags
- ✅ **Segments** - List and retrieve segments
- ✅ **Events** - Submit custom events
- ✅ **Messages** - Send in-app, email, and push messages
- ✅ **Teams & Admins** - List and retrieve teams and admins
## Installation
```bash
npm install
npm run build
```
## Configuration
Create a `.env` file:
```bash
cp .env.example .env
```
Add your Intercom access token:
```
INTERCOM_ACCESS_TOKEN=your_access_token_here
```
Get your access token from the [Intercom Developer Hub](https://app.intercom.com/a/apps/_/settings/developer-hub).
## Usage
### Standalone
```bash
npm start
```
### As MCP Server
Add to your MCP client configuration (e.g., Claude Desktop):
```json
{
"mcpServers": {
"intercom": {
"command": "node",
"args": ["/path/to/dist/main.js"],
"env": {
"INTERCOM_ACCESS_TOKEN": "your_access_token_here"
}
}
}
}
```
## Available Tools
### Contacts
- `contacts_create` - Create a new contact (user or lead)
- `contacts_get` - Retrieve a contact by ID
- `contacts_update` - Update contact details
- `contacts_delete` - Delete a contact
- `contacts_list` - List all contacts (cursor pagination)
- `contacts_search` - Search contacts with filters
### Conversations
- `conversations_create` - Start a new conversation
- `conversations_get` - Retrieve conversation details
- `conversations_list` - List conversations
- `conversations_search` - Search conversations
- `conversations_reply` - Reply to a conversation
- `conversations_close` - Close a conversation
- `conversations_assign` - Assign to admin or team
### Companies
- `companies_create` - Create a company
- `companies_get` - Retrieve company details
- `companies_list` - List companies
- `companies_update` - Update company data
### Articles
- `articles_create` - Create a help article
- `articles_get` - Get article by ID
- `articles_list` - List all articles
- `articles_update` - Update article
- `articles_delete` - Delete article
### Help Center
- `help-center_list` - List help centers
- `help-center_collections_list` - List collections
- `help-center_collections_create` - Create collection
### Tickets
- `tickets_create` - Create a ticket
- `tickets_get` - Get ticket by ID
- `tickets_list` - List tickets
- `tickets_search` - Search tickets
- `tickets_types_list` - List ticket types
### Tags
- `tags_create` - Create a tag
- `tags_list` - List all tags
- `tags_delete` - Delete a tag
### Segments
- `segments_list` - List segments
- `segments_get` - Get segment by ID
### Events
- `events_submit` - Submit a custom event
### Messages
- `messages_send` - Send in-app, email, or push message
### Teams & Admins
- `teams_list` - List all teams
- `teams_get` - Get team by ID
- `admins_list` - List all admins
- `admins_get` - Get admin by ID
## API Reference
This server uses Intercom API v2.11. For detailed API documentation, visit:
https://developers.intercom.com/docs/build-an-integration/
## Rate Limiting
The server automatically handles rate limiting (429 responses) with exponential backoff and retry logic.
## Development
```bash
# Type check
npm run typecheck
# Build
npm run build
# Watch mode
npm run dev
```
## License
MIT

View File

@ -0,0 +1,32 @@
{
"name": "@mcpengine/intercom",
"version": "1.0.0",
"description": "MCP server for Intercom API integration",
"type": "module",
"main": "dist/main.js",
"bin": {
"intercom-mcp": "dist/main.js"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/main.js",
"typecheck": "tsc --noEmit"
},
"keywords": [
"mcp",
"intercom",
"model-context-protocol"
],
"author": "",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"axios": "^1.7.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.6.0"
}
}

View File

@ -0,0 +1,701 @@
/**
* Intercom API Client
* API Version: 2.11
* Base URL: https://api.intercom.io
*/
import axios, { AxiosInstance, AxiosError } from 'axios';
import type {
Contact,
Company,
Conversation,
Article,
Collection,
Section,
HelpCenter,
Ticket,
TicketType,
Tag,
Segment,
Admin,
Team,
Note,
DataAttribute,
Subscription,
ListResponse,
ScrollResponse,
SearchQuery,
CreateContactRequest,
UpdateContactRequest,
CreateCompanyRequest,
CreateConversationRequest,
ReplyConversationRequest,
CreateTicketRequest,
CreateNoteRequest,
Event,
Message,
ContactId,
CompanyId,
ConversationId,
ArticleId,
CollectionId,
SectionId,
TicketId,
TicketTypeId,
TagId,
SegmentId,
AdminId,
TeamId,
NoteId,
} from '../types/index.js';
export interface IntercomClientConfig {
accessToken: string;
baseURL?: string;
timeout?: number;
}
export class IntercomClient {
private client: AxiosInstance;
private accessToken: string;
constructor(config: IntercomClientConfig) {
this.accessToken = config.accessToken;
this.client = axios.create({
baseURL: config.baseURL || 'https://api.intercom.io',
timeout: config.timeout || 30000,
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
'Intercom-Version': '2.11',
},
});
// Add response interceptor for rate limiting
this.client.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (error.response?.status === 429) {
const retryAfter = error.response.headers['retry-after'];
const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : 5000;
console.warn(`Rate limit hit. Waiting ${waitTime}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
// Retry the request
return this.client.request(error.config!);
}
throw error;
}
);
}
// ============================================================================
// CONTACTS
// ============================================================================
async createContact(data: CreateContactRequest): Promise<Contact> {
const response = await this.client.post<Contact>('/contacts', data);
return response.data;
}
async getContact(id: ContactId): Promise<Contact> {
const response = await this.client.get<Contact>(`/contacts/${id}`);
return response.data;
}
async updateContact(id: ContactId, data: UpdateContactRequest): Promise<Contact> {
const response = await this.client.put<Contact>(`/contacts/${id}`, data);
return response.data;
}
async deleteContact(id: ContactId): Promise<{ id: ContactId; deleted: boolean }> {
const response = await this.client.delete(`/contacts/${id}`);
return response.data;
}
async listContacts(params?: {
per_page?: number;
starting_after?: string;
}): Promise<ListResponse<Contact>> {
const response = await this.client.get<ListResponse<Contact>>('/contacts', {
params,
});
return response.data;
}
async searchContacts(query: SearchQuery): Promise<ListResponse<Contact>> {
const response = await this.client.post<ListResponse<Contact>>(
'/contacts/search',
query
);
return response.data;
}
async scrollContacts(scrollParam?: string): Promise<ScrollResponse<Contact>> {
const response = await this.client.get<ScrollResponse<Contact>>('/contacts/scroll', {
params: scrollParam ? { scroll_param: scrollParam } : undefined,
});
return response.data;
}
async mergeContacts(from: ContactId, into: ContactId): Promise<Contact> {
const response = await this.client.post<Contact>('/contacts/merge', {
from,
into,
});
return response.data;
}
async archiveContact(id: ContactId): Promise<Contact> {
const response = await this.client.post<Contact>(`/contacts/${id}/archive`);
return response.data;
}
async unarchiveContact(id: ContactId): Promise<Contact> {
const response = await this.client.post<Contact>(`/contacts/${id}/unarchive`);
return response.data;
}
// ============================================================================
// COMPANIES
// ============================================================================
async createCompany(data: CreateCompanyRequest): Promise<Company> {
const response = await this.client.post<Company>('/companies', data);
return response.data;
}
async getCompany(id: CompanyId): Promise<Company> {
const response = await this.client.get<Company>(`/companies/${id}`);
return response.data;
}
async updateCompany(id: CompanyId, data: Partial<CreateCompanyRequest>): Promise<Company> {
const response = await this.client.put<Company>(`/companies/${id}`, data);
return response.data;
}
async listCompanies(params?: {
per_page?: number;
starting_after?: string;
}): Promise<ListResponse<Company>> {
const response = await this.client.get<ListResponse<Company>>('/companies', {
params,
});
return response.data;
}
async scrollCompanies(scrollParam?: string): Promise<ScrollResponse<Company>> {
const response = await this.client.get<ScrollResponse<Company>>('/companies/scroll', {
params: scrollParam ? { scroll_param: scrollParam } : undefined,
});
return response.data;
}
async attachContactToCompany(contactId: ContactId, companyId: CompanyId): Promise<Company> {
const response = await this.client.post<Company>(
`/contacts/${contactId}/companies`,
{ id: companyId }
);
return response.data;
}
async detachContactFromCompany(contactId: ContactId, companyId: CompanyId): Promise<Company> {
const response = await this.client.delete<Company>(
`/contacts/${contactId}/companies/${companyId}`
);
return response.data;
}
// ============================================================================
// CONVERSATIONS
// ============================================================================
async createConversation(data: CreateConversationRequest): Promise<Conversation> {
const response = await this.client.post<Conversation>('/conversations', data);
return response.data;
}
async getConversation(id: ConversationId): Promise<Conversation> {
const response = await this.client.get<Conversation>(`/conversations/${id}`);
return response.data;
}
async listConversations(params?: {
per_page?: number;
starting_after?: string;
}): Promise<ListResponse<Conversation>> {
const response = await this.client.get<ListResponse<Conversation>>('/conversations', {
params,
});
return response.data;
}
async searchConversations(query: SearchQuery): Promise<ListResponse<Conversation>> {
const response = await this.client.post<ListResponse<Conversation>>(
'/conversations/search',
query
);
return response.data;
}
async replyToConversation(
id: ConversationId,
data: ReplyConversationRequest
): Promise<Conversation> {
const response = await this.client.post<Conversation>(
`/conversations/${id}/reply`,
data
);
return response.data;
}
async assignConversation(
id: ConversationId,
assignee: { type: 'admin' | 'team'; id: AdminId | TeamId; admin_id?: AdminId }
): Promise<Conversation> {
const response = await this.client.post<Conversation>(
`/conversations/${id}/parts`,
{
message_type: 'assignment',
...assignee,
}
);
return response.data;
}
async snoozeConversation(
id: ConversationId,
snoozedUntil: number
): Promise<Conversation> {
const response = await this.client.post<Conversation>(
`/conversations/${id}/parts`,
{
message_type: 'snoozed',
type: 'admin',
snoozed_until: snoozedUntil,
}
);
return response.data;
}
async closeConversation(id: ConversationId, adminId: AdminId): Promise<Conversation> {
const response = await this.client.post<Conversation>(
`/conversations/${id}/parts`,
{
message_type: 'close',
type: 'admin',
admin_id: adminId,
}
);
return response.data;
}
async openConversation(id: ConversationId, adminId: AdminId): Promise<Conversation> {
const response = await this.client.post<Conversation>(
`/conversations/${id}/parts`,
{
message_type: 'open',
type: 'admin',
admin_id: adminId,
}
);
return response.data;
}
async attachTagToConversation(id: ConversationId, tagId: TagId, adminId: AdminId): Promise<Conversation> {
const response = await this.client.post<Conversation>(
`/conversations/${id}/tags`,
{
id: tagId,
admin_id: adminId,
}
);
return response.data;
}
async detachTagFromConversation(id: ConversationId, tagId: TagId, adminId: AdminId): Promise<Conversation> {
const response = await this.client.delete<Conversation>(
`/conversations/${id}/tags/${tagId}`,
{
data: { admin_id: adminId },
}
);
return response.data;
}
// ============================================================================
// ARTICLES
// ============================================================================
async createArticle(data: {
title: string;
description?: string;
body?: string;
author_id: AdminId;
state?: 'published' | 'draft';
parent_id?: CollectionId | SectionId;
parent_type?: 'collection' | 'section';
}): Promise<Article> {
const response = await this.client.post<Article>('/articles', data);
return response.data;
}
async getArticle(id: ArticleId): Promise<Article> {
const response = await this.client.get<Article>(`/articles/${id}`);
return response.data;
}
async updateArticle(id: ArticleId, data: Partial<{
title: string;
description: string;
body: string;
author_id: AdminId;
state: 'published' | 'draft';
parent_id: CollectionId | SectionId;
parent_type: 'collection' | 'section';
}>): Promise<Article> {
const response = await this.client.put<Article>(`/articles/${id}`, data);
return response.data;
}
async deleteArticle(id: ArticleId): Promise<{ id: ArticleId; deleted: boolean }> {
const response = await this.client.delete(`/articles/${id}`);
return response.data;
}
async listArticles(params?: {
per_page?: number;
page?: number;
}): Promise<ListResponse<Article>> {
const response = await this.client.get<ListResponse<Article>>('/articles', {
params,
});
return response.data;
}
// ============================================================================
// HELP CENTER
// ============================================================================
async listHelpCenters(): Promise<ListResponse<HelpCenter>> {
const response = await this.client.get<ListResponse<HelpCenter>>('/help_center/help_centers');
return response.data;
}
async getHelpCenter(id: string): Promise<HelpCenter> {
const response = await this.client.get<HelpCenter>(`/help_center/help_centers/${id}`);
return response.data;
}
async listCollections(params?: {
per_page?: number;
page?: number;
}): Promise<ListResponse<Collection>> {
const response = await this.client.get<ListResponse<Collection>>(
'/help_center/collections',
{ params }
);
return response.data;
}
async getCollection(id: CollectionId): Promise<Collection> {
const response = await this.client.get<Collection>(
`/help_center/collections/${id}`
);
return response.data;
}
async createCollection(data: {
name: string;
description?: string;
parent_id?: string;
}): Promise<Collection> {
const response = await this.client.post<Collection>(
'/help_center/collections',
data
);
return response.data;
}
async updateCollection(
id: CollectionId,
data: Partial<{ name: string; description: string }>
): Promise<Collection> {
const response = await this.client.put<Collection>(
`/help_center/collections/${id}`,
data
);
return response.data;
}
async deleteCollection(id: CollectionId): Promise<{ id: CollectionId; deleted: boolean }> {
const response = await this.client.delete(`/help_center/collections/${id}`);
return response.data;
}
async listSections(collectionId: CollectionId): Promise<ListResponse<Section>> {
const response = await this.client.get<ListResponse<Section>>(
`/help_center/collections/${collectionId}/sections`
);
return response.data;
}
async getSection(id: SectionId): Promise<Section> {
const response = await this.client.get<Section>(`/help_center/sections/${id}`);
return response.data;
}
async createSection(collectionId: CollectionId, data: {
name: string;
}): Promise<Section> {
const response = await this.client.post<Section>(
`/help_center/collections/${collectionId}/sections`,
data
);
return response.data;
}
// ============================================================================
// TICKETS
// ============================================================================
async createTicket(data: CreateTicketRequest): Promise<Ticket> {
const response = await this.client.post<Ticket>('/tickets', data);
return response.data;
}
async getTicket(id: TicketId): Promise<Ticket> {
const response = await this.client.get<Ticket>(`/tickets/${id}`);
return response.data;
}
async updateTicket(id: TicketId, data: Partial<CreateTicketRequest>): Promise<Ticket> {
const response = await this.client.put<Ticket>(`/tickets/${id}`, data);
return response.data;
}
async listTickets(params?: {
per_page?: number;
page?: number;
}): Promise<ListResponse<Ticket>> {
const response = await this.client.get<ListResponse<Ticket>>('/tickets', {
params,
});
return response.data;
}
async searchTickets(query: SearchQuery): Promise<ListResponse<Ticket>> {
const response = await this.client.post<ListResponse<Ticket>>(
'/tickets/search',
query
);
return response.data;
}
async listTicketTypes(): Promise<ListResponse<TicketType>> {
const response = await this.client.get<ListResponse<TicketType>>('/ticket_types');
return response.data;
}
async getTicketType(id: TicketTypeId): Promise<TicketType> {
const response = await this.client.get<TicketType>(`/ticket_types/${id}`);
return response.data;
}
// ============================================================================
// TAGS
// ============================================================================
async createTag(name: string): Promise<Tag> {
const response = await this.client.post<Tag>('/tags', { name });
return response.data;
}
async getTag(id: TagId): Promise<Tag> {
const response = await this.client.get<Tag>(`/tags/${id}`);
return response.data;
}
async listTags(): Promise<ListResponse<Tag>> {
const response = await this.client.get<ListResponse<Tag>>('/tags');
return response.data;
}
async deleteTag(id: TagId): Promise<{ id: TagId; deleted: boolean }> {
const response = await this.client.delete(`/tags/${id}`);
return response.data;
}
async tagContact(contactId: ContactId, tagId: TagId): Promise<Tag> {
const response = await this.client.post<Tag>(`/contacts/${contactId}/tags`, {
id: tagId,
});
return response.data;
}
async untagContact(contactId: ContactId, tagId: TagId): Promise<Tag> {
const response = await this.client.delete<Tag>(
`/contacts/${contactId}/tags/${tagId}`
);
return response.data;
}
async tagCompany(companyId: CompanyId, tagId: TagId): Promise<Tag> {
const response = await this.client.post<Tag>(`/companies/${companyId}/tags`, {
id: tagId,
});
return response.data;
}
async untagCompany(companyId: CompanyId, tagId: TagId): Promise<Tag> {
const response = await this.client.delete<Tag>(
`/companies/${companyId}/tags/${tagId}`
);
return response.data;
}
// ============================================================================
// SEGMENTS
// ============================================================================
async listSegments(params?: {
include_count?: boolean;
}): Promise<ListResponse<Segment>> {
const response = await this.client.get<ListResponse<Segment>>('/segments', {
params,
});
return response.data;
}
async getSegment(id: SegmentId): Promise<Segment> {
const response = await this.client.get<Segment>(`/segments/${id}`);
return response.data;
}
// ============================================================================
// EVENTS
// ============================================================================
async submitEvent(event: Event): Promise<{ type: 'event'; success: boolean }> {
const response = await this.client.post<{ type: 'event'; success: boolean }>(
'/events',
event
);
return response.data;
}
async listEventSummaries(params: {
user_id?: string;
email?: string;
type?: 'user' | 'company';
count?: number;
}): Promise<{ type: 'event.summary'; events: Array<{ name: string; count: number; last: number }> }> {
const response = await this.client.get('/events/summaries', { params });
return response.data;
}
// ============================================================================
// MESSAGES
// ============================================================================
async sendMessage(message: Message): Promise<{ type: 'message'; id: string }> {
const response = await this.client.post<{ type: 'message'; id: string }>(
'/messages',
message
);
return response.data;
}
// ============================================================================
// ADMINS & TEAMS
// ============================================================================
async listAdmins(): Promise<ListResponse<Admin>> {
const response = await this.client.get<ListResponse<Admin>>('/admins');
return response.data;
}
async getAdmin(id: AdminId): Promise<Admin> {
const response = await this.client.get<Admin>(`/admins/${id}`);
return response.data;
}
async listTeams(): Promise<ListResponse<Team>> {
const response = await this.client.get<ListResponse<Team>>('/teams');
return response.data;
}
async getTeam(id: TeamId): Promise<Team> {
const response = await this.client.get<Team>(`/teams/${id}`);
return response.data;
}
// ============================================================================
// NOTES
// ============================================================================
async createNote(data: CreateNoteRequest): Promise<Note> {
const response = await this.client.post<Note>('/notes', data);
return response.data;
}
async getNote(id: NoteId): Promise<Note> {
const response = await this.client.get<Note>(`/notes/${id}`);
return response.data;
}
async listNotes(contactId: ContactId): Promise<ListResponse<Note>> {
const response = await this.client.get<ListResponse<Note>>(
`/contacts/${contactId}/notes`
);
return response.data;
}
// ============================================================================
// DATA ATTRIBUTES
// ============================================================================
async listDataAttributes(params?: {
model?: 'contact' | 'company' | 'conversation';
include_archived?: boolean;
}): Promise<ListResponse<DataAttribute>> {
const response = await this.client.get<ListResponse<DataAttribute>>(
'/data_attributes',
{ params }
);
return response.data;
}
async createDataAttribute(data: {
name: string;
model: 'contact' | 'company' | 'conversation';
data_type: string;
description?: string;
options?: string[];
}): Promise<DataAttribute> {
const response = await this.client.post<DataAttribute>('/data_attributes', data);
return response.data;
}
async updateDataAttribute(id: string, data: {
archived?: boolean;
description?: string;
options?: string[];
}): Promise<DataAttribute> {
const response = await this.client.put<DataAttribute>(`/data_attributes/${id}`, data);
return response.data;
}
// ============================================================================
// SUBSCRIPTIONS
// ============================================================================
async listSubscriptions(): Promise<ListResponse<Subscription>> {
const response = await this.client.get<ListResponse<Subscription>>('/subscription_types');
return response.data;
}
}

View File

@ -0,0 +1,49 @@
#!/usr/bin/env node
/**
* Intercom MCP Server Main Entry Point
* Supports dual transport (stdio/SSE) with graceful shutdown
*/
import { IntercomMCPServer } from './server.js';
async function main() {
const accessToken = process.env.INTERCOM_ACCESS_TOKEN;
if (!accessToken) {
console.error('Error: INTERCOM_ACCESS_TOKEN environment variable is required');
process.exit(1);
}
const server = new IntercomMCPServer(accessToken);
// Graceful shutdown handlers
const shutdown = async (signal: string) => {
console.error(`\nReceived ${signal}, shutting down gracefully...`);
process.exit(0);
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
// 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 the server
try {
await server.run();
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
main();

View File

@ -0,0 +1,757 @@
/**
* Intercom MCP Server
* Lazy-loaded tools for Intercom API integration
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { IntercomClient } from './clients/intercom.js';
import { z } from 'zod';
export class IntercomMCPServer {
private server: Server;
private client: IntercomClient;
private toolsMap: Map<string, Tool>;
constructor(accessToken: string) {
this.client = new IntercomClient({ accessToken });
this.server = new Server(
{
name: '@mcpengine/intercom',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.toolsMap = new Map();
this.setupHandlers();
this.registerTools();
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: Array.from(this.toolsMap.values()),
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await this.executeTool(name, args || {});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
}
private registerTools(): void {
// Contacts
this.toolsMap.set('contacts_create', {
name: 'contacts_create',
description: 'Create a new contact (user or lead) in Intercom',
inputSchema: {
type: 'object',
properties: {
role: { type: 'string', enum: ['user', 'lead'], description: 'Contact role' },
external_id: { type: 'string', description: 'External unique identifier' },
email: { type: 'string', description: 'Email address' },
phone: { type: 'string', description: 'Phone number' },
name: { type: 'string', description: 'Full name' },
signed_up_at: { type: 'number', description: 'Unix timestamp of signup' },
custom_attributes: { type: 'object', description: 'Custom attributes object' },
},
},
});
this.toolsMap.set('contacts_get', {
name: 'contacts_get',
description: 'Retrieve a contact by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Contact ID' },
},
required: ['id'],
},
});
this.toolsMap.set('contacts_update', {
name: 'contacts_update',
description: 'Update a contact',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Contact ID' },
email: { type: 'string' },
name: { type: 'string' },
phone: { type: 'string' },
custom_attributes: { type: 'object' },
},
required: ['id'],
},
});
this.toolsMap.set('contacts_delete', {
name: 'contacts_delete',
description: 'Delete a contact permanently',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Contact ID' },
},
required: ['id'],
},
});
this.toolsMap.set('contacts_list', {
name: 'contacts_list',
description: 'List all contacts with cursor pagination',
inputSchema: {
type: 'object',
properties: {
per_page: { type: 'number', description: 'Results per page (max 150)' },
starting_after: { type: 'string', description: 'Cursor for pagination' },
},
},
});
this.toolsMap.set('contacts_search', {
name: 'contacts_search',
description: 'Search contacts using filters',
inputSchema: {
type: 'object',
properties: {
query: { type: 'object', description: 'Search filter object' },
pagination: { type: 'object', description: 'Pagination params' },
sort: { type: 'object', description: 'Sort configuration' },
},
},
});
// Conversations
this.toolsMap.set('conversations_create', {
name: 'conversations_create',
description: 'Create a new conversation',
inputSchema: {
type: 'object',
properties: {
from: {
type: 'object',
description: 'Sender info (type, id/user_id/email)',
properties: {
type: { type: 'string', enum: ['user', 'lead', 'contact'] },
id: { type: 'string' },
user_id: { type: 'string' },
email: { type: 'string' },
},
required: ['type'],
},
body: { type: 'string', description: 'Message body' },
},
required: ['from', 'body'],
},
});
this.toolsMap.set('conversations_get', {
name: 'conversations_get',
description: 'Retrieve a conversation by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Conversation ID' },
},
required: ['id'],
},
});
this.toolsMap.set('conversations_list', {
name: 'conversations_list',
description: 'List conversations',
inputSchema: {
type: 'object',
properties: {
per_page: { type: 'number' },
starting_after: { type: 'string' },
},
},
});
this.toolsMap.set('conversations_search', {
name: 'conversations_search',
description: 'Search conversations using filters',
inputSchema: {
type: 'object',
properties: {
query: { type: 'object', description: 'Search filter' },
pagination: { type: 'object' },
},
},
});
this.toolsMap.set('conversations_reply', {
name: 'conversations_reply',
description: 'Reply to a conversation',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Conversation ID' },
message_type: { type: 'string', enum: ['comment', 'note'] },
type: { type: 'string', enum: ['admin', 'user'] },
admin_id: { type: 'string', description: 'Admin ID (if type=admin)' },
body: { type: 'string', description: 'Reply body' },
attachment_urls: { type: 'array', items: { type: 'string' } },
},
required: ['id', 'message_type', 'type', 'body'],
},
});
this.toolsMap.set('conversations_close', {
name: 'conversations_close',
description: 'Close a conversation',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Conversation ID' },
admin_id: { type: 'string', description: 'Admin ID' },
},
required: ['id', 'admin_id'],
},
});
this.toolsMap.set('conversations_assign', {
name: 'conversations_assign',
description: 'Assign a conversation to an admin or team',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Conversation ID' },
assignee_type: { type: 'string', enum: ['admin', 'team'] },
assignee_id: { type: 'string', description: 'Admin or Team ID' },
admin_id: { type: 'string', description: 'Admin making the assignment' },
},
required: ['id', 'assignee_type', 'assignee_id'],
},
});
// Companies
this.toolsMap.set('companies_create', {
name: 'companies_create',
description: 'Create a new company',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Company name' },
company_id: { type: 'string', description: 'Unique company ID' },
website: { type: 'string' },
plan: { type: 'string' },
size: { type: 'number' },
industry: { type: 'string' },
monthly_spend: { type: 'number' },
custom_attributes: { type: 'object' },
},
required: ['name'],
},
});
this.toolsMap.set('companies_get', {
name: 'companies_get',
description: 'Retrieve a company by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Company ID' },
},
required: ['id'],
},
});
this.toolsMap.set('companies_list', {
name: 'companies_list',
description: 'List companies',
inputSchema: {
type: 'object',
properties: {
per_page: { type: 'number' },
starting_after: { type: 'string' },
},
},
});
this.toolsMap.set('companies_update', {
name: 'companies_update',
description: 'Update a company',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Company ID' },
name: { type: 'string' },
website: { type: 'string' },
plan: { type: 'string' },
size: { type: 'number' },
monthly_spend: { type: 'number' },
custom_attributes: { type: 'object' },
},
required: ['id'],
},
});
// Articles
this.toolsMap.set('articles_create', {
name: 'articles_create',
description: 'Create a help center article',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: 'Article title' },
description: { type: 'string' },
body: { type: 'string', description: 'Article body (HTML or markdown)' },
author_id: { type: 'string', description: 'Admin ID' },
state: { type: 'string', enum: ['published', 'draft'] },
parent_id: { type: 'string', description: 'Collection or Section ID' },
parent_type: { type: 'string', enum: ['collection', 'section'] },
},
required: ['title', 'author_id'],
},
});
this.toolsMap.set('articles_get', {
name: 'articles_get',
description: 'Retrieve an article by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Article ID' },
},
required: ['id'],
},
});
this.toolsMap.set('articles_list', {
name: 'articles_list',
description: 'List all articles',
inputSchema: {
type: 'object',
properties: {
per_page: { type: 'number' },
page: { type: 'number' },
},
},
});
this.toolsMap.set('articles_update', {
name: 'articles_update',
description: 'Update an article',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Article ID' },
title: { type: 'string' },
body: { type: 'string' },
state: { type: 'string', enum: ['published', 'draft'] },
},
required: ['id'],
},
});
this.toolsMap.set('articles_delete', {
name: 'articles_delete',
description: 'Delete an article',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Article ID' },
},
required: ['id'],
},
});
// Help Center
this.toolsMap.set('help-center_list', {
name: 'help-center_list',
description: 'List all help centers',
inputSchema: {
type: 'object',
properties: {},
},
});
this.toolsMap.set('help-center_collections_list', {
name: 'help-center_collections_list',
description: 'List help center collections',
inputSchema: {
type: 'object',
properties: {
per_page: { type: 'number' },
page: { type: 'number' },
},
},
});
this.toolsMap.set('help-center_collections_create', {
name: 'help-center_collections_create',
description: 'Create a collection',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Collection name' },
description: { type: 'string' },
},
required: ['name'],
},
});
// Tickets
this.toolsMap.set('tickets_create', {
name: 'tickets_create',
description: 'Create a ticket',
inputSchema: {
type: 'object',
properties: {
ticket_type_id: { type: 'string', description: 'Ticket type ID' },
contacts: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
external_id: { type: 'string' },
email: { type: 'string' },
},
},
},
ticket_attributes: { type: 'object', description: 'Ticket custom attributes' },
},
required: ['ticket_type_id'],
},
});
this.toolsMap.set('tickets_get', {
name: 'tickets_get',
description: 'Retrieve a ticket by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Ticket ID' },
},
required: ['id'],
},
});
this.toolsMap.set('tickets_list', {
name: 'tickets_list',
description: 'List tickets',
inputSchema: {
type: 'object',
properties: {
per_page: { type: 'number' },
page: { type: 'number' },
},
},
});
this.toolsMap.set('tickets_search', {
name: 'tickets_search',
description: 'Search tickets',
inputSchema: {
type: 'object',
properties: {
query: { type: 'object' },
},
},
});
this.toolsMap.set('tickets_types_list', {
name: 'tickets_types_list',
description: 'List all ticket types',
inputSchema: {
type: 'object',
properties: {},
},
});
// Tags
this.toolsMap.set('tags_create', {
name: 'tags_create',
description: 'Create a tag',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Tag name' },
},
required: ['name'],
},
});
this.toolsMap.set('tags_list', {
name: 'tags_list',
description: 'List all tags',
inputSchema: {
type: 'object',
properties: {},
},
});
this.toolsMap.set('tags_delete', {
name: 'tags_delete',
description: 'Delete a tag',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tag ID' },
},
required: ['id'],
},
});
// Segments
this.toolsMap.set('segments_list', {
name: 'segments_list',
description: 'List all segments',
inputSchema: {
type: 'object',
properties: {
include_count: { type: 'boolean', description: 'Include member count' },
},
},
});
this.toolsMap.set('segments_get', {
name: 'segments_get',
description: 'Retrieve a segment by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Segment ID' },
},
required: ['id'],
},
});
// Events
this.toolsMap.set('events_submit', {
name: 'events_submit',
description: 'Submit an event',
inputSchema: {
type: 'object',
properties: {
event_name: { type: 'string', description: 'Event name' },
created_at: { type: 'number', description: 'Unix timestamp' },
user_id: { type: 'string' },
email: { type: 'string' },
metadata: { type: 'object', description: 'Event metadata' },
},
required: ['event_name'],
},
});
// Messages
this.toolsMap.set('messages_send', {
name: 'messages_send',
description: 'Send a message (email, in-app, push)',
inputSchema: {
type: 'object',
properties: {
message_type: { type: 'string', enum: ['inapp', 'email', 'push'] },
subject: { type: 'string' },
body: { type: 'string' },
from: { type: 'object', description: 'Sender (admin)' },
to: { type: 'object', description: 'Recipient (contact/user/lead)' },
},
required: ['message_type', 'body'],
},
});
// Teams
this.toolsMap.set('teams_list', {
name: 'teams_list',
description: 'List all teams',
inputSchema: {
type: 'object',
properties: {},
},
});
this.toolsMap.set('teams_get', {
name: 'teams_get',
description: 'Retrieve a team by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Team ID' },
},
required: ['id'],
},
});
// Admins
this.toolsMap.set('admins_list', {
name: 'admins_list',
description: 'List all admins',
inputSchema: {
type: 'object',
properties: {},
},
});
this.toolsMap.set('admins_get', {
name: 'admins_get',
description: 'Retrieve an admin by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Admin ID' },
},
required: ['id'],
},
});
}
private async executeTool(name: string, args: Record<string, unknown>): Promise<unknown> {
switch (name) {
// Contacts
case 'contacts_create':
return this.client.createContact(args as any);
case 'contacts_get':
return this.client.getContact(args.id as any);
case 'contacts_update':
return this.client.updateContact(args.id as any, args as any);
case 'contacts_delete':
return this.client.deleteContact(args.id as any);
case 'contacts_list':
return this.client.listContacts(args as any);
case 'contacts_search':
return this.client.searchContacts(args as any);
// Conversations
case 'conversations_create':
return this.client.createConversation(args as any);
case 'conversations_get':
return this.client.getConversation(args.id as any);
case 'conversations_list':
return this.client.listConversations(args as any);
case 'conversations_search':
return this.client.searchConversations(args as any);
case 'conversations_reply':
return this.client.replyToConversation(args.id as any, args as any);
case 'conversations_close':
return this.client.closeConversation(args.id as any, args.admin_id as any);
case 'conversations_assign':
return this.client.assignConversation(args.id as any, {
type: args.assignee_type as any,
id: args.assignee_id as any,
admin_id: args.admin_id as any,
});
// Companies
case 'companies_create':
return this.client.createCompany(args as any);
case 'companies_get':
return this.client.getCompany(args.id as any);
case 'companies_list':
return this.client.listCompanies(args as any);
case 'companies_update':
return this.client.updateCompany(args.id as any, args as any);
// Articles
case 'articles_create':
return this.client.createArticle(args as any);
case 'articles_get':
return this.client.getArticle(args.id as any);
case 'articles_list':
return this.client.listArticles(args as any);
case 'articles_update':
return this.client.updateArticle(args.id as any, args as any);
case 'articles_delete':
return this.client.deleteArticle(args.id as any);
// Help Center
case 'help-center_list':
return this.client.listHelpCenters();
case 'help-center_collections_list':
return this.client.listCollections(args as any);
case 'help-center_collections_create':
return this.client.createCollection(args as any);
// Tickets
case 'tickets_create':
return this.client.createTicket(args as any);
case 'tickets_get':
return this.client.getTicket(args.id as any);
case 'tickets_list':
return this.client.listTickets(args as any);
case 'tickets_search':
return this.client.searchTickets(args as any);
case 'tickets_types_list':
return this.client.listTicketTypes();
// Tags
case 'tags_create':
return this.client.createTag(args.name as string);
case 'tags_list':
return this.client.listTags();
case 'tags_delete':
return this.client.deleteTag(args.id as any);
// Segments
case 'segments_list':
return this.client.listSegments(args as any);
case 'segments_get':
return this.client.getSegment(args.id as any);
// Events
case 'events_submit':
return this.client.submitEvent(args as any);
// Messages
case 'messages_send':
return this.client.sendMessage(args as any);
// Teams
case 'teams_list':
return this.client.listTeams();
case 'teams_get':
return this.client.getTeam(args.id as any);
// Admins
case 'admins_list':
return this.client.listAdmins();
case 'admins_get':
return this.client.getAdmin(args.id as any);
default:
throw new Error(`Unknown tool: ${name}`);
}
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Intercom MCP Server running on stdio');
}
}

View File

@ -0,0 +1,737 @@
/**
* Intercom API Types
* Based on Intercom API v2.11
*/
// Branded types for type safety
export type ContactId = string & { readonly __brand: 'ContactId' };
export type CompanyId = string & { readonly __brand: 'CompanyId' };
export type ConversationId = string & { readonly __brand: 'ConversationId' };
export type AdminId = string & { readonly __brand: 'AdminId' };
export type TeamId = string & { readonly __brand: 'TeamId' };
export type ArticleId = string & { readonly __brand: 'ArticleId' };
export type CollectionId = string & { readonly __brand: 'CollectionId' };
export type SectionId = string & { readonly __brand: 'SectionId' };
export type TicketId = string & { readonly __brand: 'TicketId' };
export type TicketTypeId = string & { readonly __brand: 'TicketTypeId' };
export type TagId = string & { readonly __brand: 'TagId' };
export type SegmentId = string & { readonly __brand: 'SegmentId' };
export type NoteId = string & { readonly __brand: 'NoteId' };
// Common types
export interface Timestamp {
created_at: number;
updated_at: number;
}
export interface Pagination {
type: 'pages';
page: number;
per_page: number;
total_pages: number;
}
export interface CursorPagination {
type: 'pages';
next?: {
page: number;
starting_after: string;
};
per_page: number;
total_pages?: number;
}
export interface ListResponse<T> {
type: 'list';
data: T[];
pages?: CursorPagination;
total_count?: number;
}
export interface ScrollResponse<T> {
type: 'list';
data: T[];
scroll_param?: string;
pages?: {
type: 'pages';
next?: string;
};
}
// Search/Filter types
export type SearchOperator =
| '=' | '!=' | 'IN' | 'NIN'
| '>' | '<' | '>=' | '<='
| '~' | '!~' | '^' | '$';
export interface SingleFilter {
field: string;
operator: SearchOperator;
value: string | number | boolean | string[];
}
export interface MultipleFilter {
operator: 'AND' | 'OR';
value: Array<SingleFilter | MultipleFilter>;
}
export type SearchFilter = SingleFilter | MultipleFilter;
export interface SearchQuery {
query?: SearchFilter;
pagination?: {
per_page?: number;
starting_after?: string;
};
sort?: {
field: string;
order: 'asc' | 'desc';
};
}
// Contact types
export interface ContactLocation {
type: 'location';
country?: string;
region?: string;
city?: string;
}
export interface ContactSocialProfile {
type: 'social_profile';
name: string;
url: string;
}
export interface ContactAvatar {
type: 'avatar';
image_url?: string;
}
export interface ContactCompany {
type: 'company';
id: CompanyId;
name?: string;
url?: string;
}
export interface CustomAttributes {
[key: string]: string | number | boolean | null;
}
export interface Contact extends Timestamp {
type: 'contact';
id: ContactId;
workspace_id: string;
external_id?: string;
role: 'user' | 'lead';
email?: string;
phone?: string;
name?: string;
avatar?: ContactAvatar;
owner_id?: AdminId;
social_profiles?: {
type: 'list';
data: ContactSocialProfile[];
};
has_hard_bounced: boolean;
marked_email_as_spam: boolean;
unsubscribed_from_emails: boolean;
location?: ContactLocation;
last_seen_at?: number;
last_replied_at?: number;
last_contacted_at?: number;
last_email_opened_at?: number;
last_email_clicked_at?: number;
language_override?: string;
browser?: string;
browser_version?: string;
browser_language?: string;
os?: string;
android_app_name?: string;
android_app_version?: string;
android_device?: string;
android_os_version?: string;
android_sdk_version?: string;
android_last_seen_at?: number;
ios_app_name?: string;
ios_app_version?: string;
ios_device?: string;
ios_os_version?: string;
ios_sdk_version?: string;
ios_last_seen_at?: number;
custom_attributes?: CustomAttributes;
tags?: {
type: 'list';
data: Tag[];
url: string;
total_count: number;
has_more: boolean;
};
notes?: {
type: 'list';
data: Note[];
url: string;
total_count: number;
has_more: boolean;
};
companies?: {
type: 'list';
data: ContactCompany[];
url: string;
total_count: number;
has_more: boolean;
};
}
// Company types
export interface Company extends Timestamp {
type: 'company';
id: CompanyId;
name: string;
company_id?: string;
remote_created_at?: number;
plan?: string;
size?: number;
website?: string;
industry?: string;
monthly_spend?: number;
session_count?: number;
user_count?: number;
custom_attributes?: CustomAttributes;
tags?: {
type: 'list';
data: Tag[];
url: string;
total_count: number;
has_more: boolean;
};
segments?: {
type: 'list';
data: Segment[];
url: string;
total_count: number;
has_more: boolean;
};
}
// Conversation types
export interface ConversationSource {
type: 'conversation';
id: ConversationId;
delivered_as: 'operator_initiated' | 'automated' | 'admin_initiated';
subject?: string;
body?: string;
author: {
type: 'admin' | 'user' | 'lead' | 'bot';
id: string;
name?: string;
email?: string;
};
attachments?: Array<{
type: 'upload';
name: string;
url: string;
content_type: string;
filesize: number;
width?: number;
height?: number;
}>;
url?: string;
}
export interface ConversationPart extends Timestamp {
type: 'conversation_part';
id: string;
part_type: 'comment' | 'note' | 'assignment' | 'open' | 'close' | 'snooze';
body?: string;
author: {
type: 'admin' | 'user' | 'lead' | 'bot';
id: string;
name?: string;
email?: string;
};
attachments?: Array<{
type: 'upload';
name: string;
url: string;
content_type: string;
filesize: number;
}>;
notified_at?: number;
assigned_to?: {
type: 'admin' | 'team';
id: string;
};
external_id?: string;
redacted: boolean;
}
export interface ConversationStatistics {
type: 'conversation_statistics';
time_to_assignment?: number;
time_to_admin_reply?: number;
time_to_first_close?: number;
time_to_last_close?: number;
median_time_to_reply?: number;
first_contact_reply_at?: number;
first_assignment_at?: number;
first_admin_reply_at?: number;
first_close_at?: number;
last_assignment_at?: number;
last_assignment_admin_reply_at?: number;
last_contact_reply_at?: number;
last_admin_reply_at?: number;
last_close_at?: number;
last_closed_by_id?: string;
count_reopens?: number;
count_assignments?: number;
count_conversation_parts?: number;
}
export interface Conversation extends Timestamp {
type: 'conversation';
id: ConversationId;
title?: string;
state: 'open' | 'closed' | 'snoozed';
read: boolean;
priority: 'priority' | 'not_priority';
waiting_since?: number;
snoozed_until?: number;
open: boolean;
source: ConversationSource;
contacts?: {
type: 'contact.list';
contacts: Contact[];
};
teammates?: {
type: 'admin.list';
admins: Admin[];
};
assignee?: {
type: 'admin' | 'team' | 'nobody';
id?: string;
};
conversation_parts?: {
type: 'conversation_part.list';
conversation_parts: ConversationPart[];
total_count: number;
};
conversation_rating?: {
rating?: number;
remark?: string;
created_at?: number;
contact?: {
type: 'contact';
id: ContactId;
};
teammate?: {
type: 'admin';
id: AdminId;
};
};
statistics?: ConversationStatistics;
tags?: {
type: 'tag.list';
tags: Tag[];
};
linked_objects?: {
type: 'list';
data: Array<{
id: string;
category?: string;
}>;
total_count: number;
has_more: boolean;
};
}
// Article & Help Center types
export interface Article extends Timestamp {
type: 'article';
id: ArticleId;
workspace_id: string;
title: string;
description?: string;
body?: string;
author_id: AdminId;
state: 'published' | 'draft';
parent_id?: CollectionId | SectionId;
parent_type?: 'collection' | 'section';
default_locale: string;
url?: string;
statistics?: {
type: 'article_statistics';
views: number;
conversions: number;
reactions: number;
happy_reaction_count: number;
neutral_reaction_count: number;
sad_reaction_count: number;
};
}
export interface Collection extends Timestamp {
type: 'collection';
id: CollectionId;
workspace_id: string;
name: string;
description?: string;
url?: string;
order?: number;
default_locale: string;
icon?: string;
parent_id?: string;
help_center_id?: number;
}
export interface Section extends Timestamp {
type: 'section';
id: SectionId;
workspace_id: string;
name: string;
parent_id: CollectionId;
url?: string;
order?: number;
}
export interface HelpCenter extends Timestamp {
type: 'help_center';
id: string;
workspace_id: string;
identifier: string;
website_turned_on: boolean;
display_name?: string;
website_url?: string;
}
// Ticket types
export interface TicketTypeAttribute {
type: 'ticket_type_attribute';
id: string;
workspace_id: string;
name: string;
description?: string;
data_type: 'string' | 'integer' | 'decimal' | 'boolean' | 'date' | 'datetime' | 'files';
input_options?: {
multiline?: boolean;
list?: string[];
};
order: number;
required_to_create: boolean;
required_to_create_for_contacts: boolean;
visible_on_create: boolean;
visible_to_contacts: boolean;
default: boolean;
ticket_type_id: TicketTypeId;
archived: boolean;
created_at: number;
updated_at: number;
}
export interface TicketType extends Timestamp {
type: 'ticket_type';
id: TicketTypeId;
workspace_id: string;
name: string;
description?: string;
icon?: string;
archived: boolean;
ticket_type_attributes?: {
type: 'list';
ticket_type_attributes: TicketTypeAttribute[];
};
}
export interface TicketState {
type: 'ticket_state';
id: string;
name: string;
category: 'submitted' | 'in_progress' | 'waiting_on_customer' | 'resolved';
}
export interface Ticket extends Timestamp {
type: 'ticket';
id: TicketId;
ticket_type_id: TicketTypeId;
contacts?: {
type: 'contact.list';
contacts: Contact[];
};
admin_assignee_id?: AdminId;
team_assignee_id?: TeamId;
ticket_state: TicketState;
ticket_attributes?: {
[key: string]: string | number | boolean | string[] | null;
};
linked_objects?: {
type: 'list';
data: Array<{
id: string;
category?: string;
}>;
total_count: number;
};
is_shared: boolean;
category?: 'ticket' | 'back_office_ticket' | 'tracker';
}
// Tag types
export interface Tag {
type: 'tag';
id: TagId;
name: string;
applied_at?: number;
applied_by?: {
type: 'admin';
id: AdminId;
};
}
// Segment types
export interface Segment {
type: 'segment';
id: SegmentId;
name: string;
created_at: number;
updated_at: number;
person_type: 'contact' | 'user' | 'lead';
count?: number;
}
// Event types
export interface DataEvent {
type: 'event';
id: string;
event_name: string;
created_at: number;
user_id?: string;
email?: string;
metadata?: {
[key: string]: string | number | boolean;
};
}
export interface Event {
event_name: string;
created_at?: number;
user_id?: string;
id?: string;
email?: string;
metadata?: {
[key: string]: string | number | boolean | null;
};
}
// Message types
export type MessageType = 'inapp' | 'email' | 'push';
export interface MessageContent {
type: 'text' | 'button';
text?: string;
style?: 'primary' | 'secondary' | 'link';
action?: {
type: 'url' | 'sheet';
url?: string;
payload?: string;
};
}
export interface Message {
message_type: MessageType;
subject?: string;
body?: string;
template?: 'plain' | 'personal';
from?: {
type: 'admin';
id: AdminId;
};
to?: {
type: 'contact' | 'user' | 'lead';
id?: ContactId;
user_id?: string;
email?: string;
};
create_conversation_without_contact_reply?: boolean;
}
// Admin & Team types
export interface Admin {
type: 'admin';
id: AdminId;
name: string;
email: string;
email_verified: boolean;
app?: {
type: 'app';
id_code: string;
};
avatar?: {
type: 'avatar';
image_url?: string;
};
away_mode_enabled: boolean;
away_mode_reassign: boolean;
has_inbox_seat: boolean;
team_ids?: TeamId[];
team_priority_level?: {
[key: string]: number;
};
}
export interface Team {
type: 'team';
id: TeamId;
name: string;
admin_ids?: AdminId[];
admin_priority_level?: {
[key: string]: number;
};
}
// Note types
export interface Note extends Timestamp {
type: 'note';
id: NoteId;
contact?: {
type: 'contact';
id: ContactId;
};
author?: {
type: 'admin';
id: AdminId;
name?: string;
email?: string;
};
body?: string;
}
// Data Attribute types
export interface DataAttribute {
type: 'data_attribute';
id: string;
workspace_id: string;
name: string;
full_name: string;
label: string;
description?: string;
data_type:
| 'string'
| 'integer'
| 'float'
| 'boolean'
| 'date'
| 'datetime'
| 'object'
| 'array';
options?: string[];
api_writable: boolean;
ui_writable: boolean;
custom: boolean;
archived: boolean;
created_at: number;
updated_at: number;
model: 'contact' | 'company' | 'conversation' | 'ticket';
admin_id?: AdminId;
messenger_writable?: boolean;
}
// Subscription types
export interface Subscription {
type: 'subscription';
id: string;
state: 'active' | 'past_due' | 'canceled' | 'trialing';
default_translation?: {
name: string;
description?: string;
};
consent_type: 'opt_in' | 'opt_out';
content_types?: string[];
created_at: number;
updated_at: number;
}
// SLA types
export interface SLA {
type: 'sla';
sla_name: string;
sla_status: 'active' | 'missed' | 'cancelled';
}
// Request/Response helpers
export interface CreateContactRequest {
role?: 'user' | 'lead';
external_id?: string;
email?: string;
phone?: string;
name?: string;
avatar?: string;
signed_up_at?: number;
last_seen_at?: number;
owner_id?: number;
unsubscribed_from_emails?: boolean;
custom_attributes?: CustomAttributes;
}
export interface UpdateContactRequest {
role?: 'user' | 'lead';
external_id?: string;
email?: string;
phone?: string;
name?: string;
avatar?: string;
signed_up_at?: number;
last_seen_at?: number;
owner_id?: number;
unsubscribed_from_emails?: boolean;
custom_attributes?: CustomAttributes;
}
export interface CreateCompanyRequest {
company_id?: string;
name: string;
website?: string;
plan?: string;
size?: number;
industry?: string;
remote_created_at?: number;
monthly_spend?: number;
custom_attributes?: CustomAttributes;
}
export interface CreateConversationRequest {
from: {
type: 'user' | 'lead' | 'contact';
id?: string;
user_id?: string;
email?: string;
};
body: string;
}
export interface ReplyConversationRequest {
message_type: 'comment' | 'note';
type: 'admin' | 'user';
admin_id?: AdminId;
body: string;
attachment_urls?: string[];
created_at?: number;
}
export interface CreateTicketRequest {
ticket_type_id: TicketTypeId;
contacts?: Array<{
id?: ContactId;
external_id?: string;
email?: string;
}>;
ticket_attributes?: {
[key: string]: string | number | boolean | string[];
};
}
export interface CreateNoteRequest {
contact_id?: ContactId;
admin_id?: AdminId;
body: 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,3 @@
# Monday.com API Key
# Get your API key from: https://monday.com/developers/apps
MONDAY_API_KEY=your_api_key_here

35
servers/monday/.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# Dependencies
node_modules/
package-lock.json
yarn.lock
pnpm-lock.yaml
# Build output
dist/
*.tsbuildinfo
# Environment
.env
.env.local
.env.*.local
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Testing
coverage/
.nyc_output/

132
servers/monday/README.md Normal file
View File

@ -0,0 +1,132 @@
# Monday.com MCP Server
Complete Model Context Protocol (MCP) server for Monday.com GraphQL API.
## Features
- **Full Monday.com API Coverage**: Boards, items, columns, groups, updates, users, teams, workspaces, webhooks
- **GraphQL Native**: All requests use Monday.com's GraphQL endpoint
- **Type-Safe**: Comprehensive TypeScript types for all Monday.com entities
- **Complexity Tracking**: Built-in rate limit monitoring via complexity points
- **Lazy-Loaded Tools**: Efficient MCP tool registration
## Installation
```bash
npm install
npm run build
```
## Configuration
Create a `.env` file:
```bash
cp .env.example .env
```
Add your Monday.com API key:
```env
MONDAY_API_KEY=your_api_key_here
```
Get your API key from: https://monday.com/developers/apps
## Usage
### Standalone
```bash
npm start
```
### In Claude Desktop
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"monday": {
"command": "node",
"args": ["/path/to/monday/dist/main.js"],
"env": {
"MONDAY_API_KEY": "your_api_key_here"
}
}
}
}
```
## Available Tools
### Boards
- `get_boards` - List all boards with filtering
- `get_board` - Get single board with columns and groups
- `create_board` - Create new board
### Items
- `get_items` - List items from a board
- `get_item` - Get single item with column values
- `create_item` - Create new item
- `change_column_value` - Update column value
### Groups
- `create_group` - Create new group in board
### Updates (Activity Feed)
- `get_updates` - Get item updates/comments
- `create_update` - Create update/comment
### Users & Teams
- `get_users` - List all users
- `get_teams` - List all teams
- `get_workspaces` - List all workspaces
### Webhooks
- `create_webhook` - Create webhook for board events
- `delete_webhook` - Delete webhook
## Column Types
Supports ALL Monday.com column types:
- Status, Text, Numbers, Date, People, Timeline
- Dropdown, Checkbox, Rating, Label
- Link, Email, Phone, Long Text
- Color Picker, Tags, Hour, Week, World Clock
- File, Board Relation, Mirror, Formula
- Auto Number, Creation Log, Last Updated, Dependency
Each column type has proper TypeScript definitions with typed values.
## GraphQL Architecture
All requests are POST to `https://api.monday.com/v2`:
```typescript
{
"query": "query { boards { id name } }",
"variables": {}
}
```
Rate limiting uses complexity points (10,000,000 per minute). The client tracks complexity from response metadata.
## Development
```bash
# Type check
npm run typecheck
# Build
npm run build
# Watch mode
npm run dev
```
## License
MIT

View File

@ -0,0 +1,28 @@
{
"name": "@mcpengine/monday",
"version": "1.0.0",
"description": "Monday.com MCP server - full GraphQL API integration",
"type": "module",
"main": "dist/main.js",
"bin": {
"monday-mcp": "dist/main.js"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/main.js",
"typecheck": "tsc --noEmit"
},
"keywords": ["mcp", "monday", "monday.com", "graphql"],
"author": "MCP Engine",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"axios": "^1.7.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.6.0"
}
}

View File

@ -0,0 +1,616 @@
/**
* Monday.com GraphQL Client
* All requests are POST to https://api.monday.com/v2
*/
import axios, { AxiosInstance } from "axios";
import type {
Board,
Item,
Group,
Update,
User,
Team,
Workspace,
Webhook,
MondayResponse,
GraphQLResponse,
GetBoardsOptions,
GetItemsOptions,
CreateBoardOptions,
CreateItemOptions,
ChangeColumnValueOptions,
CreateGroupOptions,
CreateUpdateOptions,
CreateWebhookOptions,
Complexity,
} from "../types/index.js";
export class MondayClient {
private client: AxiosInstance;
private lastComplexity?: Complexity;
constructor(apiKey: string) {
this.client = axios.create({
baseURL: "https://api.monday.com/v2",
headers: {
"Content-Type": "application/json",
Authorization: apiKey,
},
timeout: 30000,
});
}
/**
* Get the last query complexity data
*/
getComplexity(): Complexity | undefined {
return this.lastComplexity;
}
/**
* Execute a raw GraphQL query
*/
private async query<T = any>(
query: string,
variables?: Record<string, any>
): Promise<MondayResponse<T>> {
const response = await this.client.post<GraphQLResponse<T>>("", {
query,
variables,
});
if (response.data.errors && response.data.errors.length > 0) {
throw new Error(
`Monday API Error: ${response.data.errors.map((e) => e.message).join(", ")}`
);
}
// Parse complexity from response headers or extensions
// Monday returns complexity in the response data
const result: MondayResponse<T> = {
data: response.data.data!,
account_id: response.data.account_id,
};
return result;
}
// ============================================================================
// Board Methods
// ============================================================================
/**
* Get multiple boards
*/
async getBoards(options: GetBoardsOptions = {}): Promise<Board[]> {
const {
limit = 25,
page = 1,
state = "active",
board_kind,
workspace_ids,
} = options;
let args: string[] = [`limit: ${limit}`, `page: ${page}`];
if (state !== "all") args.push(`state: ${state}`);
if (board_kind) args.push(`board_kind: ${board_kind}`);
if (workspace_ids && workspace_ids.length > 0) {
args.push(`workspace_ids: [${workspace_ids.join(", ")}]`);
}
const query = `
query {
boards(${args.join(", ")}) {
id
name
description
board_kind
state
workspace_id
owner_id
permissions
created_at
updated_at
items_count
columns {
id
title
type
description
settings_str
archived
width
}
groups {
id
title
color
position
archived
}
}
}
`;
const result = await this.query<{ boards: Board[] }>(query);
return result.data.boards;
}
/**
* Get a single board by ID
*/
async getBoard(boardId: string): Promise<Board> {
const query = `
query {
boards(ids: [${boardId}]) {
id
name
description
board_kind
state
workspace_id
folder_id
owner_id
permissions
created_at
updated_at
items_count
columns {
id
title
type
description
settings_str
archived
width
}
groups {
id
title
color
position
archived
}
}
}
`;
const result = await this.query<{ boards: Board[] }>(query);
if (!result.data.boards || result.data.boards.length === 0) {
throw new Error(`Board ${boardId} not found`);
}
return result.data.boards[0];
}
/**
* Create a new board
*/
async createBoard(options: CreateBoardOptions): Promise<Board> {
const {
board_name,
board_kind,
description,
workspace_id,
folder_id,
template_id,
} = options;
let args: string[] = [
`board_name: "${board_name}"`,
`board_kind: ${board_kind}`,
];
if (description) args.push(`description: "${description}"`);
if (workspace_id) args.push(`workspace_id: ${workspace_id}`);
if (folder_id) args.push(`folder_id: ${folder_id}`);
if (template_id) args.push(`template_id: ${template_id}`);
const query = `
mutation {
create_board(${args.join(", ")}) {
id
name
description
board_kind
state
workspace_id
owner_id
}
}
`;
const result = await this.query<{ create_board: Board }>(query);
return result.data.create_board;
}
// ============================================================================
// Item Methods
// ============================================================================
/**
* Get items from a board
*/
async getItems(boardId: string, options: GetItemsOptions = {}): Promise<Item[]> {
const { limit = 25, page = 1, ids, newest_first } = options;
let boardArgs: string[] = [`ids: [${boardId}]`];
let itemsArgs: string[] = [`limit: ${limit}`];
if (page) itemsArgs.push(`page: ${page}`);
if (ids && ids.length > 0) itemsArgs.push(`ids: [${ids.join(", ")}]`);
if (newest_first !== undefined) itemsArgs.push(`newest_first: ${newest_first}`);
const query = `
query {
boards(${boardArgs.join(", ")}) {
items_page(${itemsArgs.join(", ")}) {
cursor
items {
id
name
state
created_at
updated_at
creator_id
group {
id
title
}
column_values {
id
text
value
type
}
}
}
}
}
`;
const result = await this.query<{
boards: Array<{ items_page: { items: Item[] } }>;
}>(query);
if (!result.data.boards || result.data.boards.length === 0) {
return [];
}
return result.data.boards[0].items_page.items;
}
/**
* Get a single item by ID
*/
async getItem(itemId: string): Promise<Item> {
const query = `
query {
items(ids: [${itemId}]) {
id
name
state
created_at
updated_at
creator_id
board {
id
name
}
group {
id
title
}
column_values {
id
text
value
type
}
}
}
`;
const result = await this.query<{ items: Item[] }>(query);
if (!result.data.items || result.data.items.length === 0) {
throw new Error(`Item ${itemId} not found`);
}
return result.data.items[0];
}
/**
* Create a new item
*/
async createItem(options: CreateItemOptions): Promise<Item> {
const {
board_id,
group_id,
item_name,
column_values,
create_labels_if_missing,
} = options;
let args: string[] = [
`board_id: ${board_id}`,
`item_name: "${item_name}"`,
];
if (group_id) args.push(`group_id: "${group_id}"`);
if (column_values) {
const jsonStr = JSON.stringify(JSON.stringify(column_values));
args.push(`column_values: ${jsonStr}`);
}
if (create_labels_if_missing !== undefined) {
args.push(`create_labels_if_missing: ${create_labels_if_missing}`);
}
const query = `
mutation {
create_item(${args.join(", ")}) {
id
name
state
created_at
creator_id
board {
id
name
}
group {
id
title
}
}
}
`;
const result = await this.query<{ create_item: Item }>(query);
return result.data.create_item;
}
/**
* Change a column value for an item
*/
async changeColumnValue(options: ChangeColumnValueOptions): Promise<Item> {
const { board_id, item_id, column_id, value } = options;
const jsonValue = JSON.stringify(JSON.stringify(value));
const query = `
mutation {
change_column_value(
board_id: ${board_id}
item_id: ${item_id}
column_id: "${column_id}"
value: ${jsonValue}
) {
id
name
}
}
`;
const result = await this.query<{ change_column_value: Item }>(query);
return result.data.change_column_value;
}
// ============================================================================
// Group Methods
// ============================================================================
/**
* Create a new group
*/
async createGroup(options: CreateGroupOptions): Promise<Group> {
const { board_id, group_name, position_relative_method, relative_to } =
options;
let args: string[] = [
`board_id: ${board_id}`,
`group_name: "${group_name}"`,
];
if (position_relative_method) {
args.push(`position_relative_method: ${position_relative_method}`);
}
if (relative_to) args.push(`relative_to: "${relative_to}"`);
const query = `
mutation {
create_group(${args.join(", ")}) {
id
title
color
position
}
}
`;
const result = await this.query<{ create_group: Group }>(query);
return result.data.create_group;
}
// ============================================================================
// Update Methods
// ============================================================================
/**
* Get updates for an item
*/
async getUpdates(itemId: string, limit = 25): Promise<Update[]> {
const query = `
query {
items(ids: [${itemId}]) {
updates(limit: ${limit}) {
id
body
created_at
updated_at
creator_id
text_body
creator {
id
name
email
}
}
}
}
`;
const result = await this.query<{
items: Array<{ updates: Update[] }>;
}>(query);
if (!result.data.items || result.data.items.length === 0) {
return [];
}
return result.data.items[0].updates || [];
}
/**
* Create an update
*/
async createUpdate(options: CreateUpdateOptions): Promise<Update> {
const { item_id, body, parent_id } = options;
let args: string[] = [`item_id: ${item_id}`, `body: "${body}"`];
if (parent_id) args.push(`parent_id: ${parent_id}`);
const query = `
mutation {
create_update(${args.join(", ")}) {
id
body
created_at
creator_id
text_body
}
}
`;
const result = await this.query<{ create_update: Update }>(query);
return result.data.create_update;
}
// ============================================================================
// User, Team, Workspace Methods
// ============================================================================
/**
* Get current user
*/
async getUsers(limit = 50): Promise<User[]> {
const query = `
query {
users(limit: ${limit}) {
id
name
email
url
photo_thumb
is_guest
enabled
created_at
title
phone
location
time_zone_identifier
}
}
`;
const result = await this.query<{ users: User[] }>(query);
return result.data.users;
}
/**
* Get teams
*/
async getTeams(limit = 50): Promise<Team[]> {
const query = `
query {
teams(limit: ${limit}) {
id
name
picture_url
users {
id
name
email
}
}
}
`;
const result = await this.query<{ teams: Team[] }>(query);
return result.data.teams;
}
/**
* Get workspaces
*/
async getWorkspaces(limit = 50): Promise<Workspace[]> {
const query = `
query {
workspaces(limit: ${limit}) {
id
name
kind
description
created_at
}
}
`;
const result = await this.query<{ workspaces: Workspace[] }>(query);
return result.data.workspaces;
}
// ============================================================================
// Webhook Methods
// ============================================================================
/**
* Create a webhook
*/
async createWebhook(options: CreateWebhookOptions): Promise<Webhook> {
const { board_id, url, event, config } = options;
let args: string[] = [
`board_id: ${board_id}`,
`url: "${url}"`,
`event: ${event}`,
];
if (config) {
const jsonStr = JSON.stringify(JSON.stringify(config));
args.push(`config: ${jsonStr}`);
}
const query = `
mutation {
create_webhook(${args.join(", ")}) {
id
board_id
}
}
`;
const result = await this.query<{ create_webhook: Webhook }>(query);
return result.data.create_webhook;
}
/**
* Delete a webhook
*/
async deleteWebhook(webhookId: string): Promise<{ id: string }> {
const query = `
mutation {
delete_webhook(id: ${webhookId}) {
id
}
}
`;
const result = await this.query<{ delete_webhook: { id: string } }>(query);
return result.data.delete_webhook;
}
}

View File

@ -0,0 +1,36 @@
#!/usr/bin/env node
/**
* Monday.com MCP Server Entry Point
* Dual transport (stdio/SSE) with graceful shutdown
*/
import { MondayMCPServer } from "./server.js";
// Validate environment
const MONDAY_API_KEY = process.env.MONDAY_API_KEY;
if (!MONDAY_API_KEY) {
console.error("Error: MONDAY_API_KEY environment variable is required");
process.exit(1);
}
// Create and start server
const server = new MondayMCPServer(MONDAY_API_KEY);
// Graceful shutdown
process.on("SIGINT", () => {
console.error("Received SIGINT, shutting down gracefully...");
process.exit(0);
});
process.on("SIGTERM", () => {
console.error("Received SIGTERM, shutting down gracefully...");
process.exit(0);
});
// Start server
server.run().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});

View File

@ -0,0 +1,510 @@
/**
* Monday.com MCP Server
* Lazy-loaded tools for Monday.com GraphQL API
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { MondayClient } from "./clients/monday.js";
export class MondayMCPServer {
private server: Server;
private client: MondayClient;
constructor(apiKey: string) {
this.client = new MondayClient(apiKey);
this.server = new Server(
{
name: "@mcpengine/monday",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
private setupHandlers(): void {
// List all available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: this.getTools(),
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
let result: any;
switch (name) {
// Board tools
case "get_boards":
result = await this.client.getBoards((args || {}) as any);
break;
case "get_board":
result = await this.client.getBoard((args?.board_id as string) || "");
break;
case "create_board":
result = await this.client.createBoard((args || {}) as any);
break;
// Item tools
case "get_items":
result = await this.client.getItems(
(args?.board_id as string) || "",
(args || {}) as any
);
break;
case "get_item":
result = await this.client.getItem((args?.item_id as string) || "");
break;
case "create_item":
result = await this.client.createItem((args || {}) as any);
break;
case "change_column_value":
result = await this.client.changeColumnValue((args || {}) as any);
break;
// Group tools
case "create_group":
result = await this.client.createGroup((args || {}) as any);
break;
// Update tools
case "get_updates":
result = await this.client.getUpdates(
(args?.item_id as string) || "",
args?.limit as number | undefined
);
break;
case "create_update":
result = await this.client.createUpdate((args || {}) as any);
break;
// User, team, workspace tools
case "get_users":
result = await this.client.getUsers(args?.limit as number | undefined);
break;
case "get_teams":
result = await this.client.getTeams(args?.limit as number | undefined);
break;
case "get_workspaces":
result = await this.client.getWorkspaces(
args?.limit as number | undefined
);
break;
// Webhook tools
case "create_webhook":
result = await this.client.createWebhook((args || {}) as any);
break;
case "delete_webhook":
result = await this.client.deleteWebhook((args?.webhook_id as string) || "");
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
}
private getTools(): Tool[] {
return [
// Board tools
{
name: "get_boards",
description:
"Get all boards. Filter by state, board_kind, workspace_ids. Supports pagination.",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Number of boards to return (default: 25)",
},
page: {
type: "number",
description: "Page number for pagination (default: 1)",
},
state: {
type: "string",
enum: ["active", "archived", "deleted", "all"],
description: "Filter by board state (default: active)",
},
board_kind: {
type: "string",
enum: ["public", "private", "share"],
description: "Filter by board type",
},
workspace_ids: {
type: "array",
items: { type: "string" },
description: "Filter by workspace IDs",
},
},
},
},
{
name: "get_board",
description: "Get a single board by ID with all columns and groups",
inputSchema: {
type: "object",
properties: {
board_id: {
type: "string",
description: "Board ID",
},
},
required: ["board_id"],
},
},
{
name: "create_board",
description: "Create a new board",
inputSchema: {
type: "object",
properties: {
board_name: {
type: "string",
description: "Name of the new board",
},
board_kind: {
type: "string",
enum: ["public", "private", "share"],
description: "Board visibility type",
},
description: {
type: "string",
description: "Board description",
},
workspace_id: {
type: "string",
description: "Workspace ID to create board in",
},
folder_id: {
type: "string",
description: "Folder ID to create board in",
},
template_id: {
type: "string",
description: "Template ID to use",
},
},
required: ["board_name", "board_kind"],
},
},
// Item tools
{
name: "get_items",
description:
"Get items from a board. Supports pagination and filtering.",
inputSchema: {
type: "object",
properties: {
board_id: {
type: "string",
description: "Board ID",
},
limit: {
type: "number",
description: "Number of items to return (default: 25)",
},
page: {
type: "number",
description: "Page number for pagination",
},
ids: {
type: "array",
items: { type: "string" },
description: "Filter by specific item IDs",
},
newest_first: {
type: "boolean",
description: "Sort by newest first",
},
},
required: ["board_id"],
},
},
{
name: "get_item",
description: "Get a single item by ID with all column values",
inputSchema: {
type: "object",
properties: {
item_id: {
type: "string",
description: "Item ID",
},
},
required: ["item_id"],
},
},
{
name: "create_item",
description: "Create a new item in a board",
inputSchema: {
type: "object",
properties: {
board_id: {
type: "string",
description: "Board ID",
},
group_id: {
type: "string",
description: "Group ID to create item in",
},
item_name: {
type: "string",
description: "Name of the new item",
},
column_values: {
type: "object",
description:
"Column values as JSON object (keys are column IDs, values are column-type-specific)",
},
create_labels_if_missing: {
type: "boolean",
description: "Create labels if they don't exist",
},
},
required: ["board_id", "item_name"],
},
},
{
name: "change_column_value",
description: "Change a column value for an item",
inputSchema: {
type: "object",
properties: {
board_id: {
type: "string",
description: "Board ID",
},
item_id: {
type: "string",
description: "Item ID",
},
column_id: {
type: "string",
description: "Column ID",
},
value: {
type: "object",
description:
"New value (format depends on column type, e.g. {text: 'value'} for text)",
},
},
required: ["board_id", "item_id", "column_id", "value"],
},
},
// Group tools
{
name: "create_group",
description: "Create a new group in a board",
inputSchema: {
type: "object",
properties: {
board_id: {
type: "string",
description: "Board ID",
},
group_name: {
type: "string",
description: "Name of the new group",
},
position_relative_method: {
type: "string",
enum: ["before_at", "after_at"],
description: "Position relative to another group",
},
relative_to: {
type: "string",
description: "Group ID to position relative to",
},
},
required: ["board_id", "group_name"],
},
},
// Update tools
{
name: "get_updates",
description: "Get updates (activity feed) for an item",
inputSchema: {
type: "object",
properties: {
item_id: {
type: "string",
description: "Item ID",
},
limit: {
type: "number",
description: "Number of updates to return (default: 25)",
},
},
required: ["item_id"],
},
},
{
name: "create_update",
description: "Create an update (comment) on an item",
inputSchema: {
type: "object",
properties: {
item_id: {
type: "string",
description: "Item ID",
},
body: {
type: "string",
description: "Update text content",
},
parent_id: {
type: "string",
description: "Parent update ID (for replies)",
},
},
required: ["item_id", "body"],
},
},
// User, team, workspace tools
{
name: "get_users",
description: "Get all users in the account",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Number of users to return (default: 50)",
},
},
},
},
{
name: "get_teams",
description: "Get all teams in the account",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Number of teams to return (default: 50)",
},
},
},
},
{
name: "get_workspaces",
description: "Get all workspaces in the account",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Number of workspaces to return (default: 50)",
},
},
},
},
// Webhook tools
{
name: "create_webhook",
description: "Create a webhook for board events",
inputSchema: {
type: "object",
properties: {
board_id: {
type: "string",
description: "Board ID",
},
url: {
type: "string",
description: "Webhook URL to receive events",
},
event: {
type: "string",
enum: [
"create_item",
"change_column_value",
"change_status_column_value",
"change_specific_column_value",
"create_update",
"delete_update",
"item_archived",
"item_deleted",
"item_moved_to_group",
"item_restored",
"subitem_created",
],
description: "Event type to subscribe to",
},
config: {
type: "object",
description: "Optional webhook configuration",
},
},
required: ["board_id", "url", "event"],
},
},
{
name: "delete_webhook",
description: "Delete a webhook",
inputSchema: {
type: "object",
properties: {
webhook_id: {
type: "string",
description: "Webhook ID to delete",
},
},
required: ["webhook_id"],
},
},
];
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
}

View File

@ -0,0 +1,614 @@
/**
* Monday.com API Types
* Complete type definitions for Monday.com GraphQL API
*/
// ============================================================================
// Board Types
// ============================================================================
export type BoardKind = "public" | "private" | "share";
export interface Board {
id: string;
name: string;
description?: string;
board_kind: BoardKind;
state: "active" | "archived" | "deleted";
workspace_id?: string;
folder_id?: string;
owner_id: string;
permissions: string;
created_at?: string;
updated_at?: string;
columns?: Column[];
groups?: Group[];
items?: Item[];
items_count?: number;
tags?: Tag[];
views?: BoardView[];
}
export interface BoardView {
id: string;
name: string;
type: string;
settings_str?: string;
}
// ============================================================================
// Column Types
// ============================================================================
export type ColumnType =
| "auto_number"
| "board_relation"
| "button"
| "checkbox"
| "color_picker"
| "country"
| "creation_log"
| "date"
| "dependency"
| "doc"
| "dropdown"
| "email"
| "file"
| "formula"
| "hour"
| "item_id"
| "label"
| "last_updated"
| "link"
| "location"
| "long_text"
| "mirror"
| "name"
| "numbers"
| "people"
| "phone"
| "progress"
| "rating"
| "status"
| "subtasks"
| "tags"
| "team"
| "text"
| "time_tracking"
| "timeline"
| "vote"
| "week"
| "world_clock";
export interface Column {
id: string;
title: string;
type: ColumnType;
description?: string;
settings_str?: string;
archived?: boolean;
width?: number;
}
// ============================================================================
// Column Value Types (typed per column type)
// ============================================================================
export type ColumnValue =
| TextColumnValue
| StatusColumnValue
| NumbersColumnValue
| DateColumnValue
| PeopleColumnValue
| TimelineColumnValue
| DropdownColumnValue
| CheckboxColumnValue
| RatingColumnValue
| LabelColumnValue
| LinkColumnValue
| EmailColumnValue
| PhoneColumnValue
| LongTextColumnValue
| ColorPickerColumnValue
| TagsColumnValue
| HourColumnValue
| WeekColumnValue
| WorldClockColumnValue
| FileColumnValue
| BoardRelationColumnValue
| MirrorColumnValue
| FormulaColumnValue
| AutoNumberColumnValue
| CreationLogColumnValue
| LastUpdatedColumnValue
| DependencyColumnValue
| GenericColumnValue;
export interface BaseColumnValue {
id: string;
column?: Column;
text?: string;
value?: string | null;
type: ColumnType;
}
export interface TextColumnValue extends BaseColumnValue {
type: "text";
value: string | null;
}
export interface StatusColumnValue extends BaseColumnValue {
type: "status";
value: string | null; // JSON: { "index": number, "label": string }
}
export interface NumbersColumnValue extends BaseColumnValue {
type: "numbers";
value: string | null; // JSON: number as string
}
export interface DateColumnValue extends BaseColumnValue {
type: "date";
value: string | null; // JSON: { "date": "YYYY-MM-DD", "time": "HH:MM:SS" }
}
export interface PeopleColumnValue extends BaseColumnValue {
type: "people";
value: string | null; // JSON: { "personsAndTeams": [{ "id": number, "kind": "person" | "team" }] }
}
export interface TimelineColumnValue extends BaseColumnValue {
type: "timeline";
value: string | null; // JSON: { "from": "YYYY-MM-DD", "to": "YYYY-MM-DD" }
}
export interface DropdownColumnValue extends BaseColumnValue {
type: "dropdown";
value: string | null; // JSON: { "ids": [number] }
}
export interface CheckboxColumnValue extends BaseColumnValue {
type: "checkbox";
value: string | null; // JSON: { "checked": boolean }
}
export interface RatingColumnValue extends BaseColumnValue {
type: "rating";
value: string | null; // JSON: { "rating": number }
}
export interface LabelColumnValue extends BaseColumnValue {
type: "label";
value: string | null; // JSON: { "label": string }
}
export interface LinkColumnValue extends BaseColumnValue {
type: "link";
value: string | null; // JSON: { "url": string, "text": string }
}
export interface EmailColumnValue extends BaseColumnValue {
type: "email";
value: string | null; // JSON: { "email": string, "text": string }
}
export interface PhoneColumnValue extends BaseColumnValue {
type: "phone";
value: string | null; // JSON: { "phone": string, "countryShortName": string }
}
export interface LongTextColumnValue extends BaseColumnValue {
type: "long_text";
value: string | null; // JSON: { "text": string }
}
export interface ColorPickerColumnValue extends BaseColumnValue {
type: "color_picker";
value: string | null; // JSON: { "color": string }
}
export interface TagsColumnValue extends BaseColumnValue {
type: "tags";
value: string | null; // JSON: { "tag_ids": [number] }
}
export interface HourColumnValue extends BaseColumnValue {
type: "hour";
value: string | null; // JSON: { "hour": number, "minute": number }
}
export interface WeekColumnValue extends BaseColumnValue {
type: "week";
value: string | null; // JSON: { "week": { "startDate": "YYYY-MM-DD", "endDate": "YYYY-MM-DD" } }
}
export interface WorldClockColumnValue extends BaseColumnValue {
type: "world_clock";
value: string | null; // JSON: { "timezone": string }
}
export interface FileColumnValue extends BaseColumnValue {
type: "file";
value: string | null; // JSON: { "files": [{ "id": string, "name": string, "url": string }] }
}
export interface BoardRelationColumnValue extends BaseColumnValue {
type: "board_relation";
value: string | null; // JSON: { "linkedPulseIds": [{ "linkedPulseId": number }] }
}
export interface MirrorColumnValue extends BaseColumnValue {
type: "mirror";
value: string | null; // Mirrored value from connected board
}
export interface FormulaColumnValue extends BaseColumnValue {
type: "formula";
value: string | null; // Calculated formula result
}
export interface AutoNumberColumnValue extends BaseColumnValue {
type: "auto_number";
value: string | null; // JSON: number
}
export interface CreationLogColumnValue extends BaseColumnValue {
type: "creation_log";
value: string | null; // JSON: { "created_at": "ISO8601", "creator_id": number }
}
export interface LastUpdatedColumnValue extends BaseColumnValue {
type: "last_updated";
value: string | null; // JSON: { "updated_at": "ISO8601", "updater_id": number }
}
export interface DependencyColumnValue extends BaseColumnValue {
type: "dependency";
value: string | null; // JSON: { "linkedPulseIds": [number] }
}
export interface GenericColumnValue extends BaseColumnValue {
type: ColumnType;
}
// ============================================================================
// Item Types
// ============================================================================
export interface Item {
id: string;
name: string;
board?: Board;
group?: Group;
state: "active" | "archived" | "deleted";
creator_id?: string;
created_at?: string;
updated_at?: string;
column_values?: ColumnValue[];
subitems?: SubItem[];
parent_item?: Item;
subscribers?: User[];
updates?: Update[];
}
export interface SubItem {
id: string;
name: string;
board?: Board;
column_values?: ColumnValue[];
created_at?: string;
updated_at?: string;
parent_item?: Item;
}
// ============================================================================
// Group Types
// ============================================================================
export interface Group {
id: string;
title: string;
color?: string;
position?: string;
archived?: boolean;
deleted?: boolean;
items?: Item[];
}
// ============================================================================
// Update Types (Activity Feed)
// ============================================================================
export interface Update {
id: string;
body: string;
creator_id?: string;
creator?: User;
created_at?: string;
updated_at?: string;
item_id?: string;
text_body?: string;
replies?: Update[];
assets?: Asset[];
}
export interface Asset {
id: string;
name: string;
url: string;
public_url?: string;
file_extension?: string;
file_size?: number;
uploaded_by?: User;
created_at?: string;
}
// ============================================================================
// User, Team, Account Types
// ============================================================================
export interface User {
id: string;
name: string;
email: string;
url?: string;
photo_original?: string;
photo_thumb?: string;
photo_tiny?: string;
is_guest?: boolean;
is_pending?: boolean;
enabled?: boolean;
created_at?: string;
birthday?: string;
country_code?: string;
location?: string;
time_zone_identifier?: string;
title?: string;
phone?: string;
mobile_phone?: string;
teams?: Team[];
account?: Account;
}
export interface Team {
id: string;
name: string;
picture_url?: string;
users?: User[];
}
export interface Account {
id: string;
name: string;
slug?: string;
tier?: string;
plan?: Plan;
logo?: string;
show_timeline_weekends?: boolean;
first_day_of_the_week?: string;
}
export interface Plan {
max_users: number;
period?: string;
tier?: string;
version: number;
}
// ============================================================================
// Workspace & Folder Types
// ============================================================================
export interface Workspace {
id: string;
name: string;
kind: "open" | "closed";
description?: string;
created_at?: string;
owners_subscribers?: User[];
teams_subscribers?: Team[];
users_subscribers?: User[];
}
export interface Folder {
id: string;
name: string;
color?: string;
children?: Folder[];
workspace_id?: string;
parent_id?: string;
}
// ============================================================================
// Webhook Types
// ============================================================================
export interface Webhook {
id: string;
board_id: string;
url: string;
event:
| "create_item"
| "change_column_value"
| "change_status_column_value"
| "change_specific_column_value"
| "create_update"
| "delete_update"
| "item_archived"
| "item_deleted"
| "item_moved_to_group"
| "item_restored"
| "subitem_created";
config?: string;
}
// ============================================================================
// Automation Types
// ============================================================================
export interface Automation {
id: string;
name: string;
enabled?: boolean;
}
// ============================================================================
// Notification Types
// ============================================================================
export interface Notification {
id: string;
text: string;
}
// ============================================================================
// Tag Types
// ============================================================================
export interface Tag {
id: string;
name: string;
color?: string;
}
// ============================================================================
// Activity Log Types
// ============================================================================
export interface ActivityLog {
id: string;
account_id: string;
user_id: string;
event: string;
data?: string;
created_at: string;
entity?: string;
}
// ============================================================================
// Complexity & Rate Limiting Types
// ============================================================================
export interface Complexity {
/**
* Query complexity cost
*/
query: number;
/**
* Complexity before the query
*/
before: number;
/**
* Complexity after the query
*/
after: number;
/**
* Time to reset (Unix timestamp)
*/
reset_in_x_seconds: number;
}
// ============================================================================
// GraphQL Response Wrapper Types
// ============================================================================
export interface GraphQLResponse<T = any> {
data?: T;
errors?: GraphQLError[];
account_id?: string;
}
export interface GraphQLError {
message: string;
locations?: Array<{ line: number; column: number }>;
path?: string[];
extensions?: {
code?: string;
[key: string]: any;
};
}
export interface MondayResponse<T = any> {
data: T;
complexity?: Complexity;
account_id?: string;
}
// ============================================================================
// Pagination Types
// ============================================================================
export interface ItemsPage {
cursor?: string;
items: Item[];
}
export interface BoardsPage {
cursor?: string;
boards: Board[];
}
export interface PaginationOptions {
limit?: number;
page?: number;
cursor?: string;
}
// ============================================================================
// Query Options
// ============================================================================
export interface GetBoardsOptions extends PaginationOptions {
state?: "active" | "archived" | "deleted" | "all";
board_kind?: BoardKind;
workspace_ids?: string[];
}
export interface GetItemsOptions extends PaginationOptions {
ids?: string[];
newest_first?: boolean;
}
export interface CreateBoardOptions {
board_name: string;
board_kind: BoardKind;
description?: string;
workspace_id?: string;
folder_id?: string;
template_id?: string;
}
export interface CreateItemOptions {
board_id: string;
group_id?: string;
item_name: string;
column_values?: Record<string, any>;
create_labels_if_missing?: boolean;
}
export interface ChangeColumnValueOptions {
board_id: string;
item_id: string;
column_id: string;
value: any;
}
export interface CreateGroupOptions {
board_id: string;
group_name: string;
position_relative_method?: "before_at" | "after_at";
relative_to?: string;
}
export interface CreateUpdateOptions {
item_id: string;
body: string;
parent_id?: string;
}
export interface CreateWebhookOptions {
board_id: string;
url: string;
event: Webhook["event"];
config?: Record<string, any>;
}

View File

@ -0,0 +1,26 @@
{
"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",
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1 @@
NOTION_API_KEY=your_notion_api_key_here

6
servers/notion/.gitignore vendored Normal file
View File

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

177
servers/notion/README.md Normal file
View File

@ -0,0 +1,177 @@
# Notion MCP Server
Model Context Protocol (MCP) server for the Notion API. Provides comprehensive access to Notion workspaces including pages, databases, blocks, users, comments, and search.
## Features
- **Pages**: Create, read, update, and archive pages
- **Databases**: Query, create, and manage databases with advanced filtering and sorting
- **Blocks**: Retrieve, append, update, and delete blocks
- **Users**: Access workspace user information
- **Comments**: Create and retrieve comments on pages and blocks
- **Search**: Search across all pages and databases
- **Rate Limiting**: Built-in rate limiting (3 req/sec) with retry logic
- **Pagination**: Automatic cursor-based pagination support
- **Type Safety**: Comprehensive TypeScript types for the entire Notion API
## Installation
```bash
npm install
npm run build
```
## Configuration
Set the `NOTION_API_KEY` environment variable:
```bash
export NOTION_API_KEY="your_notion_integration_token"
```
Or create a `.env` file:
```bash
cp .env.example .env
# Edit .env and add your API key
```
### Getting a Notion API Key
1. Go to https://www.notion.so/my-integrations
2. Click "+ New integration"
3. Give it a name and select the workspace
4. Copy the "Internal Integration Token"
5. Share your Notion pages/databases with the integration
## Usage
### Development
```bash
npm run dev
```
### Production
```bash
npm start
```
### With MCP Client
Add to your MCP settings configuration:
```json
{
"mcpServers": {
"notion": {
"command": "node",
"args": ["/path/to/notion/dist/main.js"],
"env": {
"NOTION_API_KEY": "your_api_key_here"
}
}
}
}
```
## Available Tools
### Pages
- `notion_get_page` - Retrieve a page by ID
- `notion_create_page` - Create a new page
- `notion_update_page` - Update page properties or archive
### Databases
- `notion_get_database` - Get database schema and info
- `notion_query_database` - Query with filters and sorts
- `notion_create_database` - Create a new database
### Blocks
- `notion_get_block` - Get a block by ID
- `notion_get_block_children` - Get child blocks
- `notion_append_block_children` - Add blocks to a parent
- `notion_delete_block` - Archive a block
### Users
- `notion_get_user` - Get user info
- `notion_list_users` - List all workspace users
### Comments
- `notion_create_comment` - Add a comment
- `notion_list_comments` - Get comments for a block
### Search
- `notion_search` - Search pages and databases
## Architecture
### Type System (`src/types/index.ts`)
Comprehensive TypeScript definitions including:
- All block types as discriminated unions
- All property types (title, rich_text, number, select, etc.)
- User, Page, Database, Comment types
- Filter and Sort builders
- Pagination types
- Branded ID types for type safety
### Client (`src/clients/notion.ts`)
Axios-based HTTP client with:
- Bearer token authentication
- Automatic rate limiting (3 req/sec)
- Exponential backoff retry logic
- Cursor-based pagination helpers
- Async generator support for large datasets
### Server (`src/server.ts`)
MCP server implementation with:
- Lazy-loaded tool handlers
- Structured error handling
- JSON schema validation
- Comprehensive tool definitions
### Entry Point (`src/main.ts`)
- Environment validation
- Stdio transport setup
- Graceful shutdown handling
- Error logging
## Development
```bash
# Type check
npm run typecheck
# Build
npm run build
# Run in dev mode
npm run dev
```
## License
MIT
## Contributing
Contributions welcome! Please ensure:
- TypeScript compiles with zero errors
- Follow existing code style
- Add tests for new features
- Update documentation

View File

@ -0,0 +1,29 @@
{
"name": "@mcpengine/notion",
"version": "0.1.0",
"description": "MCP server for Notion API",
"type": "module",
"main": "dist/main.js",
"bin": {
"notion-mcp": "dist/main.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/main.ts",
"start": "node dist/main.js",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"axios": "^1.7.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsx": "^4.0.0",
"typescript": "^5.6.0"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@ -0,0 +1,361 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
import type {
Page,
Database,
Block,
User,
Comment,
SearchResult,
PaginatedResults,
Filter,
Sort,
PageId,
DatabaseId,
BlockId,
UserId,
CommentId,
PageProperty,
DatabaseProperty,
} from '../types/index.js';
export interface NotionClientConfig {
auth: string;
baseURL?: string;
notionVersion?: string;
maxRetries?: number;
retryDelay?: number;
}
interface CreatePageParams {
parent: { database_id: DatabaseId } | { page_id: PageId };
properties: Record<string, Partial<PageProperty>>;
icon?: Page['icon'];
cover?: Page['cover'];
children?: Block[];
}
interface UpdatePageParams {
properties?: Record<string, Partial<PageProperty>>;
icon?: Page['icon'];
cover?: Page['cover'];
archived?: boolean;
}
interface QueryDatabaseParams {
database_id: DatabaseId;
filter?: Filter;
sorts?: Sort[];
start_cursor?: string;
page_size?: number;
}
interface CreateDatabaseParams {
parent: { page_id: PageId };
title: Array<{ type: 'text'; text: { content: string } }>;
properties: Record<string, DatabaseProperty>;
icon?: Database['icon'];
cover?: Database['cover'];
is_inline?: boolean;
}
interface SearchParams {
query?: string;
filter?: { value: 'page' | 'database'; property: 'object' };
sort?: { direction: 'ascending' | 'descending'; timestamp: 'last_edited_time' };
start_cursor?: string;
page_size?: number;
}
interface CreateCommentParams {
parent: { page_id: PageId } | { block_id: BlockId };
rich_text: Array<{ type: 'text'; text: { content: string } }>;
}
export class NotionClient {
private client: AxiosInstance;
private maxRetries: number;
private retryDelay: number;
private lastRequestTime: number = 0;
private readonly rateLimitRequests = 3; // 3 requests per second
private readonly rateLimitWindow = 1000; // 1 second in ms
constructor(config: NotionClientConfig) {
this.maxRetries = config.maxRetries ?? 3;
this.retryDelay = config.retryDelay ?? 1000;
this.client = axios.create({
baseURL: config.baseURL ?? 'https://api.notion.com/v1',
headers: {
'Authorization': `Bearer ${config.auth}`,
'Notion-Version': config.notionVersion ?? '2022-06-28',
'Content-Type': 'application/json',
},
timeout: 30000,
});
// Add response interceptor for error handling
this.client.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response) {
const status = error.response.status;
const data = error.response.data as { message?: string; code?: string };
throw new Error(
`Notion API error (${status}): ${data.message || data.code || 'Unknown error'}`
);
}
throw error;
}
);
}
// Rate limiting helper
private async applyRateLimit(): Promise<void> {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
const minInterval = this.rateLimitWindow / this.rateLimitRequests;
if (timeSinceLastRequest < minInterval) {
await new Promise((resolve) =>
setTimeout(resolve, minInterval - timeSinceLastRequest)
);
}
this.lastRequestTime = Date.now();
}
// Retry helper with exponential backoff
private async retryWithBackoff<T>(
fn: () => Promise<T>,
retries: number = this.maxRetries
): Promise<T> {
try {
await this.applyRateLimit();
return await fn();
} catch (error) {
if (retries === 0) throw error;
const isRetryable =
error instanceof Error &&
(error.message.includes('429') ||
error.message.includes('503') ||
error.message.includes('timeout'));
if (!isRetryable) throw error;
const delay = this.retryDelay * Math.pow(2, this.maxRetries - retries);
await new Promise((resolve) => setTimeout(resolve, delay));
return this.retryWithBackoff(fn, retries - 1);
}
}
// Pagination helper
private async *paginate<T>(
fn: (cursor?: string) => Promise<PaginatedResults<T>>
): AsyncGenerator<T, void, undefined> {
let cursor: string | undefined;
let hasMore = true;
while (hasMore) {
const result = await fn(cursor);
for (const item of result.results) {
yield item;
}
hasMore = result.has_more;
cursor = result.next_cursor ?? undefined;
}
}
// Page methods
async getPage(pageId: PageId): Promise<Page> {
return this.retryWithBackoff(async () => {
const response = await this.client.get<Page>(`/pages/${pageId}`);
return response.data;
});
}
async createPage(params: CreatePageParams): Promise<Page> {
return this.retryWithBackoff(async () => {
const response = await this.client.post<Page>('/pages', params);
return response.data;
});
}
async updatePage(pageId: PageId, params: UpdatePageParams): Promise<Page> {
return this.retryWithBackoff(async () => {
const response = await this.client.patch<Page>(`/pages/${pageId}`, params);
return response.data;
});
}
// Database methods
async getDatabase(databaseId: DatabaseId): Promise<Database> {
return this.retryWithBackoff(async () => {
const response = await this.client.get<Database>(`/databases/${databaseId}`);
return response.data;
});
}
async createDatabase(params: CreateDatabaseParams): Promise<Database> {
return this.retryWithBackoff(async () => {
const response = await this.client.post<Database>('/databases', params);
return response.data;
});
}
async updateDatabase(
databaseId: DatabaseId,
params: Partial<CreateDatabaseParams>
): Promise<Database> {
return this.retryWithBackoff(async () => {
const response = await this.client.patch<Database>(
`/databases/${databaseId}`,
params
);
return response.data;
});
}
async queryDatabase(params: QueryDatabaseParams): Promise<PaginatedResults<Page>> {
return this.retryWithBackoff(async () => {
const { database_id, ...body } = params;
const response = await this.client.post<PaginatedResults<Page>>(
`/databases/${database_id}/query`,
body
);
return response.data;
});
}
async *queryDatabaseAll(params: QueryDatabaseParams): AsyncGenerator<Page> {
yield* this.paginate((cursor) =>
this.queryDatabase({ ...params, start_cursor: cursor })
);
}
// Block methods
async getBlock(blockId: BlockId): Promise<Block> {
return this.retryWithBackoff(async () => {
const response = await this.client.get<Block>(`/blocks/${blockId}`);
return response.data;
});
}
async getBlockChildren(
blockId: BlockId,
start_cursor?: string,
page_size?: number
): Promise<PaginatedResults<Block>> {
return this.retryWithBackoff(async () => {
const response = await this.client.get<PaginatedResults<Block>>(
`/blocks/${blockId}/children`,
{
params: { start_cursor, page_size },
}
);
return response.data;
});
}
async *getBlockChildrenAll(blockId: BlockId): AsyncGenerator<Block> {
yield* this.paginate((cursor) => this.getBlockChildren(blockId, cursor));
}
async appendBlockChildren(
blockId: BlockId,
children: Block[]
): Promise<PaginatedResults<Block>> {
return this.retryWithBackoff(async () => {
const response = await this.client.patch<PaginatedResults<Block>>(
`/blocks/${blockId}/children`,
{ children }
);
return response.data;
});
}
async updateBlock(blockId: BlockId, block: Partial<Block>): Promise<Block> {
return this.retryWithBackoff(async () => {
const response = await this.client.patch<Block>(`/blocks/${blockId}`, block);
return response.data;
});
}
async deleteBlock(blockId: BlockId): Promise<Block> {
return this.retryWithBackoff(async () => {
const response = await this.client.delete<Block>(`/blocks/${blockId}`);
return response.data;
});
}
// User methods
async getUser(userId: UserId): Promise<User> {
return this.retryWithBackoff(async () => {
const response = await this.client.get<User>(`/users/${userId}`);
return response.data;
});
}
async listUsers(
start_cursor?: string,
page_size?: number
): Promise<PaginatedResults<User>> {
return this.retryWithBackoff(async () => {
const response = await this.client.get<PaginatedResults<User>>('/users', {
params: { start_cursor, page_size },
});
return response.data;
});
}
async *listUsersAll(): AsyncGenerator<User> {
yield* this.paginate((cursor) => this.listUsers(cursor));
}
async getBotUser(): Promise<User> {
return this.retryWithBackoff(async () => {
const response = await this.client.get<User>('/users/me');
return response.data;
});
}
// Comment methods
async createComment(params: CreateCommentParams): Promise<Comment> {
return this.retryWithBackoff(async () => {
const response = await this.client.post<Comment>('/comments', params);
return response.data;
});
}
async listComments(
blockId: BlockId,
start_cursor?: string,
page_size?: number
): Promise<PaginatedResults<Comment>> {
return this.retryWithBackoff(async () => {
const response = await this.client.get<PaginatedResults<Comment>>('/comments', {
params: { block_id: blockId, start_cursor, page_size },
});
return response.data;
});
}
async *listCommentsAll(blockId: BlockId): AsyncGenerator<Comment> {
yield* this.paginate((cursor) => this.listComments(blockId, cursor));
}
// Search method
async search(params: SearchParams = {}): Promise<PaginatedResults<SearchResult>> {
return this.retryWithBackoff(async () => {
const response = await this.client.post<PaginatedResults<SearchResult>>(
'/search',
params
);
return response.data;
});
}
async *searchAll(params: SearchParams = {}): AsyncGenerator<SearchResult> {
yield* this.paginate((cursor) => this.search({ ...params, start_cursor: cursor }));
}
}

View File

@ -0,0 +1,49 @@
#!/usr/bin/env node
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { NotionServer } from './server.js';
async function main() {
// Check for required environment variable
const apiKey = process.env.NOTION_API_KEY;
if (!apiKey) {
console.error('Error: NOTION_API_KEY environment variable is required');
process.exit(1);
}
// Create server instance
const server = new NotionServer({ apiKey });
// Create stdio transport
const transport = new StdioServerTransport();
// Connect server to transport
await server.connect(transport);
// Graceful shutdown handlers
const shutdown = async () => {
console.error('Shutting down...');
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
// 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);
});
console.error('Notion MCP server running on stdio');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

View File

@ -0,0 +1,528 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { NotionClient } from './clients/notion.js';
import type {
PageId,
DatabaseId,
BlockId,
UserId,
Block,
PageProperty,
DatabaseProperty,
Filter,
Sort,
} from './types/index.js';
export interface NotionServerConfig {
apiKey: string;
}
export class NotionServer {
private server: Server;
private client: NotionClient;
constructor(config: NotionServerConfig) {
this.client = new NotionClient({ auth: config.apiKey });
this.server = new Server(
{
name: '@mcpengine/notion',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: Tool[] = [
// Pages module
{
name: 'notion_get_page',
description: 'Retrieve a Notion page by ID',
inputSchema: {
type: 'object',
properties: {
page_id: {
type: 'string',
description: 'The ID of the page to retrieve',
},
},
required: ['page_id'],
},
},
{
name: 'notion_create_page',
description: 'Create a new page in a database or as a child of another page',
inputSchema: {
type: 'object',
properties: {
parent_type: {
type: 'string',
enum: ['database_id', 'page_id'],
description: 'Type of parent',
},
parent_id: {
type: 'string',
description: 'ID of the parent database or page',
},
properties: {
type: 'object',
description: 'Page properties as JSON object',
},
children: {
type: 'array',
description: 'Array of block objects to append to the page',
},
},
required: ['parent_type', 'parent_id', 'properties'],
},
},
{
name: 'notion_update_page',
description: 'Update page properties or archive a page',
inputSchema: {
type: 'object',
properties: {
page_id: {
type: 'string',
description: 'The ID of the page to update',
},
properties: {
type: 'object',
description: 'Properties to update',
},
archived: {
type: 'boolean',
description: 'Whether to archive the page',
},
},
required: ['page_id'],
},
},
// Databases module
{
name: 'notion_get_database',
description: 'Retrieve a database by ID',
inputSchema: {
type: 'object',
properties: {
database_id: {
type: 'string',
description: 'The ID of the database',
},
},
required: ['database_id'],
},
},
{
name: 'notion_query_database',
description: 'Query a database with filters and sorting',
inputSchema: {
type: 'object',
properties: {
database_id: {
type: 'string',
description: 'The ID of the database to query',
},
filter: {
type: 'object',
description: 'Filter object (optional)',
},
sorts: {
type: 'array',
description: 'Array of sort objects (optional)',
},
page_size: {
type: 'number',
description: 'Number of results to return (max 100)',
},
},
required: ['database_id'],
},
},
{
name: 'notion_create_database',
description: 'Create a new database',
inputSchema: {
type: 'object',
properties: {
parent_page_id: {
type: 'string',
description: 'ID of the parent page',
},
title: {
type: 'string',
description: 'Database title',
},
properties: {
type: 'object',
description: 'Database schema properties',
},
},
required: ['parent_page_id', 'title', 'properties'],
},
},
// Blocks module
{
name: 'notion_get_block',
description: 'Retrieve a block by ID',
inputSchema: {
type: 'object',
properties: {
block_id: {
type: 'string',
description: 'The ID of the block',
},
},
required: ['block_id'],
},
},
{
name: 'notion_get_block_children',
description: 'Retrieve children blocks of a block',
inputSchema: {
type: 'object',
properties: {
block_id: {
type: 'string',
description: 'The ID of the parent block',
},
page_size: {
type: 'number',
description: 'Number of results to return',
},
},
required: ['block_id'],
},
},
{
name: 'notion_append_block_children',
description: 'Append child blocks to a parent block',
inputSchema: {
type: 'object',
properties: {
block_id: {
type: 'string',
description: 'The ID of the parent block',
},
children: {
type: 'array',
description: 'Array of block objects to append',
},
},
required: ['block_id', 'children'],
},
},
{
name: 'notion_delete_block',
description: 'Delete (archive) a block',
inputSchema: {
type: 'object',
properties: {
block_id: {
type: 'string',
description: 'The ID of the block to delete',
},
},
required: ['block_id'],
},
},
// Users module
{
name: 'notion_get_user',
description: 'Retrieve a user by ID',
inputSchema: {
type: 'object',
properties: {
user_id: {
type: 'string',
description: 'The ID of the user',
},
},
required: ['user_id'],
},
},
{
name: 'notion_list_users',
description: 'List all users in the workspace',
inputSchema: {
type: 'object',
properties: {
page_size: {
type: 'number',
description: 'Number of results to return',
},
},
},
},
// Comments module
{
name: 'notion_create_comment',
description: 'Add a comment to a page or block',
inputSchema: {
type: 'object',
properties: {
parent_type: {
type: 'string',
enum: ['page_id', 'block_id'],
description: 'Type of parent',
},
parent_id: {
type: 'string',
description: 'ID of the parent page or block',
},
text: {
type: 'string',
description: 'Comment text content',
},
},
required: ['parent_type', 'parent_id', 'text'],
},
},
{
name: 'notion_list_comments',
description: 'Retrieve comments for a block',
inputSchema: {
type: 'object',
properties: {
block_id: {
type: 'string',
description: 'The ID of the block',
},
},
required: ['block_id'],
},
},
// Search module
{
name: 'notion_search',
description: 'Search all pages and databases',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query text',
},
filter: {
type: 'string',
enum: ['page', 'database'],
description: 'Filter by object type',
},
sort_direction: {
type: 'string',
enum: ['ascending', 'descending'],
description: 'Sort direction for last_edited_time',
},
},
},
},
];
return { tools };
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (!args) {
throw new Error('Missing required arguments');
}
switch (name) {
// Pages
case 'notion_get_page': {
const page = await this.client.getPage(args.page_id as PageId);
return {
content: [{ type: 'text', text: JSON.stringify(page, null, 2) }],
};
}
case 'notion_create_page': {
const parent =
args.parent_type === 'database_id'
? { database_id: args.parent_id as DatabaseId }
: { page_id: args.parent_id as PageId };
const page = await this.client.createPage({
parent,
properties: args.properties as Record<string, Partial<PageProperty>>,
children: args.children as Block[] | undefined,
});
return {
content: [{ type: 'text', text: JSON.stringify(page, null, 2) }],
};
}
case 'notion_update_page': {
const page = await this.client.updatePage(args.page_id as PageId, {
properties: args.properties as Record<string, Partial<PageProperty>> | undefined,
archived: args.archived as boolean | undefined,
});
return {
content: [{ type: 'text', text: JSON.stringify(page, null, 2) }],
};
}
// Databases
case 'notion_get_database': {
const db = await this.client.getDatabase(args.database_id as DatabaseId);
return {
content: [{ type: 'text', text: JSON.stringify(db, null, 2) }],
};
}
case 'notion_query_database': {
const results = await this.client.queryDatabase({
database_id: args.database_id as DatabaseId,
filter: args.filter as Filter | undefined,
sorts: args.sorts as Sort[] | undefined,
page_size: args.page_size as number | undefined,
});
return {
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
};
}
case 'notion_create_database': {
const db = await this.client.createDatabase({
parent: { page_id: args.parent_page_id as PageId },
title: [{ type: 'text', text: { content: args.title as string } }],
properties: args.properties as Record<string, DatabaseProperty>,
});
return {
content: [{ type: 'text', text: JSON.stringify(db, null, 2) }],
};
}
// Blocks
case 'notion_get_block': {
const block = await this.client.getBlock(args.block_id as BlockId);
return {
content: [{ type: 'text', text: JSON.stringify(block, null, 2) }],
};
}
case 'notion_get_block_children': {
const children = await this.client.getBlockChildren(
args.block_id as BlockId,
undefined,
args.page_size as number | undefined
);
return {
content: [{ type: 'text', text: JSON.stringify(children, null, 2) }],
};
}
case 'notion_append_block_children': {
const result = await this.client.appendBlockChildren(
args.block_id as BlockId,
args.children as Block[]
);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
}
case 'notion_delete_block': {
const block = await this.client.deleteBlock(args.block_id as BlockId);
return {
content: [{ type: 'text', text: JSON.stringify(block, null, 2) }],
};
}
// Users
case 'notion_get_user': {
const user = await this.client.getUser(args.user_id as UserId);
return {
content: [{ type: 'text', text: JSON.stringify(user, null, 2) }],
};
}
case 'notion_list_users': {
const users = await this.client.listUsers(undefined, args.page_size as number | undefined);
return {
content: [{ type: 'text', text: JSON.stringify(users, null, 2) }],
};
}
// Comments
case 'notion_create_comment': {
const parent =
args.parent_type === 'page_id'
? { page_id: args.parent_id as PageId }
: { block_id: args.parent_id as BlockId };
const comment = await this.client.createComment({
parent,
rich_text: [{ type: 'text', text: { content: args.text as string } }],
});
return {
content: [{ type: 'text', text: JSON.stringify(comment, null, 2) }],
};
}
case 'notion_list_comments': {
const comments = await this.client.listComments(args.block_id as BlockId);
return {
content: [{ type: 'text', text: JSON.stringify(comments, null, 2) }],
};
}
// Search
case 'notion_search': {
const params: any = {};
if (args.query) params.query = args.query;
if (args.filter) {
params.filter = { value: args.filter, property: 'object' };
}
if (args.sort_direction) {
params.sort = {
direction: args.sort_direction,
timestamp: 'last_edited_time',
};
}
const results = await this.client.search(params);
return {
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred';
return {
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
isError: true,
};
}
});
}
async connect(transport: StdioServerTransport): Promise<void> {
await this.server.connect(transport);
}
getServer(): Server {
return this.server;
}
}

View File

@ -0,0 +1,742 @@
// Branded types for IDs
export type PageId = string & { readonly __brand: 'PageId' };
export type DatabaseId = string & { readonly __brand: 'DatabaseId' };
export type BlockId = string & { readonly __brand: 'BlockId' };
export type UserId = string & { readonly __brand: 'UserId' };
export type CommentId = string & { readonly __brand: 'CommentId' };
// Color types
export type Color =
| 'default'
| 'gray'
| 'brown'
| 'orange'
| 'yellow'
| 'green'
| 'blue'
| 'purple'
| 'pink'
| 'red'
| 'gray_background'
| 'brown_background'
| 'orange_background'
| 'yellow_background'
| 'green_background'
| 'blue_background'
| 'purple_background'
| 'pink_background'
| 'red_background';
// RichText types
export interface RichTextAnnotations {
bold: boolean;
italic: boolean;
strikethrough: boolean;
underline: boolean;
code: boolean;
color: Color;
}
export type RichText =
| {
type: 'text';
text: {
content: string;
link: { url: string } | null;
};
annotations: RichTextAnnotations;
plain_text: string;
href: string | null;
}
| {
type: 'mention';
mention:
| { type: 'user'; user: User }
| { type: 'page'; page: { id: PageId } }
| { type: 'database'; database: { id: DatabaseId } }
| { type: 'date'; date: DateValue }
| { type: 'link_preview'; link_preview: { url: string } }
| { type: 'template_mention'; template_mention: { type: 'template_mention_date' | 'template_mention_user' } };
annotations: RichTextAnnotations;
plain_text: string;
href: string | null;
}
| {
type: 'equation';
equation: {
expression: string;
};
annotations: RichTextAnnotations;
plain_text: string;
href: string | null;
};
// Date types
export interface DateValue {
start: string;
end: string | null;
time_zone: string | null;
}
// User types
export type User =
| {
object: 'user';
id: UserId;
type: 'person';
person: { email: string };
name: string | null;
avatar_url: string | null;
}
| {
object: 'user';
id: UserId;
type: 'bot';
bot: Record<string, unknown> | { owner: { type: 'workspace'; workspace: true } | { type: 'user'; user: User } };
name: string | null;
avatar_url: string | null;
};
export type Person = Extract<User, { type: 'person' }>;
export type Bot = Extract<User, { type: 'bot' }>;
// Parent types
export type Parent =
| { type: 'database_id'; database_id: DatabaseId }
| { type: 'page_id'; page_id: PageId }
| { type: 'workspace'; workspace: true }
| { type: 'block_id'; block_id: BlockId };
// File types
export type FileObject =
| {
type: 'external';
external: { url: string };
name?: string;
}
| {
type: 'file';
file: { url: string; expiry_time: string };
name?: string;
};
// Emoji and Icon types
export type Icon =
| { type: 'emoji'; emoji: string }
| { type: 'external'; external: { url: string } }
| { type: 'file'; file: { url: string; expiry_time: string } };
// Property value types for Pages
export type PageProperty =
| { id: string; type: 'title'; title: RichText[] }
| { id: string; type: 'rich_text'; rich_text: RichText[] }
| { id: string; type: 'number'; number: number | null }
| { id: string; type: 'select'; select: { id: string; name: string; color: Color } | null }
| { id: string; type: 'multi_select'; multi_select: Array<{ id: string; name: string; color: Color }> }
| { id: string; type: 'date'; date: DateValue | null }
| { id: string; type: 'people'; people: User[] }
| { id: string; type: 'files'; files: FileObject[] }
| { id: string; type: 'checkbox'; checkbox: boolean }
| { id: string; type: 'url'; url: string | null }
| { id: string; type: 'email'; email: string | null }
| { id: string; type: 'phone_number'; phone_number: string | null }
| { id: string; type: 'formula'; formula: { type: 'string'; string: string | null } | { type: 'number'; number: number | null } | { type: 'boolean'; boolean: boolean | null } | { type: 'date'; date: DateValue | null } }
| { id: string; type: 'relation'; relation: Array<{ id: PageId }> }
| { id: string; type: 'rollup'; rollup: { type: 'number'; number: number | null; function: string } | { type: 'date'; date: DateValue | null; function: string } | { type: 'array'; array: PageProperty[]; function: string } }
| { id: string; type: 'created_time'; created_time: string }
| { id: string; type: 'created_by'; created_by: User }
| { id: string; type: 'last_edited_time'; last_edited_time: string }
| { id: string; type: 'last_edited_by'; last_edited_by: User }
| { id: string; type: 'status'; status: { id: string; name: string; color: Color } | null }
| { id: string; type: 'unique_id'; unique_id: { number: number; prefix: string | null } };
// Database property schema types
export type DatabaseProperty =
| { id: string; name: string; type: 'title'; title: Record<string, unknown> }
| { id: string; name: string; type: 'rich_text'; rich_text: Record<string, unknown> }
| { id: string; name: string; type: 'number'; number: { format: 'number' | 'number_with_commas' | 'percent' | 'dollar' | 'canadian_dollar' | 'euro' | 'pound' | 'yen' | 'ruble' | 'rupee' | 'won' | 'yuan' | 'real' | 'lira' | 'rupiah' | 'franc' | 'hong_kong_dollar' | 'new_zealand_dollar' | 'krona' | 'norwegian_krone' | 'mexican_peso' | 'rand' | 'new_taiwan_dollar' | 'danish_krone' | 'zloty' | 'baht' | 'forint' | 'koruna' | 'shekel' | 'chilean_peso' | 'philippine_peso' | 'dirham' | 'colombian_peso' | 'riyal' | 'ringgit' | 'leu' | 'argentine_peso' | 'uruguayan_peso' } }
| { id: string; name: string; type: 'select'; select: { options: Array<{ id: string; name: string; color: Color }> } }
| { id: string; name: string; type: 'multi_select'; multi_select: { options: Array<{ id: string; name: string; color: Color }> } }
| { id: string; name: string; type: 'date'; date: Record<string, unknown> }
| { id: string; name: string; type: 'people'; people: Record<string, unknown> }
| { id: string; name: string; type: 'files'; files: Record<string, unknown> }
| { id: string; name: string; type: 'checkbox'; checkbox: Record<string, unknown> }
| { id: string; name: string; type: 'url'; url: Record<string, unknown> }
| { id: string; name: string; type: 'email'; email: Record<string, unknown> }
| { id: string; name: string; type: 'phone_number'; phone_number: Record<string, unknown> }
| { id: string; name: string; type: 'formula'; formula: { expression: string } }
| { id: string; name: string; type: 'relation'; relation: { database_id: DatabaseId; synced_property_id?: string; synced_property_name?: string } }
| { id: string; name: string; type: 'rollup'; rollup: { relation_property_id: string; relation_property_name: string; rollup_property_id: string; rollup_property_name: string; function: 'count_all' | 'count_values' | 'count_unique_values' | 'count_empty' | 'count_not_empty' | 'percent_empty' | 'percent_not_empty' | 'sum' | 'average' | 'median' | 'min' | 'max' | 'range' | 'show_original' } }
| { id: string; name: string; type: 'created_time'; created_time: Record<string, unknown> }
| { id: string; name: string; type: 'created_by'; created_by: Record<string, unknown> }
| { id: string; name: string; type: 'last_edited_time'; last_edited_time: Record<string, unknown> }
| { id: string; name: string; type: 'last_edited_by'; last_edited_by: Record<string, unknown> }
| { id: string; name: string; type: 'status'; status: { options: Array<{ id: string; name: string; color: Color }>; groups: Array<{ id: string; name: string; color: Color; option_ids: string[] }> } }
| { id: string; name: string; type: 'unique_id'; unique_id: { prefix: string | null } };
// Block types - comprehensive discriminated union
export type Block =
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'paragraph';
paragraph: {
rich_text: RichText[];
color: Color;
children?: Block[];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'heading_1';
heading_1: {
rich_text: RichText[];
color: Color;
is_toggleable: boolean;
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'heading_2';
heading_2: {
rich_text: RichText[];
color: Color;
is_toggleable: boolean;
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'heading_3';
heading_3: {
rich_text: RichText[];
color: Color;
is_toggleable: boolean;
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'bulleted_list_item';
bulleted_list_item: {
rich_text: RichText[];
color: Color;
children?: Block[];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'numbered_list_item';
numbered_list_item: {
rich_text: RichText[];
color: Color;
children?: Block[];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'to_do';
to_do: {
rich_text: RichText[];
checked: boolean;
color: Color;
children?: Block[];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'toggle';
toggle: {
rich_text: RichText[];
color: Color;
children?: Block[];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'code';
code: {
rich_text: RichText[];
caption: RichText[];
language: string;
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'callout';
callout: {
rich_text: RichText[];
icon: Icon;
color: Color;
children?: Block[];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'quote';
quote: {
rich_text: RichText[];
color: Color;
children?: Block[];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'divider';
divider: Record<string, unknown>;
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'table_of_contents';
table_of_contents: {
color: Color;
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'breadcrumb';
breadcrumb: Record<string, unknown>;
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'image';
image: FileObject & { caption?: RichText[] };
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'video';
video: FileObject & { caption?: RichText[] };
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'file';
file: FileObject & { caption?: RichText[] };
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'pdf';
pdf: FileObject & { caption?: RichText[] };
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'bookmark';
bookmark: {
url: string;
caption: RichText[];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'embed';
embed: {
url: string;
caption?: RichText[];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'link_preview';
link_preview: {
url: string;
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'equation';
equation: {
expression: string;
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'table';
table: {
table_width: number;
has_column_header: boolean;
has_row_header: boolean;
children?: Block[];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'table_row';
table_row: {
cells: RichText[][];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'column_list';
column_list: Record<string, unknown>;
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'column';
column: Record<string, unknown>;
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'synced_block';
synced_block: {
synced_from: { block_id: BlockId } | null;
children?: Block[];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'template';
template: {
rich_text: RichText[];
children?: Block[];
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'child_page';
child_page: {
title: string;
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'child_database';
child_database: {
title: string;
};
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
}
| {
object: 'block';
id: BlockId;
parent: Parent;
type: 'unsupported';
unsupported: Record<string, unknown>;
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
has_children: boolean;
archived: boolean;
};
// Page type
export interface Page {
object: 'page';
id: PageId;
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
archived: boolean;
icon: Icon | null;
cover: FileObject | null;
properties: Record<string, PageProperty>;
parent: Parent;
url: string;
}
// Database type
export interface Database {
object: 'database';
id: DatabaseId;
created_time: string;
created_by: User;
last_edited_time: string;
last_edited_by: User;
title: RichText[];
description: RichText[];
icon: Icon | null;
cover: FileObject | null;
properties: Record<string, DatabaseProperty>;
parent: Parent;
url: string;
archived: boolean;
is_inline: boolean;
}
// Comment type
export interface Comment {
object: 'comment';
id: CommentId;
parent: { type: 'page_id'; page_id: PageId } | { type: 'block_id'; block_id: BlockId };
discussion_id: string;
created_time: string;
created_by: User;
last_edited_time: string;
rich_text: RichText[];
}
// Search result type
export type SearchResult = Page | Database;
// Pagination types
export interface PaginatedResults<T> {
object: 'list';
results: T[];
next_cursor: string | null;
has_more: boolean;
type?: string;
}
// Filter types for database queries
export type PropertyFilter =
| { property: string; rich_text: { equals?: string; does_not_equal?: string; contains?: string; does_not_contain?: string; starts_with?: string; ends_with?: string; is_empty?: boolean; is_not_empty?: boolean } }
| { property: string; number: { equals?: number; does_not_equal?: number; greater_than?: number; less_than?: number; greater_than_or_equal_to?: number; less_than_or_equal_to?: number; is_empty?: boolean; is_not_empty?: boolean } }
| { property: string; checkbox: { equals?: boolean; does_not_equal?: boolean } }
| { property: string; select: { equals?: string; does_not_equal?: string; is_empty?: boolean; is_not_empty?: boolean } }
| { property: string; multi_select: { contains?: string; does_not_contain?: string; is_empty?: boolean; is_not_empty?: boolean } }
| { property: string; status: { equals?: string; does_not_equal?: string; is_empty?: boolean; is_not_empty?: boolean } }
| { property: string; date: { equals?: string; before?: string; after?: string; on_or_before?: string; on_or_after?: string; is_empty?: boolean; is_not_empty?: boolean; past_week?: Record<string, unknown>; past_month?: Record<string, unknown>; past_year?: Record<string, unknown>; next_week?: Record<string, unknown>; next_month?: Record<string, unknown>; next_year?: Record<string, unknown> } }
| { property: string; people: { contains?: UserId; does_not_contain?: UserId; is_empty?: boolean; is_not_empty?: boolean } }
| { property: string; files: { is_empty?: boolean; is_not_empty?: boolean } }
| { property: string; relation: { contains?: PageId; does_not_contain?: PageId; is_empty?: boolean; is_not_empty?: boolean } }
| { property: string; formula: { string?: { equals?: string; does_not_equal?: string; contains?: string; does_not_contain?: string; starts_with?: string; ends_with?: string; is_empty?: boolean; is_not_empty?: boolean }; checkbox?: { equals?: boolean; does_not_equal?: boolean }; number?: { equals?: number; does_not_equal?: number; greater_than?: number; less_than?: number; greater_than_or_equal_to?: number; less_than_or_equal_to?: number; is_empty?: boolean; is_not_empty?: boolean }; date?: { equals?: string; before?: string; after?: string; on_or_before?: string; on_or_after?: string; is_empty?: boolean; is_not_empty?: boolean } } };
export type CompoundFilter =
| { and: Array<PropertyFilter | CompoundFilter> }
| { or: Array<PropertyFilter | CompoundFilter> };
export type Filter = PropertyFilter | CompoundFilter;
// Sort types
export interface Sort {
property?: string;
timestamp?: 'created_time' | 'last_edited_time';
direction: 'ascending' | 'descending';
}

View File

@ -0,0 +1,23 @@
{
"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",
"allowSyntheticDefaultImports": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

19
servers/xero/.env.example Normal file
View File

@ -0,0 +1,19 @@
# Xero API Configuration
# Required: OAuth2 Bearer token
XERO_ACCESS_TOKEN=your_access_token_here
# Required: Tenant ID (organization ID)
XERO_TENANT_ID=your_tenant_id_here
# Optional: Custom base URL (default: https://api.xero.com/api.xro/2.0)
# XERO_BASE_URL=https://api.xero.com/api.xro/2.0
# Optional: Request timeout in milliseconds (default: 30000)
# XERO_TIMEOUT=30000
# Optional: Number of retry attempts (default: 3)
# XERO_RETRY_ATTEMPTS=3
# Optional: Retry delay in milliseconds (default: 1000)
# XERO_RETRY_DELAY_MS=1000

39
servers/xero/.gitignore vendored Normal file
View File

@ -0,0 +1,39 @@
# Dependencies
node_modules/
package-lock.json
yarn.lock
pnpm-lock.yaml
# Build output
dist/
build/
*.tsbuildinfo
# Environment variables
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Testing
coverage/
.nyc_output/
# Misc
.cache/
tmp/
temp/

236
servers/xero/README.md Normal file
View File

@ -0,0 +1,236 @@
# Xero MCP Server
Model Context Protocol (MCP) server for Xero Accounting API integration.
## Features
- **Complete Xero Accounting API coverage**: Invoices, Bills, Contacts, Accounts, Payments, Bank Transactions, Credit Notes, Purchase Orders, Quotes, Manual Journals, Reports, and more
- **OAuth2 authentication** with Bearer token
- **Rate limiting**: Respects Xero's 60 calls/min and 5000 calls/day limits
- **Automatic retry** with exponential backoff
- **Pagination support**: page/pageSize parameters (max 100 per page)
- **If-Modified-Since** header support for efficient polling
- **Comprehensive TypeScript types** with branded IDs (GUIDs)
- **Lazy-loaded tools** for optimal performance
- **Dual transport** support (stdio/SSE)
- **Graceful shutdown**
## Installation
```bash
npm install
npm run build
```
## Configuration
Create a `.env` file from `.env.example`:
```bash
cp .env.example .env
```
### Required Environment Variables
- `XERO_ACCESS_TOKEN`: OAuth2 Bearer token
- `XERO_TENANT_ID`: Xero organization/tenant ID
### Optional Environment Variables
- `XERO_BASE_URL`: Custom API base URL (default: `https://api.xero.com/api.xro/2.0`)
- `XERO_TIMEOUT`: Request timeout in milliseconds (default: `30000`)
- `XERO_RETRY_ATTEMPTS`: Number of retry attempts (default: `3`)
- `XERO_RETRY_DELAY_MS`: Retry delay in milliseconds (default: `1000`)
## Usage
### Running the Server
```bash
npm start
```
Or with MCP client configuration:
```json
{
"mcpServers": {
"xero": {
"command": "node",
"args": ["/path/to/xero/dist/main.js"],
"env": {
"XERO_ACCESS_TOKEN": "your_token",
"XERO_TENANT_ID": "your_tenant_id"
}
}
}
}
```
## Available Tools
### Invoices
- `xero_list_invoices` - List all invoices with filtering
- `xero_get_invoice` - Get invoice by ID
- `xero_create_invoice` - Create new invoice
- `xero_update_invoice` - Update existing invoice
### Bills (Payable Invoices)
- `xero_list_bills` - List all bills
- `xero_create_bill` - Create new bill
### Contacts
- `xero_list_contacts` - List all contacts
- `xero_get_contact` - Get contact by ID
- `xero_create_contact` - Create new contact
- `xero_update_contact` - Update existing contact
### Accounts (Chart of Accounts)
- `xero_list_accounts` - List all accounts
- `xero_get_account` - Get account by ID
- `xero_create_account` - Create new account
### Bank Transactions
- `xero_list_bank_transactions` - List all bank transactions
- `xero_get_bank_transaction` - Get bank transaction by ID
- `xero_create_bank_transaction` - Create new bank transaction
### Payments
- `xero_list_payments` - List all payments
- `xero_create_payment` - Create new payment
### Credit Notes
- `xero_list_credit_notes` - List all credit notes
- `xero_create_credit_note` - Create new credit note
### Purchase Orders
- `xero_list_purchase_orders` - List all purchase orders
- `xero_create_purchase_order` - Create new purchase order
### Quotes
- `xero_list_quotes` - List all quotes
- `xero_create_quote` - Create new quote
### Reports
- `xero_get_balance_sheet` - Get balance sheet report
- `xero_get_profit_and_loss` - Get profit & loss report
- `xero_get_trial_balance` - Get trial balance report
- `xero_get_bank_summary` - Get bank summary report
### Other
- `xero_list_employees` - List all employees
- `xero_list_tax_rates` - List all tax rates
- `xero_list_items` - List inventory/service items
- `xero_create_item` - Create new item
- `xero_get_organisation` - Get organisation details
- `xero_list_tracking_categories` - List tracking categories
## Filtering & Pagination
Most list endpoints support:
- `page`: Page number (default: 1)
- `pageSize`: Records per page (max: 100)
- `where`: Filter expression (e.g., `Status=="AUTHORISED"`)
- `order`: Sort order (e.g., `InvoiceNumber DESC`)
- `includeArchived`: Include archived records
Example:
```json
{
"name": "xero_list_invoices",
"arguments": {
"where": "Status==\"AUTHORISED\" AND AmountDue > 0",
"order": "DueDate ASC",
"page": 1,
"pageSize": 50
}
}
```
## Rate Limits
Xero enforces the following rate limits:
- **60 calls/minute** per connection
- **5000 calls/day** per connection
This server automatically handles rate limiting and will queue requests as needed.
## Authentication
This server uses OAuth2 Bearer token authentication. You'll need to:
1. Register your app in the Xero Developer Portal
2. Complete the OAuth2 flow to obtain an access token
3. Get the tenant ID from the connections endpoint
4. Set both values in your environment variables
**Note**: Access tokens expire after 30 minutes. You'll need to implement token refresh logic in your application.
## Error Handling
All errors are returned in the MCP tool response format:
```json
{
"content": [
{
"type": "text",
"text": "{\"error\": \"Error message here\"}"
}
],
"isError": true
}
```
## Development
### Type Checking
```bash
npm run typecheck
```
### Build
```bash
npm run build
```
### Watch Mode
```bash
npm run dev
```
## API Coverage
This server covers the Xero Accounting API including:
- ✅ Invoices (receivable/payable)
- ✅ Contacts & Contact Groups
- ✅ Chart of Accounts
- ✅ Bank Transactions & Transfers
- ✅ Payments, Prepayments, Overpayments
- ✅ Credit Notes
- ✅ Purchase Orders
- ✅ Quotes
- ✅ Manual Journals
- ✅ Items (inventory/service)
- ✅ Tax Rates
- ✅ Employees (basic)
- ✅ Organisation Details
- ✅ Tracking Categories
- ✅ Branding Themes
- ✅ Financial Reports
- ✅ Attachments
## License
MIT
## Links
- [Xero API Documentation](https://developer.xero.com/documentation/api/accounting/overview)
- [Model Context Protocol](https://modelcontextprotocol.io)
- [MCP SDK](https://github.com/modelcontextprotocol/sdk)

37
servers/xero/package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "@mcpengine/xero",
"version": "1.0.0",
"description": "MCP server for Xero Accounting API integration",
"type": "module",
"main": "dist/main.js",
"types": "dist/main.d.ts",
"bin": {
"xero-mcp": "./dist/main.js"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"prepare": "npm run build",
"typecheck": "tsc --noEmit"
},
"keywords": [
"mcp",
"xero",
"accounting",
"api"
],
"author": "",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"axios": "^1.7.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@ -0,0 +1,808 @@
/**
* Xero API Client
* Handles authentication, rate limiting, pagination, and all CRUD operations
*/
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
import type {
XeroClientConfig,
XeroRequestOptions,
XeroApiResponse,
XeroErrorResponse,
Invoice,
Contact,
Account,
BankTransaction,
Payment,
CreditNote,
PurchaseOrder,
Quote,
ManualJournal,
Item,
TaxRate,
Employee,
Organisation,
TrackingCategory,
BrandingTheme,
ContactGroup,
Prepayment,
Overpayment,
BankTransfer,
Attachment,
Report,
InvoiceId,
ContactId,
AccountId,
BankTransactionId,
PaymentId,
CreditNoteId,
PurchaseOrderId,
QuoteId,
ManualJournalId,
ItemId,
EmployeeId,
TrackingCategoryId,
PrepaymentId,
OverpaymentId,
BankTransferId
} from '../types/index.js';
export class XeroClient {
private client: AxiosInstance;
private config: XeroClientConfig;
private requestCount = 0;
private requestWindowStart = Date.now();
private readonly RATE_LIMIT_PER_MINUTE = 60;
private readonly RATE_LIMIT_PER_DAY = 5000;
private dailyRequestCount = 0;
private dailyWindowStart = Date.now();
constructor(config: XeroClientConfig) {
this.config = {
baseUrl: 'https://api.xero.com/api.xro/2.0',
timeout: 30000,
retryAttempts: 3,
retryDelayMs: 1000,
...config
};
this.client = axios.create({
baseURL: this.config.baseUrl,
timeout: this.config.timeout,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'xero-tenant-id': this.config.tenantId,
'Authorization': `Bearer ${this.config.accessToken}`
}
});
// Response interceptor for error handling
this.client.interceptors.response.use(
response => response,
error => this.handleError(error)
);
}
/**
* Rate limiting: 60 calls/min, 5000/day
*/
private async checkRateLimit(): Promise<void> {
const now = Date.now();
const minuteElapsed = now - this.requestWindowStart;
const dayElapsed = now - this.dailyWindowStart;
// Reset minute window
if (minuteElapsed >= 60000) {
this.requestCount = 0;
this.requestWindowStart = now;
}
// Reset daily window
if (dayElapsed >= 86400000) {
this.dailyRequestCount = 0;
this.dailyWindowStart = now;
}
// Check limits
if (this.requestCount >= this.RATE_LIMIT_PER_MINUTE) {
const waitTime = 60000 - minuteElapsed;
await this.sleep(waitTime);
this.requestCount = 0;
this.requestWindowStart = Date.now();
}
if (this.dailyRequestCount >= this.RATE_LIMIT_PER_DAY) {
throw new Error('Daily rate limit exceeded (5000 requests/day)');
}
this.requestCount++;
this.dailyRequestCount++;
}
/**
* Handle API errors with retry logic
*/
private async handleError(error: AxiosError): Promise<never> {
if (error.response?.status === 429) {
// Rate limit hit - wait and retry
const retryAfter = parseInt(error.response.headers['retry-after'] || '60', 10);
await this.sleep(retryAfter * 1000);
throw error; // Will be retried by makeRequest
}
const xeroError = error.response?.data as XeroErrorResponse | undefined;
if (xeroError) {
const message = xeroError.Detail || xeroError.Title || 'Unknown Xero API error';
throw new Error(`Xero API Error: ${message}`);
}
throw error;
}
/**
* Make an API request with retry logic
*/
private async makeRequest<T>(
config: AxiosRequestConfig,
options?: XeroRequestOptions,
attempt = 1
): Promise<T> {
await this.checkRateLimit();
try {
// Add If-Modified-Since header if provided
if (options?.ifModifiedSince) {
config.headers = {
...config.headers,
'If-Modified-Since': options.ifModifiedSince.toUTCString()
};
}
// Add query parameters
if (options) {
const params: Record<string, string | number | boolean> = {};
if (options.page !== undefined) params.page = options.page;
if (options.pageSize !== undefined) params.pageSize = options.pageSize;
if (options.where) params.where = options.where;
if (options.order) params.order = options.order;
if (options.includeArchived) params.includeArchived = true;
if (options.summarizeErrors) params.summarizeErrors = true;
config.params = { ...config.params, ...params };
}
const response = await this.client.request<XeroApiResponse<T>>(config);
// Extract the actual data from the Xero response wrapper
// Xero wraps responses in objects like { Invoices: [...] }
const data = response.data;
const keys = Object.keys(data).filter(k => !['Id', 'Status', 'ProviderName', 'DateTimeUTC'].includes(k));
const dataKey = keys[0];
return (dataKey ? data[dataKey] : data) as T;
} catch (error) {
if (attempt < (this.config.retryAttempts || 3)) {
await this.sleep((this.config.retryDelayMs || 1000) * attempt);
return this.makeRequest<T>(config, options, attempt + 1);
}
throw error;
}
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ==================== INVOICES ====================
async getInvoices(options?: XeroRequestOptions): Promise<Invoice[]> {
return this.makeRequest<Invoice[]>({ method: 'GET', url: '/Invoices' }, options);
}
async getInvoice(invoiceId: InvoiceId): Promise<Invoice> {
const invoices = await this.makeRequest<Invoice[]>({
method: 'GET',
url: `/Invoices/${invoiceId}`
});
return invoices[0];
}
async createInvoices(invoices: Invoice[]): Promise<Invoice[]> {
return this.makeRequest<Invoice[]>({
method: 'PUT',
url: '/Invoices',
data: { Invoices: invoices }
});
}
async createInvoice(invoice: Invoice): Promise<Invoice> {
const result = await this.createInvoices([invoice]);
return result[0];
}
async updateInvoice(invoiceId: InvoiceId, invoice: Partial<Invoice>): Promise<Invoice> {
const invoices = await this.makeRequest<Invoice[]>({
method: 'POST',
url: `/Invoices/${invoiceId}`,
data: { Invoices: [invoice] }
});
return invoices[0];
}
async deleteInvoice(invoiceId: InvoiceId): Promise<void> {
await this.updateInvoice(invoiceId, { Status: 'DELETED' as any });
}
// ==================== CONTACTS ====================
async getContacts(options?: XeroRequestOptions): Promise<Contact[]> {
return this.makeRequest<Contact[]>({ method: 'GET', url: '/Contacts' }, options);
}
async getContact(contactId: ContactId): Promise<Contact> {
const contacts = await this.makeRequest<Contact[]>({
method: 'GET',
url: `/Contacts/${contactId}`
});
return contacts[0];
}
async createContacts(contacts: Contact[]): Promise<Contact[]> {
return this.makeRequest<Contact[]>({
method: 'PUT',
url: '/Contacts',
data: { Contacts: contacts }
});
}
async createContact(contact: Contact): Promise<Contact> {
const result = await this.createContacts([contact]);
return result[0];
}
async updateContact(contactId: ContactId, contact: Partial<Contact>): Promise<Contact> {
const contacts = await this.makeRequest<Contact[]>({
method: 'POST',
url: `/Contacts/${contactId}`,
data: { Contacts: [contact] }
});
return contacts[0];
}
// ==================== ACCOUNTS ====================
async getAccounts(options?: XeroRequestOptions): Promise<Account[]> {
return this.makeRequest<Account[]>({ method: 'GET', url: '/Accounts' }, options);
}
async getAccount(accountId: AccountId): Promise<Account> {
const accounts = await this.makeRequest<Account[]>({
method: 'GET',
url: `/Accounts/${accountId}`
});
return accounts[0];
}
async createAccount(account: Account): Promise<Account> {
const accounts = await this.makeRequest<Account[]>({
method: 'PUT',
url: '/Accounts',
data: { Accounts: [account] }
});
return accounts[0];
}
async updateAccount(accountId: AccountId, account: Partial<Account>): Promise<Account> {
const accounts = await this.makeRequest<Account[]>({
method: 'POST',
url: `/Accounts/${accountId}`,
data: { Accounts: [account] }
});
return accounts[0];
}
async deleteAccount(accountId: AccountId): Promise<void> {
await this.updateAccount(accountId, { Status: 'DELETED' });
}
// ==================== BANK TRANSACTIONS ====================
async getBankTransactions(options?: XeroRequestOptions): Promise<BankTransaction[]> {
return this.makeRequest<BankTransaction[]>({
method: 'GET',
url: '/BankTransactions'
}, options);
}
async getBankTransaction(bankTransactionId: BankTransactionId): Promise<BankTransaction> {
const transactions = await this.makeRequest<BankTransaction[]>({
method: 'GET',
url: `/BankTransactions/${bankTransactionId}`
});
return transactions[0];
}
async createBankTransactions(transactions: BankTransaction[]): Promise<BankTransaction[]> {
return this.makeRequest<BankTransaction[]>({
method: 'PUT',
url: '/BankTransactions',
data: { BankTransactions: transactions }
});
}
async createBankTransaction(transaction: BankTransaction): Promise<BankTransaction> {
const result = await this.createBankTransactions([transaction]);
return result[0];
}
async updateBankTransaction(
bankTransactionId: BankTransactionId,
transaction: Partial<BankTransaction>
): Promise<BankTransaction> {
const transactions = await this.makeRequest<BankTransaction[]>({
method: 'POST',
url: `/BankTransactions/${bankTransactionId}`,
data: { BankTransactions: [transaction] }
});
return transactions[0];
}
// ==================== PAYMENTS ====================
async getPayments(options?: XeroRequestOptions): Promise<Payment[]> {
return this.makeRequest<Payment[]>({ method: 'GET', url: '/Payments' }, options);
}
async getPayment(paymentId: PaymentId): Promise<Payment> {
const payments = await this.makeRequest<Payment[]>({
method: 'GET',
url: `/Payments/${paymentId}`
});
return payments[0];
}
async createPayments(payments: Payment[]): Promise<Payment[]> {
return this.makeRequest<Payment[]>({
method: 'PUT',
url: '/Payments',
data: { Payments: payments }
});
}
async createPayment(payment: Payment): Promise<Payment> {
const result = await this.createPayments([payment]);
return result[0];
}
async deletePayment(paymentId: PaymentId): Promise<void> {
await this.makeRequest({
method: 'POST',
url: `/Payments/${paymentId}`,
data: { Payments: [{ Status: 'DELETED' }] }
});
}
// ==================== CREDIT NOTES ====================
async getCreditNotes(options?: XeroRequestOptions): Promise<CreditNote[]> {
return this.makeRequest<CreditNote[]>({ method: 'GET', url: '/CreditNotes' }, options);
}
async getCreditNote(creditNoteId: CreditNoteId): Promise<CreditNote> {
const creditNotes = await this.makeRequest<CreditNote[]>({
method: 'GET',
url: `/CreditNotes/${creditNoteId}`
});
return creditNotes[0];
}
async createCreditNotes(creditNotes: CreditNote[]): Promise<CreditNote[]> {
return this.makeRequest<CreditNote[]>({
method: 'PUT',
url: '/CreditNotes',
data: { CreditNotes: creditNotes }
});
}
async createCreditNote(creditNote: CreditNote): Promise<CreditNote> {
const result = await this.createCreditNotes([creditNote]);
return result[0];
}
async updateCreditNote(
creditNoteId: CreditNoteId,
creditNote: Partial<CreditNote>
): Promise<CreditNote> {
const creditNotes = await this.makeRequest<CreditNote[]>({
method: 'POST',
url: `/CreditNotes/${creditNoteId}`,
data: { CreditNotes: [creditNote] }
});
return creditNotes[0];
}
// ==================== PURCHASE ORDERS ====================
async getPurchaseOrders(options?: XeroRequestOptions): Promise<PurchaseOrder[]> {
return this.makeRequest<PurchaseOrder[]>({
method: 'GET',
url: '/PurchaseOrders'
}, options);
}
async getPurchaseOrder(purchaseOrderId: PurchaseOrderId): Promise<PurchaseOrder> {
const orders = await this.makeRequest<PurchaseOrder[]>({
method: 'GET',
url: `/PurchaseOrders/${purchaseOrderId}`
});
return orders[0];
}
async createPurchaseOrders(purchaseOrders: PurchaseOrder[]): Promise<PurchaseOrder[]> {
return this.makeRequest<PurchaseOrder[]>({
method: 'PUT',
url: '/PurchaseOrders',
data: { PurchaseOrders: purchaseOrders }
});
}
async createPurchaseOrder(purchaseOrder: PurchaseOrder): Promise<PurchaseOrder> {
const result = await this.createPurchaseOrders([purchaseOrder]);
return result[0];
}
async updatePurchaseOrder(
purchaseOrderId: PurchaseOrderId,
purchaseOrder: Partial<PurchaseOrder>
): Promise<PurchaseOrder> {
const orders = await this.makeRequest<PurchaseOrder[]>({
method: 'POST',
url: `/PurchaseOrders/${purchaseOrderId}`,
data: { PurchaseOrders: [purchaseOrder] }
});
return orders[0];
}
// ==================== QUOTES ====================
async getQuotes(options?: XeroRequestOptions): Promise<Quote[]> {
return this.makeRequest<Quote[]>({ method: 'GET', url: '/Quotes' }, options);
}
async getQuote(quoteId: QuoteId): Promise<Quote> {
const quotes = await this.makeRequest<Quote[]>({
method: 'GET',
url: `/Quotes/${quoteId}`
});
return quotes[0];
}
async createQuotes(quotes: Quote[]): Promise<Quote[]> {
return this.makeRequest<Quote[]>({
method: 'PUT',
url: '/Quotes',
data: { Quotes: quotes }
});
}
async createQuote(quote: Quote): Promise<Quote> {
const result = await this.createQuotes([quote]);
return result[0];
}
async updateQuote(quoteId: QuoteId, quote: Partial<Quote>): Promise<Quote> {
const quotes = await this.makeRequest<Quote[]>({
method: 'POST',
url: `/Quotes/${quoteId}`,
data: { Quotes: [quote] }
});
return quotes[0];
}
// ==================== MANUAL JOURNALS ====================
async getManualJournals(options?: XeroRequestOptions): Promise<ManualJournal[]> {
return this.makeRequest<ManualJournal[]>({
method: 'GET',
url: '/ManualJournals'
}, options);
}
async getManualJournal(manualJournalId: ManualJournalId): Promise<ManualJournal> {
const journals = await this.makeRequest<ManualJournal[]>({
method: 'GET',
url: `/ManualJournals/${manualJournalId}`
});
return journals[0];
}
async createManualJournals(manualJournals: ManualJournal[]): Promise<ManualJournal[]> {
return this.makeRequest<ManualJournal[]>({
method: 'PUT',
url: '/ManualJournals',
data: { ManualJournals: manualJournals }
});
}
async createManualJournal(manualJournal: ManualJournal): Promise<ManualJournal> {
const result = await this.createManualJournals([manualJournal]);
return result[0];
}
async updateManualJournal(
manualJournalId: ManualJournalId,
manualJournal: Partial<ManualJournal>
): Promise<ManualJournal> {
const journals = await this.makeRequest<ManualJournal[]>({
method: 'POST',
url: `/ManualJournals/${manualJournalId}`,
data: { ManualJournals: [manualJournal] }
});
return journals[0];
}
// ==================== ITEMS ====================
async getItems(options?: XeroRequestOptions): Promise<Item[]> {
return this.makeRequest<Item[]>({ method: 'GET', url: '/Items' }, options);
}
async getItem(itemId: ItemId): Promise<Item> {
const items = await this.makeRequest<Item[]>({
method: 'GET',
url: `/Items/${itemId}`
});
return items[0];
}
async createItems(items: Item[]): Promise<Item[]> {
return this.makeRequest<Item[]>({
method: 'PUT',
url: '/Items',
data: { Items: items }
});
}
async createItem(item: Item): Promise<Item> {
const result = await this.createItems([item]);
return result[0];
}
async updateItem(itemId: ItemId, item: Partial<Item>): Promise<Item> {
const items = await this.makeRequest<Item[]>({
method: 'POST',
url: `/Items/${itemId}`,
data: { Items: [item] }
});
return items[0];
}
// ==================== TAX RATES ====================
async getTaxRates(options?: XeroRequestOptions): Promise<TaxRate[]> {
return this.makeRequest<TaxRate[]>({ method: 'GET', url: '/TaxRates' }, options);
}
async createTaxRates(taxRates: TaxRate[]): Promise<TaxRate[]> {
return this.makeRequest<TaxRate[]>({
method: 'PUT',
url: '/TaxRates',
data: { TaxRates: taxRates }
});
}
async createTaxRate(taxRate: TaxRate): Promise<TaxRate> {
const result = await this.createTaxRates([taxRate]);
return result[0];
}
// ==================== EMPLOYEES ====================
async getEmployees(options?: XeroRequestOptions): Promise<Employee[]> {
return this.makeRequest<Employee[]>({ method: 'GET', url: '/Employees' }, options);
}
async getEmployee(employeeId: EmployeeId): Promise<Employee> {
const employees = await this.makeRequest<Employee[]>({
method: 'GET',
url: `/Employees/${employeeId}`
});
return employees[0];
}
async createEmployees(employees: Employee[]): Promise<Employee[]> {
return this.makeRequest<Employee[]>({
method: 'PUT',
url: '/Employees',
data: { Employees: employees }
});
}
async createEmployee(employee: Employee): Promise<Employee> {
const result = await this.createEmployees([employee]);
return result[0];
}
// ==================== ORGANISATION ====================
async getOrganisations(): Promise<Organisation[]> {
return this.makeRequest<Organisation[]>({ method: 'GET', url: '/Organisation' });
}
// ==================== TRACKING CATEGORIES ====================
async getTrackingCategories(options?: XeroRequestOptions): Promise<TrackingCategory[]> {
return this.makeRequest<TrackingCategory[]>({
method: 'GET',
url: '/TrackingCategories'
}, options);
}
async getTrackingCategory(trackingCategoryId: TrackingCategoryId): Promise<TrackingCategory> {
const categories = await this.makeRequest<TrackingCategory[]>({
method: 'GET',
url: `/TrackingCategories/${trackingCategoryId}`
});
return categories[0];
}
async createTrackingCategory(trackingCategory: TrackingCategory): Promise<TrackingCategory> {
const categories = await this.makeRequest<TrackingCategory[]>({
method: 'PUT',
url: '/TrackingCategories',
data: { TrackingCategories: [trackingCategory] }
});
return categories[0];
}
// ==================== BRANDING THEMES ====================
async getBrandingThemes(): Promise<BrandingTheme[]> {
return this.makeRequest<BrandingTheme[]>({ method: 'GET', url: '/BrandingThemes' });
}
// ==================== CONTACT GROUPS ====================
async getContactGroups(options?: XeroRequestOptions): Promise<ContactGroup[]> {
return this.makeRequest<ContactGroup[]>({
method: 'GET',
url: '/ContactGroups'
}, options);
}
async createContactGroup(contactGroup: ContactGroup): Promise<ContactGroup> {
const groups = await this.makeRequest<ContactGroup[]>({
method: 'PUT',
url: '/ContactGroups',
data: { ContactGroups: [contactGroup] }
});
return groups[0];
}
// ==================== PREPAYMENTS ====================
async getPrepayments(options?: XeroRequestOptions): Promise<Prepayment[]> {
return this.makeRequest<Prepayment[]>({ method: 'GET', url: '/Prepayments' }, options);
}
async getPrepayment(prepaymentId: PrepaymentId): Promise<Prepayment> {
const prepayments = await this.makeRequest<Prepayment[]>({
method: 'GET',
url: `/Prepayments/${prepaymentId}`
});
return prepayments[0];
}
// ==================== OVERPAYMENTS ====================
async getOverpayments(options?: XeroRequestOptions): Promise<Overpayment[]> {
return this.makeRequest<Overpayment[]>({ method: 'GET', url: '/Overpayments' }, options);
}
async getOverpayment(overpaymentId: OverpaymentId): Promise<Overpayment> {
const overpayments = await this.makeRequest<Overpayment[]>({
method: 'GET',
url: `/Overpayments/${overpaymentId}`
});
return overpayments[0];
}
// ==================== BANK TRANSFERS ====================
async getBankTransfers(options?: XeroRequestOptions): Promise<BankTransfer[]> {
return this.makeRequest<BankTransfer[]>({ method: 'GET', url: '/BankTransfers' }, options);
}
async getBankTransfer(bankTransferId: BankTransferId): Promise<BankTransfer> {
const transfers = await this.makeRequest<BankTransfer[]>({
method: 'GET',
url: `/BankTransfers/${bankTransferId}`
});
return transfers[0];
}
async createBankTransfer(bankTransfer: BankTransfer): Promise<BankTransfer> {
const transfers = await this.makeRequest<BankTransfer[]>({
method: 'PUT',
url: '/BankTransfers',
data: { BankTransfers: [bankTransfer] }
});
return transfers[0];
}
// ==================== REPORTS ====================
async getReport(reportUrl: string, options?: XeroRequestOptions): Promise<Report> {
const reports = await this.makeRequest<Report[]>({
method: 'GET',
url: reportUrl
}, options);
return reports[0];
}
async getBalanceSheet(date?: string, periods?: number): Promise<Report> {
const params: Record<string, string | number> = {};
if (date) params.date = date;
if (periods) params.periods = periods;
return this.getReport('/Reports/BalanceSheet', { ...params } as any);
}
async getProfitAndLoss(fromDate?: string, toDate?: string): Promise<Report> {
const params: Record<string, string> = {};
if (fromDate) params.fromDate = fromDate;
if (toDate) params.toDate = toDate;
return this.getReport('/Reports/ProfitAndLoss', { ...params } as any);
}
async getTrialBalance(date?: string): Promise<Report> {
const params: Record<string, string> = {};
if (date) params.date = date;
return this.getReport('/Reports/TrialBalance', { ...params } as any);
}
async getBankSummary(fromDate?: string, toDate?: string): Promise<Report> {
const params: Record<string, string> = {};
if (fromDate) params.fromDate = fromDate;
if (toDate) params.toDate = toDate;
return this.getReport('/Reports/BankSummary', { ...params } as any);
}
async getExecutiveSummary(date?: string): Promise<Report> {
const params: Record<string, string> = {};
if (date) params.date = date;
return this.getReport('/Reports/ExecutiveSummary', { ...params } as any);
}
// ==================== ATTACHMENTS ====================
async getAttachments(endpoint: string, entityId: string): Promise<Attachment[]> {
return this.makeRequest<Attachment[]>({
method: 'GET',
url: `/${endpoint}/${entityId}/Attachments`
});
}
async uploadAttachment(
endpoint: string,
entityId: string,
fileName: string,
fileContent: Buffer,
mimeType: string
): Promise<Attachment> {
const attachments = await this.makeRequest<Attachment[]>({
method: 'PUT',
url: `/${endpoint}/${entityId}/Attachments/${fileName}`,
data: fileContent,
headers: {
'Content-Type': mimeType
}
});
return attachments[0];
}
}

62
servers/xero/src/main.ts Normal file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env node
/**
* Xero MCP Server - Main Entry Point
* Dual transport support (stdio/SSE) with graceful shutdown
*/
import { XeroClient } from './clients/xero.js';
import { XeroMCPServer } from './server.js';
async function main() {
// Required environment variables
const accessToken = process.env.XERO_ACCESS_TOKEN;
const tenantId = process.env.XERO_TENANT_ID;
if (!accessToken) {
console.error('Error: XERO_ACCESS_TOKEN environment variable is required');
process.exit(1);
}
if (!tenantId) {
console.error('Error: XERO_TENANT_ID environment variable is required');
process.exit(1);
}
// Optional configuration
const config = {
accessToken,
tenantId,
baseUrl: process.env.XERO_BASE_URL,
timeout: process.env.XERO_TIMEOUT ? parseInt(process.env.XERO_TIMEOUT, 10) : undefined,
retryAttempts: process.env.XERO_RETRY_ATTEMPTS
? parseInt(process.env.XERO_RETRY_ATTEMPTS, 10)
: undefined,
retryDelayMs: process.env.XERO_RETRY_DELAY_MS
? parseInt(process.env.XERO_RETRY_DELAY_MS, 10)
: undefined
};
// Initialize Xero client
const xeroClient = new XeroClient(config);
// Initialize MCP server
const mcpServer = new XeroMCPServer(xeroClient);
// Graceful shutdown
const shutdown = async () => {
console.error('Shutting down Xero MCP server...');
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
// Start server
await mcpServer.run();
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

603
servers/xero/src/server.ts Normal file
View File

@ -0,0 +1,603 @@
/**
* Xero MCP Server
* Provides lazy-loaded tools for Xero Accounting API operations
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool
} from '@modelcontextprotocol/sdk/types.js';
import { XeroClient } from './clients/xero.js';
export class XeroMCPServer {
private server: Server;
private client: XeroClient;
private toolsCache: Tool[] | null = null;
constructor(client: XeroClient) {
this.client = client;
this.server = new Server(
{
name: 'xero-mcp',
version: '1.0.0'
},
{
capabilities: {
tools: {}
}
}
);
this.setupHandlers();
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: this.getTools()
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await this.handleToolCall(name, args || {});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ error: errorMessage }, null, 2)
}
],
isError: true
};
}
});
}
private getTools(): Tool[] {
if (this.toolsCache) {
return this.toolsCache;
}
this.toolsCache = [
// INVOICES
{
name: 'xero_list_invoices',
description: 'List all invoices with optional filtering',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number', description: 'Page number (default 1)' },
pageSize: { type: 'number', description: 'Page size (max 100)' },
where: { type: 'string', description: 'Filter expression (e.g., Status=="AUTHORISED")' },
order: { type: 'string', description: 'Order by field (e.g., InvoiceNumber DESC)' },
includeArchived: { type: 'boolean', description: 'Include archived records' }
}
}
},
{
name: 'xero_get_invoice',
description: 'Get a specific invoice by ID',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'string', description: 'Invoice ID (GUID)' }
},
required: ['invoiceId']
}
},
{
name: 'xero_create_invoice',
description: 'Create a new invoice',
inputSchema: {
type: 'object',
properties: {
invoice: {
type: 'object',
description: 'Invoice data (Contact, LineItems, Type, etc.)'
}
},
required: ['invoice']
}
},
{
name: 'xero_update_invoice',
description: 'Update an existing invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'string', description: 'Invoice ID (GUID)' },
invoice: { type: 'object', description: 'Invoice update data' }
},
required: ['invoiceId', 'invoice']
}
},
// CONTACTS
{
name: 'xero_list_contacts',
description: 'List all contacts with optional filtering',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
where: { type: 'string' },
order: { type: 'string' },
includeArchived: { type: 'boolean' }
}
}
},
{
name: 'xero_get_contact',
description: 'Get a specific contact by ID',
inputSchema: {
type: 'object',
properties: {
contactId: { type: 'string', description: 'Contact ID (GUID)' }
},
required: ['contactId']
}
},
{
name: 'xero_create_contact',
description: 'Create a new contact',
inputSchema: {
type: 'object',
properties: {
contact: { type: 'object', description: 'Contact data (Name required)' }
},
required: ['contact']
}
},
{
name: 'xero_update_contact',
description: 'Update an existing contact',
inputSchema: {
type: 'object',
properties: {
contactId: { type: 'string' },
contact: { type: 'object' }
},
required: ['contactId', 'contact']
}
},
// ACCOUNTS
{
name: 'xero_list_accounts',
description: 'List all chart of accounts',
inputSchema: {
type: 'object',
properties: {
where: { type: 'string' },
order: { type: 'string' }
}
}
},
{
name: 'xero_get_account',
description: 'Get a specific account by ID',
inputSchema: {
type: 'object',
properties: {
accountId: { type: 'string', description: 'Account ID (GUID)' }
},
required: ['accountId']
}
},
{
name: 'xero_create_account',
description: 'Create a new account',
inputSchema: {
type: 'object',
properties: {
account: { type: 'object', description: 'Account data (Code, Name, Type required)' }
},
required: ['account']
}
},
// BANK TRANSACTIONS
{
name: 'xero_list_bank_transactions',
description: 'List all bank transactions',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
where: { type: 'string' },
order: { type: 'string' }
}
}
},
{
name: 'xero_get_bank_transaction',
description: 'Get a specific bank transaction by ID',
inputSchema: {
type: 'object',
properties: {
bankTransactionId: { type: 'string' }
},
required: ['bankTransactionId']
}
},
{
name: 'xero_create_bank_transaction',
description: 'Create a new bank transaction',
inputSchema: {
type: 'object',
properties: {
transaction: { type: 'object' }
},
required: ['transaction']
}
},
// PAYMENTS
{
name: 'xero_list_payments',
description: 'List all payments',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
where: { type: 'string' }
}
}
},
{
name: 'xero_create_payment',
description: 'Create a new payment',
inputSchema: {
type: 'object',
properties: {
payment: { type: 'object' }
},
required: ['payment']
}
},
// BILLS (same as invoices with Type=ACCPAY)
{
name: 'xero_list_bills',
description: 'List all bills (payable invoices)',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
where: { type: 'string' },
order: { type: 'string' }
}
}
},
{
name: 'xero_create_bill',
description: 'Create a new bill',
inputSchema: {
type: 'object',
properties: {
bill: { type: 'object', description: 'Bill data (Contact, LineItems, Type=ACCPAY)' }
},
required: ['bill']
}
},
// CREDIT NOTES
{
name: 'xero_list_credit_notes',
description: 'List all credit notes',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
where: { type: 'string' }
}
}
},
{
name: 'xero_create_credit_note',
description: 'Create a new credit note',
inputSchema: {
type: 'object',
properties: {
creditNote: { type: 'object' }
},
required: ['creditNote']
}
},
// PURCHASE ORDERS
{
name: 'xero_list_purchase_orders',
description: 'List all purchase orders',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
where: { type: 'string' }
}
}
},
{
name: 'xero_create_purchase_order',
description: 'Create a new purchase order',
inputSchema: {
type: 'object',
properties: {
purchaseOrder: { type: 'object' }
},
required: ['purchaseOrder']
}
},
// QUOTES
{
name: 'xero_list_quotes',
description: 'List all quotes',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
where: { type: 'string' }
}
}
},
{
name: 'xero_create_quote',
description: 'Create a new quote',
inputSchema: {
type: 'object',
properties: {
quote: { type: 'object' }
},
required: ['quote']
}
},
// REPORTS
{
name: 'xero_get_balance_sheet',
description: 'Get balance sheet report',
inputSchema: {
type: 'object',
properties: {
date: { type: 'string', description: 'Report date (YYYY-MM-DD)' },
periods: { type: 'number', description: 'Number of periods to compare' }
}
}
},
{
name: 'xero_get_profit_and_loss',
description: 'Get profit and loss report',
inputSchema: {
type: 'object',
properties: {
fromDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
toDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
}
}
},
{
name: 'xero_get_trial_balance',
description: 'Get trial balance report',
inputSchema: {
type: 'object',
properties: {
date: { type: 'string', description: 'Report date (YYYY-MM-DD)' }
}
}
},
{
name: 'xero_get_bank_summary',
description: 'Get bank summary report',
inputSchema: {
type: 'object',
properties: {
fromDate: { type: 'string' },
toDate: { type: 'string' }
}
}
},
// EMPLOYEES
{
name: 'xero_list_employees',
description: 'List all employees (basic accounting)',
inputSchema: {
type: 'object',
properties: {
where: { type: 'string' }
}
}
},
// TAX RATES
{
name: 'xero_list_tax_rates',
description: 'List all tax rates',
inputSchema: {
type: 'object',
properties: {
where: { type: 'string' }
}
}
},
// ITEMS
{
name: 'xero_list_items',
description: 'List all inventory/service items',
inputSchema: {
type: 'object',
properties: {
where: { type: 'string' }
}
}
},
{
name: 'xero_create_item',
description: 'Create a new inventory/service item',
inputSchema: {
type: 'object',
properties: {
item: { type: 'object' }
},
required: ['item']
}
},
// ORGANISATION
{
name: 'xero_get_organisation',
description: 'Get organisation details',
inputSchema: {
type: 'object',
properties: {}
}
},
// TRACKING CATEGORIES
{
name: 'xero_list_tracking_categories',
description: 'List all tracking categories',
inputSchema: {
type: 'object',
properties: {}
}
}
];
return this.toolsCache;
}
private async handleToolCall(name: string, args: Record<string, unknown>): Promise<unknown> {
switch (name) {
// INVOICES
case 'xero_list_invoices':
return this.client.getInvoices(args);
case 'xero_get_invoice':
return this.client.getInvoice(args.invoiceId as any);
case 'xero_create_invoice':
return this.client.createInvoice(args.invoice as any);
case 'xero_update_invoice':
return this.client.updateInvoice(args.invoiceId as any, args.invoice as any);
// CONTACTS
case 'xero_list_contacts':
return this.client.getContacts(args);
case 'xero_get_contact':
return this.client.getContact(args.contactId as any);
case 'xero_create_contact':
return this.client.createContact(args.contact as any);
case 'xero_update_contact':
return this.client.updateContact(args.contactId as any, args.contact as any);
// ACCOUNTS
case 'xero_list_accounts':
return this.client.getAccounts(args);
case 'xero_get_account':
return this.client.getAccount(args.accountId as any);
case 'xero_create_account':
return this.client.createAccount(args.account as any);
// BANK TRANSACTIONS
case 'xero_list_bank_transactions':
return this.client.getBankTransactions(args);
case 'xero_get_bank_transaction':
return this.client.getBankTransaction(args.bankTransactionId as any);
case 'xero_create_bank_transaction':
return this.client.createBankTransaction(args.transaction as any);
// PAYMENTS
case 'xero_list_payments':
return this.client.getPayments(args);
case 'xero_create_payment':
return this.client.createPayment(args.payment as any);
// BILLS
case 'xero_list_bills':
return this.client.getInvoices({ ...args, where: 'Type=="ACCPAY"' });
case 'xero_create_bill':
return this.client.createInvoice({ ...(args.bill as any), Type: 'ACCPAY' });
// CREDIT NOTES
case 'xero_list_credit_notes':
return this.client.getCreditNotes(args);
case 'xero_create_credit_note':
return this.client.createCreditNote(args.creditNote as any);
// PURCHASE ORDERS
case 'xero_list_purchase_orders':
return this.client.getPurchaseOrders(args);
case 'xero_create_purchase_order':
return this.client.createPurchaseOrder(args.purchaseOrder as any);
// QUOTES
case 'xero_list_quotes':
return this.client.getQuotes(args);
case 'xero_create_quote':
return this.client.createQuote(args.quote as any);
// REPORTS
case 'xero_get_balance_sheet':
return this.client.getBalanceSheet(args.date as string, args.periods as number);
case 'xero_get_profit_and_loss':
return this.client.getProfitAndLoss(args.fromDate as string, args.toDate as string);
case 'xero_get_trial_balance':
return this.client.getTrialBalance(args.date as string);
case 'xero_get_bank_summary':
return this.client.getBankSummary(args.fromDate as string, args.toDate as string);
// EMPLOYEES
case 'xero_list_employees':
return this.client.getEmployees(args);
// TAX RATES
case 'xero_list_tax_rates':
return this.client.getTaxRates(args);
// ITEMS
case 'xero_list_items':
return this.client.getItems(args);
case 'xero_create_item':
return this.client.createItem(args.item as any);
// ORGANISATION
case 'xero_get_organisation':
return this.client.getOrganisations();
// TRACKING CATEGORIES
case 'xero_list_tracking_categories':
return this.client.getTrackingCategories(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Xero MCP Server running on stdio');
}
}

View File

@ -0,0 +1,818 @@
/**
* Xero Accounting API TypeScript Types
* Comprehensive type definitions for Xero API entities
*/
// Branded types for IDs (Xero uses GUIDs)
export type InvoiceId = string & { readonly __brand: 'InvoiceId' };
export type ContactId = string & { readonly __brand: 'ContactId' };
export type AccountId = string & { readonly __brand: 'AccountId' };
export type BankTransactionId = string & { readonly __brand: 'BankTransactionId' };
export type PaymentId = string & { readonly __brand: 'PaymentId' };
export type ItemId = string & { readonly __brand: 'ItemId' };
export type TaxRateId = string & { readonly __brand: 'TaxRateId' };
export type EmployeeId = string & { readonly __brand: 'EmployeeId' };
export type OrganisationId = string & { readonly __brand: 'OrganisationId' };
export type TrackingCategoryId = string & { readonly __brand: 'TrackingCategoryId' };
export type TrackingOptionId = string & { readonly __brand: 'TrackingOptionId' };
export type AttachmentId = string & { readonly __brand: 'AttachmentId' };
export type CreditNoteId = string & { readonly __brand: 'CreditNoteId' };
export type PurchaseOrderId = string & { readonly __brand: 'PurchaseOrderId' };
export type QuoteId = string & { readonly __brand: 'QuoteId' };
export type ManualJournalId = string & { readonly __brand: 'ManualJournalId' };
export type BrandingThemeId = string & { readonly __brand: 'BrandingThemeId' };
export type ContactGroupId = string & { readonly __brand: 'ContactGroupId' };
export type PrepaymentId = string & { readonly __brand: 'PrepaymentId' };
export type OverpaymentId = string & { readonly __brand: 'OverpaymentId' };
export type BankTransferId = string & { readonly __brand: 'BankTransferId' };
// Enums and Constants
export enum InvoiceStatus {
DRAFT = 'DRAFT',
SUBMITTED = 'SUBMITTED',
AUTHORISED = 'AUTHORISED',
PAID = 'PAID',
VOIDED = 'VOIDED',
DELETED = 'DELETED'
}
export enum InvoiceType {
ACCPAY = 'ACCPAY', // Bill
ACCREC = 'ACCREC' // Invoice
}
export enum LineAmountType {
Exclusive = 'Exclusive',
Inclusive = 'Inclusive',
NoTax = 'NoTax'
}
export enum AccountType {
BANK = 'BANK',
CURRENT = 'CURRENT',
CURRLIAB = 'CURRLIAB',
DEPRECIATN = 'DEPRECIATN',
DIRECTCOSTS = 'DIRECTCOSTS',
EQUITY = 'EQUITY',
EXPENSE = 'EXPENSE',
FIXED = 'FIXED',
INVENTORY = 'INVENTORY',
LIABILITY = 'LIABILITY',
NONCURRENT = 'NONCURRENT',
OTHERINCOME = 'OTHERINCOME',
OVERHEADS = 'OVERHEADS',
PREPAYMENT = 'PREPAYMENT',
REVENUE = 'REVENUE',
SALES = 'SALES',
TERMLIAB = 'TERMLIAB',
PAYGLIABILITY = 'PAYGLIABILITY',
SUPERANNUATIONEXPENSE = 'SUPERANNUATIONEXPENSE',
SUPERANNUATIONLIABILITY = 'SUPERANNUATIONLIABILITY',
WAGESEXPENSE = 'WAGESEXPENSE'
}
export enum AccountClass {
ASSET = 'ASSET',
EQUITY = 'EQUITY',
EXPENSE = 'EXPENSE',
LIABILITY = 'LIABILITY',
REVENUE = 'REVENUE'
}
export enum BankTransactionStatus {
AUTHORISED = 'AUTHORISED',
DELETED = 'DELETED',
VOIDED = 'VOIDED'
}
export enum BankTransactionType {
RECEIVE = 'RECEIVE',
SPEND = 'SPEND'
}
export enum PaymentStatus {
AUTHORISED = 'AUTHORISED',
DELETED = 'DELETED'
}
export enum CreditNoteStatus {
DRAFT = 'DRAFT',
SUBMITTED = 'SUBMITTED',
AUTHORISED = 'AUTHORISED',
PAID = 'PAID',
VOIDED = 'VOIDED',
DELETED = 'DELETED'
}
export enum CreditNoteType {
ACCPAYCREDIT = 'ACCPAYCREDIT',
ACCRECCREDIT = 'ACCRECCREDIT'
}
export enum PurchaseOrderStatus {
DRAFT = 'DRAFT',
SUBMITTED = 'SUBMITTED',
AUTHORISED = 'AUTHORISED',
BILLED = 'BILLED',
DELETED = 'DELETED'
}
export enum QuoteStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
ACCEPTED = 'ACCEPTED',
DECLINED = 'DECLINED',
INVOICED = 'INVOICED',
DELETED = 'DELETED'
}
export enum ManualJournalStatus {
DRAFT = 'DRAFT',
POSTED = 'POSTED',
DELETED = 'DELETED',
VOIDED = 'VOIDED'
}
export enum TaxType {
INPUT = 'INPUT',
OUTPUT = 'OUTPUT',
CAPEXINPUT = 'CAPEXINPUT',
CAPEXOUTPUT = 'CAPEXOUTPUT',
EXEMPTEXPENSES = 'EXEMPTEXPENSES',
EXEMPTINCOME = 'EXEMPTINCOME',
EXEMPTCAPITAL = 'EXEMPTCAPITAL',
EXEMPTOUTPUT = 'EXEMPTOUTPUT',
INPUTTAXED = 'INPUTTAXED',
BASEXCLUDED = 'BASEXCLUDED',
GSTONCAPIMPORTS = 'GSTONCAPIMPORTS',
GSTONIMPORTS = 'GSTONIMPORTS',
NONE = 'NONE',
INPUT2 = 'INPUT2',
ECZROUTPUT = 'ECZROUTPUT',
ZERORATEDINPUT = 'ZERORATEDINPUT',
ZERORATEDOUTPUT = 'ZERORATEDOUTPUT',
REVERSECHARGES = 'REVERSECHARGES',
RRINPUT = 'RRINPUT',
RROUTPUT = 'RROUTPUT'
}
export type CurrencyCode = string; // e.g., 'USD', 'GBP', 'EUR', 'AUD', 'NZD'
// Address
export interface Address {
AddressType?: 'POBOX' | 'STREET' | 'DELIVERY';
AddressLine1?: string;
AddressLine2?: string;
AddressLine3?: string;
AddressLine4?: string;
City?: string;
Region?: string;
PostalCode?: string;
Country?: string;
AttentionTo?: string;
}
// Phone
export interface Phone {
PhoneType?: 'DEFAULT' | 'DDI' | 'MOBILE' | 'FAX';
PhoneNumber?: string;
PhoneAreaCode?: string;
PhoneCountryCode?: string;
}
// ContactPerson
export interface ContactPerson {
FirstName?: string;
LastName?: string;
EmailAddress?: string;
IncludeInEmails?: boolean;
}
// Contact
export interface Contact {
ContactID?: ContactId;
ContactNumber?: string;
AccountNumber?: string;
ContactStatus?: 'ACTIVE' | 'ARCHIVED' | 'GDPRREQUEST';
Name: string;
FirstName?: string;
LastName?: string;
EmailAddress?: string;
SkypeUserName?: string;
ContactPersons?: ContactPerson[];
BankAccountDetails?: string;
TaxNumber?: string;
AccountsReceivableTaxType?: TaxType;
AccountsPayableTaxType?: TaxType;
Addresses?: Address[];
Phones?: Phone[];
IsSupplier?: boolean;
IsCustomer?: boolean;
DefaultCurrency?: CurrencyCode;
UpdatedDateUTC?: string;
ContactGroups?: ContactGroup[];
SalesDefaultAccountCode?: string;
PurchasesDefaultAccountCode?: string;
SalesTrackingCategories?: TrackingCategory[];
PurchasesTrackingCategories?: TrackingCategory[];
TrackingCategoryName?: string;
TrackingCategoryOption?: string;
PaymentTerms?: {
Bills?: { Day?: number; Type?: 'DAYSAFTERBILLDATE' | 'DAYSAFTERBILLMONTH' | 'OFCURRENTMONTH' | 'OFFOLLOWINGMONTH' };
Sales?: { Day?: number; Type?: 'DAYSAFTERBILLDATE' | 'DAYSAFTERBILLMONTH' | 'OFCURRENTMONTH' | 'OFFOLLOWINGMONTH' };
};
Discount?: number;
Balances?: {
AccountsReceivable?: {
Outstanding?: number;
Overdue?: number;
};
AccountsPayable?: {
Outstanding?: number;
Overdue?: number;
};
};
BatchPayments?: {
BankAccountNumber?: string;
BankAccountName?: string;
Details?: string;
};
}
// ContactGroup
export interface ContactGroup {
ContactGroupID?: ContactGroupId;
Name: string;
Status?: 'ACTIVE' | 'DELETED';
Contacts?: Contact[];
}
// TrackingCategory
export interface TrackingCategory {
TrackingCategoryID?: TrackingCategoryId;
TrackingCategoryName?: string;
Name?: string;
Status?: 'ACTIVE' | 'ARCHIVED' | 'DELETED';
Options?: TrackingOption[];
}
// TrackingOption
export interface TrackingOption {
TrackingOptionID?: TrackingOptionId;
Name?: string;
Status?: 'ACTIVE' | 'ARCHIVED' | 'DELETED';
TrackingCategoryID?: TrackingCategoryId;
}
// LineItemTracking
export interface LineItemTracking {
TrackingCategoryID?: TrackingCategoryId;
TrackingOptionID?: TrackingOptionId;
Name?: string;
Option?: string;
}
// InvoiceLineItem
export interface InvoiceLineItem {
LineItemID?: string;
Description?: string;
Quantity?: number;
UnitAmount?: number;
ItemCode?: string;
AccountCode?: string;
AccountID?: AccountId;
TaxType?: TaxType;
TaxAmount?: number;
LineAmount?: number;
DiscountRate?: number;
DiscountAmount?: number;
Tracking?: LineItemTracking[];
Item?: Item;
}
// Invoice
export interface Invoice {
InvoiceID?: InvoiceId;
Type?: InvoiceType;
InvoiceNumber?: string;
Reference?: string;
Payments?: Payment[];
CreditNotes?: CreditNote[];
Prepayments?: Prepayment[];
Overpayments?: Overpayment[];
AmountDue?: number;
AmountPaid?: number;
AmountCredited?: number;
CurrencyRate?: number;
IsDiscounted?: boolean;
HasAttachments?: boolean;
HasErrors?: boolean;
Contact: Contact;
DateString?: string;
Date?: string;
DueDateString?: string;
DueDate?: string;
Status?: InvoiceStatus;
LineAmountTypes?: LineAmountType;
LineItems?: InvoiceLineItem[];
SubTotal?: number;
TotalTax?: number;
Total?: number;
UpdatedDateUTC?: string;
CurrencyCode?: CurrencyCode;
FullyPaidOnDate?: string;
BrandingThemeID?: BrandingThemeId;
Url?: string;
SentToContact?: boolean;
ExpectedPaymentDate?: string;
PlannedPaymentDate?: string;
RepeatingInvoiceID?: string;
Attachments?: Attachment[];
}
// Bill (same as Invoice but type=ACCPAY)
export type Bill = Invoice;
// Account
export interface Account {
AccountID?: AccountId;
Code: string;
Name?: string;
Type?: AccountType;
Class?: AccountClass;
Status?: 'ACTIVE' | 'ARCHIVED' | 'DELETED';
Description?: string;
BankAccountNumber?: string;
BankAccountType?: 'BANK' | 'CREDITCARD' | 'PAYPAL';
CurrencyCode?: CurrencyCode;
TaxType?: TaxType;
EnablePaymentsToAccount?: boolean;
ShowInExpenseClaims?: boolean;
ReportingCode?: string;
ReportingCodeName?: string;
HasAttachments?: boolean;
UpdatedDateUTC?: string;
AddToWatchlist?: boolean;
}
// BankTransaction
export interface BankTransaction {
BankTransactionID?: BankTransactionId;
Type?: BankTransactionType;
Contact: Contact;
LineItems: InvoiceLineItem[];
BankAccount: Account;
IsReconciled?: boolean;
DateString?: string;
Date?: string;
Reference?: string;
CurrencyCode?: CurrencyCode;
CurrencyRate?: number;
Url?: string;
Status?: BankTransactionStatus;
LineAmountTypes?: LineAmountType;
SubTotal?: number;
TotalTax?: number;
Total?: number;
UpdatedDateUTC?: string;
HasAttachments?: boolean;
Attachments?: Attachment[];
}
// BankTransfer
export interface BankTransfer {
BankTransferID?: BankTransferId;
FromBankAccount: Account;
ToBankAccount: Account;
Amount: number;
Date?: string;
DateString?: string;
CurrencyRate?: number;
FromBankTransactionID?: BankTransactionId;
ToBankTransactionID?: BankTransactionId;
HasAttachments?: boolean;
CreatedDateUTC?: string;
Attachments?: Attachment[];
}
// Payment
export interface Payment {
PaymentID?: PaymentId;
Date?: string;
CurrencyRate?: number;
Amount?: number;
Reference?: string;
IsReconciled?: boolean;
Status?: PaymentStatus;
PaymentType?: 'ACCRECPAYMENT' | 'ACCPAYPAYMENT' | 'ARCREDITPAYMENT' | 'APCREDITPAYMENT' | 'AROVERPAYMENTPAYMENT' | 'ARPREPAYMENTPAYMENT' | 'APPREPAYMENTPAYMENT' | 'APOVERPAYMENTPAYMENT';
UpdatedDateUTC?: string;
HasAccount?: boolean;
HasValidationErrors?: boolean;
Invoice?: Invoice;
CreditNote?: CreditNote;
Prepayment?: Prepayment;
Overpayment?: Overpayment;
Account?: Account;
BankAmount?: number;
Details?: string;
}
// Prepayment
export interface Prepayment {
PrepaymentID?: PrepaymentId;
Type?: 'RECEIVE-PREPAYMENT' | 'SPEND-PREPAYMENT';
Contact?: Contact;
Date?: string;
Status?: 'AUTHORISED' | 'PAID' | 'VOIDED';
LineAmountTypes?: LineAmountType;
LineItems?: InvoiceLineItem[];
SubTotal?: number;
TotalTax?: number;
Total?: number;
UpdatedDateUTC?: string;
CurrencyCode?: CurrencyCode;
CurrencyRate?: number;
Reference?: string;
RemainingCredit?: number;
Allocations?: {
Invoice?: Invoice;
Amount?: number;
Date?: string;
}[];
Payments?: Payment[];
HasAttachments?: boolean;
Attachments?: Attachment[];
}
// Overpayment
export interface Overpayment {
OverpaymentID?: OverpaymentId;
Type?: 'RECEIVE-OVERPAYMENT' | 'SPEND-OVERPAYMENT';
Contact?: Contact;
Date?: string;
Status?: 'AUTHORISED' | 'PAID' | 'VOIDED';
LineAmountTypes?: LineAmountType;
LineItems?: InvoiceLineItem[];
SubTotal?: number;
TotalTax?: number;
Total?: number;
UpdatedDateUTC?: string;
CurrencyCode?: CurrencyCode;
CurrencyRate?: number;
RemainingCredit?: number;
Allocations?: {
Invoice?: Invoice;
Amount?: number;
Date?: string;
}[];
Payments?: Payment[];
HasAttachments?: boolean;
Attachments?: Attachment[];
}
// CreditNote
export interface CreditNote {
CreditNoteID?: CreditNoteId;
CreditNoteNumber?: string;
Type?: CreditNoteType;
Contact?: Contact;
Date?: string;
DueDate?: string;
Status?: CreditNoteStatus;
LineAmountTypes?: LineAmountType;
LineItems?: InvoiceLineItem[];
SubTotal?: number;
TotalTax?: number;
Total?: number;
UpdatedDateUTC?: string;
CurrencyCode?: CurrencyCode;
CurrencyRate?: number;
RemainingCredit?: number;
Allocations?: {
Invoice?: Invoice;
Amount?: number;
Date?: string;
}[];
Payments?: Payment[];
BrandingThemeID?: BrandingThemeId;
HasAttachments?: boolean;
Attachments?: Attachment[];
Reference?: string;
SentToContact?: boolean;
}
// PurchaseOrder
export interface PurchaseOrder {
PurchaseOrderID?: PurchaseOrderId;
PurchaseOrderNumber?: string;
DateString?: string;
Date?: string;
DeliveryDateString?: string;
DeliveryDate?: string;
DeliveryAddress?: string;
AttentionTo?: string;
Telephone?: string;
DeliveryInstructions?: string;
ExpectedArrivalDate?: string;
Contact?: Contact;
BrandingThemeID?: BrandingThemeId;
Status?: PurchaseOrderStatus;
LineAmountTypes?: LineAmountType;
LineItems?: InvoiceLineItem[];
SubTotal?: number;
TotalTax?: number;
Total?: number;
UpdatedDateUTC?: string;
CurrencyCode?: CurrencyCode;
CurrencyRate?: number;
HasAttachments?: boolean;
Attachments?: Attachment[];
SentToContact?: boolean;
Reference?: string;
}
// Quote
export interface Quote {
QuoteID?: QuoteId;
QuoteNumber?: string;
Reference?: string;
Terms?: string;
Contact?: Contact;
LineItems?: InvoiceLineItem[];
Date?: string;
DateString?: string;
ExpiryDate?: string;
ExpiryDateString?: string;
Status?: QuoteStatus;
CurrencyCode?: CurrencyCode;
CurrencyRate?: number;
SubTotal?: number;
TotalTax?: number;
Total?: number;
TotalDiscount?: number;
Title?: string;
Summary?: string;
BrandingThemeID?: BrandingThemeId;
UpdatedDateUTC?: string;
LineAmountTypes?: LineAmountType;
Url?: string;
HasAttachments?: boolean;
Attachments?: Attachment[];
}
// JournalLine
export interface JournalLine {
JournalLineID?: string;
AccountID?: AccountId;
AccountCode?: string;
LineAmount?: number;
TaxType?: TaxType;
TaxAmount?: number;
Description?: string;
Tracking?: LineItemTracking[];
}
// ManualJournal
export interface ManualJournal {
ManualJournalID?: ManualJournalId;
Narration: string;
JournalLines?: JournalLine[];
Date?: string;
DateString?: string;
Status?: ManualJournalStatus;
LineAmountTypes?: LineAmountType;
Url?: string;
ShowOnCashBasisReports?: boolean;
HasAttachments?: boolean;
UpdatedDateUTC?: string;
Attachments?: Attachment[];
}
// Item
export interface Item {
ItemID?: ItemId;
Code: string;
Name?: string;
Description?: string;
PurchaseDescription?: string;
PurchaseDetails?: {
UnitPrice?: number;
AccountCode?: string;
TaxType?: TaxType;
};
SalesDetails?: {
UnitPrice?: number;
AccountCode?: string;
TaxType?: TaxType;
};
IsTrackedAsInventory?: boolean;
InventoryAssetAccountCode?: string;
TotalCostPool?: number;
QuantityOnHand?: number;
UpdatedDateUTC?: string;
IsSold?: boolean;
IsPurchased?: boolean;
}
// TaxComponent
export interface TaxComponent {
Name?: string;
Rate?: number;
IsCompound?: boolean;
IsNonRecoverable?: boolean;
}
// TaxRate
export interface TaxRate {
TaxRateID?: TaxRateId;
Name?: string;
TaxType?: TaxType;
Status?: 'ACTIVE' | 'DELETED' | 'ARCHIVED';
ReportTaxType?: string;
CanApplyToAssets?: boolean;
CanApplyToEquity?: boolean;
CanApplyToExpenses?: boolean;
CanApplyToLiabilities?: boolean;
CanApplyToRevenue?: boolean;
DisplayTaxRate?: number;
EffectiveRate?: number;
TaxComponents?: TaxComponent[];
}
// Currency
export interface Currency {
Code: CurrencyCode;
Description?: string;
}
// Employee (basic accounting, not payroll)
export interface Employee {
EmployeeID?: EmployeeId;
Status?: 'ACTIVE' | 'DELETED';
FirstName?: string;
LastName?: string;
ExternalLink?: {
Url?: string;
};
}
// BrandingTheme
export interface BrandingTheme {
BrandingThemeID?: BrandingThemeId;
Name?: string;
LogoUrl?: string;
Type?: 'STANDARD' | 'CUSTOM';
SortOrder?: number;
CreatedDateUTC?: string;
}
// Organisation
export interface Organisation {
OrganisationID?: OrganisationId;
APIKey?: string;
Name?: string;
LegalName?: string;
PaysTax?: boolean;
Version?: 'AU' | 'GB' | 'NZ' | 'US' | 'GLOBAL';
OrganisationType?: 'COMPANY' | 'CHARITY' | 'CLUBSOCIETY' | 'PARTNERSHIP' | 'PRACTICE' | 'PERSON' | 'SOLETRADER' | 'TRUST';
BaseCurrency?: CurrencyCode;
CountryCode?: string;
IsDemoCompany?: boolean;
OrganisationStatus?: string;
RegistrationNumber?: string;
TaxNumber?: string;
FinancialYearEndDay?: number;
FinancialYearEndMonth?: number;
SalesTaxBasis?: 'ACCRUALS' | 'CASH' | 'FLATRATECASH' | 'NONE';
SalesTaxPeriod?: 'MONTHLY' | 'QUARTERLY' | 'ANNUALLY' | 'NONE';
DefaultSalesTax?: string;
DefaultPurchasesTax?: string;
PeriodLockDate?: string;
EndOfYearLockDate?: string;
CreatedDateUTC?: string;
Timezone?: string;
OrganisationEntityType?: string;
ShortCode?: string;
LineOfBusiness?: string;
Addresses?: Address[];
Phones?: Phone[];
ExternalLinks?: Array<{ LinkType?: string; Url?: string }>;
PaymentTerms?: {
Bills?: { Day?: number; Type?: string };
Sales?: { Day?: number; Type?: string };
};
}
// Attachment
export interface Attachment {
AttachmentID?: AttachmentId;
FileName?: string;
Url?: string;
MimeType?: string;
ContentLength?: number;
IncludeOnline?: boolean;
}
// Report types
export interface ReportCell {
Value?: string;
Attributes?: Array<{ Value?: string; Id?: string }>;
}
export interface ReportRow {
RowType?: 'Header' | 'Section' | 'Row' | 'SummaryRow';
Cells?: ReportCell[];
Title?: string;
Rows?: ReportRow[];
}
export interface Report {
ReportID?: string;
ReportName?: string;
ReportType?: string;
ReportTitles?: string[];
ReportDate?: string;
UpdatedDateUTC?: string;
Rows?: ReportRow[];
}
export interface BalanceSheet extends Report {
ReportType: 'BalanceSheet';
}
export interface ProfitAndLoss extends Report {
ReportType: 'ProfitAndLoss';
}
export interface TrialBalance extends Report {
ReportType: 'TrialBalance';
}
export interface BankSummary extends Report {
ReportType: 'BankSummary';
}
export interface BudgetSummary extends Report {
ReportType: 'BudgetSummary';
}
export interface ExecutiveSummary extends Report {
ReportType: 'ExecutiveSummary';
}
// Pagination
export interface PaginationParams {
page?: number;
pageSize?: number;
}
export interface PaginatedResponse<T> {
data: T[];
page?: number;
pageSize?: number;
totalPages?: number;
totalRecords?: number;
}
// API Response wrappers
export interface XeroApiResponse<T> {
Id?: string;
Status?: string;
ProviderName?: string;
DateTimeUTC?: string;
[key: string]: T[] | T | string | undefined;
}
export interface XeroErrorResponse {
Type?: string;
Title?: string;
Status?: number;
Detail?: string;
Instance?: string;
Elements?: Array<{
ValidationErrors?: Array<{
Message?: string;
}>;
}>;
}
// Client configuration
export interface XeroClientConfig {
accessToken: string;
tenantId: string;
baseUrl?: string;
timeout?: number;
retryAttempts?: number;
retryDelayMs?: number;
}
// Request options
export interface XeroRequestOptions {
summarizeErrors?: boolean;
ifModifiedSince?: Date;
page?: number;
pageSize?: number;
where?: string;
order?: string;
includeArchived?: boolean;
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}