@rashidazarang/airtable-mcp 1.2.0 ā 1.2.4
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/.claude/settings.local.json +9 -0
- package/CLAUDE_INTEGRATION.md +61 -74
- package/DEVELOPMENT.md +188 -0
- package/Dockerfile.node +20 -0
- package/ISSUE_RESPONSES.md +171 -0
- package/MCP_REVIEW_SUMMARY.md +140 -0
- package/QUICK_START.md +60 -0
- package/README.md +167 -127
- package/RELEASE_NOTES_v1.2.1.md +40 -0
- package/RELEASE_NOTES_v1.2.2.md +48 -0
- package/RELEASE_NOTES_v1.2.3.md +103 -0
- package/RELEASE_NOTES_v1.2.4.md +60 -0
- package/SECURITY_NOTICE.md +40 -0
- package/airtable_simple.js +277 -0
- package/cleanup.sh +69 -0
- package/examples/claude_simple_config.json +16 -0
- package/examples/python_debug_patch.txt +27 -0
- package/inspector_server.py +37 -1
- package/package.json +22 -19
- package/quick_test.sh +28 -0
- package/rashidazarang-airtable-mcp-1.2.1.tgz +0 -0
- package/simple_airtable_server.py +151 -0
- package/smithery.yaml +17 -13
- package/test_client.py +10 -3
- package/test_mcp_comprehensive.js +161 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Release Notes - v1.2.4
|
|
2
|
+
|
|
3
|
+
## š Security Fix Release
|
|
4
|
+
|
|
5
|
+
### Critical Security Fix
|
|
6
|
+
- **REMOVED hardcoded API tokens from test files** (Addresses Issue #7)
|
|
7
|
+
- `test_client.py` and `test_mcp_comprehensive.js` now require environment variables
|
|
8
|
+
- Added security notice documentation
|
|
9
|
+
- No exposed credentials in the codebase
|
|
10
|
+
|
|
11
|
+
### š Bug Fixes
|
|
12
|
+
|
|
13
|
+
#### Smithery Cloud Deployment Issues (Issues #5 and #6)
|
|
14
|
+
- **Fixed HTTP 400 errors** when using Smithery
|
|
15
|
+
- **Switched to JavaScript implementation** for Smithery deployment
|
|
16
|
+
- Updated `smithery.yaml` to use `airtable_simple.js` instead of problematic Python server
|
|
17
|
+
- Created dedicated `Dockerfile.node` for Node.js deployment
|
|
18
|
+
- Fixed authentication flow for Smithery connections
|
|
19
|
+
|
|
20
|
+
### š Documentation Updates
|
|
21
|
+
- Added `SECURITY_NOTICE.md` with token rotation instructions
|
|
22
|
+
- Created `.env.example` file for secure configuration
|
|
23
|
+
- Updated Dockerfile references for Glama listing (Issue #4)
|
|
24
|
+
|
|
25
|
+
### š§ Improvements
|
|
26
|
+
- Added environment variable support with dotenv
|
|
27
|
+
- Improved logging system with configurable levels (ERROR, WARN, INFO, DEBUG)
|
|
28
|
+
- Better error messages for missing credentials
|
|
29
|
+
|
|
30
|
+
### ā ļø Breaking Changes
|
|
31
|
+
- Test files now require environment variables:
|
|
32
|
+
```bash
|
|
33
|
+
export AIRTABLE_TOKEN="your_token"
|
|
34
|
+
export AIRTABLE_BASE_ID="your_base_id"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### š Migration Guide
|
|
38
|
+
|
|
39
|
+
1. **Update your environment variables:**
|
|
40
|
+
```bash
|
|
41
|
+
cp .env.example .env
|
|
42
|
+
# Edit .env with your credentials
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. **For Smithery users:**
|
|
46
|
+
- Reinstall the MCP to get the latest configuration
|
|
47
|
+
- The server now properly accepts credentials through Smithery's config
|
|
48
|
+
|
|
49
|
+
3. **For direct users:**
|
|
50
|
+
- Continue using command line arguments or switch to environment variables
|
|
51
|
+
- Both methods are supported
|
|
52
|
+
|
|
53
|
+
### š Notes
|
|
54
|
+
- All previously exposed tokens have been revoked
|
|
55
|
+
- Please use your own Airtable credentials
|
|
56
|
+
- Never commit API tokens to version control
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
**Full Changelog**: [v1.2.3...v1.2.4](https://github.com/rashidazarang/airtable-mcp/compare/v1.2.3...v1.2.4)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Security Notice
|
|
2
|
+
|
|
3
|
+
## Important: API Token Rotation Required
|
|
4
|
+
|
|
5
|
+
If you have been using or testing this repository before January 2025, please note that hardcoded API tokens were previously included in test files. These have been removed and replaced with environment variable requirements.
|
|
6
|
+
|
|
7
|
+
### Actions Required:
|
|
8
|
+
|
|
9
|
+
1. **If you used the exposed tokens**:
|
|
10
|
+
- These tokens have been revoked and are no longer valid
|
|
11
|
+
- You must use your own Airtable API credentials
|
|
12
|
+
|
|
13
|
+
2. **For all users**:
|
|
14
|
+
- Never commit API tokens to version control
|
|
15
|
+
- Always use environment variables or secure configuration files
|
|
16
|
+
- Add `.env` to your `.gitignore` file
|
|
17
|
+
|
|
18
|
+
### Secure Configuration
|
|
19
|
+
|
|
20
|
+
Set your credentials using environment variables:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
export AIRTABLE_TOKEN="your_personal_token_here"
|
|
24
|
+
export AIRTABLE_BASE_ID="your_base_id_here"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or create a `.env` file (never commit this):
|
|
28
|
+
|
|
29
|
+
```env
|
|
30
|
+
AIRTABLE_TOKEN=your_personal_token_here
|
|
31
|
+
AIRTABLE_BASE_ID=your_base_id_here
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Reporting Security Issues
|
|
35
|
+
|
|
36
|
+
If you discover any security vulnerabilities, please report them to:
|
|
37
|
+
- Open an issue on GitHub (without including sensitive details)
|
|
38
|
+
- Contact the maintainer directly for sensitive information
|
|
39
|
+
|
|
40
|
+
Thank you for helping keep this project secure.
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// Load environment variables from .env file if it exists
|
|
9
|
+
const envPath = path.join(__dirname, '.env');
|
|
10
|
+
if (fs.existsSync(envPath)) {
|
|
11
|
+
require('dotenv').config({ path: envPath });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Parse command line arguments with environment variable fallback
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
let tokenIndex = args.indexOf('--token');
|
|
17
|
+
let baseIndex = args.indexOf('--base');
|
|
18
|
+
|
|
19
|
+
// Use environment variables as fallback
|
|
20
|
+
const token = tokenIndex !== -1 ? args[tokenIndex + 1] : process.env.AIRTABLE_TOKEN || process.env.AIRTABLE_API_TOKEN;
|
|
21
|
+
const baseId = baseIndex !== -1 ? args[baseIndex + 1] : process.env.AIRTABLE_BASE_ID || process.env.AIRTABLE_BASE;
|
|
22
|
+
|
|
23
|
+
if (!token || !baseId) {
|
|
24
|
+
console.error('Error: Missing Airtable credentials');
|
|
25
|
+
console.error('\nUsage options:');
|
|
26
|
+
console.error(' 1. Command line: node airtable_simple.js --token YOUR_TOKEN --base YOUR_BASE_ID');
|
|
27
|
+
console.error(' 2. Environment variables: AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
|
|
28
|
+
console.error(' 3. .env file with AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Configure logging levels
|
|
33
|
+
const LOG_LEVELS = {
|
|
34
|
+
ERROR: 0,
|
|
35
|
+
WARN: 1,
|
|
36
|
+
INFO: 2,
|
|
37
|
+
DEBUG: 3
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const currentLogLevel = process.env.LOG_LEVEL ? LOG_LEVELS[process.env.LOG_LEVEL.toUpperCase()] || LOG_LEVELS.INFO : LOG_LEVELS.INFO;
|
|
41
|
+
|
|
42
|
+
function log(level, message, ...args) {
|
|
43
|
+
const levelName = Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === level);
|
|
44
|
+
const timestamp = new Date().toISOString();
|
|
45
|
+
|
|
46
|
+
if (level <= currentLogLevel) {
|
|
47
|
+
const prefix = `[${timestamp}] [${levelName}]`;
|
|
48
|
+
if (level === LOG_LEVELS.ERROR) {
|
|
49
|
+
console.error(prefix, message, ...args);
|
|
50
|
+
} else if (level === LOG_LEVELS.WARN) {
|
|
51
|
+
console.warn(prefix, message, ...args);
|
|
52
|
+
} else {
|
|
53
|
+
console.log(prefix, message, ...args);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
log(LOG_LEVELS.INFO, `Starting Airtable MCP server with token ${token.slice(0, 5)}...${token.slice(-5)} and base ${baseId}`);
|
|
59
|
+
|
|
60
|
+
// Create HTTP server
|
|
61
|
+
const server = http.createServer(async (req, res) => {
|
|
62
|
+
// Enable CORS
|
|
63
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
64
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
65
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
66
|
+
|
|
67
|
+
// Handle preflight request
|
|
68
|
+
if (req.method === 'OPTIONS') {
|
|
69
|
+
res.writeHead(200);
|
|
70
|
+
res.end();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Only handle POST requests to /mcp
|
|
75
|
+
if (req.method !== 'POST' || !req.url.endsWith('/mcp')) {
|
|
76
|
+
res.writeHead(404);
|
|
77
|
+
res.end();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let body = '';
|
|
82
|
+
req.on('data', chunk => {
|
|
83
|
+
body += chunk.toString();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
req.on('end', async () => {
|
|
87
|
+
try {
|
|
88
|
+
const request = JSON.parse(body);
|
|
89
|
+
|
|
90
|
+
// Handle JSON-RPC methods
|
|
91
|
+
if (request.method === 'resources/list') {
|
|
92
|
+
const response = {
|
|
93
|
+
jsonrpc: '2.0',
|
|
94
|
+
id: request.id,
|
|
95
|
+
result: {
|
|
96
|
+
resources: [
|
|
97
|
+
{
|
|
98
|
+
id: 'airtable_tables',
|
|
99
|
+
name: 'Airtable Tables',
|
|
100
|
+
description: 'Tables in your Airtable base'
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
106
|
+
res.end(JSON.stringify(response));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (request.method === 'prompts/list') {
|
|
111
|
+
const response = {
|
|
112
|
+
jsonrpc: '2.0',
|
|
113
|
+
id: request.id,
|
|
114
|
+
result: {
|
|
115
|
+
prompts: [
|
|
116
|
+
{
|
|
117
|
+
id: 'tables_prompt',
|
|
118
|
+
name: 'List Tables',
|
|
119
|
+
description: 'List all tables'
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
125
|
+
res.end(JSON.stringify(response));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Handle tool calls
|
|
130
|
+
if (request.method === 'tools/call') {
|
|
131
|
+
const toolName = request.params.name;
|
|
132
|
+
|
|
133
|
+
if (toolName === 'list_tables') {
|
|
134
|
+
// Call Airtable API to list tables
|
|
135
|
+
const result = await callAirtableAPI(`meta/bases/${baseId}/tables`);
|
|
136
|
+
const tables = result.tables || [];
|
|
137
|
+
|
|
138
|
+
const tableList = tables.map((table, i) =>
|
|
139
|
+
`${i+1}. ${table.name} (ID: ${table.id})`
|
|
140
|
+
).join('\n');
|
|
141
|
+
|
|
142
|
+
const response = {
|
|
143
|
+
jsonrpc: '2.0',
|
|
144
|
+
id: request.id,
|
|
145
|
+
result: {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: 'text',
|
|
149
|
+
text: tables.length > 0
|
|
150
|
+
? `Tables in this base:\n${tableList}`
|
|
151
|
+
: 'No tables found in this base.'
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
158
|
+
res.end(JSON.stringify(response));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (toolName === 'list_records') {
|
|
163
|
+
const tableName = request.params.arguments.table_name;
|
|
164
|
+
const maxRecords = request.params.arguments.max_records || 100;
|
|
165
|
+
|
|
166
|
+
// Call Airtable API to list records
|
|
167
|
+
const result = await callAirtableAPI(`${baseId}/${tableName}`, { maxRecords });
|
|
168
|
+
const records = result.records || [];
|
|
169
|
+
|
|
170
|
+
const recordList = records.map((record, i) => {
|
|
171
|
+
const fields = Object.entries(record.fields || {})
|
|
172
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
173
|
+
.join(', ');
|
|
174
|
+
return `${i+1}. ID: ${record.id} - ${fields}`;
|
|
175
|
+
}).join('\n');
|
|
176
|
+
|
|
177
|
+
const response = {
|
|
178
|
+
jsonrpc: '2.0',
|
|
179
|
+
id: request.id,
|
|
180
|
+
result: {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: 'text',
|
|
184
|
+
text: records.length > 0
|
|
185
|
+
? `Records:\n${recordList}`
|
|
186
|
+
: 'No records found in this table.'
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
193
|
+
res.end(JSON.stringify(response));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Tool not found
|
|
198
|
+
const response = {
|
|
199
|
+
jsonrpc: '2.0',
|
|
200
|
+
id: request.id,
|
|
201
|
+
error: {
|
|
202
|
+
code: -32601,
|
|
203
|
+
message: `Tool ${toolName} not found`
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
207
|
+
res.end(JSON.stringify(response));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Method not found
|
|
212
|
+
const response = {
|
|
213
|
+
jsonrpc: '2.0',
|
|
214
|
+
id: request.id,
|
|
215
|
+
error: {
|
|
216
|
+
code: -32601,
|
|
217
|
+
message: `Method ${request.method} not found`
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
221
|
+
res.end(JSON.stringify(response));
|
|
222
|
+
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('Error processing request:', error);
|
|
225
|
+
const response = {
|
|
226
|
+
jsonrpc: '2.0',
|
|
227
|
+
id: request.id || null,
|
|
228
|
+
error: {
|
|
229
|
+
code: -32000,
|
|
230
|
+
message: error.message || 'Unknown error'
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
234
|
+
res.end(JSON.stringify(response));
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Helper function to call Airtable API
|
|
240
|
+
function callAirtableAPI(endpoint, params = {}) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const queryParams = new URLSearchParams(params).toString();
|
|
243
|
+
const url = `https://api.airtable.com/v0/${endpoint}${queryParams ? '?' + queryParams : ''}`;
|
|
244
|
+
|
|
245
|
+
const options = {
|
|
246
|
+
headers: {
|
|
247
|
+
'Authorization': `Bearer ${token}`,
|
|
248
|
+
'Content-Type': 'application/json'
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
https.get(url, options, (response) => {
|
|
253
|
+
let data = '';
|
|
254
|
+
|
|
255
|
+
response.on('data', (chunk) => {
|
|
256
|
+
data += chunk;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
response.on('end', () => {
|
|
260
|
+
try {
|
|
261
|
+
resolve(JSON.parse(data));
|
|
262
|
+
} catch (e) {
|
|
263
|
+
reject(new Error(`Failed to parse Airtable response: ${e.message}`));
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}).on('error', (error) => {
|
|
267
|
+
reject(new Error(`Airtable API request failed: ${error.message}`));
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Start the server on port 8010
|
|
273
|
+
const PORT = 8010;
|
|
274
|
+
server.listen(PORT, () => {
|
|
275
|
+
console.log(`Airtable MCP server running at http://localhost:${PORT}/mcp`);
|
|
276
|
+
console.log(`For Claude, use this URL: http://localhost:${PORT}/mcp`);
|
|
277
|
+
});
|
package/cleanup.sh
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
echo "š§¹ Airtable MCP Cleanup Script"
|
|
4
|
+
echo "=============================="
|
|
5
|
+
|
|
6
|
+
# Function to check if port is in use
|
|
7
|
+
check_port() {
|
|
8
|
+
if lsof -Pi :8010 -sTCP:LISTEN -t >/dev/null ; then
|
|
9
|
+
echo "ā ļø Port 8010 is in use"
|
|
10
|
+
return 0
|
|
11
|
+
else
|
|
12
|
+
echo "ā
Port 8010 is free"
|
|
13
|
+
return 1
|
|
14
|
+
fi
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Function to kill processes on port 8010
|
|
18
|
+
kill_port() {
|
|
19
|
+
echo "š Killing processes on port 8010..."
|
|
20
|
+
lsof -ti:8010 | xargs kill -9 2>/dev/null
|
|
21
|
+
echo "ā
Port 8010 cleared"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Function to clean up temporary files
|
|
25
|
+
cleanup_files() {
|
|
26
|
+
echo "š§¹ Cleaning up temporary files..."
|
|
27
|
+
|
|
28
|
+
# Remove test files if they exist
|
|
29
|
+
if [ -f "test_mcp_comprehensive.js" ]; then
|
|
30
|
+
rm test_mcp_comprehensive.js
|
|
31
|
+
echo "ā
Removed test_mcp_comprehensive.js"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if [ -f "quick_test.sh" ]; then
|
|
35
|
+
rm quick_test.sh
|
|
36
|
+
echo "ā
Removed quick_test.sh"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [ -f "MCP_REVIEW_SUMMARY.md" ]; then
|
|
40
|
+
rm MCP_REVIEW_SUMMARY.md
|
|
41
|
+
echo "ā
Removed MCP_REVIEW_SUMMARY.md"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
if [ -f "cleanup.sh" ]; then
|
|
45
|
+
rm cleanup.sh
|
|
46
|
+
echo "ā
Removed cleanup.sh"
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Main cleanup process
|
|
51
|
+
echo "1. Checking port status..."
|
|
52
|
+
if check_port; then
|
|
53
|
+
echo "2. Clearing port 8010..."
|
|
54
|
+
kill_port
|
|
55
|
+
else
|
|
56
|
+
echo "2. Port is already free"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
echo "3. Cleaning up temporary files..."
|
|
60
|
+
cleanup_files
|
|
61
|
+
|
|
62
|
+
echo ""
|
|
63
|
+
echo "š Cleanup completed!"
|
|
64
|
+
echo ""
|
|
65
|
+
echo "To start the MCP server again:"
|
|
66
|
+
echo " node airtable_simple.js --token YOUR_TOKEN --base YOUR_BASE_ID"
|
|
67
|
+
echo ""
|
|
68
|
+
echo "To test the MCP:"
|
|
69
|
+
echo " npm run test"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Add proper error handling
|
|
2
|
+
import traceback
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# Override the default error handlers to format errors as proper JSON
|
|
6
|
+
def handle_exceptions(func):
|
|
7
|
+
async def wrapper(*args, **kwargs):
|
|
8
|
+
try:
|
|
9
|
+
return await func(*args, **kwargs)
|
|
10
|
+
except Exception as e:
|
|
11
|
+
error_trace = traceback.format_exc()
|
|
12
|
+
sys.stderr.write(f"Error in MCP handler: {str(e)}\n{error_trace}\n")
|
|
13
|
+
# Return a properly formatted JSON error
|
|
14
|
+
return {"error": {"code": -32000, "message": str(e)}}
|
|
15
|
+
return wrapper
|
|
16
|
+
|
|
17
|
+
# Apply the decorator to all RPC methods
|
|
18
|
+
original_rpc_method = app.rpc_method
|
|
19
|
+
def patched_rpc_method(*args, **kwargs):
|
|
20
|
+
def decorator(func):
|
|
21
|
+
wrapped_func = handle_exceptions(func)
|
|
22
|
+
return original_rpc_method(*args, **kwargs)(wrapped_func)
|
|
23
|
+
return decorator
|
|
24
|
+
|
|
25
|
+
# Then add this line right before creating the FastMCP instance:
|
|
26
|
+
# Replace app.rpc_method with our patched version
|
|
27
|
+
app.rpc_method = patched_rpc_method
|
package/inspector_server.py
CHANGED
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import logging
|
|
11
11
|
import requests
|
|
12
12
|
import argparse
|
|
13
|
+
import traceback
|
|
13
14
|
from typing import Optional, Dict, Any, List
|
|
14
15
|
|
|
15
16
|
try:
|
|
@@ -68,6 +69,36 @@ if args.config_json:
|
|
|
68
69
|
# Create MCP server
|
|
69
70
|
app = FastMCP("Airtable Tools")
|
|
70
71
|
|
|
72
|
+
# Add error handling wrapper for all MCP methods
|
|
73
|
+
def handle_exceptions(func):
|
|
74
|
+
"""Decorator to properly handle and format exceptions in MCP functions"""
|
|
75
|
+
async def wrapper(*args, **kwargs):
|
|
76
|
+
try:
|
|
77
|
+
return await func(*args, **kwargs)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
error_trace = traceback.format_exc()
|
|
80
|
+
logger.error(f"Error in MCP handler: {str(e)}\n{error_trace}")
|
|
81
|
+
sys.stderr.write(f"Error in MCP handler: {str(e)}\n{error_trace}\n")
|
|
82
|
+
|
|
83
|
+
# For tool functions that return strings, return a formatted error message
|
|
84
|
+
if hasattr(func, "__annotations__") and func.__annotations__.get("return") == str:
|
|
85
|
+
return f"Error: {str(e)}"
|
|
86
|
+
|
|
87
|
+
# For RPC methods that return dicts, return a properly formatted JSON error
|
|
88
|
+
return {"error": {"code": -32000, "message": str(e)}}
|
|
89
|
+
return wrapper
|
|
90
|
+
|
|
91
|
+
# Patch the tool method to automatically apply error handling
|
|
92
|
+
original_tool = app.tool
|
|
93
|
+
def patched_tool(*args, **kwargs):
|
|
94
|
+
def decorator(func):
|
|
95
|
+
wrapped_func = handle_exceptions(func)
|
|
96
|
+
return original_tool(*args, **kwargs)(wrapped_func)
|
|
97
|
+
return decorator
|
|
98
|
+
|
|
99
|
+
# Replace app.tool with our patched version
|
|
100
|
+
app.tool = patched_tool
|
|
101
|
+
|
|
71
102
|
# Get token from arguments, config, or environment
|
|
72
103
|
token = args.api_token or config.get("airtable_token", "") or os.environ.get("AIRTABLE_PERSONAL_ACCESS_TOKEN", "")
|
|
73
104
|
# Clean up token if it has trailing quote
|
|
@@ -297,5 +328,10 @@ async def set_base_id(base_id_param: str) -> str:
|
|
|
297
328
|
base_id = base_id_param
|
|
298
329
|
return f"Base ID set to: {base_id}"
|
|
299
330
|
|
|
331
|
+
# Note: rpc_method is not available in the current MCP version
|
|
332
|
+
# These methods would be used for Claude-specific functionality
|
|
333
|
+
# but are not needed for basic MCP operation
|
|
334
|
+
|
|
335
|
+
# Start the server
|
|
300
336
|
if __name__ == "__main__":
|
|
301
|
-
app.
|
|
337
|
+
app.start()
|
package/package.json
CHANGED
|
@@ -1,40 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rashidazarang/airtable-mcp",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "Airtable MCP for
|
|
5
|
-
"main": "
|
|
3
|
+
"version": "1.2.4",
|
|
4
|
+
"description": "Airtable MCP for Claude Desktop - Connect directly to Airtable using natural language",
|
|
5
|
+
"main": "airtable_simple.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"airtable-mcp": "
|
|
7
|
+
"airtable-mcp": "./airtable_simple.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"start": "node
|
|
10
|
+
"start": "node airtable_simple.js",
|
|
11
|
+
"start:python": "python3.10 inspector_server.py",
|
|
12
|
+
"test": "node test_mcp_comprehensive.js",
|
|
13
|
+
"test:quick": "./quick_test.sh",
|
|
14
|
+
"dev": "node airtable_simple.js --token YOUR_TOKEN --base YOUR_BASE_ID"
|
|
11
15
|
},
|
|
12
16
|
"keywords": [
|
|
13
17
|
"airtable",
|
|
14
18
|
"mcp",
|
|
15
|
-
"ai",
|
|
16
19
|
"claude",
|
|
20
|
+
"claude-desktop",
|
|
17
21
|
"anthropic",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"smithery",
|
|
21
|
-
"model-context-protocol",
|
|
22
|
-
"windsurf"
|
|
22
|
+
"ai",
|
|
23
|
+
"database"
|
|
23
24
|
],
|
|
24
25
|
"author": "Rashid Azarang",
|
|
25
26
|
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@smithery/cli": "^1.0.0",
|
|
29
|
+
"airtable": "^0.12.2",
|
|
30
|
+
"dotenv": "^16.0.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=14.0.0"
|
|
34
|
+
},
|
|
26
35
|
"repository": {
|
|
27
36
|
"type": "git",
|
|
28
|
-
"url": "
|
|
37
|
+
"url": "https://github.com/rashidazarang/airtable-mcp"
|
|
29
38
|
},
|
|
30
39
|
"bugs": {
|
|
31
40
|
"url": "https://github.com/rashidazarang/airtable-mcp/issues"
|
|
32
41
|
},
|
|
33
|
-
"homepage": "https://github.com/rashidazarang/airtable-mcp#readme"
|
|
34
|
-
"engines": {
|
|
35
|
-
"node": ">=14.0.0"
|
|
36
|
-
},
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"dotenv": "^16.0.3"
|
|
39
|
-
}
|
|
42
|
+
"homepage": "https://github.com/rashidazarang/airtable-mcp#readme"
|
|
40
43
|
}
|
package/quick_test.sh
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
echo "š Airtable MCP Quick Tests"
|
|
4
|
+
echo "==========================="
|
|
5
|
+
|
|
6
|
+
# Test 1: List Tables
|
|
7
|
+
echo "š Testing: List Tables"
|
|
8
|
+
curl -s -X POST http://localhost:8010/mcp \
|
|
9
|
+
-H "Content-Type: application/json" \
|
|
10
|
+
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "list_tables"}}' | jq '.result.content[0].text'
|
|
11
|
+
|
|
12
|
+
echo -e "\n"
|
|
13
|
+
|
|
14
|
+
# Test 2: List Records from Requests
|
|
15
|
+
echo "š Testing: List Records (Requests)"
|
|
16
|
+
curl -s -X POST http://localhost:8010/mcp \
|
|
17
|
+
-H "Content-Type: application/json" \
|
|
18
|
+
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "list_records", "arguments": {"table_name": "requests", "max_records": 2}}}' | jq '.result.content[0].text'
|
|
19
|
+
|
|
20
|
+
echo -e "\n"
|
|
21
|
+
|
|
22
|
+
# Test 3: List Records from Providers
|
|
23
|
+
echo "š„ Testing: List Records (Providers)"
|
|
24
|
+
curl -s -X POST http://localhost:8010/mcp \
|
|
25
|
+
-H "Content-Type: application/json" \
|
|
26
|
+
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "list_records", "arguments": {"table_name": "providers", "max_records": 2}}}' | jq '.result.content[0].text'
|
|
27
|
+
|
|
28
|
+
echo -e "\nā
All quick tests completed!"
|
|
Binary file
|