Airtable MCP Server

by felores
Verified
  • src
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js"; import axios, { AxiosInstance } from "axios"; import { FieldOption, fieldRequiresOptions, getDefaultOptions, FieldType } from "./types.js"; const API_KEY = process.env.AIRTABLE_API_KEY; if (!API_KEY) { throw new Error("AIRTABLE_API_KEY environment variable is required"); } class AirtableServer { private server: Server; private axiosInstance: AxiosInstance; constructor() { this.server = new Server( { name: "airtable-server", version: "0.2.0", }, { capabilities: { tools: {}, }, } ); this.axiosInstance = axios.create({ baseURL: "https://api.airtable.com/v0", headers: { Authorization: `Bearer ${API_KEY}`, }, }); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error("[MCP Error]", error); process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } private validateField(field: FieldOption): FieldOption { const { type } = field; // Remove options for fields that don't need them if (!fieldRequiresOptions(type as FieldType)) { const { options, ...rest } = field; return rest; } // Add default options for fields that require them if (!field.options) { return { ...field, options: getDefaultOptions(type as FieldType), }; } return field; } private setupToolHandlers() { // Register available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "list_bases", description: "List all accessible Airtable bases", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "list_tables", description: "List all tables in a base", inputSchema: { type: "object", properties: { base_id: { type: "string", description: "ID of the base", }, }, required: ["base_id"], }, }, { name: "create_table", description: "Create a new table in a base", inputSchema: { type: "object", properties: { base_id: { type: "string", description: "ID of the base", }, table_name: { type: "string", description: "Name of the new table", }, description: { type: "string", description: "Description of the table", }, fields: { type: "array", description: "Initial fields for the table", items: { type: "object", properties: { name: { type: "string", description: "Name of the field", }, type: { type: "string", description: "Type of the field (e.g., singleLineText, multilineText, number, etc.)", }, description: { type: "string", description: "Description of the field", }, options: { type: "object", description: "Field-specific options", }, }, required: ["name", "type"], }, }, }, required: ["base_id", "table_name"], }, }, { name: "update_table", description: "Update a table's schema", inputSchema: { type: "object", properties: { base_id: { type: "string", description: "ID of the base", }, table_id: { type: "string", description: "ID of the table to update", }, name: { type: "string", description: "New name for the table", }, description: { type: "string", description: "New description for the table", }, }, required: ["base_id", "table_id"], }, }, { name: "create_field", description: "Create a new field in a table", inputSchema: { type: "object", properties: { base_id: { type: "string", description: "ID of the base", }, table_id: { type: "string", description: "ID of the table", }, field: { type: "object", properties: { name: { type: "string", description: "Name of the field", }, type: { type: "string", description: "Type of the field", }, description: { type: "string", description: "Description of the field", }, options: { type: "object", description: "Field-specific options", }, }, required: ["name", "type"], }, }, required: ["base_id", "table_id", "field"], }, }, { name: "update_field", description: "Update a field in a table", inputSchema: { type: "object", properties: { base_id: { type: "string", description: "ID of the base", }, table_id: { type: "string", description: "ID of the table", }, field_id: { type: "string", description: "ID of the field to update", }, updates: { type: "object", properties: { name: { type: "string", description: "New name for the field", }, description: { type: "string", description: "New description for the field", }, options: { type: "object", description: "New field-specific options", }, }, }, }, required: ["base_id", "table_id", "field_id", "updates"], }, }, { name: "list_records", description: "List records in a table", inputSchema: { type: "object", properties: { base_id: { type: "string", description: "ID of the base", }, table_name: { type: "string", description: "Name of the table", }, max_records: { type: "number", description: "Maximum number of records to return", }, }, required: ["base_id", "table_name"], }, }, { name: "create_record", description: "Create a new record in a table", inputSchema: { type: "object", properties: { base_id: { type: "string", description: "ID of the base", }, table_name: { type: "string", description: "Name of the table", }, fields: { type: "object", description: "Record fields as key-value pairs", }, }, required: ["base_id", "table_name", "fields"], }, }, { name: "update_record", description: "Update an existing record in a table", inputSchema: { type: "object", properties: { base_id: { type: "string", description: "ID of the base", }, table_name: { type: "string", description: "Name of the table", }, record_id: { type: "string", description: "ID of the record to update", }, fields: { type: "object", description: "Record fields to update as key-value pairs", }, }, required: ["base_id", "table_name", "record_id", "fields"], }, }, { name: "delete_record", description: "Delete a record from a table", inputSchema: { type: "object", properties: { base_id: { type: "string", description: "ID of the base", }, table_name: { type: "string", description: "Name of the table", }, record_id: { type: "string", description: "ID of the record to delete", }, }, required: ["base_id", "table_name", "record_id"], }, }, { name: "search_records", description: "Search for records in a table", inputSchema: { type: "object", properties: { base_id: { type: "string", description: "ID of the base", }, table_name: { type: "string", description: "Name of the table", }, field_name: { type: "string", description: "Name of the field to search in", }, value: { type: "string", description: "Value to search for", }, }, required: ["base_id", "table_name", "field_name", "value"], }, }, { name: "get_record", description: "Get a single record by its ID", inputSchema: { type: "object", properties: { base_id: { type: "string", description: "ID of the base", }, table_name: { type: "string", description: "Name of the table", }, record_id: { type: "string", description: "ID of the record to retrieve", }, }, required: ["base_id", "table_name", "record_id"], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (request.params.name) { case "list_bases": { const response = await this.axiosInstance.get("/meta/bases"); return { content: [{ type: "text", text: JSON.stringify(response.data.bases, null, 2), }], }; } case "list_tables": { const { base_id } = request.params.arguments as { base_id: string }; const response = await this.axiosInstance.get(`/meta/bases/${base_id}/tables`); return { content: [{ type: "text", text: JSON.stringify(response.data.tables, null, 2), }], }; } case "create_table": { const { base_id, table_name, description, fields } = request.params.arguments as { base_id: string; table_name: string; description?: string; fields?: FieldOption[]; }; // Validate and prepare fields const validatedFields = fields?.map(field => this.validateField(field)); const response = await this.axiosInstance.post(`/meta/bases/${base_id}/tables`, { name: table_name, description, fields: validatedFields, }); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2), }], }; } case "update_table": { const { base_id, table_id, name, description } = request.params.arguments as { base_id: string; table_id: string; name?: string; description?: string; }; const response = await this.axiosInstance.patch(`/meta/bases/${base_id}/tables/${table_id}`, { name, description, }); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2), }], }; } case "create_field": { const { base_id, table_id, field } = request.params.arguments as { base_id: string; table_id: string; field: FieldOption; }; // Validate field before creation const validatedField = this.validateField(field); const response = await this.axiosInstance.post( `/meta/bases/${base_id}/tables/${table_id}/fields`, validatedField ); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2), }], }; } case "update_field": { const { base_id, table_id, field_id, updates } = request.params.arguments as { base_id: string; table_id: string; field_id: string; updates: Partial<FieldOption>; }; const response = await this.axiosInstance.patch( `/meta/bases/${base_id}/tables/${table_id}/fields/${field_id}`, updates ); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2), }], }; } case "list_records": { const { base_id, table_name, max_records } = request.params.arguments as { base_id: string; table_name: string; max_records?: number; }; const response = await this.axiosInstance.get(`/${base_id}/${table_name}`, { params: max_records ? { maxRecords: max_records } : undefined, }); return { content: [{ type: "text", text: JSON.stringify(response.data.records, null, 2), }], }; } case "create_record": { const { base_id, table_name, fields } = request.params.arguments as { base_id: string; table_name: string; fields: Record<string, any>; }; const response = await this.axiosInstance.post(`/${base_id}/${table_name}`, { fields, }); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2), }], }; } case "update_record": { const { base_id, table_name, record_id, fields } = request.params.arguments as { base_id: string; table_name: string; record_id: string; fields: Record<string, any>; }; const response = await this.axiosInstance.patch( `/${base_id}/${table_name}/${record_id}`, { fields } ); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2), }], }; } case "delete_record": { const { base_id, table_name, record_id } = request.params.arguments as { base_id: string; table_name: string; record_id: string; }; const response = await this.axiosInstance.delete( `/${base_id}/${table_name}/${record_id}` ); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2), }], }; } case "search_records": { const { base_id, table_name, field_name, value } = request.params.arguments as { base_id: string; table_name: string; field_name: string; value: string; }; const response = await this.axiosInstance.get(`/${base_id}/${table_name}`, { params: { filterByFormula: `{${field_name}} = "${value}"`, }, }); return { content: [{ type: "text", text: JSON.stringify(response.data.records, null, 2), }], }; } case "get_record": { const { base_id, table_name, record_id } = request.params.arguments as { base_id: string; table_name: string; record_id: string; }; const response = await this.axiosInstance.get( `/${base_id}/${table_name}/${record_id}` ); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2), }], }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } } catch (error) { if (axios.isAxiosError(error)) { throw new McpError( ErrorCode.InternalError, `Airtable API error: ${error.response?.data?.error?.message ?? error.message}` ); } throw error; } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Airtable MCP server running on stdio"); } } const server = new AirtableServer(); server.run().catch((error) => { console.error("Server error:", error); process.exit(1); });