@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.
- package/.claude/settings.local.json +4 -1
- package/CAPABILITY_REPORT.md +118 -0
- package/DEVELOPMENT.md +2 -0
- package/IMPROVEMENT_PROPOSAL.md +371 -0
- package/MCP_REVIEW_SUMMARY.md +2 -0
- package/README.md +98 -15
- package/RELEASE_NOTES_v1.2.3.md +2 -0
- package/RELEASE_NOTES_v1.4.0.md +104 -0
- package/RELEASE_NOTES_v1.5.0.md +185 -0
- package/airtable_enhanced.js +499 -0
- package/airtable_simple.js +921 -83
- package/airtable_simple_v1.2.4_backup.js +277 -0
- package/airtable_v1.4.0.js +654 -0
- package/cleanup.sh +2 -0
- package/inspector_server.py +1 -1
- package/package.json +1 -1
- package/quick_test.sh +2 -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 +2 -0
- package/test_mock_server.js +180 -0
- package/test_v1.4.0_final.sh +131 -0
- package/test_v1.5.0_comprehensive.sh +96 -0
- package/test_v1.5.0_final.sh +224 -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.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
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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:
|
|
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
|
-
|
|
171
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
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;
|
|
1101
|
+
// Start server
|
|
1102
|
+
const PORT = process.env.PORT || 8010;
|
|
274
1103
|
server.listen(PORT, () => {
|
|
275
|
-
|
|
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
|
+
});
|