diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java index fd97db1eba4..6668e8afbcb 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java @@ -31,10 +31,12 @@ import io.modelcontextprotocol.server.McpServerFeatures.AsyncCompletionSpecification; import io.modelcontextprotocol.server.McpServerFeatures.AsyncPromptSpecification; import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceSpecification; +import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceTemplateSpecification; import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification; import io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification; import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification; import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification; +import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceTemplateSpecification; import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.McpSyncServerExchange; @@ -111,6 +113,7 @@ public McpSyncServer mcpSyncServer(McpServerTransportProviderBase transportProvi McpServerChangeNotificationProperties changeNotificationProperties, ObjectProvider> tools, ObjectProvider> resources, + ObjectProvider> resourceTemplates, ObjectProvider> prompts, ObjectProvider> completions, ObjectProvider>> rootsChangeConsumers, @@ -157,6 +160,21 @@ public McpSyncServer mcpSyncServer(McpServerTransportProviderBase transportProvi } } + // Resources Templates + if (serverProperties.getCapabilities().isResource()) { + logger.info("Enable resources templates capabilities, notification: " + + changeNotificationProperties.isResourceChangeNotification()); + capabilitiesBuilder.resources(false, changeNotificationProperties.isResourceChangeNotification()); + + List resourceTemplateSpecifications = resourceTemplates.stream() + .flatMap(List::stream) + .toList(); + if (!CollectionUtils.isEmpty(resourceTemplateSpecifications)) { + serverBuilder.resourceTemplates(resourceTemplateSpecifications); + logger.info("Registered resource templates: " + resourceTemplateSpecifications.size()); + } + } + // Prompts if (serverProperties.getCapabilities().isPrompt()) { logger.info("Enable prompts capabilities, notification: " @@ -210,6 +228,7 @@ public McpAsyncServer mcpAsyncServer(McpServerTransportProviderBase transportPro McpServerChangeNotificationProperties changeNotificationProperties, ObjectProvider> tools, ObjectProvider> resources, + ObjectProvider> resourceTemplates, ObjectProvider> prompts, ObjectProvider> completions, ObjectProvider>> rootsChangeConsumer) { @@ -255,6 +274,21 @@ public McpAsyncServer mcpAsyncServer(McpServerTransportProviderBase transportPro } } + // Resources Templates + if (serverProperties.getCapabilities().isResource()) { + logger.info("Enable resources templates capabilities, notification: " + + changeNotificationProperties.isResourceChangeNotification()); + capabilitiesBuilder.resources(false, changeNotificationProperties.isResourceChangeNotification()); + + List resourceTemplateSpecifications = resourceTemplates.stream() + .flatMap(List::stream) + .toList(); + if (!CollectionUtils.isEmpty(resourceTemplateSpecifications)) { + serverBuilder.resourceTemplates(resourceTemplateSpecifications); + logger.info("Registered resources templates: " + resourceTemplateSpecifications.size()); + } + } + // Prompts if (serverProperties.getCapabilities().isPrompt()) { logger.info("Enable prompts capabilities, notification: " diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerStatelessAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerStatelessAutoConfiguration.java index 77ed652822f..09a426c8c6a 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerStatelessAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerStatelessAutoConfiguration.java @@ -26,10 +26,12 @@ import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncCompletionSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncPromptSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncResourceSpecification; +import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncResourceTemplateSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncToolSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification; +import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceTemplateSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification; import io.modelcontextprotocol.server.McpStatelessSyncServer; import io.modelcontextprotocol.spec.McpSchema; @@ -80,6 +82,7 @@ public McpStatelessSyncServer mcpStatelessSyncServer(McpStatelessServerTransport McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties, ObjectProvider> tools, ObjectProvider> resources, + ObjectProvider> resourceTemplates, ObjectProvider> prompts, ObjectProvider> completions, Environment environment) { @@ -113,6 +116,19 @@ public McpStatelessSyncServer mcpStatelessSyncServer(McpStatelessServerTransport } } + // Resources Templates + if (serverProperties.getCapabilities().isResource()) { + capabilitiesBuilder.resources(false, false); + + List resourceSpecifications = resourceTemplates.stream() + .flatMap(List::stream) + .toList(); + if (!CollectionUtils.isEmpty(resourceSpecifications)) { + serverBuilder.resourceTemplates(resourceSpecifications); + logger.info("Registered resource templates: " + resourceSpecifications.size()); + } + } + // Prompts if (serverProperties.getCapabilities().isPrompt()) { capabilitiesBuilder.prompts(false); @@ -156,6 +172,7 @@ public McpStatelessAsyncServer mcpStatelessAsyncServer(McpStatelessServerTranspo McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties, ObjectProvider> tools, ObjectProvider> resources, + ObjectProvider> resourceTemplates, ObjectProvider> prompts, ObjectProvider> completions) { @@ -189,6 +206,19 @@ public McpStatelessAsyncServer mcpStatelessAsyncServer(McpStatelessServerTranspo } } + // Resources Templates + if (serverProperties.getCapabilities().isResource()) { + capabilitiesBuilder.resources(false, false); + + List resourceSpecifications = resourceTemplates.stream() + .flatMap(List::stream) + .toList(); + if (!CollectionUtils.isEmpty(resourceSpecifications)) { + serverBuilder.resourceTemplates(resourceSpecifications); + logger.info("Registered resource templates: " + resourceSpecifications.size()); + } + } + // Prompts if (serverProperties.getCapabilities().isPrompt()) { capabilitiesBuilder.prompts(false); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/annotations/McpServerSpecificationFactoryAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/annotations/McpServerSpecificationFactoryAutoConfiguration.java index 67bbcf65a69..0d90786bfea 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/annotations/McpServerSpecificationFactoryAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/annotations/McpServerSpecificationFactoryAutoConfiguration.java @@ -60,6 +60,15 @@ public List resourceSpecs( return syncResourceSpecifications; } + @Bean + public List resourceTemplateSpecs( + ServerMcpAnnotatedBeans beansWithMcpMethodAnnotations) { + + List syncResourceSpecifications = SyncMcpAnnotationProviders + .resourceTemplateSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpResource.class)); + return syncResourceSpecifications; + } + @Bean public List promptSpecs( ServerMcpAnnotatedBeans beansWithMcpMethodAnnotations) { @@ -96,6 +105,14 @@ public List resourceSpecs( .resourceSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpResource.class)); } + @Bean + public List resourceTemplateSpecs( + ServerMcpAnnotatedBeans beansWithMcpMethodAnnotations) { + + return AsyncMcpAnnotationProviders + .resourceTemplateSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpResource.class)); + } + @Bean public List promptSpecs( ServerMcpAnnotatedBeans beansWithMcpMethodAnnotations) { diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/annotations/StatelessServerSpecificationFactoryAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/annotations/StatelessServerSpecificationFactoryAutoConfiguration.java index 7a04b72186c..97d01f82280 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/annotations/StatelessServerSpecificationFactoryAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/annotations/StatelessServerSpecificationFactoryAutoConfiguration.java @@ -60,6 +60,13 @@ public List resourceSpecs( .statelessResourceSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpResource.class)); } + @Bean + public List resourceTemplateSpecs( + ServerMcpAnnotatedBeans beansWithMcpMethodAnnotations) { + return SyncMcpAnnotationProviders.statelessResourceTemplateSpecifications( + beansWithMcpMethodAnnotations.getBeansByAnnotation(McpResource.class)); + } + @Bean public List promptSpecs( ServerMcpAnnotatedBeans beansWithMcpMethodAnnotations) { @@ -96,6 +103,13 @@ public List resourceSpecs .statelessResourceSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpResource.class)); } + @Bean + public List resourceTemplateSpecs( + ServerMcpAnnotatedBeans beansWithMcpMethodAnnotations) { + return AsyncMcpAnnotationProviders.statelessResourceTemplateSpecifications( + beansWithMcpMethodAnnotations.getBeansByAnnotation(McpResource.class)); + } + @Bean public List promptSpecs( ServerMcpAnnotatedBeans beansWithMcpMethodAnnotations) { diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfigurationIT.java index dc74adebb56..e0eaf09eab9 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfigurationIT.java @@ -31,6 +31,7 @@ import io.modelcontextprotocol.server.McpServerFeatures.AsyncCompletionSpecification; import io.modelcontextprotocol.server.McpServerFeatures.AsyncPromptSpecification; import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceSpecification; +import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceTemplateSpecification; import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification; import io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification; import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification; @@ -379,7 +380,12 @@ void syncServerSpecificationConfiguration() { ConcurrentHashMap resources = (ConcurrentHashMap) ReflectionTestUtils .getField(asyncServer, "resources"); assertThat(resources).hasSize(1); - assertThat(resources.get("config://{key}")).isNotNull(); + assertThat(resources.get("simple://static")).isNotNull(); + + ConcurrentHashMap resourceTemplatess = (ConcurrentHashMap) ReflectionTestUtils + .getField(asyncServer, "resourceTemplates"); + assertThat(resourceTemplatess).hasSize(1); + assertThat(resourceTemplatess.get("config://{key}")).isNotNull(); ConcurrentHashMap prompts = (ConcurrentHashMap) ReflectionTestUtils .getField(asyncServer, "prompts"); @@ -412,7 +418,12 @@ void asyncServerSpecificationConfiguration() { ConcurrentHashMap resources = (ConcurrentHashMap) ReflectionTestUtils .getField(asyncServer, "resources"); assertThat(resources).hasSize(1); - assertThat(resources.get("config://{key}")).isNotNull(); + assertThat(resources.get("simple://static")).isNotNull(); + + ConcurrentHashMap resourceTemplatess = (ConcurrentHashMap) ReflectionTestUtils + .getField(asyncServer, "resourceTemplates"); + assertThat(resourceTemplatess).hasSize(1); + assertThat(resourceTemplatess.get("config://{key}")).isNotNull(); ConcurrentHashMap prompts = (ConcurrentHashMap) ReflectionTestUtils .getField(asyncServer, "prompts"); @@ -608,6 +619,11 @@ public int add(@McpToolParam(description = "First number", required = true) int return a + b; } + @McpResource(uri = "simple://static", name = "Configuration", description = "Provides configuration data") + public String getSimple() { + return "Hi there!"; + } + @McpResource(uri = "config://{key}", name = "Configuration", description = "Provides configuration data") public String getConfig(String key) { return "config value"; @@ -644,6 +660,11 @@ public Mono add(@McpToolParam(description = "First number", required = return Mono.just(a + b); } + @McpResource(uri = "simple://static", name = "Configuration", description = "Provides configuration data") + public Mono getSimple() { + return Mono.just("Hi there!"); + } + @McpResource(uri = "config://{key}", name = "Configuration", description = "Provides configuration data") public Mono getConfig(String key) { return Mono.just("config value"); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpStatelessServerAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpStatelessServerAutoConfigurationIT.java index bc781602b96..1d57a251e1f 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpStatelessServerAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpStatelessServerAutoConfigurationIT.java @@ -30,6 +30,7 @@ import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncCompletionSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncPromptSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncResourceSpecification; +import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncResourceTemplateSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncToolSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification; @@ -330,7 +331,12 @@ void syncStatelessServerSpecificationConfiguration() { ConcurrentHashMap resources = (ConcurrentHashMap) ReflectionTestUtils .getField(asyncServer, "resources"); assertThat(resources).hasSize(1); - assertThat(resources.get("config://{key}")).isNotNull(); + assertThat(resources.get("simple://static")).isNotNull(); + + ConcurrentHashMap resourceTemplates = (ConcurrentHashMap) ReflectionTestUtils + .getField(asyncServer, "resourceTemplates"); + assertThat(resourceTemplates).hasSize(1); + assertThat(resourceTemplates.get("config://{key}")).isNotNull(); ConcurrentHashMap prompts = (ConcurrentHashMap) ReflectionTestUtils .getField(asyncServer, "prompts"); @@ -363,7 +369,12 @@ void asyncStatelessServerSpecificationConfiguration() { ConcurrentHashMap resources = (ConcurrentHashMap) ReflectionTestUtils .getField(asyncServer, "resources"); assertThat(resources).hasSize(1); - assertThat(resources.get("config://{key}")).isNotNull(); + assertThat(resources.get("simple://static")).isNotNull(); + + ConcurrentHashMap resourceTemplates = (ConcurrentHashMap) ReflectionTestUtils + .getField(asyncServer, "resourceTemplates"); + assertThat(resourceTemplates).hasSize(1); + assertThat(resourceTemplates.get("config://{key}")).isNotNull(); ConcurrentHashMap prompts = (ConcurrentHashMap) ReflectionTestUtils .getField(asyncServer, "prompts"); @@ -530,6 +541,11 @@ public int add(@McpToolParam(description = "First number", required = true) int return a + b; } + @McpResource(uri = "simple://static", name = "Configuration", description = "Provides configuration data") + public String getSimple() { + return "Hi there!"; + } + @McpResource(uri = "config://{key}", name = "Configuration", description = "Provides configuration data") public String getConfig(String key) { return "config value"; @@ -566,6 +582,11 @@ public Mono add(@McpToolParam(description = "First number", required = return Mono.just(a + b); } + @McpResource(uri = "simple://static", name = "Configuration", description = "Provides configuration data") + public Mono getSimple() { + return Mono.just("Hi there!"); + } + @McpResource(uri = "config://{key}", name = "Configuration", description = "Provides configuration data") public Mono getConfig(String key) { return Mono.just("config value"); diff --git a/mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/AsyncMcpAnnotationProviders.java b/mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/AsyncMcpAnnotationProviders.java index 4202005304a..9aff9b0d4a4 100644 --- a/mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/AsyncMcpAnnotationProviders.java +++ b/mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/AsyncMcpAnnotationProviders.java @@ -22,6 +22,7 @@ import io.modelcontextprotocol.server.McpServerFeatures.AsyncCompletionSpecification; import io.modelcontextprotocol.server.McpServerFeatures.AsyncPromptSpecification; import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceSpecification; +import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceTemplateSpecification; import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures; import org.springaicommunity.mcp.method.changed.prompt.AsyncPromptListChangedSpecification; @@ -119,6 +120,17 @@ public static List statel return new SpringAiAsyncStatelessResourceProvider(resourceObjects).getResourceSpecifications(); } + // RESOURCE TEMPLATE + public static List resourceTemplateSpecifications( + List resourceObjects) { + return new SpringAiAsyncResourceProvider(resourceObjects).getResourceTemplateSpecifications(); + } + + public static List statelessResourceTemplateSpecifications( + List resourceObjects) { + return new SpringAiAsyncStatelessResourceProvider(resourceObjects).getResourceTemplateSpecifications(); + } + // RESOURCE LIST CHANGED public static List resourceListChangedSpecifications( List resourceListChangedObjects) { diff --git a/mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/SyncMcpAnnotationProviders.java b/mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/SyncMcpAnnotationProviders.java index 1c1f2852b25..c5f74bd5a3f 100644 --- a/mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/SyncMcpAnnotationProviders.java +++ b/mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/SyncMcpAnnotationProviders.java @@ -22,6 +22,7 @@ import io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification; import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification; import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification; +import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceTemplateSpecification; import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures; import org.springaicommunity.mcp.method.changed.prompt.SyncPromptListChangedSpecification; @@ -99,6 +100,16 @@ public static List statele return new SpringAiSyncStatelessResourceProvider(resourceObjects).getResourceSpecifications(); } + // RESOURCE TEMPLATE + public static List resourceTemplateSpecifications(List resourceObjects) { + return new SpringAiSyncMcpResourceProvider(resourceObjects).getResourceTemplateSpecifications(); + } + + public static List statelessResourceTemplateSpecifications( + List resourceObjects) { + return new SpringAiSyncStatelessResourceProvider(resourceObjects).getResourceTemplateSpecifications(); + } + // LOGGING (CLIENT) public static List loggingSpecifications(List loggingObjects) { return new SpringAiSyncMcpLoggingProvider(loggingObjects).getLoggingSpecifications(); diff --git a/pom.xml b/pom.xml index f019575f230..83995702cc3 100644 --- a/pom.xml +++ b/pom.xml @@ -330,8 +330,8 @@ 4.1.0 - 0.13.1 - 0.4.1 + 0.14.0-SNAPSHOT + 0.5.0-SNAPSHOT 4.13.1