
最近、生成AIの進化が加速してて付いて行くのが大変です。
今回は、勉強の一環としてMCPのサーバーとクライアントを作成して動かしてみました。
といっても、クイックスタートをやってみただけですが。。。
今回の内容
このところ、あまり新しいことにチャレンジしてなかったので以下のことにも手を出してみました。
- VSCodeで開発
今更?って言われるかもしれませんが、実はあまりVSCode使ってないんですよね。
ずっとPyCharm使ってたので。
昔VSCode使った時使い物にならなかったので放置してました。
最近はかなりいい感じになってるので、今後はVSCodeを使おうと思います。
プロファイルで言語やExtensions(プラグイン)の切り替えもできるようになってるんですね。
Windows上からWSL2上の実行とデバッグができるのが大変ありがたいです。
(かなり前に実装されているみたいですね。。。食わず嫌いはよくないです。)
- uvで環境作成
2024年2月にはすでに出ていたそうです。知りませんでした。どれぐらい浸透しているんでしょうか?
python環境の切り替えにかなり便利です。
今まではPyVenvとかで頑張ってたので助かります。
みんなが欲しがってたあるべき機能が実現した感じです。
今後は標準になる気がします。
- Cursor
2022年からあるみたいですが、2023年からAIと連携するようになって最近よく聞くようになりました。
VSCodeベースなようなので、VSCode勉強と一緒にCursorも使ってみました。
私の周りの人は絶賛しているようですが、私はまだ良さがよくわかってないです。
VSCodeと違い、無償で使用できる機能に制限があります。
AIの機能をしっかり使いたい人は3000円($20)/月ぐらいのサブスク登録が必要です。
詳細は以下を参照してください。
https://www.cursor.com/ja/pricing
MCPって何?
まあ、何はともあれ、今回のメインはMCPなので、そこから行きましょう。
細かい説明はググればいっぱい情報が出てきます。ここでごちゃごちゃ書いても無駄なので、Anthropicのサイトから引用します。(Google翻訳)
MCPは、アプリケーションがLLMにコンテキストを提供する方法を標準化するオープンプロトコルです。MCPはAIアプリケーション用のUSB-Cポートのようなものだと考えてください。USB-Cがデバイスを様々な周辺機器やアクセサリに接続するための標準化された方法を提供するのと同様に、MCPはAIモデルを様々なデータソースやツールに接続するための標準化された方法を提供します。
https://modelcontextprotocol.io/introduction
ちょっと比喩表現すぎてよくわからんですね。
要するに、LLMとアプリやデバイスが連携できるようになるための仕組みですが、構造的には以下のようにサーバーがアプリやデバイスとやり取りして、LLMがサーバーをツールとして使えるようになり、あれこれ指示できるという感じです。

MCPとは”Model Context Protocol”なので、プロトコル、つまり「LLM(生成AI)とアプリ(とかデバイスとか)との会話する言葉を作った。」ってことです。
今まで生成AIは人間としかお話しできませんでした(本当はそんなことないけど)が、アプリとかデバイスとかともお話しできるようになったということです。
つまり、生成AIが考えてアプリとかから情報を収集して人間にいろいろ教えたり、人間の指示でアプリを動かせたりできるようになったってことです。
(AIエージェントって言われるけど、AIエージェントの定義はもうちょっと範囲が広い気がする)
※いろいろ危険な香りがしますね。もうすでにいろんな視点で脆弱性が指摘されています。(ステートフルってダメじゃね?とか、人が見えないプロンプトに変なもの入れられたら情報抜き取れたりするんじゃね?とか)

AnthropicアカウントとAPIキー作成
今回のクイックスタートではAnthropicのAPIキーが必要となります。
無料でアカウントとAPIキーの作成ができるので先に作っておきましょう。
まず、アカウント作成しましょう。以下のURLからアカウントを作成してください。
https://console.anthropic.com/login
設定したメールにパスコードが飛んでくるので設定するとアカウントを作成できます。
ログインしたら”Get API keys”からAPIキーの作成を行います。

環境構築
クイックスタートはMacかWindowsで実施することを想定しているようです。
私の場合Windowsで実施するほうがいいかもしれませんが、AI開発はLinux前提のことが多いので、今回はWSL2を使用します。
Claud for DesctipがLinuxに対応してないので、一部出来ない手順がありますが、MCPを体験する上では特に問題ありませんでした。
WSL2インストール
まずは、WSL2をインストールします。
管理者権限でコマンドプロンプトを起動し、以下のコマンドでインストールできます。
wsl --install
再起動が必要なので、一度再起動します。
起動したら、Microsoft StoreアプリからUbuntuをインストールします。

スタートメニューに登録されるので、起動するとWSLコンソールで立ち上がってきます。
ユーザ登録などの初期設定を行いましょう。
私はUbuntuイメージをいろんな開発で使いたいので、インストールしたUbuntuイメージをコピーして使ってます。
以下のコマンドで今回の検証用のイメージ(Ubuntu-24.04-mcp)を作成して起動します。
いったんWSL2サービスを終了する必要があるので、WSLコンソールを終了させましょう。
sudo poweroff
以下のコマンドでサービスを停止します。
wsl --terminate Ubuntu-24.04
今回は”C:\wsl_images”というフォルダを作って、そこで作業します。
mkdir c:\wsl_images
cd c:\wsl_images
以下のコマンドでMicrosoft Storeでインストールしたイメージをコピーします。
wsl --import Ubuntu-24.04-mcp C:\wsl_images\Ubuntu-24.04-mcp C:\Users\[ユーザID]\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu24.04LTS_********\LocalState\ext4.vhdx --version 2 --vhd
コピーしたイメージを起動します。
wsl --distribution Ubuntu-24.04-mcp --user [ubuntuで設定したユーザ]
ubuntuが起動して、使っていたコマンドプロンプトがWSLコンソールになります。
uvインストール
wsl上で以下を実行してインストールします。
cd
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env
echo 'eval "$(uv generate-shell-completion bash)"' >> ~/.bashrc
uvをアップデート
uv self update
Python3.13をインストールしてデフォルトにします。
uv python install 3.13
uv python pin 3.13
古いVSCodeをきれいに消す
今回はCursorを使用するのでVSCodeは不要ですが、参考までにVSCodeのインストールについても記載しておこうと思います。
VSCodeのインストールに興味がない人は読み飛ばしてください。
「AI要らないから無料で使いたい」人はVSCodeを使ったほうがいいと思います。
昔インストールしたVSCodeはExtensionsとかがごちゃごちゃになっていたのでいったんすべて消しました。
きれいに消すにはユーザフォルダの中も消さないといけないみたいです。
まずは通常通り、”設定”⇒”アプリ”から”Visual Studio Code”を削除します。
その後、以下のフォルダを消します。
c:\User\[ユーザID]\.vscode
c:\User\[ユーザID]\AppData\Roaming\Code
VSCodeインストール
以下からダウンロードしてインストールして、起動します。
https://code.visualstudio.com/download
日本語化が必要なのでとりあえず、Extensionsをインストールして日本語に設定します。
Extensionsアイコン(左端のツールバーの一番下)をクリックし、”japanese”で検索すると”Japanese Language Pack for Visual Studio Code”が出てくるのでインストールします。

インストール直後に右下に表示される”Change Language and Restert”をクリックするか、

“Ctrl+Shift+p”で”Configure Display Language”と入力して、”日本語”を選択します。

PythonのExtensionsをインストールしたいんですが、その前にPython用のプロファイルを作っておきたいと思います。
左下のギヤアイコンをクリックして”プロファイル”をクリックします。

“新しいプロファイル”をクリックして名前に”python”と入力し、”作成”ボタンをクリックします。

もう一度、左下のギヤからプロファイルをクリックし、”python”を選択します。

最後にExtensionsの”WSL”と”python”をインストールしておきます。
※”Pylance”と”Python Debugger”は”Python”をインストールすると自動でインストールされます。

Cursorインストール
とりあえず、ふつうにダウンロードしてインストールします。
初期セットアップで「データを共有するか?」って言われるので「Private」にしておきましょう。

まずは日本語化したいですね。
画面上部の右のほうに以下のようなアイコンがあるので左端のアイコンをクリックして左ペインを表示します。

VSCodeと同じようにExtensionsアイコンをクリックして”Japanese Language Pack for Visual Studio Code”をインストールします。
その後、”Ctrl+Shift+p”で”Configure Display Language”と入力して、”日本語”を選択します。
プロファイルも追加しておきましょう。
“ファイル” ⇒ “ユーザ設定” ⇒ “プロファイル” プロファイル画面を表示し、

“新しいプロファイル”をクリック、名前に”python”と入力してプロファイルを作成します。

その後、”ファイル” ⇒ “ユーザ設定” ⇒ “プロファイル” ⇒ “python” で選択します。
あとはExtensionsで”python”と”wsl”をインストールしましょう。
WSL側のリモート開発設定
“WSL”Excensionをインストールするとwslコンソールで”code”コマンドでVSCodeが起動しますが、Cursorの”WSL”Excensionをインストールすると”code”コマンドでCursorが起動するようになります。
私は両方使いたかったのでWSL側のシンボリックリンクを修正して”cursor”コマンドで起動するようにしました。
# シンボリックリンク削除
sudo rm /usr/local/bin/code
# codeでVSCode起動
sudo ln -s "/mnt/c/Users/<ユーザID>/AppData/Local/Programs/Microsoft VS Code/bin/code" /usr/local/bin/code
# "cursor"でCursor起動
sudo ln -s "/mnt/c/Users/<ユーザID>/AppData/Local/Programs/cursor/resources/app/bin/code" /usr/local/bin/cursor
クイックスタートやってみる
公式サイトにクイックスタートがあるので、そのままやってみます。
https://modelcontextprotocol.io/quickstart/server
https://modelcontextprotocol.io/quickstart/client
英語を読むのは面倒なので、とりあえず、Google先生に翻訳してもらいました。
コマンドの説明はMacとWindowsしかないんですが、MacのコマンドでLinuxでも問題なく実行できました。
クイックスタートのサイトにも記載がありますが、プログラムの全文は以下のGitHubで公開されています。
https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python
https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-python
サーバー作成
まずは、プロジェクトを作ります。※ディレクトリ名は若干アレンジしています。
# Create a new directory for our project
uv init quickstart-weather
cd quickstart-weather
# Create virtual environment and activate it
uv venv
ここでCursorを立ち上げます。
cursor .
Cursorが起動してくるので、インタープリタでVENVを使うように設定します。
Ctrl+Shift+p ⇒ Select interpriter ⇒ venvで作成したインタプリタを選択するか、Ctrl+Shift+@で新しいターミナルを起動して以下を実行します。
source .venv/bin/activate
必要なライブラリを追加します。
# Install dependencies
uv add "mcp[cli]" httpx
サーバー本体のファイルを作ります。
# Create our server file
touch weather.py
あとは、サーバーソースをコピーすれば、できちゃいます。
GitHubの全文を載せておきます。
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP
# Initialize FastMCP server
mcp = FastMCP("weather")
# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
async def make_nws_request(url: str) -> dict[str, Any] | None:
"""Make a request to the NWS API with proper error handling."""
headers = {
"User-Agent": USER_AGENT,
"Accept": "application/geo+json"
}
async with httpx.AsyncClient() as client:
try:
response = await client.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
def format_alert(feature: dict) -> str:
"""Format an alert feature into a readable string."""
props = feature["properties"]
return f"""
Event: {props.get('event', 'Unknown')}
Area: {props.get('areaDesc', 'Unknown')}
Severity: {props.get('severity', 'Unknown')}
Description: {props.get('description', 'No description available')}
Instructions: {props.get('instruction', 'No specific instructions provided')}
"""
@mcp.tool()
async def get_alerts(state: str) -> str:
"""Get weather alerts for a US state.
Args:
state: Two-letter US state code (e.g. CA, NY)
"""
url = f"{NWS_API_BASE}/alerts/active/area/{state}"
data = await make_nws_request(url)
if not data or "features" not in data:
return "Unable to fetch alerts or no alerts found."
if not data["features"]:
return "No active alerts for this state."
alerts = [format_alert(feature) for feature in data["features"]]
return "\n---\n".join(alerts)
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
"""Get weather forecast for a location.
Args:
latitude: Latitude of the location
longitude: Longitude of the location
"""
# First get the forecast grid endpoint
points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
points_data = await make_nws_request(points_url)
if not points_data:
return "Unable to fetch forecast data for this location."
# Get the forecast URL from the points response
forecast_url = points_data["properties"]["forecast"]
forecast_data = await make_nws_request(forecast_url)
if not forecast_data:
return "Unable to fetch detailed forecast."
# Format the periods into a readable forecast
periods = forecast_data["properties"]["periods"]
forecasts = []
for period in periods[:5]: # Only show next 5 periods
forecast = f"""
{period['name']}:
Temperature: {period['temperature']}°{period['temperatureUnit']}
Wind: {period['windSpeed']} {period['windDirection']}
Forecast: {period['detailedForecast']}
"""
forecasts.append(forecast)
return "\n---\n".join(forecasts)
if __name__ == "__main__":
# Initialize and run the server
mcp.run(transport='stdio')
サーバー単体での実行するには”Claude for Desktop”をインストールする必要がありますが、Mac用とWindows用しかありませんので、今回はClientを作ってから実行します。
サーバーソースの内容
少し、内容を確認してみましょう。
各行で何をしているかという説明についてはクイックスタートサイトに無いようですが、詳しい説明をしているサイトがいくつかあるので、細かいところはそちらを参照してもらったほうが良いかと。
https://dev.classmethod.jp/articles/model-context-protocol-weather-server-tutorial
“@mcp.tool()”デコレーターがついているものがサーバー機能として公開されるようです。
DIコンテナとかでもやった感じですね。
基本的にすべて非同期処理”async”で実装されています。
サーバーは非同期に起動したスレッド(?)になっていると思われますので、そのために非同期である必要があるのではないかと思います。
こういった構造の場合、複数クライアントでのサーバーインスタンスの共有ということも考えられますが、そういった使い方ができるのか(想定されているか)どうかについては不明です。
たぶん、サーバーファイルは共有し、インスタンスはクライアントごとに生成する使いたと思われます。(リソースをたくさん使用するものでもないし、個別のほうが排他とかを気にしないでいいので)
私が疑問に思ったのは、どのサーバーにどういった機能があるということを、AIにどうやって教えているか?というところですが、明確な記載が見当たりませんでした。
クイックスタートサイトの一文に以下のような説明があるので、メソッド宣言直後のコメント(PyDoc?)で判断していると思われます。
FastMCP クラスは、Python の型ヒントとドキュメント文字列を使用してツール定義を自動的に生成し、MCP ツールの作成と保守を容易にします。
クライアントの作成
サーバーと同じようにプロジェクトを作成します。
# Create project directory
uv init quickstart-mcp-client
cd quickstart-mcp-client
# Create virtual environment
uv venv
サーバーと同じ手順でCursorを起動してVENVを設定します。
新しいターミナルを表示して以下のコマンドでライブラリをインストールします。
# Install required packages
uv add mcp anthropic python-dotenv
不要なファイルを削除して、クライアントファイルを作成します。
# Remove boilerplate files
rm main.py
# Create our main file
touch client.py
サーバーと同様にソースの内容をコピーしましょう。
こちらも全文を載せておきます。
import asyncio
import sys
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv() # load environment variables from .env
class MCPClient:
def __init__(self):
# Initialize session and client objects
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.anthropic = Anthropic()
async def connect_to_server(self, server_script_path: str):
"""Connect to an MCP server
Args:
server_script_path: Path to the server script (.py or .js)
"""
is_python = server_script_path.endswith('.py')
is_js = server_script_path.endswith('.js')
if not (is_python or is_js):
raise ValueError("Server script must be a .py or .js file")
command = "python" if is_python else "node"
server_params = StdioServerParameters(
command=command,
args=[server_script_path],
env=None
)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
await self.session.initialize()
# List available tools
response = await self.session.list_tools()
tools = response.tools
print("\nConnected to server with tools:", [tool.name for tool in tools])
async def process_query(self, query: str) -> str:
"""Process a query using Claude and available tools"""
messages = [
{
"role": "user",
"content": query
}
]
response = await self.session.list_tools()
available_tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
} for tool in response.tools]
# Initial Claude API call
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
tools=available_tools
)
# Process response and handle tool calls
final_text = []
assistant_message_content = []
for content in response.content:
if content.type == 'text':
final_text.append(content.text)
assistant_message_content.append(content)
elif content.type == 'tool_use':
tool_name = content.name
tool_args = content.input
# Execute tool call
result = await self.session.call_tool(tool_name, tool_args)
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
assistant_message_content.append(content)
messages.append({
"role": "assistant",
"content": assistant_message_content
})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": content.id,
"content": result.content
}
]
})
# Get next response from Claude
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
tools=available_tools
)
final_text.append(response.content[0].text)
return "\n".join(final_text)
async def chat_loop(self):
"""Run an interactive chat loop"""
print("\nMCP Client Started!")
print("Type your queries or 'quit' to exit.")
while True:
try:
query = input("\nQuery: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query)
print("\n" + response)
except Exception as e:
print(f"\nError: {str(e)}")
async def cleanup(self):
"""Clean up resources"""
await self.exit_stack.aclose()
async def main():
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
sys.exit(1)
client = MCPClient()
try:
await client.connect_to_server(sys.argv[1])
await client.chat_loop()
finally:
await client.cleanup()
if __name__ == "__main__":
asyncio.run(main())
クライアントソースの内容
クライアントソースについても少し見てみましょう。
メイン処理で、”connect_to_server”、”chat_loop”を呼び出しています。
client = MCPClient()
try:
await client.connect_to_server(sys.argv[1])
await client.chat_loop()
finally:
await client.cleanup()
“chat_loop”の中でLLMに問い合わせするための”process_query”を呼び出す構造です。
通常、接続するサーバーファイルパスは固定と思いますが、引数でもらっているようです。
“connect_to_server”で以下のようにサーバーインスタンスを(おそらく)生成してコネクションを作成しています。
server_params = StdioServerParameters(
command=command,
args=[server_script_path],
env=None
)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
await self.session.initialize()
チャットで入力されたユーザからの問い合わせ(”message”)をLLM(Claude)に送るときにコネクションしたサーバーのリストをつけてます。
response = await self.session.list_tools()
available_tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
} for tool in response.tools]
# Initial Claude API call
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
tools=available_tools
)
これで、LLMが各サーバーのPythonコメントから利用できる機能を読み取って回答していると思われます。
環境設定ファイルを作る
クライアントプログラムにはAPIキーが必要になります。
APIキーは環境設定ファイル(.envファイル)に登録します。
touch .env
作成した.envファイルに以下のように追加します。
ANTHROPIC_API_KEY=[APIキー]
.envファイルが公開されないように.ignoreに登録しておきます。
echo ".env" >> .gitignore
動かしてみる
実行するとこんな感じでした。
(quickstart-mcp-client) root@*****:~/quickstart-mcp-client# uv run client.py ../quickstart-weather/weather.py
Processing request of type ListToolsRequest
Connected to server with tools: ['get_alerts', 'get_forecast']
MCP Client Started!
Type your queries or 'quit' to exit.
Query:
今日の天気を聞いてみました。
Query: 今日の天気を教えてください
Processing request of type ListToolsRequest
天気予報を調べるためには、特定の場所の緯度(latitude)と経度(longitude)が必要です。どの地域の天気を知りたいか教えていただけますでしょうか?
場所が分かりましたら、その地点の正確な天気予報をお伝えすることができます。
例えば:
- 都市名
- 地域名
などを教えていただければと思います。
Query: 東京です
Processing request of type ListToolsRequest
申し訳ありませんが、このツールは米国内の天気予報と警報のみに対応しています。東京の天気については提供できません。
米国内の特定の場所の天気予報を知りたい場合は、その場所の緯度と経度をご指定ください。また、米国の州ごとの気象警報を確認したい場合は、2文字の州コード(例:CA、NY など)をお知らせください。
ひどい。。。
まあ、でも、ちゃんとサーバーから情報を取得しようとしてるってことですよね。
CursorでAI機能を使ってみる
AI補完機能
普通にエンターキーを入力するだけでも候補を表示してくれます。
48行目でエンターするとこんな感じで表示されるので、TABキーでソースに反映してくれます。

試しに43行目のセッション初期化処理を消して、Ctrl+Kで「セッションを初期化」と入力してみたところ、こんな感じでソース生成してくれます。

AIチャット
Ctrl+Lでチャットウィンドウが開いてAIとチャットできます。
39行目の”server_params”の部分を消してエラーを起こして、AIチャットで「エラーを修正してください」とお願いしたら、めちゃめちゃいっぱいアドバイスしつつソースを直してくれました。
直した個所についてはこんな感じで表示されて、Acceptするとソースに反映されます。
ただ、ソース全体のAcceptを行わないと他の作業ができないようです。

AI機能についてあれこれ
AI機能はオフラインでも使える?
気になったのでAIチャット機能で聞いてみました。きっと自分のことだからハルシネーションしないと信じて。。。

VSCodeの機能はオフラインでも使えるってことでしょうか。
やはり、AI機能はクラウド上で動作しているようですね。
Cursorの説明では「AI内臓」となっているものがありますが、ローカルLLMというわけではないようです。
.envはAIに送信されない?
こちらもAIチャット機能で聞いてみました。

.ignoreで指定していれば、.envの内容は送信されないということらしいです。
ただ、ソースについては送信されているようです。この辺りは気を付けないといけないですね。
まとめ
今回は、Cursorを使ってMCPサーバーとクライアントを作ってみました。
MCPについてはさわり程度しかできませんでしたが、出たばかりなのに多くの人が深く使っているので今後もいろいろ試してみようかと思ってます。
おそらく、MCPが標準的な仕組みとなっていくと思うので。
Cursorについては「すごい便利」という人もいますが、今回触った感じではそれほどの感動はありませんでした。
今までもブラウザでChatGPTにソースを作ってもらってコピペって感じで作業をしているので、その感覚とあまり大差ない気がします。(コピペしなくていいぐらい)
下手なことをAIチャットで聞くとソースも送られちゃうのは心配ですね。
この辺りが解消されないと実業務で使うのは難しいかなぁ・・・って感じですね。