2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

clasp に MCP モードが入った今、GAS の自作 MCP に何を残すか ― 本体との棲み分けと、push の前に止める仕組み

2
Last updated at Posted at 2026-11-05

本記事は、

  • AI エージェントに Google Apps Script (GAS) を開発させたい人
  • clasp 本体の MCP モード(clasp mcp)と自作ツールのどちらを使うか迷っている人
  • 本番の GAS プロジェクトを AI に触らせるのが不安な人
    に向けて書いています。

※ AI エージェントは Antigravity を使用しています
※ 第1回、第2回、第3回の続編です


TL;DR

  • 前提: clasp 本体に MCP モード(clasp mcp)が入り、clasp を MCP 化しただけの自作ツールは要らなくなったように見えた
  • 起きたこと: 自作ツールを実プロジェクトで動かし直すと、push でクラウドのファイルが消えかける場面があった。自作の検証機能も、AI に必要な情報を返していなかった
  • 方針: clasp 本体と同じことは自作で持たず、本体にない「push の前に止める仕組み」「ローカルのエミュレータ」「run・logs・deploy」だけを残す棲み分けにした

1. 前提:clasp に MCP モードが入り、自作は要らなくなったように見えた

本シリーズ(第1回〜第3回)では、AI エージェントが Google Apps Script (GAS) を直接操作できるよう、CLI ツールの clasp を Model Context Protocol (MCP) サーバーとして包んだ自作ツール(gas-manager-mcp)を作ってきました。

その後、clasp 本体に MCP モード(clasp mcp)が加わりました。2026年11月時点の clasp 3.3.0 では実験的な機能(EXPERIMENTAL)で、次の5つのツールがあります。

  • push_files: ローカルのコードをクラウドへ反映する
  • pull_files: クラウドのコードをローカルへ取得する
  • create_project: プロジェクトを新しく作る
  • clone_project: 既存のプロジェクトをローカルへ複製する
  • list_projects: プロジェクトの一覧を取得する

clasp は Google の GitHub 組織で公開されているツールです。ただし README の冒頭には「Google の公式サポート対象の製品ではない(This is not an officially supported Google product.)」と明記されています。本記事では「公式」ではなく「clasp 本体」と呼びます。

clasp 本体が MCP として動くなら、clasp を呼ぶだけの自作ツールを持ち続ける理由はありません。自作ツールは役目を終え、clasp 本体に一本化できる、と考えるのが自然でした。


2. 実プロジェクトで動かし直して起きた3つのこと

一本化する前に、自作ツールに何が残せるかを確かめるため、実際に動いている GAS プロジェクトで動作を検証しました。対象は、Qiita の記事統計を毎日スプレッドシートに記録しているプロジェクトです。

1. push でクラウドの Webhook.js が消えかけた

検証の時点で、クラウド側には次の3ファイルがありました。

  • appsscript.json: マニフェスト設定
  • qiita_stats_v2.js: Qiita API から統計を取得してスプレッドシートに書き込むメイン処理
  • Webhook.js: スプレッドシートの変動を毎時監視し、Discord へ通知するスクリプト

ところが、ローカルの .claspignore は次の設定でした。

.claspignore
**/**
!qiita_stats_v2.js
!appsscript.json

Webhook.js は、ローカルに取得したあとも push の対象から外れていました。clasp push はローカルの対象ファイル一式でクラウド側を置き換えます。この状態でコードが送られていれば、Webhook.js はクラウドの HEAD から消えていました。過去のバージョンを作っていなければ、元に戻せません。

今回は clasp が「Script is already up to date」と判定して送信しなかったため、削除は起きませんでした。防げたのは偶然です。「ローカルとクラウドは一致している」という前提が崩れていた例です。

2. 自作の検証機能が、警告の件数しか返していなかった

第3回では、エミュレータが api.gs:12 のように誤りの場所を返し、AI がそれを読んで直すログを載せていました。

実装を読み直したところ、当時返していたのは「未定義の呼び出し: 1件」のような件数だけでした。ファイル名、行番号、何が足りないかは含まれていません。AI は「何か問題がある」ことはわかっても、どこを直せばよいかはわからない状態でした。

3. 自作の引数チェックが、一部の誤りを見逃していた

引数の数は、括弧の中をカンマで区切って数えていました。文字列リテラルは事前に置き換えていたので、"a,b" のような文字列は問題になりません。一方、setProperty(f(a, b)) のように引数の中に関数呼び出しがあると、1引数なのに2つと数えて見逃します。


3. なぜ起きたのか

1. clasp push はクラウド側を丸ごと置き換える

人が手で作業していれば、「ローカルにないファイルがクラウドにある」状態には気づきやすいものです。しかし clasp push は、ローカルの対象ファイルでクラウド側の構成を置き換えます。取得していないファイルや、.claspignore で外したファイルは、何の確認もなく消えます。

2. 警告だけでは AI は止まらない

エミュレータが warnings を返しても、結果が success のままなら、AI がそのまま push に進むのを止める手段はありません。人なら警告を見て立ち止まりますが、AI を止めたいならエラーとして返す必要があります。

3. clasp 本体の MCP モードは、判断を人に委ねる設計

clasp 3.3.0 の push_files は、中身を検証せずに clasp の push を実行します。その代わり、破壊的な操作であることを示す注釈(destructiveHint: true)が付いています。この注釈を扱える MCP クライアントは、実行の前に利用者へ承認を求められます。

つまり clasp 本体は「危ない操作であることをクライアントに伝え、実行するかどうかは人が決める」設計です。人が1回ずつ承認するなら、これで十分です。AI に修正と push を続けて任せたい場合は、承認のたびに人の手が止まります。


4. clasp 本体との関係と、自作に残すもの

自作ツールの立ち位置

自作ツールは、内部で clasp の CLI を呼んで動いています。clasp 本体と競合するものではなく、clasp の上に乗って、足りない部分を補うものです。

そこで、clasp 本体と同じことは自作で持たず、本体にない部分だけを残すことにしました。

担当 機能
clasp 本体の MCP モード プロジェクトの作成・複製・一覧(create_project / clone_project / list_projects)
自作 gas-manager-mcp push(事前チェック付き)、pull(本番フォルダのガード付き)、ローカルのエミュレータ、run / logs / deploy / deployments / status

push と pull は、本番のフォルダを守るガードを通したいので自作側に置きました。clasp 本体の pull_files と重なりますが、今回の検証が本番フォルダでの pull から始まったことを踏まえ、ガードの効く側に寄せています。

両方を入れるときの注意:push と pull は自作だけに通す

両方の MCP サーバーを入れると、AI からは clasp 本体の push_files と自作の gas_push の両方が見えます。AI が push_files を選ぶと、自作のチェックをすべて素通りします。pull_files と gas_pull も同じで、pull_files を使われると本番フォルダのガードが効きません。

そのため、MCP クライアントの設定で、clasp 本体の push_files と pull_files を使えないように制御する必要があります。

  • Claude Code の場合:
    個別ツールを一覧から非表示にはできませんが、設定ファイル(.claude/settings.local.json)の permissions.deny に指定することで、Claude が実行しようとした際に自動でブロック(拒否)する仕様になっています(公式リファレンス記載の権限設定)。
    .claude/settings.local.json
    {
      "permissions": {
        "deny": [
          "mcp__clasp__push_files",
          "mcp__clasp__pull_files"
        ]
      }
    }
    
  • ツール個別の無効化ができないクライアント(Antigravity など)の場合:
    MCP サーバー単位でしか登録・解除ができない環境では、「ルールに使うなと書く」だけでは AI が素通りするおそれがあり、本記事が重視する「約束ではなく仕組みで止める」原則と矛盾します。
    そのため、ツール単位のブロック機能がないクライアントでは、clasp 本体の MCP サーバーは最初から登録しない運用を推奨します。プロジェクトの作成(create)や複製(clone)は頻繁に行う作業ではないため、必要なときだけ人間が通常の clasp CLI コマンドを直接実行する方が、誤操作の抜け道を塞げます。
    (※ なお、シェル実行権限を持つエージェントの場合、自作 MCP のガードを通さずにターミナルから直接 clasp push を実行してしまう道が残ります。このリスクを防ぐには、clasp コマンドの実行は人間が手動で行う運用にするか、エージェント側のシェル実行許可で clasp push を拒否する制御が必要です。)

今後の見方

clasp の MCP モードは、2026年11月時点では実験的な機能です。今後、正式な機能になったり、push 前の検証が本体に入ったりすれば、自作はその分だけ小さくします。本記事の構成は、その時点までの棲み分けです。


5. 実装の要点:push の前に止める仕組み

1. クラウドにしかないファイルを検知して止める

push の前に Apps Script API の projects.getContent でクラウド上のファイル一覧を取得します。push の対象一覧(clasp status --json の filesToPush)と比べ、クラウドにしかないファイルがあれば push を止めます。

src/remote_guard.ts
// クラウド側のファイル名と push 対象の照合(簡略化した抜粋)
export function detectRemoteOrphanDeletions(
    filesToPush: string[],
    remoteFileNames: string[],
    rootDir?: string
): string[] {
    const localNames = new Set(filesToPush.map(f => normalizeLocalFileToGasName(f, rootDir)));
    const willDelete: string[] = [];

    for (const remote of remoteFileNames) {
        if (!localNames.has(remote)) {
            willDelete.push(remote);
        }
    }
    return willDelete;
}

認証情報が読めない、API の呼び出しに失敗したなどで、クラウド側の一覧を確認できない場合も push を止めます。確認できないまま進めると、何が消えるかわからないためです。削除を承知で進めるときだけ、{ allowDeleteRemote: true } を渡します。

2. 実行前のコード検査をエラーとして返す

push の前に、push の対象ファイルだけを対象に、関数の重複定義と引数の不足を調べます。見つかった場合は警告ではなくエラーとして返し、ファイル名・行番号・内容を添えます。トラブル2とトラブル3を受けて直した部分です。

gas_push がブロックしたときの出力(実機)
🚫 [PUSH BLOCKED] Pre-push validation detected blocking errors in your GAS code.
Fix the issues below or pass { skipValidation: true } if you explicitly wish to bypass this check:

❓ UNDEFINED CALLS: 1 issue(s) detected:
  - broken.gs:2 -> PropertiesService.getScriptProperties().setProperty (missing arguments)

3. 本番のフォルダへの操作を拒否する

本番として指定したフォルダに対する push・pull・run・deploy などは、{ allowProtected: true } を明示しない限り拒否します。保護するフォルダは環境変数 GAS_PROTECTED_DIRS で指定します(カンマ区切りで複数指定可)。指定しなければ、どのフォルダも保護されません。

MCP クライアントへの登録例(Claude Code・SSH 経由)
claude mcp add gas-manager -- ssh sv-home GAS_PROTECTED_DIRS=/path/to/production /usr/bin/node /path/to/gas-manager-mcp/out/index.js

文字列のパスを比べるだけでは、シンボリックリンク経由の指定ですり抜けられるため、fs.realpathSync で実体のパスに直してから比べます。

src/remote_guard.ts
// 実体パスに直してから、保護対象のフォルダ配下かを判定する(抜粋)
export function isProtectedDirectory(dir: string, customDirs?: string[]): boolean {
    const canonicalDir = getCanonicalPath(dir);
    const envDirs = process.env.GAS_PROTECTED_DIRS
        ? process.env.GAS_PROTECTED_DIRS.split(",").map(p => p.trim()).filter(Boolean)
        : [];

    const allProtected = [...envDirs, ...(customDirs || [])].map(p => getCanonicalPath(p));

    return allProtected.some(protectedPath => {
        return canonicalDir === protectedPath || canonicalDir.startsWith(protectedPath + path.sep);
    });
}

4. エミュレータの無限ループを打ち切る

対象の関数の呼び出しも vm.runInContext 経由にして、5000ms で打ち切ります。以前は関数を直接呼んでいたため、無限ループがあるとエミュレータごと止まり、MCP クライアントも応答しなくなっていました。

src/emulator.ts
// 関数の呼び出しも vm 経由で実行し、無限ループを timeout で打ち切る
(vmContext as any).__emuArgs = funcArgs;
result = vm.runInContext(`${functionName}(...__emuArgs)`, vmContext, { timeout: 5000 });

5. 危ない操作には clasp 本体と同じ注釈を付ける

clasp 本体にならい、クラウドやローカルを書き換える gas_push・gas_pull・gas_run・gas_deploy には destructiveHint: true を付けました。読み取りだけの gas_status・gas_logs・gas_deployments・gas_emulate には readOnlyHint: true を付けています。

自作のチェックで中身を見て止めるのに加えて、注釈を扱える MCP クライアントでは、実行前に人の承認を求める動きも使えます。どちらか一方ではなく、両方を重ねる形です。

src/index.ts
// ツール定義の注釈(抜粋)
{
    name: "gas_push",
    // ...
    annotations: {
        title: "Push files with safety verification",
        destructiveHint: true,
        readOnlyHint: false,
        openWorldHint: true
    }
}

6. 結果とトレードオフ

clasp 本体の MCP モードとの比較

項目 clasp 本体の MCP モード 自作 gas-manager-mcp
作成・複製・一覧 あり 持たない(本体に任せる)
pull あり(destructiveHint で人の承認を促す) あり(本番フォルダのガード付き)
push あり(検証なし、destructiveHint で人の承認を促す) あり(事前のコード検査、クラウドにしかないファイルの検知、本番フォルダの保護)
ローカルでの実行確認 なし あり(エミュレータ)
run・logs・deploy・deployments・status なし あり

安全側に止めることの代償

  • push の待ち時間が増える: 事前のコード検査に加え、push のたびにクラウド側のファイル一覧を取得して照合するため、clasp 単体の push より時間がかかります。実環境での計測(3回の平均)では、clasp push 単体の 2.88 秒 に対し、gas_push は 4.55 秒 でした(差分 +1.67 秒)。安全確認のための通信・処理コストとして約 1.7 秒のオーバーヘッドが生じます
  • 意図した削除にも明示が要る: クラウド側のファイルを本当に消したいときや、本番のフォルダをあえて操作したいときは、allowDeleteRemote: true や allowProtected: true を渡す必要があります
  • MCP サーバーが2つになる: 棲み分けにしたため、クライアントには両方を設定し、clasp 本体の push_files と pull_files を無効にする手間が増えます

結論

clasp 本体に MCP モードが入ったことで、clasp を MCP として呼べるようにするだけの自作ツールは不要になりました。自作に残す価値があったのは、push の前に止める仕組み(コードの検査、クラウドにしかないファイルの検知、本番フォルダの保護)と、クラウドに反映せずに試せるエミュレータでした。

人が1回ずつ承認しながら使うだけなら、clasp 本体の MCP モードで足ります。AI に修正から push まで続けて任せたい場合や、消えると困る本番のプロジェクトがある場合は、自作の仕組みを重ねる意味があります。


この記事が参考になったら、ストックしておくとシリーズの続きを追いやすくなります。
質問や、別の環境で試した結果はコメントで教えてください。

2
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?