0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

opencodeの無料枠が大盤振る舞いなので、DeepSeek V4.1 FlashでCakePHP製OSSにMCPツール33個を追加した

0
Posted at

はじめに

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」のフォークに機能追加をしてみました
(リポジトリは後述)。

  • 追加した機能
    1. MCP ツールの網羅拡張(11 個 → 44 個)
    2. 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 エージェントで通した
  • 「どの機能をスコープに含めるか」の最終判断は人間が行った
  • 画像配信の権限制御や孤児画像の削除は、スコープ外としてあえてやらせていない

対象と初期状態

本記事の変更は、フォークの 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..." }]
  }
}

保存後の本文末尾に ![構成図](https://example.com/contents/file-image/xxx.png) が追記されます。

検証結果

検証 コマンド 結果
全テスト(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()

つまずいた点と学び

  1. 設計書を先に作るのが効く
    「REST API を正とする」と決めただけで、33 ツールの権限・エラー条件がブレませんでした。

  2. GUI 操作テストは AI 開発と相性が良い
    PHPUnit が緑でも、実ブラウザでは貼付けが動かず URL が 404。E2E が無いと気づけませんでした。

  3. テストの「弱い assert」は AI も騙す
    部分文字列一致の assert は、バグを温存します。AI にテストを書かせるときは
    「否定条件(〜を含まないこと)」も書かせるのが有効でした。

  4. サブエージェント委譲が使えない構成だった
    今回はエージェントのサブエージェント(役割別エージェント)が未登録で使えなかったため、
    1 つのエージェントで設計〜実装〜検証まで通しました。その分、
    フェーズ分割とコミット粒度を人間側で厳密に決めるのが重要でした。

  5. 無料枠でも「量」は十分
    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 項目が緑
  • 一番効いたのは 「意味論の正を決める」→「フェーズ分割」→「実機で検証」 の順番

無料枠が続いているうちに、試しておく価値はあると思います。
裏では続きの開発も進めているので、進展があればまた書きます。

参考リンク

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?