chore: Spring AI 重构

This commit is contained in:
abel533
2026-03-25 00:15:00 +08:00
parent a9c71002d2
commit 2afa4712cb
124 changed files with 11777 additions and 3530 deletions
+4 -4
View File
@@ -109,12 +109,12 @@ The pattern is universal. Only the capabilities change.
- `references/agent-philosophy.md` - Deep dive into why agents work
**Implementation**:
- `references/minimal-agent.py` - Complete working agent (~80 lines)
- `references/tool-templates.py` - Capability definitions
- `references/subagent-pattern.py` - Context isolation
- `references/MinimalAgent.java` - Complete working agent (基于 Spring AI ChatClient)
- `references/ToolTemplates.java` - Capability definitions (使用 `@Tool` 注解)
- `references/SubagentPattern.java` - Context isolation
**Scaffolding**:
- `scripts/init_agent.py` - Generate new agent projects
- `scripts/init-agent.sh` - Generate new Spring Boot agent project via `mvn archetype:generate`
## The Agent Mindset
+29 -24
View File
@@ -17,14 +17,14 @@ Check for:
- [ ] **Authorization flaws**: Missing access controls, IDOR
- [ ] **Data exposure**: Sensitive data in logs, error messages
- [ ] **Cryptography**: Weak algorithms, improper key management
- [ ] **Dependencies**: Known vulnerabilities (check with `npm audit`, `pip-audit`)
- [ ] **Dependencies**: Known vulnerabilities (check with `mvn dependency-check:check`, `npm audit`)
```bash
# Quick security scans
mvn dependency-check:check # Java (OWASP Dependency-Check)
mvn versions:display-dependency-updates # Check outdated deps
npm audit # Node.js
pip-audit # Python
cargo audit # Rust
grep -r "password\|secret\|api_key" --include="*.py" --include="*.js"
grep -r "password\|secret\|api_key" --include="*.java" --include="*.properties" --include="*.yml"
```
### 2. Correctness
@@ -89,23 +89,27 @@ Check for:
## Common Patterns to Flag
### Python
```python
# Bad: SQL injection
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
# Good:
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
### Java
```java
// Bad: SQL injection
Statement stmt = conn.createStatement();
stmt.executeQuery("SELECT * FROM users WHERE id = " + userId);
// Good: 使用 PreparedStatement 参数化查询
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setLong(1, userId);
ResultSet rs = ps.executeQuery();
# Bad: Command injection
os.system(f"ls {user_input}")
# Good:
subprocess.run(["ls", user_input], check=True)
// Bad: Command injection
Runtime.getRuntime().exec("ls " + userInput);
// Good: 使用 ProcessBuilder 分离命令和参数
new ProcessBuilder("ls", userInput).start();
# Bad: Mutable default argument
def append(item, lst=[]): # Bug: shared mutable default
# Good:
def append(item, lst=None):
lst = lst or []
// Bad: 未关闭资源
InputStream is = new FileInputStream("data.txt");
// Good: 使用 try-with-resources 自动关闭
try (InputStream is = new FileInputStream("data.txt")) {
// ...
}
```
### JavaScript/TypeScript
@@ -136,14 +140,15 @@ git log --oneline -10
# Find potential issues
grep -rn "TODO\|FIXME\|HACK\|XXX" .
grep -rn "password\|secret\|token" . --include="*.py"
grep -rn "password\|secret\|token" . --include="*.java" --include="*.properties"
# Check complexity (Python)
pip install radon && radon cc . -a
# Check complexity (Java)
mvn pmd:pmd # 静态代码分析
mvn spotbugs:check # Bug 检测
# Check dependencies
npm outdated # Node
pip list --outdated # Python
mvn versions:display-dependency-updates # 检查过期依赖
mvn dependency-check:check # OWASP 安全扫描
```
## Review Workflow
+125 -81
View File
@@ -14,60 +14,83 @@ MCP servers expose:
- **Resources**: Data Claude can read (like files or database records)
- **Prompts**: Pre-built prompt templates
## Quick Start: Python MCP Server
## Quick Start: Java/Spring AI MCP Server
### 1. Project Setup
```bash
# Create project
# 使用 Spring Initializr 创建项目
# 或通过 Maven 手动创建
mkdir my-mcp-server && cd my-mcp-server
python3 -m venv venv && source venv/bin/activate
mvn archetype:generate -DgroupId=com.example -DartifactId=my-mcp-server \
-DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
```
# Install MCP SDK
pip install mcp
`pom.xml` 中添加依赖:
```xml
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
```
### 2. Basic Server Template
```python
#!/usr/bin/env python3
"""my_server.py - A simple MCP server"""
```java
// src/main/java/com/example/McpServerApplication.java
package com.example;
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
# Create server instance
server = Server("my-server")
@SpringBootApplication
public class McpServerApplication {
# Define a tool
@server.tool()
async def hello(name: str) -> str:
"""Say hello to someone.
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
Args:
name: The name to greet
"""
return f"Hello, {name}!"
@Bean
public MyTools myTools() {
return new MyTools();
}
}
@server.tool()
async def add_numbers(a: int, b: int) -> str:
"""Add two numbers together.
// 定义工具类
class MyTools {
Args:
a: First number
b: Second number
"""
return str(a + b)
@Tool(description = "Say hello to someone")
public String hello(@ToolParam(description = "The name to greet") String name) {
return "Hello, " + name + "!";
}
# Run server
async def main():
async with stdio_server() as (read, write):
await server.run(read, write)
@Tool(description = "Add two numbers together")
public String addNumbers(
@ToolParam(description = "First number") int a,
@ToolParam(description = "Second number") int b) {
return String.valueOf(a + b);
}
}
```
if __name__ == "__main__":
import asyncio
asyncio.run(main())
配置 `application.properties`:
```properties
spring.ai.mcp.server.name=my-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.type=SYNC
spring.main.web-application-type=none
spring.main.banner-mode=off
spring.ai.mcp.server.stdio=true
```
### 3. Register with Claude
@@ -77,8 +100,8 @@ Add to `~/.claude/mcp.json`:
{
"mcpServers": {
"my-server": {
"command": "python3",
"args": ["/path/to/my_server.py"]
"command": "java",
"args": ["-jar", "/path/to/my-mcp-server/target/my-mcp-server.jar"]
}
}
}
@@ -140,74 +163,95 @@ server.connect(transport);
### External API Integration
```python
import httpx
from mcp.server import Server
```java
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
server = Server("weather-server")
public class WeatherTools {
@server.tool()
async def get_weather(city: str) -> str:
"""Get current weather for a city."""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://api.weatherapi.com/v1/current.json",
params={"key": "YOUR_API_KEY", "q": city}
)
data = resp.json()
return f"{city}: {data['current']['temp_c']}C, {data['current']['condition']['text']}"
private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
@Tool(description = "Get current weather for a city")
public String getWeather(@ToolParam(description = "City name") String city) {
String url = "https://api.weatherapi.com/v1/current.json?key=YOUR_API_KEY&q=" + city;
String response = restTemplate.getForObject(url, String.class);
JsonNode data = objectMapper.readTree(response);
JsonNode current = data.get("current");
return String.format("%s: %s°C, %s",
city, current.get("temp_c"), current.get("condition").get("text").asText());
}
}
```
### Database Access
```python
import sqlite3
from mcp.server import Server
```java
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.jdbc.core.JdbcTemplate;
server = Server("db-server")
public class DatabaseTools {
@server.tool()
async def query_db(sql: str) -> str:
"""Execute a read-only SQL query."""
if not sql.strip().upper().startswith("SELECT"):
return "Error: Only SELECT queries allowed"
private final JdbcTemplate jdbcTemplate;
conn = sqlite3.connect("data.db")
cursor = conn.execute(sql)
rows = cursor.fetchall()
conn.close()
return str(rows)
public DatabaseTools(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Tool(description = "Execute a read-only SQL query")
public String queryDb(@ToolParam(description = "SQL query to execute") String sql) {
if (!sql.trim().toUpperCase().startsWith("SELECT")) {
return "Error: Only SELECT queries allowed";
}
var rows = jdbcTemplate.queryForList(sql);
return rows.toString();
}
}
```
### Resources (Read-only Data)
```python
@server.resource("config://settings")
async def get_settings() -> str:
"""Application settings."""
return open("settings.json").read()
```java
import org.springframework.ai.tool.annotation.Tool;
import java.nio.file.Files;
import java.nio.file.Path;
@server.resource("file://{path}")
async def read_file(path: str) -> str:
"""Read a file from the workspace."""
return open(path).read()
public class ResourceTools {
@Tool(description = "Read application settings")
public String getSettings() throws Exception {
return Files.readString(Path.of("settings.json"));
}
@Tool(description = "Read a file from the workspace")
public String readFile(@ToolParam(description = "Path to the file") String path) throws Exception {
return Files.readString(Path.of(path));
}
}
```
## Testing
```bash
# Build the project
mvn clean package -DskipTests
# Test with MCP Inspector
npx @anthropics/mcp-inspector python3 my_server.py
npx @anthropics/mcp-inspector java -jar target/my-mcp-server.jar
# Or send test messages directly
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | python3 my_server.py
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | java -jar target/my-mcp-server.jar
```
## Best Practices
1. **Clear tool descriptions**: Claude uses these to decide when to call tools
2. **Input validation**: Always validate and sanitize inputs
3. **Error handling**: Return meaningful error messages
4. **Async by default**: Use async/await for I/O operations
1. **Clear tool descriptions**: Claude uses `@Tool(description=...)` to decide when to call tools
2. **Input validation**: Always validate and sanitize inputs in tool methods
3. **Error handling**: Return meaningful error messages, use proper exception handling
4. **Use Spring DI**: Leverage Spring's dependency injection for service wiring
5. **Security**: Never expose sensitive operations without auth
6. **Idempotency**: Tools should be safe to retry
+81 -55
View File
@@ -14,28 +14,27 @@ You now have expertise in PDF manipulation. Follow these workflows:
# Using pdftotext (poppler-utils)
pdftotext input.pdf - # Output to stdout
pdftotext input.pdf output.txt # Output to file
# If pdftotext not available, try:
python3 -c "
import fitz # PyMuPDF
doc = fitz.open('input.pdf')
for page in doc:
print(page.get_text())
"
```
**Option 2: Page-by-page with metadata**
```python
import fitz # pip install pymupdf
**Option 2: Page-by-page with metadata (Apache PDFBox)**
```java
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import java.io.File;
doc = fitz.open("input.pdf")
print(f"Pages: {len(doc)}")
print(f"Metadata: {doc.metadata}")
PDDocument doc = PDDocument.load(new File("input.pdf"));
System.out.println("Pages: " + doc.getNumberOfPages());
System.out.println("Metadata: " + doc.getDocumentInformation().getTitle());
for i, page in enumerate(doc):
text = page.get_text()
print(f"--- Page {i+1} ---")
print(text)
PDFTextStripper stripper = new PDFTextStripper();
for (int i = 1; i <= doc.getNumberOfPages(); i++) {
stripper.setStartPage(i);
stripper.setEndPage(i);
String text = stripper.getText(doc);
System.out.println("--- Page " + i + " ---");
System.out.println(text);
}
doc.close();
```
## Creating PDFs
@@ -49,64 +48,91 @@ pandoc input.md -o output.pdf
pandoc input.md -o output.pdf --pdf-engine=xelatex -V geometry:margin=1in
```
**Option 2: Programmatically**
```python
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
**Option 2: Programmatically (Apache PDFBox)**
```java
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
c = canvas.Canvas("output.pdf", pagesize=letter)
c.drawString(100, 750, "Hello, PDF!")
c.save()
PDDocument doc = new PDDocument();
PDPage page = new PDPage();
doc.addPage(page);
try (PDPageContentStream content = new PDPageContentStream(doc, page)) {
content.beginText();
content.setFont(PDType1Font.HELVETICA, 12);
content.newLineAtOffset(100, 750);
content.showText("Hello, PDF!");
content.endText();
}
doc.save("output.pdf");
doc.close();
```
**Option 3: From HTML**
```bash
# Using wkhtmltopdf
wkhtmltopdf input.html output.pdf
**Option 3: From HTML (OpenHTMLToPDF)**
```java
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
# Or with Python
python3 -c "
import pdfkit
pdfkit.from_file('input.html', 'output.pdf')
"
String html = Files.readString(Path.of("input.html"));
try (OutputStream os = new FileOutputStream("output.pdf")) {
PdfRendererBuilder builder = new PdfRendererBuilder();
builder.withHtmlContent(html, new File("input.html").toURI().toString());
builder.toStream(os);
builder.run();
}
```
## Merging PDFs
```python
import fitz
```java
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import java.io.File;
result = fitz.open()
for pdf_path in ["file1.pdf", "file2.pdf", "file3.pdf"]:
doc = fitz.open(pdf_path)
result.insert_pdf(doc)
result.save("merged.pdf")
PDFMergerUtility merger = new PDFMergerUtility();
merger.addSource(new File("file1.pdf"));
merger.addSource(new File("file2.pdf"));
merger.addSource(new File("file3.pdf"));
merger.setDestinationFileName("merged.pdf");
merger.mergeDocuments(null);
```
## Splitting PDFs
```python
import fitz
```java
import org.apache.pdfbox.multipdf.Splitter;
import org.apache.pdfbox.pdmodel.PDDocument;
import java.io.File;
import java.util.List;
doc = fitz.open("input.pdf")
for i in range(len(doc)):
single = fitz.open()
single.insert_pdf(doc, from_page=i, to_page=i)
single.save(f"page_{i+1}.pdf")
PDDocument doc = PDDocument.load(new File("input.pdf"));
Splitter splitter = new Splitter();
splitter.setSplitAtPage(1); // 每页拆分为一个文件
List<PDDocument> pages = splitter.split(doc);
for (int i = 0; i < pages.size(); i++) {
pages.get(i).save("page_" + (i + 1) + ".pdf");
pages.get(i).close();
}
doc.close();
```
## Key Libraries
| Task | Library | Install |
|------|---------|---------|
| Read/Write/Merge | PyMuPDF | `pip install pymupdf` |
| Create from scratch | ReportLab | `pip install reportlab` |
| HTML to PDF | pdfkit | `pip install pdfkit` + wkhtmltopdf |
| Task | Library | Maven Dependency |
|------|---------|-----------------|
| Read/Write/Merge/Split | Apache PDFBox | `org.apache.pdfbox:pdfbox:3.0.x` |
| Create from HTML | OpenHTMLToPDF | `com.openhtmltopdf:openhtmltopdf-pdfbox:1.0.x` |
| Advanced layout | iText | `com.itextpdf:itext7-core:8.0.x` |
| Text extraction | pdftotext | `brew install poppler` / `apt install poppler-utils` |
## Best Practices
1. **Always check if tools are installed** before using them
2. **Handle encoding issues** - PDFs may contain various character encodings
3. **Large PDFs**: Process page by page to avoid memory issues
4. **OCR for scanned PDFs**: Use `pytesseract` if text extraction returns empty
3. **Large PDFs**: Process page by page to avoid memory issues; use try-with-resources 确保资源释放
4. **OCR for scanned PDFs**: Use Tesseract4J (`net.sourceforge.tess4j:tess4j`) if text extraction returns empty