diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml
index b81be3aa42..04df6fac0d 100644
--- a/yudao-module-ai/pom.xml
+++ b/yudao-module-ai/pom.xml
@@ -187,6 +187,12 @@
+
+ org.springframework.ai
+ spring-ai-starter-mcp-server-webmvc
+ ${spring-ai.version}
+
+
dev.tinyflow
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java
index 7773e978cc..71322132f3 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java
@@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.model;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import cn.iocoder.yudao.module.ai.service.model.tool.DirectoryListToolFunction;
-import cn.iocoder.yudao.module.ai.service.model.tool.WeatherQueryToolFunction;
+import cn.iocoder.yudao.module.ai.tool.function.DirectoryListToolFunction;
+import cn.iocoder.yudao.module.ai.tool.function.WeatherQueryToolFunction;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java
index 6fd62bcabd..26fbe0ad41 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java
+++ b/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;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
+import cn.iocoder.yudao.module.ai.tool.method.PersonService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
@@ -25,8 +26,10 @@ import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
+import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
@@ -36,6 +39,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import java.util.List;
+
/**
* 芋道 AI 自动配置
*
@@ -271,4 +276,14 @@ public class AiAutoConfiguration {
return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey());
}
+ // ========== MCP 相关 ==========
+
+ /**
+ * 参考自 MCP Server Boot Starter>
+ */
+ @Bean
+ public List toolCallbacks(PersonService personService) {
+ return List.of(ToolCallbacks.from(personService));
+ }
+
}
\ No newline at end of file
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java
new file mode 100644
index 0000000000..bd13a2c146
--- /dev/null
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.ai.framework.security.config;
+
+import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
+import jakarta.annotation.Resource;
+import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
+
+/**
+ * AI 模块的 Security 配置
+ */
+@Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration")
+public class SecurityConfiguration {
+
+ @Resource
+ private McpServerProperties serverProperties;
+
+ @Bean("aiAuthorizeRequestsCustomizer")
+ public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
+ return new AuthorizeRequestsCustomizer() {
+
+ @Override
+ public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) {
+ // MCP Server
+ registry.requestMatchers(serverProperties.getSseEndpoint()).permitAll();
+ registry.requestMatchers(serverProperties.getSseMessageEndpoint()).permitAll();
+ }
+
+ };
+ }
+
+}
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java
new file mode 100644
index 0000000000..87969449d8
--- /dev/null
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 占位
+ */
+package cn.iocoder.yudao.module.ai.framework.security.core;
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java
similarity index 98%
rename from yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java
rename to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java
index 787b2e7728..8e75d5d9e0 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java
@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.ai.service.model.tool;
+package cn.iocoder.yudao.module.ai.tool.function;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java
similarity index 97%
rename from yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java
rename to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java
index 5656d39292..a4e00d644f 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java
@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.ai.service.model.tool;
+package cn.iocoder.yudao.module.ai.tool.function;
import cn.iocoder.yudao.module.ai.util.AiUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java
similarity index 98%
rename from yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java
rename to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java
index 99262fafad..689ea00460 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java
@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.ai.service.model.tool;
+package cn.iocoder.yudao.module.ai.tool.function;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.RandomUtil;
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java
new file mode 100644
index 0000000000..0b59656352
--- /dev/null
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 参考 Tool Calling —— Methods as Tools
+ */
+package cn.iocoder.yudao.module.ai.tool.function;
\ No newline at end of file
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java
new file mode 100644
index 0000000000..66bab5a7fc
--- /dev/null
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.ai.tool.method;
+
+/**
+ * 来自 Spring AI 官方文档
+ *
+ * Represents a person with basic information.
+ * This is an immutable record.
+ */
+public record Person(
+ int id,
+ String firstName,
+ String lastName,
+ String email,
+ String sex,
+ String ipAddress,
+ String jobTitle,
+ int age
+) {
+}
\ No newline at end of file
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java
new file mode 100644
index 0000000000..52c8954945
--- /dev/null
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java
@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.module.ai.tool.method;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 来自 Spring AI 官方文档
+ *
+ * Service interface for managing Person data.
+ * Defines the contract for CRUD operations and search/filter functionalities.
+ */
+public interface PersonService {
+
+ /**
+ * Creates a new Person record.
+ * Assigns a unique ID to the person and stores it.
+ *
+ * @param personData The data for the new person (ID field is ignored). Must not be null.
+ * @return The created Person record, including the generated ID.
+ */
+ Person createPerson(Person personData);
+
+ /**
+ * Retrieves a Person by their unique ID.
+ *
+ * @param id The ID of the person to retrieve.
+ * @return An Optional containing the found Person, or an empty Optional if not found.
+ */
+ Optional getPersonById(int id);
+
+ /**
+ * Retrieves all Person records currently stored.
+ *
+ * @return An unmodifiable List containing all Persons. Returns an empty list if none exist.
+ */
+ List getAllPersons();
+
+ /**
+ * Updates an existing Person record identified by ID.
+ * Replaces the existing data with the provided data, keeping the original ID.
+ *
+ * @param id The ID of the person to update.
+ * @param updatedPersonData The new data for the person (ID field is ignored). Must not be null.
+ * @return true if the person was found and updated, false otherwise.
+ */
+ boolean updatePerson(int id, Person updatedPersonData);
+
+ /**
+ * Deletes a Person record identified by ID.
+ *
+ * @param id The ID of the person to delete.
+ * @return true if the person was found and deleted, false otherwise.
+ */
+ boolean deletePerson(int id);
+
+ /**
+ * Searches for Persons whose job title contains the given query string (case-insensitive).
+ *
+ * @param jobTitleQuery The string to search for within job titles. Can be null or blank.
+ * @return An unmodifiable List of matching Persons. Returns an empty list if no matches or query is invalid.
+ */
+ List searchByJobTitle(String jobTitleQuery);
+
+ /**
+ * Filters Persons by their exact sex (case-insensitive).
+ *
+ * @param sex The sex to filter by (e.g., "Male", "Female"). Can be null or blank.
+ * @return An unmodifiable List of matching Persons. Returns an empty list if no matches or filter is invalid.
+ */
+ List filterBySex(String sex);
+
+ /**
+ * Filters Persons by their exact age.
+ *
+ * @param age The age to filter by.
+ * @return An unmodifiable List of matching Persons. Returns an empty list if no matches.
+ */
+ List filterByAge(int age);
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java
new file mode 100644
index 0000000000..3b8c31b420
--- /dev/null
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java
@@ -0,0 +1,336 @@
+package cn.iocoder.yudao.module.ai.tool.method;
+
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 来自 Spring AI 官方文档
+ *
+ * Implementation of the PersonService interface using an in-memory data store.
+ * Manages a collection of Person objects loaded from embedded CSV data.
+ * This class is thread-safe due to the use of ConcurrentHashMap and AtomicInteger.
+ */
+@Service
+@Slf4j
+public class PersonServiceImpl implements PersonService {
+
+ private final Map personStore = new ConcurrentHashMap<>();
+
+ private AtomicInteger idGenerator;
+
+ /**
+ * Embedded CSV data for initial population
+ */
+ private static final String CSV_DATA = """
+ Id,FirstName,LastName,Email,Sex,IpAddress,JobTitle,Age
+ 1,Fons,Tollfree,ftollfree0@senate.gov,Male,55.1 Tollfree Lane,Research Associate,31
+ 2,Emlynne,Tabourier,etabourier1@networksolutions.com,Female,18 Tabourier Way,Associate Professor,38
+ 3,Shae,Johncey,sjohncey2@yellowpages.com,Male,1 Johncey Circle,Structural Analysis Engineer,30
+ 4,Sebastien,Bradly,sbradly3@mapquest.com,Male,2 Bradly Hill,Chief Executive Officer,40
+ 5,Harriott,Kitteringham,hkitteringham4@typepad.com,Female,3 Kitteringham Drive,VP Sales,47
+ 6,Anallise,Parradine,aparradine5@miibeian.gov.cn,Female,4 Parradine Street,Analog Circuit Design manager,44
+ 7,Gorden,Kirkbright,gkirkbright6@reuters.com,Male,5 Kirkbright Plaza,Senior Editor,40
+ 8,Veradis,Ledwitch,vledwitch7@google.com.au,Female,6 Ledwitch Avenue,Computer Systems Analyst IV,44
+ 9,Agnesse,Penhalurick,apenhalurick8@google.it,Female,7 Penhalurick Terrace,Automation Specialist IV,41
+ 10,Bibby,Hutable,bhutable9@craigslist.org,Female,8 Hutable Place,Account Representative I,43
+ 11,Karoly,Lightoller,klightollera@rakuten.co.jp,Female,9 Lightoller Parkway,Senior Developer,46
+ 12,Cristine,Durrad,cdurradb@aol.com,Female,10 Durrad Center,Senior Developer,48
+ 13,Aggy,Napier,anapierc@hostgator.com,Female,11 Napier Court,VP Product Management,44
+ 14,Prisca,Caddens,pcaddensd@vinaora.com,Female,12 Caddens Alley,Business Systems Development Analyst,41
+ 15,Khalil,McKernan,kmckernane@google.fr,Male,13 McKernan Pass,Engineer IV,44
+ 16,Lorry,MacTrusty,lmactrustyf@eventbrite.com,Male,14 MacTrusty Junction,Design Engineer,42
+ 17,Casandra,Worsell,cworsellg@goo.gl,Female,15 Worsell Point,Systems Administrator IV,45
+ 18,Ulrikaumeko,Haveline,uhavelineh@usgs.gov,Female,16 Haveline Trail,Financial Advisor,42
+ 19,Shurlocke,Albany,salbanyi@artisteer.com,Male,17 Albany Plaza,Software Test Engineer III,46
+ 20,Myrilla,Brimilcombe,mbrimilcombej@accuweather.com,Female,18 Brimilcombe Road,Programmer Analyst I,48
+ 21,Carlina,Scimonelli,cscimonellik@va.gov,Female,19 Scimonelli Pass,Help Desk Technician,45
+ 22,Tina,Goullee,tgoulleel@miibeian.gov.cn,Female,20 Goullee Crossing,Accountant IV,43
+ 23,Adriaens,Storek,astorekm@devhub.com,Female,21 Storek Avenue,Recruiting Manager,40
+ 24,Tedra,Giraudot,tgiraudotn@wiley.com,Female,22 Giraudot Terrace,Speech Pathologist,47
+ 25,Josiah,Soares,jsoareso@google.nl,Male,23 Soares Street,Tax Accountant,45
+ 26,Kayle,Gaukrodge,kgaukrodgep@wikispaces.com,Female,24 Gaukrodge Parkway,Accountant II,43
+ 27,Ardys,Chuter,achuterq@ustream.tv,Female,25 Chuter Drive,Engineer IV,41
+ 28,Francyne,Baudinet,fbaudinetr@newyorker.com,Female,26 Baudinet Center,VP Accounting,48
+ 29,Gerick,Bullan,gbullans@seesaa.net,Male,27 Bullan Way,Senior Financial Analyst,43
+ 30,Northrup,Grivori,ngrivorit@unc.edu,Male,28 Grivori Plaza,Systems Administrator I,45
+ 31,Town,Duguid,tduguidu@squarespace.com,Male,29 Duguid Pass,Safety Technician IV,46
+ 32,Pierette,Kopisch,pkopischv@google.com.br,Female,30 Kopisch Lane,Director of Sales,41
+ 33,Jacquenetta,Le Prevost,jleprevostw@netlog.com,Female,31 Le Prevost Trail,Senior Developer,47
+ 34,Garvy,Rusted,grustedx@aboutads.info,Male,32 Rusted Junction,Senior Developer,42
+ 35,Clarice,Aysh,cayshy@merriam-webster.com,Female,33 Aysh Avenue,VP Quality Control,40
+ 36,Tracie,Fedorski,tfedorskiz@bloglines.com,Male,34 Fedorski Terrace,Design Engineer,44
+ 37,Noelyn,Matushenko,nmatushenko10@globo.com,Female,35 Matushenko Place,VP Sales,48
+ 38,Rudiger,Klaesson,rklaesson11@usnews.com,Male,36 Klaesson Road,Database Administrator IV,43
+ 39,Mirella,Syddie,msyddie12@geocities.jp,Female,37 Syddie Circle,Geological Engineer,46
+ 40,Donalt,O'Lunny,dolunny13@elpais.com,Male,38 O'Lunny Center,Analog Circuit Design manager,41
+ 41,Guntar,Deniskevich,gdeniskevich14@google.com.hk,Male,39 Deniskevich Way,Structural Engineer,47
+ 42,Hort,Shufflebotham,hshufflebotham15@about.me,Male,40 Shufflebotham Court,Structural Analysis Engineer,45
+ 43,Dominique,Thickett,dthickett16@slashdot.org,Male,41 Thickett Crossing,Safety Technician I,42
+ 44,Zebulen,Piscopello,zpiscopello17@umich.edu,Male,42 Piscopello Parkway,Web Developer II,40
+ 45,Mellicent,Mac Giany,mmacgiany18@state.tx.us,Female,43 Mac Giany Pass,Assistant Manager,44
+ 46,Merle,Bounds,mbounds19@amazon.co.jp,Female,44 Bounds Alley,Systems Administrator III,41
+ 47,Madelle,Farbrace,mfarbrace1a@xinhuanet.com,Female,45 Farbrace Terrace,Quality Engineer,48
+ 48,Galvin,O'Sheeryne,gosheeryne1b@addtoany.com,Male,46 O'Sheeryne Way,Environmental Specialist,43
+ 49,Guillemette,Bootherstone,gbootherstone1c@nationalgeographic.com,Female,47 Bootherstone Plaza,Professor,46
+ 50,Letti,Aylmore,laylmore1d@vinaora.com,Female,48 Aylmore Circle,Automation Specialist I,40
+ 51,Nonie,Rivalland,nrivalland1e@weather.com,Female,49 Rivalland Avenue,Software Test Engineer IV,45
+ 52,Jacquelynn,Halfacre,jhalfacre1f@surveymonkey.com,Female,50 Halfacre Pass,Geologist II,42
+ 53,Anderea,MacKibbon,amackibbon1g@weibo.com,Female,51 MacKibbon Parkway,Automation Specialist II,47
+ 54,Wash,Klimko,wklimko1h@slashdot.org,Male,52 Klimko Alley,Database Administrator I,40
+ 55,Flori,Kynett,fkynett1i@auda.org.au,Female,53 Kynett Trail,Quality Control Specialist,46
+ 56,Libbey,Penswick,lpenswick1j@google.co.uk,Female,54 Penswick Point,VP Accounting,43
+ 57,Silvanus,Skellorne,sskellorne1k@booking.com,Male,55 Skellorne Drive,Account Executive,48
+ 58,Carmine,Mateos,cmateos1l@plala.or.jp,Male,56 Mateos Terrace,Systems Administrator I,41
+ 59,Sheffie,Blazewicz,sblazewicz1m@google.com.au,Male,57 Blazewicz Center,VP Sales,44
+ 60,Leanor,Worsnop,lworsnop1n@uol.com.br,Female,58 Worsnop Plaza,Systems Administrator III,45
+ 61,Caspar,Pamment,cpamment1o@google.co.jp,Male,59 Pamment Court,Senior Financial Analyst,42
+ 62,Justinian,Pentycost,jpentycost1p@sciencedaily.com,Male,60 Pentycost Way,Senior Quality Engineer,47
+ 63,Gerianne,Jarnell,gjarnell1q@bing.com,Female,61 Jarnell Avenue,Help Desk Operator,40
+ 64,Boycie,Zanetto,bzanetto1r@about.com,Male,62 Zanetto Place,Quality Engineer,46
+ 65,Camilla,Mac Giany,cmacgiany1s@state.gov,Female,63 Mac Giany Parkway,Senior Cost Accountant,43
+ 66,Hadlee,Piscopiello,hpiscopiello1t@artisteer.com,Male,64 Piscopiello Street,Account Representative III,48
+ 67,Bobbie,Penvarden,bpenvarden1u@google.cn,Male,65 Penvarden Lane,Help Desk Operator,41
+ 68,Ali,Gowlett,agowlett1v@parallels.com,Male,66 Gowlett Pass,VP Marketing,44
+ 69,Olivette,Acome,oacome1w@qq.com,Female,67 Acome Hill,VP Product Management,45
+ 70,Jehanna,Brotherheed,jbrotherheed1x@google.nl,Female,68 Brotherheed Junction,Database Administrator III,42
+ 71,Morgan,Berthomieu,mberthomieu1y@artisteer.com,Male,69 Berthomieu Alley,Systems Administrator II,47
+ 72,Linzy,Shilladay,lshilladay1z@icq.com,Female,70 Shilladay Trail,Research Assistant IV,40
+ 73,Faydra,Brimner,fbrimner20@mozilla.org,Female,71 Brimner Road,Senior Editor,46
+ 74,Gwenore,Oxlee,goxlee21@devhub.com,Female,72 Oxlee Terrace,Systems Administrator II,43
+ 75,Evangelin,Beinke,ebeinke22@mozilla.com,Female,73 Beinke Circle,Accountant I,48
+ 76,Missy,Cockling,mcockling23@si.edu,Female,74 Cockling Way,Software Engineer I,41
+ 77,Suzanne,Klimschak,sklimschak24@etsy.com,Female,75 Klimschak Plaza,Tax Accountant,44
+ 78,Candide,Goricke,cgoricke25@weebly.com,Female,76 Goricke Pass,Sales Associate,45
+ 79,Gerome,Pinsent,gpinsent26@google.com.au,Male,77 Pinsent Junction,Software Consultant,42
+ 80,Lezley,Mac Giany,lmacgiany27@scribd.com,Male,78 Mac Giany Alley,Operator,47
+ 81,Tobiah,Durn,tdurn28@state.tx.us,Male,79 Durn Court,VP Sales,40
+ 82,Sherlocke,Cockshoot,scockshoot29@yelp.com,Male,80 Cockshoot Street,Senior Financial Analyst,46
+ 83,Myrle,Speenden,mspeenden2a@utexas.edu,Female,81 Speenden Center,Senior Developer,43
+ 84,Isidore,Gorries,igorries2b@flavors.me,Male,82 Gorries Parkway,Sales Representative,48
+ 85,Isac,Kitchingman,ikitchingman2c@businessinsider.com,Male,83 Kitchingman Drive,VP Accounting,41
+ 86,Benedetta,Purrier,bpurrier2d@admin.ch,Female,84 Purrier Trail,VP Accounting,44
+ 87,Tera,Fitchell,tfitchell2e@fotki.com,Female,85 Fitchell Place,Software Engineer IV,45
+ 88,Abbe,Pamment,apamment2f@about.com,Male,86 Pamment Avenue,VP Sales,42
+ 89,Jandy,Gommowe,jgommowe2g@angelfire.com,Female,87 Gommowe Road,Financial Analyst,47
+ 90,Karena,Fussey,kfussey2h@google.com.au,Female,88 Fussey Point,Assistant Professor,40
+ 91,Gaspar,Pammenter,gpammenter2i@google.com.br,Male,89 Pammenter Hill,Help Desk Operator,46
+ 92,Stanwood,Mac Giany,smacgiany2j@prlog.org,Male,90 Mac Giany Terrace,Research Associate,43
+ 93,Byrom,Beedell,bbeedell2k@google.co.jp,Male,91 Beedell Way,VP Sales,48
+ 94,Annabella,Rowbottom,arowbottom2l@google.com.au,Female,92 Rowbottom Plaza,Help Desk Operator,41
+ 95,Rodolphe,Debell,rdebell2m@imageshack.us,Male,93 Debell Pass,Design Engineer,44
+ 96,Tyne,Gommey,tgommey2n@joomla.org,Female,94 Gommey Junction,VP Marketing,45
+ 97,Christoper,Pincked,cpincked2o@icq.com,Male,95 Pincked Alley,Human Resources Manager,42
+ 98,Kore,Le Prevost,kleprevost2p@tripadvisor.com,Female,96 Le Prevost Street,VP Quality Control,47
+ 99,Ceciley,Petrolli,cpetrolli2q@oaic.gov.au,Female,97 Petrolli Court,Senior Developer,40
+ 100,Elspeth,Mac Giany,emacgiany2r@icio.us,Female,98 Mac Giany Parkway,Internal Auditor,46
+ """;
+
+ /**
+ * Initializes the service after dependency injection by loading data from the CSV string.
+ * Uses @PostConstruct to ensure this runs after the bean is created.
+ */
+ @PostConstruct
+ private void initializeData() {
+ log.info("Initializing PersonService data store...");
+ int maxId = loadDataFromCsv();
+ idGenerator = new AtomicInteger(maxId);
+ log.info("PersonService initialized with {} records. Next ID: {}", personStore.size(), idGenerator.get() + 1);
+ }
+
+ /**
+ * Parses the embedded CSV data and populates the in-memory store.
+ * Calculates the maximum ID found in the data to initialize the ID generator.
+ *
+ * @return The maximum ID found in the loaded CSV data.
+ */
+ private int loadDataFromCsv() {
+ final AtomicInteger currentMaxId = new AtomicInteger(0);
+ // Clear existing data before loading (important for tests or re-initialization scenarios)
+ personStore.clear();
+ try (Stream lines = CSV_DATA.lines().skip(1)) { // Skip header row
+ lines.forEach(line -> {
+ try {
+ // Split carefully, handling potential commas within quoted fields if necessary (simple split here)
+ String[] fields = line.split(",", 8); // Limit split to handle potential commas in job title
+ if (fields.length == 8) {
+ int id = Integer.parseInt(fields[0].trim());
+ String firstName = fields[1].trim();
+ String lastName = fields[2].trim();
+ String email = fields[3].trim();
+ String sex = fields[4].trim();
+ String ipAddress = fields[5].trim();
+ String jobTitle = fields[6].trim();
+ int age = Integer.parseInt(fields[7].trim());
+
+ Person person = new Person(id, firstName, lastName, email, sex, ipAddress, jobTitle, age);
+ personStore.put(id, person);
+ currentMaxId.updateAndGet(max -> Math.max(max, id));
+ } else {
+ log.warn("Skipping malformed CSV line (expected 8 fields, found {}): {}", fields.length, line);
+ }
+ } catch (NumberFormatException e) {
+ log.warn("Skipping line due to parsing error (ID or Age): {} - Error: {}", line, e.getMessage());
+ } catch (Exception e) {
+ log.error("Skipping line due to unexpected error: {} - Error: {}", line, e.getMessage(), e);
+ }
+ });
+ } catch (Exception e) {
+ log.error("Fatal error reading embedded CSV data: {}", e.getMessage(), e);
+ // In a real application, might throw a specific initialization exception
+ }
+ return currentMaxId.get();
+ }
+
+ @Override
+ @Tool(
+ name = "ps_create_person",
+ description = "Create a new person record in the in-memory store."
+ )
+ public Person createPerson(Person personData) {
+ if (personData == null) {
+ throw new IllegalArgumentException("Person data cannot be null");
+ }
+ int newId = idGenerator.incrementAndGet();
+ // Create a new Person record using data from the input, but with the generated ID
+ Person newPerson = new Person(
+ newId,
+ personData.firstName(),
+ personData.lastName(),
+ personData.email(),
+ personData.sex(),
+ personData.ipAddress(),
+ personData.jobTitle(),
+ personData.age()
+ );
+ personStore.put(newId, newPerson);
+ log.debug("Created person: {}", newPerson);
+ return newPerson;
+ }
+
+ @Override
+ @Tool(
+ name = "ps_get_person_by_id",
+ description = "Retrieve a person record by ID from the in-memory store."
+ )
+ public Optional getPersonById(int id) {
+ Person person = personStore.get(id);
+ log.debug("Retrieved person by ID {}: {}", id, person);
+ return Optional.ofNullable(person);
+ }
+
+ @Override
+ @Tool(
+ name = "ps_get_all_persons",
+ description = "Retrieve all person records from the in-memory store."
+ )
+ public List getAllPersons() {
+ // Return an unmodifiable view of the values
+ List allPersons = personStore.values().stream().toList();
+ log.debug("Retrieved all persons (count: {})", allPersons.size());
+ return allPersons;
+ }
+
+ @Override
+ @Tool(
+ name = "ps_update_person",
+ description = "Update an existing person record by ID in the in-memory store."
+ )
+ public boolean updatePerson(int id, Person updatedPersonData) {
+ if (updatedPersonData == null) {
+ throw new IllegalArgumentException("Updated person data cannot be null");
+ }
+ // Use computeIfPresent for atomic update if the key exists
+ Person result = personStore.computeIfPresent(id, (key, existingPerson) ->
+ // Create a new Person record with the original ID but updated data
+ new Person(
+ id, // Keep original ID
+ updatedPersonData.firstName(),
+ updatedPersonData.lastName(),
+ updatedPersonData.email(),
+ updatedPersonData.sex(),
+ updatedPersonData.ipAddress(),
+ updatedPersonData.jobTitle(),
+ updatedPersonData.age()
+ )
+ );
+ boolean updated = result != null;
+ log.debug("Update attempt for ID {}: {}", id, updated ? "Successful" : "Failed (Not Found)");
+ if(updated) log.trace("Updated person data for ID {}: {}", id, result);
+ return updated;
+ }
+
+ @Override
+ @Tool(
+ name = "ps_delete_person",
+ description = "Delete a person record by ID from the in-memory store."
+ )
+ public boolean deletePerson(int id) {
+ boolean removed = personStore.remove(id) != null;
+ log.debug("Delete attempt for ID {}: {}", id, removed ? "Successful" : "Failed (Not Found)");
+ return removed;
+ }
+
+ @Override
+ @Tool(
+ name = "ps_search_by_job_title",
+ description = "Search for persons by job title in the in-memory store."
+ )
+ public List searchByJobTitle(String jobTitleQuery) {
+ if (jobTitleQuery == null || jobTitleQuery.isBlank()) {
+ log.debug("Search by job title skipped due to blank query.");
+ return Collections.emptyList();
+ }
+ String lowerCaseQuery = jobTitleQuery.toLowerCase();
+ List results = personStore.values().stream()
+ .filter(person -> person.jobTitle() != null && person.jobTitle().toLowerCase().contains(lowerCaseQuery))
+ .collect(Collectors.toList());
+ log.debug("Search by job title '{}' found {} results.", jobTitleQuery, results.size());
+ return Collections.unmodifiableList(results);
+ }
+
+ @Override
+ @Tool(
+ name = "ps_filter_by_sex",
+ description = "Filters Persons by sex (case-insensitive)."
+ )
+ public List filterBySex(String sex) {
+ if (sex == null || sex.isBlank()) {
+ log.debug("Filter by sex skipped due to blank filter.");
+ return Collections.emptyList();
+ }
+ List results = personStore.values().stream()
+ .filter(person -> person.sex() != null && person.sex().equalsIgnoreCase(sex))
+ .collect(Collectors.toList());
+ log.debug("Filter by sex '{}' found {} results.", sex, results.size());
+ return Collections.unmodifiableList(results);
+ }
+
+ @Override
+ @Tool(
+ name = "ps_filter_by_age",
+ description = "Filters Persons by age."
+ )
+ public List filterByAge(int age) {
+ if (age < 0) {
+ log.debug("Filter by age skipped due to negative age: {}", age);
+ return Collections.emptyList(); // Or throw IllegalArgumentException based on requirements
+ }
+ List results = personStore.values().stream()
+ .filter(person -> person.age() == age)
+ .collect(Collectors.toList());
+ log.debug("Filter by age {} found {} results.", age, results.size());
+ return Collections.unmodifiableList(results);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java
new file mode 100644
index 0000000000..44b53e1974
--- /dev/null
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 参考 Tool Calling —— Methods as Tools
+ */
+package cn.iocoder.yudao.module.ai.tool.method;
\ No newline at end of file
diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml
index 1468dc2019..7a04290cad 100644
--- a/yudao-server/src/main/resources/application.yaml
+++ b/yudao-server/src/main/resources/application.yaml
@@ -196,6 +196,13 @@ spring:
model: deepseek-chat
model:
rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启
+ mcp:
+ server:
+ enabled: true
+ name: yudao-mcp-server
+ version: 1.0.0
+ instructions: 一个 MCP 示例服务
+ sse-endpoint: /sse
yudao:
ai: