@google/clasp 3.0.4-alpha → 3.0.5-alpha

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.md CHANGED
@@ -394,6 +394,8 @@ Updates local files with Apps Script project.
394
394
  #### Options
395
395
 
396
396
  - `--versionNumber <number>`: The version number of the project to retrieve.
397
+ - `--deleteUnusedFiles`: Deletes local files that would have been pushed that were not returned by the server. Prompts for confirmation
398
+ - `--force`: Used with `--deleteUnusedFiles` to automatically confirm. Use with caution.
397
399
 
398
400
  #### Examples
399
401
 
@@ -541,6 +543,23 @@ Lists your most recent Apps Script projects.
541
543
 
542
544
  - `clasp list-scripts`: Prints `helloworld1 – xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...`
543
545
 
546
+ ### MCP (EXPERIMENTAL)
547
+
548
+ Runs clasp in MCP (model context protocol) mode for use with coding agents. Configure clasp as a local tool using STDIO transport. While running in MCP mode clasp uses the same credentials as
549
+ normal when used as a CLI. Run `clasp login` ahead of time to authorize.
550
+
551
+ When used in MCP mode clasp does not need to be started from the project directory. The project directoy is specified in the tool calls. Switching projects does not require a restart of the MCP server, while switching credentials does.
552
+
553
+ This feature is experimental and currently offers a limited subset of tools for agents. Feedback is welcome.
554
+
555
+ #### Options
556
+
557
+ N/A
558
+
559
+ #### Examples
560
+
561
+ - `clasp mcp`
562
+
544
563
  ## Advanced Commands
545
564
 
546
565
  > **NOTE**: These commands require Project ID/credentials setup ([see below](#projectid-optional)).
@@ -24,6 +24,7 @@ import { command as runCommand } from './run-function.js';
24
24
  import { command as setupLogsCommand } from './setup-logs.js';
25
25
  import { command as authStatusCommand } from './show-authorized-user.js';
26
26
  import { command as filesStatusCommand } from './show-file-status.js';
27
+ import { command as mcpCommand } from './start-mcp.js';
27
28
  import { command as tailLogsCommand } from './tail-logs.js';
28
29
  import { dirname } from 'path';
29
30
  import { fileURLToPath } from 'url';
@@ -31,10 +32,14 @@ import { readPackageUpSync } from 'read-package-up';
31
32
  import { initAuth } from '../auth/auth.js';
32
33
  import { initClaspInstance } from '../core/clasp.js';
33
34
  import { intl } from '../intl.js';
34
- export function makeProgram(exitOveride) {
35
+ export function getVersion() {
35
36
  const __dirname = dirname(fileURLToPath(import.meta.url));
36
37
  const manifest = readPackageUpSync({ cwd: __dirname });
37
38
  const version = manifest ? manifest.packageJson.version : 'unknown';
39
+ return version;
40
+ }
41
+ export function makeProgram(exitOveride) {
42
+ const version = getVersion();
38
43
  const program = new Command();
39
44
  program.exitOverride(exitOveride);
40
45
  program.storeOptionsAsProperties(false);
@@ -92,6 +97,7 @@ export function makeProgram(exitOveride) {
92
97
  listCommand,
93
98
  createVersionCommand,
94
99
  listVersionsCommand,
100
+ mcpCommand,
95
101
  ];
96
102
  for (const cmd of commandsToAdd) {
97
103
  program.addCommand(cmd);
@@ -1,19 +1,55 @@
1
1
  import { Command } from 'commander';
2
+ import fs from 'fs/promises';
3
+ import inquirer from 'inquirer';
2
4
  import { intl } from '../intl.js';
3
- import { withSpinner } from './utils.js';
5
+ import { isInteractive, withSpinner } from './utils.js';
4
6
  export const command = new Command('pull')
5
7
  .description('Fetch a remote project')
6
8
  .option('--versionNumber <version>', 'The version number of the project to retrieve.')
9
+ .option('-d, --deleteUnusedFiles ', 'Delete local files that are not in the remote project. Use with caution.')
10
+ .option('-f, --force', 'Forcibly delete local files that are not in the remote project without prompting.')
7
11
  .action(async function (options) {
8
12
  const clasp = this.opts().clasp;
9
13
  const versionNumber = options.versionNumber;
10
- const spinnerMsg = intl.formatMessage({ id: "jilcJH", defaultMessage: [{ type: 0, value: "Pulling files..." }] });
14
+ const forceDelete = options.force;
15
+ let spinnerMsg = intl.formatMessage({ id: "dh7Bw6", defaultMessage: [{ type: 0, value: "Checking local files..." }] });
16
+ const localFiles = await clasp.files.collectLocalFiles();
17
+ spinnerMsg = intl.formatMessage({ id: "jilcJH", defaultMessage: [{ type: 0, value: "Pulling files..." }] });
11
18
  const files = await withSpinner(spinnerMsg, async () => {
12
19
  return await clasp.files.pull(versionNumber);
13
20
  });
21
+ if (options.deleteUnusedFiles) {
22
+ const filesToDelete = localFiles.filter(f => !files.find(p => p.localPath === f.localPath));
23
+ await deleteLocalFiles(filesToDelete, forceDelete);
24
+ }
14
25
  files.forEach(f => console.log(`└─ ${f.localPath}`));
15
26
  const successMessage = intl.formatMessage({ id: "4mRAfN", defaultMessage: [{ type: 0, value: "Pulled " }, { type: 6, value: "count", options: { "=0": { value: [{ type: 0, value: "no files." }] }, one: { value: [{ type: 0, value: "one file." }] }, other: { value: [{ type: 7 }, { type: 0, value: " files" }] } }, offset: 0, pluralType: "cardinal" }, { type: 0, value: "." }] }, {
16
27
  count: files.length,
17
28
  });
18
29
  console.log(successMessage);
19
30
  });
31
+ async function deleteLocalFiles(filesToDelete, forceDelete = false) {
32
+ if (!filesToDelete || filesToDelete.length === 0) {
33
+ return;
34
+ }
35
+ const skipConfirmation = forceDelete;
36
+ if (!isInteractive() && !forceDelete) {
37
+ const msg = intl.formatMessage({ id: "zLuvSg", defaultMessage: [{ type: 0, value: "You are not in an interactive terminal and --force not used. Skipping file deletion." }] });
38
+ console.warn(msg);
39
+ return;
40
+ }
41
+ for (const file of filesToDelete) {
42
+ if (!skipConfirmation) {
43
+ const confirm = await inquirer.prompt({
44
+ type: 'confirm',
45
+ name: 'deleteFile',
46
+ message: intl.formatMessage({ id: "lVx/lI", defaultMessage: [{ type: 0, value: "Delete " }, { type: 1, value: "file" }, { type: 0, value: "?" }] }, { file: file.localPath }),
47
+ });
48
+ if (!confirm.deleteFile) {
49
+ continue;
50
+ }
51
+ }
52
+ await fs.unlink(file.localPath);
53
+ console.log(intl.formatMessage({ id: "Nx315v", defaultMessage: [{ type: 0, value: "Deleted " }, { type: 1, value: "file" }] }, { file: file.localPath }));
54
+ }
55
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from 'commander';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { buildMcpServer } from '../mcp/server.js';
4
+ export const command = new Command('start-mcp-server')
5
+ .alias('mcp')
6
+ .description('Starts an MCP server for interacting with apps script.')
7
+ .action(async function () {
8
+ const auth = this.opts().auth;
9
+ const server = buildMcpServer(auth);
10
+ const transport = new StdioServerTransport();
11
+ await server.connect(transport);
12
+ });
@@ -63,11 +63,13 @@ export class Clasp {
63
63
  }
64
64
  }
65
65
  export async function initClaspInstance(options) {
66
+ var _a;
66
67
  debug('Initializing clasp instance');
67
68
  const projectRoot = await findProjectRootdDir(options.configFile);
68
69
  if (!projectRoot) {
69
- debug('No project found, defaulting to cwd');
70
- const rootDir = path.resolve(process.cwd());
70
+ const dir = (_a = options.rootDir) !== null && _a !== void 0 ? _a : process.cwd();
71
+ debug(`No project found, defaulting to ${dir}`);
72
+ const rootDir = path.resolve(dir);
71
73
  const configFilePath = path.resolve(rootDir, '.clasp.json');
72
74
  const ignoreFile = await findIgnoreFile(rootDir, options.ignoreFile);
73
75
  const ignoreRules = await loadIgnoreFileOrDefaults(ignoreFile);
@@ -0,0 +1,329 @@
1
+ import path from 'path';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { mkdir } from 'fs/promises';
4
+ import { z } from 'zod';
5
+ import { getDefaultProjectName } from '../commands/create-script.js';
6
+ import { getVersion } from '../commands/program.js';
7
+ import { initClaspInstance } from '../core/clasp.js';
8
+ export function buildMcpServer(auth) {
9
+ const server = new McpServer({
10
+ name: 'Clasp',
11
+ version: getVersion(),
12
+ });
13
+ server.tool('push_files', 'Pushes the local Apps Script project to the remote server.', {
14
+ projectDir: z
15
+ .string()
16
+ .describe('The local directory of the Apps Script project to push. Must contain a .clasp.json file containing the project info.'),
17
+ }, {
18
+ title: 'Push project files to Apps Script',
19
+ openWorldHint: false,
20
+ destructiveHint: true,
21
+ idempotentHint: false,
22
+ readOnlyHint: false,
23
+ }, async ({ projectDir }) => {
24
+ if (!projectDir) {
25
+ return {
26
+ isError: true,
27
+ content: [
28
+ {
29
+ type: 'text',
30
+ text: 'Project directory is required.',
31
+ },
32
+ ],
33
+ };
34
+ }
35
+ const clasp = await initClaspInstance({
36
+ credentials: auth.credentials,
37
+ configFile: projectDir,
38
+ rootDir: projectDir,
39
+ });
40
+ try {
41
+ const files = await clasp.files.push();
42
+ const fileList = files.map(file => ({
43
+ type: 'text',
44
+ text: `Updated file: ${path.resolve(file.localPath)}`,
45
+ }));
46
+ return {
47
+ status: 'success',
48
+ content: [
49
+ {
50
+ type: 'text',
51
+ text: `Pushed project in ${projectDir} to remote server successfully.`,
52
+ },
53
+ ...fileList,
54
+ ],
55
+ structuredContent: {
56
+ scriptId: clasp.project.scriptId,
57
+ projectDir: projectDir,
58
+ files: files.map(file => path.resolve(file.localPath)),
59
+ },
60
+ };
61
+ }
62
+ catch (err) {
63
+ return {
64
+ isError: true,
65
+ content: [
66
+ {
67
+ type: 'text',
68
+ text: `Error pushing project: ${err.message}`,
69
+ },
70
+ ],
71
+ };
72
+ }
73
+ });
74
+ server.tool('pull_files', 'Pulls files from Apps Script project to local file system.', {
75
+ projectDir: z
76
+ .string()
77
+ .describe('The local directory of the Apps Script project to update. Must contain a .clasp.json file containing the project info.'),
78
+ }, {
79
+ title: 'Pull project files from Apps Script',
80
+ openWorldHint: false,
81
+ destructiveHint: true,
82
+ idempotentHint: false,
83
+ readOnlyHint: false,
84
+ }, async ({ projectDir }) => {
85
+ if (!projectDir) {
86
+ return {
87
+ isError: true,
88
+ content: [
89
+ {
90
+ type: 'text',
91
+ text: 'Project directory is required.',
92
+ },
93
+ ],
94
+ };
95
+ }
96
+ const clasp = await initClaspInstance({
97
+ credentials: auth.credentials,
98
+ configFile: projectDir,
99
+ rootDir: projectDir,
100
+ });
101
+ try {
102
+ const files = await clasp.files.pull();
103
+ const fileList = files.map(file => ({
104
+ type: 'text',
105
+ text: `Updated file: ${path.resolve(file.localPath)}`,
106
+ }));
107
+ return {
108
+ content: [
109
+ {
110
+ type: 'text',
111
+ text: `Pushed project in ${projectDir} to remote server successfully.`,
112
+ },
113
+ ...fileList,
114
+ ],
115
+ structuredContent: {
116
+ scriptId: clasp.project.scriptId,
117
+ projectDir: projectDir,
118
+ files: files.map(file => path.resolve(file.localPath)),
119
+ },
120
+ };
121
+ }
122
+ catch (err) {
123
+ return {
124
+ isError: true,
125
+ content: [
126
+ {
127
+ type: 'text',
128
+ text: `Error pushing project: ${err.message}`,
129
+ },
130
+ ],
131
+ };
132
+ }
133
+ });
134
+ server.tool('create_project', 'Create a new apps script project.', {
135
+ projectDir: z.string().describe('The local directory where the Apps Script project will be created.'),
136
+ sourceDir: z
137
+ .string()
138
+ .optional()
139
+ .describe('Local directory relative to projectDir where the Apps Script source files are located. If not specified, files are placed in the project directory.'),
140
+ projectName: z
141
+ .string()
142
+ .optional()
143
+ .describe('Name of the project. If not provided, the project name will be infered from the directory.'),
144
+ }, {
145
+ title: 'Create Apps Script project',
146
+ openWorldHint: false,
147
+ destructiveHint: true,
148
+ idempotentHint: false,
149
+ readOnlyHint: false,
150
+ }, async ({ projectDir, sourceDir, projectName }) => {
151
+ if (!projectDir) {
152
+ return {
153
+ isError: true,
154
+ content: [
155
+ {
156
+ type: 'text',
157
+ text: 'Project directory is required.',
158
+ },
159
+ ],
160
+ };
161
+ }
162
+ await mkdir(projectDir, { recursive: true });
163
+ if (!projectName) {
164
+ projectName = getDefaultProjectName(projectDir);
165
+ }
166
+ const clasp = await initClaspInstance({
167
+ credentials: auth.credentials,
168
+ configFile: projectDir,
169
+ rootDir: projectDir,
170
+ });
171
+ clasp.withContentDir(sourceDir !== null && sourceDir !== void 0 ? sourceDir : '.');
172
+ try {
173
+ const id = await clasp.project.createScript(projectName);
174
+ const files = await clasp.files.pull();
175
+ await clasp.project.updateSettings();
176
+ const fileList = files.map(file => ({
177
+ type: 'text',
178
+ text: `Updated file: ${path.resolve(file.localPath)}`,
179
+ }));
180
+ return {
181
+ content: [
182
+ {
183
+ type: 'text',
184
+ text: `Created project ${id} in ${projectDir} successfully.`,
185
+ },
186
+ ...fileList,
187
+ ],
188
+ structuredContent: {
189
+ scriptId: id,
190
+ projectDir: projectDir,
191
+ files: files.map(file => path.resolve(file.localPath)),
192
+ },
193
+ };
194
+ }
195
+ catch (err) {
196
+ return {
197
+ isError: true,
198
+ content: [
199
+ {
200
+ type: 'text',
201
+ text: `Error pushing project: ${err.message}`,
202
+ },
203
+ ],
204
+ };
205
+ }
206
+ });
207
+ server.tool('clone_project', 'Clones and pulls an existing Apps Script project to a local directory.', {
208
+ projectDir: z.string().describe('The local directory where the Apps Script project will be created.'),
209
+ sourceDir: z
210
+ .string()
211
+ .optional()
212
+ .describe('Local directory relative to projectDir where the Apps Script source files are located. If not specified, files are placed in the project directory.'),
213
+ scriptId: z.string().optional().describe('ID of the Apps Script project to clone.'),
214
+ }, {
215
+ title: 'Create Apps Script project',
216
+ openWorldHint: false,
217
+ destructiveHint: true,
218
+ idempotentHint: false,
219
+ readOnlyHint: false,
220
+ }, async ({ projectDir, sourceDir, scriptId }) => {
221
+ if (!projectDir) {
222
+ return {
223
+ isError: true,
224
+ content: [
225
+ {
226
+ type: 'text',
227
+ text: 'Project directory is required.',
228
+ },
229
+ ],
230
+ };
231
+ }
232
+ await mkdir(projectDir, { recursive: true });
233
+ if (!scriptId) {
234
+ return {
235
+ isError: true,
236
+ content: [
237
+ {
238
+ type: 'text',
239
+ text: 'Script ID is required.',
240
+ },
241
+ ],
242
+ };
243
+ }
244
+ const clasp = await initClaspInstance({
245
+ credentials: auth.credentials,
246
+ configFile: projectDir,
247
+ rootDir: projectDir,
248
+ });
249
+ clasp.withContentDir(sourceDir !== null && sourceDir !== void 0 ? sourceDir : '.').withScriptId(scriptId);
250
+ try {
251
+ const files = await clasp.files.pull();
252
+ clasp.project.updateSettings();
253
+ const fileList = files.map(file => ({
254
+ type: 'text',
255
+ text: `Updated file: ${path.resolve(file.localPath)}`,
256
+ }));
257
+ return {
258
+ content: [
259
+ {
260
+ type: 'text',
261
+ text: `Cloned project ${scriptId} in ${projectDir} successfully.`,
262
+ },
263
+ ...fileList,
264
+ ],
265
+ structuredContent: {
266
+ scriptId: scriptId,
267
+ projectDir: projectDir,
268
+ files: files.map(file => path.resolve(file.localPath)),
269
+ },
270
+ };
271
+ }
272
+ catch (err) {
273
+ return {
274
+ isError: true,
275
+ content: [
276
+ {
277
+ type: 'text',
278
+ text: `Error pushing project: ${err.message}`,
279
+ },
280
+ ],
281
+ };
282
+ }
283
+ });
284
+ server.tool('list_projects', 'List Apps Script projects', {}, {
285
+ title: 'List Apps Script projects',
286
+ openWorldHint: false,
287
+ destructiveHint: true,
288
+ idempotentHint: false,
289
+ readOnlyHint: false,
290
+ }, async () => {
291
+ const clasp = await initClaspInstance({
292
+ credentials: auth.credentials,
293
+ });
294
+ try {
295
+ const scripts = await clasp.project.listScripts();
296
+ const scriptList = scripts.results.map(script => ({
297
+ type: 'text',
298
+ text: `${script.name} (${script.id})`,
299
+ }));
300
+ return {
301
+ content: [
302
+ {
303
+ type: 'text',
304
+ text: `Found ${scripts.results.length} Apps Script projects (script ID in parentheses):`,
305
+ },
306
+ ...scriptList,
307
+ ],
308
+ structuredContent: {
309
+ scripts: scripts.results.map(script => ({
310
+ scriptId: script.id,
311
+ name: script.name,
312
+ })),
313
+ },
314
+ };
315
+ }
316
+ catch (err) {
317
+ return {
318
+ isError: true,
319
+ content: [
320
+ {
321
+ type: 'text',
322
+ text: `Error listing projects: ${err.message}`,
323
+ },
324
+ ],
325
+ };
326
+ }
327
+ });
328
+ return server;
329
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google/clasp",
3
- "version": "3.0.4-alpha",
3
+ "version": "3.0.5-alpha",
4
4
  "description": "Develop Apps Script Projects locally",
5
5
  "type": "module",
6
6
  "exports": "./build/src/index.js",
@@ -72,6 +72,7 @@
72
72
  "inquirer-autocomplete-standalone": "^0.8.1",
73
73
  "loud-rejection": "^2.2.0",
74
74
  "micromatch": "^4.0.8",
75
+ "@modelcontextprotocol/sdk": "^1.12.1",
75
76
  "normalize-path": "^3.0.0",
76
77
  "open": "^10.1.2",
77
78
  "ora": "^8.1.1",
@@ -80,7 +81,8 @@
80
81
  "read-package-up": "^11.0.0",
81
82
  "server-destroy": "^1.0.1",
82
83
  "split-lines": "^3.0.0",
83
- "strip-bom": "^5.0.0"
84
+ "strip-bom": "^5.0.0",
85
+ "zod": "^3.25.36"
84
86
  },
85
87
  "devDependencies": {
86
88
  "@biomejs/biome": "^1.9.4",