In the previous post I talked about what MCP is conceptually. Today I want to show you exactly how it works by walking through the simplest possible MCP client in Java. We're going to create a file using just 30 lines of code, but more importantly, understand what each line does and why.
The Complete Code First
Let me show you the entire working example, then we'll break it down piece by piece:
package com.example.mcp;
import java.time.Duration;
import java.util.Map;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
public class SimpleClientMCP {
public static void main(String[] args) {
int requestTimeoutSeconds = 30;
String basePath = System.getProperty("user.home") + "/Documents";
StdioClientTransport stdioTransport = new StdioClientTransport(
ServerParameters.builder("cmd.exe")
.args("/c", "npx @modelcontextprotocol/server-filesystem " + basePath)
.build());
McpSyncClient client = McpClient.sync(stdioTransport)
.requestTimeout(Duration.ofSeconds(requestTimeoutSeconds))
.build();
client.initialize();
CallToolRequest request = new CallToolRequest("write_file", Map.of(
"path", basePath + "\\test.txt",
"content", "Olá!\n\nThis is a sample using Model Context Protocol."
));
CallToolResult result = client.callTool(request);
System.out.println(result.toString());
}
}
When you run this, it creates a file called test.txt
in your Documents folder with some content. Simple, right? But there's a lot happening under the hood.
The MCP Architecture in This Example
Before diving into the code, let's understand what's actually happening:
- Your Java Application (the client, called host application in the MCP documentation)
- MCP Protocol (the communication layer, the MCP Client)
- Filesystem Server (the tool provider)
[Java App] ←→ [MCP Protocol] ←→ [Filesystem Server] ←→ [Your Files]
Your Java code doesn't directly touch the filesystem. It talks to an MCP server that provides filesystem tools, and that server does the actual file operations.
Breaking Down Each Section
The Imports: What You Need
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
These imports give you the core MCP functionality:
- McpClient/McpSyncClient: The main client that handles MCP communication.
- ServerParameters/StdioClientTransport: How to connect to MCP servers
- CallToolRequest/CallToolResult: How to call tools and get results
Setting Up Basic Configuration
int requestTimeoutSeconds = 30;
String basePath = System.getProperty("user.home") + "/Documents";
This is straightforward setup. The timeout prevents your application from hanging if the MCP server doesn't respond. The basePath determines where the filesystem server can operate - it's both a working directory and a security boundary.
Creating the Transport: How to Talk to the Server
StdioClientTransport stdioTransport = new StdioClientTransport(
ServerParameters.builder("cmd.exe")
.args("/c", "npx @modelcontextprotocol/server-filesystem " + basePath)
.build());
This is where the magic starts. Let's break it down:
STDIO Transport means we're communicating with a subprocess through standard input/output. It's like having a conversation through a pipe.
ServerParameters.builder("cmd.exe") tells the transport to run a Windows command. On Linux/Mac, you'd use "sh"
or "bash"
.
.args("/c", "npx @modelcontextprotocol/server-filesystem " + basePath) specifies the actual command to run. This starts the filesystem MCP server and tells it to operate in your Documents folder.
What happens here is:
- Your Java app starts a subprocess:
cmd.exe /c npx @modelcontextprotocol/server-filesystem C:\Users\YourName\Documents
- That subprocess runs the Node.js filesystem server
- The server starts up and waits for MCP protocol messages
- Your Java app can now talk to it through stdin/stdout
Building the Client: The MCP Connection
McpSyncClient client = McpClient.sync(stdioTransport)
.requestTimeout(Duration.ofSeconds(requestTimeoutSeconds))
.build();
- McpClient.sync() creates a synchronous client. There's also an async version, but sync is easier to understand and debug.
- .requestTimeout() sets how long to wait for responses. MCP operations can involve file I/O or network calls, so timeouts are important.
- .build() creates the actual client instance. At this point, the subprocess is running but the MCP handshake hasn't happened yet.
Initializing the Connection: The MCP Handshake
client.initialize();
This single line does a lot:
- Protocol Negotiation: Client and server agree on MCP version
- Capability Exchange: They tell each other what features they support
- Authentication (if needed): Some servers require credentials
- Tool Discovery: The server tells the client what tools are available
After initialize()
completes, your client knows exactly what the server can do.
Making the Tool Call: The Actual Work
CallToolRequest request = new CallToolRequest("write_file", Map.of(
"path", basePath + "\\test.txt",
"content", "Olá!\n\nThis is a sample using Model Context Protocol."
));
CallToolResult result = client.callTool(request);
CallToolRequest specifies:
- Tool name: "write_file" (this must match exactly what the server provides)
- Parameters: A map of parameter names to values
The filesystem server expects these specific parameters for write_file:
-
path
: Where to create the file -
content
: What to put in the file
client.callTool() sends this request to the server and waits for the response.
Understanding the Result
System.out.println(result.toString());
The result contains:
- Success/failure status
- Content: What the tool returned (usually a success message)
- Error details (if something went wrong)
For a successful file write, you'll see something like:
CallToolResult{content=[TextContent{text=File written successfully}], isError=false}
What's Happening Under the Hood
When you call client.callTool(request)
, here's the actual flow:
- Your Java app serializes the request to JSON
- Transport layer sends JSON over stdin to the subprocess
- Filesystem server receives and parses the JSON
- Server validates the parameters (path exists? content is valid?)
- Server performs the actual file operation
- Server sends response back as JSON over stdout
- Transport layer receives the JSON response
- Your Java app deserializes it to CallToolResult
All of this happens transparently. You just see the high-level tool call.
Why This Architecture Matters
Security: The filesystem server only operates in the directory you specify. It can't access your entire system.
Isolation: If the server crashes, it doesn't take down your Java application.
Language Independence: The server is written in Node.js, but your client is Java. MCP bridges different technologies.
Tool Reusability: Other applications can use the same filesystem server. You're not writing filesystem code; you're using a standard tool.
Common Issues and Solutions
"Command not found" errors: Make sure you have Node.js and npm installed. The npx
command needs to be in your PATH.
Permission errors: The server can only access the directory you specify in basePath. Make sure that directory exists and is writable.
Timeout errors: Some operations (especially on network drives) can be slow. Increase requestTimeoutSeconds if needed.
Path separator issues: On Windows, use \\
or /
in paths. The File.separator
constant is helpful for cross-platform code.
Extending This Example
Want to try more tools? The filesystem server provides several:
// Read a file
CallToolRequest readRequest = new CallToolRequest("read_file", Map.of(
"path", basePath + "\\test.txt"
));
// List directory contents
CallToolRequest listRequest = new CallToolRequest("list_files", Map.of(
"path", basePath
));
// Move a file
CallToolRequest moveRequest = new CallToolRequest("move_file", Map.of(
"source", basePath + "\\test.txt",
"destination", basePath + "\\renamed.txt"
));
Each tool has different parameters, but the calling pattern is the same.
What We've Learned
This simple example demonstrates the core MCP concepts:
- Transport: How client and server communicate (STDIO in this case)
- Initialization: The protocol handshake that establishes the connection
- Tool Calls: The request/response pattern for using server functionality
- Error Handling: How failures are communicated back to the client
Next post, I'll show you how to connect to multiple MCP servers simultaneously and how to discover tools dynamically. We'll build on this foundation to create more sophisticated applications.
The key insight is that MCP turns tools into network services, but with a standardized protocol that any client can use. Your Java application doesn't need to know how to write files, it just needs to know how to talk MCP.
Try running this code and experimenting with different file operations. What other MCP servers are you curious about? Let me know in the comments!
Top comments (0)