気軽に試せるラップトップ環境で、チャットbotを提供するオールインワンの生成AI環境構築から始め、Kubernetesを活用した本格的なGPUクラスタの構築やモデルのファインチューニングまで解説する本連載。今回は、MCPサーバを自作し、日々の作業を効率化、自動化するアプローチを解説します。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
これまで本連載ではローカルLLM(大規模言語モデル)、「Open WebUI」「MCP(Model Context Protocol)」や、Kubernetesと組み合わせてMCPを利用し、AIエージェントを構築する方法を解説してきました。
今回は、再びローカル環境に戻って、MCPサーバを自作し、日々の作業を効率化、自動化する方法について、Pythonコードを交えながら解説します。
MCPはAIエージェントが外部のツールと安全に連携するために、2024年にAnthropicが提唱したプロトコルです。現在はOpenAIなど、生成AIに関する多くの企業がサポートしています。公式MCPサーバリポジトリでは、100件以上のMCPサーバの実装が紹介されています。
MCPサーバはAIエージェントが実行可能な処理を拡張できるため、用途に合わせて選ぶことができます。Amazon Web Services(AWS)の操作をユーザーに代わって実行するAIエージェントが必要であればAWSが公式に提供するMCPサーバを使うとよいでしょう。
しかし、公式がMCPサーバを提供していない場合や自分のローカルPCの操作を実行させたい場合、サードパーティーの実装を探す必要があります。既存のツールが自分のニーズにフィットしなかったり、中身を理解せずに外部のMCPサーバを実行することによるセキュリティリスクも考慮しなければならなかったりします。
MCPサーバを自作できれば、業務や環境に合わせて必要な処理を実装できます。以下に、MCPサーバを自作する主なシチュエーションを紹介します。
社内の既存システムとAIエージェントを連携させたい場合、独自のMCPサーバを利用すると便利です。ただし、OpenAPIを利用している場合は、「AI Gateway」などの仕組みを用いることでOpenAPIとMCP間のプロトコル変換もできます。AI Gatewayの利用も併せて検討するとよいでしょう。
AI Gatewayについては、本連載の過去記事もご覧ください。
データベースと連携するMCPサーバが幾つか用意されているものの、汎用(はんよう)のSQLサーバでは、アクセス制御が弱いので、ユーザーごとの厳格なアクセス制御をするような場合、独自MCPサーバを利用した方がよい場合があります。また、個人情報のマスキング機能なども持たせることができます。
データベース連携用のMCPサーバには既存の汎用的なMCPサーバもありますが、アクセス制御機能が限定的です。ユーザーごとの厳密なアクセス権限が求められるケースでは、MCPサーバの自作を検討した方がよいでしょう。個人情報のマスキングなど、必要に応じてセキュリティ機能も持たせることができます。
社内のファイルサーバや独自のナレッジベース環境において、既存のMCPサーバが利用できない場合は、MCPサーバを自作する必要があります。
IoT(Internet of Things)やハードウェア制御においては特殊なプロトコルを利用していることがあります。そのような場合もMCPサーバを自作する必要があります。
複数のAPIを組み合わせて一連の処理を実行させたい場合も、MCPサーバが役立ちます。MCPで受け取ったリクエストを基に、内部で複数のAPIを順次呼び出し、処理結果を集約してLLMへ返すといった実装が可能です。
これらはほんの一例ですが、普段からスクリプトなどによって業務や自宅での作業を効率化している人は、MCPサーバ化を検討することで、スクリプトの実行をAIエージェント経由でできるといった、さらなる効率化を進めることができます。
今回は自宅のローカル環境での効率化を考えて、MCPサーバとそのMCPサーバを使ったOpen WebUIでの操作環境を全てローカルで実装する方法を紹介します。
なお本稿で利用したサンプルコードは、GitHubで公開していますのでぜひ利用してみてください。
筆者の構築環境は以下の通りです。
LinuxやWindowsでも同様の機能を実現できますが、一部macOS固有の機能を使っていますので、適宜読み替えてください。
今回はローカル操作をするMCPサーバのため、以前の記事で紹介したNVIDIAのグラフィックスカード「GeForce RTX4090」は利用していません。このため、結果の出力にはやや時間がかかります。MCPサーバがローカルで動いていれば、LLMをリモート環境のものに変更することもできます。用途に応じて、高性能なGPUサーバでの実行や、外部の商用LLMサービスの利用もぜひ検討してみてください。
今回は本連載でも解説してきたOllama、Open WebUIなどのソフトウェアを利用します。それぞれのインストール方法については連載の過去回を参照してください。
今回の想定ではOllama、Open WebUI、MCPサーバの全てをローカル環境で設定します。構成イメージは以下の通りです。
MCPサーバをローカル環境で実行することで、ローカル特有の操作をMCP経由でAIエージェントに実行させることができるようになります。
Visual Studio Code(VS Code)などの任意のエディタで、以下のコードを作成してください。これは、toolが実装されていないStreamable HTTP方式のシンプルなMCPサーバを実装するPythonコードです。
from starlette.applications import Starlette
from starlette.routing import Mount
from collections.abc import AsyncIterator
import contextlib
import uvicorn
from mcp.server.fastmcp import FastMCP
from langchain_openai import ChatOpenAI
# Create MCP server
mcp = FastMCP("My App")
@contextlib.asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[None]:
"""Context manager for managing session manager lifecycle."""
async with mcp.session_manager.run():
try:
yield
finally:
pass
# Mount the StreamableHTTP server to the existing ASGI server
app = Starlette(
routes=[
Mount("/", app=mcp.streamable_http_app())
],
lifespan=lifespan,
)
if __name__ == "__main__":
uvicorn.run(app, host="localhost", port=8000)
このコードではFastMCPのMy Appという名前のアプリケーションを作成し、localhostの8000番ポートでListenするStreamable HTTPのMCPサーバを起動できます。アプリケーションに利用しているStarlette、uvicornはHTTPサーバを作成するためのフレームワークであり、MCPサーバをはじめとしたさまざまなWebアプリケーションの開発に利用されています。ここではMCP公式のチュートリアルをほぼそのまま利用しています。
このコードを任意の名前を付けて保存します。ここでは、main.pyで保存します。
MCPサーバをPythonで実装するサンプルとして、FastMCPのみ利用するサンプルがよく見受けられますが、「スケーラビリティ」「認証・認可機能」「将来の機能拡張への耐性」という観点から、本番環境においては、より汎用的なAPIを提供するStarletteを組み合わせることを強く推奨します。
uvコマンドで必要なライブラリをインストールします。
uv init uv add "langchain" "langchain-openai" "mcp[cli]" "starlette" "uvicorn"
uvコマンドでMCPサーバを実行します。
uv run main.py
以下のようなログが出力されれば正常に起動しています。
INFO: Started server process [19900] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://localhost:8000 (Press CTRL+C to quit)
次に簡単なMCPツールを作成してみましょう。次のコードは軽量なことで有名な俳優の阿部寛さんのWebサイトをGoogle Chromeで開くMCPツールの実装例です。
先ほどのPythonコードに、以下のコードを追加します。
import subprocess
from typing import Dict
@mcp.tool()
async def open_abe_hiroshi_website() -> Dict:
"""
Open the Abe Hiroshi website in Google Chrome.
"""
try:
command = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"https://abehiroshi.la.coocan.jp/"
]
subprocess.run(command, check=True)
except Exception as e:
return {"status": "FAILED", "message": str(e)}
return {"status": "DONE"}
MCPサーバを再起動します。
uv run main.py
作成したMCPサーバはOpen WebUIから利用可能です。Open WebUIでは以下の手順で作成したMCPサーバを設定してください。モデルはリーズニング対応モデルであれば任意のもので構いませんが、比較的軽量で精度が高いモデルとしてgpt-oss(20B)が利用できますので、Ollama側で設定しておくとよいでしょう。
これでMCPサーバの設定は完了です。続いてチャットからMCPサーバを実行します。
左のペインから新しいチャットを開き、チャット入力の下部にある4つの四角のシンボル(Integration)からツール経由で先ほど管理者画面で設定したMCPサーバを有効化します。
有効化したら以下のチャットを実行してみてください。
阿部寛さんのWebサイトを開いてください
これで阿部寛さんのWebサイトがGoogle Chromeで開いたら成功です。
このようにMCPサーバをローカルに実装することでローカルのOS上で実行するさまざまな機能をチャット経由で実行できるようになります。
本稿では阿部寛さんのWebサイトを開くツールを実装しましたが、URLを工夫することで任意のページを任意のウィンドウで開くことができるため、Webページを読み込んでチャットに出力するよりも柔軟な機能を実現できます。
なお、MCPの実装においてはToolに適切な名前を付ける以外に、Toolに設定する説明が重要です。今回設定した「Open the Abe Hiroshi website in Google Chrome.」というコメントを「DO NOT USE this function to open abehiroshi.la.coocan.jp website. Because the function is not intended for that purpose.」に変更して再度試してみてください。この変更をすることでOpen WebUIからいくら指示をしてもWebサイトの画面を開くことができなくなっているはずです。
このように、LLMはMCPのToolの説明を参照し、ユーザーの要求に対して適切なToolを選択します。このためToolの説明は重要です。LLMが適切なToolを選択してくれない場合は、精度の高いLLMを使う以外にもこのToolの説明が適切な粒度であり、LLMにとって分かりやすいものになっているのかどうかを確認するとよいでしょう。
前節で紹介したWebページの表示は実際にはテキストベースの情報をチャットに出力することで互換可能な機能のため、もう少し複雑でローカルな機能としてメールの作成と「Microsoft Outlook」での編集画面の起動を作ってみます。
このMCPの機能を実装することでコーディングやブラウジング中にわざわざOutlookを起動し、メールの内容を考える手間を省くことができます。
import os
import json
import shlex
import html
from langchain_openai import ChatOpenAI
@mcp.tool()
async def open_new_email(
title: str = "My Test Subject",
content: str = "Test Body of My Email") -> Dict:
"""
Create a new email window via Microsoft Outlook.
:param title: The title of the email.
:param content: The content of the email.
"""
title = html.escape(title)
content = html.escape(content)
if "\n" in content:
body = "<br>".join([line for line in content.split("\n")])
content = f"<html><body>{body}</body></html>"
SCRIPT = """
on run argv
tell application "Microsoft Outlook"
set rawSubject to item 1 of argv
set rawContent to item 2 of argv
set theSubject to text 2 thru -2 of rawSubject
set theContent to text 2 thru -2 of rawContent
set newMessage to make new outgoing message with properties {subject:theSubject, content:theContent}
open newMessage
activate
end tell
end run
"""
try:
command = [
"osascript",
"-e",
SCRIPT,
shlex.quote(title),
shlex.quote(content)
]
subprocess.run(command, check=True)
except Exception as e:
return {"status": "FAILED", "message": str(e)}
return {"status": "DONE"}
@mcp.tool()
async def generate_email_content(context: str) -> Dict:
"""
Generate an email content based on the provided context.
The response is expected to be in JSON format with "title" and
"content" keys.
:param context: The context for generating the email draft.
:return: A dictionary with the status and generated content.
"""
base_url = "http://localhost:11434/v1"
os.environ["OPENAI_API_KEY"] = "dummy"
chat_model = ChatOpenAI(base_url=base_url, model="gpt-oss:20b")
messages = [
("system", 'You are an AI assistant specialized in writing '
'professional and clear emails. '
'The response should be in JSON format as follows: '
'{"title": "<email title>", "content": "<email content>"}'),
("human", "Please draft an email based on the following "
f"context: {context}")
]
chat_response = chat_model.invoke(messages)
try:
response_dict = json.loads(chat_response.content)
if not {"title", "content"} <= set(response_dict):
return {"status": "FAILED",
"message": "Response JSON does not contain required keys."}
except json.JSONDecodeError:
return {"status": "FAILED", "message": "Failed to parse response as JSON."}
return {"status": "DONE", "generated_content": response_dict}
この実装例ではopen_new_emailとgenerate_email_contentの2つのToolを実装しています。
精度の高いLLMの場合、generate_email_contentのようなToolを定義しなくてもLLM自身にメールを書かせることは可能です。ただし、この例のように、どのような文章を作成させたいかをある程度チューニングしたい場合には、個別に生成用のToolを定義することで生成する文章の内容をコントロールできます。
今回の例ではToolの中でLLMのAPIを呼び出して個別に文章を生成させていますが、MCPの仕様として呼び出し元のLLMを用いて文章を生成させるSamplingという仕組みも登場しています。連携させるアプリケーションの対応状況次第ではこちらを用いて実装をしてみるのもよいでしょう。
では、試しにメールを書かせてみましょう。Open WebUIで以下を入力します。
〇〇という商品の在庫を問い合わせたいのでメールを書いてください
generate_email_contentで生成されたメールの文面が出力されます。
続いて、以下を入力します。
その内容をメールで開いてください
生成された内容がOutlookの新規メールとして開いたでしょうか? 開くことができれば成功です。このように、チャット経由で任意のローカル操作を実現できます。
今回実装したopen_new_emailのように、任意の入力を用いたシステム操作にはコマンドインジェクションのようなセキュリティリスクが伴うことに注意が必要です。
今回紹介した実装例でもApple Scriptのプロンプト固定、Shell引数へのQuote、html構文のエスケープで危険な入力を避ける工夫をしています。
LLMが作成したコードを実行させる場合は、MCPの実行箇所がローカル上である場合、システムを破壊するような危険コマンドや個人情報を外部に送信するようなコードを許可なしに実行できないように工夫する必要があります。
MCPサーバの設定でListenの設定を `0.0.0.0` とした場合、OSのファイアウォールの設定によってはローカルMCPの実行を外部から呼び出し可能になるリスクを伴う場合もあります。
MCPのToolでローカルの環境を操作することは便利なものの、このようなセキュリティリスクにも対応しなければならないということを意識してください。
外部のREST API実行などローカルの操作が必要のないMCP機能の場合は、仮想マシンやコンテナ環境上でMCPサーバを構築するなど、実行環境を隔離することでセキュリティリスクを低減できますので、必要に応じて検討するとよいでしょう。
今回は、ローカルLLMの活用方法としてMCPサーバを構築し、自分でToolを設定するポイントや効果、注意点を紹介しました。
MCPを使いこなすことで、LLMをより便利に使いこなすこともできると思いますので、興味がある人は本記事に書かれている内容を参考に、MCP機能の実装を試してみてください。
VS Codeで注目のMCPを実践 「SQLite MCP」「Playwright MCP」でデータとテストコードを生成する方法
Agentic AI Foundation(AAIF)発足 寄贈されたMCP、AGENTS.md、gooseはどうなるのか
Claudeの「Agent Skills」がMCPと同様、オープン標準に どんな便利スキルがある?
MCPサーバの機能と実装の肝――仕組みと構築の流れをSpring AIで理解するCopyright © ITmedia, Inc. All Rights Reserved.
編集部からのお知らせ