gogcli-mcp-sheets 4.5.0 → 4.5.2

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/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp-sheets",
5
5
  "display_name": "gogcli (Sheets)",
6
- "version": "4.5.0",
6
+ "version": "4.5.2",
7
7
  "description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -349,15 +349,15 @@
349
349
  },
350
350
  {
351
351
  "name": "gog_sheets_datasource_add",
352
- "description": "Add a BigQuery Connected Sheets data source (custom SQL or a native table)"
352
+ "description": "Add a BigQuery Connected Sheets data source (custom SQL or a native table) (billed; asks the user to confirm)"
353
353
  },
354
354
  {
355
355
  "name": "gog_sheets_datasource_update",
356
- "description": "Repoint a Connected Sheets data source's SQL, table or billing project"
356
+ "description": "Repoint a Connected Sheets data source's SQL, table or billing project (billed; asks the user to confirm)"
357
357
  },
358
358
  {
359
359
  "name": "gog_sheets_datasource_refresh",
360
- "description": "Re-run a Connected Sheets data source against current BigQuery data"
360
+ "description": "Re-run a Connected Sheets data source against current BigQuery data (billed; asks the user to confirm)"
361
361
  },
362
362
  {
363
363
  "name": "gog_sheets_datasource_delete",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-sheets",
3
- "version": "4.5.0",
3
+ "version": "4.5.2",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp-sheets",
5
5
  "description": "Extended Google Sheets MCP server via gogcli — all base tools plus full Sheets support",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@chrischall/mcp-utils": "^2.6.0",
28
- "@modelcontextprotocol/server": "^2.0.0",
28
+ "@modelcontextprotocol/server": "^2.1.0",
29
29
  "zod": "^4.6.1"
30
30
  },
31
31
  "license": "MIT",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/gogcli-mcp-sheets"
9
9
  },
10
- "version": "4.5.0",
10
+ "version": "4.5.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-sheets",
15
- "version": "4.5.0",
15
+ "version": "4.5.2",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -2,7 +2,20 @@ import { McpServer } from '@modelcontextprotocol/server';
2
2
  import type { CallToolResult } from '@modelcontextprotocol/server';
3
3
  import { z } from 'zod';
4
4
  import { rawTextResult } from '@chrischall/mcp-utils';
5
- import { accountParam, runOrDiagnose, run, diagnose, errorText, payloadArg, pos, confinePath, confineAtFile } from '../../../gogcli-mcp/src/lib.js';
5
+ import {
6
+ accountParam,
7
+ runOrDiagnose,
8
+ run,
9
+ diagnose,
10
+ errorText,
11
+ payloadArg,
12
+ pos,
13
+ confinePath,
14
+ confineAtFile,
15
+ CONFIRM_FALLBACK_DESCRIPTION,
16
+ confirmTokenParam,
17
+ requireDispatchConfirmation,
18
+ } from '../../../gogcli-mcp/src/lib.js';
6
19
  import type { GogArg } from '../../../gogcli-mcp/src/lib.js';
7
20
 
8
21
  // Pull the text out of a single-text-block tool result; undefined for any
@@ -12,6 +25,44 @@ function resultText(result: CallToolResult): string | undefined {
12
25
  return first?.type === 'text' ? first.text : undefined;
13
26
  }
14
27
 
28
+ /** A spreadsheet's title from `gog sheets metadata --select=properties.title`. Unreadable output names nothing. */
29
+ export function spreadsheetTitle(raw: string | undefined): string | undefined {
30
+ try {
31
+ const parsed = JSON.parse(String(raw)) as { properties?: { title?: unknown }; spreadsheet?: { properties?: { title?: unknown } } } | null;
32
+ const title = parsed?.spreadsheet?.properties?.title ?? parsed?.properties?.title;
33
+ return typeof title === 'string' ? title : undefined;
34
+ } catch {
35
+ return undefined;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * What a data source queries and who pays, from `gog sheets datasource describe`,
41
+ * for a refresh's prompt. Best effort: only the fields found are named.
42
+ */
43
+ export function dataSourceSnapshot(raw: string | undefined): { billingProject?: string; query?: string; table?: string } {
44
+ type BigQuerySpec = {
45
+ projectId?: unknown;
46
+ querySpec?: { rawQuery?: unknown };
47
+ tableSpec?: { tableProjectId?: unknown; datasetId?: unknown; tableId?: unknown };
48
+ };
49
+ let bq: BigQuerySpec | undefined;
50
+ try {
51
+ const parsed = JSON.parse(String(raw)) as ({ dataSource?: { spec?: { bigQuery?: BigQuerySpec } }; spec?: { bigQuery?: BigQuerySpec } }) | null;
52
+ bq = (parsed?.dataSource ?? parsed)?.spec?.bigQuery;
53
+ } catch {
54
+ bq = undefined;
55
+ }
56
+ const str = (v: unknown) => (typeof v === 'string' ? v : undefined);
57
+ const t = bq?.tableSpec;
58
+ const table = [t?.tableProjectId, t?.datasetId, t?.tableId].map(str).filter(Boolean).join('.');
59
+ return {
60
+ ...(str(bq?.projectId) ? { billingProject: str(bq?.projectId) } : {}),
61
+ ...(str(bq?.querySpec?.rawQuery) ? { query: str(bq?.querySpec?.rawQuery) } : {}),
62
+ ...(table ? { table } : {}),
63
+ };
64
+ }
65
+
15
66
  // Convert a CSS-style hex color ("#FFF5D9", "#FD9", "FFF5D9") to the
16
67
  // {red, green, blue} 0-1 float triple that Sheets API CellFormat expects.
17
68
  // Returns null on unparseable input — caller decides whether to fall back
@@ -77,6 +128,42 @@ async function checkDateFormatTarget(
77
128
  );
78
129
  }
79
130
 
131
+ /**
132
+ * Fleet audit 2026-09-24 SEC-6: every datasource add / update / refresh starts
133
+ * a BigQuery job billed to a project, so each asks first — naming the
134
+ * spreadsheet by title, what will be queried and who pays.
135
+ */
136
+ async function confirmBilledExecution(
137
+ ctx: Parameters<typeof requireDispatchConfirmation>[0],
138
+ o: {
139
+ tool: string;
140
+ action: string;
141
+ account: string | undefined;
142
+ confirmToken: string | undefined;
143
+ spreadsheetId: string;
144
+ target: string;
145
+ view: Record<string, unknown>;
146
+ },
147
+ ) {
148
+ const got = await runOrDiagnose(['sheets', 'metadata', pos(o.spreadsheetId), '--select=properties.title'], { account: o.account });
149
+ if (got.isError) return got;
150
+ const title = spreadsheetTitle(resultText(got));
151
+ const view = { spreadsheet: { id: o.spreadsheetId, ...(title !== undefined ? { title } : {}) }, ...o.view };
152
+ return requireDispatchConfirmation(ctx, {
153
+ action: o.action,
154
+ message: 'Review and confirm starting a BigQuery execution — it is billed to the billing project:',
155
+ confirmationLabel: 'Confirm that this billed BigQuery query should run now.',
156
+ details: view,
157
+ unsupportedNote: 'Ask the user to run it from Google Sheets (Data > Data connectors).',
158
+ fallback: {
159
+ tool: o.tool,
160
+ account: o.account,
161
+ confirmToken: o.confirmToken,
162
+ subject: () => ({ target: o.target, payload: view, preview: view }),
163
+ },
164
+ });
165
+ }
166
+
80
167
  export function registerExtraSheetsTools(server: McpServer): void {
81
168
 
82
169
  server.registerTool('gog_sheets_list_tabs', {
@@ -1144,7 +1231,9 @@ export function registerExtraSheetsTools(server: McpServer): void {
1144
1231
  const bigQueryChargeNote =
1145
1232
  ' COSTS MONEY AND RUNS ASYNCHRONOUSLY: this starts a BigQuery execution billed to the billing project, and returns ' +
1146
1233
  'once the execution has been REQUESTED — not once it has finished. Poll gog_sheets_datasource_describe until its ' +
1147
- 'dataExecutionStatus state is SUCCEEDED or FAILED before treating the data as current.';
1234
+ 'dataExecutionStatus state is SUCCEEDED or FAILED before treating the data as current. Because it bills, it first asks ' +
1235
+ 'the MCP host to show the user a confirmation prompt naming the spreadsheet, the query or table and the billing ' +
1236
+ 'project; nothing runs unless they accept.' + CONFIRM_FALLBACK_DESCRIPTION;
1148
1237
 
1149
1238
  server.registerTool('gog_sheets_datasource_add', {
1150
1239
  description:
@@ -1162,8 +1251,9 @@ export function registerExtraSheetsTools(server: McpServer): void {
1162
1251
  table: z.string().optional().describe('BigQuery table ID. Requires dataset; mutually exclusive with query.'),
1163
1252
  tableProject: z.string().optional().describe('BigQuery project that owns the table (default: the billing project). Only valid with dataset/table.'),
1164
1253
  account: accountParam,
1254
+ confirmToken: confirmTokenParam,
1165
1255
  }),
1166
- }, async ({ spreadsheetId, billingProject, query, dataset, table, tableProject, account }) => {
1256
+ }, async ({ spreadsheetId, billingProject, query, dataset, table, tableProject, account, confirmToken }, ctx) => {
1167
1257
  const tableFlags = tableProject !== undefined || dataset !== undefined || table !== undefined;
1168
1258
  if (query !== undefined && tableFlags) {
1169
1259
  throw new Error('query and the table fields (dataset, table, tableProject) are mutually exclusive — a data source is backed by one or the other.');
@@ -1176,6 +1266,12 @@ export function registerExtraSheetsTools(server: McpServer): void {
1176
1266
  if (tableProject) args.push(`--table-project=${tableProject}`);
1177
1267
  if (dataset) args.push(`--dataset=${dataset}`);
1178
1268
  if (table) args.push(`--table=${table}`);
1269
+ const source = query !== undefined ? { query } : { table: [tableProject ?? billingProject, dataset, table].join('.') };
1270
+ const confirmation = await confirmBilledExecution(ctx, {
1271
+ tool: 'gog_sheets_datasource_add', action: 'sheets.datasource-add', account, confirmToken, spreadsheetId,
1272
+ target: spreadsheetId, view: { billingProject, ...source },
1273
+ });
1274
+ if (confirmation) return confirmation;
1179
1275
  return runOrDiagnose(args, { account });
1180
1276
  });
1181
1277
 
@@ -1196,8 +1292,9 @@ export function registerExtraSheetsTools(server: McpServer): void {
1196
1292
  table: z.string().optional().describe('Replacement table ID. Only valid on a table-backed source.'),
1197
1293
  tableProject: z.string().optional().describe('Replacement project owning the table. Only valid on a table-backed source.'),
1198
1294
  account: accountParam,
1295
+ confirmToken: confirmTokenParam,
1199
1296
  }),
1200
- }, async ({ spreadsheetId, dataSourceId, billingProject, query, dataset, table, tableProject, account }) => {
1297
+ }, async ({ spreadsheetId, dataSourceId, billingProject, query, dataset, table, tableProject, account, confirmToken }, ctx) => {
1201
1298
  const tableFlags = tableProject !== undefined || dataset !== undefined || table !== undefined;
1202
1299
  if (query !== undefined && tableFlags) {
1203
1300
  throw new Error('query and the table fields (dataset, table, tableProject) are mutually exclusive — a data source is backed by one or the other.');
@@ -1211,6 +1308,12 @@ export function registerExtraSheetsTools(server: McpServer): void {
1211
1308
  if (tableProject !== undefined) args.push(`--table-project=${tableProject}`);
1212
1309
  if (dataset !== undefined) args.push(`--dataset=${dataset}`);
1213
1310
  if (table !== undefined) args.push(`--table=${table}`);
1311
+ const changes = Object.fromEntries(Object.entries({ billingProject, query, tableProject, dataset, table }).filter(([, v]) => v !== undefined));
1312
+ const confirmation = await confirmBilledExecution(ctx, {
1313
+ tool: 'gog_sheets_datasource_update', action: 'sheets.datasource-update', account, confirmToken, spreadsheetId,
1314
+ target: `${spreadsheetId}/${dataSourceId}`, view: { dataSourceId, changes },
1315
+ });
1316
+ if (confirmation) return confirmation;
1214
1317
  return runOrDiagnose(args, { account });
1215
1318
  });
1216
1319
 
@@ -1240,10 +1343,19 @@ export function registerExtraSheetsTools(server: McpServer): void {
1240
1343
  dataSourceId: z.string().describe('Data source ID, as reported by gog_sheets_datasource_list'),
1241
1344
  forceRefresh: z.boolean().optional().describe('Refresh even when the previous execution failed'),
1242
1345
  account: accountParam,
1346
+ confirmToken: confirmTokenParam,
1243
1347
  }),
1244
- }, async ({ spreadsheetId, dataSourceId, forceRefresh, account }) => {
1348
+ }, async ({ spreadsheetId, dataSourceId, forceRefresh, account, confirmToken }, ctx) => {
1245
1349
  const args: GogArg[] = ['sheets', 'datasource', 'refresh', pos(spreadsheetId), pos(dataSourceId)];
1246
1350
  if (forceRefresh) args.push('--force-refresh');
1351
+ const described = await runOrDiagnose(['sheets', 'datasource', 'describe', pos(spreadsheetId), pos(dataSourceId)], { account });
1352
+ if (described.isError) return described;
1353
+ const confirmation = await confirmBilledExecution(ctx, {
1354
+ tool: 'gog_sheets_datasource_refresh', action: 'sheets.datasource-refresh', account, confirmToken, spreadsheetId,
1355
+ target: `${spreadsheetId}/${dataSourceId}`,
1356
+ view: { dataSourceId, ...dataSourceSnapshot(resultText(described)), forceRefresh: Boolean(forceRefresh) },
1357
+ });
1358
+ if (confirmation) return confirmation;
1247
1359
  return runOrDiagnose(args, { account });
1248
1360
  });
1249
1361
 
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { registerExtraSheetsTools, dataSourceSnapshot, spreadsheetTitle } from '../../src/tools/sheets-extra.js';
3
+ import * as lib from '../../../gogcli-mcp/src/lib.js';
4
+ import { createTestHarness } from '@chrischall/mcp-utils/test';
5
+ import { rawTextResult } from '@chrischall/mcp-utils';
6
+ import type { ElicitRequest, ElicitResult } from '@modelcontextprotocol/server';
7
+ import { pos } from '../../../gogcli-mcp/src/argv.js';
8
+ import { resetConfirmTokenState } from '../../../gogcli-mcp/src/send-confirm-token.js';
9
+
10
+ vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
11
+ const actual = await importOriginal<typeof lib>();
12
+ return { ...actual, runOrDiagnose: vi.fn() };
13
+ });
14
+
15
+ // Fleet audit 2026-09-24 SEC-6: datasource add / update / refresh each start a
16
+ // BigQuery job billed to a project, so each asks first — naming the spreadsheet,
17
+ // what is queried and who pays.
18
+
19
+ const ORIGINAL_ENV = { ...process.env };
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ process.env = { ...ORIGINAL_ENV };
23
+ delete process.env.MCP_CONFIRM_MODE;
24
+ resetConfirmTokenState();
25
+ });
26
+ afterEach(() => { process.env = ORIGINAL_ENV; });
27
+
28
+ const json = (r: { content: Array<{ text?: string }> }) => JSON.parse(r.content[0]!.text as string);
29
+ async function prompted(answer: ElicitResult = { action: 'accept', content: { confirmed: true } }) {
30
+ const seen: ElicitRequest[] = [];
31
+ const harness = await createTestHarness(registerExtraSheetsTools, { elicitation: async (r) => { seen.push(r); return answer; } });
32
+ const details = () => JSON.parse(seen[0]!.params.message.split('\n').slice(1).join('\n')).details;
33
+ return { harness, seen, details };
34
+ }
35
+ const unprompted = () => createTestHarness(registerExtraSheetsTools);
36
+
37
+ const META = JSON.stringify({ properties: { title: 'Sales dashboard' } });
38
+ const DESCRIBE = JSON.stringify({ dataSource: { dataSourceId: 'ds1', spec: { bigQuery: { projectId: 'billing-1', querySpec: { rawQuery: 'SELECT 1' } } } } });
39
+ function stub(failing?: string) {
40
+ vi.mocked(lib.runOrDiagnose).mockImplementation(async (args) => {
41
+ const key = args.filter((a): a is string => typeof a === 'string' && !a.startsWith('--')).join(' ');
42
+ if (failing && key.startsWith(failing)) return { content: [{ type: 'text', text: 'Error: not found' }], isError: true };
43
+ if (key === 'sheets metadata') return rawTextResult(META);
44
+ if (key === 'sheets datasource describe') return rawTextResult(DESCRIBE);
45
+ return rawTextResult('{"ok":true}');
46
+ });
47
+ }
48
+ const callsTo = (...words: string[]) => vi.mocked(lib.runOrDiagnose).mock.calls.filter(([args]) =>
49
+ words.every((w, i) => args[i] === w));
50
+
51
+ const SHEET = { id: 'sid', title: 'Sales dashboard' };
52
+ const CASES = [
53
+ {
54
+ tool: 'gog_sheets_datasource_add',
55
+ args: { spreadsheetId: 'sid', billingProject: 'billing-1', dataset: 'sales', table: 'orders' },
56
+ action: 'sheets.datasource-add',
57
+ details: { spreadsheet: SHEET, billingProject: 'billing-1', table: 'billing-1.sales.orders' },
58
+ write: 'add',
59
+ reads: ['sheets metadata'],
60
+ changed: { table: 'refunds' },
61
+ },
62
+ {
63
+ tool: 'gog_sheets_datasource_update',
64
+ args: { spreadsheetId: 'sid', dataSourceId: 'ds1', query: 'SELECT 2', billingProject: 'billing-2' },
65
+ action: 'sheets.datasource-update',
66
+ details: { spreadsheet: SHEET, dataSourceId: 'ds1', changes: { billingProject: 'billing-2', query: 'SELECT 2' } },
67
+ write: 'update',
68
+ reads: ['sheets metadata'],
69
+ changed: { query: 'SELECT 3' },
70
+ },
71
+ {
72
+ tool: 'gog_sheets_datasource_refresh',
73
+ args: { spreadsheetId: 'sid', dataSourceId: 'ds1', forceRefresh: true },
74
+ action: 'sheets.datasource-refresh',
75
+ details: { spreadsheet: SHEET, dataSourceId: 'ds1', billingProject: 'billing-1', query: 'SELECT 1', forceRefresh: true },
76
+ write: 'refresh',
77
+ reads: ['sheets datasource describe', 'sheets metadata'],
78
+ changed: { forceRefresh: false },
79
+ },
80
+ ];
81
+
82
+ describe.each(CASES)('$tool', (c) => {
83
+ it('asks naming the spreadsheet, the source and who pays; runs once accepted', async () => {
84
+ stub();
85
+ const { harness, details, seen } = await prompted();
86
+ await harness.callTool(c.tool, c.args);
87
+ expect(seen[0]!.params.message).toMatch(/billed/);
88
+ expect(details()).toEqual(c.details);
89
+ expect(callsTo('sheets', 'metadata')[0]![0]).toEqual(['sheets', 'metadata', pos('sid'), '--select=properties.title']);
90
+ expect(callsTo('sheets', 'datasource', c.write)).toHaveLength(1);
91
+ });
92
+
93
+ it('runs nothing when the user declines', async () => {
94
+ stub();
95
+ expect(json(await (await prompted({ action: 'decline' })).harness.callTool(c.tool, c.args)))
96
+ .toMatchObject({ cancelled: true, action: c.action });
97
+ expect(callsTo('sheets', 'datasource', c.write)).toHaveLength(0);
98
+ });
99
+
100
+ it.each(c.reads)('returns a failed read (%s) without asking or running', async (failing) => {
101
+ stub(failing);
102
+ const { harness, seen } = await prompted();
103
+ expect((await harness.callTool(c.tool, c.args)).isError).toBe(true);
104
+ expect(seen).toHaveLength(0);
105
+ expect(callsTo('sheets', 'datasource', c.write)).toHaveLength(0);
106
+ });
107
+
108
+ it('refuses a client that cannot be prompted', async () => {
109
+ process.env.MCP_CONFIRM_MODE = 'refuse';
110
+ stub();
111
+ const r = json(await (await unprompted()).callTool(c.tool, c.args));
112
+ expect(r).toMatchObject({ reason: 'confirmation-unsupported', action: c.action });
113
+ expect(r.note).toMatch(/Data connectors/);
114
+ });
115
+
116
+ it('token fallback: changed arguments are refused, the same call runs', async () => {
117
+ process.env.MCP_CONFIRM_MODE = 'ask-user';
118
+ stub();
119
+ const harness = await unprompted();
120
+ const p1 = json(await harness.callTool(c.tool, c.args));
121
+ expect(p1.preview).toEqual(c.details);
122
+ expect(json(await harness.callTool(c.tool, { ...c.args, ...c.changed, confirmToken: p1.confirmToken })))
123
+ .toMatchObject({ error: 'DRAFT_CHANGED' });
124
+ await harness.callTool(c.tool, { ...c.args, confirmToken: p1.confirmToken });
125
+ expect(callsTo('sheets', 'datasource', c.write)).toHaveLength(1);
126
+ });
127
+ });
128
+
129
+ it('a query-backed add previews the SQL', async () => {
130
+ stub();
131
+ const { harness, details } = await prompted({ action: 'decline' });
132
+ await harness.callTool('gog_sheets_datasource_add', { spreadsheetId: 'sid', billingProject: 'b', query: 'SELECT 9', });
133
+ expect(details()).toEqual({ spreadsheet: SHEET, billingProject: 'b', query: 'SELECT 9' });
134
+ });
135
+
136
+ it('a table in another project names that project', async () => {
137
+ stub();
138
+ const { harness, details } = await prompted({ action: 'decline' });
139
+ await harness.callTool('gog_sheets_datasource_add', { spreadsheetId: 'sid', billingProject: 'b', tableProject: 'p', dataset: 'd', table: 't' });
140
+ expect(details().table).toBe('p.d.t');
141
+ });
142
+
143
+ describe('parsers', () => {
144
+ it('spreadsheetTitle reads either shape and tolerates unreadable output', () => {
145
+ expect(spreadsheetTitle(JSON.stringify({ spreadsheet: { properties: { title: 'A' } } }))).toBe('A');
146
+ expect(spreadsheetTitle(JSON.stringify({ properties: { title: 7 } }))).toBeUndefined();
147
+ expect(spreadsheetTitle('null')).toBeUndefined();
148
+ expect(spreadsheetTitle(undefined)).toBeUndefined();
149
+ });
150
+
151
+ it('dataSourceSnapshot names a table source, a bare spec, and nothing for unreadable output', () => {
152
+ expect(dataSourceSnapshot(JSON.stringify({ spec: { bigQuery: { tableSpec: { tableProjectId: 'p', datasetId: 'd', tableId: 't' } } } })))
153
+ .toEqual({ table: 'p.d.t' });
154
+ expect(dataSourceSnapshot(JSON.stringify({ dataSource: { spec: { bigQuery: { projectId: 7 } } } }))).toEqual({});
155
+ expect(dataSourceSnapshot('null')).toEqual({});
156
+ expect(dataSourceSnapshot('nope')).toEqual({});
157
+ });
158
+ });
@@ -15,7 +15,10 @@ vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
15
15
  };
16
16
  });
17
17
 
18
- const setupHandlers = () => createTestHarness(registerExtraSheetsTools);
18
+ // The billed datasource tools ask first (sheets-extra-gates.test.ts covers the prompts); here the user accepts.
19
+ const setupHandlers = () => createTestHarness(registerExtraSheetsTools, {
20
+ elicitation: async () => ({ action: 'accept', content: { confirmed: true } }),
21
+ });
19
22
 
20
23
  beforeEach(() => vi.clearAllMocks());
21
24