初めに
流行りの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を用いるが、非同期通信 |
今回は一番簡単そうなToolとSTDIOで実装しました。
環境
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を指定しています。
各設定値は公式ドキュメントをご参照ください。
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サーバが起動しました!
終わりに
今回はSpring AIのサンプルプロジェクトを参考にMCPサーバを作成してみました。
サクッと実装できてお手軽でよかったです。
MCPサーバを提供できるとできることが広がるので、いろいろ活用していきたいですね。
ここまでご覧いただきありがとうございました!