chore: Spring AI 重构
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user