Repository navigation
mcp no longer works in OSGi #562
Description
Activity
One other osgi issue to be addressed.. io.modelcontextprotocol.server.transport.StdioServerTransportProvider; and io.modelcontextprotocol.server.transport.WebMvcStreamableServerTransportProvider are in the same package but in 2 different jars. This causes an issue when you osgi import the package in that it either finds one or the other. In my particular application I need both transports so I am currently forced to copy the source of one of the transports into my code base which is not at all ideal. Ideally if a class is in a different jar it should have a different package to support osgi constructs.
The same happened with the newly refactored jar separations. Packages are share between the jackson and mcp jars.
I'm seeing this problem also,
In version 0.13, the "mcp" jar has been replaced with "mcp-core" and "mcp-json-jackson2". Although "mcp-core" is still an OSGi bundle, "mcp-json-jackson2" is not.
Has anyone yet tried just adding the bnd plugin to the build of 'mcp-json-jackson2' yet? That could be the whole problem. I'll try that on my fork later today.
I've looked into this and opened new issue #574.
It seems to me that the mcp-json project and the mcp-json-jackson2 project do not have the maven bnd plugins like the mcp-core project has to generate the OSGi bundle manifest.mf contents. It's pretty easy to add, and I'll happily produce a pr, but I think #574 will have to be dealt with first, because in my fork of the mcp-sdk I've updated the mcp-json pom.xml and the mcp-json-jackson2 pom.xml....and they produce fine OSGi bundle meta-data, but it still doesn't work because of issue #574 .
One other thing to say to this audience is that as OSGi does classloading differently, the ServiceLoader mechanism doesn't work 'out of the box' with OSGi frameworks. There is a mediator spec
and an implementation of this mediator spec in the Apache Aries spi-fly project. If the osgi folks would like assistance for using spi-fly to use the ServiceLoader (now in mcp-json and mcp-json-jackson2 projects/jars/soon to be bundles), just LMK.
I use other bundles which work nicely with Apache SPI Fly. In particular, you can look at how this is done for SLF4J bundles which I use in both Felix and Equinox containers with SPI Fly. I believe the bundles just need the appropriate "Require-Capability" and "Provide-Capability" headers as described in section from the SPI Fly docs that I referenced earlier.
Unfortunately, there is apparently a deeper problem here than adding osgi meta-data to the mcp-json and mcp-json-jackson2 projects...see #574 (comment)
Unfortunately, there is apparently a deeper problem here than adding osgi meta-data to the mcp-json and mcp-json-jackson2 projects...see #574 (comment)
I want to expand on this a little bit, just so that it's clear to everyone what I perceive is happening with this issue and #574.
For those interested, a short, technical description of the issue is given by the osgi sl mediator spec...in the introduction (2nd paragraph).
I wasn't privy to any technical discussions, but it appears to me that the mcp sdk was recently broken up into three parts corresponding to projects/jars for deployment: mcp-core, mcp-json, and mcp-json-jackson2. There may be others (mcp-spring), but these three are now necessary to do almost anything wrt server creation (e.g. to create a sync server). I expect that's true for client side as well, as json parsing/object mapping is required for clients of course.
mcp-core still has the maven plugins for bnd...which is a tool for producing a jar with osgi meta-data (mostly in manifest.mf).
mcp-json and mcp-json-jackson2 don't yet have the maven plugins for producing maven metadata. I've added them on my fork of the project here the two poms are here and here. I've built these locally and the osgi meta-data is added appropriately.
Even with mcp-json and mcp-json-jackson2 as bundles the same errors reported are occurring in my test runs. That's because in osgi there is a separate classloader for each bundle, and so the mcp-json bundle's classloader cannot 'see' the mcp-json-jackson2 META-INF/services/* files. As described in the mediator spec, OSGi doesn't use the thread context classloader, and so the ServiceLoader fails to load the classes specified in the mcp-json-jackson2/services/* files, and so can't load any default impls for these two required services.
So...how to fix this? Well, one question I have is: What was driving the decision to break out the mcp-json and mcp-json-jackson2 jars/projects from mcp-core? Is this necessary, done for some other perceived reason?
If the structure remains the same, there are some possible technical solutions...and I'm playing around with these now:
Split packages. This is not recommended as it requires the use of Require-Bundle rather than Import-Package by the library consumer, which defeats a lot of the value of osgi in terms of versioning and other things, and creates a terrible burden of maintenance for the library producers; Also doesn't work well at all with OSGi tooling...as again it is 'anti-modularity'. My opinion is it would be a very bad idea to introduce such a requirement on osgi-based mcp sdk consumers before even version 1.0.0 is released.
Fragments. This is also not recommended as it also 'breaks' modularity, but fragments use their host bundle's classloader...and I think that this means that if mcp-json was a host bundle and mcp-json-jackson2 was a fragment (with mcp-json as it's host bundle) that the ServiceLoader in mcp-json would see the META-INF/services/* files and use them. I'm not certain of that though.
Neither of the above solutions is very good, and will have many costs on both the consumers/community and the producers of the sdk. My previous experiences with both split packages and fragments is that they should be avoided if at all possible by library/sdk producers. Thus the question above about what the driving force was behind the mcp sdk split into multiple projects/jars.
I've produced an 'mcp-osgi' module, with it's own pom.xml that roles mcp-core, mcp-json, and mcp-json-jackson2 into a single resulting jar with manifest.mf osgi meta-data (a bundle). It also adds spifly manifest headers (SPI-Consumer and SPI-Provider) to allow spifly to do it's stuff in an osgi environment. I've done some smoke testing and it functions (ServiceLoader.load calls work to load jackson2 mappers and schema validator defaults)...at least to allowing sync and async mcp servers to be created.
The mcp-osgi module is currently here.
This is a work-around at this point, and is not ready for pull request. To get a pull request, there should be some coordination with the current project team, as though this works, it will be difficult to maintain and brittle. It also makes extra demands on the OSGi consumer environment (e.g. usage of spifly...which requires bundle start level setting) which may be difficult or impossible to satisfy for many OSGi-based open source and commercial consumers of the mcp-java-sdk.
I've moved the mcp-osgi module to a separate branch and the mcp-osgi/pom.xml.
I'd like to clarify my workaround.
First, I'd like to point out that the "mcp-core" jar includes the classes from "mcp-json". I wasn't sure if this was a bug or a feature. This means I only had to figure out what to do with the "mcp-json-jackson2" jar which is not a bundle.
In my setup, I deployed the "mcp-core" jar as its own bundle then included the "mcp-json-jackson2" jar inside my custom bundle that has my MCP server logic. In other words, my bundle's MANIFEST.MF has "Bundle-ClassPath: .,mcp-json-jackson2.jar". This way, my bundle can import packages from "mcp-core" AND directly instantiate the JacksonMcpJsonMapper and DefaultJsonSchemaValidator classes from "mcp-json-jackson2". After instantiating those two classes directly, I pass them to various builder methods used for building my MCP server like "HttpServletStreamableServerTransportProvider.Builder.jsonMapper()", "McpServer.SyncSpecification.jsonMapper()" and "McpServer.SyncSpecification.jsonSchemaValidator()". With those objects explicitly set, the broken service lookup is avoided.
I'd like to clarify my workaround.
First, I'd like to point out that the "mcp-core" jar includes the classes from "mcp-json". I wasn't sure if this was a bug or a feature. This means I only had to figure out what to do with the "mcp-json-jackson2" jar which is not a bundle.
In my setup, I deployed the "mcp-core" jar as its own bundle then included the "mcp-json-jackson2" jar inside my custom bundle that has my MCP server logic. In other words, my bundle's MANIFEST.MF has "Bundle-ClassPath: .,mcp-json-jackson2.jar". This way, my bundle can import packages from "mcp-core" AND directly instantiate the JacksonMcpJsonMapper and DefaultJsonSchemaValidator classes from "mcp-json-jackson2". After instantiating those two classes directly, I pass them to various builder methods used for building my MCP server like "HttpServletStreamableServerTransportProvider.Builder.jsonMapper()", "McpServer.SyncSpecification.jsonMapper()" and "McpServer.SyncSpecification.jsonSchemaValidator()". With those objects explicitly set, the broken service lookup is avoided.
Question: What were the code changes you made to directly instantiate the various services...i.e. rather than the ServiceLoader?
The reason I ask is that one potential permanent fix would be to have each of the calls that currently uses ServiceLoader be generalized to use OSGi services when run in OSGi environment, and use the ServiceLoader in other/non-osgi environments. This has some releng complexities, but it has been used successfully elsewhere (some older spring libs as I remember) and isn't as hard to maintain as either your (embedded jar) or my (single bundle) work-arounds.
What were the code changes you made to directly instantiate the various services...i.e. rather than the ServiceLoader?
I already described it at a high level but it would look like this:
ObjectMapper mapper = new ObjectMapper(); McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(mapper); JsonSchemaValidator jsonSchemaValidator = new DefaultJsonSchemaValidator(mapper); HttpServletStreamableServerTransportProvider streamingTransportProvider = HttpServletStreamableServerTransportProvider.builder() .jsonMapper(jsonMapper) .build(); McpServer.sync(streamingTransportProvider) .jsonMapper(jsonMapper) .jsonSchemaValidator(jsonSchemaValidator) .build();
Or for a client you would do this:
ObjectMapper mapper = new ObjectMapper(); McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(mapper); McpClientTransport clientTransport = HttpClientStreamableHttpTransport.builder("http://localhost:8080") .jsonMapper(jsonMapper) .build()
one potential permanent fix would be to have each of the calls that currently uses ServiceLoader be generalized to use OSGi services when run in OSGi environment, and use the ServiceLoader in other/non-osgi environments
Earlier, I suggested using ServiceLoader with Apache SPI Fly. There may be other fixes needed, like not having bundles with the same java package, but ServiceLoader is supported by OSGi if you include Apache SPI Fly and make the bundles have proper "Require-Capability" and "Provide-Capability" headers.
Reacted by Huang KexinWhat were the code changes you made to directly instantiate the various services...i.e. rather than the ServiceLoader?
I already described it at a high level but it would look like this:
ObjectMapper mapper = new ObjectMapper();
McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(mapper);
JsonSchemaValidator jsonSchemaValidator = new DefaultJsonSchemaValidator(mapper);
HttpServletStreamableServerTransportProvider streamingTransportProvider = HttpServletStreamableServerTransportProvider.builder()
.jsonMapper(jsonMapper)
.mcpEndpoint("/mcp")
.build();
McpServer.sync(sseTransportProvider)
.jsonMapper(jsonMapper)
.jsonSchemaValidator(jsonSchemaValidator)
.build();I see, thanks. I thought you were modifying the creation of the default jsonMapper and default jsonSchemaValidator, but you are setting them explicitly in the McpServer.sync call, which avoids the ServiceLoader setting of defaults. I was thinking that the setting of the defaults could be done as OSGi services in osgi environments and use ServiceLoader outside of osgi environments.
one potential permanent fix would be to have each of the calls that currently uses ServiceLoader be generalized to use OSGi services when run in OSGi environment, and use the ServiceLoader in other/non-osgi environments
Earlier, I suggested using ServiceLoader with Apache SPI Fly. There may be other fixes needed, like not having bundles with the same java package, but ServiceLoader is supported by OSGi if you include Apache SPI Fly and make the bundles have proper "Require-Capability" and "Provide-Capability" headers.
Yes, understood. The work around here uses spi fly just as you say (it uses SPI-Consumer and SPI-Provider headers rather than capabilities, but that's just a short cut).
To phrase things a different way, this library uses builder pattern to simplify creation of various things, including servers and clients. After instantiating the "mapper" and "validator" objects, I simply set them on any builder methods that take them. The builders do not fall back on service lookup when I set them explicitly. Doing so also gives me control over the ObjectMapper that's used which I generally prefer anyway. For certain contexts I may use a custom ObjectMapper with specific parsing limits/constraints configured, for example. An alternative to this can be somewhat of a maintenance nightmare, having to patch and re-patch a third party library over-and-over until a fix is eventually made available. So, I look for workarounds like this which require the least amount of effort on my part.
OSGi support is not a priority for mcp-java-sdk.
While additional META-INF metadata is acceptable, rewriting jars and creating fat jars is out of scope. Includingbnd-maven-pluginincorewas a mistake that we'll clean in the follow up versions.Perhaps we can consider creating a separate GH repo (e.g.,
mcp-java-sdk-osgi) either in the MCP ecosystem or elsewhere to repackage mcp-java-sdk for OSGi users.
How those this sound?I don't understand why you would maintain 2 entirely different sets of code when you could do 1 with very very few changes. I have the current version working in an osgi environment but I have to unnecessarily extract out and duplicate a class or two that ordinarily I wouldn't have to extract. It seems just logical to work together to move forward such a crucial library rather than making the library more difficult to use. Different packages in different jars seems like a normal best practice..
4 remaining items
Summary: As described above, pr #682 issue #612 provides a permanent fix for this issue by:
-
Migrating the < 10 classes currently in the mcp-json project/jar back into mcp-core. As described in Mcp-core has compile-time dependency on mcp-json #612 mcp-core currently has a compile-time dependency on mcp-json classes.
-
Modifying the default json mapper and schema validation providers to allow OSGi declarative services to be used (available only in osgi environments) to set the providers upon startup...rather than use the ServiceLoader...which is still the default outside of osgi environments.
-
Add standard declarative services metadata to the mcp-core manifest and the mcp-jackson2 manifest creation on build. This consists of a small addition of a declarative services xml files, and references to those files in the manifest.mf as per the osgi specification.
The result is a mcp-core.jar and mcp-jackson2 jar that when used in non-osgi environments: uses the ServiceLoader to set the mapper provider and schema validation provider to set the default (jackson2, if that's the jar present at runtime, or other).
and when used in osgi environments: uses declarative services to set the mapper provider and schema validation provider defaults via osgi services.
I've tested the osgi environment behavior by using it in a recent update to this project, which is osgi based.
-
@scottslewis I am planning to contribute a new
mcp-json-jackson3module via a PR, I would welcome your feedback from an OSGI perpective.Currently
mcp-json-jackson2is using theio.modelcontextprotocol.json.jacksonandio.modelcontextprotocol.json.schema.jacksonpackages.I am wondering if we should either:
- Use the same version-less packages in both
mcp-json-jackson2andmcp-json-jackson3 - Use the Jackson version in the package, which would lead to use
io.modelcontextprotocol.json.jackson2/io.modelcontextprotocol.json.schema.jackson2formcp-json-jackson2andio.modelcontextprotocol.json.jackson3/io.modelcontextprotocol.json.schema.jackson3formcp-json-jackson3.
1 has the advantage of not breaking the current API.
mcp-json-jackson2andmcp-json-jackson3are not really designed to be used at the same time, but should we go as far as using the same package for those mutually-exclusive implementations? 2 clearly separate both at package level but is a breaking change (maybe ok since we still use 0.x versions).- Use the same version-less packages in both
@scottslewis I am planning to contribute a new
mcp-json-jackson3module via a PR, I would welcome your feedback from an OSGI perpective.Currently
mcp-json-jackson2is using theio.modelcontextprotocol.json.jacksonandio.modelcontextprotocol.json.schema.jacksonpackages.I am wondering if we should either:
1. Use the same version-less packages in both `mcp-json-jackson2` and `mcp-json-jackson3` 2. Use the Jackson version in the package, which would lead to use `io.modelcontextprotocol.json.jackson2`/`io.modelcontextprotocol.json.schema.jackson2` for `mcp-json-jackson2` and `io.modelcontextprotocol.json.jackson3`/`io.modelcontextprotocol.json.schema.jackson3` for `mcp-json-jackson3`.1 has the advantage of not breaking the current API.
mcp-json-jackson2andmcp-json-jackson3are not really designed to be used at the same time, but should we go as far as using the same package for those mutually-exclusive implementations? 2 clearly separate both at package level but is a breaking change (maybe ok since we still use 0.x versions).This is a difficult choice IMHO. My inclination is to do what you describe and a new, jackson3 project that can live/run alongside jackson2 and have different package names. Reason: You can be pretty sure that there will be some consumers of the sdk that even as jackson3 gets more common/used that they will require continuing to use Jackson2...even while they use new versions of the sdk (e.g. to keep up with enhancements/bug fixes). The last thing most consumers will want (especially large ones) is a forced upgrade of any library.
This is not a serious problem at all in OSGI environments, as the classloader-per-bundle aspect of the environment allows more than one class with the same package/class name. For example, there are separate providers for multiple major versions of httpclient in Eclipse/ECF (the project I've led for long time). These can all run in the same runtime at the same time and do so happily.
But for non OSGi environments, you can potentially have classloader/class name collision problems. This can make it impossible....or at least very difficult for consumers to use more than one major/incompatible versions of Jackson (or any other lib) at the same time...therefore requiring cutovers. Any such cutover can be very painful (or impossible) for sdk consumers...and so is to be avoided if possible.
Since I already did a PR for Jackson 3 in #742. I wanted to clarify what @sdeleuze proposed in #562 (comment).
I do understand where you are coming from @sdeleuze and I would have liked to solve it using the same package name as well. However, I have another idea.
What if we do the following, create a new
mcp-json-jacksonmodule which would have an optional dependency on Jackson 2 and Jackson 3, that's possible since they have different artifacts anyways. That way we can stay with the packageio.modelcontextprotocol.json.jackson. Then there can be 2 wrapper modulesmcp-json-jackson2andmcp-json-jackson3which would only pull in the right dependencies.The only problem though would be the schema validation. From what I can see the
com.networknt:json-schema-validatordoes not have different artifacts for the different Jackson versions. There is version 2.0.0 for Jackson 2 and version 3.0.0 for Jackson 3.Since I already did a PR for Jackson 3 in #742. I wanted to clarify what @sdeleuze proposed in #562 (comment).
I do understand where you are coming from @sdeleuze and I would have liked to solve it using the same package name as well. However, I have another idea.
What if we do the following, create a new
mcp-json-jacksonmodule which would have an optional dependency on Jackson 2 and Jackson 3, that's possible since they have different artifacts anyways. That way we can stay with the packageio.modelcontextprotocol.json.jackson. Then there can be 2 wrapper modulesmcp-json-jackson2andmcp-json-jackson3which would only pull in the right dependencies.The only problem though would be the schema validation. From what I can see the
com.networknt:json-schema-validatordoes not have different artifacts for the different Jackson versions. There is version 2.0.0 for Jackson 2 and version 3.0.0 for Jackson 3.If at all possible, I would think you would want to just have one api (i.e. mcp-json...which might go back into mcp-core...i.e. #682). Adding a another api (i.e. across Jackson major versions but not non-jackson providers of json serialization/deserialization) will require more maintenance and build/deployment subtleties over the long term.
Plus it will cause additional fits for OSGi environments as described here with use of ServiceLoader.
If at all possible, I would think you would want to just have one api (i.e. mcp-json...which might go back into mcp-core...i.e. #682). Adding a another api (i.e. across Jackson major versions but not non-jackson providers of json serialization/deserialization) will require more maintenance and build/deployment subtleties over the long term.
Plus it will cause additional fits for OSGi environments as described here with use of ServiceLoader.
I am also not convinced that this
mcp-json-jacksonproposal will help.But for non OSGi environments, you can potentially have classloader/class name collision problems.
For production I don't think we want to support both, but for tests, I can see how we could want that. Also conceptually, it would be a bit of a shame to use the same package while service loader provides a clean way to use a different one.
In the end, I think my vote is for using different packages (solution 2 from #562 (comment), let's continue this discussion in #742 where I have added a new comment) and merging
mcp-jsontomcp-core(#682).For info of people watching this issue:
pr #762 includes OSGi metadata (Manifest.mf and SCR xml files) that fully addresses this issue. I'm happy to give a technical description of how this is done, but it's mostly covered by previous discussion on this issue so I won't repeat it unless summarization is necessary.
This includes support for using either Jackson2 xor Jackson3 for mcp serialization. All that's needed to use one or the other is to include the appropirate jar/bundle (mcp-json-jackson2 or mcp-json-jackson3) into your osgi environment alongside mcp-core.
I'm testing this now in my local osgi environment, so there may be some small changes to the pr in case the osgi meta-data is not right for whatever reason...or there is something about Jackson3 that doesn't work correctly in osgi environments, but I just don't know yet.
I'm testing this now in my local osgi environment
I've now tested the fixes in pr #762 in OSGi environment (OSGi 8, Java 17 with Service Component Runtime from OSGi 8 spec). This repo has an example/test app that I've been using for testing.
These tests work with either the jackson2 jackson3 modules. See mcpserver.bndrun and mcpserver.jackson3.bndrun files in mcpserver project (or mcpclient) for details of the runtime configuration for jackson2 vs. jackson3.
@Kerlann : With pr #779 being now merged, this issue is now fixed. It can be closed.
The default mapper and the default schema validator will be null...as in the original description stack trace...if there is no jackson2 or jackson3 (or some other provider) available at runtime.
In OSGi environments the service component runtime (SCR) is required.
With the above by @scottslewis and the recent changes I am closing the issue. If that's not correct, please reopen or create a new issue to better describe the current status. Thanks!
This is still an issue in version 1.0.0
One other osgi issue to be addressed.. io.modelcontextprotocol.server.transport.StdioServerTransportProvider; and io.modelcontextprotocol.server.transport.WebMvcStreamableServerTransportProvider are in the same package but in 2 different jars. This causes an issue when you osgi import the package in that it either finds one or the other. In my particular application I need both transports so I am currently forced to copy the source of one of the transports into my code base which is not at all ideal. Ideally if a class is in a different jar it should have a different package to support osgi constructs.
I addressed the above concern in #856 (comment)
Bug description
In version 0.12, there was a single "mcp" jar which happens to be a valid OSGi bundle having a valid OSGi manifest. Its dependencies all happen to be OSGi bundles as well. So, mcp and all its dependencies have been loading in OSGi without issue.
In version 0.13, the "mcp" jar has been replaced with "mcp-core" and "mcp-json-jackson2". Although "mcp-core" is still an OSGi bundle, "mcp-json-jackson2" is not.
Environment
I have been using an Apache Felix Servlet Bridge setup but the OSGi container shouldn't matter.
Steps to reproduce
Building an McpServer from an OSGi container results in the following:
Expected behavior
The "mcp-json-jackson2" jar should have an OSGi manifest. The mcp bundles should have proper "Require-Capability" and "Provide-Capability" headers to support the OSGi Service Loader mechanism, like what's implemented by Apache SPI Fly: https://aries.apache.org/documentation/modules/spi-fly.html#specconf
Minimal Complete Reproducible example
The original Servlet Bridge example is here:
https://github.com/apache/felix-dev/tree/8c10c79ff48b6a8938e9fc8088964a2facb42da1/http/samples/bridge
You would need to add your own OSGi bundle with a bundle Activator that starts an MCP server and registers its transport using the OSGi HTTP service. Some detail: https://github.com/apache/felix-dev/blob/master/http/README.md#using-the-httpservice
Workaround
The workaround is to wrap the "mcp-json-jackson2" jar inside of a custom OSGi bundle and to explicitly call "Builder.jsonMapper" and "jsonSchemaValidator" methods on everything to avoid the broken service lookup.