JavaでつくるMCP SSEサーバー on Liberty【1: つくる編】

## はじめに
「25年はAIエージェントが流行る」という観測が多く聞かれましたが、昨今開発界隈で話題の**Model Context Protocol** (MCP)により、現実味が増してきたように思います。
既に多くの方が記事化されていますが、Claudeと開発ツール系のMCPサーバーを組み合わせるパターンが多く見受けられます。またPythonのサンプルコードは多いですが、Javaは少数派です。
そこで当記事では「**業務データを扱うMCP**」をイメージして、どんなことが実現できるのかを体験していきたいと思います。プラットフォームは業務システムで広く利用されているJavaを使用します。

### 実現すること
JavaのOpenLiberty上で動作するサービスをSSEのMCPサーバーとして実行し、AIアプリケーションと組み合わせて動作を確認します。

1:つくる編(当記事)
2:[うごかす編](https://qiita.com/mi-ta/items/afd0dca9a665aeaa8201)

当記事ではMCPサーバーを準備するところまでを紹介し、別の記事でMCPサーバーの動作を確認していきます。

### 環境
| ツール等 | バージョン |
|:-:|:-:|
| JDK  | IBM Semeru Runtime 21.0.4  |
| Liberty  | 25.0.0.4 |
| OS | Windows 11 Enterprise 23H2 |
| VSCode | 1.99.3 |
| MCP SDK | 0.9.0 |
| Python | 3.12.3 |
| node.js | 22.15 |

Pythonとnode.jsはMCPサーバーの動作検証ツールを使用するために使いますが、検証ツール使用をスキップしてもMCPサーバーは動かすことができます。

## MCPについて
### MCPとは
https://modelcontextprotocol.io/introduction

本家サイトでは「Think of MCP like a USB-C port for AI applications.」と紹介されています。これは、USB-Cのように「簡単に接続でき」「設定なしですぐに機能する」という利便性をAIアプリケーションにもたらすことを意図した表現です。

「プロトコル」と名付けられていますが、ネットワークプロトコルとは異なります。RESTやGraphQLといったAPI設計のパラダイムと同様の粒度で、AIアプリケーションとその外部との対話における共通のルールを定めたものです。各種プログラミング言語向けのSDKも提供されており、開発者は容易にMCPを組み込むことができます

MCPでは、様々な機能が定義されており、MCPクライアントの種類によって利用できる機能が異なります。[公開されている各MCPクライアントの実装対応](https://modelcontextprotocol.io/clients)を見ると、**Toolsが中心的な役割を担っている**ことがわかります。

Toolsに相当する外部連携の仕組みは、従来より「ファンクションコーリング」などの名称で存在していました。しかし、これまではAIアプリケーション側での実装が必要となる場面も少なくありませんでした。MCPは、このような外部連携機能をより簡便に利用できるようにすることを目指しています。

MCPについては、既に多くの参考記事が公開されていますので、本章ではその概要を紹介するに留めます。

https://modelcontextprotocol.io/docs/concepts/architecture

各技術要素を深掘りしたい場合は、上記ページから始まる「Concepts」を確認してみてください。

### MCPサーバーとは
MCPサーバーは、AIから見た外部リソースのプロキシのような役割を担います。主な機能は以下の通りです。

* MCPクライアントと通信するためのインターフェースを提供します
* ツールなどの提供する機能の定義をMCPクライアントに通知します
* バックエンドのサービスを呼び出します

MCPクライアントとの接続方式として、標準実装では「STDIO」（標準入出力）と「SSE」（Server-Sent Events）の2つが用意されています。STDIOは、MCPクライアントとMCPサーバーを同一マシン上で動作させる場合に適しています。一方、SSEはHTTPベースで動作するため、リモートにあるMCPサーバーとの接続が可能です。

実行環境のネットワーク構成やセキュリティ要件などを考慮して、適切な接続方式を選択することになります。
記事作成時点では、Pythonで記述されたMCPサーバーを mv や npx コマンドで起動するケースが多いようですが、実際のビジネス環境においては、クライアント端末にPython環境を組み込んだり、動的にMCPサーバーをインストールしたりすることは、保守やセキュリティの観点から難しい場合があります。そのため、実用的な運用を見据えると、リモート接続が可能なSSEの利用も検討すべきでしょう。

## 今回作成するMCPサーバー
### 実装技術
Javaの実行フレームワーク（APサーバー）であるOpenLibertyを利用してMCPサーバーを構築し、MCPクライアントとの接続にはSSE（Server-Sent Events）を採用します。
[公式サイト](https://modelcontextprotocol.io/sdk/java/mcp-overview)にはいくつかの実装パターンが示されていますが、今回はそのうち赤枠で示されているパターンに相当する構成となります。

<img width="600" alt="qiita-square" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536604/a733170c-2fa3-4f94-a71e-f4ed14b5dd34.png">

JavaのSSEの実装は3パターンありますが、Springを使用しないシンプルな**HttpServletSseServerTransport**を使用します。

### Toolの仕様
今回提供するToolは、「火星の天気を取得する」機能です。火星の大陸名を指定すると、対応する天気と気温の情報を返します。このToolが提供するデータは、アプリケーション内に固定で記述された以下の結果に基づきます。

| 大陸名 | 天気 | 気温 |
|:-:|:-:|:-:|
| aonia  | Sunny  | 10°C |
| cimmeria  | Clear  | -15°C  |
| noachis  | Cloudy | -70°C |
| sabaea  | Rainy | -120°C |
| tyrrhena  | Sunny | 0°C |

いずれの大陸名も、火星の大陸名として現実世界で定義されているものです。
火星は太陽から遠く大気も薄いため、気温は地球に比べて低めです。

## セットアップ
### JDK
MCPサーバーの実行にはJDK（Java Development Kit）が必要です。既にJDK 17以降がインストールされている環境であれば、そのままご利用いただけます。

もしJDKのインストールが必要な場合は、[こちらのQiita記事](https://qiita.com/mi-ta/items/d63b8c169d07cd15431d)をご参照いただき、記事内のJDKをインストールする手順に従って環境構築をお願いいたします。

### MCPサーバープログラムのダウンロード
GitHubからサンプルプログラムをcloneしてください。Gitが必要ですが、Gitのインストール手順は割愛します。
```
git clone https://github.com/mi-ta-build/mcpserver.git
```

## MCPサーバーの実装
もっとクールな実装はあるかと思いますが、今回次のように作成しています。

### TransportProvider
MCPクライアントとの通信を担当するのがTransportProviderです。

```MyTransportProvider.java
@WebServlet(urlPatterns = {"*"}, asyncSupported = true)
public class MyTransportProvider extends HttpServletSseServerTransportProvider {

	McpSyncServer server;

	public MyTransportProvider() {
		super(new ObjectMapper(), "/message", "/sse");
		server= new MyMcpService().getServer(this);
	}

}
```
これはアノテーションから分かるとおり実態はServletです。
superで指定しているのは「JSON変換を行なうマッパー」「通信を行なうためのエンドポイント」「SSEを開始するためのエンドポイント」（指定しない場合は/sseが設定される）です。

Servletの初期化タイミングで、MCPサーバーのインスタンスを作成しています。

:::note warn
MCPの現行仕様では、[SSEからStreamableHTTPへの移行](https://modelcontextprotocol.io/specification/2025-03-26/changelog)がアナウンスされています。上記の実装は将来的に変更が必要かもしれません。
:::

### McpServer
McpServerは、MCPサーバーの機能をクライアントと共有したり、クライアントからのリクエストを受け付ける中核機能です。

```MyMcpService.java
    McpSyncServer mcpServer;

    public McpSyncServer getServer(MyTransportProvider transportProvider) {
 
        mcpServer = McpServer.sync(transportProvider)
        .serverInfo("my-mcp-server", "0.0.1")
        .capabilities(
            ServerCapabilities.builder()
            .tools(true)
            .resources(false, false)
            .prompts(false)
            .build())
        .tools(getToolgetMarsWeather())
        .build();

        return mcpServer;
    }
```
McpServerには、レスポンス処理の違いによりsync（同期）とasync（非同期）の2種類が存在します。レスポンスの仕組みを除けば機能は同等と考えてよいかと思います。

この例では、メソッドチェーンを用いて段階的に定義を重ねてMCPServerを生成しています。

ServerCapabilitiesの下に記述されているtools、resources、promptsのtrue/falseは、このMcpServerがそれぞれの機能を提供するかを示しています。

.tools(getToolgetMarsWeather())は、提供するツール定義を引数で渡しています。

```MyMcpService.java
    private McpServerFeatures.SyncToolSpecification getToolgetMarsWeather() {

        McpSchema.Tool tool = new McpSchema.Tool(
            "mars-weather",
            "gives today's weather of mars for a given cotinent written in English.",
            """
            {
              "type": "object",
              "properties": {
                "continent": {
                  "type": "string"
                }
              },
              "required": ["continent"]
            }
            """
        );                

        return new McpServerFeatures.SyncToolSpecification(tool, (exchange, args) -> {

           String continent = args.get("continent").toString();
           Map<String, String> marsWeather = MarsLogic.getMarsWeather(continent);

          ObjectMapper objectMapper = new ObjectMapper();
           String result;
           try{
               result = objectMapper.writeValueAsString(marsWeather);
           } catch (Exception e) {
               result = "";
               e.printStackTrace();
           }
           McpSchema.Content content = new McpSchema.TextContent(result);

           return new McpSchema.CallToolResult(List.of(content), false);

        });
    }
```

コードの前半部分では、このツールがどのような機能を提供し、どのようなインターフェースを持つかをJSON形式で記述しています。
McpSchema.Tool の引数は各々、ツール名称、ツールの解説、ツールの入力定義を示しています。

AIは、このツール定義を参照することで、いつ、どのような場合にこのツールを呼び出すべきか、そして呼び出す際にどのような引数を渡すべきかを判断します。したがって、この定義はAIが外部の機能を利用するための重要な手がかりとなります。

コードの後半部分では、実際にMCPクライアントからこのツールが呼び出された際に実行される処理を、ラムダ式を用いて定義し、ツール定義を完成させています。

指定された大陸名に基づいて天気を返却する具体的なロジックは、MarsLogic#getMarsWeather メソッドに実装されています。このコード部分では、getMarsWeather から返されたロジックの結果を、MCPのレスポンスとして適切な形式に変換する処理が中心となります。

参考までに、getMarsWeather メソッドは、ここでは簡略化のため、switch 文を用いて入力された大陸名に対応する固定の気象データを返却するだけの処理となっています。実際の業務アプリケーションにおいては、この部分がより複雑になり、例えばデータベースから条件に合致するデータを抽出して返却するなどの処理が実装されることになるでしょう。

## 当記事のまとめ
MCPサーバーの実装は、その構造を一度理解してしまえば、決して難しいものではありません。
皆さんが担当されている業務システムにもMCPサーバーを組み込んで、AIの機能を拡張するようなユースケースを検討してみてください。

実際の挙動は[うごかす編](https://qiita.com/mi-ta/items/afd0dca9a665aeaa8201)で確認していきます。

## さらに詳しい情報をお探しの方へ
IBMの最新情報、イベント情報、さらに役立つ資料は、以下のIBM Communityでも発信・格納されています。
最新のトレンドや有益な情報をチェックするために、ぜひご覧ください！
- **WebSphere**: [IBM Community - WebSphere](https://community.ibm.com/community/user/wasdevops/communities/community-home?CommunityKey=d6c93aa2-6e10-48da-96dc-3831da8ee185)
WebSphere関連の最新情報やディスカッション、イベント情報、技術資料を公開中！
- **ELM (Engineering Lifecycle Management)**: [IBM Community - ELM](https://community.ibm.com/community/user/sustainability/communities/community-home?CommunityKey=f8bc69e0-9d61-4688-939b-018f5f74519e)
ELMに関する最新のイベント情報、ナレッジ共有、便利なドキュメントをチェック！
- **Integration**: [IBM Community - Integration](https://community.ibm.com/community/user/integration/communities/globalgrouphome?CommunityKey=fe1b09d3-7042-4a5f-bce0-6bcc19aa0ed9)
Integrationに関する最新のイベント情報、ナレッジ共有、便利なドキュメントをチェック！