0
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?

【MCP】Spring AIのサンプルプロジェクトを参考にMCPサーバを作成する

Last updated at Posted at 2025-04-17

初めに

流行りのMCPサーバを構築したかったのですが、普段書いてるPHPのSDKはありませんでした。
代わりに、Spring AIのサンプルプロジェクトを参考作成してみたので、備忘としてまとめます。

参考にしたサンプルプロジェクト

サンプルプロジェクトはこちら。

公式ドキュメントはこちら。

作成するMCPサーバの種類

MCPサーバにはLLMに提供する情報の違いによっていくつか種類があります(参考

MCPサーバの種類 詳細
Resources LLM Hostに提供するドキュメントなどの情報
Tool APIなど、LLM Hostが実行できるツール
Prompts LLMとの会話テンプレート

また、Spring AIを用いてMCPサーバには、クライアントとサーバ間の情報の受け渡し方にもいくつか種類があります(参考

情報の受け渡し方法 詳細
STDIO 標準入出力を用いたデータの受け渡し
Spring MVC Spring MVCを用いたServer-Sent Events (SSE) 通信
Spring WebFlux 同じくSSEを用いるが、非同期通信

今回は一番簡単そうなToolSTDIOで実装しました。

環境

Java: 21
gradle: 8.13
SpringBoot: 3.4.4

MCPサーバ構築する

それでは構築していきましょう。
今回作成するMCPサーバは「アメリカの気象情報を取得する」MCPサーバです。サンプルプロジェクトのものをそのまま利用しました。

アメリカの国立気象局(NWS: National Weather Service)が提供している天気予報APIを呼び出しています。

0. ディレクトリ構成

今回のディレクトリ構成です。
元となるプロジェクトはSpring Initializrを使って構築しています。
DependenciesにSpring Webを追加しました。

ディレクトリ構成
Project
├── src/
|   ├── main/
│   ├── java/
│   │   └── com/mcp/weather/
│   │       ├── service/
|   |       |   └── WeatherService.java  #ツールを定義するクラス
│   │       └── McpApplication.java      #メインクラス
│   └── resources/
│       └── application.properties       #設定
└── build.gradle

1. build.gradle

Spring AIを追加します。
Spring Initializrでは指定できなかったので、ここで直書きしました。

サンプルプロジェクトはmavenなので、ここが唯一の違いかも。

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.4.4'
	id 'io.spring.dependency-management' version '1.1.7'
}

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

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

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.ai:spring-ai-mcp-server-spring-boot-starter:1.0.0-M6'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

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

2. McpApplication.java

メインクラスです。
ToolCallbackProviderを返すメソッドを定義します。

ToolCallbackProviderはMCPが利用できるツールを定義するためのクラスのようです。
後述するServiceクラスではツールを定義しており、その一つ一つを定義するみたいですね。

package com.mcp.weather;

import com.mcp.weather.service.WeatherService;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class McpApplication {

	public static void main(String[] args) {
		SpringApplication.run(McpApplication.class, args);
	}

	@Bean
	public ToolCallbackProvider weatherTools(WeatherService weatherService) {
		return  MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
	}
}

3. WeatherService.java

ツールを定義するクラスです。
長いので一部抜粋です。

@ToolをつけることでMCPにツールと認識させることができます。
descriptionはツールの説明です。LLMはここを見て呼び出すAPIを決定します。

package com.mcp.weather.service;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;

@Service
public class WeatherService {

    // 長いため一部抜粋

    /**
     * 天気を取得するツール
     * 
     * @param latitude Latitude
     * @param longitude Longitude
     * @return The forecast for the given location
     * @throws RestClientException if the request fails
     */
    @Tool(description = "Get weather forecast for a specific latitude/longitude")
    public String getWeatherForecastByLocation(double latitude, double longitude) {

        var points = restClient.get()
                .uri("/points/{latitude},{longitude}", latitude, longitude)
                .retrieve()
                .body(Points.class);

        var forecast = restClient.get().uri(points.properties().forecast()).retrieve().body(Forecast.class);

        String forecastText = forecast.properties().periods().stream().map(p -> {
            return String.format("""
					%s:
					Temperature: %s %s
					Wind: %s %s
					Forecast: %s
					""", p.name(), p.temperature(), p.temperatureUnit(), p.windSpeed(), p.windDirection(),
                    p.detailedForecast());
        }).collect(Collectors.joining());

        return forecastText;
    }

    /**
     * アラート情報を取得するツール
     * 
     * @param state Area code. Two-letter US state code (e.g. CA, NY)
     * @return Human readable alert information
     * @throws RestClientException if the request fails
     */
    @Tool(description = "Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY)")
    public String getAlerts(@ToolParam( description =  "Two-letter US state code (e.g. CA, NY") String state) {
        Alert alert = restClient.get().uri("/alerts/active/area/{state}", state).retrieve().body(Alert.class);

        return alert.features()
                .stream()
                .map(f -> String.format("""
					Event: %s
					Area: %s
					Severity: %s
					Description: %s
					Instructions: %s
					""", f.properties().event(), f.properties.areaDesc(), f.properties.severity(),
                        f.properties.description(), f.properties.instruction()))
                .collect(Collectors.joining("\n"));
    }
WeatherService.javaの全量はこちら
package com.mcp.weather.service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;

@Service
public class WeatherService {

    private static final String BASE_URL = "https://api.weather.gov";

    private final RestClient restClient;

    public WeatherService() {

        this.restClient = RestClient.builder()
                .baseUrl(BASE_URL)
                .defaultHeader("Accept", "application/geo+json")
                .defaultHeader("User-Agent", "WeatherApiClient/1.0 (your@email.com)")
                .build();
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    public record Points(@JsonProperty("properties") Props properties) {
        @JsonIgnoreProperties(ignoreUnknown = true)
        public record Props(@JsonProperty("forecast") String forecast) {
        }
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    public record Forecast(@JsonProperty("properties") Props properties) {
        @JsonIgnoreProperties(ignoreUnknown = true)
        public record Props(@JsonProperty("periods") List<Period> periods) {
        }

        @JsonIgnoreProperties(ignoreUnknown = true)
        public record Period(@JsonProperty("number") Integer number, @JsonProperty("name") String name,
                             @JsonProperty("startTime") String startTime, @JsonProperty("endTime") String endTime,
                             @JsonProperty("isDaytime") Boolean isDayTime, @JsonProperty("temperature") Integer temperature,
                             @JsonProperty("temperatureUnit") String temperatureUnit,
                             @JsonProperty("temperatureTrend") String temperatureTrend,
                             @JsonProperty("probabilityOfPrecipitation") Map probabilityOfPrecipitation,
                             @JsonProperty("windSpeed") String windSpeed, @JsonProperty("windDirection") String windDirection,
                             @JsonProperty("icon") String icon, @JsonProperty("shortForecast") String shortForecast,
                             @JsonProperty("detailedForecast") String detailedForecast) {
        }
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    public record Alert(@JsonProperty("features") List<Feature> features) {

        @JsonIgnoreProperties(ignoreUnknown = true)
        public record Feature(@JsonProperty("properties") Properties properties) {
        }

        @JsonIgnoreProperties(ignoreUnknown = true)
        public record Properties(@JsonProperty("event") String event, @JsonProperty("areaDesc") String areaDesc,
                                 @JsonProperty("severity") String severity, @JsonProperty("description") String description,
                                 @JsonProperty("instruction") String instruction) {
        }
    }

    /**
     * Get forecast for a specific latitude/longitude
     * @param latitude Latitude
     * @param longitude Longitude
     * @return The forecast for the given location
     * @throws RestClientException if the request fails
     */
    @Tool(description = "Get weather forecast for a specific latitude/longitude")
    public String getWeatherForecastByLocation(double latitude, double longitude) {

        var points = restClient.get()
                .uri("/points/{latitude},{longitude}", latitude, longitude)
                .retrieve()
                .body(Points.class);

        var forecast = restClient.get().uri(points.properties().forecast()).retrieve().body(Forecast.class);

        String forecastText = forecast.properties().periods().stream().map(p -> {
            return String.format("""
					%s:
					Temperature: %s %s
					Wind: %s %s
					Forecast: %s
					""", p.name(), p.temperature(), p.temperatureUnit(), p.windSpeed(), p.windDirection(),
                    p.detailedForecast());
        }).collect(Collectors.joining());

        return forecastText;
    }

    /**
     * Get alerts for a specific area
     * @param state Area code. Two-letter US state code (e.g. CA, NY)
     * @return Human readable alert information
     * @throws RestClientException if the request fails
     */
    @Tool(description = "Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY)")
    public String getAlerts(@ToolParam( description =  "Two-letter US state code (e.g. CA, NY") String state) {
        Alert alert = restClient.get().uri("/alerts/active/area/{state}", state).retrieve().body(Alert.class);

        return alert.features()
                .stream()
                .map(f -> String.format("""
					Event: %s
					Area: %s
					Severity: %s
					Description: %s
					Instructions: %s
					""", f.properties().event(), f.properties.areaDesc(), f.properties.severity(),
                        f.properties.description(), f.properties.instruction()))
                .collect(Collectors.joining("\n"));
    }

    public static void main(String[] args) {
        WeatherService client = new WeatherService();
        System.out.println(client.getWeatherForecastByLocation(47.6062, -122.3321));
        System.out.println(client.getAlerts("LA"));
    }
}

4. application.properties

アプリケーションプロパティの設定です。
spring.ai.mcp.server.stdio=trueでSTDIOを指定しています。
各設定値は公式ドキュメントをご参照ください。

application.properties
spring.application.name=mcp-weather

# Required STDIO Configuration
spring.main.web-application-type=none
spring.main.banner-mode=off
logging.pattern.console=

# Server Configuration
spring.ai.mcp.server.enabled=true
spring.ai.mcp.server.stdio=true
spring.ai.mcp.server.name=my-weather-server
spring.ai.mcp.server.version=0.0.1
# SYNC or ASYNC
spring.ai.mcp.server.type=SYNC
spring.ai.mcp.server.resource-change-notification=true
spring.ai.mcp.server.tool-change-notification=true
spring.ai.mcp.server.prompt-change-notification=true

# Optional file logging
logging.file.name=mcp-weather-stdio-server.log

これでOKです。
buildしてjarファイルを作成します。

Claude Desktopから呼び出す

作成したMCPサーバを呼び出してみましょう。
Claudeの設定ファイルに以下のように記述しました。

設定ファイル
{
    "mcpServers": {
      "weather": {
        "command": "java",
          "args": [
          "-jar",
          "C:\\Path\\to\\Project\\build\\libs\\mcp-weather-0.0.1-SNAPSHOT.jar"
        ]
      }
    }
}

再起動してみたところ、無事にMCPサーバが起動しました!

image.png

終わりに

今回はSpring AIのサンプルプロジェクトを参考にMCPサーバを作成してみました。
サクッと実装できてお手軽でよかったです。
MCPサーバを提供できるとできることが広がるので、いろいろ活用していきたいですね。

ここまでご覧いただきありがとうございました!

0
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
0
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?