Metadata and Icons
This guide covers adding custom metadata and visual icons to MCP features (tools, resources, prompts, and resource templates).
Metadata Fields
The @MetaField annotation adds custom metadata to features.
Metadata is included in the _meta object of the feature definition and can be used by clients for filtering, categorization, or custom logic.
Basic Usage
Add custom metadata fields to any feature method:
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.MetaField;
public class MyTools {
@MetaField(name = "category", value = "data-processing")
@MetaField(name = "version", value = "2.1")
@Tool(description = "Processes data with advanced algorithms")
String processData(String input) {
return "Processed: " + input;
}
}
The tool’s metadata will include:
{
"_meta": {
"category": "data-processing",
"version": "2.1"
}
}
Metadata Types
@MetaField supports multiple value types:
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.MetaField;
import io.quarkiverse.mcp.server.MetaField.Type;
public class MyTools {
@MetaField(name = "priceLevel", value = "high") (1)
@MetaField(name = "price", type = Type.INT, value = "100") (2)
@MetaField(name = "active", type = Type.BOOLEAN, value = "true") (3)
@MetaField(name = "tags", type = Type.JSON, value = "[\"premium\", \"fast\"]") (4)
@MetaField(name = "config", type = Type.JSON, value = "{\"timeout\": 30}") (5)
@Tool(description = "Premium data service")
String premiumService(String query) {
return "Result for " + query;
}
}
| 1 | String type (default) |
| 2 | Integer type |
| 3 | Boolean type |
| 4 | JSON array |
| 5 | JSON object |
The resulting metadata:
{
"_meta": {
"priceLevel": "high",
"price": 100,
"active": true,
"tags": ["premium", "fast"],
"config": {"timeout": 30}
}
}
Metadata Key Prefixes
Use prefixes to namespace your metadata and avoid conflicts:
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.MetaField;
public class MyTools {
@MetaField(prefix = "mycompany.com/", name = "tier", value = "enterprise") (1)
@MetaField(prefix = "monitoring.io/", name = "tracked", value = "true") (2)
@Tool(description = "Enterprise tier tool")
String enterpriseTool() {
return "Enterprise result";
}
}
| 1 | Custom company prefix |
| 2 | Third-party monitoring prefix |
Certain prefixes like modelcontextprotocol.io/ and tools.mcp.com/ are reserved for the MCP specification.
|
Metadata on Resources
Metadata works the same way on resources:
import io.quarkiverse.mcp.server.Resource;
import io.quarkiverse.mcp.server.MetaField;
import io.quarkiverse.mcp.server.MetaField.Type;
import io.quarkiverse.mcp.server.TextResourceContents;
import io.quarkiverse.mcp.server.RequestUri;
public class MyResources {
@MetaField(name = "cacheTime", type = Type.INT, value = "300")
@MetaField(name = "refreshable", type = Type.BOOLEAN, value = "true")
@Resource(uri = "data://config", description = "Application configuration")
TextResourceContents config(RequestUri uri) {
return new TextResourceContents(uri.value(), "{\"setting\": \"value\"}");
}
}
Metadata on Prompts
import io.quarkiverse.mcp.server.Prompt;
import io.quarkiverse.mcp.server.MetaField;
import io.quarkiverse.mcp.server.PromptMessage;
import io.quarkiverse.mcp.server.MetaField.Type;
public class MyPrompts {
@MetaField(name = "complexity", value = "advanced")
@MetaField(name = "estimatedTokens", type = Type.INT, value = "500")
@Prompt(description = "Code review assistant prompt")
PromptMessage codeReview() {
return PromptMessage.withUserRole("Review this code for best practices...");
}
}
Metadata on Resource Templates
import io.quarkiverse.mcp.server.ResourceTemplate;
import io.quarkiverse.mcp.server.MetaField;
import io.quarkiverse.mcp.server.MetaField.Type;
import io.quarkiverse.mcp.server.TextResourceContents;
import io.quarkiverse.mcp.server.RequestUri;
public class MyResourceTemplates {
@MetaField(name = "format", value = "log")
@MetaField(name = "retention", type = Type.INT, value = "7")
@ResourceTemplate(uriTemplate = "logs://{date}", description = "Daily log files")
TextResourceContents dailyLogs(String date, RequestUri uri) {
return new TextResourceContents(uri.value(), "Log data for " + date);
}
}
Icons
The @Icons annotation associates visual icons with features.
Icons can help users quickly identify tools, resources, or prompts in user interfaces.
Basic Usage
Create an IconsProvider implementation and reference it with @Icons:
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.Icons;
import io.quarkiverse.mcp.server.IconsProvider;
import io.quarkiverse.mcp.server.Icon;
import io.quarkiverse.mcp.server.FeatureManager.FeatureInfo;
import java.util.List;
public class MyTools {
@Icons(DatabaseIcons.class) (1)
@Tool(description = "Query the database")
String queryDatabase(String sql) {
return "Query result";
}
public static class DatabaseIcons implements IconsProvider { (2)
@Override
public List<Icon> get(FeatureInfo feature) {
return List.of(
new Icon("file://icons/database.png", "image/png") (3)
);
}
}
}
| 1 | Reference the icons provider |
| 2 | Implement IconsProvider |
| 3 | Return a list of icons |
Icons with Multiple Sizes
Provide icons in different sizes for different display contexts:
import io.quarkiverse.mcp.server.IconsProvider;
import io.quarkiverse.mcp.server.Icon;
import io.quarkiverse.mcp.server.FeatureManager.FeatureInfo;
import java.util.List;
public class MultiSizeIcons implements IconsProvider {
@Override
public List<Icon> get(FeatureInfo feature) {
return List.of(
new Icon("file://icons/tool-16.png", "image/png",
List.of("16x16"), null), (1)
new Icon("file://icons/tool-32.png", "image/png",
List.of("32x32"), null), (2)
new Icon("file://icons/tool-64.png", "image/png",
List.of("64x64"), null) (3)
);
}
}
| 1 | Small icon for compact displays |
| 2 | Medium icon |
| 3 | Large icon for detailed views |
Icons with Theme Support
Provide different icons for light and dark themes:
import io.quarkiverse.mcp.server.IconsProvider;
import io.quarkiverse.mcp.server.Icon;
import io.quarkiverse.mcp.server.Icon.Theme;
import io.quarkiverse.mcp.server.FeatureManager.FeatureInfo;
import java.util.List;
public class ThemedIcons implements IconsProvider {
@Override
public List<Icon> get(FeatureInfo feature) {
return List.of(
new Icon("file://icons/tool-light.png", "image/png",
null, Theme.LIGHT), (1)
new Icon("file://icons/tool-dark.png", "image/png",
null, Theme.DARK) (2)
);
}
}
| 1 | Icon for light theme |
| 2 | Icon for dark theme |
CDI Bean Icons Provider
Icons providers can be CDI beans to access other services:
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.Icons;
import io.quarkiverse.mcp.server.IconsProvider;
import io.quarkiverse.mcp.server.Icon;
import io.quarkiverse.mcp.server.ToolManager;
import io.quarkiverse.mcp.server.FeatureManager.FeatureInfo;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.List;
public class MyTools {
@Icons(DynamicIcons.class)
@Tool(description = "Tool with dynamic icons")
String myTool() {
return "Result";
}
@ApplicationScoped (1)
public static class DynamicIcons implements IconsProvider {
@Inject (2)
ToolManager toolManager;
@Override
public List<Icon> get(FeatureInfo feature) {
if (toolManager.getTool(feature.name()) != null) {
return List.of(new Icon("file://icons/active.png", "image/png"));
}
return List.of(new Icon("file://icons/inactive.png", "image/png"));
}
}
}
| 1 | Make it a CDI bean |
| 2 | Inject dependencies |
Data URIs for Embedded Icons
Use data: URIs to embed icon data directly:
import io.quarkiverse.mcp.server.IconsProvider;
import io.quarkiverse.mcp.server.Icon;
import io.quarkiverse.mcp.server.FeatureManager.FeatureInfo;
import java.util.List;
public class EmbeddedIcons implements IconsProvider {
private static final String ICON_DATA =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; (1)
@Override
public List<Icon> get(FeatureInfo feature) {
return List.of(new Icon(ICON_DATA, "image/png"));
}
}
| 1 | Base64-encoded image data (should be cached in production) |
If using data: URIs with base64-encoded images, load and cache the data separately rather than generating it on every call. IconsProvider execution must not block the caller thread.
|
Icons on Resources
import io.quarkiverse.mcp.server.Resource;
import io.quarkiverse.mcp.server.Icons;
import io.quarkiverse.mcp.server.IconsProvider;
import io.quarkiverse.mcp.server.Icon;
import io.quarkiverse.mcp.server.FeatureManager.FeatureInfo;
import io.quarkiverse.mcp.server.TextResourceContents;
import io.quarkiverse.mcp.server.RequestUri;
import java.util.List;
public class MyResources {
@Icons(DocumentIcons.class)
@Resource(uri = "file:///docs/readme.txt", description = "README file")
TextResourceContents readme(RequestUri uri) {
return new TextResourceContents(uri.value(), "README content");
}
public static class DocumentIcons implements IconsProvider {
@Override
public List<Icon> get(FeatureInfo feature) {
return List.of(new Icon("file://icons/document.png", "image/png"));
}
}
}
Icons on Prompts
import io.quarkiverse.mcp.server.Prompt;
import io.quarkiverse.mcp.server.Icons;
import io.quarkiverse.mcp.server.IconsProvider;
import io.quarkiverse.mcp.server.Icon;
import io.quarkiverse.mcp.server.FeatureManager.FeatureInfo;
import io.quarkiverse.mcp.server.PromptMessage;
import java.util.List;
public class MyPrompts {
@Icons(ChatIcons.class)
@Prompt(description = "Chat assistant prompt")
PromptMessage chatPrompt() {
return PromptMessage.withUserRole("Hello! How can I help you?");
}
public static class ChatIcons implements IconsProvider {
@Override
public List<Icon> get(FeatureInfo feature) {
return List.of(new Icon("file://icons/chat.png", "image/png"));
}
}
}
Class-Level Icons
Apply icons to all features in a class:
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.Icons;
import io.quarkiverse.mcp.server.IconsProvider;
import io.quarkiverse.mcp.server.Icon;
import io.quarkiverse.mcp.server.FeatureManager.FeatureInfo;
import java.util.List;
@Icons(CommonIcons.class) (1)
public class MyTools {
@Tool
String tool1() {
return "Result 1"; (2)
}
@Tool
String tool2() {
return "Result 2"; (2)
}
@Icons(SpecialIcons.class) (3)
@Tool
String specialTool() {
return "Special result";
}
public static class CommonIcons implements IconsProvider {
@Override
public List<Icon> get(FeatureInfo feature) {
return List.of(new Icon("file://icons/common.png", "image/png"));
}
}
public static class SpecialIcons implements IconsProvider {
@Override
public List<Icon> get(FeatureInfo feature) {
return List.of(new Icon("file://icons/special.png", "image/png"));
}
}
}
| 1 | Class-level icons apply to all features |
| 2 | These use CommonIcons |
| 3 | Method-level annotation overrides class-level |
Programmatic Icons
Set icons programmatically using manager APIs:
import io.quarkiverse.mcp.server.ToolManager;
import io.quarkiverse.mcp.server.Icon;
import io.quarkiverse.mcp.server.ToolResponse;
import io.quarkus.runtime.Startup;
import jakarta.inject.Inject;
public class DynamicTools {
@Inject
ToolManager toolManager;
@Startup
void registerToolsWithIcons() {
toolManager.newTool("searchTool")
.setDescription("Search the database")
.setIcons(new Icon("file://icons/search.png", "image/png")) (1)
.setHandler(args -> ToolResponse.success("Search results"))
.register();
}
}
| 1 | Set icons when registering programmatically |