前回の記事(AIエージェントに GAS 開発を任せたら、MCP サーバー自作に行き着いた話)では、CLIツール(clasp)の操作をMCP (Model Context Protocol) サーバー化することで、AIエージェントがGoogle Apps Script (GAS) 環境に対して自律的なデプロイやコード実行を行えるアーキテクチャについて解説しました。
本記事はその続編として、実際の運用で見えてきた「AIの試行錯誤におけるレイテンシ(遅延)の問題」と「GAS特有のデプロイ・権限管理の仕様」に対し、ローカルエミュレータ実装とPROXYアーキテクチャパターンを導入して解決を図った事例を紹介します。
TL;DR
-
課題:
clasp push/runの数十秒の遅延と、デプロイ時の Web App 権限リセットが AI エージェントの自律開発を阻害する。 -
解決1 (高速化): Node.js
vmモジュールによるローカルエミュレータを実装。ロジック検証をミリ秒単位へ短縮。 - 解決2 (安全性): PROXYパターンを採用。公開設定(Proxy)とロジック(Logic)を分離し、インターフェースを固定化することで「権限リセット」を回避。
- 結果: AI エージェントが安全かつ高速に試行錯誤を繰り返せる、真に自律動作可能な GAS 開発環境を実現。
運用で見えてきた2つの技術的課題
MCPサーバー gas-manager-mcp の導入により、AIエージェントは自律的に clasp push を行い、記述したコードを実行できるようになりました。しかし、運用を重ねる中で以下の要改善点を確認しました。
課題1:開発サイクルの遅延(レイテンシ)
AIエージェントがデバッグを行う際、小さなコード修正と実行確認を繰り返します。しかし、GASの構成上、確認のたびに「クラウドへのデプロイ(clasp push)」と「リモート実行(clasp run)」が必要となります。
このネットワーク通信を伴う数十秒のレイテンシは、エージェントの試行錯誤のイテレーションを停滞させ、連続的な修正の効率を下げる要因となっていました。
課題2:Web App デプロイ時の「権限リセット」
外部連携用(Webhook等)のエンドポイントとしてGASを「Web App」としてデプロイしている場合、clasp deploy でバージョンを更新すると、アクセス権限が初期状態の「Only myself (自分のみ)」にリセットされるGASの仕様があります。
AIエージェントがデバッグのためにデプロイを繰り返すと、そのたびに外部システムからのアクセスが遮断されるリスクがありました。
課題1の解決:gas_emulate によるローカル検証環境
クラウドへのデプロイを待たず、ローカルで即座にロジックの動作検証が可能となるよう、MCPサーバー内にエミュレート機能を実装しました。
Node.js vm モジュールによるサンドボックス
Node.js の vm モジュールを使用して隔離された実行コンテキストを用意し、そこに GAS 特有のグローバルオブジェクト(SpreadsheetApp や PropertiesService 等)のモックを注入します。これにより、クラウドへ push せずに GAS コードをローカルで実行する gas_emulate ツールを実現しました。
実装:副作用の可視化
単にコードを実行するだけでなく、実行前のスプレッドシートの状態(MockState)を注入し、実行終了後の状態を抽出して返す構造を採用しています。これにより、ロジックによるデータ操作の結果を客観的に検証可能です。
gas-manager-mcp/src/emulator.ts における実行コンテキスト構築の抜粋です。
// --- スプレッドシートのエミュレータインスタンス生成 ---
const ss = new Spreadsheet("EmulatedSS");
if (mockState) {
ss.loadState(mockState);
}
// --- GAS環境を模倣したコンテキストの構築 ---
const context = {
SpreadsheetApp: {
...SpreadsheetAppEmulator,
getActiveSpreadsheet: () => ss,
openById: (id: string) => ss,
},
Utilities: UtilitiesEmulator,
DriveApp: DriveAppEmulator,
PropertiesService: PropertiesServiceEmulator,
console: {
log: (...m: any[]) => console.error(`[GAS_LOG] ${m.join(" ")}`),
},
Date, Math, Object, Array, String, Number, JSON, Error
};
// --- サンドボックスの作成と実行 ---
const vmContext = vm.createContext(context);
vm.runInContext(gasCode, vmContext);
const targetFunc = (vmContext as any)[functionName];
const result = targetFunc(...funcArgs);
const finalState = ss.dumpState();
結果
AIエージェントは「ミリ秒単位」で動作検証が可能になりました。エミュレータでロジックの正確性を確認した上で、最終段階で一度だけ clasp push を行うフローへ最適化されました。
エミュレータの対象範囲と限界
現時点の gas_emulate は、ロジック操作の検証に必要な最小限の機能を対象としています。
-
Spreadsheet Range (セル操作) : 対応レベル 高
-
getValues(),setValues(),copyTo(),clearContent()等。 - A1表記のパースや、
setFormulas()時の特定関数(GOOGLEFINANCE等)へのモック値返却に対応。
-
-
PropertiesService (プロパティストア) : 対応レベル 中
- スクリプトプロパティの操作をインメモリでエミュレート。実行終了時に状態はリセットされます。
-
UrlFetchApp (外部通信) : 対応レベル 限定的
- 実際のHTTPリクエストは行わず、常に固定のダミーデータを返します(特定のAPI連携テスト用)。
-
Trigger (トリガー設定) : 未対応
-
ScriptApp自体は未実装のため、doGet(e)等のテストはargsにイベントオブジェクトを擬似的に渡して検証します。
-
課題2の解決:PROXYパターンによる役割分離
「Web App権限リセット」に対し、エンドポイント(公開設定)とビジネスロジック(更新対象)を分離するアーキテクチャを採用しました。
アーキテクチャ構成
システムを「フロントエンド(Proxy)」と「バックエンド(Logic)」の2つのGASプロジェクトに分離します。
なぜこの分離で問題が解消されるのか
GASの「権限リセット」は、Web Appとして公開されているプロジェクトに対して clasp deploy(新しいバージョンの作成と公開)を行ったタイミングで発生します。
PROXYパターンでは、以下の仕組みによってこの問題を回避します。
- 公開設定の固定化: 外部と接する「Proxyプロジェクト」は、一度デプロイして権限を「Everyone」に設定した後は、二度とデプロイ操作を行いません。これにより、公開URLと権限設定が半永久的に維持されます。
- インターフェースの固定化: ProxyとLogicの間の受け渡し口(インターフェース)をシンプルに固定(例:すべてのデータを一つのオブジェクトとして渡す等)します。この「接点」さえ変わらなければ、実態であるLogic側をどれだけ変更してもProxy側を書き換える必要がなくなり、結果としてProxyの再デプロイが不要になります。
-
ロジック更新の非破壊化: AIが編集する「Logicプロジェクト」は、単なるスクリプトライブラリとして存在させます。ライブラリ側のコードを
clasp pushで更新しても、それは「保存」に過ぎず「Web Appとしてのデプロイ」ではないため、権限設定に影響を与えません。 -
最新コードの自動追従: ProxyからLogicを呼び出す際、ライブラリのバージョン指定を「Development Mode (HEAD)」に設定します。これにより、Logic側で
clasp pushするだけで、Proxy側は再デプロイすることなく即座に最新のロジックを実行できるようになります。
この「公開設定の凍結」と「ロジック更新の柔軟性」の両立が、本パターンの核心です。
サンプル構成(Before / After)
【Before】単一プロジェクト構成(権限リセットが発生)
function doGet(e) {
try {
const result = calculateSomething(e.parameter.data);
return ContentService.createTextOutput(JSON.stringify({ status: "ok", data: result }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({ status: "error" }));
}
}
function calculateSomething(data) {
// 修正のたびに clasp deploy が必要になり、権限がリセットされる
return data * 2;
}
【After】PROXYパターン(分離構成)
Proxy側(デプロイ設定を維持)
function doGet(e) {
try {
// LogicApp ライブラリ(Development Mode参照)へ移譲
const result = LogicApp.executeProcess(e.parameter.data);
return ContentService.createTextOutput(JSON.stringify({ status: "ok", data: result }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) { /* error handling */ }
}
Logic側(AIが更新する対象)
function executeProcess(data) {
// Web App としてデプロイしていないため、何度 push しても公開権限に影響しない
return data * 2;
}
FAQ
Q. エミュレータで動けば本番でも必ず動きますか?
A. 保証はありません。実行時間制限(6分)や未実装のAPI、トリガーの正確な挙動などは本番環境での確認が必要です。本ツールはロジックの高速なイテレーションを支援するものです。
Q. Proxyパターンによる性能低下はありませんか?
A. ライブラリ呼び出しのオーバーヘッドは僅かにあります。しかし、AIによる更新で外部接続性が損なわれるリスクを排除できる堅牢性のメリットを優先しています。
さらなる効率化に向けた展望
本構成を発展させ、自律開発を強化するための拡張案です。
1. モックのテンプレート化
SpreadsheetApp の最小構成モックを以下のように定義することで、導入コストを下げることが可能です。
export const SpreadsheetAppMock = {
activeSpreadsheet: null,
getActiveSpreadsheet() {
if (!this.activeSpreadsheet) {
this.activeSpreadsheet = new MockSpreadsheet("Untitled");
}
return this.activeSpreadsheet;
}
};
class MockSpreadsheet {
constructor(private name: string) {}
getSheetByName(name: string) { return new MockSheet(name); }
}
2. CI/CDパイプラインとの統合
エミュレーションによる検証をCIに組み込むことで、信頼性の高い自動デプロイを実現できます。
結論
ローカルエミュレータによるレイテンシ解消と、PROXYパターンによる権限管理の安定化により、GAS開発はAIエージェントにとってより自律運用が容易な環境となりました。
開発において直面する制約に対しては、インフラ層(MCPサーバー)への介入とアーキテクチャの再定義が、AIの利活用を最大化する鍵となりうると考えます。
