@rashidazarang/airtable-mcp 2.1.1 → 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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Airtable MCP Server
2
2
 
3
+ [![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/rashidazarang/airtable-mcp)](https://archestra.ai/mcp-catalog/rashidazarang__airtable-mcp)
3
4
  [![smithery badge](https://smithery.ai/badge/@rashidazarang/airtable-mcp)](https://smithery.ai/server/@rashidazarang/airtable-mcp)
4
- [![Trust Score](https://archestra.ai/badge/@rashidazarang/airtable-mcp)](https://archestra.ai/mcp-catalog/rashidazarang__airtable-mcp)
5
5
  ![Airtable](https://img.shields.io/badge/Airtable-18BFFF?style=for-the-badge&logo=Airtable&logoColor=white)
6
6
  [![MCP](https://img.shields.io/badge/MCP-1.6.0-green)](https://github.com/rashidazarang/airtable-mcp)
7
7
 
@@ -56,18 +56,21 @@ const CONFIG = {
56
56
 
57
57
  // Logging
58
58
  const LOG_LEVELS = { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3, TRACE: 4 };
59
- const currentLogLevel = LOG_LEVELS[CONFIG.LOG_LEVEL] || LOG_LEVELS.INFO;
59
+ let currentLogLevel = LOG_LEVELS[CONFIG.LOG_LEVEL] || LOG_LEVELS.INFO;
60
60
 
61
61
  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) => {
@@ -236,6 +262,98 @@ const TOOLS_SCHEMA = [
236
262
  }
237
263
  ];
238
264
 
265
+ // Prompts schema - AI-powered templates for common Airtable operations
266
+ const PROMPTS_SCHEMA = [
267
+ {
268
+ name: 'analyze_data',
269
+ description: 'Analyze data patterns and provide insights from Airtable records',
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, summary, patterns, insights)',
279
+ required: false
280
+ },
281
+ {
282
+ name: 'field_focus',
283
+ description: 'Specific fields to focus the analysis on',
284
+ required: false
285
+ }
286
+ ]
287
+ },
288
+ {
289
+ name: 'create_report',
290
+ description: 'Generate a comprehensive report based on Airtable data',
291
+ arguments: [
292
+ {
293
+ name: 'table',
294
+ description: 'Table name or ID for the report',
295
+ required: true
296
+ },
297
+ {
298
+ name: 'report_type',
299
+ description: 'Type of report (summary, detailed, dashboard, metrics)',
300
+ required: false
301
+ },
302
+ {
303
+ name: 'time_period',
304
+ description: 'Time period for the report (if applicable)',
305
+ required: false
306
+ }
307
+ ]
308
+ },
309
+ {
310
+ name: 'data_insights',
311
+ description: 'Discover hidden insights and correlations in your Airtable data',
312
+ arguments: [
313
+ {
314
+ name: 'tables',
315
+ description: 'Comma-separated list of table names to analyze',
316
+ required: true
317
+ },
318
+ {
319
+ name: 'insight_type',
320
+ description: 'Type of insights to find (correlations, outliers, trends, predictions)',
321
+ required: false
322
+ }
323
+ ]
324
+ },
325
+ {
326
+ name: 'optimize_workflow',
327
+ description: 'Suggest workflow optimizations based on your Airtable usage patterns',
328
+ arguments: [
329
+ {
330
+ name: 'base_overview',
331
+ description: 'Overview of the base structure and usage',
332
+ required: false
333
+ },
334
+ {
335
+ name: 'optimization_focus',
336
+ description: 'Focus area (automation, fields, views, collaboration)',
337
+ required: false
338
+ }
339
+ ]
340
+ }
341
+ ];
342
+
343
+ // Roots configuration for filesystem access
344
+ const ROOTS_CONFIG = [
345
+ {
346
+ uri: 'file:///airtable-exports',
347
+ name: 'Airtable Exports'
348
+ },
349
+ {
350
+ uri: 'file:///airtable-attachments',
351
+ name: 'Airtable Attachments'
352
+ }
353
+ ];
354
+
355
+ // Logging configuration (currentLogLevel is already declared above)
356
+
239
357
  // HTTP server
240
358
  const server = http.createServer(async (req, res) => {
241
359
  // Security headers
@@ -261,13 +379,161 @@ const server = http.createServer(async (req, res) => {
261
379
  res.writeHead(200, { 'Content-Type': 'application/json' });
262
380
  res.end(JSON.stringify({
263
381
  status: 'healthy',
264
- version: '2.1.0',
382
+ version: '2.2.1',
265
383
  timestamp: new Date().toISOString(),
266
384
  uptime: process.uptime()
267
385
  }));
268
386
  return;
269
387
  }
270
388
 
389
+ // OAuth2 authorization endpoint
390
+ if (pathname === '/oauth/authorize' && req.method === 'GET') {
391
+ const params = parsedUrl.query;
392
+ const clientId = params.client_id;
393
+ const redirectUri = params.redirect_uri;
394
+ const state = params.state;
395
+ const codeChallenge = params.code_challenge;
396
+ const codeChallengeMethod = params.code_challenge_method;
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
+
417
+ // Generate authorization code
418
+ const authCode = crypto.randomBytes(32).toString('hex');
419
+
420
+ // In a real implementation, store the auth code with expiration
421
+ // and associate it with the client and PKCE challenge
422
+
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>`);
466
+ return;
467
+ }
468
+
469
+ // OAuth2 token endpoint
470
+ if (pathname === '/oauth/token' && req.method === 'POST') {
471
+ let body = '';
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
+ });
481
+
482
+ req.on('end', () => {
483
+ try {
484
+ const params = querystring.parse(body);
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
+ }
499
+
500
+ // In a real implementation, verify the authorization code and PKCE
501
+ if (grantType === 'authorization_code' && code) {
502
+ // Generate access token
503
+ const accessToken = crypto.randomBytes(32).toString('hex');
504
+ const refreshToken = crypto.randomBytes(32).toString('hex');
505
+
506
+ res.writeHead(200, {
507
+ 'Content-Type': 'application/json',
508
+ 'Cache-Control': 'no-store',
509
+ 'Pragma': 'no-cache'
510
+ });
511
+ res.end(JSON.stringify({
512
+ access_token: accessToken,
513
+ token_type: 'Bearer',
514
+ expires_in: 3600,
515
+ refresh_token: refreshToken,
516
+ scope: 'data.records:read data.records:write schema.bases:read'
517
+ }));
518
+ } else {
519
+ res.writeHead(400, { 'Content-Type': 'application/json' });
520
+ res.end(JSON.stringify({
521
+ error: 'invalid_grant',
522
+ error_description: 'Invalid grant type or authorization code'
523
+ }));
524
+ }
525
+ } catch (error) {
526
+ log(LOG_LEVELS.WARN, 'OAuth token request parsing failed', { error: error.message });
527
+ res.writeHead(400, { 'Content-Type': 'application/json' });
528
+ res.end(JSON.stringify({
529
+ error: 'invalid_request',
530
+ error_description: 'Malformed request body'
531
+ }));
532
+ }
533
+ });
534
+ return;
535
+ }
536
+
271
537
  // MCP endpoint
272
538
  if (pathname === '/mcp' && req.method === 'POST') {
273
539
  // Rate limiting
@@ -321,9 +587,9 @@ const server = http.createServer(async (req, res) => {
321
587
  logging: {}
322
588
  },
323
589
  serverInfo: {
324
- name: 'Airtable MCP Server',
325
- version: '2.1.0',
326
- description: 'Model Context Protocol server for Airtable integration'
590
+ name: 'Airtable MCP Server Enhanced',
591
+ version: '2.2.1',
592
+ description: 'Complete MCP 2024-11-05 server with Prompts, Sampling, Roots, Logging, and OAuth2'
327
593
  }
328
594
  }
329
595
  };
@@ -344,6 +610,47 @@ const server = http.createServer(async (req, res) => {
344
610
  response = await handleToolCall(request);
345
611
  break;
346
612
 
613
+ case 'prompts/list':
614
+ response = {
615
+ jsonrpc: '2.0',
616
+ id: request.id,
617
+ result: {
618
+ prompts: PROMPTS_SCHEMA
619
+ }
620
+ };
621
+ break;
622
+
623
+ case 'prompts/get':
624
+ response = await handlePromptGet(request);
625
+ break;
626
+
627
+ case 'roots/list':
628
+ response = {
629
+ jsonrpc: '2.0',
630
+ id: request.id,
631
+ result: {
632
+ roots: ROOTS_CONFIG
633
+ }
634
+ };
635
+ break;
636
+
637
+ case 'logging/setLevel':
638
+ const level = request.params?.level;
639
+ if (level && LOG_LEVELS[level.toUpperCase()] !== undefined) {
640
+ currentLogLevel = LOG_LEVELS[level.toUpperCase()];
641
+ log(LOG_LEVELS.INFO, 'Log level updated', { newLevel: level });
642
+ }
643
+ response = {
644
+ jsonrpc: '2.0',
645
+ id: request.id,
646
+ result: {}
647
+ };
648
+ break;
649
+
650
+ case 'sampling/createMessage':
651
+ response = await handleSampling(request);
652
+ break;
653
+
347
654
  default:
348
655
  log(LOG_LEVELS.WARN, 'Unknown method', { method: request.method });
349
656
  throw new Error(`Method "${request.method}" not found`);
@@ -476,6 +783,162 @@ async function handleToolCall(request) {
476
783
  }
477
784
  }
478
785
 
786
+ // Prompt handlers
787
+ async function handlePromptGet(request) {
788
+ const promptName = request.params.name;
789
+ const promptArgs = request.params.arguments || {};
790
+
791
+ try {
792
+ const prompt = PROMPTS_SCHEMA.find(p => p.name === promptName);
793
+ if (!prompt) {
794
+ throw new Error(`Prompt "${promptName}" not found`);
795
+ }
796
+
797
+ let messages = [];
798
+
799
+ switch (promptName) {
800
+ case 'analyze_data':
801
+ const { table, analysis_type = 'summary', field_focus } = promptArgs;
802
+ messages = [
803
+ {
804
+ role: 'user',
805
+ content: {
806
+ type: 'text',
807
+ text: `Please analyze the data in table "${table}".
808
+ Analysis type: ${analysis_type}
809
+ ${field_focus ? `Focus on fields: ${field_focus}` : ''}
810
+
811
+ First, list the tables and their schemas, then retrieve sample records from "${table}"
812
+ and provide insights based on the ${analysis_type} analysis type.`
813
+ }
814
+ }
815
+ ];
816
+ break;
817
+
818
+ case 'create_report':
819
+ const { table: reportTable, report_type = 'summary', time_period } = promptArgs;
820
+ messages = [
821
+ {
822
+ role: 'user',
823
+ content: {
824
+ type: 'text',
825
+ text: `Create a ${report_type} report for table "${reportTable}".
826
+ ${time_period ? `Time period: ${time_period}` : ''}
827
+
828
+ Please gather the table schema and recent records, then generate a comprehensive
829
+ ${report_type} report with key metrics, trends, and actionable insights.`
830
+ }
831
+ }
832
+ ];
833
+ break;
834
+
835
+ case 'data_insights':
836
+ const { tables, insight_type = 'correlations' } = promptArgs;
837
+ messages = [
838
+ {
839
+ role: 'user',
840
+ content: {
841
+ type: 'text',
842
+ text: `Discover ${insight_type} insights across these tables: ${tables}
843
+
844
+ Please examine the data structures and content to identify:
845
+ - ${insight_type} patterns
846
+ - Unexpected relationships
847
+ - Optimization opportunities
848
+ - Data quality insights`
849
+ }
850
+ }
851
+ ];
852
+ break;
853
+
854
+ case 'optimize_workflow':
855
+ const { base_overview, optimization_focus = 'automation' } = promptArgs;
856
+ messages = [
857
+ {
858
+ role: 'user',
859
+ content: {
860
+ type: 'text',
861
+ text: `Analyze the current Airtable setup and suggest ${optimization_focus} optimizations.
862
+ ${base_overview ? `Base overview: ${base_overview}` : ''}
863
+
864
+ Please review the table structures, field types, and relationships to recommend:
865
+ - ${optimization_focus} improvements
866
+ - Best practice implementations
867
+ - Performance enhancements
868
+ - Workflow streamlining opportunities`
869
+ }
870
+ }
871
+ ];
872
+ break;
873
+
874
+ default:
875
+ throw new Error(`Unsupported prompt: ${promptName}`);
876
+ }
877
+
878
+ return {
879
+ jsonrpc: '2.0',
880
+ id: request.id,
881
+ result: {
882
+ description: prompt.description,
883
+ messages: messages
884
+ }
885
+ };
886
+
887
+ } catch (error) {
888
+ log(LOG_LEVELS.ERROR, `Prompt ${promptName} failed`, { error: error.message });
889
+
890
+ return {
891
+ jsonrpc: '2.0',
892
+ id: request.id,
893
+ error: {
894
+ code: -32000,
895
+ message: `Error getting prompt ${promptName}: ${error.message}`
896
+ }
897
+ };
898
+ }
899
+ }
900
+
901
+ // Sampling handler
902
+ async function handleSampling(request) {
903
+ const { messages, modelPreferences } = request.params;
904
+
905
+ try {
906
+ // Note: In a real implementation, this would integrate with an LLM API
907
+ // For now, we'll return a structured response indicating sampling capability
908
+
909
+ log(LOG_LEVELS.INFO, 'Sampling request received', {
910
+ messageCount: messages?.length,
911
+ model: modelPreferences?.model
912
+ });
913
+
914
+ return {
915
+ jsonrpc: '2.0',
916
+ id: request.id,
917
+ result: {
918
+ model: modelPreferences?.model || 'claude-3-sonnet',
919
+ role: 'assistant',
920
+ content: {
921
+ type: 'text',
922
+ 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.'
923
+ },
924
+ stopReason: 'end_turn'
925
+ }
926
+ };
927
+
928
+ } catch (error) {
929
+ log(LOG_LEVELS.ERROR, 'Sampling failed', { error: error.message });
930
+
931
+ return {
932
+ jsonrpc: '2.0',
933
+ id: request.id,
934
+ error: {
935
+ code: -32000,
936
+ message: `Sampling error: ${error.message}`
937
+ }
938
+ };
939
+ }
940
+ }
941
+
479
942
  // Server startup
480
943
  const PORT = CONFIG.PORT;
481
944
  const HOST = CONFIG.HOST;
@@ -1,15 +1,6 @@
1
1
  {
2
2
  "mcpServers": {
3
3
  "airtable": {
4
- "command": "curl",
5
- "args": [
6
- "-s",
7
- "-X",
8
- "POST",
9
- "-H",
10
- "Content-Type: application/json",
11
- "http://localhost:8010/mcp"
12
- ],
13
4
  "url": "http://localhost:8010/mcp"
14
5
  }
15
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rashidazarang/airtable-mcp",
3
- "version": "2.1.1",
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": {