@rashidazarang/airtable-mcp 1.2.1 → 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.
@@ -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.0.0"
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 Cursor, Claude Code, Cline, Zed, and other Claude-powered editors."
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 (optional)"
17
- required: ["airtable_token"]
17
+ description: "Your default Airtable base ID"
18
+ required: true
19
+ required: ["airtable_token", "base_id"]
18
20
  commandFunction: |
19
21
  (config) => {
20
- // Pass config as a JSON string to the inspector_server.py
21
- const configStr = JSON.stringify(config);
22
+ // Use the working JavaScript implementation
22
23
  return {
23
- command: "python3.10",
24
- args: ["inspector_server.py", "--config", configStr],
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: "python3.10"
31
- args: ["inspector.py"]
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"
package/test_client.py CHANGED
@@ -4,14 +4,21 @@ Simple test client for Airtable MCP
4
4
  """
5
5
  import asyncio
6
6
  import json
7
+ import os
7
8
  import sys
8
9
  import subprocess
9
10
  import time
10
11
  from typing import Dict, Any
11
12
 
12
- # Define the token and base ID
13
- TOKEN = "patnWSCSCmnsqeQ4I.2a3603372f6df67e51f9fe553012192019f2d81c3eab0f94ebd702d7fb63e338"
14
- BASE_ID = "appi7fWMQcB3BNzPs"
13
+ # Load credentials from environment variables
14
+ TOKEN = os.environ.get('AIRTABLE_TOKEN', 'YOUR_AIRTABLE_TOKEN_HERE')
15
+ BASE_ID = os.environ.get('AIRTABLE_BASE_ID', 'YOUR_BASE_ID_HERE')
16
+
17
+ if TOKEN == 'YOUR_AIRTABLE_TOKEN_HERE' or BASE_ID == 'YOUR_BASE_ID_HERE':
18
+ print("Error: Please set AIRTABLE_TOKEN and AIRTABLE_BASE_ID environment variables")
19
+ print("Example: export AIRTABLE_TOKEN=your_token_here")
20
+ print(" export AIRTABLE_BASE_ID=your_base_id_here")
21
+ sys.exit(1)
15
22
 
16
23
  # Helper function to directly make Airtable API calls
17
24
  def api_call(endpoint, token=TOKEN):
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Comprehensive Test Script for Airtable MCP
5
+ * Tests all available MCP tools and functionality
6
+ */
7
+
8
+ const http = require('http');
9
+
10
+ const MCP_SERVER_URL = 'http://localhost:8010/mcp';
11
+ const TEST_TOKEN = process.env.AIRTABLE_TOKEN || 'YOUR_AIRTABLE_TOKEN_HERE';
12
+ const TEST_BASE_ID = process.env.AIRTABLE_BASE_ID || 'YOUR_BASE_ID_HERE';
13
+
14
+ if (TEST_TOKEN === 'YOUR_AIRTABLE_TOKEN_HERE' || TEST_BASE_ID === 'YOUR_BASE_ID_HERE') {
15
+ console.error('Error: Please set AIRTABLE_TOKEN and AIRTABLE_BASE_ID environment variables');
16
+ console.error('Example: export AIRTABLE_TOKEN=your_token_here');
17
+ console.error(' export AIRTABLE_BASE_ID=your_base_id_here');
18
+ process.exit(1);
19
+ }
20
+
21
+ // Helper function to make MCP requests
22
+ function makeMCPRequest(method, params = {}) {
23
+ return new Promise((resolve, reject) => {
24
+ const postData = JSON.stringify({
25
+ jsonrpc: '2.0',
26
+ id: Date.now(),
27
+ method: method,
28
+ params: params
29
+ });
30
+
31
+ const options = {
32
+ hostname: 'localhost',
33
+ port: 8010,
34
+ path: '/mcp',
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ 'Content-Length': Buffer.byteLength(postData)
39
+ }
40
+ };
41
+
42
+ const req = http.request(options, (res) => {
43
+ let data = '';
44
+ res.on('data', (chunk) => {
45
+ data += chunk;
46
+ });
47
+ res.on('end', () => {
48
+ try {
49
+ const response = JSON.parse(data);
50
+ resolve(response);
51
+ } catch (e) {
52
+ reject(new Error(`Failed to parse response: ${e.message}`));
53
+ }
54
+ });
55
+ });
56
+
57
+ req.on('error', (e) => {
58
+ reject(new Error(`Request failed: ${e.message}`));
59
+ });
60
+
61
+ req.write(postData);
62
+ req.end();
63
+ });
64
+ }
65
+
66
+ async function runComprehensiveTest() {
67
+ console.log('🔌 Airtable MCP Comprehensive Test');
68
+ console.log('===================================');
69
+ console.log(`Server: ${MCP_SERVER_URL}`);
70
+ console.log(`Base ID: ${TEST_BASE_ID}`);
71
+ console.log(`Token: ${TEST_TOKEN.substring(0, 10)}...${TEST_TOKEN.substring(TEST_TOKEN.length - 10)}`);
72
+ console.log('');
73
+
74
+ try {
75
+ // Test 1: List Resources
76
+ console.log('📋 Test 1: Listing Resources');
77
+ console.log('----------------------------');
78
+ const resourcesResponse = await makeMCPRequest('resources/list');
79
+ console.log('✅ Resources Response:');
80
+ console.log(JSON.stringify(resourcesResponse, null, 2));
81
+ console.log('');
82
+
83
+ // Test 2: List Prompts
84
+ console.log('📝 Test 2: Listing Prompts');
85
+ console.log('-------------------------');
86
+ const promptsResponse = await makeMCPRequest('prompts/list');
87
+ console.log('✅ Prompts Response:');
88
+ console.log(JSON.stringify(promptsResponse, null, 2));
89
+ console.log('');
90
+
91
+ // Test 3: List Tables
92
+ console.log('📊 Test 3: Listing Tables');
93
+ console.log('------------------------');
94
+ const tablesResponse = await makeMCPRequest('tools/call', {
95
+ name: 'list_tables'
96
+ });
97
+ console.log('✅ Tables Response:');
98
+ console.log(JSON.stringify(tablesResponse, null, 2));
99
+ console.log('');
100
+
101
+ // Test 4: List Records from Requests table
102
+ console.log('📄 Test 4: Listing Records (Requests Table)');
103
+ console.log('-------------------------------------------');
104
+ const recordsResponse = await makeMCPRequest('tools/call', {
105
+ name: 'list_records',
106
+ arguments: {
107
+ table_name: 'requests',
108
+ max_records: 3
109
+ }
110
+ });
111
+ console.log('✅ Records Response:');
112
+ console.log(JSON.stringify(recordsResponse, null, 2));
113
+ console.log('');
114
+
115
+ // Test 5: List Records from Providers table
116
+ console.log('👥 Test 5: Listing Records (Providers Table)');
117
+ console.log('--------------------------------------------');
118
+ const providersResponse = await makeMCPRequest('tools/call', {
119
+ name: 'list_records',
120
+ arguments: {
121
+ table_name: 'providers',
122
+ max_records: 3
123
+ }
124
+ });
125
+ console.log('✅ Providers Response:');
126
+ console.log(JSON.stringify(providersResponse, null, 2));
127
+ console.log('');
128
+
129
+ // Test 6: List Records from Categories table
130
+ console.log('🏷️ Test 6: Listing Records (Categories Table)');
131
+ console.log('---------------------------------------------');
132
+ const categoriesResponse = await makeMCPRequest('tools/call', {
133
+ name: 'list_records',
134
+ arguments: {
135
+ table_name: 'categories',
136
+ max_records: 3
137
+ }
138
+ });
139
+ console.log('✅ Categories Response:');
140
+ console.log(JSON.stringify(categoriesResponse, null, 2));
141
+ console.log('');
142
+
143
+ console.log('🎉 All Tests Completed Successfully!');
144
+ console.log('');
145
+ console.log('📊 Test Summary:');
146
+ console.log('✅ MCP Server is running and accessible');
147
+ console.log('✅ Airtable API connection is working');
148
+ console.log('✅ All MCP tools are functioning properly');
149
+ console.log('✅ JSON-RPC protocol is correctly implemented');
150
+ console.log('✅ Error handling is working');
151
+ console.log('');
152
+ console.log('🚀 The Airtable MCP is ready for use!');
153
+
154
+ } catch (error) {
155
+ console.error('❌ Test failed:', error.message);
156
+ process.exit(1);
157
+ }
158
+ }
159
+
160
+ // Run the comprehensive test
161
+ runComprehensiveTest();