@rashidazarang/airtable-mcp 1.2.4 → 1.5.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.
@@ -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 airtable_simple.js --token YOUR_TOKEN --base YOUR_BASE_ID');
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 with token ${token.slice(0, 5)}...${token.slice(-5)} and base ${baseId}`);
58
+ log(LOG_LEVELS.INFO, `Starting Enhanced Airtable MCP server v1.5.0`);
59
+ log(LOG_LEVELS.INFO, `Authentication configured`);
60
+ log(LOG_LEVELS.INFO, `Base connection established`);
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,323 @@ 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
+ name: 'list_bases',
325
+ description: 'List all accessible Airtable bases',
326
+ inputSchema: {
327
+ type: 'object',
328
+ properties: {
329
+ offset: { type: 'string', description: 'Pagination offset for listing more bases' }
330
+ }
331
+ }
332
+ },
333
+ {
334
+ name: 'get_base_schema',
335
+ description: 'Get complete schema information for a base',
336
+ inputSchema: {
337
+ type: 'object',
338
+ properties: {
339
+ baseId: { type: 'string', description: 'Base ID to get schema for (optional, defaults to current base)' }
340
+ }
341
+ }
342
+ },
343
+ {
344
+ name: 'describe_table',
345
+ description: 'Get detailed information about a specific table including all fields',
346
+ inputSchema: {
347
+ type: 'object',
348
+ properties: {
349
+ table: { type: 'string', description: 'Table name or ID' }
350
+ },
351
+ required: ['table']
352
+ }
353
+ },
354
+ {
355
+ name: 'create_table',
356
+ description: 'Create a new table in the base',
357
+ inputSchema: {
358
+ type: 'object',
359
+ properties: {
360
+ name: { type: 'string', description: 'Name for the new table' },
361
+ description: { type: 'string', description: 'Optional description for the table' },
362
+ fields: {
363
+ type: 'array',
364
+ description: 'Array of field definitions',
365
+ items: {
366
+ type: 'object',
367
+ properties: {
368
+ name: { type: 'string', description: 'Field name' },
369
+ type: { type: 'string', description: 'Field type (singleLineText, number, etc.)' },
370
+ description: { type: 'string', description: 'Field description' },
371
+ options: { type: 'object', description: 'Field-specific options' }
372
+ },
373
+ required: ['name', 'type']
374
+ }
375
+ }
376
+ },
377
+ required: ['name', 'fields']
378
+ }
379
+ },
380
+ {
381
+ name: 'update_table',
382
+ description: 'Update table name or description',
383
+ inputSchema: {
384
+ type: 'object',
385
+ properties: {
386
+ table: { type: 'string', description: 'Table name or ID' },
387
+ name: { type: 'string', description: 'New table name' },
388
+ description: { type: 'string', description: 'New table description' }
389
+ },
390
+ required: ['table']
391
+ }
392
+ },
393
+ {
394
+ name: 'delete_table',
395
+ description: 'Delete a table (WARNING: This will permanently delete all data)',
396
+ inputSchema: {
397
+ type: 'object',
398
+ properties: {
399
+ table: { type: 'string', description: 'Table name or ID to delete' },
400
+ confirm: { type: 'boolean', description: 'Must be true to confirm deletion' }
401
+ },
402
+ required: ['table', 'confirm']
403
+ }
404
+ },
405
+ {
406
+ name: 'create_field',
407
+ description: 'Add a new field to an existing table',
408
+ inputSchema: {
409
+ type: 'object',
410
+ properties: {
411
+ table: { type: 'string', description: 'Table name or ID' },
412
+ name: { type: 'string', description: 'Field name' },
413
+ type: { type: 'string', description: 'Field type (singleLineText, number, multipleSelectionList, etc.)' },
414
+ description: { type: 'string', description: 'Field description' },
415
+ options: { type: 'object', description: 'Field-specific options (e.g., choices for select fields)' }
416
+ },
417
+ required: ['table', 'name', 'type']
418
+ }
419
+ },
420
+ {
421
+ name: 'update_field',
422
+ description: 'Update field properties',
423
+ inputSchema: {
424
+ type: 'object',
425
+ properties: {
426
+ table: { type: 'string', description: 'Table name or ID' },
427
+ fieldId: { type: 'string', description: 'Field ID to update' },
428
+ name: { type: 'string', description: 'New field name' },
429
+ description: { type: 'string', description: 'New field description' },
430
+ options: { type: 'object', description: 'Updated field options' }
431
+ },
432
+ required: ['table', 'fieldId']
433
+ }
434
+ },
435
+ {
436
+ name: 'delete_field',
437
+ description: 'Delete a field from a table (WARNING: This will permanently delete all data in this field)',
438
+ inputSchema: {
439
+ type: 'object',
440
+ properties: {
441
+ table: { type: 'string', description: 'Table name or ID' },
442
+ fieldId: { type: 'string', description: 'Field ID to delete' },
443
+ confirm: { type: 'boolean', description: 'Must be true to confirm deletion' }
444
+ },
445
+ required: ['table', 'fieldId', 'confirm']
446
+ }
447
+ },
448
+ {
449
+ name: 'list_field_types',
450
+ description: 'Get a reference of all available Airtable field types and their schemas',
451
+ inputSchema: {
452
+ type: 'object',
453
+ properties: {}
454
+ }
455
+ },
456
+ {
457
+ name: 'get_table_views',
458
+ description: 'List all views for a specific table',
459
+ inputSchema: {
460
+ type: 'object',
461
+ properties: {
462
+ table: { type: 'string', description: 'Table name or ID' }
463
+ },
464
+ required: ['table']
465
+ }
466
+ }
467
+ ]
468
+ }
469
+ };
470
+ res.writeHead(200, { 'Content-Type': 'application/json' });
471
+ res.end(JSON.stringify(response));
472
+ return;
473
+ }
474
+
91
475
  if (request.method === 'resources/list') {
92
476
  const response = {
93
477
  jsonrpc: '2.0',
@@ -129,15 +513,525 @@ const server = http.createServer(async (req, res) => {
129
513
  // Handle tool calls
130
514
  if (request.method === 'tools/call') {
131
515
  const toolName = request.params.name;
516
+ const toolParams = request.params.arguments || {};
517
+
518
+ let result;
519
+ let responseText;
132
520
 
133
- if (toolName === 'list_tables') {
134
- // Call Airtable API to list tables
135
- const result = await callAirtableAPI(`meta/bases/${baseId}/tables`);
136
- const tables = result.tables || [];
521
+ try {
522
+ // LIST TABLES
523
+ if (toolName === 'list_tables') {
524
+ result = await callAirtableAPI(`meta/bases/${baseId}/tables`);
525
+ const tables = result.tables || [];
526
+
527
+ responseText = tables.length > 0
528
+ ? `Found ${tables.length} table(s):\n` + tables.map((table, i) =>
529
+ `${i+1}. ${table.name} (ID: ${table.id}, Fields: ${table.fields?.length || 0})`
530
+ ).join('\n')
531
+ : 'No tables found in this base.';
532
+ }
533
+
534
+ // LIST RECORDS
535
+ else if (toolName === 'list_records') {
536
+ const { table, maxRecords, view } = toolParams;
537
+
538
+ const queryParams = {};
539
+ if (maxRecords) queryParams.maxRecords = maxRecords;
540
+ if (view) queryParams.view = view;
541
+
542
+ result = await callAirtableAPI(`${table}`, 'GET', null, queryParams);
543
+ const records = result.records || [];
544
+
545
+ responseText = records.length > 0
546
+ ? `Found ${records.length} record(s) in table "${table}":\n` +
547
+ records.map((record, i) =>
548
+ `${i+1}. ID: ${record.id}\n Fields: ${JSON.stringify(record.fields, null, 2)}`
549
+ ).join('\n\n')
550
+ : `No records found in table "${table}".`;
551
+ }
552
+
553
+ // GET SINGLE RECORD
554
+ else if (toolName === 'get_record') {
555
+ const { table, recordId } = toolParams;
556
+
557
+ result = await callAirtableAPI(`${table}/${recordId}`);
558
+
559
+ responseText = `Record ${recordId} from table "${table}":\n` +
560
+ JSON.stringify(result.fields, null, 2) +
561
+ `\n\nCreated: ${result.createdTime}`;
562
+ }
563
+
564
+ // CREATE RECORD
565
+ else if (toolName === 'create_record') {
566
+ const { table, fields } = toolParams;
567
+
568
+ const body = {
569
+ fields: fields
570
+ };
571
+
572
+ result = await callAirtableAPI(table, 'POST', body);
573
+
574
+ responseText = `Successfully created record in table "${table}":\n` +
575
+ `Record ID: ${result.id}\n` +
576
+ `Fields: ${JSON.stringify(result.fields, null, 2)}\n` +
577
+ `Created at: ${result.createdTime}`;
578
+ }
579
+
580
+ // UPDATE RECORD
581
+ else if (toolName === 'update_record') {
582
+ const { table, recordId, fields } = toolParams;
583
+
584
+ const body = {
585
+ fields: fields
586
+ };
587
+
588
+ result = await callAirtableAPI(`${table}/${recordId}`, 'PATCH', body);
589
+
590
+ responseText = `Successfully updated record ${recordId} in table "${table}":\n` +
591
+ `Updated fields: ${JSON.stringify(result.fields, null, 2)}`;
592
+ }
593
+
594
+ // DELETE RECORD
595
+ else if (toolName === 'delete_record') {
596
+ const { table, recordId } = toolParams;
597
+
598
+ result = await callAirtableAPI(`${table}/${recordId}`, 'DELETE');
599
+
600
+ responseText = `Successfully deleted record ${recordId} from table "${table}".\n` +
601
+ `Deleted record ID: ${result.id}\n` +
602
+ `Deleted: ${result.deleted}`;
603
+ }
604
+
605
+ // SEARCH RECORDS
606
+ else if (toolName === 'search_records') {
607
+ const { table, filterByFormula, sort, maxRecords, fields } = toolParams;
608
+
609
+ const queryParams = {};
610
+ if (filterByFormula) queryParams.filterByFormula = filterByFormula;
611
+ if (maxRecords) queryParams.maxRecords = maxRecords;
612
+ if (fields && fields.length > 0) queryParams.fields = fields;
613
+ if (sort && sort.length > 0) {
614
+ sort.forEach((s, i) => {
615
+ queryParams[`sort[${i}][field]`] = s.field;
616
+ queryParams[`sort[${i}][direction]`] = s.direction || 'asc';
617
+ });
618
+ }
619
+
620
+ result = await callAirtableAPI(table, 'GET', null, queryParams);
621
+ const records = result.records || [];
622
+
623
+ responseText = records.length > 0
624
+ ? `Found ${records.length} matching record(s) in table "${table}":\n` +
625
+ records.map((record, i) =>
626
+ `${i+1}. ID: ${record.id}\n Fields: ${JSON.stringify(record.fields, null, 2)}`
627
+ ).join('\n\n')
628
+ : `No records found matching the search criteria in table "${table}".`;
629
+ }
630
+
631
+ // LIST WEBHOOKS
632
+ else if (toolName === 'list_webhooks') {
633
+ result = await callAirtableAPI(`bases/${baseId}/webhooks`, 'GET');
634
+ const webhooks = result.webhooks || [];
635
+
636
+ responseText = webhooks.length > 0
637
+ ? `Found ${webhooks.length} webhook(s):\n` +
638
+ webhooks.map((webhook, i) =>
639
+ `${i+1}. ID: ${webhook.id}\n` +
640
+ ` URL: ${webhook.notificationUrl}\n` +
641
+ ` Active: ${webhook.isHookEnabled}\n` +
642
+ ` Created: ${webhook.createdTime}\n` +
643
+ ` Expires: ${webhook.expirationTime}`
644
+ ).join('\n\n')
645
+ : 'No webhooks configured for this base.';
646
+ }
647
+
648
+ // CREATE WEBHOOK
649
+ else if (toolName === 'create_webhook') {
650
+ const { notificationUrl, specification } = toolParams;
651
+
652
+ const body = {
653
+ notificationUrl: notificationUrl,
654
+ specification: specification || {
655
+ options: {
656
+ filters: {
657
+ dataTypes: ['tableData']
658
+ }
659
+ }
660
+ }
661
+ };
662
+
663
+ result = await callAirtableAPI(`bases/${baseId}/webhooks`, 'POST', body);
664
+
665
+ responseText = `Successfully created webhook:\n` +
666
+ `Webhook ID: ${result.id}\n` +
667
+ `URL: ${result.notificationUrl}\n` +
668
+ `MAC Secret: ${result.macSecretBase64}\n` +
669
+ `Expiration: ${result.expirationTime}\n` +
670
+ `Cursor: ${result.cursorForNextPayload}\n\n` +
671
+ `⚠️ IMPORTANT: Save the MAC secret - it won't be shown again!`;
672
+ }
673
+
674
+ // DELETE WEBHOOK
675
+ else if (toolName === 'delete_webhook') {
676
+ const { webhookId } = toolParams;
677
+
678
+ await callAirtableAPI(`bases/${baseId}/webhooks/${webhookId}`, 'DELETE');
679
+
680
+ responseText = `Successfully deleted webhook ${webhookId}`;
681
+ }
682
+
683
+ // GET WEBHOOK PAYLOADS
684
+ else if (toolName === 'get_webhook_payloads') {
685
+ const { webhookId, cursor } = toolParams;
686
+
687
+ const queryParams = {};
688
+ if (cursor) queryParams.cursor = cursor;
689
+
690
+ result = await callAirtableAPI(`bases/${baseId}/webhooks/${webhookId}/payloads`, 'GET', null, queryParams);
691
+
692
+ const payloads = result.payloads || [];
693
+ responseText = payloads.length > 0
694
+ ? `Found ${payloads.length} webhook payload(s):\n` +
695
+ payloads.map((payload, i) =>
696
+ `${i+1}. Timestamp: ${payload.timestamp}\n` +
697
+ ` Base/Table: ${payload.baseTransactionNumber}\n` +
698
+ ` Change Types: ${JSON.stringify(payload.changePayload?.changedTablesById || {})}`
699
+ ).join('\n\n') +
700
+ (result.cursor ? `\n\nNext cursor: ${result.cursor}` : '')
701
+ : 'No payloads found for this webhook.';
702
+ }
703
+
704
+ // REFRESH WEBHOOK
705
+ else if (toolName === 'refresh_webhook') {
706
+ const { webhookId } = toolParams;
707
+
708
+ result = await callAirtableAPI(`bases/${baseId}/webhooks/${webhookId}/refresh`, 'POST');
709
+
710
+ responseText = `Successfully refreshed webhook ${webhookId}:\n` +
711
+ `New expiration: ${result.expirationTime}`;
712
+ }
713
+
714
+ // Schema Management Tools
715
+ else if (toolName === 'list_bases') {
716
+ const { offset } = toolParams;
717
+ const queryParams = offset ? { offset } : {};
718
+
719
+ result = await callAirtableAPI('meta/bases', 'GET', null, queryParams);
720
+
721
+ if (result.bases && result.bases.length > 0) {
722
+ responseText = `Found ${result.bases.length} accessible base(s):\n`;
723
+ result.bases.forEach((base, index) => {
724
+ responseText += `${index + 1}. ${base.name} (ID: ${base.id})\n`;
725
+ if (base.permissionLevel) {
726
+ responseText += ` Permission: ${base.permissionLevel}\n`;
727
+ }
728
+ });
729
+ if (result.offset) {
730
+ responseText += `\nNext page offset: ${result.offset}`;
731
+ }
732
+ } else {
733
+ responseText = 'No accessible bases found.';
734
+ }
735
+ }
736
+
737
+ else if (toolName === 'get_base_schema') {
738
+ const { baseId: targetBaseId } = toolParams;
739
+ const targetId = targetBaseId || baseId;
740
+
741
+ result = await callAirtableAPI(`meta/bases/${targetId}/tables`, 'GET');
742
+
743
+ if (result.tables && result.tables.length > 0) {
744
+ responseText = `Base schema for ${targetId}:\n\n`;
745
+ result.tables.forEach((table, index) => {
746
+ responseText += `${index + 1}. Table: ${table.name} (ID: ${table.id})\n`;
747
+ if (table.description) {
748
+ responseText += ` Description: ${table.description}\n`;
749
+ }
750
+ responseText += ` Fields (${table.fields.length}):\n`;
751
+ table.fields.forEach((field, fieldIndex) => {
752
+ responseText += ` ${fieldIndex + 1}. ${field.name} (${field.type})\n`;
753
+ if (field.description) {
754
+ responseText += ` Description: ${field.description}\n`;
755
+ }
756
+ });
757
+ if (table.views && table.views.length > 0) {
758
+ responseText += ` Views (${table.views.length}): ${table.views.map(v => v.name).join(', ')}\n`;
759
+ }
760
+ responseText += '\n';
761
+ });
762
+ } else {
763
+ responseText = 'No tables found in this base.';
764
+ }
765
+ }
766
+
767
+ else if (toolName === 'describe_table') {
768
+ const { table } = toolParams;
769
+
770
+ // Get table schema first
771
+ const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
772
+ const tableInfo = schemaResult.tables.find(t =>
773
+ t.name.toLowerCase() === table.toLowerCase() || t.id === table
774
+ );
775
+
776
+ if (!tableInfo) {
777
+ responseText = `Table "${table}" not found.`;
778
+ } else {
779
+ responseText = `Table Details: ${tableInfo.name}\n`;
780
+ responseText += `ID: ${tableInfo.id}\n`;
781
+ if (tableInfo.description) {
782
+ responseText += `Description: ${tableInfo.description}\n`;
783
+ }
784
+ responseText += `\nFields (${tableInfo.fields.length}):\n`;
785
+
786
+ tableInfo.fields.forEach((field, index) => {
787
+ responseText += `${index + 1}. ${field.name}\n`;
788
+ responseText += ` Type: ${field.type}\n`;
789
+ responseText += ` ID: ${field.id}\n`;
790
+ if (field.description) {
791
+ responseText += ` Description: ${field.description}\n`;
792
+ }
793
+ if (field.options) {
794
+ responseText += ` Options: ${JSON.stringify(field.options, null, 2)}\n`;
795
+ }
796
+ responseText += '\n';
797
+ });
798
+
799
+ if (tableInfo.views && tableInfo.views.length > 0) {
800
+ responseText += `Views (${tableInfo.views.length}):\n`;
801
+ tableInfo.views.forEach((view, index) => {
802
+ responseText += `${index + 1}. ${view.name} (${view.type})\n`;
803
+ });
804
+ }
805
+ }
806
+ }
807
+
808
+ else if (toolName === 'create_table') {
809
+ const { name, description, fields } = toolParams;
810
+
811
+ const body = {
812
+ name,
813
+ fields: fields.map(field => ({
814
+ name: field.name,
815
+ type: field.type,
816
+ description: field.description,
817
+ options: field.options
818
+ }))
819
+ };
820
+
821
+ if (description) {
822
+ body.description = description;
823
+ }
824
+
825
+ result = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'POST', body);
826
+
827
+ responseText = `Successfully created table "${name}" (ID: ${result.id})\n`;
828
+ responseText += `Fields created: ${result.fields.length}\n`;
829
+ result.fields.forEach((field, index) => {
830
+ responseText += `${index + 1}. ${field.name} (${field.type})\n`;
831
+ });
832
+ }
137
833
 
138
- const tableList = tables.map((table, i) =>
139
- `${i+1}. ${table.name} (ID: ${table.id})`
140
- ).join('\n');
834
+ else if (toolName === 'update_table') {
835
+ const { table, name, description } = toolParams;
836
+
837
+ // Get table ID first
838
+ const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
839
+ const tableInfo = schemaResult.tables.find(t =>
840
+ t.name.toLowerCase() === table.toLowerCase() || t.id === table
841
+ );
842
+
843
+ if (!tableInfo) {
844
+ responseText = `Table "${table}" not found.`;
845
+ } else {
846
+ const body = {};
847
+ if (name) body.name = name;
848
+ if (description !== undefined) body.description = description;
849
+
850
+ if (Object.keys(body).length === 0) {
851
+ responseText = 'No updates specified. Provide name or description to update.';
852
+ } else {
853
+ result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}`, 'PATCH', body);
854
+ responseText = `Successfully updated table "${tableInfo.name}":\n`;
855
+ if (name) responseText += `New name: ${result.name}\n`;
856
+ if (description !== undefined) responseText += `New description: ${result.description || '(none)'}\n`;
857
+ }
858
+ }
859
+ }
860
+
861
+ else if (toolName === 'delete_table') {
862
+ const { table, confirm } = toolParams;
863
+
864
+ if (!confirm) {
865
+ responseText = 'Table deletion requires confirm=true to proceed. This action cannot be undone!';
866
+ } else {
867
+ // Get table ID first
868
+ const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
869
+ const tableInfo = schemaResult.tables.find(t =>
870
+ t.name.toLowerCase() === table.toLowerCase() || t.id === table
871
+ );
872
+
873
+ if (!tableInfo) {
874
+ responseText = `Table "${table}" not found.`;
875
+ } else {
876
+ result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}`, 'DELETE');
877
+ responseText = `Successfully deleted table "${tableInfo.name}" (ID: ${tableInfo.id})\n`;
878
+ responseText += 'All data in this table has been permanently removed.';
879
+ }
880
+ }
881
+ }
882
+
883
+ // Field Management Tools
884
+ else if (toolName === 'create_field') {
885
+ const { table, name, type, description, options } = toolParams;
886
+
887
+ // Get table ID first
888
+ const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
889
+ const tableInfo = schemaResult.tables.find(t =>
890
+ t.name.toLowerCase() === table.toLowerCase() || t.id === table
891
+ );
892
+
893
+ if (!tableInfo) {
894
+ responseText = `Table "${table}" not found.`;
895
+ } else {
896
+ const body = {
897
+ name,
898
+ type
899
+ };
900
+
901
+ if (description) body.description = description;
902
+ if (options) body.options = options;
903
+
904
+ result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/fields`, 'POST', body);
905
+
906
+ responseText = `Successfully created field "${name}" in table "${tableInfo.name}"\n`;
907
+ responseText += `Field ID: ${result.id}\n`;
908
+ responseText += `Type: ${result.type}\n`;
909
+ if (result.description) {
910
+ responseText += `Description: ${result.description}\n`;
911
+ }
912
+ }
913
+ }
914
+
915
+ else if (toolName === 'update_field') {
916
+ const { table, fieldId, name, description, options } = toolParams;
917
+
918
+ // Get table ID first
919
+ const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
920
+ const tableInfo = schemaResult.tables.find(t =>
921
+ t.name.toLowerCase() === table.toLowerCase() || t.id === table
922
+ );
923
+
924
+ if (!tableInfo) {
925
+ responseText = `Table "${table}" not found.`;
926
+ } else {
927
+ const body = {};
928
+ if (name) body.name = name;
929
+ if (description !== undefined) body.description = description;
930
+ if (options) body.options = options;
931
+
932
+ if (Object.keys(body).length === 0) {
933
+ responseText = 'No updates specified. Provide name, description, or options to update.';
934
+ } else {
935
+ result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/fields/${fieldId}`, 'PATCH', body);
936
+ responseText = `Successfully updated field in table "${tableInfo.name}":\n`;
937
+ responseText += `Field: ${result.name} (${result.type})\n`;
938
+ responseText += `ID: ${result.id}\n`;
939
+ if (result.description) {
940
+ responseText += `Description: ${result.description}\n`;
941
+ }
942
+ }
943
+ }
944
+ }
945
+
946
+ else if (toolName === 'delete_field') {
947
+ const { table, fieldId, confirm } = toolParams;
948
+
949
+ if (!confirm) {
950
+ responseText = 'Field deletion requires confirm=true to proceed. This action cannot be undone!';
951
+ } else {
952
+ // Get table ID first
953
+ const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
954
+ const tableInfo = schemaResult.tables.find(t =>
955
+ t.name.toLowerCase() === table.toLowerCase() || t.id === table
956
+ );
957
+
958
+ if (!tableInfo) {
959
+ responseText = `Table "${table}" not found.`;
960
+ } else {
961
+ result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/fields/${fieldId}`, 'DELETE');
962
+ responseText = `Successfully deleted field from table "${tableInfo.name}"\n`;
963
+ responseText += 'All data in this field has been permanently removed.';
964
+ }
965
+ }
966
+ }
967
+
968
+ else if (toolName === 'list_field_types') {
969
+ responseText = `Available Airtable Field Types:\n\n`;
970
+ responseText += `Basic Fields:\n`;
971
+ responseText += `• singleLineText - Single line text input\n`;
972
+ responseText += `• multilineText - Multi-line text input\n`;
973
+ responseText += `• richText - Rich text with formatting\n`;
974
+ responseText += `• number - Number field with optional formatting\n`;
975
+ responseText += `• percent - Percentage field\n`;
976
+ responseText += `• currency - Currency field\n`;
977
+ responseText += `• singleSelect - Single choice from predefined options\n`;
978
+ responseText += `• multipleSelectionList - Multiple choices from predefined options\n`;
979
+ responseText += `• date - Date field\n`;
980
+ responseText += `• dateTime - Date and time field\n`;
981
+ responseText += `• phoneNumber - Phone number field\n`;
982
+ responseText += `• email - Email address field\n`;
983
+ responseText += `• url - URL field\n`;
984
+ responseText += `• checkbox - Checkbox (true/false)\n`;
985
+ responseText += `• rating - Star rating field\n`;
986
+ responseText += `• duration - Duration/time field\n\n`;
987
+ responseText += `Advanced Fields:\n`;
988
+ responseText += `• multipleAttachment - File attachments\n`;
989
+ responseText += `• linkedRecord - Link to records in another table\n`;
990
+ responseText += `• lookup - Lookup values from linked records\n`;
991
+ responseText += `• rollup - Calculate values from linked records\n`;
992
+ responseText += `• count - Count of linked records\n`;
993
+ responseText += `• formula - Calculated field with formulas\n`;
994
+ responseText += `• createdTime - Auto-timestamp when record created\n`;
995
+ responseText += `• createdBy - Auto-user who created record\n`;
996
+ responseText += `• lastModifiedTime - Auto-timestamp when record modified\n`;
997
+ responseText += `• lastModifiedBy - Auto-user who last modified record\n`;
998
+ responseText += `• autoNumber - Auto-incrementing number\n`;
999
+ responseText += `• barcode - Barcode/QR code field\n`;
1000
+ responseText += `• button - Action button field\n`;
1001
+ }
1002
+
1003
+ else if (toolName === 'get_table_views') {
1004
+ const { table } = toolParams;
1005
+
1006
+ // Get table schema
1007
+ const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
1008
+ const tableInfo = schemaResult.tables.find(t =>
1009
+ t.name.toLowerCase() === table.toLowerCase() || t.id === table
1010
+ );
1011
+
1012
+ if (!tableInfo) {
1013
+ responseText = `Table "${table}" not found.`;
1014
+ } else {
1015
+ if (tableInfo.views && tableInfo.views.length > 0) {
1016
+ responseText = `Views for table "${tableInfo.name}" (${tableInfo.views.length}):\n\n`;
1017
+ tableInfo.views.forEach((view, index) => {
1018
+ responseText += `${index + 1}. ${view.name}\n`;
1019
+ responseText += ` Type: ${view.type}\n`;
1020
+ responseText += ` ID: ${view.id}\n`;
1021
+ if (view.visibleFieldIds && view.visibleFieldIds.length > 0) {
1022
+ responseText += ` Visible fields: ${view.visibleFieldIds.length}\n`;
1023
+ }
1024
+ responseText += '\n';
1025
+ });
1026
+ } else {
1027
+ responseText = `No views found for table "${tableInfo.name}".`;
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ else {
1033
+ throw new Error(`Unknown tool: ${toolName}`);
1034
+ }
141
1035
 
142
1036
  const response = {
143
1037
  jsonrpc: '2.0',
@@ -146,33 +1040,16 @@ const server = http.createServer(async (req, res) => {
146
1040
  content: [
147
1041
  {
148
1042
  type: 'text',
149
- text: tables.length > 0
150
- ? `Tables in this base:\n${tableList}`
151
- : 'No tables found in this base.'
1043
+ text: responseText
152
1044
  }
153
1045
  ]
154
1046
  }
155
1047
  };
156
-
157
1048
  res.writeHead(200, { 'Content-Type': 'application/json' });
158
1049
  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
-
166
- // Call Airtable API to list records
167
- const result = await callAirtableAPI(`${baseId}/${tableName}`, { maxRecords });
168
- const records = result.records || [];
169
1050
 
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');
1051
+ } catch (error) {
1052
+ log(LOG_LEVELS.ERROR, `Tool ${toolName} error:`, error.message);
176
1053
 
177
1054
  const response = {
178
1055
  jsonrpc: '2.0',
@@ -181,30 +1058,15 @@ const server = http.createServer(async (req, res) => {
181
1058
  content: [
182
1059
  {
183
1060
  type: 'text',
184
- text: records.length > 0
185
- ? `Records:\n${recordList}`
186
- : 'No records found in this table.'
1061
+ text: `Error executing ${toolName}: ${error.message}`
187
1062
  }
188
1063
  ]
189
1064
  }
190
1065
  };
191
-
192
1066
  res.writeHead(200, { 'Content-Type': 'application/json' });
193
1067
  res.end(JSON.stringify(response));
194
- return;
195
1068
  }
196
1069
 
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
1070
  return;
209
1071
  }
210
1072
 
@@ -221,7 +1083,7 @@ const server = http.createServer(async (req, res) => {
221
1083
  res.end(JSON.stringify(response));
222
1084
 
223
1085
  } catch (error) {
224
- console.error('Error processing request:', error);
1086
+ log(LOG_LEVELS.ERROR, 'Error processing request:', error);
225
1087
  const response = {
226
1088
  jsonrpc: '2.0',
227
1089
  id: request.id || null,
@@ -236,42 +1098,18 @@ const server = http.createServer(async (req, res) => {
236
1098
  });
237
1099
  });
238
1100
 
239
- // Helper function to call Airtable API
240
- function callAirtableAPI(endpoint, params = {}) {
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;
1101
+ // Start server
1102
+ const PORT = process.env.PORT || 8010;
274
1103
  server.listen(PORT, () => {
275
- console.log(`Airtable MCP server running at http://localhost:${PORT}/mcp`);
1104
+ log(LOG_LEVELS.INFO, `Enhanced Airtable MCP server v1.4.0 running at http://localhost:${PORT}/mcp`);
276
1105
  console.log(`For Claude, use this URL: http://localhost:${PORT}/mcp`);
277
- });
1106
+ });
1107
+
1108
+ // Graceful shutdown
1109
+ process.on('SIGINT', () => {
1110
+ log(LOG_LEVELS.INFO, 'Shutting down server...');
1111
+ server.close(() => {
1112
+ log(LOG_LEVELS.INFO, 'Server stopped');
1113
+ process.exit(0);
1114
+ });
1115
+ });