Skip to main content
Glama

Airtable MCP Server

by clikvn
server.js7.27 kB
#!/usr/bin/env node /** * Express server to serve the Airtable bases web page * Keeps API key secure on the server side * Compatible with both local development and Vercel deployment */ import express from 'express'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { AirtableService } from './dist/airtableService.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); const PORT = process.env.PORT || 3000; // Get API key from environment or config const API_KEY = process.env.AIRTABLE_API_KEY || ''; // Middleware for parsing JSON bodies (must be before routes) app.use(express.json()); app.use(express.urlencoded({ extended: true })); // API routes (must be before static files) // API endpoint to fetch bases app.get('/api/bases', async (req, res) => { try { const service = new AirtableService(API_KEY); const response = await service.listBases(); res.json({ success: true, bases: response.bases, total: response.bases.length, }); } catch (error) { console.error('Error fetching bases:', error); res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Unknown error', }); } }); // API endpoint to fetch tables for a specific base app.get('/api/bases/:baseId/tables', async (req, res) => { try { const { baseId } = req.params; const service = new AirtableService(API_KEY); const response = await service.getBaseSchema(baseId); res.json({ success: true, tables: response.tables, total: response.tables.length, }); } catch (error) { console.error('Error fetching tables:', error); res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Unknown error', }); } }); // API endpoint to fetch records from a specific table app.get('/api/bases/:baseId/tables/:tableId/records', async (req, res) => { try { const { baseId, tableId } = req.params; const { maxRecords = 100 } = req.query; const service = new AirtableService(API_KEY); const records = await service.listRecords(baseId, tableId, { maxRecords: parseInt(maxRecords, 10), }); res.json({ success: true, records: records, total: records.length, }); } catch (error) { console.error('Error fetching records:', error); res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Unknown error', }); } }); // API endpoint to fetch comments for a record // Comments are stored in the custom Comments table in VuPTT_Comments base app.get('/api/bases/:baseId/tables/:tableId/records/:recordId/comments', async (req, res) => { try { if (!API_KEY) { return res.status(500).json({ success: false, error: 'AIRTABLE_API_KEY environment variable is not set', }); } const { baseId, tableId, recordId } = req.params; // Comments are stored in the custom Comments table const COMMENTS_BASE_ID = 'apptnDpTBFKNhQmJw'; const COMMENTS_TABLE_ID = 'tblzDXKlDXH6nBnh9'; const service = new AirtableService(API_KEY); // Filter comments by base_id, table_id, and record_id const filterFormula = `AND({base_id} = "${baseId}", {table_id} = "${tableId}", {record_id} = "${recordId}")`; const records = await service.listRecords(COMMENTS_BASE_ID, COMMENTS_TABLE_ID, { filterByFormula: filterFormula, }); // Transform records to match the expected comment format // Note: Airtable records include createdTime as metadata, but our type doesn't capture it // We'll use the record ID timestamp or current time as fallback const comments = records.map(record => { // Try to extract createdTime from record metadata if available // Airtable API returns createdTime, but our schema might not capture it const createdTime = record.createdTime || new Date().toISOString(); return { id: record.id, text: record.fields.comment || '', author: { name: record.fields.user || 'Anonymous', email: record.fields.user || '', }, createdTime: createdTime, parentCommentId: null, // Not supported in custom table yet }; }); // Sort by createdTime descending (newest first) comments.sort((a, b) => { const timeA = new Date(a.createdTime).getTime(); const timeB = new Date(b.createdTime).getTime(); return timeB - timeA; }); res.json({ success: true, comments: comments, offset: null, }); } catch (error) { console.error('Error fetching comments:', error); res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Unknown error', }); } }); // API endpoint to create a comment on a record // Comments are stored in the custom Comments table in VuPTT_Comments base app.post('/api/bases/:baseId/tables/:tableId/records/:recordId/comments', async (req, res) => { try { if (!API_KEY) { return res.status(500).json({ success: false, error: 'AIRTABLE_API_KEY environment variable is not set', }); } const { baseId, tableId, recordId } = req.params; const { text, user } = req.body; // user is optional if (!text || text.trim().length === 0) { return res.status(400).json({ success: false, error: 'Comment text is required', }); } // Comments are stored in the custom Comments table const COMMENTS_BASE_ID = 'apptnDpTBFKNhQmJw'; const COMMENTS_TABLE_ID = 'tblzDXKlDXH6nBnh9'; const service = new AirtableService(API_KEY); // Create record in Comments table const record = await service.createRecord(COMMENTS_BASE_ID, COMMENTS_TABLE_ID, { base_id: baseId, table_id: tableId, record_id: recordId, user: user || '', // Can be empty comment: text.trim(), }); // Transform to match expected comment format // Airtable API returns createdTime as metadata const createdTime = record.createdTime || new Date().toISOString(); const comment = { id: record.id, text: record.fields.comment || text.trim(), author: { name: record.fields.user || 'Anonymous', email: record.fields.user || '', }, createdTime: createdTime, parentCommentId: null, }; res.json({ success: true, comment: comment, }); } catch (error) { console.error('Error creating comment:', error); res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Unknown error', }); } }); // Serve static files from public directory (after API routes) app.use(express.static(join(__dirname, 'public'))); // Export for Vercel (serverless function) export default app; // Start server locally (not on Vercel) if (process.env.VERCEL !== '1') { app.listen(PORT, () => { console.log(`🚀 Server running at http://localhost:${PORT}`); console.log(`📊 View your Airtable bases at http://localhost:${PORT}`); }); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/clikvn/airtable_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server