skydum

個人的な作業記録とか備忘録代わりのメモ

AntigravityでVS Codeのマーケットプレイスを利用する

Antigravityで最近よく遊んでるけどとても便利だ。
GitHub CopilotのAgentモード, Codex, Clineも使ってみたけど今のところAntigravityが今までで一番便利。
GitHub CopilotはAgentより通常のコードの補完が便利なので比較対象として少し違うのかもしれないが。

最近ChatGPT PlusからGoole AI Pro(Gemini)に契約を切り替えたので上限にも引っ掛かりづらいから便利。
(ChatGPTは5になってから回答に対する手抜きがひどいし、フォローアップの質問が鬱陶しい…。)

Google Antigravity Changelog

Antigravityを使うとデフォルトのマーケットプレイスOpen VSXになっていて、配布されている拡張機能がどこまで信用できるのか不安なのでAntigravityでMicrosoftマーケットプレイスを利用できるように設定をしたので忘れないようにメモ。

settings.jsonに以下を記載すれば良い、まだ設定したばっかりなので拡張機能を入れて問題なく動作するのかは確認中。
多分問題はないはずだけれども、自己責任でお願いします。

{
    "antigravity.marketplaceExtensionGalleryServiceURL": "https://marketplace.visualstudio.com/_apis/public/gallery",
    "antigravity.marketplaceGalleryItemURL": "https://marketplace.visualstudio.com/items"
}

GUIからなら以下の場所に記載。

動作確認したAntigravityのバージョンは以下。

バージョン: Antigravity
コミット: 1.11.14
日付: 1.104.0 (user setup)
Electron: a0709bf9d9cab28e92a313859c64f1ca9b6114f9
ElectronBuildId: 2025-12-05T05:51:32.596Z
Chromium: 37.3.1
Node.js: 138.0.7204.235
V8: 22.18.0
OS: 13.8.258.31-electron.0

VSCodeのDev Containerでgoの開発

Goで開発しないといけなくなりそうなのでGoの勉強をしている。
Goの開発をしようと思うと開発環境を作らないといけないがGoはコンパイル型言語なので環境の構築が面倒。

実際に開発する時にもやっぱり環境構築が必要になって、構築の説明とかするのがDev Containerが良いよなと思うので、Dev Containerの環境をつくった。 最近本当にDev Containerの環境は作りやすくなった。(あんまりDev Containerを使わないので、毎回作り方を忘れてしまうが…。)

プロジェクトのディレクトリを作成して、以下のディレクトリの中に以下のファイルを作成。

.devcontainer/devcontainer.json

devcontainer.jsonの中身は以下のように設定する。

{
    "name": "Go Development",
    "image": "golang:latest",
    "forwardPorts": [],
    "customizations": {
        "vscode": {
            "extensions": [
                "golang.go",
                "r3inbowari.gomodexplorer"
            ]
        }
    },
    "remoteUser": "root"
}

Goは以下が参考になった。
Goの初心者が見ると幸せになれる場所 #golang #Go - Qiita

Go 言語入門

書いてみた感じは結構開発がしやすいなと思った。
Pythonはインデントがスペースだけど、Goはインデントがタブなので中々慣れない。

最近はあんまり新しいことをしていないのでブログに書くことがあまりない、なにか面白いことがやってみたい。

python+geminiだけで動く単純なMCPサーバを作ってみる

MCPサーバとクライアントを作ってみる

世の中にあるMCPサーバの作り方の記事を読んでも私の理解力が低すぎるからだと思うが、Claude Desktopとの連携だったりGoogle Colaboを使っていたりとかであんまり良く理解できなかったので、物凄くシンプルなものから作ってみる。
作りたいのはMCPのクライアントからMCPのサーバに問い合わせを投げて最終的にMCPのクライアントから結果を返してもらうっていう単純なもの。

Claude Desktopとはは使わず、純粋にpythonだけで完結するようにする。
作ってみたらなんとなくがだ理解が捗った気がする。
でも、よくわからないのがLLMが何度もMCPサーバを呼び出して最終的に結果を取得するみたいなことをしたいとした場合、どうやってLLMを何度も呼び出せばいいのか、また結論が出たと判断するタイミングをどうするのか?がまだよくわかっていないので、もうしばらく触ってみたいと思う。
流行っているだけあって、便利そうだなとは思った。

最終版のシーケンス図

sequenceDiagram
    participant User
    participant Gemini
    participant ClientApp
    participant MCPServer
    participant greetFunction
    participant farewellFunction

    User->>ClientApp: 名前と時刻を入力(例: "Alice", "10:00")

    ClientApp->>Gemini: 挨拶してほしい内容と使える機能リストをAIに送る
    Note right of Gemini: 例:「今の時刻は10:00です。Aliceさんに挨拶してください」\n(使える機能: greet, farewell)

    Gemini-->>ClientApp: 選択した機能(function_call結果: greet, {"name": "Alice"})を返す
    ClientApp->>MCPServer: greet, {"name": "Alice"}
    MCPServer->>greetFunction: greet("Alice")
    greetFunction-->>MCPServer: "Hello, Alice!"
    MCPServer-->>ClientApp: "Hello, Alice!"
    ClientApp-->>User: "Hello, Alice!"

    alt 午後の場合
        User->>ClientApp: 名前と時刻を入力(例: "Alice", "15:00")
        ClientApp->>Gemini: 挨拶してほしい内容と使える機能リストをAIに送る
        Note right of Gemini: 例:「今の時刻は15:00です。Aliceさんに挨拶してください」\n(使える機能: greet, farewell)
        Gemini-->>ClientApp: 選択した機能(function_call結果: farewell, {"name": "Alice"})を返す
        ClientApp->>MCPServer: farewell, {"name": "Alice"}
        MCPServer->>farewellFunction: farewell("Alice")
        farewellFunction-->>MCPServer: "Goodbye, Alice!"
        MCPServer-->>ClientApp: "Goodbye, Alice!"
        ClientApp-->>User: "Goodbye, Alice!"
    end

仕様

  1. python3.13
  2. fastmcp 2.5.2
  3. google-genai 1.18.0
  4. uv

【接続の確認】いちばん簡単なMCPサーバとクライアント

実行方法

起動は以下のコマンドで実行する。
mcp_simple_server.pyは実行しなくても良い。
今回の方法の場合はmcp_simple_client.pyを起動するとmcp_simple_server.pyをサブプロセスでクライアンが起動してくれる。

geminiを使うサンプルはgeminiのAPIキーが必要なので取得して、環境変数にGEMINI_API_KEYの名称で登録してください。

$ uv init
$ uv add google-genai
$ uv add fastmcp
$ uv run mcp_simple_client.py              
Client connected: True

MCPサーバ

  • mcp_simple_server1.py
from fastmcp import FastMCP

mcp: FastMCP = FastMCP("My MCP Server")

if __name__ == "__main__":
    mcp.run()

MCPクライアント

  • mcp_simple_client1.py
import asyncio
from pathlib import Path

from fastmcp import Client

version = Path(__file__).name.split(".py")[0][-1]
server = "mcp_simple_server{}.py".format(version)


client: Client = Client(server)

async def main():
    async with client:
        print(f"Client connected: {client.is_connected()}")

if __name__ == "__main__":
    asyncio.run(main())

【基本】MCPクライアントからMCPサーバへリクエストをする

MCPクライアントからMCPサーバへのリクエストは基本的にmcp.toolに対して行われる。
これだとMCPクライアントってMCPサーバのtoolを呼び出すだけだから何が便利なんだけってなる。
便利になるのはMCPクライアントのmainにgeminiを入れてgeminiにMCPサーバのどのtoolを呼び出すのか決めさせることができるところかな?
ここでは単純にMCPクライアントからMCPサーバのtoolを自分で呼んでる。

以下のようなレスポンスが取得できる。

Client connected: True
Tool result: [TextContent(type='text', text='Hello, Alice!', annotations=None)]

サーバ

from fastmcp import FastMCP
from rich import print

mcp: FastMCP = FastMCP("My MCP Server")


@mcp.tool()
def greet(name: str) -> str:
    return f"Hello, {name}!"


if __name__ == "__main__":
    mcp.run()

クライアント

import asyncio
from pathlib import Path

from fastmcp import Client

version = Path(__file__).name.split(".py")[0][-1]
server = "mcp_simple_server{}.py".format(version)


client: Client = Client(server)


async def main():
    async with client:
        print(f"Client connected: {client.is_connected()}")

        # MCP サーバで定義した@map.tool()のdefの名称を指定
        tool_name = "greet"
        # toolに渡すパラメータを指定
        tool_args = {"name": "Alice"}

        tool_result = await client.call_tool(tool_name, tool_args)

        print(f"Tool result: {tool_result}")


if __name__ == "__main__":
    asyncio.run(main())

【応用】MCPクライアントにgeminiを入れてgeminiにMCPサーバのtoolの使い分けをさせる

単純なもののほうがシンプルでいいよねと思うので以下の仕様とする。
MCPクライアントにgeminiを入れて、geminiに時間帯によって挨拶とさようならを使い分けさせる。
余談だけどgeminiのAPIキーは無料で取得できるのでちょっと使うなら便利なので取っておくと良いと思う。
取り方は最後に記載する。

サーバ

こっちは説明するほどの内容もないが、MCPクライアントから呼び出せる機能が2個ある。
greetが呼び出されたらHelloを返し、farewellが呼び出されたらGoodbyを返すという単純なもの。

from fastmcp import FastMCP

mcp: FastMCP = FastMCP("My MCP Server")


@mcp.tool()
def greet(name: str) -> str:
    return f"Hello, {name}!"


@mcp.tool()
def farewell(name: str) -> str:
    return f"Goodbye, {name}!"


if __name__ == "__main__":
    mcp.run()

クライアント

一気にコードの量が増えてややこしくなるが、なるべく機能はシンプルにして理解しやすいように書いてみた。

仕様

MCPクライアントが実行されるときに名前を渡すと実行された時間帯によって適切な挨拶が返ってくるという機能。 プログラムではMCPサーバのどの機能を呼び出すのかは決定せず、Geminiに渡すプロンプトに含まれる時間を判断して、greet, farewellのどちらを呼び出すのかを決める。 その後、MCPクライアントがMCPサーバを呼び出してレスポンスを取得して、MCPクライアントとして挨拶文を返す。 詳細な動作は上の方に書いたシーケンス図を見たほうが分かりやすいと思う。

import asyncio
import os
from datetime import datetime
from pathlib import Path

import mcp
from fastmcp import Client
from google import genai
from google.genai import types

GEMINI_API_KEY: str = os.environ.get("GEMINI_API_KEY")  # type:ignore
GEMINI_MODEL = "gemini-2.0-flash"

if not GEMINI_API_KEY:
    raise ValueError("GEMINI_API_KEY is not set")

version = Path(__file__).name.split(".py")[0][-1]
server = "mcp_simple_server{}.py".format(version)

client: Client = Client(server)


class Gemini:
    """
    Gemini LLMとの連携およびfunction callingインターフェースを提供するクラス。
    """

    def __init__(self, api_key: str = GEMINI_API_KEY, model: str = GEMINI_MODEL) -> None:
        """
        Geminiクライアントを初期化する。

        Args:
            api_key (str): Gemini APIキー。
            model (str): 利用するGeminiモデル名。
        """

        # ここはGeminiがMCPサーバのどのtoolを呼び出すのか決めるための定義とtoolを呼び出すときの定義
        # LLMによって定義方法が異なるので利用するLLMに合わせて修正が必要
        function_declarations = [
            {
                "name": "greet",
                "description": "午前中(0時から11時59分まで)に使う、相手への挨拶を返すツールです。",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "name": {
                            "type": "string",
                            "description": "相手の名前",
                        },
                    },
                    "required": ["name"],
                },
            },
            {
                "name": "farewell",
                "description": "午後(12時から23時59分まで)に使う、相手への別れの挨拶を返すツールです。",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "name": {
                            "type": "string",
                            "description": "相手の名前",
                        },
                    },
                    "required": ["name"],
                },
            },
        ]

        self.tool_defs = types.Tool(function_declarations=function_declarations)  # type: ignore
        self.gemini = genai.Client(api_key=api_key)
        self.model = model

    def choose_function(self, prompt: str) -> tuple:
        """
        Geminiモデルにプロンプトを渡し、function callingで呼ぶべきツール名と引数を抽出する。

        Args:
            prompt (str): LLMへの入力プロンプト。

        Returns:
            tuple: (tool_name, tool_args) のタプル。

        Raises:
            ValueError: function_callが見つからない場合。
        """
        response = self.gemini.models.generate_content(
            model=self.model,
            contents=[prompt],
            config=types.GenerateContentConfig(temperature=0, tools=[self.tool_defs]),
        )

        tool_name, tool_args = self.parse_function_call(response)

        return tool_name, tool_args

    def parse_function_call(self, response: types.GenerateContentResponse):
        """
        Geminiから返されたレスポンスからfunction_callを抽出する。

        Args:
            response (types.GenerateContentResponse): Geminiのレスポンス。

        Returns:
            tuple: (tool_name, tool_args)

        Raises:
            ValueError: function_callが見つからない場合。
        """

        # Geminiが決めたfunction callingで呼び出すべきtool_nameとパラメータを返す
        for candidate in response.candidates:  # type: ignore
            for part in candidate.content.parts:  # type: ignore
                if hasattr(part, "function_call") and part.function_call:
                    func_call = part.function_call
                    tool_name = func_call.name  # type: ignore
                    tool_args = func_call.args  # type: ignore
                    return tool_name, tool_args
        raise ValueError("No function_call found in Gemini response")


class MCP:
    """
    MCPサーバへのツール呼び出しをラップするクラス。
    """

    def __init__(self, client: Client) -> None:
        """
        MCPクライアントの初期化。

        Args:
            client (Client): fastmcpのClientインスタンス。
        """
        self.client = client

    async def call_tool(self, tool_name: str, tool_args: dict) -> str | None:
        """
        MCPサーバにツール呼び出しを行い、テキストレスポンスを返す。

        Args:
            tool_name (str): 呼び出すツール名。
            tool_args (dict): ツールへ渡す引数。

        Returns:
            str | None: ツールのテキストレスポンス(TextContentのみ対応)。
        """
        async with self.client:
            # MCPサーバのツールを呼び出す
            response = await self.client.call_tool(tool_name, tool_args)
            res = self.parse_response(response)
            return res

    def parse_response(self, response: list) -> str | None:
        """
        MCPサーバから返されたレスポンスリストからテキストのみ抽出。

        Args:
            response (list): MCPサーバのレスポンス。

        Returns:
            str | None: 最初のTextContent.text、なければNone。
        """
        for res in response:
            if type(res) is mcp.types.TextContent:
                return res.text

        return None


class Coordinator:
    """
    GeminiおよびMCPクラスを連携させ、指定した名前に応じた挨拶文などを生成するコーディネータ。
    """

    def __init__(self, gemini: Gemini, mcp: MCP) -> None:
        """
        Coordinatorの初期化。

        Args:
            gemini (Gemini): Geminiクライアント。
            mcp (MCP): MCPクライアント。
        """
        self.gemini = gemini
        self.mcp = mcp

    async def get_message(self, name: str, now: str | None = None) -> str | None:
        """
        指定した名前・時刻に基づき、Gemini→MCPサーバを連携して挨拶文などを取得する。

        Args:
            name (str): 相手の名前。
            now (str | None): 現在時刻(HH:MM形式、省略時は現在時刻を自動設定)。

        Returns:
            str | None: MCPツールによる挨拶などのメッセージ。
        """
        if now is None:
            now = datetime.now().strftime("%H:%M")

        prompt = f"現在時刻は{now}です。{name}さんに挨拶をしてください。"
        tool_name, tool_args = self.gemini.choose_function(prompt)
        mcp_res = await self.mcp.call_tool(tool_name, tool_args)
        return mcp_res


async def main():
    """
    サンプル全体の実行エントリポイント。
    """
    gemini = Gemini()
    mcp = MCP(client)
    coordinator = Coordinator(gemini, mcp)

    res = await coordinator.get_message("Alice", "15:00")
    print(res)


if __name__ == "__main__":
    asyncio.run(main())

GeminiのAPIキー

Geminiは少しぐらいなら無料でAPIが使えるのでちょっとしたテストに便利だと思う。

Google AI Studio に行くと上に方にGet API KEYというのがあるので、

APIキーを作成を押すとAPIキーが発行できる。

FastAPIのmiddlewareでproxyを作った

FastAPIでPROXYのようなものが必要になったので作ってみたが、middlewareを使って簡単に作ることができた。
最初httpxの代わりにrequestsで作ったらレスポンスが返ってこないか、返ってきてもものすごく時間がかかってなんでかと考えたら非同期じゃなかったのでhttpxにしたら納得できるレスポンスで応答が返ってくるようになった。

簡単にかけてよかった。 似たようなものを作るのは何度目だろうか…。

import time

import httpx
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

app = FastAPI()

JSON_PLACEHOLDER_API_URL = "http://localhost:5000/tomorrow"

# ミドルウェアの定義
@app.middleware("http")
async def path_based_response_middleware(request: Request, call_next):
    if request.url.path == "/todo":
        async with httpx.AsyncClient() as client:
            try:
                response = await client.get(JSON_PLACEHOLDER_API_URL)
                response.raise_for_status()

                res = response.json()
                res["time"] = time.time()

                return JSONResponse(content=res)
            except httpx.RequestError as exc:
                raise HTTPException(
                    status_code=503, detail=f"API request error: {exc}"
                )
            except httpx.HTTPStatusError as exc:
                raise HTTPException(
                    status_code=503, detail=f"API request error: {exc}"
                )
    else:
        response = await call_next(request)
        return response


@app.get("/tomorrow")
async def get_todo_data():
    data = {
        "userId": "xxx",
        "id": "xxx",
        "title": "未来のTODOタスク",
        "completed": False
    }

    return JSONResponse(content=data)


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=5000)

JavaScriptのfetchのタイムアウトでAbortControlleやAbortSignalを使わない

最近はすごく古いNode.jsのコードのメンテナンスが多くてすごく面倒…。
fetchにTimeoutを入れようと思ってAbortSignal.timeoutを使ったらnot definedになって使えず、
AbortController.signalを使ってもnot definedになって使えず、
npm installも禁止なのでnpm install abort-controllerも使えず。

時間がなかったので以前のコードから引っ張ってきて対応したけど、ちゃんとしべ直したらもっと良い方法があったので忘れないように記載しておく。
Node.jsのバージョンを上げるのが一番なのだけど…。

import fetch from 'node-fetch';


// Node.js v14.17.0以降ならAbortControllerが利用可能
async function fetchDataWithAbortSignal(url, timeout = 1000) {
    let timeoutId = null;
    try {
        const controller = new AbortController();
        const signal = controller.signal;

        timeoutId = setTimeout(() => {
            controller.abort();
        }, timeout);

        console.log("fetch request with abort signal");

        const response = await fetch(url, {
            signal: signal,
        });

        clearTimeout(timeoutId);

        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
        }

        const data = await response.json();
        return JSON.stringify(data, null, 2);

    } catch (error) {
        clearTimeout(timeoutId);

        if (error.name === 'AbortError') {
            console.error("HTTP timeout error:", error);
            return "Error: Request timed out";
        }

        console.error("Fetching data failed:", error);
        return `Error: ${error.message}`;
    }
}

// Node.js v17.3.0以降ならAbortSignal.Timeoutが利用可能
async function fetchDataWithAbortTimeout(url, timeout = 1000) {
    try {
        const signal = AbortSignal.timeout(timeout);

        console.log("fetch request with abort timeout");

        const response = await fetch(url, {
            signal: signal,
        });

        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
        }

        const data = await response.json();
        return JSON.stringify(data, null, 2);

    } catch (error) {
        if (error.name === 'AbortError') {
            console.error("HTTP timeout error:", error);
            return "Error: Request timed out";
        }

        console.error("Fetching data failed:", error);
        return `Error: ${error.message}`;
    }
}


// AbortSignalやAbortControllerを使わないパターン
function fetchWithTimeout(url, timeout) {
    return Promise.race([
        fetch(url),
        new Promise((_, reject) =>
            setTimeout(() => reject(new Error('Request timed out')), timeout)
        )
    ]);
}

async function fetchDataWithPromiseRace(url, timeout = 1000) {
    try {
        console.log("fetch request with Promise.race timeout");
        const response = await fetchWithTimeout(url, timeout);
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        const data = await response.json();
        return JSON.stringify(data, null, 2);
    } catch (error) {
        console.error("Fetching data failed:", error);
        return `Error: ${error.message}`;
    }
}

export {
    fetchDataWithAbortSignal, fetchDataWithAbortTimeout, fetchDataWithPromiseRace,
};

Roo Codeを触ってみた

少し前からCLINEが気になっていたけど、触るのはもう少し先でも良いかなと考えいたが、最近CLINEの記事が多いので気になって触ってみた。

 

触ってみたのはRoo Code(CLINEの派生)の方。

利用するための準備はVS Codeを開いて以下の拡張機能をいれる。

その後使いたいLLMのAPIキーを入れたら準備は終わり。

marketplace.visualstudio.com

 

LLMのAPIキーには Claude 3.7 Sonnetがお薦めらしいが、Sonnet発行高いので少し試すのは躊躇する。

www.anthropic.com

 

 

少し試すだけならGoogle AI StudioでGemini 2.0 Flashでも良いかなと思いこっちを使ってみた。

aistudio.google.com

 

Root Codeの対応API Providerの一覧(2025/02/28現在)

 

Google AI Studioに行ってAPIキーを発行してRoo Codeの拡張機能に入れたらすぐに使える。

 

簡単な依頼をして動かしてみたらあっという間に開発環境を作ってくれたりしてとても便利だけど、結構APIにアクセスしているのかトークン数が多いのかAPIのレートリミットがかかってしまった。

 

現状だもとても便利な気がするけど料金が高すぎるって感じになりそうな気がする。

ローカルで動くまたはどこかにデプロイして定額で使えるLLMで試してみたらまた変わるのかもしれないが…。

 

GitHub Copilotみたいなサポート系のほうが普段のコードを書くのには便利そうな気がするが、Geminiで少し使う分には無料で試せるのでしばらく遊んでみようかと思う。

 

将来性はありそうな技術だと思った。

最近AI関連の技術の変化が早すぎてついて行けない。

TinyDBというドキュメント指向のデータベースがとても便利だった

最近知ったのだけどpythonで使えるドキュメント指向が便利だった。

 

仕事で色々なAPIを叩いてデータのバックアップを取ったりログを取ったりするのだけど、データの量が多くないがAPIからのレスポンスがJSONでデータがネストされていたりするとcsvなどに変換するのは少し面倒だったりする。  

バックアップを取ったデータを書き戻す際には元の形式に戻さないといけなかったりもするし、検索をしたいなとか思ったときにも不便。  

何度も利用するならsqliteとかでデータを入れておけば良いのかもしれないが、データの数は少ないのに種類が多いしそんなに何度も使わないみたいな感じでとても面倒…。

かといってMogoDBみたいなものをわざわざ用意するとか、そのままJSONで置いとけばいいかというと、準備が面倒だったり検索ができないのが辛いのもあって、あんまり良くないなと。

 

検索していたらsqliteみたいに簡単に使えるNOSQLがあってTinyDBというのを見つけた。  
結構古くからあるプロダクトのようだけど今まで知らなかった。

tinydb.readthedocs.io

 

使い方は上の説明の通りで

$ pip install tinydb

してかr

>>> from tinydb import TinyDB, Query
>>> db = TinyDB('db.json')

ってすると使える。

TinyDBの良いところはsqliteと同じくデータがファイルになって保存されるのでバックアップが簡単なのと、出来上がったDBのファイル自体の中身はJSONで保存されているところ。

サーバの準備のいらないし人にデータを渡すのも楽でいいし、gitでバックアップの管理もできる。

速度とか気にするような用途には向かないが、今使いたい要件にぴったりで、とても便利だった。