mcp-server — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited mcp-server (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Official Java SDK: https://github.com/modelcontextprotocol/java-sdk Maintained by Anthropic in collaboration with Spring AI.
The standalone SDK reached 1.0.0 GA (io.modelcontextprotocol.sdk:mcp). Most Spring Boot apps should use the Spring AI MCP starter instead — it auto-configures the server, transport, and tool scanning. The starter coordinates were renamed at Spring AI 1.0 GA to spring-ai-starter-mcp-server-*.
<!-- Recommended for Spring Boot: pick ONE transport starter -->
<!-- stdio (Claude Desktop / Claude Code launching the jar locally) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
<!-- OR remote HTTP (SSE + Streamable-HTTP) over Spring MVC -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<!-- OR reactive: spring-ai-starter-mcp-server-webflux --><!-- Or drive the raw SDK directly (no Spring AI), now at 1.0.0 GA -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>1.0.0</version>
</dependency>The oldspring-ai-mcp-server-spring-boot-startername is dead. GA isspring-ai-starter-mcp-server(stdio),-webmvc(servlet SSE / Streamable-HTTP), and-webflux(reactive).
@SpringBootApplication
public class OrderMcpServer {
public static void main(String[] args) {
var transport = new StdioServerTransportProvider();
var server = McpServer.sync(transport)
.serverInfo("order-service-mcp", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).resources(true).build())
.tools(getOrderTool(), listOrdersTool())
.build();
Runtime.getRuntime().addShutdownHook(new Thread(server::close));
}
}// Tool with typed input/output
private static McpServerFeatures.SyncToolSpecification getOrderTool() {
var schema = """
{
"type": "object",
"properties": {
"orderId": { "type": "string", "description": "UUID of the order" }
},
"required": ["orderId"]
}
""";
return McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("get_order")
.description("Get a single order by ID including all line items and status history")
.inputSchema(schema)
.build())
.callHandler((exchange, args) -> {
String orderId = (String) args.get("orderId");
try {
Order order = orderService.findById(UUID.fromString(orderId));
return new CallToolResult(List.of(
new TextContent(objectMapper.writeValueAsString(order))
), false);
} catch (EntityNotFoundException e) {
return new CallToolResult(List.of(
new TextContent("Order not found: " + orderId)
), true); // isError = true
}
})
.build();
}@Configuration
public class McpToolsConfig {
@Bean
public ToolCallbackProvider orderTools(OrderService orderService, ObjectMapper objectMapper) {
return MethodToolCallbackProvider.builder()
.toolObjects(new OrderMcpTools(orderService, objectMapper))
.build();
}
}
@Component
public class OrderMcpTools {
private final OrderService orderService;
private final ObjectMapper objectMapper;
// Spring AI annotation-based tool registration
@Tool(description = "Get order by ID with full line items and status history")
public String getOrder(@ToolParam(description = "UUID of the order") String orderId) {
try {
Order order = orderService.findById(UUID.fromString(orderId));
return objectMapper.writeValueAsString(OrderResponse.from(order));
} catch (Exception e) {
return "Error: " + e.getMessage();
}
}
@Tool(description = "List orders for a customer, optionally filtered by status")
public String listOrders(
@ToolParam(description = "Customer email address") String email,
@ToolParam(description = "Filter by status: PENDING, PROCESSING, SHIPPED, DELIVERED", required = false) String status
) {
List<Order> orders = status != null
? orderService.findByEmailAndStatus(email, OrderStatus.valueOf(status))
: orderService.findByEmail(email);
return objectMapper.writeValueAsString(orders.stream().map(OrderResponse::from).toList());
}
}spring:
ai:
mcp:
server:
name: order-service-mcp
version: 1.0.0
type: SYNC # SYNC (blocking) or ASYNC (reactive / WebFlux)
# --- stdio: needs spring-ai-starter-mcp-server + banner/console logging OFF ---
stdio: true # framing is over stdin/stdout — nothing else may write there
# --- remote: needs the -webmvc or -webflux starter instead ---
# protocol: STREAMABLE # SSE | STREAMABLE | STATELESS (Streamable-HTTP is the 2025-06-18 default)stdio servers must keep stdout clean. Any log line, banner, orSystem.out.printlncorrupts the JSON-RPC framing and the client silently drops the connection. For stdio, setspring.main.banner-mode=offand route logging to a file or stderr.
@Bean
public List<McpServerFeatures.SyncResourceSpecification> mcpResources(OrderRepository repo) {
return List.of(
McpServerFeatures.SyncResourceSpecification.builder()
.resource(Resource.builder()
.uri("orders://recent")
.name("Recent Orders")
.description("Last 50 orders across all customers")
.mimeType("application/json")
.build())
.readHandler((exchange, request) -> {
List<Order> recent = repo.findTop50ByOrderByCreatedAtDesc();
return new ReadResourceResult(List.of(
new TextResourceContents(request.uri(),
objectMapper.writeValueAsString(recent), "application/json")
));
})
.build()
);
}{
"mcpServers": {
"order-service": {
"command": "java",
"args": ["-jar", "/path/to/order-mcp-server.jar"],
"env": {
"SPRING_DATASOURCE_URL": "jdbc:postgresql://localhost:5432/orders"
}
}
}
}// Always return structured errors — never throw from tool handlers
private CallToolResult safeExecute(Supplier<Object> action) {
try {
return new CallToolResult(
List.of(new TextContent(objectMapper.writeValueAsString(action.get()))),
false
);
} catch (EntityNotFoundException e) {
return errorResult("NOT_FOUND", e.getMessage());
} catch (Exception e) {
log.error("Tool execution failed", e);
return errorResult("INTERNAL_ERROR", "Unexpected error occurred");
}
}
private CallToolResult errorResult(String code, String message) {
return new CallToolResult(
List.of(new TextContent(String.format("{\"error\":\"%s\",\"message\":\"%s\"}", code, message))),
true // isError flag — agent knows this is an error
);
}spring-ai-mcp-server-spring-boot-starter name — GA is spring-ai-starter-mcp-server[-webmvc|-webflux]0.9.0 — the standalone SDK is 1.0.0 GA (or just use the Spring AI starter)isError = true in error results — agent can't distinguish errors from dataFetchType.EAGER inside tool handlers — triggers N+1, use projectionsshutdown hooks — always close the server on JVM shutdownstdio for local tools (Claude Code, Claude Desktop); -webmvc/-webflux + Streamable-HTTP for remote~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.