@rashidazarang/airtable-mcp 1.6.0 → 2.1.1
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/README.md +1 -0
- package/airtable_simple_production.js +532 -0
- package/package.json +15 -6
- package/.claude/settings.local.json +0 -12
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
- package/.github/ISSUE_TEMPLATE/custom.md +0 -10
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/CAPABILITY_REPORT.md +0 -118
- package/CLAUDE_INTEGRATION.md +0 -96
- package/CONTRIBUTING.md +0 -81
- package/DEVELOPMENT.md +0 -190
- package/Dockerfile +0 -39
- package/Dockerfile.node +0 -20
- package/IMPROVEMENT_PROPOSAL.md +0 -371
- package/INSTALLATION.md +0 -183
- package/ISSUE_RESPONSES.md +0 -171
- package/MCP_REVIEW_SUMMARY.md +0 -142
- package/QUICK_START.md +0 -60
- package/RELEASE_NOTES_v1.2.0.md +0 -50
- package/RELEASE_NOTES_v1.2.1.md +0 -40
- package/RELEASE_NOTES_v1.2.2.md +0 -48
- package/RELEASE_NOTES_v1.2.3.md +0 -105
- package/RELEASE_NOTES_v1.2.4.md +0 -60
- package/RELEASE_NOTES_v1.4.0.md +0 -104
- package/RELEASE_NOTES_v1.5.0.md +0 -185
- package/RELEASE_NOTES_v1.6.0.md +0 -248
- package/SECURITY_NOTICE.md +0 -40
- package/airtable-mcp-1.1.0.tgz +0 -0
- package/airtable_enhanced.js +0 -499
- package/airtable_mcp/__init__.py +0 -5
- package/airtable_mcp/src/server.py +0 -329
- package/airtable_simple_v1.2.4_backup.js +0 -277
- package/airtable_v1.4.0.js +0 -654
- package/cleanup.sh +0 -71
- package/index.js +0 -179
- package/inspector.py +0 -148
- package/inspector_server.py +0 -337
- package/publish-steps.txt +0 -27
- package/quick_test.sh +0 -30
- package/rashidazarang-airtable-mcp-1.1.0.tgz +0 -0
- package/rashidazarang-airtable-mcp-1.2.0.tgz +0 -0
- package/rashidazarang-airtable-mcp-1.2.1.tgz +0 -0
- package/requirements.txt +0 -10
- package/setup.py +0 -29
- package/simple_airtable_server.py +0 -151
- package/smithery.yaml +0 -45
- package/test_all_features.sh +0 -146
- package/test_all_operations.sh +0 -120
- package/test_client.py +0 -70
- package/test_enhanced_features.js +0 -389
- package/test_mcp_comprehensive.js +0 -163
- package/test_mock_server.js +0 -180
- package/test_v1.4.0_final.sh +0 -131
- package/test_v1.5.0_comprehensive.sh +0 -96
- package/test_v1.5.0_final.sh +0 -224
- package/test_v1.6.0_comprehensive.sh +0 -187
- package/test_webhooks.sh +0 -105
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Airtable MCP Server
|
|
4
|
-
-------------------
|
|
5
|
-
This is a Model Context Protocol (MCP) server that exposes Airtable operations as tools.
|
|
6
|
-
"""
|
|
7
|
-
import os
|
|
8
|
-
import sys
|
|
9
|
-
import json
|
|
10
|
-
import asyncio
|
|
11
|
-
import logging
|
|
12
|
-
import argparse
|
|
13
|
-
from contextlib import asynccontextmanager
|
|
14
|
-
from typing import Any, Dict, List, Optional, AsyncIterator, Callable
|
|
15
|
-
from dotenv import load_dotenv
|
|
16
|
-
|
|
17
|
-
print(f"Python version: {sys.version}")
|
|
18
|
-
print(f"Python executable: {sys.executable}")
|
|
19
|
-
print(f"Python path: {sys.path}")
|
|
20
|
-
|
|
21
|
-
# Import MCP-related modules - will be available when run with Python 3.10+
|
|
22
|
-
try:
|
|
23
|
-
from mcp.server.fastmcp import FastMCP
|
|
24
|
-
from mcp.server import stdio
|
|
25
|
-
print("Successfully imported MCP modules")
|
|
26
|
-
except ImportError as e:
|
|
27
|
-
print(f"Error importing MCP modules: {e}")
|
|
28
|
-
print("Error: MCP SDK requires Python 3.10+")
|
|
29
|
-
print("Please install Python 3.10 or newer and try again.")
|
|
30
|
-
sys.exit(1)
|
|
31
|
-
|
|
32
|
-
# Set up logging
|
|
33
|
-
logging.basicConfig(level=logging.INFO)
|
|
34
|
-
logger = logging.getLogger("airtable-mcp")
|
|
35
|
-
|
|
36
|
-
# Parse command line arguments
|
|
37
|
-
def parse_args():
|
|
38
|
-
parser = argparse.ArgumentParser(description="Airtable MCP Server")
|
|
39
|
-
parser.add_argument("--token", dest="api_token", help="Airtable Personal Access Token")
|
|
40
|
-
parser.add_argument("--base", dest="base_id", help="Airtable Base ID")
|
|
41
|
-
parser.add_argument("--port", type=int, default=8080, help="MCP server port for dev mode")
|
|
42
|
-
parser.add_argument("--host", default="127.0.0.1", help="MCP server host for dev mode")
|
|
43
|
-
parser.add_argument("--dev", action="store_true", help="Run in development mode")
|
|
44
|
-
return parser.parse_args()
|
|
45
|
-
|
|
46
|
-
# Load environment variables as fallback
|
|
47
|
-
load_dotenv()
|
|
48
|
-
|
|
49
|
-
# Create MCP server
|
|
50
|
-
mcp = FastMCP("Airtable Tools")
|
|
51
|
-
|
|
52
|
-
# Server state will be initialized in main()
|
|
53
|
-
server_state = {
|
|
54
|
-
"base_id": "",
|
|
55
|
-
"token": "",
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
# Authentication middleware
|
|
59
|
-
@mcp.middleware
|
|
60
|
-
async def auth_middleware(context, next_handler):
|
|
61
|
-
# Skip auth check for tool listing
|
|
62
|
-
if hasattr(context, 'operation') and context.operation == "list_tools":
|
|
63
|
-
return await next_handler(context)
|
|
64
|
-
|
|
65
|
-
# Allow all operations without a token check - actual API calls will be checked later
|
|
66
|
-
return await next_handler(context)
|
|
67
|
-
|
|
68
|
-
# Helper functions for Airtable API calls
|
|
69
|
-
async def api_call(endpoint, method="GET", data=None, params=None):
|
|
70
|
-
"""Make an Airtable API call"""
|
|
71
|
-
import requests
|
|
72
|
-
|
|
73
|
-
# Check if token is available before making API calls
|
|
74
|
-
if not server_state["token"]:
|
|
75
|
-
return {"error": "No Airtable API token provided. Please set via --token or AIRTABLE_PERSONAL_ACCESS_TOKEN"}
|
|
76
|
-
|
|
77
|
-
headers = {
|
|
78
|
-
"Authorization": f"Bearer {server_state['token']}",
|
|
79
|
-
"Content-Type": "application/json"
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
url = f"https://api.airtable.com/v0/{endpoint}"
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
if method == "GET":
|
|
86
|
-
response = requests.get(url, headers=headers, params=params)
|
|
87
|
-
elif method == "POST":
|
|
88
|
-
response = requests.post(url, headers=headers, json=data)
|
|
89
|
-
elif method == "PATCH":
|
|
90
|
-
response = requests.patch(url, headers=headers, json=data)
|
|
91
|
-
elif method == "DELETE":
|
|
92
|
-
response = requests.delete(url, headers=headers, params=params)
|
|
93
|
-
else:
|
|
94
|
-
raise ValueError(f"Unsupported method: {method}")
|
|
95
|
-
|
|
96
|
-
response.raise_for_status()
|
|
97
|
-
return response.json()
|
|
98
|
-
except Exception as e:
|
|
99
|
-
logger.error(f"API call error: {str(e)}")
|
|
100
|
-
return {"error": str(e)}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# Define MCP tool functions
|
|
104
|
-
|
|
105
|
-
@mcp.tool()
|
|
106
|
-
async def list_bases() -> str:
|
|
107
|
-
"""List all accessible Airtable bases"""
|
|
108
|
-
if not server_state["token"]:
|
|
109
|
-
return "Please provide an Airtable API token to list your bases."
|
|
110
|
-
|
|
111
|
-
result = await api_call("meta/bases")
|
|
112
|
-
|
|
113
|
-
if "error" in result:
|
|
114
|
-
return f"Error: {result['error']}"
|
|
115
|
-
|
|
116
|
-
bases = result.get("bases", [])
|
|
117
|
-
if not bases:
|
|
118
|
-
return "No bases found accessible with your token."
|
|
119
|
-
|
|
120
|
-
base_list = [f"{i+1}. {base['name']} (ID: {base['id']})" for i, base in enumerate(bases)]
|
|
121
|
-
return "Available bases:\n" + "\n".join(base_list)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@mcp.tool()
|
|
125
|
-
async def list_tables(base_id: Optional[str] = None) -> str:
|
|
126
|
-
"""List all tables in the specified base or the default base"""
|
|
127
|
-
if not server_state["token"]:
|
|
128
|
-
return "Please provide an Airtable API token to list tables."
|
|
129
|
-
|
|
130
|
-
base = base_id or server_state["base_id"]
|
|
131
|
-
|
|
132
|
-
if not base:
|
|
133
|
-
return "Error: No base ID provided. Please specify a base_id or set AIRTABLE_BASE_ID in your .env file."
|
|
134
|
-
|
|
135
|
-
result = await api_call(f"meta/bases/{base}/tables")
|
|
136
|
-
|
|
137
|
-
if "error" in result:
|
|
138
|
-
return f"Error: {result['error']}"
|
|
139
|
-
|
|
140
|
-
tables = result.get("tables", [])
|
|
141
|
-
if not tables:
|
|
142
|
-
return "No tables found in this base."
|
|
143
|
-
|
|
144
|
-
table_list = [f"{i+1}. {table['name']} (ID: {table['id']}, Fields: {len(table.get('fields', []))})"
|
|
145
|
-
for i, table in enumerate(tables)]
|
|
146
|
-
return "Tables in this base:\n" + "\n".join(table_list)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
@mcp.tool()
|
|
150
|
-
async def list_records(table_name: str, max_records: Optional[int] = 100, filter_formula: Optional[str] = None) -> str:
|
|
151
|
-
"""List records from a table with optional filtering"""
|
|
152
|
-
if not server_state["token"]:
|
|
153
|
-
return "Please provide an Airtable API token to list records."
|
|
154
|
-
|
|
155
|
-
base = server_state["base_id"]
|
|
156
|
-
|
|
157
|
-
if not base:
|
|
158
|
-
return "Error: No base ID set. Please set a base ID."
|
|
159
|
-
|
|
160
|
-
params = {"maxRecords": max_records}
|
|
161
|
-
|
|
162
|
-
if filter_formula:
|
|
163
|
-
params["filterByFormula"] = filter_formula
|
|
164
|
-
|
|
165
|
-
result = await api_call(f"{base}/{table_name}", params=params)
|
|
166
|
-
|
|
167
|
-
if "error" in result:
|
|
168
|
-
return f"Error: {result['error']}"
|
|
169
|
-
|
|
170
|
-
records = result.get("records", [])
|
|
171
|
-
if not records:
|
|
172
|
-
return "No records found in this table."
|
|
173
|
-
|
|
174
|
-
# Format the records for display
|
|
175
|
-
formatted_records = []
|
|
176
|
-
for i, record in enumerate(records):
|
|
177
|
-
record_id = record.get("id", "unknown")
|
|
178
|
-
fields = record.get("fields", {})
|
|
179
|
-
field_text = ", ".join([f"{k}: {v}" for k, v in fields.items()])
|
|
180
|
-
formatted_records.append(f"{i+1}. ID: {record_id} - {field_text}")
|
|
181
|
-
|
|
182
|
-
return "Records:\n" + "\n".join(formatted_records)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
@mcp.tool()
|
|
186
|
-
async def get_record(table_name: str, record_id: str) -> str:
|
|
187
|
-
"""Get a specific record from a table"""
|
|
188
|
-
if not server_state["token"]:
|
|
189
|
-
return "Please provide an Airtable API token to get records."
|
|
190
|
-
|
|
191
|
-
base = server_state["base_id"]
|
|
192
|
-
|
|
193
|
-
if not base:
|
|
194
|
-
return "Error: No base ID set. Please set a base ID."
|
|
195
|
-
|
|
196
|
-
result = await api_call(f"{base}/{table_name}/{record_id}")
|
|
197
|
-
|
|
198
|
-
if "error" in result:
|
|
199
|
-
return f"Error: {result['error']}"
|
|
200
|
-
|
|
201
|
-
fields = result.get("fields", {})
|
|
202
|
-
if not fields:
|
|
203
|
-
return f"Record {record_id} found but contains no fields."
|
|
204
|
-
|
|
205
|
-
# Format the fields for display
|
|
206
|
-
formatted_fields = []
|
|
207
|
-
for key, value in fields.items():
|
|
208
|
-
formatted_fields.append(f"{key}: {value}")
|
|
209
|
-
|
|
210
|
-
return f"Record ID: {record_id}\n" + "\n".join(formatted_fields)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
@mcp.tool()
|
|
214
|
-
async def create_records(table_name: str, records_json: str) -> str:
|
|
215
|
-
"""Create records in a table from JSON string"""
|
|
216
|
-
if not server_state["token"]:
|
|
217
|
-
return "Please provide an Airtable API token to create records."
|
|
218
|
-
|
|
219
|
-
base = server_state["base_id"]
|
|
220
|
-
|
|
221
|
-
if not base:
|
|
222
|
-
return "Error: No base ID set. Please set a base ID."
|
|
223
|
-
|
|
224
|
-
try:
|
|
225
|
-
records_data = json.loads(records_json)
|
|
226
|
-
|
|
227
|
-
# Format the records for Airtable API
|
|
228
|
-
if not isinstance(records_data, list):
|
|
229
|
-
records_data = [records_data]
|
|
230
|
-
|
|
231
|
-
records = [{"fields": record} for record in records_data]
|
|
232
|
-
|
|
233
|
-
data = {"records": records}
|
|
234
|
-
result = await api_call(f"{base}/{table_name}", method="POST", data=data)
|
|
235
|
-
|
|
236
|
-
if "error" in result:
|
|
237
|
-
return f"Error: {result['error']}"
|
|
238
|
-
|
|
239
|
-
created_records = result.get("records", [])
|
|
240
|
-
return f"Successfully created {len(created_records)} records."
|
|
241
|
-
|
|
242
|
-
except json.JSONDecodeError:
|
|
243
|
-
return "Error: Invalid JSON format in records_json parameter."
|
|
244
|
-
except Exception as e:
|
|
245
|
-
return f"Error creating records: {str(e)}"
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
@mcp.tool()
|
|
249
|
-
async def update_records(table_name: str, records_json: str) -> str:
|
|
250
|
-
"""Update records in a table from JSON string"""
|
|
251
|
-
if not server_state["token"]:
|
|
252
|
-
return "Please provide an Airtable API token to update records."
|
|
253
|
-
|
|
254
|
-
base = server_state["base_id"]
|
|
255
|
-
|
|
256
|
-
if not base:
|
|
257
|
-
return "Error: No base ID set. Please set a base ID."
|
|
258
|
-
|
|
259
|
-
try:
|
|
260
|
-
records_data = json.loads(records_json)
|
|
261
|
-
|
|
262
|
-
# Format the records for Airtable API
|
|
263
|
-
if not isinstance(records_data, list):
|
|
264
|
-
records_data = [records_data]
|
|
265
|
-
|
|
266
|
-
records = []
|
|
267
|
-
for record in records_data:
|
|
268
|
-
if "id" not in record:
|
|
269
|
-
return "Error: Each record must have an 'id' field."
|
|
270
|
-
|
|
271
|
-
rec_id = record.pop("id")
|
|
272
|
-
fields = record.get("fields", record) # Support both {id, fields} format and direct fields
|
|
273
|
-
records.append({"id": rec_id, "fields": fields})
|
|
274
|
-
|
|
275
|
-
data = {"records": records}
|
|
276
|
-
result = await api_call(f"{base}/{table_name}", method="PATCH", data=data)
|
|
277
|
-
|
|
278
|
-
if "error" in result:
|
|
279
|
-
return f"Error: {result['error']}"
|
|
280
|
-
|
|
281
|
-
updated_records = result.get("records", [])
|
|
282
|
-
return f"Successfully updated {len(updated_records)} records."
|
|
283
|
-
|
|
284
|
-
except json.JSONDecodeError:
|
|
285
|
-
return "Error: Invalid JSON format in records_json parameter."
|
|
286
|
-
except Exception as e:
|
|
287
|
-
return f"Error updating records: {str(e)}"
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
@mcp.tool()
|
|
291
|
-
async def set_base_id(base_id: str) -> str:
|
|
292
|
-
"""Set the current Airtable base ID"""
|
|
293
|
-
server_state["base_id"] = base_id
|
|
294
|
-
return f"Base ID set to: {base_id}"
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def main():
|
|
298
|
-
"""Run the MCP server"""
|
|
299
|
-
try:
|
|
300
|
-
# Parse command line arguments
|
|
301
|
-
args = parse_args()
|
|
302
|
-
|
|
303
|
-
# Set server state from command line args or fallback to env vars
|
|
304
|
-
server_state["token"] = args.api_token or os.getenv("AIRTABLE_PERSONAL_ACCESS_TOKEN", "")
|
|
305
|
-
server_state["base_id"] = args.base_id or os.getenv("AIRTABLE_BASE_ID", "")
|
|
306
|
-
|
|
307
|
-
if not server_state["token"]:
|
|
308
|
-
logger.warning("No Airtable API token provided. Please set via --token or AIRTABLE_PERSONAL_ACCESS_TOKEN")
|
|
309
|
-
logger.info("Tool listing will work but API calls will require a token")
|
|
310
|
-
|
|
311
|
-
# Setup asyncio event loop
|
|
312
|
-
if sys.platform == 'win32':
|
|
313
|
-
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
314
|
-
|
|
315
|
-
# Run the server
|
|
316
|
-
if args.dev:
|
|
317
|
-
# Development mode
|
|
318
|
-
mcp.run(host=args.host, port=args.port)
|
|
319
|
-
else:
|
|
320
|
-
# Production mode - stdio interface for MCP
|
|
321
|
-
mcp.run()
|
|
322
|
-
|
|
323
|
-
except Exception as e:
|
|
324
|
-
logger.error(f"Server error: {str(e)}")
|
|
325
|
-
sys.exit(1)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if __name__ == "__main__":
|
|
329
|
-
main()
|
|
@@ -1,277 +0,0 @@
|
|
|
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
|
-
});
|