このチュートリアルでは、Spring Boot と MCP Java SDK を使用してタスク管理ツールを公開するモデル コンテキスト プロトコル ( MCP) サーバーを構築します。 サーバーを Azure Container Apps にデプロイし、VS Code の GitHub Copilot Chat からサーバーに接続します。
このチュートリアルでは、次の操作を行います。
- MCP ツールを公開する Spring Boot アプリを作成する
- GitHub Copilot を使用して MCP サーバーをローカルでテストする
- アプリをコンテナー化して Azure Container Apps にデプロイする
- デプロイされた MCP サーバーに GitHub Copilot を接続する
[前提条件]
- アクティブなサブスクリプションを持つ Azure アカウント。 無料で作成できます。
- Azure CLI バージョン 2.62.0 以降。
- Java 17 以降。
- Maven 3.8 以降。
- GitHub Copilot 拡張機能を含む Visual Studio Code。
- Docker Desktop (省略可能 - コンテナーをローカルでテストするためにのみ必要)。
アプリの雛形を作成する
このセクションでは、MCP Java SDK を使用して新しい Spring Boot プロジェクトを作成します。
プロジェクト ディレクトリを作成します。
mkdir tasks-mcp-server && cd tasks-mcp-serverpom.xmlを作成します。<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.0</version> </parent> <groupId>com.example</groupId> <artifactId>tasks-mcp-server</artifactId> <version>1.0.0</version> <name>tasks-mcp-server</name> <description>MCP server for task management on Azure Container Apps</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-spring-webmvc</artifactId> <version>0.10.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>pom.xmlは、Web フレームワークのspring-boot-starter-webと MCP SDK のmcp-spring-webmvcという 2 つの主要な依存関係を持つ Spring Boot アプリケーションを定義します。 Spring Boot Maven プラグインは、実行可能 JAR としてアプリをパッケージします。注
MCP Java SDK は、開発中です。 MCP Java SDK リリースで最新バージョンを確認し、それに応じて
<version>を更新します。ディレクトリ構造を作成します。
mkdir -p src/main/java/com/example/tasksmcp mkdir -p src/main/resourcessrc/main/resources/application.propertiesを作成します。server.port=8080
データ モデルを定義して格納する
このセクションでは、タスク データ モデルとメモリ内ストアを定義します。
src/main/java/com/example/tasksmcp/TaskItem.javaを作成します。package com.example.tasksmcp; import java.time.Instant; public class TaskItem { private int id; private String title; private String description; private boolean isComplete; private Instant createdAt; public TaskItem(int id, String title, String description, boolean isComplete) { this.id = id; this.title = title; this.description = description; this.isComplete = isComplete; this.createdAt = Instant.now(); } // Getters and setters public int getId() { return id; } public String getTitle() { return title; } public String getDescription() { return description; } public boolean isComplete() { return isComplete; } public void setComplete(boolean complete) { isComplete = complete; } public Instant getCreatedAt() { return createdAt; } }TaskItemクラスは、標準ゲッターと完了状態のセッターを使用してデータ モデルを定義します。 コンストラクターは、createdAtタイムスタンプを自動的に初期化します。src/main/java/com/example/tasksmcp/TaskStore.javaを作成します。package com.example.tasksmcp; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; @Component public class TaskStore { private final List<TaskItem> tasks = new ArrayList<>(); private final AtomicInteger nextId = new AtomicInteger(3); public TaskStore() { tasks.add(new TaskItem(1, "Buy groceries", "Milk, eggs, bread", false)); tasks.add(new TaskItem(2, "Write docs", "Draft the MCP tutorial", true)); } public List<TaskItem> getAll() { return List.copyOf(tasks); } public Optional<TaskItem> getById(int id) { return tasks.stream().filter(t -> t.getId() == id).findFirst(); } public TaskItem create(String title, String description) { TaskItem task = new TaskItem(nextId.getAndIncrement(), title, description, false); tasks.add(task); return task; } public Optional<TaskItem> toggleComplete(int id) { return getById(id).map(task -> { task.setComplete(!task.isComplete()); return task; }); } public boolean delete(int id) { return tasks.removeIf(t -> t.getId() == id); } }TaskStoreSpring コンポーネントは、サンプル データが事前に入力されたメモリ内リストを管理します。 スレッド セーフな ID 生成にAtomicIntegerを使用し、標準の CRUD 操作のメソッドを提供します。
MCP ツールを定義する
このセクションでは、AI モデルが Spring Boot アプリケーションで MCP サーバーを呼び出して構成できる MCP ツールを定義します。
src/main/java/com/example/tasksmcp/TasksMcpTools.javaを作成します。package com.example.tasksmcp; import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.TextContent; import io.modelcontextprotocol.spec.McpSchema.Tool; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; @Component public class TasksMcpTools { private final TaskStore store; private final ObjectMapper objectMapper = new ObjectMapper(); public TasksMcpTools(TaskStore store) { this.store = store; } public List<SyncToolSpecification> getToolSpecifications() { return List.of( listTasksTool(), getTaskTool(), createTaskTool(), toggleTaskCompleteTool(), deleteTaskTool() ); } private SyncToolSpecification listTasksTool() { var tool = new Tool( "list_tasks", "List all tasks with their ID, title, description, and completion status.", emptySchema() ); return new SyncToolSpecification(tool, (exchange, request) -> { try { String json = objectMapper.writeValueAsString(store.getAll()); return new CallToolResult(List.of(new TextContent(json)), false); } catch (Exception e) { return errorResult(e.getMessage()); } }); } private SyncToolSpecification getTaskTool() { var tool = new Tool( "get_task", "Get a single task by its numeric ID.", objectSchema(Map.of( "task_id", propertySchema("integer", "The numeric ID of the task to retrieve") ), List.of("task_id")) ); return new SyncToolSpecification(tool, (exchange, request) -> { int taskId = ((Number) request.arguments().get("task_id")).intValue(); return store.getById(taskId) .map(task -> { try { return new CallToolResult( List.of(new TextContent(objectMapper.writeValueAsString(task))), false); } catch (Exception e) { return errorResult(e.getMessage()); } }) .orElse(textResult("Task with ID " + taskId + " not found.")); }); } private SyncToolSpecification createTaskTool() { var tool = new Tool( "create_task", "Create a new task with the given title and description. Returns the created task.", objectSchema(Map.of( "title", propertySchema("string", "A short title for the task"), "description", propertySchema("string", "A detailed description of what the task involves") ), List.of("title", "description")) ); return new SyncToolSpecification(tool, (exchange, request) -> { String title = (String) request.arguments().get("title"); String description = (String) request.arguments().get("description"); TaskItem task = store.create(title, description); try { return new CallToolResult( List.of(new TextContent(objectMapper.writeValueAsString(task))), false); } catch (Exception e) { return errorResult(e.getMessage()); } }); } private SyncToolSpecification toggleTaskCompleteTool() { var tool = new Tool( "toggle_task_complete", "Toggle a task's completion status between complete and incomplete.", objectSchema(Map.of( "task_id", propertySchema("integer", "The numeric ID of the task to toggle") ), List.of("task_id")) ); return new SyncToolSpecification(tool, (exchange, request) -> { int taskId = ((Number) request.arguments().get("task_id")).intValue(); return store.toggleComplete(taskId) .map(task -> textResult( "Task " + task.getId() + " is now " + (task.isComplete() ? "complete" : "incomplete") + ".")) .orElse(textResult("Task with ID " + taskId + " not found.")); }); } private SyncToolSpecification deleteTaskTool() { var tool = new Tool( "delete_task", "Delete a task by its numeric ID.", objectSchema(Map.of( "task_id", propertySchema("integer", "The numeric ID of the task to delete") ), List.of("task_id")) ); return new SyncToolSpecification(tool, (exchange, request) -> { int taskId = ((Number) request.arguments().get("task_id")).intValue(); boolean deleted = store.delete(taskId); return textResult(deleted ? "Task " + taskId + " deleted." : "Task with ID " + taskId + " not found."); }); } // Helper methods for JSON Schema construction private String emptySchema() { return "{\"type\":\"object\",\"properties\":{}}"; } private String objectSchema(Map<String, String> properties, List<String> required) { ObjectNode schema = objectMapper.createObjectNode(); schema.put("type", "object"); ObjectNode propsNode = objectMapper.createObjectNode(); for (var entry : properties.entrySet()) { try { propsNode.set(entry.getKey(), objectMapper.readTree(entry.getValue())); } catch (Exception e) { propsNode.putObject(entry.getKey()).put("type", "string"); } } schema.set("properties", propsNode); schema.set("required", objectMapper.valueToTree(required)); return schema.toString(); } private String propertySchema(String type, String description) { return "{\"type\":\"" + type + "\",\"description\":\"" + description + "\"}"; } private CallToolResult textResult(String text) { return new CallToolResult(List.of(new TextContent(text)), false); } private CallToolResult errorResult(String message) { return new CallToolResult(List.of(new TextContent("Error: " + message)), true); } }注
MCP Java SDK API サーフェスは進化しています。 ここで示すツール登録パターンでは、同期ツールに
SyncToolSpecificationを使用します。 ツールの登録を簡略化できる最新のイディオムと注釈については、 MCP Java SDK のドキュメント を参照してください。src/main/java/com/example/tasksmcp/McpConfig.javaを作成します。package com.example.tasksmcp; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class McpConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // For production, restrict allowedOrigins to specific trusted domains registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "POST", "DELETE", "OPTIONS") .allowedHeaders("*"); } @Bean public WebMvcSseServerTransportProvider mcpTransportProvider() { return new WebMvcSseServerTransportProvider(new com.fasterxml.jackson.databind.ObjectMapper(), "/mcp"); } @Bean public McpSyncServer mcpServer(WebMvcSseServerTransportProvider transport, TasksMcpTools tools) { McpSyncServer server = McpServer.sync(transport) .serverInfo("TasksMCP", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) .build(); tools.getToolSpecifications().forEach(server::addTool); return server; } }重要なポイント:
-
WebMvcSseServerTransportProviderは、/mcpパスに SSE トランスポートを登録します。 -
McpServer.sync(transport)は、ツール機能を構成し、各ツール仕様を登録します。 - VS Code の GitHub Copilot が MCP サーバーにクロスオリジン要求を行うため、CORS が有効になります。
注
MCP Java SDK はまだ安定したストリーミング可能な HTTP トランスポートを提供していないため、このチュートリアルでは
WebMvcSseServerTransportProvider(SSE トランスポート) を使用します。 他の言語チュートリアル (.NET、Python、Node.js) では、ストリーミング可能な HTTP を使用します。 Java SDK でストリーミング可能な HTTP サポートが追加されたら、それに応じてトランスポート プロバイダーを更新します。 SSE トランスポートは、VS Code Copilot やその他の MCP クライアントと完全に互換性があります。-
src/main/java/com/example/tasksmcp/TasksMcpApplication.javaを作成します。package com.example.tasksmcp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController public class TasksMcpApplication { public static void main(String[] args) { SpringApplication.run(TasksMcpApplication.class, args); } @GetMapping("/health") public String health() { return "healthy"; } }メイン クラスは Spring Boot アプリケーションをブートストラップし、Container Apps 正常性プローブの
/healthエンドポイントを公開します。 MCP エンドポイントは JSON-RPC 応答を返すので、正常性チェックには別の正常性エンドポイントが必要です。
MCP サーバーをローカルでテストする
Azure にデプロイする前に、MCP サーバーがローカルで実行され、GitHub Copilot から接続されて動作することを確認します。
ビルドして実行します。
mvn spring-boot:runVS Code を開き、 Copilot チャット を開き、[ エージェント モード] を選択します。
[ ツール ] ボタンを選択し、[ その他のツールを追加]...>MCP サーバーを追加します。
HTTP (HTTP または Server-Sent イベント) を選択し、トランスポートの種類の入力を求められたら Server-Sent イベント (SSE) を選択します。
Important
このチュートリアルでは、ストリーミング可能な HTTP ではなく SSE トランスポートを使用します。 接続が正しく機能するためには、VS Code で SSE オプションを選択する必要があります。
サーバーの URL を入力します。
http://localhost:8080/mcpサーバー ID を入力します。
tasks-mcp[ ワークスペースの設定] を選択します。
テスト: "すべてのタスクを表示する"
Copilot が MCP ツールの確認を要求したら 、[続行] を 選択します。
メモリ内ストアから返されたタスク リストが表示されます。
ヒント
「PR を確認するタスクを作成する」、「タスク 1 を完了としてマークする」、「タスク 2 を削除する」などの他のプロンプトを試してください。
アプリケーションのコンテナー格納
Azure にデプロイする前にローカルでテストできるように、アプリケーションを Docker コンテナーとしてパッケージ化します。
マルチステージ ビルドを使用して
Dockerfileを作成します。FROM maven:3.9-eclipse-temurin-17-alpine AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src/ src/ RUN mvn package -DskipTests -B FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=build /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]マルチステージ ビルドでは、Maven ビルドとランタイム イメージを分離することで、最終的なイメージを小さく保ちます。
ローカルで確認する:
docker build -t tasks-mcp-server . docker run -p 8080:8080 tasks-mcp-serverヘルスエンドポイントが応答することを確認してください。
curl http://localhost:8080/health
Azure Container Apps へのデプロイ
アプリケーションをコンテナー化したら、Azure CLI を使用して Azure Container Apps にデプロイします。
az containerapp up コマンドはクラウドにコンテナー イメージをビルドするため、この手順ではマシン上に Docker は必要ありません。
環境変数を設定します。
RESOURCE_GROUP="mcp-tutorial-rg" LOCATION="eastus" ENVIRONMENT_NAME="mcp-env" APP_NAME="tasks-mcp-server-java"リソース グループと Container Apps 環境を作成します。
az group create --name $RESOURCE_GROUP --location $LOCATION az containerapp env create \ --name $ENVIRONMENT_NAME \ --resource-group $RESOURCE_GROUP \ --location $LOCATIONコンテナー アプリをデプロイします。
az containerapp up \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --environment $ENVIRONMENT_NAME \ --source . \ --ingress external \ --target-port 8080CORS を構成します。
az containerapp ingress cors enable \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --allowed-origins "*" \ --allowed-methods "GET,POST,DELETE,OPTIONS" \ --allowed-headers "*"注
運用環境では、ワイルドカードの配信元を特定の信頼できるオリジンに置き換えます。 Container Apps での MCP サーバーのセキュリティ保護に関する情報を参照してください。
デプロイを検証します。
APP_URL=$(az containerapp show \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --query "properties.configuration.ingress.fqdn" -o tsv) curl https://$APP_URL/health
デプロイされたサーバーに GitHub Copilot を接続する
MCP サーバーが Azure で実行されたので、デプロイされたエンドポイントに GitHub Copilot を接続するように VS Code を構成します。
.vscode/mcp.jsonを作成または更新します。{ "servers": { "tasks-mcp-server": { "type": "sse", "url": "https://<your-app-fqdn>/mcp" } } }<your-app-fqdn>をデプロイ出力の FQDN に置き換えます。Important
このチュートリアルでは SSE トランスポートを使用するため、
"type"を"sse"する必要があります。"http"(ストリーミング可能な HTTP) を使用すると、接続エラーが発生します。VS Code で、エージェント モードで Copilot チャットを開きます。
tasks-mcp-serverが [ツール] リストに表示されていることを確認します。 必要に応じて [開始] を選択します 。"どのようなタスクがありますか" のようなプロンプトでテストします。
対話型使用のスケーリングを構成する
Java アプリケーションのコールド スタート時間が長くなっています。 JVM をウォーム状態に保つために、最小レプリカ数を設定します。
az containerapp update \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--min-replicas 1
ヒント
JVM に適したリソース設定を検討してください。 Spring Boot アプリケーションには、少なくとも 1 つの vCPU と 2 GiB のメモリを使用することをお勧めします。
セキュリティに関する考慮事項
このチュートリアルでは、簡単にするために認証されていない MCP サーバーを使用します。 運用環境で MCP サーバーを実行する前に、次の推奨事項を確認してください。 MCP サーバーが大規模な言語モデル (LLM) を搭載したエージェントによって呼び出される場合は、 迅速なインジェクション 攻撃に注意してください。
- 認証と承認: Microsoft Entra ID を使用して MCP サーバーをセキュリティで保護します。 Container Apps での MCP サーバーのセキュリティ保護に関する情報を参照してください。
-
入力検証: ツール パラメーターの検証には、Bean 検証 (
@Valid、@NotNull、@Size) を使用します。 Spring Boot での検証を参照してください。 - HTTPS: Azure Container Apps では、自動 TLS 証明書を使用して既定で HTTPS が適用されます。
- 最小権限: ユース ケースで必要なツールのみを公開します。 確認なしで破壊的な操作を実行するツールは避けてください。
- CORS: 許可された配信元を運用環境の信頼されたドメインに制限します。
- ログ記録と監視: SLF4J と Azure Monitor を使用した監査のための MCP ツールの呼び出しをログに記録します。
リソースをクリーンアップする
このアプリケーションを引き続き使用しない場合は、リソース グループを削除して、このチュートリアルで作成したすべてのリソースを削除します。
az group delete --resource-group $RESOURCE_GROUP --yes --no-wait