4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

既存のRESTアプリを無理やりLLM連携する話

Last updated at Posted at 2025-12-23

はじめに

「自作のJakarta EEアプリとLLMを連携させたい」と考えたとき、まず想像するのは以下のような構成かと思います。

image.png

上記の例で、アプリケーションサーバーにSpringBootを使用している場合は modelcontextprotocol.io にMCPサーバーの実装例も紹介されているため、比較的簡単にアプリに組み込めます。またOpen Libertyを使用している場合もmi-ta氏の解説記事を参考に実装できそうです。

ただ、いずれもアプリケーションサーバーに由来する独自のAPIを使用しているようで、Eclipse GlassFishのような他のアプリケーションサーバーを使用している場合は以下のような対応が必要になります。

筆者もどうせやるならと下2つにトライしてみたのですが、通信まわりの処理の実装がどうも難しい…。これはMCPやSSEにもう少し詳しくなったら再挑戦したいと考えていますが、ここでふと下記のようなことを考えました。

「わざわざサーバー側にLLM連携のためにMCPサーバーを作りこまなくても、AIアプリ側にRESTクライアントを組み込めれば、RESTエンドポイント経由でアプリの情報取ったり操作したりできるのでは?」

RESTエンドポイントを持ちHTTPリクエストで情報を取ってきたり操作したりできるサービスなど世の中にありふれているため既に1億人くらい同じこと考えて実践していそうな話ではありますが、この記事は前述のようなサーバー側のJakarta EEアプリにMCPサーバーを組み込む方法ではなく、下図のようにAIアプリ側にRESTクライアントとして動作するMCPサーバーを組み込む方法で、既存アプリに一切手を加えずに無理やりLLM連携させてみたというお話になります。
image.png

用意したアプリ

  • simple-login-app : RESTエンドポイントを持つ簡易のWebアプリのプロジェクト (war)
  • simple-login-app-client : AIアプリに組み込むMCPサーバーのプロジェクト (jar)

Webアプリ(simple-login-app)の中身

Eclipse GlassFish上に配備の後、ブラウザからテキストボックスに名前を入れ「ログイン」ボタンを押すと、Helloメッセージが表示され、ログイン記録がcsvファイルに書き出されるだけのもの。

起動手順

  1. https://adoptium.net/temurin/releases/?version=21からEclipse Temurin JDK 21をダウンロードして適当なパスに展開
  2. 環境変数JAVA_HOMEに上記のJDKのパスを設定
  3. https://glassfish.org/download_gf7.html からEclipse GlassFish 7.1.0, Jakarta EE Platform, 10 (zip)を選択してダウンロードして適当なパスに展開
  4. simple-login-app内のmvnwを実行、target下に作成されるsimple-login-app.warファイルを、<GlassFishの展開パス>\glassfish7\glassfish\domains\domain1\autodeploy下に配置
  5. <GlassFishの展開パス>\glassfish7\glassfish\bin\asadmin start-domainを実行し、GlassFishを起動

ブラウザからhttp://localhost:8080/simple-login-app/にアクセスすると、以下のような画面が表示されます

image.png

名前を入れて「ログイン」ボタンを押すと、記録が以下のようにcsvファイルに出力されます

2025-12-22 00:26:33,Hiroki
2025-12-22 00:26:38,Rebecca
2025-12-22 00:29:11,Alan
2025-12-22 00:54:43,Hiroki

また、上記の記録をもとに情報を取得可能ないくつかのRESTエンドポイントを用意しています。(これらを後のMCPサーバー実装で仮想的にAIツール化してAIアプリから参照します)

パス
/api/login/history ログイン記録のリストを返却
/api/login/last 最後にログイン情報を返却
/api/login/count 誰が何回ログインしているかの情報を返却
/api/login/most-frequent 最も頻繫にログインした人の名前とその回数を返却
/api/login/statistics 雑多な統計情報を返却
/api/login/user/{name} 特定のユーザーのログイン記録を返却

MCPサーバー(simple-login-app-client)の中身

MCPサーバーは面倒な処理をだいたいmodelcontextprotocol.ioが公開しているSDKが隠蔽してくれているので、下記3つのクラスを作るだけ

MyMcpServerMainクラス

MCPサーバーをkickするためのMainクラス

public class MyMcpServerMain {
    public static void main(String[] args) {
        // JSON処理に使用するObjectMapperのラッパー。起動時にクラスローダーから使えるJSON-B実装(?)を探してくれるよう
        var jsonMapper = McpJsonMapper.getDefault();
        // 通信層のプロバイダ(今回はSTDIO用)を作成
        var transportProvider = new StdioServerTransportProvider(jsonMapper);
        // 同期型のMCPサーバーを起動(実装例に従って変数に格納しているが、必須なのだろうか...)
        var syncServer = MyMcpServer.syncServer(transportProvider);
    }
}

MyMcpServerクラス

MCPサーバーのオブジェクト。これも小さいクラス

public class MyMcpServer {
    // AIツール化したい操作リストを予めstatic finalで固めたが、
    // AIアプリからのリクエストごとにサーバーを起動しているわけでもなさそうなので
    // 後のsyncServer()内で作ってもよいかもしれない
    private static final List<McpServerFeatures.SyncToolSpecification>
            MY_MCP_TOOLS = MyMcpTools.syncToolSpecifications();
  // 同期型のMCPサーバーをビルド。ビルダーでちょこちょこ設定を追加していく形なのでとても簡単
    public static McpSyncServer syncServer(McpServerTransportProvider transportProvider) {
        return McpServer.sync(transportProvider)
                .serverInfo("simplelogin-mcp-server", "0.0.1")
                .capabilities(McpSchema.ServerCapabilities.builder()
                        .resources(true, false)
                        .tools(true)
                        .prompts(false)
                        .build())
                .tools(MY_MCP_TOOLS)
                .build();
    }
}

MyMcpToolsクラス

RESTクライアントをエンドポイントごとに個別のAIツールとして実装するクラス。ここが肝。最終的にAIツールの実装リストをList<SyncToolSpecification>としてMCPサーバーに渡します

public class MyMcpTools {
    private static final String SERVER_ENDPOINT = System.getProperty("server.endpoint", "");
    private static final McpJsonMapper jsonMapper = McpJsonMapper.getDefault();
    // ここでリストにまとめたAIツールは最終的にMCPサーバービルド時に渡す
    public static List<SyncToolSpecification> syncToolSpecifications() {
        return List.of(getLoginHistory(), getLastLogin(),
                getLoginCount(), getMostFrequentUser(),
                getStatistics(), getUserHistory());
    }

ツールの定義例(前述の/api/login/user/{name}エンドポイントをAIツール化する例):

    private static SyncToolSpecification getUserHistory() {
        return SyncToolSpecification.builder()
                .tool(Tool.builder()
                        // AIアプリがツールを特定するための名前と説明。本来はちゃんと目的や得られる情報を含めるべきと思われる
                        .name("get-user-login-history")
                        .description("get user login history")
                        // AIアプリがツールに対して行うリクエストのJSONスキーマ。APIの変数名からだいたいの用途は想像可能
                        // 少し古いバージョンではStringで直接スキーマを流し込めたようだが、本記事作成時点で最新の0.17.0ではオブジェクト定義になっている
                        .inputSchema(new McpSchema.JsonSchema(
                                "object",
                                Map.of("userName", String.class),
                                List.of("userName"),
                                null, null, null
                        ))
                        .build())
                // AIツールの実装を登録
                .callHandler(MyMcpTools::getUserHistory)
                .build();
    }
    // AIツールの実装本体。
    private static McpSchema.CallToolResult getUserHistory(McpSyncServerExchange exchange, CallToolRequest request) {
      // AIアプリからのリクエストに含まれるパラメータ要素を取得
        String userName = (String) request.arguments().get("userName");
        try {
            String encodedUserName = URLEncoder.encode(userName, StandardCharsets.UTF_8).replace("+", "%20");
            // RESTエンドポイントにHTTPリクエストを送信し、Stringに格納
            String result = httpRequest(SERVER_ENDPOINT + "/api/login/user/" + encodedUserName);
            var content = jsonMapper.writeValueAsString(result);
            return CallToolResult.builder().addTextContent(content).build();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    // 指定のエンドポイントにGETリクエストを送るHTTPクライアント
    private static String httpRequest(String endpoint) {
        try (HttpClient client = HttpClient.newHttpClient()) {
            HttpRequest req = HttpRequest.newBuilder()
                    .uri(URI.create(endpoint))
                    .GET()
                    .build();
            HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
            return res.body();
        } catch (IOException | InterruptedException ex) {
            throw new RuntimeException(ex);
        }
    }

ちゃんと起動できるかの確認

javaでjarを起動した後、コンソールが待機状態になれば起動成功です。(inspectorを使う方法もあるのですが、筆者は上手く使いこなせませんでした...)

java -Dserver.endpoint=http://localhost:8080/simple-login-app -jar <simple-login-appプロジェクトを展開したパス>\simple-login-app-client\target\simple-login-app.jar

Claude for DesktopでLLM連携

Claude for Desktopをダウンロード&インストールし、設定ファイル(%APPDATA%\Claude\claude_desktop_config.json)に以下の要領で定義を追加してClaudeを再起動すると有効になります(Claudeを終了しても何故かプロセスが残る場合があるようで、そのままでは再起動しても設定が更新されないので、その場合はタスクマネージャーなどから強制停止します)

  {
+   "mcpServers": {
+     "login-server": {
+       "command": "java",
+       "args": [
+          "-Dserver.endpoint=http://localhost:8080/simple-login-app",
+          "-jar",
+          "<simple-login-appをcloneしたパス>\\simple-login-app-client\\target\\simple-login-app.jar"
+       ]
+     }
+   },
    "preferences": {
      "menuBarEnabled": false,
      "legacyQuickEntryEnabled": false
    }
  }

上記の設定ファイルでMCPサーバー名に指定した"login-server"をキーワードに、チャットで情報を聞き出してみると..

image.png

image.png

無事AIツールの定義越しにRESTリクエストが発行されてアプリから情報が取れてそうですね

最後に

既にRESTでサービスを提供しているアプリなら、本記事で紹介した方法であれば最低3つクラスを用意するだけでLLMから呼び出せるようになるため、簡単に"なんちゃってLLM連携"が実現可能です。(ToolオブジェクトをAIアプリに許可する要求ぶん生成しないといけないのが若干面倒ですが、それはどこにMCPサーバーを用意する場合でも似たようなものなので仕方ない...)
HTTPのクライアント部分も、今回は標準APIで雑に書きましたが、Apache Commons HttpClientなどを使用すれば実用的な実装も簡単にできると思います。
modelcontextprotocol.ioが公開しているSDKも今回初めて触ったのですが、STDIOの同期処理を書くぶんには特に迷うこともなくサクサク作れたので今後もAIツール作りが捗りそうです。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?