MCP client
com.tnsai.mcp.client.McpClient talks to one MCP server over a transport. It lists and calls the server's tools, reads its resources and prompts, and answers the server's own requests for roots and LLM sampling. To hand a server's tools to an agent, use McpToolBridge (see Give an agent MCP tools).
Example
package com.example.tnsai.docs;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.mcp.McpServerConfig;
import com.tnsai.mcp.client.HttpTransport;
import com.tnsai.mcp.client.McpClient;
import com.tnsai.mcp.client.StdioTransport;
import com.tnsai.mcp.hub.McpToolBridge;
import com.tnsai.mcp.model.McpToolDefinition;
import com.tnsai.mcp.model.ModelPreferences;
import com.tnsai.mcp.model.ResourceContent;
import com.tnsai.mcp.model.Root;
import com.tnsai.mcp.model.SamplingRequest;
import com.tnsai.mcp.model.SamplingResponse;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/** Compilation contract for mcp/client.md. */
public final class McpClientExample {
private McpClientExample() {
}
static String readFile() throws IOException {
McpClient client = McpClient.builder()
.transport(StdioTransport.builder()
.command("npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp")
.build())
.clientName("MyApp")
.clientVersion("1.0.0")
.requestTimeout(Duration.ofSeconds(30))
.roots(List.of(Root.fromPath(Path.of("/tmp"), "Scratch")))
.build();
client.connect();
client.initialize();
try {
List<McpToolDefinition> tools = client.listTools();
JsonNode content = client.executeTool("read_file", Map.of("path", "/tmp/notes.txt"));
return tools.size() + " tools; " + content;
} finally {
client.disconnect();
}
}
static String readResource(McpClient client) throws IOException {
StringBuilder text = new StringBuilder();
for (ResourceContent part : client.readResource("file:///tmp/notes.txt")) {
if (part.isText()) {
text.append(part.getText());
}
}
return text.toString();
}
static JsonNode summarizePrompt(McpClient client, String longText) throws IOException {
return client.getPrompt("summarize", Map.of("text", longText)); // the prompt's messages
}
static SamplingRequest samplingRequest() {
return SamplingRequest.builder()
.addUserMessage("What is the capital of France?")
.systemPrompt("You are a geography expert.")
.maxTokens(100)
.temperature(0.7)
.includeContext("thisServer")
.modelPreferences(ModelPreferences.builder()
.intelligencePriority(0.8)
.speedPriority(0.5)
.hint("claude-sonnet")
.build())
.build();
}
static McpClient withSampling(HttpTransport transport, Function<String, String> llm) {
return McpClient.builder()
.transport(transport)
.samplingRequestHandler(request -> {
String prompt = request.getMessages().get(request.getMessages().size() - 1)
.getContent().path("text").asText();
SamplingResponse response = new SamplingResponse();
response.setRole("assistant");
response.setModel("my-model");
response.setContent(JsonNodeFactory.instance.objectNode()
.put("type", "text")
.put("text", llm.apply(prompt)));
response.setStopReason("endTurn");
return response;
})
.build();
}
static AgentBuilder withMcpTools(AgentBuilder builder, String apiKey) throws IOException {
McpToolBridge bridge = McpToolBridge.httpWithApiKey("https://mcp.example.com/mcp", apiKey);
bridge.connect(); // fails here, not silently later
return builder.dynamicTools(bridge.toTnsAITools());
}
static McpClient fromConfig() {
McpServerConfig config = McpServerConfig.builder()
.endpoint("https://api.example.com/mcp")
.apiKey("xxx", "X-API-Key") // sends X-API-Key: xxx
.header("X-Workspace", "docs")
.timeoutSeconds(60)
.build();
return McpClient.create(HttpTransport.from(config));
}
}Connect
Every client goes through the same three steps:
McpClient.builder()...build(),McpClient.create(transport)ornew McpClient(transport).build()throwsIllegalArgumentExceptionwithout a transport.connect()opens the transport.initialize()exchanges capabilities and reads the server's name, version and capabilities. Every tool, resource, prompt, sampling and completion call before it throwsMcpException("McpClient not initialized").
disconnect() closes the transport and clears the server info, so the next session needs initialize() again.
| Builder method | Default | Meaning |
|---|---|---|
transport(t) | none, required | How the client reaches the server |
clientName(name) | TnsAI-MCP-Client | Sent in initialize |
clientVersion(v) | 1.0.0 | Sent in initialize |
requestTimeout(d) | 30 seconds | How long a request waits for its response |
roots(list) | none | The roots the client advertises (see Roots) |
samplingRequestHandler(fn) | none | Answers the server's sampling requests (see Sampling) |
McpException extends IOException and carries the JSON-RPC error code in getCode().
Methods
| Method | Returns | Notes |
|---|---|---|
listTools() | List<McpToolDefinition> | |
executeTool(name, args) | JsonNode | The result's content array when the server sends one, otherwise the whole result |
listResources(), listResourceTemplates() | List<Resource>, List<ResourceTemplate> | Needs the server's resources capability |
readResource(uri) | List<ResourceContent> | One entry per part; check isText() or isBlob() |
subscribeResource(uri), unsubscribeResource(uri) | void | Needs resource subscription support |
listPrompts() | List<Prompt> | Needs the server's prompts capability |
getPrompt(name, args) | JsonNode | args is Map<String, String>; returns the prompt's messages |
complete(request), completeResourceUri(partial), completePromptArgument(prompt, arg, partial) | CompletionResponse | Argument completion |
createSamplingMessage(request) | SamplingResponse | Asks the server to sample |
setLogLevel(level), setOnLogMessage(handler) | void | Server log messages |
getServerInfo(), getServerCapabilities() | Set by initialize() | |
isConnected(), isInitialized() | boolean |
A resource or prompt method throws McpException ("Server does not support ...") when the server did not declare that capability.
Notifications
setOnToolsListChanged, setOnResourcesListChanged and setOnPromptsListChanged each take a Consumer<String> that runs when the server says that list changed. setOnNotification takes a Consumer<JsonNode> for other notifications.
Roots
Roots tell the server which locations it may work in. The client answers the server's roots/list request with its current roots. getRoots() returns a copy; setRoots, addRoot and removeRoot change them and, after initialize(), notify the server that the list changed.
A root's URI is a file URI. Root.fromPath(path, name) and Root.fromDirectory(path, name) build one from a path; new Root(uri, name) and Root.of(uri, name) take the URI as given.
Sampling
A server can ask the client for an LLM completion. The client handles that request with its sampling handler, a Function<SamplingRequest, SamplingResponse>:
initialize()advertises the sampling capability only when a handler is set on the builder.setSamplingRequestHandlerreplaces the handler later, but it does not advertise the capability.- Without a handler, or when the handler throws, the server gets JSON-RPC error
-32603. Any other server request exceptroots/listgets-32601.
SamplingRequest.builder() takes addUserMessage, addAssistantMessage, addMessage, messages, systemPrompt, includeContext (none, thisServer or allServers), temperature, maxTokens, stopSequences, modelPreferences and metadata. build() throws IllegalStateException without a message or without maxTokens.
SamplingResponse has a role, content, model and stop reason, set with setters. getTextContent() reads the text from a text node, a {"type": "text"} object or an array of such parts.
Give an agent MCP tools
com.tnsai.mcp.hub.McpToolBridge wraps a client and turns each of the server's tools into a DynamicToolMethod. Pass the list to AgentBuilder.dynamicTools, as withMcpTools does; the agent then calls them like its own tools.
| Factory | Transport |
|---|---|
stdio(command...) | StdioTransport |
http(url), http(url, headers) | HttpTransport |
httpWithApiKey(url, apiKey) | HttpTransport with Authorization: Bearer <apiKey> |
custom(transport, name, description) | Any transport |
- A bridge connects and lists the tools on first use. But
toTnsAITools()logs a failed listing and returns an empty list, so callconnect()first: it throws when the server cannot be reached. - Keep the bridge open while the agent runs; each tool calls the server through it. A server error reaches the agent as a
RuntimeException("MCP tool error: ..."). - Each tool takes its description and input schema from the server. A
destructiveHintannotation setsrequiresConfirmation, andidempotentHintsetsidempotent. When a tool requires confirmation and the agent has no tool call filter, the build reports validation warningAGENT-V006.
Server settings as data
com.tnsai.mcp.McpServerConfig holds an endpoint, headers and a timeout; HttpTransport.from(config) turns it into a transport, as fromConfig does.
McpServerConfig.of(endpoint)andof(endpoint, apiKey)cover the common cases. The builder requiresendpoint;build()throwsNullPointerExceptionwithout it.apiKey(key)sendsAuthorization: Bearer <key>.apiKey(key, header)sends the bare key in that header, unless the header isAuthorization. Both skip a null or empty key.- The timeout is 30 seconds unless you set
timeoutortimeoutSeconds. - Without
name,getName()uses the second-to-last part of the endpoint host (https://mcp.firecrawl.devgivesfirecrawl), ormcp-server. hasApiKey()checks only for anAuthorizationheader, so a key in a custom header reads as false.