CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-com-embabel-agent--embabel-agent-mcpserver

Discover and Export available Agent(s) as MCP Servers

Overview
Eval results
Files

resources-and-prompts.mddocs/guides/

Working with Resources and Prompts

Task-oriented guide for creating and managing MCP resources and prompts using factories, specifications, and examples.

Overview

Resources expose data to AI clients, while prompts define structured inputs for agent operations. The library provides:

  • Static Resources: Fixed content loaded at startup
  • Dynamic Resources: Content loaded on-demand
  • Prompt Factories: Type-safe prompt specifications from Java types
  • Mode-Specific Implementations: Sync and async variants

Resource Basics

Static Resource (Sync Mode)

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:

  • Use staticSyncResourceSpecification() for content loaded once
  • Content is evaluated immediately at bean creation
  • URI follows convention: app://category/resource-name
  • MIME types: text/markdown, application/json, text/plain, etc.

Dynamic Resource (Sync Mode)

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:

  • Lambda receives McpSyncServerExchange for request context
  • Content generated fresh on each access
  • Use for real-time data or expensive computations

Async Resource

Non-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:

  • Returns Mono<ReadResourceResult> for non-blocking execution
  • Use subscribeOn(Schedulers.boundedElastic()) for blocking operations
  • Construct result with McpSchema.ReadResourceResult and TextResourceContents

Resource URI Conventions

Standard Patterns

// 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"

Multi-Resource Publisher

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}"""
}

Prompt Basics

Creating Prompts with McpPromptFactory

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 documentation
  • Goal implements Named and Described interfaces
  • Factory generates JSON schema from type structure
  • Optional fields use nullable types with defaults

Async Prompts

import 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
)

Input Type Design Patterns

Simple Input

data class SimpleInput(
    @JsonPropertyDescription("The operation to perform")
    val operation: String
)

Complex Input with Nested Types

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
)

List and Map Inputs

data class BatchInput(
    @JsonPropertyDescription("List of item IDs to process")
    val items: List<String>,

    @JsonPropertyDescription("Configuration parameters")
    val parameters: Map<String, String> = emptyMap()
)

Combining Resources and Prompts

Unified Publisher

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>
)

Best Practices

Resource Best Practices

1. Use Appropriate Resource Types

// 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"
)

2. Choose Correct MIME Types

// Markdown documentation
mimeType = "text/markdown"

// JSON data
mimeType = "application/json"

// Plain text
mimeType = "text/plain"

// HTML content
mimeType = "text/html"

3. Provide Clear Descriptions

// Good: Specific and helpful
description = "Current system metrics including CPU, memory, and disk usage"

// Bad: Vague
description = "Metrics"

Prompt Best Practices

1. Document All Fields

// 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
)

2. Use Optional Fields Appropriately

// 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
)

3. Validate Input Types

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" }
    }
}

Error Handling

Graceful Resource Failures

@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)
    }
}

Testing

Testing Resource Publishers

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"))
    }
}

Testing Prompt Publishers

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)
    }
}

Related Documentation

  • Creating Publishers - Publisher implementation patterns
  • Execution Modes - Sync vs async mode selection
  • Factories API - Complete factory API reference
  • Publishers API - Publisher interface details
tessl i tessl/maven-com-embabel-agent--embabel-agent-mcpserver@0.3.1

docs

index.md

tile.json