fix(mcp): load json providers with fallback classloader#2155
fix(mcp): load json providers with fallback classloader#2155BobSong-dev wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses MCP JSON provider discovery failures in Spring Boot fat JAR / common-pool thread scenarios by resolving MCP JSON service providers with a context-classloader-first strategy and falling back to the AgentScope classloader when needed.
Changes:
- Introduces
McpJsonDefaultsto resolveMcpJsonMapperSupplierandJsonSchemaValidatorSupplierviaServiceLoaderwith a fallback classloader. - Updates
McpClientBuilderto explicitly pass the resolved JSON mapper into transports and the resolved schema validator into MCP client builders. - Adds a regression test ensuring fallback works when the thread context classloader cannot see MCP service providers.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpJsonDefaults.java | New utility to load MCP JSON defaults with classloader fallback. |
| agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientBuilder.java | Wires resolved JSON mapper and schema validator through transports/clients. |
| agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpJsonDefaultsTest.java | Regression test for isolated context-classloader fallback behavior. |
expanded Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
AgentScopeJavaBot
left a comment
There was a problem hiding this comment.
🤖 AI Review
This PR fixes issue #913 where MCP client construction fails with No default JsonSchemaValidatorSupplier implementation found in Spring Boot fat JAR environments. The root cause is that ForkJoinPool.commonPool() worker threads use AppClassLoader as their context classloader, which cannot see service provider metadata nested under BOOT-INF/lib. The fix introduces McpJsonDefaults, a focused utility class that attempts context-classloader discovery first, then falls back to the AgentScope class's own classloader. The resolved McpJsonMapper and JsonSchemaValidator are explicitly injected into all three transport types (Stdio, SSE, StreamableHTTP) and both client builders (sync/async), eliminating reliance on the MCP SDK's internal ServiceLoader-based discovery. A second commit adds a defensive try-catch for ServiceConfigurationError | LinkageError in loadFirst(), consistent with the pattern already used in ModelRegistry.loadServiceProviders(). The regression test effectively simulates an isolated classloader scenario. Overall, this is a clean, well-scoped fix that correctly addresses the root cause with no behavior changes for non-fat-JAR deployments.
| } | ||
|
|
||
| private static <T extends Supplier<?>> T loadSupplier( | ||
| Class<T> serviceType, String implementationType) { |
There was a problem hiding this comment.
[nitpick] The parameter name implementationType is slightly misleading — it is used as a display name in the error message (e.g. "McpJsonMapper", "JsonSchemaValidator"), not as a type reference. Consider renaming to displayName or providerName for clarity.
|
|
||
| private McpJsonDefaults() {} | ||
|
|
||
| static McpJsonMapper jsonMapper() { |
There was a problem hiding this comment.
[nitpick] jsonMapper() and jsonSchemaValidator() invoke ServiceLoader.load() on every call. Since builder methods are typically called once per client and ServiceLoader.load() is lightweight, this is acceptable. However, if clients are created frequently, consider caching the resolved supplier with a lazy-holder or static final field to avoid repeated classpath scanning. Note: ServiceLoader.load() + findFirst() should be deterministic for a given classloader, so caching is safe.
AgentScope-Java Version
2.0.1-SNAPSHOT(mainbranch)Description
Fixes #913.
Background and root cause
In a Spring Boot executable fat JAR, the application main thread uses
LaunchedClassLoader, which can access dependencies underBOOT-INF/lib. AForkJoinPool.commonPool()worker can instead use the systemAppClassLoaderas its thread context classloader.The MCP SDK default lookup uses
ServiceLoader.load(...), which relies on that thread context classloader. WhenMcpClientBuilderis called from a parallel stream or common-pool worker, the lookup cannot see the JSON provider metadata in the nestedmcp-json-jackson2JAR and throwsNo default JsonSchemaValidatorSupplier implementation found.Changes
Testing
The relevant tests have passed locally.
A Spring Boot 3.3.1 executable fat JAR was also tested locally. The original code reproduced the exception when building an AgentScope MCP client from
ForkJoinPool.commonPool(). With this patch, the same fat JAR scenario completes successfully.