├── .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 | [](https://openjdk.org/)
4 | [](https://spring.io/projects/spring-boot)
5 | [](https://docs.spring.io/spring-ai/reference/index.html)
6 | [](https://github.com/pgvector/pgvector)
7 | [](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 | 
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 | 
32 | - 数据库 SQL 操作(目前只允许查询)
33 | 
34 | - 发送邮件(给指定邮箱发送邮件)
35 | 
36 | - 获取系统资源详情/使用率/监控系统
37 | (QQ邮箱渲染问题,可自动忽略...)
38 | 
39 | 
40 | - 整合 RAG、PgVector 向量库与本地大模型搭建知识库
41 | 创建一个`introduce.txt`文件,随便放一些大模型和网上搜不到的东西
42 | 
43 | 调用`/rag/upload`接口,将文件切割向量化上传至向量库
44 | 
45 | 查看数据库表中,已经上传成功
46 | 
47 | 
48 | 然后调用`/rag/inquire`接口,询问刚才上传上去的内容
49 | 
50 | 证明本地大模型(用的 nomic-embed-text )能够正确读取和处理知识库中的消息
51 | - 实时联网搜索功能
52 | 使用本地化部署 SearXNG 结合 Ollama 本地化部署的大模型(当然,也可以使用其他大模型的 API),实现本地隐私实时智能搜索
53 | 实现了两种方式的搜索:
54 | 1. 传统调取服务接口式搜索:封装了一个 `InternetSearchService` 服务类,通过封装请求先查询浏览器,将结果与问题封装为 Prompt 丢给大模型分析然后返回给用户答案(大模型被动处理结果,传统的函数式调用),接口:**`/rag/search`**、**`/rag/stream/search`**(流式输出)
55 | 
56 | 2. 封装一个工具类,将其注入到 MCP 中,实现大模型先查询自身数据集,无结果则主动进行联网搜索,处理并返回结果
57 | 
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