@mcpher/gas-fakes 1.2.21 → 1.2.23
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/README.RU.md +2 -0
- package/README.md +2 -0
- package/gas-fakes.js +276 -79
- package/gasfakes.json +1 -1
- package/package.json +2 -2
- package/src/services/documentapp/fakedocumentapp.js +5 -1
- package/src/services/formapp/fakeform.js +10 -1
- package/src/services/formapp/fakeformresponse.js +2 -1
- package/src/services/formapp/fakeitemresponse.js +31 -0
- package/src/services/spreadsheetapp/fakespreadsheetapp.js +17 -5
package/README.RU.md
CHANGED
|
@@ -359,6 +359,8 @@ const getParentsIterator = ({
|
|
|
359
359
|
- [Supercharge Your Google Apps Script Caching with GasFlexCache](https://ramblings.mcpher.com/supercharge-your-google-apps-script-caching-with-gasflexcache/)
|
|
360
360
|
- [Fake-Sandbox for Google Apps Script: Granular controls.](https://ramblings.mcpher.com/fake-sandbox-for-google-apps-script-granular-controls/)
|
|
361
361
|
- [A Fake-Sandbox for Google Apps Script: Securely Executing Code Generated by Gemini CLI](https://ramblings.mcpher.com/gas-fakes-sandbox/)
|
|
362
|
+
- [A New Era for Google Apps Script: Unlocking the Future of Google Workspace Automation with Natural Language](https://medium.com/google-cloud/a-new-era-for-google-apps-script-unlocking-the-future-of-google-workspace-automation-with-natural-a9cecf87b4c6)
|
|
363
|
+
- [Next-Generation Google Apps Script Development: Leveraging Antigravity and Gemini 3.0](https://medium.com/google-cloud/next-generation-google-apps-script-development-leveraging-antigravity-and-gemini-3-0-c4d5affbc1a8)
|
|
362
364
|
- [Modern Google Apps Script Workflow Building on the Cloud](https://medium.com/google-cloud/modern-google-apps-script-workflow-building-on-the-cloud-2255dbd32ac3)
|
|
363
365
|
- [Bridging the Gap: Seamless Integration for Local Google Apps Script Development](https://medium.com/@tanaike/bridging-the-gap-seamless-integration-for-local-google-apps-script-development-9b9b973aeb02)
|
|
364
366
|
- [Next-Level Google Apps Script Development](https://medium.com/google-cloud/next-level-google-apps-script-development-654be5153912)
|
package/README.md
CHANGED
|
@@ -178,6 +178,8 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
178
178
|
- [Supercharge Your Google Apps Script Caching with GasFlexCache](https://ramblings.mcpher.com/supercharge-your-google-apps-script-caching-with-gasflexcache/)
|
|
179
179
|
- [Fake-Sandbox for Google Apps Script: Granular controls.](https://ramblings.mcpher.com/fake-sandbox-for-google-apps-script-granular-controls/)
|
|
180
180
|
- [A Fake-Sandbox for Google Apps Script: Securely Executing Code Generated by Gemini CLI](https://ramblings.mcpher.com/gas-fakes-sandbox/)
|
|
181
|
+
- [A New Era for Google Apps Script: Unlocking the Future of Google Workspace Automation with Natural Language](https://medium.com/google-cloud/a-new-era-for-google-apps-script-unlocking-the-future-of-google-workspace-automation-with-natural-a9cecf87b4c6)
|
|
182
|
+
- [Next-Generation Google Apps Script Development: Leveraging Antigravity and Gemini 3.0](https://medium.com/google-cloud/next-generation-google-apps-script-development-leveraging-antigravity-and-gemini-3-0-c4d5affbc1a8)
|
|
181
183
|
- [Modern Google Apps Script Workflow Building on the Cloud](https://medium.com/google-cloud/modern-google-apps-script-workflow-building-on-the-cloud-2255dbd32ac3)
|
|
182
184
|
- [Bridging the Gap: Seamless Integration for Local Google Apps Script Development](https://medium.com/@tanaike/bridging-the-gap-seamless-integration-for-local-google-apps-script-development-9b9b973aeb02)
|
|
183
185
|
- [Next-Level Google Apps Script Development](https://medium.com/google-cloud/next-level-google-apps-script-development-654be5153912)
|
package/gas-fakes.js
CHANGED
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import path from "path";
|
|
9
|
-
import {
|
|
10
|
-
import { promisify } from "util";
|
|
9
|
+
import { spawn } from "child_process";
|
|
11
10
|
import { Command } from "commander";
|
|
12
11
|
import dotenv from "dotenv";
|
|
13
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -31,9 +30,8 @@ const VERSION = pjson.version;
|
|
|
31
30
|
// CONSTANTS & UTILITIES
|
|
32
31
|
// -----------------------------------------------------------------------------
|
|
33
32
|
|
|
34
|
-
const CLI_VERSION = "0.0.
|
|
35
|
-
const MCP_VERSION = "0.0.
|
|
36
|
-
const execAsync = promisify(exec);
|
|
33
|
+
const CLI_VERSION = "0.0.14";
|
|
34
|
+
const MCP_VERSION = "0.0.4";
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
37
|
* Replaces escaped newline characters ('\\n') with actual newlines,
|
|
@@ -237,132 +235,327 @@ async function executeGasScript(options) {
|
|
|
237
235
|
// -----------------------------------------------------------------------------
|
|
238
236
|
|
|
239
237
|
/**
|
|
240
|
-
*
|
|
238
|
+
* Helper: Constructs the CLI arguments array for gas-fakes execution.
|
|
239
|
+
* Modified to return an array suitable for spawn (no shell escaping needed).
|
|
240
|
+
* @param {object} params Configuration parameters
|
|
241
|
+
* @returns {string[]} Array of CLI arguments
|
|
241
242
|
*/
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
function buildCliArguments(params) {
|
|
244
|
+
const {
|
|
245
|
+
filename,
|
|
246
|
+
script,
|
|
247
|
+
args,
|
|
248
|
+
sandbox,
|
|
249
|
+
whitelistRead,
|
|
250
|
+
whitelistReadWrite,
|
|
251
|
+
whitelistReadWriteTrash,
|
|
252
|
+
json,
|
|
253
|
+
} = params;
|
|
254
|
+
|
|
255
|
+
const cliArgs = [];
|
|
256
|
+
|
|
257
|
+
// Input source
|
|
258
|
+
if (filename) {
|
|
259
|
+
cliArgs.push("-f", filename);
|
|
260
|
+
}
|
|
261
|
+
if (script) {
|
|
262
|
+
cliArgs.push("-s", script);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Execution arguments
|
|
266
|
+
if (args) {
|
|
267
|
+
cliArgs.push("-a", JSON.stringify(args));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Sandbox & Permissions
|
|
271
|
+
if (sandbox) cliArgs.push("-x");
|
|
272
|
+
if (whitelistRead) cliArgs.push("-w", whitelistRead);
|
|
273
|
+
if (whitelistReadWrite) cliArgs.push("--ww", whitelistReadWrite);
|
|
274
|
+
if (whitelistReadWriteTrash) cliArgs.push("--wt", whitelistReadWriteTrash);
|
|
275
|
+
if (json) cliArgs.push("-j", JSON.stringify(json));
|
|
276
|
+
|
|
277
|
+
return cliArgs;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Helper: Executes the gas-fakes command via child_process.spawn.
|
|
282
|
+
* Uses spawn instead of exec to avoid shell interpretation of arguments.
|
|
283
|
+
* @param {string[]} cliArgs Arguments to pass to the command
|
|
284
|
+
* @returns {Promise<object>} MCP tool result object
|
|
285
|
+
*/
|
|
286
|
+
async function runGasFakesProcess(cliArgs) {
|
|
287
|
+
return new Promise((resolve) => {
|
|
288
|
+
// We invoke the current node executable with the current script
|
|
289
|
+
const child = spawn(process.execPath, [process.argv[1], ...cliArgs], {
|
|
290
|
+
env: process.env,
|
|
291
|
+
stdio: ["ignore", "pipe", "pipe"], // ignore stdin, capture stdout/stderr
|
|
292
|
+
shell: false, // Important: Disable shell execution
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
let stdoutData = "";
|
|
296
|
+
let stderrData = "";
|
|
297
|
+
|
|
298
|
+
child.stdout.on("data", (data) => {
|
|
299
|
+
stdoutData += data.toString();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
child.stderr.on("data", (data) => {
|
|
303
|
+
stderrData += data.toString();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
child.on("close", (code) => {
|
|
307
|
+
if (code === 0) {
|
|
308
|
+
resolve({
|
|
309
|
+
content: [
|
|
310
|
+
{ type: "text", text: stdoutData || "Execution finished." },
|
|
311
|
+
],
|
|
312
|
+
isError: false,
|
|
313
|
+
});
|
|
314
|
+
} else {
|
|
315
|
+
// If there's content in stdout, it might contain the error info from gas-fakes
|
|
316
|
+
const output = stderrData || stdoutData || "Unknown error occurred";
|
|
317
|
+
resolve({
|
|
318
|
+
content: [{ type: "text", text: output }],
|
|
319
|
+
isError: true,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
child.on("error", (err) => {
|
|
325
|
+
resolve({
|
|
326
|
+
content: [{ type: "text", text: err.message }],
|
|
327
|
+
isError: true,
|
|
328
|
+
});
|
|
329
|
+
});
|
|
246
330
|
});
|
|
331
|
+
}
|
|
247
332
|
|
|
248
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Registers the default "run-gas-by-gas-fakes" tool.
|
|
335
|
+
*/
|
|
336
|
+
function registerDefaultTool(server) {
|
|
337
|
+
const schema1 = {
|
|
249
338
|
description: [
|
|
250
339
|
`Use this to safely run Google Apps Script in a sandbox using gas-fakes.`,
|
|
251
340
|
`# Important`,
|
|
252
341
|
`- Use the extension of the Google Apps Script files as \`js\`. Don't use \`gs\``,
|
|
253
|
-
`- When
|
|
342
|
+
`- When providing script content, ensure functions are called (e.g., add \`sample();\`).`,
|
|
254
343
|
].join("\n"),
|
|
255
344
|
inputSchema: {
|
|
256
345
|
filename: z
|
|
257
346
|
.string()
|
|
258
|
-
.
|
|
259
|
-
|
|
260
|
-
|
|
347
|
+
.optional() // Made optional because script can be provided
|
|
348
|
+
.describe(`Path to the file containing Google Apps Script.`),
|
|
349
|
+
script: z
|
|
350
|
+
.string()
|
|
351
|
+
.optional()
|
|
352
|
+
.describe(`Direct GAS script content string.`),
|
|
261
353
|
sandbox: z
|
|
262
354
|
.boolean()
|
|
263
355
|
.describe("Use to run Google Apps Script in a sandbox."),
|
|
264
356
|
whitelistRead: z
|
|
265
357
|
.string()
|
|
358
|
+
.optional()
|
|
266
359
|
.describe(
|
|
267
|
-
"Whitelist of file IDs for readonly access (comma-separated).
|
|
268
|
-
)
|
|
269
|
-
.optional(),
|
|
360
|
+
"Whitelist of file IDs for readonly access (comma-separated). When the file IDs and folder IDs are used or provided, use `whiteListRead`, `whitelistReadWrite`, or `whitelistReadWriteTrash` by judging from the prompt."
|
|
361
|
+
),
|
|
270
362
|
whitelistReadWrite: z
|
|
271
363
|
.string()
|
|
364
|
+
.optional()
|
|
272
365
|
.describe(
|
|
273
|
-
"Whitelist of file IDs for read/write access (comma-separated).
|
|
274
|
-
)
|
|
275
|
-
.optional(),
|
|
366
|
+
"Whitelist of file IDs for read/write access (comma-separated). When the file IDs and folder IDs are used or provided, use `whiteListRead`, `whitelistReadWrite`, or `whitelistReadWriteTrash` by judging from the prompt."
|
|
367
|
+
),
|
|
276
368
|
whitelistReadWriteTrash: z
|
|
277
369
|
.string()
|
|
370
|
+
.optional()
|
|
278
371
|
.describe(
|
|
279
|
-
"Whitelist of file IDs for read/write/trash access (comma-separated).
|
|
280
|
-
)
|
|
281
|
-
.optional(),
|
|
372
|
+
"Whitelist of file IDs for read/write/trash access (comma-separated). When the file IDs and folder IDs are used or provided, use `whiteListRead`, `whitelistReadWrite`, or `whitelistReadWriteTrash` by judging from the prompt."
|
|
373
|
+
),
|
|
282
374
|
json: z
|
|
283
375
|
.object({
|
|
284
376
|
whitelistItems: z
|
|
285
377
|
.array(
|
|
286
378
|
z.object({
|
|
287
|
-
itemId: z
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
write: z.boolean().optional().default(false),
|
|
292
|
-
trash: z.boolean().optional().default(false),
|
|
379
|
+
itemId: z.string(),
|
|
380
|
+
read: z.boolean().default(true).optional(),
|
|
381
|
+
write: z.boolean().default(false).optional(),
|
|
382
|
+
trash: z.boolean().default(false).optional(),
|
|
293
383
|
})
|
|
294
384
|
)
|
|
295
|
-
.
|
|
385
|
+
.optional(),
|
|
296
386
|
whitelistServices: z
|
|
297
387
|
.array(
|
|
298
388
|
z.object({
|
|
299
|
-
className: z
|
|
300
|
-
|
|
301
|
-
.describe("The class name of the GAS service."),
|
|
302
|
-
methodNames: z
|
|
303
|
-
.array(z.string())
|
|
304
|
-
.describe(
|
|
305
|
-
"A list of method names for the class to be whitelisted."
|
|
306
|
-
)
|
|
307
|
-
.optional(),
|
|
389
|
+
className: z.string(),
|
|
390
|
+
methodNames: z.array(z.string()).optional(),
|
|
308
391
|
})
|
|
309
392
|
)
|
|
310
|
-
.describe("A list of services to be whitelisted.")
|
|
311
|
-
.optional(),
|
|
312
|
-
blacklistServices: z
|
|
313
|
-
.array(z.string())
|
|
314
|
-
.describe("A list of GAS services to be blacklisted.")
|
|
315
393
|
.optional(),
|
|
394
|
+
blacklistServices: z.array(z.string()).optional(),
|
|
316
395
|
})
|
|
317
|
-
.
|
|
318
|
-
.
|
|
396
|
+
.optional()
|
|
397
|
+
.describe("Advanced sandbox configuration JSON."),
|
|
319
398
|
},
|
|
320
399
|
};
|
|
321
400
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
filename,
|
|
325
|
-
sandbox,
|
|
326
|
-
whitelistRead,
|
|
327
|
-
whitelistReadWrite,
|
|
328
|
-
whitelistReadWriteTrash,
|
|
329
|
-
json,
|
|
330
|
-
} = options;
|
|
331
|
-
|
|
332
|
-
if (!filename) {
|
|
401
|
+
server.registerTool("run-gas-by-gas-fakes", schema1, async (args) => {
|
|
402
|
+
if (!args.filename && !args.script) {
|
|
333
403
|
return {
|
|
334
404
|
content: [
|
|
335
|
-
{
|
|
405
|
+
{
|
|
406
|
+
type: "text",
|
|
407
|
+
text: "Error: Either `filename` or `script` is required.",
|
|
408
|
+
},
|
|
336
409
|
],
|
|
337
410
|
isError: true,
|
|
338
411
|
};
|
|
339
412
|
}
|
|
413
|
+
const cliArgs = buildCliArguments(args);
|
|
414
|
+
return await runGasFakesProcess(cliArgs);
|
|
415
|
+
});
|
|
340
416
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
417
|
+
const schema2 = {
|
|
418
|
+
description: [
|
|
419
|
+
`Use this to create the tools of the MCP server using Google Apps Script as a new file. If a file \`settings.json\` or \`mcp_config.json\` or and so on for loading the MCP servers include \`--tools\` and the tool file to \`gas-fakes\` MCP server, ask the current client which you want to create a new file including the tools or you want to add the tools to the existing tool file. When a new tool file is created, show how to use the tool file as follows. When the created tool file is \`toolFile.js\`, modify the MCP server setting to \`"mcpServers": { "gas-fakes": { "command": "gas-fakes", "args": [ "mcp", "--tools", "{Absolute path}/toolFile.js" ] } }\`. Don't forget to replace \`{Absolute path}\` with the actual path.`,
|
|
420
|
+
].join("\n"),
|
|
421
|
+
inputSchema: {
|
|
422
|
+
filename: z
|
|
423
|
+
.string()
|
|
424
|
+
.describe(
|
|
425
|
+
"Filename of the tool file built by Google Apps Script. The extension is required to be `.js`."
|
|
426
|
+
),
|
|
427
|
+
tools: z
|
|
428
|
+
.array(
|
|
429
|
+
z
|
|
430
|
+
.object({
|
|
431
|
+
name: z.string().describe("Tool name."),
|
|
432
|
+
schema: z
|
|
433
|
+
.string()
|
|
434
|
+
.describe(
|
|
435
|
+
`JSON schema of the arguments to \`gas-script\`. This is required to be an object by creating zod. The simple sample script of Zod is \`{ description: "Use this to search files by a filename on Google Drive.", inputSchema: { filename: z.string().describe("Filename of the search file.") } }\`. Generate a JSON schema using Zod by following this sample. You are required to generate the JSON schema using Zod. This object of inputSchema will be used as "object" in Google Apps Script.`
|
|
436
|
+
),
|
|
437
|
+
gas_script: z
|
|
438
|
+
.string()
|
|
439
|
+
.describe(
|
|
440
|
+
`Generated Google Apps Script. Please be careful of the following rule. For example, when you generated a Google Apps Script like \`function sample(object) { script }\`, please add \`return sample(object);\` to execute the function. This is a very important point. When the prompt says "Create a function", please create a function and add a line of script for calling the function and returning the response like \`return sample(object);\`. Or, you can also create a Google Apps Script without enclosing the script with \`function sample(object) { script }\`. When you want to return the value, show the value using \`console.log\` in the script, or return the value using \`return\`. The simple sample script is \`const { filename } = object; const files = DriveApp.getFilesByName(filename); const ar = []; while (files.hasNext()) { const file = files.next(); ar.push({ filename: file.getName(), fileId: file.getId() }); } return ar;\` This tool is required to be used for creating tools as a new file. When you want to add more tools to the existing file, add them to an array \`tools\` in the file by following the specification of \`tools\`. If you cannot find the array, please create it as a new file.`
|
|
441
|
+
),
|
|
442
|
+
})
|
|
443
|
+
.describe("An object for each tool.")
|
|
444
|
+
)
|
|
445
|
+
.describe("An array including tools."),
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
server.registerTool("create-new-tools", schema2, async (args) => {
|
|
449
|
+
if (!args.filename || !args.tools) {
|
|
358
450
|
return {
|
|
359
|
-
content: [
|
|
451
|
+
content: [
|
|
452
|
+
{
|
|
453
|
+
type: "text",
|
|
454
|
+
text: "Error: `filename` and `tools` are required.",
|
|
455
|
+
},
|
|
456
|
+
],
|
|
360
457
|
isError: true,
|
|
361
458
|
};
|
|
362
459
|
}
|
|
363
|
-
|
|
460
|
+
const tool_ar = args.tools
|
|
461
|
+
.map(
|
|
462
|
+
({ name, schema, gas_script }) =>
|
|
463
|
+
`{ name: "${name}", schema: ${schema}, func: (object = {}) => { ${gas_script} } }`
|
|
464
|
+
)
|
|
465
|
+
.join(", ");
|
|
466
|
+
const tool_script = [
|
|
467
|
+
`import { z } from "zod";`,
|
|
468
|
+
``,
|
|
469
|
+
`const tools = [${tool_ar}];`,
|
|
470
|
+
].join("\n");
|
|
471
|
+
const absolutePath = path.resolve(process.cwd(), args.filename);
|
|
472
|
+
fs.writeFileSync(absolutePath, tool_script);
|
|
473
|
+
return {
|
|
474
|
+
content: [
|
|
475
|
+
{
|
|
476
|
+
type: "text",
|
|
477
|
+
text: `A new file including tools for gas-fakes-mcp was successfully created as "${absolutePath}".`,
|
|
478
|
+
},
|
|
479
|
+
],
|
|
480
|
+
isError: false,
|
|
481
|
+
};
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Loads and registers custom tools from an external file.
|
|
487
|
+
*/
|
|
488
|
+
async function registerCustomTools(server, toolsPath) {
|
|
489
|
+
if (!toolsPath || !fs.existsSync(toolsPath)) {
|
|
490
|
+
if (toolsPath) console.error(`No tool file: ${toolsPath}`);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const absolutePath = path.resolve(process.cwd(), toolsPath);
|
|
495
|
+
let toolsStr = fs.readFileSync(absolutePath, "utf8");
|
|
496
|
+
toolsStr = toolsStr.replace(/^import.*/gm, "");
|
|
497
|
+
const getTools = new Function("z", `${toolsStr} return tools || [];`);
|
|
498
|
+
const tools = getTools(z);
|
|
499
|
+
|
|
500
|
+
if (!tools || tools.length === 0) return;
|
|
501
|
+
|
|
502
|
+
tools.forEach((tool) => {
|
|
503
|
+
// Extend the custom tool schema with sandbox options
|
|
504
|
+
const extendedSchema = { ...tool.schema };
|
|
505
|
+
extendedSchema.inputSchema = {
|
|
506
|
+
gas_args: z
|
|
507
|
+
.object(tool.schema.inputSchema)
|
|
508
|
+
.describe("Arguments for Google Apps Script."),
|
|
509
|
+
sandbox: z.boolean().describe("Run in sandbox."),
|
|
510
|
+
whitelistRead: z.string().optional().describe("Read-only whitelist IDs."),
|
|
511
|
+
whitelistReadWrite: z
|
|
512
|
+
.string()
|
|
513
|
+
.optional()
|
|
514
|
+
.describe("Read/Write whitelist IDs."),
|
|
515
|
+
whitelistReadWriteTrash: z
|
|
516
|
+
.string()
|
|
517
|
+
.optional()
|
|
518
|
+
.describe("Read/Write/Trash whitelist IDs."),
|
|
519
|
+
json: z.any().optional().describe("Advanced sandbox JSON configuration."),
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const originalFuncStr = tool.func.toString();
|
|
364
523
|
|
|
365
|
-
|
|
524
|
+
const toolHandler = async (opts) => {
|
|
525
|
+
// Wrap the original function string to execute it with args
|
|
526
|
+
const wrappedScript = `return (${originalFuncStr})(args)`;
|
|
527
|
+
|
|
528
|
+
const cliArgs = buildCliArguments({
|
|
529
|
+
script: wrappedScript,
|
|
530
|
+
args: opts.gas_args,
|
|
531
|
+
...opts,
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
return await runGasFakesProcess(cliArgs);
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
server.registerTool(tool.name, extendedSchema, toolHandler);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Defines and runs the MCP server for gas-fakes.
|
|
543
|
+
*/
|
|
544
|
+
async function startMcpServer(options) {
|
|
545
|
+
const { tools } = options;
|
|
546
|
+
|
|
547
|
+
const server = new McpServer({
|
|
548
|
+
name: "gas-fakes-mcp",
|
|
549
|
+
version: MCP_VERSION,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Register the built-in generic runner
|
|
553
|
+
registerDefaultTool(server);
|
|
554
|
+
|
|
555
|
+
// Register dynamic custom tools if provided
|
|
556
|
+
if (tools) {
|
|
557
|
+
await registerCustomTools(server, tools);
|
|
558
|
+
}
|
|
366
559
|
|
|
367
560
|
const transport = new StdioServerTransport();
|
|
368
561
|
await server.connect(transport);
|
|
@@ -561,6 +754,10 @@ async function main() {
|
|
|
561
754
|
program
|
|
562
755
|
.command("mcp")
|
|
563
756
|
.description("Launch gas-fakes as an MCP server.")
|
|
757
|
+
.option(
|
|
758
|
+
"-t, --tools <string>",
|
|
759
|
+
"A filename of the custom MCP server tools built by Google Apps Script."
|
|
760
|
+
)
|
|
564
761
|
.action(startMcpServer);
|
|
565
762
|
|
|
566
763
|
program.showHelpAfterError("(add --help for additional information)");
|
package/gasfakes.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest": "./appsscript.json",
|
|
3
3
|
"clasp": "./.clasp.json",
|
|
4
4
|
"scriptId": "28780abe-aaec-4526-a0bf-9d5c104c68b8",
|
|
5
|
-
"documentId":
|
|
5
|
+
"documentId": "1h9IGIShgVBVUrUjjawk5MaCEQte_7t32XeEP1Z5jXKQ",
|
|
6
6
|
"cache": "/tmp/gas-fakes/cache",
|
|
7
7
|
"properties": "/tmp/gas-fakes/properties"
|
|
8
8
|
}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
},
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@mcpher/fake-gasenum": "^1.0.2",
|
|
7
|
-
"@mcpher/gas-flex-cache": "^1.1.
|
|
7
|
+
"@mcpher/gas-flex-cache": "^1.1.3",
|
|
8
8
|
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
9
9
|
"@sindresorhus/is": "^7.0.1",
|
|
10
10
|
"archiver": "^7.0.1",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"name": "@mcpher/gas-fakes",
|
|
35
35
|
"author": "bruce mcpherson",
|
|
36
|
-
"version": "1.2.
|
|
36
|
+
"version": "1.2.23",
|
|
37
37
|
"license": "MIT",
|
|
38
38
|
"main": "main.js",
|
|
39
39
|
"description": "A proof of concept implementation of Apps Script Environment on Node",
|
|
@@ -73,7 +73,11 @@ class FakeDocumentApp {
|
|
|
73
73
|
}
|
|
74
74
|
return this.openById(match[1]);
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
/**
|
|
77
|
+
* note that this in gas-fakes uses the documentId from gasfakes.json config file
|
|
78
|
+
* Returns the document to which the script is container-bound. To interact with document to which the script is not container-bound, use openById(id) or openByUrl(url) instead.
|
|
79
|
+
* @returns {Document}
|
|
80
|
+
*/
|
|
77
81
|
getActiveDocument() {
|
|
78
82
|
const documentId = Auth.getDocumentId();
|
|
79
83
|
if (!documentId) return null;
|
|
@@ -475,7 +475,16 @@ export class FakeForm {
|
|
|
475
475
|
getPublishedUrl() {
|
|
476
476
|
return `https://docs.google.com/forms/d/e/${this.getId()}/viewform`;
|
|
477
477
|
}
|
|
478
|
-
|
|
478
|
+
/**
|
|
479
|
+
* Gets the URL to respond to the form
|
|
480
|
+
* https://github.com/brucemcpherson/gas-fakes/issues/111
|
|
481
|
+
* shorten url no longer supported by google
|
|
482
|
+
* @returns {string} The form URL.
|
|
483
|
+
*/
|
|
484
|
+
shortenFormUrl() {
|
|
485
|
+
return this.getPublishedUrl()
|
|
486
|
+
}
|
|
487
|
+
|
|
479
488
|
toString() {
|
|
480
489
|
return 'Form';
|
|
481
490
|
}
|
|
@@ -69,7 +69,8 @@ export class FakeFormResponse {
|
|
|
69
69
|
}
|
|
70
70
|
// Add the raw answer object from the API response to the item's answer list.
|
|
71
71
|
// This correctly groups all row answers for a grid under the same parent item.
|
|
72
|
-
|
|
72
|
+
// We also attach the questionId to the answer object so we can identify the row later.
|
|
73
|
+
groupedAnswers.get(itemId).answers.push({ ...answer, questionId });
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -40,6 +40,37 @@ export class FakeItemResponse {
|
|
|
40
40
|
* @returns {string} the response
|
|
41
41
|
*/
|
|
42
42
|
getResponse() {
|
|
43
|
+
|
|
44
|
+
const itemType = this.__item.getType().toString();
|
|
45
|
+
if (itemType === 'GRID' || itemType === 'CHECKBOX_GRID') {
|
|
46
|
+
const rows = this.__item.getRows();
|
|
47
|
+
// Initialize with empty string for GRID, empty array for CHECKBOX_GRID
|
|
48
|
+
// This matches Apps Script behavior: String[] for Grid, String[][] for CheckboxGrid
|
|
49
|
+
const rowAnswers = new Array(rows.length).fill(itemType === 'CHECKBOX_GRID' ? [] : '');
|
|
50
|
+
|
|
51
|
+
const questionIdMap = new Map();
|
|
52
|
+
if (this.__item.__resource.questionGroupItem?.questions) {
|
|
53
|
+
this.__item.__resource.questionGroupItem.questions.forEach((q, index) => {
|
|
54
|
+
questionIdMap.set(q.questionId, index);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
this.__answers.forEach(answer => {
|
|
58
|
+
if (answer.questionId && questionIdMap.has(answer.questionId)) {
|
|
59
|
+
const rowIndex = questionIdMap.get(answer.questionId);
|
|
60
|
+
const values = answer.textAnswers?.answers?.map(a => a.value) || [];
|
|
61
|
+
|
|
62
|
+
if (itemType === 'CHECKBOX_GRID') {
|
|
63
|
+
rowAnswers[rowIndex] = values;
|
|
64
|
+
} else {
|
|
65
|
+
// For GRID, take the first value
|
|
66
|
+
rowAnswers[rowIndex] = values[0] || '';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return rowAnswers;
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
// Flatten the 'textAnswers.answers' arrays from all answer objects.
|
|
44
75
|
// This correctly combines all row answers for a grid item.
|
|
45
76
|
const allTextAnswers = this.__answers.flatMap(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Proxies } from "../../support/proxies.js";
|
|
2
|
+
import { Auth } from "../../support/auth.js";
|
|
2
3
|
import { newFakeSpreadsheet } from "./fakespreadsheet.js";
|
|
3
4
|
import {
|
|
4
5
|
notYetImplemented,
|
|
@@ -34,6 +35,8 @@ export const newFakeSpreadsheetApp = (...args) => {
|
|
|
34
35
|
*/
|
|
35
36
|
export class FakeSpreadsheetApp {
|
|
36
37
|
constructor() {
|
|
38
|
+
// in the context of gas-fakes we start with the activespreadsheet being the one mentioned in gasfakes.json
|
|
39
|
+
this.__activeSpreadsheet = null
|
|
37
40
|
const enumProps = [
|
|
38
41
|
"AutoFillSeries", // AutoFillSeries An enumeration of the types of series used to calculate auto-filled values.
|
|
39
42
|
"BandingTheme", // BandingTheme An enumeration of the possible banding themes.
|
|
@@ -75,24 +78,19 @@ export class FakeSpreadsheetApp {
|
|
|
75
78
|
});
|
|
76
79
|
|
|
77
80
|
const props = [
|
|
78
|
-
|
|
79
|
-
|
|
80
81
|
"getActive",
|
|
81
82
|
"newConditionalFormatRule",
|
|
82
|
-
"getActiveSpreadsheet",
|
|
83
83
|
"getActiveSheet",
|
|
84
84
|
"getCurrentCell",
|
|
85
85
|
"getActiveRange",
|
|
86
86
|
"getActiveRangeList",
|
|
87
87
|
"getSelection",
|
|
88
|
-
"setActiveSpreadsheet",
|
|
89
88
|
"setActiveSheet",
|
|
90
89
|
"setCurrentCell",
|
|
91
90
|
"setActiveRange",
|
|
92
91
|
"setActiveRangeList",
|
|
93
92
|
"newCellImage",
|
|
94
93
|
"getUi",
|
|
95
|
-
|
|
96
94
|
"open",
|
|
97
95
|
|
|
98
96
|
"ChartAggregationType",
|
|
@@ -108,6 +106,20 @@ export class FakeSpreadsheetApp {
|
|
|
108
106
|
toString() {
|
|
109
107
|
return "SpreadsheetApp";
|
|
110
108
|
}
|
|
109
|
+
getActiveSpreadsheet() {
|
|
110
|
+
if (this.__activeSpreadsheet) return this.__activeSpreadsheet;
|
|
111
|
+
// because this is a faked container bound app, we need to get the documentId from the config file
|
|
112
|
+
const documentId = Auth.getDocumentId();
|
|
113
|
+
if (documentId) {
|
|
114
|
+
return this.openById(documentId);
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setActiveSpreadsheet(ss) {
|
|
120
|
+
this.__activeSpreadsheet = ss;
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
111
123
|
|
|
112
124
|
|
|
113
125
|
enableBigQueryExecution() {
|