S-JIS[2025-03-30/2025-05-21] 変更履歴

Java MCPサーバー(STDIO)の実装例

Model Context ProtocolJava SDKでSTDIOのMCPサーバーを作る例。


概要

java-sdk 0.8を使って、STDIOでMCPサーバーを実装してみる。

STDIOは、MCPクライアントとの通信を(HTTPでなく)標準入出力で行う方式。
なので、ログ出力をしたい場合は標準エラーに出力しなければならない。(System.out.println()で標準出力に出力すると、MCPクライアントが通信データとして拾ってしまい、エラーになる)
(slf4j-simpleの場合、ログメッセージを標準エラーに出してくれるらしい)


ここでは、Hogeのデータを扱うMCPサーバーを作ってみる。[2025-05-21]
Hogeとはなんじゃい、と言うと、ファイルシステムのファイルやデータベースのテーブルのようなものだと思ってくれればいい。
すなわち、Hoge名はファイル名やテーブル名、Hoge一覧はファイル一覧やテーブル一覧といった感じ。
Hogeのデータは適当なテキスト。

実装する機能は以下のもの。

これらは全て必要というわけではない。最小限で試したいならどれかひとつ実装すればいい。


build.gradle

Gradle 8.13のbuild.gradleの例。

plugins {
    id 'java'
    id 'application'
    id 'io.github.goooler.shadow' version '8.1.8'
}

group = 'com.example.mcp'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.withType(JavaCompile) { task ->
    task.options.encoding = 'UTF-8'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'io.modelcontextprotocol.sdk:mcp:0.8.1'
    implementation "org.slf4j:slf4j-simple:2.0.7"

    testImplementation 'org.junit.jupiter:junit-jupiter'
    testImplementation 'io.modelcontextprotocol.sdk:mcp-test:0.8.1'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

application {
    applicationName = 'hoge-mcp-server'
    mainClass = 'com.example.mcp.HogeMcpMain'
}

shadowJar {
    archiveBaseName = 'hoge-mcp-server'
    archiveClassifier = 'all'
    mergeServiceFiles()
}

MCPのSDKはJava17でコンパイルされているようなので、JavaのバージョンにはJava17以降を指定する。[2025-04-19]

出来上がったMCPサーバーはjavaコマンドで実行できる必要がある。
依存ライブラリーのクラスパスを列挙するのは面倒なため、依存ライブラリーを全て含んだfatJarをshadowJarで作成するようにしてある。

ビルド方法


mainメソッド

HogeMcpMain.java:

package com.example.mcp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
public class HogeMcpMain {
    private static final Logger LOG = LoggerFactory.getLogger(HogeMcpMain.class);

    public static void main(String[] args) {
        LOG.info("HogeMcpMain start");

        McpServerTransportProvider transportProvider = new StdioServerTransportProvider(new ObjectMapper());
        McpSyncServer syncServer = HogeMcpServer.syncServer(transportProvider);
        LOG.info("serverInfo={}", syncServer.getServerInfo());
    }
}

java-sdk 0.8のSTDIOでは、StdioServerTransportProviderを使ってMCPサーバーを構築する。

MCPサーバーは、別スレッドでMCPクライアントからの要求待ちを開始するようだ。
MCPサーバーを起動したらmainメソッドは終了してしまう。
(受信スレッドが生きているので、アプリケーション自体は終了しない)


MCPサーバー

MCPクライアントへ返す情報を管理するサーバーを構築する部分。

(McpServerTransportProviderを使うのは、java-sdk 0.8.0で変更された仕様らしい)

HogeMcpServer.java:

package com.example.mcp;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
public class HogeMcpServer {

    public static McpSyncServer syncServer(McpServerTransportProvider transportProvider) {
        var capabilities = McpSchema.ServerCapabilities.builder()
            .resources(false, false)
            .tools(false)
            .prompts(false)
//          .logging()
            .build();

        var syncServer = McpServer.sync(transportProvider)
            .serverInfo("hoge-mcp-server", "0.0.1")
            .capabilities(capabilities)
            .resources(HogeMcpResource.syncResourceSpecification())
            .tools(HogeMcpTool.syncToolSpecification())
            .prompts(HogeMcpPrompt.syncPromptSpecification())
            .build();
        return syncServer;
    }
}

capabilitiesで、自作MCPサーバーがリソース・ツール・プロンプトをクライアントに返せるかどうかを指定する。

引数のlistChangedは、基本的にfalseでいいと思う。[2025-05-21]
MCPサーバーには、リソースやツール・プロンプトに追加削除があった場合、MCPクライアントに通知する機能があるようだ。listChangedはその通知を行うかどうかのフラグだと思う。
リソースはともかく、ツールやプロンプトは、普通は増減しないだろうから。

capabilitiesでツールを返すように指定したら、McpServerのtools()でツールを定義する。
同様に、capabilitiesでリソースを返すように指定したら、McpServerのresources()でリソースを定義する。


リソース

MCPリソースはデータそのもの。[2025-05-13]

Claude Desktopの場合、リソースを読み込ませて、そのリソースに対する指示を行うことが出来る。

package com.example.mcp;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;

import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
public class HogeMcpResource {

    private static List<String> HOGE_NAMES = List.of("abc", "def", "ghi");

    public static List<SyncResourceSpecification> syncResourceSpecification() {
        var list = new ArrayList<SyncResourceSpecification>();
        for (String hogeName : HOGE_NAMES) {
            list.add(createResource(hogeName));
        }
        return list;
    }

    private static SyncResourceSpecification createResource(String hogeName) {
        var resource = new McpSchema.Resource( //
                "hoge://%s".formatted(hogeName), // uri
                hogeName, // name
                "%s data".formatted(hogeName), // description
                "text/plain", // mimeType
                null // annotations
        );
        return new SyncResourceSpecification(resource, HogeMcpResource::getHogeData);
    }

    static ReadResourceResult getHogeData(McpSyncServerExchange exchange, ReadResourceRequest request) {
        var uri = URI.create(request.uri());
        String hogeName = uri.getAuthority();

        String result = switch (hogeName) {
        case "abc" -> """
                aaa
                bbb
                ccc
                """;
        case "def" -> """
                dddd
                eee
                ff
                """;
        case "ghi" -> """
                g1
                g2
                h1
                h2
                i0
                """;
        default -> throw new RuntimeException("not found resource. hogeName=" + hogeName);
        };

        var content = new TextResourceContents(request.uri(), "text/plain", result);
        return new ReadResourceResult(List.of(content));
    }
}

McpSchema.Resourceでリソースを定義する。

リソースを扱うURIを決め、リソース名やdescriptionを定義する。
URIのスキーマ名は独自に定義して良い。

リソース本体のデータを返す関数はReadResourceRequestを受け取るが、この中に対象リソースのURIが入ってくる。

上記の例だと、「hoge://abc」「hoge://def」「hoge://ghi」という3つのリソース(URI)を作ってMCPクライアントに返している。
MCPクライアントはこのURIを指定してリソースのデータを取得しようとする(用意しておいたメソッドが呼ばれる)ので、URIに応じたデータを返す。

Claude Desktopでリソースを読む例


ツール

MCPツールは、LLM(いわゆるAI)から呼ばれて処理を行う関数。

HogeMcpTool.java:

package com.example.mcp;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
public class HogeMcpTool {
    private static final Logger LOG = LoggerFactory.getLogger(HogeMcpTool.class);

    private static final ObjectMapper objectMapper = new ObjectMapper();
    public static List<SyncToolSpecification> syncToolSpecification() {
        return List.of(listHogeTool(), getHogeTool());
    }
    // Hoge一覧を返すツール(引数なし)

    private static SyncToolSpecification listHogeTool() {
        var tool = new McpSchema.Tool(
            "list-hoge", // tool name
            "list hoge names", // description
            new McpSchema.JsonSchema(
                "object", // type
                Map.of(), // properties
                List.of(), // required property
                null // additionalProperties
            ));
        return new SyncToolSpecification(tool, HogeMcpTool::listHoge);
    }

    private static CallToolResult listHoge(McpSyncServerExchange exchange, Map<String, Object> arguments) {
        try {
            var result = listHogeMain();

            String text = objectMapper.writeValueAsString(result);
            var content = new McpSchema.TextContent(text);
            return new CallToolResult(List.of(content), false);
        } catch (RuntimeException e) {
            LOG.warn("listHoge error", e);
            throw e;
        } catch (Exception e) {
            LOG.warn("listHoge error", e);
            throw new RuntimeException(e);
        }
    }

    record ListHogeResult(String hogeName) {
    }

    private static List<String> HOGE_NAMES = List.of("abc", "def", "ghi");

    static List<ListHogeResult> listHogeMain() {
        return HOGE_NAMES.stream().map(ListHogeResult::new).toList();
    }
    // Hogeデータを取得するツール(引数はHoge名)

    static final String HOGE_NAME = "hogeName";

    private static SyncToolSpecification getHogeTool() {
        var propertyList = List.of(
            new ToolProperty(HOGE_NAME, new ToolPropertyBody("string", "hoge name"), true)
        );
        Map<String, Object> propertyMap = propertyList.stream()
            .collect(Collectors.toMap(ToolProperty::name, p -> (Object) p));

        var tool = new McpSchema.Tool(
            "get-hoge", // tool name
            "get hoge data", // description
            new McpSchema.JsonSchema(
                "object", // type
                propertyMap, // properties
                List.of(HOGE_NAME), // required property
                null // additionalProperties
            ));
        return new SyncToolSpecification(tool, HogeMcpTool::getHoge);
    }

    private static CallToolResult getHoge(McpSyncServerExchange exchange, Map<String, Object> arguments) {
        try {
            String result = getHogeMain(arguments);

            var content = new McpSchema.TextContent(result);
            return new CallToolResult(List.of(content), false);
        } catch (RuntimeException e) {
            LOG.warn("getHoge error", e);
            throw e;
        } catch (Exception e) {
            LOG.warn("getHoge error", e);
            throw new RuntimeException(e);
        }
    }

    static String getHogeMain(Map<String, Object> arguments) {
        String hogeName = (String) arguments.get(HOGE_NAME);

        return switch (hogeName) {
        case "abc" -> """
            aaa
            bbb
            ccc
            """;
        case "def" -> """
            dddd
            eee
            ff
            """;
        case "ghi" -> """
            g1
            g2
            h1
            h2
            i0
            """;
        default -> throw new RuntimeException("not found hoge. hogeName=" + hogeName);
        };
    }
    record ToolProperty(String name, ToolPropertyBody body, boolean required) {
    }

    record ToolPropertyBody(String type, String description) {
    }
}

McpSchema.Toolでツール(MCPクライアントから呼ばれる関数)を定義する。

ツールは複数定義できるが、ツール名(関数名)は自作ツール内で被らない名前にする。
ツール名はcamelCaseでもハイフン「-」区切りでも良い。
Claude Desktopがユーザーに「MCPサーバーのツール(関数)の呼び出し許可」を求める際にこのツール名が表示されることがあるので、分かりやすい名前にしておくのが良さそう。

なにげにdescription(説明)は重要。
普通のアプリケーションならdescriptionは人間が読むものなので、ある程度適当でも良かった。なんならdescriptionが無くてもアプリケーションは動作した。
しかしLLM(いわゆるAI)では、descriptionの内容もLLMが読む。なので、むしろLLMに分かりやすい内容をdescriptionに記述しておくのが良さそう。
(対象のLLMが日本語を解するなら、descriptionは日本語で書いても良い)

McpSchema.JsonSchemaはツール(関数)の引数を定義する。

SyncToolSpecificationのコンストラクターの第2引数で、この関数の処理本体を指定する。
処理本体のメソッドのCallToolResultでTextContentを渡せば、テキスト(文字列)をクライアントに返すことが出来る。

上記のlistHoge()では、Hoge名の一覧を固定でHOGE_NAMESに定義しておき、それを返している。
例えばDBにアクセスするMCPサーバーなら、テーブル一覧を返すような関数になるだろう。

ちなみに、処理本体(xxxMain())がprivateでないのは、そのメソッドだけ呼び出してテストできるようにする為。

Claude Desktopでツールを実行する例


プロンプト

MCPプロンプトは、ユーザーがプロンプトを入力する代わりに使える定型的なプロンプトを返すもの。[2025-05-21]

HogeMcpPrompt.java:

package com.example.mcp;
import java.util.List;
import java.util.Map;

import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
public class HogeMcpPrompt {

    public static List<SyncPromptSpecification> syncPromptSpecification() {
        return List.of(assistantPrompt(), listPrompt(), showPrompt());
    }
    // LLMに対するプロンプト

    private static SyncPromptSpecification assistantPrompt() {
        var prompt = new McpSchema.Prompt(
            "assistant-prompt", // name
            "LLMに対するプロンプト", // description
            List.of() // arguments
        );
        return new SyncPromptSpecification(prompt, HogeMcpPrompt::assistant);
    }

    static GetPromptResult assistant(McpSyncServerExchange exchange, GetPromptRequest request) {
        String text = """
            あなたは執事のように振る舞ってください。
            ユーザーへの回答文の冒頭で、なるべく「お嬢様」と呼びかけるようにしてください。
            """;
        var content = new McpSchema.TextContent(text);
        var message = new PromptMessage(McpSchema.Role.ASSISTANT, content);
        return new GetPromptResult(
            "", // description
            List.of(message) // messages
        );
    }
    // Hoge一覧を表示させるプロンプト(引数なし)

    private static SyncPromptSpecification listPrompt() {
        var prompt = new McpSchema.Prompt(
            "list-hoge-prompt", // name
            "hoge名一覧を表示するプロンプト", // description
            List.of() // arguments
        );
        return new SyncPromptSpecification(prompt, HogeMcpPrompt::listHoge);
    }

    static GetPromptResult listHoge(McpSyncServerExchange exchange, GetPromptRequest request) {
        String text = """
            hoge一覧を表示してください。
            """;
        var content = new McpSchema.TextContent(text);
        var message = new PromptMessage(McpSchema.Role.USER, content);
        return new GetPromptResult(
            "hoge名の一覧を取得・表示する", // description
            List.of(message) // messages
        );
    }
    // Hogeデータを表示させるプロンプト(引数はHoge名)

    static final String HOGE_NAME = "hogeName";

    private static SyncPromptSpecification showPrompt() {
        var prompt = new McpSchema.Prompt(
            "show-hoge-prompt", // name
            "hogeの内容を表示するプロンプト", // description
            List.of( // arguments
                new PromptArgument(
                    HOGE_NAME, // name
                    "hoge名", // description
                    true) // required
        ));
        return new SyncPromptSpecification(prompt, HogeMcpPrompt::showHoge);
    }

    static GetPromptResult showHoge(McpSyncServerExchange exchange, GetPromptRequest request) {
        Map<String, Object> arguments = request.arguments();
        var hogeName = arguments.get(HOGE_NAME);

        String text = """
            hoge名 %s のデータを表示してください。
            """.formatted(hogeName);

        var content = new McpSchema.TextContent(text);
        var message = new PromptMessage(McpSchema.Role.USER, content);
        return new GetPromptResult(
            "%s のデータを取得・表示する".formatted(hogeName), // description
            List.of(message) // messages
        );
    }
}

McpSchema.Promptでプロンプトを定義する。
MCPプロンプトはいわばテンプレートのようなものだが、Hoge名などの具体的な値を埋めて、すぐに使えるプロンプトという形で返す。

PromptMessageのコンストラクターの第1引数でロール(役割)を決める。
USERはユーザーが入力するプロンプトの代わりとなるもの。
ASSISTANTはLLM(いわゆるAI)に対する指示となるもの。

Claude Desktopでプロンプトを読み込ませる例


ビルド方法

ビルド(fatJarの生成)は以下のコマンドで行う。

$ ./gradlew shadowJar

$ ls build/libs/
hoge-mcp-server-0.0.1-SNAPSHOT-all.jar

実行方法は以下のようなイメージ。

$ java -jar hoge-mcp-server-0.0.1-SNAPSHOT-all.jar

このようなコマンドをClaude Desktopclaude_desktop_config.jsonに(フルパスで)記述することになる。


Claude Desktopで試す例

Claude Desktopclaude_desktop_config.jsonに自作MCPサーバーの定義を追加する。

claude_desktop_config.json:

{
  "mcpServers": {
    "hoge-mcp-server": {
      "command": "C:/Program Files/Java/jdk-21/bin/java",
      "args": ["-jar", "C:/mcp-example/build/libs/hoge-mcp-server-0.0.1-SNAPSHOT-all.jar"]
    }
  }
}

この状態でClaude Desktopを起動すると、hoge-mcp-serverが実行される(はず)。

もしClaude Desktopを再起動するなら、メニューの「ファイル」→「終了」でバックグラウンドプロセスごと終了させてから起動する必要がある。


なお、System.out.println()で標準出力にメッセージを出力すると、Claude Desktop上にエラーメッセージが出る。[/2024-04-13]

MCP hoge-mcp-server: Unexpected token 'M', "McpExample"... is not valid JSON
MCP hoge-mcp-server: Unexpected token 'E', "HogeMcp"... is not valid JSON
MCP hoge-mcp-server: Unexpected token 'I', "Implementa"... is not valid JSON
MCP hoge-mcp-server: Unexpected token 'M', "McpExample"... is not valid JSON

エラーメッセージの内容が途中で途切れているので分かりづらいが、試しに「McpExample start」みたいなメッセージをSystem.out.println()で出していたので、それがClaude Desktopに拾われてこのようなエラーになったようだ。
(STDIOでは、MCPクライアントとMCPサーバー間の通信データは標準入出力で送受信する為)

issueがあったのでClaude Desktopのバグかと思ったんだけど、そうではなかった模様)

なお、このようなエラーは出ても、MCPサーバー自体は一応動作するようだ。


リソースの例

自作リソースを使う例。[2025-05-13]

  1. Claude Desktopのチャット欄の下にある「+」アイコンをクリックする。
  2. 「hoge-mcp-serverから追加」というメニューが出るので、それをクリックする。
  3. メニューに(プロンプト名と)リソース名の一覧が出るので、選択する。
  4. HogeMcpResource.getHogeData()が実行され、リソースのデータ本体がロードされる。(ファイルとして添付される形になる)
  5. チャット欄からこのリソースデータに対する質問を入力する。(「行数は?」とか)

ツールの例

自作ツールを実行する例。

「Hoge一覧を返すツール」を実行してみる。

  1. Claude Desktopのチャット欄に「hoge一覧を見せて」みたいに入力する。
  2. 「"hoge-mcp-server"(ローカル)からのツールを許可しますか?」 というようなダイアログが出るので、許可ボタンを押す。
  3. HogeMcpTool.listHoge()を実行した結果が表示される。

続けて「abcのデータを見せて」(abcはHoge名)のように入力すれば、HogeMcpTool.getHoge()が実行され、返されたデータが表示される。


プロンプトの例

自作プロンプトを読み込む例。[2025-05-21]

アシスタントのプロンプト(LLM向けの指示)を適用させてみる。

  1. Claude Desktopのチャット欄の下にある「+」アイコンをクリックする。
  2. 「hoge-mcp-serverから追加」というメニューが出るので、それをクリックする。
  3. メニューにプロンプト名(とリソース名)の一覧が出るので、「assistant-prompt」を選択する。
  4. HogeMcpPrompt.assistant()が実行され、 プロンプトがロードされる。(ファイルとして添付される形になる)
  5. チャット入力欄の送信ボタン(「↑」アイコン)を押すと、添付ファイルの内容(プロンプト)が実行される。

今回の例だと、以下のような回答が表示された(笑)
「こんにちは、お嬢様。執事として喜んでお手伝いさせていただきます。何かご質問やご要望がございましたら、どうぞお気軽にお申し付けください。今日は何をお手伝いできますでしょうか?」


java-sdkへ戻る / MCPへ戻る / 技術メモへ戻る
メールの送信先:ひしだま