@rashidazarang/airtable-mcp 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ # Release Notes - v1.2.4
2
+
3
+ ## 🔒 Security Fix Release
4
+
5
+ ### Critical Security Fix
6
+ - **REMOVED hardcoded API tokens from test files** (Addresses Issue #7)
7
+ - `test_client.py` and `test_mcp_comprehensive.js` now require environment variables
8
+ - Added security notice documentation
9
+ - No exposed credentials in the codebase
10
+
11
+ ### 🐛 Bug Fixes
12
+
13
+ #### Smithery Cloud Deployment Issues (Issues #5 and #6)
14
+ - **Fixed HTTP 400 errors** when using Smithery
15
+ - **Switched to JavaScript implementation** for Smithery deployment
16
+ - Updated `smithery.yaml` to use `airtable_simple.js` instead of problematic Python server
17
+ - Created dedicated `Dockerfile.node` for Node.js deployment
18
+ - Fixed authentication flow for Smithery connections
19
+
20
+ ### 📚 Documentation Updates
21
+ - Added `SECURITY_NOTICE.md` with token rotation instructions
22
+ - Created `.env.example` file for secure configuration
23
+ - Updated Dockerfile references for Glama listing (Issue #4)
24
+
25
+ ### 🔧 Improvements
26
+ - Added environment variable support with dotenv
27
+ - Improved logging system with configurable levels (ERROR, WARN, INFO, DEBUG)
28
+ - Better error messages for missing credentials
29
+
30
+ ### ⚠️ Breaking Changes
31
+ - Test files now require environment variables:
32
+ ```bash
33
+ export AIRTABLE_TOKEN="your_token"
34
+ export AIRTABLE_BASE_ID="your_base_id"
35
+ ```
36
+
37
+ ### 🚀 Migration Guide
38
+
39
+ 1. **Update your environment variables:**
40
+ ```bash
41
+ cp .env.example .env
42
+ # Edit .env with your credentials
43
+ ```
44
+
45
+ 2. **For Smithery users:**
46
+ - Reinstall the MCP to get the latest configuration
47
+ - The server now properly accepts credentials through Smithery's config
48
+
49
+ 3. **For direct users:**
50
+ - Continue using command line arguments or switch to environment variables
51
+ - Both methods are supported
52
+
53
+ ### 📝 Notes
54
+ - All previously exposed tokens have been revoked
55
+ - Please use your own Airtable credentials
56
+ - Never commit API tokens to version control
57
+
58
+ ---
59
+
60
+ **Full Changelog**: [v1.2.3...v1.2.4](https://github.com/rashidazarang/airtable-mcp/compare/v1.2.3...v1.2.4)
@@ -0,0 +1,104 @@
1
+ # Release Notes - v1.4.0
2
+
3
+ ## 🚀 Major Feature Release
4
+
5
+ ### ✨ New Features
6
+
7
+ #### 🪝 **Webhook Management** (5 new tools)
8
+ - `list_webhooks` - List all webhooks in your base
9
+ - `create_webhook` - Create webhooks for real-time notifications
10
+ - `delete_webhook` - Remove webhooks
11
+ - `get_webhook_payloads` - Retrieve webhook payload history
12
+ - `refresh_webhook` - Extend webhook expiration time
13
+
14
+ #### 🔧 **Enhanced CRUD Operations** (5 tools added since v1.2.4)
15
+ - `create_record` - Create new records in any table
16
+ - `update_record` - Update existing records
17
+ - `delete_record` - Remove records from tables
18
+ - `get_record` - Retrieve single record by ID
19
+ - `search_records` - Advanced filtering with Airtable formulas
20
+
21
+ ### 📊 **Complete Tool Set (12 tools total)**
22
+ 1. **list_tables** - List all tables in base
23
+ 2. **list_records** - List records from table
24
+ 3. **get_record** - Get single record by ID
25
+ 4. **create_record** - Create new records
26
+ 5. **update_record** - Update existing records
27
+ 6. **delete_record** - Delete records
28
+ 7. **search_records** - Search with filters
29
+ 8. **list_webhooks** - List webhooks
30
+ 9. **create_webhook** - Create webhooks
31
+ 10. **delete_webhook** - Delete webhooks
32
+ 11. **get_webhook_payloads** - Get webhook history
33
+ 12. **refresh_webhook** - Refresh webhook expiration
34
+
35
+ ### 🔐 **Security Improvements**
36
+ - Environment variable support for credentials
37
+ - Token masking in logs
38
+ - Configurable logging levels (ERROR, WARN, INFO, DEBUG)
39
+ - No hardcoded credentials in test files
40
+
41
+ ### 🛠️ **Technical Improvements**
42
+ - Full HTTP method support (GET, POST, PATCH, DELETE)
43
+ - Enhanced error handling with detailed messages
44
+ - Proper API endpoint routing
45
+ - Debug logging support
46
+ - Graceful shutdown handling
47
+
48
+ ### 📈 **Testing**
49
+ - **100% test coverage** - All 12 tools tested and verified
50
+ - Tested with real Airtable API
51
+ - Comprehensive test suite included
52
+ - Test scripts for validation
53
+
54
+ ### 💔 **Breaking Changes**
55
+ - Test files now require environment variables:
56
+ ```bash
57
+ export AIRTABLE_TOKEN="your_token"
58
+ export AIRTABLE_BASE_ID="your_base_id"
59
+ ```
60
+
61
+ ### 🔄 **Migration from v1.2.4**
62
+
63
+ 1. **Update package**:
64
+ ```bash
65
+ npm install -g @rashidazarang/airtable-mcp@latest
66
+ ```
67
+
68
+ 2. **Set credentials** (choose one method):
69
+ - Environment variables
70
+ - Command line arguments
71
+ - .env file
72
+
73
+ 3. **Update configuration** if using webhooks
74
+
75
+ ### 📝 **Webhook Usage Example**
76
+
77
+ ```javascript
78
+ // Create a webhook
79
+ {
80
+ "name": "create_webhook",
81
+ "arguments": {
82
+ "notificationUrl": "https://your-endpoint.com/webhook"
83
+ }
84
+ }
85
+
86
+ // The response includes:
87
+ // - Webhook ID
88
+ // - MAC secret (save this - shown only once!)
89
+ // - Expiration time
90
+ ```
91
+
92
+ ### 🎯 **What's Next**
93
+ - Batch operations support
94
+ - Comment management
95
+ - Attachment handling
96
+ - Schema modification tools
97
+
98
+ ### 🙏 **Acknowledgments**
99
+ - Thanks to all testers and contributors
100
+ - Special thanks for the comprehensive testing feedback
101
+
102
+ ---
103
+
104
+ **Full Changelog**: [v1.2.4...v1.4.0](https://github.com/rashidazarang/airtable-mcp/compare/v1.2.4...v1.4.0)
@@ -0,0 +1,40 @@
1
+ # Security Notice
2
+
3
+ ## Important: API Token Rotation Required
4
+
5
+ If you have been using or testing this repository before January 2025, please note that hardcoded API tokens were previously included in test files. These have been removed and replaced with environment variable requirements.
6
+
7
+ ### Actions Required:
8
+
9
+ 1. **If you used the exposed tokens**:
10
+ - These tokens have been revoked and are no longer valid
11
+ - You must use your own Airtable API credentials
12
+
13
+ 2. **For all users**:
14
+ - Never commit API tokens to version control
15
+ - Always use environment variables or secure configuration files
16
+ - Add `.env` to your `.gitignore` file
17
+
18
+ ### Secure Configuration
19
+
20
+ Set your credentials using environment variables:
21
+
22
+ ```bash
23
+ export AIRTABLE_TOKEN="your_personal_token_here"
24
+ export AIRTABLE_BASE_ID="your_base_id_here"
25
+ ```
26
+
27
+ Or create a `.env` file (never commit this):
28
+
29
+ ```env
30
+ AIRTABLE_TOKEN=your_personal_token_here
31
+ AIRTABLE_BASE_ID=your_base_id_here
32
+ ```
33
+
34
+ ### Reporting Security Issues
35
+
36
+ If you discover any security vulnerabilities, please report them to:
37
+ - Open an issue on GitHub (without including sensitive details)
38
+ - Contact the maintainer directly for sensitive information
39
+
40
+ Thank you for helping keep this project secure.
@@ -0,0 +1,499 @@
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.3.0`);
59
+ log(LOG_LEVELS.INFO, `Token: ${token.slice(0, 5)}...${token.slice(-5)}`);
60
+ log(LOG_LEVELS.INFO, `Base ID: ${baseId}`);
61
+
62
+ // Enhanced Airtable API function with full HTTP method support
63
+ function callAirtableAPI(endpoint, method = 'GET', body = null, queryParams = {}) {
64
+ return new Promise((resolve, reject) => {
65
+ const isBaseEndpoint = !endpoint.startsWith('meta/');
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
+ }
254
+ };
255
+ res.writeHead(200, { 'Content-Type': 'application/json' });
256
+ res.end(JSON.stringify(response));
257
+ return;
258
+ }
259
+
260
+ if (request.method === 'resources/list') {
261
+ const response = {
262
+ jsonrpc: '2.0',
263
+ id: request.id,
264
+ result: {
265
+ resources: [
266
+ {
267
+ id: 'airtable_tables',
268
+ name: 'Airtable Tables',
269
+ description: 'Tables in your Airtable base'
270
+ }
271
+ ]
272
+ }
273
+ };
274
+ res.writeHead(200, { 'Content-Type': 'application/json' });
275
+ res.end(JSON.stringify(response));
276
+ return;
277
+ }
278
+
279
+ if (request.method === 'prompts/list') {
280
+ const response = {
281
+ jsonrpc: '2.0',
282
+ id: request.id,
283
+ result: {
284
+ prompts: [
285
+ {
286
+ id: 'tables_prompt',
287
+ name: 'List Tables',
288
+ description: 'List all tables'
289
+ }
290
+ ]
291
+ }
292
+ };
293
+ res.writeHead(200, { 'Content-Type': 'application/json' });
294
+ res.end(JSON.stringify(response));
295
+ return;
296
+ }
297
+
298
+ // Handle tool calls
299
+ if (request.method === 'tools/call') {
300
+ const toolName = request.params.name;
301
+ const toolParams = request.params.arguments || {};
302
+
303
+ let result;
304
+ let responseText;
305
+
306
+ try {
307
+ // LIST TABLES
308
+ if (toolName === 'list_tables') {
309
+ result = await callAirtableAPI(`meta/bases/${baseId}/tables`);
310
+ const tables = result.tables || [];
311
+
312
+ responseText = tables.length > 0
313
+ ? `Found ${tables.length} table(s):\n` + tables.map((table, i) =>
314
+ `${i+1}. ${table.name} (ID: ${table.id}, Fields: ${table.fields?.length || 0})`
315
+ ).join('\n')
316
+ : 'No tables found in this base.';
317
+ }
318
+
319
+ // LIST RECORDS
320
+ else if (toolName === 'list_records') {
321
+ const { table, maxRecords, view } = toolParams;
322
+
323
+ const queryParams = {};
324
+ if (maxRecords) queryParams.maxRecords = maxRecords;
325
+ if (view) queryParams.view = view;
326
+
327
+ result = await callAirtableAPI(`${table}`, 'GET', null, queryParams);
328
+ const records = result.records || [];
329
+
330
+ responseText = records.length > 0
331
+ ? `Found ${records.length} record(s) in table "${table}":\n` +
332
+ records.map((record, i) =>
333
+ `${i+1}. ID: ${record.id}\n Fields: ${JSON.stringify(record.fields, null, 2)}`
334
+ ).join('\n\n')
335
+ : `No records found in table "${table}".`;
336
+ }
337
+
338
+ // GET SINGLE RECORD
339
+ else if (toolName === 'get_record') {
340
+ const { table, recordId } = toolParams;
341
+
342
+ result = await callAirtableAPI(`${table}/${recordId}`);
343
+
344
+ responseText = `Record ${recordId} from table "${table}":\n` +
345
+ JSON.stringify(result.fields, null, 2) +
346
+ `\n\nCreated: ${result.createdTime}`;
347
+ }
348
+
349
+ // CREATE RECORD
350
+ else if (toolName === 'create_record') {
351
+ const { table, fields } = toolParams;
352
+
353
+ const body = {
354
+ fields: fields
355
+ };
356
+
357
+ result = await callAirtableAPI(table, 'POST', body);
358
+
359
+ responseText = `Successfully created record in table "${table}":\n` +
360
+ `Record ID: ${result.id}\n` +
361
+ `Fields: ${JSON.stringify(result.fields, null, 2)}\n` +
362
+ `Created at: ${result.createdTime}`;
363
+ }
364
+
365
+ // UPDATE RECORD
366
+ else if (toolName === 'update_record') {
367
+ const { table, recordId, fields } = toolParams;
368
+
369
+ const body = {
370
+ fields: fields
371
+ };
372
+
373
+ result = await callAirtableAPI(`${table}/${recordId}`, 'PATCH', body);
374
+
375
+ responseText = `Successfully updated record ${recordId} in table "${table}":\n` +
376
+ `Updated fields: ${JSON.stringify(result.fields, null, 2)}`;
377
+ }
378
+
379
+ // DELETE RECORD
380
+ else if (toolName === 'delete_record') {
381
+ const { table, recordId } = toolParams;
382
+
383
+ result = await callAirtableAPI(`${table}/${recordId}`, 'DELETE');
384
+
385
+ responseText = `Successfully deleted record ${recordId} from table "${table}".\n` +
386
+ `Deleted record ID: ${result.id}\n` +
387
+ `Deleted: ${result.deleted}`;
388
+ }
389
+
390
+ // SEARCH RECORDS
391
+ else if (toolName === 'search_records') {
392
+ const { table, filterByFormula, sort, maxRecords, fields } = toolParams;
393
+
394
+ const queryParams = {};
395
+ if (filterByFormula) queryParams.filterByFormula = filterByFormula;
396
+ if (maxRecords) queryParams.maxRecords = maxRecords;
397
+ if (fields && fields.length > 0) queryParams.fields = fields;
398
+ if (sort && sort.length > 0) {
399
+ sort.forEach((s, i) => {
400
+ queryParams[`sort[${i}][field]`] = s.field;
401
+ queryParams[`sort[${i}][direction]`] = s.direction || 'asc';
402
+ });
403
+ }
404
+
405
+ result = await callAirtableAPI(table, 'GET', null, queryParams);
406
+ const records = result.records || [];
407
+
408
+ responseText = records.length > 0
409
+ ? `Found ${records.length} matching record(s) in table "${table}":\n` +
410
+ records.map((record, i) =>
411
+ `${i+1}. ID: ${record.id}\n Fields: ${JSON.stringify(record.fields, null, 2)}`
412
+ ).join('\n\n')
413
+ : `No records found matching the search criteria in table "${table}".`;
414
+ }
415
+
416
+ else {
417
+ throw new Error(`Unknown tool: ${toolName}`);
418
+ }
419
+
420
+ const response = {
421
+ jsonrpc: '2.0',
422
+ id: request.id,
423
+ result: {
424
+ content: [
425
+ {
426
+ type: 'text',
427
+ text: responseText
428
+ }
429
+ ]
430
+ }
431
+ };
432
+ res.writeHead(200, { 'Content-Type': 'application/json' });
433
+ res.end(JSON.stringify(response));
434
+
435
+ } catch (error) {
436
+ log(LOG_LEVELS.ERROR, `Tool ${toolName} error:`, error.message);
437
+
438
+ const response = {
439
+ jsonrpc: '2.0',
440
+ id: request.id,
441
+ result: {
442
+ content: [
443
+ {
444
+ type: 'text',
445
+ text: `Error executing ${toolName}: ${error.message}`
446
+ }
447
+ ]
448
+ }
449
+ };
450
+ res.writeHead(200, { 'Content-Type': 'application/json' });
451
+ res.end(JSON.stringify(response));
452
+ }
453
+
454
+ return;
455
+ }
456
+
457
+ // Method not found
458
+ const response = {
459
+ jsonrpc: '2.0',
460
+ id: request.id,
461
+ error: {
462
+ code: -32601,
463
+ message: `Method ${request.method} not found`
464
+ }
465
+ };
466
+ res.writeHead(200, { 'Content-Type': 'application/json' });
467
+ res.end(JSON.stringify(response));
468
+
469
+ } catch (error) {
470
+ log(LOG_LEVELS.ERROR, 'Error processing request:', error);
471
+ const response = {
472
+ jsonrpc: '2.0',
473
+ id: request.id || null,
474
+ error: {
475
+ code: -32000,
476
+ message: error.message || 'Unknown error'
477
+ }
478
+ };
479
+ res.writeHead(200, { 'Content-Type': 'application/json' });
480
+ res.end(JSON.stringify(response));
481
+ }
482
+ });
483
+ });
484
+
485
+ // Start server
486
+ const PORT = process.env.PORT || 8010;
487
+ server.listen(PORT, () => {
488
+ log(LOG_LEVELS.INFO, `Enhanced Airtable MCP server v1.3.0 running at http://localhost:${PORT}/mcp`);
489
+ console.log(`For Claude, use this URL: http://localhost:${PORT}/mcp`);
490
+ });
491
+
492
+ // Graceful shutdown
493
+ process.on('SIGINT', () => {
494
+ log(LOG_LEVELS.INFO, 'Shutting down server...');
495
+ server.close(() => {
496
+ log(LOG_LEVELS.INFO, 'Server stopped');
497
+ process.exit(0);
498
+ });
499
+ });