Skip to main content

Module 3: Connecting to Your First MCP Server

Duration: ~35 minutes | Level: Intermediate | Prerequisites: Module 2 complete, Spring Boot project running, AI provider connected


What You'll Build

A Spring Boot application that:

  1. Connects to an MCP filesystem server on startup
  2. Lists all available tools with their names and descriptions
  3. Executes a tool call directly to verify the connection
  4. Shuts down cleanly, closing the MCP connection

Along the way you'll understand exactly what happens during MCP connection, why stdio is the right choice for local servers, and how Spring AI manages the connection lifecycle.


Why stdio for Local Servers

MCP defines two standard transport types: stdio and Streamable HTTP. (The older HTTP+SSE transport was replaced by Streamable HTTP in the 2025-03-26 spec revision and is now deprecated, though Spring AI still ships a legacy SSE client transport for compatibility.)

With stdio transport, the MCP client starts the server as a child process and communicates via the process's standard input and output streams. This is:

  • Simple: no network port to configure, no firewall rules, no port conflicts
  • Secure: communication is local and isolated to the parent-child process relationship
  • Fast to start: no HTTP handshake, just fork-exec and start writing JSON-RPC

With Streamable HTTP transport, the server is a long-running HTTP service. The client sends JSON-RPC messages over HTTP POST, and the server replies with either a plain JSON response or an SSE stream when it needs to send multiple messages back.

For local development and for servers you run yourself, stdio is almost always the right choice. Streamable HTTP is for remote servers, servers running in the cloud, shared servers accessed by multiple clients simultaneously. We cover building your own Streamable HTTP MCP server in Module 6; the Remote MCP Servers course covers the transport in depth.


The Filesystem MCP Server

We'll use Anthropic's official reference implementation: @modelcontextprotocol/server-filesystem. It's a Node.js package that exposes file system operations as MCP tools:

ToolWhat it does
read_text_fileRead the contents of a text file (replaces the deprecated read_file)
read_fileDeprecated alias for read_text_file (still registered by the current package)
read_media_fileRead an image, audio, or other media file as base64
read_multiple_filesRead several text files at once
write_fileWrite content to a file
edit_fileApply targeted edits to a file
create_directoryCreate a directory (and parents)
list_directoryList the contents of a directory
list_directory_with_sizesLike list_directory, with file sizes
directory_treeRecursive directory listing as a tree
move_fileMove or rename a file
search_filesSearch for files by name pattern
get_file_infoGet metadata (size, dates, permissions)
list_allowed_directoriesList the directories the server is allowed to access

The deprecated read_file alias means tools/list returns 14 tools in total (count verified against version 2026.7.10; it may change in future releases).

This is a real, production-quality MCP server. Anthropic uses it in their own documentation as the canonical stdio example. If you've used it in Claude Desktop, you know exactly what it does.

Install it (or verify Node.js and npx are available):

node --version    # Node.js 20+ recommended
npx --version # Comes with Node.js

# Test the server directly (it will wait for MCP protocol input):
npx -y @modelcontextprotocol/server-filesystem /tmp
# Press Ctrl+C to stop

The -y flag tells npx to install the package automatically if it's not already cached. After the first run, it's cached and subsequent starts are fast.


Configure the Connection

In application.properties, add the MCP server configuration:

# ── MCP Client ──────────────────────────────────────────────────────────────

# Register MCP tools with ChatClient automatically
spring.ai.mcp.client.toolcallback.enabled=true

# stdio connection to the filesystem server
spring.ai.mcp.client.stdio.connections.filesystem.command=npx
spring.ai.mcp.client.stdio.connections.filesystem.args=-y,@modelcontextprotocol/server-filesystem,/tmp

# Global timeout for individual MCP requests (default 20s). The initialize
# handshake is separately capped at 20s inside the MCP Java SDK, so this does
# not extend startup time. If the first npx download is slow, pre-warm the
# cache once with: npx -y @modelcontextprotocol/server-filesystem /tmp
spring.ai.mcp.client.request-timeout=30s

# Debug logging to see what's happening
logging.level.org.springframework.ai.mcp=DEBUG

Let's break down the key parts:

spring.ai.mcp.client.stdio.connections.filesystem, filesystem is the connection name. You choose it. It appears in log messages and can be used to reference this connection programmatically. You can have multiple stdio connections, each with a different name.

command and args: the command and arguments used to start the server process. Spring AI runs npx -y @modelcontextprotocol/server-filesystem /tmp. The /tmp argument tells the filesystem server which directory it has access to.

spring.ai.mcp.client.request-timeout, How long the client waits for each individual MCP request (tools/list, tools/call, and so on). It is a single global setting in Spring AI 2.0.x; there are no per-connection timeout properties. The default is 20s. Note that it does not stretch the startup phase: the Java SDK bounds the whole initialize handshake with its own 20s initialization timeout, which Spring AI 2.0.x does not expose as a property, so the handshake is effectively capped at 20s no matter how high you set this. If the first npx run needs longer than that to download the package, pre-warm the cache once from a terminal (npx -y @modelcontextprotocol/server-filesystem /tmp, then Ctrl+C) instead of raising this property.

About the args format

The args value is a comma-separated string. If any argument contains a comma, you'll need to use a YAML file instead of .properties. Switch your configuration to application.yml when dealing with complex argument lists.


Inspect the Connection at Startup

Let's write code that runs on startup, inspects the MCP connection, and prints the available tools:

package com.themcpguy.springai.connection;

import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class McpConnectionInspector implements CommandLineRunner {

@Autowired
private SyncMcpToolCallbackProvider toolCallbackProvider;

@Override
public void run(String... args) {
System.out.println("\n=== MCP Connection Report ===\n");

ToolCallback[] callbacks = toolCallbackProvider.getToolCallbacks();

if (callbacks.length == 0) {
System.out.println("No MCP tools discovered. Check your server configuration.");
return;
}

System.out.printf("Discovered %d MCP tool(s):%n%n", callbacks.length);

for (ToolCallback callback : callbacks) {
System.out.printf(" Tool: %s%n", callback.getToolDefinition().name());
System.out.printf(" Desc: %s%n", callback.getToolDefinition().description());
System.out.println();
}

System.out.println("=== End of Report ===\n");
}
}

The SyncMcpToolCallbackProvider (in org.springframework.ai.mcp, as of Spring AI 2.0.x) is auto-configured by spring-ai-starter-mcp-client when toolcallback.enabled=true. It aggregates tool callbacks from all configured MCP connections into a single bean.

getToolCallbacks() returns an array of ToolCallback objects, one per discovered MCP tool. Each ToolCallback wraps the tool's metadata (name, description, input schema) and knows how to execute it by calling the MCP server over the connection.

If a future Spring AI version renames the bean or its method, the concept (a single bean aggregates all MCP tool callbacks for the configured connections) is stable; only the import and method name change.

Run the application:

mvn spring-boot:run

Expected output:

=== MCP Connection Report ===

Discovered 14 MCP tool(s):

Tool: read_file
Desc: Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead...

Tool: read_text_file
Desc: Read the complete contents of a text file from the file system...

Tool: read_multiple_files
Desc: Read the contents of multiple text files simultaneously...

Tool: write_file
Desc: Create a new file or completely overwrite an existing file...

... (10 more tools)

=== End of Report ===

If you see this, the MCP connection is working. Spring AI started the filesystem server process, completed the MCP handshake, retrieved the tool list, and wrapped each tool in a ToolCallback.


What Happened Under the Hood

The sequence of events, from SpringApplication.run() to your tool list:

This is precisely the MCP connection lifecycle from the MCP Fundamentals course: initialize handshake → capability exchange → ready to use. Spring AI handles all of it automatically from your properties configuration.


Execute a Tool Directly

Let's go one step further and call a tool directly, bypassing the LLM entirely. This is useful for testing and debugging.

package com.themcpguy.springai.connection;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
public class DirectToolCaller implements CommandLineRunner {

@Autowired
private SyncMcpToolCallbackProvider toolCallbackProvider;

@Autowired
private ObjectMapper objectMapper;

@Override
public void run(String... args) throws Exception {
// Find the list_directory tool
ToolCallback listDir = Arrays.stream(toolCallbackProvider.getToolCallbacks())
.filter(t -> t.getToolDefinition().name().equals("list_directory"))
.findFirst()
.orElseThrow(() -> new IllegalStateException("list_directory tool not found"));

// Invoke it with a JSON input, the schema matches what the server expects
String input = objectMapper.writeValueAsString(
java.util.Map.of("path", "/tmp")
);

String result = listDir.call(input);

System.out.println("Contents of /tmp:");
System.out.println(result);
}
}

The call(String input) method on ToolCallback takes a JSON string matching the tool's input schema and returns a JSON string result. No LLM involved, you're talking directly to the MCP server.

This is valuable in three scenarios:

  1. Debugging: verify a tool works correctly before involving the LLM
  2. Testing: test tool logic without API costs
  3. Direct automation: use MCP tools programmatically when you already know which tool to call

Connection Lifecycle and Shutdown

Spring AI manages the lifecycle of stdio connections as Spring beans. When the application shuts down:

  1. Spring triggers @PreDestroy callbacks and DisposableBean.destroy() implementations
  2. The MCP client closes the session and shuts down the stdio transport: it stops accepting new messages and gives pending writes a brief grace period (about 100 ms)
  3. The client sends SIGTERM to the server process (Process.destroy()) and waits for it to exit
  4. The server process exits
  5. Spring completes the context shutdown

The MCP spec recommends a gentler sequence for stdio (close the server's stdin first, then SIGTERM, then SIGKILL as a last resort). The Java SDK 2.0.0 stdio transport simplifies this: it goes straight to SIGTERM and never escalates to SIGKILL.

You don't need to manage any of this manually. If you run mvn spring-boot:run and press Ctrl+C, the MCP server process is cleaned up correctly.

To verify: run the application with logging.level.io.modelcontextprotocol=DEBUG and watch the shutdown sequence in the logs. You'll see Initiating graceful shutdown followed by Sending TERM to process before the server process terminates.


Multiple Connections

You can connect to multiple MCP servers simultaneously. Add a second connection with a different name:

# Second server: GitHub's official MCP server (Go-based, run as a Docker image).
# The old @modelcontextprotocol/server-github npm package is deprecated and archived;
# GitHub now ships its official server at github/github-mcp-server.
spring.ai.mcp.client.stdio.connections.github.command=docker
spring.ai.mcp.client.stdio.connections.github.args=run,-i,--rm,-e,GITHUB_PERSONAL_ACCESS_TOKEN,ghcr.io/github/github-mcp-server
spring.ai.mcp.client.stdio.connections.github.env.GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_TOKEN}

SyncMcpToolCallbackProvider.getToolCallbacks() will return tools from all configured connections. If two connected servers expose a tool with the same name, Spring AI 2.0 auto-disambiguates the duplicate by prefixing it with alt_<n>_ (for example alt_1_read_text_file); typically each MCP server defines distinct tool names, so this rarely triggers.

We'll explore multi-server setups in detail in Module 5.


SSE Connection Configuration (Preview)

If you need to connect to a remote MCP server that still speaks the legacy HTTP+SSE transport, the configuration pattern changes:

# SSE connection, for remote HTTP MCP servers (legacy transport)
spring.ai.mcp.client.sse.connections.remote-server.url=http://mcp-server.internal:8080

# The global request-timeout above applies to SSE operations too;
# Spring AI 2.0.x has no per-connection timeout property for SSE.

HTTP+SSE is now the deprecated, opt-in legacy transport; Streamable HTTP is the modern default for remote MCP. Everything else is the same: SyncMcpToolCallbackProvider aggregates tools from stdio and remote connections transparently. We cover remote transports in detail in Module 6, where you'll build a remote MCP server and connect to it from another Spring Boot application.


Troubleshooting

Connection timeout or Process did not start

The npx command failed to start the server. Check:

# Can you start it manually?
npx -y @modelcontextprotocol/server-filesystem /tmp

# Is npx in the PATH that Spring Boot uses?
which npx

# On some systems, Maven's PATH differs from your shell's PATH
# Try specifying the full path:
spring.ai.mcp.client.stdio.connections.filesystem.command=/usr/local/bin/npx

0 tools discovered

The connection succeeded but the server returned no tools. This usually means the server started but the tools/list request timed out or failed. Check:

logging.level.org.springframework.ai.mcp=TRACE

Look for the tools/list request and response in the logs.

SyncMcpToolCallbackProvider bean not found

Either spring.ai.mcp.client.toolcallback.enabled or spring.ai.mcp.client.enabled is explicitly set to false in your properties, spring.ai.mcp.client.type=ASYNC is set (which registers AsyncMcpToolCallbackProvider instead), or the spring-ai-starter-mcp-client dependency is missing from your pom.xml. The tool callback provider is enabled by default, so you do not need to add any property to turn it on.


Next: Module 4: ChatClient with MCP Tools. Connect the tools to the LLM and build a conversational agent that uses them automatically.