diff --git a/mcp.webmvc.log b/mcp.webmvc.log new file mode 100644 index 0000000..bf13bcb --- /dev/null +++ b/mcp.webmvc.log @@ -0,0 +1,237 @@ +2025-02-13T14:16:11.794+01:00 INFO 48979 --- [main] o.s.a.m.s.w.s.McpMvcServerApplication : Starting McpMvcServerApplication using Java 17.0.12 with PID 48979 (/Users/christiantzolov/Dev/projects/spring-ai-examples/model-context-protocol/book-library/manual-webmvc-server/target/classes started by christiantzolov in /Users/christiantzolov/Dev/projects/spring-ai-examples) +2025-02-13T14:16:11.796+01:00 INFO 48979 --- [main] o.s.a.m.s.w.s.McpMvcServerApplication : No active profile set, falling back to 1 default profile: "default" +2025-02-13T14:16:12.139+01:00 INFO 48979 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) +2025-02-13T14:16:12.145+01:00 INFO 48979 --- [main] o.apache.catalina.core.StandardService : Starting service [Tomcat] +2025-02-13T14:16:12.145+01:00 INFO 48979 --- [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.33] +2025-02-13T14:16:12.171+01:00 INFO 48979 --- [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext +2025-02-13T14:16:12.171+01:00 INFO 48979 --- [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 357 ms +2025-02-13T14:16:12.511+01:00 INFO 48979 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/' +2025-02-13T14:16:12.515+01:00 INFO 48979 --- [main] o.s.a.m.s.w.s.McpMvcServerApplication : Started McpMvcServerApplication in 0.888 seconds (process running for 1.008) +2025-02-13T14:16:27.039+01:00 INFO 48979 --- [http-nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' +2025-02-13T14:16:27.039+01:00 INFO 48979 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' +2025-02-13T14:16:27.040+01:00 INFO 48979 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms +2025-02-13T14:16:27.100+01:00 INFO 48979 --- [http-nio-8080-exec-2] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=initialize, id=a1adb0ea-0, params={protocolVersion=2024-11-05, capabilities={}, clientInfo={name=Spring AI MCP Client, version=1.0.0}}] +2025-02-13T14:16:27.105+01:00 INFO 48979 --- [http-nio-8080-exec-2] o.s.ai.mcp.server.McpAsyncServer : Client initialize request - Protocol: 2024-11-05, Capabilities: ClientCapabilities[experimental=null, roots=null, sampling=null], Info: Implementation[name=Spring AI MCP Client, version=1.0.0] +2025-02-13T14:16:27.149+01:00 INFO 48979 --- [http-nio-8080-exec-3] o.s.ai.mcp.spec.DefaultMcpSession : Received notification: JSONRPCNotification[jsonrpc=2.0, method=notifications/initialized, params=null] +2025-02-13T14:16:27.152+01:00 INFO 48979 --- [http-nio-8080-exec-4] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=ping, id=a1adb0ea-1, params=null] +2025-02-13T14:16:27.155+01:00 INFO 48979 --- [http-nio-8080-exec-5] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=tools/list, id=a1adb0ea-2, params={}] +2025-02-13T14:16:27.166+01:00 INFO 48979 --- [http-nio-8080-exec-6] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=tools/call, id=a1adb0ea-3, params={name=toUpperCase, arguments={input=accountName}}] +2025-02-13T14:16:27.187+01:00 INFO 48979 --- [http-nio-8080-exec-7] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=tools/call, id=a1adb0ea-4, params={name=getBooks, arguments={title=Spring Framework}}] +2025-02-13T14:16:28.379+01:00 INFO 48979 --- [http-nio-8080-exec-8] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=resources/list, id=a1adb0ea-5, params={}] +2025-02-13T14:16:28.389+01:00 INFO 48979 --- [http-nio-8080-exec-9] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=resources/read, id=a1adb0ea-6, params={uri=system://info}] +2025-02-13T14:16:28.400+01:00 INFO 48979 --- [http-nio-8080-exec-10] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=prompts/list, id=a1adb0ea-7, params={}] +2025-02-13T14:16:28.407+01:00 INFO 48979 --- [http-nio-8080-exec-1] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=prompts/get, id=a1adb0ea-8, params={name=greeting, arguments={name=Spring}}] +2025-02-13T14:16:35.297+01:00 INFO 48979 --- [http-nio-8080-exec-4] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=initialize, id=3b5482a0-0, params={protocolVersion=2024-11-05, capabilities={}, clientInfo={name=Spring AI MCP Client, version=1.0.0}}] +2025-02-13T14:16:35.297+01:00 INFO 48979 --- [http-nio-8080-exec-4] o.s.ai.mcp.server.McpAsyncServer : Client initialize request - Protocol: 2024-11-05, Capabilities: ClientCapabilities[experimental=null, roots=null, sampling=null], Info: Implementation[name=Spring AI MCP Client, version=1.0.0] +2025-02-13T14:16:35.333+01:00 INFO 48979 --- [http-nio-8080-exec-5] o.s.ai.mcp.spec.DefaultMcpSession : Received notification: JSONRPCNotification[jsonrpc=2.0, method=notifications/initialized, params=null] +2025-02-13T14:16:35.334+01:00 INFO 48979 --- [http-nio-8080-exec-6] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=ping, id=3b5482a0-1, params=null] +2025-02-13T14:16:35.335+01:00 ERROR 48979 --- [http-nio-8080-exec-6] o.s.a.m.s.t.WebMvcSseServerTransport : Failed to send message to session 06a86083-27a9-4c10-83e6-d5263fb9c73b: ServletOutputStream failed to flush: java.io.IOException: Broken pipe +2025-02-13T14:16:35.338+01:00 INFO 48979 --- [http-nio-8080-exec-8] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=tools/list, id=3b5482a0-2, params={}] +2025-02-13T14:16:35.338+01:00 ERROR 48979 --- [http-nio-8080-exec-8] o.s.a.m.s.t.WebMvcSseServerTransport : Failed to send message to session 06a86083-27a9-4c10-83e6-d5263fb9c73b: Response not usable after response errors. +2025-02-13T14:16:35.338+01:00 ERROR 48979 --- [http-nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exception + +java.io.IOException: Broken pipe + at java.base/sun.nio.ch.FileDispatcherImpl.write0(Native Method) ~[na:na] + at java.base/sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:62) ~[na:na] + at java.base/sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:132) ~[na:na] + at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:97) ~[na:na] + at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:53) ~[na:na] + at java.base/sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:532) ~[na:na] + at org.apache.tomcat.util.net.NioChannel.write(NioChannel.java:122) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite(NioEndpoint.java:1378) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.net.SocketWrapperBase.doWrite(SocketWrapperBase.java:764) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.net.SocketWrapperBase.flushBlocking(SocketWrapperBase.java:728) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.net.SocketWrapperBase.flush(SocketWrapperBase.java:712) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.flush(Http11OutputBuffer.java:574) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.http11.filters.ChunkedOutputFilter.flush(ChunkedOutputFilter.java:156) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.http11.Http11OutputBuffer.flush(Http11OutputBuffer.java:216) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.http11.Http11Processor.flush(Http11Processor.java:1274) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.AbstractProcessor.action(AbstractProcessor.java:408) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.Response.action(Response.java:208) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:299) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.connector.OutputBuffer.flush(OutputBuffer.java:265) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.connector.CoyoteOutputStream.flush(CoyoteOutputStream.java:136) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.springframework.web.context.request.async.StandardServletAsyncWebRequest$LifecycleServletOutputStream.flush(StandardServletAsyncWebRequest.java:412) ~[spring-web-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.function.SseServerResponse$DefaultSseBuilder.send(SseServerResponse.java:144) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.function.SseServerResponse$DefaultSseBuilder.writeString(SseServerResponse.java:206) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.function.SseServerResponse$DefaultSseBuilder.data(SseServerResponse.java:194) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.ai.mcp.server.transport.WebMvcSseServerTransport.lambda$sendMessage$0(WebMvcSseServerTransport.java:193) ~[mcp-webmvc-sse-transport-0.6.0.jar:0.6.0] + at java.base/java.util.concurrent.ConcurrentHashMap$ValuesView.forEach(ConcurrentHashMap.java:4780) ~[na:na] + at org.springframework.ai.mcp.server.transport.WebMvcSseServerTransport.lambda$sendMessage$1(WebMvcSseServerTransport.java:191) ~[mcp-webmvc-sse-transport-0.6.0.jar:0.6.0] + at reactor.core.publisher.MonoRunnable.subscribe(MonoRunnable.java:49) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4576) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribeWith(Mono.java:4642) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4403) ~[reactor-core-3.6.12.jar:3.6.12] + at org.springframework.ai.mcp.spec.DefaultMcpSession.lambda$new$0(DefaultMcpSession.java:151) ~[mcp-0.6.0.jar:0.6.0] + at reactor.core.publisher.LambdaMonoSubscriber.onNext(LambdaMonoSubscriber.java:171) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2571) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:171) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2367) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onSubscribe(FluxOnErrorResume.java:74) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:96) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:55) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:53) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4576) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribeWith(Mono.java:4642) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4542) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4478) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4450) ~[reactor-core-3.6.12.jar:3.6.12] + at org.springframework.ai.mcp.spec.DefaultMcpSession.lambda$new$3(DefaultMcpSession.java:151) ~[mcp-0.6.0.jar:0.6.0] + at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:196) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2571) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.request(FluxPeekFuseable.java:144) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.BlockingSingleSubscriber.onSubscribe(BlockingSingleSubscriber.java:54) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onSubscribe(FluxPeekFuseable.java:178) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:55) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4576) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.block(Mono.java:1778) ~[reactor-core-3.6.12.jar:3.6.12] + at org.springframework.ai.mcp.server.transport.WebMvcSseServerTransport.handleMessage(WebMvcSseServerTransport.java:274) ~[mcp-webmvc-sse-transport-0.6.0.jar:0.6.0] + at org.springframework.web.servlet.function.support.HandlerFunctionAdapter.handle(HandlerFunctionAdapter.java:108) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.1.15.jar:6.1.15] + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[tomcat-embed-core-10.1.33.jar:6.0] + at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.15.jar:6.1.15] + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.33.jar:6.0] + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.33.jar:10.1.33] + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.15.jar:6.1.15] + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.15.jar:6.1.15] + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na] + +2025-02-13T14:16:35.340+01:00 ERROR 48979 --- [http-nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception + +java.io.IOException: Broken pipe + at java.base/sun.nio.ch.FileDispatcherImpl.write0(Native Method) ~[na:na] + at java.base/sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:62) ~[na:na] + at java.base/sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:132) ~[na:na] + at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:97) ~[na:na] + at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:53) ~[na:na] + at java.base/sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:532) ~[na:na] + at org.apache.tomcat.util.net.NioChannel.write(NioChannel.java:122) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite(NioEndpoint.java:1378) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.net.SocketWrapperBase.doWrite(SocketWrapperBase.java:764) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.net.SocketWrapperBase.flushBlocking(SocketWrapperBase.java:728) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.net.SocketWrapperBase.flush(SocketWrapperBase.java:712) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.flush(Http11OutputBuffer.java:574) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.http11.filters.ChunkedOutputFilter.flush(ChunkedOutputFilter.java:156) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.http11.Http11OutputBuffer.flush(Http11OutputBuffer.java:216) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.http11.Http11Processor.flush(Http11Processor.java:1274) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.AbstractProcessor.action(AbstractProcessor.java:408) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.Response.action(Response.java:208) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:299) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.connector.OutputBuffer.flush(OutputBuffer.java:265) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.connector.CoyoteOutputStream.flush(CoyoteOutputStream.java:136) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.springframework.web.context.request.async.StandardServletAsyncWebRequest$LifecycleServletOutputStream.flush(StandardServletAsyncWebRequest.java:412) ~[spring-web-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.function.SseServerResponse$DefaultSseBuilder.send(SseServerResponse.java:144) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.function.SseServerResponse$DefaultSseBuilder.writeString(SseServerResponse.java:206) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.function.SseServerResponse$DefaultSseBuilder.data(SseServerResponse.java:194) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.ai.mcp.server.transport.WebMvcSseServerTransport.lambda$sendMessage$0(WebMvcSseServerTransport.java:193) ~[mcp-webmvc-sse-transport-0.6.0.jar:0.6.0] + at java.base/java.util.concurrent.ConcurrentHashMap$ValuesView.forEach(ConcurrentHashMap.java:4780) ~[na:na] + at org.springframework.ai.mcp.server.transport.WebMvcSseServerTransport.lambda$sendMessage$1(WebMvcSseServerTransport.java:191) ~[mcp-webmvc-sse-transport-0.6.0.jar:0.6.0] + at reactor.core.publisher.MonoRunnable.subscribe(MonoRunnable.java:49) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4576) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribeWith(Mono.java:4642) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4403) ~[reactor-core-3.6.12.jar:3.6.12] + at org.springframework.ai.mcp.spec.DefaultMcpSession.lambda$new$0(DefaultMcpSession.java:151) ~[mcp-0.6.0.jar:0.6.0] + at reactor.core.publisher.LambdaMonoSubscriber.onNext(LambdaMonoSubscriber.java:171) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2571) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:171) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2367) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onSubscribe(FluxOnErrorResume.java:74) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:96) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:55) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:53) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4576) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribeWith(Mono.java:4642) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4542) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4478) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4450) ~[reactor-core-3.6.12.jar:3.6.12] + at org.springframework.ai.mcp.spec.DefaultMcpSession.lambda$new$3(DefaultMcpSession.java:151) ~[mcp-0.6.0.jar:0.6.0] + at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:196) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2571) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.request(FluxPeekFuseable.java:144) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.BlockingSingleSubscriber.onSubscribe(BlockingSingleSubscriber.java:54) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onSubscribe(FluxPeekFuseable.java:178) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:55) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.subscribe(Mono.java:4576) ~[reactor-core-3.6.12.jar:3.6.12] + at reactor.core.publisher.Mono.block(Mono.java:1778) ~[reactor-core-3.6.12.jar:3.6.12] + at org.springframework.ai.mcp.server.transport.WebMvcSseServerTransport.handleMessage(WebMvcSseServerTransport.java:274) ~[mcp-webmvc-sse-transport-0.6.0.jar:0.6.0] + at org.springframework.web.servlet.function.support.HandlerFunctionAdapter.handle(HandlerFunctionAdapter.java:108) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.15.jar:6.1.15] + at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.1.15.jar:6.1.15] + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[tomcat-embed-core-10.1.33.jar:6.0] + at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.15.jar:6.1.15] + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.33.jar:6.0] + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.33.jar:10.1.33] + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.15.jar:6.1.15] + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.15.jar:6.1.15] + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.33.jar:10.1.33] + at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na] + +2025-02-13T14:16:35.345+01:00 INFO 48979 --- [http-nio-8080-exec-9] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=tools/call, id=3b5482a0-3, params={name=toUpperCase, arguments={input=accountName}}] +2025-02-13T14:16:35.346+01:00 ERROR 48979 --- [boundedElastic-1] o.s.a.m.s.t.WebMvcSseServerTransport : Failed to send message to session 06a86083-27a9-4c10-83e6-d5263fb9c73b: Response not usable after response errors. +2025-02-13T14:16:35.356+01:00 INFO 48979 --- [http-nio-8080-exec-10] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=tools/call, id=3b5482a0-4, params={name=getBooks, arguments={title=Spring Framework}}] +2025-02-13T14:16:35.835+01:00 ERROR 48979 --- [boundedElastic-1] o.s.a.m.s.t.WebMvcSseServerTransport : Failed to send message to session 06a86083-27a9-4c10-83e6-d5263fb9c73b: Response not usable after response errors. +2025-02-13T14:16:35.850+01:00 INFO 48979 --- [http-nio-8080-exec-1] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=resources/list, id=3b5482a0-5, params={}] +2025-02-13T14:16:35.850+01:00 ERROR 48979 --- [http-nio-8080-exec-1] o.s.a.m.s.t.WebMvcSseServerTransport : Failed to send message to session 06a86083-27a9-4c10-83e6-d5263fb9c73b: Response not usable after response errors. +2025-02-13T14:16:35.859+01:00 INFO 48979 --- [http-nio-8080-exec-2] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=resources/read, id=3b5482a0-6, params={uri=system://info}] +2025-02-13T14:16:35.861+01:00 ERROR 48979 --- [boundedElastic-1] o.s.a.m.s.t.WebMvcSseServerTransport : Failed to send message to session 06a86083-27a9-4c10-83e6-d5263fb9c73b: Response not usable after response errors. +2025-02-13T14:16:35.868+01:00 INFO 48979 --- [http-nio-8080-exec-3] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=prompts/list, id=3b5482a0-7, params={}] +2025-02-13T14:16:35.869+01:00 ERROR 48979 --- [http-nio-8080-exec-3] o.s.a.m.s.t.WebMvcSseServerTransport : Failed to send message to session 06a86083-27a9-4c10-83e6-d5263fb9c73b: Response not usable after response errors. +2025-02-13T14:16:35.875+01:00 INFO 48979 --- [http-nio-8080-exec-4] o.s.ai.mcp.spec.DefaultMcpSession : Received request: JSONRPCRequest[jsonrpc=2.0, method=prompts/get, id=3b5482a0-8, params={name=greeting, arguments={name=Spring}}] +2025-02-13T14:16:35.875+01:00 ERROR 48979 --- [boundedElastic-1] o.s.a.m.s.t.WebMvcSseServerTransport : Failed to send message to session 06a86083-27a9-4c10-83e6-d5263fb9c73b: Response not usable after response errors. +2025-02-13T14:16:55.614+01:00 WARN 48979 --- [http-nio-8080-exec-6] .w.s.m.s.DefaultHandlerExceptionResolver : Ignoring exception, response committed already: org.springframework.web.context.request.async.AsyncRequestTimeoutException +2025-02-13T14:16:55.615+01:00 WARN 48979 --- [http-nio-8080-exec-6] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.context.request.async.AsyncRequestTimeoutException] +2025-02-13T14:16:55.622+01:00 WARN 48979 --- [SpringApplicationShutdownHook] o.s.a.m.s.t.WebMvcSseServerTransport : Failed to complete SSE emitter for session 51dd1fe4-170b-488a-88a5-8fb082cd5599: The response object has been recycled and is no longer associated with this facade +2025-02-13T14:16:55.622+01:00 INFO 48979 --- [SpringApplicationShutdownHook] o.s.a.m.s.t.WebMvcSseServerTransport : Graceful shutdown completed +2025-02-13T14:16:55.622+01:00 INFO 48979 --- [SpringApplicationShutdownHook] o.s.a.m.s.t.WebMvcSseServerTransport : Graceful shutdown completed diff --git a/model-context-protocol/weather/starter-webmvc-server/.mvn/wrapper/maven-wrapper.properties b/model-context-protocol/weather/starter-webmvc-server/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /dev/null +++ b/model-context-protocol/weather/starter-webmvc-server/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/model-context-protocol/weather/starter-webmvc-server/README.md b/model-context-protocol/weather/starter-webmvc-server/README.md new file mode 100644 index 0000000..11af7b8 --- /dev/null +++ b/model-context-protocol/weather/starter-webmvc-server/README.md @@ -0,0 +1,233 @@ +# Spring AI MCP Weather Server Sample with WebMVC Starter + +This sample project demonstrates how to create an MCP server using the Spring AI MCP Server Boot Starter with WebMVC transport. It implements a weather service that exposes tools for retrieving weather information using the National Weather Service API. + +For more information, see the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) reference documentation. + +## Overview + +The sample showcases: +- Integration with `spring-ai-mcp-server-webmvc-spring-boot-starter` +- Support for both SSE (Server-Sent Events) and STDIO transports +- Automatic tool registration using Spring AI's `@Tool` annotation +- Two weather-related tools: + - Get weather forecast by location (latitude/longitude) + - Get weather alerts by US state + +## Dependencies + +The project requires the Spring AI MCP Server WebMVC Boot Starter: + +```xml + + org.springframework.ai + spring-ai-mcp-server-webmvc-spring-boot-starter + +``` + +This starter provides: +- HTTP-based transport using Spring MVC (`WebMvcSseServerTransport`) +- Auto-configured SSE endpoints +- Optional STDIO transport +- Included `spring-boot-starter-web` and `mcp-spring-webmvc` dependencies + +## Building the Project + +Build the project using Maven: +```bash +./mvnw clean install -DskipTests +``` + +## Running the Server + +The server supports two transport modes: + +### WebMVC SSE Mode (Default) +```bash +java -jar target/mcp-weather-starter-webmvc-server-0.0.1-SNAPSHOT.jar +``` + +### STDIO Mode +To enable STDIO transport, set the appropriate properties: +```bash +java -Dspring.ai.mcp.server.stdio=true -Dspring.main.web-application-type=none -jar target/mcp-weather-starter-webmvc-server-0.0.1-SNAPSHOT.jar +``` + +## Configuration + +Configure the server through `application.properties`: + +```properties +# Server identification +spring.ai.mcp.server.name=my-weather-server +spring.ai.mcp.server.version=0.0.1 + +# Server type (SYNC/ASYNC) +spring.ai.mcp.server.type=SYNC + +# Transport configuration +spring.ai.mcp.server.stdio=false +spring.ai.mcp.server.sse-message-endpoint=/mcp/message + +# Change notifications +spring.ai.mcp.server.resource-change-notification=true +spring.ai.mcp.server.tool-change-notification=true +spring.ai.mcp.server.prompt-change-notification=true + +# Logging (required for STDIO transport) +spring.main.banner-mode=off +logging.file.name=./target/starter-webmvc-server.log +``` + +## Available Tools + +### Weather Forecast Tool +- Name: `getWeatherForecastByLocation` +- Description: Get weather forecast for a specific latitude/longitude +- Parameters: + - `latitude`: double - Latitude coordinate + - `longitude`: double - Longitude coordinate + +### Weather Alerts Tool +- Name: `getAlerts` +- Description: Get weather alerts for a US state +- Parameters: + - `state`: String - Two-letter US state code (e.g., CA, NY) + +## Server Implementation + +The server uses Spring Boot and Spring AI's tool annotations for automatic tool registration: + +```java +@SpringBootApplication +public class McpServerApplication { + public static void main(String[] args) { + SpringApplication.run(McpServerApplication.class, args); + } + + @Bean + public ToolCallbackProvider weatherTools(WeatherService weatherService){ + return MethodToolCallbackProvider.builder().toolObjects(weatherService).build(); + } +} +``` + +The `WeatherService` implements the weather tools using the `@Tool` annotation: + +```java +@Service +public class WeatherService { + @Tool(description = "Get weather forecast for a specific latitude/longitude") + public String getWeatherForecastByLocation(double latitude, double longitude) { + // Implementation using weather.gov API + } + + @Tool(description = "Get weather alerts for a US state. Input is Two-letter US state code (e.g., CA, NY)") + public String getAlerts(String state) { + // Implementation using weather.gov API + } +} +``` + +## MCP Clients + +You can connect to the weather server using either STDIO or SSE transport: + +### Manual Clients + +#### WebMVC SSE Client + +For servers using SSE transport: + +```java +var transport = new HttpClientSseClientTransport("http://localhost:8080"); +var client = McpClient.sync(transport).build(); +``` + +#### STDIO Client + +For servers using STDIO transport: + +```java +var stdioParams = ServerParameters.builder("java") + .args("-Dspring.ai.mcp.server.stdio=true", + "-Dspring.main.web-application-type=none", + "-Dspring.main.banner-mode=off", + "-Dlogging.pattern.console=", + "-jar", + "target/mcp-weather-starter-webmvc-server-0.0.1-SNAPSHOT.jar") + .build(); + +var transport = new StdioClientTransport(stdioParams); +var client = McpClient.sync(transport).build(); +``` + +The sample project includes example client implementations: +- [SampleClient.java](src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java): Manual MCP client implementation +- [ClientStdio.java](src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java): STDIO transport connection +- [ClientSse.java](src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java): SSE transport connection + +For a better development experience, consider using the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html). These starters enable auto-configuration of multiple STDIO and/or SSE connections to MCP servers. See the [starter-default-client](../../client-starter/starter-default-client) project for examples. + +### Boot Starter Clients + +Let's use the [starter-default-client](../../client-starter/starter-default-client) client to connect to our weather `starter-webmvc-server`. + +Follow the `starter-default-client` readme instruction to build a `mcp-starter-default-client-0.0.1-SNAPSHOT.jar` client application. + +#### STDIO Transport + +1. Create a `mcp-servers-config.json` configuration file with this content: + +```json +{ + "mcpServers": { + "weather-starter-webmvc-server": { + "command": "java", + "args": [ + "-Dspring.ai.mcp.server.stdio=true", + "-Dspring.main.web-application-type=none", + "-Dlogging.pattern.console=", + "-jar", + "/absolute/path/to/mcp-weather-starter-webmvc-server-0.0.1-SNAPSHOT.jar" + ] + } + } +} +``` + +2. Run the client using the configuration file: + +```bash +java -Dspring.ai.mcp.client.stdio.servers-configuration=file:mcp-servers-config.json \ + -Dai.user.input='What is the weather in NY?' \ + -Dlogging.pattern.console= \ + -jar mcp-starter-default-client-0.0.1-SNAPSHOT.jar +``` + +#### SSE (WebMVC) Transport + +1. Start the `mcp-weather-starter-webmvc-server`: + +```bash +java -jar mcp-weather-starter-webmvc-server-0.0.1-SNAPSHOT.jar +``` + +starts the MCP server on port 8080. + +2. In another console start the client configured with SSE transport: + +```bash +java -Dspring.ai.mcp.client.sse.connections.weather-server.url=http://localhost:8080 \ + -Dlogging.pattern.console= \ + -Dai.user.input='What is the weather in NY?' \ + -jar mcp-starter-default-client-0.0.1-SNAPSHOT.jar +``` + +## Additional Resources + +* [Spring AI Documentation](https://docs.spring.io/spring-ai/reference/) +* [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) +* [MCP Client Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-client-docs.html) +* [Model Context Protocol Specification](https://modelcontextprotocol.github.io/specification/) +* [Spring Boot Auto-configuration](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration) diff --git a/model-context-protocol/weather/starter-webmvc-server/mvnw b/model-context-protocol/weather/starter-webmvc-server/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/model-context-protocol/weather/starter-webmvc-server/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/model-context-protocol/weather/starter-webmvc-server/mvnw.cmd b/model-context-protocol/weather/starter-webmvc-server/mvnw.cmd new file mode 100644 index 0000000..b150b91 --- /dev/null +++ b/model-context-protocol/weather/starter-webmvc-server/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/model-context-protocol/weather/starter-webmvc-server/pom.xml b/model-context-protocol/weather/starter-webmvc-server/pom.xml new file mode 100644 index 0000000..49ce75e --- /dev/null +++ b/model-context-protocol/weather/starter-webmvc-server/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.6 + + + + com.example + + mcp-weather-starter-webmvc-server + 0.0.1-SNAPSHOT + + Spring AI MCP Weather Sample + Sample Spring Boot application demonstrating MCP client and server usage + + + + + org.springframework.ai + spring-ai-bom + 1.0.0-SNAPSHOT + pom + import + + + + + + + org.springframework.ai + spring-ai-mcp-server-webmvc-spring-boot-starter + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + \ No newline at end of file diff --git a/model-context-protocol/weather/starter-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java b/model-context-protocol/weather/starter-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java new file mode 100644 index 0000000..b150ece --- /dev/null +++ b/model-context-protocol/weather/starter-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java @@ -0,0 +1,34 @@ +package org.springframework.ai.mcp.sample.server; + +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.function.FunctionToolCallback; +import org.springframework.ai.tool.method.MethodToolCallbackProvider; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class McpServerApplication { + + public static void main(String[] args) { + SpringApplication.run(McpServerApplication.class, args); + } + + @Bean + public ToolCallbackProvider weatherTools(WeatherService weatherService) { + return MethodToolCallbackProvider.builder().toolObjects(weatherService).build(); + } + + public record TextInput(String input) { + } + + @Bean + public ToolCallback toUpperCase() { + return FunctionToolCallback.builder("toUpperCase", (TextInput input) -> input.input().toUpperCase()) + .inputType(TextInput.class) + .description("Put the text to upper case") + .build(); + } + +} diff --git a/model-context-protocol/weather/starter-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java b/model-context-protocol/weather/starter-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java new file mode 100644 index 0000000..5966ea1 --- /dev/null +++ b/model-context-protocol/weather/starter-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java @@ -0,0 +1,145 @@ +/* +* Copyright 2024 - 2024 the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* https://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package org.springframework.ai.mcp.sample.server; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientException; + +@Service +public class WeatherService { + + private static final String BASE_URL = "https://api.weather.gov"; + + private final RestClient restClient; + + public WeatherService() { + + this.restClient = RestClient.builder() + .baseUrl(BASE_URL) + .defaultHeader("Accept", "application/geo+json") + .defaultHeader("User-Agent", "WeatherApiClient/1.0 (your@email.com)") + .build(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Points(@JsonProperty("properties") Props properties) { + @JsonIgnoreProperties(ignoreUnknown = true) + public record Props(@JsonProperty("forecast") String forecast) { + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Forecast(@JsonProperty("properties") Props properties) { + @JsonIgnoreProperties(ignoreUnknown = true) + public record Props(@JsonProperty("periods") List periods) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Period(@JsonProperty("number") Integer number, @JsonProperty("name") String name, + @JsonProperty("startTime") String startTime, @JsonProperty("endTime") String endTime, + @JsonProperty("isDaytime") Boolean isDayTime, @JsonProperty("temperature") Integer temperature, + @JsonProperty("temperatureUnit") String temperatureUnit, + @JsonProperty("temperatureTrend") String temperatureTrend, + @JsonProperty("probabilityOfPrecipitation") Map probabilityOfPrecipitation, + @JsonProperty("windSpeed") String windSpeed, @JsonProperty("windDirection") String windDirection, + @JsonProperty("icon") String icon, @JsonProperty("shortForecast") String shortForecast, + @JsonProperty("detailedForecast") String detailedForecast) { + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Alert(@JsonProperty("features") List features) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Feature(@JsonProperty("properties") Properties properties) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Properties(@JsonProperty("event") String event, @JsonProperty("areaDesc") String areaDesc, + @JsonProperty("severity") String severity, @JsonProperty("description") String description, + @JsonProperty("instruction") String instruction) { + } + } + + /** + * Get forecast for a specific latitude/longitude + * @param latitude Latitude + * @param longitude Longitude + * @return The forecast for the given location + * @throws RestClientException if the request fails + */ + @Tool(description = "Get weather forecast for a specific latitude/longitude") + public String getWeatherForecastByLocation(double latitude, double longitude) { + + var points = restClient.get() + .uri("/points/{latitude},{longitude}", latitude, longitude) + .retrieve() + .body(Points.class); + + var forecast = restClient.get().uri(points.properties().forecast()).retrieve().body(Forecast.class); + + String forecastText = forecast.properties().periods().stream().map(p -> { + return String.format(""" + %s: + Temperature: %s %s + Wind: %s %s + Forecast: %s + """, p.name(), p.temperature(), p.temperatureUnit(), p.windSpeed(), p.windDirection(), + p.detailedForecast()); + }).collect(Collectors.joining()); + + return forecastText; + } + + /** + * Get alerts for a specific area + * @param state Area code. Two-letter US state code (e.g. CA, NY) + * @return Human readable alert information + * @throws RestClientException if the request fails + */ + @Tool(description = "Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY)") + public String getAlerts(String state) { + Alert alert = restClient.get().uri("/alerts/active/area/{state}", state).retrieve().body(Alert.class); + + return alert.features() + .stream() + .map(f -> String.format(""" + Event: %s + Area: %s + Severity: %s + Description: %s + Instructions: %s + """, f.properties().event(), f.properties.areaDesc(), f.properties.severity(), + f.properties.description(), f.properties.instruction())) + .collect(Collectors.joining("\n")); + } + + public static void main(String[] args) { + WeatherService client = new WeatherService(); + System.out.println(client.getWeatherForecastByLocation(47.6062, -122.3321)); + System.out.println(client.getAlerts("NY")); + } + +} \ No newline at end of file diff --git a/model-context-protocol/weather/starter-webmvc-server/src/main/resources/application.properties b/model-context-protocol/weather/starter-webmvc-server/src/main/resources/application.properties new file mode 100644 index 0000000..0988617 --- /dev/null +++ b/model-context-protocol/weather/starter-webmvc-server/src/main/resources/application.properties @@ -0,0 +1,13 @@ +# spring.main.web-application-type=none + +# NOTE: You must disable the banner and the console logging +# to allow the STDIO transport to work !!! +spring.main.banner-mode=off +# logging.pattern.console= + +spring.ai.mcp.server.stdio=false + +spring.ai.mcp.server.name=my-weather-server +spring.ai.mcp.server.version=0.0.1 + +logging.file.name=./model-context-protocol/weather/starter-webmvc-server/target/starter-webmvc-server.log diff --git a/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java b/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java new file mode 100644 index 0000000..3846cc5 --- /dev/null +++ b/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java @@ -0,0 +1,31 @@ +/* +* Copyright 2024 - 2024 the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* https://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package org.springframework.ai.mcp.sample.client; + +import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; + + +/** + * @author Christian Tzolov + */ +public class ClientSse { + + public static void main(String[] args) { + var transport = new HttpClientSseClientTransport("http://localhost:8080"); + new SampleClient(transport).run(); + } + +} diff --git a/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java b/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java new file mode 100644 index 0000000..f820715 --- /dev/null +++ b/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java @@ -0,0 +1,49 @@ +/* +* Copyright 2024 - 2024 the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* https://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package org.springframework.ai.mcp.sample.client; + +import java.io.File; + +import io.modelcontextprotocol.client.transport.ServerParameters; +import io.modelcontextprotocol.client.transport.StdioClientTransport; + +/** + * With stdio transport, the MCP server is automatically started by the client. + * But you + * have to build the server jar first: + * + *
+ * ./mvnw clean install -DskipTests
+ * 
+ */ +public class ClientStdio { + + public static void main(String[] args) { + + System.out.println(new File(".").getAbsolutePath()); + + var stdioParams = ServerParameters.builder("java") + .args("-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none", + "-Dlogging.pattern.console=", "-jar", + "model-context-protocol/weather/starter-webmvc-server/target/mcp-weather-starter-webmvc-server-0.0.1-SNAPSHOT.jar") + .build(); + + var transport = new StdioClientTransport(stdioParams); + + new SampleClient(transport).run(); + } + +} diff --git a/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java b/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java new file mode 100644 index 0000000..518658d --- /dev/null +++ b/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java @@ -0,0 +1,61 @@ +/* +* Copyright 2024 - 2024 the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* https://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package org.springframework.ai.mcp.sample.client; + +import java.util.Map; + +import io.modelcontextprotocol.client.McpClient; +import io.modelcontextprotocol.spec.ClientMcpTransport; +import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; +import io.modelcontextprotocol.spec.McpSchema.CallToolResult; +import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; + +/** + * @author Christian Tzolov + */ + +public class SampleClient { + + private final ClientMcpTransport transport; + + public SampleClient(ClientMcpTransport transport) { + this.transport = transport; + } + + public void run() { + + var client = McpClient.sync(this.transport).build(); + + client.initialize(); + + client.ping(); + + // List and demonstrate tools + ListToolsResult toolsList = client.listTools(); + System.out.println("Available Tools = " + toolsList); + + CallToolResult weatherForcastResult = client.callTool(new CallToolRequest("getWeatherForecastByLocation", + Map.of("latitude", "47.6062", "longitude", "-122.3321"))); + System.out.println("Weather Forcast: " + weatherForcastResult); + + CallToolResult alertResult = client.callTool(new CallToolRequest("getAlerts", Map.of("state", "NY"))); + System.out.println("Alert Response = " + alertResult); + + client.closeGracefully(); + + } + +} diff --git a/pom.xml b/pom.xml index a240e8d..99a57c4 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,7 @@ model-context-protocol/weather/manual-webflux-server model-context-protocol/weather/starter-stdio-server model-context-protocol/weather/starter-webflux-server + model-context-protocol/weather/starter-webmvc-server model-context-protocol/web-search/brave-starter model-context-protocol/web-search/brave-chatbot