はじめに
前回の記事では、Quarkus LangChain4jのTool Callingを使用して、LLMからJavaメソッドを呼び出すアプリケーションを作成しました。
前回の構成では、AI ServiceとToolが同じQuarkusアプリケーション内に実装されていました。
一方、実際のシステムでは、同じ業務処理を複数のAIアプリケーションから利用したい場合や、AIアプリケーションと業務処理を独立して管理したい場合があります。
このようなAIアプリケーションと外部のToolやデータソースを共通の方法で接続するためのプロトコルが、MCP - Model Context Protocolです。
本記事では、前回作成した3つのToolと業務処理を独立したQuarkus MCP Serverへ切り出し、Quarkus LangChain4jを使用したSupport AssistantからMCP Client経由で利用します。
今回は、MCP Toolの基本的な実装と接続方法に焦点を当てるため、次の機能は扱いません。
- MCP Resource
- MCP Prompt
- データベースや外部サービスとの接続
また、前回と同様に参照系のToolのみを実装し、参照するデータには固定のJSONファイルを使用します。
実装には、前回と同様に、オープンソースのQuarkusをベースとするIBM Enterprise Build of Quarkusを使用します。
なお、本番運用向けには、IBMによるサポートと長期ライフサイクルが提供される製品版も用意されています。
MCPとは
MCPは、AIアプリケーションと外部のToolやデータソースを接続するためのプロトコルです。
今回の構成を単純化すると、次のようになります。
Support Assistant
↓
MCP Client
↓ Streamable HTTP
MCP Server
↓
Tool・業務処理・データ
MCP Serverは、主に次の機能をクライアントへ提供できます。
- Tool:LLMから利用できる処理
- Resource:AIアプリケーションが参照できるデータ
- Prompt:再利用可能な指示テンプレート
なお、本アプリケーションのJSONデータは、MCP Resourceとして公開するのではなく、MCP Toolの内部で利用する業務データとして扱います。
本記事では、このうちToolに限定して実装します。
今回のアプリケーションとの対応
| MCPの要素 | 今回のアプリケーション |
|---|---|
| MCP Host | Support Assistant |
| MCP Client | Quarkus LangChain4j MCP Client |
| MCP Server | Support Operations MCP Server |
| MCP Tool | サービス状態、類似インシデント、担当チームの取得処理 |
Support AssistantはMCP Clientを通じてMCP Serverへ接続します。
MCP Serverは、Toolの名前、説明、入力スキーマなどをMCP Clientへ提供します。
LLMは提供されたToolの情報を参考に、問い合わせの処理に必要なToolと引数を選択します。
実際のToolはMCP Server側で実行され、その結果がLLMへ返されます。
Tool CallingとMCPの違い
Tool CallingとMCPは、どちらか一方を選ぶ機能ではありません。
Tool Callingは、LLMが利用するToolと引数を選択する仕組みです。
MCPは、ToolをAIアプリケーションへ提供し、呼び出す方法を標準化するプロトコルです。
前回は、Support Assistant内のJavaクラスをAI Serviceへ直接登録していました。
Support Assistant
├── AI Service
└── Java Tool
└── 業務処理
この構成では、Toolの実装がSupport Assistantに組み込まれているため、基本的には同じアプリケーション内で利用します。
今回は、Toolと業務処理をMCP Serverへ切り出し、Support AssistantからMCP Client経由で利用します。
Support Assistant
├── AI Service
└── MCP Client
↓ Streamable HTTP
Support Operations MCP Server
├── MCP Tool
└── 業務処理
MCPを使用する場合も、LLMがToolと引数を選択する処理自体はTool Callingです。
変わるのは、Toolの配置場所と、AIアプリケーションへToolを提供する方法です。
ToolをMCP Serverとして切り出すことで、主に次のメリットがあります。
-
Toolを再利用しやすくなる
MCP Clientに対応した複数のAIアプリケーションから、同じToolを共通の方法で利用できます。 -
AIアプリケーションとToolを独立して管理できる
Support AssistantとMCP Serverを別々に変更、起動、デプロイできるため、LLMを利用するアプリケーションと業務処理の責務を分離できます。 -
Toolごとの個別連携を減らせる
MCPによってToolの検出と呼び出し方法が共通化されるため、AIアプリケーションごとに専用の連携処理を実装する必要を減らせます。 -
業務システムへのアクセスをMCP Server側へ集約できる
データベースや外部APIへの接続、入力検証、認証・認可、監査ログなどをMCP Server側で管理しやすくなります。
今回はSupport Assistantだけが接続しますが、将来的には同じMCP Serverを別のチャットアプリケーションや運用支援エージェントから利用することもできます。
違いを整理すると、以下のとおりです。
| 項目 | 前回 (Tool calling) | 今回 (MCP) |
|---|---|---|
| Toolの配置 | Support Assistant内 | MCP Server内 |
| Toolの登録 | AI ServiceへJavaクラスを直接登録 | MCP Serverが公開するToolを利用 |
| Toolの実行場所 | Support Assistant | MCP Server |
| 接続方法 | 同一プロセス内のJavaメソッド呼び出し | MCP over Streamable HTTP |
| プロセス数 | 1 | 2 |
| Toolの再利用 | アプリケーション内に限定 | MCP Clientに対応したアプリケーションから利用可能 |
MCPの接続方式
MCPでは、MCP ClientとMCP Serverの間でメッセージをやり取りする方法を、トランスポートと呼びます。代表的なトランスポートには、STDIOとStreamable HTTPがあります。
STDIO
MCP ClientがMCP Serverをローカルの子プロセスとして起動し、標準入力と標準出力を通じて通信します。ローカルのCLIやデスクトップアプリケーションからToolを利用する構成に適しています。
Streamable HTTP
MCP Serverを独立したプロセスとして起動し、MCP ClientがHTTP経由で接続します。MCP Serverは/mcpなどのHTTPエンドポイントを公開し、JSON-RPC形式のメッセージを受け取ります。必要に応じて、Server-Sent Eventsを使用したストリーミングレスポンスも利用できます。
Streamable HTTPは、従来のHTTP+SSEトランスポートを置き換える方式として導入されました。
今回のアプリケーションでは、Support AssistantとSupport Operations MCP Serverを別々のQuarkusプロセスとして起動します。そのため、ローカルの子プロセスとして実行するSTDIOではなく、ネットワーク経由で接続できるStreamable HTTPを使用します。
Support Assistant
↓ Streamable HTTP
http://localhost:8081/mcp
↓
Support Operations MCP Server
アプリケーションの実装
アプリケーション概要
引き続き、サポートチケットの問い合わせを調査する「AI Ticket Triage」を使用します。
ユーザーは、次のような問い合わせを入力します。
payment-serviceでConnection timeoutが発生しています。
サービス状態と過去の類似インシデントを調査し、
担当チームの連絡先も確認してください。
LLMは問い合わせ内容に応じて、MCP Serverが公開する次のToolを利用します。
| Tool名 | 処理 |
|---|---|
getServiceStatus |
サービスの現在の稼働状況を取得する |
searchSimilarIncidents |
過去の類似インシデントを検索する |
getTeamContact |
サービスを担当するチームの連絡先を取得する |
各Toolの実行結果はLLMへ返され、最終的にTicketInvestigationへ変換されます。
本記事で使用するソースコードは、次のGitHubリポジトリで公開しています。
プロジェクト構成
前回までのSupport Assistantは、リポジトリ直下のQuarkusアプリケーションとして継続利用します。新たに、独立したQuarkusアプリケーションとしてsupport-ops-mcp-serverを追加します。
ai-ticket-triage/
├── pom.xml
├── src/
│ └── main/
│ ├── java/dev/autonomura/ticket/
│ │ ├── api/
│ │ │ └── TicketResource.java
│ │ └── ai/
│ │ ├── TicketAssistant.java
│ │ └── model/
│ │ └── TicketInvestigation.java
│ └── resources/
│ └── application.properties
│
└── support-ops-mcp-server/
├── pom.xml
└── src/
└── main/
├── java/dev/autonomura/support/mcp/
│ ├── tool/
│ │ └── SupportOperationsTools.java
│ ├── application/
│ │ └── IncidentInvestigationService.java
│ ├── domain/
│ ├── dto/
│ └── infrastructure/
└── resources/
├── application.properties
└── data/
2つのアプリケーションは独立したMavenプロジェクトとして起動します。
使用するExtension
Support Assistant側
Support Assistantには、MCP Client機能を提供するExtensionを追加します。
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-mcp</artifactId>
</dependency>
前回から使用している次のExtensionは、そのまま利用します。
quarkus-rest-jacksonquarkus-langchain4j-openai
MCP Server側
MCP Serverには、HTTPトランスポートを提供するExtensionを追加します。
<dependency>
<groupId>io.quarkiverse.mcp</groupId>
<artifactId>quarkus-mcp-server-http</artifactId>
<version>${quarkus-mcp-server.version}</version>
</dependency>
quarkus-mcp-server-httpは、使用するQuarkusディストリビューションと互換性のあるバージョンを指定する必要があります。本記事では、検証環境に対応するRed Hat版1.10.5.redhat-00002を使用しました。
実際に解決されたバージョンは、次のコマンドで確認できます。
./mvnw dependency:tree -Dincludes=io.quarkiverse.mcp
MCP Serverの設定
Support Assistantは8080番ポート、MCP Serverは8081番ポートで起動します。
support-ops-mcp-server/src/main/resources/application.propertiesに、次の設定を追加します。
quarkus.http.port=8081
quarkus.log.level=INFO
ExtensionによってMCPエンドポイントが公開されます。今回は、Support Assistantからhttp://localhost:8081/mcpへStreamable HTTPで接続します。
MCP Toolの実装
MCP Toolの実装には、io.quarkiverse.mcp.server.ToolとToolArgを使用します。
以下は、サービス状態を取得するgetServiceStatusの実装です。
package dev.autonomura.support.mcp.tool;
import dev.autonomura.support.mcp.application.IncidentInvestigationService;
import dev.autonomura.support.mcp.dto.ServiceStatusResult;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
@ApplicationScoped
public class SupportOperationsTools {
private static final Logger LOG = Logger.getLogger(SupportOperationsTools.class);
@Inject
IncidentInvestigationService service;
@Tool(description = """
指定したサービスの現在の稼働状況を確認する。
障害、停止、接続エラー、タイムアウトなどが
疑われる場合に呼び出す。
""")
public ServiceStatusResult getServiceStatus(
@ToolArg(description = "稼働状況を確認したいサービスの名前(例: payment-service)")
String serviceName) {
LOG.infof("[MCP Tool] getServiceStatus start: serviceName=%s", serviceName);
if (isBlank(serviceName)) {
return ServiceStatusResult.error(serviceName);
}
try {
ServiceStatusResult result = service.checkServiceStatus(serviceName)
.map(status -> ServiceStatusResult.found(
status.serviceName(),
status.status(),
status.message()))
.orElseGet(() -> ServiceStatusResult.notFound(serviceName));
LOG.infof(
"[MCP Tool] getServiceStatus end: status=%s, resultType=%s",
result.status(),
result.resultType());
return result;
} catch (Exception e) {
LOG.errorf(e, "[MCP Tool] getServiceStatus error: %s", e.getMessage());
return ServiceStatusResult.error(serviceName);
}
}
private boolean isBlank(String value) {
return value == null || value.isBlank();
}
}
@ToolでMCP Toolとして公開する
前回と今回では、同じ@Toolという名前でも、使用するアノテーションが異なります。
前回: dev.langchain4j.agent.tool.Tool
今回: io.quarkiverse.mcp.server.Tool
前回の@Toolは、同一アプリケーション内のJavaメソッドをLangChain4jのToolとして登録するために使用しました。
今回の@Toolは、JavaメソッドをMCP ServerのToolとして公開するために使用します。Toolの名前や説明、引数の情報は、MCPを通じてクライアントへ提供されます。
@ToolArgで引数を説明する
前回は、Toolの引数を説明するために@Pを使用しました。今回はMCP Serverが提供する@ToolArgを使用します。
前回: @P
今回: @ToolArg
LLMはTool名、Toolの説明、引数の説明を参考に、使用するToolと引数を判断します。そのため、引数には技術的な型だけでなく、値の意味や入力例も記述します。
ToolからApplication Serviceを呼び出す
ToolクラスにはJSON検索などの業務処理を直接実装せず、IncidentInvestigationServiceへ委譲しています。
MCP Tool
↓
IncidentInvestigationService
↓
DataSource
↓
JSONデータ
MCP固有の公開処理と業務処理を分離することで、ToolクラスはMCPとJavaの業務処理をつなぐアダプターの役割に専念できます。
Toolの戻り値をDTOにする
Toolの実行結果は、MCP ClientとLLMが解釈しやすい形式へ変換して返します。
public record ServiceStatusResult(
String serviceName,
String status,
String message,
String resultType
) {
public static ServiceStatusResult found(
String serviceName,
String status,
String message) {
return new ServiceStatusResult(serviceName, status, message, "FOUND");
}
public static ServiceStatusResult notFound(String serviceName) {
return new ServiceStatusResult(
serviceName,
"UNKNOWN",
"該当するサービスの情報は確認できませんでした",
"NOT_FOUND");
}
public static ServiceStatusResult error(String serviceName) {
return new ServiceStatusResult(
serviceName,
"UNKNOWN",
"調査中にエラーが発生しました",
"ERROR");
}
}
resultTypeでは、次の3つの状態を区別します。
| resultType | 意味 |
|---|---|
FOUND |
処理が成功し、対象データが見つかった |
NOT_FOUND |
処理は成功したが、対象データが存在しなかった |
ERROR |
入力不備または処理中の例外によって調査に失敗した |
「データが存在しない」と「処理が失敗した」を区別することで、LLMが事実に合った回答を生成しやすくなります。
残り2つのMCP Tool
searchSimilarIncidentsとgetTeamContactも、同じ構成で実装します。
@Tool(description = """
過去の類似インシデントを検索する。
エラーコード、エラーメッセージ、具体的な症状がある場合に呼び出す。
最大3件返す。
""")
public IncidentSearchResult searchSimilarIncidents(
@ToolArg(description = "検索対象のサービス名(例: payment-service)")
String serviceName,
@ToolArg(description = "エラーコードや症状を表す検索キーワード(例: timeout)")
String keyword) {
// IncidentInvestigationServiceへ処理を委譲
}
@Tool(description = """
指定したサービスを担当するチームの連絡先を取得する。
担当チームへの連絡が必要な場合に呼び出す。
""")
public TeamContactResult getTeamContact(
@ToolArg(description = "連絡先を取得したいサービスの名前(例: payment-service)")
String serviceName) {
// IncidentInvestigationServiceへ処理を委譲
}
前回のgetSupportTeamContactはチーム名を引数にしていましたが、今回はgetTeamContactへ変更し、サービス名を引数にしています。
チーム名はLLMが推測しなければならない値ですが、サービス名は問い合わせ文に直接書かれている値です。「サービス名→担当チームの対応付け」はMCP Server側のデータが持つことで、LLMの推測に依存しない設計にしています。
MCP Clientの設定
Support Assistant側のsrc/main/resources/application.propertiesに、MCP Serverへの接続設定を追加します。
quarkus.langchain4j.mcp.support-ops.transport-type=streamable-http
quarkus.langchain4j.mcp.support-ops.url=http://localhost:8081/mcp
quarkus.langchain4j.mcp.support-ops.log-requests=true
quarkus.langchain4j.mcp.support-ops.log-responses=true
support-opsはMCP Clientの識別名です。複数のMCP Serverへ接続する場合は、接続先ごとに異なる識別名を使用できます。
今回は、ローカルで起動したMCP Serverの/mcpエンドポイントへStreamable HTTPで接続します。リクエストとレスポンスのログは、開発時の動作確認のために有効化しています。
AI ServiceからMCP Toolを利用する
前回は、@RegisterAiServiceのtools属性へJavaクラスを指定していました。
@RegisterAiService(tools = TicketInvestigationTools.class)
public interface TicketAssistant {
// ...
}
今回は、@McpToolBoxでMCP Clientを指定します。
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
import io.quarkiverse.langchain4j.mcp.runtime.McpToolBox;
@RegisterAiService
public interface TicketAssistant {
@McpToolBox("support-ops")
@SystemMessage("""
あなたはIT運用チームのシニアエンジニアです。
問い合わせ内容を調査し、最終的な調査結果を報告してください。
必要に応じて複数のMCP Toolを組み合わせて調査してください。
Toolで取得できなかった事実は推測しないでください。
""")
@UserMessage("""
以下の問い合わせ内容を調査してください。
問い合わせ:
{{ticket}}
""")
TicketInvestigation investigate(String ticket);
}
@McpToolBox("support-ops")の値は、application.propertiesで設定したMCP Clientの識別名と一致させます。
Support Assistantは、MCP Serverから取得したToolをAI Serviceで利用できるようになります。問い合わせを受け取ったLLMは、利用可能なToolの説明を基に、必要なToolと引数を選択します。
Promptの変更
System Messageは、前回の内容を基本的に引き継ぎつつ、MCP Serverが公開するToolに合わせて変更しています。
主な変更点は次のとおりです。
- Tool名をMCP Serverで公開する名前へ変更
- 担当チームの取得条件を、サービス名を基準とするように変更
-
FOUND、NOT_FOUND、ERRORの違いを回答へ反映するように指示 - Toolで確認できなかったサービス状態、過去事例、担当チームを推測しないように指示
また、Toolから取得した事実と、LLMが一般的な知識を基に生成する提案を区別するようにしています。
例えば、類似インシデントが見つからなかった場合に、LLMが一般的な確認事項を提案することはできます。ただし、その内容を過去事例から確認された解決方法として記述しないようにします。
アプリケーションの起動
2つのQuarkusアプリケーションを、別々のターミナルで起動します。
ターミナル1:MCP Server
cd support-ops-mcp-server
mvn quarkus:dev
MCP Serverは8081番ポートで起動します。
ターミナル2:Support Assistant
export OPENAI_API_KEY="your_api_key_here"
mvn quarkus:dev
Support Assistantは8080番ポートで起動します。
第2回では、AI Service、Tool、業務処理が1つのQuarkusプロセスで動作していました。今回は、Support AssistantとMCP Serverが独立したプロセスとして動作します。
MCP Toolの動作確認
複数のMCP Toolを呼び出す
次の問い合わせを送信します。
curl -s -X POST \
-H "Content-Type: text/plain" \
--data-binary \
"payment-serviceでConnection timeoutが発生しています。サービス状態と過去の類似インシデントを調査し、担当チームの連絡先も確認してください。" \
http://localhost:8080/tickets/investigate | jq .
実際に得られたレスポンスは次のとおりです。
{
"summary": "payment-serviceで接続タイムアウトが発生しており、サービスが劣化している状況が確認されました。",
"category": "APPLICATION",
"priority": "HIGH",
"investigationResult": "サービス状態: DEGRADED(外部決済ゲートウェイへのレイテンシが増加中)。過去の類似インシデントとして、1) 接続タイムアウトの多発による注文完了不可 (解決策: 接続プールの上限を拡張) と、2) 504ゲートウェイタイムアウトによる外部ゲートウェイリクエストの失敗 (解決策: フォールバック処理有効化) がありました。担当チームの連絡先はメール: payment-team@example.com、Slackチャンネル: #payment-support。",
"recommendedAction": "外部決済ゲートウェイの状態を監視し、接続プールの調整やフォールバック処理を考慮する。",
"assignedTeam": "Payment Team"
}
この問い合わせでは、次の3つの情報が最終回答へ反映されています。
-
getServiceStatusで取得したサービス状態 -
searchSimilarIncidentsで取得した過去の類似インシデント -
getTeamContactで取得した担当チームの連絡先
MCP Server側のログでも、3つのToolが実行されたことを確認できます。
[MCP Tool] getServiceStatus start: serviceName=payment-service
[MCP Tool] getServiceStatus end: status=DEGRADED, resultType=FOUND
[MCP Tool] searchSimilarIncidents start: serviceName=payment-service, keyword=timeout
[MCP Tool] searchSimilarIncidents end: totalFound=2, resultType=FOUND
[MCP Tool] getTeamContact start: serviceName=payment-service
[MCP Tool] getTeamContact end: teamName=Payment Team, resultType=FOUND
ToolはSupport Assistant内ではなく、MCP Server上で実行されています。LLMがToolの実行結果を組み合わせ、最終的なTicketInvestigationを生成しています。
categoryなどの分類結果や文章表現は、使用するモデルや実行タイミングによって変わる場合があります。
データが見つからない場合
次に、登録されていないサービス名を指定します。
curl -s -X POST \
-H "Content-Type: text/plain" \
--data-binary \
"unknown-serviceでエラーが発生しています。サービス状態と担当チームを確認してください。" \
http://localhost:8080/tickets/investigate | jq .
実際に得られたレスポンスは次のとおりです。
{
"summary": "unknown-serviceでのエラーについて調査した結果",
"category": "OTHER",
"priority": "MEDIUM",
"investigationResult": "該当するサービスの情報は確認できませんでした。",
"recommendedAction": "サービスの詳細情報を再確認し、正しいサービス名で再度問い合わせてください。",
"assignedTeam": "該当する担当チームは確認できませんでした。"
}
期待どおり、次の点を確認できました。
- 未登録サービスの状態を推測していない
- 存在しない担当チームを生成していない
-
NOT_FOUNDを「情報なし」として回答へ反映している
類似インシデントが0件の場合
最後に、サービスは登録されているものの、検索条件に一致する類似インシデントが存在しないケースを確認します。
curl -s -X POST \
-H "Content-Type: text/plain" \
--data-binary \
"auth-serviceでUnknown certificate errorが発生しました。過去の類似インシデントを検索してください。" \
http://localhost:8080/tickets/investigate | jq .
実際に得られたレスポンスは次のとおりです。
{
"summary": "Unknown certificate errorが発生",
"category": "APPLICATION",
"priority": "HIGH",
"investigationResult": "過去の類似インシデントは確認できませんでした。",
"recommendedAction": "証明書の設定や有効期限を再確認してください。",
"assignedTeam": "該当する情報は確認できませんでした"
}
このケースでは、検索処理自体は成功していますが、検索結果が0件でした。
resultType=FOUND
incidents=[]
totalFound=0
0件をシステムエラーとして扱わず、「正常に検索したが該当データがなかった」という結果をLLMへ返しています。また、存在しない過去事例を生成していません。
recommendedActionはLLMが一般知識を基に生成した確認事項です。Toolから取得した過去の解決方法ではないため、実務で利用する場合は、Toolで確認された事実と一般的な提案を回答上で区別する設計が必要です。
MCP化に適した処理
すべてのJavaメソッドをMCP Toolにする必要はありません。MCP化する単位は、再利用性や責務の境界を考慮して決めます。
MCP化に適している処理は、以下のようなものが挙げられます。
- 複数のAIアプリケーションから利用したい処理
- 既存の業務処理をAI向けに公開したい処理
- AIアプリケーションとは独立して変更、運用したい処理
- 外部APIやデータベースへのアクセスを集約したい処理
- 認証、認可、監査ログなどをTool側で一元管理したい処理
今回の例では、サービス状態、過去のインシデント、担当チームの情報は、サポートチケット調査以外のAIアプリケーションからも利用できる可能性があります。そのため、MCP Serverとして切り出す題材に適しています。
MCP Serverを実装する際の注意点
Toolの説明を具体的にする
LLMは、Toolの名前、説明、引数の説明を参考に、利用するToolと引数を判断します。
例えば、「ステータスを取得する」だけでは、どのような問い合わせで使用すべきかが分かりません。
指定したサービスの現在の稼働状況を確認する。
障害、停止、接続エラー、タイムアウトなどが疑われる場合に呼び出す。
このように、Toolが「何をするか」だけでなく「いつ使うか」も記述すると、LLMが適切なToolを選択しやすくなります。
MCP Toolの入出力を必要最小限にする
MCP Toolの戻り値には、LLMが回答を生成するために必要な情報だけを含めます。
内部ID、デバッグ情報、例外の詳細などをそのまま返すと、LLMへ不要な情報を渡すだけでなく、内部情報の露出につながる可能性があります。
MCP Serverと業務処理を分離する
MCP Toolは、プロトコルと業務処理をつなぐアダプターとして実装します。
MCP Tool
↓
Application Service
↓
DataSource
この分離により、MCP Toolの入出力や接続方式を変更した場合でも、業務処理への影響を抑えられます。
更新系Toolは慎重に扱う
今回は参照系Toolのみを実装しました。
メール送信、データ更新、サービス再起動などの処理をToolとして公開する場合は、少なくとも次の対策が必要です。
- 認証・認可
- 入力値の検証
- 実行前の利用者確認
- 冪等性や取り消し方法の検討
- 監査ログ
MCP Serverを外部公開する場合は保護する
本記事ではローカルデモのため、MCP Serverの認証・認可を実装していません。
実際の環境で公開する場合は、接続元、利用者、Toolごとの権限を考慮し、組織のセキュリティ要件に応じて保護する必要があります。
エラーと「データなし」を区別する
今回の実装では、FOUND、NOT_FOUND、ERRORを区別しました。
これにより、LLMは次の違いを判断できます。
- 対象データが見つかった
- 調査は成功したが、対象データが存在しなかった
- Toolの処理自体が失敗した
ただし、これはデモ用に簡略化した設計です。
MCPには、Toolの実行失敗をプロトコルレベルで通知する仕組みもあります。そのため、実際のMCP Serverでは、業務上の検索結果としてデータが存在しなかった場合はNOT_FOUND、システム障害などによってToolの実行自体が失敗した場合はプロトコルレベルのエラーとするなど、両者の表現方法を設計時に決める必要があります
実装の振り返り
今回の処理フローを整理すると、次のようになります。
TicketResource
↓ 問い合わせ
TicketAssistant
↓ Toolと引数を選択
MCP Client
↓ Streamable HTTP
Support Operations MCP Server
↓ MCP Toolを実行
IncidentInvestigationService
↓ JSONデータを参照
MCP Toolの実行結果
↓
LLM
↓ 最終回答を生成
TicketInvestigation
前回から変わったのは、LLMがToolと引数を選択する仕組みではなく、Toolの配置場所と提供方法です。
前回はToolと業務処理をSupport Assistant内に実装していました。
今回は、それらを独立したMCP Serverへ切り出し、MCP Client経由で利用する構成へ変更しました。
これにより、Support AssistantはLLMとの連携と最終回答の生成、MCP ServerはToolの公開と業務処理の実行を担当します。
まとめ
今回は、QuarkusでMCP Serverを作成し、Javaの業務処理をMCP Toolとして公開しました。
- @Toolを使用してJavaメソッドをMCP Toolとして公開した
- Support AssistantからStreamable HTTPでMCP Serverへ接続した
- @McpToolBoxを使用して、AI ServiceからMCP Toolを利用した
- LLMが問い合わせ内容に応じて複数のToolを選択し、実行結果を調査レポートへまとめた
-
FOUND、NOT_FOUND、ERRORによって、取得成功、データなし、処理失敗を区別した
Tool Callingは、LLMが利用するToolと引数を選択する仕組みです。一方、MCPは、そのToolをAIアプリケーションへ提供し、呼び出す方法を標準化するプロトコルです。
今回は固定のJSONデータをToolから検索しましたが、実務では監視システム、インシデント管理システム、データベース、社内APIなどへ置き換えられます。
次回は、Quarkus LangChain4jのRAGとpgvectorを使用し、独自ドキュメントをベクトル検索して、その検索結果をLLMの回答に利用する構成を確認したいと思います。