This is page 4 of 5. Use http://codebase.md/rashidazarang/airtable-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .eslintrc.js ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── custom.md │ │ └── feature_request.md │ └── pull_request_template.md ├── .gitignore ├── .nvmrc ├── .prettierrc ├── bin │ ├── airtable-crud-cli.js │ └── airtable-mcp.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── docker │ ├── Dockerfile │ └── Dockerfile.node ├── docs │ ├── guides │ │ ├── CLAUDE_INTEGRATION.md │ │ ├── ENHANCED_FEATURES.md │ │ ├── INSTALLATION.md │ │ └── QUICK_START.md │ └── releases │ ├── RELEASE_NOTES_v1.2.2.md │ ├── RELEASE_NOTES_v1.2.4.md │ ├── RELEASE_NOTES_v1.4.0.md │ ├── RELEASE_NOTES_v1.5.0.md │ └── RELEASE_NOTES_v1.6.0.md ├── examples │ ├── airtable-crud-example.js │ ├── building-mcp.md │ ├── claude_config.json │ ├── claude_simple_config.json │ ├── env-demo.js │ ├── example_usage.md │ ├── example-tasks-update.json │ ├── example-tasks.json │ ├── python_debug_patch.txt │ ├── sample-transform.js │ ├── typescript │ │ ├── advanced-ai-prompts.ts │ │ ├── basic-usage.ts │ │ └── claude-desktop-config.json │ └── windsurf_mcp_config.json ├── index.js ├── ISSUE_RESPONSES.md ├── jest.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── PROJECT_STRUCTURE.md ├── README.md ├── RELEASE_SUMMARY_v3.2.x.md ├── RELEASE_v3.2.1.md ├── RELEASE_v3.2.3.md ├── RELEASE_v3.2.4.md ├── requirements.txt ├── SECURITY_NOTICE.md ├── smithery.yaml ├── src │ ├── index.js │ ├── javascript │ │ ├── airtable_simple_production.js │ │ └── airtable_simple.js │ ├── python │ │ ├── airtable_mcp │ │ │ ├── __init__.py │ │ │ └── src │ │ │ └── server.py │ │ ├── inspector_server.py │ │ ├── inspector.py │ │ ├── setup.py │ │ ├── simple_airtable_server.py │ │ └── test_client.py │ └── typescript │ ├── ai-prompts.d.ts │ ├── airtable-mcp-server.d.ts │ ├── airtable-mcp-server.ts │ ├── errors.ts │ ├── index.d.ts │ ├── prompt-templates.ts │ ├── test-suite.d.ts │ ├── test-suite.ts │ ├── tools-schemas.ts │ └── tools.d.ts ├── TESTING_REPORT.md ├── tests │ ├── test_all_features.sh │ ├── test_mcp_comprehensive.js │ ├── test_v1.5.0_final.sh │ └── test_v1.6.0_comprehensive.sh ├── tsconfig.json └── types └── typescript ├── airtable-mcp-server.d.ts ├── errors.d.ts ├── prompt-templates.d.ts ├── test-suite.d.ts └── tools-schemas.d.ts ``` # Files -------------------------------------------------------------------------------- /src/javascript/airtable_simple.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | const http = require('http'); 4 | const https = require('https'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | // Load environment variables from .env file if it exists 9 | const envPath = path.join(__dirname, '.env'); 10 | if (fs.existsSync(envPath)) { 11 | require('dotenv').config({ path: envPath }); 12 | } 13 | 14 | // Parse command line arguments with environment variable fallback 15 | const args = process.argv.slice(2); 16 | let tokenIndex = args.indexOf('--token'); 17 | let baseIndex = args.indexOf('--base'); 18 | 19 | // Use environment variables as fallback 20 | const token = tokenIndex !== -1 ? args[tokenIndex + 1] : process.env.AIRTABLE_TOKEN || process.env.AIRTABLE_API_TOKEN; 21 | const baseId = baseIndex !== -1 ? args[baseIndex + 1] : process.env.AIRTABLE_BASE_ID || process.env.AIRTABLE_BASE; 22 | 23 | if (!token || !baseId) { 24 | console.error('Error: Missing Airtable credentials'); 25 | console.error('\nUsage options:'); 26 | console.error(' 1. Command line: node airtable_enhanced.js --token YOUR_TOKEN --base YOUR_BASE_ID'); 27 | console.error(' 2. Environment variables: AIRTABLE_TOKEN and AIRTABLE_BASE_ID'); 28 | console.error(' 3. .env file with AIRTABLE_TOKEN and AIRTABLE_BASE_ID'); 29 | process.exit(1); 30 | } 31 | 32 | // Configure logging levels 33 | const LOG_LEVELS = { 34 | ERROR: 0, 35 | WARN: 1, 36 | INFO: 2, 37 | DEBUG: 3 38 | }; 39 | 40 | const currentLogLevel = process.env.LOG_LEVEL ? LOG_LEVELS[process.env.LOG_LEVEL.toUpperCase()] || LOG_LEVELS.INFO : LOG_LEVELS.INFO; 41 | 42 | function log(level, message, ...args) { 43 | const levelName = Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === level); 44 | const timestamp = new Date().toISOString(); 45 | 46 | if (level <= currentLogLevel) { 47 | const prefix = `[${timestamp}] [${levelName}]`; 48 | if (level === LOG_LEVELS.ERROR) { 49 | console.error(prefix, message, ...args); 50 | } else if (level === LOG_LEVELS.WARN) { 51 | console.warn(prefix, message, ...args); 52 | } else { 53 | console.log(prefix, message, ...args); 54 | } 55 | } 56 | } 57 | 58 | log(LOG_LEVELS.INFO, `Starting Enhanced Airtable MCP server v1.6.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 | } 128 | 129 | // Create HTTP server 130 | const server = http.createServer(async (req, res) => { 131 | // Enable CORS 132 | res.setHeader('Access-Control-Allow-Origin', '*'); 133 | res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); 134 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); 135 | 136 | // Handle preflight request 137 | if (req.method === 'OPTIONS') { 138 | res.writeHead(200); 139 | res.end(); 140 | return; 141 | } 142 | 143 | // Only handle POST requests to /mcp 144 | if (req.method !== 'POST' || !req.url.endsWith('/mcp')) { 145 | res.writeHead(404); 146 | res.end(); 147 | return; 148 | } 149 | 150 | let body = ''; 151 | req.on('data', chunk => { 152 | body += chunk.toString(); 153 | }); 154 | 155 | req.on('end', async () => { 156 | try { 157 | const request = JSON.parse(body); 158 | log(LOG_LEVELS.DEBUG, 'Received request:', JSON.stringify(request, null, 2)); 159 | 160 | // Handle JSON-RPC methods 161 | if (request.method === 'tools/list') { 162 | const response = { 163 | jsonrpc: '2.0', 164 | id: request.id, 165 | result: { 166 | tools: [ 167 | { 168 | name: 'list_tables', 169 | description: 'List all tables in the Airtable base', 170 | inputSchema: { 171 | type: 'object', 172 | properties: {} 173 | } 174 | }, 175 | { 176 | name: 'list_records', 177 | description: 'List records from a specific table', 178 | inputSchema: { 179 | type: 'object', 180 | properties: { 181 | table: { type: 'string', description: 'Table name or ID' }, 182 | maxRecords: { type: 'number', description: 'Maximum number of records to return' }, 183 | view: { type: 'string', description: 'View name or ID' } 184 | }, 185 | required: ['table'] 186 | } 187 | }, 188 | { 189 | name: 'get_record', 190 | description: 'Get a single record by ID', 191 | inputSchema: { 192 | type: 'object', 193 | properties: { 194 | table: { type: 'string', description: 'Table name or ID' }, 195 | recordId: { type: 'string', description: 'Record ID' } 196 | }, 197 | required: ['table', 'recordId'] 198 | } 199 | }, 200 | { 201 | name: 'create_record', 202 | description: 'Create a new record in a table', 203 | inputSchema: { 204 | type: 'object', 205 | properties: { 206 | table: { type: 'string', description: 'Table name or ID' }, 207 | fields: { type: 'object', description: 'Field values for the new record' } 208 | }, 209 | required: ['table', 'fields'] 210 | } 211 | }, 212 | { 213 | name: 'update_record', 214 | description: 'Update an existing record', 215 | inputSchema: { 216 | type: 'object', 217 | properties: { 218 | table: { type: 'string', description: 'Table name or ID' }, 219 | recordId: { type: 'string', description: 'Record ID to update' }, 220 | fields: { type: 'object', description: 'Fields to update' } 221 | }, 222 | required: ['table', 'recordId', 'fields'] 223 | } 224 | }, 225 | { 226 | name: 'delete_record', 227 | description: 'Delete a record from a table', 228 | inputSchema: { 229 | type: 'object', 230 | properties: { 231 | table: { type: 'string', description: 'Table name or ID' }, 232 | recordId: { type: 'string', description: 'Record ID to delete' } 233 | }, 234 | required: ['table', 'recordId'] 235 | } 236 | }, 237 | { 238 | name: 'search_records', 239 | description: 'Search records with filtering and sorting', 240 | inputSchema: { 241 | type: 'object', 242 | properties: { 243 | table: { type: 'string', description: 'Table name or ID' }, 244 | filterByFormula: { type: 'string', description: 'Airtable formula to filter records' }, 245 | sort: { type: 'array', description: 'Sort configuration' }, 246 | maxRecords: { type: 'number', description: 'Maximum records to return' }, 247 | fields: { type: 'array', description: 'Fields to return' } 248 | }, 249 | required: ['table'] 250 | } 251 | }, 252 | { 253 | 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 | name: 'upload_attachment', 469 | description: 'Upload/attach a file from URL to a record', 470 | inputSchema: { 471 | type: 'object', 472 | properties: { 473 | table: { type: 'string', description: 'Table name or ID' }, 474 | recordId: { type: 'string', description: 'Record ID to attach file to' }, 475 | fieldName: { type: 'string', description: 'Name of the attachment field' }, 476 | url: { type: 'string', description: 'Public URL of the file to attach' }, 477 | filename: { type: 'string', description: 'Optional filename for the attachment' } 478 | }, 479 | required: ['table', 'recordId', 'fieldName', 'url'] 480 | } 481 | }, 482 | { 483 | name: 'batch_create_records', 484 | description: 'Create multiple records at once (up to 10)', 485 | inputSchema: { 486 | type: 'object', 487 | properties: { 488 | table: { type: 'string', description: 'Table name or ID' }, 489 | records: { 490 | type: 'array', 491 | description: 'Array of record objects to create (max 10)', 492 | items: { 493 | type: 'object', 494 | properties: { 495 | fields: { type: 'object', description: 'Record fields' } 496 | }, 497 | required: ['fields'] 498 | }, 499 | maxItems: 10 500 | } 501 | }, 502 | required: ['table', 'records'] 503 | } 504 | }, 505 | { 506 | name: 'batch_update_records', 507 | description: 'Update multiple records at once (up to 10)', 508 | inputSchema: { 509 | type: 'object', 510 | properties: { 511 | table: { type: 'string', description: 'Table name or ID' }, 512 | records: { 513 | type: 'array', 514 | description: 'Array of record objects to update (max 10)', 515 | items: { 516 | type: 'object', 517 | properties: { 518 | id: { type: 'string', description: 'Record ID' }, 519 | fields: { type: 'object', description: 'Fields to update' } 520 | }, 521 | required: ['id', 'fields'] 522 | }, 523 | maxItems: 10 524 | } 525 | }, 526 | required: ['table', 'records'] 527 | } 528 | }, 529 | { 530 | name: 'batch_delete_records', 531 | description: 'Delete multiple records at once (up to 10)', 532 | inputSchema: { 533 | type: 'object', 534 | properties: { 535 | table: { type: 'string', description: 'Table name or ID' }, 536 | recordIds: { 537 | type: 'array', 538 | description: 'Array of record IDs to delete (max 10)', 539 | items: { type: 'string' }, 540 | maxItems: 10 541 | } 542 | }, 543 | required: ['table', 'recordIds'] 544 | } 545 | }, 546 | { 547 | name: 'batch_upsert_records', 548 | description: 'Update existing records or create new ones based on key fields', 549 | inputSchema: { 550 | type: 'object', 551 | properties: { 552 | table: { type: 'string', description: 'Table name or ID' }, 553 | records: { 554 | type: 'array', 555 | description: 'Array of record objects (max 10)', 556 | items: { 557 | type: 'object', 558 | properties: { 559 | fields: { type: 'object', description: 'Record fields' } 560 | }, 561 | required: ['fields'] 562 | }, 563 | maxItems: 10 564 | }, 565 | keyFields: { 566 | type: 'array', 567 | description: 'Fields to use for matching existing records', 568 | items: { type: 'string' } 569 | } 570 | }, 571 | required: ['table', 'records', 'keyFields'] 572 | } 573 | }, 574 | { 575 | name: 'create_view', 576 | description: 'Create a new view for a table', 577 | inputSchema: { 578 | type: 'object', 579 | properties: { 580 | table: { type: 'string', description: 'Table name or ID' }, 581 | name: { type: 'string', description: 'Name for the new view' }, 582 | type: { type: 'string', description: 'View type (grid, form, calendar, etc.)', enum: ['grid', 'form', 'calendar', 'gallery', 'kanban', 'timeline', 'gantt'] }, 583 | visibleFieldIds: { type: 'array', description: 'Array of field IDs to show in view', items: { type: 'string' } }, 584 | fieldOrder: { type: 'array', description: 'Order of fields in view', items: { type: 'string' } } 585 | }, 586 | required: ['table', 'name', 'type'] 587 | } 588 | }, 589 | { 590 | name: 'get_view_metadata', 591 | description: 'Get detailed metadata for a specific view', 592 | inputSchema: { 593 | type: 'object', 594 | properties: { 595 | table: { type: 'string', description: 'Table name or ID' }, 596 | viewId: { type: 'string', description: 'View ID' } 597 | }, 598 | required: ['table', 'viewId'] 599 | } 600 | }, 601 | { 602 | name: 'create_base', 603 | description: 'Create a new Airtable base', 604 | inputSchema: { 605 | type: 'object', 606 | properties: { 607 | name: { type: 'string', description: 'Name for the new base' }, 608 | workspaceId: { type: 'string', description: 'Workspace ID to create the base in' }, 609 | tables: { 610 | type: 'array', 611 | description: 'Initial tables to create in the base', 612 | items: { 613 | type: 'object', 614 | properties: { 615 | name: { type: 'string', description: 'Table name' }, 616 | description: { type: 'string', description: 'Table description' }, 617 | fields: { 618 | type: 'array', 619 | description: 'Table fields', 620 | items: { 621 | type: 'object', 622 | properties: { 623 | name: { type: 'string', description: 'Field name' }, 624 | type: { type: 'string', description: 'Field type' } 625 | }, 626 | required: ['name', 'type'] 627 | } 628 | } 629 | }, 630 | required: ['name', 'fields'] 631 | } 632 | } 633 | }, 634 | required: ['name', 'tables'] 635 | } 636 | }, 637 | { 638 | name: 'list_collaborators', 639 | description: 'List collaborators and their permissions for the current base', 640 | inputSchema: { 641 | type: 'object', 642 | properties: { 643 | baseId: { type: 'string', description: 'Base ID (optional, defaults to current base)' } 644 | } 645 | } 646 | }, 647 | { 648 | name: 'list_shares', 649 | description: 'List shared views and their configurations', 650 | inputSchema: { 651 | type: 'object', 652 | properties: { 653 | baseId: { type: 'string', description: 'Base ID (optional, defaults to current base)' } 654 | } 655 | } 656 | } 657 | ] 658 | } 659 | }; 660 | res.writeHead(200, { 'Content-Type': 'application/json' }); 661 | res.end(JSON.stringify(response)); 662 | return; 663 | } 664 | 665 | if (request.method === 'resources/list') { 666 | const response = { 667 | jsonrpc: '2.0', 668 | id: request.id, 669 | result: { 670 | resources: [ 671 | { 672 | id: 'airtable_tables', 673 | name: 'Airtable Tables', 674 | description: 'Tables in your Airtable base' 675 | } 676 | ] 677 | } 678 | }; 679 | res.writeHead(200, { 'Content-Type': 'application/json' }); 680 | res.end(JSON.stringify(response)); 681 | return; 682 | } 683 | 684 | if (request.method === 'prompts/list') { 685 | const response = { 686 | jsonrpc: '2.0', 687 | id: request.id, 688 | result: { 689 | prompts: [ 690 | { 691 | id: 'tables_prompt', 692 | name: 'List Tables', 693 | description: 'List all tables' 694 | } 695 | ] 696 | } 697 | }; 698 | res.writeHead(200, { 'Content-Type': 'application/json' }); 699 | res.end(JSON.stringify(response)); 700 | return; 701 | } 702 | 703 | // Handle tool calls 704 | if (request.method === 'tools/call') { 705 | const toolName = request.params.name; 706 | const toolParams = request.params.arguments || {}; 707 | 708 | let result; 709 | let responseText; 710 | 711 | try { 712 | // LIST TABLES 713 | if (toolName === 'list_tables') { 714 | result = await callAirtableAPI(`meta/bases/${baseId}/tables`); 715 | const tables = result.tables || []; 716 | 717 | responseText = tables.length > 0 718 | ? `Found ${tables.length} table(s):\n` + tables.map((table, i) => 719 | `${i+1}. ${table.name} (ID: ${table.id}, Fields: ${table.fields?.length || 0})` 720 | ).join('\n') 721 | : 'No tables found in this base.'; 722 | } 723 | 724 | // LIST RECORDS 725 | else if (toolName === 'list_records') { 726 | const { table, maxRecords, view } = toolParams; 727 | 728 | const queryParams = {}; 729 | if (maxRecords) queryParams.maxRecords = maxRecords; 730 | if (view) queryParams.view = view; 731 | 732 | result = await callAirtableAPI(`${table}`, 'GET', null, queryParams); 733 | const records = result.records || []; 734 | 735 | responseText = records.length > 0 736 | ? `Found ${records.length} record(s) in table "${table}":\n` + 737 | records.map((record, i) => 738 | `${i+1}. ID: ${record.id}\n Fields: ${JSON.stringify(record.fields, null, 2)}` 739 | ).join('\n\n') 740 | : `No records found in table "${table}".`; 741 | } 742 | 743 | // GET SINGLE RECORD 744 | else if (toolName === 'get_record') { 745 | const { table, recordId } = toolParams; 746 | 747 | result = await callAirtableAPI(`${table}/${recordId}`); 748 | 749 | responseText = `Record ${recordId} from table "${table}":\n` + 750 | JSON.stringify(result.fields, null, 2) + 751 | `\n\nCreated: ${result.createdTime}`; 752 | } 753 | 754 | // CREATE RECORD 755 | else if (toolName === 'create_record') { 756 | const { table, fields } = toolParams; 757 | 758 | const body = { 759 | fields: fields 760 | }; 761 | 762 | result = await callAirtableAPI(table, 'POST', body); 763 | 764 | responseText = `Successfully created record in table "${table}":\n` + 765 | `Record ID: ${result.id}\n` + 766 | `Fields: ${JSON.stringify(result.fields, null, 2)}\n` + 767 | `Created at: ${result.createdTime}`; 768 | } 769 | 770 | // UPDATE RECORD 771 | else if (toolName === 'update_record') { 772 | const { table, recordId, fields } = toolParams; 773 | 774 | const body = { 775 | fields: fields 776 | }; 777 | 778 | result = await callAirtableAPI(`${table}/${recordId}`, 'PATCH', body); 779 | 780 | responseText = `Successfully updated record ${recordId} in table "${table}":\n` + 781 | `Updated fields: ${JSON.stringify(result.fields, null, 2)}`; 782 | } 783 | 784 | // DELETE RECORD 785 | else if (toolName === 'delete_record') { 786 | const { table, recordId } = toolParams; 787 | 788 | result = await callAirtableAPI(`${table}/${recordId}`, 'DELETE'); 789 | 790 | responseText = `Successfully deleted record ${recordId} from table "${table}".\n` + 791 | `Deleted record ID: ${result.id}\n` + 792 | `Deleted: ${result.deleted}`; 793 | } 794 | 795 | // SEARCH RECORDS 796 | else if (toolName === 'search_records') { 797 | const { table, filterByFormula, sort, maxRecords, fields } = toolParams; 798 | 799 | const queryParams = {}; 800 | if (filterByFormula) queryParams.filterByFormula = filterByFormula; 801 | if (maxRecords) queryParams.maxRecords = maxRecords; 802 | if (fields && fields.length > 0) queryParams.fields = fields; 803 | if (sort && sort.length > 0) { 804 | sort.forEach((s, i) => { 805 | queryParams[`sort[${i}][field]`] = s.field; 806 | queryParams[`sort[${i}][direction]`] = s.direction || 'asc'; 807 | }); 808 | } 809 | 810 | result = await callAirtableAPI(table, 'GET', null, queryParams); 811 | const records = result.records || []; 812 | 813 | responseText = records.length > 0 814 | ? `Found ${records.length} matching record(s) in table "${table}":\n` + 815 | records.map((record, i) => 816 | `${i+1}. ID: ${record.id}\n Fields: ${JSON.stringify(record.fields, null, 2)}` 817 | ).join('\n\n') 818 | : `No records found matching the search criteria in table "${table}".`; 819 | } 820 | 821 | // LIST WEBHOOKS 822 | else if (toolName === 'list_webhooks') { 823 | result = await callAirtableAPI(`bases/${baseId}/webhooks`, 'GET'); 824 | const webhooks = result.webhooks || []; 825 | 826 | responseText = webhooks.length > 0 827 | ? `Found ${webhooks.length} webhook(s):\n` + 828 | webhooks.map((webhook, i) => 829 | `${i+1}. ID: ${webhook.id}\n` + 830 | ` URL: ${webhook.notificationUrl}\n` + 831 | ` Active: ${webhook.isHookEnabled}\n` + 832 | ` Created: ${webhook.createdTime}\n` + 833 | ` Expires: ${webhook.expirationTime}` 834 | ).join('\n\n') 835 | : 'No webhooks configured for this base.'; 836 | } 837 | 838 | // CREATE WEBHOOK 839 | else if (toolName === 'create_webhook') { 840 | const { notificationUrl, specification } = toolParams; 841 | 842 | const body = { 843 | notificationUrl: notificationUrl, 844 | specification: specification || { 845 | options: { 846 | filters: { 847 | dataTypes: ['tableData'] 848 | } 849 | } 850 | } 851 | }; 852 | 853 | result = await callAirtableAPI(`bases/${baseId}/webhooks`, 'POST', body); 854 | 855 | responseText = `Successfully created webhook:\n` + 856 | `Webhook ID: ${result.id}\n` + 857 | `URL: ${result.notificationUrl}\n` + 858 | `MAC Secret: ${result.macSecretBase64}\n` + 859 | `Expiration: ${result.expirationTime}\n` + 860 | `Cursor: ${result.cursorForNextPayload}\n\n` + 861 | `⚠️ IMPORTANT: Save the MAC secret - it won't be shown again!`; 862 | } 863 | 864 | // DELETE WEBHOOK 865 | else if (toolName === 'delete_webhook') { 866 | const { webhookId } = toolParams; 867 | 868 | await callAirtableAPI(`bases/${baseId}/webhooks/${webhookId}`, 'DELETE'); 869 | 870 | responseText = `Successfully deleted webhook ${webhookId}`; 871 | } 872 | 873 | // GET WEBHOOK PAYLOADS 874 | else if (toolName === 'get_webhook_payloads') { 875 | const { webhookId, cursor } = toolParams; 876 | 877 | const queryParams = {}; 878 | if (cursor) queryParams.cursor = cursor; 879 | 880 | result = await callAirtableAPI(`bases/${baseId}/webhooks/${webhookId}/payloads`, 'GET', null, queryParams); 881 | 882 | const payloads = result.payloads || []; 883 | responseText = payloads.length > 0 884 | ? `Found ${payloads.length} webhook payload(s):\n` + 885 | payloads.map((payload, i) => 886 | `${i+1}. Timestamp: ${payload.timestamp}\n` + 887 | ` Base/Table: ${payload.baseTransactionNumber}\n` + 888 | ` Change Types: ${JSON.stringify(payload.changePayload?.changedTablesById || {})}` 889 | ).join('\n\n') + 890 | (result.cursor ? `\n\nNext cursor: ${result.cursor}` : '') 891 | : 'No payloads found for this webhook.'; 892 | } 893 | 894 | // REFRESH WEBHOOK 895 | else if (toolName === 'refresh_webhook') { 896 | const { webhookId } = toolParams; 897 | 898 | result = await callAirtableAPI(`bases/${baseId}/webhooks/${webhookId}/refresh`, 'POST'); 899 | 900 | responseText = `Successfully refreshed webhook ${webhookId}:\n` + 901 | `New expiration: ${result.expirationTime}`; 902 | } 903 | 904 | // Schema Management Tools 905 | else if (toolName === 'list_bases') { 906 | const { offset } = toolParams; 907 | const queryParams = offset ? { offset } : {}; 908 | 909 | result = await callAirtableAPI('meta/bases', 'GET', null, queryParams); 910 | 911 | if (result.bases && result.bases.length > 0) { 912 | responseText = `Found ${result.bases.length} accessible base(s):\n`; 913 | result.bases.forEach((base, index) => { 914 | responseText += `${index + 1}. ${base.name} (ID: ${base.id})\n`; 915 | if (base.permissionLevel) { 916 | responseText += ` Permission: ${base.permissionLevel}\n`; 917 | } 918 | }); 919 | if (result.offset) { 920 | responseText += `\nNext page offset: ${result.offset}`; 921 | } 922 | } else { 923 | responseText = 'No accessible bases found.'; 924 | } 925 | } 926 | 927 | else if (toolName === 'get_base_schema') { 928 | const { baseId: targetBaseId } = toolParams; 929 | const targetId = targetBaseId || baseId; 930 | 931 | result = await callAirtableAPI(`meta/bases/${targetId}/tables`, 'GET'); 932 | 933 | if (result.tables && result.tables.length > 0) { 934 | responseText = `Base schema for ${targetId}:\n\n`; 935 | result.tables.forEach((table, index) => { 936 | responseText += `${index + 1}. Table: ${table.name} (ID: ${table.id})\n`; 937 | if (table.description) { 938 | responseText += ` Description: ${table.description}\n`; 939 | } 940 | responseText += ` Fields (${table.fields.length}):\n`; 941 | table.fields.forEach((field, fieldIndex) => { 942 | responseText += ` ${fieldIndex + 1}. ${field.name} (${field.type})\n`; 943 | if (field.description) { 944 | responseText += ` Description: ${field.description}\n`; 945 | } 946 | }); 947 | if (table.views && table.views.length > 0) { 948 | responseText += ` Views (${table.views.length}): ${table.views.map(v => v.name).join(', ')}\n`; 949 | } 950 | responseText += '\n'; 951 | }); 952 | } else { 953 | responseText = 'No tables found in this base.'; 954 | } 955 | } 956 | 957 | else if (toolName === 'describe_table') { 958 | const { table } = toolParams; 959 | 960 | // Get table schema first 961 | const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET'); 962 | const tableInfo = schemaResult.tables.find(t => 963 | t.name.toLowerCase() === table.toLowerCase() || t.id === table 964 | ); 965 | 966 | if (!tableInfo) { 967 | responseText = `Table "${table}" not found.`; 968 | } else { 969 | responseText = `Table Details: ${tableInfo.name}\n`; 970 | responseText += `ID: ${tableInfo.id}\n`; 971 | if (tableInfo.description) { 972 | responseText += `Description: ${tableInfo.description}\n`; 973 | } 974 | responseText += `\nFields (${tableInfo.fields.length}):\n`; 975 | 976 | tableInfo.fields.forEach((field, index) => { 977 | responseText += `${index + 1}. ${field.name}\n`; 978 | responseText += ` Type: ${field.type}\n`; 979 | responseText += ` ID: ${field.id}\n`; 980 | if (field.description) { 981 | responseText += ` Description: ${field.description}\n`; 982 | } 983 | if (field.options) { 984 | responseText += ` Options: ${JSON.stringify(field.options, null, 2)}\n`; 985 | } 986 | responseText += '\n'; 987 | }); 988 | 989 | if (tableInfo.views && tableInfo.views.length > 0) { 990 | responseText += `Views (${tableInfo.views.length}):\n`; 991 | tableInfo.views.forEach((view, index) => { 992 | responseText += `${index + 1}. ${view.name} (${view.type})\n`; 993 | }); 994 | } 995 | } 996 | } 997 | 998 | else if (toolName === 'create_table') { 999 | const { name, description, fields } = toolParams; 1000 | 1001 | const body = { 1002 | name, 1003 | fields: fields.map(field => ({ 1004 | name: field.name, 1005 | type: field.type, 1006 | description: field.description, 1007 | options: field.options 1008 | })) 1009 | }; 1010 | 1011 | if (description) { 1012 | body.description = description; 1013 | } 1014 | 1015 | result = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'POST', body); 1016 | 1017 | responseText = `Successfully created table "${name}" (ID: ${result.id})\n`; 1018 | responseText += `Fields created: ${result.fields.length}\n`; 1019 | result.fields.forEach((field, index) => { 1020 | responseText += `${index + 1}. ${field.name} (${field.type})\n`; 1021 | }); 1022 | } 1023 | 1024 | else if (toolName === 'update_table') { 1025 | const { table, name, description } = toolParams; 1026 | 1027 | // Get table ID first 1028 | const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET'); 1029 | const tableInfo = schemaResult.tables.find(t => 1030 | t.name.toLowerCase() === table.toLowerCase() || t.id === table 1031 | ); 1032 | 1033 | if (!tableInfo) { 1034 | responseText = `Table "${table}" not found.`; 1035 | } else { 1036 | const body = {}; 1037 | if (name) body.name = name; 1038 | if (description !== undefined) body.description = description; 1039 | 1040 | if (Object.keys(body).length === 0) { 1041 | responseText = 'No updates specified. Provide name or description to update.'; 1042 | } else { 1043 | result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}`, 'PATCH', body); 1044 | responseText = `Successfully updated table "${tableInfo.name}":\n`; 1045 | if (name) responseText += `New name: ${result.name}\n`; 1046 | if (description !== undefined) responseText += `New description: ${result.description || '(none)'}\n`; 1047 | } 1048 | } 1049 | } 1050 | 1051 | else if (toolName === 'delete_table') { 1052 | const { table, confirm } = toolParams; 1053 | 1054 | if (!confirm) { 1055 | responseText = 'Table deletion requires confirm=true to proceed. This action cannot be undone!'; 1056 | } else { 1057 | // Get table ID first 1058 | const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET'); 1059 | const tableInfo = schemaResult.tables.find(t => 1060 | t.name.toLowerCase() === table.toLowerCase() || t.id === table 1061 | ); 1062 | 1063 | if (!tableInfo) { 1064 | responseText = `Table "${table}" not found.`; 1065 | } else { 1066 | result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}`, 'DELETE'); 1067 | responseText = `Successfully deleted table "${tableInfo.name}" (ID: ${tableInfo.id})\n`; 1068 | responseText += 'All data in this table has been permanently removed.'; 1069 | } 1070 | } 1071 | } 1072 | 1073 | // Field Management Tools 1074 | else if (toolName === 'create_field') { 1075 | const { table, name, type, description, options } = toolParams; 1076 | 1077 | // Get table ID first 1078 | const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET'); 1079 | const tableInfo = schemaResult.tables.find(t => 1080 | t.name.toLowerCase() === table.toLowerCase() || t.id === table 1081 | ); 1082 | 1083 | if (!tableInfo) { 1084 | responseText = `Table "${table}" not found.`; 1085 | } else { 1086 | const body = { 1087 | name, 1088 | type 1089 | }; 1090 | 1091 | if (description) body.description = description; 1092 | if (options) body.options = options; 1093 | 1094 | result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/fields`, 'POST', body); 1095 | 1096 | responseText = `Successfully created field "${name}" in table "${tableInfo.name}"\n`; 1097 | responseText += `Field ID: ${result.id}\n`; 1098 | responseText += `Type: ${result.type}\n`; 1099 | if (result.description) { 1100 | responseText += `Description: ${result.description}\n`; 1101 | } 1102 | } 1103 | } 1104 | 1105 | else if (toolName === 'update_field') { 1106 | const { table, fieldId, name, description, options } = toolParams; 1107 | 1108 | // Get table ID first 1109 | const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET'); 1110 | const tableInfo = schemaResult.tables.find(t => 1111 | t.name.toLowerCase() === table.toLowerCase() || t.id === table 1112 | ); 1113 | 1114 | if (!tableInfo) { 1115 | responseText = `Table "${table}" not found.`; 1116 | } else { 1117 | const body = {}; 1118 | if (name) body.name = name; 1119 | if (description !== undefined) body.description = description; 1120 | if (options) body.options = options; 1121 | 1122 | if (Object.keys(body).length === 0) { 1123 | responseText = 'No updates specified. Provide name, description, or options to update.'; 1124 | } else { 1125 | result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/fields/${fieldId}`, 'PATCH', body); 1126 | responseText = `Successfully updated field in table "${tableInfo.name}":\n`; 1127 | responseText += `Field: ${result.name} (${result.type})\n`; 1128 | responseText += `ID: ${result.id}\n`; 1129 | if (result.description) { 1130 | responseText += `Description: ${result.description}\n`; 1131 | } 1132 | } 1133 | } 1134 | } 1135 | 1136 | else if (toolName === 'delete_field') { 1137 | const { table, fieldId, confirm } = toolParams; 1138 | 1139 | if (!confirm) { 1140 | responseText = 'Field deletion requires confirm=true to proceed. This action cannot be undone!'; 1141 | } else { 1142 | // Get table ID first 1143 | const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET'); 1144 | const tableInfo = schemaResult.tables.find(t => 1145 | t.name.toLowerCase() === table.toLowerCase() || t.id === table 1146 | ); 1147 | 1148 | if (!tableInfo) { 1149 | responseText = `Table "${table}" not found.`; 1150 | } else { 1151 | result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/fields/${fieldId}`, 'DELETE'); 1152 | responseText = `Successfully deleted field from table "${tableInfo.name}"\n`; 1153 | responseText += 'All data in this field has been permanently removed.'; 1154 | } 1155 | } 1156 | } 1157 | 1158 | else if (toolName === 'list_field_types') { 1159 | responseText = `Available Airtable Field Types:\n\n`; 1160 | responseText += `Basic Fields:\n`; 1161 | responseText += `• singleLineText - Single line text input\n`; 1162 | responseText += `• multilineText - Multi-line text input\n`; 1163 | responseText += `• richText - Rich text with formatting\n`; 1164 | responseText += `• number - Number field with optional formatting\n`; 1165 | responseText += `• percent - Percentage field\n`; 1166 | responseText += `• currency - Currency field\n`; 1167 | responseText += `• singleSelect - Single choice from predefined options\n`; 1168 | responseText += `• multipleSelectionList - Multiple choices from predefined options\n`; 1169 | responseText += `• date - Date field\n`; 1170 | responseText += `• dateTime - Date and time field\n`; 1171 | responseText += `• phoneNumber - Phone number field\n`; 1172 | responseText += `• email - Email address field\n`; 1173 | responseText += `• url - URL field\n`; 1174 | responseText += `• checkbox - Checkbox (true/false)\n`; 1175 | responseText += `• rating - Star rating field\n`; 1176 | responseText += `• duration - Duration/time field\n\n`; 1177 | responseText += `Advanced Fields:\n`; 1178 | responseText += `• multipleAttachment - File attachments\n`; 1179 | responseText += `• linkedRecord - Link to records in another table\n`; 1180 | responseText += `• lookup - Lookup values from linked records\n`; 1181 | responseText += `• rollup - Calculate values from linked records\n`; 1182 | responseText += `• count - Count of linked records\n`; 1183 | responseText += `• formula - Calculated field with formulas\n`; 1184 | responseText += `• createdTime - Auto-timestamp when record created\n`; 1185 | responseText += `• createdBy - Auto-user who created record\n`; 1186 | responseText += `• lastModifiedTime - Auto-timestamp when record modified\n`; 1187 | responseText += `• lastModifiedBy - Auto-user who last modified record\n`; 1188 | responseText += `• autoNumber - Auto-incrementing number\n`; 1189 | responseText += `• barcode - Barcode/QR code field\n`; 1190 | responseText += `• button - Action button field\n`; 1191 | } 1192 | 1193 | else if (toolName === 'get_table_views') { 1194 | const { table } = toolParams; 1195 | 1196 | // Get table schema 1197 | const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET'); 1198 | const tableInfo = schemaResult.tables.find(t => 1199 | t.name.toLowerCase() === table.toLowerCase() || t.id === table 1200 | ); 1201 | 1202 | if (!tableInfo) { 1203 | responseText = `Table "${table}" not found.`; 1204 | } else { 1205 | if (tableInfo.views && tableInfo.views.length > 0) { 1206 | responseText = `Views for table "${tableInfo.name}" (${tableInfo.views.length}):\n\n`; 1207 | tableInfo.views.forEach((view, index) => { 1208 | responseText += `${index + 1}. ${view.name}\n`; 1209 | responseText += ` Type: ${view.type}\n`; 1210 | responseText += ` ID: ${view.id}\n`; 1211 | if (view.visibleFieldIds && view.visibleFieldIds.length > 0) { 1212 | responseText += ` Visible fields: ${view.visibleFieldIds.length}\n`; 1213 | } 1214 | responseText += '\n'; 1215 | }); 1216 | } else { 1217 | responseText = `No views found for table "${tableInfo.name}".`; 1218 | } 1219 | } 1220 | } 1221 | 1222 | // NEW v1.6.0 TOOLS - Attachment and Batch Operations 1223 | else if (toolName === 'upload_attachment') { 1224 | const { table, recordId, fieldName, url, filename } = toolParams; 1225 | 1226 | const attachment = { url }; 1227 | if (filename) attachment.filename = filename; 1228 | 1229 | const updateBody = { 1230 | fields: { 1231 | [fieldName]: [attachment] 1232 | } 1233 | }; 1234 | 1235 | result = await callAirtableAPI(`${table}/${recordId}`, 'PATCH', updateBody); 1236 | 1237 | responseText = `Successfully attached file to record ${recordId}:\n`; 1238 | responseText += `Field: ${fieldName}\n`; 1239 | responseText += `URL: ${url}\n`; 1240 | if (filename) responseText += `Filename: ${filename}\n`; 1241 | } 1242 | 1243 | else if (toolName === 'batch_create_records') { 1244 | const { table, records } = toolParams; 1245 | 1246 | if (records.length > 10) { 1247 | responseText = 'Error: Cannot create more than 10 records at once. Please split into smaller batches.'; 1248 | } else { 1249 | const body = { records }; 1250 | result = await callAirtableAPI(table, 'POST', body); 1251 | 1252 | responseText = `Successfully created ${result.records.length} records:\n`; 1253 | result.records.forEach((record, index) => { 1254 | responseText += `${index + 1}. ID: ${record.id}\n`; 1255 | const fields = Object.keys(record.fields); 1256 | if (fields.length > 0) { 1257 | responseText += ` Fields: ${fields.join(', ')}\n`; 1258 | } 1259 | }); 1260 | } 1261 | } 1262 | 1263 | else if (toolName === 'batch_update_records') { 1264 | const { table, records } = toolParams; 1265 | 1266 | if (records.length > 10) { 1267 | responseText = 'Error: Cannot update more than 10 records at once. Please split into smaller batches.'; 1268 | } else { 1269 | const body = { records }; 1270 | result = await callAirtableAPI(table, 'PATCH', body); 1271 | 1272 | responseText = `Successfully updated ${result.records.length} records:\n`; 1273 | result.records.forEach((record, index) => { 1274 | responseText += `${index + 1}. ID: ${record.id}\n`; 1275 | const fields = Object.keys(record.fields); 1276 | if (fields.length > 0) { 1277 | responseText += ` Updated fields: ${fields.join(', ')}\n`; 1278 | } 1279 | }); 1280 | } 1281 | } 1282 | 1283 | else if (toolName === 'batch_delete_records') { 1284 | const { table, recordIds } = toolParams; 1285 | 1286 | if (recordIds.length > 10) { 1287 | responseText = 'Error: Cannot delete more than 10 records at once. Please split into smaller batches.'; 1288 | } else { 1289 | const queryParams = { records: recordIds }; 1290 | result = await callAirtableAPI(table, 'DELETE', null, queryParams); 1291 | 1292 | responseText = `Successfully deleted ${result.records.length} records:\n`; 1293 | result.records.forEach((record, index) => { 1294 | responseText += `${index + 1}. Deleted ID: ${record.id}\n`; 1295 | }); 1296 | } 1297 | } 1298 | 1299 | else if (toolName === 'batch_upsert_records') { 1300 | const { table, records, keyFields } = toolParams; 1301 | 1302 | if (records.length > 10) { 1303 | responseText = 'Error: Cannot upsert more than 10 records at once. Please split into smaller batches.'; 1304 | } else { 1305 | // For simplicity, we'll implement this as a batch create with merge fields 1306 | // Note: Real upsert requires checking existing records first 1307 | const body = { 1308 | records, 1309 | performUpsert: { 1310 | fieldsToMergeOn: keyFields 1311 | } 1312 | }; 1313 | 1314 | result = await callAirtableAPI(table, 'PATCH', body); 1315 | 1316 | responseText = `Successfully upserted ${result.records.length} records:\n`; 1317 | result.records.forEach((record, index) => { 1318 | responseText += `${index + 1}. ID: ${record.id}\n`; 1319 | const fields = Object.keys(record.fields); 1320 | if (fields.length > 0) { 1321 | responseText += ` Fields: ${fields.join(', ')}\n`; 1322 | } 1323 | }); 1324 | } 1325 | } 1326 | 1327 | // NEW v1.6.0 TOOLS - Advanced View Management 1328 | else if (toolName === 'create_view') { 1329 | const { table, name, type, visibleFieldIds, fieldOrder } = toolParams; 1330 | 1331 | // Get table ID first 1332 | const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET'); 1333 | const tableInfo = schemaResult.tables.find(t => 1334 | t.name.toLowerCase() === table.toLowerCase() || t.id === table 1335 | ); 1336 | 1337 | if (!tableInfo) { 1338 | responseText = `Table "${table}" not found.`; 1339 | } else { 1340 | const body = { 1341 | name, 1342 | type 1343 | }; 1344 | 1345 | if (visibleFieldIds) body.visibleFieldIds = visibleFieldIds; 1346 | if (fieldOrder) body.fieldOrder = fieldOrder; 1347 | 1348 | result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/views`, 'POST', body); 1349 | 1350 | responseText = `Successfully created view "${name}" in table "${tableInfo.name}":\n`; 1351 | responseText += `View ID: ${result.id}\n`; 1352 | responseText += `Type: ${result.type}\n`; 1353 | if (result.visibleFieldIds && result.visibleFieldIds.length > 0) { 1354 | responseText += `Visible fields: ${result.visibleFieldIds.length}\n`; 1355 | } 1356 | } 1357 | } 1358 | 1359 | else if (toolName === 'get_view_metadata') { 1360 | const { table, viewId } = toolParams; 1361 | 1362 | // Get table ID first 1363 | const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET'); 1364 | const tableInfo = schemaResult.tables.find(t => 1365 | t.name.toLowerCase() === table.toLowerCase() || t.id === table 1366 | ); 1367 | 1368 | if (!tableInfo) { 1369 | responseText = `Table "${table}" not found.`; 1370 | } else { 1371 | result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/views/${viewId}`, 'GET'); 1372 | 1373 | responseText = `View Metadata: ${result.name}\n`; 1374 | responseText += `ID: ${result.id}\n`; 1375 | responseText += `Type: ${result.type}\n`; 1376 | 1377 | if (result.visibleFieldIds && result.visibleFieldIds.length > 0) { 1378 | responseText += `\nVisible Fields (${result.visibleFieldIds.length}):\n`; 1379 | result.visibleFieldIds.forEach((fieldId, index) => { 1380 | responseText += `${index + 1}. ${fieldId}\n`; 1381 | }); 1382 | } 1383 | 1384 | if (result.filterByFormula) { 1385 | responseText += `\nFilter Formula: ${result.filterByFormula}\n`; 1386 | } 1387 | 1388 | if (result.sorts && result.sorts.length > 0) { 1389 | responseText += `\nSort Configuration:\n`; 1390 | result.sorts.forEach((sort, index) => { 1391 | responseText += `${index + 1}. Field: ${sort.field}, Direction: ${sort.direction}\n`; 1392 | }); 1393 | } 1394 | } 1395 | } 1396 | 1397 | // NEW v1.6.0 TOOLS - Base Management 1398 | else if (toolName === 'create_base') { 1399 | const { name, workspaceId, tables } = toolParams; 1400 | 1401 | const body = { 1402 | name, 1403 | tables: tables.map(table => ({ 1404 | name: table.name, 1405 | description: table.description, 1406 | fields: table.fields 1407 | })) 1408 | }; 1409 | 1410 | if (workspaceId) { 1411 | body.workspaceId = workspaceId; 1412 | } 1413 | 1414 | result = await callAirtableAPI('meta/bases', 'POST', body); 1415 | 1416 | responseText = `Successfully created base "${name}":\n`; 1417 | responseText += `Base ID: ${result.id}\n`; 1418 | if (result.tables && result.tables.length > 0) { 1419 | responseText += `\nTables created (${result.tables.length}):\n`; 1420 | result.tables.forEach((table, index) => { 1421 | responseText += `${index + 1}. ${table.name} (ID: ${table.id})\n`; 1422 | if (table.fields && table.fields.length > 0) { 1423 | responseText += ` Fields: ${table.fields.length}\n`; 1424 | } 1425 | }); 1426 | } 1427 | } 1428 | 1429 | else if (toolName === 'list_collaborators') { 1430 | const { baseId: targetBaseId } = toolParams; 1431 | const targetId = targetBaseId || baseId; 1432 | 1433 | result = await callAirtableAPI(`meta/bases/${targetId}/collaborators`, 'GET'); 1434 | 1435 | if (result.collaborators && result.collaborators.length > 0) { 1436 | responseText = `Base collaborators (${result.collaborators.length}):\n\n`; 1437 | result.collaborators.forEach((collaborator, index) => { 1438 | responseText += `${index + 1}. ${collaborator.email || collaborator.name || 'Unknown'}\n`; 1439 | responseText += ` Permission: ${collaborator.permissionLevel || 'Unknown'}\n`; 1440 | responseText += ` Type: ${collaborator.type || 'User'}\n`; 1441 | if (collaborator.userId) { 1442 | responseText += ` User ID: ${collaborator.userId}\n`; 1443 | } 1444 | responseText += '\n'; 1445 | }); 1446 | } else { 1447 | responseText = 'No collaborators found for this base.'; 1448 | } 1449 | } 1450 | 1451 | else if (toolName === 'list_shares') { 1452 | const { baseId: targetBaseId } = toolParams; 1453 | const targetId = targetBaseId || baseId; 1454 | 1455 | result = await callAirtableAPI(`meta/bases/${targetId}/shares`, 'GET'); 1456 | 1457 | if (result.shares && result.shares.length > 0) { 1458 | responseText = `Shared views (${result.shares.length}):\n\n`; 1459 | result.shares.forEach((share, index) => { 1460 | responseText += `${index + 1}. ${share.name || 'Unnamed Share'}\n`; 1461 | responseText += ` Share ID: ${share.id}\n`; 1462 | responseText += ` URL: ${share.url}\n`; 1463 | responseText += ` Type: ${share.type || 'View'}\n`; 1464 | if (share.viewId) { 1465 | responseText += ` View ID: ${share.viewId}\n`; 1466 | } 1467 | if (share.tableId) { 1468 | responseText += ` Table ID: ${share.tableId}\n`; 1469 | } 1470 | responseText += ` Effective: ${share.effective ? 'Yes' : 'No'}\n`; 1471 | responseText += '\n'; 1472 | }); 1473 | } else { 1474 | responseText = 'No shared views found for this base.'; 1475 | } 1476 | } 1477 | 1478 | else { 1479 | throw new Error(`Unknown tool: ${toolName}`); 1480 | } 1481 | 1482 | const response = { 1483 | jsonrpc: '2.0', 1484 | id: request.id, 1485 | result: { 1486 | content: [ 1487 | { 1488 | type: 'text', 1489 | text: responseText 1490 | } 1491 | ] 1492 | } 1493 | }; 1494 | res.writeHead(200, { 'Content-Type': 'application/json' }); 1495 | res.end(JSON.stringify(response)); 1496 | 1497 | } catch (error) { 1498 | log(LOG_LEVELS.ERROR, `Tool ${toolName} error:`, error.message); 1499 | 1500 | const response = { 1501 | jsonrpc: '2.0', 1502 | id: request.id, 1503 | result: { 1504 | content: [ 1505 | { 1506 | type: 'text', 1507 | text: `Error executing ${toolName}: ${error.message}` 1508 | } 1509 | ] 1510 | } 1511 | }; 1512 | res.writeHead(200, { 'Content-Type': 'application/json' }); 1513 | res.end(JSON.stringify(response)); 1514 | } 1515 | 1516 | return; 1517 | } 1518 | 1519 | // Method not found 1520 | const response = { 1521 | jsonrpc: '2.0', 1522 | id: request.id, 1523 | error: { 1524 | code: -32601, 1525 | message: `Method ${request.method} not found` 1526 | } 1527 | }; 1528 | res.writeHead(200, { 'Content-Type': 'application/json' }); 1529 | res.end(JSON.stringify(response)); 1530 | 1531 | } catch (error) { 1532 | log(LOG_LEVELS.ERROR, 'Error processing request:', error); 1533 | const response = { 1534 | jsonrpc: '2.0', 1535 | id: request.id || null, 1536 | error: { 1537 | code: -32000, 1538 | message: error.message || 'Unknown error' 1539 | } 1540 | }; 1541 | res.writeHead(200, { 'Content-Type': 'application/json' }); 1542 | res.end(JSON.stringify(response)); 1543 | } 1544 | }); 1545 | }); 1546 | 1547 | // Start server 1548 | const PORT = process.env.PORT || 8010; 1549 | server.listen(PORT, () => { 1550 | log(LOG_LEVELS.INFO, `Enhanced Airtable MCP server v1.4.0 running at http://localhost:${PORT}/mcp`); 1551 | console.log(`For Claude, use this URL: http://localhost:${PORT}/mcp`); 1552 | }); 1553 | 1554 | // Graceful shutdown 1555 | process.on('SIGINT', () => { 1556 | log(LOG_LEVELS.INFO, 'Shutting down server...'); 1557 | server.close(() => { 1558 | log(LOG_LEVELS.INFO, 'Server stopped'); 1559 | process.exit(0); 1560 | }); 1561 | }); ```