Discover and Export available Agent(s) as MCP Servers
Task-oriented guide for creating and managing MCP resources and prompts using factories, specifications, and examples.
Resources expose data to AI clients, while prompts define structured inputs for agent operations. The library provides:
Publish fixed content resources:
import com.embabel.agent.mcpserver.sync.McpResourcePublisher
import com.embabel.agent.mcpserver.sync.SyncResourceSpecificationFactory
import io.modelcontextprotocol.server.McpServerFeatures
import org.springframework.stereotype.Service
@Service
class DocumentationPublisher : McpResourcePublisher {
override fun resources(): List<McpServerFeatures.SyncResourceSpecification> {
return listOf(
SyncResourceSpecificationFactory.staticSyncResourceSpecification(
uri = "app://docs/api-reference",
name = "APIReference",
description = "API reference documentation",
content = loadApiDocs(),
mimeType = "text/markdown"
)
)
}
override fun infoString(verbose: Boolean?, indent: Int) =
"DocumentationPublisher: ${resources().size} resources"
private fun loadApiDocs(): String = """
# API Reference
## Endpoints
- GET /api/users
- POST /api/users
""".trimIndent()
}Key Points:
staticSyncResourceSpecification() for content loaded onceapp://category/resource-nametext/markdown, application/json, text/plain, etc.Load content on each request:
import io.modelcontextprotocol.server.McpSyncServerExchange
@Service
class StatusPublisher : McpResourcePublisher {
override fun resources(): List<McpServerFeatures.SyncResourceSpecification> {
return listOf(
SyncResourceSpecificationFactory.syncResourceSpecification(
uri = "app://status/current",
name = "CurrentStatus",
description = "Current system status",
resourceLoader = { exchange: McpSyncServerExchange ->
val status = getCurrentStatus()
objectMapper.writeValueAsString(status)
},
mimeType = "application/json"
)
)
}
override fun infoString(verbose: Boolean?, indent: Int) =
"StatusPublisher: ${resources().size} resources"
private fun getCurrentStatus(): Status {
return Status(
healthy = true,
uptime = System.currentTimeMillis(),
activeUsers = getActiveUserCount()
)
}
private fun getActiveUserCount(): Int = 42
companion object {
private val objectMapper = ObjectMapper()
}
}
data class Status(val healthy: Boolean, val uptime: Long, val activeUsers: Int)Key Points:
McpSyncServerExchange for request contextNon-blocking resource loading:
import com.embabel.agent.mcpserver.async.McpAsyncResourcePublisher
import io.modelcontextprotocol.spec.McpSchema
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
@Service
@ConditionalOnProperty(
value = ["spring.ai.mcp.server.type"],
havingValue = "ASYNC"
)
class AsyncDataPublisher : McpAsyncResourcePublisher {
override fun resources(): List<McpServerFeatures.AsyncResourceSpecification> {
return listOf(
McpServerFeatures.AsyncResourceSpecification(
McpSchema.Resource(
"app://data/users",
"Users",
"User data from database",
"application/json",
null
)
) { exchange, request ->
loadUsersAsync()
.map { users ->
McpSchema.ReadResourceResult(
listOf(
McpSchema.TextResourceContents(
"app://data/users",
"application/json",
objectMapper.writeValueAsString(users)
)
)
)
}
}
)
}
override fun infoString(verbose: Boolean?, indent: Int) =
"AsyncDataPublisher: ${resources().size} resources"
private fun loadUsersAsync(): Mono<List<User>> {
return Mono.fromCallable {
// Database query
listOf(User("1", "Alice"), User("2", "Bob"))
}.subscribeOn(Schedulers.boundedElastic())
}
companion object {
private val objectMapper = ObjectMapper()
}
}
data class User(val id: String, val name: String)Key Points:
Mono<ReadResourceResult> for non-blocking executionsubscribeOn(Schedulers.boundedElastic()) for blocking operationsMcpSchema.ReadResourceResult and TextResourceContents// Documentation
uri = "app://docs/getting-started"
uri = "app://docs/api-reference"
// Application data
uri = "app://data/configuration"
uri = "app://data/schema"
// Runtime information
uri = "app://status/health"
uri = "app://status/metrics"
// User-specific
uri = "app://user/preferences"
uri = "app://user/history"Organize related resources:
@Service
class SystemResourcesPublisher : McpResourcePublisher {
override fun resources(): List<McpServerFeatures.SyncResourceSpecification> {
return listOf(
createHealthResource(),
createMetricsResource(),
createConfigResource()
)
}
override fun infoString(verbose: Boolean?, indent: Int) =
"SystemResourcesPublisher: ${resources().size} resources"
private fun createHealthResource() =
SyncResourceSpecificationFactory.staticSyncResourceSpecification(
uri = "app://system/health",
name = "Health",
description = "System health status",
content = """{"status": "healthy"}""",
mimeType = "application/json"
)
private fun createMetricsResource() =
SyncResourceSpecificationFactory.syncResourceSpecification(
uri = "app://system/metrics",
name = "Metrics",
description = "Current system metrics",
resourceLoader = { getMetrics() },
mimeType = "application/json"
)
private fun createConfigResource() =
SyncResourceSpecificationFactory.staticSyncResourceSpecification(
uri = "app://system/config",
name = "Configuration",
description = "System configuration",
content = loadConfiguration(),
mimeType = "application/json"
)
private fun getMetrics(): String = """{"cpu": 45, "memory": 67}"""
private fun loadConfiguration(): String = """{"debug": false}"""
}Generate prompts from input types:
import com.embabel.agent.mcpserver.sync.McpPromptPublisher
import com.embabel.agent.mcpserver.sync.McpPromptFactory
import com.embabel.common.core.types.Named
import com.embabel.common.core.types.Described
import com.fasterxml.jackson.annotation.JsonPropertyDescription
import io.modelcontextprotocol.server.McpServerFeatures
import org.springframework.stereotype.Service
@Service
class TaskPromptsPublisher : McpPromptPublisher {
private val factory = McpPromptFactory()
override fun prompts(): List<McpServerFeatures.SyncPromptSpecification> {
return listOf(
createTaskPrompt(),
searchTasksPrompt()
)
}
override fun infoString(verbose: Boolean?, indent: Int) =
"TaskPromptsPublisher: ${prompts().size} prompts"
private fun createTaskPrompt(): McpServerFeatures.SyncPromptSpecification {
val goal = object : Named, Described {
override val name = "createTask"
override val description = "Create a new task"
}
return factory.syncPromptSpecificationForType(
goal = goal,
inputType = CreateTaskInput::class.java
)
}
private fun searchTasksPrompt(): McpServerFeatures.SyncPromptSpecification {
val goal = object : Named, Described {
override val name = "searchTasks"
override val description = "Search for tasks"
}
return factory.syncPromptSpecificationForType(
goal = goal,
inputType = SearchTasksInput::class.java
)
}
}
data class CreateTaskInput(
@JsonPropertyDescription("Task title")
val title: String,
@JsonPropertyDescription("Task description")
val description: String,
@JsonPropertyDescription("Priority level (1-5)")
val priority: Int
)
data class SearchTasksInput(
@JsonPropertyDescription("Search query string")
val query: String,
@JsonPropertyDescription("Filter by status (optional)")
val status: String? = null
)Key Points:
@JsonPropertyDescription provides field documentationNamed and Described interfacesimport com.embabel.agent.mcpserver.async.McpAsyncPromptPublisher
import com.embabel.agent.mcpserver.async.McpAsyncPromptFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
@Service
@ConditionalOnProperty(
value = ["spring.ai.mcp.server.type"],
havingValue = "ASYNC"
)
class AsyncAnalysisPromptsPublisher : McpAsyncPromptPublisher {
private val factory = McpAsyncPromptFactory()
override fun prompts(): List<McpServerFeatures.AsyncPromptSpecification> {
val goal = object : Named, Described {
override val name = "analyzeData"
override val description = "Analyze data asynchronously"
}
return listOf(
factory.asyncPromptSpecificationForType(
goal = goal,
inputType = AnalysisInput::class.java
)
)
}
override fun infoString(verbose: Boolean?, indent: Int) =
"AsyncAnalysisPromptsPublisher: ${prompts().size} prompts"
}
data class AnalysisInput(
@JsonPropertyDescription("Data source identifier")
val dataSource: String,
@JsonPropertyDescription("Type of analysis to perform")
val analysisType: String,
@JsonPropertyDescription("Time range for analysis")
val timeRange: String? = null
)data class SimpleInput(
@JsonPropertyDescription("The operation to perform")
val operation: String
)data class ComplexInput(
@JsonPropertyDescription("User information")
val user: UserInfo,
@JsonPropertyDescription("Processing options")
val options: ProcessingOptions
)
data class UserInfo(
@JsonPropertyDescription("User ID")
val id: String,
@JsonPropertyDescription("User name")
val name: String
)
data class ProcessingOptions(
@JsonPropertyDescription("Enable verbose output")
val verbose: Boolean = false,
@JsonPropertyDescription("Maximum items to process")
val limit: Int = 100
)data class BatchInput(
@JsonPropertyDescription("List of item IDs to process")
val items: List<String>,
@JsonPropertyDescription("Configuration parameters")
val parameters: Map<String, String> = emptyMap()
)Single service for related resources and prompts:
@Service
class DataManagementPublisher : McpResourcePublisher, McpPromptPublisher {
private val promptFactory = McpPromptFactory()
// Resources
override fun resources(): List<McpServerFeatures.SyncResourceSpecification> {
return listOf(
SyncResourceSpecificationFactory.staticSyncResourceSpecification(
uri = "app://data/schema",
name = "DataSchema",
description = "Data schema documentation",
content = loadSchema(),
mimeType = "application/json"
)
)
}
// Prompts
override fun prompts(): List<McpServerFeatures.SyncPromptSpecification> {
val createGoal = object : Named, Described {
override val name = "createData"
override val description = "Create new data entry"
}
return listOf(
promptFactory.syncPromptSpecificationForType(
goal = createGoal,
inputType = CreateDataInput::class.java
)
)
}
override fun infoString(verbose: Boolean?, indent: Int): String {
return "DataManagementPublisher: ${resources().size} resources, ${prompts().size} prompts"
}
private fun loadSchema(): String = """{"type": "object"}"""
}
data class CreateDataInput(
@JsonPropertyDescription("Data type")
val type: String,
@JsonPropertyDescription("Data payload")
val payload: Map<String, Any>
)// Good: Static for unchanging content
SyncResourceSpecificationFactory.staticSyncResourceSpecification(
uri = "app://docs/readme",
name = "README",
description = "Project documentation",
content = readmeContent,
mimeType = "text/markdown"
)
// Good: Dynamic for real-time data
SyncResourceSpecificationFactory.syncResourceSpecification(
uri = "app://status/current",
name = "Status",
description = "Current status",
resourceLoader = { getCurrentStatus() },
mimeType = "application/json"
)// Markdown documentation
mimeType = "text/markdown"
// JSON data
mimeType = "application/json"
// Plain text
mimeType = "text/plain"
// HTML content
mimeType = "text/html"// Good: Specific and helpful
description = "Current system metrics including CPU, memory, and disk usage"
// Bad: Vague
description = "Metrics"// Good: Clear descriptions
data class TaskInput(
@JsonPropertyDescription("Title of the task (max 100 characters)")
val title: String,
@JsonPropertyDescription("Detailed task description")
val description: String
)
// Bad: No descriptions
data class TaskInput(
val title: String,
val description: String
)// Good: Optional fields have defaults
data class SearchInput(
@JsonPropertyDescription("Search query")
val query: String,
@JsonPropertyDescription("Maximum results (default: 10)")
val limit: Int = 10,
@JsonPropertyDescription("Result offset (default: 0)")
val offset: Int = 0
)data class CreateUserInput(
@JsonPropertyDescription("Email address")
val email: String, // Consider adding validation logic
@JsonPropertyDescription("Age (must be positive)")
val age: Int // Consider adding validation logic
) {
init {
require(email.contains("@")) { "Invalid email format" }
require(age > 0) { "Age must be positive" }
}
}@Service
class SafeResourcePublisher : McpResourcePublisher {
override fun resources(): List<McpServerFeatures.SyncResourceSpecification> {
return buildList {
// Try to add each resource, skip on failure
tryAddResource { createPrimaryResource() }
tryAddResource { createSecondaryResource() }
}
}
override fun infoString(verbose: Boolean?, indent: Int) =
"SafeResourcePublisher: ${resources().size} resources"
private fun MutableList<McpServerFeatures.SyncResourceSpecification>.tryAddResource(
factory: () -> McpServerFeatures.SyncResourceSpecification
) {
try {
add(factory())
} catch (e: Exception) {
logger.error("Failed to create resource", e)
}
}
private fun createPrimaryResource() =
SyncResourceSpecificationFactory.staticSyncResourceSpecification(
uri = "app://data/primary",
name = "Primary",
description = "Primary data",
content = loadPrimaryData(),
mimeType = "application/json"
)
private fun createSecondaryResource() =
SyncResourceSpecificationFactory.staticSyncResourceSpecification(
uri = "app://data/secondary",
name = "Secondary",
description = "Secondary data",
content = loadSecondaryData(),
mimeType = "application/json"
)
private fun loadPrimaryData(): String = """{"status": "ok"}"""
private fun loadSecondaryData(): String = """{"backup": "available"}"""
companion object {
private val logger = LoggerFactory.getLogger(SafeResourcePublisher::class.java)
}
}import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class DocumentationPublisherTest {
private val publisher = DocumentationPublisher()
@Test
fun `should provide resources`() {
val resources = publisher.resources()
assertEquals(1, resources.size)
assertEquals("APIReference", resources[0].resource.name)
assertEquals("app://docs/api-reference", resources[0].resource.uri)
}
@Test
fun `should include content`() {
val resources = publisher.resources()
val content = resources[0].staticContent
assertNotNull(content)
assertTrue(content!!.contains("API Reference"))
}
}class TaskPromptsPublisherTest {
private val publisher = TaskPromptsPublisher()
@Test
fun `should provide prompts`() {
val prompts = publisher.prompts()
assertEquals(2, prompts.size)
assertTrue(prompts.any { it.prompt.name == "createTask" })
assertTrue(prompts.any { it.prompt.name == "searchTasks" })
}
@Test
fun `should generate schema from input type`() {
val prompts = publisher.prompts()
val createTaskPrompt = prompts.first { it.prompt.name == "createTask" }
assertNotNull(createTaskPrompt.prompt.arguments)
}
}