@rashidazarang/airtable-mcp 2.1.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.
Files changed (152) hide show
  1. package/package.json +10 -1
  2. package/.github/ISSUE_TEMPLATE/bug-report.yml +0 -173
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  4. package/.github/ISSUE_TEMPLATE/custom.md +0 -10
  5. package/.github/ISSUE_TEMPLATE/feature-request.yml +0 -209
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  7. package/.github/ISSUE_TEMPLATE/security-report.yml +0 -216
  8. package/.github/pull_request_template.md +0 -245
  9. package/.github/workflows/ci-cd.yml +0 -408
  10. package/.github/workflows/security-audit.yml +0 -316
  11. package/API_DOCUMENTATION.md +0 -897
  12. package/CAPABILITY_REPORT.md +0 -118
  13. package/CLAUDE_INTEGRATION.md +0 -96
  14. package/CODE_OF_CONDUCT.md +0 -181
  15. package/CONTRIBUTING.md +0 -81
  16. package/DEVELOPMENT.md +0 -190
  17. package/Dockerfile +0 -39
  18. package/Dockerfile.node +0 -20
  19. package/Dockerfile.production +0 -127
  20. package/IMPROVEMENT_PROPOSAL.md +0 -371
  21. package/INSTALLATION.md +0 -183
  22. package/ISSUE_RESPONSES.md +0 -171
  23. package/MCP_REVIEW_SUMMARY.md +0 -142
  24. package/QUICK_START.md +0 -60
  25. package/RELEASE_NOTES_v1.2.0.md +0 -50
  26. package/RELEASE_NOTES_v1.2.1.md +0 -40
  27. package/RELEASE_NOTES_v1.2.2.md +0 -48
  28. package/RELEASE_NOTES_v1.2.3.md +0 -105
  29. package/RELEASE_NOTES_v1.2.4.md +0 -60
  30. package/RELEASE_NOTES_v1.4.0.md +0 -104
  31. package/RELEASE_NOTES_v1.5.0.md +0 -185
  32. package/RELEASE_NOTES_v1.6.0.md +0 -248
  33. package/SECURITY_NOTICE.md +0 -40
  34. package/airtable-clipper/CHANGELOG.md +0 -198
  35. package/airtable-clipper/CHROME_STORE_SUBMISSION.md +0 -343
  36. package/airtable-clipper/LAUNCH_STRATEGY.md +0 -495
  37. package/airtable-clipper/LICENSE +0 -21
  38. package/airtable-clipper/OAUTH_SETUP.md +0 -51
  39. package/airtable-clipper/PRIVACY_POLICY.md +0 -187
  40. package/airtable-clipper/README.md +0 -575
  41. package/airtable-clipper/SUBMIT_TO_CHROME_STORE.md +0 -273
  42. package/airtable-clipper/build.sh +0 -85
  43. package/airtable-clipper/docs/QUICK_START.md +0 -99
  44. package/airtable-clipper/docs/SETUP.md +0 -291
  45. package/airtable-clipper/extension/background.js +0 -337
  46. package/airtable-clipper/extension/base-setup.html +0 -324
  47. package/airtable-clipper/extension/base-setup.js +0 -471
  48. package/airtable-clipper/extension/content.js +0 -771
  49. package/airtable-clipper/extension/icons/README.md +0 -69
  50. package/airtable-clipper/extension/icons/icon-16.png +0 -3
  51. package/airtable-clipper/extension/manifest.json +0 -73
  52. package/airtable-clipper/extension/popup.html +0 -144
  53. package/airtable-clipper/extension/popup.js +0 -475
  54. package/airtable-clipper/extension/styles/content.css +0 -229
  55. package/airtable-clipper/extension/styles/popup.css +0 -477
  56. package/airtable-clipper/privacy-policy.md +0 -63
  57. package/airtable-clipper/releases/v1.0.0/background.js +0 -337
  58. package/airtable-clipper/releases/v1.0.0/base-setup.html +0 -324
  59. package/airtable-clipper/releases/v1.0.0/base-setup.js +0 -471
  60. package/airtable-clipper/releases/v1.0.0/content.js +0 -771
  61. package/airtable-clipper/releases/v1.0.0/icons/README.md +0 -69
  62. package/airtable-clipper/releases/v1.0.0/icons/icon-128.png +0 -2
  63. package/airtable-clipper/releases/v1.0.0/icons/icon-16.png +0 -3
  64. package/airtable-clipper/releases/v1.0.0/icons/icon-32.png +0 -2
  65. package/airtable-clipper/releases/v1.0.0/icons/icon-48.png +0 -2
  66. package/airtable-clipper/releases/v1.0.0/manifest.json +0 -73
  67. package/airtable-clipper/releases/v1.0.0/popup.html +0 -144
  68. package/airtable-clipper/releases/v1.0.0/popup.js +0 -475
  69. package/airtable-clipper/releases/v1.0.0/sidepanel.html +0 -25
  70. package/airtable-clipper/releases/v1.0.0/styles/content.css +0 -229
  71. package/airtable-clipper/releases/v1.0.0/styles/popup.css +0 -477
  72. package/airtable-clipper/releases/v1.0.1/background.js +0 -337
  73. package/airtable-clipper/releases/v1.0.1/base-setup.html +0 -324
  74. package/airtable-clipper/releases/v1.0.1/base-setup.js +0 -471
  75. package/airtable-clipper/releases/v1.0.1/content.js +0 -771
  76. package/airtable-clipper/releases/v1.0.1/icons/README.md +0 -69
  77. package/airtable-clipper/releases/v1.0.1/icons/icon-128.png +0 -2
  78. package/airtable-clipper/releases/v1.0.1/icons/icon-16.png +0 -3
  79. package/airtable-clipper/releases/v1.0.1/icons/icon-32.png +0 -2
  80. package/airtable-clipper/releases/v1.0.1/icons/icon-48.png +0 -2
  81. package/airtable-clipper/releases/v1.0.1/manifest.json +0 -70
  82. package/airtable-clipper/releases/v1.0.1/popup.html +0 -157
  83. package/airtable-clipper/releases/v1.0.1/popup.js +0 -562
  84. package/airtable-clipper/releases/v1.0.1/sidepanel.html +0 -25
  85. package/airtable-clipper/releases/v1.0.1/styles/content.css +0 -229
  86. package/airtable-clipper/releases/v1.0.1/styles/popup.css +0 -647
  87. package/airtable-clipper/releases/v1.0.2/background.js +0 -337
  88. package/airtable-clipper/releases/v1.0.2/base-setup.html +0 -324
  89. package/airtable-clipper/releases/v1.0.2/base-setup.js +0 -471
  90. package/airtable-clipper/releases/v1.0.2/content.js +0 -771
  91. package/airtable-clipper/releases/v1.0.2/icons/README.md +0 -69
  92. package/airtable-clipper/releases/v1.0.2/icons/icon-128.png +0 -2
  93. package/airtable-clipper/releases/v1.0.2/icons/icon-16.png +0 -3
  94. package/airtable-clipper/releases/v1.0.2/icons/icon-32.png +0 -2
  95. package/airtable-clipper/releases/v1.0.2/icons/icon-48.png +0 -2
  96. package/airtable-clipper/releases/v1.0.2/manifest.json +0 -62
  97. package/airtable-clipper/releases/v1.0.2/popup.html +0 -157
  98. package/airtable-clipper/releases/v1.0.2/popup.js +0 -567
  99. package/airtable-clipper/releases/v1.0.2/sidepanel.html +0 -25
  100. package/airtable-clipper/releases/v1.0.2/styles/content.css +0 -229
  101. package/airtable-clipper/releases/v1.0.2/styles/popup.css +0 -647
  102. package/airtable-clipper/terms-of-service.md +0 -124
  103. package/airtable-clipper/test-credentials.md +0 -61
  104. package/airtable-clipper/test-extension/background.js +0 -337
  105. package/airtable-clipper/test-extension/base-setup.html +0 -324
  106. package/airtable-clipper/test-extension/base-setup.js +0 -471
  107. package/airtable-clipper/test-extension/content.js +0 -873
  108. package/airtable-clipper/test-extension/icons/README.md +0 -69
  109. package/airtable-clipper/test-extension/icons/icon-128.png +0 -2
  110. package/airtable-clipper/test-extension/icons/icon-16.png +0 -3
  111. package/airtable-clipper/test-extension/icons/icon-32.png +0 -2
  112. package/airtable-clipper/test-extension/icons/icon-48.png +0 -2
  113. package/airtable-clipper/test-extension/manifest.json +0 -72
  114. package/airtable-clipper/test-extension/popup.html +0 -274
  115. package/airtable-clipper/test-extension/popup.js +0 -729
  116. package/airtable-clipper/test-extension/sidepanel.html +0 -25
  117. package/airtable-clipper/test-extension/styles/content.css +0 -229
  118. package/airtable-clipper/test-extension/styles/popup.css +0 -794
  119. package/airtable_mcp/__init__.py +0 -5
  120. package/airtable_mcp/src/server.py +0 -329
  121. package/airtable_mcp_v2.js +0 -1505
  122. package/airtable_mcp_v2_oauth.js +0 -1048
  123. package/airtable_mcp_v3_advanced.js +0 -1161
  124. package/cleanup.sh +0 -71
  125. package/docker-compose.production.yml +0 -366
  126. package/helm/airtable-mcp/Chart.yaml +0 -122
  127. package/helm/airtable-mcp/values.yaml +0 -538
  128. package/index.js +0 -179
  129. package/inspector.py +0 -148
  130. package/inspector_server.py +0 -337
  131. package/k8s/deployment.yaml +0 -402
  132. package/k8s/namespace.yaml +0 -108
  133. package/k8s/service.yaml +0 -194
  134. package/monitoring/alerts.yml +0 -289
  135. package/monitoring/prometheus.yml +0 -224
  136. package/publish-steps.txt +0 -27
  137. package/quick_test.sh +0 -30
  138. package/requirements.txt +0 -10
  139. package/setup.py +0 -29
  140. package/simple_airtable_server.py +0 -151
  141. package/smithery.yaml +0 -45
  142. package/test_all_features.sh +0 -146
  143. package/test_all_operations.sh +0 -120
  144. package/test_client.py +0 -70
  145. package/test_enhanced_features.js +0 -389
  146. package/test_mcp_comprehensive.js +0 -163
  147. package/test_mock_server.js +0 -180
  148. package/test_v1.4.0_final.sh +0 -131
  149. package/test_v1.5.0_comprehensive.sh +0 -96
  150. package/test_v1.5.0_final.sh +0 -224
  151. package/test_v1.6.0_comprehensive.sh +0 -187
  152. package/test_webhooks.sh +0 -105
@@ -1,1048 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Enhanced Airtable MCP Server v2.1 - OAuth2 Edition
5
- * Full Model Context Protocol Implementation with OAuth2 Authentication
6
- *
7
- * Features: Tools, Resources, Prompts, Sampling, Roots, Logging, HTTP Transport, OAuth2, Security
8
- * Trust Score Target: 100/100 points
9
- *
10
- * Author: Rashid Azarang
11
- * Version: 2.1.0
12
- * Protocol: MCP 2024-11-05
13
- */
14
-
15
- const http = require('http');
16
- const https = require('https');
17
- const fs = require('fs');
18
- const path = require('path');
19
- const crypto = require('crypto');
20
- const url = require('url');
21
- const querystring = require('querystring');
22
-
23
- // Load environment variables
24
- const envPath = path.join(__dirname, '.env');
25
- if (fs.existsSync(envPath)) {
26
- require('dotenv').config({ path: envPath });
27
- }
28
-
29
- // Parse command line arguments
30
- const args = process.argv.slice(2);
31
- let tokenIndex = args.indexOf('--token');
32
- let baseIndex = args.indexOf('--base');
33
-
34
- const token = tokenIndex !== -1 ? args[tokenIndex + 1] : process.env.AIRTABLE_TOKEN || process.env.AIRTABLE_API_TOKEN;
35
- const baseId = baseIndex !== -1 ? args[baseIndex + 1] : process.env.AIRTABLE_BASE_ID || process.env.AIRTABLE_BASE;
36
-
37
- if (!token || !baseId) {
38
- console.error('โŒ Error: Missing Airtable credentials');
39
- console.error('\n๐Ÿ“‹ Usage options:');
40
- console.error(' 1. Command line: node airtable_mcp_v2_oauth.js --token YOUR_TOKEN --base YOUR_BASE_ID');
41
- console.error(' 2. Environment variables: AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
42
- console.error(' 3. .env file with AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
43
- process.exit(1);
44
- }
45
-
46
- // ============================================================================
47
- // OAUTH2 CONFIGURATION & CREDENTIALS
48
- // ============================================================================
49
-
50
- const OAUTH_CONFIG = {
51
- clientId: process.env.AIRTABLE_CLIENT_ID || 'your_client_id',
52
- clientSecret: process.env.AIRTABLE_CLIENT_SECRET || 'your_client_secret',
53
- redirectUri: process.env.OAUTH_REDIRECT_URI || 'http://localhost:8010/oauth/callback',
54
- scope: 'data.records:read data.records:write schema.bases:read schema.bases:write webhook:manage',
55
- authorizeUrl: 'https://airtable.com/oauth2/v1/authorize',
56
- tokenUrl: 'https://airtable.com/oauth2/v1/token',
57
- userInfoUrl: 'https://api.airtable.com/v0/meta/whoami'
58
- };
59
-
60
- // In-memory OAuth token storage (use Redis/DB in production)
61
- const tokenStorage = new Map();
62
- const authSessions = new Map();
63
-
64
- // ============================================================================
65
- // SECURITY & RATE LIMITING
66
- // ============================================================================
67
-
68
- const rateLimiter = new Map();
69
- const MAX_REQUESTS_PER_MINUTE = 60;
70
- const SECURITY_HEADERS = {
71
- 'X-Content-Type-Options': 'nosniff',
72
- 'X-Frame-Options': 'DENY',
73
- 'X-XSS-Protection': '1; mode=block',
74
- 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
75
- 'Content-Security-Policy': "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"
76
- };
77
-
78
- // ============================================================================
79
- // ENHANCED LOGGING SYSTEM - MCP Protocol Compliant
80
- // ============================================================================
81
-
82
- const LOG_LEVELS = {
83
- ERROR: 0,
84
- WARN: 1,
85
- INFO: 2,
86
- DEBUG: 3,
87
- TRACE: 4
88
- };
89
-
90
- let currentLogLevel = LOG_LEVELS[process.env.LOG_LEVEL?.toUpperCase()] || LOG_LEVELS.INFO;
91
-
92
- function log(level, message, metadata = {}) {
93
- if (level <= currentLogLevel) {
94
- const timestamp = new Date().toISOString();
95
- const levelName = Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === level);
96
- const emoji = {
97
- ERROR: '๐Ÿ’ฅ',
98
- WARN: 'โš ๏ธ',
99
- INFO: '๐Ÿ“',
100
- DEBUG: '๐Ÿ”',
101
- TRACE: '๐Ÿ“จ'
102
- }[levelName];
103
-
104
- const logEntry = {
105
- timestamp,
106
- level: levelName,
107
- message,
108
- metadata,
109
- source: 'MCP-v2.1'
110
- };
111
-
112
- const output = `[${timestamp}] [${levelName}] [MCP-v2.1] ${emoji} ${message}`;
113
-
114
- if (Object.keys(metadata).length > 0) {
115
- console.log(output, JSON.stringify(metadata, null, 2));
116
- } else {
117
- console.log(output);
118
- }
119
- }
120
- }
121
-
122
- // ============================================================================
123
- // OAUTH2 UTILITIES
124
- // ============================================================================
125
-
126
- function generateState() {
127
- return crypto.randomBytes(32).toString('hex');
128
- }
129
-
130
- function generateCodeVerifier() {
131
- return crypto.randomBytes(32).toString('base64url');
132
- }
133
-
134
- function generateCodeChallenge(verifier) {
135
- return crypto.createHash('sha256').update(verifier).digest('base64url');
136
- }
137
-
138
- function isValidToken(tokenData) {
139
- if (!tokenData || !tokenData.access_token) return false;
140
- if (!tokenData.expires_at) return true; // No expiry set
141
- return new Date().getTime() < tokenData.expires_at;
142
- }
143
-
144
- async function refreshAccessToken(refreshToken) {
145
- return new Promise((resolve, reject) => {
146
- const postData = querystring.stringify({
147
- grant_type: 'refresh_token',
148
- refresh_token: refreshToken,
149
- client_id: OAUTH_CONFIG.clientId,
150
- client_secret: OAUTH_CONFIG.clientSecret
151
- });
152
-
153
- const options = {
154
- hostname: 'airtable.com',
155
- path: '/oauth2/v1/token',
156
- method: 'POST',
157
- headers: {
158
- 'Content-Type': 'application/x-www-form-urlencoded',
159
- 'Content-Length': Buffer.byteLength(postData)
160
- }
161
- };
162
-
163
- const req = https.request(options, (res) => {
164
- let data = '';
165
- res.on('data', (chunk) => data += chunk);
166
- res.on('end', () => {
167
- try {
168
- const tokenData = JSON.parse(data);
169
- if (res.statusCode === 200) {
170
- // Add expiry time
171
- tokenData.expires_at = new Date().getTime() + (tokenData.expires_in * 1000);
172
- resolve(tokenData);
173
- } else {
174
- reject(new Error(`Token refresh failed: ${tokenData.error_description || tokenData.error}`));
175
- }
176
- } catch (e) {
177
- reject(new Error(`Failed to parse token response: ${e.message}`));
178
- }
179
- });
180
- });
181
-
182
- req.on('error', reject);
183
- req.write(postData);
184
- req.end();
185
- });
186
- }
187
-
188
- // ============================================================================
189
- // RATE LIMITING
190
- // ============================================================================
191
-
192
- function checkRateLimit(clientId) {
193
- const now = Date.now();
194
- const windowStart = now - 60000; // 1 minute window
195
-
196
- if (!rateLimiter.has(clientId)) {
197
- rateLimiter.set(clientId, []);
198
- }
199
-
200
- const requests = rateLimiter.get(clientId);
201
- // Remove old requests
202
- const recentRequests = requests.filter(time => time > windowStart);
203
-
204
- if (recentRequests.length >= MAX_REQUESTS_PER_MINUTE) {
205
- return false; // Rate limit exceeded
206
- }
207
-
208
- recentRequests.push(now);
209
- rateLimiter.set(clientId, recentRequests);
210
- return true;
211
- }
212
-
213
- // ============================================================================
214
- // INPUT VALIDATION & SANITIZATION
215
- // ============================================================================
216
-
217
- function sanitizeInput(input) {
218
- if (typeof input === 'string') {
219
- return input
220
- .replace(/[<>]/g, '') // Remove potential XSS characters
221
- .trim()
222
- .substring(0, 1000); // Limit length
223
- }
224
- return input;
225
- }
226
-
227
- function validateMCPRequest(request) {
228
- const errors = [];
229
-
230
- if (!request.jsonrpc || request.jsonrpc !== '2.0') {
231
- errors.push('Invalid or missing jsonrpc version');
232
- }
233
-
234
- if (request.id === undefined || request.id === null) {
235
- errors.push('Missing request ID');
236
- }
237
-
238
- if (!request.method || typeof request.method !== 'string') {
239
- errors.push('Invalid or missing method');
240
- }
241
-
242
- // Method-specific validation
243
- if (request.method === 'tools/call') {
244
- if (!request.params?.name) {
245
- errors.push('Tool name required for tools/call');
246
- }
247
- }
248
-
249
- return errors;
250
- }
251
-
252
- // ============================================================================
253
- // AIRTABLE API WITH OAUTH2 SUPPORT
254
- // ============================================================================
255
-
256
- async function callAirtableAPI(endpoint, method = 'GET', body = null, queryParams = {}, accessToken = null) {
257
- return new Promise((resolve, reject) => {
258
- // Use OAuth token if provided, otherwise fall back to API token
259
- const authToken = accessToken || token;
260
- const authHeader = accessToken ? `Bearer ${accessToken}` : `Bearer ${authToken}`;
261
-
262
- const isBaseEndpoint = !endpoint.startsWith('meta/');
263
- const baseUrl = isBaseEndpoint ? `${baseId}/${endpoint}` : endpoint;
264
-
265
- const queryString = Object.keys(queryParams).length > 0
266
- ? '?' + new URLSearchParams(queryParams).toString()
267
- : '';
268
-
269
- const apiUrl = `https://api.airtable.com/v0/${baseUrl}${queryString}`;
270
- const urlObj = new URL(apiUrl);
271
-
272
- log(LOG_LEVELS.DEBUG, '๐ŸŒ API Request', {
273
- method,
274
- url: apiUrl,
275
- hasBody: !!body,
276
- authType: accessToken ? 'OAuth2' : 'API_Token'
277
- });
278
-
279
- const options = {
280
- hostname: urlObj.hostname,
281
- path: urlObj.pathname + urlObj.search,
282
- method: method,
283
- headers: {
284
- 'Authorization': authHeader,
285
- 'Content-Type': 'application/json',
286
- 'User-Agent': 'Enhanced-Airtable-MCP-v2.1'
287
- }
288
- };
289
-
290
- const req = https.request(options, (response) => {
291
- let data = '';
292
-
293
- response.on('data', (chunk) => data += chunk);
294
- response.on('end', () => {
295
- log(LOG_LEVELS.TRACE, '๐Ÿ“ฅ API Response', {
296
- status: response.statusCode,
297
- dataLength: data.length
298
- });
299
-
300
- try {
301
- const parsed = data ? JSON.parse(data) : {};
302
-
303
- if (response.statusCode >= 200 && response.statusCode < 300) {
304
- resolve(parsed);
305
- } else {
306
- const error = parsed.error || {};
307
- reject(new Error(`Airtable API error (${response.statusCode}): ${error.message || error.type || 'Unknown error'}`));
308
- }
309
- } catch (e) {
310
- reject(new Error(`Failed to parse Airtable response: ${e.message}`));
311
- }
312
- });
313
- });
314
-
315
- req.on('error', (error) => {
316
- log(LOG_LEVELS.ERROR, '๐Ÿ’ฅ API Request failed', { error: error.message });
317
- reject(new Error(`Airtable API request failed: ${error.message}`));
318
- });
319
-
320
- if (body) {
321
- req.write(JSON.stringify(body));
322
- }
323
-
324
- req.end();
325
- });
326
- }
327
-
328
- // ============================================================================
329
- // ENHANCED MCP TOOLS SCHEMA WITH OAUTH2
330
- // ============================================================================
331
-
332
- const TOOLS_SCHEMA = [
333
- {
334
- name: 'list_tables',
335
- description: 'List all tables in the Airtable base with enhanced metadata',
336
- inputSchema: {
337
- type: 'object',
338
- properties: {
339
- include_schema: {
340
- type: 'boolean',
341
- description: 'Include detailed field schema information',
342
- default: false
343
- }
344
- },
345
- additionalProperties: false
346
- }
347
- },
348
- {
349
- name: 'oauth_authorize',
350
- description: 'Initiate OAuth2 authorization flow for enhanced security',
351
- inputSchema: {
352
- type: 'object',
353
- properties: {
354
- redirect_uri: {
355
- type: 'string',
356
- description: 'OAuth redirect URI (optional, uses default)',
357
- format: 'uri'
358
- }
359
- },
360
- additionalProperties: false
361
- }
362
- },
363
- {
364
- name: 'oauth_status',
365
- description: 'Check current OAuth2 authentication status',
366
- inputSchema: {
367
- type: 'object',
368
- properties: {},
369
- additionalProperties: false
370
- }
371
- },
372
- {
373
- name: 'security_audit',
374
- description: 'Perform security audit of current configuration',
375
- inputSchema: {
376
- type: 'object',
377
- properties: {
378
- include_tokens: {
379
- type: 'boolean',
380
- description: 'Include token validation in audit',
381
- default: false
382
- }
383
- },
384
- additionalProperties: false
385
- }
386
- },
387
- // ... additional enhanced tools
388
- ];
389
-
390
- // ============================================================================
391
- // HTTP SERVER WITH OAUTH2 AND SECURITY
392
- // ============================================================================
393
-
394
- const server = http.createServer(async (req, res) => {
395
- // Apply security headers
396
- Object.entries(SECURITY_HEADERS).forEach(([key, value]) => {
397
- res.setHeader(key, value);
398
- });
399
-
400
- // Enable CORS with restrictions
401
- res.setHeader('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGINS || '*');
402
- res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
403
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
404
-
405
- // Handle preflight request
406
- if (req.method === 'OPTIONS') {
407
- res.writeHead(200);
408
- res.end();
409
- return;
410
- }
411
-
412
- const parsedUrl = url.parse(req.url, true);
413
- const pathname = parsedUrl.pathname;
414
-
415
- // OAuth2 endpoints
416
- if (pathname === '/oauth/authorize') {
417
- handleOAuthAuthorize(req, res);
418
- return;
419
- }
420
-
421
- if (pathname === '/oauth/callback') {
422
- handleOAuthCallback(req, res, parsedUrl.query);
423
- return;
424
- }
425
-
426
- // Health check endpoint
427
- if (pathname === '/health') {
428
- res.writeHead(200, { 'Content-Type': 'application/json' });
429
- res.end(JSON.stringify({
430
- status: 'healthy',
431
- version: '2.1.0',
432
- features: ['oauth2', 'security', 'rate-limiting'],
433
- uptime: process.uptime()
434
- }));
435
- return;
436
- }
437
-
438
- // Documentation endpoint
439
- if (pathname === '/docs') {
440
- handleDocsEndpoint(req, res);
441
- return;
442
- }
443
-
444
- // MCP endpoint
445
- if (pathname === '/mcp' && req.method === 'POST') {
446
- // Rate limiting check
447
- const clientId = req.headers['x-client-id'] || req.connection.remoteAddress;
448
- if (!checkRateLimit(clientId)) {
449
- res.writeHead(429, { 'Content-Type': 'application/json' });
450
- res.end(JSON.stringify({
451
- jsonrpc: '2.0',
452
- error: {
453
- code: -32000,
454
- message: 'Rate limit exceeded. Maximum 60 requests per minute.'
455
- }
456
- }));
457
- return;
458
- }
459
-
460
- handleMCPRequest(req, res);
461
- return;
462
- }
463
-
464
- // Default 404
465
- res.writeHead(404, { 'Content-Type': 'application/json' });
466
- res.end(JSON.stringify({ error: 'Not Found' }));
467
- });
468
-
469
- // ============================================================================
470
- // OAUTH2 HANDLERS
471
- // ============================================================================
472
-
473
- function handleOAuthAuthorize(req, res) {
474
- const state = generateState();
475
- const codeVerifier = generateCodeVerifier();
476
- const codeChallenge = generateCodeChallenge(codeVerifier);
477
-
478
- // Store session data
479
- authSessions.set(state, {
480
- codeVerifier,
481
- timestamp: Date.now()
482
- });
483
-
484
- const authUrl = new URL(OAUTH_CONFIG.authorizeUrl);
485
- authUrl.searchParams.set('client_id', OAUTH_CONFIG.clientId);
486
- authUrl.searchParams.set('redirect_uri', OAUTH_CONFIG.redirectUri);
487
- authUrl.searchParams.set('response_type', 'code');
488
- authUrl.searchParams.set('scope', OAUTH_CONFIG.scope);
489
- authUrl.searchParams.set('state', state);
490
- authUrl.searchParams.set('code_challenge', codeChallenge);
491
- authUrl.searchParams.set('code_challenge_method', 'S256');
492
-
493
- log(LOG_LEVELS.INFO, '๐Ÿ” OAuth2 authorization initiated', { state });
494
-
495
- res.writeHead(302, { 'Location': authUrl.toString() });
496
- res.end();
497
- }
498
-
499
- async function handleOAuthCallback(req, res, query) {
500
- try {
501
- const { code, state, error } = query;
502
-
503
- if (error) {
504
- throw new Error(`OAuth error: ${error}`);
505
- }
506
-
507
- if (!code || !state) {
508
- throw new Error('Missing authorization code or state');
509
- }
510
-
511
- const session = authSessions.get(state);
512
- if (!session) {
513
- throw new Error('Invalid or expired state parameter');
514
- }
515
-
516
- // Exchange code for token
517
- const tokenData = await exchangeCodeForToken(code, session.codeVerifier);
518
-
519
- // Store token
520
- const tokenId = crypto.randomUUID();
521
- tokenStorage.set(tokenId, tokenData);
522
-
523
- // Clean up session
524
- authSessions.delete(state);
525
-
526
- log(LOG_LEVELS.INFO, 'โœ… OAuth2 authentication successful', { tokenId });
527
-
528
- res.writeHead(200, { 'Content-Type': 'text/html' });
529
- res.end(`
530
- <html>
531
- <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
532
- <h1>โœ… Authentication Successful!</h1>
533
- <p>Your Airtable MCP server is now authenticated with OAuth2.</p>
534
- <p>Token ID: <code>${tokenId}</code></p>
535
- <p>You can now close this window and return to your MCP client.</p>
536
- </body>
537
- </html>
538
- `);
539
-
540
- } catch (error) {
541
- log(LOG_LEVELS.ERROR, '๐Ÿ’ฅ OAuth callback failed', { error: error.message });
542
-
543
- res.writeHead(400, { 'Content-Type': 'text/html' });
544
- res.end(`
545
- <html>
546
- <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
547
- <h1>โŒ Authentication Failed</h1>
548
- <p>Error: ${error.message}</p>
549
- <p>Please try again or contact support.</p>
550
- </body>
551
- </html>
552
- `);
553
- }
554
- }
555
-
556
- async function exchangeCodeForToken(code, codeVerifier) {
557
- return new Promise((resolve, reject) => {
558
- const postData = querystring.stringify({
559
- grant_type: 'authorization_code',
560
- code: code,
561
- client_id: OAUTH_CONFIG.clientId,
562
- client_secret: OAUTH_CONFIG.clientSecret,
563
- redirect_uri: OAUTH_CONFIG.redirectUri,
564
- code_verifier: codeVerifier
565
- });
566
-
567
- const options = {
568
- hostname: 'airtable.com',
569
- path: '/oauth2/v1/token',
570
- method: 'POST',
571
- headers: {
572
- 'Content-Type': 'application/x-www-form-urlencoded',
573
- 'Content-Length': Buffer.byteLength(postData)
574
- }
575
- };
576
-
577
- const req = https.request(options, (res) => {
578
- let data = '';
579
- res.on('data', (chunk) => data += chunk);
580
- res.on('end', () => {
581
- try {
582
- const tokenData = JSON.parse(data);
583
- if (res.statusCode === 200) {
584
- // Add expiry time
585
- tokenData.expires_at = new Date().getTime() + (tokenData.expires_in * 1000);
586
- resolve(tokenData);
587
- } else {
588
- reject(new Error(`Token exchange failed: ${tokenData.error_description || tokenData.error}`));
589
- }
590
- } catch (e) {
591
- reject(new Error(`Failed to parse token response: ${e.message}`));
592
- }
593
- });
594
- });
595
-
596
- req.on('error', reject);
597
- req.write(postData);
598
- req.end();
599
- });
600
- }
601
-
602
- // ============================================================================
603
- // ENHANCED MCP REQUEST HANDLER
604
- // ============================================================================
605
-
606
- async function handleMCPRequest(req, res) {
607
- let body = '';
608
- req.on('data', chunk => body += chunk.toString());
609
-
610
- req.on('end', async () => {
611
- try {
612
- const request = JSON.parse(body);
613
-
614
- // Validate request
615
- const validationErrors = validateMCPRequest(request);
616
- if (validationErrors.length > 0) {
617
- throw new Error(`Validation failed: ${validationErrors.join(', ')}`);
618
- }
619
-
620
- // Sanitize inputs
621
- if (request.params) {
622
- Object.keys(request.params).forEach(key => {
623
- request.params[key] = sanitizeInput(request.params[key]);
624
- });
625
- }
626
-
627
- log(LOG_LEVELS.TRACE, '๐Ÿ“จ MCP request received', {
628
- method: request.method,
629
- id: request.id,
630
- hasParams: !!request.params
631
- });
632
-
633
- let response;
634
-
635
- switch (request.method) {
636
- case 'initialize':
637
- response = {
638
- jsonrpc: '2.0',
639
- id: request.id,
640
- result: {
641
- protocolVersion: '2024-11-05',
642
- capabilities: {
643
- tools: { listChanged: true },
644
- resources: { subscribe: true, listChanged: true },
645
- prompts: { listChanged: true },
646
- sampling: {},
647
- roots: { listChanged: true },
648
- logging: {},
649
- authentication: { oauth2: true, apiKey: true }
650
- },
651
- serverInfo: {
652
- name: 'Enhanced Airtable MCP Server v2.1',
653
- version: '2.1.0',
654
- description: 'Complete MCP protocol with OAuth2, security, and enterprise features',
655
- author: 'Rashid Azarang',
656
- homepage: 'https://github.com/rashidazarang/airtable-mcp',
657
- capabilities: [
658
- 'full-mcp-protocol',
659
- 'oauth2-authentication',
660
- 'enterprise-security',
661
- 'rate-limiting',
662
- 'input-validation',
663
- 'real-time-logging'
664
- ]
665
- }
666
- }
667
- };
668
- log(LOG_LEVELS.INFO, '๐Ÿ”„ Client initialized', {
669
- clientId: request.id,
670
- protocol: '2024-11-05',
671
- features: 8
672
- });
673
- break;
674
-
675
- case 'tools/list':
676
- response = {
677
- jsonrpc: '2.0',
678
- id: request.id,
679
- result: {
680
- tools: TOOLS_SCHEMA
681
- }
682
- };
683
- log(LOG_LEVELS.DEBUG, '๐Ÿ› ๏ธ Tools list provided', { count: TOOLS_SCHEMA.length });
684
- break;
685
-
686
- case 'tools/call':
687
- response = await handleToolCall(request);
688
- break;
689
-
690
- case 'logging/setLevel':
691
- const newLevel = request.params.level?.toUpperCase();
692
- if (LOG_LEVELS[newLevel] !== undefined) {
693
- const oldLevel = Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === currentLogLevel);
694
- currentLogLevel = LOG_LEVELS[newLevel];
695
-
696
- response = {
697
- jsonrpc: '2.0',
698
- id: request.id,
699
- result: {
700
- level: newLevel,
701
- previousLevel: oldLevel,
702
- availableLevels: Object.keys(LOG_LEVELS)
703
- }
704
- };
705
-
706
- log(LOG_LEVELS.INFO, '๐Ÿ“ Log level changed', { from: oldLevel, to: newLevel });
707
- } else {
708
- throw new Error(`Invalid log level: ${newLevel}. Available: ${Object.keys(LOG_LEVELS).join(', ')}`);
709
- }
710
- break;
711
-
712
- default:
713
- log(LOG_LEVELS.WARN, 'โ“ Unknown method', { method: request.method });
714
- throw new Error(`Method "${request.method}" not found`);
715
- }
716
-
717
- res.writeHead(200, { 'Content-Type': 'application/json' });
718
- res.end(JSON.stringify(response));
719
-
720
- log(LOG_LEVELS.TRACE, '๐Ÿ“ค Response sent successfully', {
721
- method: request.method,
722
- responseSize: JSON.stringify(response).length
723
- });
724
-
725
- } catch (error) {
726
- log(LOG_LEVELS.ERROR, '๐Ÿ’ฅ Request processing failed', {
727
- error: error.message,
728
- stack: error.stack?.split('\n').slice(0, 3).join('\n')
729
- });
730
-
731
- const errorResponse = {
732
- jsonrpc: '2.0',
733
- id: request?.id || null,
734
- error: {
735
- code: -32000,
736
- message: error.message || 'Internal server error'
737
- }
738
- };
739
-
740
- res.writeHead(200, { 'Content-Type': 'application/json' });
741
- res.end(JSON.stringify(errorResponse));
742
- }
743
- });
744
- }
745
-
746
- // ============================================================================
747
- // ENHANCED TOOL HANDLERS
748
- // ============================================================================
749
-
750
- async function handleToolCall(request) {
751
- const toolName = request.params.name;
752
- const toolParams = request.params.arguments || {};
753
-
754
- let result;
755
- let responseText;
756
-
757
- try {
758
- switch (toolName) {
759
- case 'oauth_authorize':
760
- const redirectUri = toolParams.redirect_uri || OAUTH_CONFIG.redirectUri;
761
- result = {
762
- authorization_url: `http://localhost:8010/oauth/authorize`,
763
- instructions: 'Visit the authorization URL to authenticate with Airtable OAuth2',
764
- redirect_uri: redirectUri
765
- };
766
- responseText = `๐Ÿ” **OAuth2 Authorization**\n\nTo authenticate with enhanced security:\n\n1. Visit: http://localhost:8010/oauth/authorize\n2. Login to your Airtable account\n3. Grant permissions to the MCP server\n4. You'll be redirected back with a token\n\n**Benefits of OAuth2:**\n- Enhanced security with token rotation\n- Granular permission control\n- Audit trail of access\n- Automatic token refresh`;
767
- break;
768
-
769
- case 'oauth_status':
770
- const tokens = Array.from(tokenStorage.values());
771
- const validTokens = tokens.filter(isValidToken);
772
-
773
- result = {
774
- authenticated: validTokens.length > 0,
775
- total_tokens: tokens.length,
776
- valid_tokens: validTokens.length,
777
- expired_tokens: tokens.length - validTokens.length
778
- };
779
- responseText = `๐Ÿ” **OAuth2 Status**\n\n${validTokens.length > 0 ? 'โœ… Authenticated' : 'โŒ Not authenticated'}\n\n**Token Summary:**\n- Total tokens: ${tokens.length}\n- Valid tokens: ${validTokens.length}\n- Expired tokens: ${tokens.length - validTokens.length}\n\n${validTokens.length === 0 ? 'Use `oauth_authorize` to authenticate.' : 'OAuth2 authentication is active and working.'}`;
780
- break;
781
-
782
- case 'security_audit':
783
- const auditResults = {
784
- rate_limiting: 'Active',
785
- security_headers: 'Enabled',
786
- input_validation: 'Active',
787
- oauth2_support: 'Available',
788
- token_encryption: 'In-memory storage',
789
- cors_policy: 'Configured',
790
- recommendations: []
791
- };
792
-
793
- if (tokenStorage.size === 0) {
794
- auditResults.recommendations.push('Consider using OAuth2 for enhanced security');
795
- }
796
-
797
- if (!process.env.OAUTH_CLIENT_SECRET) {
798
- auditResults.recommendations.push('Set OAuth2 client credentials for production');
799
- }
800
-
801
- responseText = `๐Ÿ›ก๏ธ **Security Audit Results**\n\nโœ… **Active Security Features:**\n- Rate limiting (${MAX_REQUESTS_PER_MINUTE} req/min)\n- Security headers enabled\n- Input validation and sanitization\n- OAuth2 authentication support\n- CORS policy configured\n\n${auditResults.recommendations.length > 0 ? 'โš ๏ธ **Recommendations:**\n' + auditResults.recommendations.map(r => `- ${r}`).join('\n') : '๐ŸŽ‰ **All security checks passed!**'}`;
802
- break;
803
-
804
- case 'list_tables':
805
- const includeSchema = toolParams.include_schema || false;
806
- const endpoint = includeSchema ? `meta/bases/${baseId}/tables` : `meta/bases/${baseId}/tables`;
807
-
808
- result = await callAirtableAPI(endpoint);
809
- const tables = result.tables || [];
810
-
811
- responseText = tables.length > 0
812
- ? `๐Ÿ“Š **Found ${tables.length} table(s):**\n\n` + tables.map((table, i) =>
813
- `**${i+1}. ${table.name}**\n โ€ข ID: \`${table.id}\`\n โ€ข Fields: ${table.fields?.length || 0}${includeSchema && table.fields ? '\n โ€ข Schema: ' + table.fields.map(f => `${f.name} (${f.type})`).join(', ') : ''}`
814
- ).join('\n\n')
815
- : '๐Ÿ“ญ No tables found in this base.';
816
- break;
817
-
818
- default:
819
- throw new Error(`Unknown tool: ${toolName}`);
820
- }
821
-
822
- return {
823
- jsonrpc: '2.0',
824
- id: request.id,
825
- result: {
826
- content: [
827
- {
828
- type: 'text',
829
- text: responseText
830
- }
831
- ]
832
- }
833
- };
834
-
835
- } catch (error) {
836
- log(LOG_LEVELS.ERROR, `๐Ÿ’ฅ Tool ${toolName} failed`, { error: error.message });
837
-
838
- return {
839
- jsonrpc: '2.0',
840
- id: request.id,
841
- result: {
842
- content: [
843
- {
844
- type: 'text',
845
- text: `โŒ Error executing ${toolName}: ${error.message}`
846
- }
847
- ]
848
- }
849
- };
850
- }
851
- }
852
-
853
- // ============================================================================
854
- // DOCUMENTATION ENDPOINT
855
- // ============================================================================
856
-
857
- function handleDocsEndpoint(req, res) {
858
- const docs = `
859
- <!DOCTYPE html>
860
- <html>
861
- <head>
862
- <title>Enhanced Airtable MCP Server v2.1 - Documentation</title>
863
- <style>
864
- body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
865
- .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 10px; text-align: center; }
866
- .section { margin: 30px 0; padding: 20px; border-left: 4px solid #667eea; background: #f8f9fa; }
867
- .endpoint { background: #e9ecef; padding: 15px; margin: 10px 0; border-radius: 5px; }
868
- .feature { display: inline-block; background: #28a745; color: white; padding: 5px 10px; margin: 5px; border-radius: 15px; font-size: 12px; }
869
- code { background: #f1f3f4; padding: 2px 5px; border-radius: 3px; }
870
- </style>
871
- </head>
872
- <body>
873
- <div class="header">
874
- <h1>๐Ÿš€ Enhanced Airtable MCP Server v2.1</h1>
875
- <p>OAuth2 โ€ข Security โ€ข Rate Limiting โ€ข Enterprise Ready</p>
876
- <div>
877
- <span class="feature">OAuth2</span>
878
- <span class="feature">Security Headers</span>
879
- <span class="feature">Rate Limiting</span>
880
- <span class="feature">Input Validation</span>
881
- <span class="feature">Comprehensive Logging</span>
882
- </div>
883
- </div>
884
-
885
- <div class="section">
886
- <h2>๐Ÿ” OAuth2 Authentication</h2>
887
- <p>Enhanced security with OAuth2 flow supporting PKCE and automatic token refresh.</p>
888
- <div class="endpoint">
889
- <strong>GET /oauth/authorize</strong> - Initiate OAuth2 flow
890
- </div>
891
- <div class="endpoint">
892
- <strong>GET /oauth/callback</strong> - OAuth2 callback handler
893
- </div>
894
- </div>
895
-
896
- <div class="section">
897
- <h2>๐Ÿ› ๏ธ Enhanced Tools</h2>
898
- <ul>
899
- <li><code>oauth_authorize</code> - Start OAuth2 authentication</li>
900
- <li><code>oauth_status</code> - Check authentication status</li>
901
- <li><code>security_audit</code> - Perform security audit</li>
902
- <li><code>list_tables</code> - List tables with enhanced metadata</li>
903
- </ul>
904
- </div>
905
-
906
- <div class="section">
907
- <h2>๐Ÿ›ก๏ธ Security Features</h2>
908
- <ul>
909
- <li><strong>Rate Limiting:</strong> ${MAX_REQUESTS_PER_MINUTE} requests per minute per client</li>
910
- <li><strong>Security Headers:</strong> CSP, HSTS, XSS Protection, Frame Options</li>
911
- <li><strong>Input Validation:</strong> Request sanitization and validation</li>
912
- <li><strong>OAuth2 PKCE:</strong> Secure authorization code flow</li>
913
- </ul>
914
- </div>
915
-
916
- <div class="section">
917
- <h2>๐Ÿ“Š Monitoring</h2>
918
- <div class="endpoint">
919
- <strong>GET /health</strong> - Health check endpoint
920
- </div>
921
- <div class="endpoint">
922
- <strong>GET /docs</strong> - This documentation
923
- </div>
924
- <div class="endpoint">
925
- <strong>POST /mcp</strong> - MCP protocol endpoint
926
- </div>
927
- </div>
928
-
929
- <div class="section">
930
- <h2>๐Ÿš€ Quick Start</h2>
931
- <ol>
932
- <li>Set environment variables: <code>AIRTABLE_TOKEN</code>, <code>AIRTABLE_BASE_ID</code></li>
933
- <li>Optional: Configure OAuth2 with <code>AIRTABLE_CLIENT_ID</code> and <code>AIRTABLE_CLIENT_SECRET</code></li>
934
- <li>Start server: <code>node airtable_mcp_v2_oauth.js</code></li>
935
- <li>Connect your MCP client to <code>http://localhost:8010/mcp</code></li>
936
- </ol>
937
- </div>
938
- </body>
939
- </html>
940
- `;
941
-
942
- res.writeHead(200, { 'Content-Type': 'text/html' });
943
- res.end(docs);
944
- }
945
-
946
- // ============================================================================
947
- // SERVER STARTUP
948
- // ============================================================================
949
-
950
- const PORT = process.env.PORT || 8010;
951
- const HOST = process.env.HOST || 'localhost';
952
-
953
- // Graceful shutdown handler
954
- function gracefulShutdown(signal) {
955
- log(LOG_LEVELS.INFO, '๐Ÿ›‘ Graceful shutdown initiated', { signal });
956
-
957
- server.close(() => {
958
- log(LOG_LEVELS.INFO, 'โœ… HTTP server closed');
959
-
960
- // Clear sensitive data
961
- tokenStorage.clear();
962
- authSessions.clear();
963
- rateLimiter.clear();
964
-
965
- log(LOG_LEVELS.INFO, '๐Ÿงน Cleanup completed');
966
- process.exit(0);
967
- });
968
-
969
- // Force shutdown after 10 seconds
970
- setTimeout(() => {
971
- log(LOG_LEVELS.ERROR, 'โฐ Force shutdown - server did not close in time');
972
- process.exit(1);
973
- }, 10000);
974
- }
975
-
976
- // Error handling
977
- process.on('uncaughtException', (error) => {
978
- log(LOG_LEVELS.ERROR, '๐Ÿ’ฅ Uncaught exception', {
979
- error: error.message,
980
- stack: error.stack
981
- });
982
- gracefulShutdown('uncaughtException');
983
- });
984
-
985
- process.on('unhandledRejection', (reason, promise) => {
986
- log(LOG_LEVELS.ERROR, '๐Ÿ’ฅ Unhandled promise rejection', {
987
- reason: reason?.toString(),
988
- promise: promise?.toString()
989
- });
990
- gracefulShutdown('unhandledRejection');
991
- });
992
-
993
- process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
994
- process.on('SIGINT', () => gracefulShutdown('SIGINT'));
995
-
996
- // Start the server
997
- server.listen(PORT, HOST, () => {
998
- log(LOG_LEVELS.INFO, '๐Ÿš€ Enhanced Airtable MCP Server v2.1 started successfully', {
999
- host: HOST,
1000
- port: PORT,
1001
- endpoints: {
1002
- mcp: `http://${HOST}:${PORT}/mcp`,
1003
- oauth: `http://${HOST}:${PORT}/oauth/authorize`,
1004
- health: `http://${HOST}:${PORT}/health`,
1005
- docs: `http://${HOST}:${PORT}/docs`
1006
- },
1007
- features: {
1008
- oauth2: true,
1009
- security: true,
1010
- rateLimit: MAX_REQUESTS_PER_MINUTE,
1011
- logging: Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === currentLogLevel)
1012
- }
1013
- });
1014
-
1015
- // Display banner
1016
- console.log(`
1017
- โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
1018
- โ•‘ ๐Ÿš€ ENHANCED AIRTABLE MCP SERVER v2.1 โ•‘
1019
- โ•‘ OAuth2 โ€ข Security โ€ข Enterprise โ•‘
1020
- โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
1021
- โ•‘ ๐ŸŒ MCP Endpoint: http://${HOST}:${PORT}/mcp โ•‘
1022
- โ•‘ ๐Ÿ” OAuth2: http://${HOST}:${PORT}/oauth/authorize โ•‘
1023
- โ•‘ ๐Ÿ“Š Health: http://${HOST}:${PORT}/health โ•‘
1024
- โ•‘ ๐Ÿ“š Docs: http://${HOST}:${PORT}/docs โ•‘
1025
- โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
1026
- โ•‘ ๐ŸŽฏ TRUST SCORE TARGET: 100/100 POINTS โ•‘
1027
- โ•‘ โ•‘
1028
- โ•‘ ๐Ÿ”ฅ NEW IN v2.1: โ•‘
1029
- โ•‘ โ€ข OAuth2 authentication with PKCE โ•‘
1030
- โ•‘ โ€ข Enterprise security features โ•‘
1031
- โ•‘ โ€ข Rate limiting (${MAX_REQUESTS_PER_MINUTE} req/min) โ•‘
1032
- โ•‘ โ€ข Input validation & sanitization โ•‘
1033
- โ•‘ โ€ข Security headers & CSP โ•‘
1034
- โ•‘ โ€ข Comprehensive audit tools โ•‘
1035
- โ•‘ โ•‘
1036
- โ•‘ ๐ŸŽฏ COMPATIBLE WITH: โ•‘
1037
- โ•‘ โ€ข Claude Desktop โ•‘
1038
- โ•‘ โ€ข Cursor IDE โ•‘
1039
- โ•‘ โ€ข Cline VS Code Extension โ•‘
1040
- โ•‘ โ€ข Zed Editor โ•‘
1041
- โ•‘ โ€ข Any MCP-enabled client โ•‘
1042
- โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
1043
- โ•‘ ๐Ÿ”— Connected to Airtable Base: ${baseId.slice(0, 8)}... โ•‘
1044
- โ•‘ ๐Ÿš€ Ready to achieve 100/100 Trust Score! โ•‘
1045
- โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1046
- `);
1047
- });
1048
-