Skip to content

Commit dbd84bc

Browse files
ARHAEEMclaude
andcommitted
feat(mcp): add delete_fields bulk tool with checkpoint support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 12cb872 commit dbd84bc

3 files changed

Lines changed: 78 additions & 7 deletions

File tree

packages/mcp-server/src/index.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,39 @@ REPLACING ALL CHOICES: just pass the new choices without any IDs.`,
682682
required: ['appId', 'fieldId', 'expectedName'],
683683
},
684684
},
685+
{
686+
name: 'delete_fields',
687+
description: 'Delete multiple fields from an Airtable table in a single call. Each entry requires fieldId and expectedName as a safety guard (deletion is refused if names do not match). Fields are processed sequentially and all are attempted even if some fail — partial results are always returned. Optionally writes a JSON checkpoint file after each deletion so the batch can be resumed if interrupted.',
688+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
689+
inputSchema: {
690+
type: 'object',
691+
properties: {
692+
appId: { type: 'string', description: 'The Airtable base/application ID' },
693+
fields: {
694+
type: 'array',
695+
description: 'Fields to delete. Each entry must have fieldId and expectedName.',
696+
items: {
697+
type: 'object',
698+
properties: {
699+
fieldId: { type: 'string', description: 'Field/column ID (e.g. "fldXXX")' },
700+
expectedName: { type: 'string', description: 'Expected name — deletion is refused if it does not match' },
701+
},
702+
required: ['fieldId', 'expectedName'],
703+
},
704+
},
705+
force: {
706+
type: 'boolean',
707+
description: 'When true, delete each field even if it has downstream formula/rollup dependencies. Default: false.',
708+
},
709+
checkpointFile: {
710+
type: 'string',
711+
description: 'Absolute path to a JSON file updated after each deletion. Stores remaining fields so the batch can be resumed after a crash.',
712+
},
713+
debug: debugProp,
714+
},
715+
required: ['appId', 'fields'],
716+
},
717+
},
685718

686719
// ── View Tools ──
687720
{
@@ -1805,6 +1838,43 @@ const handlers = {
18051838
return ok(result, result, debug);
18061839
},
18071840

1841+
async delete_fields({ appId, fields, force, checkpointFile, debug }) {
1842+
if (!Array.isArray(fields) || fields.length === 0) {
1843+
return err('fields must be a non-empty array of { fieldId, expectedName } objects');
1844+
}
1845+
1846+
const checkpointPath = typeof checkpointFile === 'string' && checkpointFile ? checkpointFile : null;
1847+
1848+
const result = await client.deleteFields(appId, fields, {
1849+
force: !!force,
1850+
onProgress: checkpointPath
1851+
? async ({ index, total, succeeded, failed }) => {
1852+
const checkpoint = {
1853+
appId,
1854+
processedUpTo: index,
1855+
total,
1856+
remaining: fields.slice(index + 1),
1857+
succeededCount: succeeded,
1858+
failedCount: failed,
1859+
savedAt: new Date().toISOString(),
1860+
};
1861+
await writeFile(checkpointPath, JSON.stringify(checkpoint, null, 2)).catch(() => {});
1862+
}
1863+
: undefined,
1864+
});
1865+
1866+
return ok(
1867+
{
1868+
succeeded: result.succeeded.length,
1869+
failed: result.failed.length,
1870+
total: fields.length,
1871+
...(result.failed.length > 0 ? { failures: result.failed } : {}),
1872+
},
1873+
result,
1874+
debug,
1875+
);
1876+
},
1877+
18081878
// ── View Mutations ──
18091879

18101880
async create_view({ appId, tableId, name, type, copyFromViewId, debug }) {

packages/mcp-server/src/tool-config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const TOOL_CATEGORIES = {
5656

5757
// Field destructive
5858
delete_field: 'field-destructive',
59+
delete_fields: 'field-destructive',
5960

6061
// View mutations (non-destructive)
6162
create_view: 'view-write',

packages/mcp-server/test/test-tool-config.test.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
describe('TOOL_CATEGORIES', () => {
1818
it('maps all tools to valid categories', () => {
1919
const tools = Object.keys(TOOL_CATEGORIES);
20-
assert.equal(tools.length, 63, `Expected 63 tools, got ${tools.length}`);
20+
assert.equal(tools.length, 64, `Expected 64 tools, got ${tools.length}`);
2121
for (const [tool, cat] of Object.entries(TOOL_CATEGORIES)) {
2222
assert.ok(CATEGORY_LABELS[cat], `Tool "${tool}" has unknown category "${cat}"`);
2323
}
@@ -83,9 +83,9 @@ describe('ToolConfigManager', () => {
8383
assert.equal(mgr.activeProfile, 'full');
8484
});
8585

86-
it('enables all 63 tools on full profile', () => {
86+
it('enables all 64 tools on full profile', () => {
8787
const enabled = mgr.enabledToolNames();
88-
assert.equal(enabled.size, 63);
88+
assert.equal(enabled.size, 64);
8989
});
9090

9191
it('manage_tools is always enabled', () => {
@@ -121,10 +121,10 @@ describe('ToolConfigManager', () => {
121121
assert.ok(!enabled.has('create_extension'));
122122
});
123123

124-
it('full enables all 63 tools', async () => {
124+
it('full enables all 64 tools', async () => {
125125
await mgr.switchProfile('full');
126126
const enabled = mgr.enabledToolNames();
127-
assert.equal(enabled.size, 63);
127+
assert.equal(enabled.size, 64);
128128
});
129129
});
130130

@@ -194,10 +194,10 @@ describe('ToolConfigManager', () => {
194194
});
195195

196196
describe('getToolStatus()', () => {
197-
it('returns status for all 63 tools', async () => {
197+
it('returns status for all 64 tools', async () => {
198198
await mgr.switchProfile('full');
199199
const status = mgr.getToolStatus();
200-
assert.equal(status.length, 63);
200+
assert.equal(status.length, 64);
201201
assert.ok(status.every(s => s.enabled === true));
202202
});
203203

0 commit comments

Comments
 (0)