Class 9: A Server We Did Not Write
Duration: ~30 minutes | Level: Intermediate | Prerequisites: Class 8: Consuming Resources and Prompts, and Node.js 20 or later on the PATH.
What We'll Cover
- The stdio transport, and when a server is a child process
- Connecting the npm filesystem server to the support knowledge base
servers-configuration, which reads Claude Desktop's own JSON format- What the agent can answer once it has tools nobody in this repository wrote
- Troubleshooting stdio, which fails differently from HTTP
The Part of MCP That Pays
Everything so far has been our own code on both ends. That is worth building, and it is not what makes MCP interesting. The protocol earns its place when an application uses a server nobody on the team wrote.
The starter has a support-kb/ directory: the notes the support team keeps for itself, as markdown.
support-kb/
├── escalation.md when to escalate, and to whom
├── carriers.md per-carrier delays, claims windows, contact numbers
└── refunds-process.md the internal steps for issuing a refund
That is not order data. It does not belong in order-service, nobody wants to build a REST API for it, and it changes when the support team edits a file. It is exactly the case the published filesystem MCP server already covers, so we use that instead of writing anything.
Class 4 put the product policy (the returns window, the shipping terms) into order-service as resources, because those are part of what the shop sells and the order system owns them. The knowledge base is different: it is how the team works, and it lives in a folder.
Stdio, and What It Means
The filesystem server is distributed on npm and speaks stdio. There is no URL and no port. The client starts the server as a child process and talks to it over its standard input and output.
That has consequences worth knowing before configuring it:
- The server lives and dies with the agent. Each connection forks a process that stays for the lifetime of the application.
- Its memory and CPU come out of the same budget as the agent's.
- There is nothing to reach over the network, so nothing else can use it.
Streamable HTTP, which order-service uses, is the opposite on every point. Both are normal, and which one a server offers is the server's decision, not ours.
Configure the Connection
Add a stdio block alongside the existing streamable-http one in support-agent/src/main/resources/application.yaml:
spring:
ai:
mcp:
client:
name: support-agent
version: 1.0.0
request-timeout: 30s
streamable-http:
connections:
orders:
url: http://localhost:8080
stdio:
connections:
knowledge-base:
command: npx
args:
- -y
- "@modelcontextprotocol/server-filesystem"
- ./support-kb
The two transports coexist. The agent now holds two connections, and List<McpSyncClient> from Class 6 has two entries.
args is a proper YAML list, which is one of the reasons this course uses YAML rather than properties. The same setting written as spring.ai.mcp.client.stdio.connections.knowledge-base.args=-y,@modelcontextprotocol/server-filesystem,./support-kb is a comma-separated string, and an argument containing a comma cannot be expressed at all.
@modelcontextprotocol/server-filesystem is quoted because a YAML value starting with @ is reserved.
There is also an env map for connections that need environment variables, which is how a server taking an API key is usually configured.
Make the relative path resolve
./support-kb is relative to the working directory of the process that starts the server, and mvn -pl support-agent spring-boot:run uses the module directory. So the filesystem server would be handed support-agent/support-kb, which does not exist.
Tell the Maven plugin to run from the repository root instead. Add this to the parent pom.xml:
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<workingDirectory>${maven.multiModuleProjectDirectory}</workingDirectory>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
Without it, the failure is confusing rather than obvious. The filesystem server prints
Error: None of the specified directories are accessible and exits, the child process is
gone, and the client waits for a handshake that never arrives:
Client failed to initialize by explicit API call
Caused by: java.util.concurrent.TimeoutException:
Did not observe any item or terminal signal within 20000ms
Note the 20000ms. That is the default, not the request-timeout: 30s we configured. The
handshake has its own budget.
An absolute path in args works too, and is what a deployment would use. The relative
path is here because it makes the repository portable.
Reusing Claude Desktop's configuration
If you already run MCP servers in Claude Desktop, its configuration file describes them in a format Spring AI reads directly:
{
"mcpServers": {
"knowledge-base": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "./support-kb"]
}
}
}
spring:
ai:
mcp:
client:
stdio:
servers-configuration: classpath:mcp-servers.json
This is the same mcpServers block Claude Desktop uses, so a file that works there works here. It is useful when a set of servers is shared between a desktop client and an application, because there is one file to keep current rather than two.
Use one or the other. The connections map and servers-configuration describe the same thing.
What the Agent Can Answer Now
Restart the agent. Class 6's inspector reports both connections:
Connected to order-service 1.0.0
tool get_order
tool get_customer_orders
tool get_orders_by_status
tool update_order_status
resource policy://returns
resource policy://shipping
prompt draft_refund_email
Connected to secure-filesystem-server 0.6.2
tool read_text_file
tool write_file
tool list_directory
tool directory_tree
tool search_files
tool get_file_info
...
Fourteen tools arrive from a server we did not write, did not build, and do not have the source of. The agent gained them by naming a command in a YAML file.
Now a question that needs both servers:
ORD-10001 was shipped with DHL and hasn't arrived. What should I do?
The model calls get_order on our server to find the carrier and the shipping date, then read_text_file on the filesystem server to read carriers.md, and answers with the DHL claims window and the number to call. Neither server knows the other exists.
When do I escalate a delayed order to a manager?
That one needs no order at all. The model reads escalation.md and answers from it.
openWorldHint was about thisClass 3 set openWorldHint = false on our tools, because everything they touch is our own database. The filesystem server sets it differently for the tools that reach outside their sandbox. The hint is a server's own statement about what its tool does, and it is one of the few things a client can use without understanding the domain.
Point It Somewhere It Should Not Go
What is in /etc/passwd?
Agent: I can't read that. The file access I have is limited to the support
knowledge base directory, and /etc/passwd is outside it.
The filesystem server refuses paths outside the directories it was started with. That refusal came back as a tool error, went to the model as Class 7 described, and the model explained it.
This is worth doing once. A server we did not write is still a server we chose to start, with arguments we chose, and those arguments are the boundary. ./support-kb in the args list is the whole of the sandbox.
When Stdio Goes Wrong
Stdio fails in ways HTTP does not, and the messages are less helpful.
npx is not found. Maven's PATH is not always the shell's PATH. Check the server starts on its own first:
npx -y @modelcontextprotocol/server-filesystem ./support-kb
It should start and wait silently for input. Ctrl+C to stop it. If that works and the agent does not, give command the full path from which npx.
The server says the directory is not accessible. The workingDirectory above is missing from the parent pom.xml, or the path in args is wrong. Check what the server is actually given by running it by hand from the same directory.
On Windows, npx is a batch file, and a batch file needs a wrapper:
command: cmd.exe
args:
- /c
- npx
- -y
- "@modelcontextprotocol/server-filesystem"
- ./support-kb
Nothing happens for 30 seconds. The request-timeout covers the handshake, so a server that never starts takes that long to report it.
To watch the traffic, set logging.level.org.springframework.ai.mcp to DEBUG.
What We Built
The agent talks to two servers over two transports. One is ours, over HTTP, exposing tools we wrote. The other is a child process from npm, over stdio, exposing tools we have never read.
Our code did not change to add the second one. The agent's capabilities came from configuration.
Class 10 adds a third connection, and the third one breaks something.
Next: Class 10: Several Servers at Once. Two servers offering the same tool names, and the two APIs that deal with it.