@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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/.github/ISSUE_TEMPLATE/custom.md +10 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/CLAUDE_INTEGRATION.md +109 -0
- package/CONTRIBUTING.md +81 -0
- package/Dockerfile +39 -0
- package/INSTALLATION.md +183 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/RELEASE_NOTES_v1.2.0.md +50 -0
- package/airtable-mcp-1.1.0.tgz +0 -0
- package/airtable_mcp/__init__.py +5 -0
- package/airtable_mcp/src/server.py +329 -0
- package/bin/airtable-crud-cli.js +445 -0
- package/bin/airtable-mcp.js +44 -0
- package/examples/airtable-crud-example.js +203 -0
- package/examples/building-mcp.md +6666 -0
- package/examples/claude_config.json +4 -0
- package/examples/env-demo.js +172 -0
- package/examples/example-tasks-update.json +23 -0
- package/examples/example-tasks.json +26 -0
- package/examples/example_usage.md +124 -0
- package/examples/sample-transform.js +76 -0
- package/examples/windsurf_mcp_config.json +17 -0
- package/index.js +179 -0
- package/inspector.py +148 -0
- package/inspector_server.py +301 -0
- package/package.json +40 -0
- package/publish-steps.txt +27 -0
- package/rashidazarang-airtable-mcp-1.1.0.tgz +0 -0
- package/rashidazarang-airtable-mcp-1.2.0.tgz +0 -0
- package/requirements.txt +10 -0
- package/setup.py +29 -0
- package/smithery.yaml +41 -0
- package/test_client.py +63 -0
|
@@ -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
|
+
};
|
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
|
+
});
|