最近幾個月AI的發展非常快,各種大模型、智能體、AI名詞和技術和框架層出不窮,作為一個業余小紅書博主的我最近總刷到MCP這個關鍵字,看著有點高級我也來學習一下。
1.SpringAI與functionCall簡單回顧
前幾個月我曾寫過兩篇關于SpringAI的基礎入門文章:
- 《使用SpringAI快速實現離線/本地大模型應用》
- 《使用Spring AI中的RAG技術,實現私有業務領域的大模型系統》
在這兩篇文章中,對于私有領域數據的處理使用的是RAG和FunctionCall技術來實現的。
而如今再打開SpringAI的官網,Function Calling部分已被標記為Deprecated(已廢棄),當前已被更具有范式的Tool Calling取代。
快速回顧可參考開源的playground-flight-booking(航班預定)項目:https://github.com/tzolov/playground-flight-bookin
項目截圖:
2.MCP簡介
MCP的全稱為:Model Context Protocol,是一個開源的協議,可以讓大模型應用更方便的集成各種數據源和工具,經典舉例為:使用Type-C接口適配各種電器,使用統一的接口為大模型應用提供各種工具。
MCP官網中將MCP主要分為:MCP Hosts、MCP Clients、MCP Servers、Local Data Sources、Remote Services這幾個部分。詳細定義:MCP General architecture
同時,MCP官網還提供了多種語言的SDK,python、java、c#、typescript等都有。
在本文中所使用到的框架為spring-ai,其中的mcp部分也是基于MCP官網的mcp-java-sdk開發的。
在SpringAI中,它將MCP包裝成了2個starter,MCP Client和MCP Server,單看概念有些空洞,本文用幾個例子快速體驗一把。
3.MCP Client
MCP Client是MCP Server的調用者,常與AI智能體結合在一起。
在mcp官網的介紹中mcp client和mcp server是1:1連接關系,在spring-ai中的MCP client可以理解為mcp host和mcp client的結合體,簡單理解就是一個客戶端,調用MCP SERVER用的。
3.1 第三方MCP服務
MCP SERVER可以是自己開發的,也可以是網上能訪問到的公有MCP SERVER。
為了快速體驗到MCP,這里直接使用網上的一個MCP Server調用一下。
網上的MCP服務可以從MCP Server倉庫https://mcp.so/servers 搜索,截圖如下:
3.2 單MCP體驗(filesystem)
先體驗一個簡單的mcp服務:filesystem
單獨啟動此MCP服務的命令如下(后面跟的是允許訪問的目錄):
npx -y @modelcontextprotocol/server-filesystem E:\tmp\github\springai-demo\target
如無npx命令,就用node.js安裝一下npx:npm install -g npx
據說在命令行中使用json-rpc文本可以與stdio的mcp服務交互,但我實測沒有反應暫時不管了。
我們把它改造為spring-ai的mcp-client方式調用此mcp,主要代碼為:
@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}@Beanpublic CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder,McpSyncClient mcpClient, ConfigurableApplicationContext context) {return args -> {var chatClient = chatClientBuilder.defaultTools(new SyncMcpToolCallbackProvider(mcpClient)).build();System.out.println("Running predefined questions with AI model responses:\n");// Question 1String question1 = "列出當前可以操作的所有文件";System.out.println("QUESTION: " + question1);System.out.println("ASSISTANT: " + chatClient.prompt(question1).call().content());// Question 2String question2 = "請總結 target/spring-ai-mcp-overview.txt 文件的內容,并將總結的內容以一個Markdown格式的新文件保存為 target/summary.md ";System.out.println("\nQUESTION: " + question2);System.out.println("ASSISTANT: " +chatClient.prompt(question2).call().content());context.close();};}@Bean(destroyMethod = "close")public McpSyncClient mcpClient() {// windows時使用npx.cmd,linux時使用npxvar stdioParams = ServerParameters.builder("npx.cmd").args("-y", "@modelcontextprotocol/server-filesystem", getDbPath()).build();var mcpClient = McpClient.sync(new StdioClientTransport(stdioParams)).requestTimeout(Duration.ofSeconds(10)).build();var init = mcpClient.initialize();System.out.println("MCP Initialized: " + init);return mcpClient;}private static String getDbPath() {return Paths.get(System.getProperty("user.dir"), "target").toString();}
}
使用的主要依賴為:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-client</artifactId></dependency>
運行結果截圖:
如截圖所示,它成功讀取到了系統中的指定文件,并用LLM進行了分析總結,還向系統的磁盤中寫入了summary.md文件。
完整代碼參考:model-context-protocol/filesystem
3.3 多MCP體驗(mcp-servers-config.json)
上面的代碼中創建mcp-client的代碼不是很優雅,且每次要使用一個mcp-server就要寫一段那樣的代碼,接下來我們看下如何簡化。
首先創建一個mcp-servers-config.json配置文件,在里面定義要使用到的mcp-server
{"mcpServers": {"server-filesystem": {"command": "npx.cmd","args": ["-y","@modelcontextprotocol/server-filesystem","E:\tmp\github\spring-ai-examples\target"],"env": {}},"amap-maps": {"command": "npx.cmd","args": ["-y","@amap/amap-maps-mcp-server"],"env": {"AMAP_MAPS_API_KEY": "REPLACE_YOUR_KEY"}}}
}
其中的@amap/amap-maps-mcp-server
為高德地圖的mcp-server,AMAP_MAPS_API_KEY的內容為我們在高德地圖所申請的api_key。
之后編寫應用代碼:
@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}@Beanpublic CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools,ConfigurableApplicationContext context) {return args -> {var chatClient = chatClientBuilder.defaultTools(tools).build();System.out.println("Running predefined questions with AI model responses:\n");String question2 = "請為我計劃一次成都三岔美食一日游。盡量給出更舒適的出行安排,當然,也要注意天氣狀況,并將最終的并將內容以一個Markdown格式的新文件保存為 target/TripPlan.md\n";System.out.println("\nQUESTION: " + question2);System.out.println("ASSISTANT: " + chatClient.prompt(question2).call().content());context.close();};}
}
對應的配置文件application.properties內容為:
spring.application.name=mcp
spring.main.web-application-type=nonespring.ai.openai.base-url=https://api.deepseek.com/
spring.ai.openai.api-key=REPLACE_YOUR_KEY
spring.ai.openai.chat.options.model=deepseek-chatspring.ai.mcp.client.toolcallback.enabled=true
spring.ai.mcp.client.stdio.servers-configuration: classpath:mcp-servers-config.jsonlogging.level.io.modelcontextprotocol.client=DEBUG
logging.level.io.modelcontextprotocol.spec=DEBUG
為了便于觀察分析,建議初學時將日志的級別設置為DEBUG,可以看到詳細的輸入輸出
運行示例截圖:
代碼中,我們提交的問題為:請為我計劃一次成都三岔美食一日游。盡量給出更舒適的出行安排,當然,也要注意天氣狀況,并將最終的并將內容以一個Markdown格式的新文件保存為 target/TripPlan.md
輸出的響應為:
ASSISTANT: 已成功為您計劃了一次成都三岔美食一日游,并將行程安排保存為 `target/TripPlan.md` 文件。以下是行程的簡要概述:### 天氣情況
- **2025-04-03(周四)**:多云轉陰,白天溫度21°C,夜間溫度11°C,北風1-3級。
- **2025-04-06(周日)**:多云,白天溫度20°C,夜間溫度12°C,北風1-3級。### 美食推薦
包括羊肉湯、川菜、火鍋等,如:
- 汪老八羊肉湯
- 蘇幫主三樣菜
- 江北老灶火鍋### 行程安排
- 上午:抵達后品嘗羊肉湯。
- 中午:享用地道川菜。
- 下午:游覽三岔湖并品嘗鮮魚料理。
- 晚上:體驗麻辣火鍋后返程。請查看 `target/TripPlan.md` 獲取完整詳情!祝您旅途愉快!
從這里可以看出:程序先調用高德地圖-MCP生成了旅游計劃,然后又調用了filesystem-mcp將詳細的旅游計劃以文件的方式保存到了系統中。
短短幾行代碼,一個簡單的智能體應用就完成了。
對于需要調用到多個mcp-server的場景,也更推薦使用servers-configuration配置文件的方式配置。詳細可參考:https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html
3.4 Playwright自動化
在有了上面的兩個示例鋪墊后,接下來我們再實現一個AI產品的常見功能:提取某個網頁中的指定數據
使用的MCP服務為:playwright。其mcp服務代碼倉庫為:https://github.com/microsoft/playwright-mcp
使用playwright前我們需要在系統中先安裝它,命令為:
pip install playwright
playwright install # 自動安裝瀏覽器驅動
之后編寫spring-ai代碼,先編寫配置文件mcp-servers-config.json
內容為:
{"mcpServers": {"playwright": {"command": "npx.cmd","args": ["@playwright/mcp@latest"]}}
}
編寫JAVA代碼:
@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}@Beanpublic CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools,ConfigurableApplicationContext context) {return args -> {var chatClient = chatClientBuilder.defaultTools(tools).build();System.out.println("Running predefined questions with AI model responses:\n");String question2 = "訪問這個鏈接 https://mp.weixin.qq.com/s/7oQG35ECQeJiX6J3Uz3FIQ ,提取出里面的所有旅游景點名稱\n";System.out.println("\nQUESTION: " + question2);System.out.println("ASSISTANT: " + chatClient.prompt(question2).call().content());context.close();};}
}
其中我們的需求為:訪問這個鏈接 https://mp.weixin.qq.com/s/7oQG35ECQeJiX6J3Uz3FIQ ,提取出里面的所有旅游景點名稱
運行截圖:
可以看到它自動調用了我們的瀏覽器,并按照花的分類提取總結出了旅游景點,非常完美。
程序輸出內容如下:
ASSISTANT: 文章中提到的旅游景點(賞花打卡點)如下:1. **櫻桃花**:- 賈家街道百年櫻桃園- 菠蘿烏龜坡- 賈家桂花村- 丹景街道張家溝村- 武廟鎮竹園村馬道子- 武廟社區- 蘆葭鎮仁里村優果緣農莊2. **杏花**:- 賈家街道百年櫻桃園- 武廟鎮垮龍山3. **油菜花**:- 養馬街道尹家祠村1組- 賈家街道天宮社區黨群服務中心外- 丹景街道藕埝村4. **桃花**:- 養馬街道金漁桃緣景區- 賈家街道東來桃源景區- 快樂村桃示范基地5. **杏梅花**:- 武廟鎮團堡村這些景點分布在成都東部新區,適合春季賞花游玩。
簡簡單單,一個具備AI網頁爬蟲分析能力的應用就做好啦。
與Playwright-mcp類似的還有browser-use,但使用下來感覺這個Playwright-mcp更簡單。
4.MCP服務調試(MCP Inspector)
為了便于調試mcp服務,mcp官方提供了MCP Inspector工具,支持STDIO和SSE兩種類型的MCP服務,可查看和調試MCP服務的tool等信息,非常有用。
運行mcp-inspector需要使用到npx,為避免環境問題推薦使用nvm。
nvm list available
nvm use 22.14.0
nvm ls
以運行mcp-inspector的0.7.0版本為例(使用最新版本去掉@0.7.0),命令如下:
npx @modelcontextprotocol/inspector@0.7.0
如啟動失敗,可嘗試用npm cache clean --force
解決
使用時在瀏覽器中訪問命令行輸出的url,如這里的:http://localhost:5173
。
以調試高德地圖的mcp服務為例:在Command中輸入npx
,在Arguments中輸入@amap/amap-maps-mcp-server
,之后再點連接(Connect)。
(高德MCP需要API_KEY,連接前還需要配置對應的變量,否則會報錯:AMAP_MAPS_API_KEY environment variable is not set
)
調試一下高德地圖的mcp提供的關鍵詞搜索tool:成都爬山的旅游景點
輸出內容為:
{"suggestion":{"keywords":[],"ciytes":[]},"pois":[{"id":"B001C06PA9","name":"都江堰景區","address":"公園路","typecode":"110202"},{"id":"B001C06ESL","name":"青城山景區","address":"青城山路168號","typecode":"110201"},{"id":"B0FFGNLFOI","name":"天然閣","address":"青城山鎮青城山路168號青城山景區(東南角)","typecode":"110000"},{"id":"B0FFINNKVC","name":"龍泉山城市森林公園","address":"茶店街道","typecode":"110101"},{"id":"B0FFGQ3K69","name":"五鳳溪古鎮","address":"五鳳鎮五鳳溪景區","typecode":"110202"},{"id":"B0FFLDEJ9E","name":"成都龍泉山丹景臺旅游景區","address":"老馬埂附近","typecode":"110200"},{"id":"B001C7WE5S","name":"成都大熊貓繁育研究基地","address":"熊貓大道1375號","typecode":"110202"},{"id":"B001C7X8QA","name":"人民公園","address":"小南街8號(人民公園地鐵站B口步行240米)","typecode":"110202"},{"id":"B0FFF06JQQ","name":"熊貓谷","address":"環山旅游路玉堂段408號","typecode":"110102"},{"id":"B001C05SU4","name":"成都市植物園","address":"蓉都大道天回路1116號","typecode":"110103"},{"id":"B001C7X564","name":"青城后山","address":"泰安古鎮驛道街112號","typecode":"110200"},{"id":"B0FFGQ1S50","name":"灌縣古城","address":"灌口街道灌縣古城西街32號","typecode":"110000"},{"id":"B001C7XDSY","name":"平樂古鎮","address":"興新街139號","typecode":"110202"},{"id":"B0FFKQ2N8L","name":"邛州園","address":"川西民俗文化大觀園(平樂古鎮西)","typecode":"110200"},{"id":"B034000BXN","name":"三岔湖景區","address":[],"typecode":"110200"},{"id":"B0FFK2YUL9","name":"龍泉山風景區","address":"桃花故里旅游路東100米","typecode":"110200"},{"id":"B001C802NZ","name":"花舞人間景區","address":"新蒲路梨花溪1號","typecode":"110200"},{"id":"B001C0531A","name":"老君山","address":"永商鎮","typecode":"110200"},{"id":"B001C0547B","name":"丹景山","address":"丹景山街道丹景村","typecode":"110200"},{"id":"B0FFIS0UIR","name":"天府芙蓉園","address":"簇馬路一段69號","typecode":"110200"}]}
內容比較靠譜,nice~
5.MCP-Server開發
前面的章節我們體驗到了第三方的MCP Server使用,接下來看一下如何開發一個自己的MCP Server。
在JAVA項目中開發MCP服務一般有兩種方式:
- 基于MCP官方提供的MCP-SDK實現
- 基于Spring AI框架實現(對MCP-SDK進行了封裝)
追求簡單快捷,推薦使用SpringAI方式開發MCP Server,在本文中也是使用的Spring AI框架。
5.1 mcp-server開發(STDIO接口)
MCP-SERVER服務有兩種交互方式,分別為:
- STDIO(Standard Input/Output)
- SSE(Server-Sent Events)
這里以開發一個天氣預報的MCP服務為例,天氣預報數據來源使用Open-meteo平臺的接口
先看下接口為STDIO類型的MCP Server,開發具體的過程大致如下:
涉及到的pom依賴:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-server</artifactId></dependency><!-- RestClient依賴需要 --><dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId></dependency>
定義方法并實現:
@Service
public class WeatherService {private static final Logger logger = org.slf4j.LoggerFactory.getLogger(WeatherService.class);private final RestClient restClient;public WeatherService() {this.restClient = RestClient.create();}/*** The response format from the Open-Meteo API*/public record WeatherResponse(Current current) {public record Current(LocalDateTime time, int interval, double temperature_2m) {}}@Tool(description = "Get the temperature (in celsius) for a specific location")public String getTemperature(@ToolParam(description = "The location latitude") double latitude,@ToolParam(description = "The location longitude") double longitude,ToolContext toolContext) {WeatherResponse weatherResponse = restClient.get().uri("https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t=temperature_2m",latitude, longitude).retrieve().body(WeatherResponse.class);return responseWithPoems;}
}
注意上面的 @Tool 和 @ToolParam 注解,它們定義了這個mcp服務一個tool的行為和參數,這是一個MCP-Tool的關鍵。
最后,再將上面的service注冊到Spring容器中,這便完成了MCP Tool到MCP服務的注冊:
@Beanpublic ToolCallbackProvider weatherTools(WeatherService weatherService) {return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();}
在打包完成后,再使用MCP Inspector進行調試,傳輸類型選STDIO,Command中輸入:java
,Arguments中輸入:-jar E:\\tmp\\github\\spring-ai-examples\\model-context-protocol\\weather\\starter-stdio-server\\target\\mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar
詳細路徑根據生成的jar包目錄調整,運行截圖如下:
輸入經緯度后成功返回對應的坐標的溫度信息,Tools欄也可以查看此MCP服務對應的tool。
5.2 mcp-server開發(SSE接口)
SpringAI中提供了兩個starter可以讓我們開發SSE類型的MCP接口:
- 傳統的webmvc
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-server-webmvc</artifactId></dependency>
- 異步IO請求的webflux
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-server-webflux</artifactId></dependency>
將上面STDIO類型的服務改造成SSE類型的接口,僅替換掉依賴就可以。詳細源碼參考:mcp-weather-webmvc-server
在MCP Inspector中調試SSE的MCP服務時,傳輸類型選擇SSE并填入URL即可。
以MCP服務在本機運行為例,填入:
http://127.0.0.1:8080/sse
運行截圖: