@rashidazarang/airtable-mcp 2.1.1 → 2.2.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 -1
- package/airtable_simple_production.js +472 -9
- package/examples/claude_simple_config.json +0 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Airtable MCP Server
|
|
2
2
|
|
|
3
|
+
[](https://archestra.ai/mcp-catalog/rashidazarang__airtable-mcp)
|
|
3
4
|
[](https://smithery.ai/server/@rashidazarang/airtable-mcp)
|
|
4
|
-
[](https://archestra.ai/mcp-catalog/rashidazarang__airtable-mcp)
|
|
5
5
|

|
|
6
6
|
[](https://github.com/rashidazarang/airtable-mcp)
|
|
7
7
|
|
|
@@ -56,18 +56,21 @@ const CONFIG = {
|
|
|
56
56
|
|
|
57
57
|
// Logging
|
|
58
58
|
const LOG_LEVELS = { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3, TRACE: 4 };
|
|
59
|
-
|
|
59
|
+
let currentLogLevel = LOG_LEVELS[CONFIG.LOG_LEVEL] || LOG_LEVELS.INFO;
|
|
60
60
|
|
|
61
61
|
function log(level, message, metadata = {}) {
|
|
62
62
|
if (level <= currentLogLevel) {
|
|
63
63
|
const timestamp = new Date().toISOString();
|
|
64
64
|
const levelName = Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === level);
|
|
65
|
-
|
|
65
|
+
// Sanitize message to prevent format string attacks
|
|
66
|
+
const safeMessage = String(message).replace(/%/g, '%%');
|
|
67
|
+
const output = `[${timestamp}] [${levelName}] ${safeMessage}`;
|
|
66
68
|
|
|
67
69
|
if (Object.keys(metadata).length > 0) {
|
|
68
|
-
|
|
70
|
+
// Use separate arguments to avoid format string injection
|
|
71
|
+
console.log('%s %s', output, JSON.stringify(metadata));
|
|
69
72
|
} else {
|
|
70
|
-
console.log(output);
|
|
73
|
+
console.log('%s', output);
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
}
|
|
@@ -95,7 +98,7 @@ function checkRateLimit(clientId) {
|
|
|
95
98
|
return true;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
// Input validation
|
|
101
|
+
// Input validation and HTML escaping
|
|
99
102
|
function sanitizeInput(input) {
|
|
100
103
|
if (typeof input === 'string') {
|
|
101
104
|
return input.replace(/[<>]/g, '').trim().substring(0, 1000);
|
|
@@ -103,6 +106,29 @@ function sanitizeInput(input) {
|
|
|
103
106
|
return input;
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
function escapeHtml(unsafe) {
|
|
110
|
+
if (typeof unsafe !== 'string') {
|
|
111
|
+
return String(unsafe);
|
|
112
|
+
}
|
|
113
|
+
return unsafe
|
|
114
|
+
.replace(/&/g, "&")
|
|
115
|
+
.replace(/</g, "<")
|
|
116
|
+
.replace(/>/g, ">")
|
|
117
|
+
.replace(/"/g, """)
|
|
118
|
+
.replace(/'/g, "'")
|
|
119
|
+
.replace(/\//g, "/");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateUrl(url) {
|
|
123
|
+
try {
|
|
124
|
+
const parsed = new URL(url);
|
|
125
|
+
// Only allow http and https protocols
|
|
126
|
+
return ['http:', 'https:'].includes(parsed.protocol);
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
106
132
|
// Airtable API integration
|
|
107
133
|
function callAirtableAPI(endpoint, method = 'GET', body = null, queryParams = {}) {
|
|
108
134
|
return new Promise((resolve, reject) => {
|
|
@@ -236,6 +262,98 @@ const TOOLS_SCHEMA = [
|
|
|
236
262
|
}
|
|
237
263
|
];
|
|
238
264
|
|
|
265
|
+
// Prompts schema - AI-powered templates for common Airtable operations
|
|
266
|
+
const PROMPTS_SCHEMA = [
|
|
267
|
+
{
|
|
268
|
+
name: 'analyze_data',
|
|
269
|
+
description: 'Analyze data patterns and provide insights from Airtable records',
|
|
270
|
+
arguments: [
|
|
271
|
+
{
|
|
272
|
+
name: 'table',
|
|
273
|
+
description: 'Table name or ID to analyze',
|
|
274
|
+
required: true
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'analysis_type',
|
|
278
|
+
description: 'Type of analysis (trends, summary, patterns, insights)',
|
|
279
|
+
required: false
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'field_focus',
|
|
283
|
+
description: 'Specific fields to focus the analysis on',
|
|
284
|
+
required: false
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'create_report',
|
|
290
|
+
description: 'Generate a comprehensive report based on Airtable data',
|
|
291
|
+
arguments: [
|
|
292
|
+
{
|
|
293
|
+
name: 'table',
|
|
294
|
+
description: 'Table name or ID for the report',
|
|
295
|
+
required: true
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: 'report_type',
|
|
299
|
+
description: 'Type of report (summary, detailed, dashboard, metrics)',
|
|
300
|
+
required: false
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
name: 'time_period',
|
|
304
|
+
description: 'Time period for the report (if applicable)',
|
|
305
|
+
required: false
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: 'data_insights',
|
|
311
|
+
description: 'Discover hidden insights and correlations in your Airtable data',
|
|
312
|
+
arguments: [
|
|
313
|
+
{
|
|
314
|
+
name: 'tables',
|
|
315
|
+
description: 'Comma-separated list of table names to analyze',
|
|
316
|
+
required: true
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'insight_type',
|
|
320
|
+
description: 'Type of insights to find (correlations, outliers, trends, predictions)',
|
|
321
|
+
required: false
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: 'optimize_workflow',
|
|
327
|
+
description: 'Suggest workflow optimizations based on your Airtable usage patterns',
|
|
328
|
+
arguments: [
|
|
329
|
+
{
|
|
330
|
+
name: 'base_overview',
|
|
331
|
+
description: 'Overview of the base structure and usage',
|
|
332
|
+
required: false
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'optimization_focus',
|
|
336
|
+
description: 'Focus area (automation, fields, views, collaboration)',
|
|
337
|
+
required: false
|
|
338
|
+
}
|
|
339
|
+
]
|
|
340
|
+
}
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
// Roots configuration for filesystem access
|
|
344
|
+
const ROOTS_CONFIG = [
|
|
345
|
+
{
|
|
346
|
+
uri: 'file:///airtable-exports',
|
|
347
|
+
name: 'Airtable Exports'
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
uri: 'file:///airtable-attachments',
|
|
351
|
+
name: 'Airtable Attachments'
|
|
352
|
+
}
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
// Logging configuration (currentLogLevel is already declared above)
|
|
356
|
+
|
|
239
357
|
// HTTP server
|
|
240
358
|
const server = http.createServer(async (req, res) => {
|
|
241
359
|
// Security headers
|
|
@@ -261,13 +379,161 @@ const server = http.createServer(async (req, res) => {
|
|
|
261
379
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
262
380
|
res.end(JSON.stringify({
|
|
263
381
|
status: 'healthy',
|
|
264
|
-
version: '2.1
|
|
382
|
+
version: '2.2.1',
|
|
265
383
|
timestamp: new Date().toISOString(),
|
|
266
384
|
uptime: process.uptime()
|
|
267
385
|
}));
|
|
268
386
|
return;
|
|
269
387
|
}
|
|
270
388
|
|
|
389
|
+
// OAuth2 authorization endpoint
|
|
390
|
+
if (pathname === '/oauth/authorize' && req.method === 'GET') {
|
|
391
|
+
const params = parsedUrl.query;
|
|
392
|
+
const clientId = params.client_id;
|
|
393
|
+
const redirectUri = params.redirect_uri;
|
|
394
|
+
const state = params.state;
|
|
395
|
+
const codeChallenge = params.code_challenge;
|
|
396
|
+
const codeChallengeMethod = params.code_challenge_method;
|
|
397
|
+
|
|
398
|
+
// Validate inputs to prevent XSS
|
|
399
|
+
if (!clientId || !redirectUri) {
|
|
400
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
401
|
+
res.end(JSON.stringify({ error: 'invalid_request', error_description: 'Missing required parameters' }));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Validate redirect URI
|
|
406
|
+
if (!validateUrl(redirectUri)) {
|
|
407
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
408
|
+
res.end(JSON.stringify({ error: 'invalid_request', error_description: 'Invalid redirect URI' }));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Sanitize all user inputs for HTML output
|
|
413
|
+
const safeClientId = escapeHtml(clientId);
|
|
414
|
+
const safeRedirectUri = escapeHtml(redirectUri);
|
|
415
|
+
const safeState = escapeHtml(state || '');
|
|
416
|
+
|
|
417
|
+
// Generate authorization code
|
|
418
|
+
const authCode = crypto.randomBytes(32).toString('hex');
|
|
419
|
+
|
|
420
|
+
// In a real implementation, store the auth code with expiration
|
|
421
|
+
// and associate it with the client and PKCE challenge
|
|
422
|
+
|
|
423
|
+
res.writeHead(200, {
|
|
424
|
+
'Content-Type': 'text/html',
|
|
425
|
+
'Content-Security-Policy': "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'",
|
|
426
|
+
'X-Content-Type-Options': 'nosniff',
|
|
427
|
+
'X-Frame-Options': 'DENY'
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
res.end(`<!DOCTYPE html>
|
|
431
|
+
<html>
|
|
432
|
+
<head>
|
|
433
|
+
<meta charset="UTF-8">
|
|
434
|
+
<title>OAuth2 Authorization</title>
|
|
435
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
436
|
+
</head>
|
|
437
|
+
<body>
|
|
438
|
+
<h2>Airtable MCP Server - OAuth2 Authorization</h2>
|
|
439
|
+
<p>Client ID: ${safeClientId}</p>
|
|
440
|
+
<p>Redirect URI: ${safeRedirectUri}</p>
|
|
441
|
+
<div style="margin: 20px 0;">
|
|
442
|
+
<button onclick="authorize()" style="background: #18BFFF; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;">
|
|
443
|
+
Authorize Application
|
|
444
|
+
</button>
|
|
445
|
+
<button onclick="deny()" style="background: #ff4444; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; margin-left: 10px;">
|
|
446
|
+
Deny Access
|
|
447
|
+
</button>
|
|
448
|
+
</div>
|
|
449
|
+
<script>
|
|
450
|
+
function authorize() {
|
|
451
|
+
const baseUrl = ${JSON.stringify(redirectUri)};
|
|
452
|
+
const code = ${JSON.stringify(authCode)};
|
|
453
|
+
const state = ${JSON.stringify(state || '')};
|
|
454
|
+
const url = baseUrl + '?code=' + encodeURIComponent(code) + '&state=' + encodeURIComponent(state);
|
|
455
|
+
window.location.href = url;
|
|
456
|
+
}
|
|
457
|
+
function deny() {
|
|
458
|
+
const baseUrl = ${JSON.stringify(redirectUri)};
|
|
459
|
+
const state = ${JSON.stringify(state || '')};
|
|
460
|
+
const url = baseUrl + '?error=access_denied&state=' + encodeURIComponent(state);
|
|
461
|
+
window.location.href = url;
|
|
462
|
+
}
|
|
463
|
+
</script>
|
|
464
|
+
</body>
|
|
465
|
+
</html>`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// OAuth2 token endpoint
|
|
470
|
+
if (pathname === '/oauth/token' && req.method === 'POST') {
|
|
471
|
+
let body = '';
|
|
472
|
+
req.on('data', chunk => {
|
|
473
|
+
body += chunk.toString();
|
|
474
|
+
// Prevent DoS by limiting body size
|
|
475
|
+
if (body.length > 10000) {
|
|
476
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
477
|
+
res.end(JSON.stringify({ error: 'payload_too_large', error_description: 'Request body too large' }));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
req.on('end', () => {
|
|
483
|
+
try {
|
|
484
|
+
const params = querystring.parse(body);
|
|
485
|
+
const grantType = sanitizeInput(params.grant_type);
|
|
486
|
+
const code = sanitizeInput(params.code);
|
|
487
|
+
const codeVerifier = sanitizeInput(params.code_verifier);
|
|
488
|
+
const clientId = sanitizeInput(params.client_id);
|
|
489
|
+
|
|
490
|
+
// Validate required parameters
|
|
491
|
+
if (!grantType || !code || !clientId) {
|
|
492
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
493
|
+
res.end(JSON.stringify({
|
|
494
|
+
error: 'invalid_request',
|
|
495
|
+
error_description: 'Missing required parameters'
|
|
496
|
+
}));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// In a real implementation, verify the authorization code and PKCE
|
|
501
|
+
if (grantType === 'authorization_code' && code) {
|
|
502
|
+
// Generate access token
|
|
503
|
+
const accessToken = crypto.randomBytes(32).toString('hex');
|
|
504
|
+
const refreshToken = crypto.randomBytes(32).toString('hex');
|
|
505
|
+
|
|
506
|
+
res.writeHead(200, {
|
|
507
|
+
'Content-Type': 'application/json',
|
|
508
|
+
'Cache-Control': 'no-store',
|
|
509
|
+
'Pragma': 'no-cache'
|
|
510
|
+
});
|
|
511
|
+
res.end(JSON.stringify({
|
|
512
|
+
access_token: accessToken,
|
|
513
|
+
token_type: 'Bearer',
|
|
514
|
+
expires_in: 3600,
|
|
515
|
+
refresh_token: refreshToken,
|
|
516
|
+
scope: 'data.records:read data.records:write schema.bases:read'
|
|
517
|
+
}));
|
|
518
|
+
} else {
|
|
519
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
520
|
+
res.end(JSON.stringify({
|
|
521
|
+
error: 'invalid_grant',
|
|
522
|
+
error_description: 'Invalid grant type or authorization code'
|
|
523
|
+
}));
|
|
524
|
+
}
|
|
525
|
+
} catch (error) {
|
|
526
|
+
log(LOG_LEVELS.WARN, 'OAuth token request parsing failed', { error: error.message });
|
|
527
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
528
|
+
res.end(JSON.stringify({
|
|
529
|
+
error: 'invalid_request',
|
|
530
|
+
error_description: 'Malformed request body'
|
|
531
|
+
}));
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
271
537
|
// MCP endpoint
|
|
272
538
|
if (pathname === '/mcp' && req.method === 'POST') {
|
|
273
539
|
// Rate limiting
|
|
@@ -321,9 +587,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
321
587
|
logging: {}
|
|
322
588
|
},
|
|
323
589
|
serverInfo: {
|
|
324
|
-
name: 'Airtable MCP Server',
|
|
325
|
-
version: '2.1
|
|
326
|
-
description: '
|
|
590
|
+
name: 'Airtable MCP Server Enhanced',
|
|
591
|
+
version: '2.2.1',
|
|
592
|
+
description: 'Complete MCP 2024-11-05 server with Prompts, Sampling, Roots, Logging, and OAuth2'
|
|
327
593
|
}
|
|
328
594
|
}
|
|
329
595
|
};
|
|
@@ -344,6 +610,47 @@ const server = http.createServer(async (req, res) => {
|
|
|
344
610
|
response = await handleToolCall(request);
|
|
345
611
|
break;
|
|
346
612
|
|
|
613
|
+
case 'prompts/list':
|
|
614
|
+
response = {
|
|
615
|
+
jsonrpc: '2.0',
|
|
616
|
+
id: request.id,
|
|
617
|
+
result: {
|
|
618
|
+
prompts: PROMPTS_SCHEMA
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
break;
|
|
622
|
+
|
|
623
|
+
case 'prompts/get':
|
|
624
|
+
response = await handlePromptGet(request);
|
|
625
|
+
break;
|
|
626
|
+
|
|
627
|
+
case 'roots/list':
|
|
628
|
+
response = {
|
|
629
|
+
jsonrpc: '2.0',
|
|
630
|
+
id: request.id,
|
|
631
|
+
result: {
|
|
632
|
+
roots: ROOTS_CONFIG
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
break;
|
|
636
|
+
|
|
637
|
+
case 'logging/setLevel':
|
|
638
|
+
const level = request.params?.level;
|
|
639
|
+
if (level && LOG_LEVELS[level.toUpperCase()] !== undefined) {
|
|
640
|
+
currentLogLevel = LOG_LEVELS[level.toUpperCase()];
|
|
641
|
+
log(LOG_LEVELS.INFO, 'Log level updated', { newLevel: level });
|
|
642
|
+
}
|
|
643
|
+
response = {
|
|
644
|
+
jsonrpc: '2.0',
|
|
645
|
+
id: request.id,
|
|
646
|
+
result: {}
|
|
647
|
+
};
|
|
648
|
+
break;
|
|
649
|
+
|
|
650
|
+
case 'sampling/createMessage':
|
|
651
|
+
response = await handleSampling(request);
|
|
652
|
+
break;
|
|
653
|
+
|
|
347
654
|
default:
|
|
348
655
|
log(LOG_LEVELS.WARN, 'Unknown method', { method: request.method });
|
|
349
656
|
throw new Error(`Method "${request.method}" not found`);
|
|
@@ -476,6 +783,162 @@ async function handleToolCall(request) {
|
|
|
476
783
|
}
|
|
477
784
|
}
|
|
478
785
|
|
|
786
|
+
// Prompt handlers
|
|
787
|
+
async function handlePromptGet(request) {
|
|
788
|
+
const promptName = request.params.name;
|
|
789
|
+
const promptArgs = request.params.arguments || {};
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
const prompt = PROMPTS_SCHEMA.find(p => p.name === promptName);
|
|
793
|
+
if (!prompt) {
|
|
794
|
+
throw new Error(`Prompt "${promptName}" not found`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
let messages = [];
|
|
798
|
+
|
|
799
|
+
switch (promptName) {
|
|
800
|
+
case 'analyze_data':
|
|
801
|
+
const { table, analysis_type = 'summary', field_focus } = promptArgs;
|
|
802
|
+
messages = [
|
|
803
|
+
{
|
|
804
|
+
role: 'user',
|
|
805
|
+
content: {
|
|
806
|
+
type: 'text',
|
|
807
|
+
text: `Please analyze the data in table "${table}".
|
|
808
|
+
Analysis type: ${analysis_type}
|
|
809
|
+
${field_focus ? `Focus on fields: ${field_focus}` : ''}
|
|
810
|
+
|
|
811
|
+
First, list the tables and their schemas, then retrieve sample records from "${table}"
|
|
812
|
+
and provide insights based on the ${analysis_type} analysis type.`
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
];
|
|
816
|
+
break;
|
|
817
|
+
|
|
818
|
+
case 'create_report':
|
|
819
|
+
const { table: reportTable, report_type = 'summary', time_period } = promptArgs;
|
|
820
|
+
messages = [
|
|
821
|
+
{
|
|
822
|
+
role: 'user',
|
|
823
|
+
content: {
|
|
824
|
+
type: 'text',
|
|
825
|
+
text: `Create a ${report_type} report for table "${reportTable}".
|
|
826
|
+
${time_period ? `Time period: ${time_period}` : ''}
|
|
827
|
+
|
|
828
|
+
Please gather the table schema and recent records, then generate a comprehensive
|
|
829
|
+
${report_type} report with key metrics, trends, and actionable insights.`
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
];
|
|
833
|
+
break;
|
|
834
|
+
|
|
835
|
+
case 'data_insights':
|
|
836
|
+
const { tables, insight_type = 'correlations' } = promptArgs;
|
|
837
|
+
messages = [
|
|
838
|
+
{
|
|
839
|
+
role: 'user',
|
|
840
|
+
content: {
|
|
841
|
+
type: 'text',
|
|
842
|
+
text: `Discover ${insight_type} insights across these tables: ${tables}
|
|
843
|
+
|
|
844
|
+
Please examine the data structures and content to identify:
|
|
845
|
+
- ${insight_type} patterns
|
|
846
|
+
- Unexpected relationships
|
|
847
|
+
- Optimization opportunities
|
|
848
|
+
- Data quality insights`
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
];
|
|
852
|
+
break;
|
|
853
|
+
|
|
854
|
+
case 'optimize_workflow':
|
|
855
|
+
const { base_overview, optimization_focus = 'automation' } = promptArgs;
|
|
856
|
+
messages = [
|
|
857
|
+
{
|
|
858
|
+
role: 'user',
|
|
859
|
+
content: {
|
|
860
|
+
type: 'text',
|
|
861
|
+
text: `Analyze the current Airtable setup and suggest ${optimization_focus} optimizations.
|
|
862
|
+
${base_overview ? `Base overview: ${base_overview}` : ''}
|
|
863
|
+
|
|
864
|
+
Please review the table structures, field types, and relationships to recommend:
|
|
865
|
+
- ${optimization_focus} improvements
|
|
866
|
+
- Best practice implementations
|
|
867
|
+
- Performance enhancements
|
|
868
|
+
- Workflow streamlining opportunities`
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
];
|
|
872
|
+
break;
|
|
873
|
+
|
|
874
|
+
default:
|
|
875
|
+
throw new Error(`Unsupported prompt: ${promptName}`);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return {
|
|
879
|
+
jsonrpc: '2.0',
|
|
880
|
+
id: request.id,
|
|
881
|
+
result: {
|
|
882
|
+
description: prompt.description,
|
|
883
|
+
messages: messages
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
} catch (error) {
|
|
888
|
+
log(LOG_LEVELS.ERROR, `Prompt ${promptName} failed`, { error: error.message });
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
jsonrpc: '2.0',
|
|
892
|
+
id: request.id,
|
|
893
|
+
error: {
|
|
894
|
+
code: -32000,
|
|
895
|
+
message: `Error getting prompt ${promptName}: ${error.message}`
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Sampling handler
|
|
902
|
+
async function handleSampling(request) {
|
|
903
|
+
const { messages, modelPreferences } = request.params;
|
|
904
|
+
|
|
905
|
+
try {
|
|
906
|
+
// Note: In a real implementation, this would integrate with an LLM API
|
|
907
|
+
// For now, we'll return a structured response indicating sampling capability
|
|
908
|
+
|
|
909
|
+
log(LOG_LEVELS.INFO, 'Sampling request received', {
|
|
910
|
+
messageCount: messages?.length,
|
|
911
|
+
model: modelPreferences?.model
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
return {
|
|
915
|
+
jsonrpc: '2.0',
|
|
916
|
+
id: request.id,
|
|
917
|
+
result: {
|
|
918
|
+
model: modelPreferences?.model || 'claude-3-sonnet',
|
|
919
|
+
role: 'assistant',
|
|
920
|
+
content: {
|
|
921
|
+
type: 'text',
|
|
922
|
+
text: 'Sampling capability is available. This MCP server can request AI assistance for complex data analysis and insights generation. In a full implementation, this would connect to your preferred LLM for intelligent Airtable operations.'
|
|
923
|
+
},
|
|
924
|
+
stopReason: 'end_turn'
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
} catch (error) {
|
|
929
|
+
log(LOG_LEVELS.ERROR, 'Sampling failed', { error: error.message });
|
|
930
|
+
|
|
931
|
+
return {
|
|
932
|
+
jsonrpc: '2.0',
|
|
933
|
+
id: request.id,
|
|
934
|
+
error: {
|
|
935
|
+
code: -32000,
|
|
936
|
+
message: `Sampling error: ${error.message}`
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
479
942
|
// Server startup
|
|
480
943
|
const PORT = CONFIG.PORT;
|
|
481
944
|
const HOST = CONFIG.HOST;
|
package/package.json
CHANGED