はじめに
opencode は「The open source coding agent」を名乗る高機能なコーディングエージェントです。
2026年10月15日、その opencode が DeepSeek V4.1 Flash を無料化 すると発表しました。
DeepSeek V4.1 Flash is now free in OpenCode
https://x.com/opencode/status/2110635747648909472 (3.2M views / 24K likes)
さらに翌 16 日には GLM 5.3 Flash も無料化。
GLM 5.3 Flash is now free in OpenCode
https://x.com/opencode/status/2110986357539754114 (398.9K views / 8.6K likes)
いま opencode はかなり大盤振る舞いしていて、モデル一覧を見ると Flash 級のモデルを含む
11 モデルが $0 で並んでいます。
ここまで高機能なコーディングエージェントを無料で使える機会は、そう長くは続かないと思います。
この無料枠を使って、CakePHP 製の eラーニング OSS「iroha Board」のフォークに機能追加をしてみました
(リポジトリは後述)。
- 追加した機能
- MCP ツールの網羅拡張(11 個 → 44 個)
- Markdown の画像貼付け(管理画面 EasyMDE の修正、REST API、MCP 経由の添付)
- 結果
- 全テスト 938 tests / 4620 assertions / OK
- 実ブラウザの GUI 操作テスト(Playwright)16/16 PASS
- 実機 MCP のスモークテスト 6/6 PASS
-
devブランチに 11 コミット(設計書 → 実装 → 検証まで)
「AI に丸投げ」ではなく、仕様書を先に作らせる → フェーズ分割して都度コミット → 実機で検証
という流れにしたのが良かったので、その進め方も含めて書きます。
この記事の執筆時点で、裏では続きの開発も進めています。
opencode の無料枠(2026年10月時点)
opencode のモデル一覧(opencode プロバイダ)で 入力・出力とも $0 のモデルは 11 本ありました。
| モデル | リリース(モデル公開日) |
|---|---|
ll-alpha-free |
2026-10-15 |
nemotron-3.5-super-vl-free(VL モデル) |
2026-10-15 |
cat-jam-free |
2026-10-12 |
longcat-2.5-preview-free |
2026-09-25 |
space-bunny-free |
2026-09-23 |
mimo-v2.6-flash-free |
2026-09-22 |
deepseek-v4.1-flash-free |
2026-09-10 |
muse-spark-1.3-contributor-free |
2026-09-02 |
glm-5.3-flash-free |
2026-08-26 |
nemotron-3.5-lightning-free |
2026-08-11 |
nemotron-3-ultra-free |
2026-06-04 |
高機能なコーディングエージェントが無料でここまで動くのは今だけかもしれません。
試すなら今がチャンスだと思います。
(料金・モデル名は変わりやすいので、実行前に必ず確認してください)
今回はこの中の DeepSeek V4.1 Flash(無料枠) を使いました。
高機能コーディングエージェントに、どこまでやらせたか
今回 opencode にやらせたのは「コードを書く」だけではありません。
| 工程 | 実際にやったこと |
|---|---|
| 調査 | リポジトリ全体を読み、既存 MCP 11 ツール・REST API 28 ルート・Model/Table・テスト構成を把握 |
| 設計 | 設計書を作成(スコープ・追加ツール一覧・引数仕様・段階的実装計画・テスト計画・非目標・リスク) |
| 実装 | MCP ツール 33 個、画像基盤、REST エンドポイント、管理画面テンプレート、設定を 11 コミットに分割 |
| テスト | PHPUnit を 100 件以上追加。落ちたら自分で原因を特定して修正 |
| E2E | Playwright(Python)で実ブラウザ操作テストを新規作成し、既存バグを 2 件検出 |
| 検証 | Docker の実機に対して、DB 再作成つき全テスト・MCP スモーク(curl)・GUI テストを実行 |
| 記録 | 設計書に実装状況と検証結果、Dev/scripts/README.md に実行手順を追記 |
「補完してくれる」ではなく、調べる → 設計する → 実装する → テストを書く → 実機で検証する
までを一気通貫でやれるのが、高機能コーディングエージェントの強みだと感じました。
逆に、できなかったこと・任せきれなかったことも書いておきます。
- 役割別サブエージェントへの委譲は、この環境では使える構成になっていなかったため、1 エージェントで通した
- 「どの機能をスコープに含めるか」の最終判断は人間が行った
- 画像配信の権限制御や孤児画像の削除は、スコープ外としてあえてやらせていない
対象と初期状態
- 作業したフォーク: https://github.com/yanma0102/irohaboard
- 本家: https://github.com/irohasoft/irohaboard (GPL-3.0 / シンプルなオープンソース LMS)
本記事の変更は、フォークの
devブランチで作業しています。
iroha Board は CakePHP 5.4 / PHP 8.4(動作要件は PHP 8.2 以上)/
MariaDB の eラーニング OSS で、Docker で起動しています。
すでに Markdown コンテンツと MCP サーバ(/mcp)が入っており、11 ツールが動いていました。
一方で、
- REST API v1 は users / groups / courses / contents / records の CRUD と割当を含む 28 ルートがあるのに、
MCP は 11 ツールしかない(AI クライアントから運用しきれない) - Markdown の画像は管理画面の手動アップロードのみ。しかも貼付け(Ctrl+V / ドラッグ&ドロップ)が
動かず、挿入される URL も 404 になる(後述の F-1 / F-2)
という状態でした。
進め方(ここが一番大事だった)
1. 先に「設計書」を AI に書かせる
いきなり実装させず、Dev/Docs/design/15-mcp-extension-markdown-image.md という設計書を作らせました。
ポイントは、意味論(権限・エラー条件・フィールド)は既存の REST API v1 を正とすると明記したことです。
- **意味論は REST API v1 を正とする。** 権限・絞り込み・更新可能フィールド・
エラー条件は `Api\*` コントローラと一致させる。
これで「MCP と REST で権限判定がズレる」という一番ありがちな事故を防げます。
設計書には 段階的実装計画(読み取り系 → 書き込み系 → 画像系)と
完了条件、テスト計画まで書かせてから、人間がスコープを承認しました。
2. フェーズごとにコミット
dev ブランチを切って、1 フェーズ = 1 コミット。
72fc291 docs: MCPツール網羅拡張とMarkdown画像添付の設計書を追加
d6aa368 test: 管理画面EasyMDE画像貼付けのGUI操作テストを追加
6045cd4 feat: 画像アップロード基盤(ImageStorageService)を追加
80c7888 fix: 管理画面の画像アップロードを共通サービス化し、EasyMDEの貼付けを修正
82b1099 feat: 画像アップロードのREST APIを追加
200f55b feat: MCP経由の画像添付(upload_image / images[])を追加
bd683b8 feat: MCPにコンテンツ・学習履歴の管理ツールを追加
7b97178 feat: MCPにコース・ユーザ管理ツールを追加
e18f202 feat: MCPにグループ管理ツールを追加
056c567 feat: MCPに参照系ツール11個を追加
848ee54 docs: 設計書15に実装状況(コミット・検証結果)を追記し、phpcsエラーを修正
3. テストを先に用意して、実機で検証する
- PHPUnit(既存の統合テストの流儀に合わせる)
- Playwright(Python)で実ブラウザの GUI 操作テスト
- 実機 MCP への curl スモークテスト
特に GUI 操作テストは、PHPUnit では見つからないバグを 2 件検出しました。
実装① MCP ツールの網羅拡張(11 → 44)
既存ツールの実装パターンをそのまま踏襲させました。
class DeleteContentTool
{
use HasOAuthContextTrait;
public function __construct(private AccessControlService $accessControl) {}
#[McpTool(
name: 'delete_content',
description: 'Delete a content (staff only, course membership required). '
. 'Related test questions are also deleted.',
)]
public function __invoke(RequestContext $context, int $content_id): array
{
$userId = $this->getUserId($context);
$role = $this->getRole($context);
if (!$this->accessControl->isStaff($role)) {
throw new ToolCallException('Only staff members can delete content.');
}
// ... 権限・存在チェック → 削除
return ['data' => ['id' => $content_id, 'deleted' => true]];
}
}
登録は McpServerFactory に明示(#[McpTool] 属性の name は明示登録時に読まれないため)。
->addTool(
[new DeleteContentTool($accessControl), '__invoke'],
name: 'delete_content',
description: 'Delete a content (staff only, course membership required). '
. 'Related test questions are also deleted.',
)
そして書き込みツールのレート制限漏れを忘れずに。
private const WRITE_TOOLS = [
'create_content', 'update_content',
'create_test_question', 'update_test_question', 'delete_test_question',
'delete_content', 'reorder_contents', 'copy_content', 'create_record',
'create_course', 'update_course', 'delete_course',
'create_user', 'update_user', 'delete_user', 'change_user_password',
'assign_user_course', 'unassign_user_course',
'create_group', 'update_group', 'delete_group',
'assign_group_user', 'unassign_group_user',
'assign_group_course', 'unassign_group_course',
'upload_image',
];
ちなみに既存の create_test_question / update_test_question も漏れていたので、ついでに直しました
(書き込みなのに読み取り枠の 60 回/分で実行されていた)。
実装② Markdown の画像貼付け(F-1 / F-2 の実バグ)
ここが今回いちばん面白いところでした。GUI 操作テストを書いたら、既存の不具合が 2 件出てきました。
F-1: 貼付け・ドラッグ&ドロップが動かない(オプション名の誤り)
同梱の EasyMDE は貼付けハンドラを uploadImage で判定しますが、テンプレートは imageUpload でした。
そのためツールバーのボタンだけが動き、Ctrl+V / ドラッグ&ドロップが無効でした。
- imageUpload: true,
+ // EasyMDE のオプション名は uploadImage(imageUpload は無視される)
+ uploadImage: true,
+ imageAccept: 'image/png, image/gif, image/jpeg',
+ imageMaxSize: 2097152,
F-2: 挿入される画像 URL が 404(admin prefix の混入)
管理画面(admin prefix)から Router::url() で URL を組み立てると、
存在しない /admin/contents/file-image/... が生成されていました。
- $file_url = Router::url(['controller' => 'Contents', 'action' => 'fileImage', $new_name], true);
+ return Router::url([
+ 'controller' => 'Contents',
+ 'action' => 'fileImage',
+ 0 => $fileName,
+ 'prefix' => false, // admin prefix を明示的に解除
+ ], true);
さらに、PHPUnit がこのバグを見逃していた理由
既存テストはこう書かれていました。
$this->assertStringContainsString('/contents/file-image/', $decoded[0]);
/admin/contents/file-image/xxx.png にも部分一致してしまうため、バグがあっても緑でした。
プレフィックスを検証する形に直しました。
$this->assertStringNotContainsString('/admin/', $decoded[0], '配信 URL に admin prefix が混入しないこと');
「テストがある」だけでは足りない、というのが身に沁みました。
共通サービス化して 3 経路で再利用
検証・保存・URL 生成は ImageStorageService に集約し、管理画面 / REST / MCP から共用します。
実体が画像かを getimagesize() で検証するので、拡張子偽装(.png を名乗る PHP)も弾けます。
$info = @getimagesize($path);
if ($info === false || empty($info['mime'])) {
throw new ImageUploadException(ImageUploadException::REASON_NOT_IMAGE, 'getimagesize() failed');
}
実装③ REST API / MCP 経由の画像アップロード
REST は POST /api/v1/contents/images(multipart/form-data、staff のみ)。
curl -sS -X POST 'https://example.com/api/v1/contents/images' \
-H 'Authorization: Bearer <selector:validator>' \
-F 'file=@diagram.png' -F 'course_id=3'
MCP は JSON なので base64 で受けます。upload_image で URL を得るか、
create_content / update_content の images[] で本文末尾にまとめて埋め込めます。
{
"name": "create_content",
"arguments": {
"course_id": 3,
"title": "図解つきレッスン",
"kind": "markdown",
"body": "# 図解",
"images": [{ "file_name": "diagram.png", "alt": "構成図", "data": "data:image/png;base64,iVBORw0KGgo..." }]
}
}
保存後の本文末尾に  が追記されます。
検証結果
| 検証 | コマンド | 結果 |
|---|---|---|
| 全テスト(DB 再作成) | bash Dev/scripts/test-fresh.sh |
938 tests / 4620 assertions / OK |
| MCP スモーク(実機) | bash Dev/scripts/smoke-mcp.sh |
6/6 PASS(tools/list 44 件、upload_image、list_users) |
| GUI 操作(実ブラウザ) | python3 Dev/scripts/gui-image-upload.py |
16/16 PASS |
| コーディング規約 | composer cs-check |
追加・変更した app/src は 0 error |
GUI 操作テストは Playwright で、実際にログイン → コンテンツ編集画面 →
upload-image ボタン / Ctrl+V 貼付け → プレビューと画像 URL の 200 まで確認します。
# 貼付け(クリップボードに画像を入れて Ctrl+V)まで実ブラウザで検証
page.keyboard.press("Control+V")
expect(page.locator(".editor-preview img")).to_be_visible()
つまずいた点と学び
-
設計書を先に作るのが効く
「REST API を正とする」と決めただけで、33 ツールの権限・エラー条件がブレませんでした。 -
GUI 操作テストは AI 開発と相性が良い
PHPUnit が緑でも、実ブラウザでは貼付けが動かず URL が 404。E2E が無いと気づけませんでした。 -
テストの「弱い assert」は AI も騙す
部分文字列一致の assert は、バグを温存します。AI にテストを書かせるときは
「否定条件(〜を含まないこと)」も書かせるのが有効でした。 -
サブエージェント委譲が使えない構成だった
今回はエージェントのサブエージェント(役割別エージェント)が未登録で使えなかったため、
1 つのエージェントで設計〜実装〜検証まで通しました。その分、
フェーズ分割とコミット粒度を人間側で厳密に決めるのが重要でした。 -
無料枠でも「量」は十分
33 ツール + 画像基盤 + REST/MCP + テスト 100 件超という規模でも、
無料枠の範囲で完走できました(トークン量より、指示の質のほうがボトルネックでした)。
まとめ
- opencode は 2026年10月時点で Flash 級モデルを含む 11 モデルを無料提供
(deepseek-v4.1-flash-free、glm-5.3-flash-freeなど) - その無料枠の高機能コーディングエージェントで、CakePHP 製 OSS に
**MCP ツール 33 個追加(計 44)**と **Markdown 画像貼付け(管理画面 / REST / MCP)**を実装 - 全テスト 938 件・GUI 操作 16 項目・MCP スモーク 6 項目が緑
- 一番効いたのは 「意味論の正を決める」→「フェーズ分割」→「実機で検証」 の順番
無料枠が続いているうちに、試しておく価値はあると思います。
裏では続きの開発も進めているので、進展があればまた書きます。