feat: Support the MCP client and dynamically fetch the model list#298
feat: Support the MCP client and dynamically fetch the model list#298lhpqaq wants to merge 7 commits intoapache:mainfrom
Conversation
- Deleted ClusterFunctions, HostFunctions, StackFunctions, AIServiceToolsProvider, and InfoToolsProvider classes as they are no longer needed. - Removed associated test classes: ClusterFunctionsTest, HostFunctionsTest, StackFunctionsTest, and AIServiceToolsProviderTest. - Updated application.yml to include new client configuration. - Added new API methods in llm-config for fetching platform models. - Enhanced UI components to support model fetching with loading states and error handling. - Updated localization files for new UI strings related to model fetching. - Added repository for Spring milestones in pom.xml.
There was a problem hiding this comment.
Pull request overview
This PR expands the AI assistant feature set by adding MCP client support (local/SSE/streamable HTTP), enabling dynamic model discovery from providers, and surfacing tool invocation progress in the UI during streaming responses.
Changes:
- Add MCP client configuration/initialization and wire MCP tool callbacks into Spring AI assistants with tool-execution event streaming.
- Add a backend API to fetch models dynamically and update UI to refresh model options on demand.
- Update AI assistant UI/store to display tool execution steps alongside the streaming assistant message.
Reviewed changes
Copilot reviewed 42 out of 42 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| pom.xml | Adds Spring milestone repository configuration. |
| bigtop-manager-ui/src/store/llm-config/index.ts | Adds store action to refresh platform models. |
| bigtop-manager-ui/src/store/ai-assistant/index.ts | Tracks tool executions + pending streaming AI record. |
| bigtop-manager-ui/src/pages/system-manage/llm-config/components/add-llm-item.vue | Adds “Fetch Models” button to model selector. |
| bigtop-manager-ui/src/locales/zh_CN/llm-config.ts | Adds new i18n strings for model fetching. |
| bigtop-manager-ui/src/locales/en_US/llm-config.ts | Adds new i18n strings for model fetching. |
| bigtop-manager-ui/src/features/ai-assistant/index.vue | Renders streaming tool/AI records during SSE. |
| bigtop-manager-ui/src/features/ai-assistant/chat-message.vue | Adds UI rendering for tool execution records. |
| bigtop-manager-ui/src/api/llm-config/types.ts | Adds request type for platform model fetch. |
| bigtop-manager-ui/src/api/llm-config/index.ts | Adds API call for platform model list endpoint. |
| bigtop-manager-ui/src/api/ai-assistant/types.ts | Extends message/event typing for tool execution fields. |
| bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/provider/AIServiceToolsProviderTest.java | Removes tests for removed langchain4j tool provider. |
| bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/StackFunctionsTest.java | Removes tests for removed langchain4j stack functions. |
| bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/HostFunctionsTest.java | Removes tests for removed langchain4j host functions. |
| bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/ClusterFunctionsTest.java | Removes tests for removed langchain4j cluster functions. |
| bigtop-manager-server/src/main/resources/application.yml | Adds MCP client configuration + defaults. |
| bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/provider/InfoToolsProvider.java | Removes langchain4j ToolProvider implementation. |
| bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/provider/AIServiceToolsProvider.java | Removes tool provider selector used previously by chatbot. |
| bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/StackFunctions.java | Removes langchain4j tool functions. |
| bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/HostFunctions.java | Removes langchain4j tool functions. |
| bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/ClusterFunctions.java | Removes langchain4j tool functions. |
| bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LLMConfigServiceImpl.java | Adds dynamic model fetch + uses it in model validation. |
| bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/ChatbotServiceImpl.java | Streams tool execution events via SSE and improves emitter lifecycle handling. |
| bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/LLMConfigService.java | Adds platformModels service API. |
| bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/vo/TalkVO.java | Extends SSE payload with tool execution fields. |
| bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/converter/AuthPlatformConverter.java | Filters null credential keys before toMap conversion. |
| bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/mcp/tool/StackMcpTool.java | Adds logging when MCP tool is invoked. |
| bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/controller/LLMConfigController.java | Adds /platforms/{platformId}/models endpoint. |
| bigtop-manager-server/pom.xml | Removes direct langchain4j dependency from server module. |
| bigtop-manager-bom/pom.xml | Bumps Spring AI version; adds MCP client starter to BOM. |
| bigtop-manager-ai/src/test/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManagerTest.java | Adds tests for MCP connection parsing behavior. |
| bigtop-manager-ai/src/test/java/assistant/GeneralAssistantFactoryTest.java | Updates tests for revised factory method signatures. |
| bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java | Adds dynamic model fetch + MCP tool callback observation. |
| bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java | Adds MCP tool callback observation + base URL/model streaming tweaks. |
| bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java | Adds MCP tool callback observation + streaming safety tweaks. |
| bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java | Adds MCP tool callback observation + streaming safety tweaks. |
| bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistantFactory.java | Adjusts factory API; adds model discovery and tool-event hook. |
| bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistant.java | Adds MCP client + tool-event listener support and event record type. |
| bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java | Implements model discovery helper and tool-execution event emission wiring. |
| bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java | Adds MCP async client manager with connection parsing and initialization. |
| bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java | Wires MCP clients + tool execution listener; adds model discovery method. |
| bigtop-manager-ai/pom.xml | Adds MCP client starter dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <repositories> | ||
| <repository> | ||
| <snapshots> | ||
| <enabled>false</enabled> | ||
| </snapshots> | ||
| <id>spring-milestones</id> | ||
| <name>Spring Milestones</name> | ||
| <url>https://repo.spring.io/milestone</url> | ||
| </repository> | ||
| </repositories> |
There was a problem hiding this comment.
The added Spring milestone repository appears unnecessary for Spring AI 1.1.3 / spring-ai-starter-mcp-client-webflux (both are published to Maven Central). Keeping an extra repository can make builds less reproducible and increases supply-chain surface; consider removing it (or gating it behind a profile only when needed).
| @Operation(summary = "list platform models", description = "List models from /v1/models") | ||
| @PostMapping("/platforms/{platformId}/models") | ||
| public ResponseEntity<List<String>> platformModels( | ||
| @PathVariable(name = "platformId") Long platformId, @RequestBody AuthPlatformReq authPlatformReq) { | ||
| AuthPlatformDTO authPlatformDTO = AuthPlatformConverter.INSTANCE.fromReq2DTO(authPlatformReq); | ||
| Map<String, String> authCredentials = authPlatformDTO.getAuthCredentials(); | ||
| return ResponseEntity.success(llmConfigService.platformModels(platformId, authCredentials)); | ||
| } |
There was a problem hiding this comment.
The new /platforms/{platformId}/models endpoint accepts AuthPlatformReq, but platformId is already provided in the path and the request type contains many unrelated fields. Consider introducing a dedicated request DTO (e.g., { authCredentials: [...] }) to keep the API contract clear and avoid implying that other fields are required/used.
| AIAssistant aiAssistant = prepareTalk(threadId, command, emitter, emitterClosed); | ||
|
|
||
| Flux<String> stringFlux = | ||
| (command == null) ? aiAssistant.streamAsk(message) : Flux.just(aiAssistant.ask(message)); |
There was a problem hiding this comment.
Flux.just(aiAssistant.ask(message)) evaluates ask() eagerly (before subscription). With tool-execution events now emitted during ask(), this can send SSE events before the emitter lifecycle handlers are registered / before the client is fully connected. Use a deferred publisher (e.g., Flux.defer/Mono.fromCallable) for the non-streaming path so execution starts on subscribe and can be cancelled/cleaned up consistently.
| (command == null) ? aiAssistant.streamAsk(message) : Flux.just(aiAssistant.ask(message)); | |
| (command == null) | |
| ? aiAssistant.streamAsk(message) | |
| : Flux.defer(() -> Flux.just(aiAssistant.ask(message))); |
| OpenAiApi openAiApi = OpenAiApi.builder() | ||
| .baseUrl(resolveDefaultBaseUrl()) | ||
| .apiKey(apiKey) | ||
| .build(); | ||
| OpenAiChatOptions.Builder optionsBuilder = | ||
| OpenAiChatOptions.builder().model(model); | ||
| List<io.modelcontextprotocol.client.McpAsyncClient> mcpClients = getMcpAsyncClients(); | ||
| if (!mcpClients.isEmpty()) { | ||
| optionsBuilder.toolCallbacks(buildObservedToolCallbacks(mcpClients)); | ||
| } | ||
| OpenAiChatOptions options = optionsBuilder.build(); |
There was a problem hiding this comment.
OpenAIAssistant supports a credential-provided baseUrl for model discovery, but chat requests still always use resolveDefaultBaseUrl() (ignoring that credential). This can make model listing succeed against a custom endpoint while chat requests go to a different host. Consider using the same base-url resolution for both chat and model listing to keep behavior consistent.
| if (supportModels == null || supportModels.isBlank()) { | ||
| return Collections.emptyList(); | ||
| } | ||
| return List.of(supportModels.split(",")); |
There was a problem hiding this comment.
getDefaultModels returns List.of(supportModels.split(",")) without trimming/filtering blanks. If supportModels contains spaces or trailing commas, callers may see invalid model IDs (e.g., " gpt-4" or ""). Consider trimming each entry and filtering empty strings before returning.
| return List.of(supportModels.split(",")); | |
| String[] rawModels = supportModels.split(","); | |
| List<String> models = new ArrayList<>(); | |
| for (String rawModel : rawModels) { | |
| if (rawModel == null) { | |
| continue; | |
| } | |
| String trimmed = rawModel.trim(); | |
| if (!trimmed.isEmpty()) { | |
| models.add(trimmed); | |
| } | |
| } | |
| if (models.isEmpty()) { | |
| return Collections.emptyList(); | |
| } | |
| return models; |
| const prevRecord = toolExecutions.value.find((execution) => execution.executionId === executionId) | ||
| const record = { | ||
| sender: 'AI' as const, | ||
| message: '', | ||
| messageType: 'tool' as const, | ||
| executionId, | ||
| toolName, | ||
| toolStatus, | ||
| toolPayload, | ||
| toolInput: toolStatus === 'started' ? toolPayload || '' : prevRecord?.toolInput || '', | ||
| toolOutput: toolStatus === 'completed' ? toolPayload || '' : prevRecord?.toolOutput || '', | ||
| toolError: toolStatus === 'failed' ? toolPayload || '' : prevRecord?.toolError || '', | ||
| createTime: dayjs().format() | ||
| } |
There was a problem hiding this comment.
updateToolExecution overwrites createTime on every tool status update. This can reorder tool records / show misleading timestamps (e.g., completed time instead of started time). Preserve the original createTime from prevRecord when present and only set it when creating a new execution record.
| <div class="tool-section"> | ||
| <span class="tool-section-title">Input</span> | ||
| <markdown-view :mark-raw="toolInput" /> | ||
| </div> | ||
| <div v-if="props.record.toolStatus === 'completed'" class="tool-section"> | ||
| <span class="tool-section-title">Output</span> | ||
| <markdown-view :mark-raw="toolOutput" /> | ||
| </div> | ||
| <div v-if="props.record.toolStatus === 'failed'" class="tool-section"> | ||
| <span class="tool-section-title">Error</span> | ||
| <markdown-view :mark-raw="toolError" /> |
There was a problem hiding this comment.
This component introduces new user-visible labels ("Input", "Output", "Error") and displays raw status strings (started/completed/failed) without i18n. Other ai-assistant components use useI18n(), so these should be translated and status values mapped to localized labels.
| .filter(item -> item != null && item.getKey() != null) | ||
| .collect(Collectors.toMap(AuthCredentialReq::getKey, AuthCredentialReq::getValue)); |
There was a problem hiding this comment.
Collectors.toMap(AuthCredentialReq::getKey, AuthCredentialReq::getValue) will throw a NullPointerException if any credential has a null value (e.g., frontend sends {key: "apiKey"} when value is undefined). Filter out null values (and ideally handle duplicate keys via a merge function) to avoid 500s on the new models endpoint and existing auth-platform endpoints.
| .filter(item -> item != null && item.getKey() != null) | |
| .collect(Collectors.toMap(AuthCredentialReq::getKey, AuthCredentialReq::getValue)); | |
| .filter(item -> item != null && item.getKey() != null && item.getValue() != null) | |
| .collect(Collectors.toMap(AuthCredentialReq::getKey, AuthCredentialReq::getValue, (existing, replacement) -> replacement)); |
| client: | ||
| enabled: true | ||
| request-timeout-seconds: 120 | ||
| init-timeout-seconds: 20 | ||
| # Optional multi-connection definition. | ||
| # Format: | ||
| # name=<id>,type=<sse|streamable-http|local>,baseUrl=<url>,endpoint=<path>,command=<cmd>,args=<a|b>,env=<K:V|K2:V2>,requestTimeoutSeconds=<n>,initTimeoutSeconds=<n>; | ||
| # Notes: | ||
| # - sse / streamable-http use baseUrl (+ optional endpoint) | ||
| # - local uses command/args/env | ||
| # - requestTimeoutSeconds/initTimeoutSeconds are optional per-connection overrides | ||
| # Example: | ||
| # connections: > | ||
| # name=embedded,type=sse,baseUrl=http://localhost:8080,endpoint=/mcp/sse; | ||
| # name=remote,type=streamable-http,baseUrl=http://127.0.0.1:9000,endpoint=/mcp,requestTimeoutSeconds=180; | ||
| # name=memory,type=local,command=npx,args=-y|@modelcontextprotocol/server-memory,env=NODE_ENV:production | ||
| connections: > | ||
| name=bm,type=sse,baseUrl=http://localhost:8080,endpoint=/mcp/sse |
There was a problem hiding this comment.
MCP client is enabled by default and ships with a default connection to http://localhost:8080. This can lead to unexpected outbound connections/timeouts in real deployments and makes the feature non-optional by default. Consider defaulting spring.ai.mcp.client.enabled to false and leaving connections empty (keep examples commented) so operators opt in explicitly.
| stackVOList.add(stackVO); | ||
| } | ||
|
|
||
| log.info("ListStacks tool called, total stacks: {}", stackVOList.size()); |
There was a problem hiding this comment.
Logging every tool invocation at INFO (ListStacks tool called...) may be noisy if tools are invoked frequently during chats. Consider lowering this to DEBUG (or sampling) to avoid log volume growth in production.
| log.info("ListStacks tool called, total stacks: {}", stackVOList.size()); | |
| log.debug("ListStacks tool called, total stacks: {}", stackVOList.size()); |
Uh oh!
There was an error while loading. Please reload this page.