@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/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,16 @@
1
+ {
2
+ "mcpServers": {
3
+ "airtable": {
4
+ "command": "curl",
5
+ "args": [
6
+ "-s",
7
+ "-X",
8
+ "POST",
9
+ "-H",
10
+ "Content-Type: application/json",
11
+ "http://localhost:8010/mcp"
12
+ ],
13
+ "url": "http://localhost:8010/mcp"
14
+ }
15
+ }
16
+ }
@@ -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
@@ -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
- # Add Claude-specific methods
301
- @app.rpc_method("resources/list")
302
- async def resources_list(params: Dict = None) -> Dict:
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.2.1",
4
- "description": "Airtable MCP for AI tools - compatible with MCP SDK 1.4.1+, includes enhanced Claude integration",
5
- "main": "index.js",
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": "index.js"
7
+ "airtable-mcp": "./airtable_simple.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node index.js"
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
- "cursor",
19
- "claude-code",
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": "git+https://github.com/rashidazarang/airtable-mcp.git"
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.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"
@@ -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