├── .gitattributes ├── .gitignore ├── README.md ├── app.log ├── data └── spring-ai-mcp-overview.txt ├── docs ├── pgvector.sh └── vector_knowledge.sql ├── pom.xml └── src ├── main ├── java │ └── cn │ │ └── onism │ │ └── mcp │ │ ├── McpDemoApplication.java │ │ ├── annotations │ │ └── McpTool.java │ │ ├── common │ │ ├── Result.java │ │ └── package-info.java │ │ ├── config │ │ ├── ChatClientConfig.java │ │ ├── McpConfig.java │ │ ├── RagEmbeddingConfig.java │ │ └── package-info.java │ │ ├── constants │ │ ├── ChatClientOption.java │ │ ├── CodeEnum.java │ │ └── package-info.java │ │ ├── controller │ │ ├── McpController.java │ │ ├── RagController.java │ │ └── package-info.java │ │ ├── entity │ │ ├── SearchResult.java │ │ ├── SearxngResponse.java │ │ ├── VectorRelation.java │ │ └── package-info.java │ │ ├── exception │ │ ├── CustomException.java │ │ └── package-info.java │ │ ├── handler │ │ ├── AllExceptionHandler.java │ │ └── package-info.java │ │ ├── repository │ │ ├── VectorRelationRepository.java │ │ └── package-info.java │ │ ├── service │ │ ├── DocumentService.java │ │ ├── VectorRelationService.java │ │ ├── package-info.java │ │ └── search │ │ │ ├── InternetSearchService.java │ │ │ └── SearxngSearchService.java │ │ ├── tool │ │ ├── DateTool.java │ │ ├── EmailTool.java │ │ ├── FileTool.java │ │ ├── InternetSearchTool.java │ │ ├── MonitorTool.java │ │ ├── database │ │ │ ├── DatabaseTool.java │ │ │ ├── manage │ │ │ │ └── DataSourceManager.java │ │ │ └── strategy │ │ │ │ ├── AbstractDataSourceStrategy.java │ │ │ │ ├── DataSourceStrategy.java │ │ │ │ ├── MySQLStrategy.java │ │ │ │ ├── OracleStrategy.java │ │ │ │ ├── PostgreSQLStrategy.java │ │ │ │ └── config │ │ │ │ └── DataSourceProperties.java │ │ └── package-info.java │ │ └── util │ │ ├── package-info.java │ │ └── prompt │ │ └── InternetSearchPromptBuilder.java └── resources │ ├── .env │ ├── application.yml │ └── static │ ├── getDatabase.png │ ├── getFile.png │ ├── getTime.png │ ├── inquire.png │ ├── introduce.png │ ├── mcpSearch.png │ ├── monitor1.png │ ├── monitor2.png │ ├── ragSearch.png │ ├── senEmail.png │ ├── uploadFile.png │ ├── vectorKnowledge.png │ └── vectorRelation.png └── test └── java └── cn └── onism └── mcp ├── McpDemoApplicationTests.java └── RagEmbeddingTests.java /.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | app.log 3 | .env 4 | target/ 5 | !.mvn/wrapper/maven-wrapper.jar 6 | !**/src/main/**/target/ 7 | !**/src/test/**/target/ 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | 18 | ### IntelliJ IDEA ### 19 | .idea 20 | *.iws 21 | *.iml 22 | *.ipr 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | build/ 31 | !**/src/main/**/build/ 32 | !**/src/test/**/build/ 33 | 34 | ### VS Code ### 35 | .vscode/ 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP Demo 2 | 3 | [![Java Version](https://img.shields.io/badge/java-17%2B-orange?logo=java)](https://openjdk.org/) 4 | [![Spring Boot](https://img.shields.io/badge/spring%20boot-3.4.4-brightgreen)](https://spring.io/projects/spring-boot) 5 | [![Spring AI](https://img.shields.io/badge/Spring%20AI-1.0.0%20M6-green?logo=spring)](https://docs.spring.io/spring-ai/reference/index.html) 6 | [![Postgres PgVector](https://img.shields.io/badge/postgres-pgvector-blue?logo=postgresql)](https://github.com/pgvector/pgvector) 7 | [![SearXNG](https://img.shields.io/badge/search-searXNG-blue?logo=searxng)](https://github.com/searxng/searxng) 8 | 9 | 最近 Spring AI 发布了 1.0.0-M6,引入了一个新特性`MCP`(Model Context Protocol),关于这个概念在这里就不过多赘述,文档介绍的比较清楚:
10 | - [MCP 中文文档](https://mcp-docs.cn/quickstart) 11 | - [Spring AI](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html) 12 | 13 | 简单来说,本地部署的 LLM 或调用三方 AI API的功能是部分缺失的(无法联网,无法访问本地附件等),MCP 就是通过给大模型 LLM 提供各种各样的第三方工具(封装为工具类/函数),赋予大模型 LLM 各种各样的能力(例如,访问本地文件系统的能力、访问数据库的能力、发送邮件的能力等等)
14 | 15 | 跟着官方文档和网上的资料,结合着 AI (DeepSeek),自己写了个 Demo 玩玩( Server 与 Client 写在一起的)。 16 | ## 目录 17 | 18 | - [功能特性](#功能特性) 19 | - [技术栈](#技术栈) 20 | - [项目结构](#项目结构) 21 | - [快速开始](#快速开始) 22 | - [环境要求](#环境要求) 23 | - [安装步骤](#安装步骤) 24 | 25 | ## 功能特性 26 | 大模型方面采用 DeepSeek V3 模型(使用官方 API),通过整合自己封装的工具使其具备下面的能力: 27 | - 获取时间
28 | ![img.png](src/main/resources/static/getTime.png) 29 | - 读取本地文件系统
30 | (Spring AI Alibaba Examples 提供的样例,仓库:[https://github.com/springaialibaba/spring-ai-alibaba-examples/tree/main](https://github.com/springaialibaba/spring-ai-alibaba-examples/tree/main)) 31 | ![img.png](src/main/resources/static/getFile.png) 32 | - 数据库 SQL 操作(目前只允许查询)
33 | ![img.png](src/main/resources/static/getDatabase.png) 34 | - 发送邮件(给指定邮箱发送邮件) 35 | ![img.png](src/main/resources/static/senEmail.png) 36 | - 获取系统资源详情/使用率/监控系统
37 | (QQ邮箱渲染问题,可自动忽略...) 38 | ![img.png](src/main/resources/static/monitor1.png) 39 | ![img.png](src/main/resources/static/monitor2.png) 40 | - 整合 RAG、PgVector 向量库与本地大模型搭建知识库
41 | 创建一个`introduce.txt`文件,随便放一些大模型和网上搜不到的东西 42 | ![img.png](src/main/resources/static/introduce.png) 43 | 调用`/rag/upload`接口,将文件切割向量化上传至向量库
44 | ![img.png](src/main/resources/static/uploadFile.png) 45 | 查看数据库表中,已经上传成功
46 | ![img.png](src/main/resources/static/vectorKnowledge.png) 47 | ![img.png](src/main/resources/static/vectorRelation.png) 48 | 然后调用`/rag/inquire`接口,询问刚才上传上去的内容 49 | ![img.png](src/main/resources/static/inquire.png) 50 | 证明本地大模型(用的 nomic-embed-text )能够正确读取和处理知识库中的消息 51 | - 实时联网搜索功能
52 | 使用本地化部署 SearXNG 结合 Ollama 本地化部署的大模型(当然,也可以使用其他大模型的 API),实现本地隐私实时智能搜索
53 | 实现了两种方式的搜索: 54 | 1. 传统调取服务接口式搜索:封装了一个 `InternetSearchService` 服务类,通过封装请求先查询浏览器,将结果与问题封装为 Prompt 丢给大模型分析然后返回给用户答案(大模型被动处理结果,传统的函数式调用),接口:**`/rag/search`**、**`/rag/stream/search`**(流式输出)
55 | ![img.png](src/main/resources/static/ragSearch.png) 56 | 2. 封装一个工具类,将其注入到 MCP 中,实现大模型先查询自身数据集,无结果则主动进行联网搜索,处理并返回结果
57 | ![img.png](src/main/resources/static/mcpSearch.png) 58 | - ...(其他功能,可继续扩展) 59 | 60 | ## 技术栈 61 | 62 | - **后端框架**: Spring Boot 3.x / Spring AI 1.0.0-M6 63 | - **数据库**: MySQL 8.0 / PostgreSQL 14 / Oracle(使用多数据源策略模式,需要多少个数据源可自行添加相关数据源依赖即可) 64 | - **API 文档**: Swagger 3 65 | - **构建工具**: Maven 66 | - **搜素引擎**:[SearXNG](https://docs.searxng.org/) 67 | - **其他技术**: JDBC / JMX / Java Email / JPA 68 | 69 | # 项目结构 70 | ```text 71 | data/ # 数据文件 72 | docs/ # 所需文档(命令,SQL 文件等) 73 | src/ 74 | ├── main/ 75 | │ ├── java/ 76 | │ │ └── cn/onism/mcp/ 77 | │ │ ├── common/ # 通用类 78 | │ │ ├── config/ # 配置类 79 | │ │ ├── constants/ # 常量类 80 | │ │ ├── controller/ # REST API 81 | │ │ ├── entity/ # 实体类 82 | │ │ ├── exception/ # 自定义异常类 83 | │ │ ├── handler/ # 处理器类 84 | │ │ ├── repository/ # 仓储存储接口 85 | │ │ ├── service/ # 提供服务类 86 | │ │ ├── tool/ # (LLM)封装工具类 87 | │ │ ├── util/ # 工具类 88 | │ │ └── McpDemoApplication.java # 启动类 89 | │ └── resources/ 90 | │ ├── application.yml # 主配置文件 91 | │ ├── static/ # 静态资源 92 | │ └── .env/ # 环境配置文件 93 | └── test/ # 单元测试 94 | ``` 95 | 96 | ## 快速开始 97 | 98 | ### 环境要求 99 | 100 | - JDK 17+ 101 | - Maven 3.8+ 102 | - PostgreSQL(PgVector) 17 103 | - MySQL / 其他数据库 104 | - Git 105 | - SearXNG 106 | 107 | ### 安装步骤 108 | 109 | 1. 克隆仓库: 110 | ```bash 111 | git clone https://github.com/OnismExplorer/mcp-demo.git 112 | 113 | 2. 配置数据源 114 | ```yaml 115 | spring: 116 | datasources: 117 | datasource: 118 | - id: mysql 119 | type: mysql 120 | url: jdbc:mysql://localhost:5206/power_buckle?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8 121 | username: root 122 | password: 123456 123 | driver-class-name: com.mysql.cj.jdbc.Driver 124 | maxPoolSize: 15 125 | - id: postgres 126 | type: postgres 127 | url: jdbc:postgresql://localhost:5432/friends 128 | username: root 129 | password: 123456 130 | driver-class-name: org.postgresql.Driver 131 | max-pool-size: 10 132 | # 其他数据源,可继续添加,需要保证 id 全局唯一(数据源),后续与大模型对话会用到 133 | ``` 134 | 3. 配置邮箱(选配,用不到就不配)
135 | 修改 `resources` 中的 `.env` 环境变量文件,替换其中的 $$$ 内容 136 | ```dotenv 137 | EMAIL_ADDRESS=$$$EMAIL_ADDRESS$$$ 138 | EMAIL_PASSWORD=$$$EMAIL_PASSWORD$$$ 139 | ``` 140 | 如果用的不是 QQ 邮箱,则还需要修改`application.yml`文件内容(按照需要修改) 141 | ```yaml 142 | spring: 143 | mail: 144 | # 下面这个是QQ邮箱host , 企业邮箱 : smtp.exmail.qq.com 145 | host: smtp.qq.com 146 | # tencent mail port 这个是固定的 147 | port: 465 148 | properties: 149 | mail: 150 | smtp: 151 | socketFactory: 152 | port: 465 153 | class: javax.net.ssl.SSLSocketFactory 154 | ssl: 155 | enable: true 156 | ``` 157 | 4. 配置大模型 API
158 | 修改 `resources` 中的 `.env` 环境变量文件,用真实数据替换其中的 $$$ 内容 159 | ```dotenv 160 | AI_BASE_URL=$$$AI_BASE_URL$$$ 161 | # AI 密钥,可通过 https://platform.deepseek.com/api_keys 充值获取 deepseek 官方 api key 162 | AI_API_KEY=$$$AI_API_KEY$$$ 163 | # DeepSeek v3 聊天模型 164 | AI_MODEL=$$$AI_MODEL$$$ 165 | ``` 166 | 5. 配置本地部署大模型(可选:这里主要是) 167 | 推荐几个不错,可以本地适用 Ollama 部署的[嵌入式](https://zhuanlan.zhihu.com/p/164502624)模型: 168 | - **nomic-embed-text**:支持长上下文窗口(最高支持 8192 token),适合**语义搜索**、**文档聚类**等任务,最大支持向量 768 维度 169 | - 适用场景:长文本语义分析、大规模知识库检索 170 | - **mxbai-embed-large**:混合高精度优化模型,支持多语言,在语义搜索任务中表现优异,最大支持向量 1024 维度 171 | - 适用场景:多语言语义匹配、企业级高精度检索 172 | - **bge-m3**:多语言支持(支持中文、英文等),专门为多粒度语义任务设计,最大支持向量 1024 维度 173 | - 适用场景:跨语言文档检索、多粒度知识库构建 174 | - **snowflake-arctic-embed**:由 Snowflake 开发,优化了多语言和长文本处理,支持动态调整上下文窗口 175 | - 适用场景:企业级数据分析、多语言内容推荐 176 | - **all-minilm**:轻量级模型(参数较小,33M的大小才为 67 MB,非常迷你),适合低配置设备,支持基础语义嵌入任务 177 | - 适用场景:移动端应用、实时搜索 178 | 179 | 切换模型需要修改两个地方,一个是`application.yml`文件
180 | ```yaml 181 | ollama: 182 | embedding: 183 | options: 184 | num-batch: 1024 185 | num-ctx: 8192 # 上下文长度 token 数 186 | model: nomic-embed-text # 换成其他想用的模型 187 | vectorstore: 188 | pgvector: 189 | dimensions: 768 # 需要与表中向量维度一致(根据模型修改而修改) 190 | ``` 191 | 另一个是`/config/RagEmbeddingConfig` 中 192 | ```java 193 | @Bean 194 | public PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate) { 195 | //.... 196 | // 设置向量模型 197 | .defaultOptions(OllamaOptions.builder().model("nomic-embed-text") 198 | //... 199 | // 默认是 768 200 | .dimensions(768) 201 | //... 202 | } 203 | ``` 204 | 6. 部署 `SearXNG` 搜索引擎 205 | 这里不过多赘述,没有本地部署的可以参考下面的文章自行进行部署: 206 | - [本地部署 SearXNG](https://onism.cn/article?id=105) 207 | - [本地部署 SearXNG - CSDN 博客](https://blog.csdn.net/qq_73574147/article/details/147073524?spm=1001.2014.3001.5502) 208 | 209 | 相关 `application.yml` 配置如下 210 | ```yaml 211 | spring: 212 | ai: 213 | chat: 214 | client: 215 | type: ollama # 目前只有 ollama(默认) 、openai 两种 216 | websearch: 217 | searxng: 218 | url: "http://localhost:8088/search" # SearXNG 服务的 API 地址 219 | nums: 25 220 | ``` 221 | 7. 构建项目 222 | ```bash 223 | mvn clean install 224 | ``` 225 | 8. 运行应用 226 | ```bash 227 | java -jar target/mcp-demo-1.0-SNAPSHOT.jar 228 | ``` 229 | 9. 访问应用 230 | ```bash 231 | http://localhost:8089/chat?message=hi 232 | ``` 233 | -------------------------------------------------------------------------------- /app.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/app.log -------------------------------------------------------------------------------- /data/spring-ai-mcp-overview.txt: -------------------------------------------------------------------------------- 1 | Model Context Protocol (MCP) Java SDK 2 | 3 | A Java implementation of the Model Context Protocol specification, providing both synchronous and 4 | asynchronous clients for MCP server interactions. 5 | 6 | Overview: This SDK implements the Model Context Protocol, enabling seamless integration with AI models and tools through 7 | a standardized interface. It supports both synchronous and asynchronous communication patterns, making it suitable for 8 | various use cases and integration scenarios. 9 | 10 | Features 11 | 12 | Synchronous and Asynchronous client implementations 13 | Standard MCP operations support: 14 | Tool discovery and execution 15 | Resource management and templates 16 | Prompt handling and management 17 | Resource subscription system 18 | Server initialization and ping 19 | Stdio-based server transport 20 | Reactive programming support using Project Reactor -------------------------------------------------------------------------------- /docs/pgvector.sh: -------------------------------------------------------------------------------- 1 | # Docker 部署运行 PG 向量库容器 2 | # 需要将 /path/to/data 换成实际路径地址 3 | docker run -d --name pgvector -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=123456 -v /path/to/data:/var/lib/postgresql/data pgvector/pgvector:pg17 4 | -------------------------------------------------------------------------------- /docs/vector_knowledge.sql: -------------------------------------------------------------------------------- 1 | -- 创建数据库 2 | CREATE DATABASE ai_knowledge 3 | -- 指定所有者(替换为你的用户名) 4 | OWNER = root 5 | -- 设置字符编码为 UTF8 6 | ENCODING = 'UTF8' 7 | -- 使用干净的默认模板 8 | TEMPLATE = template0; 9 | 10 | -- 切换数据库 11 | \c ai_knowledge 12 | 13 | -- 启用 PG 向量库拓展 14 | CREATE EXTENSION IF NOT EXISTS vector; 15 | CREATE EXTENSION IF NOT EXISTS hstore; 16 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 17 | 18 | -- 创建知识库向量存储表 19 | CREATE TABLE IF NOT EXISTS vector_knowledge 20 | ( 21 | id uuid DEFAULT uuid_generate_v4( ) PRIMARY KEY, 22 | content TEXT NOT NULL, -- 存储解析后的文本内容 23 | metadata JSON, -- 存储元数据(如文件名等) 24 | embedding vector(768) -- 向量字段(维度需与嵌入模型匹配) 25 | ); 26 | 27 | -- 添加注释 28 | COMMENT ON TABLE vector_knowledge IS '知识库向量表'; 29 | COMMENT ON COLUMN vector_knowledge.id IS '唯一标识(UUID)'; 30 | COMMENT ON COLUMN vector_knowledge.content IS '存储解析后的文本内容'; 31 | COMMENT ON COLUMN vector_knowledge.metadata IS '存储元数据(如文件名等)'; 32 | COMMENT ON COLUMN vector_knowledge.embedding IS '向量字段(维度需与嵌入模型匹配)'; 33 | 34 | -- 创建 HNSW 索引(加速相似性搜索) 35 | CREATE INDEX ON vector_knowledge USING hnsw (embedding vector_cosine_ops); 36 | 37 | -- 创建向量知识库关系表 38 | CREATE TABLE IF NOT EXISTS vector_relation 39 | ( 40 | id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 41 | file_hash VARCHAR(64) NOT NULL, -- 文档哈希值 42 | file_name VARCHAR(255) NOT NULL, -- 文档名称 43 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- 创建时间 44 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -- 更新时间 45 | ); 46 | 47 | -- 创建单独索引 48 | CREATE INDEX IF NOT EXISTS idx_file_hash ON vector_relation (file_hash); 49 | 50 | -- 添加注释 51 | COMMENT ON TABLE vector_relation IS '向量知识库关系表'; 52 | COMMENT ON COLUMN vector_relation.id IS '自增ID'; 53 | COMMENT ON COLUMN vector_relation.file_hash IS '文档哈希值'; 54 | COMMENT ON COLUMN vector_relation.file_name IS '文档名称'; 55 | COMMENT ON COLUMN vector_relation.created_at IS '创建时间'; 56 | COMMENT ON COLUMN vector_relation.updated_at IS '更新时间'; 57 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.4 9 | 10 | 11 | cn.onism 12 | mcp-demo 13 | 1.0-SNAPSHOT 14 | mcp-demo 15 | mcp-demo 16 | 17 | 18 | 17 19 | 1.0.0-M6 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | org.springframework.ai 28 | spring-ai-mcp-client-webflux-spring-boot-starter 29 | 30 | 31 | 32 | org.springframework.ai 33 | spring-ai-mcp-server-webflux-spring-boot-starter 34 | 35 | 36 | org.springframework.ai 37 | spring-ai-openai-spring-boot-starter 38 | 39 | 40 | org.springframework.ai 41 | spring-ai-ollama-spring-boot-starter 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-test 46 | test 47 | 48 | 49 | 50 | io.github.cdimascio 51 | dotenv-java 52 | 3.2.0 53 | 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-data-jdbc 58 | 59 | 60 | com.mysql 61 | mysql-connector-j 62 | 8.3.0 63 | 64 | 65 | 66 | org.postgresql 67 | postgresql 68 | 42.7.5 69 | 70 | 71 | org.projectlombok 72 | lombok 73 | 74 | 75 | cn.hutool 76 | hutool-all 77 | 5.8.30 78 | 79 | 80 | com.github.oshi 81 | oshi-core 82 | 6.6.3 83 | 84 | 85 | 86 | org.springframework.boot 87 | spring-boot-starter-mail 88 | 89 | 90 | 91 | com.oracle.database.jdbc 92 | ojdbc11 93 | 23.5.0.24.07 94 | 95 | 96 | 97 | com.vladsch.flexmark 98 | flexmark-all 99 | 0.64.8 100 | 101 | 102 | 103 | org.springframework.ai 104 | spring-ai-tika-document-reader 105 | 106 | 107 | 108 | org.springframework.ai 109 | spring-ai-pgvector-store-spring-boot-starter 110 | 111 | 112 | org.springframework.boot 113 | spring-boot-starter-data-jpa 114 | 115 | 116 | 117 | com.squareup.okhttp3 118 | okhttp 119 | 4.12.0 120 | 121 | 122 | 123 | 124 | 125 | org.springframework.ai 126 | spring-ai-bom 127 | ${spring-ai.version} 128 | pom 129 | import 130 | 131 | 132 | 133 | 134 | 135 | Central Portal Snapshots 136 | central-portal-snapshots 137 | https://central.sonatype.com/repository/maven-snapshots/ 138 | 139 | false 140 | 141 | 142 | true 143 | 144 | 145 | 146 | spring-milestones 147 | Spring Milestones 148 | https://repo.spring.io/milestone 149 | 150 | false 151 | 152 | 153 | 154 | spring-snapshots 155 | Spring Snapshots 156 | https://repo.spring.io/snapshot 157 | 158 | false 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | org.springframework.boot 167 | spring-boot-maven-plugin 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/McpDemoApplication.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp; 2 | 3 | import cn.onism.mcp.tool.database.strategy.config.DataSourceProperties; 4 | import io.github.cdimascio.dotenv.Dotenv; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 8 | 9 | /** 10 | * MCP 客户端应用程序 11 | * 12 | * @author Onism 13 | * @date 2025-03-27 14 | */ 15 | @SpringBootApplication 16 | @EnableConfigurationProperties(DataSourceProperties.class) 17 | public class McpDemoApplication { 18 | 19 | public static void main(String[] args) { 20 | // 加载.env文件 21 | Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load(); 22 | // 将变量设置为系统属性 23 | dotenv.entries().forEach(e -> System.setProperty(e.getKey(), e.getValue())); 24 | SpringApplication.run(McpDemoApplication.class, args); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/annotations/McpTool.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * MCP 工具注解 10 | * 11 | * @author Onism 12 | * @date 2025-04-14 13 | */ 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.TYPE) 16 | public @interface McpTool { 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/common/Result.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.common; 2 | 3 | import cn.onism.mcp.constants.CodeEnum; 4 | import io.swagger.v3.oas.annotations.media.Schema; 5 | import lombok.Data; 6 | 7 | /** 8 | * 通用返回类 9 | */ 10 | @Data 11 | @Schema(name = "Result", title = "通用返回对象带泛型,用于传输简单对象") 12 | public class Result { 13 | 14 | @Schema(name = "code", title = "返回状态", example = "200") 15 | Integer code; 16 | @Schema(name = "message", title = "返回信息", example = "success") 17 | String message; 18 | @Schema(name = "data", title = "返回数据") 19 | T data; 20 | 21 | Result() { 22 | } 23 | 24 | public static Result fail() { 25 | return Result.fail(CodeEnum.FAIL); 26 | } 27 | 28 | public static Result fail(String message) { 29 | Result result = new Result<>(); 30 | result.code = CodeEnum.FAIL.getCode(); 31 | result.message = message; 32 | return result; 33 | } 34 | 35 | public static Result fail(CodeEnum codeMsg) { 36 | Result result = new Result<>(); 37 | result.code = codeMsg.getCode(); 38 | result.message = codeMsg.getMsg(); 39 | return result; 40 | } 41 | 42 | public static Result fail(Integer code, String message) { 43 | Result result = new Result<>(); 44 | result.code = code; 45 | result.message = message; 46 | return result; 47 | } 48 | 49 | public static Result success(T data) { 50 | Result result = new Result<>(); 51 | result.code = CodeEnum.SUCCESS.getCode(); 52 | result.data = data; 53 | return result; 54 | } 55 | 56 | public static Result isSuccess(Boolean isSuccess) { 57 | if (Boolean.TRUE.equals(isSuccess)) { 58 | return success(); 59 | } else { 60 | return fail(); 61 | } 62 | } 63 | 64 | public static Result success() { 65 | Result result = new Result<>(); 66 | result.message = CodeEnum.SUCCESS.getMsg(); 67 | result.code = CodeEnum.SUCCESS.getCode(); 68 | return result; 69 | } 70 | 71 | /** 72 | * 返回结果 73 | */ 74 | public static Result result(CodeEnum codeEnum) { 75 | return new Result().message(codeEnum.getMsg()).code(codeEnum.getCode()); 76 | } 77 | 78 | public Result message(String message) { 79 | this.message = message; 80 | return this; 81 | } 82 | 83 | public Result code(int code) { 84 | this.code = code; 85 | return this; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/common/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 通用类 3 | **/ 4 | package cn.onism.mcp.common; 5 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/config/ChatClientConfig.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.config; 2 | 3 | import cn.onism.mcp.constants.ChatClientOption; 4 | import jakarta.annotation.Resource; 5 | import org.springframework.ai.chat.client.ChatClient; 6 | import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; 7 | import org.springframework.ai.chat.memory.InMemoryChatMemory; 8 | import org.springframework.ai.ollama.OllamaChatModel; 9 | import org.springframework.ai.openai.OpenAiChatModel; 10 | import org.springframework.ai.tool.ToolCallbackProvider; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | 15 | /** 16 | * 聊天客户端配置 17 | * 18 | * @author Onism 19 | * @date 2025-03-25 20 | */ 21 | @Configuration 22 | public class ChatClientConfig { 23 | 24 | /** 25 | * OpenAI 聊天模型 26 | */ 27 | @Resource 28 | private OpenAiChatModel openAiChatModel; 29 | 30 | /** 31 | * OLLAMA 聊天模型 32 | */ 33 | @Resource 34 | private OllamaChatModel ollamaChatModel; 35 | 36 | @Resource 37 | private ToolCallbackProvider toolCallbackProvider; 38 | 39 | @Value("${spring.ai.chat.client.type:ollama}") 40 | private ChatClientOption clientType; 41 | 42 | /** 43 | * OpenAI 聊天客户端 44 | * 45 | * @return {@link ChatClient } 46 | */ 47 | @Bean(name = "openAiChatClient") 48 | public ChatClient openAiChatClient() { 49 | return ChatClient.builder(openAiChatModel) 50 | // 默认加载所有的工具,避免重复 new 51 | .defaultTools(toolCallbackProvider.getToolCallbacks()) 52 | .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory())) 53 | .build(); 54 | } 55 | 56 | /** 57 | * OLLAMA 聊天客户端 58 | * 59 | * @return {@link ChatClient } 60 | */ 61 | @Bean(name = "ollamaChatClient") 62 | public ChatClient ollamaChatClient() { 63 | return ChatClient.builder(ollamaChatModel) 64 | // 默认加载所有的工具,避免重复 new 65 | .defaultTools(toolCallbackProvider.getToolCallbacks()) 66 | .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory())) 67 | .build(); 68 | } 69 | 70 | /** 71 | * 聊天客户端(根据配置注入) 72 | * 73 | * @return {@link ChatClient } 74 | */ 75 | @Bean(name = "chatClient") 76 | public ChatClient chatClient() { 77 | if(clientType.equals(ChatClientOption.OPENAI)) { 78 | return openAiChatClient(); 79 | } 80 | 81 | return ollamaChatClient(); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/config/McpConfig.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.config; 2 | 3 | import cn.onism.mcp.annotations.McpTool; 4 | import cn.onism.mcp.service.search.SearxngSearchService; 5 | import cn.onism.mcp.tool.database.manage.DataSourceManager; 6 | import jakarta.annotation.Resource; 7 | import org.springframework.ai.tool.ToolCallbackProvider; 8 | import org.springframework.ai.tool.method.MethodToolCallbackProvider; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.context.annotation.Primary; 14 | import org.springframework.mail.javamail.JavaMailSender; 15 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 16 | 17 | /** 18 | * MCP 配置 19 | * 20 | * @author Onism 21 | * @date 2025-03-25 22 | */ 23 | @Configuration 24 | public class McpConfig implements WebMvcConfigurer { 25 | 26 | @Resource 27 | private DataSourceManager manager; 28 | 29 | @Resource 30 | private JavaMailSender mailSender; 31 | 32 | @Value("${spring.mail.username}") 33 | private String sendMailer; 34 | 35 | @Resource 36 | private SearxngSearchService searxngSearchService; 37 | 38 | @Resource 39 | private ApplicationContext context; 40 | 41 | /** 42 | * 添加工具回调提供程序 43 | * 44 | * @return {@link ToolCallbackProvider } 45 | */ 46 | @Bean 47 | @Primary 48 | public ToolCallbackProvider addToolCallbackProvider() { 49 | return MethodToolCallbackProvider 50 | .builder() 51 | .toolObjects( 52 | context.getBeansWithAnnotation(McpTool.class).values().toArray() 53 | ) 54 | .build(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/config/RagEmbeddingConfig.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.config; 2 | 3 | import jakarta.annotation.Resource; 4 | import org.springframework.ai.embedding.EmbeddingModel; 5 | import org.springframework.ai.ollama.OllamaEmbeddingModel; 6 | import org.springframework.ai.ollama.api.OllamaApi; 7 | import org.springframework.ai.ollama.api.OllamaOptions; 8 | import org.springframework.ai.transformer.splitter.TokenTextSplitter; 9 | import org.springframework.ai.vectorstore.pgvector.PgVectorStore; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.jdbc.core.JdbcTemplate; 13 | 14 | /** 15 | * rag 嵌入配置 16 | * 17 | * @author Onism 18 | * @date 2025-03-30 19 | */ 20 | @Configuration 21 | public class RagEmbeddingConfig { 22 | 23 | @Resource 24 | private OllamaApi ollamaApi; 25 | 26 | @Bean 27 | public TokenTextSplitter tokenTextSplitter() { 28 | return new TokenTextSplitter(); 29 | } 30 | 31 | @Bean 32 | public EmbeddingModel embeddingModel() { 33 | return OllamaEmbeddingModel.builder() 34 | .ollamaApi(ollamaApi) 35 | // 设置向量模型 36 | .defaultOptions(OllamaOptions.builder().model("nomic-embed-text") 37 | .numBatch(1024).build()) 38 | .build(); 39 | } 40 | 41 | /** 42 | * pg 向量库存储 43 | * 44 | * @param jdbcTemplate JDBC 模板 45 | * @return {@link PgVectorStore } 46 | */ 47 | @Bean 48 | public PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate,EmbeddingModel embeddingModel) { 49 | 50 | return PgVectorStore.builder(jdbcTemplate, embeddingModel) 51 | // 设置表名 52 | .vectorTableName("vector_knowledge") 53 | // 校验表是否存在 54 | .vectorTableValidationsEnabled(true) 55 | // 默认是 768 56 | .dimensions(768) 57 | .build(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/config/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 配置类 3 | **/ 4 | package cn.onism.mcp.config; 5 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/constants/ChatClientOption.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.constants; 2 | 3 | /** 4 | * 聊天客户端选项枚举 5 | * 6 | * @author Onism 7 | * @date 2025-04-08 8 | */ 9 | public enum ChatClientOption { 10 | OPENAI, 11 | OLLAMA 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/constants/CodeEnum.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.constants; 2 | 3 | /** 4 | * 系统状态常量 5 | * 6 | * @author Onism 7 | * @date 2025-03-27 8 | */ 9 | public enum CodeEnum { 10 | /** 11 | * 成功标志 12 | */ 13 | SUCCESS(200, "成功!"), 14 | 15 | /** 16 | * 参数异常 17 | */ 18 | PARAMETER_ERROR(400, "参数异常!"), 19 | 20 | /** 21 | * 邮箱为空 22 | */ 23 | EMAIL_EMPTY(265, "邮箱为空!"), 24 | 25 | /** 26 | * 邮箱格式错误 27 | */ 28 | EMAIL_FORMAT_ERROR(275, "邮箱格式错误!"), 29 | 30 | /** 31 | * 邮件发送失败 32 | */ 33 | EMAIL_SEND_ERROR(295, "邮件发送失败,请稍后重试!"), 34 | /** 35 | * 数据不存在 36 | */ 37 | DATA_NOT_EXIST(429,"当前查询的数据不存在,请稍后再试"), 38 | 39 | /** 40 | * 系统维护 41 | */ 42 | SYSTEM_REPAIR(501, "系统维护中,请稍后!"), 43 | 44 | /** 45 | * 服务异常 46 | */ 47 | FAIL(500, "服务异常!"), 48 | 49 | /** 50 | * 系统异常 51 | */ 52 | SYSTEM_ERROR(502, "服务器异常!"); 53 | 54 | private final int code; 55 | private final String msg; 56 | 57 | CodeEnum(int code, String msg) { 58 | this.code = code; 59 | this.msg = msg; 60 | } 61 | 62 | public int getCode() { 63 | return code; 64 | } 65 | 66 | public String getMsg() { 67 | return msg; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/constants/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 枚举/常量类 3 | **/ 4 | package cn.onism.mcp.constants; 5 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/controller/McpController.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.controller; 2 | 3 | import cn.onism.mcp.common.Result; 4 | import jakarta.annotation.Resource; 5 | import org.springframework.ai.chat.client.ChatClient; 6 | import org.springframework.ai.chat.messages.SystemMessage; 7 | import org.springframework.ai.chat.messages.UserMessage; 8 | import org.springframework.ai.chat.model.ChatResponse; 9 | import org.springframework.ai.chat.prompt.Prompt; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | import reactor.core.publisher.Flux; 14 | 15 | import java.util.List; 16 | 17 | /** 18 | * MCP 控制器 19 | * 20 | * @author Onism 21 | * @date 2025-03-26 22 | */ 23 | @RestController 24 | public class McpController { 25 | 26 | @Resource 27 | private ChatClient chatClient; 28 | 29 | /** 30 | * 提供一个对外的聊天接口 31 | * 32 | * @param message 消息 33 | * @return {@link Flux }<{@link String }> 34 | */ 35 | @GetMapping("/chat") 36 | public Result chat( 37 | @RequestParam String message, 38 | @RequestParam(defaultValue = "你是一个助手,请用中文回答。", required = false) String promptMessage 39 | ) { 40 | SystemMessage systemMessage = new SystemMessage(promptMessage); 41 | UserMessage userMessage = new UserMessage(message); 42 | Prompt prompt = new Prompt(List.of(systemMessage, userMessage)); 43 | 44 | String response = chatClient.prompt(prompt) 45 | .call().content(); 46 | 47 | return Result.success(response); 48 | } 49 | 50 | /** 51 | * 提供一个对外的聊天接口(流式) 52 | * 53 | * @param message 消息 54 | * @return {@link Flux }<{@link String }> 55 | */ 56 | @GetMapping("/stream/chat") 57 | public Flux chatStream( 58 | @RequestParam String message, 59 | @RequestParam(defaultValue = "你是一个助手,请用中文回答。", required = false) String promptMessage 60 | ) { 61 | SystemMessage systemMessage = new SystemMessage(promptMessage); 62 | UserMessage userMessage = new UserMessage(message); 63 | Prompt prompt = new Prompt(List.of(systemMessage, userMessage)); 64 | 65 | return chatClient.prompt(prompt) 66 | .stream().chatResponse(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/controller/RagController.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.controller; 2 | 3 | import cn.onism.mcp.common.Result; 4 | import cn.onism.mcp.service.DocumentService; 5 | import cn.onism.mcp.service.search.InternetSearchService; 6 | import jakarta.annotation.Resource; 7 | import org.springframework.ai.chat.client.ChatClient; 8 | import org.springframework.ai.chat.model.ChatResponse; 9 | import org.springframework.ai.chat.prompt.Prompt; 10 | import org.springframework.ai.document.Document; 11 | import org.springframework.ai.vectorstore.VectorStore; 12 | import org.springframework.web.bind.annotation.*; 13 | import org.springframework.web.multipart.MultipartFile; 14 | import reactor.core.publisher.Flux; 15 | 16 | import java.io.IOException; 17 | import java.util.List; 18 | import java.util.stream.Collectors; 19 | 20 | /** 21 | * RAG 控制器 22 | * 23 | * @author Onism 24 | * @date 2025-03-31 25 | */ 26 | @RestController 27 | @RequestMapping("/rag") 28 | public class RagController { 29 | 30 | @Resource 31 | private ChatClient ollamaChatClient; 32 | 33 | @Resource 34 | private VectorStore vectorStore; 35 | 36 | @Resource 37 | private DocumentService documentService; 38 | 39 | @Resource 40 | private InternetSearchService internetSearchService; 41 | 42 | private static final String PROMPT = """ 43 | 基于以下知识库内容回答问题: 44 | {context} 45 | 问题:{question} 46 | """; 47 | 48 | @PostMapping("/upload") 49 | public Result upload(@RequestParam("file") MultipartFile file) { 50 | try { 51 | documentService.processDocument(file); 52 | } catch (IOException e) { 53 | return Result.fail().message(e.getMessage()); 54 | } 55 | return Result.success(); 56 | } 57 | 58 | @GetMapping("/inquire") 59 | public Result inquire(@RequestParam String question) { 60 | //检索相似文档作为上下文 61 | List contextDocs = vectorStore.similaritySearch(question); 62 | 63 | // 构建提示词模板 64 | String context = null; 65 | if (contextDocs != null) { 66 | context = contextDocs.stream() 67 | .map(Document::getText) 68 | .collect(Collectors.joining("\n")); 69 | } 70 | 71 | // 调用大模型回答问题 72 | return Result.success(ollamaChatClient 73 | .prompt(new Prompt(PROMPT.replace("{context}", context).replace("{question}", question))) 74 | .call().content() 75 | ); 76 | } 77 | 78 | @GetMapping("/stream/inquire") 79 | public Flux streamInquire( 80 | @RequestParam String question 81 | ) { 82 | //检索相似文档作为上下文 83 | List contextDocs = vectorStore.similaritySearch(question); 84 | 85 | // 构建提示词模板 86 | String context = null; 87 | if (contextDocs != null) { 88 | context = contextDocs.stream() 89 | .map(Document::getText) 90 | .collect(Collectors.joining("\n")); 91 | } 92 | 93 | // 设置提示词 94 | Prompt prompt = new Prompt(PROMPT.replace("{context}", context).replace("{question}", question)); 95 | 96 | // 输出完成标识:["finishReason": "stop"] 97 | return ollamaChatClient.prompt(prompt).stream().chatResponse(); 98 | } 99 | 100 | @GetMapping("/search") 101 | public Result search(@RequestParam String question) { 102 | return Result.success(internetSearchService.searXNGSearch(question)); 103 | } 104 | 105 | @GetMapping("/stream/search") 106 | public Flux streamSearch(@RequestParam String question) { 107 | return internetSearchService.searXNGstreamSearch(question); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/controller/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 控制器类 3 | **/ 4 | package cn.onism.mcp.controller; 5 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/entity/SearchResult.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.entity; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 联网搜索结果实体类 7 | * 8 | * @author Onism 9 | * @date 2025-04-08 10 | */ 11 | @Data 12 | public class SearchResult { 13 | private String title; 14 | private String url; 15 | private String content; 16 | private double score; 17 | 18 | public String getTitle() { 19 | return title; 20 | } 21 | 22 | public void setTitle(String title) { 23 | this.title = title; 24 | } 25 | 26 | public String getUrl() { 27 | return url; 28 | } 29 | 30 | public void setUrl(String url) { 31 | this.url = url; 32 | } 33 | 34 | public String getContent() { 35 | return content; 36 | } 37 | 38 | public void setContent(String content) { 39 | this.content = content; 40 | } 41 | 42 | public double getScore() { 43 | return score; 44 | } 45 | 46 | public void setScore(double score) { 47 | this.score = score; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/entity/SearxngResponse.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.entity; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * searxng 响应体 9 | * 10 | * @author Onism 11 | * @date 2025-04-08 12 | */ 13 | @Data 14 | public class SearxngResponse { 15 | private String query; 16 | private List results; 17 | 18 | public String getQuery() { 19 | return query; 20 | } 21 | 22 | public void setQuery(String query) { 23 | this.query = query; 24 | } 25 | 26 | public List getResults() { 27 | return results; 28 | } 29 | 30 | public void setResults(List results) { 31 | this.results = results; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/entity/VectorRelation.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.entity; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Data; 5 | import lombok.experimental.Accessors; 6 | import org.hibernate.annotations.CreationTimestamp; 7 | import org.hibernate.annotations.UpdateTimestamp; 8 | 9 | import java.io.Serial; 10 | import java.io.Serializable; 11 | import java.util.Date; 12 | 13 | /** 14 | * 向量知识库关系实体类 15 | * 16 | * @author Onism 17 | * @date 2025-04-01 18 | */ 19 | @Entity 20 | @Table(name ="vector_relation") 21 | @Data 22 | @Accessors(chain = true) 23 | public class VectorRelation implements Serializable { 24 | /** 25 | * id 26 | */ 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.IDENTITY) 29 | private Long id; 30 | 31 | /** 32 | * 文件 Hash 值 33 | */ 34 | @Column(name = "file_hash", nullable = false, length = 64) 35 | private String fileHash; 36 | 37 | /** 38 | * 文件名 39 | */ 40 | @Column(name = "file_name", nullable = false, length = 255) 41 | private String fileName; 42 | 43 | /** 44 | * 创建时间 45 | */ 46 | @CreationTimestamp 47 | @Column(name = "created_at", updatable = false) 48 | private Date createdAt; 49 | 50 | /** 51 | * 更新时间 52 | */ 53 | @UpdateTimestamp 54 | @Column(name = "updated_at") 55 | private Date updatedAt; 56 | 57 | @Serial 58 | private static final long serialVersionUID = 1L; 59 | 60 | public VectorRelation() { 61 | } 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/entity/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 实体类 3 | **/ 4 | package cn.onism.mcp.entity; 5 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/exception/CustomException.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.exception; 2 | 3 | import cn.onism.mcp.constants.CodeEnum; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | 8 | /** 9 | * 自定义异常 10 | */ 11 | @EqualsAndHashCode(callSuper = true) 12 | @Data 13 | public class CustomException extends RuntimeException { 14 | private final Integer code; 15 | 16 | public CustomException(Integer code) { 17 | this.code = code; 18 | } 19 | 20 | public CustomException(String message, Integer code) { 21 | super(message); 22 | this.code = code; 23 | } 24 | 25 | public CustomException(String message, Throwable cause, Integer code) { 26 | super(message, cause); 27 | this.code = code; 28 | } 29 | 30 | public CustomException(Throwable cause, Integer code) { 31 | super(cause); 32 | this.code = code; 33 | } 34 | 35 | public CustomException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, Integer code) { 36 | super(message, cause, enableSuppression, writableStackTrace); 37 | this.code = code; 38 | } 39 | 40 | public CustomException() { 41 | this.code = CodeEnum.SYSTEM_ERROR.getCode(); 42 | } 43 | 44 | public CustomException(String message) { 45 | super(message); 46 | this.code = CodeEnum.FAIL.getCode(); 47 | } 48 | 49 | public CustomException(CodeEnum codeEnum) { 50 | super(codeEnum.getMsg()); 51 | this.code = codeEnum.getCode(); 52 | } 53 | 54 | public CustomException(Throwable cause) { 55 | super(cause); 56 | this.code = 502; 57 | } 58 | 59 | public CustomException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 60 | super(message, cause, enableSuppression, writableStackTrace); 61 | this.code = 502; 62 | } 63 | 64 | public int getCode() { 65 | return code; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/exception/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 异常类 3 | **/ 4 | package cn.onism.mcp.exception; 5 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/handler/AllExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.handler; 2 | 3 | import cn.onism.mcp.common.Result; 4 | import cn.onism.mcp.constants.CodeEnum; 5 | import cn.onism.mcp.exception.CustomException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.RestControllerAdvice; 10 | import org.springframework.web.context.request.RequestContextHolder; 11 | import org.springframework.web.context.request.ServletRequestAttributes; 12 | 13 | /** 14 | * 异常处理器 15 | * 16 | * @author Onism 17 | * @date 2025-03-27 18 | */ 19 | @Slf4j 20 | @RestControllerAdvice 21 | public class AllExceptionHandler { 22 | 23 | /** 24 | * 服务器异常 25 | */ 26 | @ExceptionHandler(RuntimeException.class) 27 | public Result deException(RuntimeException ex) { 28 | HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); 29 | log.error("端口{}发生异常:", request.getServletPath(), ex); 30 | log.error("发生异常:", ex); 31 | return Result.fail().message(CodeEnum.SYSTEM_REPAIR.getMsg() + "\n异常信息:" + ex.getMessage()).code(CodeEnum.SYSTEM_REPAIR.getCode()); 32 | } 33 | 34 | /** 35 | * 自定义异常 36 | */ 37 | @ExceptionHandler(CustomException.class) 38 | public Result handleCustomException(CustomException ex) { 39 | log.error("异常:{}", ex.getMessage()); 40 | return Result.fail().message(ex.getMessage()).code(ex.getCode()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/handler/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 处理器类 3 | **/ 4 | package cn.onism.mcp.handler; 5 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/repository/VectorRelationRepository.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.repository; 2 | 3 | import cn.onism.mcp.entity.VectorRelation; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | /** 7 | * 向量关系存储库 8 | * 9 | * @author Onism 10 | * @date 2025-04-01 11 | */ 12 | public interface VectorRelationRepository extends JpaRepository { 13 | 14 | /** 15 | * 按文件哈希值获取 16 | * 17 | * @param fileHash 文件哈希 18 | * @return {@link VectorRelation } 19 | */ 20 | VectorRelation getByFileHash(String fileHash); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/repository/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 存储类 3 | **/ 4 | package cn.onism.mcp.repository; 5 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/service/DocumentService.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.service; 2 | 3 | import cn.onism.mcp.entity.VectorRelation; 4 | import jakarta.annotation.Resource; 5 | import org.apache.commons.codec.digest.DigestUtils; 6 | import org.springframework.ai.document.Document; 7 | import org.springframework.ai.reader.tika.TikaDocumentReader; 8 | import org.springframework.ai.transformer.splitter.TokenTextSplitter; 9 | import org.springframework.ai.vectorstore.VectorStore; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.web.multipart.MultipartFile; 12 | 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.util.List; 16 | 17 | /** 18 | * 文档服务 19 | * 20 | * @author Onism 21 | * @date 2025-03-30 22 | */ 23 | @Service 24 | public class DocumentService { 25 | 26 | @Resource 27 | private VectorStore vectorStore; 28 | 29 | @Resource 30 | private VectorRelationService vectorRelationService; 31 | 32 | /** 33 | * 处理文档 34 | * 35 | * @param file 文件 36 | * @throws IOException io异常 37 | */ 38 | public void processDocument(MultipartFile file) throws IOException { 39 | // 计算文件 hash 40 | String fileHash = generateFileHash(file); 41 | 42 | // 查询是否已经存在该文件 43 | VectorRelation vectorRelation = vectorRelationService.getByFileHash(fileHash); 44 | 45 | if(vectorRelation != null){ 46 | // 更新记录 47 | vectorRelationService.updateById(vectorRelation); 48 | } else { // 新增操作 49 | // 解析文档 50 | TikaDocumentReader tikaReader = new TikaDocumentReader(file.getResource()); 51 | List documents = tikaReader.read(); 52 | 53 | // 分割文本 54 | TokenTextSplitter splitter = new TokenTextSplitter(); 55 | List documentList = splitter.transform(documents); 56 | // 添加文件名称分类 57 | documentList.forEach(doc -> doc.getMetadata().put("fileName",file.getOriginalFilename())); 58 | 59 | // 存储到表 60 | vectorStore.accept(documentList); 61 | 62 | // 插入新记录 63 | vectorRelation = new VectorRelation() 64 | .setFileName(file.getOriginalFilename()) 65 | .setFileHash(fileHash); 66 | 67 | vectorRelationService.createRelation(vectorRelation); 68 | } 69 | } 70 | 71 | /** 72 | * 生成文件哈希值 73 | * 74 | * @param file 文件 75 | * @return {@link String } 76 | */ 77 | private String generateFileHash(MultipartFile file) { 78 | try (InputStream inputStream = file.getInputStream()) { 79 | return DigestUtils.sha256Hex(inputStream); 80 | } catch (IOException e) { 81 | throw new RuntimeException(e); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/service/VectorRelationService.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.service; 2 | 3 | import cn.onism.mcp.constants.CodeEnum; 4 | import cn.onism.mcp.entity.VectorRelation; 5 | import cn.onism.mcp.exception.CustomException; 6 | import cn.onism.mcp.repository.VectorRelationRepository; 7 | import jakarta.annotation.Resource; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | /** 12 | * Vector 关系服务 13 | * 14 | * @author Onism 15 | * @date 2025-04-01 16 | */ 17 | @Service 18 | public class VectorRelationService { 19 | 20 | @Resource 21 | private VectorRelationRepository repository; 22 | 23 | /** 24 | * 创建记录 25 | * 26 | * @param relation 关系 27 | * @return {@link VectorRelation } 28 | */ 29 | @Transactional 30 | public VectorRelation createRelation(VectorRelation relation) { 31 | return repository.save(relation); 32 | } 33 | 34 | public VectorRelation getByFileHash(String fileHash) { 35 | return repository.getByFileHash(fileHash); 36 | } 37 | 38 | /** 39 | * 按 ID 获取 40 | * 41 | * @param id 身份证 42 | * @return {@link VectorRelation } 43 | */ 44 | public VectorRelation getById(Long id) { 45 | return repository.findById(id) 46 | .orElseThrow(() -> new CustomException(CodeEnum.DATA_NOT_EXIST)); 47 | } 48 | 49 | @Transactional 50 | public void updateById(VectorRelation relation) { 51 | getById(relation.getId()); 52 | repository.save(relation); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/service/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 服务类 3 | **/ 4 | package cn.onism.mcp.service; 5 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/service/search/InternetSearchService.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.service.search; 2 | 3 | import cn.onism.mcp.entity.SearchResult; 4 | import cn.onism.mcp.util.prompt.InternetSearchPromptBuilder; 5 | import jakarta.annotation.Resource; 6 | import org.springframework.ai.chat.client.ChatClient; 7 | import org.springframework.ai.chat.model.ChatResponse; 8 | import org.springframework.ai.chat.prompt.Prompt; 9 | import org.springframework.stereotype.Service; 10 | import reactor.core.publisher.Flux; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * 联网搜索服务 16 | * 17 | * @author Onism 18 | * @date 2025-04-02 19 | */ 20 | @Service 21 | public class InternetSearchService { 22 | 23 | @Resource 24 | private SearxngSearchService searxngSearchService; 25 | 26 | /** 27 | * 聊天客户端 28 | */ 29 | @Resource 30 | private ChatClient chatClient; 31 | 32 | 33 | /** 34 | * SearXNG 联网搜索 35 | * 36 | * @param question 问题 37 | * @return {@link String } 38 | */ 39 | public String searXNGSearch(String question) { 40 | // 执行搜索 41 | List results = searxngSearchService.search(question); 42 | 43 | // 构建增强Prompt 44 | String augmentedPrompt = InternetSearchPromptBuilder.buildRAGPrompt(question, results); 45 | 46 | // 调用大模型 47 | return chatClient.prompt(new Prompt(augmentedPrompt)).call().content(); 48 | } 49 | 50 | /** 51 | * searXNG 联网搜索(流式) 52 | * 53 | * @param question 问题 54 | * @return {@link Flux }<{@link ChatResponse }> 55 | */ 56 | public Flux searXNGstreamSearch(String question) { 57 | // 执行搜索 58 | List results = searxngSearchService.search(question); 59 | 60 | // 构建增强Prompt 61 | String augmentedPrompt = InternetSearchPromptBuilder.buildRAGPrompt(question, results); 62 | 63 | // 调用大模型 64 | return chatClient.prompt(new Prompt(augmentedPrompt)).stream().chatResponse(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/service/search/SearxngSearchService.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.service.search; 2 | 3 | import cn.hutool.json.JSONUtil; 4 | import cn.onism.mcp.entity.SearchResult; 5 | import cn.onism.mcp.entity.SearxngResponse; 6 | import groovy.json.StringEscapeUtils; 7 | import lombok.extern.slf4j.Slf4j; 8 | import okhttp3.HttpUrl; 9 | import okhttp3.OkHttpClient; 10 | import okhttp3.Request; 11 | import okhttp3.Response; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.io.IOException; 17 | import java.util.Collections; 18 | import java.util.Comparator; 19 | import java.util.List; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | /** 23 | * SEARXNG 搜索服务 24 | * 25 | * @author Onism 26 | * @date 2025-04-08 27 | */ 28 | @Service 29 | @Slf4j 30 | public class SearxngSearchService { 31 | 32 | @Value("${spring.ai.websearch.searxng.url}") 33 | private String SEARXNG_URL; 34 | 35 | @Value("${spring.ai.websearch.searxng.nums:20}") 36 | private int NUMS; 37 | 38 | private final OkHttpClient httpClient; 39 | 40 | public SearxngSearchService() { 41 | this.httpClient = buildRequest(); 42 | } 43 | 44 | private OkHttpClient buildRequest() { 45 | return new OkHttpClient.Builder() 46 | .connectTimeout(30, TimeUnit.SECONDS) 47 | .readTimeout(60, TimeUnit.SECONDS) 48 | .build(); 49 | } 50 | 51 | /** 52 | * 联网实时搜索 53 | * 54 | * @param question 问题 55 | * @return {@link List }<{@link SearchResult }> 56 | */ 57 | public List search(String question) { 58 | HttpUrl url = HttpUrl.get(SEARXNG_URL).newBuilder() 59 | // 搜索问题 60 | .addQueryParameter("q", question) 61 | // 返回结果格式 62 | .addQueryParameter("format", "json") 63 | .build(); 64 | log.info("搜索链接 => {}", url.url()); 65 | 66 | Request request = new Request.Builder().url(url).build(); 67 | 68 | try (Response response = httpClient.newCall(request).execute()) { 69 | if (!response.isSuccessful()) { 70 | throw new IOException("请求失败: HTTP " + response.code()); 71 | } 72 | if (response.body() != null) { 73 | // 获取相应结果 74 | String responseBody = response.body().string(); 75 | // 记录搜索结果的前 200 个字符 76 | log.info("搜索结果 <= {}", StringUtils.abbreviate(StringEscapeUtils.unescapeJava(responseBody),200)); 77 | return parseResults(responseBody); 78 | } 79 | log.error("搜索失败:{}",response.message()); 80 | return Collections.emptyList(); 81 | } catch (IOException e) { 82 | throw new RuntimeException(e); 83 | } 84 | } 85 | 86 | /** 87 | * 解析搜索相应结果 88 | * 89 | * @param resultJson 结果 JSON 字符串 90 | * @return {@link List }<{@link SearchResult }> 91 | */ 92 | private List parseResults(String resultJson) { 93 | if (StringUtils.isBlank(resultJson)) { 94 | return Collections.emptyList(); 95 | } 96 | 97 | List results = JSONUtil.toBean(resultJson, SearxngResponse.class).getResults(); 98 | results = results.subList(0,Math.min(NUMS,results.size())) 99 | .parallelStream() 100 | // 按score降序排序 101 | .sorted(Comparator.comparingDouble(SearchResult::getScore).reversed()) 102 | // 截取前num个元素 103 | .limit(NUMS).toList(); 104 | return results; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/DateTool.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool; 2 | 3 | import cn.onism.mcp.annotations.McpTool; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.springframework.ai.tool.annotation.Tool; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.time.LocalDateTime; 10 | import java.time.format.DateTimeFormatter; 11 | 12 | /** 13 | * 日期工具 14 | * 15 | * @author Onism 16 | * @date 2025-03-24 17 | */ 18 | @Component 19 | @McpTool 20 | public class DateTool { 21 | 22 | /** 23 | * 地址请求 24 | * 25 | * @author Onism 26 | * @date 2025-03-28 27 | */ 28 | @Setter 29 | @Getter 30 | public static class AddressRequest { 31 | private String address; 32 | 33 | } 34 | 35 | /** 36 | * 日期响应 37 | * 38 | * @author Onism 39 | * @date 2025-03-28 40 | */ 41 | @Setter 42 | @Getter 43 | public static class DateResponse { 44 | private String result; 45 | 46 | public DateResponse(String result) { 47 | this.result = result; 48 | } 49 | 50 | } 51 | 52 | @Tool(description = "获取指定地点的当前时间") 53 | public DateResponse getAddressDate(AddressRequest request) { 54 | String result = String.format("%s的当前时间是%s", 55 | request.getAddress(), 56 | LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); 57 | return new DateResponse(result); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/EmailTool.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool; 2 | 3 | import cn.onism.mcp.annotations.McpTool; 4 | import cn.onism.mcp.constants.CodeEnum; 5 | import cn.onism.mcp.exception.CustomException; 6 | import com.vladsch.flexmark.html.HtmlRenderer; 7 | import com.vladsch.flexmark.parser.Parser; 8 | import com.vladsch.flexmark.util.data.MutableDataSet; 9 | import jakarta.mail.MessagingException; 10 | import jakarta.mail.internet.MimeMessage; 11 | import lombok.Getter; 12 | import lombok.Setter; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.apache.commons.lang3.StringUtils; 15 | import org.springframework.ai.tool.annotation.Tool; 16 | import org.springframework.ai.tool.annotation.ToolParam; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.beans.factory.annotation.Value; 19 | import org.springframework.mail.MailException; 20 | import org.springframework.mail.javamail.JavaMailSender; 21 | import org.springframework.mail.javamail.MimeMessageHelper; 22 | import org.springframework.scheduling.annotation.Async; 23 | import org.springframework.stereotype.Component; 24 | 25 | import java.io.Serializable; 26 | import java.util.regex.Pattern; 27 | 28 | /** 29 | * 邮件工具 30 | * 31 | * @author Onism 32 | * @date 2025-03-27 33 | */ 34 | @Slf4j 35 | @Component 36 | @McpTool 37 | public class EmailTool { 38 | 39 | private final JavaMailSender mailSender; 40 | 41 | private final String sendMailer; 42 | 43 | @Autowired 44 | public EmailTool(JavaMailSender mailSender, @Value("${spring.mail.username}") String sendMailer) { 45 | this.mailSender = mailSender; 46 | this.sendMailer = sendMailer; 47 | } 48 | 49 | /** 50 | * 判断邮箱是否合法 51 | */ 52 | public static void isValidEmail(String email) { 53 | if (StringUtils.isBlank(email)) { 54 | log.error("邮箱为空!"); 55 | throw new CustomException(CodeEnum.EMAIL_EMPTY); 56 | } 57 | if (!Pattern.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", email)) { 58 | log.error("邮箱格式不合法!"); 59 | throw new CustomException(CodeEnum.EMAIL_FORMAT_ERROR); 60 | } 61 | } 62 | 63 | /** 64 | * 发送邮件 65 | */ 66 | @Async 67 | @Tool(description = "给指定邮箱发送邮件消息,email 为收件人邮箱,subject 为邮件标题,message 为邮件的内容") 68 | public void sendMailMessage(EmailRequest request) { 69 | // 校验邮箱是否合法 70 | isValidEmail(request.getEmail()); 71 | try { 72 | MimeMessage mimeMessage = mailSender.createMimeMessage(); 73 | MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); // true 表示支持附件 74 | // 发件人邮箱 75 | helper.setFrom(sendMailer); 76 | // 收件人邮箱 77 | helper.setTo(request.getEmail()); 78 | // 邮件标题 79 | helper.setSubject(request.getSubject()); 80 | // 邮件正文 81 | helper.setText(convertToHtml(request.getMessage())); 82 | 83 | mailSender.send(mimeMessage); 84 | } catch (MailException | MessagingException e) { 85 | log.error("邮箱发送失败,报错:{}", e.getMessage()); 86 | throw new CustomException(CodeEnum.EMAIL_SEND_ERROR); 87 | } 88 | } 89 | 90 | /** 91 | * 转换为 HTML 92 | * 93 | * @param markdown Markdown 94 | * @return {@link String } 95 | */ 96 | public static String convertToHtml(String markdown) { 97 | MutableDataSet options = new MutableDataSet(); 98 | Parser parser = Parser.builder(options).build(); 99 | HtmlRenderer renderer = HtmlRenderer.builder(options).build(); 100 | return renderer.render(parser.parse(markdown)); 101 | } 102 | 103 | @Setter 104 | @Getter 105 | public static class EmailRequest implements Serializable { 106 | /** 107 | * 收件人邮件 108 | */ 109 | @ToolParam(description = "收件人邮箱") 110 | private String email; 111 | 112 | /** 113 | * 主题 114 | */ 115 | @ToolParam(description = "发送邮件的标题/主题") 116 | private String subject; 117 | 118 | /** 119 | * 消息 120 | */ 121 | @ToolParam(description = "发送邮件的正文消息内容") 122 | private String message; 123 | 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/FileTool.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool; 2 | 3 | import cn.onism.mcp.annotations.McpTool; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.springframework.ai.tool.annotation.Tool; 7 | import org.springframework.ai.tool.annotation.ToolParam; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.io.IOException; 11 | import java.nio.file.*; 12 | import java.util.Comparator; 13 | import java.util.List; 14 | import java.util.Set; 15 | import java.util.stream.Stream; 16 | 17 | /** 18 | * 文件工具 19 | * 赋予大模型最基础的本地文件系统的 20 | * 21 | * @author Onism 22 | * @date 2025-03-24 23 | */ 24 | @Component 25 | @McpTool 26 | public class FileTool { 27 | 28 | private static final Set WINDOWS_RESERVED_NAMES = Set.of( 29 | "CON", "PRN", "AUX", "NUL", 30 | "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", 31 | "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" 32 | ); 33 | 34 | @Tool(description = "读取指定文件内容") 35 | public ReadResponse readFile(ReadRequest request) { 36 | if (!isValidPath(request.filePath)) { 37 | return new ReadResponse(null, "文件读取失败,`" + request.filePath + "` 文件路径不合法"); 38 | } 39 | try { 40 | String content = new String(Files.readAllBytes(Paths.get(request.getFilePath()))); 41 | return new ReadResponse(content, null); 42 | } catch (IOException e) { 43 | return new ReadResponse(null, "文件读取失败: " + e.getMessage()); 44 | } 45 | } 46 | 47 | @Tool(description = "写入内容到文件") 48 | public WriteResponse writeFile(WriteRequest request) { 49 | if (!isValidPath(request.filePath)) { 50 | return new WriteResponse(false, "文件读取失败,`" + request.filePath + "` 文件路径不合法"); 51 | } 52 | try { 53 | OpenOption option = request.isAppend() ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING; 54 | Files.writeString(Path.of(request.getFilePath()), 55 | request.getContent(), 56 | StandardOpenOption.CREATE, 57 | option); 58 | return new WriteResponse(true, null); 59 | } catch (IOException e) { 60 | return new WriteResponse(false, "文件写入失败: " + e.getMessage()); 61 | } 62 | } 63 | 64 | @Tool(description = "创建目录") 65 | public WriteResponse createDirectory(DirectoryRequest request) { 66 | try { 67 | Files.createDirectories(Path.of(request.getDirPath())); 68 | return new WriteResponse(true, null); 69 | } catch (IOException e) { 70 | return new WriteResponse(false, "目录创建失败: " + e.getMessage()); 71 | } 72 | } 73 | 74 | @Tool(description = "删除文件或目录") 75 | public WriteResponse deletePath(ReadRequest request) { 76 | if (!isValidPath(request.filePath)) { 77 | return new WriteResponse(false, "文件读取失败,`" + request.filePath + "` 文件路径不合法"); 78 | } 79 | try { 80 | Path path = Path.of(request.getFilePath()); 81 | if (Files.isDirectory(path)) { 82 | Files.walk(path) 83 | .sorted(Comparator.reverseOrder()) 84 | .forEach(p -> { 85 | try { 86 | Files.delete(p); 87 | } catch (IOException ignored) { 88 | } 89 | }); 90 | } else { 91 | Files.delete(path); 92 | } 93 | return new WriteResponse(true, null); 94 | } catch (IOException e) { 95 | return new WriteResponse(false, "删除失败: " + e.getMessage()); 96 | } 97 | } 98 | 99 | @Tool(description = "列出目录内容") 100 | public FileListResponse listDirectory(DirectoryRequest request) { 101 | if (!isValidPath(request.dirPath)) { 102 | return new FileListResponse(null, "文件读取失败,`" + request.dirPath + "` 文件路径不合法"); 103 | } 104 | try (Stream stream = Files.list(Path.of(request.getDirPath()))) { 105 | List files = stream 106 | .filter(p -> !p.getFileName().toString().startsWith(".")) 107 | .map(p -> p.getFileName().toString()) 108 | .toList(); 109 | return new FileListResponse(files, null); 110 | } catch (IOException e) { 111 | return new FileListResponse(null, "目录读取失败: " + e.getMessage()); 112 | } 113 | } 114 | 115 | public static boolean isValidPath(String path) { 116 | if (path == null || path.isEmpty()) { 117 | return false; 118 | } 119 | 120 | // 基础路径格式校验 121 | try { 122 | Paths.get(path); 123 | } catch (InvalidPathException ex) { 124 | return false; 125 | } 126 | 127 | // 获取操作系统类型 128 | boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); 129 | 130 | // Windows专属校验 131 | if (isWindows) { 132 | try { 133 | Path pathObj = Paths.get(path); 134 | for (Path component : pathObj) { 135 | String fileName = component.toString().trim(); 136 | if (fileName.isEmpty()) continue; 137 | 138 | // 保留名称检查 139 | if (isReservedName(fileName)) { 140 | return false; 141 | } 142 | 143 | // 非法结尾检查(空格或点) 144 | if (fileName.matches(".*[. ]$")) { 145 | return false; 146 | } 147 | 148 | // 文件名长度限制(255字符) 149 | if (fileName.length() > 255) { 150 | return false; 151 | } 152 | } 153 | } catch (InvalidPathException ex) { 154 | return false; // 冗余校验确保异常捕获 155 | } 156 | } 157 | 158 | return true; 159 | } 160 | 161 | private static boolean isReservedName(String fileName) { 162 | String upperName = fileName.toUpperCase(); 163 | int dotIndex = upperName.indexOf('.'); 164 | String baseName = (dotIndex == -1) ? upperName : upperName.substring(0, dotIndex); 165 | return WINDOWS_RESERVED_NAMES.contains(baseName); 166 | } 167 | 168 | /** 169 | * 文件读取请求 170 | * 171 | * @author Onism 172 | * @date 2025-03-24 173 | */ 174 | @Setter 175 | @Getter 176 | static class ReadRequest { 177 | /** 178 | * 文件路径 179 | */ 180 | @ToolParam(description = "文件路径") 181 | private String filePath; 182 | /** 183 | * 文件名 184 | */ 185 | @ToolParam(description = "文件名") 186 | private String fileName; 187 | 188 | public String getFilePath() { 189 | return filePath + fileName; 190 | } 191 | } 192 | 193 | /** 194 | * 写入请求 195 | * 196 | * @author Onism 197 | * @date 2025-03-24 198 | */ 199 | @Setter 200 | @Getter 201 | static class WriteRequest { 202 | /** 203 | * 文件路径 204 | */ 205 | @ToolParam(description = "文件路径") 206 | private String filePath; 207 | 208 | /** 209 | * 文件名 210 | */ 211 | @ToolParam(description = "文件名") 212 | private String fileName; 213 | 214 | /** 215 | * 内容 216 | */ 217 | @ToolParam(description = "需要写入文件的内容") 218 | private String content; 219 | /** 220 | * 是否为追加 221 | */ 222 | @ToolParam(description = "写入内容是否为追加内容") 223 | private boolean append; 224 | 225 | 226 | /** 227 | * 获取文件路径 228 | * 229 | * @return {@link String } 230 | */ 231 | public String getFilePath() { 232 | return filePath + fileName; 233 | } 234 | } 235 | 236 | @Getter 237 | @Setter 238 | static final class ReadResponse { 239 | private String content; 240 | private String error; 241 | 242 | 243 | public ReadResponse(String content, String error) { 244 | this.content = content; 245 | this.error = error; 246 | } 247 | 248 | } 249 | 250 | @Setter 251 | @Getter 252 | static 253 | class WriteResponse { 254 | private boolean success; 255 | private String error; 256 | 257 | public WriteResponse(boolean success, String error) { 258 | this.success = success; 259 | this.error = error; 260 | } 261 | 262 | } 263 | 264 | /** 265 | * 文件列表响应 266 | * 267 | * @author Onism 268 | * @date 2025-03-28 269 | */ 270 | @Getter 271 | @Setter 272 | static class FileListResponse { 273 | /** 274 | * 文件列表 275 | */ 276 | private List files; 277 | /** 278 | * 错误信息 279 | */ 280 | private String error; 281 | 282 | public FileListResponse(List files, String error) { 283 | this.files = files; 284 | this.error = error; 285 | } 286 | } 287 | 288 | @Setter 289 | @Getter 290 | static 291 | class DirectoryRequest { 292 | /** 293 | * 文件夹路径 294 | */ 295 | @ToolParam(description = "文件夹路径") 296 | private String dirPath; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/InternetSearchTool.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool; 2 | 3 | import cn.hutool.json.JSONUtil; 4 | import cn.onism.mcp.annotations.McpTool; 5 | import cn.onism.mcp.service.search.SearxngSearchService; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.ai.tool.annotation.Tool; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * 联网搜索工具 13 | * 14 | * @author Onism 15 | * @date 2025-04-08 16 | */ 17 | @Component 18 | @Slf4j 19 | @McpTool 20 | public class InternetSearchTool { 21 | 22 | private final SearxngSearchService searxngSearchService; 23 | 24 | @Autowired 25 | public InternetSearchTool(SearxngSearchService searxngSearchService) { 26 | this.searxngSearchService = searxngSearchService; 27 | } 28 | 29 | @Tool(description = "实时联网搜索问题,返回搜索结果,需要整理处理后再返回") 30 | public String internetSearch(String question) { 31 | // 执行搜索 32 | return JSONUtil.toJsonStr(searxngSearchService.search(question)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/MonitorTool.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool; 2 | 3 | import cn.hutool.json.JSONUtil; 4 | import cn.hutool.system.oshi.OshiUtil; 5 | import cn.onism.mcp.annotations.McpTool; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.springframework.ai.tool.annotation.Tool; 11 | import org.springframework.ai.tool.annotation.ToolParam; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.stereotype.Component; 14 | import oshi.SystemInfo; 15 | import oshi.hardware.CentralProcessor; 16 | 17 | import java.io.File; 18 | import java.lang.management.*; 19 | import java.net.URI; 20 | import java.net.http.HttpClient; 21 | import java.net.http.HttpRequest; 22 | import java.net.http.HttpResponse; 23 | import java.util.EnumMap; 24 | import java.util.HashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.concurrent.ConcurrentHashMap; 28 | import java.util.concurrent.TimeUnit; 29 | 30 | /** 31 | * 监控工具 32 | * 33 | * @author Onism 34 | * @date 2025-03-25 35 | */ 36 | @Slf4j 37 | @Component 38 | @McpTool 39 | public class MonitorTool { 40 | 41 | private enum MonitorResponseType { 42 | SUCCESS, FAIL 43 | } 44 | 45 | /** 46 | * HTTP 客户端 47 | */ 48 | private static final HttpClient httpClient = HttpClient.newHttpClient(); 49 | 50 | /** 51 | * 存储监控配置 52 | */ 53 | private static final Map monitoringConfigs = new ConcurrentHashMap<>(); 54 | 55 | /** 56 | * 已启用监控 57 | */ 58 | @Value("${spring.ai.monitor.enabled}") 59 | private static boolean MONITORING_ENABLED; 60 | 61 | /** 62 | * 监控线程 63 | */ 64 | private static Thread monitoringThread; 65 | 66 | /** 67 | * Webhook 回调URL 68 | */ 69 | @Value("${spring.ai.monitor.web-hook-url}") 70 | private static String webhookUrl; 71 | 72 | /** 73 | * 阈值 74 | */ 75 | @Value("${spring.ai.monitor.threshold:0.75}") 76 | private static double threshold; 77 | 78 | /** 79 | * 检查间隔(单位:秒),默认值 5 (秒) 80 | */ 81 | @Value("${spring.ai.monitor.check-interval:5}") 82 | private static int checkInterval; 83 | 84 | /** 85 | * OSHI 获取系统信息 86 | */ 87 | private static final SystemInfo SYSTEM_INFO = new SystemInfo(); 88 | 89 | @Tool(description = "获取系统资源使用率") 90 | public MonitorResponse getResourceUsage(MonitorRequest request) { 91 | Map metrics = new HashMap<>(); 92 | try { 93 | if (request.getResourceType() == null || request.getResourceType() == ResourceType.CPU) { 94 | metrics.put("cpuUsage", getCpuUsage()); 95 | } 96 | if (request.getResourceType() == null || request.getResourceType() == ResourceType.MEMORY) { 97 | metrics.put("memoryUsage", getMemoryUsage()); 98 | } 99 | if (request.getResourceType() == null || request.getResourceType() == ResourceType.DISK) { 100 | metrics.put("diskUsage", getResourceUsage(ResourceType.DISK)); 101 | } 102 | 103 | if (request.getResourceType() == null || request.getResourceType() == ResourceType.THREADS) { 104 | metrics.put("threads", getResourceUsage(ResourceType.THREADS)); 105 | } 106 | return new MonitorResponse(MonitorResponseType.SUCCESS.name(), "资源状态获取成功", metrics); 107 | } catch (Exception e) { 108 | log.error("获取系统资源使用率,失败原因:{}",e.getMessage()); 109 | return new MonitorResponse(MonitorResponseType.FAIL.name(), "监控数据获取失败: " + e.getMessage(), null); 110 | } 111 | } 112 | 113 | @Tool(description = "获取系统资源详情信息,resourceType 为资源类型,传 null 则表明获取所有资源类型(CPU、内存、线程、磁盘等)") 114 | public MonitorResponse getResourceDetail(MonitorRequest request) { 115 | Map metrics = new HashMap<>(); 116 | try { 117 | if (request.getResourceType() == null || request.getResourceType() == ResourceType.CPU) { 118 | metrics.put("cpuDetail", getCpuInfo()); 119 | } 120 | if (request.getResourceType() == null || request.getResourceType() == ResourceType.MEMORY) { 121 | metrics.put("memoryDetail", getMemoryInfo()); 122 | } 123 | 124 | if (request.getResourceType() == null || request.getResourceType() == ResourceType.THREADS) { 125 | metrics.put("threadDetail", getThreadInfo()); 126 | } 127 | if (request.getResourceType() == null || request.getResourceType() == ResourceType.DISK) { 128 | metrics.put("dickUsage", getResourceUsage(ResourceType.DISK)); 129 | } 130 | return new MonitorResponse(MonitorResponseType.SUCCESS.name(), "资源详情获取成功", metrics); 131 | } catch (Exception e) { 132 | log.error("获取系统资源详情数据失败,失败原因:{}",e.getMessage()); 133 | return new MonitorResponse(MonitorResponseType.FAIL.name(), "资源详情数据获取失败: " + e.getMessage(), null); 134 | } 135 | } 136 | 137 | @Tool(description = "设置资源监控参数并启用主动监控") 138 | public MonitorResponse setupMonitoring(MonitorRequest request) { 139 | try { 140 | MonitoringConfig config = new MonitoringConfig(); 141 | config.setResourceType(request.getResourceType()); 142 | config.setThreshold(threshold); 143 | if (StringUtils.isNotBlank(webhookUrl)) { 144 | config.setWebhookUrl(webhookUrl); 145 | } 146 | config.setCheckIntervalSeconds(checkInterval); 147 | 148 | monitoringConfigs.put(request.getResourceType().name(), config); 149 | 150 | if (MONITORING_ENABLED) { 151 | startMonitoringThread(); 152 | } 153 | 154 | return new MonitorResponse(MonitorResponseType.SUCCESS.name(), "监控配置已更新并启用", null); 155 | } catch (Exception e) { 156 | log.error("监控配置失败,失败原因:{}",e.getMessage()); 157 | return new MonitorResponse(MonitorResponseType.FAIL.name(), "监控配置失败: " + e.getMessage(), null); 158 | } 159 | } 160 | 161 | /** 162 | * 开始监控线程 163 | */ 164 | private static void startMonitoringThread() { 165 | if (monitoringThread == null || !monitoringThread.isAlive()) { 166 | monitoringThread = new Thread(() -> { 167 | while (!Thread.currentThread().isInterrupted()) { 168 | monitoringConfigs.values().forEach(config -> { 169 | double currentUsage = getResourceUsage(config.getResourceType()); 170 | if (currentUsage > config.getThreshold()) { 171 | triggerAlert(config, currentUsage); 172 | } 173 | try { 174 | Thread.sleep(config.getCheckIntervalSeconds() * 1000L); 175 | } catch (InterruptedException e) { 176 | log.error("开启监控线程失败!"); 177 | Thread.currentThread().interrupt(); 178 | } 179 | }); 180 | } 181 | }); 182 | monitoringThread.setDaemon(true); 183 | monitoringThread.start(); 184 | } 185 | } 186 | 187 | /** 188 | * @param type 资源类型 189 | * @return double 190 | */ 191 | private static double getResourceUsage(ResourceType type) { 192 | return switch (type) { 193 | case CPU -> getCpuUsage(); 194 | case MEMORY -> getMemoryUsage(); 195 | case DISK -> getDiskUsage(null); 196 | case THREADS -> { 197 | ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); 198 | yield (double) threadBean.getThreadCount() / threadBean.getPeakThreadCount() * 100; 199 | } 200 | }; 201 | } 202 | 203 | /** 204 | * 获取 CPU 使用率 205 | * 206 | * @return double 207 | */ 208 | private static double getCpuUsage() { 209 | return Double.parseDouble(java.lang.String.format("%.2f", 100 - OshiUtil.getCpuInfo().getFree())); 210 | } 211 | 212 | /** 213 | * 获取 CPU 信息 214 | * 215 | * @return {@link String } 216 | */ 217 | public static String getCpuInfo() { 218 | return getCpuInfo(SYSTEM_INFO.getHardware().getProcessor()); 219 | } 220 | 221 | private static String getCpuInfo(CentralProcessor processor) { 222 | long[] preTicks = processor.getSystemCpuLoadTicks(); // 保存第一次ticks 223 | 224 | try { 225 | TimeUnit.SECONDS.sleep(1); 226 | } catch (InterruptedException e) { 227 | Thread.currentThread().interrupt(); 228 | return "CPU信息获取失败"; 229 | } 230 | 231 | long[] postTicks = processor.getSystemCpuLoadTicks(); 232 | Map tickDiffs = calculateTickDiffs(preTicks, postTicks); 233 | 234 | long total = tickDiffs.values().stream().mapToLong(Long::longValue).sum(); 235 | if (total == 0) return "无法计算CPU使用率"; 236 | 237 | return buildCpuInfoString(processor, tickDiffs, total, preTicks); // 传递preTicks 238 | } 239 | 240 | 241 | /** 242 | * 动态计算 ticks 差值 243 | * 244 | * @param preTicks 前 ticks 值 245 | * @param postTicks 后 ticks 值 246 | * @return {@link Map }<{@link CentralProcessor.TickType }, {@link Long }> 247 | */ 248 | private static Map calculateTickDiffs(long[] preTicks, long[] postTicks) { 249 | Map diffs = new EnumMap<>(CentralProcessor.TickType.class); 250 | for (CentralProcessor.TickType type : CentralProcessor.TickType.values()) { 251 | int idx = type.getIndex(); 252 | diffs.put(type, postTicks[idx] - preTicks[idx]); 253 | } 254 | return diffs; 255 | } 256 | 257 | 258 | /** 259 | * 构建 CPU 信息结果字符串 260 | * 261 | * @param processor 中央处理器(CPU) 262 | * @param diffs 差异 263 | * @param total 总 264 | * @param preTicks 前报价 265 | * @return {@link String } 266 | */ 267 | private static String buildCpuInfoString(CentralProcessor processor, 268 | Map diffs, 269 | long total, 270 | long[] preTicks) { 271 | double systemUsage = percentage(diffs.get(CentralProcessor.TickType.SYSTEM), total); 272 | double userUsage = percentage(diffs.get(CentralProcessor.TickType.USER), total); 273 | double ioWait = percentage(diffs.get(CentralProcessor.TickType.IOWAIT), total); 274 | double idle = percentage(diffs.get(CentralProcessor.TickType.IDLE), total); 275 | 276 | return java.lang.String.format( 277 | """ 278 | CPU 核数: %d 279 | 系统使用率: %.2f%% 280 | 用户使用率: %.2f%% 281 | I/O 等待率: %.2f%% 282 | 空闲率: %.2f%% 283 | Tick负载: %.1f%% 284 | OS上报负载: %.1f%%""", 285 | processor.getLogicalProcessorCount(), 286 | systemUsage, 287 | userUsage, 288 | ioWait, 289 | idle, 290 | processor.getSystemCpuLoadBetweenTicks(preTicks) * 100, // 传入preTicks 291 | processor.getSystemCpuLoad(1000) * 100 292 | ); 293 | } 294 | 295 | /** 296 | * 计算百分比 297 | * 298 | * @param part 部分 299 | * @param total 总 300 | * @return double 301 | */ 302 | private static double percentage(long part, long total) { 303 | return total == 0 ? 0.0 : (part * 100.0) / total; 304 | } 305 | 306 | /** 307 | * 获取内存使用情况 308 | * 309 | * @return double 310 | */ 311 | private static double getMemoryUsage() { 312 | double usage = (double) (OshiUtil.getMemory().getTotal() - OshiUtil.getMemory() 313 | .getAvailable()) / OshiUtil.getMemory().getTotal() * 100; 314 | return Double.parseDouble(java.lang.String.format("%.2f", usage)); 315 | } 316 | 317 | /** 318 | * 获取内存信息 319 | * 320 | * @return {@link String } 321 | */ 322 | public static String getMemoryInfo() { 323 | List memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans(); 324 | 325 | StringBuilder builder = new StringBuilder(); 326 | 327 | // 堆内存信息 328 | builder.append("堆内存信息:").append(getMemoryInfo(memoryPoolMXBeans, MemoryType.HEAP)).append("\n"); 329 | 330 | // 非堆内存信息 331 | builder.append("非堆内存信息:").append(getMemoryInfo(memoryPoolMXBeans, MemoryType.NON_HEAP)).append("\n"); 332 | 333 | // NIO 相关内存 334 | try { 335 | Class clazz = Class.forName("java.lang.management.BufferPoolMXBean"); 336 | List bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(clazz); 337 | 338 | builder.append("NIO 相关内容:"); 339 | bufferPoolMXBeans.forEach(x -> builder.append("name: ") 340 | .append(x.getName()) 341 | // 已使用的内存 342 | .append(" 已使用内存: ") 343 | .append(x.getMemoryUsed() / 1024 / 1024).append(" MB") // 以 MB 为单位 344 | // 已申请的内存 345 | .append(" 容量: ") 346 | .append(x.getTotalCapacity() / 1024 / 1024).append(" MB").append("\n")); 347 | } catch (ClassNotFoundException ignore) { 348 | 349 | } 350 | 351 | return builder.toString(); 352 | } 353 | 354 | /** 355 | * 获取内存信息 356 | * 357 | * @param memoryPoolMXBeans 内存池 MXBebeans 358 | * @param type 类型 359 | * @return {@link String } 360 | */ 361 | private static String getMemoryInfo(List memoryPoolMXBeans, MemoryType type) { 362 | StringBuilder builder = new StringBuilder(); 363 | memoryPoolMXBeans.parallelStream().filter(x -> x.getType().equals(type)) 364 | .forEach(x -> { 365 | String info = "名称: " + 366 | x.getName() + 367 | // 已使用的内存 368 | " 已使用内存: " + 369 | x.getUsage().getUsed() / 1024 / 1024 + " MB" + // 以 MB 为单位 370 | // 已申请的内存 371 | " 已申请内存: " + 372 | x.getUsage().getCommitted() / 1024 / 1024 + " MB" + 373 | // 最大的内存 374 | " 能申请最大内存: " + 375 | x.getUsage().getMax() / 1024 / 1024 + " MB"; 376 | builder.append(info).append("\n"); 377 | }); 378 | 379 | return builder.toString(); 380 | } 381 | 382 | /** 383 | * 获取磁盘使用情况(默认为系统盘:C盘 or /) 384 | * 385 | * @return double 386 | */ 387 | private static double getDiskUsage(String diskPath) { 388 | if (diskPath == null) { 389 | return getDiskUsage(getDefaultDiskPath()); 390 | } 391 | 392 | File file = new File(diskPath); 393 | boolean isValid = checkPathValidity(file); 394 | 395 | if (!isValid) { 396 | // 路径无效时,回退到操作系统默认磁盘 397 | String defaultPath = getDefaultDiskPath(); 398 | if (!diskPath.equals(defaultPath)) { // 避免重复调用 399 | return getDiskUsage(defaultPath); // 递归查询默认磁盘 400 | } else { 401 | return 0.0; // 默认路径也无效时终止递归 402 | } 403 | } 404 | 405 | try { 406 | long total = file.getTotalSpace(); 407 | if (total == 0) return 0.0; 408 | long used = total - file.getFreeSpace(); 409 | return Double.parseDouble(String.format("%.2f",(double) used / total * 100)); 410 | } catch (SecurityException e) { 411 | System.err.println("权限不足,无法访问磁盘: " + diskPath); 412 | return 0.0; 413 | } 414 | } 415 | 416 | /** 417 | * 校验路径是否有效(存在且有容量) 418 | */ 419 | private static boolean checkPathValidity(File file) { 420 | try { 421 | return file.exists() && file.getTotalSpace() > 0; 422 | } catch (SecurityException e) { 423 | return false; 424 | } 425 | } 426 | 427 | /** 428 | * 获取操作系统默认磁盘路径 429 | */ 430 | private static String getDefaultDiskPath() { 431 | String os = System.getProperty("os.name").toLowerCase(); 432 | return os.contains("win") ? "C:\\\\" : "/"; 433 | } 434 | 435 | /** 436 | * 获取线程信息 437 | * 438 | * @return {@link String } 439 | */ 440 | public static String getThreadInfo() { 441 | ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); 442 | // 所有的线程信息 443 | ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(threadMXBean.isObjectMonitorUsageSupported(), 444 | threadMXBean.isSynchronizerUsageSupported()); 445 | 446 | StringBuilder builder = new StringBuilder(); 447 | for (ThreadInfo threadInfo : threadInfos) { 448 | builder.append("线程名称:").append(threadInfo.getThreadName()).append("\n") 449 | .append("线程 ID:").append(threadInfo.getThreadId()).append("\n") 450 | .append("线程状态:").append(threadInfo.getThreadState()).append("\n"); 451 | 452 | builder.append("堆信息:"); 453 | for (StackTraceElement traceElement : threadInfo.getStackTrace()) { 454 | builder.append(JSONUtil.toJsonStr(traceElement)).append("\n"); 455 | } 456 | } 457 | 458 | return builder.toString(); 459 | } 460 | 461 | // 告警触发方法 462 | private static void triggerAlert(MonitoringConfig config, double currentUsage) { 463 | String message = java.lang.String.format("[系统告警] %s使用率过高: %.2f%% > 阈值 %.2f%%", 464 | config.getResourceType(), currentUsage, config.getThreshold()); 465 | 466 | if (config.getWebhookUrl() != null) { 467 | try { 468 | HttpRequest request = HttpRequest.newBuilder() 469 | .uri(URI.create(config.getWebhookUrl())) 470 | .header("Content-Type", "application/json") 471 | .POST(HttpRequest.BodyPublishers.ofString( 472 | java.lang.String.format("{\"alert\": \"%s\", \"timestamp\": %d}", 473 | message, System.currentTimeMillis()))) 474 | .build(); 475 | 476 | httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) 477 | .thenAccept(response -> log.info("告警通知发送成功: {}", response.statusCode())) 478 | .exceptionally(e -> { 479 | log.error("告警通知发送失败", e); 480 | return null; 481 | }); 482 | } catch (Exception e) { 483 | log.error("创建HTTP请求失败", e); 484 | } 485 | } 486 | } 487 | 488 | /** 489 | * 资源类型 490 | * 491 | * @author Onism 492 | * @date 2025-03-25 493 | */ 494 | public enum ResourceType { 495 | CPU, MEMORY, DISK, THREADS 496 | } 497 | 498 | /** 499 | * 监控配置 500 | * 501 | * @author Onism 502 | * @date 2025-03-25 503 | */ 504 | @Setter 505 | @Getter 506 | public static class MonitoringConfig { 507 | private ResourceType resourceType; 508 | private double threshold; 509 | private String webhookUrl; 510 | private int checkIntervalSeconds = 5; 511 | 512 | } 513 | 514 | /** 515 | * 监控请求 516 | * 517 | * @author Onism 518 | * @date 2025-03-25 519 | */ 520 | @Setter 521 | @Getter 522 | public static class MonitorRequest { 523 | /** 524 | * 资源类型 525 | */ 526 | @ToolParam(required = false, description = "需要获取的系统资源类型,传 null 则表明获取所有资源类型(CPU、内存、线程、磁盘等)") 527 | private ResourceType resourceType; 528 | } 529 | 530 | /** 531 | * 监控响应 532 | * 533 | * @author Onism 534 | * @date 2025-03-25 535 | */ 536 | @Getter 537 | @Setter 538 | public static class MonitorResponse { 539 | /** 540 | * 状态 541 | */ 542 | private String status; 543 | /** 544 | * 消息 545 | */ 546 | private String message; 547 | /** 548 | * 指标 549 | */ 550 | private Map metrics; 551 | 552 | public MonitorResponse(String status, String message, Map metrics) { 553 | this.status = status; 554 | this.message = message; 555 | this.metrics = metrics; 556 | } 557 | 558 | } 559 | } 560 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/database/DatabaseTool.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool.database; 2 | 3 | import cn.onism.mcp.annotations.McpTool; 4 | import cn.onism.mcp.tool.database.manage.DataSourceManager; 5 | import cn.onism.mcp.tool.database.strategy.DataSourceStrategy; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import org.springframework.ai.tool.annotation.Tool; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Component; 11 | 12 | import javax.validation.constraints.NotNull; 13 | import java.sql.SQLException; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | /** 18 | * 数据库工具 19 | * 20 | * @author Onism 21 | * @date 2025-03-24 22 | */ 23 | @Component 24 | @McpTool 25 | public class DatabaseTool { 26 | 27 | /** 28 | * 数据源管理器 29 | */ 30 | private final DataSourceManager dataSourceManager; 31 | 32 | @Autowired 33 | public DatabaseTool(DataSourceManager dataSourceManager) { 34 | this.dataSourceManager = dataSourceManager; 35 | } 36 | 37 | 38 | @Tool(description = "执行数据库语句,其中 datasourceId 为数据源;param 为需要封装的参数,key 为封装参数的索引位置,value 为封装参数的值") 39 | public DatabaseResponse executeSQL(DatabaseRequest request) { 40 | try { 41 | DataSourceStrategy strategy = dataSourceManager.getStrategy(request.getDatasourceId()); 42 | List> result = strategy.executeQuery( 43 | request.getSql() 44 | ); 45 | return new DatabaseResponse(result, null); 46 | } catch (SQLException e) { 47 | return new DatabaseResponse(null, "SQL执行错误: " + e.getMessage()); 48 | } catch (IllegalArgumentException e) { 49 | return new DatabaseResponse(null, e.getMessage()); 50 | } 51 | } 52 | 53 | @Setter 54 | @Getter 55 | public static class DatabaseRequest { 56 | /** 57 | * 数据源唯一标识 58 | */ 59 | @NotNull 60 | private String datasourceId; 61 | 62 | /** 63 | * SQL 语句 64 | */ 65 | @NotNull 66 | private String sql; 67 | 68 | private Map params; 69 | 70 | } 71 | 72 | @Setter 73 | @Getter 74 | public static class DatabaseResponse { 75 | private List> data; 76 | private String error; 77 | 78 | public DatabaseResponse(List> data, String error) { 79 | this.data = data; 80 | this.error = error; 81 | } 82 | 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/database/manage/DataSourceManager.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool.database.manage; 2 | 3 | import cn.onism.mcp.tool.database.strategy.DataSourceStrategy; 4 | import cn.onism.mcp.tool.database.strategy.config.DataSourceProperties; 5 | import jakarta.annotation.PreDestroy; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | import java.util.function.Function; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * 数据源管理器 18 | * 19 | * @author Onism 20 | * @date 2025-03-25 21 | */ 22 | @Component 23 | public class DataSourceManager { 24 | /** 25 | * 策略映射 26 | */ 27 | private final Map strategyMap = new ConcurrentHashMap<>(); 28 | /** 29 | * 数据源参数 30 | */ 31 | private final DataSourceProperties properties; 32 | /** 33 | * 策略实施 34 | */ 35 | private final Map strategyImplementations; 36 | 37 | @Autowired 38 | public DataSourceManager(DataSourceProperties properties, 39 | List strategies) { 40 | this.properties = properties; 41 | this.strategyImplementations = strategies.stream() 42 | .collect(Collectors.toMap(DataSourceStrategy::getDbType, Function.identity())); 43 | 44 | initDataSources(); 45 | } 46 | 47 | /** 48 | * 初始化数据源 49 | */ 50 | private void initDataSources() { 51 | for (DataSourceProperties.DataSourceProperty config : properties.getDatasource()) { 52 | DataSourceStrategy strategy = strategyImplementations.get(config.getType().toLowerCase()); 53 | if (strategy != null) { 54 | strategy.init(config); 55 | strategyMap.put(config.getId(), strategy); 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * 根据数据源 ID(唯一标识) 获取数据源策略 62 | * 63 | * @param datasourceId 数据源 ID 64 | * @return {@link DataSourceStrategy } 65 | */ 66 | public DataSourceStrategy getStrategy(String datasourceId) { 67 | DataSourceStrategy strategy = strategyMap.get(datasourceId); 68 | if (strategy == null) { 69 | throw new IllegalArgumentException("未配置的数据源 ID: " + datasourceId); 70 | } 71 | return strategy; 72 | } 73 | 74 | /** 75 | * 根据数据源类型获取数据源策略 76 | * 77 | * @param datasourceType 数据源类型 78 | * @return {@link List }<{@link DataSourceStrategy }> 79 | */ 80 | public List getStrategys(String datasourceType) { 81 | // 根据 数据源类型 分组 82 | Map> strategysMap = this.strategyMap.values() 83 | // 使用并行流的方式进行处理 84 | .parallelStream() 85 | .collect(Collectors.groupingBy(DataSourceStrategy::getDbType)); 86 | 87 | return strategysMap.getOrDefault(datasourceType,new ArrayList<>()); 88 | } 89 | 90 | @PreDestroy 91 | public void destroy() { 92 | strategyMap.values().forEach(DataSourceStrategy::shutdown); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/database/strategy/AbstractDataSourceStrategy.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool.database.strategy; 2 | 3 | import cn.onism.mcp.tool.database.strategy.config.DataSourceProperties; 4 | import com.zaxxer.hikari.HikariConfig; 5 | import com.zaxxer.hikari.HikariDataSource; 6 | 7 | import java.sql.Date; 8 | import java.sql.*; 9 | import java.time.LocalDate; 10 | import java.time.LocalDateTime; 11 | import java.time.temporal.Temporal; 12 | import java.util.*; 13 | 14 | 15 | /** 16 | * 抽象数据库策略 17 | * 18 | * @author Onism 19 | * @date 2025-03-24 20 | */ 21 | public abstract class AbstractDataSourceStrategy implements DataSourceStrategy { 22 | 23 | /** 24 | * 后续有其他类型的数据源(如 Oracle、sqlite 等),直接补充常量即可(不建议在代码中硬编码魔法值) 25 | */ 26 | protected static final String MYSQL = "mysql"; 27 | protected static final String ORACLE = "oracle"; 28 | protected static final String POSTGRESQL = "postgres"; 29 | 30 | protected HikariDataSource dataSource; 31 | /** 32 | * 允许关键字 33 | */ 34 | private static final Set ALLOWED_KEYWORDS = Set.of( 35 | // 基础查询 36 | "SELECT", "*", "FROM", "AS", 37 | // 表连接 38 | "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "ON", 39 | // 条件过滤 40 | "WHERE", "AND", "OR", "NOT", 41 | "BETWEEN", "IN", "LIKE", "IS", "NULL", "EXISTS", 42 | // 分组与排序 43 | "GROUP", "ORDER", "BY", "ASC", "DESC", "HAVING", 44 | // 分页与限制 45 | "LIMIT", "OFFSET", 46 | // 集合操作 47 | "UNION", "ALL", 48 | // 高级查询 49 | "DISTINCT", "CASE", "WHEN", "THEN", "ELSE", "END", 50 | "WITH", "EXPLAIN" 51 | ); 52 | 53 | /** 54 | * 初始化数据源参数 55 | * 56 | * @param config 配置 57 | */ 58 | @Override 59 | public void init(DataSourceProperties.DataSourceProperty config) { 60 | HikariConfig hikariConfig = new HikariConfig(); 61 | hikariConfig.setJdbcUrl(config.getUrl()); 62 | hikariConfig.setUsername(config.getUsername()); 63 | hikariConfig.setPassword(config.getPassword()); 64 | hikariConfig.setDriverClassName(config.getDriverClassName()); 65 | hikariConfig.setMaximumPoolSize(config.getMaxPoolSize()); 66 | hikariConfig.setConnectionTimeout(config.getConnectionTimeOut()); 67 | 68 | this.dataSource = new HikariDataSource(hikariConfig); 69 | } 70 | 71 | /** 72 | * 关闭数据源 73 | */ 74 | @Override 75 | public void shutdown() { 76 | if (dataSource != null && !dataSource.isClosed()) { 77 | dataSource.close(); 78 | } 79 | } 80 | 81 | @Override 82 | public List> executeQuery(String sql) throws SQLException { 83 | try (Connection conn = dataSource.getConnection(); 84 | PreparedStatement stmt = conn.prepareStatement(sql)){ 85 | 86 | return processResultSet(stmt.executeQuery()); 87 | } 88 | } 89 | 90 | @Override 91 | public List> executeQuery(String sql, Map params) throws SQLException { 92 | validate(sql); 93 | try (Connection conn = dataSource.getConnection(); 94 | PreparedStatement stmt = conn.prepareStatement(sql)) { 95 | 96 | bindParameters(stmt,params); 97 | return processResultSet(stmt.executeQuery()); 98 | } 99 | } 100 | 101 | /** 102 | * 绑定参数 103 | * 104 | * @param statement 语句 105 | * @param params 参数列表 106 | * @throws SQLException SQL 异常 107 | */ 108 | protected void bindParameters(PreparedStatement statement,Map params) 109 | throws SQLException { 110 | 111 | for (Map.Entry entry : params.entrySet()) { 112 | int index = entry.getKey(); 113 | Object value = entry.getValue(); 114 | 115 | if (value instanceof Temporal) { // 处理Java 8+时间类型 116 | if (value instanceof LocalDate) { 117 | statement.setDate(index, Date.valueOf((LocalDate) value)); 118 | } else if (value instanceof LocalDateTime) { 119 | statement.setTimestamp(index, Timestamp.valueOf((LocalDateTime) value)); 120 | } 121 | } else { 122 | statement.setObject(index, value); // 通用处理 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * 处理返回结果集 129 | * @param rs 结果集 130 | * @return {@link List }<{@link Map }<{@link String }, {@link Object }>> 131 | * @throws SQLException sql异常 132 | */ 133 | protected List> processResultSet(ResultSet rs) 134 | throws SQLException { 135 | 136 | List> resultList = new ArrayList<>(); 137 | ResultSetMetaData metaData = rs.getMetaData(); 138 | int columnCount = metaData.getColumnCount(); 139 | 140 | while (rs.next()) { 141 | Map row = new LinkedHashMap<>(); 142 | for (int i = 1; i <= columnCount; i++) { 143 | String columnName = metaData.getColumnLabel(i).toLowerCase(); 144 | Object value = rs.getObject(i); 145 | 146 | // 转换SQL日期到Java时间 147 | if (value instanceof Date) { 148 | value = ((Date) value).toLocalDate(); 149 | } 150 | row.put(columnName, value); 151 | } 152 | resultList.add(row); 153 | } 154 | return resultList; 155 | } 156 | 157 | /** 158 | * 校验 SQL 语句(这里只是简单校验了一下) 159 | * @param sql SQL 160 | */ 161 | protected void validate(String sql) { 162 | String[] tokens = sql.split("\\s+"); 163 | for (String token : tokens) { 164 | if (!ALLOWED_KEYWORDS.contains(token.toUpperCase())) { 165 | // 这里只允许查询语句通过 166 | // 例如:drop table 等危险操作会被拦截(避免人为恶意删库跑路(bushi)) 167 | throw new SecurityException("禁止的SQL操作: " + token); 168 | } 169 | } 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/database/strategy/DataSourceStrategy.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool.database.strategy; 2 | 3 | import cn.onism.mcp.tool.database.strategy.config.DataSourceProperties; 4 | 5 | import javax.validation.constraints.NotNull; 6 | import java.sql.SQLException; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * 数据库策略 12 | * 13 | * @author Onism 14 | * @date 2025-03-24 15 | */ 16 | public interface DataSourceStrategy { 17 | 18 | /** 19 | * 根据配置初始化数据源 20 | */ 21 | void init(DataSourceProperties.DataSourceProperty config); 22 | 23 | /** 24 | * 获取数据库类型(代替之前的类型推断) 25 | */ 26 | String getDbType(); 27 | 28 | /** 29 | * 执行查询(无需封装参数) 30 | * 31 | * @param sql SQL 32 | * @return {@link List }<{@link Map }<{@link String },{@link Object }>> 33 | * @throws SQLException sql异常 34 | */ 35 | List> executeQuery(@NotNull String sql) throws SQLException; 36 | 37 | /** 38 | * 执行查询(需要手动封装参数) 39 | */ 40 | List> executeQuery(@NotNull String sql, Map params) throws SQLException; 41 | 42 | /** 43 | * 关闭连接池 44 | */ 45 | void shutdown(); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/database/strategy/MySQLStrategy.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool.database.strategy; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | /** 6 | * MySQL 数据源策略 7 | * 8 | * @author Onism 9 | * @date 2025-03-24 10 | */ 11 | @Component 12 | public class MySQLStrategy extends AbstractDataSourceStrategy { 13 | @Override 14 | public String getDbType() { 15 | return MYSQL; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/database/strategy/OracleStrategy.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool.database.strategy; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | /** 6 | * Oracle 数据源策略 7 | * 8 | * @author Onism 9 | * @date 2025-03-27 10 | */ 11 | @Component 12 | public class OracleStrategy extends AbstractDataSourceStrategy{ 13 | @Override 14 | public String getDbType() { 15 | return ORACLE; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/database/strategy/PostgreSQLStrategy.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool.database.strategy; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | /** 6 | * Postgre SQL策略 7 | * 8 | * @author Onism 9 | * @date 2025-03-27 10 | */ 11 | @Component 12 | public class PostgreSQLStrategy extends AbstractDataSourceStrategy { 13 | @Override 14 | public String getDbType() { 15 | return POSTGRESQL; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/database/strategy/config/DataSourceProperties.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.tool.database.strategy.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | import javax.validation.Valid; 6 | import javax.validation.constraints.NotNull; 7 | import javax.validation.constraints.Pattern; 8 | import java.util.List; 9 | 10 | /** 11 | * 数据源属性 12 | * 13 | * @author Onism 14 | * @date 2025-03-27 15 | */ 16 | @ConfigurationProperties(prefix = "spring.ai.datasources", ignoreInvalidFields = true) 17 | public class DataSourceProperties { 18 | 19 | @Valid 20 | private List datasource; 21 | 22 | public List getDatasource() { 23 | return datasource; 24 | } 25 | 26 | public void setDatasource(List datasource) { 27 | this.datasource = datasource; 28 | } 29 | 30 | public static class DataSourceProperty { 31 | 32 | /** 33 | * 数据源唯一标识 34 | */ 35 | @NotNull 36 | private String id; 37 | /** 38 | * 数据源 url 39 | */ 40 | @NotNull 41 | private String url; 42 | /** 43 | * 用户名 44 | */ 45 | private String username; 46 | /** 47 | * 密码 48 | */ 49 | private String password; 50 | /** 51 | * 数据源类型 52 | */ 53 | @Pattern(regexp = "(?i)mysql|postgresql|oracle", message = "不支持的数据库类型") 54 | private String type; 55 | /** 56 | * 驱动程序类名称 57 | */ 58 | private String driverClassName; 59 | 60 | /** 61 | * 连接池大小 62 | */ 63 | private int maxPoolSize = 10; 64 | /** 65 | * 连接超时时长 66 | */ 67 | private int connectionTimeOut = 30000; 68 | 69 | public String getId() { 70 | return id; 71 | } 72 | 73 | public void setId(String id) { 74 | this.id = id; 75 | } 76 | 77 | public String getUrl() { 78 | return url; 79 | } 80 | 81 | public void setUrl(String url) { 82 | this.url = url; 83 | } 84 | 85 | public String getUsername() { 86 | return username; 87 | } 88 | 89 | public void setUsername(String username) { 90 | this.username = username; 91 | } 92 | 93 | public String getPassword() { 94 | return password; 95 | } 96 | 97 | public void setPassword(String password) { 98 | this.password = password; 99 | } 100 | 101 | public String getType() { 102 | return type; 103 | } 104 | 105 | public void setType(String type) { 106 | this.type = type; 107 | } 108 | 109 | public String getDriverClassName() { 110 | return driverClassName; 111 | } 112 | 113 | public void setDriverClassName(String driverClassName) { 114 | this.driverClassName = driverClassName; 115 | } 116 | 117 | public int getMaxPoolSize() { 118 | return maxPoolSize; 119 | } 120 | 121 | public void setMaxPoolSize(int maxPoolSize) { 122 | this.maxPoolSize = maxPoolSize; 123 | } 124 | 125 | public int getConnectionTimeOut() { 126 | return connectionTimeOut; 127 | } 128 | 129 | public void setConnectionTimeOut(int connectionTimeOut) { 130 | this.connectionTimeOut = connectionTimeOut; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/tool/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * MCP 工具类 3 | **/ 4 | package cn.onism.mcp.tool; 5 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/util/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 辅助工具类 3 | **/ 4 | package cn.onism.mcp.util; 5 | -------------------------------------------------------------------------------- /src/main/java/cn/onism/mcp/util/prompt/InternetSearchPromptBuilder.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp.util.prompt; 2 | 3 | import cn.onism.mcp.entity.SearchResult; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * 提示词生成器 9 | * 10 | * @author Onism 11 | * @date 2025-04-08 12 | */ 13 | public class InternetSearchPromptBuilder { 14 | public static String buildRAGPrompt(String question, List results) { 15 | StringBuilder context = new StringBuilder(); 16 | context.append("基于以下联网搜索返回的结果,请生成专业回答:\n"); 17 | 18 | results.forEach(result -> 19 | context.append(String.format("\n[来源] %s\n[摘要] %s\n\n", 20 | result.getUrl(), 21 | result.getContent()) 22 | )); 23 | 24 | return String.format("%s\n问题:%s", context, question); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/.env: -------------------------------------------------------------------------------- 1 | AI_BASE_URL=$$$AI_BASE_URL$$$ 2 | # AI 密钥,可通过 https://platform.deepseek.com/api_keys 充值获取 deepseek 官方 api key 3 | AI_API_KEY=$$$AI_API_KEY$$$ 4 | # DeepSeek v3 聊天模型 5 | AI_MODEL=$$$AI_MODEL$$$ 6 | 7 | # 邮箱 8 | EMAIL_ADDRESS=$$$EMAIL_ADDRESS$$$ 9 | EMAIL_PASSWORD=$$$EMAIL_PASSWORD$$$ 10 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:postgresql://localhost:5432/ai_knowledge 4 | username: root 5 | password: 123456 6 | jpa: 7 | hibernate: 8 | ddl-auto: validate # 第一次建表为 create,后面都为 update 9 | ai: 10 | chat: 11 | client: 12 | type: openai 13 | websearch: 14 | searxng: 15 | url: "http://localhost:8088/search" # SearXNG 服务的 API 地址 16 | nums: 25 # 返回搜索结果数量,数量越多,可能会越精确(大模型可能会被多种结果误导,产生“AI 幻觉”现象),但是耗时会增加(默认为 20 条) 17 | openai: 18 | base-url: ${AI_BASE_URL} 19 | api-key: ${AI_API_KEY} # 通过环境变量文件 .env 获取 20 | chat: 21 | options: 22 | model: ${AI_MODEL} 23 | temperature: 0.8 24 | ollama: 25 | init: 26 | pull-model-strategy: when_missing # 当缺失模型会自动拉取 27 | base-url: http://localhost:11434 28 | chat: 29 | options: 30 | model: qwen2.5:3b 31 | temperature: 0.8 32 | embedding: 33 | options: 34 | num-batch: 1024 35 | num-ctx: 8192 # 上下文长度 token 数 36 | model: nomic-embed-text 37 | vectorstore: 38 | pgvector: 39 | index-type: hnsw # 高效近似最近邻索引 40 | distance-type: cosine_distance # 相似度计算方式(余弦距离) 41 | initialize-schema: false # 首次启动是否自动创建向量表(默认为 false,需要可自行打开) 42 | table-name: vector_knowledge # 自行建表的话需要指定表名 43 | dimensions: 768 # 需要与表中向量维度一致(nomic-embed-text 模型支持维度为 50 - 768) 44 | # 多数据源配置 45 | datasources: 46 | datasource: 47 | - id: mysql 48 | type: mysql 49 | url: jdbc:mysql://localhost:5206/power_buckle?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8 50 | username: root 51 | password: 123456 52 | driver-class-name: com.mysql.cj.jdbc.Driver 53 | maxPoolSize: 15 54 | - id: postgres 55 | type: postgres 56 | url: jdbc:postgresql://localhost:5432/friends 57 | username: root 58 | password: 123456 59 | driver-class-name: org.postgresql.Driver 60 | max-pool-size: 10 61 | application: 62 | name: mcp-demo 63 | # 邮箱配置 64 | mail: 65 | # 下面这个是QQ邮箱host , 企业邮箱 : smtp.exmail.qq.com 66 | host: smtp.qq.com 67 | # tencent mail port 这个是固定的 68 | port: 465 69 | username: ${EMAIL_ADDRESS} 70 | password: ${EMAIL_PASSWORD} 71 | properties: 72 | mail: 73 | smtp: 74 | socketFactory: 75 | port: 465 76 | class: javax.net.ssl.SSLSocketFactory 77 | ssl: 78 | enable: true 79 | server: 80 | port: 8089 81 | 82 | logging: 83 | file: 84 | name: app.log 85 | path: logs 86 | pattern: 87 | console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n' 88 | file: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n' 89 | logback: 90 | rollingpolicy: 91 | max-file-size: 20MB 92 | max-history: 3 93 | -------------------------------------------------------------------------------- /src/main/resources/static/getDatabase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/getDatabase.png -------------------------------------------------------------------------------- /src/main/resources/static/getFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/getFile.png -------------------------------------------------------------------------------- /src/main/resources/static/getTime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/getTime.png -------------------------------------------------------------------------------- /src/main/resources/static/inquire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/inquire.png -------------------------------------------------------------------------------- /src/main/resources/static/introduce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/introduce.png -------------------------------------------------------------------------------- /src/main/resources/static/mcpSearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/mcpSearch.png -------------------------------------------------------------------------------- /src/main/resources/static/monitor1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/monitor1.png -------------------------------------------------------------------------------- /src/main/resources/static/monitor2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/monitor2.png -------------------------------------------------------------------------------- /src/main/resources/static/ragSearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/ragSearch.png -------------------------------------------------------------------------------- /src/main/resources/static/senEmail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/senEmail.png -------------------------------------------------------------------------------- /src/main/resources/static/uploadFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/uploadFile.png -------------------------------------------------------------------------------- /src/main/resources/static/vectorKnowledge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/vectorKnowledge.png -------------------------------------------------------------------------------- /src/main/resources/static/vectorRelation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnismExplorer/mcp-demo/a59bd78576cab8d179adc59688f847057da417dd/src/main/resources/static/vectorRelation.png -------------------------------------------------------------------------------- /src/test/java/cn/onism/mcp/McpDemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp; 2 | 3 | import cn.onism.mcp.tool.EmailTool; 4 | import cn.onism.mcp.tool.MonitorTool; 5 | import cn.onism.mcp.tool.database.DatabaseTool; 6 | import jakarta.annotation.Resource; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | 10 | @SpringBootTest 11 | class McpDemoApplicationTests { 12 | 13 | @Resource 14 | private DatabaseTool databaseTool; 15 | 16 | @Resource 17 | private MonitorTool monitorTool; 18 | 19 | @Resource 20 | private EmailTool emailTool; 21 | 22 | 23 | /** 24 | * 测试数据库 25 | */ 26 | @Test 27 | void testDatabase() { 28 | DatabaseTool.DatabaseRequest queryRequest = new DatabaseTool.DatabaseRequest(); 29 | // 先查询 postgres 数据库中数据 30 | queryRequest.setDatasourceId("postgres"); 31 | queryRequest.setSql("select * from friends where id = 1"); 32 | DatabaseTool.DatabaseResponse queryResult = databaseTool.executeSQL(queryRequest); 33 | StringBuilder result = new StringBuilder(queryResult.getData().toString()); 34 | 35 | // 再查询 mysql 数据库中数据 36 | queryRequest.setDatasourceId("mysql"); 37 | queryRequest.setSql("select * from friends where id = 2"); 38 | queryResult = databaseTool.executeSQL(queryRequest); 39 | result.append(queryResult.getData().toString()); 40 | 41 | System.out.println(result); 42 | } 43 | 44 | /** 45 | * 测试系统监控功能 46 | */ 47 | @Test 48 | void testMonitor() { 49 | MonitorTool.MonitorResponse response = monitorTool.getResourceUsage(new MonitorTool.MonitorRequest()); 50 | String result = "系统资源使用率:" + "\n" + 51 | response.getMetrics().toString() + "\n" + "\n" + 52 | "系统资源详情信息:" + "\n" + 53 | MonitorTool.getMemoryInfo() + "\n" + 54 | MonitorTool.getThreadInfo() + "\n" + 55 | MonitorTool.getCpuInfo() + "\n"; 56 | 57 | System.out.println(result); 58 | } 59 | 60 | /** 61 | * 测试发送邮件功能 62 | */ 63 | @Test 64 | void testSendEmail() { 65 | EmailTool.EmailRequest emailRequest = new EmailTool.EmailRequest(); 66 | emailRequest.setEmail("xxxxxxx@qq.com"); 67 | emailRequest.setSubject("生日快乐祝福"); 68 | emailRequest.setMessage("生日快乐!愿你的每一天都充满阳光和快乐!"); 69 | emailTool.sendMailMessage(emailRequest); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/cn/onism/mcp/RagEmbeddingTests.java: -------------------------------------------------------------------------------- 1 | package cn.onism.mcp; 2 | 3 | import cn.onism.mcp.entity.VectorRelation; 4 | import cn.onism.mcp.service.DocumentService; 5 | import cn.onism.mcp.service.VectorRelationService; 6 | import jakarta.annotation.Resource; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | 10 | @SpringBootTest 11 | public class RagEmbeddingTests { 12 | @Resource 13 | private DocumentService documentService; 14 | 15 | @Resource 16 | private VectorRelationService vectorRelationService; 17 | 18 | @Test 19 | public void testCreateVectorRelation() { 20 | VectorRelation vectorRelation = new VectorRelation(). 21 | setFileName("introduce.txt") 22 | .setFileHash("11ioqokkaiqo"); 23 | vectorRelationService.createRelation(vectorRelation); 24 | } 25 | } 26 | --------------------------------------------------------------------------------