@rashidazarang/airtable-mcp 2.2.0 → 2.2.1
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/airtable_simple_production.js +124 -43
- package/package.json +1 -1
|
@@ -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.1',
|
|
357
383
|
timestamp: new Date().toISOString(),
|
|
358
384
|
uptime: process.uptime()
|
|
359
385
|
}));
|
|
@@ -369,57 +395,107 @@ 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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
423
|
+
res.writeHead(200, {
|
|
424
|
+
'Content-Type': 'text/html',
|
|
425
|
+
'Content-Security-Policy': "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'",
|
|
426
|
+
'X-Content-Type-Options': 'nosniff',
|
|
427
|
+
'X-Frame-Options': 'DENY'
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
res.end(`<!DOCTYPE html>
|
|
431
|
+
<html>
|
|
432
|
+
<head>
|
|
433
|
+
<meta charset="UTF-8">
|
|
434
|
+
<title>OAuth2 Authorization</title>
|
|
435
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
436
|
+
</head>
|
|
437
|
+
<body>
|
|
438
|
+
<h2>Airtable MCP Server - OAuth2 Authorization</h2>
|
|
439
|
+
<p>Client ID: ${safeClientId}</p>
|
|
440
|
+
<p>Redirect URI: ${safeRedirectUri}</p>
|
|
441
|
+
<div style="margin: 20px 0;">
|
|
442
|
+
<button onclick="authorize()" style="background: #18BFFF; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;">
|
|
443
|
+
Authorize Application
|
|
444
|
+
</button>
|
|
445
|
+
<button onclick="deny()" style="background: #ff4444; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; margin-left: 10px;">
|
|
446
|
+
Deny Access
|
|
447
|
+
</button>
|
|
448
|
+
</div>
|
|
449
|
+
<script>
|
|
450
|
+
function authorize() {
|
|
451
|
+
const baseUrl = ${JSON.stringify(redirectUri)};
|
|
452
|
+
const code = ${JSON.stringify(authCode)};
|
|
453
|
+
const state = ${JSON.stringify(state || '')};
|
|
454
|
+
const url = baseUrl + '?code=' + encodeURIComponent(code) + '&state=' + encodeURIComponent(state);
|
|
455
|
+
window.location.href = url;
|
|
456
|
+
}
|
|
457
|
+
function deny() {
|
|
458
|
+
const baseUrl = ${JSON.stringify(redirectUri)};
|
|
459
|
+
const state = ${JSON.stringify(state || '')};
|
|
460
|
+
const url = baseUrl + '?error=access_denied&state=' + encodeURIComponent(state);
|
|
461
|
+
window.location.href = url;
|
|
462
|
+
}
|
|
463
|
+
</script>
|
|
464
|
+
</body>
|
|
465
|
+
</html>`);
|
|
408
466
|
return;
|
|
409
467
|
}
|
|
410
468
|
|
|
411
469
|
// OAuth2 token endpoint
|
|
412
470
|
if (pathname === '/oauth/token' && req.method === 'POST') {
|
|
413
471
|
let body = '';
|
|
414
|
-
req.on('data', chunk =>
|
|
472
|
+
req.on('data', chunk => {
|
|
473
|
+
body += chunk.toString();
|
|
474
|
+
// Prevent DoS by limiting body size
|
|
475
|
+
if (body.length > 10000) {
|
|
476
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
477
|
+
res.end(JSON.stringify({ error: 'payload_too_large', error_description: 'Request body too large' }));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
415
481
|
|
|
416
482
|
req.on('end', () => {
|
|
417
483
|
try {
|
|
418
484
|
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;
|
|
485
|
+
const grantType = sanitizeInput(params.grant_type);
|
|
486
|
+
const code = sanitizeInput(params.code);
|
|
487
|
+
const codeVerifier = sanitizeInput(params.code_verifier);
|
|
488
|
+
const clientId = sanitizeInput(params.client_id);
|
|
489
|
+
|
|
490
|
+
// Validate required parameters
|
|
491
|
+
if (!grantType || !code || !clientId) {
|
|
492
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
493
|
+
res.end(JSON.stringify({
|
|
494
|
+
error: 'invalid_request',
|
|
495
|
+
error_description: 'Missing required parameters'
|
|
496
|
+
}));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
423
499
|
|
|
424
500
|
// In a real implementation, verify the authorization code and PKCE
|
|
425
501
|
if (grantType === 'authorization_code' && code) {
|
|
@@ -427,7 +503,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
427
503
|
const accessToken = crypto.randomBytes(32).toString('hex');
|
|
428
504
|
const refreshToken = crypto.randomBytes(32).toString('hex');
|
|
429
505
|
|
|
430
|
-
res.writeHead(200, {
|
|
506
|
+
res.writeHead(200, {
|
|
507
|
+
'Content-Type': 'application/json',
|
|
508
|
+
'Cache-Control': 'no-store',
|
|
509
|
+
'Pragma': 'no-cache'
|
|
510
|
+
});
|
|
431
511
|
res.end(JSON.stringify({
|
|
432
512
|
access_token: accessToken,
|
|
433
513
|
token_type: 'Bearer',
|
|
@@ -438,11 +518,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
438
518
|
} else {
|
|
439
519
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
440
520
|
res.end(JSON.stringify({
|
|
441
|
-
error: '
|
|
521
|
+
error: 'invalid_grant',
|
|
442
522
|
error_description: 'Invalid grant type or authorization code'
|
|
443
523
|
}));
|
|
444
524
|
}
|
|
445
525
|
} catch (error) {
|
|
526
|
+
log(LOG_LEVELS.WARN, 'OAuth token request parsing failed', { error: error.message });
|
|
446
527
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
447
528
|
res.end(JSON.stringify({
|
|
448
529
|
error: 'invalid_request',
|
|
@@ -507,7 +588,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
507
588
|
},
|
|
508
589
|
serverInfo: {
|
|
509
590
|
name: 'Airtable MCP Server Enhanced',
|
|
510
|
-
version: '2.2.
|
|
591
|
+
version: '2.2.1',
|
|
511
592
|
description: 'Complete MCP 2024-11-05 server with Prompts, Sampling, Roots, Logging, and OAuth2'
|
|
512
593
|
}
|
|
513
594
|
}
|
package/package.json
CHANGED