はじめに
前回まで、認証の処理や通信の暗号化などセキュリティ面の確認を行ってきました。
今回は、そういうのは一旦さておき、MCPサーバーとしての実際の機能を作りこんでみたいと思います。
構造化データ
前回までの実装では、チャンネル一覧取得の結果として、1つの文字列(チャンネル名)のみを通知していました。
この方法でも、賢いLLMはいろいろと考え対応してくれるのかもしれませんが、出力内容の構造を詳しく伝えることで、有効に活用してもらえるのではと思い、Toolの定義としてInput Schemaだけではなく、Output Schemaも通知するようにしたいと思います。
Output Schema
以下のようにチャンネル番号とサービス名(チャンネル名)を配列の形式を定義し、tools/listで通知します。
{
"type":"object"
"properties": {
"content": {
"type":"array"
"items": {
"type": "object"
"properties": {
"channel_no": {
"type": "string"
"description": "channel no"
}
"service_name": {
"type": "string"
"description": "service name"
}
}
}
}
"required": [ "content" ]
}
structuredContent
Output Schemaを定義した場合、tools/callでは、structuredContentで通知します。
しかし、structuredContentをcontent同様に配列とすることはできません。
その為、上記のように、contentという配列を定義して、その中にチャンネルの一覧を定義するようにしています。
結果、tools/callで通知する内容は以下のような形式になりました。
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"content": [
{
"type": "text",
"text": "{\"channel_no\": \"011\", \"service_name\": \"NHK G\"}"
},
{
"type": "text",
"text": "{\"channel_no\": \"021\", \"service_name\": \"ETV\"}"
}
],
"structuredContent": {
"content": {
"items": [
{
"channel_no": "011",
"service_name": "NHK G"
},
{
"channel_no": "021",
"service_name": "ETV"
}
]
}
}
}
}
後方互換性のため、従来のcontentでもtext形式でデータを通知するとのことです。
LLMは、structuredContentが存在しても、まだこちらのcontentの方を参照していることが多いようです。
この方法であってるのか?
正直、この方法が正しいのか、ハッキリしません。
MCPのSpecには、typeにarrayなるものは定義されておらず、 そもそもtypeに何が使えるかさえ定義されていません。
が、そもそもそこはLLMです。
このような曖昧な通知でも、いくらでも勝手に解釈してくれるのだろうと今の時点では判断しました。
(それなら、OutputShema自体要らないのではというのは、さておき...)
MCPサーバーへの実装の反映
OutputSchemaの通知やstructuredContentの形式でデータを通知する機能をMCPServerライブラリに追加しました。
以下のような実装で、その機能を利用します。
server.AddTool(
"get_channels",
"Returns a list of available TV channels.",
// InputSchemaの定義
std::vector<McpServer::McpProperty> {
{ "location", McpServer::PROPERTY_STRING, "location of TV", true }
},
// OutputSchemaの定義
std::vector<McpServer::McpProperty> {
{ "channel_no", McpServer::PROPERTY_STRING, "channel no", true },
{ "service_name", McpServer::PROPERTY_STRING, "service name", true }
},
[](const std::map<std::string, std::string>& args) -> std::vector<McpServer::McpContent> {
std::vector<McpServer::McpContent> contents;
McpServer::McpContent content{
.property_type = McpServer::PROPERTY_OBJECT,
.value = ""
};
// 1つめのデータを通知
content.properties.push_back({
.property_name = "channel_no",
.value = "011"
});
content.properties.push_back({
.property_name = "service_name",
.value = "NHK G"
});
contents.push_back(content);
// 2つめのデータを通知
content.properties.push_back({
.property_name = "channel_no",
.value = "021"
});
content.properties.push_back({
.property_name = "service_name",
.value = "ETV"
});
contents.push_back(content);
return contents;
}
);
複雑なOutputSchemaを定義したいと思うようなことがある場合、このような仕組みではなく、シンプルにJson文字列を受け取り、それを転送するだけの方がよいかもしれません。
ですが、そこまでキッチリと構造化したデータをLLMに伝えても、それが利用されるのかは、何とも言えず。
現時点では、そこまで考える必要はないのかもしれません。
動作確認
そろそろ MCP Inspector ではなく、MCPサーバー本来の使い方として、LLM から呼び出してみたいと思います。
LM Studio
今回は、LM Studioを使ってみようと思います。
LLMには、openai の gpt-oss-20b を使用しました。
(このあたりの設定は、少し横道にそれてしまうので省きます)
MCP Serverの設定
LM Studio に今回作成した MCP Server を設定します。
ウィンドウの下にあるコンセントのようなアイコンをクリック、Install から Edit mcp.json を実行します。
以下のような警告が表示されますが、Got It と進みます。
時間の経過とともに、もっと厳しいチェックが入るようになるのだと思いますが、まだ過渡期なので、自己判断で気を付けてねというような警告なのでしょう。
mcp.json の内容を編集します。
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest"
]
},
+ "tvgate": {
+ "url": "http://localhost:8000/mcp"
+ }
}
}
playwrightは以前から別に使っているだけで、今回は関係ありません。^^;
設定を追加すると以下のようにメッセージが表示されます。
その後、再度コンセントのアイコンをクリック。mcp/tvgate を有効にします。
使ってみる
MCPサーバーが有効になったので、「リビングのTVのチャンネル一覧をリストアップして」と聞いてみます。
LLMが MCPサーバーを使用したいけど良いですか?と確認が表示されます。
実行をクリックします。
結果は以下の通りです。
流石、OpenAIのLLMさんです。チャンネル追加や削除なんて機能はありませんが、対応してくれるらしいです。そんな事はないですが笑
ちなみに、上記の表示は、以下のように私の作成している別アプリのソースを利用しています。
もちろん勉強がてら、わざわざC言語で作っているというのもありますが、過去の資産を利用しやすい状況を作るというのも、C言語で作ってきた一つの理由だったりします。
server.AddTool(
"get_channels",
:
[](const std::map<std::string, std::string>& args) -> std::vector<McpServer::McpContent> {
std::vector<McpServer::McpContent> contents;
CChannelManager* channel_manager = CChannelManager::GetInstance();
ChannelList& channels = channel_manager->GetChannels(CHANNEL_TYPE_ISDB_T_JPN);
for (auto it = channels.begin(); it != channels.end(); it++)
{
McpServer::McpContent content{
.property_type = McpServer::PROPERTY_OBJECT,
.value = ""
};
content.properties.push_back({
.property_name = "channel_no",
.value = CChannelManager::ConvertChannelNo(it->channel_no)
});
char service_name[255];
utf16_to_utf8(it->service_name, service_name, sizeof(service_name));
content.properties.push_back({
.property_name = "service_name",
.value = service_name
});
contents.emplace_back(content);
}
return contents;
}
);
おわりに
MCP Inspectorではなく、LLMから実行すると一気にらしくなってきました。
チャンネル情報だけではなく、番組情報と組み合わせることで、いろいろできるかもと期待させる結果になりました。
GitHub Copilotと組み合わせると、よりサクサク動作したりますが、ローカルLLMでもこのように動作させることは可能です。
追記...
番組表の検索も追加してみた。
しかし情報量が多いようで、ローカルLLMだと厳しいようでした。
そこで、VS Codeから Github copilot を利用して番組表検索ツールを実行。
時間での検索しかMCPサーバー側では処理していないにも関わらず、LLMが追加でニュースと言うキーワードでフィルタリングを行い、さらに番組内容も要約して表示してくれている。
自分のアプリにAIをこんなにも簡単に組み込むことができるんだと改めて思う今日この頃です。
(いやまだ組み込んだ訳ではないのですが...)