#
tokens: 18621/50000 1/81 files (page 4/5)
lines: on (toggle) GitHub
raw markdown copy reset
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 | });
```
Page 4/5FirstPrevNextLast