CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-org-springframework-ai--spring-ai-starter-mcp-server

Spring Boot Starter for building Model Context Protocol (MCP) servers with auto-configuration, annotation-based tool/resource/prompt definitions, and support for STDIO, SSE, and Streamable-HTTP transports

Overview
Eval results
Files

programmatic-registration.mddocs/reference/

Programmatic Bean Registration

Low-level approach for registering MCP specifications as Spring beans. This provides maximum control over tool, resource, prompt, and completion registration.

Capabilities

Tool Specification Registration

Register tools using MCP tool specifications.

/**
 * Synchronous tool specification (Java record)
 * Create instances with: new SyncToolSpecification(tool, handler)
 */
record SyncToolSpecification(
    /**
     * The tool definition
     */
    Tool tool,

    /**
     * The tool implementation handler
     */
    BiFunction<McpSyncServerExchange, CallToolRequest, CallToolResult> handler
) {}

/**
 * Asynchronous tool specification (Java record)
 * Create instances with: new AsyncToolSpecification(tool, handler)
 */
record AsyncToolSpecification(
    /**
     * The tool definition
     */
    Tool tool,

    /**
     * The async tool implementation handler
     */
    BiFunction<McpAsyncServerExchange, CallToolRequest, Mono<CallToolResult>> handler
) {}

Usage Examples:

Synchronous tool registration:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springaicommunity.mcp.server.McpServerFeatures.SyncToolSpecification;
import org.springaicommunity.mcp.schema.McpSchema.*;

@Configuration
public class ToolConfig {

    @Bean
    public List<SyncToolSpecification> calculatorTools() {
        // Define add tool
        Tool addTool = new Tool(
            "add",
            "Add two numbers",
            Map.of(
                "type", "object",
                "properties", Map.of(
                    "a", Map.of("type", "number", "description", "First number"),
                    "b", Map.of("type", "number", "description", "Second number")
                ),
                "required", List.of("a", "b")
            )
        );

        // Create specification with handler
        SyncToolSpecification addSpec = new SyncToolSpecification(
            addTool,
            (exchange, request) -> {
                double a = ((Number) request.arguments().get("a")).doubleValue();
                double b = ((Number) request.arguments().get("b")).doubleValue();
                double result = a + b;

                return CallToolResult.builder()
                    .addTextContent("Result: " + result)
                    .build();
            }
        );

        // Define multiply tool
        Tool multiplyTool = new Tool(
            "multiply",
            "Multiply two numbers",
            Map.of(
                "type", "object",
                "properties", Map.of(
                    "a", Map.of("type", "number"),
                    "b", Map.of("type", "number")
                ),
                "required", List.of("a", "b")
            )
        );

        SyncToolSpecification multiplySpec = new SyncToolSpecification(
            multiplyTool,
            (exchange, request) -> {
                double a = ((Number) request.arguments().get("a")).doubleValue();
                double b = ((Number) request.arguments().get("b")).doubleValue();
                double result = a * b;

                return CallToolResult.builder()
                    .addTextContent("Result: " + result)
                    .build();
            }
        );

        return List.of(addSpec, multiplySpec);
    }
}

Tool with logging and progress:

@Bean
public List<SyncToolSpecification> advancedTools() {
    Tool processTool = new Tool(
        "process",
        "Process data with logging and progress",
        Map.of(
            "type", "object",
            "properties", Map.of(
                "data", Map.of("type", "string", "description", "Data to process")
            ),
            "required", List.of("data")
        )
    );

    SyncToolSpecification spec = new SyncToolSpecification(
        processTool,
        (exchange, request) -> {
            String data = (String) request.arguments().get("data");

            // Log to client
            exchange.loggingNotification(LoggingMessageNotification.builder()
                .level(LoggingLevel.INFO)
                .logger("process-tool")
                .data("Starting processing")
                .build());

            // Send progress
            exchange.progressNotification(ProgressNotification.builder()
                .progressToken(request.progressToken())
                .progress(0.0)
                .total(1.0)
                .message("Starting")
                .build());

            // Process data
            String result = processData(data);

            exchange.progressNotification(ProgressNotification.builder()
                .progressToken(request.progressToken())
                .progress(1.0)
                .total(1.0)
                .message("Complete")
                .build());

            return CallToolResult.builder()
                .addTextContent("Processed: " + result)
                .build();
        }
    );

    return List.of(spec);
}

Asynchronous tool registration:

@Bean
public List<AsyncToolSpecification> asyncTools() {
    Tool asyncTool = new Tool(
        "async-process",
        "Async data processing",
        Map.of("type", "object", "properties", Map.of("input", Map.of("type", "string")))
    );

    AsyncToolSpecification spec = new AsyncToolSpecification(
        asyncTool,
        (exchange, request) -> {
            String input = (String) request.arguments().get("input");

            return exchange.loggingNotification(LoggingMessageNotification.builder()
                    .level(LoggingLevel.INFO)
                    .data("Processing async")
                    .build())
                .then(processDataAsync(input))
                .map(result -> CallToolResult.builder()
                    .addTextContent("Result: " + result)
                    .build());
        }
    );

    return List.of(spec);
}

Resource Specification Registration

Register resources using MCP resource specifications.

/**
 * Synchronous resource specification (Java record)
 * Create instances with: new SyncResourceSpecification(resource, handler)
 */
record SyncResourceSpecification(
    /**
     * The resource definition
     */
    Resource resource,

    /**
     * The resource read handler
     */
    BiFunction<McpSyncServerExchange, ReadResourceRequest, ReadResourceResult> handler
) {}

/**
 * Asynchronous resource specification (Java record)
 * Create instances with: new AsyncResourceSpecification(resource, handler)
 */
record AsyncResourceSpecification(
    /**
     * The resource definition
     */
    Resource resource,

    /**
     * The async resource read handler
     */
    BiFunction<McpAsyncServerExchange, ReadResourceRequest, Mono<ReadResourceResult>> handler
) {}

Usage Example:

@Configuration
public class ResourceConfig {

    @Bean
    public List<SyncResourceSpecification> systemResources() {
        // System info resource
        Resource systemInfoResource = new Resource(
            "system://info",
            "System Information",
            "Provides system information",
            "application/json"
        );

        SyncResourceSpecification systemInfoSpec = new SyncResourceSpecification(
            systemInfoResource,
            (exchange, request) -> {
                Map<String, Object> systemInfo = Map.of(
                    "os", System.getProperty("os.name"),
                    "javaVersion", System.getProperty("java.version"),
                    "processors", Runtime.getRuntime().availableProcessors()
                );

                String jsonContent = new ObjectMapper().writeValueAsString(systemInfo);

                return new ReadResourceResult(
                    List.of(new TextResourceContents(
                        request.uri(),
                        "application/json",
                        jsonContent
                    ))
                );
            }
        );

        return List.of(systemInfoSpec);
    }
}

Resource Template Specification Registration

Register resource templates with URI placeholders.

/**
 * Synchronous resource template specification (Java record)
 * Create instances with: new SyncResourceTemplateSpecification(template, handler)
 */
record SyncResourceTemplateSpecification(
    /**
     * The resource template with URI placeholders
     */
    ResourceTemplate template,

    /**
     * The resource read handler
     */
    BiFunction<McpSyncServerExchange, ReadResourceRequest, ReadResourceResult> handler
) {}

/**
 * Asynchronous resource template specification (Java record)
 * Create instances with: new AsyncResourceTemplateSpecification(template, handler)
 */
record AsyncResourceTemplateSpecification(
    /**
     * The resource template
     */
    ResourceTemplate template,

    /**
     * The async resource read handler
     */
    BiFunction<McpAsyncServerExchange, ReadResourceRequest, Mono<ReadResourceResult>> handler
) {}

Usage Example:

@Bean
public List<SyncResourceTemplateSpecification> templateResources() {
    ResourceTemplate configTemplate = new ResourceTemplate(
        "config://{key}",
        "Configuration Value",
        "Retrieve configuration by key",
        "text/plain"
    );

    SyncResourceTemplateSpecification spec = new SyncResourceTemplateSpecification(
        configTemplate,
        (exchange, request) -> {
            // Extract key from URI (e.g., "config://database" -> "database")
            String key = extractKeyFromUri(request.uri());
            String value = configurationService.get(key);

            return new ReadResourceResult(
                List.of(new TextResourceContents(
                    request.uri(),
                    "text/plain",
                    value
                ))
            );
        }
    );

    return List.of(spec);
}

Prompt Specification Registration

Register prompts using MCP prompt specifications.

/**
 * Synchronous prompt specification (Java record)
 * Create instances with: new SyncPromptSpecification(prompt, handler)
 */
record SyncPromptSpecification(
    /**
     * The prompt definition
     */
    Prompt prompt,

    /**
     * The prompt handler
     */
    BiFunction<McpSyncServerExchange, GetPromptRequest, GetPromptResult> handler
) {}

/**
 * Asynchronous prompt specification (Java record)
 * Create instances with: new AsyncPromptSpecification(prompt, handler)
 */
record AsyncPromptSpecification(
    /**
     * The prompt definition
     */
    Prompt prompt,

    /**
     * The async prompt handler
     */
    BiFunction<McpAsyncServerExchange, GetPromptRequest, Mono<GetPromptResult>> handler
) {}

Usage Example:

@Configuration
public class PromptConfig {

    @Bean
    public List<SyncPromptSpecification> customPrompts() {
        Prompt greetingPrompt = new Prompt(
            "greeting",
            "Generate a personalized greeting",
            List.of(
                new PromptArgument("name", "User's name", true),
                new PromptArgument("language", "Language for greeting", false)
            )
        );

        SyncPromptSpecification spec = new SyncPromptSpecification(
            greetingPrompt,
            (exchange, request) -> {
                String name = request.arguments().get("name");
                String language = request.arguments().getOrDefault("language", "en");

                String greeting = switch (language) {
                    case "es" -> "¡Hola, " + name + "!";
                    case "fr" -> "Bonjour, " + name + "!";
                    case "de" -> "Guten Tag, " + name + "!";
                    default -> "Hello, " + name + "!";
                };

                return new GetPromptResult(
                    "Personalized greeting",
                    List.of(new PromptMessage(
                        Role.ASSISTANT,
                        new TextContent(greeting + " How can I help you today?")
                    ))
                );
            }
        );

        return List.of(spec);
    }
}

Completion Specification Registration

Register completion providers using MCP completion specifications.

/**
 * Synchronous completion specification (Java record)
 * Create instances with: new SyncCompletionSpecification(ref, handler)
 */
record SyncCompletionSpecification(
    /**
     * The prompt reference for completions
     */
    PromptReference ref,

    /**
     * The completion handler
     */
    BiFunction<McpSyncServerExchange, CompleteRequest, CompleteResult> handler
) {}

/**
 * Asynchronous completion specification (Java record)
 * Create instances with: new AsyncCompletionSpecification(ref, handler)
 */
record AsyncCompletionSpecification(
    /**
     * The prompt reference
     */
    PromptReference ref,

    /**
     * The async completion handler
     */
    BiFunction<McpAsyncServerExchange, CompleteRequest, Mono<CompleteResult>> handler
) {}

Usage Example:

@Configuration
public class CompletionConfig {

    private final List<String> cities = List.of(
        "New York", "Los Angeles", "Chicago", "Houston", "Phoenix",
        "Philadelphia", "San Antonio", "San Diego", "Dallas", "San Jose"
    );

    @Bean
    public List<SyncCompletionSpecification> completions() {
        PromptReference cityPromptRef = new PromptReference(
            "ref/prompt",
            "city-search",
            "City name completion"
        );

        SyncCompletionSpecification spec = new SyncCompletionSpecification(
            cityPromptRef,
            (exchange, request) -> {
                String prefix = request.argument().value().toLowerCase();

                List<String> matches = cities.stream()
                    .filter(city -> city.toLowerCase().startsWith(prefix))
                    .limit(10)
                    .toList();

                return new CompleteResult(
                    new CompleteResult.CompleteCompletion(
                        matches,
                        matches.size(),
                        false
                    )
                );
            }
        );

        return List.of(spec);
    }
}

Stateless Specifications

For stateless servers, use stateless specification variants:

/**
 * Stateless synchronous tool specification
 */
interface StatelessSyncToolSpecification {
    Tool tool();
    BiFunction<McpTransportContext, CallToolRequest, CallToolResult> handler();
}

/**
 * Stateless asynchronous tool specification
 */
interface StatelessAsyncToolSpecification {
    Tool tool();
    BiFunction<McpTransportContext, CallToolRequest, Mono<CallToolResult>> handler();
}

// Similar stateless variants exist for:
// - StatelessSyncResourceSpecification
// - StatelessAsyncResourceSpecification
// - StatelessSyncResourceTemplateSpecification
// - StatelessAsyncResourceTemplateSpecification
// - StatelessSyncPromptSpecification
// - StatelessAsyncPromptSpecification

Usage Example:

@Bean
public List<StatelessSyncToolSpecification> statelessTools() {
    Tool simpleTool = new Tool("simple", "Simple stateless tool", simpleSchema);

    StatelessSyncToolSpecification spec = new StatelessSyncToolSpecification(
        simpleTool,
        (context, request) -> {
            // No bidirectional operations available
            String input = (String) request.arguments().get("input");
            return CallToolResult.builder()
                .addTextContent("Processed: " + input)
                .build();
        }
    );

    return List.of(spec);
}

Exchange Objects

Handlers receive exchange objects providing access to MCP operations:

/**
 * Synchronous server exchange
 */
interface McpSyncServerExchange {
    /**
     * Send a logging notification to the client
     */
    void loggingNotification(LoggingMessageNotification notification);

    /**
     * Send a progress notification to the client
     */
    void progressNotification(ProgressNotification notification);

    /**
     * Ping the client
     */
    void ping();

    /**
     * Request user input via elicitation (stateful only)
     */
    <T> StructuredElicitResult<T> elicit(Class<T> type);

    /**
     * Request LLM sampling (stateful only)
     */
    CreateMessageResult createMessage(CreateMessageRequest request);
}
/**
 * Asynchronous server exchange
 */
interface McpAsyncServerExchange {
    /**
     * Send a logging notification (reactive)
     */
    Mono<Void> loggingNotification(LoggingMessageNotification notification);

    /**
     * Send a progress notification (reactive)
     */
    Mono<Void> progressNotification(ProgressNotification notification);

    /**
     * Ping the client (reactive)
     */
    Mono<Void> ping();

    /**
     * Request user input (reactive, stateful only)
     */
    <T> Mono<StructuredElicitResult<T>> elicit(Class<T> type);

    /**
     * Request LLM sampling (reactive, stateful only)
     */
    Mono<CreateMessageResult> createMessage(CreateMessageRequest request);
}

Complete Configuration Example

@Configuration
public class McpSpecificationConfig {

    @Bean
    public List<SyncToolSpecification> tools() {
        return List.of(
            createCalculatorTool(),
            createWeatherTool()
        );
    }

    @Bean
    public List<SyncResourceSpecification> resources() {
        return List.of(
            createSystemInfoResource(),
            createStatusResource()
        );
    }

    @Bean
    public List<SyncResourceTemplateSpecification> resourceTemplates() {
        return List.of(
            createConfigResource()
        );
    }

    @Bean
    public List<SyncPromptSpecification> prompts() {
        return List.of(
            createGreetingPrompt(),
            createSummaryPrompt()
        );
    }

    @Bean
    public List<SyncCompletionSpecification> completions() {
        return List.of(
            createCityCompletion()
        );
    }

    private SyncToolSpecification createCalculatorTool() {
        // Implementation...
    }

    // Other factory methods...
}

Comparison: Programmatic vs Annotations

FeatureProgrammaticAnnotations
Flexibility✓ Maximum controlLimited
VerbosityMore verboseConcise
Type SafetyRuntimeCompile-time
Schema ControlManualAutomatic
Dynamic Registration✓ EasyDifficult
Learning CurveSteeperGentle
Use CaseComplex requirements, dynamic toolsStandard use cases

When to Use Programmatic Registration

  • Dynamic Tools: Tools that change at runtime based on external data
  • Complex Logic: Handlers requiring complex initialization or dependencies
  • Custom Schemas: Need precise control over JSON schemas
  • Integration: Integrating with existing non-Spring systems
  • Performance: Optimizing for specific performance requirements
  • Testing: Easier to unit test handlers in isolation

Core Imports

// Specification interfaces (synchronous)
import org.springaicommunity.mcp.server.SyncToolSpecification;
import org.springaicommunity.mcp.server.SyncResourceSpecification;
import org.springaicommunity.mcp.server.SyncResourceTemplateSpecification;
import org.springaicommunity.mcp.server.SyncPromptSpecification;
import org.springaicommunity.mcp.server.SyncCompletionSpecification;

// Specification interfaces (asynchronous)
import org.springaicommunity.mcp.server.AsyncToolSpecification;
import org.springaicommunity.mcp.server.AsyncResourceSpecification;
import org.springaicommunity.mcp.server.AsyncResourceTemplateSpecification;
import org.springaicommunity.mcp.server.AsyncPromptSpecification;
import org.springaicommunity.mcp.server.AsyncCompletionSpecification;

// Stateless specification variants (synchronous)
import org.springaicommunity.mcp.server.StatelessSyncToolSpecification;
import org.springaicommunity.mcp.server.StatelessSyncResourceSpecification;
import org.springaicommunity.mcp.server.StatelessSyncPromptSpecification;

// Stateless specification variants (asynchronous)
import org.springaicommunity.mcp.server.StatelessAsyncToolSpecification;
import org.springaicommunity.mcp.server.StatelessAsyncResourceSpecification;
import org.springaicommunity.mcp.server.StatelessAsyncPromptSpecification;

// Server exchange types
import org.springaicommunity.mcp.server.McpSyncServerExchange;
import org.springaicommunity.mcp.server.McpAsyncServerExchange;

// MCP schema types
import org.springaicommunity.mcp.schema.McpSchema.CallToolRequest;
import org.springaicommunity.mcp.schema.McpSchema.CallToolResult;
import org.springaicommunity.mcp.schema.McpSchema.ReadResourceResult;
import org.springaicommunity.mcp.schema.McpSchema.TextResourceContents;
import org.springaicommunity.mcp.schema.McpSchema.GetPromptResult;
import org.springaicommunity.mcp.schema.McpSchema.PromptMessage;
import org.springaicommunity.mcp.schema.McpSchema.CompleteResult;
import org.springaicommunity.mcp.schema.McpSchema.Tool;

// Spring framework
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// Reactive types (for async specifications)
import reactor.core.publisher.Mono;

// Java standard library
import java.util.List;
import java.util.Map;
tessl i tessl/maven-org-springframework-ai--spring-ai-starter-mcp-server@1.1.0

docs

index.md

tile.json