@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.
@@ -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
- const output = `[${timestamp}] [${levelName}] ${message}`;
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
- console.log(output, JSON.stringify(metadata));
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, "&amp;")
115
+ .replace(/</g, "&lt;")
116
+ .replace(/>/g, "&gt;")
117
+ .replace(/"/g, "&quot;")
118
+ .replace(/'/g, "&#039;")
119
+ .replace(/\//g, "&#x2F;");
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.0',
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, { 'Content-Type': 'text/html' });
379
- res.end(`
380
- <!DOCTYPE html>
381
- <html>
382
- <head><title>OAuth2 Authorization</title></head>
383
- <body>
384
- <h2>Airtable MCP Server - OAuth2 Authorization</h2>
385
- <p>Client ID: ${clientId}</p>
386
- <p>Redirect URI: ${redirectUri}</p>
387
- <div style="margin: 20px 0;">
388
- <button onclick="authorize()" style="background: #18BFFF; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;">
389
- Authorize Application
390
- </button>
391
- <button onclick="deny()" style="background: #ff4444; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; margin-left: 10px;">
392
- Deny Access
393
- </button>
394
- </div>
395
- <script>
396
- function authorize() {
397
- const url = '${redirectUri}?code=${authCode}&state=${state || ''}';
398
- window.location.href = url;
399
- }
400
- function deny() {
401
- const url = '${redirectUri}?error=access_denied&state=${state || ''}';
402
- window.location.href = url;
403
- }
404
- </script>
405
- </body>
406
- </html>
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 => body += chunk.toString());
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, { 'Content-Type': 'application/json' });
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: 'invalid_request',
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.0',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rashidazarang/airtable-mcp",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Airtable MCP server for Claude Desktop - Connect directly to Airtable using natural language",
5
5
  "main": "airtable_simple_production.js",
6
6
  "bin": {