This is page 2 of 5. Use http://codebase.md/rashidazarang/airtable-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .eslintrc.js
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── custom.md
│ │ └── feature_request.md
│ └── pull_request_template.md
├── .gitignore
├── .nvmrc
├── .prettierrc
├── bin
│ ├── airtable-crud-cli.js
│ └── airtable-mcp.js
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── docker
│ ├── Dockerfile
│ └── Dockerfile.node
├── docs
│ ├── guides
│ │ ├── CLAUDE_INTEGRATION.md
│ │ ├── ENHANCED_FEATURES.md
│ │ ├── INSTALLATION.md
│ │ └── QUICK_START.md
│ └── releases
│ ├── RELEASE_NOTES_v1.2.2.md
│ ├── RELEASE_NOTES_v1.2.4.md
│ ├── RELEASE_NOTES_v1.4.0.md
│ ├── RELEASE_NOTES_v1.5.0.md
│ └── RELEASE_NOTES_v1.6.0.md
├── examples
│ ├── airtable-crud-example.js
│ ├── building-mcp.md
│ ├── claude_config.json
│ ├── claude_simple_config.json
│ ├── env-demo.js
│ ├── example_usage.md
│ ├── example-tasks-update.json
│ ├── example-tasks.json
│ ├── python_debug_patch.txt
│ ├── sample-transform.js
│ ├── typescript
│ │ ├── advanced-ai-prompts.ts
│ │ ├── basic-usage.ts
│ │ └── claude-desktop-config.json
│ └── windsurf_mcp_config.json
├── index.js
├── ISSUE_RESPONSES.md
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── PROJECT_STRUCTURE.md
├── README.md
├── RELEASE_SUMMARY_v3.2.x.md
├── RELEASE_v3.2.1.md
├── RELEASE_v3.2.3.md
├── RELEASE_v3.2.4.md
├── requirements.txt
├── SECURITY_NOTICE.md
├── smithery.yaml
├── src
│ ├── index.js
│ ├── javascript
│ │ ├── airtable_simple_production.js
│ │ └── airtable_simple.js
│ ├── python
│ │ ├── airtable_mcp
│ │ │ ├── __init__.py
│ │ │ └── src
│ │ │ └── server.py
│ │ ├── inspector_server.py
│ │ ├── inspector.py
│ │ ├── setup.py
│ │ ├── simple_airtable_server.py
│ │ └── test_client.py
│ └── typescript
│ ├── ai-prompts.d.ts
│ ├── airtable-mcp-server.d.ts
│ ├── airtable-mcp-server.ts
│ ├── app
│ │ ├── airtable-client.ts
│ │ ├── config.ts
│ │ ├── context.ts
│ │ ├── exceptions.ts
│ │ ├── governance.ts
│ │ ├── logger.ts
│ │ ├── rateLimiter.ts
│ │ ├── tools
│ │ │ ├── create.ts
│ │ │ ├── describe.ts
│ │ │ ├── handleError.ts
│ │ │ ├── index.ts
│ │ │ ├── listBases.ts
│ │ │ ├── listExceptions.ts
│ │ │ ├── listGovernance.ts
│ │ │ ├── query.ts
│ │ │ ├── update.ts
│ │ │ ├── upsert.ts
│ │ │ └── webhooks.ts
│ │ └── types.ts
│ ├── errors.ts
│ ├── index.d.ts
│ ├── index.ts
│ ├── prompt-templates.ts
│ ├── tools-schemas.ts
│ └── tools.d.ts
├── TESTING_REPORT.md
├── tests
│ ├── test_all_features.sh
│ ├── test_mcp_comprehensive.js
│ ├── test_v1.5.0_final.sh
│ └── test_v1.6.0_comprehensive.sh
├── tsconfig.json
└── types
└── typescript
├── airtable-mcp-server.d.ts
├── app
│ ├── airtable-client.d.ts
│ ├── config.d.ts
│ ├── context.d.ts
│ ├── exceptions.d.ts
│ ├── governance.d.ts
│ ├── logger.d.ts
│ ├── rateLimiter.d.ts
│ ├── tools
│ │ ├── create.d.ts
│ │ ├── describe.d.ts
│ │ ├── handleError.d.ts
│ │ ├── index.d.ts
│ │ ├── listBases.d.ts
│ │ ├── listExceptions.d.ts
│ │ ├── listGovernance.d.ts
│ │ ├── query.d.ts
│ │ ├── update.d.ts
│ │ ├── upsert.d.ts
│ │ └── webhooks.d.ts
│ └── types.d.ts
├── errors.d.ts
├── index.d.ts
├── prompt-templates.d.ts
├── test-suite.d.ts
└── tools-schemas.d.ts
```
# Files
--------------------------------------------------------------------------------
/src/python/inspector.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | MCP Tool Inspector
4 | -----------------
5 | A simple script to list tools in a format Smithery can understand
6 | """
7 | import json
8 |
9 | # Define the tools manually
10 | tools = [
11 | {
12 | "name": "list_bases",
13 | "description": "List all accessible Airtable bases",
14 | "parameters": {
15 | "type": "object",
16 | "properties": {},
17 | "required": []
18 | },
19 | "returns": {
20 | "type": "string"
21 | }
22 | },
23 | {
24 | "name": "list_tables",
25 | "description": "List all tables in the specified base or the default base",
26 | "parameters": {
27 | "type": "object",
28 | "properties": {
29 | "base_id": {
30 | "type": "string",
31 | "description": "Optional base ID to use instead of the default"
32 | }
33 | },
34 | "required": []
35 | },
36 | "returns": {
37 | "type": "string"
38 | }
39 | },
40 | {
41 | "name": "list_records",
42 | "description": "List records from a table with optional filtering",
43 | "parameters": {
44 | "type": "object",
45 | "properties": {
46 | "table_name": {
47 | "type": "string",
48 | "description": "Name of the table to list records from"
49 | },
50 | "max_records": {
51 | "type": "integer",
52 | "description": "Maximum number of records to return (default: 100)"
53 | },
54 | "filter_formula": {
55 | "type": "string",
56 | "description": "Optional Airtable formula to filter records"
57 | }
58 | },
59 | "required": ["table_name"]
60 | },
61 | "returns": {
62 | "type": "string"
63 | }
64 | },
65 | {
66 | "name": "get_record",
67 | "description": "Get a specific record from a table",
68 | "parameters": {
69 | "type": "object",
70 | "properties": {
71 | "table_name": {
72 | "type": "string",
73 | "description": "Name of the table"
74 | },
75 | "record_id": {
76 | "type": "string",
77 | "description": "ID of the record to retrieve"
78 | }
79 | },
80 | "required": ["table_name", "record_id"]
81 | },
82 | "returns": {
83 | "type": "string"
84 | }
85 | },
86 | {
87 | "name": "create_records",
88 | "description": "Create records in a table from JSON string",
89 | "parameters": {
90 | "type": "object",
91 | "properties": {
92 | "table_name": {
93 | "type": "string",
94 | "description": "Name of the table"
95 | },
96 | "records_json": {
97 | "type": "string",
98 | "description": "JSON string containing the records to create"
99 | }
100 | },
101 | "required": ["table_name", "records_json"]
102 | },
103 | "returns": {
104 | "type": "string"
105 | }
106 | },
107 | {
108 | "name": "update_records",
109 | "description": "Update records in a table from JSON string",
110 | "parameters": {
111 | "type": "object",
112 | "properties": {
113 | "table_name": {
114 | "type": "string",
115 | "description": "Name of the table"
116 | },
117 | "records_json": {
118 | "type": "string",
119 | "description": "JSON string containing the records to update with IDs"
120 | }
121 | },
122 | "required": ["table_name", "records_json"]
123 | },
124 | "returns": {
125 | "type": "string"
126 | }
127 | },
128 | {
129 | "name": "set_base_id",
130 | "description": "Set the current Airtable base ID",
131 | "parameters": {
132 | "type": "object",
133 | "properties": {
134 | "base_id": {
135 | "type": "string",
136 | "description": "Base ID to set as the current base"
137 | }
138 | },
139 | "required": ["base_id"]
140 | },
141 | "returns": {
142 | "type": "string"
143 | }
144 | }
145 | ]
146 |
147 | # Print the tools as JSON
148 | print(json.dumps({"tools": tools}, indent=2))
```
--------------------------------------------------------------------------------
/src/typescript/prompt-templates.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Runtime AI prompt templates for Airtable MCP Server
3 | */
4 |
5 | import type { PromptSchema } from './index';
6 |
7 | export const AI_PROMPT_TEMPLATES: Record<string, PromptSchema> = {
8 | analyze_data: {
9 | name: 'analyze_data',
10 | description: 'Advanced AI data analysis with statistical insights, pattern recognition, and predictive modeling',
11 | arguments: [
12 | { name: 'table', description: 'Table name or ID to analyze', required: true, type: 'string' },
13 | { name: 'analysis_type', description: 'Type of analysis', required: false, type: 'string', enum: ['trends', 'statistical', 'patterns', 'predictive', 'anomaly_detection', 'correlation_matrix'] },
14 | { name: 'field_focus', description: 'Specific fields to focus the analysis on', required: false, type: 'string' },
15 | { name: 'time_dimension', description: 'Time field for temporal analysis', required: false, type: 'string' },
16 | { name: 'confidence_level', description: 'Statistical confidence level', required: false, type: 'number', enum: ['0.90', '0.95', '0.99'] }
17 | ]
18 | },
19 |
20 | create_report: {
21 | name: 'create_report',
22 | description: 'Intelligent report generation with business insights and stakeholder-specific recommendations',
23 | arguments: [
24 | { name: 'table', description: 'Table name or ID for report data', required: true, type: 'string' },
25 | { name: 'report_type', description: 'Type of report to generate', required: true, type: 'string', enum: ['executive_summary', 'detailed_analysis', 'dashboard', 'stakeholder_report'] },
26 | { name: 'target_audience', description: 'Primary audience for the report', required: true, type: 'string', enum: ['executives', 'managers', 'analysts', 'technical_team'] },
27 | { name: 'include_recommendations', description: 'Include actionable recommendations', required: false, type: 'boolean' },
28 | { name: 'time_period', description: 'Time period for analysis', required: false, type: 'string' },
29 | { name: 'format_preference', description: 'Preferred report format', required: false, type: 'string', enum: ['narrative', 'bullet_points', 'charts', 'mixed'] }
30 | ]
31 | },
32 |
33 | predictive_analytics: {
34 | name: 'predictive_analytics',
35 | description: 'Advanced forecasting and trend prediction with multiple algorithms and uncertainty quantification',
36 | arguments: [
37 | { name: 'table', description: 'Table name or ID for prediction', required: true, type: 'string' },
38 | { name: 'target_field', description: 'Field to predict', required: true, type: 'string' },
39 | { name: 'prediction_periods', description: 'Number of periods to predict', required: false, type: 'number' },
40 | { name: 'algorithm', description: 'Prediction algorithm to use', required: false, type: 'string', enum: ['linear_regression', 'arima', 'exponential_smoothing', 'random_forest', 'neural_network'] },
41 | { name: 'include_confidence_intervals', description: 'Include confidence intervals', required: false, type: 'boolean' },
42 | { name: 'historical_periods', description: 'Historical periods for training', required: false, type: 'number' },
43 | { name: 'external_factors', description: 'External factors to consider', required: false, type: 'string' },
44 | { name: 'business_context', description: 'Business context for predictions', required: false, type: 'string' }
45 | ]
46 | },
47 |
48 | natural_language_query: {
49 | name: 'natural_language_query',
50 | description: 'Process natural language questions about data with intelligent context awareness',
51 | arguments: [
52 | { name: 'question', description: 'Natural language question about the data', required: true, type: 'string' },
53 | { name: 'tables', description: 'Specific tables to search (optional)', required: false, type: 'string' },
54 | { name: 'response_format', description: 'Preferred response format', required: false, type: 'string', enum: ['natural_language', 'structured_data', 'visualization_ready', 'action_items'] },
55 | { name: 'context_awareness', description: 'Use context from previous queries', required: false, type: 'boolean' },
56 | { name: 'confidence_threshold', description: 'Minimum confidence for responses', required: false, type: 'number' },
57 | { name: 'clarifying_questions', description: 'Ask clarifying questions if needed', required: false, type: 'boolean' }
58 | ]
59 | }
60 | };
```
--------------------------------------------------------------------------------
/examples/typescript/basic-usage.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Basic TypeScript Usage Example
3 | * Demonstrates type-safe Airtable MCP operations
4 | */
5 |
6 | import {
7 | AirtableMCPServer,
8 | MCPServerCapabilities,
9 | ListRecordsInput,
10 | CreateRecordInput,
11 | AnalyzeDataPrompt
12 | } from '@rashidazarang/airtable-mcp/types';
13 |
14 | // Type-safe server initialization
15 | async function initializeServer(): Promise<void> {
16 | const server = new AirtableMCPServer();
17 |
18 | const capabilities: MCPServerCapabilities = {
19 | tools: { listChanged: false },
20 | prompts: { listChanged: false },
21 | resources: { subscribe: false, listChanged: false },
22 | roots: { listChanged: false },
23 | sampling: {},
24 | logging: {}
25 | };
26 |
27 | const serverInfo = await server.initialize(capabilities);
28 | console.log('Server initialized:', serverInfo);
29 | }
30 |
31 | // Type-safe data operations
32 | async function performDataOperations(): Promise<void> {
33 | const server = new AirtableMCPServer();
34 |
35 | // List records with type safety
36 | const listParams: ListRecordsInput = {
37 | table: 'Tasks',
38 | maxRecords: 10,
39 | filterByFormula: "Status = 'Active'"
40 | };
41 |
42 | const records = await server.handleToolCall('list_records', listParams);
43 | console.log('Records retrieved:', records);
44 |
45 | // Create record with validated types
46 | const createParams: CreateRecordInput = {
47 | table: 'Tasks',
48 | fields: {
49 | 'Name': 'New Task',
50 | 'Status': 'Active',
51 | 'Priority': 'High',
52 | 'Due Date': new Date().toISOString()
53 | },
54 | typecast: true
55 | };
56 |
57 | const newRecord = await server.handleToolCall('create_record', createParams);
58 | console.log('Record created:', newRecord);
59 | }
60 |
61 | // Type-safe AI prompt usage
62 | async function useAIPrompts(): Promise<void> {
63 | const server = new AirtableMCPServer();
64 |
65 | // Advanced data analysis with strict typing
66 | const analysisParams: AnalyzeDataPrompt = {
67 | table: 'Sales',
68 | analysis_type: 'predictive',
69 | field_focus: 'revenue,conversion_rate',
70 | time_dimension: 'created_date',
71 | confidence_level: 0.95
72 | };
73 |
74 | const analysis = await server.handlePromptGet('analyze_data', analysisParams);
75 | console.log('AI Analysis:', analysis);
76 |
77 | // Type-safe error handling
78 | try {
79 | // This will cause a TypeScript compile error if types don't match
80 | const invalidParams = {
81 | table: 'Sales',
82 | analysis_type: 'invalid_type', // TypeScript will catch this!
83 | confidence_level: 1.5 // TypeScript will catch this too!
84 | };
85 |
86 | // await server.handlePromptGet('analyze_data', invalidParams);
87 | } catch (error) {
88 | console.error('Type-safe error handling:', error);
89 | }
90 | }
91 |
92 | // Enterprise-grade type validation
93 | interface BusinessMetrics {
94 | revenue: number;
95 | conversion_rate: number;
96 | customer_count: number;
97 | timestamp: Date;
98 | }
99 |
100 | function validateBusinessMetrics(data: unknown): data is BusinessMetrics {
101 | const metrics = data as BusinessMetrics;
102 | return (
103 | typeof metrics.revenue === 'number' &&
104 | typeof metrics.conversion_rate === 'number' &&
105 | typeof metrics.customer_count === 'number' &&
106 | metrics.timestamp instanceof Date
107 | );
108 | }
109 |
110 | // Type-safe configuration
111 | interface AppConfig {
112 | airtable: {
113 | token: string;
114 | baseId: string;
115 | };
116 | server: {
117 | port: number;
118 | host: string;
119 | logLevel: 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE';
120 | };
121 | ai: {
122 | enablePredictiveAnalytics: boolean;
123 | confidenceThreshold: number;
124 | maxAnalysisFields: number;
125 | };
126 | }
127 |
128 | const config: AppConfig = {
129 | airtable: {
130 | token: process.env.AIRTABLE_TOKEN!,
131 | baseId: process.env.AIRTABLE_BASE_ID!
132 | },
133 | server: {
134 | port: 8010,
135 | host: 'localhost',
136 | logLevel: 'INFO'
137 | },
138 | ai: {
139 | enablePredictiveAnalytics: true,
140 | confidenceThreshold: 0.85,
141 | maxAnalysisFields: 10
142 | }
143 | };
144 |
145 | // Main execution with comprehensive error handling
146 | async function main(): Promise<void> {
147 | try {
148 | console.log('🚀 Starting TypeScript Airtable MCP Example');
149 |
150 | await initializeServer();
151 | await performDataOperations();
152 | await useAIPrompts();
153 |
154 | console.log('✅ All operations completed successfully with type safety!');
155 | } catch (error) {
156 | console.error('❌ Error occurred:', error);
157 | process.exit(1);
158 | }
159 | }
160 |
161 | // Export for testing and reuse
162 | export {
163 | initializeServer,
164 | performDataOperations,
165 | useAIPrompts,
166 | validateBusinessMetrics,
167 | BusinessMetrics,
168 | AppConfig
169 | };
170 |
171 | // Run if this file is executed directly
172 | if (require.main === module) {
173 | main();
174 | }
```
--------------------------------------------------------------------------------
/src/typescript/app/tools/query.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createHash } from 'node:crypto';
2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
3 | import {
4 | QueryInput,
5 | QueryOutput,
6 | queryInputSchema,
7 | queryInputShape,
8 | queryOutputSchema
9 | } from '../types';
10 | import { AppContext } from '../context';
11 | import { handleToolError } from './handleError';
12 |
13 | type PiiPolicy = {
14 | field: string;
15 | policy: 'mask' | 'hash' | 'drop';
16 | };
17 |
18 | function maskValue(value: unknown): unknown {
19 | if (value === null || value === undefined) {
20 | return value;
21 | }
22 | if (Array.isArray(value)) {
23 | return value.map(() => '••••');
24 | }
25 | if (typeof value === 'object') {
26 | return '[redacted]';
27 | }
28 | return '••••';
29 | }
30 |
31 | function hashValue(value: unknown): string {
32 | const serialized =
33 | typeof value === 'string' ? value : JSON.stringify(value ?? '');
34 | return createHash('sha256').update(serialized).digest('hex');
35 | }
36 |
37 | function applyPiiPolicies(
38 | fields: Record<string, unknown>,
39 | policies: PiiPolicy[]
40 | ): Record<string, unknown> {
41 | if (!policies.length) {
42 | return fields;
43 | }
44 | const result: Record<string, unknown> = { ...fields };
45 | for (const policy of policies) {
46 | if (!(policy.field in result)) continue;
47 | switch (policy.policy) {
48 | case 'drop':
49 | delete result[policy.field];
50 | break;
51 | case 'mask':
52 | result[policy.field] = maskValue(result[policy.field]);
53 | break;
54 | case 'hash':
55 | result[policy.field] = hashValue(result[policy.field]);
56 | break;
57 | default:
58 | break;
59 | }
60 | }
61 | return result;
62 | }
63 |
64 | export function registerQueryTool(server: McpServer, ctx: AppContext): void {
65 | server.registerTool(
66 | 'query',
67 | {
68 | description: 'Query Airtable records with filtering, sorting, and pagination.',
69 | inputSchema: queryInputShape,
70 | outputSchema: queryOutputSchema.shape
71 | },
72 | async (args: QueryInput, _extra: unknown) => {
73 | try {
74 | const input = queryInputSchema.parse(args);
75 | ctx.governance.ensureOperationAllowed('query');
76 | ctx.governance.ensureBaseAllowed(input.baseId);
77 | ctx.governance.ensureTableAllowed(input.baseId, input.table);
78 |
79 | const logger = ctx.logger.child({
80 | tool: 'query',
81 | baseId: input.baseId,
82 | table: input.table
83 | });
84 |
85 | const queryParams: Record<string, string | number | boolean | Array<string>> = {};
86 |
87 | if (input.fields) {
88 | queryParams.fields = input.fields;
89 | }
90 | if (input.filterByFormula) {
91 | queryParams.filterByFormula = input.filterByFormula;
92 | }
93 | if (input.view) {
94 | queryParams.view = input.view;
95 | }
96 | if (input.pageSize) {
97 | queryParams.pageSize = input.pageSize;
98 | }
99 | if (input.maxRecords) {
100 | queryParams.maxRecords = input.maxRecords;
101 | }
102 | if (input.offset) {
103 | queryParams.offset = input.offset;
104 | }
105 | if (typeof input.returnFieldsByFieldId === 'boolean') {
106 | queryParams.returnFieldsByFieldId = input.returnFieldsByFieldId;
107 | }
108 |
109 | if (input.sorts) {
110 | input.sorts.forEach((sort, index) => {
111 | queryParams[`sort[${index}][field]`] = sort.field;
112 | queryParams[`sort[${index}][direction]`] = sort.direction ?? 'asc';
113 | });
114 | }
115 |
116 | const response: any = await ctx.airtable.queryRecords(input.baseId, input.table, queryParams);
117 | const rawRecords: Array<Record<string, unknown>> = Array.isArray(response?.records)
118 | ? response.records
119 | : [];
120 |
121 | const piiPolicies = ctx.governance.listPiiPolicies(input.baseId, input.table) as PiiPolicy[];
122 |
123 | const sanitizedRecords = rawRecords.map((record) => {
124 | const fields = typeof record.fields === 'object' && record.fields !== null ? record.fields : {};
125 | return {
126 | id: String(record.id ?? ''),
127 | createdTime: record.createdTime ? String(record.createdTime) : undefined,
128 | fields: applyPiiPolicies(fields as Record<string, unknown>, piiPolicies)
129 | };
130 | });
131 |
132 | const structuredContent: QueryOutput = {
133 | records: sanitizedRecords,
134 | offset: typeof response?.offset === 'string' ? response.offset : undefined,
135 | summary: {
136 | returned: sanitizedRecords.length,
137 | hasMore: Boolean(response?.offset)
138 | }
139 | };
140 |
141 | logger.debug('Query completed', {
142 | returned: sanitizedRecords.length,
143 | hasMore: structuredContent.summary?.hasMore
144 | });
145 |
146 | return {
147 | structuredContent,
148 | content: [] as const
149 | };
150 | } catch (error) {
151 | return handleToolError('query', error, ctx);
152 | }
153 | }
154 | );
155 | }
156 |
```
--------------------------------------------------------------------------------
/src/python/simple_airtable_server.py:
--------------------------------------------------------------------------------
```python
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()
```
--------------------------------------------------------------------------------
/tests/test_mcp_comprehensive.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Comprehensive Test Script for Airtable MCP
5 | * Tests all available MCP tools and functionality
6 | */
7 |
8 | const http = require('http');
9 |
10 | const MCP_SERVER_URL = 'http://localhost:8010/mcp';
11 | const TEST_TOKEN = process.env.AIRTABLE_TOKEN || 'YOUR_AIRTABLE_TOKEN_HERE';
12 | const TEST_BASE_ID = process.env.AIRTABLE_BASE_ID || 'YOUR_BASE_ID_HERE';
13 |
14 | if (TEST_TOKEN === 'YOUR_AIRTABLE_TOKEN_HERE' || TEST_BASE_ID === 'YOUR_BASE_ID_HERE') {
15 | console.error('Error: Please set AIRTABLE_TOKEN and AIRTABLE_BASE_ID environment variables');
16 | console.error('Example: export AIRTABLE_TOKEN=your_token_here');
17 | console.error(' export AIRTABLE_BASE_ID=your_base_id_here');
18 | process.exit(1);
19 | }
20 |
21 | // Helper function to make MCP requests
22 | function makeMCPRequest(method, params = {}) {
23 | return new Promise((resolve, reject) => {
24 | const postData = JSON.stringify({
25 | jsonrpc: '2.0',
26 | id: Date.now(),
27 | method: method,
28 | params: params
29 | });
30 |
31 | const options = {
32 | hostname: 'localhost',
33 | port: 8010,
34 | path: '/mcp',
35 | method: 'POST',
36 | headers: {
37 | 'Content-Type': 'application/json',
38 | 'Content-Length': Buffer.byteLength(postData)
39 | }
40 | };
41 |
42 | const req = http.request(options, (res) => {
43 | let data = '';
44 | res.on('data', (chunk) => {
45 | data += chunk;
46 | });
47 | res.on('end', () => {
48 | try {
49 | const response = JSON.parse(data);
50 | resolve(response);
51 | } catch (e) {
52 | reject(new Error(`Failed to parse response: ${e.message}`));
53 | }
54 | });
55 | });
56 |
57 | req.on('error', (e) => {
58 | reject(new Error(`Request failed: ${e.message}`));
59 | });
60 |
61 | req.write(postData);
62 | req.end();
63 | });
64 | }
65 |
66 | async function runComprehensiveTest() {
67 | console.log('🔌 Airtable MCP Comprehensive Test');
68 | console.log('===================================');
69 | console.log(`Server: ${MCP_SERVER_URL}`);
70 | console.log(`Base ID: ${TEST_BASE_ID}`);
71 | console.log(`Token: ${TEST_TOKEN.substring(0, 10)}...${TEST_TOKEN.substring(TEST_TOKEN.length - 10)}`);
72 | console.log('');
73 |
74 | try {
75 | // Test 1: List Resources
76 | console.log('📋 Test 1: Listing Resources');
77 | console.log('----------------------------');
78 | const resourcesResponse = await makeMCPRequest('resources/list');
79 | console.log('✅ Resources Response:');
80 | console.log(JSON.stringify(resourcesResponse, null, 2));
81 | console.log('');
82 |
83 | // Test 2: List Prompts
84 | console.log('📝 Test 2: Listing Prompts');
85 | console.log('-------------------------');
86 | const promptsResponse = await makeMCPRequest('prompts/list');
87 | console.log('✅ Prompts Response:');
88 | console.log(JSON.stringify(promptsResponse, null, 2));
89 | console.log('');
90 |
91 | // Test 3: List Tables
92 | console.log('📊 Test 3: Listing Tables');
93 | console.log('------------------------');
94 | const tablesResponse = await makeMCPRequest('tools/call', {
95 | name: 'list_tables'
96 | });
97 | console.log('✅ Tables Response:');
98 | console.log(JSON.stringify(tablesResponse, null, 2));
99 | console.log('');
100 |
101 | // Test 4: List Records from Requests table
102 | console.log('📄 Test 4: Listing Records (Requests Table)');
103 | console.log('-------------------------------------------');
104 | const recordsResponse = await makeMCPRequest('tools/call', {
105 | name: 'list_records',
106 | arguments: {
107 | table_name: 'requests',
108 | max_records: 3
109 | }
110 | });
111 | console.log('✅ Records Response:');
112 | console.log(JSON.stringify(recordsResponse, null, 2));
113 | console.log('');
114 |
115 | // Test 5: List Records from Providers table
116 | console.log('👥 Test 5: Listing Records (Providers Table)');
117 | console.log('--------------------------------------------');
118 | const providersResponse = await makeMCPRequest('tools/call', {
119 | name: 'list_records',
120 | arguments: {
121 | table_name: 'providers',
122 | max_records: 3
123 | }
124 | });
125 | console.log('✅ Providers Response:');
126 | console.log(JSON.stringify(providersResponse, null, 2));
127 | console.log('');
128 |
129 | // Test 6: List Records from Categories table
130 | console.log('🏷️ Test 6: Listing Records (Categories Table)');
131 | console.log('---------------------------------------------');
132 | const categoriesResponse = await makeMCPRequest('tools/call', {
133 | name: 'list_records',
134 | arguments: {
135 | table_name: 'categories',
136 | max_records: 3
137 | }
138 | });
139 | console.log('✅ Categories Response:');
140 | console.log(JSON.stringify(categoriesResponse, null, 2));
141 | console.log('');
142 |
143 | console.log('🎉 All Tests Completed Successfully!');
144 | console.log('');
145 | console.log('📊 Test Summary:');
146 | console.log('✅ MCP Server is running and accessible');
147 | console.log('✅ Airtable API connection is working');
148 | console.log('✅ All MCP tools are functioning properly');
149 | console.log('✅ JSON-RPC protocol is correctly implemented');
150 | console.log('✅ Error handling is working');
151 | console.log('');
152 | console.log('🚀 The Airtable MCP is ready for use!');
153 |
154 | } catch (error) {
155 | console.error('❌ Test failed:', error.message);
156 | process.exit(1);
157 | }
158 | }
159 |
160 | // Run the comprehensive test
161 | runComprehensiveTest();
162 |
163 |
```
--------------------------------------------------------------------------------
/docs/guides/INSTALLATION.md:
--------------------------------------------------------------------------------
```markdown
1 | # Installation
2 |
3 | Airtable MCP embeds Airtable database connectivity directly into your AI-powered code editor
4 |
5 | ## Getting Started
6 |
7 | Built by Rashid Azarang,
8 |
9 | Airtable MCP gives AI code editors and agents the ability to access and manipulate your Airtable databases for powerful data management capabilities - all in a secure manner with your own API tokens.
10 |
11 | With this MCP server tool, you can enable AI code editors and agents to have access to:
12 |
13 | * List and access all your Airtable bases
14 | * Browse tables, fields, and record data
15 | * Create, read, update, and delete records
16 | * Export and manipulate schemas
17 | * Perform complex queries against your data
18 | * Create data migration mappings
19 | * Analyze and transform your Airtable data
20 |
21 | That way, you can simply tell Cursor or any AI code editor with MCP integrations:
22 |
23 | "Show me all the tables in my Airtable base"
24 |
25 | "Find all records from the Customers table where the status is Active and the last purchase was after January 1st"
26 |
27 | "Create a new record in the Products table with these fields..."
28 |
29 | "Export the schema of my current Airtable base"
30 |
31 | "Help me create a mapping between these two tables for data migration"
32 |
33 | ---
34 |
35 | ## Requirements
36 |
37 | * Node.js 14+ installed on your machine
38 | * Python 3.10+ installed on your machine (automatically detected)
39 | * Airtable Personal Access Token (API Key)
40 | * MCP Client Application (Cursor, Claude Desktop, Cline, Zed, etc.)
41 |
42 | **Note**: Model Context Protocol (MCP) is specific to Anthropic models. When using an editor like Cursor, make sure to enable composer agent with Claude 3.5 Sonnet selected as the model.
43 |
44 | ---
45 |
46 | ## Installation
47 |
48 | ### 1. Install via Smithery (Easiest)
49 |
50 | The easiest way to install Airtable MCP is through Smithery:
51 |
52 | 1. Visit [Smithery](https://smithery.ai)
53 | 2. Search for "@rashidazarang/airtable-mcp"
54 | 3. Click "Install" and follow the prompts to configure with your Airtable token and base ID
55 |
56 | ### 2. Install via NPX (Alternative)
57 |
58 | Another simple way to install and use Airtable MCP is via NPX:
59 |
60 | ```bash
61 | # Install globally
62 | npm install -g airtable-mcp
63 |
64 | # Or use directly with npx (no installation needed)
65 | npx airtable-mcp --token YOUR_AIRTABLE_TOKEN --base YOUR_BASE_ID
66 | ```
67 |
68 | ### 3. Get Your Airtable API Token
69 |
70 | 1. Log in to your Airtable account
71 | 2. Go to your [Account Settings](https://airtable.com/account)
72 | 3. Navigate to the "API" section
73 | 4. Create a Personal Access Token with appropriate permissions
74 | 5. Copy your token to use in the configuration
75 |
76 | ### 4. Configure Your MCP Client
77 |
78 | #### For Cursor:
79 |
80 | 1. Go to Cursor Settings
81 | 2. Navigate to Features, scroll down to MCP Servers and click "Add new MCP server"
82 | 3. Give it a unique name (airtable-tools), set type to "command" and set the command to:
83 |
84 | **For macOS/Linux/Windows:**
85 | ```bash
86 | npx airtable-mcp --token YOUR_AIRTABLE_TOKEN --base YOUR_BASE_ID
87 | ```
88 |
89 | Replace `YOUR_AIRTABLE_TOKEN` with your Airtable Personal Access Token and `YOUR_BASE_ID` with your default Airtable base ID (optional).
90 |
91 | #### For Advanced Users via ~/.cursor/mcp.json:
92 |
93 | Edit your `~/.cursor/mcp.json` file to include:
94 |
95 | ```json
96 | {
97 | "mcpServers": {
98 | "airtable-tools": {
99 | "command": "npx",
100 | "args": [
101 | "airtable-mcp",
102 | "--token", "YOUR_AIRTABLE_TOKEN",
103 | "--base", "YOUR_BASE_ID"
104 | ]
105 | }
106 | }
107 | }
108 | ```
109 |
110 | ### 5. Verify Connection
111 |
112 | 1. Restart your MCP client (Cursor, etc.)
113 | 2. Create a new query using the Composer Agent with Claude 3.5 Sonnet model
114 | 3. Ask something like "List my Airtable bases" or "Show me the tables in my current base"
115 | 4. You should see a response with your Airtable data
116 |
117 | ### 6. For Production Use (Optional)
118 |
119 | For continuous availability, you can set up Airtable MCP using PM2:
120 |
121 | ```bash
122 | # Install PM2 if you don't have it
123 | npm install -g pm2
124 |
125 | # Create a PM2 config file
126 | echo 'module.exports = {
127 | apps: [
128 | {
129 | name: "airtable-mcp",
130 | script: "npx",
131 | args: [
132 | "airtable-mcp",
133 | "--token", "YOUR_AIRTABLE_TOKEN",
134 | "--base", "YOUR_BASE_ID"
135 | ],
136 | env: {
137 | PATH: process.env.PATH,
138 | },
139 | },
140 | ],
141 | };' > ecosystem.config.js
142 |
143 | # Start the process
144 | pm2 start ecosystem.config.js
145 |
146 | # Set it to start on boot
147 | pm2 startup
148 | pm2 save
149 | ```
150 |
151 | ---
152 |
153 | ## Troubleshooting
154 |
155 | Here are some common issues and their solutions:
156 |
157 | ### Error: Unable to connect to Airtable API
158 |
159 | - Double-check your Airtable API token is correct and has sufficient permissions
160 | - Verify your internet connection
161 | - Check if Airtable API is experiencing downtime
162 |
163 | ### Issue: MCP server not connecting
164 |
165 | - Make sure Node.js 14+ and Python 3.10+ are installed and in your PATH
166 | - Try specifying a specific version: `npx airtable-mcp@latest`
167 | - Check the Cursor logs for any connection errors
168 |
169 | ### Error: Base not found
170 |
171 | - Verify your base ID is correct
172 | - Make sure your API token has access to the specified base
173 | - Try listing all bases first to confirm access
174 |
175 | ### Issue: Permission denied errors
176 |
177 | - Make sure your token has the necessary permissions for the operations you're trying to perform
178 | - Check if you're attempting operations on tables/bases that your token doesn't have access to
179 |
180 | ### For more help
181 |
182 | - Open an issue on the [GitHub repository](https://github.com/rashidazarang/airtable-mcp/issues)
183 | - Check the Airtable API documentation for any API-specific errors
```
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | const path = require('path');
4 | const { execSync } = require('child_process');
5 | const { spawn } = require('child_process');
6 |
7 | // Polyfill for AbortController in older Node.js versions
8 | if (typeof globalThis.AbortController === 'undefined') {
9 | globalThis.AbortController = class AbortController {
10 | constructor() {
11 | this.signal = {
12 | aborted: false,
13 | addEventListener: () => {},
14 | removeEventListener: () => {},
15 | dispatchEvent: () => true
16 | };
17 | }
18 | abort() {
19 | this.signal.aborted = true;
20 | }
21 | };
22 | console.log('ℹ️ Added AbortController polyfill for compatibility with older Node.js versions');
23 | }
24 |
25 | // Parse command-line arguments
26 | const args = process.argv.slice(2);
27 | let tokenIndex = args.indexOf('--token');
28 | let baseIndex = args.indexOf('--base');
29 | let configIndex = args.indexOf('--config');
30 |
31 | // Extract token, base ID and config
32 | const token = tokenIndex !== -1 && tokenIndex + 1 < args.length ? args[tokenIndex + 1] : null;
33 | const baseId = baseIndex !== -1 && baseIndex + 1 < args.length ? args[baseIndex + 1] : null;
34 | const config = configIndex !== -1 && configIndex + 1 < args.length ? args[configIndex + 1] : null;
35 |
36 | console.log('🔌 Airtable MCP - Connecting your AI to Airtable');
37 | console.log('-----------------------------------------------');
38 |
39 | // Find Python interpreter
40 | const getPythonPath = () => {
41 | try {
42 | const whichPython = execSync('which python3.10').toString().trim();
43 | return whichPython;
44 | } catch (e) {
45 | try {
46 | const whichPython = execSync('which python3').toString().trim();
47 | return whichPython;
48 | } catch (e) {
49 | return 'python';
50 | }
51 | }
52 | };
53 |
54 | // Check Python version
55 | const checkPythonVersion = (pythonPath) => {
56 | try {
57 | const versionStr = execSync(`${pythonPath} --version`).toString().trim();
58 | const versionMatch = versionStr.match(/Python (\d+)\.(\d+)/);
59 | if (versionMatch) {
60 | const major = parseInt(versionMatch[1]);
61 | const minor = parseInt(versionMatch[2]);
62 | return (major > 3 || (major === 3 && minor >= 10));
63 | }
64 | return false;
65 | } catch (e) {
66 | return false;
67 | }
68 | };
69 |
70 | const pythonPath = getPythonPath();
71 |
72 | // Verify Python compatibility
73 | if (!checkPythonVersion(pythonPath)) {
74 | console.error('❌ Error: MCP SDK requires Python 3.10+');
75 | console.error('Please install Python 3.10 or newer and try again.');
76 | process.exit(1);
77 | }
78 |
79 | // We now use inspector_server.py instead of server.py
80 | const serverScript = path.join(__dirname, 'inspector_server.py');
81 |
82 | // Check if the script exists
83 | try {
84 | require('fs').accessSync(serverScript, require('fs').constants.F_OK);
85 | } catch (e) {
86 | console.error(`❌ Error: Could not find server script at ${serverScript}`);
87 | console.error('Please make sure you have the complete package installed.');
88 | process.exit(1);
89 | }
90 |
91 | // Prepare arguments for the Python script
92 | const scriptArgs = [serverScript];
93 | if (token) {
94 | scriptArgs.push('--token', token);
95 | }
96 | if (baseId) {
97 | scriptArgs.push('--base', baseId);
98 | }
99 | if (config) {
100 | scriptArgs.push('--config', config);
101 |
102 | // Try to extract and log info from config
103 | try {
104 | const configObj = JSON.parse(config);
105 | if (configObj.airtable_token) {
106 | console.log('✅ Using API token from config');
107 | }
108 | if (configObj.base_id) {
109 | console.log(`✅ Using base ID from config: ${configObj.base_id}`);
110 | }
111 | } catch (e) {
112 | console.warn('⚠️ Could not parse config JSON, attempting to sanitize...');
113 |
114 | // Sanitize config JSON - fix common formatting issues
115 | try {
116 | // Remove any unexpected line breaks, extra quotes, and escape characters
117 | const sanitizedConfig = config
118 | .replace(/[\r\n]+/g, '')
119 | .replace(/\\+"/g, '"')
120 | .replace(/^"/, '')
121 | .replace(/"$/, '')
122 | .replace(/\\/g, '');
123 |
124 | // Try parsing it
125 | const configObj = JSON.parse(sanitizedConfig);
126 | if (configObj) {
127 | console.log('✅ Successfully sanitized config JSON');
128 | // Update config with sanitized version
129 | scriptArgs[scriptArgs.indexOf(config)] = sanitizedConfig;
130 | config = sanitizedConfig;
131 |
132 | if (configObj.airtable_token) {
133 | console.log('✅ Using API token from sanitized config');
134 | }
135 | if (configObj.base_id) {
136 | console.log(`✅ Using base ID from sanitized config: ${configObj.base_id}`);
137 | }
138 | }
139 | } catch (sanitizeErr) {
140 | console.warn('⚠️ Could not sanitize config JSON, passing it directly to Python script');
141 | }
142 | }
143 | } else {
144 | if (token) {
145 | console.log('✅ Using provided API token');
146 | } else {
147 | console.log('⚠️ No API token provided, will try to use .env file');
148 | }
149 |
150 | if (baseId) {
151 | console.log(`✅ Using base ID: ${baseId}`);
152 | } else {
153 | console.log('ℹ️ No base ID provided, will need to set one later');
154 | }
155 | }
156 |
157 | // Execute the Python script
158 | const serverProcess = spawn(pythonPath, scriptArgs, {
159 | stdio: 'inherit',
160 | });
161 |
162 | // Handle process exit
163 | serverProcess.on('close', (code) => {
164 | if (code !== 0) {
165 | console.error(`❌ Airtable MCP server exited with code ${code}`);
166 | }
167 | process.exit(code);
168 | });
169 |
170 | // Handle signals
171 | process.on('SIGINT', () => {
172 | console.log('\n👋 Shutting down Airtable MCP server...');
173 | serverProcess.kill('SIGINT');
174 | });
175 |
176 | process.on('SIGTERM', () => {
177 | console.log('\n👋 Shutting down Airtable MCP server...');
178 | serverProcess.kill('SIGTERM');
179 | });
```
--------------------------------------------------------------------------------
/ISSUE_RESPONSES.md:
--------------------------------------------------------------------------------
```markdown
1 | # GitHub Issue Responses
2 |
3 | ## Issue #7: Personal Access Token Leakage
4 |
5 | Thank you for responsibly disclosing this security vulnerability. This has been fixed in v1.2.4.
6 |
7 | ### Actions Taken:
8 | ✅ Removed all hardcoded tokens from test files
9 | ✅ Updated code to require environment variables for credentials
10 | ✅ Added SECURITY_NOTICE.md with rotation instructions
11 | ✅ The exposed tokens have been invalidated
12 |
13 | ### Changes Made:
14 | - `test_client.py` - Now uses environment variables
15 | - `test_mcp_comprehensive.js` - Now uses environment variables
16 | - Added `.env.example` file for secure configuration
17 | - Updated documentation with security best practices
18 |
19 | All users should update to v1.2.4 immediately. The exposed tokens are no longer valid, and users must use their own Airtable credentials.
20 |
21 | Thank you for helping improve the security of this project!
22 |
23 | ---
24 |
25 | ## Issue #6: [Server Bug] @rashidazarang/airtable-mcp
26 |
27 | This issue has been resolved in v1.2.4!
28 |
29 | ### Root Cause:
30 | The Smithery configuration was using the Python implementation which had compatibility issues with MCP 1.4.1.
31 |
32 | ### Solution:
33 | - Updated `smithery.yaml` to use the stable JavaScript implementation (`airtable_simple.js`)
34 | - Fixed authentication flow to properly handle credentials
35 | - Added proper environment variable support
36 |
37 | ### To fix:
38 | 1. Update to v1.2.4: `npm install @rashidazarang/airtable-mcp@latest`
39 | 2. Reconfigure with your credentials as shown in the updated README
40 | 3. Restart Claude Desktop
41 |
42 | The server should now connect properly without the "API key is required" error.
43 |
44 | ---
45 |
46 | ## Issue #5: When Using Smithy, throwing 'Streamable HTTP error: Error POSTing to endpoint (HTTP 400): null'
47 |
48 | Fixed in v1.2.4! This was the same root cause as Issue #6.
49 |
50 | ### What was wrong:
51 | - The Python server had MCP compatibility issues
52 | - Authentication wasn't being handled correctly
53 | - The Smithery configuration was misconfigured
54 |
55 | ### What we fixed:
56 | - Switched to the JavaScript implementation
57 | - Updated smithery.yaml with proper configuration
58 | - Fixed credential passing through Smithery
59 |
60 | ### How to resolve:
61 | ```bash
62 | # Update to latest version
63 | npm install -g @rashidazarang/airtable-mcp@latest
64 |
65 | # Or if using Smithery directly:
66 | npx @smithery/cli install @rashidazarang/airtable-mcp --update
67 | ```
68 |
69 | Then reconfigure with your Airtable credentials. The HTTP 400 errors should be resolved.
70 |
71 | ---
72 |
73 | ## Issue #4: Glama listing is missing Dockerfile
74 |
75 | Fixed in v1.2.4!
76 |
77 | ### Changes:
78 | - Created `Dockerfile.node` specifically for Node.js deployment
79 | - Updated `smithery.yaml` to reference the correct Dockerfile
80 | - The original Dockerfile is retained for backward compatibility
81 |
82 | The Dockerfile is now included and properly configured for cloud deployments.
83 |
84 | ---
85 |
86 | # GitHub Release Text for v1.2.4
87 |
88 | ## 🚨 Critical Security Release - v1.2.4
89 |
90 | ### ⚠️ IMPORTANT SECURITY FIX
91 |
92 | This release addresses a **critical security vulnerability** where API tokens were hardcoded in test files. All users should update immediately.
93 |
94 | ### 🔒 Security Fixes
95 | - **Removed hardcoded API tokens** from all test files (fixes #7)
96 | - Test files now require environment variables for credentials
97 | - Added comprehensive security documentation
98 | - Previously exposed tokens have been invalidated
99 |
100 | ### 🐛 Bug Fixes
101 | - **Fixed Smithery deployment issues** (fixes #5, #6)
102 | - Resolved HTTP 400 errors when connecting through Smithery
103 | - Fixed "API key is required for remote connections" error
104 | - Switched to stable JavaScript implementation for cloud deployments
105 | - **Added missing Dockerfile** for Glama listing (fixes #4)
106 |
107 | ### ✨ Improvements
108 | - Added environment variable support for secure credential management
109 | - Improved logging with configurable levels (ERROR, WARN, INFO, DEBUG)
110 | - Enhanced error messages for better debugging
111 | - Updated documentation with clear setup instructions
112 |
113 | ### 📦 What's Changed
114 | - `test_client.py` - Now uses environment variables
115 | - `test_mcp_comprehensive.js` - Now uses environment variables
116 | - `airtable_simple.js` - Added env variable and logging support
117 | - `smithery.yaml` - Fixed to use JavaScript implementation
118 | - `Dockerfile.node` - New optimized Docker image for Node.js
119 | - `SECURITY_NOTICE.md` - Important security information
120 | - `README.md` - Complete rewrite with better instructions
121 |
122 | ### 💔 Breaking Changes
123 | Test files now require environment variables:
124 | ```bash
125 | export AIRTABLE_TOKEN="your_token"
126 | export AIRTABLE_BASE_ID="your_base_id"
127 | ```
128 |
129 | ### 📋 Migration Instructions
130 |
131 | 1. **Update to v1.2.4:**
132 | ```bash
133 | npm install -g @rashidazarang/airtable-mcp@latest
134 | ```
135 |
136 | 2. **Set up environment variables:**
137 | ```bash
138 | export AIRTABLE_TOKEN="your_personal_token"
139 | export AIRTABLE_BASE_ID="your_base_id"
140 | ```
141 |
142 | 3. **Update your MCP configuration** (see README for details)
143 |
144 | 4. **Restart your MCP client**
145 |
146 | ### 🙏 Acknowledgments
147 | Special thanks to @BXXC-SDXZ for responsibly disclosing the security vulnerability, and to @ricklesgibson and @punkpeye for reporting the deployment issues.
148 |
149 | ### ⚠️ Security Note
150 | If you were using the previously exposed tokens, they have been revoked. You must use your own Airtable credentials going forward.
151 |
152 | **Full Changelog**: https://github.com/rashidazarang/airtable-mcp/compare/v1.2.3...v1.2.4
153 |
154 | ---
155 |
156 | ## NPM Publish Commands
157 |
158 | ```bash
159 | # Make sure you're logged in to npm
160 | npm login
161 |
162 | # Update version (already done in package.json)
163 | npm version 1.2.4
164 |
165 | # Publish to npm
166 | npm publish --access public
167 |
168 | # Create git tag
169 | git tag -a v1.2.4 -m "Critical security fix and Smithery deployment fixes"
170 | git push origin v1.2.4
171 | ```
```
--------------------------------------------------------------------------------
/src/typescript/app/config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import fs from 'node:fs';
2 | import path from 'node:path';
3 | import { createHash } from 'node:crypto';
4 | import { config as loadEnv } from 'dotenv';
5 | import { governanceOutputSchema, GovernanceSnapshot } from './types';
6 | import { GovernanceError } from '../errors';
7 |
8 | loadEnv();
9 |
10 | export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
11 |
12 | export interface AirtableAuthConfig {
13 | personalAccessToken: string;
14 | patHash: string;
15 | defaultBaseId?: string;
16 | allowedBases: string[];
17 | }
18 |
19 | export interface AppConfig {
20 | version: string;
21 | auth: AirtableAuthConfig;
22 | governance: GovernanceSnapshot;
23 | logLevel: LogLevel;
24 | exceptionQueueSize: number;
25 | }
26 |
27 | const DEFAULT_EXCEPTION_QUEUE_SIZE = 500;
28 |
29 | function parseCsv(value?: string | null): string[] {
30 | if (!value) {
31 | return [];
32 | }
33 | return value
34 | .split(',')
35 | .map((entry) => entry.trim())
36 | .filter((entry) => entry.length > 0);
37 | }
38 |
39 | function hashSecret(secret: string): string {
40 | return createHash('sha256').update(secret).digest('hex').slice(0, 12);
41 | }
42 |
43 | function resolveLogLevel(): LogLevel {
44 | const raw = (process.env.LOG_LEVEL || 'info').toLowerCase();
45 | if (raw === 'error' || raw === 'warn' || raw === 'info' || raw === 'debug') {
46 | return raw;
47 | }
48 | return 'info';
49 | }
50 |
51 | function determineAllowedBases(defaultBaseId?: string): string[] {
52 | const fromEnv = parseCsv(process.env.AIRTABLE_ALLOWED_BASES || process.env.AIRTABLE_BASE_ALLOWLIST);
53 | const baseSet = new Set<string>();
54 | if (defaultBaseId) {
55 | baseSet.add(defaultBaseId);
56 | }
57 | fromEnv.forEach((base) => baseSet.add(base));
58 | // Allow empty base list - users can use list_bases tool to discover bases
59 | // and then specify them dynamically in tool calls
60 | return Array.from(baseSet);
61 | }
62 |
63 | function parseAllowedTables(raw?: string | null): Array<{ baseId: string; table: string }> {
64 | if (!raw) {
65 | return [];
66 | }
67 | const tables: Array<{ baseId: string; table: string }> = [];
68 | for (const entry of raw.split(',')) {
69 | const trimmed = entry.trim();
70 | if (!trimmed) continue;
71 | const [baseId, table] = trimmed.split(':');
72 | if (!baseId || !table) {
73 | throw new GovernanceError(
74 | `Invalid AIRTABLE_ALLOWED_TABLES entry "${trimmed}". Expected format baseId:tableName.`
75 | );
76 | }
77 | tables.push({ baseId: baseId.trim(), table: table.trim() });
78 | }
79 | return tables;
80 | }
81 |
82 | function readGovernanceFile(): Partial<GovernanceSnapshot> | undefined {
83 | const explicitPath = process.env.AIRTABLE_GOVERNANCE_PATH;
84 | const fallbackPath = path.resolve(process.cwd(), 'config', 'governance.json');
85 |
86 | const filePath = explicitPath || fallbackPath;
87 | if (!fs.existsSync(filePath)) {
88 | return undefined;
89 | }
90 |
91 | try {
92 | const raw = fs.readFileSync(filePath, 'utf8');
93 | const parsed = JSON.parse(raw);
94 | const partialSchema = governanceOutputSchema.partial();
95 | const result = partialSchema.parse(parsed) as Partial<GovernanceSnapshot>;
96 | return result;
97 | } catch (error) {
98 | throw new GovernanceError(
99 | `Failed to parse governance configuration at ${filePath}: ${error instanceof Error ? error.message : String(error)}`
100 | );
101 | }
102 | }
103 |
104 | function buildGovernanceSnapshot(allowedBases: string[]): GovernanceSnapshot {
105 | const baseSnapshot: GovernanceSnapshot = {
106 | allowedBases,
107 | allowedTables: [],
108 | allowedOperations: ['describe', 'query', 'create', 'update', 'upsert'],
109 | piiFields: [],
110 | redactionPolicy: 'mask_on_inline',
111 | loggingPolicy: 'minimal',
112 | retentionDays: 7
113 | };
114 |
115 | const overrides = readGovernanceFile();
116 |
117 | const envAllowedTables = parseAllowedTables(process.env.AIRTABLE_ALLOWED_TABLES);
118 |
119 | const merged: GovernanceSnapshot = {
120 | ...baseSnapshot,
121 | ...(overrides ?? {})
122 | };
123 |
124 | // Ensure allow-lists include env tables/bases.
125 | const bases = new Set<string>(merged.allowedBases);
126 | allowedBases.forEach((base) => bases.add(base));
127 | merged.allowedBases = Array.from(bases);
128 |
129 | if (overrides?.allowedTables || envAllowedTables.length > 0) {
130 | const tableSet = new Map<string, { baseId: string; table: string }>();
131 | (overrides?.allowedTables ?? []).forEach((table) => {
132 | tableSet.set(`${table.baseId}:${table.table}`, table);
133 | });
134 | envAllowedTables.forEach((table) => {
135 | tableSet.set(`${table.baseId}:${table.table}`, table);
136 | });
137 | merged.allowedTables = Array.from(tableSet.values());
138 | }
139 |
140 | return governanceOutputSchema.parse(merged);
141 | }
142 |
143 | export function loadConfig(): AppConfig {
144 | const personalAccessToken =
145 | process.env.AIRTABLE_PAT ||
146 | process.env.AIRTABLE_TOKEN ||
147 | process.env.AIRTABLE_API_TOKEN ||
148 | process.env.AIRTABLE_API_KEY;
149 |
150 | if (!personalAccessToken) {
151 | throw new GovernanceError(
152 | 'Missing Airtable credentials. Set AIRTABLE_PAT (preferred) or AIRTABLE_TOKEN.'
153 | );
154 | }
155 |
156 | const defaultBaseId = process.env.AIRTABLE_DEFAULT_BASE ?? process.env.AIRTABLE_BASE_ID ?? process.env.AIRTABLE_BASE;
157 | const allowedBases = determineAllowedBases(defaultBaseId);
158 | const governance = buildGovernanceSnapshot(allowedBases);
159 |
160 | const auth: AirtableAuthConfig = {
161 | personalAccessToken,
162 | patHash: hashSecret(personalAccessToken),
163 | allowedBases
164 | };
165 | if (defaultBaseId) {
166 | auth.defaultBaseId = defaultBaseId;
167 | }
168 |
169 | return {
170 | version: process.env.npm_package_version || '0.0.0',
171 | auth,
172 | governance,
173 | logLevel: resolveLogLevel(),
174 | exceptionQueueSize:
175 | Number.parseInt(process.env.EXCEPTION_QUEUE_SIZE || '', 10) > 0
176 | ? Number.parseInt(process.env.EXCEPTION_QUEUE_SIZE as string, 10)
177 | : DEFAULT_EXCEPTION_QUEUE_SIZE
178 | };
179 | }
180 |
```
--------------------------------------------------------------------------------
/examples/env-demo.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Demo script that uses the AIRTABLE_BASE_ID from the .env file
5 | * Demonstrates various operations with the Airtable API
6 | */
7 |
8 | require('dotenv').config();
9 | const baseUtils = require('../tools/airtable-base');
10 | const crudUtils = require('../tools/airtable-crud');
11 | const schemaUtils = require('../tools/airtable-schema');
12 |
13 | // Constants
14 | const DEMO_TABLE_NAME = 'ENV Demo Table';
15 | const SAMPLE_RECORDS = [
16 | { Name: 'Record from ENV Demo', Description: 'Created using AIRTABLE_BASE_ID from .env file', Status: 'Active' },
17 | { Name: 'Another ENV Record', Description: 'Second record from the environment demo', Status: 'Pending' }
18 | ];
19 |
20 | async function runDemo() {
21 | console.log('=================================');
22 | console.log(' AIRTABLE ENV DEMO SCRIPT ');
23 | console.log('=================================');
24 |
25 | // Check environment variables
26 | if (!process.env.AIRTABLE_PERSONAL_ACCESS_TOKEN) {
27 | console.error('❌ Error: AIRTABLE_PERSONAL_ACCESS_TOKEN is not set in .env file');
28 | process.exit(1);
29 | }
30 |
31 | if (!process.env.AIRTABLE_BASE_ID) {
32 | console.error('❌ Error: AIRTABLE_BASE_ID is not set in .env file');
33 | process.exit(1);
34 | }
35 |
36 | const baseId = process.env.AIRTABLE_BASE_ID;
37 | console.log(`✅ Using AIRTABLE_BASE_ID: ${baseId}`);
38 |
39 | try {
40 | // Step 1: Verify base access
41 | console.log('\nStep 1: Verifying access to the base...');
42 | const baseAccess = await baseUtils.checkBaseAccess(baseId);
43 |
44 | if (!baseAccess.accessible) {
45 | console.error(`❌ Error: Cannot access base with ID ${baseId}`);
46 | console.error(` Reason: ${baseAccess.error}`);
47 | process.exit(1);
48 | }
49 |
50 | console.log(`✅ Access confirmed to base: ${baseAccess.name}`);
51 |
52 | // Step 2: List existing tables
53 | console.log('\nStep 2: Listing existing tables...');
54 | const tables = await baseUtils.listTables(baseId);
55 | console.log(`✅ Found ${tables.length} tables in the base`);
56 |
57 | // Step 3: Check if our demo table exists
58 | console.log('\nStep 3: Checking if demo table exists...');
59 | const demoTableExists = await crudUtils.tableExists(baseId, DEMO_TABLE_NAME);
60 |
61 | if (demoTableExists) {
62 | console.log(`✅ Demo table "${DEMO_TABLE_NAME}" already exists`);
63 | } else {
64 | console.log(`ℹ️ Demo table "${DEMO_TABLE_NAME}" does not exist, creating it...`);
65 |
66 | // Step 4: Create the demo table
67 | console.log('\nStep 4: Creating the demo table...');
68 | const tableConfig = {
69 | name: DEMO_TABLE_NAME,
70 | description: 'Table created from the Environment Demo script',
71 | fields: [
72 | {
73 | name: 'Name',
74 | type: 'singleLineText',
75 | description: 'Record name'
76 | },
77 | {
78 | name: 'Description',
79 | type: 'multilineText',
80 | description: 'Record description'
81 | },
82 | {
83 | name: 'Status',
84 | type: 'singleSelect',
85 | options: {
86 | choices: [
87 | { name: 'Active' },
88 | { name: 'Pending' },
89 | { name: 'Completed' }
90 | ]
91 | },
92 | description: 'Current status'
93 | },
94 | {
95 | name: 'Created',
96 | type: 'date',
97 | options: {
98 | dateFormat: {
99 | name: 'local'
100 | }
101 | },
102 | description: 'Creation date'
103 | }
104 | ]
105 | };
106 |
107 | await schemaUtils.createTable(baseId, tableConfig);
108 | console.log(`✅ Created demo table: ${DEMO_TABLE_NAME}`);
109 | }
110 |
111 | // Step 5: Create sample records
112 | console.log('\nStep 5: Creating sample records...');
113 | // Add today's date to all records
114 | const recordsWithDate = SAMPLE_RECORDS.map(record => ({
115 | ...record,
116 | Created: new Date().toISOString().split('T')[0] // Format as YYYY-MM-DD
117 | }));
118 |
119 | const createdRecords = await crudUtils.createRecords(baseId, DEMO_TABLE_NAME, recordsWithDate);
120 | console.log(`✅ Created ${createdRecords.length} sample records`);
121 |
122 | // Step 6: Read records back
123 | console.log('\nStep 6: Reading records from the table...');
124 | const records = await crudUtils.readRecords(baseId, DEMO_TABLE_NAME, 100);
125 | console.log(`✅ Read ${records.length} records from the table`);
126 |
127 | console.log('\nSample record:');
128 | console.log(JSON.stringify(records[0], null, 2));
129 |
130 | // Step 7: Update a record
131 | console.log('\nStep 7: Updating the first record...');
132 | const recordToUpdate = {
133 | id: createdRecords[0].id,
134 | fields: {
135 | Description: createdRecords[0].Description + ' (UPDATED)',
136 | Status: 'Completed'
137 | }
138 | };
139 |
140 | const updatedRecords = await crudUtils.updateRecords(baseId, DEMO_TABLE_NAME, [recordToUpdate]);
141 | console.log(`✅ Updated ${updatedRecords.length} record`);
142 |
143 | // Step 8: Get the updated record
144 | console.log('\nStep 8: Getting the updated record...');
145 | const updatedRecord = await crudUtils.getRecord(baseId, DEMO_TABLE_NAME, createdRecords[0].id);
146 | console.log('Updated record:');
147 | console.log(JSON.stringify(updatedRecord, null, 2));
148 |
149 | // Step 9: Demonstrate filtering records
150 | console.log('\nStep 9: Filtering records by status...');
151 | const completedRecords = await crudUtils.readRecords(baseId, DEMO_TABLE_NAME, 100, 'Status="Completed"');
152 | console.log(`✅ Found ${completedRecords.length} records with Status="Completed"`);
153 |
154 | console.log('\n=================================');
155 | console.log(' ENV DEMO COMPLETED ');
156 | console.log('=================================');
157 | console.log('\nThis script demonstrated:');
158 | console.log('1. Loading environment variables from .env file');
159 | console.log('2. Accessing an Airtable base using AIRTABLE_BASE_ID');
160 | console.log('3. Creating a table (if it doesn\'t exist)');
161 | console.log('4. Creating, reading, and updating records');
162 | console.log('5. Filtering records using Airtable formulas');
163 | console.log('\nAll operations used the AIRTABLE_BASE_ID environment variable');
164 |
165 | } catch (error) {
166 | console.error(`❌ Error: ${error.message}`);
167 | process.exit(1);
168 | }
169 | }
170 |
171 | // Run the demo
172 | runDemo();
```
--------------------------------------------------------------------------------
/src/typescript/app/tools/describe.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
2 | import {
3 | DescribeInput,
4 | DescribeOutput,
5 | describeInputSchema,
6 | describeInputShape,
7 | describeOutputSchema
8 | } from '../types';
9 | import { AppContext } from '../context';
10 | import { GovernanceError, NotFoundError } from '../../errors';
11 | import { handleToolError } from './handleError';
12 |
13 | type DescribeTableEntry = NonNullable<DescribeOutput['tables']>[number];
14 | type DescribeFieldEntry = NonNullable<DescribeTableEntry['fields']>[number];
15 | type DescribeViewEntry = NonNullable<DescribeTableEntry['views']>[number];
16 |
17 | function normalizeField(raw: unknown): DescribeFieldEntry {
18 | const source = raw as Record<string, unknown>;
19 | const field: DescribeFieldEntry = {
20 | id: String(source?.id ?? ''),
21 | name: String(source?.name ?? ''),
22 | type: String(source?.type ?? '')
23 | };
24 | if (source?.description && typeof source.description === 'string') {
25 | field.description = source.description;
26 | }
27 | if (source?.options && typeof source.options === 'object') {
28 | field.options = source.options as Record<string, unknown>;
29 | }
30 | return field;
31 | }
32 |
33 | function normalizeView(raw: unknown): DescribeViewEntry {
34 | const source = raw as Record<string, unknown>;
35 | const view: DescribeViewEntry = {
36 | id: String(source?.id ?? ''),
37 | name: String(source?.name ?? '')
38 | };
39 | if (source?.type && typeof source.type === 'string') {
40 | view.type = source.type;
41 | }
42 | return view;
43 | }
44 |
45 | function normalizeTable(
46 | raw: unknown,
47 | { includeFields, includeViews }: { includeFields: boolean; includeViews: boolean }
48 | ): DescribeTableEntry {
49 | const source = raw as Record<string, unknown>;
50 | const table: DescribeTableEntry = {
51 | id: String(source?.id ?? ''),
52 | name: String(source?.name ?? '')
53 | };
54 | if (source?.primaryFieldId && typeof source.primaryFieldId === 'string') {
55 | table.primaryFieldId = source.primaryFieldId;
56 | }
57 | if (includeFields && Array.isArray(source?.fields)) {
58 | table.fields = (source.fields as unknown[]).map((field) => normalizeField(field));
59 | }
60 | if (includeViews && Array.isArray(source?.views)) {
61 | table.views = (source.views as unknown[]).map((view) => normalizeView(view));
62 | }
63 | return table;
64 | }
65 |
66 | export function registerDescribeTool(server: McpServer, ctx: AppContext): void {
67 | server.registerTool(
68 | 'describe',
69 | {
70 | description: 'Describe Airtable base or table schema.',
71 | inputSchema: describeInputShape,
72 | outputSchema: describeOutputSchema.shape
73 | },
74 | async (args: DescribeInput, _extra: unknown) => {
75 | try {
76 | const input = describeInputSchema.parse(args);
77 | ctx.governance.ensureOperationAllowed('describe');
78 | ctx.governance.ensureBaseAllowed(input.baseId);
79 |
80 | const includeFields = input.includeFields ?? true;
81 | const includeViews = input.includeViews ?? false;
82 |
83 | const logger = ctx.logger.child({
84 | tool: 'describe',
85 | baseId: input.baseId,
86 | scope: input.scope
87 | });
88 |
89 | const [baseInfo, tableInfo] = await Promise.all([
90 | ctx.airtable.getBase(input.baseId),
91 | ctx.airtable.listTables(input.baseId)
92 | ]);
93 |
94 | const baseName =
95 | typeof (baseInfo as any)?.name === 'string'
96 | ? String((baseInfo as any).name)
97 | : input.baseId;
98 |
99 | const rawTables: unknown[] = Array.isArray((tableInfo as any)?.tables)
100 | ? ((tableInfo as any).tables as unknown[])
101 | : [];
102 |
103 | const tables: DescribeTableEntry[] = rawTables
104 | .filter((rawTable: unknown) => {
105 | const record = rawTable as Record<string, unknown>;
106 | const tableId = typeof record.id === 'string' ? record.id : '';
107 | const tableName = typeof record.name === 'string' ? record.name : '';
108 | const idAllowed = tableId
109 | ? ctx.governance.isTableAllowed(input.baseId, tableId)
110 | : false;
111 | const nameAllowed = tableName
112 | ? ctx.governance.isTableAllowed(input.baseId, tableName)
113 | : false;
114 | return idAllowed || nameAllowed;
115 | })
116 | .map((table: unknown) => normalizeTable(table, { includeFields, includeViews }));
117 |
118 | let selectedTables: DescribeTableEntry[] = tables;
119 |
120 | if (input.scope === 'table') {
121 | const target = tables.find(
122 | (tableRecord) =>
123 | String(tableRecord.id) === input.table ||
124 | String(tableRecord.name).toLowerCase() === input.table?.toLowerCase()
125 | );
126 | if (!target) {
127 | const context: Record<string, string> = { baseId: input.baseId };
128 | if (input.table) {
129 | context.table = input.table;
130 | }
131 | throw new NotFoundError(`Table ${input.table} not found in base ${input.baseId}`, {
132 | context
133 | });
134 | }
135 | const targetId = String(target.id);
136 | const targetName = String(target.name);
137 | if (
138 | !ctx.governance.isTableAllowed(input.baseId, targetId) &&
139 | !ctx.governance.isTableAllowed(input.baseId, targetName)
140 | ) {
141 | const context: Record<string, string> = { baseId: input.baseId };
142 | if (input.table) {
143 | context.table = input.table;
144 | }
145 | throw new GovernanceError(`Table ${input.table} is not allowed in base ${input.baseId}`, {
146 | context
147 | });
148 | }
149 | selectedTables = [target];
150 | }
151 |
152 | const structuredContent: DescribeOutput = {
153 | base: {
154 | id: input.baseId,
155 | name: baseName
156 | },
157 | tables: selectedTables
158 | };
159 |
160 | if (input.scope === 'base' && includeViews) {
161 | structuredContent.views = rawTables
162 | .flatMap((table: unknown) => {
163 | const record = table as Record<string, unknown>;
164 | return Array.isArray(record.views) ? (record.views as unknown[]) : [];
165 | })
166 | .map((view: unknown) => normalizeView(view));
167 | }
168 |
169 | logger.debug('Describe completed', {
170 | tableCount: selectedTables.length
171 | });
172 |
173 | return {
174 | structuredContent,
175 | content: [] as const
176 | };
177 | } catch (error) {
178 | return handleToolError('describe', error, ctx);
179 | }
180 | }
181 | );
182 | }
183 |
```
--------------------------------------------------------------------------------
/tests/test_all_features.sh:
--------------------------------------------------------------------------------
```bash
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
```
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
```markdown
1 | # 🚀 Pull Request - Trust Score 100/100
2 |
3 | <!--
4 | Thank you for contributing to the Airtable MCP Server!
5 | Your contribution helps us achieve our goal of a perfect 100/100 Trust Score.
6 | -->
7 |
8 | ## 📋 PR Information
9 |
10 | **PR Type**: <!-- Check all that apply -->
11 | - [ ] 🐛 Bug Fix
12 | - [ ] ✨ New Feature
13 | - [ ] 🔒 Security Enhancement
14 | - [ ] 📚 Documentation Update
15 | - [ ] 🧹 Code Refactoring
16 | - [ ] ⚡ Performance Improvement
17 | - [ ] 🧪 Test Enhancement
18 | - [ ] 🔧 Build/CI Changes
19 | - [ ] 💥 Breaking Change
20 |
21 | **Issue Reference**:
22 | <!-- Link to the issue this PR addresses -->
23 | - Closes #[issue_number]
24 | - Related to #[issue_number]
25 |
26 | ## 📝 Description
27 |
28 | ### What Changed
29 | <!-- Provide a clear and concise description of what this PR does -->
30 |
31 | ### Why This Change
32 | <!-- Explain the motivation behind this change -->
33 |
34 | ### How It Works
35 | <!-- Describe the technical approach and implementation -->
36 |
37 | ## 🎯 Trust Score Impact
38 |
39 | **Trust Score Categories Affected**: <!-- Check all that apply -->
40 | - [ ] 🛡️ Security & Authentication
41 | - [ ] 📊 Code Quality & Standards
42 | - [ ] 🧪 Testing & Reliability
43 | - [ ] 📚 Documentation & Usability
44 | - [ ] 🚀 Performance & Scalability
45 | - [ ] 🔧 CI/CD & Automation
46 | - [ ] 🌐 Protocol Compliance
47 | - [ ] 👥 Community & Support
48 |
49 | **Expected Impact**:
50 | <!-- Describe how this contributes to our 100/100 Trust Score goal -->
51 |
52 | ## 🧪 Testing Checklist
53 |
54 | ### Automated Tests
55 | - [ ] Unit tests added/updated
56 | - [ ] Integration tests added/updated
57 | - [ ] Security tests added/updated
58 | - [ ] Performance tests added/updated
59 | - [ ] All existing tests pass
60 | - [ ] Coverage maintained or improved
61 |
62 | ### Manual Testing
63 | - [ ] MCP protocol functionality verified
64 | - [ ] OAuth2 authentication tested (if applicable)
65 | - [ ] Rate limiting verified (if applicable)
66 | - [ ] Error handling tested
67 | - [ ] Edge cases covered
68 | - [ ] Backward compatibility confirmed
69 |
70 | ### Test Environment
71 | **Tested On**:
72 | - [ ] Node.js 16.x
73 | - [ ] Node.js 18.x
74 | - [ ] Node.js 20.x
75 | - [ ] Docker container
76 | - [ ] Multiple operating systems
77 |
78 | **MCP Clients Tested**:
79 | - [ ] Claude Desktop
80 | - [ ] Cursor IDE
81 | - [ ] VS Code with Cline
82 | - [ ] Custom MCP client
83 |
84 | ## 🔒 Security Review
85 |
86 | ### Security Checklist
87 | - [ ] No hardcoded secrets or credentials
88 | - [ ] Input validation implemented
89 | - [ ] Output sanitization applied
90 | - [ ] Authentication/authorization checked
91 | - [ ] SQL injection prevention verified
92 | - [ ] XSS prevention implemented
93 | - [ ] CSRF protection maintained
94 | - [ ] Rate limiting respected
95 | - [ ] Error messages don't leak sensitive info
96 | - [ ] Dependencies updated and secure
97 |
98 | ### Security Impact Assessment
99 | <!-- If this PR has security implications, describe them -->
100 | - **Authentication Changes**:
101 | - **Data Access Changes**:
102 | - **New Attack Vectors**:
103 | - **Mitigation Measures**:
104 |
105 | ## 📊 Performance Impact
106 |
107 | ### Performance Checklist
108 | - [ ] No significant performance regression
109 | - [ ] Memory usage optimized
110 | - [ ] Database queries optimized (if applicable)
111 | - [ ] Network requests minimized
112 | - [ ] Caching implemented where appropriate
113 | - [ ] Async/await used properly
114 |
115 | ### Benchmarks
116 | <!-- If applicable, include performance measurements -->
117 | **Before**:
118 | ```
119 | Metric: [value]
120 | ```
121 |
122 | **After**:
123 | ```
124 | Metric: [value]
125 | ```
126 |
127 | ## 📚 Documentation
128 |
129 | ### Documentation Updates
130 | - [ ] README.md updated
131 | - [ ] API documentation updated
132 | - [ ] Code comments added/updated
133 | - [ ] Examples updated
134 | - [ ] Troubleshooting guide updated
135 | - [ ] CHANGELOG.md updated
136 | - [ ] Migration guide provided (for breaking changes)
137 |
138 | ### Documentation Quality
139 | - [ ] Clear and concise explanations
140 | - [ ] Code examples provided
141 | - [ ] Screenshots/diagrams included (if applicable)
142 | - [ ] Links verified and working
143 |
144 | ## 🔄 Breaking Changes
145 |
146 | ### Breaking Change Assessment
147 | - [ ] This is NOT a breaking change
148 | - [ ] This is a breaking change (explain below)
149 |
150 | <!-- If breaking change, provide details -->
151 | **Breaking Changes**:
152 | - **What breaks**:
153 | - **Migration path**:
154 | - **Deprecation timeline**:
155 |
156 | ## 🎬 Demo/Examples
157 |
158 | ### How to Test This PR
159 | ```bash
160 | # Step-by-step instructions to test this PR
161 | git checkout [branch-name]
162 | npm install
163 | # ... additional setup steps
164 | ```
165 |
166 | ### Usage Examples
167 | ```javascript
168 | // Provide code examples showing the new functionality
169 | ```
170 |
171 | ## 📋 Review Checklist
172 |
173 | ### Code Quality
174 | - [ ] Code follows project style guidelines
175 | - [ ] No console.log or debug statements
176 | - [ ] Error handling is comprehensive
177 | - [ ] Code is well-commented
178 | - [ ] Functions are properly documented
179 | - [ ] Variable names are descriptive
180 | - [ ] Magic numbers avoided
181 |
182 | ### Git History
183 | - [ ] Commit messages are clear and descriptive
184 | - [ ] Commits are logically organized
185 | - [ ] No merge commits (rebased if needed)
186 | - [ ] No sensitive information in commit history
187 |
188 | ## 🤝 Collaboration
189 |
190 | ### Review Requests
191 | **Reviewers Needed**:
192 | - [ ] Security review required
193 | - [ ] Performance review required
194 | - [ ] Documentation review required
195 | - [ ] UI/UX review required
196 |
197 | **Specific Review Areas**:
198 | <!-- Ask reviewers to focus on specific aspects -->
199 | - Please review the OAuth2 implementation for security
200 | - Please check the new API endpoints for usability
201 | - Please verify the documentation is clear
202 |
203 | ### Follow-up Tasks
204 | <!-- List any follow-up work needed -->
205 | - [ ] Create/update related issues
206 | - [ ] Plan future enhancements
207 | - [ ] Update project roadmap
208 | - [ ] Coordinate with documentation team
209 |
210 | ## 🎯 Success Criteria
211 |
212 | ### Definition of Done
213 | - [ ] All acceptance criteria met
214 | - [ ] All tests passing
215 | - [ ] Security review completed
216 | - [ ] Documentation updated
217 | - [ ] Performance impact assessed
218 | - [ ] Backward compatibility verified
219 | - [ ] CI/CD pipeline passing
220 |
221 | ### Trust Score Validation
222 | - [ ] Contributes to security improvements
223 | - [ ] Maintains or improves code quality
224 | - [ ] Includes comprehensive testing
225 | - [ ] Provides clear documentation
226 | - [ ] Follows community best practices
227 |
228 | ## 📸 Screenshots/Media
229 |
230 | <!-- Include screenshots, GIFs, or videos demonstrating the changes -->
231 |
232 | ## 🙏 Acknowledgments
233 |
234 | <!-- Thank contributors, mention inspiration, or credit sources -->
235 |
236 | ---
237 |
238 | ## 📞 Need Help?
239 |
240 | - 💬 **Questions**: Start a [discussion](https://github.com/rashidazarang/airtable-mcp/discussions)
241 | - 🐛 **Issues**: Check our [issue tracker](https://github.com/rashidazarang/airtable-mcp/issues)
242 | - 📚 **Docs**: Read our [documentation](./README.md)
243 | - 🔒 **Security**: Email security@[domain] for private matters
244 |
245 | **🎯 Our Mission**: Building the most trusted and comprehensive MCP server for Airtable with a perfect **100/100 Trust Score**. Thank you for contributing to this goal! 🚀
```
--------------------------------------------------------------------------------
/RELEASE_SUMMARY_v3.2.x.md:
--------------------------------------------------------------------------------
```markdown
1 | # Release Summary: v3.2.1 - v3.2.4
2 | ## Major Security & Architecture Updates
3 |
4 | This document summarizes all releases from v3.2.1 to v3.2.4, representing a comprehensive overhaul of the Airtable MCP server with critical security fixes and architectural improvements.
5 |
6 | ---
7 |
8 | ## 📦 v3.2.4 - Complete XSS Security Fix
9 | **Released:** September 9, 2025
10 | **Type:** 🔒 Security Release
11 | **GitHub Alerts:** #10 & #11 Resolved
12 |
13 | ### What's Fixed
14 | - **XSS Vulnerabilities** in OAuth2 endpoint (`airtable_simple_production.js:708-710`)
15 | - ✅ Unicode escaping for all special characters in JSON
16 | - ✅ Using `textContent` instead of `innerHTML` for dynamic content
17 | - ✅ Multiple layers of character escaping
18 | - ✅ Defense-in-depth XSS prevention
19 |
20 | ### Technical Details
21 | ```javascript
22 | // Before (Vulnerable)
23 | var config = ${JSON.stringify(data)};
24 | <p>Client ID: ${clientId}</p>
25 |
26 | // After (Secure)
27 | var config = ${safeJsonConfig}; // Unicode-escaped
28 | document.getElementById('client-id').textContent = clientId;
29 | ```
30 |
31 | ---
32 |
33 | ## 📦 v3.2.3 - Command Injection Complete Fix
34 | **Released:** September 9, 2025
35 | **Type:** 🔒 Security Release
36 | **GitHub Alert:** #10 (Python) Resolved
37 |
38 | ### What's Fixed
39 | - **Command Injection** in Python test client (`test_client.py`)
40 | - ✅ BASE_ID validation at startup
41 | - ✅ Eliminated string interpolation vulnerabilities
42 | - ✅ Path traversal protection
43 | - ✅ Token format validation
44 | - ✅ Complete input sanitization
45 |
46 | ### Security Improvements
47 | ```python
48 | # Before (Vulnerable)
49 | result = api_call(f"meta/bases/{BASE_ID}/tables")
50 |
51 | # After (Secure)
52 | # BASE_ID validated at startup
53 | if not all(c.isalnum() or c in '-_' for c in BASE_ID):
54 | print(f"Error: Invalid BASE_ID format")
55 | sys.exit(1)
56 | endpoint = "meta/bases/" + BASE_ID + "/tables"
57 | ```
58 |
59 | ---
60 |
61 | ## 📦 v3.2.2 - Initial Security Patches
62 | **Released:** September 9, 2025
63 | **Type:** 🔒 Security Release
64 | **GitHub Alert:** #10 Partial Fix
65 |
66 | ### What's Fixed
67 | - **Initial command injection fixes** in `test_client.py`
68 | - ✅ Added input validation for API endpoints
69 | - ✅ Removed unused subprocess import
70 | - ✅ Basic endpoint sanitization
71 |
72 | ### Note
73 | This was a partial fix. Complete resolution came in v3.2.3.
74 |
75 | ---
76 |
77 | ## 📦 v3.2.1 - TypeScript Architecture Fix & Project Restructure
78 | **Released:** September 9, 2025
79 | **Type:** 🏗️ Major Architecture Update
80 |
81 | ### Critical Fix
82 | - **TypeScript Compilation Issue** completely resolved
83 | - ✅ Fixed `.d.ts` files containing runtime code
84 | - ✅ Proper separation of types and implementation
85 |
86 | ### New Files Created
87 | ```
88 | src/typescript/
89 | ├── errors.ts # Runtime error classes
90 | ├── tools-schemas.ts # Tool schema constants
91 | └── prompt-templates.ts # AI prompt templates
92 | ```
93 |
94 | ### Project Restructure
95 | ```
96 | airtable-mcp/
97 | ├── src/
98 | │ ├── index.js # Main entry point
99 | │ ├── typescript/ # TypeScript implementation
100 | │ ├── javascript/ # JavaScript implementation
101 | │ └── python/ # Python implementation
102 | ├── dist/ # Compiled output
103 | ├── docs/
104 | │ ├── guides/ # User guides
105 | │ └── releases/ # Release notes
106 | ├── tests/ # All test files
107 | └── types/ # TypeScript definitions
108 | ```
109 |
110 | ### What Changed
111 | - ✅ World-class project organization
112 | - ✅ TypeScript now compiles successfully
113 | - ✅ Proper build system with npm scripts
114 | - ✅ ESLint and Prettier configurations
115 | - ✅ Jest testing framework setup
116 | - ✅ CI/CD pipeline structure
117 |
118 | ---
119 |
120 | ## 🎯 Combined Impact
121 |
122 | ### Security Fixes Summary
123 | | Alert | Type | File | Version | Status |
124 | |-------|------|------|---------|---------|
125 | | #10 | XSS | `airtable_simple_production.js:708` | v3.2.4 | ✅ Fixed |
126 | | #11 | XSS | `airtable_simple_production.js:710` | v3.2.4 | ✅ Fixed |
127 | | #10 | Command Injection | `test_client.py` | v3.2.3 | ✅ Fixed |
128 |
129 | ### Architecture Improvements
130 | - ✅ TypeScript compilation working
131 | - ✅ Proper file organization
132 | - ✅ Clean separation of concerns
133 | - ✅ Professional build system
134 | - ✅ Comprehensive testing setup
135 |
136 | ### Backwards Compatibility
137 | ✅ **No breaking changes** across all versions
138 | - All existing functionality preserved
139 | - API endpoints unchanged
140 | - Both JS and TS implementations working
141 |
142 | ---
143 |
144 | ## 📥 Installation
145 |
146 | ### New Installation
147 | ```bash
148 | npm install @rashidazarang/[email protected]
149 | ```
150 |
151 | ### Update from Any Previous Version
152 | ```bash
153 | npm update @rashidazarang/airtable-mcp
154 | ```
155 |
156 | ### Verify Installation
157 | ```bash
158 | npm list @rashidazarang/airtable-mcp
159 | # Should show: @rashidazarang/[email protected]
160 | ```
161 |
162 | ---
163 |
164 | ## 🚀 Quick Start
165 |
166 | ### JavaScript
167 | ```bash
168 | AIRTABLE_TOKEN=your_token AIRTABLE_BASE_ID=your_base \
169 | node node_modules/@rashidazarang/airtable-mcp/src/javascript/airtable_simple_production.js
170 | ```
171 |
172 | ### TypeScript
173 | ```bash
174 | # Build first
175 | npm run build
176 |
177 | # Then run
178 | AIRTABLE_TOKEN=your_token AIRTABLE_BASE_ID=your_base \
179 | node node_modules/@rashidazarang/airtable-mcp/dist/typescript/airtable-mcp-server.js
180 | ```
181 |
182 | ---
183 |
184 | ## 📋 Migration Guide
185 |
186 | ### From v3.0.x or earlier
187 | 1. Update to v3.2.4: `npm update @rashidazarang/airtable-mcp`
188 | 2. If using TypeScript, rebuild: `npm run build`
189 | 3. No code changes required
190 |
191 | ### From v3.1.x
192 | 1. Update to v3.2.4: `npm update @rashidazarang/airtable-mcp`
193 | 2. No changes required - security patches only
194 |
195 | ### From v3.2.1-3.2.3
196 | 1. Update to v3.2.4: `npm update @rashidazarang/airtable-mcp`
197 | 2. Get latest security fixes
198 |
199 | ---
200 |
201 | ## ⚠️ Important Security Notice
202 |
203 | **All users should update to v3.2.4 immediately** to get:
204 | - Complete XSS protection in OAuth2 flows
205 | - Full command injection prevention
206 | - Path traversal protection
207 | - Comprehensive input validation
208 |
209 | ---
210 |
211 | ## 📊 Version Comparison
212 |
213 | | Feature | v3.2.1 | v3.2.2 | v3.2.3 | v3.2.4 |
214 | |---------|--------|--------|--------|--------|
215 | | TypeScript Compilation | ✅ Fixed | ✅ | ✅ | ✅ |
216 | | Project Structure | ✅ New | ✅ | ✅ | ✅ |
217 | | Command Injection Fix | ❌ | ⚠️ Partial | ✅ Complete | ✅ |
218 | | XSS Protection | ❌ | ❌ | ❌ | ✅ Complete |
219 | | Production Ready | ✅ | ✅ | ✅ | ✅ |
220 |
221 | ---
222 |
223 | ## 🙏 Acknowledgments
224 |
225 | - GitHub Security Scanning for identifying vulnerabilities
226 | - Community for patience during rapid security updates
227 | - Contributors to the TypeScript architecture improvements
228 |
229 | ---
230 |
231 | ## 📚 Resources
232 |
233 | - **Repository:** https://github.com/rashidazarang/airtable-mcp
234 | - **Issues:** https://github.com/rashidazarang/airtable-mcp/issues
235 | - **NPM:** https://www.npmjs.com/package/@rashidazarang/airtable-mcp
236 | - **Changelog:** [CHANGELOG.md](./CHANGELOG.md)
237 |
238 | ---
239 |
240 | **Current Version: v3.2.4**
241 | **Status: Fully Secure & Production Ready**
242 | **Last Updated: September 9, 2025**
```
--------------------------------------------------------------------------------
/examples/airtable-crud-example.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Example script demonstrating how to use the Airtable CRUD utilities
3 | */
4 | const dotenv = require('dotenv');
5 | const baseUtils = require('../tools/airtable-base');
6 | const crudUtils = require('../tools/airtable-crud');
7 | const schemaUtils = require('../tools/airtable-schema');
8 |
9 | // Load environment variables
10 | dotenv.config();
11 |
12 | // Configuration
13 | const EXAMPLE_TABLE_NAME = 'Example Tasks';
14 | const EXAMPLE_RECORDS = [
15 | {
16 | Name: 'Complete project documentation',
17 | Description: 'Write comprehensive documentation for the project',
18 | Status: 'Not Started',
19 | Priority: 'High',
20 | DueDate: '2023-12-31'
21 | },
22 | {
23 | Name: 'Fix login bug',
24 | Description: 'Users are experiencing issues with the login process',
25 | Status: 'In Progress',
26 | Priority: 'Critical',
27 | DueDate: '2023-11-15'
28 | },
29 | {
30 | Name: 'Add new feature',
31 | Description: 'Implement the new feature requested by the client',
32 | Status: 'Not Started',
33 | Priority: 'Medium',
34 | DueDate: '2024-01-15'
35 | }
36 | ];
37 |
38 | /**
39 | * Main function to run the example
40 | */
41 | async function runExample() {
42 | console.log('Starting Airtable CRUD Example...\n');
43 |
44 | const baseId = process.env.AIRTABLE_BASE_ID;
45 | if (!baseId) {
46 | console.error('AIRTABLE_BASE_ID not set in .env file');
47 | process.exit(1);
48 | }
49 |
50 | try {
51 | // Step 1: Check if we have access to the base
52 | console.log('Step 1: Checking base access...');
53 | const bases = await baseUtils.listAllBases();
54 | const hasAccess = bases.some(base => base.id === baseId);
55 |
56 | if (!hasAccess) {
57 | throw new Error(`No access to base with ID: ${baseId}`);
58 | }
59 |
60 | console.log(`✅ Access confirmed to base: ${baseId}\n`);
61 |
62 | // Step 2: List existing tables
63 | console.log('Step 2: Listing existing tables...');
64 | const tables = await baseUtils.listTables(baseId);
65 | console.log(`Found ${tables.length} tables in the base:`);
66 | tables.forEach(table => console.log(`- ${table.name}`));
67 | console.log();
68 |
69 | // Step 3: Check if our example table exists
70 | console.log('Step 3: Checking if example table exists...');
71 | let tableExists = await crudUtils.tableExists(baseId, EXAMPLE_TABLE_NAME);
72 |
73 | if (tableExists) {
74 | console.log(`Table "${EXAMPLE_TABLE_NAME}" already exists\n`);
75 | } else {
76 | console.log(`Table "${EXAMPLE_TABLE_NAME}" does not exist, creating it...\n`);
77 |
78 | // Step 4: Create the example table
79 | console.log('Step 4: Creating example table...');
80 | const tableConfig = {
81 | name: EXAMPLE_TABLE_NAME,
82 | description: 'Example table for demonstrating CRUD operations',
83 | fields: [
84 | {
85 | name: 'Name',
86 | type: 'singleLineText',
87 | description: 'Task name'
88 | },
89 | {
90 | name: 'Description',
91 | type: 'multilineText',
92 | description: 'Task description'
93 | },
94 | {
95 | name: 'Status',
96 | type: 'singleSelect',
97 | options: {
98 | choices: [
99 | { name: 'Not Started' },
100 | { name: 'In Progress' },
101 | { name: 'Completed' }
102 | ]
103 | },
104 | description: 'Current status of the task'
105 | },
106 | {
107 | name: 'Priority',
108 | type: 'singleSelect',
109 | options: {
110 | choices: [
111 | { name: 'Low' },
112 | { name: 'Medium' },
113 | { name: 'High' },
114 | { name: 'Critical' }
115 | ]
116 | },
117 | description: 'Task priority'
118 | },
119 | {
120 | name: 'DueDate',
121 | type: 'date',
122 | description: 'When the task is due',
123 | options: {
124 | dateFormat: {
125 | name: 'local'
126 | }
127 | }
128 | }
129 | ]
130 | };
131 |
132 | await schemaUtils.createTable(baseId, tableConfig);
133 | console.log(`✅ Created table: ${EXAMPLE_TABLE_NAME}\n`);
134 | }
135 |
136 | // Step 5: Create records
137 | console.log('Step 5: Creating example records...');
138 | const createdRecords = await crudUtils.createRecords(baseId, EXAMPLE_TABLE_NAME, EXAMPLE_RECORDS);
139 | console.log(`✅ Created ${createdRecords.length} records\n`);
140 |
141 | // Step 6: Read all records
142 | console.log('Step 6: Reading all records...');
143 | const allRecords = await crudUtils.readRecords(baseId, EXAMPLE_TABLE_NAME, 100);
144 | console.log(`✅ Read ${allRecords.length} records`);
145 | console.log('Sample record:');
146 | console.log(JSON.stringify(allRecords[0], null, 2));
147 | console.log();
148 |
149 | // Step 7: Filter records
150 | console.log('Step 7: Filtering records by status...');
151 | const notStartedRecords = await crudUtils.readRecords(
152 | baseId,
153 | EXAMPLE_TABLE_NAME,
154 | 100,
155 | 'Status="Not Started"'
156 | );
157 | console.log(`✅ Found ${notStartedRecords.length} records with Status="Not Started"`);
158 | notStartedRecords.forEach(record => console.log(`- ${record.Name} (Priority: ${record.Priority})`));
159 | console.log();
160 |
161 | // Step 8: Update records
162 | console.log('Step 8: Updating records...');
163 | const recordsToUpdate = notStartedRecords.map(record => ({
164 | id: record.id,
165 | fields: { Status: 'In Progress' }
166 | }));
167 |
168 | const updatedRecords = await crudUtils.updateRecords(baseId, EXAMPLE_TABLE_NAME, recordsToUpdate);
169 | console.log(`✅ Updated ${updatedRecords.length} records to Status="In Progress"\n`);
170 |
171 | // Step 9: Verify updates
172 | console.log('Step 9: Verifying updates...');
173 | const inProgressRecords = await crudUtils.readRecords(
174 | baseId,
175 | EXAMPLE_TABLE_NAME,
176 | 100,
177 | 'Status="In Progress"'
178 | );
179 | console.log(`✅ Found ${inProgressRecords.length} records with Status="In Progress"`);
180 | inProgressRecords.forEach(record => console.log(`- ${record.Name} (Priority: ${record.Priority})`));
181 | console.log();
182 |
183 | // Step 10: Delete records (optional - commented out to preserve data)
184 | console.log('Step 10: Deleting records (optional)...');
185 | console.log('Skipping deletion to preserve example data.');
186 | console.log('To delete records, uncomment the code below:');
187 | console.log('```');
188 | console.log('const recordIdsToDelete = allRecords.map(record => record.id);');
189 | console.log('const deletedRecords = await crudUtils.deleteRecords(baseId, EXAMPLE_TABLE_NAME, recordIdsToDelete);');
190 | console.log('console.log(`✅ Deleted ${deletedRecords.length} records`);');
191 | console.log('```\n');
192 |
193 | console.log('Example completed successfully!');
194 | console.log('You can now view the data in your Airtable base.');
195 |
196 | } catch (error) {
197 | console.error('Error during example:', error.message);
198 | process.exit(1);
199 | }
200 | }
201 |
202 | // Run the example
203 | runExample();
```
--------------------------------------------------------------------------------
/docs/releases/RELEASE_NOTES_v1.5.0.md:
--------------------------------------------------------------------------------
```markdown
1 | # 🚀 Airtable MCP Server v1.5.0 Release Notes
2 |
3 | **Release Date**: August 15, 2025
4 | **Major Update**: Enhanced Schema Management & Advanced Features
5 |
6 | ## 🎯 Overview
7 |
8 | Version 1.5.0 represents a **major expansion** of the Airtable MCP Server, adding comprehensive schema management capabilities inspired by the best features from domdomegg's airtable-mcp-server while maintaining our unique webhook support. This release **doubles** the number of available tools from 12 to **23 tools**.
9 |
10 | ## ✨ New Features
11 |
12 | ### 📊 Schema Discovery Tools (6 New Tools)
13 |
14 | 1. **`list_bases`** - Discover all accessible Airtable bases
15 | - Lists all bases with permissions
16 | - Supports pagination with offset parameter
17 | - Shows base names, IDs, and permission levels
18 |
19 | 2. **`get_base_schema`** - Complete base schema information
20 | - Detailed table structures and relationships
21 | - Field definitions with types and options
22 | - View configurations and metadata
23 |
24 | 3. **`describe_table`** - Enhanced table inspection
25 | - Comprehensive field information including IDs, types, descriptions
26 | - View details and configurations
27 | - Much more detailed than the basic `list_tables`
28 |
29 | 4. **`list_field_types`** - Field type reference
30 | - Complete documentation of all Airtable field types
31 | - Includes basic fields (text, number, date) and advanced fields (formulas, lookups)
32 | - Helpful for understanding what field types are available for creation
33 |
34 | 5. **`get_table_views`** - View management
35 | - Lists all views for a specific table
36 | - Shows view types, IDs, and configurations
37 | - Includes visible field information
38 |
39 | ### 🏗️ Table Management Tools (3 New Tools)
40 |
41 | 6. **`create_table`** - Programmatic table creation
42 | - Create new tables with custom field definitions
43 | - Support for all field types with proper validation
44 | - Optional table descriptions
45 |
46 | 7. **`update_table`** - Table metadata modification
47 | - Update table names and descriptions
48 | - Non-destructive metadata changes
49 |
50 | 8. **`delete_table`** - Table removal (with safety checks)
51 | - Requires explicit confirmation with `confirm=true`
52 | - Permanently removes table and all data
53 | - Safety warnings to prevent accidental deletions
54 |
55 | ### 🔧 Field Management Tools (4 New Tools)
56 |
57 | 9. **`create_field`** - Add fields to existing tables
58 | - Support for all Airtable field types
59 | - Custom field options and descriptions
60 | - Validates field types and configurations
61 |
62 | 10. **`update_field`** - Modify existing field properties
63 | - Update field names, descriptions, and options
64 | - Change field configurations safely
65 |
66 | 11. **`delete_field`** - Remove fields (with safety checks)
67 | - Requires explicit confirmation with `confirm=true`
68 | - Permanently removes field and all data
69 | - Safety warnings to prevent accidental deletions
70 |
71 | ## 🔄 Enhanced Existing Features
72 |
73 | - **Improved error handling** for all metadata operations
74 | - **Better table/field lookup** supporting both names and IDs
75 | - **Enhanced validation** for destructive operations
76 | - **Consistent response formatting** across all tools
77 |
78 | ## 📊 Tool Count Summary
79 |
80 | | Category | v1.4.0 | v1.5.0 | New in v1.5.0 |
81 | |----------|--------|--------|----------------|
82 | | **Data Operations** | 7 | 7 | - |
83 | | **Webhook Management** | 5 | 5 | - |
84 | | **Schema Management** | 0 | 11 | ✅ 11 new tools |
85 | | **Total Tools** | **12** | **23** | **+11 tools** |
86 |
87 | ## 🛠️ Technical Improvements
88 |
89 | ### API Enhancements
90 | - **Metadata API Support**: Full integration with Airtable's metadata API endpoints
91 | - **Enhanced callAirtableAPI Function**: Already supported metadata endpoints
92 | - **Improved Error Handling**: Better error messages for schema operations
93 |
94 | ### Security & Safety
95 | - **Confirmation Required**: Destructive operations require explicit confirmation
96 | - **Validation Checks**: Proper field type and option validation
97 | - **Safety Warnings**: Clear warnings for irreversible operations
98 |
99 | ### Authentication
100 | - **Extended Scope Support**: Now leverages `schema.bases:read` and `schema.bases:write` scopes
101 | - **Backward Compatibility**: All existing functionality remains unchanged
102 |
103 | ## 📚 New Capabilities
104 |
105 | ### For Users
106 | - **Complete Base Discovery**: Find and explore all accessible bases
107 | - **Advanced Schema Inspection**: Understand table and field structures in detail
108 | - **Programmatic Table Creation**: Build tables through natural language
109 | - **Dynamic Field Management**: Add, modify, and remove fields as needed
110 | - **Comprehensive Field Reference**: Quick access to all available field types
111 |
112 | ### For Developers
113 | - **Full CRUD for Schema**: Complete Create, Read, Update, Delete operations for tables and fields
114 | - **Metadata-First Approach**: Rich schema information before data operations
115 | - **Enhanced Automation**: Build complex Airtable structures programmatically
116 |
117 | ## 🚀 Getting Started with v1.5.0
118 |
119 | ### Installation
120 | ```bash
121 | npm install -g @rashidazarang/[email protected]
122 | ```
123 |
124 | ### Required Token Scopes
125 | For full v1.5.0 functionality, ensure your Airtable Personal Access Token includes:
126 | - `data.records:read` - Read records
127 | - `data.records:write` - Create, update, delete records
128 | - `schema.bases:read` - View table schemas (**New requirement**)
129 | - `schema.bases:write` - Create, modify tables and fields (**New requirement**)
130 | - `webhook:manage` - Webhook operations (optional)
131 |
132 | ### Example Usage
133 |
134 | ```javascript
135 | // Discover available bases
136 | "List all my accessible Airtable bases"
137 |
138 | // Explore a base structure
139 | "Show me the complete schema for this base"
140 |
141 | // Create a new table
142 | "Create a new table called 'Projects' with fields: Name (text), Status (single select with options: Active, Completed, On Hold), and Due Date (date)"
143 |
144 | // Add a field to existing table
145 | "Add a 'Priority' field to the Projects table as a single select with options: Low, Medium, High"
146 |
147 | // Get detailed table information
148 | "Describe the Projects table with all field details"
149 | ```
150 |
151 | ## 🔧 Breaking Changes
152 |
153 | **None** - v1.5.0 is fully backward compatible with v1.4.0. All existing tools and functionality remain unchanged.
154 |
155 | ## 🐛 Bug Fixes
156 |
157 | - **Security**: Fixed clear-text logging of sensitive information (GitHub security alerts)
158 | - **API Error Handling**: Improved error messages for invalid table/field references
159 | - **Response Formatting**: Consistent JSON response structure across all tools
160 |
161 | ## 🌟 What's Next
162 |
163 | - Enhanced search capabilities with field-specific filtering
164 | - Batch operations for bulk table/field management
165 | - Advanced view creation and management
166 | - Performance optimizations for large bases
167 |
168 | ## 📈 Performance & Compatibility
169 |
170 | - **Node.js**: Requires Node.js 14+
171 | - **Rate Limits**: Respects Airtable's 5 requests/second limit
172 | - **Memory Usage**: Optimized for efficient schema operations
173 | - **Response Times**: Fast metadata operations with caching
174 |
175 | ## 🤝 Community & Support
176 |
177 | This release incorporates community feedback and feature requests. The v1.5.0 implementation draws inspiration from domdomegg's airtable-mcp-server while maintaining our unique webhook capabilities and enhanced error handling.
178 |
179 | **GitHub**: https://github.com/rashidazarang/airtable-mcp
180 | **NPM**: https://www.npmjs.com/package/@rashidazarang/airtable-mcp
181 | **Issues**: https://github.com/rashidazarang/airtable-mcp/issues
182 |
183 | ---
184 |
185 | 🎉 **Thank you for using Airtable MCP Server!** This release makes it the most comprehensive Airtable integration available for AI assistants, combining powerful schema management with robust webhook support.
```
--------------------------------------------------------------------------------
/tests/test_v1.6.0_comprehensive.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | # COMPREHENSIVE TEST SUITE - Airtable MCP Server v1.6.0
4 | # Testing ALL 33 tools including 10 new v1.6.0 features
5 |
6 | set -e
7 | SERVER_URL="http://localhost:8010/mcp"
8 | PASSED=0
9 | FAILED=0
10 | BATCH_RECORD_IDS=()
11 |
12 | echo "🚀 COMPREHENSIVE TEST SUITE - v1.6.0"
13 | echo "===================================="
14 | echo "Testing ALL 33 tools with real API calls"
15 | echo "New in v1.6.0: Batch operations, attachments, advanced views, base management"
16 | echo ""
17 |
18 | # Function to make MCP calls
19 | call_tool() {
20 | local tool_name="$1"
21 | local params="$2"
22 | curl -s -X POST "$SERVER_URL" \
23 | -H "Content-Type: application/json" \
24 | -d "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\", \"params\": {\"name\": \"$tool_name\", \"arguments\": $params}}"
25 | }
26 |
27 | # Enhanced test function
28 | test_tool() {
29 | local tool_name="$1"
30 | local params="$2"
31 | local description="$3"
32 | local expect_fail="$4"
33 |
34 | echo -n "🔧 $tool_name: $description... "
35 |
36 | if result=$(call_tool "$tool_name" "$params" 2>&1); then
37 | if echo "$result" | jq -e '.result.content[0].text' > /dev/null 2>&1; then
38 | response_text=$(echo "$result" | jq -r '.result.content[0].text')
39 | if [[ "$expect_fail" == "true" ]]; then
40 | if echo "$response_text" | grep -q "error\|Error\|not found\|Unknown field"; then
41 | echo "✅ PASS (Expected failure)"
42 | ((PASSED++))
43 | else
44 | echo "❌ FAIL (Should have failed)"
45 | ((FAILED++))
46 | fi
47 | else
48 | echo "✅ PASS"
49 | ((PASSED++))
50 | # Store batch record IDs for cleanup
51 | if [[ "$tool_name" == "batch_create_records" ]]; then
52 | while IFS= read -r line; do
53 | if [[ $line =~ ID:\ (rec[a-zA-Z0-9]+) ]]; then
54 | BATCH_RECORD_IDS+=(${BASH_REMATCH[1]})
55 | fi
56 | done <<< "$response_text"
57 | fi
58 | fi
59 | else
60 | if echo "$result" | jq -e '.error' > /dev/null 2>&1; then
61 | error_msg=$(echo "$result" | jq -r '.error.message')
62 | if [[ "$expect_fail" == "true" ]]; then
63 | echo "✅ PASS (Expected error: $error_msg)"
64 | ((PASSED++))
65 | else
66 | echo "❌ FAIL (API Error: $error_msg)"
67 | ((FAILED++))
68 | fi
69 | else
70 | echo "❌ FAIL (Invalid response)"
71 | ((FAILED++))
72 | fi
73 | fi
74 | else
75 | echo "❌ FAIL (Request failed)"
76 | ((FAILED++))
77 | fi
78 | }
79 |
80 | echo "📊 PHASE 1: Original Data Operations (7 tools)"
81 | echo "=============================================="
82 |
83 | test_tool "list_tables" "{}" "List all tables"
84 | test_tool "list_records" "{\"table\": \"Test Table CRUD\", \"maxRecords\": 2}" "List limited records"
85 | test_tool "search_records" "{\"table\": \"Test Table CRUD\", \"searchTerm\": \"test\"}" "Search records"
86 |
87 | echo ""
88 | echo "🪝 PHASE 2: Webhook Management (5 tools)"
89 | echo "========================================"
90 |
91 | test_tool "list_webhooks" "{}" "List existing webhooks"
92 |
93 | echo ""
94 | echo "🏗️ PHASE 3: Schema Management (11 tools)"
95 | echo "========================================"
96 |
97 | test_tool "list_bases" "{}" "List accessible bases"
98 | test_tool "get_base_schema" "{}" "Get complete base schema"
99 | test_tool "describe_table" "{\"table\": \"Test Table CRUD\"}" "Describe table details"
100 | test_tool "list_field_types" "{}" "List field types reference"
101 | test_tool "get_table_views" "{\"table\": \"Test Table CRUD\"}" "Get table views"
102 |
103 | echo ""
104 | echo "🚀 PHASE 4: NEW v1.6.0 Batch Operations (4 tools)"
105 | echo "================================================="
106 |
107 | test_tool "batch_create_records" "{\"table\": \"Test Table CRUD\", \"records\": [{\"fields\": {\"Name\": \"Batch Test A\", \"Description\": \"Batch created\", \"Status\": \"Testing\"}}, {\"fields\": {\"Name\": \"Batch Test B\", \"Description\": \"Also batch created\", \"Status\": \"Testing\"}}]}" "Create multiple records at once"
108 |
109 | # Test batch operations with the created records
110 | if [ ${#BATCH_RECORD_IDS[@]} -ge 2 ]; then
111 | test_tool "batch_update_records" "{\"table\": \"Test Table CRUD\", \"records\": [{\"id\": \"${BATCH_RECORD_IDS[0]}\", \"fields\": {\"Status\": \"Updated\"}}, {\"id\": \"${BATCH_RECORD_IDS[1]}\", \"fields\": {\"Status\": \"Updated\"}}]}" "Update multiple records at once"
112 | test_tool "batch_delete_records" "{\"table\": \"Test Table CRUD\", \"recordIds\": [\"${BATCH_RECORD_IDS[0]}\", \"${BATCH_RECORD_IDS[1]}\"]}" "Delete multiple records at once"
113 | else
114 | echo "⚠️ Skipping batch update/delete tests (no record IDs)"
115 | ((FAILED += 2))
116 | fi
117 |
118 | # Test batch limits
119 | test_tool "batch_create_records" "{\"table\": \"Test Table CRUD\", \"records\": []}" "Test with empty records array" "true"
120 |
121 | echo ""
122 | echo "📎 PHASE 5: NEW v1.6.0 Attachment Operations (1 tool)"
123 | echo "===================================================="
124 |
125 | # Test attachment with non-existent field (expected to fail)
126 | test_tool "upload_attachment" "{\"table\": \"Test Table CRUD\", \"recordId\": \"recDummyID\", \"fieldName\": \"NonExistentField\", \"url\": \"https://via.placeholder.com/150.png\"}" "Test attachment to non-existent field" "true"
127 |
128 | echo ""
129 | echo "👁️ PHASE 6: NEW v1.6.0 Advanced Views (2 tools)"
130 | echo "==============================================="
131 |
132 | # Test view operations (some may fail if permissions don't allow)
133 | test_tool "get_view_metadata" "{\"table\": \"Test Table CRUD\", \"viewId\": \"viw123InvalidID\"}" "Test view metadata with invalid ID" "true"
134 |
135 | echo ""
136 | echo "🏢 PHASE 7: NEW v1.6.0 Base Management (3 tools)"
137 | echo "==============================================="
138 |
139 | test_tool "list_collaborators" "{}" "List base collaborators"
140 | test_tool "list_shares" "{}" "List shared views"
141 |
142 | # Test create_base (may fail without workspace permissions)
143 | test_tool "create_base" "{\"name\": \"Test Base\", \"tables\": [{\"name\": \"Test Table\", \"fields\": [{\"name\": \"Name\", \"type\": \"singleLineText\"}]}]}" "Test base creation (may fail due to permissions)" "true"
144 |
145 | echo ""
146 | echo "⚠️ PHASE 8: Error Handling & Edge Cases"
147 | echo "======================================="
148 |
149 | test_tool "batch_create_records" "{\"table\": \"NonExistentTable\", \"records\": [{\"fields\": {\"Name\": \"Test\"}}]}" "Test batch create with non-existent table" "true"
150 | test_tool "get_view_metadata" "{\"table\": \"NonExistentTable\", \"viewId\": \"viwTest\"}" "Test view metadata with non-existent table" "true"
151 |
152 | echo ""
153 | echo "📈 FINAL TEST RESULTS - v1.6.0"
154 | echo "==============================="
155 | echo "✅ Passed: $PASSED"
156 | echo "❌ Failed: $FAILED"
157 | echo "📊 Total Tests: $((PASSED + FAILED))"
158 | echo "📊 Success Rate: $(echo "scale=1; $PASSED * 100 / ($PASSED + $FAILED)" | bc -l)%"
159 |
160 | if [ $FAILED -eq 0 ]; then
161 | echo ""
162 | echo "🎉 🎉 🎉 ALL TESTS PASSED! 🎉 🎉 🎉"
163 | echo ""
164 | echo "✅ v1.6.0 is READY FOR PRODUCTION!"
165 | echo ""
166 | echo "🚀 NEW v1.6.0 ACHIEVEMENTS:"
167 | echo "• 33 total tools (+ 10 from v1.5.0)"
168 | echo "• Batch operations (create/update/delete up to 10 records)"
169 | echo "• Attachment management via URLs"
170 | echo "• Advanced view metadata and creation"
171 | echo "• Base management and collaboration tools"
172 | echo "• Enhanced error handling and validation"
173 | echo ""
174 | echo "📦 Ready for GitHub and NPM release!"
175 | exit 0
176 | else
177 | echo ""
178 | echo "❌ SOME TESTS FAILED"
179 | echo "Review failures above. Some failures may be expected (permissions, non-existent resources)."
180 | echo ""
181 | echo "🎯 v1.6.0 SUMMARY:"
182 | echo "• Core functionality working"
183 | echo "• New batch operations implemented"
184 | echo "• Attachment support added"
185 | echo "• Advanced features may need specific permissions"
186 | exit 1
187 | fi
```
--------------------------------------------------------------------------------
/src/typescript/ai-prompts.d.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * AI-Powered Prompt Templates Type Definitions
3 | * Enterprise-grade TypeScript types for all 10 AI prompt templates
4 | */
5 |
6 | import { PromptSchema, PromptArgument } from './index';
7 |
8 | // ============================================================================
9 | // AI Prompt Template Interfaces
10 | // ============================================================================
11 |
12 | export interface AnalyzeDataPrompt {
13 | table: string;
14 | analysis_type?: 'trends' | 'statistical' | 'patterns' | 'predictive' | 'anomaly_detection' | 'correlation_matrix';
15 | field_focus?: string;
16 | time_dimension?: string;
17 | confidence_level?: 0.90 | 0.95 | 0.99;
18 | }
19 |
20 | export interface CreateReportPrompt {
21 | table: string;
22 | report_type: 'executive_summary' | 'detailed_analysis' | 'dashboard' | 'stakeholder_report';
23 | target_audience: 'executives' | 'managers' | 'analysts' | 'technical_team';
24 | include_recommendations?: boolean;
25 | time_period?: string;
26 | format_preference?: 'narrative' | 'bullet_points' | 'charts' | 'mixed';
27 | }
28 |
29 | export interface DataInsightsPrompt {
30 | table: string;
31 | insight_type: 'business_intelligence' | 'trend_analysis' | 'performance_metrics' | 'opportunity_identification';
32 | focus_areas?: string[];
33 | comparison_period?: string;
34 | include_forecasting?: boolean;
35 | stakeholder_context?: string;
36 | }
37 |
38 | export interface OptimizeWorkflowPrompt {
39 | table: string;
40 | current_process_description: string;
41 | optimization_goals: ('efficiency' | 'accuracy' | 'speed' | 'cost_reduction' | 'compliance')[];
42 | constraints?: string[];
43 | automation_preference?: 'minimal' | 'moderate' | 'aggressive';
44 | change_tolerance?: 'low' | 'medium' | 'high';
45 | }
46 |
47 | export interface SmartSchemaDesignPrompt {
48 | purpose: string;
49 | data_types: string[];
50 | expected_volume: 'small' | 'medium' | 'large' | 'enterprise';
51 | compliance_requirements?: ('GDPR' | 'HIPAA' | 'SOX' | 'PCI_DSS')[];
52 | performance_priorities?: ('query_speed' | 'storage_efficiency' | 'scalability' | 'maintainability')[];
53 | integration_needs?: string[];
54 | user_access_patterns?: string;
55 | }
56 |
57 | export interface DataQualityAuditPrompt {
58 | table: string;
59 | quality_dimensions: ('completeness' | 'accuracy' | 'consistency' | 'timeliness' | 'validity' | 'uniqueness')[];
60 | automated_fixes?: boolean;
61 | severity_threshold?: 'low' | 'medium' | 'high' | 'critical';
62 | compliance_context?: string;
63 | reporting_requirements?: string[];
64 | }
65 |
66 | export interface PredictiveAnalyticsPrompt {
67 | table: string;
68 | target_field: string;
69 | prediction_periods?: number;
70 | algorithm?: 'linear_regression' | 'arima' | 'exponential_smoothing' | 'random_forest' | 'neural_network';
71 | include_confidence_intervals?: boolean;
72 | historical_periods?: number;
73 | external_factors?: string[];
74 | business_context?: string;
75 | }
76 |
77 | export interface NaturalLanguageQueryPrompt {
78 | question: string;
79 | tables?: string[];
80 | response_format?: 'natural_language' | 'structured_data' | 'visualization_ready' | 'action_items';
81 | context_awareness?: boolean;
82 | confidence_threshold?: number;
83 | clarifying_questions?: boolean;
84 | }
85 |
86 | export interface SmartDataTransformationPrompt {
87 | source_table: string;
88 | target_schema?: string;
89 | transformation_goals: ('normalization' | 'aggregation' | 'enrichment' | 'validation' | 'standardization')[];
90 | data_quality_rules?: string[];
91 | preserve_history?: boolean;
92 | validation_strategy?: 'strict' | 'permissive' | 'custom';
93 | error_handling?: 'fail_fast' | 'log_and_continue' | 'manual_review';
94 | }
95 |
96 | export interface AutomationRecommendationsPrompt {
97 | workflow_description: string;
98 | current_pain_points: string[];
99 | automation_scope: 'single_task' | 'workflow_segment' | 'end_to_end' | 'cross_system';
100 | technical_constraints?: string[];
101 | business_impact_priority?: ('cost_savings' | 'time_efficiency' | 'error_reduction' | 'scalability')[];
102 | implementation_timeline?: 'immediate' | 'short_term' | 'medium_term' | 'long_term';
103 | risk_tolerance?: 'conservative' | 'moderate' | 'aggressive';
104 | }
105 |
106 | // ============================================================================
107 | // AI Prompt Response Types
108 | // ============================================================================
109 |
110 | export interface AnalysisResult {
111 | summary: string;
112 | key_findings: string[];
113 | statistical_measures?: {
114 | mean?: number;
115 | median?: number;
116 | std_deviation?: number;
117 | correlation_coefficients?: Record<string, number>;
118 | confidence_intervals?: Array<{ field: string; lower: number; upper: number; confidence: number }>;
119 | };
120 | trends?: Array<{
121 | field: string;
122 | direction: 'increasing' | 'decreasing' | 'stable' | 'volatile';
123 | strength: 'weak' | 'moderate' | 'strong';
124 | significance: number;
125 | }>;
126 | anomalies?: Array<{
127 | record_id: string;
128 | field: string;
129 | expected_value: unknown;
130 | actual_value: unknown;
131 | deviation_score: number;
132 | }>;
133 | recommendations: string[];
134 | next_steps: string[];
135 | }
136 |
137 | export interface ReportResult {
138 | title: string;
139 | executive_summary: string;
140 | detailed_sections: Array<{
141 | heading: string;
142 | content: string;
143 | supporting_data?: unknown[];
144 | visualizations?: Array<{ type: string; data: unknown; description: string }>;
145 | }>;
146 | key_metrics: Record<string, { value: unknown; change: string; significance: string }>;
147 | recommendations: Array<{
148 | priority: 'high' | 'medium' | 'low';
149 | recommendation: string;
150 | expected_impact: string;
151 | implementation_effort: 'low' | 'medium' | 'high';
152 | }>;
153 | appendices?: Array<{ title: string; content: string }>;
154 | }
155 |
156 | export interface WorkflowOptimizationResult {
157 | current_state_analysis: {
158 | efficiency_score: number;
159 | bottlenecks: Array<{ step: string; impact: 'high' | 'medium' | 'low'; description: string }>;
160 | resource_utilization: Record<string, number>;
161 | };
162 | optimization_recommendations: Array<{
163 | category: 'automation' | 'process_redesign' | 'tool_integration' | 'skill_development';
164 | recommendation: string;
165 | expected_benefits: string[];
166 | implementation_complexity: 'simple' | 'moderate' | 'complex';
167 | estimated_roi: string;
168 | timeline: string;
169 | }>;
170 | implementation_roadmap: Array<{
171 | phase: number;
172 | duration: string;
173 | objectives: string[];
174 | deliverables: string[];
175 | success_metrics: string[];
176 | }>;
177 | risk_assessment: Array<{
178 | risk: string;
179 | probability: 'low' | 'medium' | 'high';
180 | impact: 'low' | 'medium' | 'high';
181 | mitigation: string;
182 | }>;
183 | }
184 |
185 | export interface SchemaDesignResult {
186 | recommended_schema: {
187 | tables: Array<{
188 | name: string;
189 | purpose: string;
190 | fields: Array<{
191 | name: string;
192 | type: string;
193 | constraints: string[];
194 | description: string;
195 | }>;
196 | relationships: Array<{
197 | type: 'one_to_one' | 'one_to_many' | 'many_to_many';
198 | target_table: string;
199 | description: string;
200 | }>;
201 | }>;
202 | };
203 | design_principles: string[];
204 | performance_considerations: string[];
205 | scalability_notes: string[];
206 | compliance_alignment: Record<string, string[]>;
207 | migration_strategy?: {
208 | phases: Array<{ phase: number; description: string; estimated_time: string }>;
209 | data_migration_notes: string[];
210 | validation_checkpoints: string[];
211 | };
212 | }
213 |
214 | export interface PredictionResult {
215 | predictions: Array<{
216 | period: string;
217 | predicted_value: number;
218 | confidence_interval?: { lower: number; upper: number };
219 | probability_bands?: Array<{ probability: number; range: [number, number] }>;
220 | }>;
221 | model_performance: {
222 | algorithm_used: string;
223 | accuracy_metrics: Record<string, number>;
224 | feature_importance?: Record<string, number>;
225 | validation_results: Record<string, number>;
226 | };
227 | business_insights: {
228 | trend_direction: 'positive' | 'negative' | 'stable';
229 | seasonality_detected: boolean;
230 | external_factors_impact: string[];
231 | risk_factors: string[];
232 | };
233 | recommendations: Array<{
234 | type: 'operational' | 'strategic' | 'tactical';
235 | recommendation: string;
236 | timing: string;
237 | confidence: number;
238 | }>;
239 | }
240 |
241 | // ============================================================================
242 | // Prompt Template Definitions (Type-Safe)
243 | // ============================================================================
244 |
245 | // AI prompt templates are defined in prompt-templates.ts for runtime use
246 |
247 | // ============================================================================
248 | // Export All AI Prompt Types
249 | // ============================================================================
250 |
251 | export {
252 | AnalyzeDataPrompt,
253 | CreateReportPrompt,
254 | DataInsightsPrompt,
255 | OptimizeWorkflowPrompt,
256 | SmartSchemaDesignPrompt,
257 | DataQualityAuditPrompt,
258 | PredictiveAnalyticsPrompt,
259 | NaturalLanguageQueryPrompt,
260 | SmartDataTransformationPrompt,
261 | AutomationRecommendationsPrompt,
262 |
263 | AnalysisResult,
264 | ReportResult,
265 | WorkflowOptimizationResult,
266 | SchemaDesignResult,
267 | PredictionResult
268 | };
```
--------------------------------------------------------------------------------
/src/typescript/app/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from 'zod';
2 |
3 | /**
4 | * Shared Zod schemas and TypeScript types for Airtable Brain tools.
5 | * Keep these aligned with the JSON Schemas under docs/prd/schemas.
6 | */
7 |
8 | const describeInputBase = z
9 | .object({
10 | scope: z.enum(['base', 'table']),
11 | baseId: z.string().min(1, 'baseId is required'),
12 | table: z
13 | .string()
14 | .min(1, 'table is required when scope=table')
15 | .optional(),
16 | includeFields: z.boolean().optional().default(true),
17 | includeViews: z.boolean().optional().default(false)
18 | })
19 | .strict();
20 |
21 | export const describeInputSchema = describeInputBase.superRefine((data, ctx) => {
22 | if (data.scope === 'table' && !data.table) {
23 | ctx.addIssue({
24 | code: z.ZodIssueCode.custom,
25 | path: ['table'],
26 | message: 'table is required when scope is "table"'
27 | });
28 | }
29 | });
30 |
31 | export const describeInputShape = describeInputBase.shape;
32 |
33 | const describeFieldSchema = z
34 | .object({
35 | id: z.string(),
36 | name: z.string(),
37 | type: z.string(),
38 | options: z.record(z.unknown()).optional()
39 | })
40 | .passthrough();
41 |
42 | const describeTableSchema = z
43 | .object({
44 | id: z.string(),
45 | name: z.string(),
46 | description: z.string().optional(),
47 | primaryFieldId: z.string().optional(),
48 | fields: z.array(describeFieldSchema).optional(),
49 | views: z.array(z.record(z.unknown())).optional()
50 | })
51 | .passthrough();
52 |
53 | export const describeOutputSchema = z
54 | .object({
55 | base: z
56 | .object({
57 | id: z.string(),
58 | name: z.string()
59 | })
60 | .passthrough(),
61 | tables: z.array(describeTableSchema).optional(),
62 | views: z.array(z.record(z.unknown())).optional()
63 | })
64 | .strict();
65 |
66 | const sortDirectionSchema = z.enum(['asc', 'desc']);
67 |
68 | const queryInputBase = z
69 | .object({
70 | baseId: z.string().min(1, 'baseId is required'),
71 | table: z.string().min(1, 'table is required'),
72 | fields: z.array(z.string().min(1)).optional(),
73 | filterByFormula: z.string().optional(),
74 | view: z.string().optional(),
75 | sorts: z
76 | .array(
77 | z
78 | .object({
79 | field: z.string().min(1),
80 | direction: sortDirectionSchema.optional().default('asc')
81 | })
82 | .strict()
83 | )
84 | .optional(),
85 | pageSize: z
86 | .number()
87 | .int()
88 | .min(1)
89 | .max(100)
90 | .optional(),
91 | maxRecords: z
92 | .number()
93 | .int()
94 | .min(1)
95 | .optional(),
96 | offset: z.string().optional(),
97 | returnFieldsByFieldId: z.boolean().optional().default(false)
98 | })
99 | .strict();
100 |
101 | export const queryInputSchema = queryInputBase;
102 | export const queryInputShape = queryInputBase.shape;
103 |
104 | const recordSchema = z
105 | .object({
106 | id: z.string(),
107 | createdTime: z.string().optional(),
108 | fields: z.record(z.unknown())
109 | })
110 | .strict();
111 |
112 | export const queryOutputSchema = z
113 | .object({
114 | records: z.array(recordSchema),
115 | offset: z.string().optional(),
116 | summary: z
117 | .object({
118 | returned: z.number().int().nonnegative(),
119 | hasMore: z.boolean()
120 | })
121 | .strict()
122 | .optional()
123 | })
124 | .strict();
125 |
126 | export const createInputSchema = z
127 | .object({
128 | baseId: z.string().min(1),
129 | table: z.string().min(1),
130 | records: z
131 | .array(
132 | z
133 | .object({
134 | fields: z.record(z.unknown())
135 | })
136 | .strict()
137 | )
138 | .min(1),
139 | typecast: z.boolean().optional().default(false),
140 | idempotencyKey: z.string().min(1).optional(),
141 | dryRun: z.boolean().optional().default(false)
142 | })
143 | .strict();
144 |
145 | const createDiffSchema = z
146 | .object({
147 | added: z.number().int().nonnegative(),
148 | updated: z.number().int().nonnegative(),
149 | unchanged: z.number().int().nonnegative()
150 | })
151 | .strict();
152 |
153 | export const createOutputSchema = z
154 | .object({
155 | diff: createDiffSchema,
156 | records: z.array(recordSchema).optional(),
157 | dryRun: z.boolean(),
158 | warnings: z.array(z.string()).optional()
159 | })
160 | .strict();
161 |
162 | const conflictSchema = z
163 | .object({
164 | id: z.string(),
165 | field: z.string(),
166 | before: z.unknown().optional(),
167 | after: z.unknown().optional(),
168 | current: z.unknown()
169 | })
170 | .strict();
171 |
172 | const mutationDiffSchema = z
173 | .object({
174 | added: z.number().int().nonnegative(),
175 | updated: z.number().int().nonnegative(),
176 | unchanged: z.number().int().nonnegative(),
177 | conflicts: z.number().int().nonnegative()
178 | })
179 | .strict();
180 |
181 | export const updateOutputSchema = z
182 | .object({
183 | diff: mutationDiffSchema,
184 | records: z.array(recordSchema).optional(),
185 | dryRun: z.boolean(),
186 | conflicts: z.array(conflictSchema).optional()
187 | })
188 | .strict();
189 |
190 | export const updateInputSchema = z
191 | .object({
192 | baseId: z.string().min(1),
193 | table: z.string().min(1),
194 | records: z
195 | .array(
196 | z
197 | .object({
198 | id: z.string(),
199 | fields: z.record(z.unknown())
200 | })
201 | .strict()
202 | )
203 | .min(1),
204 | typecast: z.boolean().optional().default(false),
205 | idempotencyKey: z.string().min(1).optional(),
206 | dryRun: z.boolean().optional().default(false),
207 | conflictStrategy: z
208 | .enum(['fail_on_conflict', 'server_merge', 'client_merge'])
209 | .optional()
210 | .default('fail_on_conflict'),
211 | ifUnchangedHash: z.string().optional()
212 | })
213 | .strict();
214 |
215 | export const upsertInputSchema = z
216 | .object({
217 | baseId: z.string().min(1),
218 | table: z.string().min(1),
219 | records: z
220 | .array(
221 | z
222 | .object({
223 | fields: z.record(z.unknown())
224 | })
225 | .strict()
226 | )
227 | .min(1),
228 | performUpsert: z
229 | .object({
230 | fieldsToMergeOn: z.array(z.string().min(1)).min(1)
231 | })
232 | .strict(),
233 | typecast: z.boolean().optional().default(false),
234 | idempotencyKey: z.string().min(1).optional(),
235 | dryRun: z.boolean().optional().default(false),
236 | conflictStrategy: z
237 | .enum(['fail_on_conflict', 'server_merge', 'client_merge'])
238 | .optional()
239 | .default('fail_on_conflict')
240 | })
241 | .strict();
242 |
243 | export const upsertOutputSchema = z
244 | .object({
245 | diff: mutationDiffSchema,
246 | records: z.array(recordSchema).optional(),
247 | dryRun: z.boolean(),
248 | conflicts: z.array(conflictSchema).optional()
249 | })
250 | .strict();
251 |
252 | export const listExceptionsInputSchema = z
253 | .object({
254 | since: z.string().optional(),
255 | severity: z.enum(['info', 'warning', 'error']).optional(),
256 | limit: z.number().int().min(1).max(500).optional().default(100),
257 | cursor: z.string().optional()
258 | })
259 | .strict();
260 |
261 | export const exceptionItemSchema = z
262 | .object({
263 | id: z.string(),
264 | timestamp: z.string(),
265 | severity: z.enum(['info', 'warning', 'error']),
266 | category: z.enum(['rate_limit', 'validation', 'auth', 'conflict', 'schema_drift', 'other']),
267 | summary: z.string(),
268 | details: z.string().optional(),
269 | proposedFix: z.record(z.unknown()).optional()
270 | })
271 | .strict();
272 |
273 | export const listExceptionsOutputSchema = z
274 | .object({
275 | items: z.array(exceptionItemSchema),
276 | cursor: z.string().optional()
277 | })
278 | .strict();
279 |
280 | const allowedOperations = ['describe', 'query', 'create', 'update', 'upsert'] as const;
281 |
282 | export const governanceOutputSchema = z
283 | .object({
284 | allowedBases: z.array(z.string()),
285 | allowedTables: z
286 | .array(
287 | z
288 | .object({
289 | baseId: z.string(),
290 | table: z.string()
291 | })
292 | .strict()
293 | )
294 | .optional()
295 | .default([]),
296 | allowedOperations: z
297 | .array(z.enum(allowedOperations))
298 | .default([...allowedOperations]),
299 | piiFields: z
300 | .array(
301 | z
302 | .object({
303 | baseId: z.string(),
304 | table: z.string(),
305 | field: z.string(),
306 | policy: z.enum(['mask', 'hash', 'drop'])
307 | })
308 | .strict()
309 | )
310 | .optional()
311 | .default([]),
312 | redactionPolicy: z.enum(['mask_all_pii', 'mask_on_inline', 'none']).default('mask_on_inline'),
313 | loggingPolicy: z.enum(['errors_only', 'minimal', 'verbose']).default('minimal'),
314 | retentionDays: z.number().int().min(0).default(7)
315 | })
316 | .strict();
317 |
318 | export type DescribeInput = z.infer<typeof describeInputSchema>;
319 | export type DescribeOutput = z.infer<typeof describeOutputSchema>;
320 |
321 | export type QueryInput = z.infer<typeof queryInputSchema>;
322 | export type QueryOutput = z.infer<typeof queryOutputSchema>;
323 |
324 | export type CreateInput = z.infer<typeof createInputSchema>;
325 | export type CreateOutput = z.infer<typeof createOutputSchema>;
326 | export type UpdateInput = z.infer<typeof updateInputSchema>;
327 | export type UpdateOutput = z.infer<typeof updateOutputSchema>;
328 | export type UpsertInput = z.infer<typeof upsertInputSchema>;
329 | export type UpsertOutput = z.infer<typeof upsertOutputSchema>;
330 |
331 | export type ListExceptionsInput = z.infer<typeof listExceptionsInputSchema>;
332 | export type ExceptionItem = z.infer<typeof exceptionItemSchema>;
333 | export type ListExceptionsOutput = z.infer<typeof listExceptionsOutputSchema>;
334 |
335 | export type GovernanceSnapshot = z.infer<typeof governanceOutputSchema>;
336 |
```
--------------------------------------------------------------------------------
/src/typescript/index.d.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Airtable MCP Server TypeScript Definitions
3 | * Enterprise-grade type safety for AI-powered Airtable operations
4 | */
5 |
6 | // ============================================================================
7 | // MCP Protocol Types (2024-11-05 Specification)
8 | // ============================================================================
9 |
10 | export interface MCPRequest {
11 | jsonrpc: '2.0';
12 | id: string | number;
13 | method: string;
14 | params?: Record<string, unknown>;
15 | }
16 |
17 | export interface MCPResponse {
18 | jsonrpc: '2.0';
19 | id: string | number;
20 | result?: unknown;
21 | error?: MCPError;
22 | }
23 |
24 | export interface MCPError {
25 | code: number;
26 | message: string;
27 | data?: unknown;
28 | }
29 |
30 | export interface MCPServerCapabilities {
31 | tools?: {
32 | listChanged?: boolean;
33 | };
34 | prompts?: {
35 | listChanged?: boolean;
36 | };
37 | resources?: {
38 | subscribe?: boolean;
39 | listChanged?: boolean;
40 | };
41 | roots?: {
42 | listChanged?: boolean;
43 | };
44 | sampling?: Record<string, unknown>;
45 | logging?: Record<string, unknown>;
46 | }
47 |
48 | export interface MCPServerInfo {
49 | name: string;
50 | version: string;
51 | protocolVersion: string;
52 | capabilities: MCPServerCapabilities;
53 | }
54 |
55 | // ============================================================================
56 | // Tool Schema Types
57 | // ============================================================================
58 |
59 | export interface ToolParameter {
60 | type: 'string' | 'number' | 'boolean' | 'object' | 'array';
61 | description: string;
62 | required?: boolean;
63 | default?: unknown;
64 | enum?: string[];
65 | }
66 |
67 | export interface ToolSchema {
68 | name: string;
69 | description: string;
70 | inputSchema: {
71 | type: 'object';
72 | properties: Record<string, ToolParameter>;
73 | required?: string[];
74 | };
75 | }
76 |
77 | // ============================================================================
78 | // AI Prompt Types
79 | // ============================================================================
80 |
81 | export interface PromptArgument {
82 | name: string;
83 | description: string;
84 | required: boolean;
85 | type?: 'string' | 'number' | 'boolean';
86 | enum?: string[];
87 | }
88 |
89 | export interface PromptSchema {
90 | name: string;
91 | description: string;
92 | arguments: PromptArgument[];
93 | }
94 |
95 | export type AnalysisType =
96 | | 'trends'
97 | | 'statistical'
98 | | 'patterns'
99 | | 'predictive'
100 | | 'anomaly_detection'
101 | | 'correlation_matrix';
102 |
103 | export type ConfidenceLevel = 0.90 | 0.95 | 0.99;
104 |
105 | export interface AnalysisOptions {
106 | table: string;
107 | analysis_type?: AnalysisType;
108 | field_focus?: string;
109 | time_dimension?: string;
110 | confidence_level?: ConfidenceLevel;
111 | }
112 |
113 | export interface PredictiveAnalyticsOptions {
114 | table: string;
115 | target_field: string;
116 | prediction_periods?: number;
117 | algorithm?: 'linear_regression' | 'arima' | 'exponential_smoothing' | 'random_forest';
118 | include_confidence_intervals?: boolean;
119 | historical_periods?: number;
120 | }
121 |
122 | export interface StatisticalResult {
123 | confidence_interval: [number, number];
124 | significance_level: number;
125 | p_value?: number;
126 | correlation_coefficient?: number;
127 | }
128 |
129 | // ============================================================================
130 | // Airtable API Types
131 | // ============================================================================
132 |
133 | export interface AirtableFieldType {
134 | type: 'singleLineText' | 'multilineText' | 'richText' | 'email' | 'url' | 'phoneNumber' |
135 | 'number' | 'percent' | 'currency' | 'singleSelect' | 'multipleSelects' |
136 | 'date' | 'dateTime' | 'checkbox' | 'rating' | 'formula' | 'rollup' |
137 | 'count' | 'lookup' | 'createdTime' | 'lastModifiedTime' | 'createdBy' |
138 | 'lastModifiedBy' | 'attachment' | 'barcode' | 'button';
139 | }
140 |
141 | export interface AirtableField {
142 | id: string;
143 | name: string;
144 | type: AirtableFieldType['type'];
145 | options?: Record<string, unknown>;
146 | description?: string;
147 | }
148 |
149 | export interface AirtableTable {
150 | id: string;
151 | name: string;
152 | description?: string;
153 | primaryFieldId: string;
154 | fields: AirtableField[];
155 | views: AirtableView[];
156 | }
157 |
158 | export interface AirtableView {
159 | id: string;
160 | name: string;
161 | type: 'grid' | 'form' | 'calendar' | 'gallery' | 'kanban';
162 | }
163 |
164 | export interface AirtableRecord {
165 | id: string;
166 | fields: Record<string, unknown>;
167 | createdTime: string;
168 | }
169 |
170 | export interface AirtableBase {
171 | id: string;
172 | name: string;
173 | permissionLevel: 'read' | 'comment' | 'edit' | 'create';
174 | tables: AirtableTable[];
175 | }
176 |
177 | export interface AirtableWebhook {
178 | id: string;
179 | macSecretBase64: string;
180 | expirationTime: string;
181 | notificationUrl: string;
182 | isHookEnabled: boolean;
183 | cursorForNextPayload: number;
184 | lastSuccessfulNotificationTime?: string;
185 | }
186 |
187 | export interface WebhookPayload {
188 | timestamp: string;
189 | base: {
190 | id: string;
191 | };
192 | webhook: {
193 | id: string;
194 | };
195 | changedTablesById: Record<string, {
196 | changedRecordsById: Record<string, {
197 | current?: AirtableRecord;
198 | previous?: AirtableRecord;
199 | }>;
200 | }>;
201 | }
202 |
203 | // ============================================================================
204 | // Server Configuration Types
205 | // ============================================================================
206 |
207 | export interface ServerConfig {
208 | PORT: number;
209 | HOST: string;
210 | MAX_REQUESTS_PER_MINUTE: number;
211 | LOG_LEVEL: 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE';
212 | }
213 |
214 | export interface AuthConfig {
215 | AIRTABLE_TOKEN: string;
216 | AIRTABLE_BASE_ID: string;
217 | }
218 |
219 | export interface OAuth2Config {
220 | client_id: string;
221 | redirect_uri: string;
222 | state: string;
223 | code_challenge?: string;
224 | code_challenge_method?: 'S256';
225 | }
226 |
227 | // ============================================================================
228 | // Batch Operation Types
229 | // ============================================================================
230 |
231 | export interface BatchCreateRecord {
232 | fields: Record<string, unknown>;
233 | }
234 |
235 | export interface BatchUpdateRecord {
236 | id: string;
237 | fields: Record<string, unknown>;
238 | }
239 |
240 | export interface BatchDeleteRecord {
241 | id: string;
242 | }
243 |
244 | export interface BatchUpsertRecord {
245 | key_field: string;
246 | key_value: string;
247 | fields: Record<string, unknown>;
248 | }
249 |
250 | // ============================================================================
251 | // Advanced Analytics Types
252 | // ============================================================================
253 |
254 | export interface DataQualityReport {
255 | total_records: number;
256 | missing_values: Record<string, number>;
257 | duplicate_records: string[];
258 | data_types: Record<string, string>;
259 | quality_score: number;
260 | recommendations: string[];
261 | }
262 |
263 | export interface WorkflowOptimization {
264 | current_efficiency: number;
265 | bottlenecks: string[];
266 | automation_opportunities: Array<{
267 | field: string;
268 | suggestion: string;
269 | impact_level: 'high' | 'medium' | 'low';
270 | implementation_complexity: 'simple' | 'moderate' | 'complex';
271 | }>;
272 | estimated_time_savings: string;
273 | }
274 |
275 | export interface SchemaOptimization {
276 | field_recommendations: Array<{
277 | field: string;
278 | current_type: string;
279 | suggested_type: string;
280 | reason: string;
281 | }>;
282 | index_suggestions: string[];
283 | normalization_opportunities: string[];
284 | compliance_notes: string[];
285 | }
286 |
287 | // ============================================================================
288 | // Root Directory Types
289 | // ============================================================================
290 |
291 | export interface RootDirectory {
292 | uri: string;
293 | name: string;
294 | description?: string;
295 | }
296 |
297 | // ============================================================================
298 | // Error Types (defined in errors.ts)
299 | // ============================================================================
300 |
301 | export interface AirtableError extends Error {
302 | code: string;
303 | statusCode?: number;
304 | }
305 |
306 | export interface ValidationError extends Error {
307 | field: string;
308 | }
309 |
310 | // ============================================================================
311 | // Utility Types
312 | // ============================================================================
313 |
314 | export type DeepPartial<T> = {
315 | [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
316 | };
317 |
318 | export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
319 |
320 | export type OptionalFields<T, K extends keyof T> = T & Partial<Pick<T, K>>;
321 |
322 | // ============================================================================
323 | // Main Server Class Type
324 | // ============================================================================
325 |
326 | export interface AirtableMCPServer {
327 | config: ServerConfig;
328 | authConfig: AuthConfig;
329 | tools: ToolSchema[];
330 | prompts: PromptSchema[];
331 |
332 | initialize(capabilities: MCPServerCapabilities): Promise<MCPServerInfo>;
333 | handleToolCall(name: string, params: Record<string, unknown>): Promise<unknown>;
334 | handlePromptGet(name: string, args: Record<string, unknown>): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }>;
335 | start(): Promise<void>;
336 | stop(): Promise<void>;
337 | }
338 |
339 | // ============================================================================
340 | // Export All Types
341 | // ============================================================================
342 |
343 | export * from './tools';
344 | export * from './ai-prompts';
```
--------------------------------------------------------------------------------
/src/typescript/tools.d.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tool Schema Type Definitions
3 | * Comprehensive TypeScript types for all 33 Airtable MCP tools
4 | */
5 |
6 | import { ToolSchema } from './index';
7 |
8 | // ============================================================================
9 | // Data Operation Tool Interfaces
10 | // ============================================================================
11 |
12 | export interface ListTablesInput {
13 | include_schema?: boolean;
14 | }
15 |
16 | export interface ListRecordsInput {
17 | [key: string]: unknown;
18 | table: string;
19 | maxRecords?: number;
20 | view?: string;
21 | filterByFormula?: string;
22 | sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
23 | pageSize?: number;
24 | offset?: string;
25 | }
26 |
27 | export interface GetRecordInput {
28 | table: string;
29 | recordId: string;
30 | }
31 |
32 | export interface CreateRecordInput {
33 | table: string;
34 | fields: Record<string, unknown>;
35 | typecast?: boolean;
36 | }
37 |
38 | export interface UpdateRecordInput {
39 | table: string;
40 | recordId: string;
41 | fields: Record<string, unknown>;
42 | typecast?: boolean;
43 | }
44 |
45 | export interface DeleteRecordInput {
46 | table: string;
47 | recordId: string;
48 | }
49 |
50 | export interface SearchRecordsInput {
51 | table: string;
52 | filterByFormula?: string;
53 | sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
54 | maxRecords?: number;
55 | view?: string;
56 | }
57 |
58 | // ============================================================================
59 | // Webhook Management Tool Interfaces
60 | // ============================================================================
61 |
62 | export interface ListWebhooksInput {
63 | cursor?: string;
64 | }
65 |
66 | export interface CreateWebhookInput {
67 | notificationUrl: string;
68 | specification?: {
69 | options?: {
70 | filters?: {
71 | dataTypes?: ('tableData' | 'tableSchema')[];
72 | recordChangeScope?: string;
73 | watchDataInTableIds?: string[];
74 | };
75 | };
76 | };
77 | }
78 |
79 | export interface DeleteWebhookInput {
80 | webhookId: string;
81 | }
82 |
83 | export interface GetWebhookPayloadsInput {
84 | webhookId: string;
85 | cursor?: string;
86 | limit?: number;
87 | }
88 |
89 | export interface RefreshWebhookInput {
90 | webhookId: string;
91 | }
92 |
93 | // ============================================================================
94 | // Schema Discovery Tool Interfaces
95 | // ============================================================================
96 |
97 | export interface ListBasesInput {
98 | offset?: string;
99 | }
100 |
101 | export interface GetBaseSchemaInput {
102 | baseId?: string;
103 | }
104 |
105 | export interface DescribeTableInput {
106 | table: string;
107 | include_sample_data?: boolean;
108 | }
109 |
110 | export interface ListFieldTypesInput {
111 | category?: 'basic' | 'advanced' | 'computed';
112 | }
113 |
114 | export interface GetTableViewsInput {
115 | table: string;
116 | }
117 |
118 | // ============================================================================
119 | // Table Management Tool Interfaces
120 | // ============================================================================
121 |
122 | export interface CreateTableInput {
123 | name: string;
124 | description?: string;
125 | fields: Array<{
126 | name: string;
127 | type: string;
128 | description?: string;
129 | options?: Record<string, unknown>;
130 | }>;
131 | }
132 |
133 | export interface UpdateTableInput {
134 | table: string;
135 | name?: string;
136 | description?: string;
137 | }
138 |
139 | export interface DeleteTableInput {
140 | table: string;
141 | confirmation?: string;
142 | }
143 |
144 | // ============================================================================
145 | // Field Management Tool Interfaces
146 | // ============================================================================
147 |
148 | export interface CreateFieldInput {
149 | table: string;
150 | name: string;
151 | type: string;
152 | description?: string;
153 | options?: Record<string, unknown>;
154 | }
155 |
156 | export interface UpdateFieldInput {
157 | table: string;
158 | fieldId: string;
159 | name?: string;
160 | description?: string;
161 | options?: Record<string, unknown>;
162 | }
163 |
164 | export interface DeleteFieldInput {
165 | table: string;
166 | fieldId: string;
167 | confirmation?: string;
168 | }
169 |
170 | // ============================================================================
171 | // Batch Operations Tool Interfaces
172 | // ============================================================================
173 |
174 | export interface BatchCreateRecordsInput {
175 | table: string;
176 | records: Array<{
177 | fields: Record<string, unknown>;
178 | }>;
179 | typecast?: boolean;
180 | }
181 |
182 | export interface BatchUpdateRecordsInput {
183 | table: string;
184 | records: Array<{
185 | id: string;
186 | fields: Record<string, unknown>;
187 | }>;
188 | typecast?: boolean;
189 | }
190 |
191 | export interface BatchDeleteRecordsInput {
192 | table: string;
193 | records: Array<{
194 | id: string;
195 | }>;
196 | }
197 |
198 | export interface BatchUpsertRecordsInput {
199 | table: string;
200 | records: Array<{
201 | key_field: string;
202 | key_value: string;
203 | fields: Record<string, unknown>;
204 | }>;
205 | typecast?: boolean;
206 | }
207 |
208 | // ============================================================================
209 | // Attachment Management Tool Interfaces
210 | // ============================================================================
211 |
212 | export interface UploadAttachmentInput {
213 | table: string;
214 | recordId: string;
215 | fieldName: string;
216 | url: string;
217 | filename?: string;
218 | }
219 |
220 | // ============================================================================
221 | // Advanced Views Tool Interfaces
222 | // ============================================================================
223 |
224 | export interface CreateViewInput {
225 | table: string;
226 | name: string;
227 | type: 'grid' | 'form' | 'calendar' | 'gallery' | 'kanban';
228 | visibleFieldIds?: string[];
229 | filterByFormula?: string;
230 | sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
231 | }
232 |
233 | export interface GetViewMetadataInput {
234 | table: string;
235 | viewId: string;
236 | }
237 |
238 | // ============================================================================
239 | // Base Management Tool Interfaces
240 | // ============================================================================
241 |
242 | export interface CreateBaseInput {
243 | name: string;
244 | workspaceId?: string;
245 | tables?: Array<{
246 | name: string;
247 | description?: string;
248 | fields: Array<{
249 | name: string;
250 | type: string;
251 | options?: Record<string, unknown>;
252 | }>;
253 | }>;
254 | }
255 |
256 | export interface ListCollaboratorsInput {
257 | baseId?: string;
258 | }
259 |
260 | export interface ListSharesInput {
261 | baseId?: string;
262 | }
263 |
264 | // ============================================================================
265 | // Tool Response Interfaces
266 | // ============================================================================
267 |
268 | export interface ToolResponse<T = unknown> {
269 | content: Array<{
270 | type: 'text' | 'image' | 'resource';
271 | text?: string;
272 | data?: T;
273 | mimeType?: string;
274 | }>;
275 | isError?: boolean;
276 | }
277 |
278 | export interface PaginatedResponse<T> {
279 | records?: T[];
280 | offset?: string;
281 | }
282 |
283 | export interface TableInfo {
284 | id: string;
285 | name: string;
286 | description?: string;
287 | primaryFieldId: string;
288 | fields: Array<{
289 | id: string;
290 | name: string;
291 | type: string;
292 | options?: Record<string, unknown>;
293 | description?: string;
294 | }>;
295 | views: Array<{
296 | id: string;
297 | name: string;
298 | type: string;
299 | }>;
300 | }
301 |
302 | export interface RecordInfo {
303 | id: string;
304 | fields: Record<string, unknown>;
305 | createdTime: string;
306 | commentCount?: number;
307 | }
308 |
309 | export interface WebhookInfo {
310 | id: string;
311 | macSecretBase64: string;
312 | expirationTime: string;
313 | notificationUrl: string;
314 | isHookEnabled: boolean;
315 | specification: {
316 | options: {
317 | filters: {
318 | dataTypes: string[];
319 | recordChangeScope?: string;
320 | watchDataInTableIds?: string[];
321 | };
322 | };
323 | };
324 | }
325 |
326 | export interface BaseInfo {
327 | id: string;
328 | name: string;
329 | permissionLevel: 'read' | 'comment' | 'edit' | 'create';
330 | }
331 |
332 | export interface FieldTypeInfo {
333 | type: string;
334 | name: string;
335 | description: string;
336 | supportedOptions?: string[];
337 | examples?: Record<string, unknown>[];
338 | }
339 |
340 | export interface ViewInfo {
341 | id: string;
342 | name: string;
343 | type: 'grid' | 'form' | 'calendar' | 'gallery' | 'kanban' | 'timeline' | 'block';
344 | visibleFieldIds?: string[];
345 | filterByFormula?: string;
346 | sort?: Array<{
347 | field: string;
348 | direction: 'asc' | 'desc';
349 | }>;
350 | }
351 |
352 | export interface CollaboratorInfo {
353 | type: 'user' | 'group';
354 | id: string;
355 | email?: string;
356 | name?: string;
357 | permissionLevel: 'read' | 'comment' | 'edit' | 'create';
358 | createdTime: string;
359 | }
360 |
361 | export interface ShareInfo {
362 | id: string;
363 | type: 'view' | 'base';
364 | url: string;
365 | isPasswordRequired: boolean;
366 | allowedActions: string[];
367 | restriction?: {
368 | dateRange?: {
369 | startDate?: string;
370 | endDate?: string;
371 | };
372 | allowCommenting?: boolean;
373 | allowCopyingData?: boolean;
374 | };
375 | }
376 |
377 | // ============================================================================
378 | // Complete Tool Schema Definitions
379 | // ============================================================================
380 |
381 | // Tool schemas are defined in tools-schemas.ts for runtime use
382 |
383 | // ============================================================================
384 | // Export All Tool Types
385 | // ============================================================================
386 |
387 | export {
388 | ListTablesInput,
389 | ListRecordsInput,
390 | GetRecordInput,
391 | CreateRecordInput,
392 | UpdateRecordInput,
393 | DeleteRecordInput,
394 | SearchRecordsInput,
395 |
396 | ListWebhooksInput,
397 | CreateWebhookInput,
398 | DeleteWebhookInput,
399 | GetWebhookPayloadsInput,
400 | RefreshWebhookInput,
401 |
402 | BatchCreateRecordsInput,
403 | BatchUpdateRecordsInput,
404 | BatchDeleteRecordsInput,
405 | BatchUpsertRecordsInput,
406 |
407 | ToolResponse,
408 | PaginatedResponse,
409 | TableInfo,
410 | RecordInfo,
411 | WebhookInfo,
412 | BaseInfo,
413 | FieldTypeInfo,
414 | ViewInfo,
415 | CollaboratorInfo,
416 | ShareInfo
417 | };
```
--------------------------------------------------------------------------------
/docs/releases/RELEASE_NOTES_v1.6.0.md:
--------------------------------------------------------------------------------
```markdown
1 | # 🚀 Airtable MCP Server v1.6.0 Release Notes
2 |
3 | **Release Date**: August 15, 2025
4 | **Major Update**: Batch Operations, Attachment Management & Advanced Features
5 |
6 | ## 🎯 Overview
7 |
8 | Version 1.6.0 represents another **major expansion** of the Airtable MCP Server, adding powerful batch operations, attachment management, and advanced base management capabilities. This release increases the total tools from 23 to **33 tools**, providing the most comprehensive Airtable API coverage available for AI assistants.
9 |
10 | ## ✨ New Features (10 New Tools)
11 |
12 | ### ⚡ Batch Operations (4 New Tools)
13 |
14 | 1. **`batch_create_records`** - Create up to 10 records simultaneously
15 | - Significantly improves performance for bulk data entry
16 | - Maintains atomicity - all records created or none
17 | - Proper error handling for validation failures
18 |
19 | 2. **`batch_update_records`** - Update up to 10 records at once
20 | - Efficient bulk updates with field-level precision
21 | - Maintains data integrity across operations
22 | - Returns detailed success/failure information
23 |
24 | 3. **`batch_delete_records`** - Delete up to 10 records in one operation
25 | - Fast bulk deletion with safety validation
26 | - Atomic operation ensures consistency
27 | - Detailed deletion confirmation
28 |
29 | 4. **`batch_upsert_records`** - Smart update-or-create operations
30 | - Updates existing records or creates new ones based on key fields
31 | - Intelligent matching using specified key fields
32 | - Optimizes data synchronization workflows
33 |
34 | ### 📎 Attachment Management (1 New Tool)
35 |
36 | 5. **`upload_attachment`** - Attach files from URLs to records
37 | - Supports any publicly accessible file URL
38 | - Automatic file type detection and validation
39 | - Optional custom filename specification
40 | - Works with all Airtable-supported file types
41 |
42 | ### 👁️ Advanced View Management (2 New Tools)
43 |
44 | 6. **`create_view`** - Create custom views programmatically
45 | - Support for all view types: grid, form, calendar, gallery, kanban, timeline, gantt
46 | - Custom field visibility and ordering
47 | - Configurable filters and sorts
48 | - Automated view setup for workflows
49 |
50 | 7. **`get_view_metadata`** - Detailed view configuration retrieval
51 | - Complete view settings and configurations
52 | - Filter formulas and sort specifications
53 | - Field visibility and ordering information
54 | - Perfect for view replication and analysis
55 |
56 | ### 🏢 Base Management (3 New Tools)
57 |
58 | 8. **`create_base`** - Create new Airtable bases
59 | - Programmatic base creation with initial table structures
60 | - Support for workspace organization
61 | - Batch table and field creation
62 | - Perfect for template deployment
63 |
64 | 9. **`list_collaborators`** - View base collaboration details
65 | - Complete collaborator list with permission levels
66 | - User type identification (user, group, etc.)
67 | - Permission auditing and management
68 | - Security compliance support
69 |
70 | 10. **`list_shares`** - Manage shared view configurations
71 | - Public share URLs and settings
72 | - Share type and effectiveness status
73 | - View and table relationship mapping
74 | - Privacy and access control management
75 |
76 | ## 🔄 Enhanced Existing Features
77 |
78 | ### Performance Improvements
79 | - **Batch Operations**: Up to 10x faster for bulk operations
80 | - **Error Handling**: More detailed error messages and validation
81 | - **API Efficiency**: Reduced API calls through intelligent batching
82 |
83 | ### Security Enhancements
84 | - **Input Validation**: Enhanced parameter validation for all new tools
85 | - **Permission Checking**: Better handling of permission-restricted operations
86 | - **Safe Defaults**: Conservative defaults for destructive operations
87 |
88 | ### User Experience
89 | - **Better Error Messages**: More descriptive error responses
90 | - **Consistent Interface**: Uniform parameter naming across all tools
91 | - **Enhanced Documentation**: Detailed examples and use cases
92 |
93 | ## 📊 Tool Count Progression
94 |
95 | | Version | Total Tools | New Features |
96 | |---------|-------------|--------------|
97 | | **v1.6.0** | **33** | Batch ops, attachments, advanced views, base mgmt |
98 | | v1.5.0 | 23 | Schema management |
99 | | v1.4.0 | 12 | Webhooks |
100 | | v1.2.4 | 5 | Basic CRUD |
101 |
102 | ## 🛠️ Technical Improvements
103 |
104 | ### API Coverage
105 | - **Complete Airtable API**: Now covers virtually all public Airtable API endpoints
106 | - **Batch Endpoints**: Full support for Airtable's batch operation limits
107 | - **Metadata API**: Complete integration with Airtable's metadata capabilities
108 |
109 | ### Architecture
110 | - **Modular Design**: Clean separation of concerns for each tool category
111 | - **Error Resilience**: Improved error handling and recovery
112 | - **Performance Optimized**: Efficient API usage patterns
113 |
114 | ### Compatibility
115 | - **Backward Compatible**: All v1.5.0 tools unchanged
116 | - **API Limits**: Respects Airtable's rate limits and batch size restrictions
117 | - **Token Scopes**: Graceful handling of insufficient permissions
118 |
119 | ## 📚 New Capabilities
120 |
121 | ### For Users
122 | - **Bulk Data Operations**: Efficiently manage large datasets
123 | - **File Management**: Easy attachment handling through URLs
124 | - **Advanced Workflows**: Create complex multi-step processes
125 | - **Collaboration Insights**: Understand base sharing and permissions
126 | - **Template Creation**: Programmatically create standardized bases
127 |
128 | ### For Developers
129 | - **High-Performance Bulk Ops**: Optimize data synchronization
130 | - **Complete Base Lifecycle**: Full cradle-to-grave base management
131 | - **Advanced View Control**: Programmatic UI customization
132 | - **Security Auditing**: Comprehensive permission monitoring
133 |
134 | ## 🚀 Getting Started with v1.6.0
135 |
136 | ### Installation
137 | ```bash
138 | npm install -g @rashidazarang/[email protected]
139 | ```
140 |
141 | ### New Usage Examples
142 |
143 | #### Batch Operations
144 | ```javascript
145 | // Create multiple records efficiently
146 | "Create 5 new project records with these details: [project data]"
147 |
148 | // Update multiple records at once
149 | "Update all records where status is 'pending' to 'in progress'"
150 |
151 | // Delete multiple records
152 | "Delete these 3 completed tasks: rec123, rec456, rec789"
153 | ```
154 |
155 | #### Attachment Management
156 | ```javascript
157 | // Attach files to records
158 | "Attach this image https://example.com/image.jpg to the product photo field in record rec123"
159 |
160 | // Batch create with attachments
161 | "Create a new product record and attach the logo from this URL"
162 | ```
163 |
164 | #### Advanced Views
165 | ```javascript
166 | // Create custom views
167 | "Create a calendar view for the Events table showing only future events"
168 |
169 | // Analyze view configurations
170 | "Show me the detailed configuration of the 'Active Projects' view"
171 | ```
172 |
173 | #### Base Management
174 | ```javascript
175 | // Create new bases
176 | "Create a new base called 'Project Tracker' with tables for Projects, Tasks, and Team Members"
177 |
178 | // Collaboration insights
179 | "Who has access to this base and what are their permission levels?"
180 | ```
181 |
182 | ## 🔧 Breaking Changes
183 |
184 | **None** - v1.6.0 maintains full backward compatibility with all previous versions.
185 |
186 | ## 🐛 Bug Fixes
187 |
188 | - **Batch Size Validation**: Proper enforcement of 10-record limits
189 | - **Error Message Clarity**: More descriptive API error responses
190 | - **Permission Handling**: Better graceful degradation for insufficient permissions
191 | - **URL Validation**: Enhanced validation for attachment URLs
192 |
193 | ## ⚡ Performance Improvements
194 |
195 | - **Batch Operations**: Up to 10x performance improvement for bulk operations
196 | - **API Efficiency**: Reduced API calls through intelligent batching
197 | - **Memory Usage**: Optimized memory usage for large operations
198 | - **Response Processing**: Faster JSON parsing and response handling
199 |
200 | ## 🌟 What's Next
201 |
202 | Based on user feedback and Airtable API evolution:
203 | - Enhanced search and filtering capabilities
204 | - Advanced automation triggers
205 | - Real-time collaboration features
206 | - Performance analytics and monitoring
207 | - Enterprise-grade security features
208 |
209 | ## 📈 Compatibility & Requirements
210 |
211 | - **Node.js**: Requires Node.js 14+
212 | - **Airtable API**: Compatible with latest Airtable API version
213 | - **Rate Limits**: Respects Airtable's 5 requests/second limit
214 | - **Token Scopes**: Requires appropriate scopes for advanced features
215 |
216 | ### Required Scopes for Full Functionality
217 | - `data.records:read` - Read records
218 | - `data.records:write` - Create, update, delete records
219 | - `schema.bases:read` - View schemas and metadata
220 | - `schema.bases:write` - Create/modify tables, fields, views, bases
221 | - `webhook:manage` - Webhook operations (optional)
222 |
223 | ## 📊 Testing & Quality
224 |
225 | - **100% Test Coverage**: All 33 tools tested with real API calls
226 | - **Edge Case Handling**: Comprehensive error condition testing
227 | - **Performance Testing**: Batch operation efficiency verification
228 | - **Security Testing**: Permission and validation testing
229 |
230 | ## 🤝 Community Impact
231 |
232 | v1.6.0 establishes this MCP server as the definitive Airtable integration for AI assistants, providing:
233 |
234 | - **Most Comprehensive Coverage**: 33 tools covering entire Airtable API
235 | - **Best Performance**: Intelligent batching and optimization
236 | - **Enterprise Ready**: Advanced collaboration and security features
237 | - **Developer Friendly**: Clean, consistent, well-documented interface
238 |
239 | ## 🔗 Resources
240 |
241 | **GitHub**: https://github.com/rashidazarang/airtable-mcp
242 | **NPM**: https://www.npmjs.com/package/@rashidazarang/airtable-mcp
243 | **Issues**: https://github.com/rashidazarang/airtable-mcp/issues
244 | **Documentation**: https://github.com/rashidazarang/airtable-mcp#readme
245 |
246 | ---
247 |
248 | 🎉 **Thank you for using Airtable MCP Server v1.6.0!** This release represents the culmination of comprehensive Airtable API integration, providing AI assistants with unprecedented access to Airtable's full feature set through natural language interactions.
```