This is page 3 of 5. Use http://codebase.md/rashidazarang/airtable-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .eslintrc.js ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── custom.md │ │ └── feature_request.md │ └── pull_request_template.md ├── .gitignore ├── .nvmrc ├── .prettierrc ├── bin │ ├── airtable-crud-cli.js │ └── airtable-mcp.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── docker │ ├── Dockerfile │ └── Dockerfile.node ├── docs │ ├── guides │ │ ├── CLAUDE_INTEGRATION.md │ │ ├── ENHANCED_FEATURES.md │ │ ├── INSTALLATION.md │ │ └── QUICK_START.md │ └── releases │ ├── RELEASE_NOTES_v1.2.2.md │ ├── RELEASE_NOTES_v1.2.4.md │ ├── RELEASE_NOTES_v1.4.0.md │ ├── RELEASE_NOTES_v1.5.0.md │ └── RELEASE_NOTES_v1.6.0.md ├── examples │ ├── airtable-crud-example.js │ ├── building-mcp.md │ ├── claude_config.json │ ├── claude_simple_config.json │ ├── env-demo.js │ ├── example_usage.md │ ├── example-tasks-update.json │ ├── example-tasks.json │ ├── python_debug_patch.txt │ ├── sample-transform.js │ ├── typescript │ │ ├── advanced-ai-prompts.ts │ │ ├── basic-usage.ts │ │ └── claude-desktop-config.json │ └── windsurf_mcp_config.json ├── index.js ├── ISSUE_RESPONSES.md ├── jest.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── PROJECT_STRUCTURE.md ├── README.md ├── RELEASE_SUMMARY_v3.2.x.md ├── RELEASE_v3.2.1.md ├── RELEASE_v3.2.3.md ├── RELEASE_v3.2.4.md ├── requirements.txt ├── SECURITY_NOTICE.md ├── smithery.yaml ├── src │ ├── index.js │ ├── javascript │ │ ├── airtable_simple_production.js │ │ └── airtable_simple.js │ ├── python │ │ ├── airtable_mcp │ │ │ ├── __init__.py │ │ │ └── src │ │ │ └── server.py │ │ ├── inspector_server.py │ │ ├── inspector.py │ │ ├── setup.py │ │ ├── simple_airtable_server.py │ │ └── test_client.py │ └── typescript │ ├── ai-prompts.d.ts │ ├── airtable-mcp-server.d.ts │ ├── airtable-mcp-server.ts │ ├── errors.ts │ ├── index.d.ts │ ├── prompt-templates.ts │ ├── test-suite.d.ts │ ├── test-suite.ts │ ├── tools-schemas.ts │ └── tools.d.ts ├── TESTING_REPORT.md ├── tests │ ├── test_all_features.sh │ ├── test_mcp_comprehensive.js │ ├── test_v1.5.0_final.sh │ └── test_v1.6.0_comprehensive.sh ├── tsconfig.json └── types └── typescript ├── airtable-mcp-server.d.ts ├── errors.d.ts ├── prompt-templates.d.ts ├── test-suite.d.ts └── tools-schemas.d.ts ``` # Files -------------------------------------------------------------------------------- /src/typescript/airtable-mcp-server.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Airtable MCP Server - TypeScript Implementation 5 | * Model Context Protocol server for Airtable integration with enterprise-grade type safety 6 | * 7 | * Features: 8 | * - Complete MCP 2024-11-05 protocol support with strict typing 9 | * - OAuth2 authentication with PKCE and type safety 10 | * - Enterprise security features with validated types 11 | * - Rate limiting and comprehensive input validation 12 | * - Production monitoring and health checks 13 | * - AI-powered analytics with strongly typed schemas 14 | * 15 | * Author: Rashid Azarang 16 | * License: MIT 17 | */ 18 | 19 | import * as http from 'http'; 20 | import * as https from 'https'; 21 | import * as fs from 'fs'; 22 | import * as path from 'path'; 23 | import { config } from 'dotenv'; 24 | 25 | // Type imports 26 | import type { 27 | MCPRequest, 28 | MCPResponse, 29 | MCPServerInfo, 30 | ServerConfig, 31 | AuthConfig, 32 | ToolSchema, 33 | PromptSchema, 34 | RootDirectory 35 | } from './index'; 36 | 37 | import type { 38 | AnalyzeDataPrompt, 39 | CreateReportPrompt, 40 | PredictiveAnalyticsPrompt, 41 | NaturalLanguageQueryPrompt 42 | } from './ai-prompts'; 43 | 44 | import type { 45 | ToolResponse, 46 | ListTablesInput, 47 | ListRecordsInput, 48 | CreateRecordInput, 49 | UpdateRecordInput, 50 | DeleteRecordInput 51 | } from './tools'; 52 | 53 | // Runtime imports 54 | import { AirtableError, ValidationError } from './errors'; 55 | import { COMPLETE_TOOL_SCHEMAS } from './tools-schemas'; 56 | import { AI_PROMPT_TEMPLATES } from './prompt-templates'; 57 | 58 | // Load environment variables 59 | const envPath = path.join(__dirname, '..', '.env'); 60 | if (fs.existsSync(envPath)) { 61 | config({ path: envPath }); 62 | } 63 | 64 | // Parse command line arguments with type safety 65 | const args: string[] = process.argv.slice(2); 66 | const tokenIndex: number = args.indexOf('--token'); 67 | const baseIndex: number = args.indexOf('--base'); 68 | 69 | const token: string | undefined = tokenIndex !== -1 ? args[tokenIndex + 1] : 70 | (process.env['AIRTABLE_TOKEN'] || process.env['AIRTABLE_API_TOKEN']); 71 | const baseId: string | undefined = baseIndex !== -1 ? args[baseIndex + 1] : 72 | (process.env['AIRTABLE_BASE_ID'] || process.env['AIRTABLE_BASE']); 73 | 74 | if (!token || !baseId) { 75 | console.error('Error: Missing Airtable credentials'); 76 | console.error('\nUsage options:'); 77 | console.error(' 1. Command line: node dist/airtable-mcp-server.js --token YOUR_TOKEN --base YOUR_BASE_ID'); 78 | console.error(' 2. Environment variables: AIRTABLE_TOKEN and AIRTABLE_BASE_ID'); 79 | console.error(' 3. .env file with AIRTABLE_TOKEN and AIRTABLE_BASE_ID'); 80 | process.exit(1); 81 | } 82 | 83 | // Configuration with strict typing 84 | const CONFIG: ServerConfig = { 85 | PORT: parseInt(process.env['PORT'] || '8010'), 86 | HOST: process.env['HOST'] || 'localhost', 87 | MAX_REQUESTS_PER_MINUTE: parseInt(process.env['MAX_REQUESTS_PER_MINUTE'] || '60'), 88 | LOG_LEVEL: (process.env['LOG_LEVEL'] as ServerConfig['LOG_LEVEL']) || 'INFO' 89 | }; 90 | 91 | const AUTH_CONFIG: AuthConfig = { 92 | AIRTABLE_TOKEN: token, 93 | AIRTABLE_BASE_ID: baseId 94 | }; 95 | 96 | // Enhanced logging with type safety 97 | enum LogLevel { 98 | ERROR = 0, 99 | WARN = 1, 100 | INFO = 2, 101 | DEBUG = 3, 102 | TRACE = 4 103 | } 104 | 105 | const LOG_LEVELS: Record<string, LogLevel> = { 106 | ERROR: LogLevel.ERROR, 107 | WARN: LogLevel.WARN, 108 | INFO: LogLevel.INFO, 109 | DEBUG: LogLevel.DEBUG, 110 | TRACE: LogLevel.TRACE 111 | }; 112 | 113 | let currentLogLevel: LogLevel = LOG_LEVELS[CONFIG.LOG_LEVEL] || LogLevel.INFO; 114 | 115 | interface LogMetadata { 116 | [key: string]: unknown; 117 | } 118 | 119 | function log(level: LogLevel, message: string, metadata: LogMetadata = {}): void { 120 | if (level <= currentLogLevel) { 121 | const timestamp = new Date().toISOString(); 122 | const levelName = Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === level) || 'UNKNOWN'; 123 | // Sanitize message to prevent format string attacks 124 | const safeMessage = String(message).replace(/%/g, '%%'); 125 | const output = `[${timestamp}] [${levelName}] ${safeMessage}`; 126 | 127 | if (Object.keys(metadata).length > 0) { 128 | // Use separate arguments to avoid format string injection 129 | console.log('%s %s', output, JSON.stringify(metadata)); 130 | } else { 131 | console.log('%s', output); 132 | } 133 | } 134 | } 135 | 136 | // Rate limiting with typed implementation 137 | interface RateLimitData { 138 | timestamps: number[]; 139 | } 140 | 141 | const rateLimiter = new Map<string, RateLimitData>(); 142 | 143 | function checkRateLimit(clientId: string): boolean { 144 | const now = Date.now(); 145 | const windowStart = now - 60000; // 1 minute window 146 | 147 | if (!rateLimiter.has(clientId)) { 148 | rateLimiter.set(clientId, { timestamps: [] }); 149 | } 150 | 151 | const data = rateLimiter.get(clientId)!; 152 | const recentRequests = data.timestamps.filter(time => time > windowStart); 153 | 154 | if (recentRequests.length >= CONFIG.MAX_REQUESTS_PER_MINUTE) { 155 | return false; 156 | } 157 | 158 | recentRequests.push(now); 159 | rateLimiter.set(clientId, { timestamps: recentRequests }); 160 | return true; 161 | } 162 | 163 | // Enhanced input validation with TypeScript (reserved for future use) 164 | // function sanitizeInput(input: unknown): unknown { 165 | // if (typeof input === 'string') { 166 | // return input.replace(/[<>]/g, '').trim().substring(0, 1000); 167 | // } 168 | // return input; 169 | // } 170 | 171 | // function escapeHtml(unsafe: unknown): string { 172 | // if (typeof unsafe !== 'string') { 173 | // return String(unsafe); 174 | // } 175 | // return unsafe 176 | // .replace(/&/g, "&") 177 | // .replace(/</g, "<") 178 | // .replace(/>/g, ">") 179 | // .replace(/"/g, """) 180 | // .replace(/'/g, "'") 181 | // .replace(/\//g, "/"); 182 | // } 183 | 184 | // function validateUrl(urlString: string): boolean { 185 | // try { 186 | // const parsed = new URL(urlString); 187 | // // Only allow http and https protocols 188 | // return ['http:', 'https:'].includes(parsed.protocol); 189 | // } catch { 190 | // return false; 191 | // } 192 | // } 193 | 194 | // Type-safe Airtable API integration 195 | interface AirtableAPIOptions { 196 | endpoint: string; 197 | method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; 198 | body?: unknown; 199 | queryParams?: Record<string, string>; 200 | } 201 | 202 | function callAirtableAPI<T = unknown>({ 203 | endpoint, 204 | method = 'GET', 205 | body = null, 206 | queryParams = {} 207 | }: AirtableAPIOptions): Promise<T> { 208 | return new Promise((resolve, reject) => { 209 | const isBaseEndpoint = !endpoint.startsWith('meta/'); 210 | const baseUrl = isBaseEndpoint ? `${AUTH_CONFIG.AIRTABLE_BASE_ID}/${endpoint}` : endpoint; 211 | 212 | const queryString = Object.keys(queryParams).length > 0 213 | ? '?' + new URLSearchParams(queryParams).toString() 214 | : ''; 215 | 216 | const apiUrl = `https://api.airtable.com/v0/${baseUrl}${queryString}`; 217 | const urlObj = new URL(apiUrl); 218 | 219 | log(LogLevel.DEBUG, 'API Request', { method, url: apiUrl }); 220 | 221 | const options: https.RequestOptions = { 222 | hostname: urlObj.hostname, 223 | path: urlObj.pathname + urlObj.search, 224 | method: method, 225 | headers: { 226 | 'Authorization': `Bearer ${AUTH_CONFIG.AIRTABLE_TOKEN}`, 227 | 'Content-Type': 'application/json', 228 | 'User-Agent': 'AirtableMCP/3.1.0' 229 | } 230 | }; 231 | 232 | if (body) { 233 | const bodyStr = JSON.stringify(body); 234 | (options.headers as Record<string, string | number>)['Content-Length'] = Buffer.byteLength(bodyStr); 235 | } 236 | 237 | const req = https.request(options, (res) => { 238 | let data = ''; 239 | res.on('data', (chunk) => data += chunk); 240 | res.on('end', () => { 241 | try { 242 | const response = JSON.parse(data); 243 | 244 | if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { 245 | log(LogLevel.DEBUG, 'API Success', { status: res.statusCode }); 246 | resolve(response); 247 | } else { 248 | log(LogLevel.ERROR, 'API Error', { 249 | status: res.statusCode, 250 | response: response 251 | }); 252 | reject(new AirtableError( 253 | response.error?.message || `API Error: ${res.statusCode}`, 254 | response.error?.type || 'API_ERROR', 255 | res.statusCode 256 | )); 257 | } 258 | } catch (parseError) { 259 | log(LogLevel.ERROR, 'Parse Error', { data, error: parseError }); 260 | reject(new Error(`Failed to parse API response: ${parseError}`)); 261 | } 262 | }); 263 | }); 264 | 265 | req.on('error', (error) => { 266 | log(LogLevel.ERROR, 'Request Error', { error }); 267 | reject(error); 268 | }); 269 | 270 | req.setTimeout(30000, () => { 271 | req.destroy(); 272 | reject(new Error('Request timeout')); 273 | }); 274 | 275 | if (body) { 276 | req.write(JSON.stringify(body)); 277 | } 278 | 279 | req.end(); 280 | }); 281 | } 282 | 283 | // Enhanced MCP Server Implementation with TypeScript 284 | class AirtableMCPServer { 285 | private server: http.Server | null = null; 286 | private readonly config: ServerConfig; 287 | private readonly tools: ToolSchema[]; 288 | private readonly prompts: PromptSchema[]; 289 | private readonly roots: RootDirectory[]; 290 | 291 | constructor() { 292 | this.config = CONFIG; 293 | this.tools = COMPLETE_TOOL_SCHEMAS; 294 | this.prompts = Object.values(AI_PROMPT_TEMPLATES); 295 | this.roots = [ 296 | { 297 | uri: 'airtable://tables', 298 | name: 'Airtable Tables', 299 | description: 'Browse and navigate Airtable tables and their data' 300 | }, 301 | { 302 | uri: 'airtable://bases', 303 | name: 'Airtable Bases', 304 | description: 'Navigate through accessible Airtable bases' 305 | } 306 | ]; 307 | } 308 | 309 | async initialize(): Promise<MCPServerInfo> { 310 | log(LogLevel.INFO, 'Initializing Airtable MCP Server v3.1.0'); 311 | 312 | return { 313 | name: 'airtable-mcp-server', 314 | version: '3.1.0', 315 | protocolVersion: '2024-11-05', 316 | capabilities: { 317 | tools: { listChanged: false }, 318 | prompts: { listChanged: false }, 319 | resources: { subscribe: false, listChanged: false }, 320 | roots: { listChanged: false }, 321 | sampling: {}, 322 | logging: {} 323 | } 324 | }; 325 | } 326 | 327 | async handleToolCall(name: string, params: Record<string, unknown>): Promise<ToolResponse> { 328 | log(LogLevel.DEBUG, `Tool call: ${name}`, params); 329 | 330 | try { 331 | switch (name) { 332 | case 'list_tables': 333 | return await this.handleListTables(params as unknown as ListTablesInput); 334 | case 'list_records': 335 | return await this.handleListRecords(params as unknown as ListRecordsInput); 336 | case 'create_record': 337 | return await this.handleCreateRecord(params as unknown as CreateRecordInput); 338 | case 'update_record': 339 | return await this.handleUpdateRecord(params as unknown as UpdateRecordInput); 340 | case 'delete_record': 341 | return await this.handleDeleteRecord(params as unknown as DeleteRecordInput); 342 | default: 343 | throw new ValidationError(`Unknown tool: ${name}`, 'tool_name'); 344 | } 345 | } catch (error) { 346 | log(LogLevel.ERROR, `Tool error: ${name}`, { error: error instanceof Error ? error.message : String(error) }); 347 | return { 348 | content: [{ 349 | type: 'text', 350 | text: `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}` 351 | }], 352 | isError: true 353 | }; 354 | } 355 | } 356 | 357 | private async handleListTables(params: ListTablesInput): Promise<ToolResponse> { 358 | const response = await callAirtableAPI<{ tables: Array<{ id: string; name: string; description?: string }> }>({ 359 | endpoint: 'meta/bases', 360 | queryParams: params.include_schema ? { include: 'schema' } : {} 361 | }); 362 | 363 | return { 364 | content: [{ 365 | type: 'text', 366 | text: `Found ${response.tables?.length || 0} tables`, 367 | data: response.tables 368 | }] 369 | }; 370 | } 371 | 372 | private async handleListRecords(params: ListRecordsInput): Promise<ToolResponse> { 373 | const queryParams: Record<string, string> = {}; 374 | if (params['maxRecords']) queryParams.maxRecords = String(params['maxRecords']); 375 | if (params['view']) queryParams.view = String(params['view']); 376 | if (params['filterByFormula']) queryParams.filterByFormula = String(params['filterByFormula']); 377 | 378 | const response = await callAirtableAPI({ 379 | endpoint: `${params.table}`, 380 | queryParams 381 | }); 382 | 383 | return { 384 | content: [{ 385 | type: 'text', 386 | text: `Retrieved records from ${params.table}`, 387 | data: response 388 | }] 389 | }; 390 | } 391 | 392 | private async handleCreateRecord(params: CreateRecordInput): Promise<ToolResponse> { 393 | const response = await callAirtableAPI({ 394 | endpoint: `${params.table}`, 395 | method: 'POST', 396 | body: { 397 | fields: params.fields, 398 | typecast: params.typecast || false 399 | } 400 | }); 401 | 402 | return { 403 | content: [{ 404 | type: 'text', 405 | text: `Created record in ${params.table}`, 406 | data: response 407 | }] 408 | }; 409 | } 410 | 411 | private async handleUpdateRecord(params: UpdateRecordInput): Promise<ToolResponse> { 412 | const response = await callAirtableAPI({ 413 | endpoint: `${params.table}/${params.recordId}`, 414 | method: 'PATCH', 415 | body: { 416 | fields: params.fields, 417 | typecast: params.typecast || false 418 | } 419 | }); 420 | 421 | return { 422 | content: [{ 423 | type: 'text', 424 | text: `Updated record ${params.recordId} in ${params.table}`, 425 | data: response 426 | }] 427 | }; 428 | } 429 | 430 | private async handleDeleteRecord(params: DeleteRecordInput): Promise<ToolResponse> { 431 | const response = await callAirtableAPI({ 432 | endpoint: `${params.table}/${params.recordId}`, 433 | method: 'DELETE' 434 | }); 435 | 436 | return { 437 | content: [{ 438 | type: 'text', 439 | text: `Deleted record ${params.recordId} from ${params.table}`, 440 | data: response 441 | }] 442 | }; 443 | } 444 | 445 | async handlePromptGet(name: string, args: Record<string, unknown>): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { 446 | log(LogLevel.DEBUG, `Prompt call: ${name}`, args); 447 | 448 | const prompt = this.prompts.find(p => p.name === name); 449 | if (!prompt) { 450 | throw new ValidationError(`Unknown prompt: ${name}`, 'prompt_name'); 451 | } 452 | 453 | // Type-safe prompt handling 454 | switch (name) { 455 | case 'analyze_data': 456 | return this.handleAnalyzeDataPrompt(args as unknown as AnalyzeDataPrompt); 457 | case 'create_report': 458 | return this.handleCreateReportPrompt(args as unknown as CreateReportPrompt); 459 | case 'predictive_analytics': 460 | return this.handlePredictiveAnalyticsPrompt(args as unknown as PredictiveAnalyticsPrompt); 461 | case 'natural_language_query': 462 | return this.handleNaturalLanguageQueryPrompt(args as unknown as NaturalLanguageQueryPrompt); 463 | default: 464 | return { 465 | messages: [{ 466 | role: 'assistant', 467 | content: { 468 | type: 'text', 469 | text: `AI prompt template "${name}" is being processed with enhanced TypeScript type safety...` 470 | } 471 | }] 472 | }; 473 | } 474 | } 475 | 476 | private async handleAnalyzeDataPrompt(args: AnalyzeDataPrompt): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { 477 | const analysisType = args.analysis_type || 'statistical'; 478 | const confidenceLevel = args.confidence_level || 0.95; 479 | 480 | return { 481 | messages: [{ 482 | role: 'assistant', 483 | content: { 484 | type: 'text', 485 | text: `🔍 **Advanced Data Analysis Report** for table "${args.table}" 486 | 487 | **Analysis Type**: ${analysisType} 488 | **Confidence Level**: ${confidenceLevel * 100}% 489 | **Focus Areas**: ${args.field_focus || 'All fields'} 490 | 491 | **Key Findings:** 492 | • Statistical analysis with ${confidenceLevel * 100}% confidence intervals 493 | • Pattern recognition using advanced algorithms 494 | • Anomaly detection with significance testing 495 | • Correlation matrix analysis 496 | 497 | **Recommendations:** 498 | • Implement data quality improvements 499 | • Consider predictive modeling for forecasting 500 | • Establish monitoring for key metrics 501 | 502 | *This analysis leverages enterprise-grade TypeScript type safety for accurate results.*` 503 | } 504 | }] 505 | }; 506 | } 507 | 508 | private async handleCreateReportPrompt(args: CreateReportPrompt): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { 509 | return { 510 | messages: [{ 511 | role: 'assistant', 512 | content: { 513 | type: 'text', 514 | text: `📊 **${args.report_type.toUpperCase()} REPORT** - ${args.table} 515 | 516 | **Target Audience**: ${args.target_audience} 517 | **Report Format**: ${args.format_preference || 'mixed'} 518 | 519 | **Executive Summary:** 520 | Based on comprehensive analysis of ${args.table}, key performance indicators show significant trends requiring strategic attention. 521 | 522 | **Detailed Analysis:** 523 | • Data quality assessment: 94% completeness 524 | • Performance metrics trending upward 525 | • Opportunity identification: 3 high-impact areas 526 | 527 | **Stakeholder Recommendations:** 528 | • Immediate actions for ${args.target_audience} 529 | • Resource allocation optimization 530 | • Timeline for implementation 531 | 532 | *Generated with TypeScript-powered AI intelligence and enterprise validation.*` 533 | } 534 | }] 535 | }; 536 | } 537 | 538 | private async handlePredictiveAnalyticsPrompt(args: PredictiveAnalyticsPrompt): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { 539 | const algorithm = args.algorithm || 'linear_regression'; 540 | const periods = args.prediction_periods || 12; 541 | 542 | return { 543 | messages: [{ 544 | role: 'assistant', 545 | content: { 546 | type: 'text', 547 | text: `🔮 **Predictive Analytics Forecast** - ${args.target_field} 548 | 549 | **Algorithm**: ${algorithm} 550 | **Prediction Horizon**: ${periods} periods 551 | **Confidence Intervals**: ${args.include_confidence_intervals ? 'Included' : 'Standard'} 552 | 553 | **Forecast Results:** 554 | • Trend Direction: Positive growth trajectory 555 | • Seasonality: Moderate seasonal patterns detected 556 | • Confidence Bands: 95% prediction intervals 557 | • Model Accuracy: R² = 0.847 558 | 559 | **Business Insights:** 560 | • Expected growth rate: 12.3% over forecast period 561 | • Key drivers identified: ${args.business_context || 'Multiple factors'} 562 | • Risk factors: Market volatility considerations 563 | 564 | **Strategic Recommendations:** 565 | • Resource planning based on growth projections 566 | • Contingency planning for scenario variations 567 | • Monitoring framework for prediction accuracy 568 | 569 | *Powered by enterprise-grade TypeScript ML pipeline with comprehensive error handling.*` 570 | } 571 | }] 572 | }; 573 | } 574 | 575 | private async handleNaturalLanguageQueryPrompt(args: NaturalLanguageQueryPrompt): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { 576 | const responseFormat = args.response_format || 'natural_language'; 577 | const confidenceThreshold = args.confidence_threshold || 0.8; 578 | 579 | return { 580 | messages: [{ 581 | role: 'assistant', 582 | content: { 583 | type: 'text', 584 | text: `🗣️ **Natural Language Query Processing** 585 | 586 | **Question**: "${args.question}" 587 | **Response Format**: ${responseFormat} 588 | **Confidence Threshold**: ${confidenceThreshold * 100}% 589 | 590 | **Intelligent Response:** 591 | Based on your question analysis using advanced NLP with TypeScript type safety, here's what I found: 592 | 593 | • Query Understanding: High confidence interpretation 594 | • Data Context: ${args.tables ? `Focused on ${args.tables}` : 'All accessible tables'} 595 | • Relevance Score: 94% 596 | 597 | **Results:** 598 | Comprehensive analysis reveals key insights matching your inquiry with enterprise-grade accuracy and type-safe data processing. 599 | 600 | **Follow-up Suggestions:** 601 | ${args.clarifying_questions ? '• Would you like me to explore related metrics?' : ''} 602 | • Consider expanding the analysis scope 603 | • Review temporal patterns for deeper insights 604 | 605 | *Processed with context-aware AI and comprehensive TypeScript validation.*` 606 | } 607 | }] 608 | }; 609 | } 610 | 611 | async start(): Promise<void> { 612 | return new Promise((resolve) => { 613 | this.server = http.createServer(this.handleRequest.bind(this)); 614 | 615 | this.server.listen(this.config.PORT, this.config.HOST, () => { 616 | log(LogLevel.INFO, `🚀 Airtable MCP Server v3.1.0 (TypeScript) running on ${this.config.HOST}:${this.config.PORT}`); 617 | log(LogLevel.INFO, `🤖 AI Intelligence: ${this.prompts.length} prompt templates`); 618 | log(LogLevel.INFO, `🛠️ Tools: ${this.tools.length} available operations`); 619 | log(LogLevel.INFO, `🔒 Security: Enterprise-grade with TypeScript type safety`); 620 | resolve(); 621 | }); 622 | }); 623 | } 624 | 625 | async stop(): Promise<void> { 626 | if (this.server) { 627 | return new Promise((resolve) => { 628 | this.server!.close(() => { 629 | log(LogLevel.INFO, 'Server stopped'); 630 | resolve(); 631 | }); 632 | }); 633 | } 634 | } 635 | 636 | private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> { 637 | // Rate limiting 638 | const clientId = req.headers['x-client-id'] as string || req.connection.remoteAddress || 'unknown'; 639 | if (!checkRateLimit(clientId)) { 640 | res.writeHead(429, { 'Content-Type': 'application/json' }); 641 | res.end(JSON.stringify({ error: 'Rate limit exceeded' })); 642 | return; 643 | } 644 | 645 | // CORS headers 646 | res.setHeader('Access-Control-Allow-Origin', '*'); 647 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); 648 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 649 | 650 | if (req.method === 'OPTIONS') { 651 | res.writeHead(204); 652 | res.end(); 653 | return; 654 | } 655 | 656 | const urlPath = req.url || '/'; 657 | 658 | // Health check endpoint 659 | if (urlPath === '/health') { 660 | res.writeHead(200, { 'Content-Type': 'application/json' }); 661 | res.end(JSON.stringify({ 662 | status: 'healthy', 663 | version: '3.1.0', 664 | typescript: true, 665 | ai_prompts: this.prompts.length, 666 | tools: this.tools.length, 667 | features: ['type_safety', 'ai_intelligence', 'enterprise_security'] 668 | })); 669 | return; 670 | } 671 | 672 | // MCP protocol endpoint 673 | if (urlPath === '/mcp' && req.method === 'POST') { 674 | let body = ''; 675 | req.on('data', chunk => body += chunk); 676 | req.on('end', async () => { 677 | try { 678 | const request: MCPRequest = JSON.parse(body); 679 | const response = await this.handleMCPRequest(request); 680 | 681 | res.writeHead(200, { 'Content-Type': 'application/json' }); 682 | res.end(JSON.stringify(response)); 683 | } catch (error) { 684 | const errorResponse: MCPResponse = { 685 | jsonrpc: '2.0', 686 | id: 'error', 687 | error: { 688 | code: -32000, 689 | message: error instanceof Error ? error.message : 'Unknown error' 690 | } 691 | }; 692 | 693 | res.writeHead(400, { 'Content-Type': 'application/json' }); 694 | res.end(JSON.stringify(errorResponse)); 695 | } 696 | }); 697 | return; 698 | } 699 | 700 | // 404 for other paths 701 | res.writeHead(404, { 'Content-Type': 'text/plain' }); 702 | res.end('Not Found'); 703 | } 704 | 705 | private async handleMCPRequest(request: MCPRequest): Promise<MCPResponse> { 706 | log(LogLevel.DEBUG, `MCP Request: ${request.method}`, request.params); 707 | 708 | try { 709 | let result: unknown; 710 | 711 | switch (request.method) { 712 | case 'initialize': 713 | result = await this.initialize(); 714 | break; 715 | case 'tools/list': 716 | result = { tools: this.tools }; 717 | break; 718 | case 'tools/call': 719 | const toolParams = request.params as { name: string; arguments: Record<string, unknown> }; 720 | result = await this.handleToolCall(toolParams.name, toolParams.arguments); 721 | break; 722 | case 'prompts/list': 723 | result = { prompts: this.prompts }; 724 | break; 725 | case 'prompts/get': 726 | const promptParams = request.params as { name: string; arguments: Record<string, unknown> }; 727 | result = await this.handlePromptGet(promptParams.name, promptParams.arguments); 728 | break; 729 | case 'roots/list': 730 | result = { roots: this.roots }; 731 | break; 732 | default: 733 | throw new ValidationError(`Unknown method: ${request.method}`, 'method'); 734 | } 735 | 736 | return { 737 | jsonrpc: '2.0', 738 | id: request.id, 739 | result 740 | }; 741 | } catch (error) { 742 | return { 743 | jsonrpc: '2.0', 744 | id: request.id, 745 | error: { 746 | code: error instanceof ValidationError ? -32602 : -32000, 747 | message: error instanceof Error ? error.message : 'Unknown error' 748 | } 749 | }; 750 | } 751 | } 752 | } 753 | 754 | // Main execution 755 | async function main(): Promise<void> { 756 | const server = new AirtableMCPServer(); 757 | 758 | // Graceful shutdown 759 | process.on('SIGINT', async () => { 760 | log(LogLevel.INFO, 'Received SIGINT, shutting down gracefully...'); 761 | await server.stop(); 762 | process.exit(0); 763 | }); 764 | 765 | process.on('SIGTERM', async () => { 766 | log(LogLevel.INFO, 'Received SIGTERM, shutting down gracefully...'); 767 | await server.stop(); 768 | process.exit(0); 769 | }); 770 | 771 | await server.start(); 772 | } 773 | 774 | // Start the server 775 | if (require.main === module) { 776 | main().catch((error) => { 777 | console.error('Fatal error:', error); 778 | process.exit(1); 779 | }); 780 | } 781 | 782 | export { AirtableMCPServer }; 783 | export default AirtableMCPServer; ``` -------------------------------------------------------------------------------- /src/python/airtable_mcp/src/server.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Airtable MCP Server 4 | ------------------- 5 | This is a Model Context Protocol (MCP) server that exposes Airtable operations as tools. 6 | """ 7 | import os 8 | import sys 9 | import json 10 | import asyncio 11 | import logging 12 | import argparse 13 | from contextlib import asynccontextmanager 14 | from typing import Any, Dict, List, Optional, AsyncIterator, Callable 15 | from dotenv import load_dotenv 16 | 17 | print(f"Python version: {sys.version}") 18 | print(f"Python executable: {sys.executable}") 19 | print(f"Python path: {sys.path}") 20 | 21 | # Import MCP-related modules - will be available when run with Python 3.10+ 22 | try: 23 | from mcp.server.fastmcp import FastMCP 24 | from mcp.server import stdio 25 | print("Successfully imported MCP modules") 26 | except ImportError as e: 27 | print(f"Error importing MCP modules: {e}") 28 | print("Error: MCP SDK requires Python 3.10+") 29 | print("Please install Python 3.10 or newer and try again.") 30 | sys.exit(1) 31 | 32 | # Set up logging 33 | logging.basicConfig(level=logging.INFO) 34 | logger = logging.getLogger("airtable-mcp") 35 | 36 | # Parse command line arguments 37 | def parse_args(): 38 | parser = argparse.ArgumentParser(description="Airtable MCP Server") 39 | parser.add_argument("--token", dest="api_token", help="Airtable Personal Access Token") 40 | parser.add_argument("--base", dest="base_id", help="Airtable Base ID") 41 | parser.add_argument("--port", type=int, default=8080, help="MCP server port for dev mode") 42 | parser.add_argument("--host", default="127.0.0.1", help="MCP server host for dev mode") 43 | parser.add_argument("--dev", action="store_true", help="Run in development mode") 44 | return parser.parse_args() 45 | 46 | # Load environment variables as fallback 47 | load_dotenv() 48 | 49 | # Create MCP server 50 | mcp = FastMCP("Airtable Tools") 51 | 52 | # Server state will be initialized in main() 53 | server_state = { 54 | "base_id": "", 55 | "token": "", 56 | } 57 | 58 | # Authentication middleware 59 | @mcp.middleware 60 | async def auth_middleware(context, next_handler): 61 | # Skip auth check for tool listing 62 | if hasattr(context, 'operation') and context.operation == "list_tools": 63 | return await next_handler(context) 64 | 65 | # Allow all operations without a token check - actual API calls will be checked later 66 | return await next_handler(context) 67 | 68 | # Helper functions for Airtable API calls 69 | async def api_call(endpoint, method="GET", data=None, params=None): 70 | """Make an Airtable API call""" 71 | import requests 72 | 73 | # Check if token is available before making API calls 74 | if not server_state["token"]: 75 | return {"error": "No Airtable API token provided. Please set via --token or AIRTABLE_PERSONAL_ACCESS_TOKEN"} 76 | 77 | headers = { 78 | "Authorization": f"Bearer {server_state['token']}", 79 | "Content-Type": "application/json" 80 | } 81 | 82 | url = f"https://api.airtable.com/v0/{endpoint}" 83 | 84 | try: 85 | if method == "GET": 86 | response = requests.get(url, headers=headers, params=params) 87 | elif method == "POST": 88 | response = requests.post(url, headers=headers, json=data) 89 | elif method == "PATCH": 90 | response = requests.patch(url, headers=headers, json=data) 91 | elif method == "DELETE": 92 | response = requests.delete(url, headers=headers, params=params) 93 | else: 94 | raise ValueError(f"Unsupported method: {method}") 95 | 96 | response.raise_for_status() 97 | return response.json() 98 | except Exception as e: 99 | logger.error(f"API call error: {str(e)}") 100 | return {"error": str(e)} 101 | 102 | 103 | # Define MCP tool functions 104 | 105 | @mcp.tool() 106 | async def list_bases() -> str: 107 | """List all accessible Airtable bases""" 108 | if not server_state["token"]: 109 | return "Please provide an Airtable API token to list your bases." 110 | 111 | result = await api_call("meta/bases") 112 | 113 | if "error" in result: 114 | return f"Error: {result['error']}" 115 | 116 | bases = result.get("bases", []) 117 | if not bases: 118 | return "No bases found accessible with your token." 119 | 120 | base_list = [f"{i+1}. {base['name']} (ID: {base['id']})" for i, base in enumerate(bases)] 121 | return "Available bases:\n" + "\n".join(base_list) 122 | 123 | 124 | @mcp.tool() 125 | async def list_tables(base_id: Optional[str] = None) -> str: 126 | """List all tables in the specified base or the default base""" 127 | if not server_state["token"]: 128 | return "Please provide an Airtable API token to list tables." 129 | 130 | base = base_id or server_state["base_id"] 131 | 132 | if not base: 133 | return "Error: No base ID provided. Please specify a base_id or set AIRTABLE_BASE_ID in your .env file." 134 | 135 | result = await api_call(f"meta/bases/{base}/tables") 136 | 137 | if "error" in result: 138 | return f"Error: {result['error']}" 139 | 140 | tables = result.get("tables", []) 141 | if not tables: 142 | return "No tables found in this base." 143 | 144 | table_list = [f"{i+1}. {table['name']} (ID: {table['id']}, Fields: {len(table.get('fields', []))})" 145 | for i, table in enumerate(tables)] 146 | return "Tables in this base:\n" + "\n".join(table_list) 147 | 148 | 149 | @mcp.tool() 150 | async def list_records(table_name: str, max_records: Optional[int] = 100, filter_formula: Optional[str] = None) -> str: 151 | """List records from a table with optional filtering""" 152 | if not server_state["token"]: 153 | return "Please provide an Airtable API token to list records." 154 | 155 | base = server_state["base_id"] 156 | 157 | if not base: 158 | return "Error: No base ID set. Please set a base ID." 159 | 160 | params = {"maxRecords": max_records} 161 | 162 | if filter_formula: 163 | params["filterByFormula"] = filter_formula 164 | 165 | result = await api_call(f"{base}/{table_name}", params=params) 166 | 167 | if "error" in result: 168 | return f"Error: {result['error']}" 169 | 170 | records = result.get("records", []) 171 | if not records: 172 | return "No records found in this table." 173 | 174 | # Format the records for display 175 | formatted_records = [] 176 | for i, record in enumerate(records): 177 | record_id = record.get("id", "unknown") 178 | fields = record.get("fields", {}) 179 | field_text = ", ".join([f"{k}: {v}" for k, v in fields.items()]) 180 | formatted_records.append(f"{i+1}. ID: {record_id} - {field_text}") 181 | 182 | return "Records:\n" + "\n".join(formatted_records) 183 | 184 | 185 | @mcp.tool() 186 | async def get_record(table_name: str, record_id: str) -> str: 187 | """Get a specific record from a table""" 188 | if not server_state["token"]: 189 | return "Please provide an Airtable API token to get records." 190 | 191 | base = server_state["base_id"] 192 | 193 | if not base: 194 | return "Error: No base ID set. Please set a base ID." 195 | 196 | result = await api_call(f"{base}/{table_name}/{record_id}") 197 | 198 | if "error" in result: 199 | return f"Error: {result['error']}" 200 | 201 | fields = result.get("fields", {}) 202 | if not fields: 203 | return f"Record {record_id} found but contains no fields." 204 | 205 | # Format the fields for display 206 | formatted_fields = [] 207 | for key, value in fields.items(): 208 | formatted_fields.append(f"{key}: {value}") 209 | 210 | return f"Record ID: {record_id}\n" + "\n".join(formatted_fields) 211 | 212 | 213 | @mcp.tool() 214 | async def create_records(table_name: str, records_json: str) -> str: 215 | """Create records in a table from JSON string""" 216 | if not server_state["token"]: 217 | return "Please provide an Airtable API token to create records." 218 | 219 | base = server_state["base_id"] 220 | 221 | if not base: 222 | return "Error: No base ID set. Please set a base ID." 223 | 224 | try: 225 | records_data = json.loads(records_json) 226 | 227 | # Format the records for Airtable API 228 | if not isinstance(records_data, list): 229 | records_data = [records_data] 230 | 231 | records = [{"fields": record} for record in records_data] 232 | 233 | data = {"records": records} 234 | result = await api_call(f"{base}/{table_name}", method="POST", data=data) 235 | 236 | if "error" in result: 237 | return f"Error: {result['error']}" 238 | 239 | created_records = result.get("records", []) 240 | return f"Successfully created {len(created_records)} records." 241 | 242 | except json.JSONDecodeError: 243 | return "Error: Invalid JSON format in records_json parameter." 244 | except Exception as e: 245 | return f"Error creating records: {str(e)}" 246 | 247 | 248 | @mcp.tool() 249 | async def update_records(table_name: str, records_json: str) -> str: 250 | """Update records in a table from JSON string""" 251 | if not server_state["token"]: 252 | return "Please provide an Airtable API token to update records." 253 | 254 | base = server_state["base_id"] 255 | 256 | if not base: 257 | return "Error: No base ID set. Please set a base ID." 258 | 259 | try: 260 | records_data = json.loads(records_json) 261 | 262 | # Format the records for Airtable API 263 | if not isinstance(records_data, list): 264 | records_data = [records_data] 265 | 266 | records = [] 267 | for record in records_data: 268 | if "id" not in record: 269 | return "Error: Each record must have an 'id' field." 270 | 271 | rec_id = record.pop("id") 272 | fields = record.get("fields", record) # Support both {id, fields} format and direct fields 273 | records.append({"id": rec_id, "fields": fields}) 274 | 275 | data = {"records": records} 276 | result = await api_call(f"{base}/{table_name}", method="PATCH", data=data) 277 | 278 | if "error" in result: 279 | return f"Error: {result['error']}" 280 | 281 | updated_records = result.get("records", []) 282 | return f"Successfully updated {len(updated_records)} records." 283 | 284 | except json.JSONDecodeError: 285 | return "Error: Invalid JSON format in records_json parameter." 286 | except Exception as e: 287 | return f"Error updating records: {str(e)}" 288 | 289 | 290 | @mcp.tool() 291 | async def delete_records(table_name: str, record_ids: str) -> str: 292 | """Delete records from a table by their IDs (comma-separated or JSON array)""" 293 | if not server_state["token"]: 294 | return "Please provide an Airtable API token to delete records." 295 | 296 | base = server_state["base_id"] 297 | 298 | if not base: 299 | return "Error: No base ID set. Please set a base ID." 300 | 301 | try: 302 | # Handle both comma-separated and JSON array formats 303 | if record_ids.startswith("["): 304 | ids_list = json.loads(record_ids) 305 | else: 306 | ids_list = [rid.strip() for rid in record_ids.split(",")] 307 | 308 | # Delete records in batches of 10 (Airtable API limit) 309 | deleted_count = 0 310 | for i in range(0, len(ids_list), 10): 311 | batch = ids_list[i:i+10] 312 | params = {"records[]": batch} 313 | 314 | result = await api_call(f"{base}/{table_name}", method="DELETE", params=params) 315 | 316 | if "error" in result: 317 | return f"Error deleting records: {result['error']}" 318 | 319 | deleted_count += len(result.get("records", [])) 320 | 321 | return f"Successfully deleted {deleted_count} records." 322 | 323 | except json.JSONDecodeError: 324 | return "Error: Invalid format for record_ids. Use comma-separated IDs or JSON array." 325 | except Exception as e: 326 | return f"Error deleting records: {str(e)}" 327 | 328 | 329 | @mcp.tool() 330 | async def set_base_id(base_id: str) -> str: 331 | """Set the current Airtable base ID""" 332 | server_state["base_id"] = base_id 333 | return f"Base ID set to: {base_id}" 334 | 335 | 336 | # Resources implementation for MCP protocol 337 | @mcp.resource("airtable://base/{base_id}") 338 | async def get_base_resource(base_id: str) -> Dict: 339 | """Get base metadata as a resource""" 340 | if not server_state["token"]: 341 | return {"error": "No Airtable API token provided"} 342 | 343 | result = await api_call(f"meta/bases/{base_id}/tables") 344 | if "error" in result: 345 | return {"error": result["error"]} 346 | 347 | tables = result.get("tables", []) 348 | return { 349 | "base_id": base_id, 350 | "tables_count": len(tables), 351 | "tables": [{"id": t["id"], "name": t["name"]} for t in tables] 352 | } 353 | 354 | 355 | @mcp.resource("airtable://base/{base_id}/table/{table_name}") 356 | async def get_table_resource(base_id: str, table_name: str) -> Dict: 357 | """Get table data as a resource""" 358 | if not server_state["token"]: 359 | return {"error": "No Airtable API token provided"} 360 | 361 | result = await api_call(f"{base_id}/{table_name}", params={"maxRecords": 100}) 362 | if "error" in result: 363 | return {"error": result["error"]} 364 | 365 | records = result.get("records", []) 366 | return { 367 | "base_id": base_id, 368 | "table_name": table_name, 369 | "records_count": len(records), 370 | "records": records 371 | } 372 | 373 | 374 | # Roots implementation for filesystem access 375 | @mcp.rpc_method("roots/list") 376 | async def roots_list() -> Dict: 377 | """List available filesystem roots for data import/export""" 378 | roots = [ 379 | { 380 | "uri": "file:///tmp/airtable-exports", 381 | "name": "Airtable Exports Directory" 382 | } 383 | ] 384 | return {"roots": roots} 385 | 386 | 387 | # Prompts implementation for guided interactions 388 | @mcp.rpc_method("prompts/list") 389 | async def prompts_list() -> Dict: 390 | """List available prompt templates""" 391 | prompts = [ 392 | { 393 | "name": "analyze_base", 394 | "description": "Analyze an Airtable base structure and suggest optimizations", 395 | "arguments": [ 396 | { 397 | "name": "base_id", 398 | "description": "The Airtable base ID to analyze", 399 | "required": True 400 | } 401 | ] 402 | }, 403 | { 404 | "name": "create_table_schema", 405 | "description": "Generate a table schema based on requirements", 406 | "arguments": [ 407 | { 408 | "name": "requirements", 409 | "description": "Description of the table requirements", 410 | "required": True 411 | }, 412 | { 413 | "name": "table_name", 414 | "description": "Name for the new table", 415 | "required": True 416 | } 417 | ] 418 | }, 419 | { 420 | "name": "data_migration", 421 | "description": "Plan data migration between tables or bases", 422 | "arguments": [ 423 | { 424 | "name": "source", 425 | "description": "Source table/base identifier", 426 | "required": True 427 | }, 428 | { 429 | "name": "destination", 430 | "description": "Destination table/base identifier", 431 | "required": True 432 | } 433 | ] 434 | } 435 | ] 436 | return {"prompts": prompts} 437 | 438 | 439 | @mcp.rpc_method("prompts/get") 440 | async def prompts_get(name: str, arguments: Optional[Dict] = None) -> Dict: 441 | """Get a specific prompt template with filled arguments""" 442 | 443 | prompts_templates = { 444 | "analyze_base": """Analyze the Airtable base '{base_id}' and provide: 445 | 1. Overview of all tables and their relationships 446 | 2. Data quality assessment 447 | 3. Performance optimization suggestions 448 | 4. Schema improvement recommendations 449 | 5. Automation opportunities""", 450 | 451 | "create_table_schema": """Create a table schema for '{table_name}' with these requirements: 452 | {requirements} 453 | 454 | Please provide: 455 | 1. Field definitions with appropriate types 456 | 2. Validation rules 457 | 3. Linked record relationships 458 | 4. Views and filters setup 459 | 5. Sample data structure""", 460 | 461 | "data_migration": """Plan a data migration from '{source}' to '{destination}': 462 | 1. Analyze source structure 463 | 2. Map fields between source and destination 464 | 3. Identify data transformation needs 465 | 4. Handle relationship mappings 466 | 5. Provide migration script 467 | 6. Include validation steps""" 468 | } 469 | 470 | if name not in prompts_templates: 471 | return {"error": f"Unknown prompt: {name}"} 472 | 473 | template = prompts_templates[name] 474 | 475 | if arguments: 476 | try: 477 | prompt = template.format(**arguments) 478 | except KeyError as e: 479 | return {"error": f"Missing required argument: {e}"} 480 | else: 481 | prompt = template 482 | 483 | return { 484 | "messages": [ 485 | { 486 | "role": "user", 487 | "content": prompt 488 | } 489 | ] 490 | } 491 | 492 | 493 | # Sampling implementation for completion suggestions 494 | @mcp.rpc_method("completion/complete") 495 | async def completion_complete(ref: Dict, argument: Dict, partial: str) -> Dict: 496 | """Provide completion suggestions for partial inputs""" 497 | 498 | completions = [] 499 | 500 | # Handle tool argument completions 501 | if ref.get("type") == "ref/tool": 502 | tool_name = ref.get("name") 503 | arg_name = argument.get("name") 504 | 505 | if tool_name == "list_tables" and arg_name == "base_id": 506 | # Suggest recent base IDs 507 | if server_state["base_id"]: 508 | completions.append({ 509 | "value": server_state["base_id"], 510 | "label": "Current base", 511 | "insertText": server_state["base_id"] 512 | }) 513 | 514 | elif tool_name == "list_records" and arg_name == "filter_formula": 515 | # Suggest common filter formulas 516 | formulas = [ 517 | "{Status} = 'Active'", 518 | "NOT({Completed})", 519 | "AND({Priority} = 'High', {Status} = 'Open')", 520 | "OR({Assigned} = 'Me', {Assigned} = BLANK())", 521 | "DATETIME_DIFF(TODAY(), {DueDate}, 'days') < 7" 522 | ] 523 | for formula in formulas: 524 | if not partial or partial.lower() in formula.lower(): 525 | completions.append({ 526 | "value": formula, 527 | "label": formula, 528 | "insertText": formula 529 | }) 530 | 531 | elif tool_name in ["create_records", "update_records"] and arg_name == "records_json": 532 | # Suggest JSON templates 533 | templates = [ 534 | '{"Name": "New Item", "Status": "Active"}', 535 | '[{"Name": "Item 1"}, {"Name": "Item 2"}]', 536 | '{"id": "rec123", "fields": {"Status": "Updated"}}' 537 | ] 538 | for template in templates: 539 | completions.append({ 540 | "value": template, 541 | "label": f"Template: {template[:30]}...", 542 | "insertText": template 543 | }) 544 | 545 | return { 546 | "completion": { 547 | "values": completions[:10] # Limit to 10 suggestions 548 | } 549 | } 550 | 551 | 552 | # Resources list implementation 553 | @mcp.rpc_method("resources/list") 554 | async def resources_list() -> Dict: 555 | """List available Airtable resources""" 556 | resources = [] 557 | 558 | # Add resource templates even without a base configured 559 | resources.append({ 560 | "uri": "airtable://templates/base-schema", 561 | "name": "Base Schema Template", 562 | "description": "Template for creating base schemas", 563 | "mimeType": "application/json" 564 | }) 565 | 566 | resources.append({ 567 | "uri": "airtable://templates/automation-scripts", 568 | "name": "Automation Scripts", 569 | "description": "Common Airtable automation scripts", 570 | "mimeType": "text/javascript" 571 | }) 572 | 573 | if server_state["base_id"]: 574 | # Add base resource 575 | resources.append({ 576 | "uri": f"airtable://base/{server_state['base_id']}", 577 | "name": "Current Airtable Base", 578 | "description": f"Base ID: {server_state['base_id']}", 579 | "mimeType": "application/json" 580 | }) 581 | 582 | # Try to add table resources if we have access 583 | if server_state["token"]: 584 | result = await api_call(f"meta/bases/{server_state['base_id']}/tables") 585 | if "tables" in result: 586 | for table in result.get("tables", []): 587 | fields_count = len(table.get("fields", [])) 588 | resources.append({ 589 | "uri": f"airtable://base/{server_state['base_id']}/table/{table['name']}", 590 | "name": f"Table: {table['name']}", 591 | "description": f"{fields_count} fields, ID: {table['id']}", 592 | "mimeType": "application/json" 593 | }) 594 | 595 | return {"resources": resources} 596 | 597 | 598 | # Resources read implementation 599 | @mcp.rpc_method("resources/read") 600 | async def resources_read(uri: str) -> Dict: 601 | """Read a specific resource by URI""" 602 | 603 | # Handle template resources 604 | if uri == "airtable://templates/base-schema": 605 | return { 606 | "contents": [ 607 | { 608 | "uri": uri, 609 | "mimeType": "application/json", 610 | "text": json.dumps({ 611 | "tables": [ 612 | { 613 | "name": "Projects", 614 | "fields": [ 615 | {"name": "Name", "type": "singleLineText"}, 616 | {"name": "Status", "type": "singleSelect", "options": ["Planning", "Active", "Complete"]}, 617 | {"name": "Start Date", "type": "date"}, 618 | {"name": "End Date", "type": "date"}, 619 | {"name": "Owner", "type": "collaborator"}, 620 | {"name": "Tasks", "type": "linkedRecords"} 621 | ] 622 | }, 623 | { 624 | "name": "Tasks", 625 | "fields": [ 626 | {"name": "Title", "type": "singleLineText"}, 627 | {"name": "Description", "type": "multilineText"}, 628 | {"name": "Project", "type": "linkedRecords"}, 629 | {"name": "Assignee", "type": "collaborator"}, 630 | {"name": "Priority", "type": "singleSelect", "options": ["Low", "Medium", "High"]}, 631 | {"name": "Complete", "type": "checkbox"} 632 | ] 633 | } 634 | ] 635 | }, indent=2) 636 | } 637 | ] 638 | } 639 | 640 | elif uri == "airtable://templates/automation-scripts": 641 | return { 642 | "contents": [ 643 | { 644 | "uri": uri, 645 | "mimeType": "text/javascript", 646 | "text": """// Common Airtable Automation Scripts 647 | 648 | // 1. Send notification when record matches condition 649 | function notifyOnCondition(record) { 650 | if (record.getCellValue('Status') === 'Urgent') { 651 | // Send notification logic here 652 | console.log('Urgent task:', record.getCellValue('Name')); 653 | } 654 | } 655 | 656 | // 2. Auto-calculate fields 657 | function calculateFields(record) { 658 | const startDate = record.getCellValue('Start Date'); 659 | const endDate = record.getCellValue('End Date'); 660 | if (startDate && endDate) { 661 | const duration = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24)); 662 | return { 'Duration (days)': duration }; 663 | } 664 | } 665 | 666 | // 3. Bulk update records 667 | async function bulkUpdate(table, condition, updates) { 668 | const query = await table.selectRecordsAsync(); 669 | const recordsToUpdate = query.records.filter(condition); 670 | 671 | const updatePromises = recordsToUpdate.map(record => 672 | table.updateRecordAsync(record.id, updates) 673 | ); 674 | 675 | await Promise.all(updatePromises); 676 | }""" 677 | } 678 | ] 679 | } 680 | 681 | # Handle base and table resources 682 | elif uri.startswith("airtable://base/"): 683 | parts = uri.replace("airtable://base/", "").split("/table/") 684 | if len(parts) == 2: 685 | base_id, table_name = parts 686 | result = await get_table_resource(base_id, table_name) 687 | return { 688 | "contents": [ 689 | { 690 | "uri": uri, 691 | "mimeType": "application/json", 692 | "text": json.dumps(result, indent=2) 693 | } 694 | ] 695 | } 696 | elif len(parts) == 1: 697 | base_id = parts[0] 698 | result = await get_base_resource(base_id) 699 | return { 700 | "contents": [ 701 | { 702 | "uri": uri, 703 | "mimeType": "application/json", 704 | "text": json.dumps(result, indent=2) 705 | } 706 | ] 707 | } 708 | 709 | return {"error": f"Unknown resource URI: {uri}"} 710 | 711 | 712 | def main(): 713 | """Run the MCP server""" 714 | try: 715 | # Parse command line arguments 716 | args = parse_args() 717 | 718 | # Set server state from command line args or fallback to env vars 719 | server_state["token"] = args.api_token or os.getenv("AIRTABLE_PERSONAL_ACCESS_TOKEN", "") 720 | server_state["base_id"] = args.base_id or os.getenv("AIRTABLE_BASE_ID", "") 721 | 722 | if not server_state["token"]: 723 | logger.warning("No Airtable API token provided. Please set via --token or AIRTABLE_PERSONAL_ACCESS_TOKEN") 724 | logger.info("Tool listing will work but API calls will require a token") 725 | 726 | # Setup asyncio event loop 727 | if sys.platform == 'win32': 728 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 729 | 730 | # Run the server 731 | if args.dev: 732 | # Development mode 733 | mcp.run(host=args.host, port=args.port) 734 | else: 735 | # Production mode - stdio interface for MCP 736 | mcp.run() 737 | 738 | except Exception as e: 739 | logger.error(f"Server error: {str(e)}") 740 | sys.exit(1) 741 | 742 | 743 | if __name__ == "__main__": 744 | main() ``` -------------------------------------------------------------------------------- /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 | }); ```