This is an automated email from the ASF dual-hosted git repository.
xcsnx pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/shenyu-website.git
The following commit(s) were added to refs/heads/main by this push:
new 66c49cc0236 add:mcpServer plugin analysis blog (#1086)
66c49cc0236 is described below
commit 66c49cc023660aa68dd6c3379823a37dfdb55f0c
Author: Yu Siheng <[email protected]>
AuthorDate: Sat Oct 18 14:49:35 2025 +0800
add:mcpServer plugin analysis blog (#1086)
* add:mcpServer plugin analysis blog
* fix
* fix
---
...Plugin-SourceCode-Analysis-Mcp-Server-Plugin.md | 497 ++++++++++++++++++++
i18n/zh/code.json | 3 +
...Plugin-SourceCode-Analysis-Mcp-Server-Plugin.md | 522 +++++++++++++++++++++
src/data/blogInfo.js | 10 +
.../Mcp-server-execute-en.png | Bin 0 -> 16794 bytes
.../Mcp-server-execute-zh.png | Bin 0 -> 24826 bytes
.../Mcp-server-register-en.png | Bin 0 -> 27125 bytes
.../Mcp-server-register-zh.png | Bin 0 -> 28758 bytes
.../Mcp-server-tool-call-en.png | Bin 0 -> 4947 bytes
.../Mcp-server-tool-call-zh.png | Bin 0 -> 6829 bytes
static/img/blog/yusiheng.png | Bin 0 -> 72550 bytes
11 files changed, 1032 insertions(+)
diff --git a/blog/Plugin-SourceCode-Analysis-Mcp-Server-Plugin.md
b/blog/Plugin-SourceCode-Analysis-Mcp-Server-Plugin.md
new file mode 100644
index 00000000000..46054118732
--- /dev/null
+++ b/blog/Plugin-SourceCode-Analysis-Mcp-Server-Plugin.md
@@ -0,0 +1,497 @@
+---
+title: McpServer Plugin Source Code Analysis
+author: yusiheng
+author_title: Apache ShenYu Contributor
+author_url: https://github.com/478320
+tags: [plugin,mcp,Apache ShenYu]
+---
+
+In the Shenyu gateway, when you start this plugin, Shenyu becomes a
fully-featured McpServer.
+You can easily register a service as a tool within the Shenyu gateway by
simple configuration and use the extended functions the gateway offers.
+
+> This article is based on version `shenyu-2.7.0.2`. Here, I will track the
Shenyu Mcp plugin chain and analyze the source code of its SSE communication.
+
+### Introduction
+
+> The Shenyu gateway's Mcp plugin is built on top of the spring-ai-mcp
extension. To better understand how the Mcp plugin works, I’ll briefly
introduce how some official Mcp Java classes collaborate within its JDK.
+
+I want to start by introducing three key official Mcp Java classes:
+
+>1. **McpServer**
+ > This class manages resources like tools, Resource, promote, etc.
+>2. **TransportProvider**
+ > Provides corresponding communication methods based on client-server
communication protocols.
+>3. **Session**
+ > Handles request data, response data, and notifications, offers some
basic methods and corresponding handlers, and executes tool queries and calls
here.
+
+### 1. Service Registration
+
+In Shenyu Admin, after filling in endpoint and tool information for the
McpServer plugin, this info is automatically registered into Shenyu bootstrap.
+You can refer to the official [websocket data sync source
code](https://shenyu.incubator.apache.org/blog/DataSync-SourceCode-Analysis-WebSocket-Data-Sync)
for details.
+
+Shenyu bootstrap receives the data synced from admin in the `handler()` method
of `McpServerPluginDataHandler`.
+
+- `handlerSelector()` receives URL data and creates McpServer.
+- `handlerRule()` receives tool info and registers tools.
+
+These two methods together form the service registration part of the Shenyu
Mcp plugin. Below, I will analyze these two methods in detail.
+
+#### 1.1 Transport and McpServer Registration
+
+Let’s analyze the `handlerSelector()` method, which handles McpServer
registration.
+
+* What `handlerSelector()` does:
+
+```java
+public class McpServerPluginDataHandler implements PluginDataHandler {
+ @Override
+ public void handlerSelector(final SelectorData selectorData) {
+ // Get URI
+ String uri = selectorData.getConditionList().stream()
+ .filter(condition ->
Constants.URI.equals(condition.getParamType()))
+ .map(ConditionData::getParamValue)
+ .findFirst()
+ .orElse(null);
+
+ // Build McpServer
+ ShenyuMcpServer shenyuMcpServer =
GsonUtils.getInstance().fromJson(Objects.isNull(selectorData.getHandle()) ?
DEFAULT_MESSAGE_ENDPOINT : selectorData.getHandle(), ShenyuMcpServer.class);
+ shenyuMcpServer.setPath(path);
+ // Cache shenyuMcpServer
+ CACHED_SERVER.get().cachedHandle(
+ selectorData.getId(),
+ shenyuMcpServer);
+ String messageEndpoint = shenyuMcpServer.getMessageEndpoint();
+ // Try to get or register transportProvider
+ shenyuMcpServerManager.getOrCreateMcpServerTransport(uri,
messageEndpoint);
+ }
+
+}
+```
+
+> `ShenyuMcpServerManager` is the management center of McpServer in Shenyu. It
not only stores `McpAsyncServer`, `CompositeTransportProvider`, etc., but also
contains methods to register Transport and McpServer.
+
+* The `getOrCreateMcpServerTransport()` method works as follows:
+
+```java
+@Component
+public class ShenyuMcpServerManager {
+ public ShenyuSseServerTransportProvider
getOrCreateMcpServerTransport(final String uri, final String messageEndPoint) {
+ // Remove /streamablehttp and /message suffixes
+ String normalizedPath = processPath(uri);
+ return getOrCreateTransport(normalizedPath, SSE_PROTOCOL,
+ () -> createSseTransport(normalizedPath, messageEndPoint));
+ }
+
+ private <T> T getOrCreateTransport(final String normalizedPath, final
String protocol,
+ final java.util.function.Supplier<T>
transportFactory) {
+ // Get composite Transport instance
+ CompositeTransportProvider compositeTransport =
getOrCreateCompositeTransport(normalizedPath);
+
+ T transport = switch (protocol) {
+ case SSE_PROTOCOL -> (T) compositeTransport.getSseTransport();
+ case STREAMABLE_HTTP_PROTOCOL -> (T)
compositeTransport.getStreamableHttpTransport();
+ default -> null;
+ };
+ // If instance is missing in cache, create a new one
+ if (Objects.isNull(transport)) {
+ // Call createSseTransport() to create and store a new transport
+ transport = transportFactory.get();
+ // Create McpAsyncServer and register the transport
+ addTransportToSharedServer(normalizedPath, protocol, transport);
+ }
+
+ return transport;
+ }
+}
+```
+
+##### 1.1.1 Transport Registration
+
+* `createSseTransport()` method
+> This method is called within `getOrCreateMcpServerTransport()` and is used
to create a Transport
+
+```java
+@Component
+public class ShenyuMcpServerManager {
+ private ShenyuSseServerTransportProvider createSseTransport(final String
normalizedPath, final String messageEndPoint) {
+ String messageEndpoint = normalizedPath + messageEndPoint;
+ ShenyuSseServerTransportProvider transportProvider =
ShenyuSseServerTransportProvider.builder()
+ .objectMapper(objectMapper)
+ .sseEndpoint(normalizedPath)
+ .messageEndpoint(messageEndpoint)
+ .build();
+ // Register the two functions of transportProvider to the Manager's
routeMap
+ registerRoutes(normalizedPath, messageEndpoint,
transportProvider::handleSseConnection, transportProvider::handleMessage);
+ return transportProvider;
+ }
+}
+```
+
+##### 1.1.2 McpServer Registration
+
+* `addTransportToSharedServer()` method
+> This method is called within `getOrCreateMcpServerTransport()` and is used
to create and save McpServer
+
+This method creates a new McpServer, stores it in `sharedServerMap`, and saves
the TransportProvider obtained above into `compositeTransportMap`.
+
+```java
+@Component
+public class ShenyuMcpServerManager {
+ private void addTransportToSharedServer(final String normalizedPath, final
String protocol, final Object transportProvider) {
+ // Get or create and register McpServer
+ getOrCreateSharedServer(normalizedPath);
+
+ // Save the new transport protocol into compositeTransportMap
+ compositeTransport.addTransport(protocol, transportProvider);
+
+ }
+
+ private McpAsyncServer getOrCreateSharedServer(final String
normalizedPath) {
+ return sharedServerMap.computeIfAbsent(normalizedPath, path -> {
+ // Get transport protocols
+ CompositeTransportProvider compositeTransport =
getOrCreateCompositeTransport(path);
+
+ // Select server capabilities
+ var capabilities = McpSchema.ServerCapabilities.builder()
+ .tools(true)
+ .logging()
+ .build();
+
+ // Create and store McpServer
+ McpAsyncServer server = McpServer
+ .async(compositeTransport)
+ .serverInfo("MCP Shenyu Server (Multi-Protocol)", "1.0.0")
+ .capabilities(capabilities)
+ .tools(Lists.newArrayList())
+ .build();
+
+ return server;
+ });
+ }
+}
+```
+
+#### 1.2 Tools Registration
+
+* `handlerRule()` method works as follows:
+
+1. Captures the tool configuration info users fill in for the Tool, all used
to build the tool
+2. Deserializes to create `ShenyuMcpServerTool` and obtains tool info
+
+> Note: `ShenyuMcpServerTool` is also a Shenyu-side object for storing tool
info, unrelated by inheritance to `McpServerTool`
+
+3. Calls `addTool()` method to create the tool using this info and registers
the tool to the matching McpServer based on SelectorId
+
+```java
+public class McpServerPluginDataHandler implements PluginDataHandler {
+ @Override
+ public void handlerRule(final RuleData ruleData) {
+ Optional.ofNullable(ruleData.getHandle()).ifPresent(s -> {
+ // Deserialize a new ShenyuMcpServerTool
+ ShenyuMcpServerTool mcpServerTool =
GsonUtils.getInstance().fromJson(s, ShenyuMcpServerTool.class);
+ // Cache mcpServerTool
+
CACHED_TOOL.get().cachedHandle(CacheKeyUtils.INST.getKey(ruleData),
mcpServerTool);
+ // Build MCP schema
+ List<McpServerToolParameter> parameters =
mcpServerTool.getParameters();
+ String inputSchema =
JsonSchemaUtil.createParameterSchema(parameters);
+ ShenyuMcpServer server =
CACHED_SERVER.get().obtainHandle(ruleData.getSelectorId());
+ if (Objects.nonNull(server)) {
+ // Save tool info into Manager's sharedServerMap
+ shenyuMcpServerManager.addTool(server.getPath(),
+ StringUtils.isBlank(mcpServerTool.getName()) ?
ruleData.getName()
+ : mcpServerTool.getName(),
+ mcpServerTool.getDescription(),
+ mcpServerTool.getRequestConfig(),
+ inputSchema);
+ }
+ });
+ }
+}
+```
+
+* `addTool()` method
+> This method is called by `handlerRule()` to add a new tool
+
+This method performs:
+
+1. Converts the previous tool info into a `shenyuToolDefinition` object
+2. Creates a `ShenyuToolCallback` object using the converted
`shenyuToolDefinition`
+> `ShenyuToolCallback` overrides the `call()` method of `ToolCallBack` and
registers this overridden method to `AsyncToolSpecification`, so calling the
tool's `call()` will actually invoke this overridden method
+
+3. Converts `ShenyuToolCallback` to `AsyncToolSpecification` and registers it
to the corresponding McpServer
+
+```java
+public class McpServerPluginDataHandler implements PluginDataHandler {
+ public void addTool(final String serverPath, final String name, final
String description,
+ final String requestTemplate, final String
inputSchema) {
+ String normalizedPath = normalizeServerPath(serverPath);
+ // Build Definition object
+ ToolDefinition shenyuToolDefinition = ShenyuToolDefinition.builder()
+ .name(name)
+ .description(description)
+ .requestConfig(requestTemplate)
+ .inputSchema(inputSchema)
+ .build();
+
+ ShenyuToolCallback shenyuToolCallback = new
ShenyuToolCallback(shenyuToolDefinition);
+
+ // Get previously registered McpServer and register the Tool
+ McpAsyncServer sharedServer = sharedServerMap.get(normalizedPath);
+ for (AsyncToolSpecification asyncToolSpecification :
McpToolUtils.toAsyncToolSpecifications(shenyuToolCallback)) {
+ sharedServer.addTool(asyncToolSpecification).block();
+ }
+ }
+}
+```
+
+With this, service registration analysis is complete.
+
+Service registration overview diagram
+
+
+### 2. Plugin Execution
+
+Clients will send two types of messages with `/sse` and `/message` suffixes.
These messages are captured by the Shenyu McpServer plugin, which handles them
differently. When receiving `/sse` messages, the plugin creates and saves a
session object, then returns a session id for `/message` usage. When receiving
`/message` messages, the plugin executes methods based on the method info
carried by the `/message` message, such as fetching work lists, tool
invocation, and resource lists.
+
+* `doExecute()` method works as follows:
+
+1. Matches the path and checks if the Mcp plugin registered it
+2. Calls `routeByProtocol()` to choose the appropriate handling plan based on
the request protocol
+
+> This article focuses on the SSE request mode, so we enter the
`handleSseRequest()` method
+
+```java
+public class McpServerPlugin extends AbstractShenyuPlugin {
+ @Override
+ protected Mono<Void> doExecute(final ServerWebExchange exchange,
+ final ShenyuPluginChain chain,
+ final SelectorData selector,
+ final RuleData rule) {
+ final String uri = exchange.getRequest().getURI().getRawPath();
+ // Check if Mcp plugin registered this route; if not, continue chain
without handling
+ if (!shenyuMcpServerManager.canRoute(uri)) {
+ return chain.execute(exchange);
+ }
+ final ServerRequest request = ServerRequest.create(exchange,
messageReaders);
+ // Choose handling method based on URI protocol
+ return routeByProtocol(exchange, chain, request, selector, uri);
+ }
+
+ private Mono<Void> routeByProtocol(final ServerWebExchange exchange,
+ final ShenyuPluginChain chain,
+ final ServerRequest request,
+ final SelectorData selector,
+ final String uri) {
+
+ if (isStreamableHttpProtocol(uri)) {
+ return handleStreamableHttpRequest(exchange, chain, request, uri);
+ } else if (isSseProtocol(uri)) {
+ // Handle SSE requests
+ return handleSseRequest(exchange, chain, request, selector, uri);
+ }
+ }
+}
+```
+
+* `handleSseRequest()` method
+> Called by `routeByProtocol()` to determine if the client wants to create a
session or call a tool based on URI suffix
+
+```java
+public class McpServerPlugin extends AbstractShenyuPlugin {
+ private Mono<Void> handleSseRequest(final ServerWebExchange exchange,
+ final ShenyuPluginChain chain,
+ final ServerRequest request,
+ final SelectorData selector,
+ final String uri) {
+ ShenyuMcpServer server =
McpServerPluginDataHandler.CACHED_SERVER.get().obtainHandle(selector.getId());
+ String messageEndpoint = server.getMessageEndpoint();
+ // Get the transport provider
+ ShenyuSseServerTransportProvider transportProvider
+ = shenyuMcpServerManager.getOrCreateMcpServerTransport(uri,
messageEndpoint);
+ // Determine if the request is an SSE connection or a message call
+ if (uri.endsWith(messageEndpoint)) {
+ setupSessionContext(exchange, chain);
+ return handleMessageEndpoint(exchange, transportProvider, request);
+ } else {
+ return handleSseEndpoint(exchange, transportProvider, request);
+ }
+ }
+}
+```
+
+#### 2.1 Client Sends SSE Request
+
+> If the client sends a request ending with `/sse`, the `handleSseEndpoint()`
method is executed
+
+* `handleSseEndpoint()` mainly does:
+
+1. Sets SSE request headers
+2. Calls `ShenyuSseServerTransportProvider.createSseFlux()` to create the SSE
stream
+
+```java
+public class McpServerPlugin extends AbstractShenyuPlugin {
+ private Mono<Void> handleSseEndpoint(final ServerWebExchange exchange,
+ final
ShenyuSseServerTransportProvider transportProvider,
+ final ServerRequest request) {
+ // Configure SSE request headers
+ configureSseHeaders(exchange);
+
+ // Create SSE stream
+ return exchange.getResponse()
+ .writeWith(transportProvider
+ .createSseFlux(request));
+ }
+}
+```
+
+* `createSseFlux()` method
+> Called by `handleSseEndpoint()`; mainly used to create and save a session
+> 1. Creates session; the session factory registers a series of handlers,
which are the objects actually executing tool calls
+> 2. Saves the session for reuse
+> 3. Sends the session id as a parameter of the endpoint URL back to the
client, to be used when calling the message endpoint
+
+```java
+public class ShenyuSseServerTransportProvider implements
McpServerTransportProvider {
+ public Flux<ServerSentEvent<?>> createSseFlux(final ServerRequest request)
{
+ return Flux.<ServerSentEvent<?>>create(sink -> {
+ WebFluxMcpSessionTransport sessionTransport = new
WebFluxMcpSessionTransport(sink);
+ // Create McpServerSession and temporarily store plugin
chain info
+ McpServerSession session =
sessionFactory.create(sessionTransport);
+ String sessionId = session.getId();
+ sessions.put(sessionId, session);
+
+ // Send session id info back to client
+ String endpointUrl = this.baseUrl + this.messageEndpoint +
"?sessionId=" + sessionId;
+ ServerSentEvent<String> endpointEvent =
ServerSentEvent.<String>builder()
+ .event(ENDPOINT_EVENT_TYPE)
+ .data(endpointUrl)
+ .build();
+ }).doOnSubscribe(subscription -> LOGGER.info("SSE Flux
subscribed"))
+ .doOnRequest(n -> LOGGER.debug("SSE Flux requested {} items",
n));
+ }
+}
+```
+
+#### 2.2 Client Sends Message Request
+
+> If the client sends a request ending with `/message`, the current
`ShenyuPluginChain` info is saved into the session, and
`handleMessageEndpoint()` is called.
+> Subsequent tool calls continue executing this plugin chain, so plugins after
the Mcp plugin will affect tool requests.
+
+* `handleMessageEndpoint()` method, calls
`ShenyuSseServerTransportProvider.handleMessageEndpoint()` to process
+
+```
+if (uri.endsWith(messageEndpoint)) {
+ setupSessionContext(exchange, chain);
+ return handleMessageEndpoint(exchange, transportProvider, request);
+}
+```
+
+```java
+public class McpServerPlugin extends AbstractShenyuPlugin {
+ private Mono<Void> handleMessageEndpoint(final ServerWebExchange exchange,
+ final
ShenyuSseServerTransportProvider transportProvider,
+ final ServerRequest request) {
+ // Handle message requests
+ return transportProvider.handleMessageEndpoint(request)
+ .flatMap(result -> {
+ return exchange.getResponse()
+
.writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(responseBody.getBytes())));
+ });
+ }
+}
+```
+
+* `handleMessageEndpoint()` method
+> Called by `McpServerPlugin.handleMessageEndpoint()`, hands over the request
to the session for processing
+
+The session's `handler()` method performs different actions depending on the
message.
+For example, when the method in the message is `"tools/call"`, the tool
invocation handler executes the `call()` method to call the tool.
+The related source is omitted here.
+
+```java
+public class ShenyuSseServerTransportProvider implements
McpServerTransportProvider {
+ public Mono<MessageHandlingResult> handleMessageEndpoint(final
ServerRequest request) {
+ // Get session
+ String sessionId = request.queryParam("sessionId").get();
+ McpServerSession session = sessions.get(sessionId);
+ return request.bodyToMono(String.class)
+ .flatMap(body -> {
+ McpSchema.JSONRPCMessage message =
McpSchema.deserializeJsonRpcMessage(objectMapper, body);
+ return session.handle(message);
+ });
+ }
+}
+```
+
+At this point, the Shenyu Mcp Plugin service invocation source code analysis
is complete.
+
+Process flow overview
+
+
+### 3. Tool Invocation
+
+> If the client sends a message to invoke a tool, the session will use the
tool invocation handler to execute the tool’s `call()` method.
+> From service registration, we know the tool call actually runs the `call()`
method of `ShenyuToolCallback`.
+
+Therefore, the tool invocation executes the following:
+
+* `call()` method mainly does:
+
+1. Gets session id
+2. Gets `requestTemplate`, the extra configuration provided by Shenyu
+3. Gets the previously stored Shenyu plugin chain and passes the tool call
info to the chain for continued execution
+4. Asynchronously waits for the tool response
+
+After the plugin chain completes, the tool call request is actually sent to
the service hosting the tool.
+
+```java
+public class ShenyuToolCallback implements ToolCallback {
+ @NonNull
+ @Override
+ public String call(@NonNull final String input, final ToolContext
toolContext) {
+ // Extract sessionId from MCP request
+ final McpSyncServerExchange mcpExchange =
extractMcpExchange(toolContext);
+ final String sessionId = extractSessionId(mcpExchange);
+ // Extract requestTemplate info
+ final String configStr = extractRequestConfig(shenyuTool);
+
+ // Get the previously stored plugin chain by sessionId
+ final ServerWebExchange originExchange = getOriginExchange(sessionId);
+ final ShenyuPluginChain chain = getPluginChain(originExchange);
+
+ // Execute the tool call
+ return executeToolCall(originExchange, chain, sessionId, configStr,
input);
+ }
+
+ private String executeToolCall(final ServerWebExchange originExchange,
+ final ShenyuPluginChain chain,
+ final String sessionId,
+ final String configStr,
+ final String input) {
+
+ final CompletableFuture<String> responseFuture = new
CompletableFuture<>();
+ final ServerWebExchange decoratedExchange = buildDecoratedExchange(
+ originExchange, responseFuture, sessionId, configStr, input);
+ // Execute plugin chain, call the actual tool
+ chain.execute(decoratedExchange)
+ .subscribe();
+
+ // Wait for response
+ final String result = responseFuture.get(DEFAULT_TIMEOUT_SECONDS,
TimeUnit.SECONDS);
+ return result;
+ }
+}
+```
+
+This concludes the Shenyu MCP Plugin tool invocation analysis.
+
+
+
+---
+
+### 4. Summary
+
+This article analyzed the source code from Mcp service registration, through
Mcp plugin service invocation, to tool invocation.
+The McpServer plugin makes Shenyu a powerful and centralized McpServer.
+
+---
diff --git a/i18n/zh/code.json b/i18n/zh/code.json
index 330146bed2c..bd988da5096 100755
--- a/i18n/zh/code.json
+++ b/i18n/zh/code.json
@@ -406,6 +406,9 @@
"Code Analysis For Divide Plugin": {
"message": "Divide插件源码分析"
},
+ "Code Analysis For McpServer Plugin": {
+ "message": "McpServer插件源码分析"
+ },
"E2e Test": {
"message": "E2e测试"
},
diff --git
a/i18n/zh/docusaurus-plugin-content-blog/Plugin-SourceCode-Analysis-Mcp-Server-Plugin.md
b/i18n/zh/docusaurus-plugin-content-blog/Plugin-SourceCode-Analysis-Mcp-Server-Plugin.md
new file mode 100644
index 00000000000..55f268999ff
--- /dev/null
+++
b/i18n/zh/docusaurus-plugin-content-blog/Plugin-SourceCode-Analysis-Mcp-Server-Plugin.md
@@ -0,0 +1,522 @@
+---
+title: McpServer 插件源码分析
+author: yusiheng
+author_title: Apache ShenYu Contributor
+author_url: https://github.com/478320
+tags: [plugin,mcp,Apache ShenYu]
+---
+
+在 shenyu 网关中,启动该插件,shenyu 将成为一个功能丰富的 mcpServer,
+你可以通过简单配置来将一个服务作为 tool 注册到 shenyu 网关中,并使用网关提供的扩展功能。
+
+> 本文基于`shenyu-2.7.0.2`版本进行源码分析, 在本篇中我将追踪 Shenyu Mcp 插件链路,对 Mcp 插件的 sse
通信方式进行源码分析
+
+### 前言
+
+> shenyu 网关的 mcp 插件基于 spring-ai-mcp 扩展而来,为了更好的了解 mcp 插件的工作原理
+> ,我将简单介绍 mcp 官方提供的 jdk 中各个 java 类是如何协同运作的
+
+我想先简单介绍三个 Mcp 官方提供的 java 类
+
+>1. `McpServer`
+>
+>该类负责管理,tool,Resource,promote 等资源
+>
+>2. `TransportProvider`
+>
+>根据客户端和服务端之间通信协议,提供之相对应的通讯方法
+>
+>3. `Session`
+>
+>处理请求数据、响应数据和通知数据,提供一些基本方法和其对应的处理器,查询工具,调用工具都在此处执行
+
+### 1. 服务注册
+
+在 shenyu admin 的 McpServer 中插件填写 endpoint 和 tool 信息后,这些信息将自动注册到 shenyu
bootstrap 中,
+数据同步源码可以参考官网[websocket数据同步](https://shenyu.incubator.apache.org/zh/blog/DataSync-SourceCode-Analysis-WebSocket-Data-Sync)
+
+shenyu bootstrap 将在 `McpServerPluginDataHandler` 的 `handler()` 方法中接收到 admin
同步来的数据。
+
+`handlerSelector()` 方法接收 url 数据创建 McpServer
+
+`handlerRule()` 方法接收 tool 信息,注册 tool
+
+这两个方法共同组成了 Shenyu Mcp 插件的服务注册部分,下面我将对这个两个方法,详细展开分析
+
+#### 1.1 Transport,McpServer注册
+
+我们先来分析 `handlerSelector()` 方法,也就是 McpServer 的注册
+
+* `handlerSelector()` 方法 工作内容如下
+
+1. 捕捉用户在 Selector 上的填写的 url,这个 url 将作为一个 key 存储 McpServer TransportProvider 等信息
+2. 序列化创建 `ShenyuMcpServer`,`ShenyuMcpServer` 将 SelectorId 和这些 url 也就是这些 key
绑定,以此来实现 selectorId 和 key 的绑定。
+
+> 注意 `ShenyuMcpServer` 是 Shenyu 用于绑定 SelectorId 和 url 的对象,和 `McpServer`
没有继承关系,功能也完全不同
+
+3. 调用 `ShenyuMcpServerManager` 的 `getOrCreateMcpServerTransport()` 方法注册
McpServer TransportProvider
+
+```java
+public class McpServerPluginDataHandler implements PluginDataHandler {
+ @Override
+ public void handlerSelector(final SelectorData selectorData) {
+ // 获取URI
+ String uri = selectorData.getConditionList().stream()
+ .filter(condition ->
Constants.URI.equals(condition.getParamType()))
+ .map(ConditionData::getParamValue)
+ .findFirst()
+ .orElse(null);
+
+ // 构建McpServer
+ ShenyuMcpServer shenyuMcpServer =
GsonUtils.getInstance().fromJson(Objects.isNull(selectorData.getHandle()) ?
DEFAULT_MESSAGE_ENDPOINT : selectorData.getHandle(), ShenyuMcpServer.class);
+ shenyuMcpServer.setPath(path);
+ // 缓存shenyuMcpServer
+ CACHED_SERVER.get().cachedHandle(
+ selectorData.getId(),
+ shenyuMcpServer);
+ String messageEndpoint = shenyuMcpServer.getMessageEndpoint();
+ // 尝试获取或者注册transportProvider
+ shenyuMcpServerManager.getOrCreateMcpServerTransport(uri,
messageEndpoint);
+ }
+
+}
+```
+
+> `ShenyuMcpServerManager` 该类是 ShenYu 中 McpServer 的管理中心,不仅储存了 `McpAsyncServer`
`CompositeTransportProvider` 等内容,注册 Transport 和 McpServer
+的方法也在其中
+* `getOrCreateMcpServerTransport()` 方法工作内容具体如下
+
+1. 处理传递来的 url 去除/streamablehttp 以及 /message后缀 使其恢复为原始的 url
+2. 尝试获取 `CompositeTransportProvider` 对象,该对象是 Transport 的复合对象,包含了多种协议对应的
Transport
+3. 如果没有获取到,则调用 `createSseTransport()` 方法创建 `CompositeTransportProvider` 对象
+4. 创建 `McpAsyncServer` 对象,保存 Transport 对象到 Map 中,将 Transport 注册到
`McpAsyncServer` 中
+
+```java
+@Component
+public class ShenyuMcpServerManager {
+ public ShenyuSseServerTransportProvider
getOrCreateMcpServerTransport(final String uri, final String messageEndPoint) {
+ // 去除/streamablehttp 以及 /message后缀
+ String normalizedPath = processPath(uri);
+ return getOrCreateTransport(normalizedPath, SSE_PROTOCOL,
+ () -> createSseTransport(normalizedPath, messageEndPoint));
+ }
+
+ private <T> T getOrCreateTransport(final String normalizedPath, final
String protocol,
+ final java.util.function.Supplier<T>
transportFactory) {
+ // 获取复合Transport实例
+ CompositeTransportProvider compositeTransport =
getOrCreateCompositeTransport(normalizedPath);
+
+ T transport = switch (protocol) {
+ case SSE_PROTOCOL -> (T) compositeTransport.getSseTransport();
+ case STREAMABLE_HTTP_PROTOCOL -> (T)
compositeTransport.getStreamableHttpTransport();
+ default -> null;
+ };
+ // 如果缓存中没有该实例,则需要重新创建
+ if (Objects.isNull(transport)) {
+ // 调用createSseTransport()方法,创建一个新的transport并存储
+ transport = transportFactory.get();
+ // 创建McpAsyncServer,并注册transport
+ addTransportToSharedServer(normalizedPath, protocol, transport);
+ }
+
+ return transport;
+ }
+}
+```
+
+##### 1.1.1 Transport注册
+
+* `createSseTransport()` 方法
+> 该方法在 `getOrCreateMcpServerTransport()` 方法被调用,用于创建 Transport
+
+```java
+
+@Component
+public class ShenyuMcpServerManager {
+ private ShenyuSseServerTransportProvider createSseTransport(final String
normalizedPath, final String messageEndPoint) {
+ String messageEndpoint = normalizedPath + messageEndPoint;
+ ShenyuSseServerTransportProvider transportProvider =
ShenyuSseServerTransportProvider.builder()
+ .objectMapper(objectMapper)
+ .sseEndpoint(normalizedPath)
+ .messageEndpoint(messageEndpoint)
+ .build();
+ // 向Manager的routeMap中注册transportProvider的两个函数
+ registerRoutes(normalizedPath, messageEndpoint,
transportProvider::handleSseConnection, transportProvider::handleMessage);
+ return transportProvider;
+ }
+}
+```
+
+##### 1.1.2 mcpServer注册
+
+* `addTransportToSharedServer()` 方法
+> 该方法在 `getOrCreateMcpServerTransport()` 方法被调用,用于创建 McpServer 并保存
+
+该方法创建了一个新的 McpServer并存储到 `sharedServerMap` 中,并将上一步得到的 TransportProvider 存入
`compositeTransportMap` 中
+
+```java
+@Component
+public class ShenyuMcpServerManager {
+ private void addTransportToSharedServer(final String normalizedPath, final
String protocol, final Object transportProvider) {
+ // 获取或者创建并注册 McpServer
+ getOrCreateSharedServer(normalizedPath);
+
+ // 将新增的传输协议存进compositeTransportMap中
+ compositeTransport.addTransport(protocol, transportProvider);
+
+ }
+
+ private McpAsyncServer getOrCreateSharedServer(final String
normalizedPath) {
+ return sharedServerMap.computeIfAbsent(normalizedPath, path -> {
+ // 获取传输协议
+ CompositeTransportProvider compositeTransport =
getOrCreateCompositeTransport(path);
+
+ // 选择Server拥有的能力
+ var capabilities = McpSchema.ServerCapabilities.builder()
+ .tools(true)
+ .logging()
+ .build();
+
+ // 创建McpServer并存储
+ McpAsyncServer server = McpServer
+ .async(compositeTransport)
+ .serverInfo("MCP Shenyu Server (Multi-Protocol)", "1.0.0")
+ .capabilities(capabilities)
+ .tools(Lists.newArrayList())
+ .build();
+
+ return server;
+ });
+ }
+}
+```
+
+#### 1.2 Tools注册
+
+* `handlerRule()` 方法 工作内容如下
+
+1. 捕捉用户在 Tool 上的填写的 tool 配置信息,这些信息将全部用于 tool 的构建
+2. 序列化创建 `ShenyuMcpServerTool` 获取 tool 信息
+
+> 注意 `ShenyuMcpServerTool` 也是 Shenyu 存储 tool 信息的工具,和 `McpServerTool` 没有继承关系
+
+3. 调用 `addTool()` 方法, 利用该 tool 信息创建 tool,并根据 SelectorId 将 tool 注册到与之匹配的
McpServer 中
+
+```java
+public class McpServerPluginDataHandler implements PluginDataHandler {
+ @Override
+ public void handlerRule(final RuleData ruleData) {
+ Optional.ofNullable(ruleData.getHandle()).ifPresent(s -> {
+ // 序列化一个新的 ShenyuMcpServerTool
+ ShenyuMcpServerTool mcpServerTool =
GsonUtils.getInstance().fromJson(s, ShenyuMcpServerTool.class);
+ // 缓存mcpServerTool
+
CACHED_TOOL.get().cachedHandle(CacheKeyUtils.INST.getKey(ruleData),
mcpServerTool);
+ // 获取并构建 mcp schema
+ List<McpServerToolParameter> parameters =
mcpServerTool.getParameters();
+ String inputSchema =
JsonSchemaUtil.createParameterSchema(parameters);
+ ShenyuMcpServer server =
CACHED_SERVER.get().obtainHandle(ruleData.getSelectorId());
+ if (Objects.nonNull(server)) {
+ // 向Manager的sharedServerMap中存储Tool信息
+ shenyuMcpServerManager.addTool(server.getPath(),
+ StringUtils.isBlank(mcpServerTool.getName()) ?
ruleData.getName()
+ : mcpServerTool.getName(),
+ mcpServerTool.getDescription(),
+ mcpServerTool.getRequestConfig(),
+ inputSchema);
+ }
+ });
+ }
+}
+```
+
+* `addTool()`方法
+> 该方法被 `handlerRule()` 方法调用,用于新增工具
+
+该方法做了下述工作
+1. 将上一步传来的 tool 信息转换为 `shenyuToolDefinition` 对象
+2. 利用转换来的 `shenyuToolDefinition` 对象创建 `ShenyuToolCallback` 对象
+> `ShenyuToolCallback` 重写了 `ToolCallBack` 的 `call()` 方法,并将该 `call()` 方法注册到了
`AsyncToolSpecification` 中,
+> 此后调用 tool 的 `call()` 方法,则实际会调用这个重写的 `call()` 方法
+
+3. 将 `ShenyuToolCallback` 转换为 `AsyncToolSpecification` 并注册到相关的 mcpServer 中
+
+```java
+public class McpServerPluginDataHandler implements PluginDataHandler {
+ public void addTool(final String serverPath, final String name, final
String description,
+ final String requestTemplate, final String
inputSchema) {
+ String normalizedPath = normalizeServerPath(serverPath);
+ // 构建Definition对象
+ ToolDefinition shenyuToolDefinition = ShenyuToolDefinition.builder()
+ .name(name)
+ .description(description)
+ .requestConfig(requestTemplate)
+ .inputSchema(inputSchema)
+ .build();
+
+ ShenyuToolCallback shenyuToolCallback = new
ShenyuToolCallback(shenyuToolDefinition);
+
+ // 获取到先前注册的 McpServer, 并向其中注册Tool
+ McpAsyncServer sharedServer = sharedServerMap.get(normalizedPath);
+ for (AsyncToolSpecification asyncToolSpecification :
McpToolUtils.toAsyncToolSpecifications(shenyuToolCallback)) {
+ sharedServer.addTool(asyncToolSpecification).block();
+ }
+ }
+}
+```
+
+到此为止,服务注册分析完毕
+
+服务注册一图览
+
+
+### 2. 插件调用
+
+客户端先后会发送后缀为 `/sse` 和 `/message` 的两种消息,这两种消息都会被 `Shenyu McpServer plugin`
捕捉,`Shenyu McpServer plugin`
+会对 `/sse` 消息和 `/message` 消息做不同处理。收到 `/sse` 消息时 plugin 会创建 session 对象并保存,最后返回
session id
+供 message 消息使用。收到 `/message` 消息时,会根据 `/message` 消息携带的 method 信息,选择执行的方法
+如:获取工作列表,工具调用,获取资源列表等等
+
+* `doExecute()` 方法 工作内容如下
+
+1. 路径匹配,判断 mcp plugin 是否注册该路径
+2. 调用 `routeByProtocol()` 方法,根据请求协议选择合适的处理方案
+
+> 本篇是对 sse 请求方式的解析,因此接着进入 `handleSseRequest()` 方法
+
+```java
+public class McpServerPlugin extends AbstractShenyuPlugin {
+ @Override
+ protected Mono<Void> doExecute(final ServerWebExchange exchange,
+ final ShenyuPluginChain chain,
+ final SelectorData selector,
+ final RuleData rule) {
+ final String uri = exchange.getRequest().getURI().getRawPath();
+ // 判断 Mcp 插件是否注册了该路由规则,没有则不执行
+ if (!shenyuMcpServerManager.canRoute(uri)) {
+ return chain.execute(exchange);
+ }
+ final ServerRequest request = ServerRequest.create(exchange,
messageReaders);
+ // 根据 uri 判断路由协议,选择对应的处理方案
+ return routeByProtocol(exchange, chain, request, selector, uri);
+ }
+
+ private Mono<Void> routeByProtocol(final ServerWebExchange exchange,
+ final ShenyuPluginChain chain,
+ final ServerRequest request,
+ final SelectorData selector,
+ final String uri) {
+
+ if (isStreamableHttpProtocol(uri)) {
+ return handleStreamableHttpRequest(exchange, chain, request, uri);
+ } else if (isSseProtocol(uri)) {
+ // 处理sse请求
+ return handleSseRequest(exchange, chain, request, selector, uri);
+ }
+ }
+}
+```
+
+`handlerSseRequest()` 方法
+> 该方法由 `routeByProtocol()` 方法调用,根据请求后缀判断客户端是要创建 session 还是调用工具
+
+```java
+public class McpServerPlugin extends AbstractShenyuPlugin {
+ private Mono<Void> handleSseRequest(final ServerWebExchange exchange,
+ final ShenyuPluginChain chain,
+ final ServerRequest request,
+ final SelectorData selector,
+ final String uri) {
+ ShenyuMcpServer server =
McpServerPluginDataHandler.CACHED_SERVER.get().obtainHandle(selector.getId());
+ String messageEndpoint = server.getMessageEndpoint();
+ // 获取传输者
+ ShenyuSseServerTransportProvider transportProvider
+ = shenyuMcpServerManager.getOrCreateMcpServerTransport(uri,
messageEndpoint);
+ // 根据请求的后缀判断是 sse 连接请求还是 message 调用请求
+ if (uri.endsWith(messageEndpoint)) {
+ setupSessionContext(exchange, chain);
+ return handleMessageEndpoint(exchange, transportProvider, request);
+ } else {
+ return handleSseEndpoint(exchange, transportProvider, request);
+ }
+ }
+}
+```
+
+#### 2.1 客户端发送 sse 请求
+
+> 如果我们客户端发送的是后缀为 `/sse` 的请求,那么将会执行 `handleSseEndpoint()` 方法
+
+* `handleSseEndpoint()` 方法主要做了如下工作
+
+1. 配置 sse 请求头
+2. 调用 `ShenyuSseServerTransportProvider` 的 `createSseFlux()` 创建 sse 流
+
+```java
+public class McpServerPlugin extends AbstractShenyuPlugin {
+ private Mono<Void> handleSseEndpoint(final ServerWebExchange exchange,
+ final
ShenyuSseServerTransportProvider transportProvider,
+ final ServerRequest request) {
+ // 配置 sse 请求头
+ configureSseHeaders(exchange);
+
+ // 创建 sse 流
+ return exchange.getResponse()
+ .writeWith(transportProvider
+ .createSseFlux(request));
+ }
+}
+```
+
+* `createSseFlux()` 方法
+> 该方法被 `handleSseEndpoint()`调用 主要用于创建并保存 session
+1. 创建 session ,创建 session 的工厂在创建 session 时会将一系列 handler 注册到 session 中,这些
handler 是真正执行
+ callTool 的对象
+2. 将 session 保存,session复用
+3. 将 session id 作为 endpoint 的请求参数返回给客户端,在调用 message 方法时会使用该 endpoint
+
+```java
+public class ShenyuSseServerTransportProvider implements
McpServerTransportProvider {
+ public Flux<ServerSentEvent<?>> createSseFlux(final ServerRequest request)
{
+ return Flux.<ServerSentEvent<?>>create(sink -> {
+ WebFluxMcpSessionTransport sessionTransport = new
WebFluxMcpSessionTransport(sink);
+ // 创建 McpServerSession 并暂存插件链信息
+ McpServerSession session =
sessionFactory.create(sessionTransport);
+ String sessionId = session.getId();
+ sessions.put(sessionId, session);
+
+ // 将 session id等信息传递回客户端
+ String endpointUrl = this.baseUrl + this.messageEndpoint +
"?sessionId=" + sessionId;
+ ServerSentEvent<String> endpointEvent =
ServerSentEvent.<String>builder()
+ .event(ENDPOINT_EVENT_TYPE)
+ .data(endpointUrl)
+ .build();
+ }).doOnSubscribe(subscription -> LOGGER.info("SSE Flux
subscribed"))
+ .doOnRequest(n -> LOGGER.debug("SSE Flux requested {} items",
n));
+ }
+}
+```
+
+#### 2.2 客户端发送 message 请求
+
+> 如果我们客户端发送的是后缀为 `/message` 的请求,那么将会执行 把当前 `ShenyuPluginChain` 信息存入 session
中,并调用 `handleMessageEndpoint()` 方法,
+> 后续工具调用时会继续执行该插件链,因此 mcp plugin 后的插件会对进入 tool 的请求造成影响
+
+* `handleMessageEndpoint()` 方法,调用 `ShenyuSseServerTransportProvider` 的
`handleMessageEndpoint()` 方法
+
+```
+if (uri.endsWith(messageEndpoint)) {
+ setupSessionContext(exchange, chain);
+ return handleMessageEndpoint(exchange, transportProvider, request);
+}
+```
+
+```java
+public class McpServerPlugin extends AbstractShenyuPlugin {
+ private Mono<Void> handleMessageEndpoint(final ServerWebExchange exchange,
+ final
ShenyuSseServerTransportProvider transportProvider,
+ final ServerRequest request) {
+ // 处理message请求
+ return transportProvider.handleMessageEndpoint(request)
+ .flatMap(result -> {
+ return exchange.getResponse()
+
.writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(responseBody.getBytes())));
+ });
+ }
+}
+```
+
+* `handleMessageEndpoint()` 方法
+> 该方法由 `McpServerPlugin.handleMessageEndpoint()` 调用,将请求交给 session 处理
+
+session 的 `handler()` 方法会对 message 的不同,而进行对应的操作
+例如 : 当 message 中 method 是 "tools/call" 时,则会使用工具调用的 handler() 执行 `call()` 方法调用工具
+相关源码在此不过多赘述
+
+```java
+public class ShenyuSseServerTransportProvider implements
McpServerTransportProvider {
+ public Mono<MessageHandlingResult> handleMessageEndpoint(final
ServerRequest request) {
+ // 获取到session
+ String sessionId = request.queryParam("sessionId").get();
+ McpServerSession session = sessions.get(sessionId);
+ return request.bodyToMono(String.class)
+ .flatMap(body -> {
+ McpSchema.JSONRPCMessage message =
McpSchema.deserializeJsonRpcMessage(objectMapper, body);
+ return session.handle(message);
+ });
+ }
+}
+```
+
+至此 `Shenyu Mcp Plugin` 服务调用源码分析完毕
+
+流程图一览
+
+
+### 3. 工具调用
+
+> 如果客户端传递的消息是调用工具的消息,那么 session 将使用工具调用的 handler() 并执行 tool 的 `call()` 方法,
+> 在服务注册中,我们说明了 tool 在被调用时,实际执行的是 `ShenyuToolCallback()` 的 `call()` 方法
+
+因此执行工具调用时会执行以下方法
+* `call()` 主要工作内容如下
+
+1. 获取 session id
+2. 获取 `requestTemplate` 即 shenyu 提供的额外功能的配置信息
+3. 获取上一步暂存的 shenyu 插件链,并将工具调用的信息交给插件链继续执行
+4. 异步等待工具响应
+
+插件链执行完成后,会将调用 tool 请求真正的发送到 tool 所在的服务之中
+
+```java
+public class ShenyuToolCallback implements ToolCallback {
+ @NonNull
+ @Override
+ public String call(@NonNull final String input, final ToolContext
toolContext) {
+ // 从 mcp 请求中提取 sessionId
+ final McpSyncServerExchange mcpExchange =
extractMcpExchange(toolContext);
+ final String sessionId = extractSessionId(mcpExchange);
+ // 提取requestTemplate信息
+ final String configStr = extractRequestConfig(shenyuTool);
+
+ // 利用sessionId 获取到先前暂存的插件执行链
+ final ServerWebExchange originExchange = getOriginExchange(sessionId);
+ final ShenyuPluginChain chain = getPluginChain(originExchange);
+
+ // 执行工具调用
+ return executeToolCall(originExchange, chain, sessionId, configStr,
input);
+
+ }
+
+ private String executeToolCall(final ServerWebExchange originExchange,
+ final ShenyuPluginChain chain,
+ final String sessionId,
+ final String configStr,
+ final String input) {
+
+ final CompletableFuture<String> responseFuture = new
CompletableFuture<>();
+ final ServerWebExchange decoratedExchange = buildDecoratedExchange(
+ originExchange, responseFuture, sessionId, configStr, input);
+ // 执行插件链,调用实际工具
+ chain.execute(decoratedExchange)
+ .subscribe();
+
+ // 等待响应
+ final String result = responseFuture.get(DEFAULT_TIMEOUT_SECONDS,
TimeUnit.SECONDS);
+ return result;
+
+ }
+}
+```
+
+至此Shenyu MCP Plugin 工具调用分析完毕
+
+
+
+---
+
+### 4. 小结
+
+本文源码分析从 mcp 服务注册开始,到 mcp 插件的服务调用,再到 tool 的调用。
+mcpServer 插件让 shenyu 成为一个功能强大,集中管理的 mcpServer。
+
+---
diff --git a/src/data/blogInfo.js b/src/data/blogInfo.js
index e586cc36970..508bb828a66 100644
--- a/src/data/blogInfo.js
+++ b/src/data/blogInfo.js
@@ -231,6 +231,16 @@ export default [
date: '2024-02-04',
abs:<Translate>Code Analysis Ext Plugin Loader</Translate>
},
+ {
+ title: <Translate>Code Analysis For McpServer
Plugin</Translate>,
+ author: "yusiheng (contributor)",
+ autImg: "/img/blog/yusiheng.png",
+ autPage: "https://github.com/478320",
+ src: "Plugin-SourceCode-Analysis-Mcp-Server-Plugin",
+ cover: "/img/logo.svg",
+ date: '2025-09-22',
+ abs:<Translate>Code Analysis For McpServer Plugin</Translate>
+ },
]
},
{
diff --git
a/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-execute-en.png
b/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-execute-en.png
new file mode 100644
index 00000000000..d4b71962c7b
Binary files /dev/null and
b/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-execute-en.png
differ
diff --git
a/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-execute-zh.png
b/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-execute-zh.png
new file mode 100644
index 00000000000..f2b31c5b858
Binary files /dev/null and
b/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-execute-zh.png
differ
diff --git
a/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-register-en.png
b/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-register-en.png
new file mode 100644
index 00000000000..d8c97f9248a
Binary files /dev/null and
b/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-register-en.png
differ
diff --git
a/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-register-zh.png
b/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-register-zh.png
new file mode 100644
index 00000000000..15006ce4bb4
Binary files /dev/null and
b/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-register-zh.png
differ
diff --git
a/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-tool-call-en.png
b/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-tool-call-en.png
new file mode 100644
index 00000000000..7a6f372bdeb
Binary files /dev/null and
b/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-tool-call-en.png
differ
diff --git
a/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-tool-call-zh.png
b/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-tool-call-zh.png
new file mode 100644
index 00000000000..502703d02da
Binary files /dev/null and
b/static/img/activities/code-analysis-mcp-server-plugin/Mcp-server-tool-call-zh.png
differ
diff --git a/static/img/blog/yusiheng.png b/static/img/blog/yusiheng.png
new file mode 100644
index 00000000000..4f8057c2f3c
Binary files /dev/null and b/static/img/blog/yusiheng.png differ