@rashidazarang/airtable-mcp 1.2.4 → 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 +4 -1
- package/CAPABILITY_REPORT.md +118 -0
- package/DEVELOPMENT.md +1 -0
- package/IMPROVEMENT_PROPOSAL.md +371 -0
- package/MCP_REVIEW_SUMMARY.md +1 -0
- package/RELEASE_NOTES_v1.2.3.md +1 -0
- package/RELEASE_NOTES_v1.4.0.md +104 -0
- package/airtable_enhanced.js +499 -0
- package/airtable_simple.js +459 -83
- package/airtable_simple_v1.2.4_backup.js +277 -0
- package/airtable_v1.4.0.js +654 -0
- package/cleanup.sh +1 -0
- package/package.json +1 -1
- package/quick_test.sh +1 -0
- package/test_all_features.sh +146 -0
- package/test_all_operations.sh +120 -0
- package/test_enhanced_features.js +389 -0
- package/test_mcp_comprehensive.js +1 -0
- package/test_mock_server.js +180 -0
- package/test_v1.4.0_final.sh +131 -0
- package/test_webhooks.sh +105 -0
package/airtable_simple.js
CHANGED
|
@@ -23,7 +23,7 @@ const baseId = baseIndex !== -1 ? args[baseIndex + 1] : process.env.AIRTABLE_BAS
|
|
|
23
23
|
if (!token || !baseId) {
|
|
24
24
|
console.error('Error: Missing Airtable credentials');
|
|
25
25
|
console.error('\nUsage options:');
|
|
26
|
-
console.error(' 1. Command line: node
|
|
26
|
+
console.error(' 1. Command line: node airtable_enhanced.js --token YOUR_TOKEN --base YOUR_BASE_ID');
|
|
27
27
|
console.error(' 2. Environment variables: AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
|
|
28
28
|
console.error(' 3. .env file with AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
|
|
29
29
|
process.exit(1);
|
|
@@ -55,7 +55,76 @@ function log(level, message, ...args) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
log(LOG_LEVELS.INFO, `Starting Airtable MCP server
|
|
58
|
+
log(LOG_LEVELS.INFO, `Starting Enhanced Airtable MCP server v1.4.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/') && !endpoint.startsWith('bases/');
|
|
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
|
+
}
|
|
59
128
|
|
|
60
129
|
// Create HTTP server
|
|
61
130
|
const server = http.createServer(async (req, res) => {
|
|
@@ -86,8 +155,179 @@ const server = http.createServer(async (req, res) => {
|
|
|
86
155
|
req.on('end', async () => {
|
|
87
156
|
try {
|
|
88
157
|
const request = JSON.parse(body);
|
|
158
|
+
log(LOG_LEVELS.DEBUG, 'Received request:', JSON.stringify(request, null, 2));
|
|
89
159
|
|
|
90
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
|
+
name: 'list_webhooks',
|
|
254
|
+
description: 'List all webhooks for the base',
|
|
255
|
+
inputSchema: {
|
|
256
|
+
type: 'object',
|
|
257
|
+
properties: {}
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'create_webhook',
|
|
262
|
+
description: 'Create a new webhook for a table',
|
|
263
|
+
inputSchema: {
|
|
264
|
+
type: 'object',
|
|
265
|
+
properties: {
|
|
266
|
+
notificationUrl: { type: 'string', description: 'URL to receive webhook notifications' },
|
|
267
|
+
specification: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
description: 'Webhook specification',
|
|
270
|
+
properties: {
|
|
271
|
+
options: {
|
|
272
|
+
type: 'object',
|
|
273
|
+
properties: {
|
|
274
|
+
filters: {
|
|
275
|
+
type: 'object',
|
|
276
|
+
properties: {
|
|
277
|
+
dataTypes: { type: 'array', items: { type: 'string' } },
|
|
278
|
+
recordChangeScope: { type: 'string' }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
required: ['notificationUrl']
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: 'delete_webhook',
|
|
291
|
+
description: 'Delete a webhook',
|
|
292
|
+
inputSchema: {
|
|
293
|
+
type: 'object',
|
|
294
|
+
properties: {
|
|
295
|
+
webhookId: { type: 'string', description: 'Webhook ID to delete' }
|
|
296
|
+
},
|
|
297
|
+
required: ['webhookId']
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: 'get_webhook_payloads',
|
|
302
|
+
description: 'Get webhook payload history',
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: 'object',
|
|
305
|
+
properties: {
|
|
306
|
+
webhookId: { type: 'string', description: 'Webhook ID' },
|
|
307
|
+
cursor: { type: 'number', description: 'Cursor for pagination' }
|
|
308
|
+
},
|
|
309
|
+
required: ['webhookId']
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: 'refresh_webhook',
|
|
314
|
+
description: 'Refresh a webhook to extend its expiration',
|
|
315
|
+
inputSchema: {
|
|
316
|
+
type: 'object',
|
|
317
|
+
properties: {
|
|
318
|
+
webhookId: { type: 'string', description: 'Webhook ID to refresh' }
|
|
319
|
+
},
|
|
320
|
+
required: ['webhookId']
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
327
|
+
res.end(JSON.stringify(response));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
91
331
|
if (request.method === 'resources/list') {
|
|
92
332
|
const response = {
|
|
93
333
|
jsonrpc: '2.0',
|
|
@@ -129,15 +369,207 @@ const server = http.createServer(async (req, res) => {
|
|
|
129
369
|
// Handle tool calls
|
|
130
370
|
if (request.method === 'tools/call') {
|
|
131
371
|
const toolName = request.params.name;
|
|
372
|
+
const toolParams = request.params.arguments || {};
|
|
373
|
+
|
|
374
|
+
let result;
|
|
375
|
+
let responseText;
|
|
132
376
|
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
377
|
+
try {
|
|
378
|
+
// LIST TABLES
|
|
379
|
+
if (toolName === 'list_tables') {
|
|
380
|
+
result = await callAirtableAPI(`meta/bases/${baseId}/tables`);
|
|
381
|
+
const tables = result.tables || [];
|
|
382
|
+
|
|
383
|
+
responseText = tables.length > 0
|
|
384
|
+
? `Found ${tables.length} table(s):\n` + tables.map((table, i) =>
|
|
385
|
+
`${i+1}. ${table.name} (ID: ${table.id}, Fields: ${table.fields?.length || 0})`
|
|
386
|
+
).join('\n')
|
|
387
|
+
: 'No tables found in this base.';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// LIST RECORDS
|
|
391
|
+
else if (toolName === 'list_records') {
|
|
392
|
+
const { table, maxRecords, view } = toolParams;
|
|
393
|
+
|
|
394
|
+
const queryParams = {};
|
|
395
|
+
if (maxRecords) queryParams.maxRecords = maxRecords;
|
|
396
|
+
if (view) queryParams.view = view;
|
|
397
|
+
|
|
398
|
+
result = await callAirtableAPI(`${table}`, 'GET', null, queryParams);
|
|
399
|
+
const records = result.records || [];
|
|
400
|
+
|
|
401
|
+
responseText = records.length > 0
|
|
402
|
+
? `Found ${records.length} record(s) in table "${table}":\n` +
|
|
403
|
+
records.map((record, i) =>
|
|
404
|
+
`${i+1}. ID: ${record.id}\n Fields: ${JSON.stringify(record.fields, null, 2)}`
|
|
405
|
+
).join('\n\n')
|
|
406
|
+
: `No records found in table "${table}".`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// GET SINGLE RECORD
|
|
410
|
+
else if (toolName === 'get_record') {
|
|
411
|
+
const { table, recordId } = toolParams;
|
|
412
|
+
|
|
413
|
+
result = await callAirtableAPI(`${table}/${recordId}`);
|
|
414
|
+
|
|
415
|
+
responseText = `Record ${recordId} from table "${table}":\n` +
|
|
416
|
+
JSON.stringify(result.fields, null, 2) +
|
|
417
|
+
`\n\nCreated: ${result.createdTime}`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// CREATE RECORD
|
|
421
|
+
else if (toolName === 'create_record') {
|
|
422
|
+
const { table, fields } = toolParams;
|
|
423
|
+
|
|
424
|
+
const body = {
|
|
425
|
+
fields: fields
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
result = await callAirtableAPI(table, 'POST', body);
|
|
429
|
+
|
|
430
|
+
responseText = `Successfully created record in table "${table}":\n` +
|
|
431
|
+
`Record ID: ${result.id}\n` +
|
|
432
|
+
`Fields: ${JSON.stringify(result.fields, null, 2)}\n` +
|
|
433
|
+
`Created at: ${result.createdTime}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// UPDATE RECORD
|
|
437
|
+
else if (toolName === 'update_record') {
|
|
438
|
+
const { table, recordId, fields } = toolParams;
|
|
439
|
+
|
|
440
|
+
const body = {
|
|
441
|
+
fields: fields
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
result = await callAirtableAPI(`${table}/${recordId}`, 'PATCH', body);
|
|
445
|
+
|
|
446
|
+
responseText = `Successfully updated record ${recordId} in table "${table}":\n` +
|
|
447
|
+
`Updated fields: ${JSON.stringify(result.fields, null, 2)}`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// DELETE RECORD
|
|
451
|
+
else if (toolName === 'delete_record') {
|
|
452
|
+
const { table, recordId } = toolParams;
|
|
453
|
+
|
|
454
|
+
result = await callAirtableAPI(`${table}/${recordId}`, 'DELETE');
|
|
455
|
+
|
|
456
|
+
responseText = `Successfully deleted record ${recordId} from table "${table}".\n` +
|
|
457
|
+
`Deleted record ID: ${result.id}\n` +
|
|
458
|
+
`Deleted: ${result.deleted}`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// SEARCH RECORDS
|
|
462
|
+
else if (toolName === 'search_records') {
|
|
463
|
+
const { table, filterByFormula, sort, maxRecords, fields } = toolParams;
|
|
464
|
+
|
|
465
|
+
const queryParams = {};
|
|
466
|
+
if (filterByFormula) queryParams.filterByFormula = filterByFormula;
|
|
467
|
+
if (maxRecords) queryParams.maxRecords = maxRecords;
|
|
468
|
+
if (fields && fields.length > 0) queryParams.fields = fields;
|
|
469
|
+
if (sort && sort.length > 0) {
|
|
470
|
+
sort.forEach((s, i) => {
|
|
471
|
+
queryParams[`sort[${i}][field]`] = s.field;
|
|
472
|
+
queryParams[`sort[${i}][direction]`] = s.direction || 'asc';
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
result = await callAirtableAPI(table, 'GET', null, queryParams);
|
|
477
|
+
const records = result.records || [];
|
|
478
|
+
|
|
479
|
+
responseText = records.length > 0
|
|
480
|
+
? `Found ${records.length} matching record(s) in table "${table}":\n` +
|
|
481
|
+
records.map((record, i) =>
|
|
482
|
+
`${i+1}. ID: ${record.id}\n Fields: ${JSON.stringify(record.fields, null, 2)}`
|
|
483
|
+
).join('\n\n')
|
|
484
|
+
: `No records found matching the search criteria in table "${table}".`;
|
|
485
|
+
}
|
|
137
486
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
487
|
+
// LIST WEBHOOKS
|
|
488
|
+
else if (toolName === 'list_webhooks') {
|
|
489
|
+
result = await callAirtableAPI(`bases/${baseId}/webhooks`, 'GET');
|
|
490
|
+
const webhooks = result.webhooks || [];
|
|
491
|
+
|
|
492
|
+
responseText = webhooks.length > 0
|
|
493
|
+
? `Found ${webhooks.length} webhook(s):\n` +
|
|
494
|
+
webhooks.map((webhook, i) =>
|
|
495
|
+
`${i+1}. ID: ${webhook.id}\n` +
|
|
496
|
+
` URL: ${webhook.notificationUrl}\n` +
|
|
497
|
+
` Active: ${webhook.isHookEnabled}\n` +
|
|
498
|
+
` Created: ${webhook.createdTime}\n` +
|
|
499
|
+
` Expires: ${webhook.expirationTime}`
|
|
500
|
+
).join('\n\n')
|
|
501
|
+
: 'No webhooks configured for this base.';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// CREATE WEBHOOK
|
|
505
|
+
else if (toolName === 'create_webhook') {
|
|
506
|
+
const { notificationUrl, specification } = toolParams;
|
|
507
|
+
|
|
508
|
+
const body = {
|
|
509
|
+
notificationUrl: notificationUrl,
|
|
510
|
+
specification: specification || {
|
|
511
|
+
options: {
|
|
512
|
+
filters: {
|
|
513
|
+
dataTypes: ['tableData']
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
result = await callAirtableAPI(`bases/${baseId}/webhooks`, 'POST', body);
|
|
520
|
+
|
|
521
|
+
responseText = `Successfully created webhook:\n` +
|
|
522
|
+
`Webhook ID: ${result.id}\n` +
|
|
523
|
+
`URL: ${result.notificationUrl}\n` +
|
|
524
|
+
`MAC Secret: ${result.macSecretBase64}\n` +
|
|
525
|
+
`Expiration: ${result.expirationTime}\n` +
|
|
526
|
+
`Cursor: ${result.cursorForNextPayload}\n\n` +
|
|
527
|
+
`⚠️ IMPORTANT: Save the MAC secret - it won't be shown again!`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// DELETE WEBHOOK
|
|
531
|
+
else if (toolName === 'delete_webhook') {
|
|
532
|
+
const { webhookId } = toolParams;
|
|
533
|
+
|
|
534
|
+
await callAirtableAPI(`bases/${baseId}/webhooks/${webhookId}`, 'DELETE');
|
|
535
|
+
|
|
536
|
+
responseText = `Successfully deleted webhook ${webhookId}`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// GET WEBHOOK PAYLOADS
|
|
540
|
+
else if (toolName === 'get_webhook_payloads') {
|
|
541
|
+
const { webhookId, cursor } = toolParams;
|
|
542
|
+
|
|
543
|
+
const queryParams = {};
|
|
544
|
+
if (cursor) queryParams.cursor = cursor;
|
|
545
|
+
|
|
546
|
+
result = await callAirtableAPI(`bases/${baseId}/webhooks/${webhookId}/payloads`, 'GET', null, queryParams);
|
|
547
|
+
|
|
548
|
+
const payloads = result.payloads || [];
|
|
549
|
+
responseText = payloads.length > 0
|
|
550
|
+
? `Found ${payloads.length} webhook payload(s):\n` +
|
|
551
|
+
payloads.map((payload, i) =>
|
|
552
|
+
`${i+1}. Timestamp: ${payload.timestamp}\n` +
|
|
553
|
+
` Base/Table: ${payload.baseTransactionNumber}\n` +
|
|
554
|
+
` Change Types: ${JSON.stringify(payload.changePayload?.changedTablesById || {})}`
|
|
555
|
+
).join('\n\n') +
|
|
556
|
+
(result.cursor ? `\n\nNext cursor: ${result.cursor}` : '')
|
|
557
|
+
: 'No payloads found for this webhook.';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// REFRESH WEBHOOK
|
|
561
|
+
else if (toolName === 'refresh_webhook') {
|
|
562
|
+
const { webhookId } = toolParams;
|
|
563
|
+
|
|
564
|
+
result = await callAirtableAPI(`bases/${baseId}/webhooks/${webhookId}/refresh`, 'POST');
|
|
565
|
+
|
|
566
|
+
responseText = `Successfully refreshed webhook ${webhookId}:\n` +
|
|
567
|
+
`New expiration: ${result.expirationTime}`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
else {
|
|
571
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
572
|
+
}
|
|
141
573
|
|
|
142
574
|
const response = {
|
|
143
575
|
jsonrpc: '2.0',
|
|
@@ -146,33 +578,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
146
578
|
content: [
|
|
147
579
|
{
|
|
148
580
|
type: 'text',
|
|
149
|
-
text:
|
|
150
|
-
? `Tables in this base:\n${tableList}`
|
|
151
|
-
: 'No tables found in this base.'
|
|
581
|
+
text: responseText
|
|
152
582
|
}
|
|
153
583
|
]
|
|
154
584
|
}
|
|
155
585
|
};
|
|
156
|
-
|
|
157
586
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
158
587
|
res.end(JSON.stringify(response));
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (toolName === 'list_records') {
|
|
163
|
-
const tableName = request.params.arguments.table_name;
|
|
164
|
-
const maxRecords = request.params.arguments.max_records || 100;
|
|
165
588
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const records = result.records || [];
|
|
169
|
-
|
|
170
|
-
const recordList = records.map((record, i) => {
|
|
171
|
-
const fields = Object.entries(record.fields || {})
|
|
172
|
-
.map(([k, v]) => `${k}: ${v}`)
|
|
173
|
-
.join(', ');
|
|
174
|
-
return `${i+1}. ID: ${record.id} - ${fields}`;
|
|
175
|
-
}).join('\n');
|
|
589
|
+
} catch (error) {
|
|
590
|
+
log(LOG_LEVELS.ERROR, `Tool ${toolName} error:`, error.message);
|
|
176
591
|
|
|
177
592
|
const response = {
|
|
178
593
|
jsonrpc: '2.0',
|
|
@@ -181,30 +596,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
181
596
|
content: [
|
|
182
597
|
{
|
|
183
598
|
type: 'text',
|
|
184
|
-
text:
|
|
185
|
-
? `Records:\n${recordList}`
|
|
186
|
-
: 'No records found in this table.'
|
|
599
|
+
text: `Error executing ${toolName}: ${error.message}`
|
|
187
600
|
}
|
|
188
601
|
]
|
|
189
602
|
}
|
|
190
603
|
};
|
|
191
|
-
|
|
192
604
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
193
605
|
res.end(JSON.stringify(response));
|
|
194
|
-
return;
|
|
195
606
|
}
|
|
196
607
|
|
|
197
|
-
// Tool not found
|
|
198
|
-
const response = {
|
|
199
|
-
jsonrpc: '2.0',
|
|
200
|
-
id: request.id,
|
|
201
|
-
error: {
|
|
202
|
-
code: -32601,
|
|
203
|
-
message: `Tool ${toolName} not found`
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
207
|
-
res.end(JSON.stringify(response));
|
|
208
608
|
return;
|
|
209
609
|
}
|
|
210
610
|
|
|
@@ -221,7 +621,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
221
621
|
res.end(JSON.stringify(response));
|
|
222
622
|
|
|
223
623
|
} catch (error) {
|
|
224
|
-
|
|
624
|
+
log(LOG_LEVELS.ERROR, 'Error processing request:', error);
|
|
225
625
|
const response = {
|
|
226
626
|
jsonrpc: '2.0',
|
|
227
627
|
id: request.id || null,
|
|
@@ -236,42 +636,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
236
636
|
});
|
|
237
637
|
});
|
|
238
638
|
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
return new Promise((resolve, reject) => {
|
|
242
|
-
const queryParams = new URLSearchParams(params).toString();
|
|
243
|
-
const url = `https://api.airtable.com/v0/${endpoint}${queryParams ? '?' + queryParams : ''}`;
|
|
244
|
-
|
|
245
|
-
const options = {
|
|
246
|
-
headers: {
|
|
247
|
-
'Authorization': `Bearer ${token}`,
|
|
248
|
-
'Content-Type': 'application/json'
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
https.get(url, options, (response) => {
|
|
253
|
-
let data = '';
|
|
254
|
-
|
|
255
|
-
response.on('data', (chunk) => {
|
|
256
|
-
data += chunk;
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
response.on('end', () => {
|
|
260
|
-
try {
|
|
261
|
-
resolve(JSON.parse(data));
|
|
262
|
-
} catch (e) {
|
|
263
|
-
reject(new Error(`Failed to parse Airtable response: ${e.message}`));
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
}).on('error', (error) => {
|
|
267
|
-
reject(new Error(`Airtable API request failed: ${error.message}`));
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Start the server on port 8010
|
|
273
|
-
const PORT = 8010;
|
|
639
|
+
// Start server
|
|
640
|
+
const PORT = process.env.PORT || 8010;
|
|
274
641
|
server.listen(PORT, () => {
|
|
275
|
-
|
|
642
|
+
log(LOG_LEVELS.INFO, `Enhanced Airtable MCP server v1.4.0 running at http://localhost:${PORT}/mcp`);
|
|
276
643
|
console.log(`For Claude, use this URL: http://localhost:${PORT}/mcp`);
|
|
277
|
-
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// Graceful shutdown
|
|
647
|
+
process.on('SIGINT', () => {
|
|
648
|
+
log(LOG_LEVELS.INFO, 'Shutting down server...');
|
|
649
|
+
server.close(() => {
|
|
650
|
+
log(LOG_LEVELS.INFO, 'Server stopped');
|
|
651
|
+
process.exit(0);
|
|
652
|
+
});
|
|
653
|
+
});
|