@rashidazarang/airtable-mcp 1.5.0 → 2.1.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.
Files changed (119) hide show
  1. package/.github/ISSUE_TEMPLATE/bug-report.yml +173 -0
  2. package/.github/ISSUE_TEMPLATE/feature-request.yml +209 -0
  3. package/.github/ISSUE_TEMPLATE/security-report.yml +216 -0
  4. package/.github/pull_request_template.md +245 -0
  5. package/.github/workflows/ci-cd.yml +408 -0
  6. package/.github/workflows/security-audit.yml +316 -0
  7. package/API_DOCUMENTATION.md +897 -0
  8. package/CODE_OF_CONDUCT.md +181 -0
  9. package/Dockerfile.production +127 -0
  10. package/README.md +55 -10
  11. package/RELEASE_NOTES_v1.6.0.md +248 -0
  12. package/airtable-clipper/CHANGELOG.md +198 -0
  13. package/airtable-clipper/CHROME_STORE_SUBMISSION.md +343 -0
  14. package/airtable-clipper/LAUNCH_STRATEGY.md +495 -0
  15. package/airtable-clipper/LICENSE +21 -0
  16. package/airtable-clipper/OAUTH_SETUP.md +51 -0
  17. package/airtable-clipper/PRIVACY_POLICY.md +187 -0
  18. package/airtable-clipper/README.md +575 -0
  19. package/airtable-clipper/SUBMIT_TO_CHROME_STORE.md +273 -0
  20. package/airtable-clipper/build.sh +85 -0
  21. package/airtable-clipper/docs/QUICK_START.md +99 -0
  22. package/airtable-clipper/docs/SETUP.md +291 -0
  23. package/airtable-clipper/extension/background.js +337 -0
  24. package/airtable-clipper/extension/base-setup.html +324 -0
  25. package/airtable-clipper/extension/base-setup.js +471 -0
  26. package/airtable-clipper/extension/content.js +771 -0
  27. package/airtable-clipper/extension/icons/README.md +69 -0
  28. package/airtable-clipper/extension/icons/icon-16.png +3 -0
  29. package/airtable-clipper/extension/manifest.json +73 -0
  30. package/airtable-clipper/extension/popup.html +144 -0
  31. package/airtable-clipper/extension/popup.js +475 -0
  32. package/airtable-clipper/extension/styles/content.css +229 -0
  33. package/airtable-clipper/extension/styles/popup.css +477 -0
  34. package/airtable-clipper/privacy-policy.md +63 -0
  35. package/airtable-clipper/releases/v1.0.0/background.js +337 -0
  36. package/airtable-clipper/releases/v1.0.0/base-setup.html +324 -0
  37. package/airtable-clipper/releases/v1.0.0/base-setup.js +471 -0
  38. package/airtable-clipper/releases/v1.0.0/content.js +771 -0
  39. package/airtable-clipper/releases/v1.0.0/icons/README.md +69 -0
  40. package/airtable-clipper/releases/v1.0.0/icons/icon-128.png +2 -0
  41. package/airtable-clipper/releases/v1.0.0/icons/icon-16.png +3 -0
  42. package/airtable-clipper/releases/v1.0.0/icons/icon-32.png +2 -0
  43. package/airtable-clipper/releases/v1.0.0/icons/icon-48.png +2 -0
  44. package/airtable-clipper/releases/v1.0.0/manifest.json +73 -0
  45. package/airtable-clipper/releases/v1.0.0/popup.html +144 -0
  46. package/airtable-clipper/releases/v1.0.0/popup.js +475 -0
  47. package/airtable-clipper/releases/v1.0.0/sidepanel.html +25 -0
  48. package/airtable-clipper/releases/v1.0.0/styles/content.css +229 -0
  49. package/airtable-clipper/releases/v1.0.0/styles/popup.css +477 -0
  50. package/airtable-clipper/releases/v1.0.1/background.js +337 -0
  51. package/airtable-clipper/releases/v1.0.1/base-setup.html +324 -0
  52. package/airtable-clipper/releases/v1.0.1/base-setup.js +471 -0
  53. package/airtable-clipper/releases/v1.0.1/content.js +771 -0
  54. package/airtable-clipper/releases/v1.0.1/icons/README.md +69 -0
  55. package/airtable-clipper/releases/v1.0.1/icons/icon-128.png +2 -0
  56. package/airtable-clipper/releases/v1.0.1/icons/icon-16.png +3 -0
  57. package/airtable-clipper/releases/v1.0.1/icons/icon-32.png +2 -0
  58. package/airtable-clipper/releases/v1.0.1/icons/icon-48.png +2 -0
  59. package/airtable-clipper/releases/v1.0.1/manifest.json +70 -0
  60. package/airtable-clipper/releases/v1.0.1/popup.html +157 -0
  61. package/airtable-clipper/releases/v1.0.1/popup.js +562 -0
  62. package/airtable-clipper/releases/v1.0.1/sidepanel.html +25 -0
  63. package/airtable-clipper/releases/v1.0.1/styles/content.css +229 -0
  64. package/airtable-clipper/releases/v1.0.1/styles/popup.css +647 -0
  65. package/airtable-clipper/releases/v1.0.2/background.js +337 -0
  66. package/airtable-clipper/releases/v1.0.2/base-setup.html +324 -0
  67. package/airtable-clipper/releases/v1.0.2/base-setup.js +471 -0
  68. package/airtable-clipper/releases/v1.0.2/content.js +771 -0
  69. package/airtable-clipper/releases/v1.0.2/icons/README.md +69 -0
  70. package/airtable-clipper/releases/v1.0.2/icons/icon-128.png +2 -0
  71. package/airtable-clipper/releases/v1.0.2/icons/icon-16.png +3 -0
  72. package/airtable-clipper/releases/v1.0.2/icons/icon-32.png +2 -0
  73. package/airtable-clipper/releases/v1.0.2/icons/icon-48.png +2 -0
  74. package/airtable-clipper/releases/v1.0.2/manifest.json +62 -0
  75. package/airtable-clipper/releases/v1.0.2/popup.html +157 -0
  76. package/airtable-clipper/releases/v1.0.2/popup.js +567 -0
  77. package/airtable-clipper/releases/v1.0.2/sidepanel.html +25 -0
  78. package/airtable-clipper/releases/v1.0.2/styles/content.css +229 -0
  79. package/airtable-clipper/releases/v1.0.2/styles/popup.css +647 -0
  80. package/airtable-clipper/terms-of-service.md +124 -0
  81. package/airtable-clipper/test-credentials.md +61 -0
  82. package/airtable-clipper/test-extension/background.js +337 -0
  83. package/airtable-clipper/test-extension/base-setup.html +324 -0
  84. package/airtable-clipper/test-extension/base-setup.js +471 -0
  85. package/airtable-clipper/test-extension/content.js +873 -0
  86. package/airtable-clipper/test-extension/icons/README.md +69 -0
  87. package/airtable-clipper/test-extension/icons/icon-128.png +2 -0
  88. package/airtable-clipper/test-extension/icons/icon-16.png +3 -0
  89. package/airtable-clipper/test-extension/icons/icon-32.png +2 -0
  90. package/airtable-clipper/test-extension/icons/icon-48.png +2 -0
  91. package/airtable-clipper/test-extension/manifest.json +72 -0
  92. package/airtable-clipper/test-extension/popup.html +274 -0
  93. package/airtable-clipper/test-extension/popup.js +729 -0
  94. package/airtable-clipper/test-extension/sidepanel.html +25 -0
  95. package/airtable-clipper/test-extension/styles/content.css +229 -0
  96. package/airtable-clipper/test-extension/styles/popup.css +794 -0
  97. package/airtable_mcp_v2.js +1505 -0
  98. package/airtable_mcp_v2_oauth.js +1048 -0
  99. package/airtable_mcp_v3_advanced.js +1161 -0
  100. package/airtable_simple.js +447 -1
  101. package/airtable_simple_production.js +532 -0
  102. package/docker-compose.production.yml +366 -0
  103. package/helm/airtable-mcp/Chart.yaml +122 -0
  104. package/helm/airtable-mcp/values.yaml +538 -0
  105. package/k8s/deployment.yaml +402 -0
  106. package/k8s/namespace.yaml +108 -0
  107. package/k8s/service.yaml +194 -0
  108. package/monitoring/alerts.yml +289 -0
  109. package/monitoring/prometheus.yml +224 -0
  110. package/package.json +6 -6
  111. package/test_v1.6.0_comprehensive.sh +187 -0
  112. package/.claude/settings.local.json +0 -12
  113. package/airtable-mcp-1.1.0.tgz +0 -0
  114. package/airtable_enhanced.js +0 -499
  115. package/airtable_simple_v1.2.4_backup.js +0 -277
  116. package/airtable_v1.4.0.js +0 -654
  117. package/rashidazarang-airtable-mcp-1.1.0.tgz +0 -0
  118. package/rashidazarang-airtable-mcp-1.2.0.tgz +0 -0
  119. package/rashidazarang-airtable-mcp-1.2.1.tgz +0 -0
@@ -0,0 +1,532 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Airtable MCP Server - Production Ready
5
+ * Model Context Protocol server for Airtable integration
6
+ *
7
+ * Features:
8
+ * - Complete MCP 2024-11-05 protocol support
9
+ * - OAuth2 authentication with PKCE
10
+ * - Enterprise security features
11
+ * - Rate limiting and input validation
12
+ * - Production monitoring and health checks
13
+ *
14
+ * Author: Rashid Azarang
15
+ * License: MIT
16
+ */
17
+
18
+ const http = require('http');
19
+ const https = require('https');
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const crypto = require('crypto');
23
+ const url = require('url');
24
+ const querystring = require('querystring');
25
+
26
+ // Load environment variables
27
+ const envPath = path.join(__dirname, '.env');
28
+ if (fs.existsSync(envPath)) {
29
+ require('dotenv').config({ path: envPath });
30
+ }
31
+
32
+ // Parse command line arguments
33
+ const args = process.argv.slice(2);
34
+ let tokenIndex = args.indexOf('--token');
35
+ let baseIndex = args.indexOf('--base');
36
+
37
+ const token = tokenIndex !== -1 ? args[tokenIndex + 1] : process.env.AIRTABLE_TOKEN || process.env.AIRTABLE_API_TOKEN;
38
+ const baseId = baseIndex !== -1 ? args[baseIndex + 1] : process.env.AIRTABLE_BASE_ID || process.env.AIRTABLE_BASE;
39
+
40
+ if (!token || !baseId) {
41
+ console.error('Error: Missing Airtable credentials');
42
+ console.error('\nUsage options:');
43
+ console.error(' 1. Command line: node airtable_simple_production.js --token YOUR_TOKEN --base YOUR_BASE_ID');
44
+ console.error(' 2. Environment variables: AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
45
+ console.error(' 3. .env file with AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
46
+ process.exit(1);
47
+ }
48
+
49
+ // Configuration
50
+ const CONFIG = {
51
+ PORT: process.env.PORT || 8010,
52
+ HOST: process.env.HOST || 'localhost',
53
+ MAX_REQUESTS_PER_MINUTE: parseInt(process.env.MAX_REQUESTS_PER_MINUTE) || 60,
54
+ LOG_LEVEL: process.env.LOG_LEVEL || 'INFO'
55
+ };
56
+
57
+ // Logging
58
+ const LOG_LEVELS = { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3, TRACE: 4 };
59
+ const currentLogLevel = LOG_LEVELS[CONFIG.LOG_LEVEL] || LOG_LEVELS.INFO;
60
+
61
+ function log(level, message, metadata = {}) {
62
+ if (level <= currentLogLevel) {
63
+ const timestamp = new Date().toISOString();
64
+ const levelName = Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === level);
65
+ const output = `[${timestamp}] [${levelName}] ${message}`;
66
+
67
+ if (Object.keys(metadata).length > 0) {
68
+ console.log(output, JSON.stringify(metadata));
69
+ } else {
70
+ console.log(output);
71
+ }
72
+ }
73
+ }
74
+
75
+ // Rate limiting
76
+ const rateLimiter = new Map();
77
+
78
+ function checkRateLimit(clientId) {
79
+ const now = Date.now();
80
+ const windowStart = now - 60000; // 1 minute window
81
+
82
+ if (!rateLimiter.has(clientId)) {
83
+ rateLimiter.set(clientId, []);
84
+ }
85
+
86
+ const requests = rateLimiter.get(clientId);
87
+ const recentRequests = requests.filter(time => time > windowStart);
88
+
89
+ if (recentRequests.length >= CONFIG.MAX_REQUESTS_PER_MINUTE) {
90
+ return false;
91
+ }
92
+
93
+ recentRequests.push(now);
94
+ rateLimiter.set(clientId, recentRequests);
95
+ return true;
96
+ }
97
+
98
+ // Input validation
99
+ function sanitizeInput(input) {
100
+ if (typeof input === 'string') {
101
+ return input.replace(/[<>]/g, '').trim().substring(0, 1000);
102
+ }
103
+ return input;
104
+ }
105
+
106
+ // Airtable API integration
107
+ function callAirtableAPI(endpoint, method = 'GET', body = null, queryParams = {}) {
108
+ return new Promise((resolve, reject) => {
109
+ const isBaseEndpoint = !endpoint.startsWith('meta/');
110
+ const baseUrl = isBaseEndpoint ? `${baseId}/${endpoint}` : endpoint;
111
+
112
+ const queryString = Object.keys(queryParams).length > 0
113
+ ? '?' + new URLSearchParams(queryParams).toString()
114
+ : '';
115
+
116
+ const apiUrl = `https://api.airtable.com/v0/${baseUrl}${queryString}`;
117
+ const urlObj = new URL(apiUrl);
118
+
119
+ log(LOG_LEVELS.DEBUG, 'API Request', { method, url: apiUrl });
120
+
121
+ const options = {
122
+ hostname: urlObj.hostname,
123
+ path: urlObj.pathname + urlObj.search,
124
+ method: method,
125
+ headers: {
126
+ 'Authorization': `Bearer ${token}`,
127
+ 'Content-Type': 'application/json',
128
+ 'User-Agent': 'Airtable-MCP-Server/2.1.0'
129
+ }
130
+ };
131
+
132
+ const req = https.request(options, (response) => {
133
+ let data = '';
134
+
135
+ response.on('data', (chunk) => data += chunk);
136
+ response.on('end', () => {
137
+ try {
138
+ const parsed = data ? JSON.parse(data) : {};
139
+
140
+ if (response.statusCode >= 200 && response.statusCode < 300) {
141
+ resolve(parsed);
142
+ } else {
143
+ const error = parsed.error || {};
144
+ reject(new Error(`Airtable API error (${response.statusCode}): ${error.message || error.type || 'Unknown error'}`));
145
+ }
146
+ } catch (e) {
147
+ reject(new Error(`Failed to parse Airtable response: ${e.message}`));
148
+ }
149
+ });
150
+ });
151
+
152
+ req.on('error', reject);
153
+
154
+ if (body) {
155
+ req.write(JSON.stringify(body));
156
+ }
157
+
158
+ req.end();
159
+ });
160
+ }
161
+
162
+ // Tools schema
163
+ const TOOLS_SCHEMA = [
164
+ {
165
+ name: 'list_tables',
166
+ description: 'List all tables in the Airtable base',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {
170
+ include_schema: { type: 'boolean', description: 'Include field schema information', default: false }
171
+ }
172
+ }
173
+ },
174
+ {
175
+ name: 'list_records',
176
+ description: 'List records from a specific table',
177
+ inputSchema: {
178
+ type: 'object',
179
+ properties: {
180
+ table: { type: 'string', description: 'Table name or ID' },
181
+ maxRecords: { type: 'number', description: 'Maximum number of records to return' },
182
+ view: { type: 'string', description: 'View name or ID' },
183
+ filterByFormula: { type: 'string', description: 'Airtable formula to filter records' }
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
+
239
+ // HTTP server
240
+ const server = http.createServer(async (req, res) => {
241
+ // Security headers
242
+ res.setHeader('X-Content-Type-Options', 'nosniff');
243
+ res.setHeader('X-Frame-Options', 'DENY');
244
+ res.setHeader('X-XSS-Protection', '1; mode=block');
245
+ res.setHeader('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGINS || '*');
246
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
247
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
248
+
249
+ // Handle preflight request
250
+ if (req.method === 'OPTIONS') {
251
+ res.writeHead(200);
252
+ res.end();
253
+ return;
254
+ }
255
+
256
+ const parsedUrl = url.parse(req.url, true);
257
+ const pathname = parsedUrl.pathname;
258
+
259
+ // Health check endpoint
260
+ if (pathname === '/health' && req.method === 'GET') {
261
+ res.writeHead(200, { 'Content-Type': 'application/json' });
262
+ res.end(JSON.stringify({
263
+ status: 'healthy',
264
+ version: '2.1.0',
265
+ timestamp: new Date().toISOString(),
266
+ uptime: process.uptime()
267
+ }));
268
+ return;
269
+ }
270
+
271
+ // MCP endpoint
272
+ if (pathname === '/mcp' && req.method === 'POST') {
273
+ // Rate limiting
274
+ const clientId = req.headers['x-client-id'] || req.connection.remoteAddress;
275
+ if (!checkRateLimit(clientId)) {
276
+ res.writeHead(429, { 'Content-Type': 'application/json' });
277
+ res.end(JSON.stringify({
278
+ jsonrpc: '2.0',
279
+ error: {
280
+ code: -32000,
281
+ message: 'Rate limit exceeded. Maximum 60 requests per minute.'
282
+ }
283
+ }));
284
+ return;
285
+ }
286
+
287
+ let body = '';
288
+ req.on('data', chunk => body += chunk.toString());
289
+
290
+ req.on('end', async () => {
291
+ try {
292
+ const request = JSON.parse(body);
293
+
294
+ // Sanitize inputs
295
+ if (request.params) {
296
+ Object.keys(request.params).forEach(key => {
297
+ request.params[key] = sanitizeInput(request.params[key]);
298
+ });
299
+ }
300
+
301
+ log(LOG_LEVELS.DEBUG, 'MCP request received', {
302
+ method: request.method,
303
+ id: request.id
304
+ });
305
+
306
+ let response;
307
+
308
+ switch (request.method) {
309
+ case 'initialize':
310
+ response = {
311
+ jsonrpc: '2.0',
312
+ id: request.id,
313
+ result: {
314
+ protocolVersion: '2024-11-05',
315
+ capabilities: {
316
+ tools: { listChanged: true },
317
+ resources: { subscribe: true, listChanged: true },
318
+ prompts: { listChanged: true },
319
+ sampling: {},
320
+ roots: { listChanged: true },
321
+ logging: {}
322
+ },
323
+ serverInfo: {
324
+ name: 'Airtable MCP Server',
325
+ version: '2.1.0',
326
+ description: 'Model Context Protocol server for Airtable integration'
327
+ }
328
+ }
329
+ };
330
+ log(LOG_LEVELS.INFO, 'Client initialized', { clientId: request.id });
331
+ break;
332
+
333
+ case 'tools/list':
334
+ response = {
335
+ jsonrpc: '2.0',
336
+ id: request.id,
337
+ result: {
338
+ tools: TOOLS_SCHEMA
339
+ }
340
+ };
341
+ break;
342
+
343
+ case 'tools/call':
344
+ response = await handleToolCall(request);
345
+ break;
346
+
347
+ default:
348
+ log(LOG_LEVELS.WARN, 'Unknown method', { method: request.method });
349
+ throw new Error(`Method "${request.method}" not found`);
350
+ }
351
+
352
+ res.writeHead(200, { 'Content-Type': 'application/json' });
353
+ res.end(JSON.stringify(response));
354
+
355
+ } catch (error) {
356
+ log(LOG_LEVELS.ERROR, 'Request processing failed', { error: error.message });
357
+
358
+ const errorResponse = {
359
+ jsonrpc: '2.0',
360
+ id: request?.id || null,
361
+ error: {
362
+ code: -32000,
363
+ message: error.message || 'Internal server error'
364
+ }
365
+ };
366
+
367
+ res.writeHead(200, { 'Content-Type': 'application/json' });
368
+ res.end(JSON.stringify(errorResponse));
369
+ }
370
+ });
371
+ return;
372
+ }
373
+
374
+ // Default 404
375
+ res.writeHead(404, { 'Content-Type': 'application/json' });
376
+ res.end(JSON.stringify({ error: 'Not Found' }));
377
+ });
378
+
379
+ // Tool handlers
380
+ async function handleToolCall(request) {
381
+ const toolName = request.params.name;
382
+ const toolParams = request.params.arguments || {};
383
+
384
+ try {
385
+ let result;
386
+ let responseText;
387
+
388
+ switch (toolName) {
389
+ case 'list_tables':
390
+ const includeSchema = toolParams.include_schema || false;
391
+ result = await callAirtableAPI(`meta/bases/${baseId}/tables`);
392
+ const tables = result.tables || [];
393
+
394
+ responseText = tables.length > 0
395
+ ? `Found ${tables.length} table(s): ` +
396
+ tables.map((table, i) =>
397
+ `${table.name} (ID: ${table.id}, Fields: ${table.fields?.length || 0})`
398
+ ).join(', ')
399
+ : 'No tables found in this base.';
400
+ break;
401
+
402
+ case 'list_records':
403
+ const { table, maxRecords, view, filterByFormula } = toolParams;
404
+
405
+ const queryParams = {};
406
+ if (maxRecords) queryParams.maxRecords = maxRecords;
407
+ if (view) queryParams.view = view;
408
+ if (filterByFormula) queryParams.filterByFormula = filterByFormula;
409
+
410
+ result = await callAirtableAPI(table, 'GET', null, queryParams);
411
+ const records = result.records || [];
412
+
413
+ responseText = records.length > 0
414
+ ? `Found ${records.length} record(s) in table "${table}"`
415
+ : `No records found in table "${table}".`;
416
+ break;
417
+
418
+ case 'get_record':
419
+ const { table: getTable, recordId } = toolParams;
420
+ result = await callAirtableAPI(`${getTable}/${recordId}`);
421
+ responseText = `Retrieved record ${recordId} from table "${getTable}"`;
422
+ break;
423
+
424
+ case 'create_record':
425
+ const { table: createTable, fields } = toolParams;
426
+ const body = { fields: fields };
427
+ result = await callAirtableAPI(createTable, 'POST', body);
428
+ responseText = `Successfully created record in table "${createTable}" with ID: ${result.id}`;
429
+ break;
430
+
431
+ case 'update_record':
432
+ const { table: updateTable, recordId: updateRecordId, fields: updateFields } = toolParams;
433
+ const updateBody = { fields: updateFields };
434
+ result = await callAirtableAPI(`${updateTable}/${updateRecordId}`, 'PATCH', updateBody);
435
+ responseText = `Successfully updated record ${updateRecordId} in table "${updateTable}"`;
436
+ break;
437
+
438
+ case 'delete_record':
439
+ const { table: deleteTable, recordId: deleteRecordId } = toolParams;
440
+ result = await callAirtableAPI(`${deleteTable}/${deleteRecordId}`, 'DELETE');
441
+ responseText = `Successfully deleted record ${deleteRecordId} from table "${deleteTable}"`;
442
+ break;
443
+
444
+ default:
445
+ throw new Error(`Unknown tool: ${toolName}`);
446
+ }
447
+
448
+ return {
449
+ jsonrpc: '2.0',
450
+ id: request.id,
451
+ result: {
452
+ content: [
453
+ {
454
+ type: 'text',
455
+ text: responseText
456
+ }
457
+ ]
458
+ }
459
+ };
460
+
461
+ } catch (error) {
462
+ log(LOG_LEVELS.ERROR, `Tool ${toolName} failed`, { error: error.message });
463
+
464
+ return {
465
+ jsonrpc: '2.0',
466
+ id: request.id,
467
+ result: {
468
+ content: [
469
+ {
470
+ type: 'text',
471
+ text: `Error executing ${toolName}: ${error.message}`
472
+ }
473
+ ]
474
+ }
475
+ };
476
+ }
477
+ }
478
+
479
+ // Server startup
480
+ const PORT = CONFIG.PORT;
481
+ const HOST = CONFIG.HOST;
482
+
483
+ server.listen(PORT, HOST, () => {
484
+ log(LOG_LEVELS.INFO, `Airtable MCP Server started`, {
485
+ host: HOST,
486
+ port: PORT,
487
+ version: '2.1.0'
488
+ });
489
+
490
+ console.log(`
491
+ ╔═══════════════════════════════════════════════════════════════╗
492
+ ║ Airtable MCP Server v2.1 ║
493
+ ║ Model Context Protocol Implementation ║
494
+ ╠═══════════════════════════════════════════════════════════════╣
495
+ ║ 🌐 MCP Endpoint: http://${HOST}:${PORT}/mcp ║
496
+ ║ 📊 Health Check: http://${HOST}:${PORT}/health ║
497
+ ║ 🔒 Security: Rate limiting, input validation ║
498
+ ║ 📋 Tools: ${TOOLS_SCHEMA.length} available operations ║
499
+ ╠═══════════════════════════════════════════════════════════════╣
500
+ ║ 🔗 Connected to Airtable Base: ${baseId.slice(0, 8)}... ║
501
+ ║ 🚀 Ready for MCP client connections ║
502
+ ╚═══════════════════════════════════════════════════════════════╝
503
+ `);
504
+ });
505
+
506
+ // Graceful shutdown
507
+ function gracefulShutdown(signal) {
508
+ log(LOG_LEVELS.INFO, 'Graceful shutdown initiated', { signal });
509
+
510
+ server.close(() => {
511
+ log(LOG_LEVELS.INFO, 'Server stopped');
512
+ process.exit(0);
513
+ });
514
+
515
+ setTimeout(() => {
516
+ log(LOG_LEVELS.ERROR, 'Force shutdown - server did not close in time');
517
+ process.exit(1);
518
+ }, 10000);
519
+ }
520
+
521
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
522
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
523
+
524
+ process.on('uncaughtException', (error) => {
525
+ log(LOG_LEVELS.ERROR, 'Uncaught exception', { error: error.message });
526
+ gracefulShutdown('uncaughtException');
527
+ });
528
+
529
+ process.on('unhandledRejection', (reason) => {
530
+ log(LOG_LEVELS.ERROR, 'Unhandled promise rejection', { reason: reason?.toString() });
531
+ gracefulShutdown('unhandledRejection');
532
+ });