Class 17: Into Claude Desktop
Duration: ~25 minutes | Level: Intermediate | Prerequisites: Class 16: Production, and Claude Desktop installed.
What We'll Cover
- Why Claude Desktop cannot reach the HTTP server on
localhost spring.ai.mcp.server.stdio, and the same tools over a different transport- The one thing that breaks a stdio server, and how to stop it
- Registering it in
claude_desktop_config.json - What deploying it publicly would need instead
Why the HTTP Server Will Not Do
order-service publishes /mcp on port 8080. Claude Desktop connects to MCP servers. It seems like it should just work, and it does not.
Claude Desktop reaches a server in two ways, and neither fits.
claude_desktop_config.json describes servers it launches itself, as a command with arguments. That is stdio: Claude Desktop starts the process and talks to it over standard input and output. There is no place in that file for a URL.
Connectors take a URL, but that connection is opened from Anthropic's servers rather than from your machine. http://localhost:8080 means localhost there, which is not our service.
So a server running on a developer's machine gets in through stdio. That is the same transport Class 9 used for the filesystem server, seen from the other end: this time we are the child process.
The Same Service, Over Stdio
Spring AI does not need a different application. It needs a different profile.
Create order-service/src/main/resources/application-stdio.yaml:
spring:
ai:
mcp:
server:
stdio: true
name: order-service
version: 1.0.0
main:
web-application-type: none
banner-mode: "off"
logging:
threshold:
console: OFF
file:
name: /tmp/order-service-stdio.log
spring.ai.mcp.server.stdio: true publishes the MCP server on standard input and output. web-application-type: none stops Tomcat starting, because nothing is serving HTTP in this mode.
The three tools that need a stateful connection still work. Stdio is stateful, so cancel_order from Class 12 can still ask Claude Desktop to confirm, and Claude Desktop will show the dialog. This is a good demonstration of the point Class 15 made: stateful and stateless is a separate question from which transport is used.
The one thing that breaks it
A stdio server must write nothing to standard output except protocol messages. Standard output is the connection. A banner, a log line, a stray System.out.println: each one arrives at the client as malformed JSON-RPC, and the connection fails with an error that does not mention printing.
That is what the last lines of the profile are for:
banner-mode: "off"stops the Spring banner. Note the quotes, for the reason Class 6 gave.logging.threshold.console: OFFsilences the console appender.logging.file.namesends the logs somewhere useful instead.
logging.threshold.console rather than the older trick of setting logging.pattern.console to an empty string. Both silence the output, but the empty pattern makes logback complain on startup:
ERROR in ch.qos.logback.classic.PatternLayout("") - Empty or null pattern.
That goes to standard error rather than standard output, so it does not corrupt the protocol. It is still a line in Claude Desktop's logs that means nothing, and the threshold property avoids it.
The log file matters more here than usual. A stdio server has nowhere else to report a problem, and Claude Desktop shows very little. When something does not work, that file is the first place to look.
If any code in the application prints to System.out, this is where it has to stop. Class 6's McpInspector and Class 7's CLI both do, which is one reason they live in support-agent rather than order-service.
Build and Register It
mvn -pl order-service -am clean package
That produces order-service/target/order-service-1.0.0-SNAPSHOT.jar.
-pl picks the module, as it has since Class 1. -am is short for --also-make, and it adds whatever order-service depends on inside this repository to the build, so a fresh clone produces the jar in one command.
Then edit claude_desktop_config.json. On macOS it is at ~/Library/Application Support/Claude/claude_desktop_config.json; on Windows, %APPDATA%\Claude\claude_desktop_config.json.
{
"mcpServers": {
"order-service": {
"command": "java",
"args": [
"-jar",
"/absolute/path/to/order-service/target/order-service-1.0.0-SNAPSHOT.jar",
"--spring.profiles.active=stdio"
]
}
}
}
Absolute paths, because Claude Desktop's working directory is not the project.
That is the same mcpServers structure Class 9 pointed Spring AI at with servers-configuration. One format, read by both a desktop client and our own application.
Restart Claude Desktop. The order tools appear in its tools list, and it can answer:
Look up order ORD-10001 and tell me whether it has shipped.
Claude Desktop calls get_order on a Spring Boot application it started itself, holding an H2 database seeded at startup. Ask it to cancel an order and the confirmation dialog from Class 12 appears, driven by context.elicit(...).
When it does not appear
Check the log file first. /tmp/order-service-stdio.log will have the startup failure if there was one.
Check java is on the PATH Claude Desktop sees, which is not always the login shell's PATH. Use the absolute path from which java if in doubt.
Check nothing is printing to stdout. Run the jar by hand and look:
java -jar order-service/target/order-service-1.0.0-SNAPSHOT.jar --spring.profiles.active=stdio
It should print nothing at all and wait. Anything on the screen is something Claude Desktop would receive as a protocol message.
The Database Question
Each launch starts a fresh H2 database seeded by DataInitializer, because Claude Desktop starts the process. Cancel an order, quit Claude Desktop, and the cancellation is gone.
That is fine for the course and wrong for anything real. A stdio server that owns data needs the data outside the process, which is one property:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/orders
A stdio server is a process per client, so several clients mean several servers against one database. Anything cached in memory is per client, and anything written needs the same care as any other service with more than one instance.
Or Deploy It Properly
Stdio suits a server on one person's machine. A support desk used by a team belongs somewhere with an address, which is Streamable HTTP as in every class before this one, plus two things this course has not covered.
TLS, because the traffic includes customer names and order totals.
Authorisation, because /mcp published on the internet with no authentication gives anyone update_order_status and cancel_order. MCP specifies this on top of OAuth 2.1, with protected resource metadata (RFC 9728) telling a token-less client where to authenticate. It is a subject of its own and this course does not cover it.
With those in place, Claude Desktop reaches it as a Connector, and so does anything else that speaks the protocol.
What We Built
The order service runs unchanged in a client we did not write, launched as a child process, with the same tools, resources, prompt and confirmation dialog that support-agent uses over HTTP. One profile is the whole difference.
The Course, End to End
Seventeen classes started from a Spring Boot application with a REST controller and no AI in it. What it has now:
- Tools: six of them, with generated schemas, behavioural hints, and descriptions and errors written for a model
- Resources: the policies as fixed resources, orders as a template
- A prompt: the refund email, with order-ID completion
- A client:
support-agent, connected to three servers over two transports, filtering and renaming what it receives - Two-way traffic: progress and log messages during a long job, and a confirmation before anything is cancelled
- Tests: that call no model
- Production: health, metrics, retries and timeouts
- Two front doors: Streamable HTTP for the agent, stdio for Claude Desktop
The thing worth taking from it is the division. OrderService was never changed to add any of this. Every class described what the application could already do, in terms the protocol defines, and the capability came from the description.
Where to go next: Building MCP Servers in Java for the protocol underneath Spring AI's annotations, and MCP Fundamentals, Class 8 for where the specification is heading.