@rashidazarang/airtable-mcp 2.1.1 → 2.2.0
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 +1 -1
- package/airtable_simple_production.js +387 -5
- package/examples/claude_simple_config.json +0 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Airtable MCP Server
|
|
2
2
|
|
|
3
|
+
[](https://archestra.ai/mcp-catalog/rashidazarang__airtable-mcp)
|
|
3
4
|
[](https://smithery.ai/server/@rashidazarang/airtable-mcp)
|
|
4
|
-
[](https://archestra.ai/mcp-catalog/rashidazarang__airtable-mcp)
|
|
5
5
|

|
|
6
6
|
[](https://github.com/rashidazarang/airtable-mcp)
|
|
7
7
|
|
|
@@ -56,7 +56,7 @@ const CONFIG = {
|
|
|
56
56
|
|
|
57
57
|
// Logging
|
|
58
58
|
const LOG_LEVELS = { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3, TRACE: 4 };
|
|
59
|
-
|
|
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) {
|
|
@@ -236,6 +236,98 @@ const TOOLS_SCHEMA = [
|
|
|
236
236
|
}
|
|
237
237
|
];
|
|
238
238
|
|
|
239
|
+
// Prompts schema - AI-powered templates for common Airtable operations
|
|
240
|
+
const PROMPTS_SCHEMA = [
|
|
241
|
+
{
|
|
242
|
+
name: 'analyze_data',
|
|
243
|
+
description: 'Analyze data patterns and provide insights from Airtable records',
|
|
244
|
+
arguments: [
|
|
245
|
+
{
|
|
246
|
+
name: 'table',
|
|
247
|
+
description: 'Table name or ID to analyze',
|
|
248
|
+
required: true
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'analysis_type',
|
|
252
|
+
description: 'Type of analysis (trends, summary, patterns, insights)',
|
|
253
|
+
required: false
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'field_focus',
|
|
257
|
+
description: 'Specific fields to focus the analysis on',
|
|
258
|
+
required: false
|
|
259
|
+
}
|
|
260
|
+
]
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'create_report',
|
|
264
|
+
description: 'Generate a comprehensive report based on Airtable data',
|
|
265
|
+
arguments: [
|
|
266
|
+
{
|
|
267
|
+
name: 'table',
|
|
268
|
+
description: 'Table name or ID for the report',
|
|
269
|
+
required: true
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'report_type',
|
|
273
|
+
description: 'Type of report (summary, detailed, dashboard, metrics)',
|
|
274
|
+
required: false
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'time_period',
|
|
278
|
+
description: 'Time period for the report (if applicable)',
|
|
279
|
+
required: false
|
|
280
|
+
}
|
|
281
|
+
]
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: 'data_insights',
|
|
285
|
+
description: 'Discover hidden insights and correlations in your Airtable data',
|
|
286
|
+
arguments: [
|
|
287
|
+
{
|
|
288
|
+
name: 'tables',
|
|
289
|
+
description: 'Comma-separated list of table names to analyze',
|
|
290
|
+
required: true
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'insight_type',
|
|
294
|
+
description: 'Type of insights to find (correlations, outliers, trends, predictions)',
|
|
295
|
+
required: false
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'optimize_workflow',
|
|
301
|
+
description: 'Suggest workflow optimizations based on your Airtable usage patterns',
|
|
302
|
+
arguments: [
|
|
303
|
+
{
|
|
304
|
+
name: 'base_overview',
|
|
305
|
+
description: 'Overview of the base structure and usage',
|
|
306
|
+
required: false
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: 'optimization_focus',
|
|
310
|
+
description: 'Focus area (automation, fields, views, collaboration)',
|
|
311
|
+
required: false
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
}
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
// Roots configuration for filesystem access
|
|
318
|
+
const ROOTS_CONFIG = [
|
|
319
|
+
{
|
|
320
|
+
uri: 'file:///airtable-exports',
|
|
321
|
+
name: 'Airtable Exports'
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
uri: 'file:///airtable-attachments',
|
|
325
|
+
name: 'Airtable Attachments'
|
|
326
|
+
}
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
// Logging configuration (currentLogLevel is already declared above)
|
|
330
|
+
|
|
239
331
|
// HTTP server
|
|
240
332
|
const server = http.createServer(async (req, res) => {
|
|
241
333
|
// Security headers
|
|
@@ -261,13 +353,106 @@ const server = http.createServer(async (req, res) => {
|
|
|
261
353
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
262
354
|
res.end(JSON.stringify({
|
|
263
355
|
status: 'healthy',
|
|
264
|
-
version: '2.
|
|
356
|
+
version: '2.2.0',
|
|
265
357
|
timestamp: new Date().toISOString(),
|
|
266
358
|
uptime: process.uptime()
|
|
267
359
|
}));
|
|
268
360
|
return;
|
|
269
361
|
}
|
|
270
362
|
|
|
363
|
+
// OAuth2 authorization endpoint
|
|
364
|
+
if (pathname === '/oauth/authorize' && req.method === 'GET') {
|
|
365
|
+
const params = parsedUrl.query;
|
|
366
|
+
const clientId = params.client_id;
|
|
367
|
+
const redirectUri = params.redirect_uri;
|
|
368
|
+
const state = params.state;
|
|
369
|
+
const codeChallenge = params.code_challenge;
|
|
370
|
+
const codeChallengeMethod = params.code_challenge_method;
|
|
371
|
+
|
|
372
|
+
// Generate authorization code
|
|
373
|
+
const authCode = crypto.randomBytes(32).toString('hex');
|
|
374
|
+
|
|
375
|
+
// In a real implementation, store the auth code with expiration
|
|
376
|
+
// and associate it with the client and PKCE challenge
|
|
377
|
+
|
|
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
|
+
`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// OAuth2 token endpoint
|
|
412
|
+
if (pathname === '/oauth/token' && req.method === 'POST') {
|
|
413
|
+
let body = '';
|
|
414
|
+
req.on('data', chunk => body += chunk.toString());
|
|
415
|
+
|
|
416
|
+
req.on('end', () => {
|
|
417
|
+
try {
|
|
418
|
+
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;
|
|
423
|
+
|
|
424
|
+
// In a real implementation, verify the authorization code and PKCE
|
|
425
|
+
if (grantType === 'authorization_code' && code) {
|
|
426
|
+
// Generate access token
|
|
427
|
+
const accessToken = crypto.randomBytes(32).toString('hex');
|
|
428
|
+
const refreshToken = crypto.randomBytes(32).toString('hex');
|
|
429
|
+
|
|
430
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
431
|
+
res.end(JSON.stringify({
|
|
432
|
+
access_token: accessToken,
|
|
433
|
+
token_type: 'Bearer',
|
|
434
|
+
expires_in: 3600,
|
|
435
|
+
refresh_token: refreshToken,
|
|
436
|
+
scope: 'data.records:read data.records:write schema.bases:read'
|
|
437
|
+
}));
|
|
438
|
+
} else {
|
|
439
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
440
|
+
res.end(JSON.stringify({
|
|
441
|
+
error: 'invalid_request',
|
|
442
|
+
error_description: 'Invalid grant type or authorization code'
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
447
|
+
res.end(JSON.stringify({
|
|
448
|
+
error: 'invalid_request',
|
|
449
|
+
error_description: 'Malformed request body'
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
271
456
|
// MCP endpoint
|
|
272
457
|
if (pathname === '/mcp' && req.method === 'POST') {
|
|
273
458
|
// Rate limiting
|
|
@@ -321,9 +506,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
321
506
|
logging: {}
|
|
322
507
|
},
|
|
323
508
|
serverInfo: {
|
|
324
|
-
name: 'Airtable MCP Server',
|
|
325
|
-
version: '2.
|
|
326
|
-
description: '
|
|
509
|
+
name: 'Airtable MCP Server Enhanced',
|
|
510
|
+
version: '2.2.0',
|
|
511
|
+
description: 'Complete MCP 2024-11-05 server with Prompts, Sampling, Roots, Logging, and OAuth2'
|
|
327
512
|
}
|
|
328
513
|
}
|
|
329
514
|
};
|
|
@@ -344,6 +529,47 @@ const server = http.createServer(async (req, res) => {
|
|
|
344
529
|
response = await handleToolCall(request);
|
|
345
530
|
break;
|
|
346
531
|
|
|
532
|
+
case 'prompts/list':
|
|
533
|
+
response = {
|
|
534
|
+
jsonrpc: '2.0',
|
|
535
|
+
id: request.id,
|
|
536
|
+
result: {
|
|
537
|
+
prompts: PROMPTS_SCHEMA
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
break;
|
|
541
|
+
|
|
542
|
+
case 'prompts/get':
|
|
543
|
+
response = await handlePromptGet(request);
|
|
544
|
+
break;
|
|
545
|
+
|
|
546
|
+
case 'roots/list':
|
|
547
|
+
response = {
|
|
548
|
+
jsonrpc: '2.0',
|
|
549
|
+
id: request.id,
|
|
550
|
+
result: {
|
|
551
|
+
roots: ROOTS_CONFIG
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
break;
|
|
555
|
+
|
|
556
|
+
case 'logging/setLevel':
|
|
557
|
+
const level = request.params?.level;
|
|
558
|
+
if (level && LOG_LEVELS[level.toUpperCase()] !== undefined) {
|
|
559
|
+
currentLogLevel = LOG_LEVELS[level.toUpperCase()];
|
|
560
|
+
log(LOG_LEVELS.INFO, 'Log level updated', { newLevel: level });
|
|
561
|
+
}
|
|
562
|
+
response = {
|
|
563
|
+
jsonrpc: '2.0',
|
|
564
|
+
id: request.id,
|
|
565
|
+
result: {}
|
|
566
|
+
};
|
|
567
|
+
break;
|
|
568
|
+
|
|
569
|
+
case 'sampling/createMessage':
|
|
570
|
+
response = await handleSampling(request);
|
|
571
|
+
break;
|
|
572
|
+
|
|
347
573
|
default:
|
|
348
574
|
log(LOG_LEVELS.WARN, 'Unknown method', { method: request.method });
|
|
349
575
|
throw new Error(`Method "${request.method}" not found`);
|
|
@@ -476,6 +702,162 @@ async function handleToolCall(request) {
|
|
|
476
702
|
}
|
|
477
703
|
}
|
|
478
704
|
|
|
705
|
+
// Prompt handlers
|
|
706
|
+
async function handlePromptGet(request) {
|
|
707
|
+
const promptName = request.params.name;
|
|
708
|
+
const promptArgs = request.params.arguments || {};
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
const prompt = PROMPTS_SCHEMA.find(p => p.name === promptName);
|
|
712
|
+
if (!prompt) {
|
|
713
|
+
throw new Error(`Prompt "${promptName}" not found`);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
let messages = [];
|
|
717
|
+
|
|
718
|
+
switch (promptName) {
|
|
719
|
+
case 'analyze_data':
|
|
720
|
+
const { table, analysis_type = 'summary', field_focus } = promptArgs;
|
|
721
|
+
messages = [
|
|
722
|
+
{
|
|
723
|
+
role: 'user',
|
|
724
|
+
content: {
|
|
725
|
+
type: 'text',
|
|
726
|
+
text: `Please analyze the data in table "${table}".
|
|
727
|
+
Analysis type: ${analysis_type}
|
|
728
|
+
${field_focus ? `Focus on fields: ${field_focus}` : ''}
|
|
729
|
+
|
|
730
|
+
First, list the tables and their schemas, then retrieve sample records from "${table}"
|
|
731
|
+
and provide insights based on the ${analysis_type} analysis type.`
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
];
|
|
735
|
+
break;
|
|
736
|
+
|
|
737
|
+
case 'create_report':
|
|
738
|
+
const { table: reportTable, report_type = 'summary', time_period } = promptArgs;
|
|
739
|
+
messages = [
|
|
740
|
+
{
|
|
741
|
+
role: 'user',
|
|
742
|
+
content: {
|
|
743
|
+
type: 'text',
|
|
744
|
+
text: `Create a ${report_type} report for table "${reportTable}".
|
|
745
|
+
${time_period ? `Time period: ${time_period}` : ''}
|
|
746
|
+
|
|
747
|
+
Please gather the table schema and recent records, then generate a comprehensive
|
|
748
|
+
${report_type} report with key metrics, trends, and actionable insights.`
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
];
|
|
752
|
+
break;
|
|
753
|
+
|
|
754
|
+
case 'data_insights':
|
|
755
|
+
const { tables, insight_type = 'correlations' } = promptArgs;
|
|
756
|
+
messages = [
|
|
757
|
+
{
|
|
758
|
+
role: 'user',
|
|
759
|
+
content: {
|
|
760
|
+
type: 'text',
|
|
761
|
+
text: `Discover ${insight_type} insights across these tables: ${tables}
|
|
762
|
+
|
|
763
|
+
Please examine the data structures and content to identify:
|
|
764
|
+
- ${insight_type} patterns
|
|
765
|
+
- Unexpected relationships
|
|
766
|
+
- Optimization opportunities
|
|
767
|
+
- Data quality insights`
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
];
|
|
771
|
+
break;
|
|
772
|
+
|
|
773
|
+
case 'optimize_workflow':
|
|
774
|
+
const { base_overview, optimization_focus = 'automation' } = promptArgs;
|
|
775
|
+
messages = [
|
|
776
|
+
{
|
|
777
|
+
role: 'user',
|
|
778
|
+
content: {
|
|
779
|
+
type: 'text',
|
|
780
|
+
text: `Analyze the current Airtable setup and suggest ${optimization_focus} optimizations.
|
|
781
|
+
${base_overview ? `Base overview: ${base_overview}` : ''}
|
|
782
|
+
|
|
783
|
+
Please review the table structures, field types, and relationships to recommend:
|
|
784
|
+
- ${optimization_focus} improvements
|
|
785
|
+
- Best practice implementations
|
|
786
|
+
- Performance enhancements
|
|
787
|
+
- Workflow streamlining opportunities`
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
];
|
|
791
|
+
break;
|
|
792
|
+
|
|
793
|
+
default:
|
|
794
|
+
throw new Error(`Unsupported prompt: ${promptName}`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return {
|
|
798
|
+
jsonrpc: '2.0',
|
|
799
|
+
id: request.id,
|
|
800
|
+
result: {
|
|
801
|
+
description: prompt.description,
|
|
802
|
+
messages: messages
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
} catch (error) {
|
|
807
|
+
log(LOG_LEVELS.ERROR, `Prompt ${promptName} failed`, { error: error.message });
|
|
808
|
+
|
|
809
|
+
return {
|
|
810
|
+
jsonrpc: '2.0',
|
|
811
|
+
id: request.id,
|
|
812
|
+
error: {
|
|
813
|
+
code: -32000,
|
|
814
|
+
message: `Error getting prompt ${promptName}: ${error.message}`
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Sampling handler
|
|
821
|
+
async function handleSampling(request) {
|
|
822
|
+
const { messages, modelPreferences } = request.params;
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
// Note: In a real implementation, this would integrate with an LLM API
|
|
826
|
+
// For now, we'll return a structured response indicating sampling capability
|
|
827
|
+
|
|
828
|
+
log(LOG_LEVELS.INFO, 'Sampling request received', {
|
|
829
|
+
messageCount: messages?.length,
|
|
830
|
+
model: modelPreferences?.model
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
return {
|
|
834
|
+
jsonrpc: '2.0',
|
|
835
|
+
id: request.id,
|
|
836
|
+
result: {
|
|
837
|
+
model: modelPreferences?.model || 'claude-3-sonnet',
|
|
838
|
+
role: 'assistant',
|
|
839
|
+
content: {
|
|
840
|
+
type: 'text',
|
|
841
|
+
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.'
|
|
842
|
+
},
|
|
843
|
+
stopReason: 'end_turn'
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
} catch (error) {
|
|
848
|
+
log(LOG_LEVELS.ERROR, 'Sampling failed', { error: error.message });
|
|
849
|
+
|
|
850
|
+
return {
|
|
851
|
+
jsonrpc: '2.0',
|
|
852
|
+
id: request.id,
|
|
853
|
+
error: {
|
|
854
|
+
code: -32000,
|
|
855
|
+
message: `Sampling error: ${error.message}`
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
479
861
|
// Server startup
|
|
480
862
|
const PORT = CONFIG.PORT;
|
|
481
863
|
const HOST = CONFIG.HOST;
|
package/package.json
CHANGED