@rashidazarang/airtable-mcp 2.2.0 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -5
- package/airtable_simple_production.js +147 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,13 +3,21 @@
|
|
|
3
3
|
[](https://archestra.ai/mcp-catalog/rashidazarang__airtable-mcp)
|
|
4
4
|
[](https://smithery.ai/server/@rashidazarang/airtable-mcp)
|
|
5
5
|

|
|
6
|
-
[](https://github.com/rashidazarang/airtable-mcp)
|
|
7
|
+
[](https://github.com/rashidazarang/airtable-mcp)
|
|
8
|
+
[](https://modelcontextprotocol.io/)
|
|
7
9
|
|
|
8
|
-
A Model Context Protocol
|
|
10
|
+
🏆 **Complete MCP 2024-11-05 Implementation** - A production-ready Model Context Protocol server that enables AI assistants like Claude to interact with your Airtable bases through a secure, feature-complete interface.
|
|
9
11
|
|
|
10
|
-
##
|
|
12
|
+
## 🚀 Latest: Enhanced v2.2.1
|
|
11
13
|
|
|
12
|
-
**
|
|
14
|
+
**Complete MCP Protocol Support** with enterprise security:
|
|
15
|
+
- ✅ **Prompts** - 4 AI-powered templates for data analysis
|
|
16
|
+
- ✅ **Sampling** - LLM integration for intelligent operations
|
|
17
|
+
- ✅ **Roots** - Filesystem boundary management
|
|
18
|
+
- ✅ **Logging** - Dynamic structured logging
|
|
19
|
+
- ✅ **OAuth2** - PKCE authentication flow
|
|
20
|
+
- ✅ **Security** - XSS protection, input validation, CSP headers
|
|
13
21
|
|
|
14
22
|
## ✨ Features
|
|
15
23
|
|
|
@@ -26,6 +34,8 @@ A Model Context Protocol (MCP) server that enables AI assistants like Claude to
|
|
|
26
34
|
- 📎 **Attachment Management** - Upload files via URLs to attachment fields
|
|
27
35
|
- ⚡ **Batch Operations** - Create, update, delete up to 10 records at once
|
|
28
36
|
- 👥 **Collaboration Tools** - Manage base collaborators and shared views
|
|
37
|
+
- 🤖 **AI Integration** - Prompts and sampling for intelligent data operations
|
|
38
|
+
- 🔐 **Enterprise Security** - OAuth2, rate limiting, comprehensive validation
|
|
29
39
|
|
|
30
40
|
## 📋 Prerequisites
|
|
31
41
|
|
|
@@ -238,6 +248,14 @@ Once configured, you can interact with your Airtable data naturally:
|
|
|
238
248
|
| `list_collaborators` | View base collaborators and their permission levels |
|
|
239
249
|
| `list_shares` | List shared views and their public configurations |
|
|
240
250
|
|
|
251
|
+
### 🤖 AI Integration (4 prompts) - **New in v2.2.0**
|
|
252
|
+
| Prompt | Description |
|
|
253
|
+
|--------|-------------|
|
|
254
|
+
| `analyze_data` | AI-powered data analysis with trends and insights |
|
|
255
|
+
| `create_report` | Generate comprehensive reports with AI assistance |
|
|
256
|
+
| `data_insights` | Discover hidden correlations and patterns |
|
|
257
|
+
| `optimize_workflow` | Get AI recommendations for workflow improvements |
|
|
258
|
+
|
|
241
259
|
## 🔧 Advanced Configuration
|
|
242
260
|
|
|
243
261
|
### Using with Smithery Cloud
|
|
@@ -346,6 +364,8 @@ lsof -ti:8010 | xargs kill -9
|
|
|
346
364
|
|
|
347
365
|
## 📦 Version History
|
|
348
366
|
|
|
367
|
+
- **v2.2.1** (2025-08-16) - 🔒 **Security release**: Fixed XSS and format string vulnerabilities
|
|
368
|
+
- **v2.2.0** (2025-08-16) - 🏆 **Major release**: Complete MCP 2024-11-05 protocol implementation
|
|
349
369
|
- **v1.6.0** (2025-08-15) - 🎆 **Major release**: Added batch operations & attachment management (33 total tools)
|
|
350
370
|
- **v1.5.0** (2025-08-15) - Added comprehensive schema management (23 total tools)
|
|
351
371
|
- **v1.4.0** (2025-08-14) - Added webhook support and enhanced CRUD operations (12 tools)
|
|
@@ -374,4 +394,4 @@ MIT License - see [LICENSE](./LICENSE) file for details
|
|
|
374
394
|
|
|
375
395
|
---
|
|
376
396
|
|
|
377
|
-
**Version**:
|
|
397
|
+
**Version**: 2.2.1 | **Status**: ✅ Production Ready | **MCP Protocol**: 2024-11-05 Complete | **Last Updated**: August 16, 2025
|
|
@@ -62,12 +62,15 @@ function log(level, message, metadata = {}) {
|
|
|
62
62
|
if (level <= currentLogLevel) {
|
|
63
63
|
const timestamp = new Date().toISOString();
|
|
64
64
|
const levelName = Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === level);
|
|
65
|
-
|
|
65
|
+
// Sanitize message to prevent format string attacks
|
|
66
|
+
const safeMessage = String(message).replace(/%/g, '%%');
|
|
67
|
+
const output = `[${timestamp}] [${levelName}] ${safeMessage}`;
|
|
66
68
|
|
|
67
69
|
if (Object.keys(metadata).length > 0) {
|
|
68
|
-
|
|
70
|
+
// Use separate arguments to avoid format string injection
|
|
71
|
+
console.log('%s %s', output, JSON.stringify(metadata));
|
|
69
72
|
} else {
|
|
70
|
-
console.log(output);
|
|
73
|
+
console.log('%s', output);
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
}
|
|
@@ -95,7 +98,7 @@ function checkRateLimit(clientId) {
|
|
|
95
98
|
return true;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
// Input validation
|
|
101
|
+
// Input validation and HTML escaping
|
|
99
102
|
function sanitizeInput(input) {
|
|
100
103
|
if (typeof input === 'string') {
|
|
101
104
|
return input.replace(/[<>]/g, '').trim().substring(0, 1000);
|
|
@@ -103,6 +106,29 @@ function sanitizeInput(input) {
|
|
|
103
106
|
return input;
|
|
104
107
|
}
|
|
105
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
|
+
|
|
106
132
|
// Airtable API integration
|
|
107
133
|
function callAirtableAPI(endpoint, method = 'GET', body = null, queryParams = {}) {
|
|
108
134
|
return new Promise((resolve, reject) => {
|
|
@@ -353,7 +379,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
353
379
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
354
380
|
res.end(JSON.stringify({
|
|
355
381
|
status: 'healthy',
|
|
356
|
-
version: '2.2.
|
|
382
|
+
version: '2.2.2',
|
|
357
383
|
timestamp: new Date().toISOString(),
|
|
358
384
|
uptime: process.uptime()
|
|
359
385
|
}));
|
|
@@ -369,57 +395,132 @@ const server = http.createServer(async (req, res) => {
|
|
|
369
395
|
const codeChallenge = params.code_challenge;
|
|
370
396
|
const codeChallengeMethod = params.code_challenge_method;
|
|
371
397
|
|
|
398
|
+
// Validate inputs to prevent XSS
|
|
399
|
+
if (!clientId || !redirectUri) {
|
|
400
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
401
|
+
res.end(JSON.stringify({ error: 'invalid_request', error_description: 'Missing required parameters' }));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Validate redirect URI
|
|
406
|
+
if (!validateUrl(redirectUri)) {
|
|
407
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
408
|
+
res.end(JSON.stringify({ error: 'invalid_request', error_description: 'Invalid redirect URI' }));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Sanitize all user inputs for HTML output
|
|
413
|
+
const safeClientId = escapeHtml(clientId);
|
|
414
|
+
const safeRedirectUri = escapeHtml(redirectUri);
|
|
415
|
+
const safeState = escapeHtml(state || '');
|
|
416
|
+
|
|
372
417
|
// Generate authorization code
|
|
373
418
|
const authCode = crypto.randomBytes(32).toString('hex');
|
|
374
419
|
|
|
375
420
|
// In a real implementation, store the auth code with expiration
|
|
376
421
|
// and associate it with the client and PKCE challenge
|
|
377
422
|
|
|
378
|
-
res.writeHead(200, {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
423
|
+
res.writeHead(200, {
|
|
424
|
+
'Content-Type': 'text/html',
|
|
425
|
+
'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';",
|
|
426
|
+
'X-Content-Type-Options': 'nosniff',
|
|
427
|
+
'X-Frame-Options': 'DENY',
|
|
428
|
+
'X-XSS-Protection': '1; mode=block',
|
|
429
|
+
'Referrer-Policy': 'no-referrer'
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
res.end(`<!DOCTYPE html>
|
|
433
|
+
<html>
|
|
434
|
+
<head>
|
|
435
|
+
<meta charset="UTF-8">
|
|
436
|
+
<title>OAuth2 Authorization</title>
|
|
437
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
438
|
+
</head>
|
|
439
|
+
<body>
|
|
440
|
+
<h2>Airtable MCP Server - OAuth2 Authorization</h2>
|
|
441
|
+
<p>Client ID: ${safeClientId}</p>
|
|
442
|
+
<p>Redirect URI: ${safeRedirectUri}</p>
|
|
443
|
+
<div style="margin: 20px 0;">
|
|
444
|
+
<button onclick="authorize()" style="background: #18BFFF; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;">
|
|
445
|
+
Authorize Application
|
|
446
|
+
</button>
|
|
447
|
+
<button onclick="deny()" style="background: #ff4444; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; margin-left: 10px;">
|
|
448
|
+
Deny Access
|
|
449
|
+
</button>
|
|
450
|
+
</div>
|
|
451
|
+
<script>
|
|
452
|
+
// Use validated and sanitized values to prevent XSS
|
|
453
|
+
(function() {
|
|
454
|
+
const baseUrl = ${JSON.stringify(redirectUri.slice(0, 2000))};
|
|
455
|
+
const code = ${JSON.stringify(authCode)};
|
|
456
|
+
const state = ${JSON.stringify((state || '').slice(0, 200))};
|
|
457
|
+
|
|
458
|
+
window.authorize = function() {
|
|
459
|
+
try {
|
|
460
|
+
// Additional validation in JavaScript
|
|
461
|
+
const url = new URL(baseUrl);
|
|
462
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
463
|
+
throw new Error('Invalid protocol');
|
|
399
464
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
465
|
+
const finalUrl = baseUrl + '?code=' + encodeURIComponent(code) + '&state=' + encodeURIComponent(state);
|
|
466
|
+
window.location.href = finalUrl;
|
|
467
|
+
} catch (e) {
|
|
468
|
+
console.error('Authorization failed:', e);
|
|
469
|
+
alert('Invalid redirect URL');
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
window.deny = function() {
|
|
474
|
+
try {
|
|
475
|
+
// Additional validation in JavaScript
|
|
476
|
+
const url = new URL(baseUrl);
|
|
477
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
478
|
+
throw new Error('Invalid protocol');
|
|
403
479
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
480
|
+
const finalUrl = baseUrl + '?error=access_denied&state=' + encodeURIComponent(state);
|
|
481
|
+
window.location.href = finalUrl;
|
|
482
|
+
} catch (e) {
|
|
483
|
+
console.error('Denial failed:', e);
|
|
484
|
+
alert('Invalid redirect URL');
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
})();
|
|
488
|
+
</script>
|
|
489
|
+
</body>
|
|
490
|
+
</html>`);
|
|
408
491
|
return;
|
|
409
492
|
}
|
|
410
493
|
|
|
411
494
|
// OAuth2 token endpoint
|
|
412
495
|
if (pathname === '/oauth/token' && req.method === 'POST') {
|
|
413
496
|
let body = '';
|
|
414
|
-
req.on('data', chunk =>
|
|
497
|
+
req.on('data', chunk => {
|
|
498
|
+
body += chunk.toString();
|
|
499
|
+
// Prevent DoS by limiting body size
|
|
500
|
+
if (body.length > 10000) {
|
|
501
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
502
|
+
res.end(JSON.stringify({ error: 'payload_too_large', error_description: 'Request body too large' }));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
});
|
|
415
506
|
|
|
416
507
|
req.on('end', () => {
|
|
417
508
|
try {
|
|
418
509
|
const params = querystring.parse(body);
|
|
419
|
-
const grantType = params.grant_type;
|
|
420
|
-
const code = params.code;
|
|
421
|
-
const codeVerifier = params.code_verifier;
|
|
422
|
-
const clientId = params.client_id;
|
|
510
|
+
const grantType = sanitizeInput(params.grant_type);
|
|
511
|
+
const code = sanitizeInput(params.code);
|
|
512
|
+
const codeVerifier = sanitizeInput(params.code_verifier);
|
|
513
|
+
const clientId = sanitizeInput(params.client_id);
|
|
514
|
+
|
|
515
|
+
// Validate required parameters
|
|
516
|
+
if (!grantType || !code || !clientId) {
|
|
517
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
518
|
+
res.end(JSON.stringify({
|
|
519
|
+
error: 'invalid_request',
|
|
520
|
+
error_description: 'Missing required parameters'
|
|
521
|
+
}));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
423
524
|
|
|
424
525
|
// In a real implementation, verify the authorization code and PKCE
|
|
425
526
|
if (grantType === 'authorization_code' && code) {
|
|
@@ -427,7 +528,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
427
528
|
const accessToken = crypto.randomBytes(32).toString('hex');
|
|
428
529
|
const refreshToken = crypto.randomBytes(32).toString('hex');
|
|
429
530
|
|
|
430
|
-
res.writeHead(200, {
|
|
531
|
+
res.writeHead(200, {
|
|
532
|
+
'Content-Type': 'application/json',
|
|
533
|
+
'Cache-Control': 'no-store',
|
|
534
|
+
'Pragma': 'no-cache'
|
|
535
|
+
});
|
|
431
536
|
res.end(JSON.stringify({
|
|
432
537
|
access_token: accessToken,
|
|
433
538
|
token_type: 'Bearer',
|
|
@@ -438,11 +543,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
438
543
|
} else {
|
|
439
544
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
440
545
|
res.end(JSON.stringify({
|
|
441
|
-
error: '
|
|
546
|
+
error: 'invalid_grant',
|
|
442
547
|
error_description: 'Invalid grant type or authorization code'
|
|
443
548
|
}));
|
|
444
549
|
}
|
|
445
550
|
} catch (error) {
|
|
551
|
+
log(LOG_LEVELS.WARN, 'OAuth token request parsing failed', { error: error.message });
|
|
446
552
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
447
553
|
res.end(JSON.stringify({
|
|
448
554
|
error: 'invalid_request',
|
|
@@ -507,7 +613,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
507
613
|
},
|
|
508
614
|
serverInfo: {
|
|
509
615
|
name: 'Airtable MCP Server Enhanced',
|
|
510
|
-
version: '2.2.
|
|
616
|
+
version: '2.2.2',
|
|
511
617
|
description: 'Complete MCP 2024-11-05 server with Prompts, Sampling, Roots, Logging, and OAuth2'
|
|
512
618
|
}
|
|
513
619
|
}
|
package/package.json
CHANGED