Skip to main content

Module 4: Implementing Resources

Duration: ~35 minutes | Level: Intermediate | Prerequisites: Module 3: Implementing Tools


Resources vs Tools: A Practical Recap

Before implementation, a quick decision reminder:

  • Use a Tool when the AI needs to take an action with side effects
  • Use a Resource when the AI needs to read data without side effects

In practice: if you'd write getUser(42) in a REST API as GET /users/42, it should be an MCP Resource. If you'd write it as POST /users/42/sendMessage, it's a Tool.

This matters because resources inform the host application about what's available before the AI starts reasoning. A client can subscribe to resource changes, pre-fetch frequently used resources, and cache them. Tools are only called on demand.


Static Resources

A static resource has a fixed URI and fixed content. Good for configuration, documentation, and reference data that changes rarely.

// A static resource: application configuration
var configResource = new McpServerFeatures.SyncResourceSpecification(
McpSchema.Resource.builder()
.uri("config://app/settings") // unique identifier
.name("Application Settings") // human-readable name
.description("Current application configuration as JSON")
.mimeType("application/json")
.build(),
(exchange, request) -> {
String configJson = loadApplicationConfig(); // your config reader
return new McpSchema.ReadResourceResult(
List.of(new McpSchema.TextResourceContents(
request.uri(), // echo back the requested URI
"application/json",
configJson
))
);
}
);

The uri field is the resource's unique identifier. Clients use it to fetch and subscribe to the resource. Design URIs to be:

  • Stable: don't change URIs between sessions
  • Meaningful: users://42/profile is better than r://a8f3bc
  • Hierarchical: mirror the data hierarchy in the URI structure

Dynamic Resources (Resource Templates)

A resource template is a pattern that matches multiple URIs. Instead of defining a separate resource for each user, you define one template: users://{userId}/profile.

var userResourceTemplate = new McpServerFeatures.SyncResourceTemplateSpecification(
new McpSchema.ResourceTemplate(
"users://{userId}/profile", // URI template (RFC 6570)
"User Profile",
"Profile data for a specific user by ID",
"application/json",
null
),
(exchange, request) -> {
// Extract the userId from the URI. extractUserId() is a one-liner you write:
// private String extractUserId(String uri) {
// return uri.replaceFirst("users://", "").replaceFirst("/profile$", "");
// }
// For more complex URI templates, parse against the RFC 6570 template
// you registered (libraries: handy-uri-templates, uri-template).
String uri = request.uri(); // e.g. "users://42/profile"
String userId = extractUserId(uri);

User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found: " + userId));

return new McpSchema.ReadResourceResult(
List.of(new McpSchema.TextResourceContents(
uri,
"application/json",
toJson(user)
))
);
}
);

The {userId} placeholder follows RFC 6570 URI Template syntax. The SDK handles URI matching; you receive the full URI in the handler.


Binary Resources (Images, PDFs, etc.)

Resources can return binary content as base64-encoded blobs:

var avatarResourceTemplate = new McpServerFeatures.SyncResourceTemplateSpecification(
new McpSchema.ResourceTemplate(
"users://{userId}/avatar",
"User Avatar",
"Profile photo for a user",
"image/jpeg",
null
),
(exchange, request) -> {
String userId = extractUserId(request.uri());
byte[] imageBytes = avatarStorage.load(userId);
String base64 = Base64.getEncoder().encodeToString(imageBytes);

return new McpSchema.ReadResourceResult(
List.of(new McpSchema.BlobResourceContents(
request.uri(),
"image/jpeg",
base64
))
);
}
);

Resource Listing

When a client sends resources/list, the server returns the registered static resources. Templates are listed separately via resources/templates/list; register each template with a description of its URI pattern:

McpServer.sync(transport)
.serverInfo("acme-resources", "1.0.0")
.capabilities(McpSchema.ServerCapabilities.builder()
.resources(true, true) // subscribe, listChanged
.build())
.resources(configResource)
.resourceTemplates(userResourceTemplate, avatarResourceTemplate)
.build();

The SDK handles listing automatically: resources/list returns your registered static resources, and clients discover templates through the separate resources/templates/list request, which returns your registered templates. The two booleans on .resources(subscribe, listChanged) in ServerCapabilities.builder() advertise whether the server supports resources/subscribe (first argument) and whether it emits notifications/resources/list_changed (second argument).


Live Resource Updates (Subscriptions)

Resources can change. A log file grows. A database record updates. MCP supports subscription-based change notifications.

When a client sends resources/subscribe for a URI, your server should notify it when that resource changes:

// Server with subscription support.
// Subscription support is advertised via ServerCapabilities (the first
// argument to .resources(subscribe, listChanged) is the subscribe flag).
// There's no per-resource "withSubscription" flag; capability is server-wide.
// Keep a reference to the built server so you can fire notifications later.
McpSyncServer server = McpServer.sync(transport)
.serverInfo("acme-resources", "1.0.0")
.capabilities(McpSchema.ServerCapabilities.builder()
.resources(true, true) // subscribe, listChanged both enabled
.build())
.resources(configResource)
.build();

// When the resource changes, notify all subscribers.
// The SDK method takes a ResourcesUpdatedNotification record (the URI is one of its fields).
server.notifyResourcesUpdated(
new McpSchema.ResourcesUpdatedNotification("config://app/settings"));

This is particularly useful for:

  • Log files the AI is monitoring
  • Database records that change frequently
  • Configuration that can be updated at runtime
  • Live metrics and dashboards

Practical Example: Log Viewer

A complete resource that exposes recent application logs:

var logResource = new McpServerFeatures.SyncResourceSpecification(
McpSchema.Resource.builder()
.uri("logs://app/recent")
.name("Recent Application Logs")
.description("Last 100 log lines from the application log file")
.mimeType("text/plain")
.build(),
(exchange, request) -> {
try {
Path logFile = Path.of("/var/log/app/app.log");
List<String> lines = Files.readAllLines(logFile);
// Return only the last 100 lines
int start = Math.max(0, lines.size() - 100);
String content = String.join("\n", lines.subList(start, lines.size()));

return new McpSchema.ReadResourceResult(
List.of(new McpSchema.TextResourceContents(
request.uri(), "text/plain", content
))
);
} catch (IOException e) {
// Resources don't have `isError`; throw an exception that the SDK
// converts to a protocol error
throw new RuntimeException("Log file unavailable: " + e.getMessage());
}
}
);

Key Takeaways

  • Resources are for reads, no side effects; use URIs to address data
  • Static resources have fixed URIs; templates match URI patterns
  • Binary resources use base64-encoded BlobResourceContents
  • Subscription support enables live-updating context for the AI
  • Resources can be listed, fetched, and subscribed to by clients

Companion code

A runnable server exposing all four patterns from this module (a static config resource, an RFC 6570 user-profile template, a binary avatar template, and a logs://app/recent log viewer) plus a background change simulator that fires notifications/resources/updated is in the companion repository at projects/mcp-java-sdk/resources-server/.

In the next module, we implement Prompts, the reusable workflow template primitive.

→ Module 5: Implementing Prompts