V3 Batch 2 Foundation: Notion, Airtable, Intercom, Monday.com, Xero - types + clients + server + main, zero TSC errors
This commit is contained in:
parent
062e0f281a
commit
6763409b5e
4
servers/airtable/.env.example
Normal file
4
servers/airtable/.env.example
Normal 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
42
servers/airtable/.gitignore
vendored
Normal 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
220
servers/airtable/README.md
Normal 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)
|
||||
32
servers/airtable/package.json
Normal file
32
servers/airtable/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
438
servers/airtable/src/clients/airtable.ts
Normal file
438
servers/airtable/src/clients/airtable.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
50
servers/airtable/src/main.ts
Normal file
50
servers/airtable/src/main.ts
Normal 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);
|
||||
});
|
||||
690
servers/airtable/src/server.ts
Normal file
690
servers/airtable/src/server.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
563
servers/airtable/src/types/index.ts
Normal file
563
servers/airtable/src/types/index.ts
Normal 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(),
|
||||
})
|
||||
),
|
||||
});
|
||||
22
servers/airtable/tsconfig.json
Normal file
22
servers/airtable/tsconfig.json
Normal 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"]
|
||||
}
|
||||
3
servers/intercom/.env.example
Normal file
3
servers/intercom/.env.example
Normal 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
29
servers/intercom/.gitignore
vendored
Normal 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
168
servers/intercom/README.md
Normal 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
|
||||
32
servers/intercom/package.json
Normal file
32
servers/intercom/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
701
servers/intercom/src/clients/intercom.ts
Normal file
701
servers/intercom/src/clients/intercom.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
49
servers/intercom/src/main.ts
Normal file
49
servers/intercom/src/main.ts
Normal 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();
|
||||
757
servers/intercom/src/server.ts
Normal file
757
servers/intercom/src/server.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
737
servers/intercom/src/types/index.ts
Normal file
737
servers/intercom/src/types/index.ts
Normal 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;
|
||||
}
|
||||
21
servers/intercom/tsconfig.json
Normal file
21
servers/intercom/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
3
servers/monday/.env.example
Normal file
3
servers/monday/.env.example
Normal 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
35
servers/monday/.gitignore
vendored
Normal 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
132
servers/monday/README.md
Normal 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
|
||||
28
servers/monday/package.json
Normal file
28
servers/monday/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
616
servers/monday/src/clients/monday.ts
Normal file
616
servers/monday/src/clients/monday.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
36
servers/monday/src/main.ts
Normal file
36
servers/monday/src/main.ts
Normal 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);
|
||||
});
|
||||
510
servers/monday/src/server.ts
Normal file
510
servers/monday/src/server.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
614
servers/monday/src/types/index.ts
Normal file
614
servers/monday/src/types/index.ts
Normal 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>;
|
||||
}
|
||||
26
servers/monday/tsconfig.json
Normal file
26
servers/monday/tsconfig.json
Normal 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"]
|
||||
}
|
||||
1
servers/notion/.env.example
Normal file
1
servers/notion/.env.example
Normal file
@ -0,0 +1 @@
|
||||
NOTION_API_KEY=your_notion_api_key_here
|
||||
6
servers/notion/.gitignore
vendored
Normal file
6
servers/notion/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
*.tsbuildinfo
|
||||
177
servers/notion/README.md
Normal file
177
servers/notion/README.md
Normal 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
|
||||
29
servers/notion/package.json
Normal file
29
servers/notion/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
361
servers/notion/src/clients/notion.ts
Normal file
361
servers/notion/src/clients/notion.ts
Normal 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 }));
|
||||
}
|
||||
}
|
||||
49
servers/notion/src/main.ts
Normal file
49
servers/notion/src/main.ts
Normal 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);
|
||||
});
|
||||
528
servers/notion/src/server.ts
Normal file
528
servers/notion/src/server.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
742
servers/notion/src/types/index.ts
Normal file
742
servers/notion/src/types/index.ts
Normal 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';
|
||||
}
|
||||
23
servers/notion/tsconfig.json
Normal file
23
servers/notion/tsconfig.json
Normal 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
19
servers/xero/.env.example
Normal 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
39
servers/xero/.gitignore
vendored
Normal 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
236
servers/xero/README.md
Normal 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
37
servers/xero/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
808
servers/xero/src/clients/xero.ts
Normal file
808
servers/xero/src/clients/xero.ts
Normal 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
62
servers/xero/src/main.ts
Normal 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
603
servers/xero/src/server.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
818
servers/xero/src/types/index.ts
Normal file
818
servers/xero/src/types/index.ts
Normal 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;
|
||||
}
|
||||
26
servers/xero/tsconfig.json
Normal file
26
servers/xero/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user