@rashidazarang/airtable-mcp 1.2.1 โ 1.4.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/.claude/settings.local.json +12 -0
- package/CAPABILITY_REPORT.md +118 -0
- package/CLAUDE_INTEGRATION.md +61 -74
- package/DEVELOPMENT.md +189 -0
- package/Dockerfile.node +20 -0
- package/IMPROVEMENT_PROPOSAL.md +371 -0
- package/ISSUE_RESPONSES.md +171 -0
- package/MCP_REVIEW_SUMMARY.md +141 -0
- package/QUICK_START.md +60 -0
- package/README.md +167 -143
- 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 +104 -0
- package/RELEASE_NOTES_v1.2.4.md +60 -0
- package/RELEASE_NOTES_v1.4.0.md +104 -0
- package/SECURITY_NOTICE.md +40 -0
- package/airtable_enhanced.js +499 -0
- package/airtable_simple.js +653 -0
- package/airtable_simple_v1.2.4_backup.js +277 -0
- package/airtable_v1.4.0.js +654 -0
- package/cleanup.sh +70 -0
- package/examples/claude_simple_config.json +16 -0
- package/examples/python_debug_patch.txt +27 -0
- package/inspector_server.py +34 -44
- package/package.json +22 -19
- package/quick_test.sh +29 -0
- package/simple_airtable_server.py +151 -0
- package/smithery.yaml +17 -13
- package/test_all_features.sh +146 -0
- package/test_all_operations.sh +120 -0
- package/test_client.py +10 -3
- package/test_enhanced_features.js +389 -0
- package/test_mcp_comprehensive.js +162 -0
- package/test_mock_server.js +180 -0
- package/test_v1.4.0_final.sh +131 -0
- package/test_webhooks.sh +105 -0
package/cleanup.sh
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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"
|
|
70
|
+
|
|
@@ -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,50 +328,9 @@ 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
|
|
|
300
|
-
#
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
"""List available Airtable resources for Claude"""
|
|
304
|
-
resources = [
|
|
305
|
-
{
|
|
306
|
-
"id": "airtable_bases",
|
|
307
|
-
"name": "Airtable Bases",
|
|
308
|
-
"description": "The Airtable bases accessible with your API token"
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
"id": "airtable_tables",
|
|
312
|
-
"name": "Airtable Tables",
|
|
313
|
-
"description": "Tables in your current Airtable base"
|
|
314
|
-
},
|
|
315
|
-
{
|
|
316
|
-
"id": "airtable_records",
|
|
317
|
-
"name": "Airtable Records",
|
|
318
|
-
"description": "Records in your Airtable tables"
|
|
319
|
-
}
|
|
320
|
-
]
|
|
321
|
-
return {"resources": resources}
|
|
322
|
-
|
|
323
|
-
@app.rpc_method("prompts/list")
|
|
324
|
-
async def prompts_list(params: Dict = None) -> Dict:
|
|
325
|
-
"""List available prompts for Claude"""
|
|
326
|
-
prompts = [
|
|
327
|
-
{
|
|
328
|
-
"id": "list_tables_prompt",
|
|
329
|
-
"name": "List Tables",
|
|
330
|
-
"description": "List all tables in your Airtable base"
|
|
331
|
-
},
|
|
332
|
-
{
|
|
333
|
-
"id": "list_records_prompt",
|
|
334
|
-
"name": "List Records",
|
|
335
|
-
"description": "List records from a specific Airtable table"
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
"id": "create_record_prompt",
|
|
339
|
-
"name": "Create Record",
|
|
340
|
-
"description": "Create a new record in an Airtable table"
|
|
341
|
-
}
|
|
342
|
-
]
|
|
343
|
-
return {"prompts": prompts}
|
|
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
|
|
344
334
|
|
|
345
335
|
# Start the server
|
|
346
336
|
if __name__ == "__main__":
|
package/package.json
CHANGED
|
@@ -1,40 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rashidazarang/airtable-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Airtable MCP for
|
|
5
|
-
"main": "
|
|
3
|
+
"version": "1.4.0",
|
|
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,29 @@
|
|
|
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!"
|
|
29
|
+
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Simple Airtable MCP Server for Claude
|
|
4
|
+
-------------------------------------
|
|
5
|
+
A minimal MCP server that implements Airtable tools and Claude's special methods
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import requests
|
|
12
|
+
import traceback
|
|
13
|
+
from typing import Dict, Any, List, Optional
|
|
14
|
+
|
|
15
|
+
# Check if MCP SDK is installed
|
|
16
|
+
try:
|
|
17
|
+
from mcp.server.fastmcp import FastMCP
|
|
18
|
+
except ImportError:
|
|
19
|
+
print("Error: MCP SDK not found. Please install with 'pip install mcp'")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
# Parse command line arguments
|
|
23
|
+
if len(sys.argv) < 5:
|
|
24
|
+
print("Usage: python3 simple_airtable_server.py --token YOUR_TOKEN --base YOUR_BASE_ID")
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
# Get the token and base ID from command line arguments
|
|
28
|
+
token = None
|
|
29
|
+
base_id = None
|
|
30
|
+
for i in range(1, len(sys.argv)):
|
|
31
|
+
if sys.argv[i] == "--token" and i+1 < len(sys.argv):
|
|
32
|
+
token = sys.argv[i+1]
|
|
33
|
+
elif sys.argv[i] == "--base" and i+1 < len(sys.argv):
|
|
34
|
+
base_id = sys.argv[i+1]
|
|
35
|
+
|
|
36
|
+
if not token:
|
|
37
|
+
print("Error: No Airtable token provided. Use --token parameter.")
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
if not base_id:
|
|
41
|
+
print("Error: No base ID provided. Use --base parameter.")
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
# Set up logging
|
|
45
|
+
logging.basicConfig(level=logging.INFO)
|
|
46
|
+
logger = logging.getLogger("airtable-mcp")
|
|
47
|
+
|
|
48
|
+
# Create MCP server
|
|
49
|
+
app = FastMCP("Airtable Tools")
|
|
50
|
+
|
|
51
|
+
# Helper function for Airtable API calls
|
|
52
|
+
async def airtable_api_call(endpoint, method="GET", data=None, params=None):
|
|
53
|
+
"""Make an Airtable API call with error handling"""
|
|
54
|
+
headers = {
|
|
55
|
+
"Authorization": f"Bearer {token}",
|
|
56
|
+
"Content-Type": "application/json"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
url = f"https://api.airtable.com/v0/{endpoint}"
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
if method == "GET":
|
|
63
|
+
response = requests.get(url, headers=headers, params=params)
|
|
64
|
+
elif method == "POST":
|
|
65
|
+
response = requests.post(url, headers=headers, json=data)
|
|
66
|
+
else:
|
|
67
|
+
raise ValueError(f"Unsupported method: {method}")
|
|
68
|
+
|
|
69
|
+
response.raise_for_status()
|
|
70
|
+
return response.json()
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"API call error: {str(e)}")
|
|
73
|
+
return {"error": str(e)}
|
|
74
|
+
|
|
75
|
+
# Claude-specific methods
|
|
76
|
+
@app.rpc_method("resources/list")
|
|
77
|
+
async def resources_list(params: Dict = None) -> Dict:
|
|
78
|
+
"""List available Airtable resources for Claude"""
|
|
79
|
+
try:
|
|
80
|
+
# Return a simple list of resources
|
|
81
|
+
resources = [
|
|
82
|
+
{"id": "airtable_tables", "name": "Airtable Tables", "description": "Tables in your Airtable base"}
|
|
83
|
+
]
|
|
84
|
+
return {"resources": resources}
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(f"Error in resources/list: {str(e)}")
|
|
87
|
+
return {"error": {"code": -32000, "message": str(e)}}
|
|
88
|
+
|
|
89
|
+
@app.rpc_method("prompts/list")
|
|
90
|
+
async def prompts_list(params: Dict = None) -> Dict:
|
|
91
|
+
"""List available prompts for Claude"""
|
|
92
|
+
try:
|
|
93
|
+
# Return a simple list of prompts
|
|
94
|
+
prompts = [
|
|
95
|
+
{"id": "tables_prompt", "name": "List Tables", "description": "List all tables"}
|
|
96
|
+
]
|
|
97
|
+
return {"prompts": prompts}
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Error in prompts/list: {str(e)}")
|
|
100
|
+
return {"error": {"code": -32000, "message": str(e)}}
|
|
101
|
+
|
|
102
|
+
# Airtable tool functions
|
|
103
|
+
@app.tool()
|
|
104
|
+
async def list_tables() -> str:
|
|
105
|
+
"""List all tables in the specified base"""
|
|
106
|
+
try:
|
|
107
|
+
result = await airtable_api_call(f"meta/bases/{base_id}/tables")
|
|
108
|
+
|
|
109
|
+
if "error" in result:
|
|
110
|
+
return f"Error: {result['error']}"
|
|
111
|
+
|
|
112
|
+
tables = result.get("tables", [])
|
|
113
|
+
if not tables:
|
|
114
|
+
return "No tables found in this base."
|
|
115
|
+
|
|
116
|
+
table_list = [f"{i+1}. {table['name']} (ID: {table['id']})"
|
|
117
|
+
for i, table in enumerate(tables)]
|
|
118
|
+
return "Tables in this base:\n" + "\n".join(table_list)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return f"Error listing tables: {str(e)}"
|
|
121
|
+
|
|
122
|
+
@app.tool()
|
|
123
|
+
async def list_records(table_name: str, max_records: int = 100) -> str:
|
|
124
|
+
"""List records from a table"""
|
|
125
|
+
try:
|
|
126
|
+
params = {"maxRecords": max_records}
|
|
127
|
+
result = await airtable_api_call(f"{base_id}/{table_name}", params=params)
|
|
128
|
+
|
|
129
|
+
if "error" in result:
|
|
130
|
+
return f"Error: {result['error']}"
|
|
131
|
+
|
|
132
|
+
records = result.get("records", [])
|
|
133
|
+
if not records:
|
|
134
|
+
return "No records found in this table."
|
|
135
|
+
|
|
136
|
+
# Format the records for display
|
|
137
|
+
formatted_records = []
|
|
138
|
+
for i, record in enumerate(records):
|
|
139
|
+
record_id = record.get("id", "unknown")
|
|
140
|
+
fields = record.get("fields", {})
|
|
141
|
+
field_text = ", ".join([f"{k}: {v}" for k, v in fields.items()])
|
|
142
|
+
formatted_records.append(f"{i+1}. ID: {record_id} - {field_text}")
|
|
143
|
+
|
|
144
|
+
return "Records:\n" + "\n".join(formatted_records)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return f"Error listing records: {str(e)}"
|
|
147
|
+
|
|
148
|
+
# Start the server
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
print(f"Starting Airtable MCP Server with token {token[:5]}...{token[-5:]} and base {base_id}")
|
|
151
|
+
app.start()
|
package/smithery.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Smithery.ai configuration
|
|
2
2
|
name: "@rashidazarang/airtable-mcp"
|
|
3
|
-
version: "1.
|
|
4
|
-
description: "Connect your AI tools directly to Airtable. Query, create, update, and delete records using natural language. Features include base management, table operations, schema manipulation, record filtering, and data migrationโall through a standardized MCP interface compatible with
|
|
3
|
+
version: "1.2.4"
|
|
4
|
+
description: "Connect your AI tools directly to Airtable. Query, create, update, and delete records using natural language. Features include base management, table operations, schema manipulation, record filtering, and data migrationโall through a standardized MCP interface compatible with Claude Desktop and other Claude-powered editors."
|
|
5
5
|
|
|
6
6
|
startCommand:
|
|
7
7
|
type: stdio
|
|
@@ -11,31 +11,35 @@ startCommand:
|
|
|
11
11
|
airtable_token:
|
|
12
12
|
type: string
|
|
13
13
|
description: "Your Airtable Personal Access Token"
|
|
14
|
+
required: true
|
|
14
15
|
base_id:
|
|
15
16
|
type: string
|
|
16
|
-
description: "Your default Airtable base ID
|
|
17
|
-
|
|
17
|
+
description: "Your default Airtable base ID"
|
|
18
|
+
required: true
|
|
19
|
+
required: ["airtable_token", "base_id"]
|
|
18
20
|
commandFunction: |
|
|
19
21
|
(config) => {
|
|
20
|
-
//
|
|
21
|
-
const configStr = JSON.stringify(config);
|
|
22
|
+
// Use the working JavaScript implementation
|
|
22
23
|
return {
|
|
23
|
-
command: "
|
|
24
|
-
args: ["
|
|
25
|
-
env: {
|
|
24
|
+
command: "node",
|
|
25
|
+
args: ["airtable_simple.js", "--token", config.airtable_token, "--base", config.base_id],
|
|
26
|
+
env: {
|
|
27
|
+
AIRTABLE_TOKEN: config.airtable_token,
|
|
28
|
+
AIRTABLE_BASE_ID: config.base_id
|
|
29
|
+
}
|
|
26
30
|
};
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
listTools:
|
|
30
|
-
command: "
|
|
31
|
-
args: ["
|
|
34
|
+
command: "node"
|
|
35
|
+
args: ["airtable_simple.js", "--list-tools"]
|
|
32
36
|
env: {}
|
|
33
37
|
|
|
34
38
|
build:
|
|
35
|
-
dockerfile: "Dockerfile"
|
|
39
|
+
dockerfile: "Dockerfile.node"
|
|
36
40
|
|
|
37
41
|
metadata:
|
|
38
42
|
author: "Rashid Azarang"
|
|
39
43
|
license: "MIT"
|
|
40
44
|
repository: "https://github.com/rashidazarang/airtable-mcp"
|
|
41
|
-
homepage: "https://github.com/rashidazarang/airtable-mcp#readme"
|
|
45
|
+
homepage: "https://github.com/rashidazarang/airtable-mcp#readme"
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
echo "๐ฏ COMPREHENSIVE TEST - AIRTABLE MCP v1.4.0"
|
|
4
|
+
echo "==========================================="
|
|
5
|
+
echo ""
|
|
6
|
+
|
|
7
|
+
PASSED=0
|
|
8
|
+
FAILED=0
|
|
9
|
+
TOTAL=0
|
|
10
|
+
|
|
11
|
+
# Test function
|
|
12
|
+
test_feature() {
|
|
13
|
+
local name=$1
|
|
14
|
+
local result=$2
|
|
15
|
+
((TOTAL++))
|
|
16
|
+
|
|
17
|
+
if [ "$result" = "PASS" ]; then
|
|
18
|
+
echo "โ
$name"
|
|
19
|
+
((PASSED++))
|
|
20
|
+
else
|
|
21
|
+
echo "โ $name"
|
|
22
|
+
((FAILED++))
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
echo "๐ TESTING ALL 12 TOOLS"
|
|
27
|
+
echo "======================="
|
|
28
|
+
echo ""
|
|
29
|
+
|
|
30
|
+
# 1. List tables
|
|
31
|
+
result=$(curl -s -X POST http://localhost:8010/mcp -H "Content-Type: application/json" \
|
|
32
|
+
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "list_tables"}}')
|
|
33
|
+
if [[ "$result" == *"table"* ]]; then
|
|
34
|
+
test_feature "list_tables" "PASS"
|
|
35
|
+
else
|
|
36
|
+
test_feature "list_tables" "FAIL"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# 2. Create record
|
|
40
|
+
result=$(curl -s -X POST http://localhost:8010/mcp -H "Content-Type: application/json" \
|
|
41
|
+
-d '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "create_record", "arguments": {"table": "tblH7TnJxYpNqhQYK", "fields": {"Name": "Final Test", "Status": "Active"}}}}')
|
|
42
|
+
if [[ "$result" == *"Successfully created"* ]]; then
|
|
43
|
+
test_feature "create_record" "PASS"
|
|
44
|
+
RECORD_ID=$(echo "$result" | grep -o 'rec[a-zA-Z0-9]\{10,20\}' | head -1)
|
|
45
|
+
else
|
|
46
|
+
test_feature "create_record" "FAIL"
|
|
47
|
+
RECORD_ID=""
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# 3. Get record
|
|
51
|
+
if [ ! -z "$RECORD_ID" ]; then
|
|
52
|
+
result=$(curl -s -X POST http://localhost:8010/mcp -H "Content-Type: application/json" \
|
|
53
|
+
-d "{\"jsonrpc\": \"2.0\", \"id\": 3, \"method\": \"tools/call\", \"params\": {\"name\": \"get_record\", \"arguments\": {\"table\": \"tblH7TnJxYpNqhQYK\", \"recordId\": \"$RECORD_ID\"}}}")
|
|
54
|
+
[[ "$result" == *"Record $RECORD_ID"* ]] && test_feature "get_record" "PASS" || test_feature "get_record" "FAIL"
|
|
55
|
+
else
|
|
56
|
+
test_feature "get_record" "SKIP"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# 4. Update record
|
|
60
|
+
if [ ! -z "$RECORD_ID" ]; then
|
|
61
|
+
result=$(curl -s -X POST http://localhost:8010/mcp -H "Content-Type: application/json" \
|
|
62
|
+
-d "{\"jsonrpc\": \"2.0\", \"id\": 4, \"method\": \"tools/call\", \"params\": {\"name\": \"update_record\", \"arguments\": {\"table\": \"tblH7TnJxYpNqhQYK\", \"recordId\": \"$RECORD_ID\", \"fields\": {\"Status\": \"Completed\"}}}}")
|
|
63
|
+
[[ "$result" == *"Successfully updated"* ]] && test_feature "update_record" "PASS" || test_feature "update_record" "FAIL"
|
|
64
|
+
else
|
|
65
|
+
test_feature "update_record" "SKIP"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# 5. List records
|
|
69
|
+
result=$(curl -s -X POST http://localhost:8010/mcp -H "Content-Type: application/json" \
|
|
70
|
+
-d '{"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "list_records", "arguments": {"table": "tblH7TnJxYpNqhQYK", "maxRecords": 3}}}')
|
|
71
|
+
[[ "$result" == *"record"* ]] && test_feature "list_records" "PASS" || test_feature "list_records" "FAIL"
|
|
72
|
+
|
|
73
|
+
# 6. Search records
|
|
74
|
+
result=$(curl -s -X POST http://localhost:8010/mcp -H "Content-Type: application/json" \
|
|
75
|
+
-d '{"jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": {"name": "search_records", "arguments": {"table": "tblH7TnJxYpNqhQYK", "maxRecords": 3}}}')
|
|
76
|
+
[[ "$result" == *"record"* ]] && test_feature "search_records" "PASS" || test_feature "search_records" "FAIL"
|
|
77
|
+
|
|
78
|
+
# 7. Delete record
|
|
79
|
+
if [ ! -z "$RECORD_ID" ]; then
|
|
80
|
+
result=$(curl -s -X POST http://localhost:8010/mcp -H "Content-Type: application/json" \
|
|
81
|
+
-d "{\"jsonrpc\": \"2.0\", \"id\": 7, \"method\": \"tools/call\", \"params\": {\"name\": \"delete_record\", \"arguments\": {\"table\": \"tblH7TnJxYpNqhQYK\", \"recordId\": \"$RECORD_ID\"}}}")
|
|
82
|
+
[[ "$result" == *"Successfully deleted"* ]] && test_feature "delete_record" "PASS" || test_feature "delete_record" "FAIL"
|
|
83
|
+
else
|
|
84
|
+
test_feature "delete_record" "SKIP"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# 8. List webhooks
|
|
88
|
+
result=$(curl -s -X POST http://localhost:8010/mcp -H "Content-Type: application/json" \
|
|
89
|
+
-d '{"jsonrpc": "2.0", "id": 8, "method": "tools/call", "params": {"name": "list_webhooks"}}')
|
|
90
|
+
[[ "$result" == *"webhook"* ]] && test_feature "list_webhooks" "PASS" || test_feature "list_webhooks" "FAIL"
|
|
91
|
+
|
|
92
|
+
# 9. Create webhook
|
|
93
|
+
result=$(curl -s -X POST http://localhost:8010/mcp -H "Content-Type: application/json" \
|
|
94
|
+
-d '{"jsonrpc": "2.0", "id": 9, "method": "tools/call", "params": {"name": "create_webhook", "arguments": {"notificationUrl": "https://webhook.site/test-final"}}}')
|
|
95
|
+
if [[ "$result" == *"Successfully created"* ]]; then
|
|
96
|
+
test_feature "create_webhook" "PASS"
|
|
97
|
+
WEBHOOK_ID=$(echo "$result" | grep -o 'ach[a-zA-Z0-9]*' | head -1)
|
|
98
|
+
else
|
|
99
|
+
test_feature "create_webhook" "FAIL"
|
|
100
|
+
WEBHOOK_ID=""
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# 10. Get webhook payloads
|
|
104
|
+
if [ ! -z "$WEBHOOK_ID" ]; then
|
|
105
|
+
result=$(curl -s -X POST http://localhost:8010/mcp -H "Content-Type: application/json" \
|
|
106
|
+
-d "{\"jsonrpc\": \"2.0\", \"id\": 10, \"method\": \"tools/call\", \"params\": {\"name\": \"get_webhook_payloads\", \"arguments\": {\"webhookId\": \"$WEBHOOK_ID\"}}}")
|
|
107
|
+
[[ "$result" == *"payload"* ]] && test_feature "get_webhook_payloads" "PASS" || test_feature "get_webhook_payloads" "FAIL"
|
|
108
|
+
else
|
|
109
|
+
test_feature "get_webhook_payloads" "SKIP"
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# 11. Refresh webhook
|
|
113
|
+
if [ ! -z "$WEBHOOK_ID" ]; then
|
|
114
|
+
result=$(curl -s -X POST http://localhost:8010/mcp -H "Content-Type: application/json" \
|
|
115
|
+
-d "{\"jsonrpc\": \"2.0\", \"id\": 11, \"method\": \"tools/call\", \"params\": {\"name\": \"refresh_webhook\", \"arguments\": {\"webhookId\": \"$WEBHOOK_ID\"}}}")
|
|
116
|
+
[[ "$result" == *"refreshed"* ]] && test_feature "refresh_webhook" "PASS" || test_feature "refresh_webhook" "FAIL"
|
|
117
|
+
else
|
|
118
|
+
test_feature "refresh_webhook" "SKIP"
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# 12. Delete webhook
|
|
122
|
+
if [ ! -z "$WEBHOOK_ID" ]; then
|
|
123
|
+
result=$(curl -s -X POST http://localhost:8010/mcp -H "Content-Type: application/json" \
|
|
124
|
+
-d "{\"jsonrpc\": \"2.0\", \"id\": 12, \"method\": \"tools/call\", \"params\": {\"name\": \"delete_webhook\", \"arguments\": {\"webhookId\": \"$WEBHOOK_ID\"}}}")
|
|
125
|
+
[[ "$result" == *"deleted"* ]] && test_feature "delete_webhook" "PASS" || test_feature "delete_webhook" "FAIL"
|
|
126
|
+
else
|
|
127
|
+
test_feature "delete_webhook" "SKIP"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
echo ""
|
|
131
|
+
echo "๐ FINAL RESULTS"
|
|
132
|
+
echo "==============="
|
|
133
|
+
echo "Total Tests: $TOTAL"
|
|
134
|
+
echo "โ
Passed: $PASSED"
|
|
135
|
+
echo "โ Failed: $FAILED"
|
|
136
|
+
echo "Success Rate: $(( PASSED * 100 / TOTAL ))%"
|
|
137
|
+
|
|
138
|
+
if [ $FAILED -eq 0 ]; then
|
|
139
|
+
echo ""
|
|
140
|
+
echo "๐ ALL TESTS PASSED! v1.4.0 is ready for production!"
|
|
141
|
+
exit 0
|
|
142
|
+
else
|
|
143
|
+
echo ""
|
|
144
|
+
echo "โ ๏ธ $FAILED test(s) failed. Please review."
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|