【1】はじめに

Windows 11のPCに付いているWebカメラを、GitHub Copilot(VS Code)のプロンプトから使い、撮影した写真をGoogleレンズのようにAIに解説させる仕組みを作りました。 MCP(Model Context Protocol)は、AIアプリに外部のツールやデータを接続するための共通の仕組みです。 今回は「カメラで撮影する」「撮影してAIで解析する」という2つのツールを、自作のMCPサーバーとして提供し、GitHub Copilotから呼び出します。
この記事の特徴は、私はコードを1行も書いていない(触っていない)ことです。 コードはすべてAI(GeminiとGitHub Copilot)が作成しました。いわゆるバイブコーディングの記録として、経緯と失敗も含めて紹介します。
実際に動かすと、次のような結果が返ってきます。

カメラで撮影して、Googleレンズのように撮影内容を解説して。 撮影してGeminiで解析しました。画像には、セブンプレミアムのブラックコーヒーと思われるペットボトルが手前に写り、 中央には眼鏡をかけた短髪の人物、その後ろには木製の収納家具やリモコンが見えます。 商品名やボトルの残量は画像からの推定です。 撮影画像はcaptures\snapshot_1792896858357.bmpに保存されています。
システム構成図
システム構成図

【2】経緯

2-1.Geminiに相談

最初はGeminiに相談しました。私は「既存のMCPサーバーを探して、それを設定する」ことを想定していました。 ところが、Geminiは既存のカメラ用MCPサーバーを紹介せず、MCPサーバーを自作することを提案してきました。 カメラ用として適切な既存のサーバーが見つからなかったのかもしれません。他に選択肢もなかったので、この提案を採用しました。

2-2.Geminiのコードでは動かなかった

Geminiが出したコードは、そのままでは動きませんでした。主な問題は次のとおりです。

そこで、GitHub Copilot(エージェントモード)にコードの修正を依頼しました。 以降はGitHub Copilotが修正を続け、最終的に動作するコードになりました。 BMPとJPGの問題は、BMPを削除するのではなく、「Windowsでは最初からBMPで撮影し、sharpでPNGに変換してGeminiへ送る」方式に変更されました。

2-3.役割分担

役割担当
構成の提案、最初のコードGemini
コードの修正、環境変数の設定方法の再案内GitHub Copilot
APIキーの取得、環境変数の設定、動作確認私(人間)

【3】実行環境

使用した環境は次のとおりです。

Node.jsのパッケージ(npm)は、AIが選んだものをそのまま使っています。

パッケージ用途
@modelcontextprotocol/sdkMCPサーバーのSDK
node-webcamWebカメラでの撮影
sharp画像の変換(BMP → PNG)
@google/genaiGemini APIのクライアント

仕組みは、システム構成図のとおりです。GitHub Copilotがツールを呼び出すと、MCPサーバー(Node.js)がカメラで撮影し、画像をGemini APIに送って解説を受け取り、結果をテキストでGitHub Copilotに返します。 撮影した画像は、MCPサーバーのフォルダ内のcapturesフォルダに保存されます。

【4】Gemini APIキーの取得

Google AI Studioで取得します。無料枠が用意されていますが、利用条件は変わることがあるので、AI Studioの画面で最新の内容を確認してください。

  1. ブラウザでGoogle AI Studioにアクセスし、Googleアカウントでログインする
  2. 初回は利用規約への同意画面が出るので、同意して進む
  3. 「Get API key」を開く
  4. 「Create API key」をクリックし、プロジェクトを選択(または新規作成)する
  5. 生成されたキーをコピーする
※ APIキーはパスワードと同じです。GitHubなどの公開リポジトリにアップロードしたり、SNSや記事のスクリーンショットに写り込ませたりしないよう注意してください。

【5】APIキーの設定(ここでつまずきました)

5-1.Geminiの案内(うまくいかなかった)

Geminiは、次のようにターミナルで設定する方法を案内しました。

(コマンドプロンプトの場合) set GEMINI_API_KEY=あなたのAPIキー (PowerShellの場合) $env:GEMINI_API_KEY="あなたのAPIキー"

この方法は、そのターミナルのウィンドウの中だけで有効な設定です。 VS Codeをアイコンなどから起動した場合、この設定は引き継がれないため、MCPサーバーからキーが見えず、動作しませんでした。

5-2.GitHub Copilotに聞き直して解決

GitHub Copilotに聞き直したところ、ユーザー環境変数として登録する方法を案内されました。

  1. スタートメニューで「環境変数を編集」を検索して開く
  2. 「環境変数(N)...」を選び、「ユーザー環境変数」の「新規(N)...」をクリックする
  3. 次のように入力する
    変数名:GEMINI_API_KEY
    変数値:Google AI Studioで発行したAPIキー
  4. 開いているVS Codeをすべて終了してから、起動し直す

環境変数は、起動後のMCPサーバーにも引き継がれます。起動したままのVS Codeには反映されないので、いったん完全に終了して起動し直すことが重要です。

5-3.APIキーの扱い方

今回の構成では、APIキーをコードにも設定ファイルにも書いていません。キーがあるのは、Windowsのユーザー環境変数だけです。
server.jsは、process.env.GEMINI_API_KEYから実行時にキーを読み込みます。 .vscode/mcp.jsonにも、キーを渡す記述はありません。 このため、ソースコードをGitHubに公開したり、記事に貼ったりしても、キーは漏れません。

キーが設定されていない場合、撮影後のAI解析(take_and_analyze)は「GEMINI_API_KEY 環境変数が設定されていません。」というエラーを返します。 撮影のみのツール(take_snapshot)は、キーなしでも動きます。

キーが設定されているかどうかは、値を表示せずに確認できます。VS Codeを起動し直したあと、VS Codeのターミナル(PowerShell)で実行します。

if ($env:GEMINI_API_KEY) { "設定あり" } else { "未設定" }
※ キーの値を表示すると、スクリーンショットや画面共有で写り込む恐れがあるので、上記の方法にしています。 キーが漏れた可能性があるときは、Google AI Studioでキーを削除して、新しく発行し直してください。

【6】完成したMCPサーバー

最終的に、次の3つのファイルで動作しました。ファイルはすべてAIが作成・修正したもので、私は一切触っていません。

mcp-camera-server/ ├── .vscode/ │ └── mcp.json ├── captures/ (撮影画像が保存される場所) ├── package.json └── server.js

package.json(npm installで依存パッケージをインストールします)

{ "name": "mcp-camera-server", "version": "2.0.0", "type": "module", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "@google/genai": "^2.31.0", "@modelcontextprotocol/sdk": "^1.32.1", "node-webcam": "^0.8.2", "sharp": "^0.34.5" } }

.vscode/mcp.json(VS CodeのGitHub Copilotに、このサーバーを認識させる設定です)

{ "servers": { "camera-server": { "type": "stdio", "command": "node", "args": [ "${workspaceFolder}/server.js" ] } } }

server.js

import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import NodeWebcam from "node-webcam"; import fs from "fs"; import path from "path"; import { GoogleGenAI } from "@google/genai"; import sharp from "sharp"; // 保存先フォルダの作成 const outputDir = path.join(process.cwd(), "captures"); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir); } // カメラの設定 const Webcam = NodeWebcam.create({ width: 1280, height: 720, quality: 100, output: process.platform === "win32" ? "bmp" : "jpeg", callbackReturn: "location", verbose: false }); const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); async function getPngBuffer(imagePath) { const imageBuffer = fs.readFileSync(imagePath); if (imageBuffer.toString("ascii", 0, 2) !== "BM") { return sharp(imageBuffer).png().toBuffer(); } const pixelOffset = imageBuffer.readUInt32LE(10); const headerSize = imageBuffer.readUInt32LE(14); const width = imageBuffer.readInt32LE(18); const signedHeight = imageBuffer.readInt32LE(22); const bitsPerPixel = imageBuffer.readUInt16LE(28); const compression = imageBuffer.readUInt32LE(30); if (headerSize < 40 || width <= 0 || signedHeight === 0 || bitsPerPixel !== 24 || compression !== 0) { throw new Error("撮影画像のBMP形式に対応していません。"); } const height = Math.abs(signedHeight); const rowStride = Math.ceil((width * 3) / 4) * 4; if (pixelOffset + rowStride * height > imageBuffer.length) { throw new Error("撮影画像のBMPデータが不完全です。"); } const pixels = Buffer.alloc(width * height * 3); for (let y = 0; y < height; y++) { const sourceY = signedHeight > 0 ? height - 1 - y : y; for (let x = 0; x < width; x++) { const sourceOffset = pixelOffset + sourceY * rowStride + x * 3; const targetOffset = (y * width + x) * 3; pixels[targetOffset] = imageBuffer[sourceOffset + 2]; pixels[targetOffset + 1] = imageBuffer[sourceOffset + 1]; pixels[targetOffset + 2] = imageBuffer[sourceOffset]; } } return sharp(pixels, { raw: { width, height, channels: 3 } }).png().toBuffer(); } // MCPサーバーの初期化 const server = new Server( { name: "camera-mcp-server", version: "2.0.0", }, { capabilities: { tools: {}, }, } ); // 利用可能なツールの定義 server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "take_snapshot", description: "PCのWebカメラを使って静止画を撮影し、保存した画像のファイルパスを返します。", inputSchema: { type: "object", properties: {}, }, }, { name: "take_and_analyze", description: "PCのWebカメラで写真を撮影し、GoogleレンズのようにAI(Gemini)に写っているものを解析・解説させます。", inputSchema: { type: "object", properties: {}, }, }, ], }; }); // ツールが呼び出されたときの処理 server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "take_snapshot" || request.params.name === "take_and_analyze") { return new Promise((resolve, reject) => { const fileName = `snapshot_${Date.now()}`; const baseFullPath = path.join(outputDir, fileName); Webcam.capture(baseFullPath, async (err, data) => { if (err) { reject(new Error(`カメラの撮影に失敗しました: ${err.message}`)); return; } // dataには拡張子を含む撮影画像のパスが返ります。 const imagePath = data; if (request.params.name === "take_snapshot") { resolve({ content: [ { type: "text", text: `カメラの撮影に成功しました。保存先: ${imagePath}`, }, ], }); return; } try { if (!process.env.GEMINI_API_KEY) { throw new Error("GEMINI_API_KEY 環境変数が設定されていません。"); } const pngBuffer = await getPngBuffer(imagePath); const response = await ai.models.generateContent({ model: "gemini-3.8-flash", contents: [ { inlineData: { data: pngBuffer.toString("base64"), mimeType: "image/png", }, }, "この画像に写っているものについて、Googleレンズのように詳細に解説やコメントをしてください。日本語で分かりやすくお願いします。", ], }); const analysisText = response.text || "解析結果を取得できませんでした。"; resolve({ content: [ { type: "text", text: `【撮影・AI解析完了】\n保存先: ${imagePath}\n\n■ AIからのコメント:\n${analysisText}`, }, ], }); } catch (aiError) { reject(new Error(`画像のAI解析に失敗しました: ${aiError.message}`)); } }); }); } throw new Error(`未知のツールです: ${request.params.name}`); }); // 標準入出力でサーバーを起動 async function main() { const transport = new StdioServerTransport(); await server.connect(transport); } main().catch((error) => { console.error("サーバーエラー:", error); process.exit(1); });

AIが作成したコードを読んで、分かった範囲のポイントです。

※ モデル名のgemini-3.8-flashは、GitHub Copilotが書いたものがそのまま動作しています。 将来モデルが整理されて動かなくなった場合は、まずこの部分を、その時点で利用可能なモデル名に直してください。

【7】使い方

  1. VS Codeでプロジェクトのフォルダを開く(.vscode/mcp.jsonが読み込まれます)
  2. GitHub Copilot Chatを開き、エージェントモードにする
  3. 「カメラで撮影して、Googleレンズのように撮影内容を解説して。」のように話しかける
  4. 初回は、MCPツールの実行を許可するかどうかの確認が表示されるので、「このセッションで許可する」を選ぶ
ツールの許可の確認画面
ツールの実行を許可する確認画面

カメラが一瞬動き、capturesフォルダに画像が保存され、Geminiによる解説がチャットに表示されます。

【8】感想と注意点

8-1.バイブコーディングで感じたこと

8-2.安全面の注意

8-3.今後やってみたいこと

【9】おわりに

カメラ用の既存MCPサーバーを探すつもりが、Geminiの提案で自作することになりました。 Geminiのコードはそのままでは動かず、GitHub Copilotに修正してもらって完成しました。 APIキーは、ターミナルのコマンドではなく、Windowsのユーザー環境変数に登録してVS Codeを起動し直す必要がありました。
コードを一切触らずに、AIとの対話だけで、Googleレンズのようなカメラ解説ツールが動いたことには驚きました。


【改訂記録】
2026/10/25版:公開

[ホームへ戻る]