@components-kit/open-workbook-mcp-server 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -45,7 +45,14 @@ const STYLE_COPY_TOOL_DIMENSIONS = {
45
45
  "excel.style.copy_hidden_rows_columns": "hiddenRowsColumns"
46
46
  };
47
47
  const runtime = await createRuntimeFacade();
48
- const runtimeVersion = process.env.OPEN_WORKBOOK_VERSION ?? "0.1.7";
48
+ const runtimeVersion = process.env.OPEN_WORKBOOK_VERSION ?? "0.1.8";
49
+ const COMPACT_RESOURCE_LIMIT = 100;
50
+ const COMPACT_DEFAULT_RESOURCE_THRESHOLD_BYTES = 24_000;
51
+ const compactResources = new Map();
52
+ const compactCache = new Map();
53
+ let compactCacheInvalidationCount = 0;
54
+ let compactCacheLastInvalidatedAt;
55
+ let compactLastObservedEventId;
49
56
  const server = new McpServer({
50
57
  name: "open-workbook",
51
58
  version: runtimeVersion
@@ -55,6 +62,7 @@ registerWorkbookTools(server);
55
62
  registerBackupTools(server);
56
63
  registerSheetTools(server);
57
64
  registerRangeTools(server);
65
+ registerLookupTools(server);
58
66
  registerBatchTools(server);
59
67
  registerWorkflowTools(server);
60
68
  registerPlanTools(server);
@@ -81,6 +89,7 @@ registerRepairTools(server);
81
89
  registerSnapshotTools(server);
82
90
  registerDiffTools(server);
83
91
  registerEventTools(server);
92
+ registerCompactTools(server);
84
93
  registerResources(server);
85
94
  registerPrompts(server);
86
95
  await server.connect(new StdioServerTransport());
@@ -211,6 +220,7 @@ function registerResources(mcp) {
211
220
  const planId = resourceVariable(variables, "plan_id");
212
221
  return runtime.getPlanDiffResource(workbookId, planId);
213
222
  });
223
+ registerJsonTemplateResource(mcp, "compact detail resource", "excel://compact/{resource_id}", "Stored compact-context detail payload returned by token-saving Open Workbook tools.", async (_uri, variables) => getCompactResource(resourceVariable(variables, "resource_id")));
214
224
  }
215
225
  function registerJsonResource(mcp, name, uri, description, read) {
216
226
  mcp.registerResource(name, uri, {
@@ -460,6 +470,26 @@ function registerWorkbookTools(mcp) {
460
470
  openWorldHint: false
461
471
  }
462
472
  }, async () => jsonResult(await runtime.getWorkbookMap()));
473
+ registerMcpTool(mcp, "excel.workbook.get_summary", {
474
+ title: "Get compact workbook summary",
475
+ description: "Return compact workbook structure, sheet counts, table names, used-range dimensions, and token telemetry without cell bodies.",
476
+ inputSchema: { workbookId: z.string().optional() },
477
+ annotations: {
478
+ readOnlyHint: true,
479
+ destructiveHint: false,
480
+ openWorldHint: false
481
+ }
482
+ }, async ({ workbookId }) => jsonResult(await workbookSummary(workbookId)));
483
+ registerMcpTool(mcp, "excel.workbook.get_used_range_summary", {
484
+ title: "Get compact workbook used-range summary",
485
+ description: "Return used-range dimensions for workbook sheets without loading cell payloads.",
486
+ inputSchema: { workbookId: z.string().optional() },
487
+ annotations: {
488
+ readOnlyHint: true,
489
+ destructiveHint: false,
490
+ openWorldHint: false
491
+ }
492
+ }, async ({ workbookId }) => jsonResult(await workbookUsedRangeSummary(workbookId)));
463
493
  registerMcpTool(mcp, "excel.workbook.snapshot", {
464
494
  title: "Create workbook snapshot",
465
495
  description: "Capture a restorable snapshot of used ranges or specific ranges.",
@@ -879,6 +909,12 @@ function registerSheetTools(mcp) {
879
909
  inputSchema: { sheetName: z.string() },
880
910
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
881
911
  }, async ({ sheetName }) => jsonResult(await selectSheetInfo(sheetName)));
912
+ registerMcpTool(mcp, "excel.sheet.get_summary", {
913
+ title: "Get compact worksheet summary",
914
+ description: "Return one worksheet's used-range dimensions, table names, and token telemetry without cell bodies.",
915
+ inputSchema: { workbookId: z.string().optional(), sheetName: z.string() },
916
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
917
+ }, async ({ workbookId, sheetName }) => jsonResult(await sheetSummary(sheetName, workbookId)));
882
918
  registerMcpTool(mcp, "excel.sheet.get_used_range", {
883
919
  title: "Get worksheet used range",
884
920
  description: "Return the used range for a worksheet.",
@@ -986,6 +1022,46 @@ function registerRangeTools(mcp) {
986
1022
  }
987
1023
  }, async ({ workbookId, sheetName, address }) => jsonResult(await readRangeSnapshot(workbookId, sheetName, address, rangeReadFacets(name))));
988
1024
  }
1025
+ const compactReadSchema = {
1026
+ ...readSchema,
1027
+ mode: z.enum(["window", "summary", "sample"]).optional(),
1028
+ rowOffset: z.number().int().min(0).optional(),
1029
+ columnOffset: z.number().int().min(0).optional(),
1030
+ maxRows: z.number().int().min(0).max(10000).optional(),
1031
+ maxColumns: z.number().int().min(0).max(1000).optional(),
1032
+ maxCells: z.number().int().min(0).max(1000000).optional(),
1033
+ maxPayloadBytes: z.number().int().min(0).optional(),
1034
+ maxEstimatedTokens: z.number().int().min(0).optional(),
1035
+ includeValues: z.boolean().optional(),
1036
+ includeFormulas: z.boolean().optional(),
1037
+ includeText: z.boolean().optional(),
1038
+ includeNumberFormats: z.boolean().optional(),
1039
+ includeStyles: z.boolean().optional()
1040
+ };
1041
+ registerMcpTool(mcp, "excel.range.read_compact", {
1042
+ title: "Read compact Excel range",
1043
+ description: "Read a bounded values-first range window with opt-in facets, truncation metadata, and token telemetry.",
1044
+ inputSchema: compactReadSchema,
1045
+ annotations: {
1046
+ readOnlyHint: true,
1047
+ destructiveHint: false,
1048
+ openWorldHint: false
1049
+ }
1050
+ }, async (args) => jsonResult(await compactRangeRead(args)));
1051
+ registerMcpTool(mcp, "excel.range.get_summary", {
1052
+ title: "Get compact range summary",
1053
+ description: "Return range dimensions, cell count, default compact-read window, and token telemetry without cell bodies.",
1054
+ inputSchema: {
1055
+ workbookId: z.string(),
1056
+ sheetName: z.string(),
1057
+ address: z.string()
1058
+ },
1059
+ annotations: {
1060
+ readOnlyHint: true,
1061
+ destructiveHint: false,
1062
+ openWorldHint: false
1063
+ }
1064
+ }, async ({ workbookId, sheetName, address }) => jsonResult(rangeSummary(workbookId, sheetName, address)));
989
1065
  for (const [name, method] of [
990
1066
  ["excel.range.read_hyperlinks", "range.read_hyperlinks"],
991
1067
  ["excel.range.read_comments", "range.read_comments"],
@@ -1214,6 +1290,111 @@ function registerRangeTools(mcp) {
1214
1290
  }));
1215
1291
  }
1216
1292
  }
1293
+ function registerLookupTools(mcp) {
1294
+ const budgetSchema = {
1295
+ maxRows: z.number().int().min(0).max(10000).optional(),
1296
+ maxColumns: z.number().int().min(0).max(1000).optional(),
1297
+ maxCells: z.number().int().min(0).max(1000000).optional(),
1298
+ maxPayloadBytes: z.number().int().min(0).optional(),
1299
+ maxEstimatedTokens: z.number().int().min(0).optional()
1300
+ };
1301
+ const lookupAnnotations = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
1302
+ registerMcpTool(mcp, "excel.lookup.search_workbook", {
1303
+ title: "Search workbook compactly",
1304
+ description: "Search sheet names, table schemas, and used ranges across a workbook, returning ranked compact matches instead of full sheets.",
1305
+ inputSchema: {
1306
+ workbookId: z.string(),
1307
+ query: z.string(),
1308
+ sheetNames: z.array(z.string()).optional(),
1309
+ includeSheets: z.boolean().optional(),
1310
+ includeTables: z.boolean().optional(),
1311
+ completeMatch: z.boolean().optional(),
1312
+ matchCase: z.boolean().optional(),
1313
+ maxMatches: z.number().int().min(1).max(1000).optional(),
1314
+ maxPreviewRows: z.number().int().min(0).max(25).optional(),
1315
+ ...budgetSchema
1316
+ },
1317
+ annotations: lookupAnnotations
1318
+ }, async (args) => jsonResult(await lookupSearchWorkbook(args)));
1319
+ registerMcpTool(mcp, "excel.lookup.find_headers", {
1320
+ title: "Find workbook headers compactly",
1321
+ description: "Find matching table columns and likely header cells from bounded top-of-sheet scans.",
1322
+ inputSchema: {
1323
+ workbookId: z.string(),
1324
+ query: z.string().optional(),
1325
+ headers: z.array(z.string()).optional(),
1326
+ sheetNames: z.array(z.string()).optional(),
1327
+ maxRowsPerSheet: z.number().int().min(1).max(100).optional(),
1328
+ maxMatches: z.number().int().min(1).max(1000).optional(),
1329
+ ...budgetSchema
1330
+ },
1331
+ annotations: lookupAnnotations
1332
+ }, async (args) => jsonResult(await lookupFindHeaders(args)));
1333
+ registerMcpTool(mcp, "excel.lookup.find_tables_by_columns", {
1334
+ title: "Find tables by column set",
1335
+ description: "Rank workbook tables by required and optional column names without reading table rows.",
1336
+ inputSchema: {
1337
+ workbookId: z.string(),
1338
+ requiredColumns: z.array(z.string()),
1339
+ optionalColumns: z.array(z.string()).optional(),
1340
+ minScore: z.number().min(0).max(1).optional(),
1341
+ maxMatches: z.number().int().min(1).max(1000).optional(),
1342
+ ...budgetSchema
1343
+ },
1344
+ annotations: lookupAnnotations
1345
+ }, async (args) => jsonResult(await lookupFindTablesByColumns(args)));
1346
+ registerMcpTool(mcp, "excel.lookup.find_entity", {
1347
+ title: "Find entity compactly",
1348
+ description: "Locate a text, number, date, or generic entity across workbook used ranges with compact ranked results.",
1349
+ inputSchema: {
1350
+ workbookId: z.string(),
1351
+ entity: z.string(),
1352
+ kind: z.enum(["text", "number", "date", "any"]).optional(),
1353
+ sheetNames: z.array(z.string()).optional(),
1354
+ completeMatch: z.boolean().optional(),
1355
+ matchCase: z.boolean().optional(),
1356
+ maxMatches: z.number().int().min(1).max(1000).optional(),
1357
+ maxPreviewRows: z.number().int().min(0).max(25).optional(),
1358
+ ...budgetSchema
1359
+ },
1360
+ annotations: lookupAnnotations
1361
+ }, async (args) => jsonResult(await lookupFindEntity(args)));
1362
+ registerMcpTool(mcp, "excel.lookup.resolve_range", {
1363
+ title: "Resolve workbook range target",
1364
+ description: "Resolve a natural target string to a table, column, header, entity, sheet, or A1 range candidate before reading data.",
1365
+ inputSchema: {
1366
+ workbookId: z.string(),
1367
+ target: z.string(),
1368
+ kind: z.enum(["table", "column", "header", "entity", "range", "any"]).optional(),
1369
+ preferredSheetName: z.string().optional(),
1370
+ preferredTableName: z.string().optional(),
1371
+ maxMatches: z.number().int().min(1).max(1000).optional(),
1372
+ ...budgetSchema
1373
+ },
1374
+ annotations: lookupAnnotations
1375
+ }, async (args) => jsonResult(await lookupResolveRange(args)));
1376
+ registerMcpTool(mcp, "excel.lookup.inspect_match", {
1377
+ title: "Inspect lookup match compactly",
1378
+ description: "Read a bounded preview for one lookup match, preserving compact telemetry and avoiding broad sheet reads.",
1379
+ inputSchema: {
1380
+ workbookId: z.string(),
1381
+ matchId: z.string().optional(),
1382
+ kind: z.enum(["sheet", "table", "column", "header", "entity", "range"]).optional(),
1383
+ sheetName: z.string().optional(),
1384
+ tableName: z.string().optional(),
1385
+ columnName: z.string().optional(),
1386
+ address: z.string().optional(),
1387
+ maxRows: z.number().int().min(0).max(10000).optional(),
1388
+ maxColumns: z.number().int().min(0).max(1000).optional(),
1389
+ includeValues: z.boolean().optional(),
1390
+ includeFormulas: z.boolean().optional(),
1391
+ includeText: z.boolean().optional(),
1392
+ maxPayloadBytes: z.number().int().min(0).optional(),
1393
+ maxEstimatedTokens: z.number().int().min(0).optional()
1394
+ },
1395
+ annotations: lookupAnnotations
1396
+ }, async (args) => jsonResult(await lookupInspectMatch(args)));
1397
+ }
1217
1398
  function registerBatchTools(mcp) {
1218
1399
  registerMcpTool(mcp, "excel.batch.validate", {
1219
1400
  title: "Validate Excel batch",
@@ -2596,6 +2777,12 @@ function registerTableTools(mcp) {
2596
2777
  inputSchema: tableSelectorSchema(),
2597
2778
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
2598
2779
  }, async (args) => jsonResult(await runtime.getTableInfo(tableSelector(args))));
2780
+ registerMcpTool(mcp, "excel.table.get_schema", {
2781
+ title: "Get compact Excel table schema",
2782
+ description: "Return table columns, dimensions, style flags, and token telemetry without row data.",
2783
+ inputSchema: tableSelectorSchema(),
2784
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
2785
+ }, async (args) => jsonResult(await tableSchema(tableSelector(args))));
2599
2786
  registerMcpTool(mcp, "excel.table.read", {
2600
2787
  title: "Read Excel table",
2601
2788
  description: "Read table headers, selected data facets, optional columns, optional row page, and metadata.",
@@ -2611,6 +2798,26 @@ function registerTableTools(mcp) {
2611
2798
  },
2612
2799
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
2613
2800
  }, async (args) => jsonResult(await runtime.readTable(tableReadRequest(args))));
2801
+ registerMcpTool(mcp, "excel.table.read_compact", {
2802
+ title: "Read compact Excel table",
2803
+ description: "Read a bounded values-first table page with projection, opt-in facets, truncation metadata, and token telemetry.",
2804
+ inputSchema: {
2805
+ ...tableSelectorSchema(),
2806
+ mode: z.enum(["window", "summary", "sample"]).optional(),
2807
+ includeValues: z.boolean().optional(),
2808
+ includeFormulas: z.boolean().optional(),
2809
+ includeText: z.boolean().optional(),
2810
+ includeNumberFormats: z.boolean().optional(),
2811
+ columns: z.array(z.union([z.string(), z.number().int().min(0)])).optional(),
2812
+ rowOffset: z.number().int().min(0).optional(),
2813
+ maxRows: z.number().int().min(0).max(10000).optional(),
2814
+ maxColumns: z.number().int().min(0).max(1000).optional(),
2815
+ maxCells: z.number().int().min(0).max(1000000).optional(),
2816
+ maxPayloadBytes: z.number().int().min(0).optional(),
2817
+ maxEstimatedTokens: z.number().int().min(0).optional()
2818
+ },
2819
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
2820
+ }, async (args) => jsonResult(await compactTableRead(args)));
2614
2821
  registerMcpTool(mcp, "excel.table.create", {
2615
2822
  title: "Create Excel table",
2616
2823
  description: "Create a structured table from a range, optionally writing values first.",
@@ -3641,6 +3848,36 @@ function registerValidateTools(mcp) {
3641
3848
  inputSchema: { workbookId: z.string() },
3642
3849
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
3643
3850
  }, async ({ workbookId }) => jsonResult(await runtime.validateWorkbook({ workbookId: workbookId })));
3851
+ registerMcpTool(mcp, "excel.validate.compact", {
3852
+ title: "Run compact validation",
3853
+ description: "Run a validator and return concise issue counts/examples with full details stored behind a compact resource URI.",
3854
+ inputSchema: {
3855
+ workbookId: z.string(),
3856
+ validator: z.enum([
3857
+ "workbook",
3858
+ "sheet",
3859
+ "formulas",
3860
+ "styles",
3861
+ "tables",
3862
+ "filters",
3863
+ "print_layout",
3864
+ "no_broken_references",
3865
+ "no_formula_errors",
3866
+ "no_unintended_changes"
3867
+ ]),
3868
+ sheetName: z.string().optional(),
3869
+ address: z.string().optional(),
3870
+ tableName: z.string().optional(),
3871
+ templateId: z.string().optional(),
3872
+ snapshotId: z.string().optional(),
3873
+ leftSnapshotId: z.string().optional(),
3874
+ rightSnapshotId: z.string().optional(),
3875
+ maxIssues: z.number().int().min(0).max(100).optional(),
3876
+ maxPayloadBytes: z.number().int().min(0).optional(),
3877
+ maxEstimatedTokens: z.number().int().min(0).optional()
3878
+ },
3879
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
3880
+ }, async (args) => jsonResult(await compactValidation(args)));
3644
3881
  registerMcpTool(mcp, "excel.validate.sheet", {
3645
3882
  title: "Validate sheet",
3646
3883
  description: "Validate one worksheet's used range and formula-error state.",
@@ -3876,6 +4113,12 @@ function registerSnapshotTools(mcp) {
3876
4113
  inputSchema: { snapshotId: z.string() },
3877
4114
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
3878
4115
  }, async ({ snapshotId }) => jsonResult(runtime.getSnapshot(snapshotId)));
4116
+ registerMcpTool(mcp, "excel.snapshot.get_compact", {
4117
+ title: "Get compact snapshot",
4118
+ description: "Return snapshot metadata and store full snapshot payload behind a compact resource URI.",
4119
+ inputSchema: { snapshotId: z.string(), maxPayloadBytes: z.number().int().min(0).optional(), maxEstimatedTokens: z.number().int().min(0).optional() },
4120
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
4121
+ }, ({ snapshotId, maxPayloadBytes, maxEstimatedTokens }) => jsonResult(compactSnapshot(snapshotId, maxPayloadBytes, maxEstimatedTokens)));
3879
4122
  registerMcpTool(mcp, "excel.snapshot.list", {
3880
4123
  title: "List snapshots",
3881
4124
  description: "List stored snapshots for a workbook.",
@@ -3891,6 +4134,17 @@ function registerSnapshotTools(mcp) {
3891
4134
  },
3892
4135
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
3893
4136
  }, async ({ leftSnapshotId, rightSnapshotId }) => jsonResult(runtime.compareSnapshots(leftSnapshotId, rightSnapshotId)));
4137
+ registerMcpTool(mcp, "excel.snapshot.compare_compact", {
4138
+ title: "Compare snapshots compactly",
4139
+ description: "Return a compact snapshot diff summary and store full diff details behind a compact resource URI.",
4140
+ inputSchema: {
4141
+ leftSnapshotId: z.string(),
4142
+ rightSnapshotId: z.string(),
4143
+ maxPayloadBytes: z.number().int().min(0).optional(),
4144
+ maxEstimatedTokens: z.number().int().min(0).optional()
4145
+ },
4146
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
4147
+ }, ({ leftSnapshotId, rightSnapshotId, maxPayloadBytes, maxEstimatedTokens }) => jsonResult(compactSnapshotDiff(leftSnapshotId, rightSnapshotId, maxPayloadBytes, maxEstimatedTokens)));
3894
4148
  registerMcpTool(mcp, "excel.snapshot.invalidate", {
3895
4149
  title: "Invalidate snapshot",
3896
4150
  description: "Mark a snapshot as stale without deleting it.",
@@ -3919,6 +4173,17 @@ function registerDiffTools(mcp) {
3919
4173
  return jsonResult(name.endsWith("export_json") ? { ok: true, json: JSON.stringify(diff, null, 2) } : diff);
3920
4174
  });
3921
4175
  }
4176
+ registerMcpTool(mcp, "excel.diff.get_compact", {
4177
+ title: "Get compact diff",
4178
+ description: "Return a compact diff summary and store full diff details behind a compact resource URI.",
4179
+ inputSchema: {
4180
+ leftSnapshotId: z.string(),
4181
+ rightSnapshotId: z.string(),
4182
+ maxPayloadBytes: z.number().int().min(0).optional(),
4183
+ maxEstimatedTokens: z.number().int().min(0).optional()
4184
+ },
4185
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
4186
+ }, ({ leftSnapshotId, rightSnapshotId, maxPayloadBytes, maxEstimatedTokens }) => jsonResult(compactSnapshotDiff(leftSnapshotId, rightSnapshotId, maxPayloadBytes, maxEstimatedTokens)));
3922
4187
  registerMcpTool(mcp, "excel.diff.export_html", {
3923
4188
  title: "Export diff HTML",
3924
4189
  description: "Return a small HTML representation of a snapshot diff.",
@@ -3965,6 +4230,44 @@ function registerEventTools(mcp) {
3965
4230
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
3966
4231
  }, async ({ debounceMs }) => jsonResult(runtime.setEventDebounce(debounceMs)));
3967
4232
  }
4233
+ function registerCompactTools(mcp) {
4234
+ registerMcpTool(mcp, "excel.compact.get_resource", {
4235
+ title: "Get compact detail resource",
4236
+ description: "Fetch a stored compact detail payload by resource id or excel://compact URI.",
4237
+ inputSchema: { resourceId: z.string().optional(), resourceUri: z.string().optional() },
4238
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
4239
+ }, ({ resourceId, resourceUri }) => jsonResult(getCompactResource(resourceId ?? compactResourceIdFromUri(resourceUri ?? ""))));
4240
+ registerMcpTool(mcp, "excel.compact.list_resources", {
4241
+ title: "List compact detail resources",
4242
+ description: "List stored compact detail resources without returning their payloads.",
4243
+ inputSchema: { kind: z.string().optional(), limit: z.number().int().min(1).max(COMPACT_RESOURCE_LIMIT).optional() },
4244
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
4245
+ }, ({ kind, limit }) => jsonResult(listCompactResources(kind, limit)));
4246
+ registerMcpTool(mcp, "excel.compact.delete_resource", {
4247
+ title: "Delete compact detail resource",
4248
+ description: "Delete one stored compact detail payload.",
4249
+ inputSchema: { resourceId: z.string().optional(), resourceUri: z.string().optional() },
4250
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
4251
+ }, ({ resourceId, resourceUri }) => jsonResult(deleteCompactResource(resourceId ?? compactResourceIdFromUri(resourceUri ?? ""))));
4252
+ registerMcpTool(mcp, "excel.compact.clear_resources", {
4253
+ title: "Clear compact detail resources",
4254
+ description: "Clear stored compact detail payloads from this MCP process.",
4255
+ inputSchema: { kind: z.string().optional() },
4256
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
4257
+ }, ({ kind }) => jsonResult(clearCompactResources(kind)));
4258
+ registerMcpTool(mcp, "excel.compact.get_cache_status", {
4259
+ title: "Get compact cache status",
4260
+ description: "Return compact summary/schema cache size and invalidation metadata.",
4261
+ inputSchema: {},
4262
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
4263
+ }, () => jsonResult(getCompactCacheStatus()));
4264
+ registerMcpTool(mcp, "excel.compact.clear_cache", {
4265
+ title: "Clear compact cache",
4266
+ description: "Clear compact summary/schema cache entries from this MCP process.",
4267
+ inputSchema: {},
4268
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
4269
+ }, () => jsonResult(clearCompactCache("manual")));
4270
+ }
3968
4271
  function rangeReadFacets(toolName) {
3969
4272
  switch (toolName) {
3970
4273
  case "excel.range.read_values":
@@ -4048,6 +4351,1305 @@ function tableReadRequest(args) {
4048
4351
  }
4049
4352
  return request;
4050
4353
  }
4354
+ function withCompactTelemetry(payload, options) {
4355
+ const originalPayloadBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
4356
+ const originalEstimatedTokens = Math.ceil(originalPayloadBytes / 4);
4357
+ const maxPayloadBytes = options.maxPayloadBytes ?? (options.budgetSummary !== undefined ? COMPACT_DEFAULT_RESOURCE_THRESHOLD_BYTES : undefined);
4358
+ const overBudget = (maxPayloadBytes !== undefined && originalPayloadBytes > maxPayloadBytes) ||
4359
+ (options.maxEstimatedTokens !== undefined && originalEstimatedTokens > options.maxEstimatedTokens);
4360
+ const stored = options.storeResource || overBudget
4361
+ ? storeCompactResource(options.resourceKind ?? "generic", options.resourcePayload ?? payload, options.resourceTitle)
4362
+ : undefined;
4363
+ const output = overBudget && options.budgetSummary !== undefined
4364
+ ? {
4365
+ ...options.budgetSummary,
4366
+ budgetExceeded: true,
4367
+ warnings: [
4368
+ ...asWarningArray(options.budgetSummary.warnings),
4369
+ {
4370
+ code: "COMPACT_BUDGET_EXCEEDED",
4371
+ message: "Full compact detail exceeded the response budget and was stored behind resourceUri."
4372
+ }
4373
+ ],
4374
+ resourcePayloadBytes: originalPayloadBytes,
4375
+ resourceEstimatedTokens: originalEstimatedTokens
4376
+ }
4377
+ : payload;
4378
+ const payloadBytes = Buffer.byteLength(JSON.stringify(output), "utf8");
4379
+ return {
4380
+ ...output,
4381
+ payloadBytes,
4382
+ estimatedTokens: Math.ceil(payloadBytes / 4),
4383
+ truncated: options.truncated ?? false,
4384
+ detailLevel: options.detailLevel,
4385
+ ...(options.nextPage !== undefined ? { nextPage: options.nextPage } : {}),
4386
+ ...(stored !== undefined ? { resourceUri: stored.uri } : options.resourceUri !== undefined ? { resourceUri: options.resourceUri } : {})
4387
+ };
4388
+ }
4389
+ function asWarningArray(value) {
4390
+ return Array.isArray(value) ? value.filter((item) => typeof item === "object" && item !== null) : [];
4391
+ }
4392
+ function storeCompactResource(kind, payload, title) {
4393
+ const resourceId = makeId("compact");
4394
+ const payloadBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
4395
+ const resource = {
4396
+ resourceId,
4397
+ uri: `excel://compact/${resourceId}`,
4398
+ kind,
4399
+ title,
4400
+ createdAt: new Date().toISOString(),
4401
+ payloadBytes,
4402
+ estimatedTokens: Math.ceil(payloadBytes / 4),
4403
+ payload
4404
+ };
4405
+ compactResources.set(resourceId, resource);
4406
+ while (compactResources.size > COMPACT_RESOURCE_LIMIT) {
4407
+ const oldestKey = compactResources.keys().next().value;
4408
+ if (oldestKey === undefined) {
4409
+ break;
4410
+ }
4411
+ compactResources.delete(oldestKey);
4412
+ }
4413
+ return resource;
4414
+ }
4415
+ function getCompactResource(resourceId) {
4416
+ const resource = compactResources.get(compactResourceIdFromUri(resourceId));
4417
+ if (!resource) {
4418
+ return { ok: false, error: { code: "COMPACT_RESOURCE_NOT_FOUND", message: "Compact detail resource was not found or has expired." } };
4419
+ }
4420
+ return { ok: true, ...resource };
4421
+ }
4422
+ function listCompactResources(kind, limit = COMPACT_RESOURCE_LIMIT) {
4423
+ const resources = [...compactResources.values()]
4424
+ .filter((resource) => kind === undefined || resource.kind === kind)
4425
+ .slice(-limit)
4426
+ .map(({ payload, ...summary }) => summary);
4427
+ return { ok: true, resources };
4428
+ }
4429
+ function deleteCompactResource(resourceId) {
4430
+ const normalized = compactResourceIdFromUri(resourceId);
4431
+ return { ok: compactResources.delete(normalized), resourceId: normalized };
4432
+ }
4433
+ function clearCompactResources(kind) {
4434
+ let deleted = 0;
4435
+ for (const [resourceId, resource] of compactResources) {
4436
+ if (kind === undefined || resource.kind === kind) {
4437
+ compactResources.delete(resourceId);
4438
+ deleted += 1;
4439
+ }
4440
+ }
4441
+ return { ok: true, deleted };
4442
+ }
4443
+ function compactResourceIdFromUri(value) {
4444
+ return value.startsWith("excel://compact/") ? value.slice("excel://compact/".length) : value;
4445
+ }
4446
+ async function compactCacheValue(key, producer) {
4447
+ await invalidateCompactCacheForWorkbookEvents();
4448
+ const cached = compactCache.get(key);
4449
+ if (cached !== undefined) {
4450
+ return cached.value;
4451
+ }
4452
+ const value = await producer();
4453
+ compactCache.set(key, { key, createdAt: new Date().toISOString(), value });
4454
+ return value;
4455
+ }
4456
+ async function invalidateCompactCacheForWorkbookEvents() {
4457
+ try {
4458
+ const recent = await runtime.getRecentEvents(1);
4459
+ const event = recent.events?.[0];
4460
+ if (!event?.eventId || event.eventId === compactLastObservedEventId) {
4461
+ return;
4462
+ }
4463
+ compactLastObservedEventId = event.eventId;
4464
+ if (event.method !== "addin.heartbeat") {
4465
+ clearCompactCache(`event:${event.method ?? "unknown"}`);
4466
+ }
4467
+ }
4468
+ catch {
4469
+ // Compact caching must never make read-only workbook discovery fail.
4470
+ }
4471
+ }
4472
+ function getCompactCacheStatus() {
4473
+ return {
4474
+ ok: true,
4475
+ size: compactCache.size,
4476
+ invalidationCount: compactCacheInvalidationCount,
4477
+ lastInvalidatedAt: compactCacheLastInvalidatedAt,
4478
+ keys: [...compactCache.keys()]
4479
+ };
4480
+ }
4481
+ function clearCompactCache(reason) {
4482
+ const cleared = compactCache.size;
4483
+ compactCache.clear();
4484
+ compactCacheInvalidationCount += 1;
4485
+ compactCacheLastInvalidatedAt = new Date().toISOString();
4486
+ return { ok: true, cleared, reason, invalidationCount: compactCacheInvalidationCount, invalidatedAt: compactCacheLastInvalidatedAt };
4487
+ }
4488
+ async function workbookSummary(workbookId) {
4489
+ return compactCacheValue(`workbookSummary:${workbookId ?? "active"}`, async () => {
4490
+ const result = await runtime.getWorkbookMap();
4491
+ const map = "map" in result ? result.map : undefined;
4492
+ if (!result.ok || !map) {
4493
+ return withCompactTelemetry({ ok: false, workbookId, source: result }, { detailLevel: "summary" });
4494
+ }
4495
+ const sheets = map.sheets ?? [];
4496
+ const tableNames = sheets.flatMap((sheet) => (sheet.tables ?? []).map((table) => table.name));
4497
+ const usedRangeCells = sheets.reduce((sum, sheet) => sum + usedRangeCellCount(sheet.usedRange), 0);
4498
+ return withCompactTelemetry({
4499
+ ok: true,
4500
+ workbook: map.workbook,
4501
+ workbookId: workbookId ?? map.workbook?.workbookId,
4502
+ sheetCount: sheets.length,
4503
+ tableCount: tableNames.length,
4504
+ usedRangeCells,
4505
+ sheets: sheets.map((sheet) => ({
4506
+ name: sheet.name,
4507
+ position: sheet.position,
4508
+ visibility: sheet.visibility,
4509
+ usedRange: sheet.usedRange,
4510
+ tableCount: sheet.tables?.length ?? 0,
4511
+ tables: (sheet.tables ?? []).map((table) => table.name)
4512
+ }))
4513
+ }, { detailLevel: "summary" });
4514
+ });
4515
+ }
4516
+ async function workbookUsedRangeSummary(workbookId) {
4517
+ return compactCacheValue(`workbookUsedRangeSummary:${workbookId ?? "active"}`, async () => {
4518
+ const result = await runtime.getWorkbookMap();
4519
+ const map = "map" in result ? result.map : undefined;
4520
+ if (!result.ok || !map) {
4521
+ return withCompactTelemetry({ ok: false, workbookId, source: result }, { detailLevel: "summary" });
4522
+ }
4523
+ const sheets = map.sheets ?? [];
4524
+ return withCompactTelemetry({
4525
+ ok: true,
4526
+ workbookId: workbookId ?? map.workbook?.workbookId,
4527
+ usedRanges: sheets.map((sheet) => ({
4528
+ sheetName: sheet.name,
4529
+ usedRange: sheet.usedRange,
4530
+ cellCount: usedRangeCellCount(sheet.usedRange)
4531
+ })),
4532
+ totalCells: sheets.reduce((sum, sheet) => sum + usedRangeCellCount(sheet.usedRange), 0)
4533
+ }, { detailLevel: "summary" });
4534
+ });
4535
+ }
4536
+ async function sheetSummary(sheetName, workbookId) {
4537
+ return compactCacheValue(`sheetSummary:${workbookId ?? "active"}:${sheetName}`, async () => {
4538
+ const info = await selectSheetInfo(sheetName);
4539
+ const workbook = info.result.map?.workbook;
4540
+ return withCompactTelemetry({
4541
+ ok: info.ok,
4542
+ workbookId: workbookId ?? workbook?.workbookId,
4543
+ sheetName,
4544
+ usedRange: info.sheet?.usedRange,
4545
+ cellCount: usedRangeCellCount(info.sheet?.usedRange),
4546
+ tableCount: info.sheet?.tables?.length ?? 0,
4547
+ tables: (info.sheet?.tables ?? []).map((table) => table.name),
4548
+ sheet: info.sheet
4549
+ }, { detailLevel: "summary" });
4550
+ });
4551
+ }
4552
+ function rangeSummary(workbookId, sheetName, address) {
4553
+ const parsed = parseCompactA1Address(address);
4554
+ const rowCount = parsed.endRow - parsed.startRow + 1;
4555
+ const columnCount = parsed.endColumn - parsed.startColumn + 1;
4556
+ const defaultRows = Math.min(rowCount, 50);
4557
+ const defaultColumns = Math.min(columnCount, 25);
4558
+ return withCompactTelemetry({
4559
+ ok: true,
4560
+ workbookId,
4561
+ sheetName,
4562
+ address,
4563
+ rowCount,
4564
+ columnCount,
4565
+ cellCount: rowCount * columnCount,
4566
+ defaultCompactWindow: {
4567
+ address: compactWindowAddress(address, 0, 0, defaultRows, defaultColumns),
4568
+ rowCount: defaultRows,
4569
+ columnCount: defaultColumns,
4570
+ cellCount: defaultRows * defaultColumns
4571
+ }
4572
+ }, { detailLevel: "summary", truncated: rowCount > defaultRows || columnCount > defaultColumns });
4573
+ }
4574
+ async function compactRangeRead(args) {
4575
+ const mode = args.mode ?? "window";
4576
+ const parsed = parseCompactA1Address(args.address);
4577
+ const sourceRowCount = parsed.endRow - parsed.startRow + 1;
4578
+ const sourceColumnCount = parsed.endColumn - parsed.startColumn + 1;
4579
+ const rowOffset = Math.min(args.rowOffset ?? 0, sourceRowCount);
4580
+ const columnOffset = Math.min(args.columnOffset ?? 0, sourceColumnCount);
4581
+ const maxRows = args.maxRows ?? 50;
4582
+ const maxColumns = args.maxColumns ?? 25;
4583
+ const availableRows = Math.max(0, sourceRowCount - rowOffset);
4584
+ const availableColumns = Math.max(0, sourceColumnCount - columnOffset);
4585
+ const columnLimit = Math.min(maxColumns, availableColumns);
4586
+ const maxRowsByCells = args.maxCells !== undefined && columnLimit > 0 ? Math.floor(args.maxCells / columnLimit) : maxRows;
4587
+ const rowLimit = Math.min(maxRows, maxRowsByCells, availableRows);
4588
+ const windowAddress = rowLimit > 0 && columnLimit > 0
4589
+ ? compactWindowAddress(args.address, rowOffset, columnOffset, rowLimit, columnLimit)
4590
+ : compactWindowAddress(args.address, rowOffset, columnOffset, 1, 1);
4591
+ const truncated = rowOffset + rowLimit < sourceRowCount || columnOffset + columnLimit < sourceColumnCount;
4592
+ const summary = {
4593
+ workbookId: args.workbookId,
4594
+ sheetName: args.sheetName,
4595
+ address: args.address,
4596
+ mode,
4597
+ source: {
4598
+ rowCount: sourceRowCount,
4599
+ columnCount: sourceColumnCount,
4600
+ cellCount: sourceRowCount * sourceColumnCount
4601
+ },
4602
+ window: {
4603
+ address: windowAddress,
4604
+ rowOffset,
4605
+ columnOffset,
4606
+ rowCount: rowLimit,
4607
+ columnCount: columnLimit,
4608
+ cellCount: rowLimit * columnLimit
4609
+ },
4610
+ sampled: mode === "sample"
4611
+ };
4612
+ const nextPage = truncated ? { rowOffset: rowOffset + rowLimit, columnOffset } : undefined;
4613
+ if (mode === "summary" || rowLimit === 0 || columnLimit === 0) {
4614
+ return withCompactTelemetry({ ok: true, ...summary }, { detailLevel: "summary", truncated, nextPage, maxPayloadBytes: args.maxPayloadBytes, maxEstimatedTokens: args.maxEstimatedTokens, resourceKind: "read", resourceTitle: "Compact range summary", budgetSummary: { ok: true, ...summary } });
4615
+ }
4616
+ const facets = compactRangeFacets(args);
4617
+ if (facets.length === 0) {
4618
+ return withCompactTelemetry({ ok: true, ...summary }, { detailLevel: "compact", truncated, nextPage, maxPayloadBytes: args.maxPayloadBytes, maxEstimatedTokens: args.maxEstimatedTokens, resourceKind: "read", resourceTitle: "Compact range read", budgetSummary: { ok: true, ...summary } });
4619
+ }
4620
+ if (mode === "sample" && sourceRowCount > rowLimit) {
4621
+ const samples = await Promise.all(compactSampleWindows(sourceRowCount, rowLimit).map(async (sample) => {
4622
+ const sampleAddress = compactWindowAddress(args.address, sample.rowOffset, columnOffset, sample.rowCount, columnLimit);
4623
+ const sampleResult = await readRangeSnapshot(args.workbookId, args.sheetName, sampleAddress, facets);
4624
+ const sampleSnapshot = (sampleResult.data ?? [])[0]?.snapshot;
4625
+ return {
4626
+ label: sample.label,
4627
+ rowOffset: sample.rowOffset,
4628
+ rowCount: sample.rowCount,
4629
+ address: sampleAddress,
4630
+ fingerprint: sampleSnapshot?.fingerprint,
4631
+ values: sampleSnapshot?.values,
4632
+ formulas: sampleSnapshot?.formulas,
4633
+ text: sampleSnapshot?.text,
4634
+ numberFormat: sampleSnapshot?.numberFormat,
4635
+ style: sampleSnapshot?.style,
4636
+ ok: sampleResult.ok,
4637
+ warnings: sampleResult.warnings ?? []
4638
+ };
4639
+ }));
4640
+ return withCompactTelemetry({ ok: true, ...summary, samples }, {
4641
+ detailLevel: "compact",
4642
+ truncated,
4643
+ maxPayloadBytes: args.maxPayloadBytes,
4644
+ maxEstimatedTokens: args.maxEstimatedTokens,
4645
+ resourceKind: "read",
4646
+ resourceTitle: "Compact range sample",
4647
+ budgetSummary: { ok: true, ...summary, sampleCount: samples.length }
4648
+ });
4649
+ }
4650
+ const result = await readRangeSnapshot(args.workbookId, args.sheetName, windowAddress, facets);
4651
+ if (!result.ok) {
4652
+ return withCompactTelemetry({ ok: false, ...summary, source: result }, { detailLevel: "compact", truncated, nextPage, maxPayloadBytes: args.maxPayloadBytes, maxEstimatedTokens: args.maxEstimatedTokens, resourceKind: "read", resourceTitle: "Compact range read error", budgetSummary: { ok: false, ...summary } });
4653
+ }
4654
+ const snapshot = (result.data ?? [])[0]?.snapshot;
4655
+ return withCompactTelemetry({
4656
+ ok: true,
4657
+ ...summary,
4658
+ fingerprint: snapshot?.fingerprint,
4659
+ values: snapshot?.values,
4660
+ formulas: snapshot?.formulas,
4661
+ text: snapshot?.text,
4662
+ numberFormat: snapshot?.numberFormat,
4663
+ style: snapshot?.style,
4664
+ warnings: result.warnings ?? [],
4665
+ telemetry: result.telemetry
4666
+ }, {
4667
+ detailLevel: "compact",
4668
+ truncated,
4669
+ nextPage,
4670
+ maxPayloadBytes: args.maxPayloadBytes,
4671
+ maxEstimatedTokens: args.maxEstimatedTokens,
4672
+ resourceKind: "read",
4673
+ resourceTitle: "Compact range read",
4674
+ budgetSummary: { ok: true, ...summary, warnings: result.warnings ?? [] }
4675
+ });
4676
+ }
4677
+ async function tableSchema(selector) {
4678
+ return compactCacheValue(`tableSchema:${selector.workbookId}:${selector.tableName}`, async () => {
4679
+ const result = await runtime.getTableInfo(selector);
4680
+ const info = result.info;
4681
+ return withCompactTelemetry({
4682
+ ok: Boolean(result.ok && info),
4683
+ workbookId: selector.workbookId,
4684
+ tableName: selector.tableName,
4685
+ schema: info ? tableInfoSchema(info) : undefined,
4686
+ source: info ? undefined : result
4687
+ }, { detailLevel: "summary" });
4688
+ });
4689
+ }
4690
+ async function compactTableRead(args) {
4691
+ const mode = args.mode ?? "window";
4692
+ const schemaResult = await runtime.getTableInfo(tableSelector(args));
4693
+ const info = schemaResult.info;
4694
+ if (!schemaResult.ok || !info) {
4695
+ return withCompactTelemetry({ ok: false, workbookId: args.workbookId, tableName: args.tableName, source: schemaResult }, { detailLevel: "summary" });
4696
+ }
4697
+ const selectedColumns = compactTableColumns(info, args.columns, args.maxColumns);
4698
+ const rowOffset = Math.min(args.rowOffset ?? 0, info.rowCount);
4699
+ const maxRows = args.maxRows ?? 50;
4700
+ const maxRowsByCells = args.maxCells !== undefined && selectedColumns.length > 0 ? Math.floor(args.maxCells / selectedColumns.length) : maxRows;
4701
+ const rowLimit = Math.min(maxRows, maxRowsByCells, Math.max(0, info.rowCount - rowOffset));
4702
+ const truncated = rowOffset + rowLimit < info.rowCount || selectedColumns.length < info.columnCount;
4703
+ const summary = {
4704
+ workbookId: args.workbookId,
4705
+ tableName: args.tableName,
4706
+ mode,
4707
+ schema: tableInfoSchema(info),
4708
+ rowOffset,
4709
+ rowLimit,
4710
+ projectedColumns: selectedColumns,
4711
+ sampled: mode === "sample"
4712
+ };
4713
+ const nextPage = truncated && rowOffset + rowLimit < info.rowCount ? { rowOffset: rowOffset + rowLimit } : undefined;
4714
+ if (mode === "summary" || rowLimit === 0 || selectedColumns.length === 0) {
4715
+ return withCompactTelemetry({ ok: true, ...summary }, { detailLevel: "summary", truncated, nextPage, maxPayloadBytes: args.maxPayloadBytes, maxEstimatedTokens: args.maxEstimatedTokens, resourceKind: "read", resourceTitle: "Compact table summary", budgetSummary: { ok: true, ...summary } });
4716
+ }
4717
+ if (mode === "sample" && info.rowCount > rowLimit) {
4718
+ const samples = await Promise.all(compactSampleWindows(info.rowCount, rowLimit).map(async (sample) => {
4719
+ const sampleResult = await runtime.readTable({
4720
+ ...tableSelector(args),
4721
+ includeValues: args.includeValues ?? true,
4722
+ includeFormulas: args.includeFormulas === true,
4723
+ includeText: args.includeText === true,
4724
+ includeNumberFormats: args.includeNumberFormats === true,
4725
+ columns: selectedColumns.map((column) => column.name),
4726
+ rowOffset: sample.rowOffset,
4727
+ rowLimit: sample.rowCount
4728
+ });
4729
+ const sampleTable = sampleResult.table;
4730
+ return {
4731
+ label: sample.label,
4732
+ rowOffset: sample.rowOffset,
4733
+ rowCount: sample.rowCount,
4734
+ ok: sampleResult.ok,
4735
+ headers: sampleTable?.headers,
4736
+ values: sampleTable?.values,
4737
+ formulas: sampleTable?.formulas,
4738
+ text: sampleTable?.text,
4739
+ numberFormat: sampleTable?.numberFormat
4740
+ };
4741
+ }));
4742
+ return withCompactTelemetry({ ok: true, ...summary, samples }, {
4743
+ detailLevel: "compact",
4744
+ truncated,
4745
+ maxPayloadBytes: args.maxPayloadBytes,
4746
+ maxEstimatedTokens: args.maxEstimatedTokens,
4747
+ resourceKind: "read",
4748
+ resourceTitle: "Compact table sample",
4749
+ budgetSummary: { ok: true, ...summary, sampleCount: samples.length }
4750
+ });
4751
+ }
4752
+ const result = await runtime.readTable({
4753
+ ...tableSelector(args),
4754
+ includeValues: args.includeValues ?? true,
4755
+ includeFormulas: args.includeFormulas === true,
4756
+ includeText: args.includeText === true,
4757
+ includeNumberFormats: args.includeNumberFormats === true,
4758
+ columns: selectedColumns.map((column) => column.name),
4759
+ rowOffset,
4760
+ rowLimit
4761
+ });
4762
+ const table = result.table;
4763
+ return withCompactTelemetry({
4764
+ ok: Boolean(result.ok),
4765
+ ...summary,
4766
+ headers: table?.headers,
4767
+ values: table?.values,
4768
+ formulas: table?.formulas,
4769
+ text: table?.text,
4770
+ numberFormat: table?.numberFormat,
4771
+ source: result.ok ? undefined : result
4772
+ }, {
4773
+ detailLevel: "compact",
4774
+ truncated,
4775
+ nextPage,
4776
+ maxPayloadBytes: args.maxPayloadBytes,
4777
+ maxEstimatedTokens: args.maxEstimatedTokens,
4778
+ resourceKind: "read",
4779
+ resourceTitle: "Compact table read",
4780
+ budgetSummary: { ok: Boolean(result.ok), ...summary }
4781
+ });
4782
+ }
4783
+ async function lookupSearchWorkbook(args) {
4784
+ const workbookId = args.workbookId;
4785
+ const context = await lookupWorkbookContext(workbookId, args.sheetNames);
4786
+ if (!context.ok) {
4787
+ return withCompactTelemetry({ ok: false, workbookId, query: args.query, source: context.source }, { detailLevel: "summary" });
4788
+ }
4789
+ const matches = [];
4790
+ if (args.includeSheets !== false) {
4791
+ for (const sheet of context.sheets) {
4792
+ if (lookupMatches(String(sheet.name), args.query, args)) {
4793
+ matches.push(lookupMatch({
4794
+ kind: "sheet",
4795
+ sheetName: sheet.name,
4796
+ address: sheet.usedRange?.address,
4797
+ score: lookupScore(sheet.name, args.query, args.completeMatch),
4798
+ reason: "sheet name"
4799
+ }));
4800
+ }
4801
+ }
4802
+ }
4803
+ if (args.includeTables !== false) {
4804
+ matches.push(...lookupTableMatches(context.tables, args.query, args));
4805
+ }
4806
+ matches.push(...await lookupUsedRangeMatches(workbookId, context.sheets, args.query, args, "range"));
4807
+ return compactLookupResponse({
4808
+ workbookId,
4809
+ query: args.query,
4810
+ matches,
4811
+ maxMatches: args.maxMatches ?? 25,
4812
+ maxPayloadBytes: args.maxPayloadBytes,
4813
+ maxEstimatedTokens: args.maxEstimatedTokens,
4814
+ resourceTitle: `Workbook lookup: ${args.query}`
4815
+ });
4816
+ }
4817
+ async function lookupFindHeaders(args) {
4818
+ const workbookId = args.workbookId;
4819
+ const terms = lookupTerms(args.headers ?? (args.query ? [args.query] : []));
4820
+ const context = await lookupWorkbookContext(workbookId, args.sheetNames);
4821
+ if (!context.ok) {
4822
+ return withCompactTelemetry({ ok: false, workbookId, query: args.query, headers: args.headers, source: context.source }, { detailLevel: "summary" });
4823
+ }
4824
+ const matches = [];
4825
+ for (const table of context.tables) {
4826
+ for (const column of table.columns ?? []) {
4827
+ const columnName = String(column.name ?? "");
4828
+ if (terms.length === 0 || terms.some((term) => lookupMatches(columnName, term, args))) {
4829
+ matches.push(lookupMatch({
4830
+ kind: "header",
4831
+ sheetName: table.sheetName,
4832
+ tableName: table.tableName,
4833
+ columnName,
4834
+ address: table.headerAddress ?? table.address,
4835
+ score: terms.length === 0 ? 0.55 : Math.max(...terms.map((term) => lookupScore(columnName, term, false))),
4836
+ reason: "table header",
4837
+ schema: { tableName: table.tableName, column }
4838
+ }));
4839
+ }
4840
+ }
4841
+ }
4842
+ const maxRowsPerSheet = args.maxRowsPerSheet ?? 10;
4843
+ const maxColumns = args.maxColumns ?? 50;
4844
+ for (const sheet of context.sheets) {
4845
+ const usedAddress = sheet.usedRange?.address;
4846
+ if (!usedAddress) {
4847
+ continue;
4848
+ }
4849
+ try {
4850
+ const result = await compactRangeRead({
4851
+ workbookId,
4852
+ sheetName: sheet.name,
4853
+ address: usedAddress,
4854
+ mode: "window",
4855
+ maxRows: maxRowsPerSheet,
4856
+ maxColumns,
4857
+ includeValues: true,
4858
+ includeText: true
4859
+ });
4860
+ const rows = lookupRowsFromCompactRead(result);
4861
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
4862
+ for (let columnIndex = 0; columnIndex < (rows[rowIndex] ?? []).length; columnIndex += 1) {
4863
+ const value = rows[rowIndex]?.[columnIndex];
4864
+ const text = value === undefined || value === null ? "" : String(value);
4865
+ if (text === "" || (terms.length > 0 && !terms.some((term) => lookupMatches(text, term, args)))) {
4866
+ continue;
4867
+ }
4868
+ matches.push(lookupMatch({
4869
+ kind: "header",
4870
+ sheetName: sheet.name,
4871
+ address: compactWindowAddress(usedAddress, rowIndex, columnIndex, 1, 1),
4872
+ columnName: text,
4873
+ score: terms.length === 0 ? Math.max(0.15, 0.5 - rowIndex * 0.03) : Math.max(...terms.map((term) => lookupScore(text, term, false))),
4874
+ reason: `bounded sheet header scan row ${rowIndex + 1}`
4875
+ }));
4876
+ }
4877
+ }
4878
+ }
4879
+ catch {
4880
+ // Lookup should remain useful even when one sheet cannot be scanned.
4881
+ }
4882
+ }
4883
+ return compactLookupResponse({
4884
+ workbookId,
4885
+ query: args.query,
4886
+ headers: args.headers,
4887
+ matches,
4888
+ maxMatches: args.maxMatches ?? 25,
4889
+ maxPayloadBytes: args.maxPayloadBytes,
4890
+ maxEstimatedTokens: args.maxEstimatedTokens,
4891
+ resourceTitle: "Header lookup"
4892
+ });
4893
+ }
4894
+ async function lookupFindTablesByColumns(args) {
4895
+ const workbookId = args.workbookId;
4896
+ const context = await lookupWorkbookContext(workbookId);
4897
+ if (!context.ok) {
4898
+ return withCompactTelemetry({ ok: false, workbookId, requiredColumns: args.requiredColumns, source: context.source }, { detailLevel: "summary" });
4899
+ }
4900
+ const required = lookupTerms(args.requiredColumns);
4901
+ const optional = lookupTerms(args.optionalColumns ?? []);
4902
+ const matches = context.tables
4903
+ .map((table) => {
4904
+ const columns = (table.columns ?? []).map((column) => String(column.name ?? ""));
4905
+ const requiredHits = required.filter((term) => columns.some((column) => lookupNormalized(column) === term || lookupNormalized(column).includes(term)));
4906
+ if (required.length > 0 && requiredHits.length < required.length) {
4907
+ return undefined;
4908
+ }
4909
+ const optionalHits = optional.filter((term) => columns.some((column) => lookupNormalized(column) === term || lookupNormalized(column).includes(term)));
4910
+ const denominator = Math.max(1, required.length + optional.length * 0.5);
4911
+ const score = Math.min(1, (requiredHits.length + optionalHits.length * 0.5) / denominator);
4912
+ if (score < (args.minScore ?? 0)) {
4913
+ return undefined;
4914
+ }
4915
+ return lookupMatch({
4916
+ kind: "table",
4917
+ sheetName: table.sheetName,
4918
+ tableName: table.tableName,
4919
+ address: table.address,
4920
+ score,
4921
+ reason: `matched ${requiredHits.length}/${required.length} required and ${optionalHits.length}/${optional.length} optional columns`,
4922
+ schema: tableInfoSchema(table)
4923
+ });
4924
+ })
4925
+ .filter((match) => match !== undefined);
4926
+ return compactLookupResponse({
4927
+ workbookId,
4928
+ requiredColumns: args.requiredColumns,
4929
+ optionalColumns: args.optionalColumns,
4930
+ matches,
4931
+ maxMatches: args.maxMatches ?? 25,
4932
+ maxPayloadBytes: args.maxPayloadBytes,
4933
+ maxEstimatedTokens: args.maxEstimatedTokens,
4934
+ resourceTitle: "Table column lookup"
4935
+ });
4936
+ }
4937
+ async function lookupFindEntity(args) {
4938
+ const workbookId = args.workbookId;
4939
+ const context = await lookupWorkbookContext(workbookId, args.sheetNames);
4940
+ if (!context.ok) {
4941
+ return withCompactTelemetry({ ok: false, workbookId, entity: args.entity, source: context.source }, { detailLevel: "summary" });
4942
+ }
4943
+ const matches = await lookupUsedRangeMatches(workbookId, context.sheets, args.entity, args, "entity");
4944
+ return compactLookupResponse({
4945
+ workbookId,
4946
+ entity: args.entity,
4947
+ entityKind: args.kind ?? "any",
4948
+ matches,
4949
+ maxMatches: args.maxMatches ?? 25,
4950
+ maxPayloadBytes: args.maxPayloadBytes,
4951
+ maxEstimatedTokens: args.maxEstimatedTokens,
4952
+ resourceTitle: `Entity lookup: ${args.entity}`
4953
+ });
4954
+ }
4955
+ async function lookupResolveRange(args) {
4956
+ const workbookId = args.workbookId;
4957
+ const kind = args.kind ?? "any";
4958
+ const context = await lookupWorkbookContext(workbookId, args.preferredSheetName ? [args.preferredSheetName] : undefined);
4959
+ if (!context.ok) {
4960
+ return withCompactTelemetry({ ok: false, workbookId, target: args.target, source: context.source }, { detailLevel: "summary" });
4961
+ }
4962
+ const matches = [];
4963
+ const parsedRange = lookupParseRangeTarget(args.target, args.preferredSheetName);
4964
+ if ((kind === "any" || kind === "range") && parsedRange) {
4965
+ matches.push(lookupMatch({
4966
+ kind: "range",
4967
+ sheetName: parsedRange.sheetName,
4968
+ address: parsedRange.address,
4969
+ score: 1,
4970
+ reason: "explicit A1 range"
4971
+ }));
4972
+ }
4973
+ if (kind === "any" || kind === "table" || kind === "column") {
4974
+ for (const table of context.tables) {
4975
+ if (args.preferredTableName && table.tableName !== args.preferredTableName) {
4976
+ continue;
4977
+ }
4978
+ if ((kind === "any" || kind === "table") && lookupMatches(String(table.tableName), args.target, args)) {
4979
+ matches.push(lookupMatch({
4980
+ kind: "table",
4981
+ sheetName: table.sheetName,
4982
+ tableName: table.tableName,
4983
+ address: table.address,
4984
+ score: lookupScore(table.tableName, args.target, false),
4985
+ reason: "table name",
4986
+ schema: tableInfoSchema(table)
4987
+ }));
4988
+ }
4989
+ if (kind === "any" || kind === "column") {
4990
+ for (const column of table.columns ?? []) {
4991
+ if (lookupMatches(String(column.name), args.target, args)) {
4992
+ matches.push(lookupMatch({
4993
+ kind: "column",
4994
+ sheetName: table.sheetName,
4995
+ tableName: table.tableName,
4996
+ columnName: column.name,
4997
+ address: table.address,
4998
+ score: lookupScore(column.name, args.target, false),
4999
+ reason: "table column",
5000
+ schema: { tableName: table.tableName, column }
5001
+ }));
5002
+ }
5003
+ }
5004
+ }
5005
+ }
5006
+ }
5007
+ if (kind === "any" || kind === "header") {
5008
+ const headerRequest = {
5009
+ workbookId,
5010
+ query: args.target,
5011
+ maxRowsPerSheet: 10
5012
+ };
5013
+ if (args.preferredSheetName !== undefined) {
5014
+ headerRequest.sheetNames = [args.preferredSheetName];
5015
+ }
5016
+ if (args.maxMatches !== undefined) {
5017
+ headerRequest.maxMatches = args.maxMatches;
5018
+ }
5019
+ const headerResult = await lookupFindHeaders(headerRequest);
5020
+ matches.push(...(headerResult.matches ?? []));
5021
+ }
5022
+ if (kind === "any" || kind === "entity") {
5023
+ matches.push(...await lookupUsedRangeMatches(workbookId, context.sheets, args.target, args, "entity"));
5024
+ }
5025
+ return compactLookupResponse({
5026
+ workbookId,
5027
+ target: args.target,
5028
+ targetKind: kind,
5029
+ matches,
5030
+ maxMatches: args.maxMatches ?? 10,
5031
+ maxPayloadBytes: args.maxPayloadBytes,
5032
+ maxEstimatedTokens: args.maxEstimatedTokens,
5033
+ resourceTitle: `Range resolution: ${args.target}`
5034
+ });
5035
+ }
5036
+ async function lookupInspectMatch(args) {
5037
+ const decoded = args.matchId ? lookupDecodeMatchId(args.matchId) : undefined;
5038
+ const match = { ...decoded, ...args };
5039
+ const workbookId = args.workbookId;
5040
+ if (match.tableName) {
5041
+ const request = {
5042
+ workbookId,
5043
+ tableName: match.tableName,
5044
+ mode: "window",
5045
+ maxRows: args.maxRows ?? 10,
5046
+ maxColumns: args.maxColumns ?? 25,
5047
+ includeValues: args.includeValues ?? true,
5048
+ includeFormulas: args.includeFormulas === true,
5049
+ includeText: args.includeText === true
5050
+ };
5051
+ if (match.columnName !== undefined) {
5052
+ request.columns = [match.columnName];
5053
+ }
5054
+ if (args.maxPayloadBytes !== undefined) {
5055
+ request.maxPayloadBytes = args.maxPayloadBytes;
5056
+ }
5057
+ if (args.maxEstimatedTokens !== undefined) {
5058
+ request.maxEstimatedTokens = args.maxEstimatedTokens;
5059
+ }
5060
+ return compactTableRead(request);
5061
+ }
5062
+ let sheetName = match.sheetName;
5063
+ let address = match.address;
5064
+ if (sheetName && !address) {
5065
+ const context = await lookupWorkbookContext(workbookId, [sheetName]);
5066
+ address = context.sheets[0]?.usedRange?.address;
5067
+ }
5068
+ if (sheetName && address) {
5069
+ const request = {
5070
+ workbookId,
5071
+ sheetName,
5072
+ address,
5073
+ mode: "window",
5074
+ maxRows: args.maxRows ?? 10,
5075
+ maxColumns: args.maxColumns ?? 25,
5076
+ includeValues: args.includeValues ?? true,
5077
+ includeFormulas: args.includeFormulas === true,
5078
+ includeText: args.includeText === true
5079
+ };
5080
+ if (args.maxPayloadBytes !== undefined) {
5081
+ request.maxPayloadBytes = args.maxPayloadBytes;
5082
+ }
5083
+ if (args.maxEstimatedTokens !== undefined) {
5084
+ request.maxEstimatedTokens = args.maxEstimatedTokens;
5085
+ }
5086
+ return compactRangeRead(request);
5087
+ }
5088
+ return withCompactTelemetry({
5089
+ ok: false,
5090
+ workbookId,
5091
+ error: {
5092
+ code: "LOOKUP_MATCH_TARGET_REQUIRED",
5093
+ message: "Provide matchId from a lookup response or direct sheetName/address/tableName fields."
5094
+ }
5095
+ }, { detailLevel: "summary" });
5096
+ }
5097
+ async function compactValidation(args) {
5098
+ const workbookId = args.workbookId;
5099
+ const report = await runCompactValidator(args, workbookId);
5100
+ const issues = Array.isArray(report.issues) ? report.issues : [];
5101
+ const maxIssues = args.maxIssues ?? 5;
5102
+ const summary = {
5103
+ ok: report.ok,
5104
+ workbookId,
5105
+ validator: args.validator,
5106
+ scope: report.scope,
5107
+ issueCount: report.issueCount ?? issues.length,
5108
+ severityCounts: compactSeverityCounts(issues),
5109
+ categories: compactIssueCategories(issues),
5110
+ examples: issues.slice(0, maxIssues).map(compactIssueExample),
5111
+ examplesTruncated: issues.length > maxIssues
5112
+ };
5113
+ return withCompactTelemetry(summary, {
5114
+ detailLevel: "summary",
5115
+ truncated: issues.length > maxIssues,
5116
+ storeResource: true,
5117
+ resourceKind: "validation",
5118
+ resourceTitle: `Validation detail: ${args.validator}`,
5119
+ resourcePayload: report,
5120
+ maxPayloadBytes: args.maxPayloadBytes,
5121
+ maxEstimatedTokens: args.maxEstimatedTokens,
5122
+ budgetSummary: summary
5123
+ });
5124
+ }
5125
+ function compactSnapshot(snapshotId, maxPayloadBytes, maxEstimatedTokens) {
5126
+ const result = runtime.getSnapshot(snapshotId);
5127
+ const snapshot = result.snapshot;
5128
+ const summary = snapshot
5129
+ ? {
5130
+ ok: true,
5131
+ snapshotId,
5132
+ workbookId: snapshot.workbookId,
5133
+ createdAt: snapshot.createdAt,
5134
+ reason: snapshot.reason,
5135
+ affectedRangeCount: snapshot.affectedRanges?.length ?? 0,
5136
+ affectedRanges: snapshot.affectedRanges,
5137
+ payloadRangeCount: snapshot.payload?.rangeSnapshots?.length ?? 0
5138
+ }
5139
+ : { ok: false, snapshotId, error: result.error };
5140
+ return withCompactTelemetry(summary, {
5141
+ detailLevel: "summary",
5142
+ truncated: false,
5143
+ storeResource: Boolean(snapshot),
5144
+ resourceKind: "snapshot",
5145
+ resourceTitle: `Snapshot detail: ${snapshotId}`,
5146
+ resourcePayload: result,
5147
+ maxPayloadBytes,
5148
+ maxEstimatedTokens,
5149
+ budgetSummary: summary
5150
+ });
5151
+ }
5152
+ function compactSnapshotDiff(leftSnapshotId, rightSnapshotId, maxPayloadBytes, maxEstimatedTokens) {
5153
+ const result = runtime.compareSnapshots(leftSnapshotId, rightSnapshotId);
5154
+ const diff = result.diff;
5155
+ const summary = diff
5156
+ ? {
5157
+ ok: true,
5158
+ leftSnapshotId,
5159
+ rightSnapshotId,
5160
+ title: diff.summary?.title,
5161
+ changedRangeCount: diff.summary?.changedRanges?.length ?? diff.changedRanges?.length ?? 0,
5162
+ cellsChanged: diff.summary?.cellsChanged ?? diff.cellsChanged,
5163
+ formulasChanged: diff.summary?.formulasChanged ?? diff.formulasChanged,
5164
+ stylesChanged: diff.summary?.stylesChanged ?? diff.stylesChanged,
5165
+ tablesChanged: diff.summary?.tablesChanged ?? diff.tablesChanged,
5166
+ sheetsChanged: diff.summary?.sheetsChanged ?? diff.sheetsChanged,
5167
+ destructiveLevel: diff.summary?.destructiveLevel ?? diff.destructiveLevel,
5168
+ changedRanges: (diff.summary?.changedRanges ?? diff.changedRanges ?? []).slice(0, 20),
5169
+ changedRangesTruncated: (diff.summary?.changedRanges ?? diff.changedRanges ?? []).length > 20
5170
+ }
5171
+ : { ok: false, leftSnapshotId, rightSnapshotId, error: result.error };
5172
+ return withCompactTelemetry(summary, {
5173
+ detailLevel: "summary",
5174
+ truncated: Boolean(summary.changedRangesTruncated),
5175
+ storeResource: Boolean(diff),
5176
+ resourceKind: "diff",
5177
+ resourceTitle: `Snapshot diff: ${leftSnapshotId}..${rightSnapshotId}`,
5178
+ resourcePayload: result,
5179
+ maxPayloadBytes,
5180
+ maxEstimatedTokens,
5181
+ budgetSummary: summary
5182
+ });
5183
+ }
5184
+ async function runCompactValidator(args, workbookId) {
5185
+ switch (args.validator) {
5186
+ case "workbook":
5187
+ return runtime.validateWorkbook({ workbookId });
5188
+ case "sheet":
5189
+ return runtime.validateSheet({ workbookId, sheetName: requiredCompactArg(args.sheetName, "sheetName") });
5190
+ case "formulas":
5191
+ return runtime.validateFormulas({ workbookId, ...compactOptional({ sheetName: args.sheetName, address: args.address }) });
5192
+ case "styles":
5193
+ return runtime.validateStyles(compactStylesValidationRequest(workbookId, args));
5194
+ case "tables":
5195
+ return runtime.validateTables(compactTablesValidationRequest(workbookId, args));
5196
+ case "filters":
5197
+ return runtime.validateFilters({ workbookId, ...compactOptional({ tableName: args.tableName }) });
5198
+ case "print_layout":
5199
+ return runtime.validatePrintLayout(compactPrintLayoutValidationRequest(workbookId, args));
5200
+ case "no_broken_references":
5201
+ return runtime.validateNoBrokenReferences({ workbookId, ...compactOptional({ sheetName: args.sheetName, address: args.address }) });
5202
+ case "no_formula_errors":
5203
+ return runtime.validateNoFormulaErrors({ workbookId, ...compactOptional({ sheetName: args.sheetName, address: args.address }) });
5204
+ case "no_unintended_changes":
5205
+ return runtime.validateNoUnintendedChanges(compactUnintendedChangesValidationRequest(workbookId, args));
5206
+ default:
5207
+ return {
5208
+ ok: false,
5209
+ workbookId,
5210
+ scope: args.validator,
5211
+ issueCount: 1,
5212
+ issues: [
5213
+ {
5214
+ code: "VALIDATOR_UNSUPPORTED",
5215
+ severity: "error",
5216
+ category: "validation",
5217
+ message: `Unsupported compact validator: ${args.validator}`
5218
+ }
5219
+ ]
5220
+ };
5221
+ }
5222
+ }
5223
+ function compactSeverityCounts(issues) {
5224
+ const counts = {};
5225
+ for (const issue of issues) {
5226
+ const severity = typeof issue === "object" && issue !== null && "severity" in issue ? String(issue.severity) : "unknown";
5227
+ counts[severity] = (counts[severity] ?? 0) + 1;
5228
+ }
5229
+ return counts;
5230
+ }
5231
+ function compactStylesValidationRequest(workbookId, args) {
5232
+ const request = { workbookId };
5233
+ if (args.sheetName !== undefined) {
5234
+ request.sheetName = args.sheetName;
5235
+ }
5236
+ if (args.templateId !== undefined) {
5237
+ request.templateId = args.templateId;
5238
+ }
5239
+ if (args.targetSheetName !== undefined) {
5240
+ request.targetSheetName = args.targetSheetName;
5241
+ }
5242
+ return request;
5243
+ }
5244
+ function compactTablesValidationRequest(workbookId, args) {
5245
+ const request = { workbookId };
5246
+ if (args.tableName !== undefined) {
5247
+ request.tableName = args.tableName;
5248
+ }
5249
+ if (args.templateId !== undefined) {
5250
+ request.templateId = args.templateId;
5251
+ }
5252
+ return request;
5253
+ }
5254
+ function compactPrintLayoutValidationRequest(workbookId, args) {
5255
+ const request = { workbookId };
5256
+ if (args.templateId !== undefined) {
5257
+ request.templateId = args.templateId;
5258
+ }
5259
+ if (args.targetSheetName !== undefined) {
5260
+ request.targetSheetName = args.targetSheetName;
5261
+ }
5262
+ return request;
5263
+ }
5264
+ function compactUnintendedChangesValidationRequest(workbookId, args) {
5265
+ const request = { workbookId };
5266
+ if (args.snapshotId !== undefined) {
5267
+ request.snapshotId = args.snapshotId;
5268
+ }
5269
+ if (args.leftSnapshotId !== undefined) {
5270
+ request.leftSnapshotId = args.leftSnapshotId;
5271
+ }
5272
+ if (args.rightSnapshotId !== undefined) {
5273
+ request.rightSnapshotId = args.rightSnapshotId;
5274
+ }
5275
+ return request;
5276
+ }
5277
+ function compactIssueCategories(issues) {
5278
+ return [...new Set(issues.map((issue) => typeof issue === "object" && issue !== null && "category" in issue ? String(issue.category) : "unknown"))];
5279
+ }
5280
+ function compactIssueExample(issue) {
5281
+ if (typeof issue !== "object" || issue === null) {
5282
+ return issue;
5283
+ }
5284
+ const typed = issue;
5285
+ return {
5286
+ code: typed.code,
5287
+ severity: typed.severity,
5288
+ category: typed.category,
5289
+ message: typed.message,
5290
+ target: typed.target
5291
+ };
5292
+ }
5293
+ function requiredCompactArg(value, name) {
5294
+ if (value === undefined || value === "") {
5295
+ throw new Error(`${name} is required for this compact validator.`);
5296
+ }
5297
+ return value;
5298
+ }
5299
+ function usedRangeCellCount(usedRange) {
5300
+ return (usedRange?.rowCount ?? 0) * (usedRange?.columnCount ?? 0);
5301
+ }
5302
+ function tableInfoSchema(info) {
5303
+ return {
5304
+ workbookId: info.workbookId,
5305
+ tableName: info.tableName,
5306
+ sheetName: info.sheetName,
5307
+ address: info.address,
5308
+ headerAddress: info.headerAddress,
5309
+ rowCount: info.rowCount,
5310
+ columnCount: info.columnCount,
5311
+ columns: info.columns ?? [],
5312
+ style: info.style,
5313
+ showHeaders: info.showHeaders,
5314
+ showTotals: info.showTotals,
5315
+ showFilterButton: info.showFilterButton,
5316
+ showBandedRows: info.showBandedRows,
5317
+ showBandedColumns: info.showBandedColumns,
5318
+ hasFilters: info.filters !== undefined,
5319
+ hasSort: info.sort !== undefined
5320
+ };
5321
+ }
5322
+ async function lookupWorkbookContext(workbookId, sheetNames) {
5323
+ const result = await runtime.getWorkbookMap();
5324
+ const map = "map" in result ? result.map : undefined;
5325
+ if (!result.ok || !map) {
5326
+ return { ok: false, workbookId, source: result, sheets: [], tables: [] };
5327
+ }
5328
+ const requestedSheets = new Set((sheetNames ?? []).map(lookupNormalized));
5329
+ const sheets = (map.sheets ?? []).filter((sheet) => requestedSheets.size === 0 || requestedSheets.has(lookupNormalized(sheet.name)));
5330
+ const tableRefs = sheets.flatMap((sheet) => (sheet.tables ?? []).map((table) => ({
5331
+ sheetName: sheet.name,
5332
+ tableName: table.name ?? table.tableName
5333
+ }))).filter((table) => table.tableName);
5334
+ const tables = await Promise.all(tableRefs.map(async (table) => {
5335
+ try {
5336
+ const infoResult = await runtime.getTableInfo({ workbookId, tableName: table.tableName });
5337
+ const info = infoResult.info;
5338
+ return info ? { ...info, sheetName: info.sheetName ?? table.sheetName } : undefined;
5339
+ }
5340
+ catch {
5341
+ return undefined;
5342
+ }
5343
+ }));
5344
+ return {
5345
+ ok: true,
5346
+ workbookId,
5347
+ workbook: map.workbook,
5348
+ sheets,
5349
+ tables: tables.filter((table) => table !== undefined)
5350
+ };
5351
+ }
5352
+ function lookupTableMatches(tables, query, options) {
5353
+ const matches = [];
5354
+ for (const table of tables) {
5355
+ const schema = tableInfoSchema(table);
5356
+ if (lookupMatches(String(table.tableName), query, options)) {
5357
+ matches.push(lookupMatch({
5358
+ kind: "table",
5359
+ sheetName: table.sheetName,
5360
+ tableName: table.tableName,
5361
+ address: table.address,
5362
+ score: lookupScore(table.tableName, query, options.completeMatch),
5363
+ reason: "table name",
5364
+ schema
5365
+ }));
5366
+ }
5367
+ for (const column of table.columns ?? []) {
5368
+ if (lookupMatches(String(column.name), query, options)) {
5369
+ matches.push(lookupMatch({
5370
+ kind: "column",
5371
+ sheetName: table.sheetName,
5372
+ tableName: table.tableName,
5373
+ columnName: column.name,
5374
+ address: table.address,
5375
+ score: lookupScore(column.name, query, options.completeMatch),
5376
+ reason: "table column",
5377
+ schema: { tableName: table.tableName, column }
5378
+ }));
5379
+ }
5380
+ }
5381
+ }
5382
+ return matches;
5383
+ }
5384
+ async function lookupUsedRangeMatches(workbookId, sheets, query, options, kind) {
5385
+ const matches = [];
5386
+ const lookupOptions = options;
5387
+ for (const sheet of sheets) {
5388
+ const address = sheet.usedRange?.address;
5389
+ if (!address) {
5390
+ continue;
5391
+ }
5392
+ try {
5393
+ const result = await runtime.readRangeMetadata("range.search", {
5394
+ workbookId,
5395
+ sheetName: sheet.name,
5396
+ address,
5397
+ text: query,
5398
+ completeMatch: lookupOptions.completeMatch,
5399
+ matchCase: lookupOptions.matchCase
5400
+ });
5401
+ const found = result.matches;
5402
+ if (!found?.address || found.isNullObject) {
5403
+ continue;
5404
+ }
5405
+ const match = lookupMatch({
5406
+ kind,
5407
+ sheetName: sheet.name,
5408
+ address: found.address,
5409
+ score: lookupOptions.completeMatch ? 0.92 : 0.82,
5410
+ reason: `${kind === "entity" ? "entity" : "range"} text match`,
5411
+ preview: {
5412
+ areaCount: found.areaCount,
5413
+ cellCount: found.cellCount
5414
+ }
5415
+ });
5416
+ if ((lookupOptions.maxPreviewRows ?? 0) > 0) {
5417
+ match.preview = await lookupPreviewRange(workbookId, sheet.name, found.address, lookupOptions.maxPreviewRows ?? 3);
5418
+ }
5419
+ matches.push(match);
5420
+ }
5421
+ catch {
5422
+ // Ignore sheet-local search failures; other sheets and metadata can still guide the model.
5423
+ }
5424
+ }
5425
+ return matches;
5426
+ }
5427
+ async function lookupPreviewRange(workbookId, sheetName, address, maxRows) {
5428
+ try {
5429
+ const result = await compactRangeRead({
5430
+ workbookId,
5431
+ sheetName,
5432
+ address,
5433
+ mode: "window",
5434
+ maxRows,
5435
+ maxColumns: 10,
5436
+ includeValues: true,
5437
+ includeText: true
5438
+ });
5439
+ return {
5440
+ address: result.window?.address ?? address,
5441
+ values: result.values,
5442
+ text: result.text
5443
+ };
5444
+ }
5445
+ catch {
5446
+ return { address };
5447
+ }
5448
+ }
5449
+ function compactLookupResponse(args) {
5450
+ const allMatches = lookupSortAndDedupe(args.matches);
5451
+ const shownMatches = allMatches.slice(0, args.maxMatches);
5452
+ const summary = {
5453
+ ok: true,
5454
+ workbookId: args.workbookId,
5455
+ ...Object.fromEntries(Object.entries(args).filter(([key]) => !["workbookId", "matches", "maxMatches", "maxPayloadBytes", "maxEstimatedTokens", "resourceTitle"].includes(key))),
5456
+ matchCount: allMatches.length,
5457
+ matches: shownMatches,
5458
+ matchesTruncated: shownMatches.length < allMatches.length
5459
+ };
5460
+ return withCompactTelemetry(summary, {
5461
+ detailLevel: "summary",
5462
+ truncated: shownMatches.length < allMatches.length,
5463
+ maxPayloadBytes: args.maxPayloadBytes,
5464
+ maxEstimatedTokens: args.maxEstimatedTokens,
5465
+ resourceKind: "summary",
5466
+ resourceTitle: args.resourceTitle,
5467
+ resourcePayload: { ...summary, matches: allMatches, matchesTruncated: false },
5468
+ budgetSummary: { ...summary, matches: shownMatches.map(({ preview, schema, ...match }) => match) }
5469
+ });
5470
+ }
5471
+ function lookupMatch(match) {
5472
+ return {
5473
+ ...match,
5474
+ score: Number(match.score.toFixed(3)),
5475
+ matchId: lookupEncodeMatchId(match)
5476
+ };
5477
+ }
5478
+ function lookupSortAndDedupe(matches) {
5479
+ const best = new Map();
5480
+ for (const match of matches) {
5481
+ const key = [match.kind, match.sheetName, match.tableName, match.columnName, match.address].map((value) => value ?? "").join("|");
5482
+ const existing = best.get(key);
5483
+ if (!existing || match.score > existing.score) {
5484
+ best.set(key, match);
5485
+ }
5486
+ }
5487
+ return [...best.values()].sort((left, right) => right.score - left.score ||
5488
+ String(left.sheetName ?? "").localeCompare(String(right.sheetName ?? "")) ||
5489
+ String(left.tableName ?? "").localeCompare(String(right.tableName ?? "")) ||
5490
+ String(left.address ?? "").localeCompare(String(right.address ?? "")));
5491
+ }
5492
+ function lookupTerms(values) {
5493
+ return values.map(lookupNormalized).filter((value) => value !== "");
5494
+ }
5495
+ function lookupMatches(candidate, query, options) {
5496
+ const lookupOptions = options;
5497
+ const left = lookupOptions.matchCase ? candidate.trim() : lookupNormalized(candidate);
5498
+ const right = lookupOptions.matchCase ? query.trim() : lookupNormalized(query);
5499
+ return lookupOptions.completeMatch ? left === right : left.includes(right);
5500
+ }
5501
+ function lookupScore(candidate, query, completeMatch) {
5502
+ const left = lookupNormalized(candidate);
5503
+ const right = lookupNormalized(query);
5504
+ if (left === right) {
5505
+ return 1;
5506
+ }
5507
+ if (completeMatch) {
5508
+ return 0;
5509
+ }
5510
+ if (left.startsWith(right)) {
5511
+ return 0.9;
5512
+ }
5513
+ return left.includes(right) ? 0.75 : 0;
5514
+ }
5515
+ function lookupNormalized(value) {
5516
+ return String(value ?? "").trim().toLowerCase();
5517
+ }
5518
+ function lookupRowsFromCompactRead(result) {
5519
+ const typed = result;
5520
+ return Array.isArray(typed.text) ? typed.text : Array.isArray(typed.values) ? typed.values : [];
5521
+ }
5522
+ function lookupParseRangeTarget(target, preferredSheetName) {
5523
+ const parts = target.split("!");
5524
+ const sheetName = parts.length > 1 ? parts.slice(0, -1).join("!").replace(/^'|'$/g, "") : preferredSheetName;
5525
+ const address = parts.length > 1 ? parts[parts.length - 1] : target;
5526
+ if (address === undefined) {
5527
+ return undefined;
5528
+ }
5529
+ try {
5530
+ parseCompactA1Address(address);
5531
+ return sheetName ? { sheetName, address } : undefined;
5532
+ }
5533
+ catch {
5534
+ return undefined;
5535
+ }
5536
+ }
5537
+ function lookupEncodeMatchId(match) {
5538
+ const payload = JSON.stringify({
5539
+ kind: match.kind,
5540
+ sheetName: match.sheetName,
5541
+ tableName: match.tableName,
5542
+ columnName: match.columnName,
5543
+ address: match.address
5544
+ });
5545
+ return `lookup:${Buffer.from(payload, "utf8").toString("base64url")}`;
5546
+ }
5547
+ function lookupDecodeMatchId(matchId) {
5548
+ if (!matchId.startsWith("lookup:")) {
5549
+ return undefined;
5550
+ }
5551
+ try {
5552
+ return JSON.parse(Buffer.from(matchId.slice("lookup:".length), "base64url").toString("utf8"));
5553
+ }
5554
+ catch {
5555
+ return undefined;
5556
+ }
5557
+ }
5558
+ function compactTableColumns(info, requested, maxColumns) {
5559
+ const columns = info.columns ?? [];
5560
+ if (requested?.length) {
5561
+ return columns.filter((column) => requested.some((item) => item === column.name || item === column.index || item === column.id));
5562
+ }
5563
+ return columns.slice(0, maxColumns ?? 25);
5564
+ }
5565
+ function compactRangeFacets(args) {
5566
+ const facets = [];
5567
+ if (args.includeValues !== false) {
5568
+ facets.push("values");
5569
+ }
5570
+ if (args.includeFormulas === true) {
5571
+ facets.push("formulas");
5572
+ }
5573
+ if (args.includeText === true) {
5574
+ facets.push("text");
5575
+ }
5576
+ if (args.includeNumberFormats === true) {
5577
+ facets.push("numberFormat");
5578
+ }
5579
+ if (args.includeStyles === true) {
5580
+ facets.push("style");
5581
+ }
5582
+ return facets;
5583
+ }
5584
+ function compactSampleWindows(totalRows, requestedRows) {
5585
+ const sampleRows = Math.max(1, Math.min(totalRows, requestedRows));
5586
+ if (totalRows <= sampleRows) {
5587
+ return [{ label: "head", rowOffset: 0, rowCount: totalRows }];
5588
+ }
5589
+ const headRows = Math.max(1, Math.floor(sampleRows / 3));
5590
+ const middleRows = Math.max(1, Math.floor(sampleRows / 3));
5591
+ const tailRows = Math.max(1, sampleRows - headRows - middleRows);
5592
+ const middleOffset = Math.max(headRows, Math.floor((totalRows - middleRows) / 2));
5593
+ const tailOffset = Math.max(middleOffset + middleRows, totalRows - tailRows);
5594
+ const windows = [
5595
+ { label: "head", rowOffset: 0, rowCount: headRows },
5596
+ { label: "middle", rowOffset: middleOffset, rowCount: Math.min(middleRows, totalRows - middleOffset) },
5597
+ { label: "tail", rowOffset: tailOffset, rowCount: Math.min(tailRows, totalRows - tailOffset) }
5598
+ ];
5599
+ return windows.filter((window, index, all) => window.rowCount > 0 &&
5600
+ all.findIndex((candidate) => rangesOverlap(candidate.rowOffset, candidate.rowCount, window.rowOffset, window.rowCount)) === index);
5601
+ }
5602
+ function rangesOverlap(leftOffset, leftCount, rightOffset, rightCount) {
5603
+ const leftEnd = leftOffset + leftCount;
5604
+ const rightEnd = rightOffset + rightCount;
5605
+ return leftOffset < rightEnd && rightOffset < leftEnd;
5606
+ }
5607
+ function compactWindowAddress(address, rowOffset, columnOffset, rowCount, columnCount) {
5608
+ const parsed = parseCompactA1Address(address);
5609
+ const startRow = parsed.startRow + rowOffset;
5610
+ const startColumn = parsed.startColumn + columnOffset;
5611
+ const endRow = startRow + Math.max(rowCount, 1) - 1;
5612
+ const endColumn = startColumn + Math.max(columnCount, 1) - 1;
5613
+ const start = `${compactColumnName(startColumn)}${startRow}`;
5614
+ const end = `${compactColumnName(endColumn)}${endRow}`;
5615
+ return start === end ? start : `${start}:${end}`;
5616
+ }
5617
+ function parseCompactA1Address(address) {
5618
+ const range = stripResourceSheetName(address).trim();
5619
+ const match = /^(?<startCol>[A-Z]+)(?<startRow>\d+)(?::(?<endCol>[A-Z]+)(?<endRow>\d+))?$/i.exec(range);
5620
+ if (!match?.groups?.startCol || !match.groups.startRow) {
5621
+ throw new Error(`Invalid A1 address: ${address}`);
5622
+ }
5623
+ const startColumn = compactColumnNumber(match.groups.startCol);
5624
+ const startRow = Number(match.groups.startRow);
5625
+ const endColumn = compactColumnNumber(match.groups.endCol ?? match.groups.startCol);
5626
+ const endRow = Number(match.groups.endRow ?? match.groups.startRow);
5627
+ if (startRow < 1 || endRow < startRow || endColumn < startColumn) {
5628
+ throw new Error(`Invalid A1 range bounds: ${address}`);
5629
+ }
5630
+ return { startRow, startColumn, endRow, endColumn };
5631
+ }
5632
+ function compactColumnNumber(columnName) {
5633
+ let value = 0;
5634
+ for (const char of columnName.toUpperCase()) {
5635
+ const code = char.charCodeAt(0);
5636
+ if (code < 65 || code > 90) {
5637
+ throw new Error(`Invalid column name: ${columnName}`);
5638
+ }
5639
+ value = value * 26 + (code - 64);
5640
+ }
5641
+ return value;
5642
+ }
5643
+ function compactColumnName(columnNumber) {
5644
+ let value = "";
5645
+ let n = columnNumber;
5646
+ while (n > 0) {
5647
+ n -= 1;
5648
+ value = String.fromCharCode(65 + (n % 26)) + value;
5649
+ n = Math.floor(n / 26);
5650
+ }
5651
+ return value;
5652
+ }
4051
5653
  function tableCreateRequest(args) {
4052
5654
  const request = {
4053
5655
  workbookId: args.workbookId,
@@ -4642,7 +6244,14 @@ function registerMcpTool(mcp, name, config, callback) {
4642
6244
  if (!isToolExposed(name, catalogOptions)) {
4643
6245
  return;
4644
6246
  }
4645
- mcp.registerTool(name, config, callback);
6247
+ mcp.registerTool(name, config, async (args, extra) => {
6248
+ const result = await callback(args, extra);
6249
+ const decorated = isWorkbookMutatingMcpTool(name) ? addCompactMutationProof(result) : result;
6250
+ if (isWorkbookMutatingMcpTool(name)) {
6251
+ clearCompactCache(`mutation:${name}`);
6252
+ }
6253
+ return decorated;
6254
+ });
4646
6255
  }
4647
6256
  function jsonResult(value) {
4648
6257
  return {
@@ -4654,6 +6263,88 @@ function jsonResult(value) {
4654
6263
  ]
4655
6264
  };
4656
6265
  }
6266
+ function isWorkbookMutatingMcpTool(name) {
6267
+ const namespace = name.split(".")[1];
6268
+ if (!namespace || name.startsWith("excel.compact.")) {
6269
+ return false;
6270
+ }
6271
+ if (namespace === "workbook") {
6272
+ return /\.(calculate|save|save_as|restore_backup|close|embed|import)/.test(name);
6273
+ }
6274
+ if (namespace === "plan") {
6275
+ return /\.(apply|rollback)/.test(name);
6276
+ }
6277
+ if (namespace === "transaction") {
6278
+ return /\.(rollback|rollback_chain)/.test(name);
6279
+ }
6280
+ if (!new Set(["sheet", "range", "batch", "workflow", "template", "style", "formula", "table", "filter", "sort", "pivot", "chart", "names", "region", "repair", "clean"]).has(namespace)) {
6281
+ return false;
6282
+ }
6283
+ if (name === "excel.workflow.preview_risky_edit") {
6284
+ return true;
6285
+ }
6286
+ return /\.(set_|write_|create|copy|rename|delete|move|hide|unhide|protect|unprotect|clear|apply|repair|fill|append|update|resize|sort|save|restore|close|insert|merge|unmerge|lock|unlock|convert|calculate|recalculate|register|unregister|commit|rollback|cancel|refresh|invalidate|parse|normalize|trim|remove|standardize|split|import|embed)/.test(name);
6287
+ }
6288
+ function addCompactMutationProof(result) {
6289
+ if (!isJsonTextResult(result)) {
6290
+ return result;
6291
+ }
6292
+ try {
6293
+ const payload = JSON.parse(result.content[0].text);
6294
+ if (payload.compactProof !== undefined) {
6295
+ return result;
6296
+ }
6297
+ return jsonResult({
6298
+ ...payload,
6299
+ compactProof: summarizeMutationProof(payload)
6300
+ });
6301
+ }
6302
+ catch {
6303
+ return result;
6304
+ }
6305
+ }
6306
+ function isJsonTextResult(result) {
6307
+ return Boolean(result &&
6308
+ typeof result === "object" &&
6309
+ Array.isArray(result.content) &&
6310
+ result.content[0]?.text !== undefined);
6311
+ }
6312
+ function summarizeMutationProof(payload) {
6313
+ const diffSummary = payload.diffSummary;
6314
+ const telemetry = payload.telemetry;
6315
+ const warnings = Array.isArray(payload.warnings) ? payload.warnings : [];
6316
+ const validation = validationSummary(payload);
6317
+ return {
6318
+ ok: payload.ok,
6319
+ transactionId: payload.transactionId,
6320
+ transactionStatus: payload.transactionStatus,
6321
+ taskId: payload.taskId,
6322
+ rollbackAvailable: payload.rollbackAvailable,
6323
+ backups: payload.backups,
6324
+ changedRanges: diffSummary?.changedRanges,
6325
+ cellsChanged: diffSummary?.cellsChanged ?? telemetry?.cellsWritten,
6326
+ formulasChanged: diffSummary?.formulasChanged,
6327
+ stylesChanged: diffSummary?.stylesChanged,
6328
+ tablesChanged: diffSummary?.tablesChanged,
6329
+ sheetsChanged: diffSummary?.sheetsChanged,
6330
+ warnings: warnings.length,
6331
+ validation,
6332
+ payloadBytes: Buffer.byteLength(JSON.stringify(payload), "utf8"),
6333
+ estimatedTokens: Math.ceil(Buffer.byteLength(JSON.stringify(payload), "utf8") / 4)
6334
+ };
6335
+ }
6336
+ function validationSummary(payload) {
6337
+ const issueCount = typeof payload.issueCount === "number" ? payload.issueCount : undefined;
6338
+ const issues = Array.isArray(payload.issues) ? payload.issues : undefined;
6339
+ const severityCounts = (payload.severityCounts ?? payload.summary);
6340
+ if (issueCount === undefined && issues === undefined && severityCounts === undefined) {
6341
+ return undefined;
6342
+ }
6343
+ return {
6344
+ issueCount: issueCount ?? issues?.length ?? 0,
6345
+ severityCounts
6346
+ };
6347
+ }
4657
6348
  function jsonResource(uri, value) {
4658
6349
  return {
4659
6350
  contents: [