Class 14: When DIY Pays Off
Duration: 15 minutes | Difficulty: Intermediate | Prerequisites: Class 13.
This lesson carries on from Class 13. If you followed along, keep working on the project you already have. If you skipped it, clone the mcp_class_13 branch to start from the same place:
git clone --branch mcp_class_13 https://github.com/graphqlguy/graphQLMovieDB-MCP.git
By the end of this lesson we will have a checklist for choosing between the Spring AI starter (Track B) and a server you write yourself (Track C). Class 13 showed that building an MCP server yourself is quite manageable. This lesson asks when that effort is worth it, and when it only produces a weaker copy of the starter. For most teams it is rarely worth it, and this lesson names the cases where it is.
The Default Choice
For most teams, the Spring AI starter is the right MCP server. It handles capability negotiation, schema generation, content shaping, progress notifications, elicitation and sampling. It also works with Spring Security's filter chain and the standard MVC metrics. If you write your own, you rebuild each of these pieces when you find you need it. The result is usually a rougher copy of what the starter already gives you.
Reading the DIY path in Class 13 is still useful. You now understand the protocol, so you can debug the starter's behavior with real understanding. You keep that benefit whether you ship the DIY code or leave it as an exercise. Track B remains the right default for almost all production work.
The rest of this lesson covers five situations where DIY is worth it, each with its own pattern.
Non-Standard Transports
The starter ships streamable HTTP (stateful or stateless), the older SSE transport (deprecated in Spring AI 2.0), and stdio. Most teams need one of those. Some teams have transport requirements that the three cannot meet.
Three examples:
- service mesh deployments where MCP traffic must go over an internal gRPC layer or a proprietary message bus
- embedded environments where MCP runs over an IPC channel inside a desktop application
- internal RPC layers that are older than the organization's HTTP-based stack and use binary framing
In each case the starter's HTTP transports do not fit. Adapting them takes about as much work as writing the JSON-RPC handlers yourself.
The DIY route here is simple. Take the controller from Class 13 and replace the @RestController with your transport: a gRPC service, a message-queue listener or a Unix domain socket handler. Keep the dispatch logic and the tool registry. The MCP messages have the same shape on every transport, and only the framing changes.
Specialized Auth That Does Not Fit Spring Security
The starter's endpoint is a Spring MVC controller, so it works with Spring Security's filter chain. That covers almost every auth setup: JWT, session cookies, an OAuth 2.1 resource server, mTLS with X509 principals, and most other setups. Class 15 shows the resource-server pattern.
Some auth models do not fit Spring Security well:
- token-based auth that verifies tokens through a custom protocol
- broker-based auth that validates by asking a separate service, which is not an OAuth authorization server
- per-tenant auth that builds the security context from the MCP message contents, not from HTTP headers
The starter's hooks cover most of these, through security filters, SecurityContextHolder and @PreAuthorize. When the validation logic really sits outside Spring Security, write your own controller and add auth at the JSON-RPC layer. That is simpler than working against the framework.
The pattern is to do auth in the controller's handle(...) method, fill in whatever context your tools need, and dispatch the method as usual. Mark the endpoint as anonymous in Spring Security, so the framework leaves the auth to your controller.
Custom Tool Registry Sources
The starter's default tool registry is annotation-driven: scan @McpTool methods, register them at startup, and keep the same set of tools for the application's lifetime. Most servers want exactly this, but a few need the tool set to change while the application runs.
Three examples:
- tools defined by database rows, where each row is a tool with its own schema and execution callback
- tools defined at runtime by configuration, so adding a tool needs only a configuration change
- per-tenant registries, so two clients connected to the same server see different tool sets
The starter has hooks for some of this: you can contribute several ToolCallbackProvider beans, one per source. The DIY path's Map<String, Tool> can change while the application runs, which suits a fully dynamic registry.
Class 13's ToolRegistry already has a public register(Tool) method. Each of these is about a five-line addition to the DIY path:
- a scheduled job that polls a configuration source and updates the registry
- a per-request filter that limits which tools are visible
- an admin endpoint that adds and removes tools while the server runs
The starter can add and remove tools at runtime too. The MCP Java SDK has addTool(...) and removeTool(...) on the running McpSyncServer, and you can inject that bean to change the tool set live.
Where the DIY path helps is the registry itself. Per-request visibility and per-tenant tool sets need the server to consult the registry on every tools/list. A plain Map<String, Tool> that you own makes that easy. A registry that the starter builds by scanning beans at startup makes it harder.
Embedded MCP in Non-Boot Applications
The starter needs Spring Boot. Most teams already use it. For some applications, adding Spring Boot only for an MCP layer costs more than it gives.
Three examples:
- plain Java applications, such as a CLI tool, a desktop application or a build-time agent, that want to expose MCP without a full Spring container
- library-mode MCP, where MCP is one feature of an application whose main loop does something else
- agent frameworks that embed an MCP server in their own runtime and want to keep Spring Boot out of their users' dependency graph
In these cases the starter does not fit. The MCP Java SDK underneath the starter is plain Java and runs without Spring. Use the SDK directly, or take Class 13's controller logic, remove the @RestController annotations, and connect the resulting handler class to your application's runtime.
Full Protocol-Level Control
The starter decides how you use MCP features:
- tool descriptions come from annotations or
ToolCallbackProviders - progress notifications go through the request context (
McpSyncRequestContexton our sync server) - sampling and elicitation go through the same context object
These are good decisions, and they make the common case easy to write.
A few cases need full control:
- tools whose schemas need raw JSON construction, because JSON Schema generated from method signatures cannot express them
- tools that batch several MCP responses into one for transport efficiency
- tools that mix server-initiated requests in patterns the framework does not expect
These cases are rare, and most turn out to be unnecessary. When one is real, the DIY path lets you build it without working around the framework's abstractions.
The Real Cost of DIY
Each of those situations is a real reason to consider DIY, and each has a cost. The work the starter did for you moves into your own codebase.
What you take on:
- Schema generation. Rebuild it, or write the schemas by hand and maintain them.
- Progress, elicitation and sampling. Rebuild them, or go without them.
- Auth and metrics. Spring Security and the standard MVC metrics keep working only while your transport is a Spring MVC controller. With a gRPC service or a plain handler class, you rebuild the auth integration and the observability yourself.
- Tests at every level. The four levels from Class 12 still apply, now to code you wrote.
The starter stays small because it makes these choices for you. When you write your own server, you make every one of those choices yourself, and most teams do not have time to revisit every decision the starter already made.
A Decision Checklist
If you answer "no" to all five questions below, use the starter.
- Does your team have a non-standard transport requirement that streamable HTTP, SSE, and stdio do not satisfy?
- Does your auth model sit completely outside Spring Security? An unusual setup that still works within Spring Security counts as "no".
- Is your tool registry dynamic in a way that does not fit the annotation-driven model?
- Does your application run without Spring Boot?
- Do you need protocol-level control that the starter's abstractions block?
One "yes" makes DIY worth considering. Two make DIY probably right. Three or more make DIY almost certainly right, because the starter would work against you at every layer.
Summary
DIY is worth it in five situations:
- transports the starter does not ship
- auth models that sit outside Spring Security
- tool registries that change at runtime
- applications without Spring Boot
- the rare need for protocol-level control that the starter's abstractions block
One "yes" on the checklist makes DIY worth considering. If every answer is "no", use the starter, and treat the DIY lesson as the reason you understand what the starter does for you.
Further Reading
- Spring AI: MCP Server Boot Starter customization - the customization hooks you would use before reaching for DIY.
- MCP Java SDK - the framework-agnostic SDK Spring AI is built on, and one of the two options this chapter suggests for non-Boot applications.
- Apollo MCP Server: when to use it - Apollo's framing of the use cases that justify a separate gateway, useful contrast against DIY.
What's Next
With Track C done, we have seen all three tracks: Apollo MCP Server in Track A, the Spring AI starter in Track B and DIY in Track C. You can now choose between them on evidence. Class 15 starts the shared final lessons, which apply whichever track you picked. Classes 15 to 17 cover production:
- OAuth 2.1 resource server configuration
- agent-specific security threats and how to reduce them
- observability and deployment
Classes 18 and 19 then connect real clients to the server.