@rashidazarang/airtable-mcp 1.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.
@@ -0,0 +1,4 @@
1
+ {
2
+ "airtable_token": "YOUR_AIRTABLE_TOKEN",
3
+ "base_id": "YOUR_BASE_ID"
4
+ }
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Demo script that uses the AIRTABLE_BASE_ID from the .env file
5
+ * Demonstrates various operations with the Airtable API
6
+ */
7
+
8
+ require('dotenv').config();
9
+ const baseUtils = require('../tools/airtable-base');
10
+ const crudUtils = require('../tools/airtable-crud');
11
+ const schemaUtils = require('../tools/airtable-schema');
12
+
13
+ // Constants
14
+ const DEMO_TABLE_NAME = 'ENV Demo Table';
15
+ const SAMPLE_RECORDS = [
16
+ { Name: 'Record from ENV Demo', Description: 'Created using AIRTABLE_BASE_ID from .env file', Status: 'Active' },
17
+ { Name: 'Another ENV Record', Description: 'Second record from the environment demo', Status: 'Pending' }
18
+ ];
19
+
20
+ async function runDemo() {
21
+ console.log('=================================');
22
+ console.log(' AIRTABLE ENV DEMO SCRIPT ');
23
+ console.log('=================================');
24
+
25
+ // Check environment variables
26
+ if (!process.env.AIRTABLE_PERSONAL_ACCESS_TOKEN) {
27
+ console.error('❌ Error: AIRTABLE_PERSONAL_ACCESS_TOKEN is not set in .env file');
28
+ process.exit(1);
29
+ }
30
+
31
+ if (!process.env.AIRTABLE_BASE_ID) {
32
+ console.error('❌ Error: AIRTABLE_BASE_ID is not set in .env file');
33
+ process.exit(1);
34
+ }
35
+
36
+ const baseId = process.env.AIRTABLE_BASE_ID;
37
+ console.log(`✅ Using AIRTABLE_BASE_ID: ${baseId}`);
38
+
39
+ try {
40
+ // Step 1: Verify base access
41
+ console.log('\nStep 1: Verifying access to the base...');
42
+ const baseAccess = await baseUtils.checkBaseAccess(baseId);
43
+
44
+ if (!baseAccess.accessible) {
45
+ console.error(`❌ Error: Cannot access base with ID ${baseId}`);
46
+ console.error(` Reason: ${baseAccess.error}`);
47
+ process.exit(1);
48
+ }
49
+
50
+ console.log(`✅ Access confirmed to base: ${baseAccess.name}`);
51
+
52
+ // Step 2: List existing tables
53
+ console.log('\nStep 2: Listing existing tables...');
54
+ const tables = await baseUtils.listTables(baseId);
55
+ console.log(`✅ Found ${tables.length} tables in the base`);
56
+
57
+ // Step 3: Check if our demo table exists
58
+ console.log('\nStep 3: Checking if demo table exists...');
59
+ const demoTableExists = await crudUtils.tableExists(baseId, DEMO_TABLE_NAME);
60
+
61
+ if (demoTableExists) {
62
+ console.log(`✅ Demo table "${DEMO_TABLE_NAME}" already exists`);
63
+ } else {
64
+ console.log(`ℹ️ Demo table "${DEMO_TABLE_NAME}" does not exist, creating it...`);
65
+
66
+ // Step 4: Create the demo table
67
+ console.log('\nStep 4: Creating the demo table...');
68
+ const tableConfig = {
69
+ name: DEMO_TABLE_NAME,
70
+ description: 'Table created from the Environment Demo script',
71
+ fields: [
72
+ {
73
+ name: 'Name',
74
+ type: 'singleLineText',
75
+ description: 'Record name'
76
+ },
77
+ {
78
+ name: 'Description',
79
+ type: 'multilineText',
80
+ description: 'Record description'
81
+ },
82
+ {
83
+ name: 'Status',
84
+ type: 'singleSelect',
85
+ options: {
86
+ choices: [
87
+ { name: 'Active' },
88
+ { name: 'Pending' },
89
+ { name: 'Completed' }
90
+ ]
91
+ },
92
+ description: 'Current status'
93
+ },
94
+ {
95
+ name: 'Created',
96
+ type: 'date',
97
+ options: {
98
+ dateFormat: {
99
+ name: 'local'
100
+ }
101
+ },
102
+ description: 'Creation date'
103
+ }
104
+ ]
105
+ };
106
+
107
+ await schemaUtils.createTable(baseId, tableConfig);
108
+ console.log(`✅ Created demo table: ${DEMO_TABLE_NAME}`);
109
+ }
110
+
111
+ // Step 5: Create sample records
112
+ console.log('\nStep 5: Creating sample records...');
113
+ // Add today's date to all records
114
+ const recordsWithDate = SAMPLE_RECORDS.map(record => ({
115
+ ...record,
116
+ Created: new Date().toISOString().split('T')[0] // Format as YYYY-MM-DD
117
+ }));
118
+
119
+ const createdRecords = await crudUtils.createRecords(baseId, DEMO_TABLE_NAME, recordsWithDate);
120
+ console.log(`✅ Created ${createdRecords.length} sample records`);
121
+
122
+ // Step 6: Read records back
123
+ console.log('\nStep 6: Reading records from the table...');
124
+ const records = await crudUtils.readRecords(baseId, DEMO_TABLE_NAME, 100);
125
+ console.log(`✅ Read ${records.length} records from the table`);
126
+
127
+ console.log('\nSample record:');
128
+ console.log(JSON.stringify(records[0], null, 2));
129
+
130
+ // Step 7: Update a record
131
+ console.log('\nStep 7: Updating the first record...');
132
+ const recordToUpdate = {
133
+ id: createdRecords[0].id,
134
+ fields: {
135
+ Description: createdRecords[0].Description + ' (UPDATED)',
136
+ Status: 'Completed'
137
+ }
138
+ };
139
+
140
+ const updatedRecords = await crudUtils.updateRecords(baseId, DEMO_TABLE_NAME, [recordToUpdate]);
141
+ console.log(`✅ Updated ${updatedRecords.length} record`);
142
+
143
+ // Step 8: Get the updated record
144
+ console.log('\nStep 8: Getting the updated record...');
145
+ const updatedRecord = await crudUtils.getRecord(baseId, DEMO_TABLE_NAME, createdRecords[0].id);
146
+ console.log('Updated record:');
147
+ console.log(JSON.stringify(updatedRecord, null, 2));
148
+
149
+ // Step 9: Demonstrate filtering records
150
+ console.log('\nStep 9: Filtering records by status...');
151
+ const completedRecords = await crudUtils.readRecords(baseId, DEMO_TABLE_NAME, 100, 'Status="Completed"');
152
+ console.log(`✅ Found ${completedRecords.length} records with Status="Completed"`);
153
+
154
+ console.log('\n=================================');
155
+ console.log(' ENV DEMO COMPLETED ');
156
+ console.log('=================================');
157
+ console.log('\nThis script demonstrated:');
158
+ console.log('1. Loading environment variables from .env file');
159
+ console.log('2. Accessing an Airtable base using AIRTABLE_BASE_ID');
160
+ console.log('3. Creating a table (if it doesn\'t exist)');
161
+ console.log('4. Creating, reading, and updating records');
162
+ console.log('5. Filtering records using Airtable formulas');
163
+ console.log('\nAll operations used the AIRTABLE_BASE_ID environment variable');
164
+
165
+ } catch (error) {
166
+ console.error(`❌ Error: ${error.message}`);
167
+ process.exit(1);
168
+ }
169
+ }
170
+
171
+ // Run the demo
172
+ runDemo();
@@ -0,0 +1,23 @@
1
+ [
2
+ {
3
+ "id": "rec1qeTzIUy1p8DF5",
4
+ "fields": {
5
+ "Status": "Completed",
6
+ "Description": "Implement the new feature requested by the client (UPDATED)"
7
+ }
8
+ },
9
+ {
10
+ "id": "recA443jGkhk4fe8B",
11
+ "fields": {
12
+ "Status": "Completed",
13
+ "Priority": "High"
14
+ }
15
+ },
16
+ {
17
+ "id": "recvMTGZYKi8Dcds4",
18
+ "fields": {
19
+ "Status": "In Progress",
20
+ "Description": "Write comprehensive documentation for the project (IN PROGRESS)"
21
+ }
22
+ }
23
+ ]
@@ -0,0 +1,26 @@
1
+ [
2
+ {
3
+ "id": "rec1qeTzIUy1p8DF5",
4
+ "Name": "Add new feature",
5
+ "Description": "Implement the new feature requested by the client",
6
+ "Status": "In Progress",
7
+ "Priority": "Medium",
8
+ "DueDate": "2024-01-15"
9
+ },
10
+ {
11
+ "id": "recA443jGkhk4fe8B",
12
+ "Name": "Fix login bug",
13
+ "Description": "Users are experiencing issues with the login process",
14
+ "Status": "In Progress",
15
+ "Priority": "Critical",
16
+ "DueDate": "2023-11-15"
17
+ },
18
+ {
19
+ "id": "recvMTGZYKi8Dcds4",
20
+ "Name": "Complete project documentation",
21
+ "Description": "Write comprehensive documentation for the project",
22
+ "Status": "In Progress",
23
+ "Priority": "High",
24
+ "DueDate": "2023-12-31"
25
+ }
26
+ ]
@@ -0,0 +1,124 @@
1
+ # Airtable MCP Example Usage
2
+
3
+ This document provides examples of how to use the Airtable MCP tools within a compatible MCP client like Cursor.
4
+
5
+ ## Base Management
6
+
7
+ ### List all available bases
8
+
9
+ ```
10
+ Using the Airtable MCP, please list all the bases I have access to.
11
+ ```
12
+
13
+ ### Set the active base
14
+
15
+ ```
16
+ Set the active Airtable base to "Project Management" (or use the base ID directly).
17
+ ```
18
+
19
+ ## Table Operations
20
+
21
+ ### List all tables in the current base
22
+
23
+ ```
24
+ Show me all the tables in my current Airtable base.
25
+ ```
26
+
27
+ ### View table structure
28
+
29
+ ```
30
+ Show me the structure of the "Tasks" table, including all fields and their types.
31
+ ```
32
+
33
+ ## Record Operations
34
+
35
+ ### List records
36
+
37
+ ```
38
+ Show me the first 10 records from the "Clients" table.
39
+ ```
40
+
41
+ ### Filter records
42
+
43
+ ```
44
+ Find all "Tasks" with a status of "In Progress" and due date before today.
45
+ ```
46
+
47
+ ### Get a specific record
48
+
49
+ ```
50
+ Get the record with ID "rec123456" from the "Projects" table.
51
+ ```
52
+
53
+ ### Create a new record
54
+
55
+ ```
56
+ Create a new record in the "Tasks" table with the following information:
57
+ - Title: "Complete project documentation"
58
+ - Status: "Not Started"
59
+ - Due Date: "2024-12-31"
60
+ - Assigned To: "John Smith"
61
+ ```
62
+
63
+ ### Update an existing record
64
+
65
+ ```
66
+ Update the task with ID "rec123456" in the "Tasks" table:
67
+ - Change status to "In Progress"
68
+ - Update due date to "2024-11-30"
69
+ ```
70
+
71
+ ### Delete a record
72
+
73
+ ```
74
+ Delete the record with ID "rec123456" from the "Tasks" table.
75
+ ```
76
+
77
+ ## Schema Management
78
+
79
+ ### Export the schema
80
+
81
+ ```
82
+ Export the schema of my current Airtable base in JSON format.
83
+ ```
84
+
85
+ ### Compare schemas
86
+
87
+ ```
88
+ Compare this schema with my current base schema to identify any differences.
89
+ ```
90
+
91
+ ## Data Migration
92
+
93
+ ### Generate field mapping
94
+
95
+ ```
96
+ Generate a field mapping between the "Clients" and "Customers" tables.
97
+ ```
98
+
99
+ ### Migrate data
100
+
101
+ ```
102
+ Migrate data from the "Clients" table to the "Customers" table using the generated mapping.
103
+ ```
104
+
105
+ ## Tips for Better Results
106
+
107
+ 1. **Be specific** when referencing table and field names
108
+ 2. **Use record IDs** when updating or deleting specific records
109
+ 3. **Use natural language** to describe the operations you want to perform
110
+ 4. **Check your base ID** is correctly set if you get unexpected results
111
+ 5. **Format JSON data** properly when creating or updating records
112
+
113
+ ## Combining Operations
114
+
115
+ You can combine multiple operations in a single request:
116
+
117
+ ```
118
+ Please help me organize my project data:
119
+ 1. First, show me all the tables in my base
120
+ 2. Then, list the overdue tasks (status is not "Complete" and due date is before today)
121
+ 3. Finally, update those tasks to have a status of "Urgent"
122
+ ```
123
+
124
+ The Airtable MCP can help with complex workflows by understanding your intentions and executing the appropriate sequence of operations.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Sample transform function for syncing data between tables
3
+ *
4
+ * This module demonstrates how to transform records when syncing
5
+ * between two tables with different schemas.
6
+ *
7
+ * To use with the airtable-crud.js sync command:
8
+ * node airtable-crud.js sync "Source Table" "Target Table" sample-transform.js
9
+ */
10
+
11
+ /**
12
+ * Transform function that converts records from source table format to target table format
13
+ * @param {Object} sourceRecord - Record from the source table
14
+ * @returns {Object} - Transformed record for the target table
15
+ */
16
+ function transform(sourceRecord) {
17
+ // Example: Converting a customer record to a simplified format
18
+
19
+ // Extract the needed fields
20
+ const { id, Name, Email, Phone, "Company Name": Company, "Date Added": DateAdded, Status } = sourceRecord;
21
+
22
+ // Create the transformed record
23
+ const transformedRecord = {
24
+ // You can optionally include the source record ID
25
+ // This is useful for updating existing records in sync operations
26
+ // "Source Record ID": id,
27
+
28
+ // Map fields from source to target
29
+ CustomerName: Name,
30
+ CustomerEmail: Email,
31
+ CustomerPhone: Phone || '',
32
+ Organization: Company || 'Individual',
33
+
34
+ // Transform dates
35
+ JoinDate: DateAdded,
36
+
37
+ // Add calculated fields
38
+ CustomerCategory: Company ? 'Business' : 'Individual',
39
+
40
+ // Transform status to a different format
41
+ IsActive: Status === 'Active',
42
+
43
+ // Add constant values
44
+ DataSource: 'Customer Table Sync',
45
+ LastSyncedAt: new Date().toISOString()
46
+ };
47
+
48
+ return transformedRecord;
49
+ }
50
+
51
+ // You can define other utility functions here
52
+
53
+ /**
54
+ * Helper function to clean and format phone numbers
55
+ * @param {string} phone - Raw phone number
56
+ * @returns {string} - Formatted phone number
57
+ */
58
+ function formatPhoneNumber(phone) {
59
+ if (!phone) return '';
60
+
61
+ // Remove non-numeric characters
62
+ const cleaned = ('' + phone).replace(/\D/g, '');
63
+
64
+ // Format as (XXX) XXX-XXXX
65
+ const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/);
66
+ if (match) {
67
+ return '(' + match[1] + ') ' + match[2] + '-' + match[3];
68
+ }
69
+
70
+ return phone;
71
+ }
72
+
73
+ // Export the transform function
74
+ module.exports = {
75
+ transform
76
+ };
@@ -0,0 +1,17 @@
1
+ {
2
+ "mcpServers": {
3
+ "AIRTABLE": {
4
+ "command": "npx",
5
+ "args": [
6
+ "-y",
7
+ "@smithery/cli@latest",
8
+ "run",
9
+ "@rashidazarang/airtable-mcp",
10
+ "--token",
11
+ "YOUR_AIRTABLE_TOKEN",
12
+ "--base",
13
+ "YOUR_BASE_ID"
14
+ ]
15
+ }
16
+ }
17
+ }
package/index.js ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+ const { execSync } = require('child_process');
5
+ const { spawn } = require('child_process');
6
+
7
+ // Polyfill for AbortController in older Node.js versions
8
+ if (typeof globalThis.AbortController === 'undefined') {
9
+ globalThis.AbortController = class AbortController {
10
+ constructor() {
11
+ this.signal = {
12
+ aborted: false,
13
+ addEventListener: () => {},
14
+ removeEventListener: () => {},
15
+ dispatchEvent: () => true
16
+ };
17
+ }
18
+ abort() {
19
+ this.signal.aborted = true;
20
+ }
21
+ };
22
+ console.log('ℹ️ Added AbortController polyfill for compatibility with older Node.js versions');
23
+ }
24
+
25
+ // Parse command-line arguments
26
+ const args = process.argv.slice(2);
27
+ let tokenIndex = args.indexOf('--token');
28
+ let baseIndex = args.indexOf('--base');
29
+ let configIndex = args.indexOf('--config');
30
+
31
+ // Extract token, base ID and config
32
+ const token = tokenIndex !== -1 && tokenIndex + 1 < args.length ? args[tokenIndex + 1] : null;
33
+ const baseId = baseIndex !== -1 && baseIndex + 1 < args.length ? args[baseIndex + 1] : null;
34
+ const config = configIndex !== -1 && configIndex + 1 < args.length ? args[configIndex + 1] : null;
35
+
36
+ console.log('🔌 Airtable MCP - Connecting your AI to Airtable');
37
+ console.log('-----------------------------------------------');
38
+
39
+ // Find Python interpreter
40
+ const getPythonPath = () => {
41
+ try {
42
+ const whichPython = execSync('which python3.10').toString().trim();
43
+ return whichPython;
44
+ } catch (e) {
45
+ try {
46
+ const whichPython = execSync('which python3').toString().trim();
47
+ return whichPython;
48
+ } catch (e) {
49
+ return 'python';
50
+ }
51
+ }
52
+ };
53
+
54
+ // Check Python version
55
+ const checkPythonVersion = (pythonPath) => {
56
+ try {
57
+ const versionStr = execSync(`${pythonPath} --version`).toString().trim();
58
+ const versionMatch = versionStr.match(/Python (\d+)\.(\d+)/);
59
+ if (versionMatch) {
60
+ const major = parseInt(versionMatch[1]);
61
+ const minor = parseInt(versionMatch[2]);
62
+ return (major > 3 || (major === 3 && minor >= 10));
63
+ }
64
+ return false;
65
+ } catch (e) {
66
+ return false;
67
+ }
68
+ };
69
+
70
+ const pythonPath = getPythonPath();
71
+
72
+ // Verify Python compatibility
73
+ if (!checkPythonVersion(pythonPath)) {
74
+ console.error('❌ Error: MCP SDK requires Python 3.10+');
75
+ console.error('Please install Python 3.10 or newer and try again.');
76
+ process.exit(1);
77
+ }
78
+
79
+ // We now use inspector_server.py instead of server.py
80
+ const serverScript = path.join(__dirname, 'inspector_server.py');
81
+
82
+ // Check if the script exists
83
+ try {
84
+ require('fs').accessSync(serverScript, require('fs').constants.F_OK);
85
+ } catch (e) {
86
+ console.error(`❌ Error: Could not find server script at ${serverScript}`);
87
+ console.error('Please make sure you have the complete package installed.');
88
+ process.exit(1);
89
+ }
90
+
91
+ // Prepare arguments for the Python script
92
+ const scriptArgs = [serverScript];
93
+ if (token) {
94
+ scriptArgs.push('--token', token);
95
+ }
96
+ if (baseId) {
97
+ scriptArgs.push('--base', baseId);
98
+ }
99
+ if (config) {
100
+ scriptArgs.push('--config', config);
101
+
102
+ // Try to extract and log info from config
103
+ try {
104
+ const configObj = JSON.parse(config);
105
+ if (configObj.airtable_token) {
106
+ console.log('✅ Using API token from config');
107
+ }
108
+ if (configObj.base_id) {
109
+ console.log(`✅ Using base ID from config: ${configObj.base_id}`);
110
+ }
111
+ } catch (e) {
112
+ console.warn('⚠️ Could not parse config JSON, attempting to sanitize...');
113
+
114
+ // Sanitize config JSON - fix common formatting issues
115
+ try {
116
+ // Remove any unexpected line breaks, extra quotes, and escape characters
117
+ const sanitizedConfig = config
118
+ .replace(/[\r\n]+/g, '')
119
+ .replace(/\\+"/g, '"')
120
+ .replace(/^"/, '')
121
+ .replace(/"$/, '')
122
+ .replace(/\\/g, '');
123
+
124
+ // Try parsing it
125
+ const configObj = JSON.parse(sanitizedConfig);
126
+ if (configObj) {
127
+ console.log('✅ Successfully sanitized config JSON');
128
+ // Update config with sanitized version
129
+ scriptArgs[scriptArgs.indexOf(config)] = sanitizedConfig;
130
+ config = sanitizedConfig;
131
+
132
+ if (configObj.airtable_token) {
133
+ console.log('✅ Using API token from sanitized config');
134
+ }
135
+ if (configObj.base_id) {
136
+ console.log(`✅ Using base ID from sanitized config: ${configObj.base_id}`);
137
+ }
138
+ }
139
+ } catch (sanitizeErr) {
140
+ console.warn('⚠️ Could not sanitize config JSON, passing it directly to Python script');
141
+ }
142
+ }
143
+ } else {
144
+ if (token) {
145
+ console.log('✅ Using provided API token');
146
+ } else {
147
+ console.log('⚠️ No API token provided, will try to use .env file');
148
+ }
149
+
150
+ if (baseId) {
151
+ console.log(`✅ Using base ID: ${baseId}`);
152
+ } else {
153
+ console.log('ℹ️ No base ID provided, will need to set one later');
154
+ }
155
+ }
156
+
157
+ // Execute the Python script
158
+ const serverProcess = spawn(pythonPath, scriptArgs, {
159
+ stdio: 'inherit',
160
+ });
161
+
162
+ // Handle process exit
163
+ serverProcess.on('close', (code) => {
164
+ if (code !== 0) {
165
+ console.error(`❌ Airtable MCP server exited with code ${code}`);
166
+ }
167
+ process.exit(code);
168
+ });
169
+
170
+ // Handle signals
171
+ process.on('SIGINT', () => {
172
+ console.log('\n👋 Shutting down Airtable MCP server...');
173
+ serverProcess.kill('SIGINT');
174
+ });
175
+
176
+ process.on('SIGTERM', () => {
177
+ console.log('\n👋 Shutting down Airtable MCP server...');
178
+ serverProcess.kill('SIGTERM');
179
+ });