@rashidazarang/airtable-mcp 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +12 -0
- package/CAPABILITY_REPORT.md +118 -0
- package/CLAUDE_INTEGRATION.md +61 -74
- package/DEVELOPMENT.md +189 -0
- package/Dockerfile.node +20 -0
- package/IMPROVEMENT_PROPOSAL.md +371 -0
- package/ISSUE_RESPONSES.md +171 -0
- package/MCP_REVIEW_SUMMARY.md +141 -0
- package/QUICK_START.md +60 -0
- package/README.md +167 -143
- package/RELEASE_NOTES_v1.2.1.md +40 -0
- package/RELEASE_NOTES_v1.2.2.md +48 -0
- package/RELEASE_NOTES_v1.2.3.md +104 -0
- package/RELEASE_NOTES_v1.2.4.md +60 -0
- package/RELEASE_NOTES_v1.4.0.md +104 -0
- package/SECURITY_NOTICE.md +40 -0
- package/airtable_enhanced.js +499 -0
- package/airtable_simple.js +653 -0
- package/airtable_simple_v1.2.4_backup.js +277 -0
- package/airtable_v1.4.0.js +654 -0
- package/cleanup.sh +70 -0
- package/examples/claude_simple_config.json +16 -0
- package/examples/python_debug_patch.txt +27 -0
- package/inspector_server.py +34 -44
- package/package.json +22 -19
- package/quick_test.sh +29 -0
- package/simple_airtable_server.py +151 -0
- package/smithery.yaml +17 -13
- package/test_all_features.sh +146 -0
- package/test_all_operations.sh +120 -0
- package/test_client.py +10 -3
- package/test_enhanced_features.js +389 -0
- package/test_mcp_comprehensive.js +162 -0
- package/test_mock_server.js +180 -0
- package/test_v1.4.0_final.sh +131 -0
- package/test_webhooks.sh +105 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Release Notes - v1.2.4
|
|
2
|
+
|
|
3
|
+
## 🔒 Security Fix Release
|
|
4
|
+
|
|
5
|
+
### Critical Security Fix
|
|
6
|
+
- **REMOVED hardcoded API tokens from test files** (Addresses Issue #7)
|
|
7
|
+
- `test_client.py` and `test_mcp_comprehensive.js` now require environment variables
|
|
8
|
+
- Added security notice documentation
|
|
9
|
+
- No exposed credentials in the codebase
|
|
10
|
+
|
|
11
|
+
### 🐛 Bug Fixes
|
|
12
|
+
|
|
13
|
+
#### Smithery Cloud Deployment Issues (Issues #5 and #6)
|
|
14
|
+
- **Fixed HTTP 400 errors** when using Smithery
|
|
15
|
+
- **Switched to JavaScript implementation** for Smithery deployment
|
|
16
|
+
- Updated `smithery.yaml` to use `airtable_simple.js` instead of problematic Python server
|
|
17
|
+
- Created dedicated `Dockerfile.node` for Node.js deployment
|
|
18
|
+
- Fixed authentication flow for Smithery connections
|
|
19
|
+
|
|
20
|
+
### 📚 Documentation Updates
|
|
21
|
+
- Added `SECURITY_NOTICE.md` with token rotation instructions
|
|
22
|
+
- Created `.env.example` file for secure configuration
|
|
23
|
+
- Updated Dockerfile references for Glama listing (Issue #4)
|
|
24
|
+
|
|
25
|
+
### 🔧 Improvements
|
|
26
|
+
- Added environment variable support with dotenv
|
|
27
|
+
- Improved logging system with configurable levels (ERROR, WARN, INFO, DEBUG)
|
|
28
|
+
- Better error messages for missing credentials
|
|
29
|
+
|
|
30
|
+
### ⚠️ Breaking Changes
|
|
31
|
+
- Test files now require environment variables:
|
|
32
|
+
```bash
|
|
33
|
+
export AIRTABLE_TOKEN="your_token"
|
|
34
|
+
export AIRTABLE_BASE_ID="your_base_id"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 🚀 Migration Guide
|
|
38
|
+
|
|
39
|
+
1. **Update your environment variables:**
|
|
40
|
+
```bash
|
|
41
|
+
cp .env.example .env
|
|
42
|
+
# Edit .env with your credentials
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. **For Smithery users:**
|
|
46
|
+
- Reinstall the MCP to get the latest configuration
|
|
47
|
+
- The server now properly accepts credentials through Smithery's config
|
|
48
|
+
|
|
49
|
+
3. **For direct users:**
|
|
50
|
+
- Continue using command line arguments or switch to environment variables
|
|
51
|
+
- Both methods are supported
|
|
52
|
+
|
|
53
|
+
### 📝 Notes
|
|
54
|
+
- All previously exposed tokens have been revoked
|
|
55
|
+
- Please use your own Airtable credentials
|
|
56
|
+
- Never commit API tokens to version control
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
**Full Changelog**: [v1.2.3...v1.2.4](https://github.com/rashidazarang/airtable-mcp/compare/v1.2.3...v1.2.4)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Release Notes - v1.4.0
|
|
2
|
+
|
|
3
|
+
## 🚀 Major Feature Release
|
|
4
|
+
|
|
5
|
+
### ✨ New Features
|
|
6
|
+
|
|
7
|
+
#### 🪝 **Webhook Management** (5 new tools)
|
|
8
|
+
- `list_webhooks` - List all webhooks in your base
|
|
9
|
+
- `create_webhook` - Create webhooks for real-time notifications
|
|
10
|
+
- `delete_webhook` - Remove webhooks
|
|
11
|
+
- `get_webhook_payloads` - Retrieve webhook payload history
|
|
12
|
+
- `refresh_webhook` - Extend webhook expiration time
|
|
13
|
+
|
|
14
|
+
#### 🔧 **Enhanced CRUD Operations** (5 tools added since v1.2.4)
|
|
15
|
+
- `create_record` - Create new records in any table
|
|
16
|
+
- `update_record` - Update existing records
|
|
17
|
+
- `delete_record` - Remove records from tables
|
|
18
|
+
- `get_record` - Retrieve single record by ID
|
|
19
|
+
- `search_records` - Advanced filtering with Airtable formulas
|
|
20
|
+
|
|
21
|
+
### 📊 **Complete Tool Set (12 tools total)**
|
|
22
|
+
1. **list_tables** - List all tables in base
|
|
23
|
+
2. **list_records** - List records from table
|
|
24
|
+
3. **get_record** - Get single record by ID
|
|
25
|
+
4. **create_record** - Create new records
|
|
26
|
+
5. **update_record** - Update existing records
|
|
27
|
+
6. **delete_record** - Delete records
|
|
28
|
+
7. **search_records** - Search with filters
|
|
29
|
+
8. **list_webhooks** - List webhooks
|
|
30
|
+
9. **create_webhook** - Create webhooks
|
|
31
|
+
10. **delete_webhook** - Delete webhooks
|
|
32
|
+
11. **get_webhook_payloads** - Get webhook history
|
|
33
|
+
12. **refresh_webhook** - Refresh webhook expiration
|
|
34
|
+
|
|
35
|
+
### 🔐 **Security Improvements**
|
|
36
|
+
- Environment variable support for credentials
|
|
37
|
+
- Token masking in logs
|
|
38
|
+
- Configurable logging levels (ERROR, WARN, INFO, DEBUG)
|
|
39
|
+
- No hardcoded credentials in test files
|
|
40
|
+
|
|
41
|
+
### 🛠️ **Technical Improvements**
|
|
42
|
+
- Full HTTP method support (GET, POST, PATCH, DELETE)
|
|
43
|
+
- Enhanced error handling with detailed messages
|
|
44
|
+
- Proper API endpoint routing
|
|
45
|
+
- Debug logging support
|
|
46
|
+
- Graceful shutdown handling
|
|
47
|
+
|
|
48
|
+
### 📈 **Testing**
|
|
49
|
+
- **100% test coverage** - All 12 tools tested and verified
|
|
50
|
+
- Tested with real Airtable API
|
|
51
|
+
- Comprehensive test suite included
|
|
52
|
+
- Test scripts for validation
|
|
53
|
+
|
|
54
|
+
### 💔 **Breaking Changes**
|
|
55
|
+
- Test files now require environment variables:
|
|
56
|
+
```bash
|
|
57
|
+
export AIRTABLE_TOKEN="your_token"
|
|
58
|
+
export AIRTABLE_BASE_ID="your_base_id"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 🔄 **Migration from v1.2.4**
|
|
62
|
+
|
|
63
|
+
1. **Update package**:
|
|
64
|
+
```bash
|
|
65
|
+
npm install -g @rashidazarang/airtable-mcp@latest
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
2. **Set credentials** (choose one method):
|
|
69
|
+
- Environment variables
|
|
70
|
+
- Command line arguments
|
|
71
|
+
- .env file
|
|
72
|
+
|
|
73
|
+
3. **Update configuration** if using webhooks
|
|
74
|
+
|
|
75
|
+
### 📝 **Webhook Usage Example**
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
// Create a webhook
|
|
79
|
+
{
|
|
80
|
+
"name": "create_webhook",
|
|
81
|
+
"arguments": {
|
|
82
|
+
"notificationUrl": "https://your-endpoint.com/webhook"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The response includes:
|
|
87
|
+
// - Webhook ID
|
|
88
|
+
// - MAC secret (save this - shown only once!)
|
|
89
|
+
// - Expiration time
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 🎯 **What's Next**
|
|
93
|
+
- Batch operations support
|
|
94
|
+
- Comment management
|
|
95
|
+
- Attachment handling
|
|
96
|
+
- Schema modification tools
|
|
97
|
+
|
|
98
|
+
### 🙏 **Acknowledgments**
|
|
99
|
+
- Thanks to all testers and contributors
|
|
100
|
+
- Special thanks for the comprehensive testing feedback
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
**Full Changelog**: [v1.2.4...v1.4.0](https://github.com/rashidazarang/airtable-mcp/compare/v1.2.4...v1.4.0)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Security Notice
|
|
2
|
+
|
|
3
|
+
## Important: API Token Rotation Required
|
|
4
|
+
|
|
5
|
+
If you have been using or testing this repository before January 2025, please note that hardcoded API tokens were previously included in test files. These have been removed and replaced with environment variable requirements.
|
|
6
|
+
|
|
7
|
+
### Actions Required:
|
|
8
|
+
|
|
9
|
+
1. **If you used the exposed tokens**:
|
|
10
|
+
- These tokens have been revoked and are no longer valid
|
|
11
|
+
- You must use your own Airtable API credentials
|
|
12
|
+
|
|
13
|
+
2. **For all users**:
|
|
14
|
+
- Never commit API tokens to version control
|
|
15
|
+
- Always use environment variables or secure configuration files
|
|
16
|
+
- Add `.env` to your `.gitignore` file
|
|
17
|
+
|
|
18
|
+
### Secure Configuration
|
|
19
|
+
|
|
20
|
+
Set your credentials using environment variables:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
export AIRTABLE_TOKEN="your_personal_token_here"
|
|
24
|
+
export AIRTABLE_BASE_ID="your_base_id_here"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or create a `.env` file (never commit this):
|
|
28
|
+
|
|
29
|
+
```env
|
|
30
|
+
AIRTABLE_TOKEN=your_personal_token_here
|
|
31
|
+
AIRTABLE_BASE_ID=your_base_id_here
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Reporting Security Issues
|
|
35
|
+
|
|
36
|
+
If you discover any security vulnerabilities, please report them to:
|
|
37
|
+
- Open an issue on GitHub (without including sensitive details)
|
|
38
|
+
- Contact the maintainer directly for sensitive information
|
|
39
|
+
|
|
40
|
+
Thank you for helping keep this project secure.
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// Load environment variables from .env file if it exists
|
|
9
|
+
const envPath = path.join(__dirname, '.env');
|
|
10
|
+
if (fs.existsSync(envPath)) {
|
|
11
|
+
require('dotenv').config({ path: envPath });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Parse command line arguments with environment variable fallback
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
let tokenIndex = args.indexOf('--token');
|
|
17
|
+
let baseIndex = args.indexOf('--base');
|
|
18
|
+
|
|
19
|
+
// Use environment variables as fallback
|
|
20
|
+
const token = tokenIndex !== -1 ? args[tokenIndex + 1] : process.env.AIRTABLE_TOKEN || process.env.AIRTABLE_API_TOKEN;
|
|
21
|
+
const baseId = baseIndex !== -1 ? args[baseIndex + 1] : process.env.AIRTABLE_BASE_ID || process.env.AIRTABLE_BASE;
|
|
22
|
+
|
|
23
|
+
if (!token || !baseId) {
|
|
24
|
+
console.error('Error: Missing Airtable credentials');
|
|
25
|
+
console.error('\nUsage options:');
|
|
26
|
+
console.error(' 1. Command line: node airtable_enhanced.js --token YOUR_TOKEN --base YOUR_BASE_ID');
|
|
27
|
+
console.error(' 2. Environment variables: AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
|
|
28
|
+
console.error(' 3. .env file with AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Configure logging levels
|
|
33
|
+
const LOG_LEVELS = {
|
|
34
|
+
ERROR: 0,
|
|
35
|
+
WARN: 1,
|
|
36
|
+
INFO: 2,
|
|
37
|
+
DEBUG: 3
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const currentLogLevel = process.env.LOG_LEVEL ? LOG_LEVELS[process.env.LOG_LEVEL.toUpperCase()] || LOG_LEVELS.INFO : LOG_LEVELS.INFO;
|
|
41
|
+
|
|
42
|
+
function log(level, message, ...args) {
|
|
43
|
+
const levelName = Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === level);
|
|
44
|
+
const timestamp = new Date().toISOString();
|
|
45
|
+
|
|
46
|
+
if (level <= currentLogLevel) {
|
|
47
|
+
const prefix = `[${timestamp}] [${levelName}]`;
|
|
48
|
+
if (level === LOG_LEVELS.ERROR) {
|
|
49
|
+
console.error(prefix, message, ...args);
|
|
50
|
+
} else if (level === LOG_LEVELS.WARN) {
|
|
51
|
+
console.warn(prefix, message, ...args);
|
|
52
|
+
} else {
|
|
53
|
+
console.log(prefix, message, ...args);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
log(LOG_LEVELS.INFO, `Starting Enhanced Airtable MCP server v1.3.0`);
|
|
59
|
+
log(LOG_LEVELS.INFO, `Token: ${token.slice(0, 5)}...${token.slice(-5)}`);
|
|
60
|
+
log(LOG_LEVELS.INFO, `Base ID: ${baseId}`);
|
|
61
|
+
|
|
62
|
+
// Enhanced Airtable API function with full HTTP method support
|
|
63
|
+
function callAirtableAPI(endpoint, method = 'GET', body = null, queryParams = {}) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const isBaseEndpoint = !endpoint.startsWith('meta/');
|
|
66
|
+
const baseUrl = isBaseEndpoint ? `${baseId}/${endpoint}` : endpoint;
|
|
67
|
+
|
|
68
|
+
// Build query string
|
|
69
|
+
const queryString = Object.keys(queryParams).length > 0
|
|
70
|
+
? '?' + new URLSearchParams(queryParams).toString()
|
|
71
|
+
: '';
|
|
72
|
+
|
|
73
|
+
const url = `https://api.airtable.com/v0/${baseUrl}${queryString}`;
|
|
74
|
+
const urlObj = new URL(url);
|
|
75
|
+
|
|
76
|
+
log(LOG_LEVELS.DEBUG, `API Request: ${method} ${url}`);
|
|
77
|
+
if (body) {
|
|
78
|
+
log(LOG_LEVELS.DEBUG, `Request body:`, JSON.stringify(body, null, 2));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const options = {
|
|
82
|
+
hostname: urlObj.hostname,
|
|
83
|
+
path: urlObj.pathname + urlObj.search,
|
|
84
|
+
method: method,
|
|
85
|
+
headers: {
|
|
86
|
+
'Authorization': `Bearer ${token}`,
|
|
87
|
+
'Content-Type': 'application/json'
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const req = https.request(options, (response) => {
|
|
92
|
+
let data = '';
|
|
93
|
+
|
|
94
|
+
response.on('data', (chunk) => {
|
|
95
|
+
data += chunk;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
response.on('end', () => {
|
|
99
|
+
log(LOG_LEVELS.DEBUG, `Response status: ${response.statusCode}`);
|
|
100
|
+
log(LOG_LEVELS.DEBUG, `Response data:`, data);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const parsed = data ? JSON.parse(data) : {};
|
|
104
|
+
|
|
105
|
+
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
106
|
+
resolve(parsed);
|
|
107
|
+
} else {
|
|
108
|
+
const error = parsed.error || {};
|
|
109
|
+
reject(new Error(`Airtable API error (${response.statusCode}): ${error.message || error.type || 'Unknown error'}`));
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
reject(new Error(`Failed to parse Airtable response: ${e.message}`));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
req.on('error', (error) => {
|
|
118
|
+
reject(new Error(`Airtable API request failed: ${error.message}`));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (body) {
|
|
122
|
+
req.write(JSON.stringify(body));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
req.end();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Create HTTP server
|
|
130
|
+
const server = http.createServer(async (req, res) => {
|
|
131
|
+
// Enable CORS
|
|
132
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
133
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
134
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
135
|
+
|
|
136
|
+
// Handle preflight request
|
|
137
|
+
if (req.method === 'OPTIONS') {
|
|
138
|
+
res.writeHead(200);
|
|
139
|
+
res.end();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Only handle POST requests to /mcp
|
|
144
|
+
if (req.method !== 'POST' || !req.url.endsWith('/mcp')) {
|
|
145
|
+
res.writeHead(404);
|
|
146
|
+
res.end();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let body = '';
|
|
151
|
+
req.on('data', chunk => {
|
|
152
|
+
body += chunk.toString();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
req.on('end', async () => {
|
|
156
|
+
try {
|
|
157
|
+
const request = JSON.parse(body);
|
|
158
|
+
log(LOG_LEVELS.DEBUG, 'Received request:', JSON.stringify(request, null, 2));
|
|
159
|
+
|
|
160
|
+
// Handle JSON-RPC methods
|
|
161
|
+
if (request.method === 'tools/list') {
|
|
162
|
+
const response = {
|
|
163
|
+
jsonrpc: '2.0',
|
|
164
|
+
id: request.id,
|
|
165
|
+
result: {
|
|
166
|
+
tools: [
|
|
167
|
+
{
|
|
168
|
+
name: 'list_tables',
|
|
169
|
+
description: 'List all tables in the Airtable base',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'list_records',
|
|
177
|
+
description: 'List records from a specific table',
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {
|
|
181
|
+
table: { type: 'string', description: 'Table name or ID' },
|
|
182
|
+
maxRecords: { type: 'number', description: 'Maximum number of records to return' },
|
|
183
|
+
view: { type: 'string', description: 'View name or ID' }
|
|
184
|
+
},
|
|
185
|
+
required: ['table']
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'get_record',
|
|
190
|
+
description: 'Get a single record by ID',
|
|
191
|
+
inputSchema: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
table: { type: 'string', description: 'Table name or ID' },
|
|
195
|
+
recordId: { type: 'string', description: 'Record ID' }
|
|
196
|
+
},
|
|
197
|
+
required: ['table', 'recordId']
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'create_record',
|
|
202
|
+
description: 'Create a new record in a table',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
table: { type: 'string', description: 'Table name or ID' },
|
|
207
|
+
fields: { type: 'object', description: 'Field values for the new record' }
|
|
208
|
+
},
|
|
209
|
+
required: ['table', 'fields']
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'update_record',
|
|
214
|
+
description: 'Update an existing record',
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
properties: {
|
|
218
|
+
table: { type: 'string', description: 'Table name or ID' },
|
|
219
|
+
recordId: { type: 'string', description: 'Record ID to update' },
|
|
220
|
+
fields: { type: 'object', description: 'Fields to update' }
|
|
221
|
+
},
|
|
222
|
+
required: ['table', 'recordId', 'fields']
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'delete_record',
|
|
227
|
+
description: 'Delete a record from a table',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
properties: {
|
|
231
|
+
table: { type: 'string', description: 'Table name or ID' },
|
|
232
|
+
recordId: { type: 'string', description: 'Record ID to delete' }
|
|
233
|
+
},
|
|
234
|
+
required: ['table', 'recordId']
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'search_records',
|
|
239
|
+
description: 'Search records with filtering and sorting',
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: 'object',
|
|
242
|
+
properties: {
|
|
243
|
+
table: { type: 'string', description: 'Table name or ID' },
|
|
244
|
+
filterByFormula: { type: 'string', description: 'Airtable formula to filter records' },
|
|
245
|
+
sort: { type: 'array', description: 'Sort configuration' },
|
|
246
|
+
maxRecords: { type: 'number', description: 'Maximum records to return' },
|
|
247
|
+
fields: { type: 'array', description: 'Fields to return' }
|
|
248
|
+
},
|
|
249
|
+
required: ['table']
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
256
|
+
res.end(JSON.stringify(response));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (request.method === 'resources/list') {
|
|
261
|
+
const response = {
|
|
262
|
+
jsonrpc: '2.0',
|
|
263
|
+
id: request.id,
|
|
264
|
+
result: {
|
|
265
|
+
resources: [
|
|
266
|
+
{
|
|
267
|
+
id: 'airtable_tables',
|
|
268
|
+
name: 'Airtable Tables',
|
|
269
|
+
description: 'Tables in your Airtable base'
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
275
|
+
res.end(JSON.stringify(response));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (request.method === 'prompts/list') {
|
|
280
|
+
const response = {
|
|
281
|
+
jsonrpc: '2.0',
|
|
282
|
+
id: request.id,
|
|
283
|
+
result: {
|
|
284
|
+
prompts: [
|
|
285
|
+
{
|
|
286
|
+
id: 'tables_prompt',
|
|
287
|
+
name: 'List Tables',
|
|
288
|
+
description: 'List all tables'
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
294
|
+
res.end(JSON.stringify(response));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Handle tool calls
|
|
299
|
+
if (request.method === 'tools/call') {
|
|
300
|
+
const toolName = request.params.name;
|
|
301
|
+
const toolParams = request.params.arguments || {};
|
|
302
|
+
|
|
303
|
+
let result;
|
|
304
|
+
let responseText;
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
// LIST TABLES
|
|
308
|
+
if (toolName === 'list_tables') {
|
|
309
|
+
result = await callAirtableAPI(`meta/bases/${baseId}/tables`);
|
|
310
|
+
const tables = result.tables || [];
|
|
311
|
+
|
|
312
|
+
responseText = tables.length > 0
|
|
313
|
+
? `Found ${tables.length} table(s):\n` + tables.map((table, i) =>
|
|
314
|
+
`${i+1}. ${table.name} (ID: ${table.id}, Fields: ${table.fields?.length || 0})`
|
|
315
|
+
).join('\n')
|
|
316
|
+
: 'No tables found in this base.';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// LIST RECORDS
|
|
320
|
+
else if (toolName === 'list_records') {
|
|
321
|
+
const { table, maxRecords, view } = toolParams;
|
|
322
|
+
|
|
323
|
+
const queryParams = {};
|
|
324
|
+
if (maxRecords) queryParams.maxRecords = maxRecords;
|
|
325
|
+
if (view) queryParams.view = view;
|
|
326
|
+
|
|
327
|
+
result = await callAirtableAPI(`${table}`, 'GET', null, queryParams);
|
|
328
|
+
const records = result.records || [];
|
|
329
|
+
|
|
330
|
+
responseText = records.length > 0
|
|
331
|
+
? `Found ${records.length} record(s) in table "${table}":\n` +
|
|
332
|
+
records.map((record, i) =>
|
|
333
|
+
`${i+1}. ID: ${record.id}\n Fields: ${JSON.stringify(record.fields, null, 2)}`
|
|
334
|
+
).join('\n\n')
|
|
335
|
+
: `No records found in table "${table}".`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// GET SINGLE RECORD
|
|
339
|
+
else if (toolName === 'get_record') {
|
|
340
|
+
const { table, recordId } = toolParams;
|
|
341
|
+
|
|
342
|
+
result = await callAirtableAPI(`${table}/${recordId}`);
|
|
343
|
+
|
|
344
|
+
responseText = `Record ${recordId} from table "${table}":\n` +
|
|
345
|
+
JSON.stringify(result.fields, null, 2) +
|
|
346
|
+
`\n\nCreated: ${result.createdTime}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// CREATE RECORD
|
|
350
|
+
else if (toolName === 'create_record') {
|
|
351
|
+
const { table, fields } = toolParams;
|
|
352
|
+
|
|
353
|
+
const body = {
|
|
354
|
+
fields: fields
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
result = await callAirtableAPI(table, 'POST', body);
|
|
358
|
+
|
|
359
|
+
responseText = `Successfully created record in table "${table}":\n` +
|
|
360
|
+
`Record ID: ${result.id}\n` +
|
|
361
|
+
`Fields: ${JSON.stringify(result.fields, null, 2)}\n` +
|
|
362
|
+
`Created at: ${result.createdTime}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// UPDATE RECORD
|
|
366
|
+
else if (toolName === 'update_record') {
|
|
367
|
+
const { table, recordId, fields } = toolParams;
|
|
368
|
+
|
|
369
|
+
const body = {
|
|
370
|
+
fields: fields
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
result = await callAirtableAPI(`${table}/${recordId}`, 'PATCH', body);
|
|
374
|
+
|
|
375
|
+
responseText = `Successfully updated record ${recordId} in table "${table}":\n` +
|
|
376
|
+
`Updated fields: ${JSON.stringify(result.fields, null, 2)}`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// DELETE RECORD
|
|
380
|
+
else if (toolName === 'delete_record') {
|
|
381
|
+
const { table, recordId } = toolParams;
|
|
382
|
+
|
|
383
|
+
result = await callAirtableAPI(`${table}/${recordId}`, 'DELETE');
|
|
384
|
+
|
|
385
|
+
responseText = `Successfully deleted record ${recordId} from table "${table}".\n` +
|
|
386
|
+
`Deleted record ID: ${result.id}\n` +
|
|
387
|
+
`Deleted: ${result.deleted}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// SEARCH RECORDS
|
|
391
|
+
else if (toolName === 'search_records') {
|
|
392
|
+
const { table, filterByFormula, sort, maxRecords, fields } = toolParams;
|
|
393
|
+
|
|
394
|
+
const queryParams = {};
|
|
395
|
+
if (filterByFormula) queryParams.filterByFormula = filterByFormula;
|
|
396
|
+
if (maxRecords) queryParams.maxRecords = maxRecords;
|
|
397
|
+
if (fields && fields.length > 0) queryParams.fields = fields;
|
|
398
|
+
if (sort && sort.length > 0) {
|
|
399
|
+
sort.forEach((s, i) => {
|
|
400
|
+
queryParams[`sort[${i}][field]`] = s.field;
|
|
401
|
+
queryParams[`sort[${i}][direction]`] = s.direction || 'asc';
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
result = await callAirtableAPI(table, 'GET', null, queryParams);
|
|
406
|
+
const records = result.records || [];
|
|
407
|
+
|
|
408
|
+
responseText = records.length > 0
|
|
409
|
+
? `Found ${records.length} matching record(s) in table "${table}":\n` +
|
|
410
|
+
records.map((record, i) =>
|
|
411
|
+
`${i+1}. ID: ${record.id}\n Fields: ${JSON.stringify(record.fields, null, 2)}`
|
|
412
|
+
).join('\n\n')
|
|
413
|
+
: `No records found matching the search criteria in table "${table}".`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
else {
|
|
417
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const response = {
|
|
421
|
+
jsonrpc: '2.0',
|
|
422
|
+
id: request.id,
|
|
423
|
+
result: {
|
|
424
|
+
content: [
|
|
425
|
+
{
|
|
426
|
+
type: 'text',
|
|
427
|
+
text: responseText
|
|
428
|
+
}
|
|
429
|
+
]
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
433
|
+
res.end(JSON.stringify(response));
|
|
434
|
+
|
|
435
|
+
} catch (error) {
|
|
436
|
+
log(LOG_LEVELS.ERROR, `Tool ${toolName} error:`, error.message);
|
|
437
|
+
|
|
438
|
+
const response = {
|
|
439
|
+
jsonrpc: '2.0',
|
|
440
|
+
id: request.id,
|
|
441
|
+
result: {
|
|
442
|
+
content: [
|
|
443
|
+
{
|
|
444
|
+
type: 'text',
|
|
445
|
+
text: `Error executing ${toolName}: ${error.message}`
|
|
446
|
+
}
|
|
447
|
+
]
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
451
|
+
res.end(JSON.stringify(response));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Method not found
|
|
458
|
+
const response = {
|
|
459
|
+
jsonrpc: '2.0',
|
|
460
|
+
id: request.id,
|
|
461
|
+
error: {
|
|
462
|
+
code: -32601,
|
|
463
|
+
message: `Method ${request.method} not found`
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
467
|
+
res.end(JSON.stringify(response));
|
|
468
|
+
|
|
469
|
+
} catch (error) {
|
|
470
|
+
log(LOG_LEVELS.ERROR, 'Error processing request:', error);
|
|
471
|
+
const response = {
|
|
472
|
+
jsonrpc: '2.0',
|
|
473
|
+
id: request.id || null,
|
|
474
|
+
error: {
|
|
475
|
+
code: -32000,
|
|
476
|
+
message: error.message || 'Unknown error'
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
480
|
+
res.end(JSON.stringify(response));
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Start server
|
|
486
|
+
const PORT = process.env.PORT || 8010;
|
|
487
|
+
server.listen(PORT, () => {
|
|
488
|
+
log(LOG_LEVELS.INFO, `Enhanced Airtable MCP server v1.3.0 running at http://localhost:${PORT}/mcp`);
|
|
489
|
+
console.log(`For Claude, use this URL: http://localhost:${PORT}/mcp`);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Graceful shutdown
|
|
493
|
+
process.on('SIGINT', () => {
|
|
494
|
+
log(LOG_LEVELS.INFO, 'Shutting down server...');
|
|
495
|
+
server.close(() => {
|
|
496
|
+
log(LOG_LEVELS.INFO, 'Server stopped');
|
|
497
|
+
process.exit(0);
|
|
498
|
+
});
|
|
499
|
+
});
|