Commit 5b31f27bd5958aa1a4e10907c0f69a4e7893a0e2

Authored by YunaiV
1 parent 4afa67f3

feat:【ai 大模型】增加 MCP Server Boot Starter 示例

Showing 14 changed files with 514 additions and 5 deletions
yudao-module-ai/pom.xml
... ... @@ -187,6 +187,12 @@
187 187 </exclusions>
188 188 </dependency>
189 189  
  190 + <dependency>
  191 + <groupId>org.springframework.ai</groupId>
  192 + <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
  193 + <version>${spring-ai.version}</version>
  194 + </dependency>
  195 +
190 196 <!-- TinyFlow:AI 工作流 -->
191 197 <dependency>
192 198 <groupId>dev.tinyflow</groupId>
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java
1 1 package cn.iocoder.yudao.module.ai.dal.dataobject.model;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4   -import cn.iocoder.yudao.module.ai.service.model.tool.DirectoryListToolFunction;
5   -import cn.iocoder.yudao.module.ai.service.model.tool.WeatherQueryToolFunction;
  4 +import cn.iocoder.yudao.module.ai.tool.function.DirectoryListToolFunction;
  5 +import cn.iocoder.yudao.module.ai.tool.function.WeatherQueryToolFunction;
6 6 import com.baomidou.mybatisplus.annotation.KeySequence;
7 7 import com.baomidou.mybatisplus.annotation.TableId;
8 8 import com.baomidou.mybatisplus.annotation.TableName;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java
... ... @@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
15 15 import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
16 16 import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
17 17 import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
  18 +import cn.iocoder.yudao.module.ai.tool.method.PersonService;
18 19 import lombok.extern.slf4j.Slf4j;
19 20 import org.springframework.ai.deepseek.DeepSeekChatModel;
20 21 import org.springframework.ai.deepseek.DeepSeekChatOptions;
... ... @@ -25,8 +26,10 @@ import org.springframework.ai.model.tool.ToolCallingManager;
25 26 import org.springframework.ai.openai.OpenAiChatModel;
26 27 import org.springframework.ai.openai.OpenAiChatOptions;
27 28 import org.springframework.ai.openai.api.OpenAiApi;
  29 +import org.springframework.ai.support.ToolCallbacks;
28 30 import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
29 31 import org.springframework.ai.tokenizer.TokenCountEstimator;
  32 +import org.springframework.ai.tool.ToolCallback;
30 33 import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
31 34 import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
32 35 import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
... ... @@ -36,6 +39,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
36 39 import org.springframework.context.annotation.Bean;
37 40 import org.springframework.context.annotation.Configuration;
38 41  
  42 +import java.util.List;
  43 +
39 44 /**
40 45 * 芋道 AI 自动配置
41 46 *
... ... @@ -271,4 +276,14 @@ public class AiAutoConfiguration {
271 276 return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey());
272 277 }
273 278  
  279 + // ========== MCP 相关 ==========
  280 +
  281 + /**
  282 + * 参考自 <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">MCP Server Boot Starter</>
  283 + */
  284 + @Bean
  285 + public List<ToolCallback> toolCallbacks(PersonService personService) {
  286 + return List.of(ToolCallbacks.from(personService));
  287 + }
  288 +
274 289 }
275 290 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.security.config;
  2 +
  3 +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
  4 +import jakarta.annotation.Resource;
  5 +import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
  6 +import org.springframework.context.annotation.Bean;
  7 +import org.springframework.context.annotation.Configuration;
  8 +import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  9 +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
  10 +
  11 +/**
  12 + * AI 模块的 Security 配置
  13 + */
  14 +@Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration")
  15 +public class SecurityConfiguration {
  16 +
  17 + @Resource
  18 + private McpServerProperties serverProperties;
  19 +
  20 + @Bean("aiAuthorizeRequestsCustomizer")
  21 + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
  22 + return new AuthorizeRequestsCustomizer() {
  23 +
  24 + @Override
  25 + public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
  26 + // MCP Server
  27 + registry.requestMatchers(serverProperties.getSseEndpoint()).permitAll();
  28 + registry.requestMatchers(serverProperties.getSseMessageEndpoint()).permitAll();
  29 + }
  30 +
  31 + };
  32 + }
  33 +
  34 +}
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java 0 → 100644
  1 +/**
  2 + * 占位
  3 + */
  4 +package cn.iocoder.yudao.module.ai.framework.security.core;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java
1   -package cn.iocoder.yudao.module.ai.service.model.tool;
  1 +package cn.iocoder.yudao.module.ai.tool.function;
2 2  
3 3 import cn.hutool.core.date.LocalDateTimeUtil;
4 4 import cn.hutool.core.io.FileUtil;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java
1   -package cn.iocoder.yudao.module.ai.service.model.tool;
  1 +package cn.iocoder.yudao.module.ai.tool.function;
2 2  
3 3 import cn.iocoder.yudao.module.ai.util.AiUtils;
4 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java
1   -package cn.iocoder.yudao.module.ai.service.model.tool;
  1 +package cn.iocoder.yudao.module.ai.tool.function;
2 2  
3 3 import cn.hutool.core.date.LocalDateTimeUtil;
4 4 import cn.hutool.core.util.RandomUtil;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java 0 → 100644
  1 +/**
  2 + * 参考 <a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Tool Calling —— Methods as Tools</a>
  3 + */
  4 +package cn.iocoder.yudao.module.ai.tool.function;
0 5 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.tool.method;
  2 +
  3 +/**
  4 + * 来自 Spring AI 官方文档
  5 + *
  6 + * Represents a person with basic information.
  7 + * This is an immutable record.
  8 + */
  9 +public record Person(
  10 + int id,
  11 + String firstName,
  12 + String lastName,
  13 + String email,
  14 + String sex,
  15 + String ipAddress,
  16 + String jobTitle,
  17 + int age
  18 +) {
  19 +}
0 20 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.tool.method;
  2 +
  3 +import java.util.List;
  4 +import java.util.Optional;
  5 +
  6 +/**
  7 + * 来自 Spring AI 官方文档
  8 + *
  9 + * Service interface for managing Person data.
  10 + * Defines the contract for CRUD operations and search/filter functionalities.
  11 + */
  12 +public interface PersonService {
  13 +
  14 + /**
  15 + * Creates a new Person record.
  16 + * Assigns a unique ID to the person and stores it.
  17 + *
  18 + * @param personData The data for the new person (ID field is ignored). Must not be null.
  19 + * @return The created Person record, including the generated ID.
  20 + */
  21 + Person createPerson(Person personData);
  22 +
  23 + /**
  24 + * Retrieves a Person by their unique ID.
  25 + *
  26 + * @param id The ID of the person to retrieve.
  27 + * @return An Optional containing the found Person, or an empty Optional if not found.
  28 + */
  29 + Optional<Person> getPersonById(int id);
  30 +
  31 + /**
  32 + * Retrieves all Person records currently stored.
  33 + *
  34 + * @return An unmodifiable List containing all Persons. Returns an empty list if none exist.
  35 + */
  36 + List<Person> getAllPersons();
  37 +
  38 + /**
  39 + * Updates an existing Person record identified by ID.
  40 + * Replaces the existing data with the provided data, keeping the original ID.
  41 + *
  42 + * @param id The ID of the person to update.
  43 + * @param updatedPersonData The new data for the person (ID field is ignored). Must not be null.
  44 + * @return true if the person was found and updated, false otherwise.
  45 + */
  46 + boolean updatePerson(int id, Person updatedPersonData);
  47 +
  48 + /**
  49 + * Deletes a Person record identified by ID.
  50 + *
  51 + * @param id The ID of the person to delete.
  52 + * @return true if the person was found and deleted, false otherwise.
  53 + */
  54 + boolean deletePerson(int id);
  55 +
  56 + /**
  57 + * Searches for Persons whose job title contains the given query string (case-insensitive).
  58 + *
  59 + * @param jobTitleQuery The string to search for within job titles. Can be null or blank.
  60 + * @return An unmodifiable List of matching Persons. Returns an empty list if no matches or query is invalid.
  61 + */
  62 + List<Person> searchByJobTitle(String jobTitleQuery);
  63 +
  64 + /**
  65 + * Filters Persons by their exact sex (case-insensitive).
  66 + *
  67 + * @param sex The sex to filter by (e.g., "Male", "Female"). Can be null or blank.
  68 + * @return An unmodifiable List of matching Persons. Returns an empty list if no matches or filter is invalid.
  69 + */
  70 + List<Person> filterBySex(String sex);
  71 +
  72 + /**
  73 + * Filters Persons by their exact age.
  74 + *
  75 + * @param age The age to filter by.
  76 + * @return An unmodifiable List of matching Persons. Returns an empty list if no matches.
  77 + */
  78 + List<Person> filterByAge(int age);
  79 +
  80 +}
0 81 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.tool.method;
  2 +
  3 +import jakarta.annotation.PostConstruct;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.ai.tool.annotation.Tool;
  6 +import org.springframework.stereotype.Service;
  7 +
  8 +import java.util.Collections;
  9 +import java.util.List;
  10 +import java.util.Map;
  11 +import java.util.Optional;
  12 +import java.util.concurrent.ConcurrentHashMap;
  13 +import java.util.concurrent.atomic.AtomicInteger;
  14 +import java.util.stream.Collectors;
  15 +import java.util.stream.Stream;
  16 +
  17 +/**
  18 + * 来自 Spring AI 官方文档
  19 + *
  20 + * Implementation of the PersonService interface using an in-memory data store.
  21 + * Manages a collection of Person objects loaded from embedded CSV data.
  22 + * This class is thread-safe due to the use of ConcurrentHashMap and AtomicInteger.
  23 + */
  24 +@Service
  25 +@Slf4j
  26 +public class PersonServiceImpl implements PersonService {
  27 +
  28 + private final Map<Integer, Person> personStore = new ConcurrentHashMap<>();
  29 +
  30 + private AtomicInteger idGenerator;
  31 +
  32 + /**
  33 + * Embedded CSV data for initial population
  34 + */
  35 + private static final String CSV_DATA = """
  36 + Id,FirstName,LastName,Email,Sex,IpAddress,JobTitle,Age
  37 + 1,Fons,Tollfree,ftollfree0@senate.gov,Male,55.1 Tollfree Lane,Research Associate,31
  38 + 2,Emlynne,Tabourier,etabourier1@networksolutions.com,Female,18 Tabourier Way,Associate Professor,38
  39 + 3,Shae,Johncey,sjohncey2@yellowpages.com,Male,1 Johncey Circle,Structural Analysis Engineer,30
  40 + 4,Sebastien,Bradly,sbradly3@mapquest.com,Male,2 Bradly Hill,Chief Executive Officer,40
  41 + 5,Harriott,Kitteringham,hkitteringham4@typepad.com,Female,3 Kitteringham Drive,VP Sales,47
  42 + 6,Anallise,Parradine,aparradine5@miibeian.gov.cn,Female,4 Parradine Street,Analog Circuit Design manager,44
  43 + 7,Gorden,Kirkbright,gkirkbright6@reuters.com,Male,5 Kirkbright Plaza,Senior Editor,40
  44 + 8,Veradis,Ledwitch,vledwitch7@google.com.au,Female,6 Ledwitch Avenue,Computer Systems Analyst IV,44
  45 + 9,Agnesse,Penhalurick,apenhalurick8@google.it,Female,7 Penhalurick Terrace,Automation Specialist IV,41
  46 + 10,Bibby,Hutable,bhutable9@craigslist.org,Female,8 Hutable Place,Account Representative I,43
  47 + 11,Karoly,Lightoller,klightollera@rakuten.co.jp,Female,9 Lightoller Parkway,Senior Developer,46
  48 + 12,Cristine,Durrad,cdurradb@aol.com,Female,10 Durrad Center,Senior Developer,48
  49 + 13,Aggy,Napier,anapierc@hostgator.com,Female,11 Napier Court,VP Product Management,44
  50 + 14,Prisca,Caddens,pcaddensd@vinaora.com,Female,12 Caddens Alley,Business Systems Development Analyst,41
  51 + 15,Khalil,McKernan,kmckernane@google.fr,Male,13 McKernan Pass,Engineer IV,44
  52 + 16,Lorry,MacTrusty,lmactrustyf@eventbrite.com,Male,14 MacTrusty Junction,Design Engineer,42
  53 + 17,Casandra,Worsell,cworsellg@goo.gl,Female,15 Worsell Point,Systems Administrator IV,45
  54 + 18,Ulrikaumeko,Haveline,uhavelineh@usgs.gov,Female,16 Haveline Trail,Financial Advisor,42
  55 + 19,Shurlocke,Albany,salbanyi@artisteer.com,Male,17 Albany Plaza,Software Test Engineer III,46
  56 + 20,Myrilla,Brimilcombe,mbrimilcombej@accuweather.com,Female,18 Brimilcombe Road,Programmer Analyst I,48
  57 + 21,Carlina,Scimonelli,cscimonellik@va.gov,Female,19 Scimonelli Pass,Help Desk Technician,45
  58 + 22,Tina,Goullee,tgoulleel@miibeian.gov.cn,Female,20 Goullee Crossing,Accountant IV,43
  59 + 23,Adriaens,Storek,astorekm@devhub.com,Female,21 Storek Avenue,Recruiting Manager,40
  60 + 24,Tedra,Giraudot,tgiraudotn@wiley.com,Female,22 Giraudot Terrace,Speech Pathologist,47
  61 + 25,Josiah,Soares,jsoareso@google.nl,Male,23 Soares Street,Tax Accountant,45
  62 + 26,Kayle,Gaukrodge,kgaukrodgep@wikispaces.com,Female,24 Gaukrodge Parkway,Accountant II,43
  63 + 27,Ardys,Chuter,achuterq@ustream.tv,Female,25 Chuter Drive,Engineer IV,41
  64 + 28,Francyne,Baudinet,fbaudinetr@newyorker.com,Female,26 Baudinet Center,VP Accounting,48
  65 + 29,Gerick,Bullan,gbullans@seesaa.net,Male,27 Bullan Way,Senior Financial Analyst,43
  66 + 30,Northrup,Grivori,ngrivorit@unc.edu,Male,28 Grivori Plaza,Systems Administrator I,45
  67 + 31,Town,Duguid,tduguidu@squarespace.com,Male,29 Duguid Pass,Safety Technician IV,46
  68 + 32,Pierette,Kopisch,pkopischv@google.com.br,Female,30 Kopisch Lane,Director of Sales,41
  69 + 33,Jacquenetta,Le Prevost,jleprevostw@netlog.com,Female,31 Le Prevost Trail,Senior Developer,47
  70 + 34,Garvy,Rusted,grustedx@aboutads.info,Male,32 Rusted Junction,Senior Developer,42
  71 + 35,Clarice,Aysh,cayshy@merriam-webster.com,Female,33 Aysh Avenue,VP Quality Control,40
  72 + 36,Tracie,Fedorski,tfedorskiz@bloglines.com,Male,34 Fedorski Terrace,Design Engineer,44
  73 + 37,Noelyn,Matushenko,nmatushenko10@globo.com,Female,35 Matushenko Place,VP Sales,48
  74 + 38,Rudiger,Klaesson,rklaesson11@usnews.com,Male,36 Klaesson Road,Database Administrator IV,43
  75 + 39,Mirella,Syddie,msyddie12@geocities.jp,Female,37 Syddie Circle,Geological Engineer,46
  76 + 40,Donalt,O'Lunny,dolunny13@elpais.com,Male,38 O'Lunny Center,Analog Circuit Design manager,41
  77 + 41,Guntar,Deniskevich,gdeniskevich14@google.com.hk,Male,39 Deniskevich Way,Structural Engineer,47
  78 + 42,Hort,Shufflebotham,hshufflebotham15@about.me,Male,40 Shufflebotham Court,Structural Analysis Engineer,45
  79 + 43,Dominique,Thickett,dthickett16@slashdot.org,Male,41 Thickett Crossing,Safety Technician I,42
  80 + 44,Zebulen,Piscopello,zpiscopello17@umich.edu,Male,42 Piscopello Parkway,Web Developer II,40
  81 + 45,Mellicent,Mac Giany,mmacgiany18@state.tx.us,Female,43 Mac Giany Pass,Assistant Manager,44
  82 + 46,Merle,Bounds,mbounds19@amazon.co.jp,Female,44 Bounds Alley,Systems Administrator III,41
  83 + 47,Madelle,Farbrace,mfarbrace1a@xinhuanet.com,Female,45 Farbrace Terrace,Quality Engineer,48
  84 + 48,Galvin,O'Sheeryne,gosheeryne1b@addtoany.com,Male,46 O'Sheeryne Way,Environmental Specialist,43
  85 + 49,Guillemette,Bootherstone,gbootherstone1c@nationalgeographic.com,Female,47 Bootherstone Plaza,Professor,46
  86 + 50,Letti,Aylmore,laylmore1d@vinaora.com,Female,48 Aylmore Circle,Automation Specialist I,40
  87 + 51,Nonie,Rivalland,nrivalland1e@weather.com,Female,49 Rivalland Avenue,Software Test Engineer IV,45
  88 + 52,Jacquelynn,Halfacre,jhalfacre1f@surveymonkey.com,Female,50 Halfacre Pass,Geologist II,42
  89 + 53,Anderea,MacKibbon,amackibbon1g@weibo.com,Female,51 MacKibbon Parkway,Automation Specialist II,47
  90 + 54,Wash,Klimko,wklimko1h@slashdot.org,Male,52 Klimko Alley,Database Administrator I,40
  91 + 55,Flori,Kynett,fkynett1i@auda.org.au,Female,53 Kynett Trail,Quality Control Specialist,46
  92 + 56,Libbey,Penswick,lpenswick1j@google.co.uk,Female,54 Penswick Point,VP Accounting,43
  93 + 57,Silvanus,Skellorne,sskellorne1k@booking.com,Male,55 Skellorne Drive,Account Executive,48
  94 + 58,Carmine,Mateos,cmateos1l@plala.or.jp,Male,56 Mateos Terrace,Systems Administrator I,41
  95 + 59,Sheffie,Blazewicz,sblazewicz1m@google.com.au,Male,57 Blazewicz Center,VP Sales,44
  96 + 60,Leanor,Worsnop,lworsnop1n@uol.com.br,Female,58 Worsnop Plaza,Systems Administrator III,45
  97 + 61,Caspar,Pamment,cpamment1o@google.co.jp,Male,59 Pamment Court,Senior Financial Analyst,42
  98 + 62,Justinian,Pentycost,jpentycost1p@sciencedaily.com,Male,60 Pentycost Way,Senior Quality Engineer,47
  99 + 63,Gerianne,Jarnell,gjarnell1q@bing.com,Female,61 Jarnell Avenue,Help Desk Operator,40
  100 + 64,Boycie,Zanetto,bzanetto1r@about.com,Male,62 Zanetto Place,Quality Engineer,46
  101 + 65,Camilla,Mac Giany,cmacgiany1s@state.gov,Female,63 Mac Giany Parkway,Senior Cost Accountant,43
  102 + 66,Hadlee,Piscopiello,hpiscopiello1t@artisteer.com,Male,64 Piscopiello Street,Account Representative III,48
  103 + 67,Bobbie,Penvarden,bpenvarden1u@google.cn,Male,65 Penvarden Lane,Help Desk Operator,41
  104 + 68,Ali,Gowlett,agowlett1v@parallels.com,Male,66 Gowlett Pass,VP Marketing,44
  105 + 69,Olivette,Acome,oacome1w@qq.com,Female,67 Acome Hill,VP Product Management,45
  106 + 70,Jehanna,Brotherheed,jbrotherheed1x@google.nl,Female,68 Brotherheed Junction,Database Administrator III,42
  107 + 71,Morgan,Berthomieu,mberthomieu1y@artisteer.com,Male,69 Berthomieu Alley,Systems Administrator II,47
  108 + 72,Linzy,Shilladay,lshilladay1z@icq.com,Female,70 Shilladay Trail,Research Assistant IV,40
  109 + 73,Faydra,Brimner,fbrimner20@mozilla.org,Female,71 Brimner Road,Senior Editor,46
  110 + 74,Gwenore,Oxlee,goxlee21@devhub.com,Female,72 Oxlee Terrace,Systems Administrator II,43
  111 + 75,Evangelin,Beinke,ebeinke22@mozilla.com,Female,73 Beinke Circle,Accountant I,48
  112 + 76,Missy,Cockling,mcockling23@si.edu,Female,74 Cockling Way,Software Engineer I,41
  113 + 77,Suzanne,Klimschak,sklimschak24@etsy.com,Female,75 Klimschak Plaza,Tax Accountant,44
  114 + 78,Candide,Goricke,cgoricke25@weebly.com,Female,76 Goricke Pass,Sales Associate,45
  115 + 79,Gerome,Pinsent,gpinsent26@google.com.au,Male,77 Pinsent Junction,Software Consultant,42
  116 + 80,Lezley,Mac Giany,lmacgiany27@scribd.com,Male,78 Mac Giany Alley,Operator,47
  117 + 81,Tobiah,Durn,tdurn28@state.tx.us,Male,79 Durn Court,VP Sales,40
  118 + 82,Sherlocke,Cockshoot,scockshoot29@yelp.com,Male,80 Cockshoot Street,Senior Financial Analyst,46
  119 + 83,Myrle,Speenden,mspeenden2a@utexas.edu,Female,81 Speenden Center,Senior Developer,43
  120 + 84,Isidore,Gorries,igorries2b@flavors.me,Male,82 Gorries Parkway,Sales Representative,48
  121 + 85,Isac,Kitchingman,ikitchingman2c@businessinsider.com,Male,83 Kitchingman Drive,VP Accounting,41
  122 + 86,Benedetta,Purrier,bpurrier2d@admin.ch,Female,84 Purrier Trail,VP Accounting,44
  123 + 87,Tera,Fitchell,tfitchell2e@fotki.com,Female,85 Fitchell Place,Software Engineer IV,45
  124 + 88,Abbe,Pamment,apamment2f@about.com,Male,86 Pamment Avenue,VP Sales,42
  125 + 89,Jandy,Gommowe,jgommowe2g@angelfire.com,Female,87 Gommowe Road,Financial Analyst,47
  126 + 90,Karena,Fussey,kfussey2h@google.com.au,Female,88 Fussey Point,Assistant Professor,40
  127 + 91,Gaspar,Pammenter,gpammenter2i@google.com.br,Male,89 Pammenter Hill,Help Desk Operator,46
  128 + 92,Stanwood,Mac Giany,smacgiany2j@prlog.org,Male,90 Mac Giany Terrace,Research Associate,43
  129 + 93,Byrom,Beedell,bbeedell2k@google.co.jp,Male,91 Beedell Way,VP Sales,48
  130 + 94,Annabella,Rowbottom,arowbottom2l@google.com.au,Female,92 Rowbottom Plaza,Help Desk Operator,41
  131 + 95,Rodolphe,Debell,rdebell2m@imageshack.us,Male,93 Debell Pass,Design Engineer,44
  132 + 96,Tyne,Gommey,tgommey2n@joomla.org,Female,94 Gommey Junction,VP Marketing,45
  133 + 97,Christoper,Pincked,cpincked2o@icq.com,Male,95 Pincked Alley,Human Resources Manager,42
  134 + 98,Kore,Le Prevost,kleprevost2p@tripadvisor.com,Female,96 Le Prevost Street,VP Quality Control,47
  135 + 99,Ceciley,Petrolli,cpetrolli2q@oaic.gov.au,Female,97 Petrolli Court,Senior Developer,40
  136 + 100,Elspeth,Mac Giany,emacgiany2r@icio.us,Female,98 Mac Giany Parkway,Internal Auditor,46
  137 + """;
  138 +
  139 + /**
  140 + * Initializes the service after dependency injection by loading data from the CSV string.
  141 + * Uses @PostConstruct to ensure this runs after the bean is created.
  142 + */
  143 + @PostConstruct
  144 + private void initializeData() {
  145 + log.info("Initializing PersonService data store...");
  146 + int maxId = loadDataFromCsv();
  147 + idGenerator = new AtomicInteger(maxId);
  148 + log.info("PersonService initialized with {} records. Next ID: {}", personStore.size(), idGenerator.get() + 1);
  149 + }
  150 +
  151 + /**
  152 + * Parses the embedded CSV data and populates the in-memory store.
  153 + * Calculates the maximum ID found in the data to initialize the ID generator.
  154 + *
  155 + * @return The maximum ID found in the loaded CSV data.
  156 + */
  157 + private int loadDataFromCsv() {
  158 + final AtomicInteger currentMaxId = new AtomicInteger(0);
  159 + // Clear existing data before loading (important for tests or re-initialization scenarios)
  160 + personStore.clear();
  161 + try (Stream<String> lines = CSV_DATA.lines().skip(1)) { // Skip header row
  162 + lines.forEach(line -> {
  163 + try {
  164 + // Split carefully, handling potential commas within quoted fields if necessary (simple split here)
  165 + String[] fields = line.split(",", 8); // Limit split to handle potential commas in job title
  166 + if (fields.length == 8) {
  167 + int id = Integer.parseInt(fields[0].trim());
  168 + String firstName = fields[1].trim();
  169 + String lastName = fields[2].trim();
  170 + String email = fields[3].trim();
  171 + String sex = fields[4].trim();
  172 + String ipAddress = fields[5].trim();
  173 + String jobTitle = fields[6].trim();
  174 + int age = Integer.parseInt(fields[7].trim());
  175 +
  176 + Person person = new Person(id, firstName, lastName, email, sex, ipAddress, jobTitle, age);
  177 + personStore.put(id, person);
  178 + currentMaxId.updateAndGet(max -> Math.max(max, id));
  179 + } else {
  180 + log.warn("Skipping malformed CSV line (expected 8 fields, found {}): {}", fields.length, line);
  181 + }
  182 + } catch (NumberFormatException e) {
  183 + log.warn("Skipping line due to parsing error (ID or Age): {} - Error: {}", line, e.getMessage());
  184 + } catch (Exception e) {
  185 + log.error("Skipping line due to unexpected error: {} - Error: {}", line, e.getMessage(), e);
  186 + }
  187 + });
  188 + } catch (Exception e) {
  189 + log.error("Fatal error reading embedded CSV data: {}", e.getMessage(), e);
  190 + // In a real application, might throw a specific initialization exception
  191 + }
  192 + return currentMaxId.get();
  193 + }
  194 +
  195 + @Override
  196 + @Tool(
  197 + name = "ps_create_person",
  198 + description = "Create a new person record in the in-memory store."
  199 + )
  200 + public Person createPerson(Person personData) {
  201 + if (personData == null) {
  202 + throw new IllegalArgumentException("Person data cannot be null");
  203 + }
  204 + int newId = idGenerator.incrementAndGet();
  205 + // Create a new Person record using data from the input, but with the generated ID
  206 + Person newPerson = new Person(
  207 + newId,
  208 + personData.firstName(),
  209 + personData.lastName(),
  210 + personData.email(),
  211 + personData.sex(),
  212 + personData.ipAddress(),
  213 + personData.jobTitle(),
  214 + personData.age()
  215 + );
  216 + personStore.put(newId, newPerson);
  217 + log.debug("Created person: {}", newPerson);
  218 + return newPerson;
  219 + }
  220 +
  221 + @Override
  222 + @Tool(
  223 + name = "ps_get_person_by_id",
  224 + description = "Retrieve a person record by ID from the in-memory store."
  225 + )
  226 + public Optional<Person> getPersonById(int id) {
  227 + Person person = personStore.get(id);
  228 + log.debug("Retrieved person by ID {}: {}", id, person);
  229 + return Optional.ofNullable(person);
  230 + }
  231 +
  232 + @Override
  233 + @Tool(
  234 + name = "ps_get_all_persons",
  235 + description = "Retrieve all person records from the in-memory store."
  236 + )
  237 + public List<Person> getAllPersons() {
  238 + // Return an unmodifiable view of the values
  239 + List<Person> allPersons = personStore.values().stream().toList();
  240 + log.debug("Retrieved all persons (count: {})", allPersons.size());
  241 + return allPersons;
  242 + }
  243 +
  244 + @Override
  245 + @Tool(
  246 + name = "ps_update_person",
  247 + description = "Update an existing person record by ID in the in-memory store."
  248 + )
  249 + public boolean updatePerson(int id, Person updatedPersonData) {
  250 + if (updatedPersonData == null) {
  251 + throw new IllegalArgumentException("Updated person data cannot be null");
  252 + }
  253 + // Use computeIfPresent for atomic update if the key exists
  254 + Person result = personStore.computeIfPresent(id, (key, existingPerson) ->
  255 + // Create a new Person record with the original ID but updated data
  256 + new Person(
  257 + id, // Keep original ID
  258 + updatedPersonData.firstName(),
  259 + updatedPersonData.lastName(),
  260 + updatedPersonData.email(),
  261 + updatedPersonData.sex(),
  262 + updatedPersonData.ipAddress(),
  263 + updatedPersonData.jobTitle(),
  264 + updatedPersonData.age()
  265 + )
  266 + );
  267 + boolean updated = result != null;
  268 + log.debug("Update attempt for ID {}: {}", id, updated ? "Successful" : "Failed (Not Found)");
  269 + if(updated) log.trace("Updated person data for ID {}: {}", id, result);
  270 + return updated;
  271 + }
  272 +
  273 + @Override
  274 + @Tool(
  275 + name = "ps_delete_person",
  276 + description = "Delete a person record by ID from the in-memory store."
  277 + )
  278 + public boolean deletePerson(int id) {
  279 + boolean removed = personStore.remove(id) != null;
  280 + log.debug("Delete attempt for ID {}: {}", id, removed ? "Successful" : "Failed (Not Found)");
  281 + return removed;
  282 + }
  283 +
  284 + @Override
  285 + @Tool(
  286 + name = "ps_search_by_job_title",
  287 + description = "Search for persons by job title in the in-memory store."
  288 + )
  289 + public List<Person> searchByJobTitle(String jobTitleQuery) {
  290 + if (jobTitleQuery == null || jobTitleQuery.isBlank()) {
  291 + log.debug("Search by job title skipped due to blank query.");
  292 + return Collections.emptyList();
  293 + }
  294 + String lowerCaseQuery = jobTitleQuery.toLowerCase();
  295 + List<Person> results = personStore.values().stream()
  296 + .filter(person -> person.jobTitle() != null && person.jobTitle().toLowerCase().contains(lowerCaseQuery))
  297 + .collect(Collectors.toList());
  298 + log.debug("Search by job title '{}' found {} results.", jobTitleQuery, results.size());
  299 + return Collections.unmodifiableList(results);
  300 + }
  301 +
  302 + @Override
  303 + @Tool(
  304 + name = "ps_filter_by_sex",
  305 + description = "Filters Persons by sex (case-insensitive)."
  306 + )
  307 + public List<Person> filterBySex(String sex) {
  308 + if (sex == null || sex.isBlank()) {
  309 + log.debug("Filter by sex skipped due to blank filter.");
  310 + return Collections.emptyList();
  311 + }
  312 + List<Person> results = personStore.values().stream()
  313 + .filter(person -> person.sex() != null && person.sex().equalsIgnoreCase(sex))
  314 + .collect(Collectors.toList());
  315 + log.debug("Filter by sex '{}' found {} results.", sex, results.size());
  316 + return Collections.unmodifiableList(results);
  317 + }
  318 +
  319 + @Override
  320 + @Tool(
  321 + name = "ps_filter_by_age",
  322 + description = "Filters Persons by age."
  323 + )
  324 + public List<Person> filterByAge(int age) {
  325 + if (age < 0) {
  326 + log.debug("Filter by age skipped due to negative age: {}", age);
  327 + return Collections.emptyList(); // Or throw IllegalArgumentException based on requirements
  328 + }
  329 + List<Person> results = personStore.values().stream()
  330 + .filter(person -> person.age() == age)
  331 + .collect(Collectors.toList());
  332 + log.debug("Filter by age {} found {} results.", age, results.size());
  333 + return Collections.unmodifiableList(results);
  334 + }
  335 +
  336 +}
0 337 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java 0 → 100644
  1 +/**
  2 + * 参考 <a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Tool Calling —— Methods as Tools</a>
  3 + */
  4 +package cn.iocoder.yudao.module.ai.tool.method;
0 5 \ No newline at end of file
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -196,6 +196,13 @@ spring:
196 196 model: deepseek-chat
197 197 model:
198 198 rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启
  199 + mcp:
  200 + server:
  201 + enabled: true
  202 + name: yudao-mcp-server
  203 + version: 1.0.0
  204 + instructions: 一个 MCP 示例服务
  205 + sse-endpoint: /sse
199 206  
200 207 yudao:
201 208 ai:
... ...