模型上下文协议注解工具

Spring AI的模型上下文协议(MCP)注解工具,提供声明式注解驱动的开发方式,支持工具、资源、提示和完成等多种功能。

  • by DaGlaswen
  • Java
  • 本地部署
  • 更新于
在 GitHub 查看 DaGlaswen/mcp-demo

使用说明

Spring AI MCP注释示例

本目录包含使用Spring AI基于注释的方法演示模型上下文协议(MCP)的示例。这些示例展示了MCP的功能,包括工具、资源、提示、补全和使用声明性注释的客户端处理程序。

概述

MCP注释示例演示了:

  • 注释驱动开发:使用简化的MCP开发 @McpTool, @McpResource, @McpPrompt, @McpComplete,以及客户端注释
  • 声明性配置:通过注释自动注册和配置
  • 客户端-服务器通信:全双向MCP通信模式
  • 多种MCP功能:工具、资源、提示、补全和客户端处理程序
  • 混合注释样式:MCP注释和Spring AI @Tool 注释

项目

1.mcp注释服务器

一个全面的MCP服务器实现,使用注释展示了所有主要的MCP功能。

工具

  • 天气工具:来自Open-Meteo API的温度数据,使用 @McpTool
  • 其他工具:Weather.gov API使用Spring AI发布的天气预报和警报 @Tool

资源

  • 用户档案:具有多种访问模式的完整用户信息
  • 用户属性:使用URI变量进行单个属性访问
  • 用户状态:具有不同返回类型的动态状态信息
  • 用户连接:社会联系和关系
  • 用户通知:动态通知生成
  • 用户头像:使用自定义MIME类型的二进制数据处理
  • 用户位置:简单的位置信息

提示

  • 问候提示:简单的参数化问候语
  • 个性化消息:具有多个参数和逻辑的复杂提示
  • 对话启动:多消息对话流
  • 动态内容:基于映射的参数处理
  • 单一消息:单个消息响应
  • 字符串列表:基于列表的响应

补全

  • 用户名填写:自动完成用户状态URI
  • 名称完成:自动完成个性化消息提示
  • 国家完成:填写与旅行有关的国家名称

2.mcp注释客户端

一个简单的MCP客户端实现,使用注释演示客户端MCP功能。

处理程序类型

  • 进度处理程序:跟踪长时间运行的操作 @McpProgress
  • 日志处理程序:MCP服务器的集中日志记录 @McpLogging
  • 采样处理器:LLM请求授权 @McpSampling
  • 激励处理人员:用户交互提示 @McpElicitation

架构概述

服务器架构

@SpringBootApplication
public class McpServerApplication {
    // Traditional Spring AI tool integration
    @Bean
    public ToolCallbackProvider weatherTools(SpringAiToolProvider weatherService) {
        return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
    }
}

服务器使用自动注释扫描来注册MCP提供者。全部 @McpTool, @McpResource, @McpPrompt,以及 @McpComplete 带注释的方法会被自动发现和注册。

客户端体系结构

@SpringBootApplication
public class McpClientApplication {
    @Bean
    public CommandLineRunner predefinedQuestions(List<McpSyncClient> mcpClients) {
        return args -> {
            for (McpSyncClient mcpClient : mcpClients) {
                // Call tools and interact with MCP servers
                CallToolRequest toolRequest = CallToolRequest.builder()
                    .name("tool1")
                    .arguments(Map.of("input", "test input"))
                    .progressToken("test-progress-token")
                    .build();
                
                CallToolResult response = mcpClient.callTool(toolRequest);
            }
        };
    }
}

客户端处理程序通过注释扫描自动注册。

关键实施示例

工具实施

@Service
public class ToolProvider {
    @McpTool(description = "Get the temperature (in celsius) for a specific location")
    public WeatherResponse getTemperature(
            @McpToolParam(description = "The location latitude") double latitude,
            @McpToolParam(description = "The location longitude") double longitude,
            @McpToolParam(description = "The city name") String city) {
        
        return restClient.get()
            .uri("https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m",
                latitude, longitude)
            .retrieve()
            .body(WeatherResponse.class);
    }
}

资源实施

@Service
public class UserProfileResourceProvider {
    @McpResource(uri = "user-profile://{username}", 
                 name = "User Profile", 
                 description = "Provides user profile information for a specific user")
    public ReadResourceResult getUserProfile(ReadResourceRequest request, String username) {
        String profileInfo = formatProfileInfo(userProfiles.getOrDefault(username.toLowerCase(), new HashMap<>()));
        return new ReadResourceResult(List.of(new TextResourceContents(request.uri(), "text/plain", profileInfo)));
    }

    @McpResource(uri = "user-attribute://{username}/{attribute}", 
                 name = "User Attribute", 
                 description = "Provides a specific attribute from a user's profile")
    public ReadResourceResult getUserAttribute(String username, String attribute) {
        Map<String, String> profile = userProfiles.getOrDefault(username.toLowerCase(), new HashMap<>());
        String attributeValue = profile.getOrDefault(attribute, "Attribute not found");
        
        return new ReadResourceResult(
            List.of(new TextResourceContents("user-attribute://" + username + "/" + attribute, 
                   "text/plain", username + "'s " + attribute + ": " + attributeValue)));
    }
}

迅速实施

@Service
public class PromptProvider {
    @McpPrompt(name = "personalized-message", 
               description = "Generates a personalized message based on user information")
    public GetPromptResult personalizedMessage(
            McpSyncServerExchange exchange,
            @McpArg(name = "name", description = "The user's name", required = true) String name,
            @McpArg(name = "age", description = "The user's age", required = false) Integer age,
            @McpArg(name = "interests", description = "The user's interests", required = false) String interests) {
        
        // Log the prompt generation
        exchange.loggingNotification(LoggingMessageNotification.builder()
            .level(LoggingLevel.INFO)
            .data("personalized-message event").build());

        StringBuilder message = new StringBuilder();
        message.append("Hello, ").append(name).append("!\n\n");
        
        if (age != null) {
            message.append("At ").append(age).append(" years old, you have ");
            if (age < 30) {
                message.append("so much ahead of you.\n\n");
            } else if (age < 60) {
                message.append("gained valuable life experience.\n\n");
            } else {
                message.append("accumulated wisdom to share with others.\n\n");
            }
        }

        return new GetPromptResult("Personalized Message",
            List.of(new PromptMessage(Role.ASSISTANT, new TextContent(message.toString()))));
    }
}

竣工实施

@Service
public class CompletionProvider {
    @McpComplete(uri = "user-status://{username}")
    public List<String> completeUsername(String usernamePrefix) {
        String prefix = usernamePrefix.toLowerCase();
        if (prefix.isEmpty()) {
            return List.of("Enter a username");
        }

        String firstLetter = prefix.substring(0, 1);
        List<String> usernames = usernameDatabase.getOrDefault(firstLetter, List.of());

        return usernames.stream()
            .filter(username -> username.toLowerCase().startsWith(prefix))
            .toList();
    }

    @McpComplete(prompt = "personalized-message")
    public CompleteResult completeCountryName(CompleteRequest request) {
        String prefix = request.argument().value().toLowerCase();
        String firstLetter = prefix.substring(0, 1);
        List<String> countries = countryDatabase.getOrDefault(firstLetter, List.of());

        List<String> matches = countries.stream()
            .filter(country -> country.toLowerCase().startsWith(prefix))
            .toList();

        return new CompleteResult(new CompleteCompletion(matches, matches.size(), false));
    }
}

客户端处理程序实现

@Service
public class McpClientHandlerProviders {
    @McpProgress(clients = "server1")
    public void progressHandler(ProgressNotification progressNotification) {
        logger.info("MCP PROGRESS: [{}] progress: {} total: {} message: {}",
            progressNotification.progressToken(), progressNotification.progress(),
            progressNotification.total(), progressNotification.message());
    }

    @McpLogging(clients = "server1")
    public void loggingHandler(LoggingMessageNotification loggingMessage) {
        logger.info("MCP LOGGING: [{}] {}", loggingMessage.level(), loggingMessage.data());
    }

    @McpSampling(clients = "server1")
    public CreateMessageResult samplingHandler(CreateMessageRequest llmRequest) {
        String userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text();
        String modelHint = llmRequest.modelPreferences().hints().get(0).name();

        return CreateMessageResult.builder()
            .content(new McpSchema.TextContent("Response " + userPrompt + " with model hint " + modelHint))
            .build();
    }

    @McpElicitation(clients = "server1")
    public ElicitResult elicitationHandler(McpSchema.ElicitRequest request) {
        logger.info("MCP ELICITATION: {}", request);
        return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
    }
}

关键依赖关系

服务器依赖关系

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

客户端依赖关系

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>

运行示例

先决条件

  • Java 17或更高版本
  • Maven 3.6+

步骤1:启动MCP注释服务器

cd mcp-annotations-server
./mvnw clean package -DskipTests
java -jar target/mcp-annotations-server-0.0.1-SNAPSHOT.jar

服务器将于启动 http://localhost:8080 启用SSE传输。

步骤2:运行MCP注释客户端

cd mcp-annotations-client
./mvnw clean package
java -jar target/mcp-annotations-client-0.0.1-SNAPSHOT.jar

可用特征

工具

  • getTemperature:使用Open-Meteo API获取温度数据

资源

  • user-profile://{username}:完整的用户资料信息
  • user-attribute://{username}/{attribute}:特定用户属性
  • user-status://{username}:用户在线状态
  • user-connections://{username}:用户社交关系
  • user-notifications://{username}:用户通知
  • user-location://{username}:用户位置信息
  • user-avatar://{username}:用户头像图像(base64)

提示

  • greeting:带名称参数的简单问候语
  • personalized-message:包含年龄和兴趣的复杂个性化信息
  • conversation-starter:多消息对话流
  • map-arguments:演示基于映射的参数处理
  • single-message:单个消息响应
  • string-list:基于列表的响应

补全

  • 用户名完成 user-status:// URI
  • 姓名填写 personalized-message 提示
  • 旅行提示的国家名称填写

配置

服务器配置(application.properties)

# Server identification
spring.ai.mcp.server.name=my-weather-server
spring.ai.mcp.server.version=0.0.1

# Disable banner for STDIO transport compatibility
spring.main.banner-mode=off

# Logging configuration
logging.file.name=./model-context-protocol/mcp-annotations/mcp-annotations-server/target/mcp-annotations-server.log

# Uncomment for STDIO transport
# spring.ai.mcp.server.stdio=true
# spring.main.web-application-type=none

# Uncomment for different protocols
# spring.ai.mcp.server.protocol=STREAMABLE
# spring.ai.mcp.server.protocol=STATELESS

客户端配置(application.properties)

spring.application.name=mcp
spring.main.web-application-type=none

# MCP client connection
spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080

# Logging configuration
logging.level.io.modelcontextprotocol.client=WARN
logging.level.io.modelcontextprotocol.spec=WARN

测试服务器

您可以使用附带的测试客户端测试服务器:

SSE客户端测试

// Located in src/test/java/.../client/ClientSse.java
var transport = HttpClientSseClientTransport.builder("http://localhost:8080").build();
var client = McpClient.sync(transport).build();

STDIO客户端测试

// Located in src/test/java/.../client/ClientStdio.java
var stdioParams = ServerParameters.builder("java")
    .args("-jar", "target/mcp-annotations-server-0.0.1-SNAPSHOT.jar")
    .build();
var transport = new StdioClientTransport(stdioParams);
var client = McpClient.sync(transport).build();

高级功能

混合注释样式

服务器演示了MCP特定的注释(@McpTool)Spring AI注释(@Tool),展示了它们如何在同一应用程序中共存。

URI模板变量

资源支持具有多个变量的复杂URI模板,这些变量会自动提取并作为方法参数传递。

退货类型灵活性

方法可以返回各种类型:

  • ReadResourceResult 完全控制
  • List<ResourceContents> 用于多个内容项
  • ResourceContents 对于单个内容项
  • String 用于简单的文本响应
  • List<String> 用于多个文本项

自定义MIME类型

资源可以为不同的内容类型指定自定义MIME类型,包括二进制数据处理。

服务器交换集成

方法可以访问 McpSyncServerExchange 用于高级服务器操作,如记录通知和进度跟踪。

样品数据

服务器包括用于测试的示例用户配置文件:

  • 约翰约翰·史密斯(纽约,32岁)
  • 简:无名氏(伦敦,28岁)
  • 鲍勃鲍勃·约翰逊(东京,45岁)
  • 爱丽丝:爱丽丝·布朗(悉尼,36岁)

相关项目

其他资源