This is page 3 of 4. Use http://codebase.md/rashidazarang/airtable-mcp?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
--------------------------------------------------------------------------------
/types/typescript/app/types.d.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
export declare const describeInputSchema: z.ZodEffects<z.ZodObject<{
scope: z.ZodEnum<["base", "table"]>;
baseId: z.ZodString;
table: z.ZodOptional<z.ZodString>;
includeFields: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
includeViews: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
}, "strict", z.ZodTypeAny, {
scope: "base" | "table";
baseId: string;
includeFields: boolean;
includeViews: boolean;
table?: string | undefined;
}, {
scope: "base" | "table";
baseId: string;
table?: string | undefined;
includeFields?: boolean | undefined;
includeViews?: boolean | undefined;
}>, {
scope: "base" | "table";
baseId: string;
includeFields: boolean;
includeViews: boolean;
table?: string | undefined;
}, {
scope: "base" | "table";
baseId: string;
table?: string | undefined;
includeFields?: boolean | undefined;
includeViews?: boolean | undefined;
}>;
export declare const describeInputShape: {
scope: z.ZodEnum<["base", "table"]>;
baseId: z.ZodString;
table: z.ZodOptional<z.ZodString>;
includeFields: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
includeViews: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
};
export declare const describeOutputSchema: z.ZodObject<{
base: z.ZodObject<{
id: z.ZodString;
name: z.ZodString;
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
id: z.ZodString;
name: z.ZodString;
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
id: z.ZodString;
name: z.ZodString;
}, z.ZodTypeAny, "passthrough">>;
tables: z.ZodOptional<z.ZodArray<z.ZodObject<{
id: z.ZodString;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
primaryFieldId: z.ZodOptional<z.ZodString>;
fields: z.ZodOptional<z.ZodArray<z.ZodObject<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, z.ZodTypeAny, "passthrough">>, "many">>;
views: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">>;
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
id: z.ZodString;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
primaryFieldId: z.ZodOptional<z.ZodString>;
fields: z.ZodOptional<z.ZodArray<z.ZodObject<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, z.ZodTypeAny, "passthrough">>, "many">>;
views: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">>;
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
id: z.ZodString;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
primaryFieldId: z.ZodOptional<z.ZodString>;
fields: z.ZodOptional<z.ZodArray<z.ZodObject<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, z.ZodTypeAny, "passthrough">>, "many">>;
views: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">>;
}, z.ZodTypeAny, "passthrough">>, "many">>;
views: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">>;
}, "strict", z.ZodTypeAny, {
base: {
id: string;
name: string;
} & {
[k: string]: unknown;
};
views?: Record<string, unknown>[] | undefined;
tables?: z.objectOutputType<{
id: z.ZodString;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
primaryFieldId: z.ZodOptional<z.ZodString>;
fields: z.ZodOptional<z.ZodArray<z.ZodObject<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, z.ZodTypeAny, "passthrough">>, "many">>;
views: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">>;
}, z.ZodTypeAny, "passthrough">[] | undefined;
}, {
base: {
id: string;
name: string;
} & {
[k: string]: unknown;
};
views?: Record<string, unknown>[] | undefined;
tables?: z.objectInputType<{
id: z.ZodString;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
primaryFieldId: z.ZodOptional<z.ZodString>;
fields: z.ZodOptional<z.ZodArray<z.ZodObject<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, z.ZodTypeAny, "passthrough">>, "many">>;
views: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">>;
}, z.ZodTypeAny, "passthrough">[] | undefined;
}>;
export declare const queryInputSchema: z.ZodObject<{
baseId: z.ZodString;
table: z.ZodString;
fields: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
filterByFormula: z.ZodOptional<z.ZodString>;
view: z.ZodOptional<z.ZodString>;
sorts: z.ZodOptional<z.ZodArray<z.ZodObject<{
field: z.ZodString;
direction: z.ZodDefault<z.ZodOptional<z.ZodEnum<["asc", "desc"]>>>;
}, "strict", z.ZodTypeAny, {
field: string;
direction: "asc" | "desc";
}, {
field: string;
direction?: "asc" | "desc" | undefined;
}>, "many">>;
pageSize: z.ZodOptional<z.ZodNumber>;
maxRecords: z.ZodOptional<z.ZodNumber>;
offset: z.ZodOptional<z.ZodString>;
returnFieldsByFieldId: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
}, "strict", z.ZodTypeAny, {
table: string;
baseId: string;
returnFieldsByFieldId: boolean;
fields?: string[] | undefined;
filterByFormula?: string | undefined;
view?: string | undefined;
sorts?: {
field: string;
direction: "asc" | "desc";
}[] | undefined;
pageSize?: number | undefined;
maxRecords?: number | undefined;
offset?: string | undefined;
}, {
table: string;
baseId: string;
fields?: string[] | undefined;
filterByFormula?: string | undefined;
view?: string | undefined;
sorts?: {
field: string;
direction?: "asc" | "desc" | undefined;
}[] | undefined;
pageSize?: number | undefined;
maxRecords?: number | undefined;
offset?: string | undefined;
returnFieldsByFieldId?: boolean | undefined;
}>;
export declare const queryInputShape: {
baseId: z.ZodString;
table: z.ZodString;
fields: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
filterByFormula: z.ZodOptional<z.ZodString>;
view: z.ZodOptional<z.ZodString>;
sorts: z.ZodOptional<z.ZodArray<z.ZodObject<{
field: z.ZodString;
direction: z.ZodDefault<z.ZodOptional<z.ZodEnum<["asc", "desc"]>>>;
}, "strict", z.ZodTypeAny, {
field: string;
direction: "asc" | "desc";
}, {
field: string;
direction?: "asc" | "desc" | undefined;
}>, "many">>;
pageSize: z.ZodOptional<z.ZodNumber>;
maxRecords: z.ZodOptional<z.ZodNumber>;
offset: z.ZodOptional<z.ZodString>;
returnFieldsByFieldId: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
};
export declare const queryOutputSchema: z.ZodObject<{
records: z.ZodArray<z.ZodObject<{
id: z.ZodString;
createdTime: z.ZodOptional<z.ZodString>;
fields: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, "strict", z.ZodTypeAny, {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}, {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}>, "many">;
offset: z.ZodOptional<z.ZodString>;
summary: z.ZodOptional<z.ZodObject<{
returned: z.ZodNumber;
hasMore: z.ZodBoolean;
}, "strict", z.ZodTypeAny, {
returned: number;
hasMore: boolean;
}, {
returned: number;
hasMore: boolean;
}>>;
}, "strict", z.ZodTypeAny, {
records: {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}[];
offset?: string | undefined;
summary?: {
returned: number;
hasMore: boolean;
} | undefined;
}, {
records: {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}[];
offset?: string | undefined;
summary?: {
returned: number;
hasMore: boolean;
} | undefined;
}>;
export declare const createInputSchema: z.ZodObject<{
baseId: z.ZodString;
table: z.ZodString;
records: z.ZodArray<z.ZodObject<{
fields: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, "strict", z.ZodTypeAny, {
fields: Record<string, unknown>;
}, {
fields: Record<string, unknown>;
}>, "many">;
typecast: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
idempotencyKey: z.ZodOptional<z.ZodString>;
dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
}, "strict", z.ZodTypeAny, {
table: string;
baseId: string;
records: {
fields: Record<string, unknown>;
}[];
typecast: boolean;
dryRun: boolean;
idempotencyKey?: string | undefined;
}, {
table: string;
baseId: string;
records: {
fields: Record<string, unknown>;
}[];
typecast?: boolean | undefined;
idempotencyKey?: string | undefined;
dryRun?: boolean | undefined;
}>;
export declare const createOutputSchema: z.ZodObject<{
diff: z.ZodObject<{
added: z.ZodNumber;
updated: z.ZodNumber;
unchanged: z.ZodNumber;
}, "strict", z.ZodTypeAny, {
added: number;
updated: number;
unchanged: number;
}, {
added: number;
updated: number;
unchanged: number;
}>;
records: z.ZodOptional<z.ZodArray<z.ZodObject<{
id: z.ZodString;
createdTime: z.ZodOptional<z.ZodString>;
fields: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, "strict", z.ZodTypeAny, {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}, {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}>, "many">>;
dryRun: z.ZodBoolean;
warnings: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
}, "strict", z.ZodTypeAny, {
dryRun: boolean;
diff: {
added: number;
updated: number;
unchanged: number;
};
records?: {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}[] | undefined;
warnings?: string[] | undefined;
}, {
dryRun: boolean;
diff: {
added: number;
updated: number;
unchanged: number;
};
records?: {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}[] | undefined;
warnings?: string[] | undefined;
}>;
export declare const updateOutputSchema: z.ZodObject<{
diff: z.ZodObject<{
added: z.ZodNumber;
updated: z.ZodNumber;
unchanged: z.ZodNumber;
conflicts: z.ZodNumber;
}, "strict", z.ZodTypeAny, {
added: number;
updated: number;
unchanged: number;
conflicts: number;
}, {
added: number;
updated: number;
unchanged: number;
conflicts: number;
}>;
records: z.ZodOptional<z.ZodArray<z.ZodObject<{
id: z.ZodString;
createdTime: z.ZodOptional<z.ZodString>;
fields: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, "strict", z.ZodTypeAny, {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}, {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}>, "many">>;
dryRun: z.ZodBoolean;
conflicts: z.ZodOptional<z.ZodArray<z.ZodObject<{
id: z.ZodString;
field: z.ZodString;
before: z.ZodOptional<z.ZodUnknown>;
after: z.ZodOptional<z.ZodUnknown>;
current: z.ZodUnknown;
}, "strict", z.ZodTypeAny, {
id: string;
field: string;
before?: unknown;
after?: unknown;
current?: unknown;
}, {
id: string;
field: string;
before?: unknown;
after?: unknown;
current?: unknown;
}>, "many">>;
}, "strict", z.ZodTypeAny, {
dryRun: boolean;
diff: {
added: number;
updated: number;
unchanged: number;
conflicts: number;
};
records?: {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}[] | undefined;
conflicts?: {
id: string;
field: string;
before?: unknown;
after?: unknown;
current?: unknown;
}[] | undefined;
}, {
dryRun: boolean;
diff: {
added: number;
updated: number;
unchanged: number;
conflicts: number;
};
records?: {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}[] | undefined;
conflicts?: {
id: string;
field: string;
before?: unknown;
after?: unknown;
current?: unknown;
}[] | undefined;
}>;
export declare const updateInputSchema: z.ZodObject<{
baseId: z.ZodString;
table: z.ZodString;
records: z.ZodArray<z.ZodObject<{
id: z.ZodString;
fields: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, "strict", z.ZodTypeAny, {
id: string;
fields: Record<string, unknown>;
}, {
id: string;
fields: Record<string, unknown>;
}>, "many">;
typecast: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
idempotencyKey: z.ZodOptional<z.ZodString>;
dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
conflictStrategy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["fail_on_conflict", "server_merge", "client_merge"]>>>;
ifUnchangedHash: z.ZodOptional<z.ZodString>;
}, "strict", z.ZodTypeAny, {
table: string;
baseId: string;
records: {
id: string;
fields: Record<string, unknown>;
}[];
typecast: boolean;
dryRun: boolean;
conflictStrategy: "fail_on_conflict" | "server_merge" | "client_merge";
idempotencyKey?: string | undefined;
ifUnchangedHash?: string | undefined;
}, {
table: string;
baseId: string;
records: {
id: string;
fields: Record<string, unknown>;
}[];
typecast?: boolean | undefined;
idempotencyKey?: string | undefined;
dryRun?: boolean | undefined;
conflictStrategy?: "fail_on_conflict" | "server_merge" | "client_merge" | undefined;
ifUnchangedHash?: string | undefined;
}>;
export declare const upsertInputSchema: z.ZodObject<{
baseId: z.ZodString;
table: z.ZodString;
records: z.ZodArray<z.ZodObject<{
fields: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, "strict", z.ZodTypeAny, {
fields: Record<string, unknown>;
}, {
fields: Record<string, unknown>;
}>, "many">;
performUpsert: z.ZodObject<{
fieldsToMergeOn: z.ZodArray<z.ZodString, "many">;
}, "strict", z.ZodTypeAny, {
fieldsToMergeOn: string[];
}, {
fieldsToMergeOn: string[];
}>;
typecast: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
idempotencyKey: z.ZodOptional<z.ZodString>;
dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
conflictStrategy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["fail_on_conflict", "server_merge", "client_merge"]>>>;
}, "strict", z.ZodTypeAny, {
table: string;
baseId: string;
records: {
fields: Record<string, unknown>;
}[];
typecast: boolean;
dryRun: boolean;
conflictStrategy: "fail_on_conflict" | "server_merge" | "client_merge";
performUpsert: {
fieldsToMergeOn: string[];
};
idempotencyKey?: string | undefined;
}, {
table: string;
baseId: string;
records: {
fields: Record<string, unknown>;
}[];
performUpsert: {
fieldsToMergeOn: string[];
};
typecast?: boolean | undefined;
idempotencyKey?: string | undefined;
dryRun?: boolean | undefined;
conflictStrategy?: "fail_on_conflict" | "server_merge" | "client_merge" | undefined;
}>;
export declare const upsertOutputSchema: z.ZodObject<{
diff: z.ZodObject<{
added: z.ZodNumber;
updated: z.ZodNumber;
unchanged: z.ZodNumber;
conflicts: z.ZodNumber;
}, "strict", z.ZodTypeAny, {
added: number;
updated: number;
unchanged: number;
conflicts: number;
}, {
added: number;
updated: number;
unchanged: number;
conflicts: number;
}>;
records: z.ZodOptional<z.ZodArray<z.ZodObject<{
id: z.ZodString;
createdTime: z.ZodOptional<z.ZodString>;
fields: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, "strict", z.ZodTypeAny, {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}, {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}>, "many">>;
dryRun: z.ZodBoolean;
conflicts: z.ZodOptional<z.ZodArray<z.ZodObject<{
id: z.ZodString;
field: z.ZodString;
before: z.ZodOptional<z.ZodUnknown>;
after: z.ZodOptional<z.ZodUnknown>;
current: z.ZodUnknown;
}, "strict", z.ZodTypeAny, {
id: string;
field: string;
before?: unknown;
after?: unknown;
current?: unknown;
}, {
id: string;
field: string;
before?: unknown;
after?: unknown;
current?: unknown;
}>, "many">>;
}, "strict", z.ZodTypeAny, {
dryRun: boolean;
diff: {
added: number;
updated: number;
unchanged: number;
conflicts: number;
};
records?: {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}[] | undefined;
conflicts?: {
id: string;
field: string;
before?: unknown;
after?: unknown;
current?: unknown;
}[] | undefined;
}, {
dryRun: boolean;
diff: {
added: number;
updated: number;
unchanged: number;
conflicts: number;
};
records?: {
id: string;
fields: Record<string, unknown>;
createdTime?: string | undefined;
}[] | undefined;
conflicts?: {
id: string;
field: string;
before?: unknown;
after?: unknown;
current?: unknown;
}[] | undefined;
}>;
export declare const listExceptionsInputSchema: z.ZodObject<{
since: z.ZodOptional<z.ZodString>;
severity: z.ZodOptional<z.ZodEnum<["info", "warning", "error"]>>;
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
cursor: z.ZodOptional<z.ZodString>;
}, "strict", z.ZodTypeAny, {
limit: number;
since?: string | undefined;
severity?: "info" | "warning" | "error" | undefined;
cursor?: string | undefined;
}, {
since?: string | undefined;
severity?: "info" | "warning" | "error" | undefined;
limit?: number | undefined;
cursor?: string | undefined;
}>;
export declare const exceptionItemSchema: z.ZodObject<{
id: z.ZodString;
timestamp: z.ZodString;
severity: z.ZodEnum<["info", "warning", "error"]>;
category: z.ZodEnum<["rate_limit", "validation", "auth", "conflict", "schema_drift", "other"]>;
summary: z.ZodString;
details: z.ZodOptional<z.ZodString>;
proposedFix: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, "strict", z.ZodTypeAny, {
id: string;
summary: string;
severity: "info" | "warning" | "error";
timestamp: string;
category: "validation" | "rate_limit" | "auth" | "conflict" | "schema_drift" | "other";
details?: string | undefined;
proposedFix?: Record<string, unknown> | undefined;
}, {
id: string;
summary: string;
severity: "info" | "warning" | "error";
timestamp: string;
category: "validation" | "rate_limit" | "auth" | "conflict" | "schema_drift" | "other";
details?: string | undefined;
proposedFix?: Record<string, unknown> | undefined;
}>;
export declare const listExceptionsOutputSchema: z.ZodObject<{
items: z.ZodArray<z.ZodObject<{
id: z.ZodString;
timestamp: z.ZodString;
severity: z.ZodEnum<["info", "warning", "error"]>;
category: z.ZodEnum<["rate_limit", "validation", "auth", "conflict", "schema_drift", "other"]>;
summary: z.ZodString;
details: z.ZodOptional<z.ZodString>;
proposedFix: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, "strict", z.ZodTypeAny, {
id: string;
summary: string;
severity: "info" | "warning" | "error";
timestamp: string;
category: "validation" | "rate_limit" | "auth" | "conflict" | "schema_drift" | "other";
details?: string | undefined;
proposedFix?: Record<string, unknown> | undefined;
}, {
id: string;
summary: string;
severity: "info" | "warning" | "error";
timestamp: string;
category: "validation" | "rate_limit" | "auth" | "conflict" | "schema_drift" | "other";
details?: string | undefined;
proposedFix?: Record<string, unknown> | undefined;
}>, "many">;
cursor: z.ZodOptional<z.ZodString>;
}, "strict", z.ZodTypeAny, {
items: {
id: string;
summary: string;
severity: "info" | "warning" | "error";
timestamp: string;
category: "validation" | "rate_limit" | "auth" | "conflict" | "schema_drift" | "other";
details?: string | undefined;
proposedFix?: Record<string, unknown> | undefined;
}[];
cursor?: string | undefined;
}, {
items: {
id: string;
summary: string;
severity: "info" | "warning" | "error";
timestamp: string;
category: "validation" | "rate_limit" | "auth" | "conflict" | "schema_drift" | "other";
details?: string | undefined;
proposedFix?: Record<string, unknown> | undefined;
}[];
cursor?: string | undefined;
}>;
export declare const governanceOutputSchema: z.ZodObject<{
allowedBases: z.ZodArray<z.ZodString, "many">;
allowedTables: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
baseId: z.ZodString;
table: z.ZodString;
}, "strict", z.ZodTypeAny, {
table: string;
baseId: string;
}, {
table: string;
baseId: string;
}>, "many">>>;
allowedOperations: z.ZodDefault<z.ZodArray<z.ZodEnum<["describe", "query", "create", "update", "upsert"]>, "many">>;
piiFields: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
baseId: z.ZodString;
table: z.ZodString;
field: z.ZodString;
policy: z.ZodEnum<["mask", "hash", "drop"]>;
}, "strict", z.ZodTypeAny, {
table: string;
baseId: string;
field: string;
policy: "mask" | "hash" | "drop";
}, {
table: string;
baseId: string;
field: string;
policy: "mask" | "hash" | "drop";
}>, "many">>>;
redactionPolicy: z.ZodDefault<z.ZodEnum<["mask_all_pii", "mask_on_inline", "none"]>>;
loggingPolicy: z.ZodDefault<z.ZodEnum<["errors_only", "minimal", "verbose"]>>;
retentionDays: z.ZodDefault<z.ZodNumber>;
}, "strict", z.ZodTypeAny, {
allowedBases: string[];
allowedTables: {
table: string;
baseId: string;
}[];
allowedOperations: ("describe" | "query" | "create" | "update" | "upsert")[];
piiFields: {
table: string;
baseId: string;
field: string;
policy: "mask" | "hash" | "drop";
}[];
redactionPolicy: "mask_all_pii" | "mask_on_inline" | "none";
loggingPolicy: "errors_only" | "minimal" | "verbose";
retentionDays: number;
}, {
allowedBases: string[];
allowedTables?: {
table: string;
baseId: string;
}[] | undefined;
allowedOperations?: ("describe" | "query" | "create" | "update" | "upsert")[] | undefined;
piiFields?: {
table: string;
baseId: string;
field: string;
policy: "mask" | "hash" | "drop";
}[] | undefined;
redactionPolicy?: "mask_all_pii" | "mask_on_inline" | "none" | undefined;
loggingPolicy?: "errors_only" | "minimal" | "verbose" | undefined;
retentionDays?: number | undefined;
}>;
export type DescribeInput = z.infer<typeof describeInputSchema>;
export type DescribeOutput = z.infer<typeof describeOutputSchema>;
export type QueryInput = z.infer<typeof queryInputSchema>;
export type QueryOutput = z.infer<typeof queryOutputSchema>;
export type CreateInput = z.infer<typeof createInputSchema>;
export type CreateOutput = z.infer<typeof createOutputSchema>;
export type UpdateInput = z.infer<typeof updateInputSchema>;
export type UpdateOutput = z.infer<typeof updateOutputSchema>;
export type UpsertInput = z.infer<typeof upsertInputSchema>;
export type UpsertOutput = z.infer<typeof upsertOutputSchema>;
export type ListExceptionsInput = z.infer<typeof listExceptionsInputSchema>;
export type ExceptionItem = z.infer<typeof exceptionItemSchema>;
export type ListExceptionsOutput = z.infer<typeof listExceptionsOutputSchema>;
export type GovernanceSnapshot = z.infer<typeof governanceOutputSchema>;
```
--------------------------------------------------------------------------------
/src/javascript/airtable_simple_production.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
/**
* Airtable MCP Server - Production Ready
* Model Context Protocol server for Airtable integration
*
* Features:
* - Complete MCP 2024-11-05 protocol support
* - OAuth2 authentication with PKCE
* - Enterprise security features
* - Rate limiting and input validation
* - Production monitoring and health checks
*
* Author: Rashid Azarang
* License: MIT
*/
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const url = require('url');
const querystring = require('querystring');
// Load environment variables
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
require('dotenv').config({ path: envPath });
}
// Parse command line arguments
const args = process.argv.slice(2);
let tokenIndex = args.indexOf('--token');
let baseIndex = args.indexOf('--base');
const token = tokenIndex !== -1 ? args[tokenIndex + 1] : process.env.AIRTABLE_TOKEN || process.env.AIRTABLE_API_TOKEN;
const baseId = baseIndex !== -1 ? args[baseIndex + 1] : process.env.AIRTABLE_BASE_ID || process.env.AIRTABLE_BASE;
if (!token || !baseId) {
console.error('Error: Missing Airtable credentials');
console.error('\nUsage options:');
console.error(' 1. Command line: node airtable_simple_production.js --token YOUR_TOKEN --base YOUR_BASE_ID');
console.error(' 2. Environment variables: AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
console.error(' 3. .env file with AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
process.exit(1);
}
// Configuration
const CONFIG = {
PORT: process.env.PORT || 8010,
HOST: process.env.HOST || 'localhost',
MAX_REQUESTS_PER_MINUTE: parseInt(process.env.MAX_REQUESTS_PER_MINUTE) || 60,
LOG_LEVEL: process.env.LOG_LEVEL || 'INFO'
};
// Logging
const LOG_LEVELS = { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3, TRACE: 4 };
let currentLogLevel = LOG_LEVELS[CONFIG.LOG_LEVEL] || LOG_LEVELS.INFO;
function log(level, message, metadata = {}) {
if (level <= currentLogLevel) {
const timestamp = new Date().toISOString();
const levelName = Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === level);
// Sanitize message to prevent format string attacks
const safeMessage = String(message).replace(/%/g, '%%');
const output = `[${timestamp}] [${levelName}] ${safeMessage}`;
if (Object.keys(metadata).length > 0) {
// Use separate arguments to avoid format string injection
console.log('%s %s', output, JSON.stringify(metadata));
} else {
console.log('%s', output);
}
}
}
// Rate limiting
const rateLimiter = new Map();
function checkRateLimit(clientId) {
const now = Date.now();
const windowStart = now - 60000; // 1 minute window
if (!rateLimiter.has(clientId)) {
rateLimiter.set(clientId, []);
}
const requests = rateLimiter.get(clientId);
const recentRequests = requests.filter(time => time > windowStart);
if (recentRequests.length >= CONFIG.MAX_REQUESTS_PER_MINUTE) {
return false;
}
recentRequests.push(now);
rateLimiter.set(clientId, recentRequests);
return true;
}
// Input validation and HTML escaping
function sanitizeInput(input) {
if (typeof input === 'string') {
return input.replace(/[<>]/g, '').trim().substring(0, 1000);
}
return input;
}
function escapeHtml(unsafe) {
if (typeof unsafe !== 'string') {
return String(unsafe);
}
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/\//g, "/");
}
function validateUrl(url) {
try {
const parsed = new URL(url);
// Only allow http and https protocols
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}
// Airtable API integration
function callAirtableAPI(endpoint, method = 'GET', body = null, queryParams = {}) {
return new Promise((resolve, reject) => {
const isBaseEndpoint = !endpoint.startsWith('meta/');
const baseUrl = isBaseEndpoint ? `${baseId}/${endpoint}` : endpoint;
const queryString = Object.keys(queryParams).length > 0
? '?' + new URLSearchParams(queryParams).toString()
: '';
const apiUrl = `https://api.airtable.com/v0/${baseUrl}${queryString}`;
const urlObj = new URL(apiUrl);
log(LOG_LEVELS.DEBUG, 'API Request', { method, url: apiUrl });
const options = {
hostname: urlObj.hostname,
path: urlObj.pathname + urlObj.search,
method: method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'User-Agent': 'Airtable-MCP-Server/2.1.0'
}
};
const req = https.request(options, (response) => {
let data = '';
response.on('data', (chunk) => data += chunk);
response.on('end', () => {
try {
const parsed = data ? JSON.parse(data) : {};
if (response.statusCode >= 200 && response.statusCode < 300) {
resolve(parsed);
} else {
const error = parsed.error || {};
reject(new Error(`Airtable API error (${response.statusCode}): ${error.message || error.type || 'Unknown error'}`));
}
} catch (e) {
reject(new Error(`Failed to parse Airtable response: ${e.message}`));
}
});
});
req.on('error', reject);
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
// Tools schema
const TOOLS_SCHEMA = [
{
name: 'list_tables',
description: 'List all tables in the Airtable base',
inputSchema: {
type: 'object',
properties: {
include_schema: { type: 'boolean', description: 'Include field schema information', default: false }
}
}
},
{
name: 'list_records',
description: 'List records from a specific table',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
maxRecords: { type: 'number', description: 'Maximum number of records to return' },
view: { type: 'string', description: 'View name or ID' },
filterByFormula: { type: 'string', description: 'Airtable formula to filter records' }
},
required: ['table']
}
},
{
name: 'get_record',
description: 'Get a single record by ID',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
recordId: { type: 'string', description: 'Record ID' }
},
required: ['table', 'recordId']
}
},
{
name: 'create_record',
description: 'Create a new record in a table',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
fields: { type: 'object', description: 'Field values for the new record' }
},
required: ['table', 'fields']
}
},
{
name: 'update_record',
description: 'Update an existing record',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
recordId: { type: 'string', description: 'Record ID to update' },
fields: { type: 'object', description: 'Fields to update' }
},
required: ['table', 'recordId', 'fields']
}
},
{
name: 'delete_record',
description: 'Delete a record from a table',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
recordId: { type: 'string', description: 'Record ID to delete' }
},
required: ['table', 'recordId']
}
}
];
// Enhanced AI-powered prompts for intelligent Airtable operations
const PROMPTS_SCHEMA = [
{
name: 'analyze_data',
description: 'Advanced AI data analysis with statistical insights, pattern recognition, and predictive modeling',
arguments: [
{
name: 'table',
description: 'Table name or ID to analyze',
required: true
},
{
name: 'analysis_type',
description: 'Type of analysis (trends, statistical, patterns, predictive, anomaly_detection, correlation_matrix)',
required: false
},
{
name: 'field_focus',
description: 'Specific fields to focus the analysis on',
required: false
},
{
name: 'time_dimension',
description: 'Time field for temporal analysis',
required: false
},
{
name: 'confidence_level',
description: 'Statistical confidence level (0.90, 0.95, 0.99)',
required: false
}
]
},
{
name: 'create_report',
description: 'Generate intelligent reports with AI-powered insights, visualizations, and actionable recommendations',
arguments: [
{
name: 'table',
description: 'Table name or ID for the report',
required: true
},
{
name: 'report_type',
description: 'Type of report (executive_summary, operational_dashboard, analytical_deep_dive, performance_metrics, predictive_forecast)',
required: false
},
{
name: 'time_period',
description: 'Time period for analysis (last_7_days, last_30_days, last_quarter, year_to_date, custom)',
required: false
},
{
name: 'stakeholder_level',
description: 'Target audience (executive, manager, analyst, operational)',
required: false
},
{
name: 'include_recommendations',
description: 'Include AI-generated actionable recommendations (true/false)',
required: false
}
]
},
{
name: 'data_insights',
description: 'Discover hidden patterns, correlations, and business insights using advanced AI algorithms',
arguments: [
{
name: 'tables',
description: 'Comma-separated list of table names to analyze',
required: true
},
{
name: 'insight_type',
description: 'Type of insights (correlations, outliers, trends, predictions, segmentation, attribution, churn_analysis)',
required: false
},
{
name: 'business_context',
description: 'Business domain context (sales, marketing, operations, finance, customer_success)',
required: false
},
{
name: 'insight_depth',
description: 'Analysis depth (surface, moderate, deep, comprehensive)',
required: false
}
]
},
{
name: 'optimize_workflow',
description: 'AI-powered workflow optimization with automation recommendations and efficiency improvements',
arguments: [
{
name: 'base_overview',
description: 'Overview of the base structure and current workflows',
required: false
},
{
name: 'optimization_focus',
description: 'Focus area (automation, data_quality, collaboration, performance, integration, user_experience)',
required: false
},
{
name: 'current_pain_points',
description: 'Known issues or bottlenecks in current workflow',
required: false
},
{
name: 'team_size',
description: 'Number of users working with this base',
required: false
}
]
},
{
name: 'smart_schema_design',
description: 'AI-assisted database schema optimization and field relationship analysis',
arguments: [
{
name: 'use_case',
description: 'Primary use case (crm, project_management, inventory, content_management, hr, finance)',
required: true
},
{
name: 'data_volume',
description: 'Expected data volume (small, medium, large, enterprise)',
required: false
},
{
name: 'integration_needs',
description: 'External systems to integrate with',
required: false
},
{
name: 'compliance_requirements',
description: 'Data compliance needs (gdpr, hipaa, sox, none)',
required: false
}
]
},
{
name: 'data_quality_audit',
description: 'Comprehensive AI-powered data quality assessment with cleansing recommendations',
arguments: [
{
name: 'tables',
description: 'Tables to audit (comma-separated or "all")',
required: true
},
{
name: 'quality_dimensions',
description: 'Quality aspects to check (completeness, accuracy, consistency, validity, uniqueness, timeliness)',
required: false
},
{
name: 'severity_threshold',
description: 'Minimum severity level to report (low, medium, high, critical)',
required: false
},
{
name: 'auto_fix_suggestions',
description: 'Include automated fix suggestions (true/false)',
required: false
}
]
},
{
name: 'predictive_analytics',
description: 'Advanced predictive modeling and forecasting using historical Airtable data',
arguments: [
{
name: 'table',
description: 'Table containing historical data',
required: true
},
{
name: 'target_field',
description: 'Field to predict or forecast',
required: true
},
{
name: 'prediction_horizon',
description: 'Forecast period (next_week, next_month, next_quarter, next_year)',
required: false
},
{
name: 'model_type',
description: 'Prediction model (trend_analysis, seasonal_forecast, regression, classification, time_series)',
required: false
},
{
name: 'feature_fields',
description: 'Fields to use as predictive features',
required: false
}
]
},
{
name: 'natural_language_query',
description: 'Process natural language questions about your data and provide intelligent answers',
arguments: [
{
name: 'question',
description: 'Natural language question about your data',
required: true
},
{
name: 'context_tables',
description: 'Tables that might contain relevant data',
required: false
},
{
name: 'response_format',
description: 'Desired response format (narrative, data_summary, visualization_suggestion, action_items)',
required: false
},
{
name: 'include_confidence',
description: 'Include confidence scores for answers (true/false)',
required: false
}
]
},
{
name: 'smart_data_transformation',
description: 'AI-assisted data transformation, cleaning, and enrichment with intelligent suggestions',
arguments: [
{
name: 'source_table',
description: 'Source table for transformation',
required: true
},
{
name: 'transformation_goal',
description: 'Goal (normalize, standardize, enrich, cleanse, aggregate, pivot)',
required: true
},
{
name: 'target_format',
description: 'Desired output format or structure',
required: false
},
{
name: 'quality_rules',
description: 'Data quality rules to apply during transformation',
required: false
},
{
name: 'preserve_history',
description: 'Maintain audit trail of changes (true/false)',
required: false
}
]
},
{
name: 'automation_recommendations',
description: 'Generate intelligent automation suggestions based on workflow patterns and data analysis',
arguments: [
{
name: 'workflow_description',
description: 'Description of current manual processes',
required: false
},
{
name: 'automation_scope',
description: 'Scope (single_table, multi_table, cross_base, external_integration)',
required: false
},
{
name: 'frequency_patterns',
description: 'How often tasks are performed',
required: false
},
{
name: 'complexity_tolerance',
description: 'Acceptable automation complexity (simple, moderate, advanced)',
required: false
},
{
name: 'integration_capabilities',
description: 'Available integration tools (zapier, make, custom_api, native_automations)',
required: false
}
]
}
];
// Roots configuration for filesystem access
const ROOTS_CONFIG = [
{
uri: 'file:///airtable-exports',
name: 'Airtable Exports'
},
{
uri: 'file:///airtable-attachments',
name: 'Airtable Attachments'
}
];
// Logging configuration (currentLogLevel is already declared above)
// HTTP server
const server = http.createServer(async (req, res) => {
// Security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGINS || '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Handle preflight request
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
// Health check endpoint
if (pathname === '/health' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'healthy',
version: '3.0.0',
timestamp: new Date().toISOString(),
uptime: process.uptime()
}));
return;
}
// OAuth2 authorization endpoint
if (pathname === '/oauth/authorize' && req.method === 'GET') {
const params = parsedUrl.query;
const clientId = params.client_id;
const redirectUri = params.redirect_uri;
const state = params.state;
const codeChallenge = params.code_challenge;
const codeChallengeMethod = params.code_challenge_method;
// Validate inputs to prevent XSS
if (!clientId || !redirectUri) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'invalid_request', error_description: 'Missing required parameters' }));
return;
}
// Validate redirect URI
if (!validateUrl(redirectUri)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'invalid_request', error_description: 'Invalid redirect URI' }));
return;
}
// Create safe copies of all variables for JavaScript use
const safeRedirectUri = redirectUri.slice(0, 2000); // Limit length
const safeState = (state || '').slice(0, 200); // Limit length
const safeClientId = clientId.slice(0, 200); // Limit length
// Sanitize for HTML display only
const displayClientId = escapeHtml(safeClientId);
const displayRedirectUri = escapeHtml(safeRedirectUri);
// Generate authorization code
const authCode = crypto.randomBytes(32).toString('hex');
// In a real implementation, store the auth code with expiration
// and associate it with the client and PKCE challenge
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Security-Policy': "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none';",
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'no-referrer',
'Cache-Control': 'no-store, no-cache, must-revalidate, private'
});
// Safely encode data for embedding in HTML attributes and JavaScript
// This prevents XSS by encoding any potentially dangerous characters
const safeJsonConfig = JSON.stringify({
redirectUri: safeRedirectUri,
code: authCode,
state: safeState,
clientId: displayClientId,
displayRedirectUri: displayRedirectUri
}).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026').replace(/'/g, '\\u0027').replace(/"/g, '\\u0022');
// Build HTML with all dynamic content properly escaped
// Using template literals but with pre-escaped content only
const htmlContent = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>OAuth2 Authorization</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline';">
</head>
<body>
<h2>Airtable MCP Server - OAuth2 Authorization</h2>
<p>Client ID: <span id="client-id"></span></p>
<p>Redirect URI: <span id="redirect-uri"></span></p>
<div style="margin: 20px 0;">
<button onclick="authorize()" style="background: #18BFFF; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;">
Authorize Application
</button>
<button onclick="deny()" style="background: #ff4444; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; margin-left: 10px;">
Deny Access
</button>
</div>
<script>
// Parse safely encoded JSON config
(function() {
// Config is safely encoded to prevent XSS
var config = ${safeJsonConfig};
// Safely set text content (not innerHTML) to prevent XSS
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('client-id').textContent = config.clientId;
document.getElementById('redirect-uri').textContent = config.displayRedirectUri;
});
window.authorize = function() {
try {
var url = new URL(config.redirectUri);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('Invalid protocol');
}
var finalUrl = config.redirectUri + '?code=' + encodeURIComponent(config.code) + '&state=' + encodeURIComponent(config.state);
window.location.href = finalUrl;
} catch (e) {
console.error('Authorization failed:', e);
alert('Invalid redirect URL');
}
};
window.deny = function() {
try {
var url = new URL(config.redirectUri);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('Invalid protocol');
}
var finalUrl = config.redirectUri + '?error=access_denied&state=' + encodeURIComponent(config.state);
window.location.href = finalUrl;
} catch (e) {
console.error('Denial failed:', e);
alert('Invalid redirect URL');
}
};
})();
</script>
</body>
</html>`;
// Write response with explicit UTF-8 encoding
res.end(htmlContent, 'utf8');
return;
}
// OAuth2 token endpoint
if (pathname === '/oauth/token' && req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
// Prevent DoS by limiting body size
if (body.length > 10000) {
res.writeHead(413, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'payload_too_large', error_description: 'Request body too large' }));
return;
}
});
req.on('end', () => {
try {
const params = querystring.parse(body);
const grantType = sanitizeInput(params.grant_type);
const code = sanitizeInput(params.code);
const codeVerifier = sanitizeInput(params.code_verifier);
const clientId = sanitizeInput(params.client_id);
// Validate required parameters
if (!grantType || !code || !clientId) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'invalid_request',
error_description: 'Missing required parameters'
}));
return;
}
// In a real implementation, verify the authorization code and PKCE
if (grantType === 'authorization_code' && code) {
// Generate access token
const accessToken = crypto.randomBytes(32).toString('hex');
const refreshToken = crypto.randomBytes(32).toString('hex');
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'Pragma': 'no-cache'
});
res.end(JSON.stringify({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
refresh_token: refreshToken,
scope: 'data.records:read data.records:write schema.bases:read'
}));
} else {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'invalid_grant',
error_description: 'Invalid grant type or authorization code'
}));
}
} catch (error) {
log(LOG_LEVELS.WARN, 'OAuth token request parsing failed', { error: error.message });
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'invalid_request',
error_description: 'Malformed request body'
}));
}
});
return;
}
// MCP endpoint
if (pathname === '/mcp' && req.method === 'POST') {
// Rate limiting
const clientId = req.headers['x-client-id'] || req.connection.remoteAddress;
if (!checkRateLimit(clientId)) {
res.writeHead(429, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Rate limit exceeded. Maximum 60 requests per minute.'
}
}));
return;
}
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', async () => {
try {
const request = JSON.parse(body);
// Sanitize inputs
if (request.params) {
Object.keys(request.params).forEach(key => {
request.params[key] = sanitizeInput(request.params[key]);
});
}
log(LOG_LEVELS.DEBUG, 'MCP request received', {
method: request.method,
id: request.id
});
let response;
switch (request.method) {
case 'initialize':
response = {
jsonrpc: '2.0',
id: request.id,
result: {
protocolVersion: '2024-11-05',
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true, listChanged: true },
prompts: { listChanged: true },
sampling: {},
roots: { listChanged: true },
logging: {}
},
serverInfo: {
name: 'Airtable MCP Server - AI Agent Enhanced',
version: '3.0.0',
description: 'Advanced AI-powered MCP server with 10 intelligent prompt templates, predictive analytics, and enterprise automation capabilities'
}
}
};
log(LOG_LEVELS.INFO, 'Client initialized', { clientId: request.id });
break;
case 'tools/list':
response = {
jsonrpc: '2.0',
id: request.id,
result: {
tools: TOOLS_SCHEMA
}
};
break;
case 'tools/call':
response = await handleToolCall(request);
break;
case 'prompts/list':
response = {
jsonrpc: '2.0',
id: request.id,
result: {
prompts: PROMPTS_SCHEMA
}
};
break;
case 'prompts/get':
response = await handlePromptGet(request);
break;
case 'roots/list':
response = {
jsonrpc: '2.0',
id: request.id,
result: {
roots: ROOTS_CONFIG
}
};
break;
case 'logging/setLevel':
const level = request.params?.level;
if (level && LOG_LEVELS[level.toUpperCase()] !== undefined) {
currentLogLevel = LOG_LEVELS[level.toUpperCase()];
log(LOG_LEVELS.INFO, 'Log level updated', { newLevel: level });
}
response = {
jsonrpc: '2.0',
id: request.id,
result: {}
};
break;
case 'sampling/createMessage':
response = await handleSampling(request);
break;
default:
log(LOG_LEVELS.WARN, 'Unknown method', { method: request.method });
throw new Error(`Method "${request.method}" not found`);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
} catch (error) {
log(LOG_LEVELS.ERROR, 'Request processing failed', { error: error.message });
const errorResponse = {
jsonrpc: '2.0',
id: request?.id || null,
error: {
code: -32000,
message: error.message || 'Internal server error'
}
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(errorResponse));
}
});
return;
}
// Default 404
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not Found' }));
});
// Tool handlers
async function handleToolCall(request) {
const toolName = request.params.name;
const toolParams = request.params.arguments || {};
try {
let result;
let responseText;
switch (toolName) {
case 'list_tables':
const includeSchema = toolParams.include_schema || false;
result = await callAirtableAPI(`meta/bases/${baseId}/tables`);
const tables = result.tables || [];
responseText = tables.length > 0
? `Found ${tables.length} table(s): ` +
tables.map((table, i) =>
`${table.name} (ID: ${table.id}, Fields: ${table.fields?.length || 0})`
).join(', ')
: 'No tables found in this base.';
break;
case 'list_records':
const { table, maxRecords, view, filterByFormula } = toolParams;
const queryParams = {};
if (maxRecords) queryParams.maxRecords = maxRecords;
if (view) queryParams.view = view;
if (filterByFormula) queryParams.filterByFormula = filterByFormula;
result = await callAirtableAPI(table, 'GET', null, queryParams);
const records = result.records || [];
responseText = records.length > 0
? `Found ${records.length} record(s) in table "${table}"`
: `No records found in table "${table}".`;
break;
case 'get_record':
const { table: getTable, recordId } = toolParams;
result = await callAirtableAPI(`${getTable}/${recordId}`);
responseText = `Retrieved record ${recordId} from table "${getTable}"`;
break;
case 'create_record':
const { table: createTable, fields } = toolParams;
const body = { fields: fields };
result = await callAirtableAPI(createTable, 'POST', body);
responseText = `Successfully created record in table "${createTable}" with ID: ${result.id}`;
break;
case 'update_record':
const { table: updateTable, recordId: updateRecordId, fields: updateFields } = toolParams;
const updateBody = { fields: updateFields };
result = await callAirtableAPI(`${updateTable}/${updateRecordId}`, 'PATCH', updateBody);
responseText = `Successfully updated record ${updateRecordId} in table "${updateTable}"`;
break;
case 'delete_record':
const { table: deleteTable, recordId: deleteRecordId } = toolParams;
result = await callAirtableAPI(`${deleteTable}/${deleteRecordId}`, 'DELETE');
responseText = `Successfully deleted record ${deleteRecordId} from table "${deleteTable}"`;
break;
default:
throw new Error(`Unknown tool: ${toolName}`);
}
return {
jsonrpc: '2.0',
id: request.id,
result: {
content: [
{
type: 'text',
text: responseText
}
]
}
};
} catch (error) {
log(LOG_LEVELS.ERROR, `Tool ${toolName} failed`, { error: error.message });
return {
jsonrpc: '2.0',
id: request.id,
result: {
content: [
{
type: 'text',
text: `Error executing ${toolName}: ${error.message}`
}
]
}
};
}
}
// Enhanced AI-powered prompt handlers
async function handlePromptGet(request) {
const promptName = request.params.name;
const promptArgs = request.params.arguments || {};
try {
const prompt = PROMPTS_SCHEMA.find(p => p.name === promptName);
if (!prompt) {
throw new Error(`Prompt "${promptName}" not found`);
}
let messages = [];
switch (promptName) {
case 'analyze_data':
const { table, analysis_type = 'statistical', field_focus, time_dimension, confidence_level = '0.95' } = promptArgs;
messages = [
{
role: 'user',
content: {
type: 'text',
text: `🤖 ADVANCED DATA ANALYSIS REQUEST
**Table**: ${table}
**Analysis Type**: ${analysis_type}
**Confidence Level**: ${confidence_level}
${field_focus ? `**Focus Fields**: ${field_focus}` : ''}
${time_dimension ? `**Time Dimension**: ${time_dimension}` : ''}
**Instructions**:
1. First, examine the table schema and structure using list_tables with include_schema=true
2. Retrieve representative sample data using list_records with appropriate filters
3. Perform ${analysis_type} analysis with statistical rigor
4. Generate insights with confidence intervals and significance testing
5. Provide actionable recommendations based on findings
**Expected Deliverables**:
- Statistical summary with key metrics
- Pattern identification and trend analysis
- Anomaly detection if applicable
- Predictive insights where relevant
- Visualization recommendations
- Business impact assessment
Please use the available Airtable tools to gather data and provide comprehensive ${analysis_type} analysis.`
}
}
];
break;
case 'create_report':
const { table: reportTable, report_type = 'executive_summary', time_period = 'last_30_days', stakeholder_level = 'manager', include_recommendations = 'true' } = promptArgs;
messages = [
{
role: 'user',
content: {
type: 'text',
text: `📊 INTELLIGENT REPORT GENERATION
**Target Table**: ${reportTable}
**Report Type**: ${report_type}
**Time Period**: ${time_period}
**Stakeholder Level**: ${stakeholder_level}
**Include Recommendations**: ${include_recommendations}
**Report Generation Process**:
1. Analyze table structure and data types
2. Extract relevant data for specified time period
3. Calculate key performance indicators
4. Identify trends and patterns
5. Generate visualizations suggestions
6. Create ${stakeholder_level}-appropriate narrative
**Report Sections**:
- Executive Summary (key findings)
- Data Overview and Quality Assessment
- Trend Analysis and Patterns
- Performance Metrics and KPIs
- Risk Assessment and Opportunities
${include_recommendations === 'true' ? '- AI-Generated Recommendations' : ''}
- Next Steps and Action Items
Please gather the necessary data and create a comprehensive ${report_type} tailored for ${stakeholder_level} level stakeholders.`
}
}
];
break;
case 'data_insights':
const { tables, insight_type = 'correlations', business_context = 'general', insight_depth = 'moderate' } = promptArgs;
messages = [
{
role: 'user',
content: {
type: 'text',
text: `🔍 ADVANCED DATA INSIGHTS DISCOVERY
**Target Tables**: ${tables}
**Insight Type**: ${insight_type}
**Business Context**: ${business_context}
**Analysis Depth**: ${insight_depth}
**Discovery Framework**:
1. Multi-table schema analysis and relationship mapping
2. Cross-table data correlation analysis
3. Pattern recognition using ${business_context} domain knowledge
4. Statistical significance testing
5. Business impact quantification
**Insight Categories**:
- ${insight_type} analysis with statistical validation
- Hidden patterns and unexpected relationships
- Segmentation opportunities
- Predictive indicators
- Data quality insights
- Business optimization opportunities
**${business_context.toUpperCase()} CONTEXT ANALYSIS**:
${business_context === 'sales' ? '- Revenue drivers and conversion patterns\n- Customer lifetime value indicators\n- Sales cycle optimization opportunities' : ''}
${business_context === 'marketing' ? '- Campaign effectiveness and attribution\n- Customer segmentation insights\n- Channel performance analysis' : ''}
${business_context === 'operations' ? '- Process efficiency metrics\n- Resource utilization patterns\n- Bottleneck identification' : ''}
Please conduct ${insight_depth} analysis across the specified tables and provide actionable business insights.`
}
}
];
break;
case 'optimize_workflow':
const { base_overview, optimization_focus = 'automation', current_pain_points, team_size } = promptArgs;
messages = [
{
role: 'user',
content: {
type: 'text',
text: `⚡ AI-POWERED WORKFLOW OPTIMIZATION
**Optimization Focus**: ${optimization_focus}
**Team Size**: ${team_size || 'Not specified'}
${base_overview ? `**Base Overview**: ${base_overview}` : ''}
${current_pain_points ? `**Current Pain Points**: ${current_pain_points}` : ''}
**Optimization Analysis**:
1. Workflow pattern analysis and bottleneck identification
2. Automation opportunity assessment
3. User experience and efficiency evaluation
4. Integration and scaling considerations
5. ROI analysis for proposed improvements
**${optimization_focus.toUpperCase()} OPTIMIZATION**:
${optimization_focus === 'automation' ? '- Identify repetitive manual tasks\n- Suggest automation workflows\n- Estimate time savings and ROI' : ''}
${optimization_focus === 'data_quality' ? '- Data validation and cleansing rules\n- Consistency and accuracy improvements\n- Quality monitoring systems' : ''}
${optimization_focus === 'collaboration' ? '- Team workflow improvements\n- Permission and access optimization\n- Communication enhancement strategies' : ''}
**Deliverables**:
- Workflow efficiency assessment
- Prioritized improvement recommendations
- Implementation roadmap with timelines
- Cost-benefit analysis
- Change management considerations
Please analyze the current setup and provide comprehensive ${optimization_focus} optimization recommendations.`
}
}
];
break;
case 'smart_schema_design':
const { use_case, data_volume = 'medium', integration_needs, compliance_requirements = 'none' } = promptArgs;
messages = [
{
role: 'user',
content: {
type: 'text',
text: `🏗️ AI-ASSISTED SCHEMA OPTIMIZATION
**Use Case**: ${use_case}
**Data Volume**: ${data_volume}
**Compliance**: ${compliance_requirements}
${integration_needs ? `**Integrations**: ${integration_needs}` : ''}
**Schema Design Analysis**:
1. Current schema evaluation for ${use_case} best practices
2. Field type and relationship optimization
3. Performance and scalability assessment
4. Compliance requirement implementation
5. Integration compatibility review
**${use_case.toUpperCase()} OPTIMIZATION**:
${use_case === 'crm' ? '- Customer lifecycle tracking\n- Sales pipeline optimization\n- Contact relationship mapping' : ''}
${use_case === 'project_management' ? '- Task dependency modeling\n- Resource allocation tracking\n- Timeline and milestone management' : ''}
${use_case === 'inventory' ? '- Stock level monitoring\n- Supplier relationship tracking\n- Cost and pricing optimization' : ''}
**Recommendations**:
- Optimal field types and relationships
- Indexing and performance suggestions
- Data validation and integrity rules
- Automation and workflow triggers
- Scaling and maintenance considerations
Please analyze the current schema and provide ${use_case}-optimized recommendations.`
}
}
];
break;
case 'data_quality_audit':
const { tables: auditTables, quality_dimensions = 'completeness,accuracy,consistency', severity_threshold = 'medium', auto_fix_suggestions = 'true' } = promptArgs;
messages = [
{
role: 'user',
content: {
type: 'text',
text: `🔍 COMPREHENSIVE DATA QUALITY AUDIT
**Tables to Audit**: ${auditTables}
**Quality Dimensions**: ${quality_dimensions}
**Severity Threshold**: ${severity_threshold}
**Auto-Fix Suggestions**: ${auto_fix_suggestions}
**Audit Framework**:
1. Data completeness analysis (missing values, empty fields)
2. Accuracy assessment (format validation, range checks)
3. Consistency evaluation (cross-field validation, duplicates)
4. Validity verification (data type compliance, constraints)
5. Uniqueness analysis (duplicate detection, key integrity)
6. Timeliness review (data freshness, update patterns)
**Quality Assessment Process**:
- Statistical analysis of data distribution
- Pattern recognition for anomalies
- Cross-table consistency validation
- Historical trend analysis
- Business rule compliance checking
**Deliverables**:
- Quality score by dimension and table
- Detailed issue identification and classification
- Impact assessment and prioritization
${auto_fix_suggestions === 'true' ? '- Automated fix suggestions and scripts' : ''}
- Data governance recommendations
- Monitoring and maintenance strategies
Please conduct a thorough data quality audit focusing on ${quality_dimensions} dimensions.`
}
}
];
break;
case 'predictive_analytics':
const { table: predTable, target_field, prediction_horizon = 'next_month', model_type = 'trend_analysis', feature_fields } = promptArgs;
messages = [
{
role: 'user',
content: {
type: 'text',
text: `🔮 ADVANCED PREDICTIVE ANALYTICS
**Source Table**: ${predTable}
**Target Field**: ${target_field}
**Prediction Horizon**: ${prediction_horizon}
**Model Type**: ${model_type}
${feature_fields ? `**Feature Fields**: ${feature_fields}` : ''}
**Predictive Modeling Process**:
1. Historical data analysis and trend identification
2. Feature engineering and variable selection
3. Model development using ${model_type} approach
4. Validation and accuracy assessment
5. Forecast generation for ${prediction_horizon}
6. Confidence intervals and uncertainty quantification
**${model_type.toUpperCase()} ANALYSIS**:
${model_type === 'time_series' ? '- Seasonal pattern detection\n- Trend decomposition\n- Cyclical behavior analysis' : ''}
${model_type === 'regression' ? '- Variable relationship modeling\n- Predictive factor identification\n- Statistical significance testing' : ''}
${model_type === 'classification' ? '- Category prediction modeling\n- Feature importance analysis\n- Classification accuracy metrics' : ''}
**Outputs**:
- Historical pattern analysis
- Predictive model performance metrics
- Forecast values with confidence intervals
- Key influencing factors identification
- Model limitations and assumptions
- Actionable insights and recommendations
Please develop a ${model_type} model to predict ${target_field} over ${prediction_horizon}.`
}
}
];
break;
case 'natural_language_query':
const { question, context_tables, response_format = 'narrative', include_confidence = 'true' } = promptArgs;
messages = [
{
role: 'user',
content: {
type: 'text',
text: `🗣️ NATURAL LANGUAGE DATA QUERY
**Question**: "${question}"
${context_tables ? `**Context Tables**: ${context_tables}` : ''}
**Response Format**: ${response_format}
**Include Confidence**: ${include_confidence}
**Query Processing Framework**:
1. Question analysis and intent recognition
2. Relevant table and field identification
3. Data retrieval strategy formulation
4. Analysis execution and result compilation
5. Natural language response generation
**Analysis Approach**:
- Semantic understanding of the question
- Automatic table and field mapping
- Intelligent data filtering and aggregation
- Statistical analysis where appropriate
- Context-aware interpretation
**Response Requirements**:
${response_format === 'narrative' ? '- Conversational, easy-to-understand explanation\n- Supporting data and evidence\n- Contextual insights and implications' : ''}
${response_format === 'data_summary' ? '- Structured data summary\n- Key metrics and statistics\n- Trend identification' : ''}
${response_format === 'visualization_suggestion' ? '- Chart and graph recommendations\n- Data visualization best practices\n- Tool-specific guidance' : ''}
${include_confidence === 'true' ? '\n- Confidence scores for answers\n- Data quality indicators\n- Uncertainty acknowledgment' : ''}
Please analyze the available data and provide a comprehensive answer to: "${question}"`
}
}
];
break;
case 'smart_data_transformation':
const { source_table, transformation_goal, target_format, quality_rules, preserve_history = 'true' } = promptArgs;
messages = [
{
role: 'user',
content: {
type: 'text',
text: `🔄 INTELLIGENT DATA TRANSFORMATION
**Source Table**: ${source_table}
**Transformation Goal**: ${transformation_goal}
${target_format ? `**Target Format**: ${target_format}` : ''}
${quality_rules ? `**Quality Rules**: ${quality_rules}` : ''}
**Preserve History**: ${preserve_history}
**Transformation Framework**:
1. Source data analysis and quality assessment
2. Transformation strategy development
3. Data mapping and conversion rules
4. Quality validation and error handling
5. Output optimization and validation
**${transformation_goal.toUpperCase()} PROCESS**:
${transformation_goal === 'normalize' ? '- Database normalization principles\n- Redundancy elimination\n- Relationship optimization' : ''}
${transformation_goal === 'standardize' ? '- Format standardization\n- Value normalization\n- Consistency enforcement' : ''}
${transformation_goal === 'enrich' ? '- Data augmentation strategies\n- External data integration\n- Value-added field creation' : ''}
${transformation_goal === 'cleanse' ? '- Data validation and correction\n- Duplicate removal\n- Missing value handling' : ''}
**Deliverables**:
- Transformation execution plan
- Data mapping specifications
- Quality validation results
- Performance optimization recommendations
${preserve_history === 'true' ? '- Change audit trail and versioning' : ''}
- Post-transformation validation
Please analyze the source data and execute ${transformation_goal} transformation with intelligent optimization.`
}
}
];
break;
case 'automation_recommendations':
const { workflow_description, automation_scope = 'single_table', frequency_patterns, complexity_tolerance = 'moderate', integration_capabilities } = promptArgs;
messages = [
{
role: 'user',
content: {
type: 'text',
text: `🤖 INTELLIGENT AUTOMATION RECOMMENDATIONS
**Automation Scope**: ${automation_scope}
**Complexity Tolerance**: ${complexity_tolerance}
${workflow_description ? `**Current Workflow**: ${workflow_description}` : ''}
${frequency_patterns ? `**Frequency Patterns**: ${frequency_patterns}` : ''}
${integration_capabilities ? `**Integration Tools**: ${integration_capabilities}` : ''}
**Automation Analysis Framework**:
1. Workflow pattern analysis and task identification
2. Automation opportunity assessment and prioritization
3. Technical feasibility and complexity evaluation
4. ROI calculation and benefit quantification
5. Implementation roadmap development
**${automation_scope.toUpperCase()} AUTOMATION**:
${automation_scope === 'single_table' ? '- Field auto-population rules\n- Data validation automation\n- Notification triggers' : ''}
${automation_scope === 'multi_table' ? '- Cross-table data synchronization\n- Workflow orchestration\n- Complex business logic automation' : ''}
${automation_scope === 'external_integration' ? '- API integration strategies\n- Data pipeline automation\n- Third-party tool connectivity' : ''}
**Recommendations**:
- High-impact automation opportunities
- Implementation complexity assessment
- Cost-benefit analysis with ROI projections
- Technical requirements and dependencies
- Risk assessment and mitigation strategies
- Success metrics and monitoring approach
Please analyze the workflow patterns and provide ${complexity_tolerance}-level automation recommendations for ${automation_scope} scope.`
}
}
];
break;
default:
throw new Error(`Unsupported prompt: ${promptName}`);
}
return {
jsonrpc: '2.0',
id: request.id,
result: {
description: prompt.description,
messages: messages
}
};
} catch (error) {
log(LOG_LEVELS.ERROR, `Prompt ${promptName} failed`, { error: error.message });
return {
jsonrpc: '2.0',
id: request.id,
error: {
code: -32000,
message: `Error getting prompt ${promptName}: ${error.message}`
}
};
}
}
// Sampling handler
async function handleSampling(request) {
const { messages, modelPreferences } = request.params;
try {
// Note: In a real implementation, this would integrate with an LLM API
// For now, we'll return a structured response indicating sampling capability
log(LOG_LEVELS.INFO, 'Sampling request received', {
messageCount: messages?.length,
model: modelPreferences?.model
});
return {
jsonrpc: '2.0',
id: request.id,
result: {
model: modelPreferences?.model || 'claude-3-sonnet',
role: 'assistant',
content: {
type: 'text',
text: 'Sampling capability is available. This MCP server can request AI assistance for complex data analysis and insights generation. In a full implementation, this would connect to your preferred LLM for intelligent Airtable operations.'
},
stopReason: 'end_turn'
}
};
} catch (error) {
log(LOG_LEVELS.ERROR, 'Sampling failed', { error: error.message });
return {
jsonrpc: '2.0',
id: request.id,
error: {
code: -32000,
message: `Sampling error: ${error.message}`
}
};
}
}
// Server startup
const PORT = CONFIG.PORT;
const HOST = CONFIG.HOST;
server.listen(PORT, HOST, () => {
log(LOG_LEVELS.INFO, `Airtable MCP Server started`, {
host: HOST,
port: PORT,
version: '2.1.0'
});
console.log(`
╔═══════════════════════════════════════════════════════════════╗
║ Airtable MCP Server v2.1 ║
║ Model Context Protocol Implementation ║
╠═══════════════════════════════════════════════════════════════╣
║ 🌐 MCP Endpoint: http://${HOST}:${PORT}/mcp ║
║ 📊 Health Check: http://${HOST}:${PORT}/health ║
║ 🔒 Security: Rate limiting, input validation ║
║ 📋 Tools: ${TOOLS_SCHEMA.length} available operations ║
╠═══════════════════════════════════════════════════════════════╣
║ 🔗 Connected to Airtable Base: ${baseId.slice(0, 8)}... ║
║ 🚀 Ready for MCP client connections ║
╚═══════════════════════════════════════════════════════════════╝
`);
});
// Graceful shutdown
function gracefulShutdown(signal) {
log(LOG_LEVELS.INFO, 'Graceful shutdown initiated', { signal });
server.close(() => {
log(LOG_LEVELS.INFO, 'Server stopped');
process.exit(0);
});
setTimeout(() => {
log(LOG_LEVELS.ERROR, 'Force shutdown - server did not close in time');
process.exit(1);
}, 10000);
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('uncaughtException', (error) => {
log(LOG_LEVELS.ERROR, 'Uncaught exception', { error: error.message });
gracefulShutdown('uncaughtException');
});
process.on('unhandledRejection', (reason) => {
log(LOG_LEVELS.ERROR, 'Unhandled promise rejection', { reason: reason?.toString() });
gracefulShutdown('unhandledRejection');
});
```
--------------------------------------------------------------------------------
/src/javascript/airtable_simple.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
// Load environment variables from .env file if it exists
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
require('dotenv').config({ path: envPath });
}
// Parse command line arguments with environment variable fallback
const args = process.argv.slice(2);
let tokenIndex = args.indexOf('--token');
let baseIndex = args.indexOf('--base');
// Use environment variables as fallback
const token = tokenIndex !== -1 ? args[tokenIndex + 1] : process.env.AIRTABLE_TOKEN || process.env.AIRTABLE_API_TOKEN;
const baseId = baseIndex !== -1 ? args[baseIndex + 1] : process.env.AIRTABLE_BASE_ID || process.env.AIRTABLE_BASE;
if (!token || !baseId) {
console.error('Error: Missing Airtable credentials');
console.error('\nUsage options:');
console.error(' 1. Command line: node airtable_enhanced.js --token YOUR_TOKEN --base YOUR_BASE_ID');
console.error(' 2. Environment variables: AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
console.error(' 3. .env file with AIRTABLE_TOKEN and AIRTABLE_BASE_ID');
process.exit(1);
}
// Configure logging levels
const LOG_LEVELS = {
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3
};
const currentLogLevel = process.env.LOG_LEVEL ? LOG_LEVELS[process.env.LOG_LEVEL.toUpperCase()] || LOG_LEVELS.INFO : LOG_LEVELS.INFO;
function log(level, message, ...args) {
const levelName = Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === level);
const timestamp = new Date().toISOString();
if (level <= currentLogLevel) {
const prefix = `[${timestamp}] [${levelName}]`;
if (level === LOG_LEVELS.ERROR) {
console.error(prefix, message, ...args);
} else if (level === LOG_LEVELS.WARN) {
console.warn(prefix, message, ...args);
} else {
console.log(prefix, message, ...args);
}
}
}
log(LOG_LEVELS.INFO, `Starting Enhanced Airtable MCP server v1.6.0`);
log(LOG_LEVELS.INFO, `Authentication configured`);
log(LOG_LEVELS.INFO, `Base connection established`);
// Enhanced Airtable API function with full HTTP method support
function callAirtableAPI(endpoint, method = 'GET', body = null, queryParams = {}) {
return new Promise((resolve, reject) => {
const isBaseEndpoint = !endpoint.startsWith('meta/') && !endpoint.startsWith('bases/');
const baseUrl = isBaseEndpoint ? `${baseId}/${endpoint}` : endpoint;
// Build query string
const queryString = Object.keys(queryParams).length > 0
? '?' + new URLSearchParams(queryParams).toString()
: '';
const url = `https://api.airtable.com/v0/${baseUrl}${queryString}`;
const urlObj = new URL(url);
log(LOG_LEVELS.DEBUG, `API Request: ${method} ${url}`);
if (body) {
log(LOG_LEVELS.DEBUG, `Request body:`, JSON.stringify(body, null, 2));
}
const options = {
hostname: urlObj.hostname,
path: urlObj.pathname + urlObj.search,
method: method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
const req = https.request(options, (response) => {
let data = '';
response.on('data', (chunk) => {
data += chunk;
});
response.on('end', () => {
log(LOG_LEVELS.DEBUG, `Response status: ${response.statusCode}`);
log(LOG_LEVELS.DEBUG, `Response data:`, data);
try {
const parsed = data ? JSON.parse(data) : {};
if (response.statusCode >= 200 && response.statusCode < 300) {
resolve(parsed);
} else {
const error = parsed.error || {};
reject(new Error(`Airtable API error (${response.statusCode}): ${error.message || error.type || 'Unknown error'}`));
}
} catch (e) {
reject(new Error(`Failed to parse Airtable response: ${e.message}`));
}
});
});
req.on('error', (error) => {
reject(new Error(`Airtable API request failed: ${error.message}`));
});
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
// Create HTTP server
const server = http.createServer(async (req, res) => {
// Enable CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
// Handle preflight request
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
// Only handle POST requests to /mcp
if (req.method !== 'POST' || !req.url.endsWith('/mcp')) {
res.writeHead(404);
res.end();
return;
}
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', async () => {
try {
const request = JSON.parse(body);
log(LOG_LEVELS.DEBUG, 'Received request:', JSON.stringify(request, null, 2));
// Handle JSON-RPC methods
if (request.method === 'tools/list') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
tools: [
{
name: 'list_tables',
description: 'List all tables in the Airtable base',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'list_records',
description: 'List records from a specific table',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
maxRecords: { type: 'number', description: 'Maximum number of records to return' },
view: { type: 'string', description: 'View name or ID' }
},
required: ['table']
}
},
{
name: 'get_record',
description: 'Get a single record by ID',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
recordId: { type: 'string', description: 'Record ID' }
},
required: ['table', 'recordId']
}
},
{
name: 'create_record',
description: 'Create a new record in a table',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
fields: { type: 'object', description: 'Field values for the new record' }
},
required: ['table', 'fields']
}
},
{
name: 'update_record',
description: 'Update an existing record',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
recordId: { type: 'string', description: 'Record ID to update' },
fields: { type: 'object', description: 'Fields to update' }
},
required: ['table', 'recordId', 'fields']
}
},
{
name: 'delete_record',
description: 'Delete a record from a table',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
recordId: { type: 'string', description: 'Record ID to delete' }
},
required: ['table', 'recordId']
}
},
{
name: 'search_records',
description: 'Search records with filtering and sorting',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
filterByFormula: { type: 'string', description: 'Airtable formula to filter records' },
sort: { type: 'array', description: 'Sort configuration' },
maxRecords: { type: 'number', description: 'Maximum records to return' },
fields: { type: 'array', description: 'Fields to return' }
},
required: ['table']
}
},
{
name: 'list_webhooks',
description: 'List all webhooks for the base',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'create_webhook',
description: 'Create a new webhook for a table',
inputSchema: {
type: 'object',
properties: {
notificationUrl: { type: 'string', description: 'URL to receive webhook notifications' },
specification: {
type: 'object',
description: 'Webhook specification',
properties: {
options: {
type: 'object',
properties: {
filters: {
type: 'object',
properties: {
dataTypes: { type: 'array', items: { type: 'string' } },
recordChangeScope: { type: 'string' }
}
}
}
}
}
}
},
required: ['notificationUrl']
}
},
{
name: 'delete_webhook',
description: 'Delete a webhook',
inputSchema: {
type: 'object',
properties: {
webhookId: { type: 'string', description: 'Webhook ID to delete' }
},
required: ['webhookId']
}
},
{
name: 'get_webhook_payloads',
description: 'Get webhook payload history',
inputSchema: {
type: 'object',
properties: {
webhookId: { type: 'string', description: 'Webhook ID' },
cursor: { type: 'number', description: 'Cursor for pagination' }
},
required: ['webhookId']
}
},
{
name: 'refresh_webhook',
description: 'Refresh a webhook to extend its expiration',
inputSchema: {
type: 'object',
properties: {
webhookId: { type: 'string', description: 'Webhook ID to refresh' }
},
required: ['webhookId']
}
},
{
name: 'list_bases',
description: 'List all accessible Airtable bases',
inputSchema: {
type: 'object',
properties: {
offset: { type: 'string', description: 'Pagination offset for listing more bases' }
}
}
},
{
name: 'get_base_schema',
description: 'Get complete schema information for a base',
inputSchema: {
type: 'object',
properties: {
baseId: { type: 'string', description: 'Base ID to get schema for (optional, defaults to current base)' }
}
}
},
{
name: 'describe_table',
description: 'Get detailed information about a specific table including all fields',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' }
},
required: ['table']
}
},
{
name: 'create_table',
description: 'Create a new table in the base',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Name for the new table' },
description: { type: 'string', description: 'Optional description for the table' },
fields: {
type: 'array',
description: 'Array of field definitions',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Field name' },
type: { type: 'string', description: 'Field type (singleLineText, number, etc.)' },
description: { type: 'string', description: 'Field description' },
options: { type: 'object', description: 'Field-specific options' }
},
required: ['name', 'type']
}
}
},
required: ['name', 'fields']
}
},
{
name: 'update_table',
description: 'Update table name or description',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
name: { type: 'string', description: 'New table name' },
description: { type: 'string', description: 'New table description' }
},
required: ['table']
}
},
{
name: 'delete_table',
description: 'Delete a table (WARNING: This will permanently delete all data)',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID to delete' },
confirm: { type: 'boolean', description: 'Must be true to confirm deletion' }
},
required: ['table', 'confirm']
}
},
{
name: 'create_field',
description: 'Add a new field to an existing table',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
name: { type: 'string', description: 'Field name' },
type: { type: 'string', description: 'Field type (singleLineText, number, multipleSelectionList, etc.)' },
description: { type: 'string', description: 'Field description' },
options: { type: 'object', description: 'Field-specific options (e.g., choices for select fields)' }
},
required: ['table', 'name', 'type']
}
},
{
name: 'update_field',
description: 'Update field properties',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
fieldId: { type: 'string', description: 'Field ID to update' },
name: { type: 'string', description: 'New field name' },
description: { type: 'string', description: 'New field description' },
options: { type: 'object', description: 'Updated field options' }
},
required: ['table', 'fieldId']
}
},
{
name: 'delete_field',
description: 'Delete a field from a table (WARNING: This will permanently delete all data in this field)',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
fieldId: { type: 'string', description: 'Field ID to delete' },
confirm: { type: 'boolean', description: 'Must be true to confirm deletion' }
},
required: ['table', 'fieldId', 'confirm']
}
},
{
name: 'list_field_types',
description: 'Get a reference of all available Airtable field types and their schemas',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_table_views',
description: 'List all views for a specific table',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' }
},
required: ['table']
}
},
{
name: 'upload_attachment',
description: 'Upload/attach a file from URL to a record',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
recordId: { type: 'string', description: 'Record ID to attach file to' },
fieldName: { type: 'string', description: 'Name of the attachment field' },
url: { type: 'string', description: 'Public URL of the file to attach' },
filename: { type: 'string', description: 'Optional filename for the attachment' }
},
required: ['table', 'recordId', 'fieldName', 'url']
}
},
{
name: 'batch_create_records',
description: 'Create multiple records at once (up to 10)',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
records: {
type: 'array',
description: 'Array of record objects to create (max 10)',
items: {
type: 'object',
properties: {
fields: { type: 'object', description: 'Record fields' }
},
required: ['fields']
},
maxItems: 10
}
},
required: ['table', 'records']
}
},
{
name: 'batch_update_records',
description: 'Update multiple records at once (up to 10)',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
records: {
type: 'array',
description: 'Array of record objects to update (max 10)',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Record ID' },
fields: { type: 'object', description: 'Fields to update' }
},
required: ['id', 'fields']
},
maxItems: 10
}
},
required: ['table', 'records']
}
},
{
name: 'batch_delete_records',
description: 'Delete multiple records at once (up to 10)',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
recordIds: {
type: 'array',
description: 'Array of record IDs to delete (max 10)',
items: { type: 'string' },
maxItems: 10
}
},
required: ['table', 'recordIds']
}
},
{
name: 'batch_upsert_records',
description: 'Update existing records or create new ones based on key fields',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
records: {
type: 'array',
description: 'Array of record objects (max 10)',
items: {
type: 'object',
properties: {
fields: { type: 'object', description: 'Record fields' }
},
required: ['fields']
},
maxItems: 10
},
keyFields: {
type: 'array',
description: 'Fields to use for matching existing records',
items: { type: 'string' }
}
},
required: ['table', 'records', 'keyFields']
}
},
{
name: 'create_view',
description: 'Create a new view for a table',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
name: { type: 'string', description: 'Name for the new view' },
type: { type: 'string', description: 'View type (grid, form, calendar, etc.)', enum: ['grid', 'form', 'calendar', 'gallery', 'kanban', 'timeline', 'gantt'] },
visibleFieldIds: { type: 'array', description: 'Array of field IDs to show in view', items: { type: 'string' } },
fieldOrder: { type: 'array', description: 'Order of fields in view', items: { type: 'string' } }
},
required: ['table', 'name', 'type']
}
},
{
name: 'get_view_metadata',
description: 'Get detailed metadata for a specific view',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Table name or ID' },
viewId: { type: 'string', description: 'View ID' }
},
required: ['table', 'viewId']
}
},
{
name: 'create_base',
description: 'Create a new Airtable base',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Name for the new base' },
workspaceId: { type: 'string', description: 'Workspace ID to create the base in' },
tables: {
type: 'array',
description: 'Initial tables to create in the base',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Table name' },
description: { type: 'string', description: 'Table description' },
fields: {
type: 'array',
description: 'Table fields',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Field name' },
type: { type: 'string', description: 'Field type' }
},
required: ['name', 'type']
}
}
},
required: ['name', 'fields']
}
}
},
required: ['name', 'tables']
}
},
{
name: 'list_collaborators',
description: 'List collaborators and their permissions for the current base',
inputSchema: {
type: 'object',
properties: {
baseId: { type: 'string', description: 'Base ID (optional, defaults to current base)' }
}
}
},
{
name: 'list_shares',
description: 'List shared views and their configurations',
inputSchema: {
type: 'object',
properties: {
baseId: { type: 'string', description: 'Base ID (optional, defaults to current base)' }
}
}
}
]
}
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
return;
}
if (request.method === 'resources/list') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
resources: [
{
id: 'airtable_tables',
name: 'Airtable Tables',
description: 'Tables in your Airtable base'
}
]
}
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
return;
}
if (request.method === 'prompts/list') {
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
prompts: [
{
id: 'tables_prompt',
name: 'List Tables',
description: 'List all tables'
}
]
}
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
return;
}
// Handle tool calls
if (request.method === 'tools/call') {
const toolName = request.params.name;
const toolParams = request.params.arguments || {};
let result;
let responseText;
try {
// LIST TABLES
if (toolName === 'list_tables') {
result = await callAirtableAPI(`meta/bases/${baseId}/tables`);
const tables = result.tables || [];
responseText = tables.length > 0
? `Found ${tables.length} table(s):\n` + tables.map((table, i) =>
`${i+1}. ${table.name} (ID: ${table.id}, Fields: ${table.fields?.length || 0})`
).join('\n')
: 'No tables found in this base.';
}
// LIST RECORDS
else if (toolName === 'list_records') {
const { table, maxRecords, view } = toolParams;
const queryParams = {};
if (maxRecords) queryParams.maxRecords = maxRecords;
if (view) queryParams.view = view;
result = await callAirtableAPI(`${table}`, 'GET', null, queryParams);
const records = result.records || [];
responseText = records.length > 0
? `Found ${records.length} record(s) in table "${table}":\n` +
records.map((record, i) =>
`${i+1}. ID: ${record.id}\n Fields: ${JSON.stringify(record.fields, null, 2)}`
).join('\n\n')
: `No records found in table "${table}".`;
}
// GET SINGLE RECORD
else if (toolName === 'get_record') {
const { table, recordId } = toolParams;
result = await callAirtableAPI(`${table}/${recordId}`);
responseText = `Record ${recordId} from table "${table}":\n` +
JSON.stringify(result.fields, null, 2) +
`\n\nCreated: ${result.createdTime}`;
}
// CREATE RECORD
else if (toolName === 'create_record') {
const { table, fields } = toolParams;
const body = {
fields: fields
};
result = await callAirtableAPI(table, 'POST', body);
responseText = `Successfully created record in table "${table}":\n` +
`Record ID: ${result.id}\n` +
`Fields: ${JSON.stringify(result.fields, null, 2)}\n` +
`Created at: ${result.createdTime}`;
}
// UPDATE RECORD
else if (toolName === 'update_record') {
const { table, recordId, fields } = toolParams;
const body = {
fields: fields
};
result = await callAirtableAPI(`${table}/${recordId}`, 'PATCH', body);
responseText = `Successfully updated record ${recordId} in table "${table}":\n` +
`Updated fields: ${JSON.stringify(result.fields, null, 2)}`;
}
// DELETE RECORD
else if (toolName === 'delete_record') {
const { table, recordId } = toolParams;
result = await callAirtableAPI(`${table}/${recordId}`, 'DELETE');
responseText = `Successfully deleted record ${recordId} from table "${table}".\n` +
`Deleted record ID: ${result.id}\n` +
`Deleted: ${result.deleted}`;
}
// SEARCH RECORDS
else if (toolName === 'search_records') {
const { table, filterByFormula, sort, maxRecords, fields } = toolParams;
const queryParams = {};
if (filterByFormula) queryParams.filterByFormula = filterByFormula;
if (maxRecords) queryParams.maxRecords = maxRecords;
if (fields && fields.length > 0) queryParams.fields = fields;
if (sort && sort.length > 0) {
sort.forEach((s, i) => {
queryParams[`sort[${i}][field]`] = s.field;
queryParams[`sort[${i}][direction]`] = s.direction || 'asc';
});
}
result = await callAirtableAPI(table, 'GET', null, queryParams);
const records = result.records || [];
responseText = records.length > 0
? `Found ${records.length} matching record(s) in table "${table}":\n` +
records.map((record, i) =>
`${i+1}. ID: ${record.id}\n Fields: ${JSON.stringify(record.fields, null, 2)}`
).join('\n\n')
: `No records found matching the search criteria in table "${table}".`;
}
// LIST WEBHOOKS
else if (toolName === 'list_webhooks') {
result = await callAirtableAPI(`bases/${baseId}/webhooks`, 'GET');
const webhooks = result.webhooks || [];
responseText = webhooks.length > 0
? `Found ${webhooks.length} webhook(s):\n` +
webhooks.map((webhook, i) =>
`${i+1}. ID: ${webhook.id}\n` +
` URL: ${webhook.notificationUrl}\n` +
` Active: ${webhook.isHookEnabled}\n` +
` Created: ${webhook.createdTime}\n` +
` Expires: ${webhook.expirationTime}`
).join('\n\n')
: 'No webhooks configured for this base.';
}
// CREATE WEBHOOK
else if (toolName === 'create_webhook') {
const { notificationUrl, specification } = toolParams;
const body = {
notificationUrl: notificationUrl,
specification: specification || {
options: {
filters: {
dataTypes: ['tableData']
}
}
}
};
result = await callAirtableAPI(`bases/${baseId}/webhooks`, 'POST', body);
responseText = `Successfully created webhook:\n` +
`Webhook ID: ${result.id}\n` +
`URL: ${result.notificationUrl}\n` +
`MAC Secret: ${result.macSecretBase64}\n` +
`Expiration: ${result.expirationTime}\n` +
`Cursor: ${result.cursorForNextPayload}\n\n` +
`⚠️ IMPORTANT: Save the MAC secret - it won't be shown again!`;
}
// DELETE WEBHOOK
else if (toolName === 'delete_webhook') {
const { webhookId } = toolParams;
await callAirtableAPI(`bases/${baseId}/webhooks/${webhookId}`, 'DELETE');
responseText = `Successfully deleted webhook ${webhookId}`;
}
// GET WEBHOOK PAYLOADS
else if (toolName === 'get_webhook_payloads') {
const { webhookId, cursor } = toolParams;
const queryParams = {};
if (cursor) queryParams.cursor = cursor;
result = await callAirtableAPI(`bases/${baseId}/webhooks/${webhookId}/payloads`, 'GET', null, queryParams);
const payloads = result.payloads || [];
responseText = payloads.length > 0
? `Found ${payloads.length} webhook payload(s):\n` +
payloads.map((payload, i) =>
`${i+1}. Timestamp: ${payload.timestamp}\n` +
` Base/Table: ${payload.baseTransactionNumber}\n` +
` Change Types: ${JSON.stringify(payload.changePayload?.changedTablesById || {})}`
).join('\n\n') +
(result.cursor ? `\n\nNext cursor: ${result.cursor}` : '')
: 'No payloads found for this webhook.';
}
// REFRESH WEBHOOK
else if (toolName === 'refresh_webhook') {
const { webhookId } = toolParams;
result = await callAirtableAPI(`bases/${baseId}/webhooks/${webhookId}/refresh`, 'POST');
responseText = `Successfully refreshed webhook ${webhookId}:\n` +
`New expiration: ${result.expirationTime}`;
}
// Schema Management Tools
else if (toolName === 'list_bases') {
const { offset } = toolParams;
const queryParams = offset ? { offset } : {};
result = await callAirtableAPI('meta/bases', 'GET', null, queryParams);
if (result.bases && result.bases.length > 0) {
responseText = `Found ${result.bases.length} accessible base(s):\n`;
result.bases.forEach((base, index) => {
responseText += `${index + 1}. ${base.name} (ID: ${base.id})\n`;
if (base.permissionLevel) {
responseText += ` Permission: ${base.permissionLevel}\n`;
}
});
if (result.offset) {
responseText += `\nNext page offset: ${result.offset}`;
}
} else {
responseText = 'No accessible bases found.';
}
}
else if (toolName === 'get_base_schema') {
const { baseId: targetBaseId } = toolParams;
const targetId = targetBaseId || baseId;
result = await callAirtableAPI(`meta/bases/${targetId}/tables`, 'GET');
if (result.tables && result.tables.length > 0) {
responseText = `Base schema for ${targetId}:\n\n`;
result.tables.forEach((table, index) => {
responseText += `${index + 1}. Table: ${table.name} (ID: ${table.id})\n`;
if (table.description) {
responseText += ` Description: ${table.description}\n`;
}
responseText += ` Fields (${table.fields.length}):\n`;
table.fields.forEach((field, fieldIndex) => {
responseText += ` ${fieldIndex + 1}. ${field.name} (${field.type})\n`;
if (field.description) {
responseText += ` Description: ${field.description}\n`;
}
});
if (table.views && table.views.length > 0) {
responseText += ` Views (${table.views.length}): ${table.views.map(v => v.name).join(', ')}\n`;
}
responseText += '\n';
});
} else {
responseText = 'No tables found in this base.';
}
}
else if (toolName === 'describe_table') {
const { table } = toolParams;
// Get table schema first
const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
const tableInfo = schemaResult.tables.find(t =>
t.name.toLowerCase() === table.toLowerCase() || t.id === table
);
if (!tableInfo) {
responseText = `Table "${table}" not found.`;
} else {
responseText = `Table Details: ${tableInfo.name}\n`;
responseText += `ID: ${tableInfo.id}\n`;
if (tableInfo.description) {
responseText += `Description: ${tableInfo.description}\n`;
}
responseText += `\nFields (${tableInfo.fields.length}):\n`;
tableInfo.fields.forEach((field, index) => {
responseText += `${index + 1}. ${field.name}\n`;
responseText += ` Type: ${field.type}\n`;
responseText += ` ID: ${field.id}\n`;
if (field.description) {
responseText += ` Description: ${field.description}\n`;
}
if (field.options) {
responseText += ` Options: ${JSON.stringify(field.options, null, 2)}\n`;
}
responseText += '\n';
});
if (tableInfo.views && tableInfo.views.length > 0) {
responseText += `Views (${tableInfo.views.length}):\n`;
tableInfo.views.forEach((view, index) => {
responseText += `${index + 1}. ${view.name} (${view.type})\n`;
});
}
}
}
else if (toolName === 'create_table') {
const { name, description, fields } = toolParams;
const body = {
name,
fields: fields.map(field => ({
name: field.name,
type: field.type,
description: field.description,
options: field.options
}))
};
if (description) {
body.description = description;
}
result = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'POST', body);
responseText = `Successfully created table "${name}" (ID: ${result.id})\n`;
responseText += `Fields created: ${result.fields.length}\n`;
result.fields.forEach((field, index) => {
responseText += `${index + 1}. ${field.name} (${field.type})\n`;
});
}
else if (toolName === 'update_table') {
const { table, name, description } = toolParams;
// Get table ID first
const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
const tableInfo = schemaResult.tables.find(t =>
t.name.toLowerCase() === table.toLowerCase() || t.id === table
);
if (!tableInfo) {
responseText = `Table "${table}" not found.`;
} else {
const body = {};
if (name) body.name = name;
if (description !== undefined) body.description = description;
if (Object.keys(body).length === 0) {
responseText = 'No updates specified. Provide name or description to update.';
} else {
result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}`, 'PATCH', body);
responseText = `Successfully updated table "${tableInfo.name}":\n`;
if (name) responseText += `New name: ${result.name}\n`;
if (description !== undefined) responseText += `New description: ${result.description || '(none)'}\n`;
}
}
}
else if (toolName === 'delete_table') {
const { table, confirm } = toolParams;
if (!confirm) {
responseText = 'Table deletion requires confirm=true to proceed. This action cannot be undone!';
} else {
// Get table ID first
const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
const tableInfo = schemaResult.tables.find(t =>
t.name.toLowerCase() === table.toLowerCase() || t.id === table
);
if (!tableInfo) {
responseText = `Table "${table}" not found.`;
} else {
result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}`, 'DELETE');
responseText = `Successfully deleted table "${tableInfo.name}" (ID: ${tableInfo.id})\n`;
responseText += 'All data in this table has been permanently removed.';
}
}
}
// Field Management Tools
else if (toolName === 'create_field') {
const { table, name, type, description, options } = toolParams;
// Get table ID first
const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
const tableInfo = schemaResult.tables.find(t =>
t.name.toLowerCase() === table.toLowerCase() || t.id === table
);
if (!tableInfo) {
responseText = `Table "${table}" not found.`;
} else {
const body = {
name,
type
};
if (description) body.description = description;
if (options) body.options = options;
result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/fields`, 'POST', body);
responseText = `Successfully created field "${name}" in table "${tableInfo.name}"\n`;
responseText += `Field ID: ${result.id}\n`;
responseText += `Type: ${result.type}\n`;
if (result.description) {
responseText += `Description: ${result.description}\n`;
}
}
}
else if (toolName === 'update_field') {
const { table, fieldId, name, description, options } = toolParams;
// Get table ID first
const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
const tableInfo = schemaResult.tables.find(t =>
t.name.toLowerCase() === table.toLowerCase() || t.id === table
);
if (!tableInfo) {
responseText = `Table "${table}" not found.`;
} else {
const body = {};
if (name) body.name = name;
if (description !== undefined) body.description = description;
if (options) body.options = options;
if (Object.keys(body).length === 0) {
responseText = 'No updates specified. Provide name, description, or options to update.';
} else {
result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/fields/${fieldId}`, 'PATCH', body);
responseText = `Successfully updated field in table "${tableInfo.name}":\n`;
responseText += `Field: ${result.name} (${result.type})\n`;
responseText += `ID: ${result.id}\n`;
if (result.description) {
responseText += `Description: ${result.description}\n`;
}
}
}
}
else if (toolName === 'delete_field') {
const { table, fieldId, confirm } = toolParams;
if (!confirm) {
responseText = 'Field deletion requires confirm=true to proceed. This action cannot be undone!';
} else {
// Get table ID first
const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
const tableInfo = schemaResult.tables.find(t =>
t.name.toLowerCase() === table.toLowerCase() || t.id === table
);
if (!tableInfo) {
responseText = `Table "${table}" not found.`;
} else {
result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/fields/${fieldId}`, 'DELETE');
responseText = `Successfully deleted field from table "${tableInfo.name}"\n`;
responseText += 'All data in this field has been permanently removed.';
}
}
}
else if (toolName === 'list_field_types') {
responseText = `Available Airtable Field Types:\n\n`;
responseText += `Basic Fields:\n`;
responseText += `• singleLineText - Single line text input\n`;
responseText += `• multilineText - Multi-line text input\n`;
responseText += `• richText - Rich text with formatting\n`;
responseText += `• number - Number field with optional formatting\n`;
responseText += `• percent - Percentage field\n`;
responseText += `• currency - Currency field\n`;
responseText += `• singleSelect - Single choice from predefined options\n`;
responseText += `• multipleSelectionList - Multiple choices from predefined options\n`;
responseText += `• date - Date field\n`;
responseText += `• dateTime - Date and time field\n`;
responseText += `• phoneNumber - Phone number field\n`;
responseText += `• email - Email address field\n`;
responseText += `• url - URL field\n`;
responseText += `• checkbox - Checkbox (true/false)\n`;
responseText += `• rating - Star rating field\n`;
responseText += `• duration - Duration/time field\n\n`;
responseText += `Advanced Fields:\n`;
responseText += `• multipleAttachment - File attachments\n`;
responseText += `• linkedRecord - Link to records in another table\n`;
responseText += `• lookup - Lookup values from linked records\n`;
responseText += `• rollup - Calculate values from linked records\n`;
responseText += `• count - Count of linked records\n`;
responseText += `• formula - Calculated field with formulas\n`;
responseText += `• createdTime - Auto-timestamp when record created\n`;
responseText += `• createdBy - Auto-user who created record\n`;
responseText += `• lastModifiedTime - Auto-timestamp when record modified\n`;
responseText += `• lastModifiedBy - Auto-user who last modified record\n`;
responseText += `• autoNumber - Auto-incrementing number\n`;
responseText += `• barcode - Barcode/QR code field\n`;
responseText += `• button - Action button field\n`;
}
else if (toolName === 'get_table_views') {
const { table } = toolParams;
// Get table schema
const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
const tableInfo = schemaResult.tables.find(t =>
t.name.toLowerCase() === table.toLowerCase() || t.id === table
);
if (!tableInfo) {
responseText = `Table "${table}" not found.`;
} else {
if (tableInfo.views && tableInfo.views.length > 0) {
responseText = `Views for table "${tableInfo.name}" (${tableInfo.views.length}):\n\n`;
tableInfo.views.forEach((view, index) => {
responseText += `${index + 1}. ${view.name}\n`;
responseText += ` Type: ${view.type}\n`;
responseText += ` ID: ${view.id}\n`;
if (view.visibleFieldIds && view.visibleFieldIds.length > 0) {
responseText += ` Visible fields: ${view.visibleFieldIds.length}\n`;
}
responseText += '\n';
});
} else {
responseText = `No views found for table "${tableInfo.name}".`;
}
}
}
// NEW v1.6.0 TOOLS - Attachment and Batch Operations
else if (toolName === 'upload_attachment') {
const { table, recordId, fieldName, url, filename } = toolParams;
const attachment = { url };
if (filename) attachment.filename = filename;
const updateBody = {
fields: {
[fieldName]: [attachment]
}
};
result = await callAirtableAPI(`${table}/${recordId}`, 'PATCH', updateBody);
responseText = `Successfully attached file to record ${recordId}:\n`;
responseText += `Field: ${fieldName}\n`;
responseText += `URL: ${url}\n`;
if (filename) responseText += `Filename: ${filename}\n`;
}
else if (toolName === 'batch_create_records') {
const { table, records } = toolParams;
if (records.length > 10) {
responseText = 'Error: Cannot create more than 10 records at once. Please split into smaller batches.';
} else {
const body = { records };
result = await callAirtableAPI(table, 'POST', body);
responseText = `Successfully created ${result.records.length} records:\n`;
result.records.forEach((record, index) => {
responseText += `${index + 1}. ID: ${record.id}\n`;
const fields = Object.keys(record.fields);
if (fields.length > 0) {
responseText += ` Fields: ${fields.join(', ')}\n`;
}
});
}
}
else if (toolName === 'batch_update_records') {
const { table, records } = toolParams;
if (records.length > 10) {
responseText = 'Error: Cannot update more than 10 records at once. Please split into smaller batches.';
} else {
const body = { records };
result = await callAirtableAPI(table, 'PATCH', body);
responseText = `Successfully updated ${result.records.length} records:\n`;
result.records.forEach((record, index) => {
responseText += `${index + 1}. ID: ${record.id}\n`;
const fields = Object.keys(record.fields);
if (fields.length > 0) {
responseText += ` Updated fields: ${fields.join(', ')}\n`;
}
});
}
}
else if (toolName === 'batch_delete_records') {
const { table, recordIds } = toolParams;
if (recordIds.length > 10) {
responseText = 'Error: Cannot delete more than 10 records at once. Please split into smaller batches.';
} else {
const queryParams = { records: recordIds };
result = await callAirtableAPI(table, 'DELETE', null, queryParams);
responseText = `Successfully deleted ${result.records.length} records:\n`;
result.records.forEach((record, index) => {
responseText += `${index + 1}. Deleted ID: ${record.id}\n`;
});
}
}
else if (toolName === 'batch_upsert_records') {
const { table, records, keyFields } = toolParams;
if (records.length > 10) {
responseText = 'Error: Cannot upsert more than 10 records at once. Please split into smaller batches.';
} else {
// For simplicity, we'll implement this as a batch create with merge fields
// Note: Real upsert requires checking existing records first
const body = {
records,
performUpsert: {
fieldsToMergeOn: keyFields
}
};
result = await callAirtableAPI(table, 'PATCH', body);
responseText = `Successfully upserted ${result.records.length} records:\n`;
result.records.forEach((record, index) => {
responseText += `${index + 1}. ID: ${record.id}\n`;
const fields = Object.keys(record.fields);
if (fields.length > 0) {
responseText += ` Fields: ${fields.join(', ')}\n`;
}
});
}
}
// NEW v1.6.0 TOOLS - Advanced View Management
else if (toolName === 'create_view') {
const { table, name, type, visibleFieldIds, fieldOrder } = toolParams;
// Get table ID first
const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
const tableInfo = schemaResult.tables.find(t =>
t.name.toLowerCase() === table.toLowerCase() || t.id === table
);
if (!tableInfo) {
responseText = `Table "${table}" not found.`;
} else {
const body = {
name,
type
};
if (visibleFieldIds) body.visibleFieldIds = visibleFieldIds;
if (fieldOrder) body.fieldOrder = fieldOrder;
result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/views`, 'POST', body);
responseText = `Successfully created view "${name}" in table "${tableInfo.name}":\n`;
responseText += `View ID: ${result.id}\n`;
responseText += `Type: ${result.type}\n`;
if (result.visibleFieldIds && result.visibleFieldIds.length > 0) {
responseText += `Visible fields: ${result.visibleFieldIds.length}\n`;
}
}
}
else if (toolName === 'get_view_metadata') {
const { table, viewId } = toolParams;
// Get table ID first
const schemaResult = await callAirtableAPI(`meta/bases/${baseId}/tables`, 'GET');
const tableInfo = schemaResult.tables.find(t =>
t.name.toLowerCase() === table.toLowerCase() || t.id === table
);
if (!tableInfo) {
responseText = `Table "${table}" not found.`;
} else {
result = await callAirtableAPI(`meta/bases/${baseId}/tables/${tableInfo.id}/views/${viewId}`, 'GET');
responseText = `View Metadata: ${result.name}\n`;
responseText += `ID: ${result.id}\n`;
responseText += `Type: ${result.type}\n`;
if (result.visibleFieldIds && result.visibleFieldIds.length > 0) {
responseText += `\nVisible Fields (${result.visibleFieldIds.length}):\n`;
result.visibleFieldIds.forEach((fieldId, index) => {
responseText += `${index + 1}. ${fieldId}\n`;
});
}
if (result.filterByFormula) {
responseText += `\nFilter Formula: ${result.filterByFormula}\n`;
}
if (result.sorts && result.sorts.length > 0) {
responseText += `\nSort Configuration:\n`;
result.sorts.forEach((sort, index) => {
responseText += `${index + 1}. Field: ${sort.field}, Direction: ${sort.direction}\n`;
});
}
}
}
// NEW v1.6.0 TOOLS - Base Management
else if (toolName === 'create_base') {
const { name, workspaceId, tables } = toolParams;
const body = {
name,
tables: tables.map(table => ({
name: table.name,
description: table.description,
fields: table.fields
}))
};
if (workspaceId) {
body.workspaceId = workspaceId;
}
result = await callAirtableAPI('meta/bases', 'POST', body);
responseText = `Successfully created base "${name}":\n`;
responseText += `Base ID: ${result.id}\n`;
if (result.tables && result.tables.length > 0) {
responseText += `\nTables created (${result.tables.length}):\n`;
result.tables.forEach((table, index) => {
responseText += `${index + 1}. ${table.name} (ID: ${table.id})\n`;
if (table.fields && table.fields.length > 0) {
responseText += ` Fields: ${table.fields.length}\n`;
}
});
}
}
else if (toolName === 'list_collaborators') {
const { baseId: targetBaseId } = toolParams;
const targetId = targetBaseId || baseId;
result = await callAirtableAPI(`meta/bases/${targetId}/collaborators`, 'GET');
if (result.collaborators && result.collaborators.length > 0) {
responseText = `Base collaborators (${result.collaborators.length}):\n\n`;
result.collaborators.forEach((collaborator, index) => {
responseText += `${index + 1}. ${collaborator.email || collaborator.name || 'Unknown'}\n`;
responseText += ` Permission: ${collaborator.permissionLevel || 'Unknown'}\n`;
responseText += ` Type: ${collaborator.type || 'User'}\n`;
if (collaborator.userId) {
responseText += ` User ID: ${collaborator.userId}\n`;
}
responseText += '\n';
});
} else {
responseText = 'No collaborators found for this base.';
}
}
else if (toolName === 'list_shares') {
const { baseId: targetBaseId } = toolParams;
const targetId = targetBaseId || baseId;
result = await callAirtableAPI(`meta/bases/${targetId}/shares`, 'GET');
if (result.shares && result.shares.length > 0) {
responseText = `Shared views (${result.shares.length}):\n\n`;
result.shares.forEach((share, index) => {
responseText += `${index + 1}. ${share.name || 'Unnamed Share'}\n`;
responseText += ` Share ID: ${share.id}\n`;
responseText += ` URL: ${share.url}\n`;
responseText += ` Type: ${share.type || 'View'}\n`;
if (share.viewId) {
responseText += ` View ID: ${share.viewId}\n`;
}
if (share.tableId) {
responseText += ` Table ID: ${share.tableId}\n`;
}
responseText += ` Effective: ${share.effective ? 'Yes' : 'No'}\n`;
responseText += '\n';
});
} else {
responseText = 'No shared views found for this base.';
}
}
else {
throw new Error(`Unknown tool: ${toolName}`);
}
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
content: [
{
type: 'text',
text: responseText
}
]
}
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
} catch (error) {
log(LOG_LEVELS.ERROR, `Tool ${toolName} error:`, error.message);
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
content: [
{
type: 'text',
text: `Error executing ${toolName}: ${error.message}`
}
]
}
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
}
return;
}
// Method not found
const response = {
jsonrpc: '2.0',
id: request.id,
error: {
code: -32601,
message: `Method ${request.method} not found`
}
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
} catch (error) {
log(LOG_LEVELS.ERROR, 'Error processing request:', error);
const response = {
jsonrpc: '2.0',
id: request.id || null,
error: {
code: -32000,
message: error.message || 'Unknown error'
}
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
}
});
});
// Start server
const PORT = process.env.PORT || 8010;
server.listen(PORT, () => {
log(LOG_LEVELS.INFO, `Enhanced Airtable MCP server v1.4.0 running at http://localhost:${PORT}/mcp`);
console.log(`For Claude, use this URL: http://localhost:${PORT}/mcp`);
});
// Graceful shutdown
process.on('SIGINT', () => {
log(LOG_LEVELS.INFO, 'Shutting down server...');
server.close(() => {
log(LOG_LEVELS.INFO, 'Server stopped');
process.exit(0);
});
});
```