Tools and Structured Content
Structured content allows tools to return data in a structured JSON format with a defined schema, enabling clients to parse and process tool responses programmatically.
What is Structured Content?
Structured content is a way for tools to return data in a structured, machine-readable format instead of plain text. When a tool returns structured content:
-
The response includes a
structuredContentfield containing the JSON-serialized data -
An optional output schema describes the structure of the returned data
-
Clients can parse and use the data programmatically without text parsing
This is useful when:
-
Returning complex objects with multiple fields
-
Enabling clients to process tool results programmatically
-
Providing type information and validation for tool outputs
-
Building UIs that need structured data rather than text
Enabling Structured Content
There are two main ways to enable structured content for your tools.
Using the structuredContent Attribute
Set structuredContent = true on the @Tool annotation and return a POJO:
import io.quarkiverse.mcp.server.Tool;
public class MyTools {
@Tool(description = "Get user information", structuredContent = true) (1)
User getUserInfo(String username) {
User user = new User();
user.setName(username);
user.setEmail(username + "@example.com");
user.setActive(true);
return user; (2)
}
public static class User {
private String name;
private String email;
private boolean active;
// Getters and setters...
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
}
}
| 1 | Enable structured content for this tool. |
| 2 | The returned object is automatically serialized to JSON and set as structuredContent in the response. |
When structuredContent = true, the output schema is automatically generated from the return type.
Using ToolResponse.structuredSuccess()
Alternatively, return a ToolResponse with structured content explicitly:
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolResponse;
public class MyTools {
@Tool(description = "Get user information")
ToolResponse getUserInfo(String username) {
User user = new User();
user.setName(username);
user.setEmail(username + "@example.com");
user.setActive(true);
return ToolResponse.structuredSuccess(user); (1)
}
}
| 1 | Use structuredSuccess() to return structured content. |
This approach is useful when you need to return structured content conditionally or when the output schema needs to be specified explicitly.
Async Tools with Structured Content
Structured content works with async tools that return Uni:
import io.quarkiverse.mcp.server.Tool;
import io.smallrye.mutiny.Uni;
public class MyTools {
@Tool(description = "Fetch user asynchronously", structuredContent = true)
Uni<User> fetchUserAsync(String username) {
return fetchFromDatabase(username)
.map(data -> {
User user = new User();
user.setName(data.getName());
user.setEmail(data.getEmail());
return user;
});
}
}
Output Schema
The output schema describes the structure of the data returned by a tool. It helps clients understand what data to expect and enables validation.
Automatic Schema Generation
When structuredContent = true, the schema is generated automatically from the return type:
@Tool(description = "Get product details", structuredContent = true)
Product getProduct(String productId) {
Product product = new Product();
product.setId(productId);
product.setPrice(29.99);
return product;
}
The generated schema will include type information for all fields in the Product class.
Explicit Schema Source
Use @OutputSchema(from = …) to specify which class to generate the schema from:
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.Tool.OutputSchema;
import io.quarkiverse.mcp.server.ToolResponse;
public class MyTools {
@Tool(outputSchema = @OutputSchema(from = User.class)) (1)
ToolResponse getUserInfo(String username) {
User user = new User();
user.setName(username);
user.setEmail(username + "@example.com");
return ToolResponse.structuredSuccess(user);
}
}
| 1 | Generate the output schema from the User class. |
This is necessary when returning ToolResponse directly, as the return type doesn’t provide schema information.
Custom Schema Generator
For more control, implement a custom OutputSchemaGenerator:
import io.quarkiverse.mcp.server.OutputSchemaGenerator;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.Tool.OutputSchema;
import jakarta.inject.Singleton;
import io.vertx.core.json.JsonObject;
import java.util.List;
@Singleton (1)
public class CustomSchemaGenerator implements OutputSchemaGenerator {
@Override
public Object generate(Class<?> from) {
return new JsonObject()
.put("type", "object")
.put("properties", new JsonObject()
.put("name", new JsonObject()
.put("type", "string")
.put("minLength", 1))
.put("email", new JsonObject()
.put("type", "string")
.put("format", "email"))
.put("active", new JsonObject()
.put("type", "boolean")))
.put("required", List.of("name", "email")); (2)
}
}
public class MyTools {
@Tool(
description = "Get user with custom schema",
structuredContent = true,
outputSchema = @OutputSchema(generator = CustomSchemaGenerator.class)) (3)
User getUser(String username) {
// ...
return user;
}
}
| 1 | Schema generators must be CDI beans. |
| 2 | Return a JSON Schema object. The return value is serialized with Jackson. |
| 3 | Use the custom generator for this tool. |
Global Schema Generator
To customize schema generation for all tools, implement GlobalOutputSchemaGenerator:
import io.quarkiverse.mcp.server.GlobalOutputSchemaGenerator;
import jakarta.inject.Singleton;
import io.vertx.core.json.JsonObject;
@Singleton
public class MyGlobalSchemaGenerator implements GlobalOutputSchemaGenerator {
@Override
public Object generate(Class<?> from) {
// Custom schema generation logic for all tools
return new JsonObject()
.put("type", "object")
.put("properties", generateProperties(from))
.put("additionalProperties", false);
}
private JsonObject generateProperties(Class<?> from) {
// Introspect the class and generate properties...
return new JsonObject();
}
}
The global generator is used for all tools unless a specific generator is provided via @OutputSchema(generator = …).
Programmatic API
When registering tools programmatically, use generateOutputSchema(Class<?> from) to set the output schema:
import io.quarkiverse.mcp.server.ToolManager;
import io.quarkiverse.mcp.server.ToolResponse;
import io.quarkus.runtime.Startup;
import jakarta.inject.Inject;
public class MyTools {
@Inject
ToolManager toolManager;
@Startup
void addTool() {
toolManager.newTool("getUserInfo")
.setDescription("Get user information")
.addArgument("username", "The username", true, String.class)
.generateOutputSchema(User.class) (1)
.setHandler(args -> {
User user = new User();
user.setName(args.args().get("username").toString());
return ToolResponse.structuredSuccess(user);
})
.register();
}
}
| 1 | Generate the output schema from the User class. |
Alternatively, provide a schema object directly:
toolManager.newTool("getUserInfo")
.setDescription("Get user information")
.setOutputSchema(new JsonObject() (1)
.put("type", "object")
.put("properties", new JsonObject()
.put("name", new JsonObject().put("type", "string"))))
.setHandler(args -> {
// ...
})
.register();
| 1 | Set the output schema directly. |
Compatibility Mode
Compatibility mode ensures backwards compatibility with clients that don’t support structured content.
When compatibility mode is enabled and a tool returns structured content without text content:
-
The JSON representation is automatically added as a
TextContentitem -
Clients that don’t understand structured content can still see the data as text
-
The
structuredContentfield is still populated for modern clients
Enable compatibility mode in application.properties:
quarkus.mcp.server.tools.structured-content.compatibility-mode=true
Example behavior with compatibility mode enabled:
@Tool(description = "Get user", structuredContent = true)
User getUser(String username) {
User user = new User();
user.setName("John Doe");
user.setEmail("john@example.com");
return user;
}
Response without compatibility mode:
{
"isError": false,
"content": [],
"structuredContent": {
"name": "John Doe",
"email": "john@example.com"
}
}
Response with compatibility mode:
{
"isError": false,
"content": [
{
"type": "text",
"text": "{\"name\":\"John Doe\",\"email\":\"john@example.com\"}"
}
],
"structuredContent": {
"name": "John Doe",
"email": "john@example.com"
}
}
| Compatibility mode is disabled by default. Only enable it if you need to support legacy clients. |