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
│ ├── app
│ │ ├── airtable-client.ts
│ │ ├── config.ts
│ │ ├── context.ts
│ │ ├── exceptions.ts
│ │ ├── governance.ts
│ │ ├── logger.ts
│ │ ├── rateLimiter.ts
│ │ ├── tools
│ │ │ ├── create.ts
│ │ │ ├── describe.ts
│ │ │ ├── handleError.ts
│ │ │ ├── index.ts
│ │ │ ├── listBases.ts
│ │ │ ├── listExceptions.ts
│ │ │ ├── listGovernance.ts
│ │ │ ├── query.ts
│ │ │ ├── update.ts
│ │ │ ├── upsert.ts
│ │ │ └── webhooks.ts
│ │ └── types.ts
│ ├── errors.ts
│ ├── index.d.ts
│ ├── index.ts
│ ├── prompt-templates.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
├── app
│ ├── airtable-client.d.ts
│ ├── config.d.ts
│ ├── context.d.ts
│ ├── exceptions.d.ts
│ ├── governance.d.ts
│ ├── logger.d.ts
│ ├── rateLimiter.d.ts
│ ├── tools
│ │ ├── create.d.ts
│ │ ├── describe.d.ts
│ │ ├── handleError.d.ts
│ │ ├── index.d.ts
│ │ ├── listBases.d.ts
│ │ ├── listExceptions.d.ts
│ │ ├── listGovernance.d.ts
│ │ ├── query.d.ts
│ │ ├── update.d.ts
│ │ ├── upsert.d.ts
│ │ └── webhooks.d.ts
│ └── types.d.ts
├── errors.d.ts
├── index.d.ts
├── prompt-templates.d.ts
├── test-suite.d.ts
└── tools-schemas.d.ts
```
# Files
--------------------------------------------------------------------------------
/src/javascript/airtable_simple_production.js:
--------------------------------------------------------------------------------
```javascript
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 | let 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 | // Sanitize message to prevent format string attacks
66 | const safeMessage = String(message).replace(/%/g, '%%');
67 | const output = `[${timestamp}] [${levelName}] ${safeMessage}`;
68 |
69 | if (Object.keys(metadata).length > 0) {
70 | // Use separate arguments to avoid format string injection
71 | console.log('%s %s', output, JSON.stringify(metadata));
72 | } else {
73 | console.log('%s', output);
74 | }
75 | }
76 | }
77 |
78 | // Rate limiting
79 | const rateLimiter = new Map();
80 |
81 | function checkRateLimit(clientId) {
82 | const now = Date.now();
83 | const windowStart = now - 60000; // 1 minute window
84 |
85 | if (!rateLimiter.has(clientId)) {
86 | rateLimiter.set(clientId, []);
87 | }
88 |
89 | const requests = rateLimiter.get(clientId);
90 | const recentRequests = requests.filter(time => time > windowStart);
91 |
92 | if (recentRequests.length >= CONFIG.MAX_REQUESTS_PER_MINUTE) {
93 | return false;
94 | }
95 |
96 | recentRequests.push(now);
97 | rateLimiter.set(clientId, recentRequests);
98 | return true;
99 | }
100 |
101 | // Input validation and HTML escaping
102 | function sanitizeInput(input) {
103 | if (typeof input === 'string') {
104 | return input.replace(/[<>]/g, '').trim().substring(0, 1000);
105 | }
106 | return input;
107 | }
108 |
109 | function escapeHtml(unsafe) {
110 | if (typeof unsafe !== 'string') {
111 | return String(unsafe);
112 | }
113 | return unsafe
114 | .replace(/&/g, "&")
115 | .replace(/</g, "<")
116 | .replace(/>/g, ">")
117 | .replace(/"/g, """)
118 | .replace(/'/g, "'")
119 | .replace(/\//g, "/");
120 | }
121 |
122 | function validateUrl(url) {
123 | try {
124 | const parsed = new URL(url);
125 | // Only allow http and https protocols
126 | return ['http:', 'https:'].includes(parsed.protocol);
127 | } catch {
128 | return false;
129 | }
130 | }
131 |
132 | // Airtable API integration
133 | function callAirtableAPI(endpoint, method = 'GET', body = null, queryParams = {}) {
134 | return new Promise((resolve, reject) => {
135 | const isBaseEndpoint = !endpoint.startsWith('meta/');
136 | const baseUrl = isBaseEndpoint ? `${baseId}/${endpoint}` : endpoint;
137 |
138 | const queryString = Object.keys(queryParams).length > 0
139 | ? '?' + new URLSearchParams(queryParams).toString()
140 | : '';
141 |
142 | const apiUrl = `https://api.airtable.com/v0/${baseUrl}${queryString}`;
143 | const urlObj = new URL(apiUrl);
144 |
145 | log(LOG_LEVELS.DEBUG, 'API Request', { method, url: apiUrl });
146 |
147 | const options = {
148 | hostname: urlObj.hostname,
149 | path: urlObj.pathname + urlObj.search,
150 | method: method,
151 | headers: {
152 | 'Authorization': `Bearer ${token}`,
153 | 'Content-Type': 'application/json',
154 | 'User-Agent': 'Airtable-MCP-Server/2.1.0'
155 | }
156 | };
157 |
158 | const req = https.request(options, (response) => {
159 | let data = '';
160 |
161 | response.on('data', (chunk) => data += chunk);
162 | response.on('end', () => {
163 | try {
164 | const parsed = data ? JSON.parse(data) : {};
165 |
166 | if (response.statusCode >= 200 && response.statusCode < 300) {
167 | resolve(parsed);
168 | } else {
169 | const error = parsed.error || {};
170 | reject(new Error(`Airtable API error (${response.statusCode}): ${error.message || error.type || 'Unknown error'}`));
171 | }
172 | } catch (e) {
173 | reject(new Error(`Failed to parse Airtable response: ${e.message}`));
174 | }
175 | });
176 | });
177 |
178 | req.on('error', reject);
179 |
180 | if (body) {
181 | req.write(JSON.stringify(body));
182 | }
183 |
184 | req.end();
185 | });
186 | }
187 |
188 | // Tools schema
189 | const TOOLS_SCHEMA = [
190 | {
191 | name: 'list_tables',
192 | description: 'List all tables in the Airtable base',
193 | inputSchema: {
194 | type: 'object',
195 | properties: {
196 | include_schema: { type: 'boolean', description: 'Include field schema information', default: false }
197 | }
198 | }
199 | },
200 | {
201 | name: 'list_records',
202 | description: 'List records from a specific table',
203 | inputSchema: {
204 | type: 'object',
205 | properties: {
206 | table: { type: 'string', description: 'Table name or ID' },
207 | maxRecords: { type: 'number', description: 'Maximum number of records to return' },
208 | view: { type: 'string', description: 'View name or ID' },
209 | filterByFormula: { type: 'string', description: 'Airtable formula to filter records' }
210 | },
211 | required: ['table']
212 | }
213 | },
214 | {
215 | name: 'get_record',
216 | description: 'Get a single record by ID',
217 | inputSchema: {
218 | type: 'object',
219 | properties: {
220 | table: { type: 'string', description: 'Table name or ID' },
221 | recordId: { type: 'string', description: 'Record ID' }
222 | },
223 | required: ['table', 'recordId']
224 | }
225 | },
226 | {
227 | name: 'create_record',
228 | description: 'Create a new record in a table',
229 | inputSchema: {
230 | type: 'object',
231 | properties: {
232 | table: { type: 'string', description: 'Table name or ID' },
233 | fields: { type: 'object', description: 'Field values for the new record' }
234 | },
235 | required: ['table', 'fields']
236 | }
237 | },
238 | {
239 | name: 'update_record',
240 | description: 'Update an existing record',
241 | inputSchema: {
242 | type: 'object',
243 | properties: {
244 | table: { type: 'string', description: 'Table name or ID' },
245 | recordId: { type: 'string', description: 'Record ID to update' },
246 | fields: { type: 'object', description: 'Fields to update' }
247 | },
248 | required: ['table', 'recordId', 'fields']
249 | }
250 | },
251 | {
252 | name: 'delete_record',
253 | description: 'Delete a record from a table',
254 | inputSchema: {
255 | type: 'object',
256 | properties: {
257 | table: { type: 'string', description: 'Table name or ID' },
258 | recordId: { type: 'string', description: 'Record ID to delete' }
259 | },
260 | required: ['table', 'recordId']
261 | }
262 | }
263 | ];
264 |
265 | // Enhanced AI-powered prompts for intelligent Airtable operations
266 | const PROMPTS_SCHEMA = [
267 | {
268 | name: 'analyze_data',
269 | description: 'Advanced AI data analysis with statistical insights, pattern recognition, and predictive modeling',
270 | arguments: [
271 | {
272 | name: 'table',
273 | description: 'Table name or ID to analyze',
274 | required: true
275 | },
276 | {
277 | name: 'analysis_type',
278 | description: 'Type of analysis (trends, statistical, patterns, predictive, anomaly_detection, correlation_matrix)',
279 | required: false
280 | },
281 | {
282 | name: 'field_focus',
283 | description: 'Specific fields to focus the analysis on',
284 | required: false
285 | },
286 | {
287 | name: 'time_dimension',
288 | description: 'Time field for temporal analysis',
289 | required: false
290 | },
291 | {
292 | name: 'confidence_level',
293 | description: 'Statistical confidence level (0.90, 0.95, 0.99)',
294 | required: false
295 | }
296 | ]
297 | },
298 | {
299 | name: 'create_report',
300 | description: 'Generate intelligent reports with AI-powered insights, visualizations, and actionable recommendations',
301 | arguments: [
302 | {
303 | name: 'table',
304 | description: 'Table name or ID for the report',
305 | required: true
306 | },
307 | {
308 | name: 'report_type',
309 | description: 'Type of report (executive_summary, operational_dashboard, analytical_deep_dive, performance_metrics, predictive_forecast)',
310 | required: false
311 | },
312 | {
313 | name: 'time_period',
314 | description: 'Time period for analysis (last_7_days, last_30_days, last_quarter, year_to_date, custom)',
315 | required: false
316 | },
317 | {
318 | name: 'stakeholder_level',
319 | description: 'Target audience (executive, manager, analyst, operational)',
320 | required: false
321 | },
322 | {
323 | name: 'include_recommendations',
324 | description: 'Include AI-generated actionable recommendations (true/false)',
325 | required: false
326 | }
327 | ]
328 | },
329 | {
330 | name: 'data_insights',
331 | description: 'Discover hidden patterns, correlations, and business insights using advanced AI algorithms',
332 | arguments: [
333 | {
334 | name: 'tables',
335 | description: 'Comma-separated list of table names to analyze',
336 | required: true
337 | },
338 | {
339 | name: 'insight_type',
340 | description: 'Type of insights (correlations, outliers, trends, predictions, segmentation, attribution, churn_analysis)',
341 | required: false
342 | },
343 | {
344 | name: 'business_context',
345 | description: 'Business domain context (sales, marketing, operations, finance, customer_success)',
346 | required: false
347 | },
348 | {
349 | name: 'insight_depth',
350 | description: 'Analysis depth (surface, moderate, deep, comprehensive)',
351 | required: false
352 | }
353 | ]
354 | },
355 | {
356 | name: 'optimize_workflow',
357 | description: 'AI-powered workflow optimization with automation recommendations and efficiency improvements',
358 | arguments: [
359 | {
360 | name: 'base_overview',
361 | description: 'Overview of the base structure and current workflows',
362 | required: false
363 | },
364 | {
365 | name: 'optimization_focus',
366 | description: 'Focus area (automation, data_quality, collaboration, performance, integration, user_experience)',
367 | required: false
368 | },
369 | {
370 | name: 'current_pain_points',
371 | description: 'Known issues or bottlenecks in current workflow',
372 | required: false
373 | },
374 | {
375 | name: 'team_size',
376 | description: 'Number of users working with this base',
377 | required: false
378 | }
379 | ]
380 | },
381 | {
382 | name: 'smart_schema_design',
383 | description: 'AI-assisted database schema optimization and field relationship analysis',
384 | arguments: [
385 | {
386 | name: 'use_case',
387 | description: 'Primary use case (crm, project_management, inventory, content_management, hr, finance)',
388 | required: true
389 | },
390 | {
391 | name: 'data_volume',
392 | description: 'Expected data volume (small, medium, large, enterprise)',
393 | required: false
394 | },
395 | {
396 | name: 'integration_needs',
397 | description: 'External systems to integrate with',
398 | required: false
399 | },
400 | {
401 | name: 'compliance_requirements',
402 | description: 'Data compliance needs (gdpr, hipaa, sox, none)',
403 | required: false
404 | }
405 | ]
406 | },
407 | {
408 | name: 'data_quality_audit',
409 | description: 'Comprehensive AI-powered data quality assessment with cleansing recommendations',
410 | arguments: [
411 | {
412 | name: 'tables',
413 | description: 'Tables to audit (comma-separated or "all")',
414 | required: true
415 | },
416 | {
417 | name: 'quality_dimensions',
418 | description: 'Quality aspects to check (completeness, accuracy, consistency, validity, uniqueness, timeliness)',
419 | required: false
420 | },
421 | {
422 | name: 'severity_threshold',
423 | description: 'Minimum severity level to report (low, medium, high, critical)',
424 | required: false
425 | },
426 | {
427 | name: 'auto_fix_suggestions',
428 | description: 'Include automated fix suggestions (true/false)',
429 | required: false
430 | }
431 | ]
432 | },
433 | {
434 | name: 'predictive_analytics',
435 | description: 'Advanced predictive modeling and forecasting using historical Airtable data',
436 | arguments: [
437 | {
438 | name: 'table',
439 | description: 'Table containing historical data',
440 | required: true
441 | },
442 | {
443 | name: 'target_field',
444 | description: 'Field to predict or forecast',
445 | required: true
446 | },
447 | {
448 | name: 'prediction_horizon',
449 | description: 'Forecast period (next_week, next_month, next_quarter, next_year)',
450 | required: false
451 | },
452 | {
453 | name: 'model_type',
454 | description: 'Prediction model (trend_analysis, seasonal_forecast, regression, classification, time_series)',
455 | required: false
456 | },
457 | {
458 | name: 'feature_fields',
459 | description: 'Fields to use as predictive features',
460 | required: false
461 | }
462 | ]
463 | },
464 | {
465 | name: 'natural_language_query',
466 | description: 'Process natural language questions about your data and provide intelligent answers',
467 | arguments: [
468 | {
469 | name: 'question',
470 | description: 'Natural language question about your data',
471 | required: true
472 | },
473 | {
474 | name: 'context_tables',
475 | description: 'Tables that might contain relevant data',
476 | required: false
477 | },
478 | {
479 | name: 'response_format',
480 | description: 'Desired response format (narrative, data_summary, visualization_suggestion, action_items)',
481 | required: false
482 | },
483 | {
484 | name: 'include_confidence',
485 | description: 'Include confidence scores for answers (true/false)',
486 | required: false
487 | }
488 | ]
489 | },
490 | {
491 | name: 'smart_data_transformation',
492 | description: 'AI-assisted data transformation, cleaning, and enrichment with intelligent suggestions',
493 | arguments: [
494 | {
495 | name: 'source_table',
496 | description: 'Source table for transformation',
497 | required: true
498 | },
499 | {
500 | name: 'transformation_goal',
501 | description: 'Goal (normalize, standardize, enrich, cleanse, aggregate, pivot)',
502 | required: true
503 | },
504 | {
505 | name: 'target_format',
506 | description: 'Desired output format or structure',
507 | required: false
508 | },
509 | {
510 | name: 'quality_rules',
511 | description: 'Data quality rules to apply during transformation',
512 | required: false
513 | },
514 | {
515 | name: 'preserve_history',
516 | description: 'Maintain audit trail of changes (true/false)',
517 | required: false
518 | }
519 | ]
520 | },
521 | {
522 | name: 'automation_recommendations',
523 | description: 'Generate intelligent automation suggestions based on workflow patterns and data analysis',
524 | arguments: [
525 | {
526 | name: 'workflow_description',
527 | description: 'Description of current manual processes',
528 | required: false
529 | },
530 | {
531 | name: 'automation_scope',
532 | description: 'Scope (single_table, multi_table, cross_base, external_integration)',
533 | required: false
534 | },
535 | {
536 | name: 'frequency_patterns',
537 | description: 'How often tasks are performed',
538 | required: false
539 | },
540 | {
541 | name: 'complexity_tolerance',
542 | description: 'Acceptable automation complexity (simple, moderate, advanced)',
543 | required: false
544 | },
545 | {
546 | name: 'integration_capabilities',
547 | description: 'Available integration tools (zapier, make, custom_api, native_automations)',
548 | required: false
549 | }
550 | ]
551 | }
552 | ];
553 |
554 | // Roots configuration for filesystem access
555 | const ROOTS_CONFIG = [
556 | {
557 | uri: 'file:///airtable-exports',
558 | name: 'Airtable Exports'
559 | },
560 | {
561 | uri: 'file:///airtable-attachments',
562 | name: 'Airtable Attachments'
563 | }
564 | ];
565 |
566 | // Logging configuration (currentLogLevel is already declared above)
567 |
568 | // HTTP server
569 | const server = http.createServer(async (req, res) => {
570 | // Security headers
571 | res.setHeader('X-Content-Type-Options', 'nosniff');
572 | res.setHeader('X-Frame-Options', 'DENY');
573 | res.setHeader('X-XSS-Protection', '1; mode=block');
574 | res.setHeader('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGINS || '*');
575 | res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
576 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
577 |
578 | // Handle preflight request
579 | if (req.method === 'OPTIONS') {
580 | res.writeHead(200);
581 | res.end();
582 | return;
583 | }
584 |
585 | const parsedUrl = url.parse(req.url, true);
586 | const pathname = parsedUrl.pathname;
587 |
588 | // Health check endpoint
589 | if (pathname === '/health' && req.method === 'GET') {
590 | res.writeHead(200, { 'Content-Type': 'application/json' });
591 | res.end(JSON.stringify({
592 | status: 'healthy',
593 | version: '3.0.0',
594 | timestamp: new Date().toISOString(),
595 | uptime: process.uptime()
596 | }));
597 | return;
598 | }
599 |
600 | // OAuth2 authorization endpoint
601 | if (pathname === '/oauth/authorize' && req.method === 'GET') {
602 | const params = parsedUrl.query;
603 | const clientId = params.client_id;
604 | const redirectUri = params.redirect_uri;
605 | const state = params.state;
606 | const codeChallenge = params.code_challenge;
607 | const codeChallengeMethod = params.code_challenge_method;
608 |
609 | // Validate inputs to prevent XSS
610 | if (!clientId || !redirectUri) {
611 | res.writeHead(400, { 'Content-Type': 'application/json' });
612 | res.end(JSON.stringify({ error: 'invalid_request', error_description: 'Missing required parameters' }));
613 | return;
614 | }
615 |
616 | // Validate redirect URI
617 | if (!validateUrl(redirectUri)) {
618 | res.writeHead(400, { 'Content-Type': 'application/json' });
619 | res.end(JSON.stringify({ error: 'invalid_request', error_description: 'Invalid redirect URI' }));
620 | return;
621 | }
622 |
623 | // Create safe copies of all variables for JavaScript use
624 | const safeRedirectUri = redirectUri.slice(0, 2000); // Limit length
625 | const safeState = (state || '').slice(0, 200); // Limit length
626 | const safeClientId = clientId.slice(0, 200); // Limit length
627 |
628 | // Sanitize for HTML display only
629 | const displayClientId = escapeHtml(safeClientId);
630 | const displayRedirectUri = escapeHtml(safeRedirectUri);
631 |
632 | // Generate authorization code
633 | const authCode = crypto.randomBytes(32).toString('hex');
634 |
635 | // In a real implementation, store the auth code with expiration
636 | // and associate it with the client and PKCE challenge
637 |
638 | res.writeHead(200, {
639 | 'Content-Type': 'text/html; charset=utf-8',
640 | 'Content-Security-Policy': "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none';",
641 | 'X-Content-Type-Options': 'nosniff',
642 | 'X-Frame-Options': 'DENY',
643 | 'X-XSS-Protection': '1; mode=block',
644 | 'Referrer-Policy': 'no-referrer',
645 | 'Cache-Control': 'no-store, no-cache, must-revalidate, private'
646 | });
647 |
648 | // Safely encode data for embedding in HTML attributes and JavaScript
649 | // This prevents XSS by encoding any potentially dangerous characters
650 | const safeJsonConfig = JSON.stringify({
651 | redirectUri: safeRedirectUri,
652 | code: authCode,
653 | state: safeState,
654 | clientId: displayClientId,
655 | displayRedirectUri: displayRedirectUri
656 | }).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026').replace(/'/g, '\\u0027').replace(/"/g, '\\u0022');
657 |
658 | // Build HTML with all dynamic content properly escaped
659 | // Using template literals but with pre-escaped content only
660 | const htmlContent = `<!DOCTYPE html>
661 | <html>
662 | <head>
663 | <meta charset="UTF-8">
664 | <title>OAuth2 Authorization</title>
665 | <meta name="viewport" content="width=device-width, initial-scale=1.0">
666 | <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline';">
667 | </head>
668 | <body>
669 | <h2>Airtable MCP Server - OAuth2 Authorization</h2>
670 | <p>Client ID: <span id="client-id"></span></p>
671 | <p>Redirect URI: <span id="redirect-uri"></span></p>
672 | <div style="margin: 20px 0;">
673 | <button onclick="authorize()" style="background: #18BFFF; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;">
674 | Authorize Application
675 | </button>
676 | <button onclick="deny()" style="background: #ff4444; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; margin-left: 10px;">
677 | Deny Access
678 | </button>
679 | </div>
680 | <script>
681 | // Parse safely encoded JSON config
682 | (function() {
683 | // Config is safely encoded to prevent XSS
684 | var config = ${safeJsonConfig};
685 |
686 | // Safely set text content (not innerHTML) to prevent XSS
687 | document.addEventListener('DOMContentLoaded', function() {
688 | document.getElementById('client-id').textContent = config.clientId;
689 | document.getElementById('redirect-uri').textContent = config.displayRedirectUri;
690 | });
691 |
692 | window.authorize = function() {
693 | try {
694 | var url = new URL(config.redirectUri);
695 | if (url.protocol !== 'http:' && url.protocol !== 'https:') {
696 | throw new Error('Invalid protocol');
697 | }
698 | var finalUrl = config.redirectUri + '?code=' + encodeURIComponent(config.code) + '&state=' + encodeURIComponent(config.state);
699 | window.location.href = finalUrl;
700 | } catch (e) {
701 | console.error('Authorization failed:', e);
702 | alert('Invalid redirect URL');
703 | }
704 | };
705 |
706 | window.deny = function() {
707 | try {
708 | var url = new URL(config.redirectUri);
709 | if (url.protocol !== 'http:' && url.protocol !== 'https:') {
710 | throw new Error('Invalid protocol');
711 | }
712 | var finalUrl = config.redirectUri + '?error=access_denied&state=' + encodeURIComponent(config.state);
713 | window.location.href = finalUrl;
714 | } catch (e) {
715 | console.error('Denial failed:', e);
716 | alert('Invalid redirect URL');
717 | }
718 | };
719 | })();
720 | </script>
721 | </body>
722 | </html>`;
723 |
724 | // Write response with explicit UTF-8 encoding
725 | res.end(htmlContent, 'utf8');
726 | return;
727 | }
728 |
729 | // OAuth2 token endpoint
730 | if (pathname === '/oauth/token' && req.method === 'POST') {
731 | let body = '';
732 | req.on('data', chunk => {
733 | body += chunk.toString();
734 | // Prevent DoS by limiting body size
735 | if (body.length > 10000) {
736 | res.writeHead(413, { 'Content-Type': 'application/json' });
737 | res.end(JSON.stringify({ error: 'payload_too_large', error_description: 'Request body too large' }));
738 | return;
739 | }
740 | });
741 |
742 | req.on('end', () => {
743 | try {
744 | const params = querystring.parse(body);
745 | const grantType = sanitizeInput(params.grant_type);
746 | const code = sanitizeInput(params.code);
747 | const codeVerifier = sanitizeInput(params.code_verifier);
748 | const clientId = sanitizeInput(params.client_id);
749 |
750 | // Validate required parameters
751 | if (!grantType || !code || !clientId) {
752 | res.writeHead(400, { 'Content-Type': 'application/json' });
753 | res.end(JSON.stringify({
754 | error: 'invalid_request',
755 | error_description: 'Missing required parameters'
756 | }));
757 | return;
758 | }
759 |
760 | // In a real implementation, verify the authorization code and PKCE
761 | if (grantType === 'authorization_code' && code) {
762 | // Generate access token
763 | const accessToken = crypto.randomBytes(32).toString('hex');
764 | const refreshToken = crypto.randomBytes(32).toString('hex');
765 |
766 | res.writeHead(200, {
767 | 'Content-Type': 'application/json',
768 | 'Cache-Control': 'no-store',
769 | 'Pragma': 'no-cache'
770 | });
771 | res.end(JSON.stringify({
772 | access_token: accessToken,
773 | token_type: 'Bearer',
774 | expires_in: 3600,
775 | refresh_token: refreshToken,
776 | scope: 'data.records:read data.records:write schema.bases:read'
777 | }));
778 | } else {
779 | res.writeHead(400, { 'Content-Type': 'application/json' });
780 | res.end(JSON.stringify({
781 | error: 'invalid_grant',
782 | error_description: 'Invalid grant type or authorization code'
783 | }));
784 | }
785 | } catch (error) {
786 | log(LOG_LEVELS.WARN, 'OAuth token request parsing failed', { error: error.message });
787 | res.writeHead(400, { 'Content-Type': 'application/json' });
788 | res.end(JSON.stringify({
789 | error: 'invalid_request',
790 | error_description: 'Malformed request body'
791 | }));
792 | }
793 | });
794 | return;
795 | }
796 |
797 | // MCP endpoint
798 | if (pathname === '/mcp' && req.method === 'POST') {
799 | // Rate limiting
800 | const clientId = req.headers['x-client-id'] || req.connection.remoteAddress;
801 | if (!checkRateLimit(clientId)) {
802 | res.writeHead(429, { 'Content-Type': 'application/json' });
803 | res.end(JSON.stringify({
804 | jsonrpc: '2.0',
805 | error: {
806 | code: -32000,
807 | message: 'Rate limit exceeded. Maximum 60 requests per minute.'
808 | }
809 | }));
810 | return;
811 | }
812 |
813 | let body = '';
814 | req.on('data', chunk => body += chunk.toString());
815 |
816 | req.on('end', async () => {
817 | try {
818 | const request = JSON.parse(body);
819 |
820 | // Sanitize inputs
821 | if (request.params) {
822 | Object.keys(request.params).forEach(key => {
823 | request.params[key] = sanitizeInput(request.params[key]);
824 | });
825 | }
826 |
827 | log(LOG_LEVELS.DEBUG, 'MCP request received', {
828 | method: request.method,
829 | id: request.id
830 | });
831 |
832 | let response;
833 |
834 | switch (request.method) {
835 | case 'initialize':
836 | response = {
837 | jsonrpc: '2.0',
838 | id: request.id,
839 | result: {
840 | protocolVersion: '2024-11-05',
841 | capabilities: {
842 | tools: { listChanged: true },
843 | resources: { subscribe: true, listChanged: true },
844 | prompts: { listChanged: true },
845 | sampling: {},
846 | roots: { listChanged: true },
847 | logging: {}
848 | },
849 | serverInfo: {
850 | name: 'Airtable MCP Server - AI Agent Enhanced',
851 | version: '3.0.0',
852 | description: 'Advanced AI-powered MCP server with 10 intelligent prompt templates, predictive analytics, and enterprise automation capabilities'
853 | }
854 | }
855 | };
856 | log(LOG_LEVELS.INFO, 'Client initialized', { clientId: request.id });
857 | break;
858 |
859 | case 'tools/list':
860 | response = {
861 | jsonrpc: '2.0',
862 | id: request.id,
863 | result: {
864 | tools: TOOLS_SCHEMA
865 | }
866 | };
867 | break;
868 |
869 | case 'tools/call':
870 | response = await handleToolCall(request);
871 | break;
872 |
873 | case 'prompts/list':
874 | response = {
875 | jsonrpc: '2.0',
876 | id: request.id,
877 | result: {
878 | prompts: PROMPTS_SCHEMA
879 | }
880 | };
881 | break;
882 |
883 | case 'prompts/get':
884 | response = await handlePromptGet(request);
885 | break;
886 |
887 | case 'roots/list':
888 | response = {
889 | jsonrpc: '2.0',
890 | id: request.id,
891 | result: {
892 | roots: ROOTS_CONFIG
893 | }
894 | };
895 | break;
896 |
897 | case 'logging/setLevel':
898 | const level = request.params?.level;
899 | if (level && LOG_LEVELS[level.toUpperCase()] !== undefined) {
900 | currentLogLevel = LOG_LEVELS[level.toUpperCase()];
901 | log(LOG_LEVELS.INFO, 'Log level updated', { newLevel: level });
902 | }
903 | response = {
904 | jsonrpc: '2.0',
905 | id: request.id,
906 | result: {}
907 | };
908 | break;
909 |
910 | case 'sampling/createMessage':
911 | response = await handleSampling(request);
912 | break;
913 |
914 | default:
915 | log(LOG_LEVELS.WARN, 'Unknown method', { method: request.method });
916 | throw new Error(`Method "${request.method}" not found`);
917 | }
918 |
919 | res.writeHead(200, { 'Content-Type': 'application/json' });
920 | res.end(JSON.stringify(response));
921 |
922 | } catch (error) {
923 | log(LOG_LEVELS.ERROR, 'Request processing failed', { error: error.message });
924 |
925 | const errorResponse = {
926 | jsonrpc: '2.0',
927 | id: request?.id || null,
928 | error: {
929 | code: -32000,
930 | message: error.message || 'Internal server error'
931 | }
932 | };
933 |
934 | res.writeHead(200, { 'Content-Type': 'application/json' });
935 | res.end(JSON.stringify(errorResponse));
936 | }
937 | });
938 | return;
939 | }
940 |
941 | // Default 404
942 | res.writeHead(404, { 'Content-Type': 'application/json' });
943 | res.end(JSON.stringify({ error: 'Not Found' }));
944 | });
945 |
946 | // Tool handlers
947 | async function handleToolCall(request) {
948 | const toolName = request.params.name;
949 | const toolParams = request.params.arguments || {};
950 |
951 | try {
952 | let result;
953 | let responseText;
954 |
955 | switch (toolName) {
956 | case 'list_tables':
957 | const includeSchema = toolParams.include_schema || false;
958 | result = await callAirtableAPI(`meta/bases/${baseId}/tables`);
959 | const tables = result.tables || [];
960 |
961 | responseText = tables.length > 0
962 | ? `Found ${tables.length} table(s): ` +
963 | tables.map((table, i) =>
964 | `${table.name} (ID: ${table.id}, Fields: ${table.fields?.length || 0})`
965 | ).join(', ')
966 | : 'No tables found in this base.';
967 | break;
968 |
969 | case 'list_records':
970 | const { table, maxRecords, view, filterByFormula } = toolParams;
971 |
972 | const queryParams = {};
973 | if (maxRecords) queryParams.maxRecords = maxRecords;
974 | if (view) queryParams.view = view;
975 | if (filterByFormula) queryParams.filterByFormula = filterByFormula;
976 |
977 | result = await callAirtableAPI(table, 'GET', null, queryParams);
978 | const records = result.records || [];
979 |
980 | responseText = records.length > 0
981 | ? `Found ${records.length} record(s) in table "${table}"`
982 | : `No records found in table "${table}".`;
983 | break;
984 |
985 | case 'get_record':
986 | const { table: getTable, recordId } = toolParams;
987 | result = await callAirtableAPI(`${getTable}/${recordId}`);
988 | responseText = `Retrieved record ${recordId} from table "${getTable}"`;
989 | break;
990 |
991 | case 'create_record':
992 | const { table: createTable, fields } = toolParams;
993 | const body = { fields: fields };
994 | result = await callAirtableAPI(createTable, 'POST', body);
995 | responseText = `Successfully created record in table "${createTable}" with ID: ${result.id}`;
996 | break;
997 |
998 | case 'update_record':
999 | const { table: updateTable, recordId: updateRecordId, fields: updateFields } = toolParams;
1000 | const updateBody = { fields: updateFields };
1001 | result = await callAirtableAPI(`${updateTable}/${updateRecordId}`, 'PATCH', updateBody);
1002 | responseText = `Successfully updated record ${updateRecordId} in table "${updateTable}"`;
1003 | break;
1004 |
1005 | case 'delete_record':
1006 | const { table: deleteTable, recordId: deleteRecordId } = toolParams;
1007 | result = await callAirtableAPI(`${deleteTable}/${deleteRecordId}`, 'DELETE');
1008 | responseText = `Successfully deleted record ${deleteRecordId} from table "${deleteTable}"`;
1009 | break;
1010 |
1011 | default:
1012 | throw new Error(`Unknown tool: ${toolName}`);
1013 | }
1014 |
1015 | return {
1016 | jsonrpc: '2.0',
1017 | id: request.id,
1018 | result: {
1019 | content: [
1020 | {
1021 | type: 'text',
1022 | text: responseText
1023 | }
1024 | ]
1025 | }
1026 | };
1027 |
1028 | } catch (error) {
1029 | log(LOG_LEVELS.ERROR, `Tool ${toolName} failed`, { error: error.message });
1030 |
1031 | return {
1032 | jsonrpc: '2.0',
1033 | id: request.id,
1034 | result: {
1035 | content: [
1036 | {
1037 | type: 'text',
1038 | text: `Error executing ${toolName}: ${error.message}`
1039 | }
1040 | ]
1041 | }
1042 | };
1043 | }
1044 | }
1045 |
1046 | // Enhanced AI-powered prompt handlers
1047 | async function handlePromptGet(request) {
1048 | const promptName = request.params.name;
1049 | const promptArgs = request.params.arguments || {};
1050 |
1051 | try {
1052 | const prompt = PROMPTS_SCHEMA.find(p => p.name === promptName);
1053 | if (!prompt) {
1054 | throw new Error(`Prompt "${promptName}" not found`);
1055 | }
1056 |
1057 | let messages = [];
1058 |
1059 | switch (promptName) {
1060 | case 'analyze_data':
1061 | const { table, analysis_type = 'statistical', field_focus, time_dimension, confidence_level = '0.95' } = promptArgs;
1062 | messages = [
1063 | {
1064 | role: 'user',
1065 | content: {
1066 | type: 'text',
1067 | text: `🤖 ADVANCED DATA ANALYSIS REQUEST
1068 |
1069 | **Table**: ${table}
1070 | **Analysis Type**: ${analysis_type}
1071 | **Confidence Level**: ${confidence_level}
1072 | ${field_focus ? `**Focus Fields**: ${field_focus}` : ''}
1073 | ${time_dimension ? `**Time Dimension**: ${time_dimension}` : ''}
1074 |
1075 | **Instructions**:
1076 | 1. First, examine the table schema and structure using list_tables with include_schema=true
1077 | 2. Retrieve representative sample data using list_records with appropriate filters
1078 | 3. Perform ${analysis_type} analysis with statistical rigor
1079 | 4. Generate insights with confidence intervals and significance testing
1080 | 5. Provide actionable recommendations based on findings
1081 |
1082 | **Expected Deliverables**:
1083 | - Statistical summary with key metrics
1084 | - Pattern identification and trend analysis
1085 | - Anomaly detection if applicable
1086 | - Predictive insights where relevant
1087 | - Visualization recommendations
1088 | - Business impact assessment
1089 |
1090 | Please use the available Airtable tools to gather data and provide comprehensive ${analysis_type} analysis.`
1091 | }
1092 | }
1093 | ];
1094 | break;
1095 |
1096 | case 'create_report':
1097 | const { table: reportTable, report_type = 'executive_summary', time_period = 'last_30_days', stakeholder_level = 'manager', include_recommendations = 'true' } = promptArgs;
1098 | messages = [
1099 | {
1100 | role: 'user',
1101 | content: {
1102 | type: 'text',
1103 | text: `📊 INTELLIGENT REPORT GENERATION
1104 |
1105 | **Target Table**: ${reportTable}
1106 | **Report Type**: ${report_type}
1107 | **Time Period**: ${time_period}
1108 | **Stakeholder Level**: ${stakeholder_level}
1109 | **Include Recommendations**: ${include_recommendations}
1110 |
1111 | **Report Generation Process**:
1112 | 1. Analyze table structure and data types
1113 | 2. Extract relevant data for specified time period
1114 | 3. Calculate key performance indicators
1115 | 4. Identify trends and patterns
1116 | 5. Generate visualizations suggestions
1117 | 6. Create ${stakeholder_level}-appropriate narrative
1118 |
1119 | **Report Sections**:
1120 | - Executive Summary (key findings)
1121 | - Data Overview and Quality Assessment
1122 | - Trend Analysis and Patterns
1123 | - Performance Metrics and KPIs
1124 | - Risk Assessment and Opportunities
1125 | ${include_recommendations === 'true' ? '- AI-Generated Recommendations' : ''}
1126 | - Next Steps and Action Items
1127 |
1128 | Please gather the necessary data and create a comprehensive ${report_type} tailored for ${stakeholder_level} level stakeholders.`
1129 | }
1130 | }
1131 | ];
1132 | break;
1133 |
1134 | case 'data_insights':
1135 | const { tables, insight_type = 'correlations', business_context = 'general', insight_depth = 'moderate' } = promptArgs;
1136 | messages = [
1137 | {
1138 | role: 'user',
1139 | content: {
1140 | type: 'text',
1141 | text: `🔍 ADVANCED DATA INSIGHTS DISCOVERY
1142 |
1143 | **Target Tables**: ${tables}
1144 | **Insight Type**: ${insight_type}
1145 | **Business Context**: ${business_context}
1146 | **Analysis Depth**: ${insight_depth}
1147 |
1148 | **Discovery Framework**:
1149 | 1. Multi-table schema analysis and relationship mapping
1150 | 2. Cross-table data correlation analysis
1151 | 3. Pattern recognition using ${business_context} domain knowledge
1152 | 4. Statistical significance testing
1153 | 5. Business impact quantification
1154 |
1155 | **Insight Categories**:
1156 | - ${insight_type} analysis with statistical validation
1157 | - Hidden patterns and unexpected relationships
1158 | - Segmentation opportunities
1159 | - Predictive indicators
1160 | - Data quality insights
1161 | - Business optimization opportunities
1162 |
1163 | **${business_context.toUpperCase()} CONTEXT ANALYSIS**:
1164 | ${business_context === 'sales' ? '- Revenue drivers and conversion patterns\n- Customer lifetime value indicators\n- Sales cycle optimization opportunities' : ''}
1165 | ${business_context === 'marketing' ? '- Campaign effectiveness and attribution\n- Customer segmentation insights\n- Channel performance analysis' : ''}
1166 | ${business_context === 'operations' ? '- Process efficiency metrics\n- Resource utilization patterns\n- Bottleneck identification' : ''}
1167 |
1168 | Please conduct ${insight_depth} analysis across the specified tables and provide actionable business insights.`
1169 | }
1170 | }
1171 | ];
1172 | break;
1173 |
1174 | case 'optimize_workflow':
1175 | const { base_overview, optimization_focus = 'automation', current_pain_points, team_size } = promptArgs;
1176 | messages = [
1177 | {
1178 | role: 'user',
1179 | content: {
1180 | type: 'text',
1181 | text: `⚡ AI-POWERED WORKFLOW OPTIMIZATION
1182 |
1183 | **Optimization Focus**: ${optimization_focus}
1184 | **Team Size**: ${team_size || 'Not specified'}
1185 | ${base_overview ? `**Base Overview**: ${base_overview}` : ''}
1186 | ${current_pain_points ? `**Current Pain Points**: ${current_pain_points}` : ''}
1187 |
1188 | **Optimization Analysis**:
1189 | 1. Workflow pattern analysis and bottleneck identification
1190 | 2. Automation opportunity assessment
1191 | 3. User experience and efficiency evaluation
1192 | 4. Integration and scaling considerations
1193 | 5. ROI analysis for proposed improvements
1194 |
1195 | **${optimization_focus.toUpperCase()} OPTIMIZATION**:
1196 | ${optimization_focus === 'automation' ? '- Identify repetitive manual tasks\n- Suggest automation workflows\n- Estimate time savings and ROI' : ''}
1197 | ${optimization_focus === 'data_quality' ? '- Data validation and cleansing rules\n- Consistency and accuracy improvements\n- Quality monitoring systems' : ''}
1198 | ${optimization_focus === 'collaboration' ? '- Team workflow improvements\n- Permission and access optimization\n- Communication enhancement strategies' : ''}
1199 |
1200 | **Deliverables**:
1201 | - Workflow efficiency assessment
1202 | - Prioritized improvement recommendations
1203 | - Implementation roadmap with timelines
1204 | - Cost-benefit analysis
1205 | - Change management considerations
1206 |
1207 | Please analyze the current setup and provide comprehensive ${optimization_focus} optimization recommendations.`
1208 | }
1209 | }
1210 | ];
1211 | break;
1212 |
1213 | case 'smart_schema_design':
1214 | const { use_case, data_volume = 'medium', integration_needs, compliance_requirements = 'none' } = promptArgs;
1215 | messages = [
1216 | {
1217 | role: 'user',
1218 | content: {
1219 | type: 'text',
1220 | text: `🏗️ AI-ASSISTED SCHEMA OPTIMIZATION
1221 |
1222 | **Use Case**: ${use_case}
1223 | **Data Volume**: ${data_volume}
1224 | **Compliance**: ${compliance_requirements}
1225 | ${integration_needs ? `**Integrations**: ${integration_needs}` : ''}
1226 |
1227 | **Schema Design Analysis**:
1228 | 1. Current schema evaluation for ${use_case} best practices
1229 | 2. Field type and relationship optimization
1230 | 3. Performance and scalability assessment
1231 | 4. Compliance requirement implementation
1232 | 5. Integration compatibility review
1233 |
1234 | **${use_case.toUpperCase()} OPTIMIZATION**:
1235 | ${use_case === 'crm' ? '- Customer lifecycle tracking\n- Sales pipeline optimization\n- Contact relationship mapping' : ''}
1236 | ${use_case === 'project_management' ? '- Task dependency modeling\n- Resource allocation tracking\n- Timeline and milestone management' : ''}
1237 | ${use_case === 'inventory' ? '- Stock level monitoring\n- Supplier relationship tracking\n- Cost and pricing optimization' : ''}
1238 |
1239 | **Recommendations**:
1240 | - Optimal field types and relationships
1241 | - Indexing and performance suggestions
1242 | - Data validation and integrity rules
1243 | - Automation and workflow triggers
1244 | - Scaling and maintenance considerations
1245 |
1246 | Please analyze the current schema and provide ${use_case}-optimized recommendations.`
1247 | }
1248 | }
1249 | ];
1250 | break;
1251 |
1252 | case 'data_quality_audit':
1253 | const { tables: auditTables, quality_dimensions = 'completeness,accuracy,consistency', severity_threshold = 'medium', auto_fix_suggestions = 'true' } = promptArgs;
1254 | messages = [
1255 | {
1256 | role: 'user',
1257 | content: {
1258 | type: 'text',
1259 | text: `🔍 COMPREHENSIVE DATA QUALITY AUDIT
1260 |
1261 | **Tables to Audit**: ${auditTables}
1262 | **Quality Dimensions**: ${quality_dimensions}
1263 | **Severity Threshold**: ${severity_threshold}
1264 | **Auto-Fix Suggestions**: ${auto_fix_suggestions}
1265 |
1266 | **Audit Framework**:
1267 | 1. Data completeness analysis (missing values, empty fields)
1268 | 2. Accuracy assessment (format validation, range checks)
1269 | 3. Consistency evaluation (cross-field validation, duplicates)
1270 | 4. Validity verification (data type compliance, constraints)
1271 | 5. Uniqueness analysis (duplicate detection, key integrity)
1272 | 6. Timeliness review (data freshness, update patterns)
1273 |
1274 | **Quality Assessment Process**:
1275 | - Statistical analysis of data distribution
1276 | - Pattern recognition for anomalies
1277 | - Cross-table consistency validation
1278 | - Historical trend analysis
1279 | - Business rule compliance checking
1280 |
1281 | **Deliverables**:
1282 | - Quality score by dimension and table
1283 | - Detailed issue identification and classification
1284 | - Impact assessment and prioritization
1285 | ${auto_fix_suggestions === 'true' ? '- Automated fix suggestions and scripts' : ''}
1286 | - Data governance recommendations
1287 | - Monitoring and maintenance strategies
1288 |
1289 | Please conduct a thorough data quality audit focusing on ${quality_dimensions} dimensions.`
1290 | }
1291 | }
1292 | ];
1293 | break;
1294 |
1295 | case 'predictive_analytics':
1296 | const { table: predTable, target_field, prediction_horizon = 'next_month', model_type = 'trend_analysis', feature_fields } = promptArgs;
1297 | messages = [
1298 | {
1299 | role: 'user',
1300 | content: {
1301 | type: 'text',
1302 | text: `🔮 ADVANCED PREDICTIVE ANALYTICS
1303 |
1304 | **Source Table**: ${predTable}
1305 | **Target Field**: ${target_field}
1306 | **Prediction Horizon**: ${prediction_horizon}
1307 | **Model Type**: ${model_type}
1308 | ${feature_fields ? `**Feature Fields**: ${feature_fields}` : ''}
1309 |
1310 | **Predictive Modeling Process**:
1311 | 1. Historical data analysis and trend identification
1312 | 2. Feature engineering and variable selection
1313 | 3. Model development using ${model_type} approach
1314 | 4. Validation and accuracy assessment
1315 | 5. Forecast generation for ${prediction_horizon}
1316 | 6. Confidence intervals and uncertainty quantification
1317 |
1318 | **${model_type.toUpperCase()} ANALYSIS**:
1319 | ${model_type === 'time_series' ? '- Seasonal pattern detection\n- Trend decomposition\n- Cyclical behavior analysis' : ''}
1320 | ${model_type === 'regression' ? '- Variable relationship modeling\n- Predictive factor identification\n- Statistical significance testing' : ''}
1321 | ${model_type === 'classification' ? '- Category prediction modeling\n- Feature importance analysis\n- Classification accuracy metrics' : ''}
1322 |
1323 | **Outputs**:
1324 | - Historical pattern analysis
1325 | - Predictive model performance metrics
1326 | - Forecast values with confidence intervals
1327 | - Key influencing factors identification
1328 | - Model limitations and assumptions
1329 | - Actionable insights and recommendations
1330 |
1331 | Please develop a ${model_type} model to predict ${target_field} over ${prediction_horizon}.`
1332 | }
1333 | }
1334 | ];
1335 | break;
1336 |
1337 | case 'natural_language_query':
1338 | const { question, context_tables, response_format = 'narrative', include_confidence = 'true' } = promptArgs;
1339 | messages = [
1340 | {
1341 | role: 'user',
1342 | content: {
1343 | type: 'text',
1344 | text: `🗣️ NATURAL LANGUAGE DATA QUERY
1345 |
1346 | **Question**: "${question}"
1347 | ${context_tables ? `**Context Tables**: ${context_tables}` : ''}
1348 | **Response Format**: ${response_format}
1349 | **Include Confidence**: ${include_confidence}
1350 |
1351 | **Query Processing Framework**:
1352 | 1. Question analysis and intent recognition
1353 | 2. Relevant table and field identification
1354 | 3. Data retrieval strategy formulation
1355 | 4. Analysis execution and result compilation
1356 | 5. Natural language response generation
1357 |
1358 | **Analysis Approach**:
1359 | - Semantic understanding of the question
1360 | - Automatic table and field mapping
1361 | - Intelligent data filtering and aggregation
1362 | - Statistical analysis where appropriate
1363 | - Context-aware interpretation
1364 |
1365 | **Response Requirements**:
1366 | ${response_format === 'narrative' ? '- Conversational, easy-to-understand explanation\n- Supporting data and evidence\n- Contextual insights and implications' : ''}
1367 | ${response_format === 'data_summary' ? '- Structured data summary\n- Key metrics and statistics\n- Trend identification' : ''}
1368 | ${response_format === 'visualization_suggestion' ? '- Chart and graph recommendations\n- Data visualization best practices\n- Tool-specific guidance' : ''}
1369 | ${include_confidence === 'true' ? '\n- Confidence scores for answers\n- Data quality indicators\n- Uncertainty acknowledgment' : ''}
1370 |
1371 | Please analyze the available data and provide a comprehensive answer to: "${question}"`
1372 | }
1373 | }
1374 | ];
1375 | break;
1376 |
1377 | case 'smart_data_transformation':
1378 | const { source_table, transformation_goal, target_format, quality_rules, preserve_history = 'true' } = promptArgs;
1379 | messages = [
1380 | {
1381 | role: 'user',
1382 | content: {
1383 | type: 'text',
1384 | text: `🔄 INTELLIGENT DATA TRANSFORMATION
1385 |
1386 | **Source Table**: ${source_table}
1387 | **Transformation Goal**: ${transformation_goal}
1388 | ${target_format ? `**Target Format**: ${target_format}` : ''}
1389 | ${quality_rules ? `**Quality Rules**: ${quality_rules}` : ''}
1390 | **Preserve History**: ${preserve_history}
1391 |
1392 | **Transformation Framework**:
1393 | 1. Source data analysis and quality assessment
1394 | 2. Transformation strategy development
1395 | 3. Data mapping and conversion rules
1396 | 4. Quality validation and error handling
1397 | 5. Output optimization and validation
1398 |
1399 | **${transformation_goal.toUpperCase()} PROCESS**:
1400 | ${transformation_goal === 'normalize' ? '- Database normalization principles\n- Redundancy elimination\n- Relationship optimization' : ''}
1401 | ${transformation_goal === 'standardize' ? '- Format standardization\n- Value normalization\n- Consistency enforcement' : ''}
1402 | ${transformation_goal === 'enrich' ? '- Data augmentation strategies\n- External data integration\n- Value-added field creation' : ''}
1403 | ${transformation_goal === 'cleanse' ? '- Data validation and correction\n- Duplicate removal\n- Missing value handling' : ''}
1404 |
1405 | **Deliverables**:
1406 | - Transformation execution plan
1407 | - Data mapping specifications
1408 | - Quality validation results
1409 | - Performance optimization recommendations
1410 | ${preserve_history === 'true' ? '- Change audit trail and versioning' : ''}
1411 | - Post-transformation validation
1412 |
1413 | Please analyze the source data and execute ${transformation_goal} transformation with intelligent optimization.`
1414 | }
1415 | }
1416 | ];
1417 | break;
1418 |
1419 | case 'automation_recommendations':
1420 | const { workflow_description, automation_scope = 'single_table', frequency_patterns, complexity_tolerance = 'moderate', integration_capabilities } = promptArgs;
1421 | messages = [
1422 | {
1423 | role: 'user',
1424 | content: {
1425 | type: 'text',
1426 | text: `🤖 INTELLIGENT AUTOMATION RECOMMENDATIONS
1427 |
1428 | **Automation Scope**: ${automation_scope}
1429 | **Complexity Tolerance**: ${complexity_tolerance}
1430 | ${workflow_description ? `**Current Workflow**: ${workflow_description}` : ''}
1431 | ${frequency_patterns ? `**Frequency Patterns**: ${frequency_patterns}` : ''}
1432 | ${integration_capabilities ? `**Integration Tools**: ${integration_capabilities}` : ''}
1433 |
1434 | **Automation Analysis Framework**:
1435 | 1. Workflow pattern analysis and task identification
1436 | 2. Automation opportunity assessment and prioritization
1437 | 3. Technical feasibility and complexity evaluation
1438 | 4. ROI calculation and benefit quantification
1439 | 5. Implementation roadmap development
1440 |
1441 | **${automation_scope.toUpperCase()} AUTOMATION**:
1442 | ${automation_scope === 'single_table' ? '- Field auto-population rules\n- Data validation automation\n- Notification triggers' : ''}
1443 | ${automation_scope === 'multi_table' ? '- Cross-table data synchronization\n- Workflow orchestration\n- Complex business logic automation' : ''}
1444 | ${automation_scope === 'external_integration' ? '- API integration strategies\n- Data pipeline automation\n- Third-party tool connectivity' : ''}
1445 |
1446 | **Recommendations**:
1447 | - High-impact automation opportunities
1448 | - Implementation complexity assessment
1449 | - Cost-benefit analysis with ROI projections
1450 | - Technical requirements and dependencies
1451 | - Risk assessment and mitigation strategies
1452 | - Success metrics and monitoring approach
1453 |
1454 | Please analyze the workflow patterns and provide ${complexity_tolerance}-level automation recommendations for ${automation_scope} scope.`
1455 | }
1456 | }
1457 | ];
1458 | break;
1459 |
1460 | default:
1461 | throw new Error(`Unsupported prompt: ${promptName}`);
1462 | }
1463 |
1464 | return {
1465 | jsonrpc: '2.0',
1466 | id: request.id,
1467 | result: {
1468 | description: prompt.description,
1469 | messages: messages
1470 | }
1471 | };
1472 |
1473 | } catch (error) {
1474 | log(LOG_LEVELS.ERROR, `Prompt ${promptName} failed`, { error: error.message });
1475 |
1476 | return {
1477 | jsonrpc: '2.0',
1478 | id: request.id,
1479 | error: {
1480 | code: -32000,
1481 | message: `Error getting prompt ${promptName}: ${error.message}`
1482 | }
1483 | };
1484 | }
1485 | }
1486 |
1487 | // Sampling handler
1488 | async function handleSampling(request) {
1489 | const { messages, modelPreferences } = request.params;
1490 |
1491 | try {
1492 | // Note: In a real implementation, this would integrate with an LLM API
1493 | // For now, we'll return a structured response indicating sampling capability
1494 |
1495 | log(LOG_LEVELS.INFO, 'Sampling request received', {
1496 | messageCount: messages?.length,
1497 | model: modelPreferences?.model
1498 | });
1499 |
1500 | return {
1501 | jsonrpc: '2.0',
1502 | id: request.id,
1503 | result: {
1504 | model: modelPreferences?.model || 'claude-3-sonnet',
1505 | role: 'assistant',
1506 | content: {
1507 | type: 'text',
1508 | text: 'Sampling capability is available. This MCP server can request AI assistance for complex data analysis and insights generation. In a full implementation, this would connect to your preferred LLM for intelligent Airtable operations.'
1509 | },
1510 | stopReason: 'end_turn'
1511 | }
1512 | };
1513 |
1514 | } catch (error) {
1515 | log(LOG_LEVELS.ERROR, 'Sampling failed', { error: error.message });
1516 |
1517 | return {
1518 | jsonrpc: '2.0',
1519 | id: request.id,
1520 | error: {
1521 | code: -32000,
1522 | message: `Sampling error: ${error.message}`
1523 | }
1524 | };
1525 | }
1526 | }
1527 |
1528 | // Server startup
1529 | const PORT = CONFIG.PORT;
1530 | const HOST = CONFIG.HOST;
1531 |
1532 | server.listen(PORT, HOST, () => {
1533 | log(LOG_LEVELS.INFO, `Airtable MCP Server started`, {
1534 | host: HOST,
1535 | port: PORT,
1536 | version: '2.1.0'
1537 | });
1538 |
1539 | console.log(`
1540 | ╔═══════════════════════════════════════════════════════════════╗
1541 | ║ Airtable MCP Server v2.1 ║
1542 | ║ Model Context Protocol Implementation ║
1543 | ╠═══════════════════════════════════════════════════════════════╣
1544 | ║ 🌐 MCP Endpoint: http://${HOST}:${PORT}/mcp ║
1545 | ║ 📊 Health Check: http://${HOST}:${PORT}/health ║
1546 | ║ 🔒 Security: Rate limiting, input validation ║
1547 | ║ 📋 Tools: ${TOOLS_SCHEMA.length} available operations ║
1548 | ╠═══════════════════════════════════════════════════════════════╣
1549 | ║ 🔗 Connected to Airtable Base: ${baseId.slice(0, 8)}... ║
1550 | ║ 🚀 Ready for MCP client connections ║
1551 | ╚═══════════════════════════════════════════════════════════════╝
1552 | `);
1553 | });
1554 |
1555 | // Graceful shutdown
1556 | function gracefulShutdown(signal) {
1557 | log(LOG_LEVELS.INFO, 'Graceful shutdown initiated', { signal });
1558 |
1559 | server.close(() => {
1560 | log(LOG_LEVELS.INFO, 'Server stopped');
1561 | process.exit(0);
1562 | });
1563 |
1564 | setTimeout(() => {
1565 | log(LOG_LEVELS.ERROR, 'Force shutdown - server did not close in time');
1566 | process.exit(1);
1567 | }, 10000);
1568 | }
1569 |
1570 | process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
1571 | process.on('SIGINT', () => gracefulShutdown('SIGINT'));
1572 |
1573 | process.on('uncaughtException', (error) => {
1574 | log(LOG_LEVELS.ERROR, 'Uncaught exception', { error: error.message });
1575 | gracefulShutdown('uncaughtException');
1576 | });
1577 |
1578 | process.on('unhandledRejection', (reason) => {
1579 | log(LOG_LEVELS.ERROR, 'Unhandled promise rejection', { reason: reason?.toString() });
1580 | gracefulShutdown('unhandledRejection');
1581 | });
```
--------------------------------------------------------------------------------
/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 | });
```