Skip to content

Commit 7f4eebd

Browse files
ARHAEEMclaude
andcommitted
fix(mcp-server): get_view returns real filter/sort/group state
The old implementation read view config from /v0.3/application/{id}/read, which only carries view metadata (id, name, type). Filters, sorts, groupLevels, and columnOrder all came back null regardless of what the view actually had configured — bug report confirmed via capture. Rewrite getView to fetch live state from /v0.3/table/{tableId}/readData with includeDataForViewIds=[viewId], where Airtable stores the real view config under data.viewDatas[]. Maps lastSortsApplied -> sorts and derives visibleColumnOrder from the rich columnOrder array for backcompat. Adds operation=append to apply_view_sorts and update_view_group_levels (matching the existing pattern on update_view_filters) so agents can add to an existing sort/group stack without rewriting it — only possible now that get_view actually returns current state. Also closes pre-existing UI drift: - get_view was silently missing from the extension's 'read' category mirror - table-write / table-destructive categories (create_table, rename_table, delete_table) existed in mcp-server but had no VS Code setting or dashboard toggle — added tableWrite + tableDestructive to ToolCategories, new package.json settings, new SettingToggle rows, updated profile counts (6/23/32 -> 7/26/36) and the "30 MCP tools" description string. - New scripts/check-tool-sync.mjs runs in prebuild and pretest; fails if the extension mirror drifts from mcp-server or if profile counts in package.json go stale. npm metadata: added funding (GitHub Sponsors), author, engines.node, qna (Discussions), sponsor, keyword expansion, structured bugs URL. Added .github/FUNDING.yml so the repo-level Sponsor button surfaces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6ec64ab commit 7f4eebd

File tree

13 files changed

+440
-109
lines changed

13 files changed

+440
-109
lines changed

.github/FUNDING.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# GitHub Sponsor button for this repository.
2+
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
3+
github: [Nskha]

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
"name": "airtable-formula-workspace",
33
"private": true,
44
"scripts": {
5-
"build": "pnpm -F shared build && pnpm -F webview build && node scripts/bundle-mcp.mjs && pnpm -F airtable-formula build",
5+
"build": "node scripts/check-tool-sync.mjs && pnpm -F shared build && pnpm -F webview build && node scripts/bundle-mcp.mjs && pnpm -F airtable-formula build",
66
"dev": "pnpm -F webview dev",
77
"package": "pnpm -F airtable-formula package",
8-
"test": "pnpm -F shared test && pnpm -F webview test && pnpm -F airtable-formula test",
8+
"check:tool-sync": "node scripts/check-tool-sync.mjs",
9+
"test": "node scripts/check-tool-sync.mjs && pnpm -F shared test && pnpm -F airtable-user-mcp test && pnpm -F webview test && pnpm -F airtable-formula test",
910
"packx": "pnpm -F airtable-formula packx",
1011
"packx:no-bump": "pnpm -F airtable-formula packx:no-bump",
1112
"version:bump": "pnpm -F airtable-formula version:bump",

packages/extension/package.json

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,35 @@
77
"description": "Airtable formula editor, MCP server installer, and AI skills for VS Code.",
88
"icon": "images/icon.png",
99
"license": "MIT",
10-
"repository": "https://github.com/Automations-Project/VSCode-Airtable-Formula",
10+
"author": {
11+
"name": "Nskha",
12+
"url": "https://github.com/Nskha"
13+
},
14+
"repository": {
15+
"type": "git",
16+
"url": "https://github.com/Automations-Project/VSCode-Airtable-Formula.git"
17+
},
18+
"homepage": "https://github.com/Automations-Project/VSCode-Airtable-Formula#readme",
19+
"bugs": {
20+
"url": "https://github.com/Automations-Project/VSCode-Airtable-Formula/issues"
21+
},
22+
"qna": "https://github.com/Automations-Project/VSCode-Airtable-Formula/discussions",
23+
"sponsor": {
24+
"url": "https://github.com/sponsors/Nskha"
25+
},
26+
"funding": [
27+
{
28+
"type": "github",
29+
"url": "https://github.com/sponsors/Nskha"
30+
},
31+
{
32+
"type": "individual",
33+
"url": "https://github.com/Automations-Project/VSCode-Airtable-Formula"
34+
}
35+
],
1136
"engines": {
12-
"vscode": "^1.100.0"
37+
"vscode": "^1.100.0",
38+
"node": ">=20"
1339
},
1440
"categories": [
1541
"AI",
@@ -273,17 +299,27 @@
273299
"custom"
274300
],
275301
"enumDescriptions": [
276-
"Schema inspection and formula validation only (6 tools)",
277-
"Read + create/update fields and views, no deletes (23 tools)",
278-
"All tools enabled including destructive and extensions (32 tools)",
302+
"Schema inspection and formula validation only (7 tools)",
303+
"Read + create/update tables, fields, and views, no deletes (26 tools)",
304+
"All tools enabled including destructive and extensions (36 tools)",
279305
"User-defined per-tool selection"
280306
],
281307
"description": "Active MCP tool profile. Controls which tools are exposed to AI agents through the bundled Airtable MCP server."
282308
},
283309
"airtableFormula.mcp.categories.read": {
284310
"type": "boolean",
285311
"default": true,
286-
"description": "Read / Inspect tools: get_base_schema, list_tables, get_table_schema, list_fields, list_views, validate_formula."
312+
"description": "Read / Inspect tools: get_base_schema, list_tables, get_table_schema, list_fields, list_views, get_view, validate_formula."
313+
},
314+
"airtableFormula.mcp.categories.tableWrite": {
315+
"type": "boolean",
316+
"default": true,
317+
"description": "Table Write tools: create_table, rename_table."
318+
},
319+
"airtableFormula.mcp.categories.tableDestructive": {
320+
"type": "boolean",
321+
"default": true,
322+
"description": "Table Destructive tool: delete_table."
287323
},
288324
"airtableFormula.mcp.categories.fieldWrite": {
289325
"type": "boolean",
@@ -341,7 +377,7 @@
341377
{
342378
"id": "AirtableFormula.server",
343379
"label": "Airtable User MCP",
344-
"description": "Manage Airtable bases via 30 MCP tools: schema read, field CRUD (including formula/rollup/lookup), view configuration, formula validation, and extension management. Operates over stdio using JSON-RPC 2.0."
380+
"description": "Manage Airtable bases via 36 MCP tools: schema read, table CRUD, field CRUD (including formula/rollup/lookup), view configuration (filters / sorts / groups / visibility — with merge-safe append mode), formula validation, and extension management. Operates over stdio using JSON-RPC 2.0."
345381
}
346382
]
347383
},

packages/extension/src/mcp/tool-profile.ts

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,37 +20,44 @@ import type { ToolCategories, ToolProfileName, ToolProfileSnapshot } from '@airt
2020
*/
2121

2222
// These must mirror `packages/mcp-server/src/tool-config.js::TOOL_CATEGORIES`.
23-
// If the mcp-server source adds or removes a tool, update this table too —
24-
// a discrepancy only affects the UI's enabled-count display, not correctness.
25-
export const TOOL_CATEGORIES: Record<string, keyof ToolCategories | 'field-destructive' | 'view-destructive'> = {
23+
// Drift is caught at build time by scripts/check-tool-sync.mjs — don't edit
24+
// this table without also editing the mcp-server source (or vice versa).
25+
type ExtraCategoryKey = 'table-write' | 'table-destructive' | 'field-write' | 'field-destructive' | 'view-write' | 'view-destructive';
26+
export const TOOL_CATEGORIES: Record<string, keyof ToolCategories | ExtraCategoryKey> = {
2627
// Read-only / inspection
2728
get_base_schema: 'read',
2829
list_tables: 'read',
2930
get_table_schema: 'read',
3031
list_fields: 'read',
3132
list_views: 'read',
33+
get_view: 'read',
3234
validate_formula: 'read',
35+
// Table mutations (non-destructive)
36+
create_table: 'table-write',
37+
rename_table: 'table-write',
38+
// Table destructive
39+
delete_table: 'table-destructive',
3340
// Field mutations (non-destructive)
34-
create_field: 'fieldWrite',
35-
create_formula_field: 'fieldWrite',
36-
update_field_config: 'fieldWrite',
37-
update_formula_field: 'fieldWrite',
38-
rename_field: 'fieldWrite',
39-
update_field_description: 'fieldWrite',
40-
duplicate_field: 'fieldWrite',
41+
create_field: 'field-write',
42+
create_formula_field: 'field-write',
43+
update_field_config: 'field-write',
44+
update_formula_field: 'field-write',
45+
rename_field: 'field-write',
46+
update_field_description: 'field-write',
47+
duplicate_field: 'field-write',
4148
// Field destructive
4249
delete_field: 'field-destructive',
4350
// View mutations
44-
create_view: 'viewWrite',
45-
duplicate_view: 'viewWrite',
46-
rename_view: 'viewWrite',
47-
update_view_description: 'viewWrite',
48-
update_view_filters: 'viewWrite',
49-
reorder_view_fields: 'viewWrite',
50-
show_or_hide_view_columns: 'viewWrite',
51-
apply_view_sorts: 'viewWrite',
52-
update_view_group_levels: 'viewWrite',
53-
update_view_row_height: 'viewWrite',
51+
create_view: 'view-write',
52+
duplicate_view: 'view-write',
53+
rename_view: 'view-write',
54+
update_view_description: 'view-write',
55+
update_view_filters: 'view-write',
56+
reorder_view_fields: 'view-write',
57+
show_or_hide_view_columns: 'view-write',
58+
apply_view_sorts: 'view-write',
59+
update_view_group_levels: 'view-write',
60+
update_view_row_height: 'view-write',
5461
// View destructive
5562
delete_view: 'view-destructive',
5663
// Extension management
@@ -64,12 +71,14 @@ export const TOOL_CATEGORIES: Record<string, keyof ToolCategories | 'field-destr
6471
};
6572

6673
export const CATEGORY_LABELS: Record<string, string> = {
67-
read: 'Read / Inspect',
68-
fieldWrite: 'Field Write',
69-
'field-destructive':'Field Destructive',
70-
viewWrite: 'View Write',
71-
'view-destructive':'View Destructive',
72-
extension: 'Extension Management',
74+
'read': 'Read / Inspect',
75+
'table-write': 'Table Write',
76+
'table-destructive': 'Table Destructive',
77+
'field-write': 'Field Write',
78+
'field-destructive': 'Field Destructive',
79+
'view-write': 'View Write',
80+
'view-destructive': 'View Destructive',
81+
'extension': 'Extension Management',
7382
};
7483

7584
interface ProfileDef {
@@ -80,18 +89,20 @@ interface ProfileDef {
8089

8190
export const BUILTIN_PROFILES: Record<'read-only' | 'safe-write' | 'full', ProfileDef> = {
8291
'read-only': { description: 'Schema inspection and formula validation only', categories: ['read'] },
83-
'safe-write':{ description: 'Read + create/update fields and views (no deletes)',
84-
categories: ['read', 'fieldWrite', 'viewWrite'] },
92+
'safe-write':{ description: 'Read + create/update tables, fields, and views (no deletes)',
93+
categories: ['read', 'table-write', 'field-write', 'view-write'] },
8594
full: { description: 'All tools enabled including destructive and extensions',
86-
categories: ['read', 'fieldWrite', 'field-destructive', 'viewWrite', 'view-destructive', 'extension'] },
95+
categories: ['read', 'table-write', 'table-destructive', 'field-write', 'field-destructive', 'view-write', 'view-destructive', 'extension'] },
8796
};
8897

8998
// Settings key suffix → file-format category key
9099
const SETTINGS_TO_CATEGORY: Record<keyof ToolCategories, string> = {
91100
read: 'read',
92-
fieldWrite: 'fieldWrite',
101+
tableWrite: 'table-write',
102+
tableDestructive: 'table-destructive',
103+
fieldWrite: 'field-write',
93104
fieldDestructive: 'field-destructive',
94-
viewWrite: 'viewWrite',
105+
viewWrite: 'view-write',
95106
viewDestructive: 'view-destructive',
96107
extension: 'extension',
97108
};
@@ -184,6 +195,8 @@ export class ToolProfileManager implements vscode.Disposable {
184195
const profile = (cfg.get<string>('mcp.toolProfile', 'full') as ToolProfileName) ?? 'full';
185196
const categories: ToolCategories = {
186197
read: cfg.get('mcp.categories.read', true),
198+
tableWrite: cfg.get('mcp.categories.tableWrite', true),
199+
tableDestructive: cfg.get('mcp.categories.tableDestructive', true),
187200
fieldWrite: cfg.get('mcp.categories.fieldWrite', true),
188201
fieldDestructive: cfg.get('mcp.categories.fieldDestructive', true),
189202
viewWrite: cfg.get('mcp.categories.viewWrite', true),
@@ -235,7 +248,7 @@ export class ToolProfileManager implements vscode.Disposable {
235248
'',
236249
];
237250
// Group by category file-key, preserving order
238-
const categoryOrder = ['read', 'fieldWrite', 'field-destructive', 'viewWrite', 'view-destructive', 'extension'];
251+
const categoryOrder = ['read', 'table-write', 'table-destructive', 'field-write', 'field-destructive', 'view-write', 'view-destructive', 'extension'];
239252
for (const cat of categoryOrder) {
240253
const label = CATEGORY_LABELS[cat] ?? cat;
241254
const tools = Object.entries(TOOL_CATEGORIES).filter(([, c]) => c === cat);

packages/mcp-server/package.json

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
"name": "airtable-user-mcp",
33
"version": "2.2.1",
44
"mcpName": "io.github.Automations-Project/airtable-user-mcp",
5-
"description": "Community MCP server for Airtable. Schema inspection, field CRUD, view configuration, formula validation, and extension management. Not affiliated with Airtable Inc.",
5+
"description": "Community MCP server for Airtable. Schema inspection, table / field / view CRUD (including merge-safe filter / sort / group updates), formula validation, and extension management. Not affiliated with Airtable Inc.",
66
"main": "src/index.js",
77
"bin": {
88
"airtable-user-mcp": "src/index.js"
99
},
1010
"type": "module",
11+
"engines": {
12+
"node": ">=20"
13+
},
1114
"files": [
1215
"src/**/*.js",
1316
"src/**/*.mjs",
@@ -24,16 +27,36 @@
2427
"directory": "packages/mcp-server"
2528
},
2629
"homepage": "https://github.com/Automations-Project/VSCode-Airtable-Formula/tree/main/packages/mcp-server#readme",
27-
"bugs": "https://github.com/Automations-Project/VSCode-Airtable-Formula/issues",
30+
"bugs": {
31+
"url": "https://github.com/Automations-Project/VSCode-Airtable-Formula/issues"
32+
},
33+
"author": {
34+
"name": "Nskha",
35+
"url": "https://github.com/Nskha"
36+
},
37+
"funding": [
38+
{
39+
"type": "github",
40+
"url": "https://github.com/sponsors/Nskha"
41+
},
42+
{
43+
"type": "individual",
44+
"url": "https://github.com/Automations-Project/VSCode-Airtable-Formula"
45+
}
46+
],
2847
"keywords": [
2948
"mcp",
3049
"model-context-protocol",
3150
"airtable",
51+
"airtable-api",
3252
"mcp-server",
3353
"ai",
54+
"claude",
55+
"cursor",
3456
"formula",
3557
"schema",
36-
"automation"
58+
"automation",
59+
"no-code"
3760
],
3861
"license": "MIT",
3962
"scripts": {

packages/mcp-server/src/client.js

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -574,32 +574,85 @@ export class AirtableClient {
574574
// ─── View Reads ───────────────────────────────────────────────
575575

576576
/**
577-
* Read a view's current configuration (filters, sorts, grouping, visibility)
578-
* from the base schema. Uses the cached getApplicationData call — no separate
579-
* endpoint is required because the schema already carries every view's state.
577+
* Fetch the live state of one view from `/v0.3/table/{tableId}/readData`.
578+
* The application/read endpoint only carries static view metadata (id, name,
579+
* type) — it does NOT include filters, sorts, groupLevels, or columnOrder.
580+
* Those fields live in table/readData under `data.viewDatas[]`.
581+
*
582+
* Verified against captured traffic 2026-04-18 (appnnJC0PWnw1kVMF).
583+
*/
584+
async readTableData(appId, tableId, viewId) {
585+
const params = new URLSearchParams({
586+
stringifiedObjectParams: JSON.stringify({
587+
includeDataForViewIds: [viewId],
588+
shouldIncludeSchemaChecksum: false,
589+
mayOnlyIncludeRowAndCellDataForIncludedViews: true,
590+
mayExcludeCellDataForLargeViews: true,
591+
}),
592+
requestId: this._genRequestId(),
593+
});
594+
const url = `https://airtable.com/v0.3/table/${tableId}/readData?${params}`;
595+
const res = await this.auth.get(url, appId);
596+
if (!res.ok) {
597+
const text = await res.text();
598+
throw new Error(`readTableData failed (${res.status}): ${text}`);
599+
}
600+
return res.json();
601+
}
602+
603+
/**
604+
* Read a view's current configuration (filters, sorts, grouping, column
605+
* visibility, row height). Uses two calls:
606+
* 1. Cached application/read to resolve tableId + static metadata
607+
* 2. Un-cached table/{tableId}/readData for the live view state
580608
*/
581609
async getView(appId, viewId) {
582610
const data = await this.getApplicationData(appId);
583611
const tables = data?.data?.tableSchemas || data?.data?.tables || [];
612+
let tableId = null;
613+
let staticMeta = null;
584614
for (const table of tables) {
585-
const views = table.views || [];
586-
const view = views.find(v => v.id === viewId);
587-
if (!view) continue;
588-
return {
589-
id: view.id,
590-
name: view.name,
591-
type: view.type,
592-
tableId: table.id,
593-
filters: view.filters ?? view.filtersById ?? null,
594-
sorts: view.sorts ?? null,
595-
groupLevels: view.groupLevels ?? null,
596-
visibleColumnOrder: view.visibleColumnOrder ?? view.columnOrder ?? null,
597-
rowHeight: view.rowHeight ?? null,
598-
description: view.description ?? null,
599-
raw: view,
600-
};
615+
const match = (table.views || []).find(v => v.id === viewId);
616+
if (match) {
617+
tableId = table.id;
618+
staticMeta = match;
619+
break;
620+
}
621+
}
622+
if (!tableId) {
623+
throw new Error(`View "${viewId}" not found in base ${appId}.`);
601624
}
602-
throw new Error(`View "${viewId}" not found in base ${appId}.`);
625+
626+
const tableData = await this.readTableData(appId, tableId, viewId);
627+
const viewDatas = tableData?.data?.viewDatas || [];
628+
const viewData = viewDatas.find(v => v.id === viewId) || {};
629+
630+
const columnOrder = Array.isArray(viewData.columnOrder) ? viewData.columnOrder : null;
631+
const visibleColumnOrder = columnOrder
632+
? columnOrder.filter(c => c && c.visibility !== false).map(c => c.columnId)
633+
: null;
634+
635+
return {
636+
id: viewId,
637+
name: staticMeta?.name ?? null,
638+
type: viewData.type ?? staticMeta?.type ?? null,
639+
tableId,
640+
filters: viewData.filters ?? null,
641+
sorts: viewData.lastSortsApplied ?? null,
642+
groupLevels: viewData.groupLevels ?? null,
643+
columnOrder,
644+
visibleColumnOrder,
645+
frozenColumnCount: viewData.frozenColumnCount ?? null,
646+
colorConfig: viewData.colorConfig ?? null,
647+
metadata: viewData.metadata ?? null,
648+
rowHeight: viewData.rowHeight
649+
?? viewData.metadata?.grid?.rowHeight
650+
?? viewData.metadata?.rowHeight
651+
?? null,
652+
description: viewData.description ?? staticMeta?.description ?? null,
653+
createdByUserId: staticMeta?.createdByUserId ?? viewData.createdByUserId ?? null,
654+
personalForUserId: staticMeta?.personalForUserId ?? null,
655+
};
603656
}
604657

605658
// ─── View Mutations ───────────────────────────────────────────

0 commit comments

Comments
 (0)