Reference: AI: MCP SDK

pro
This is an http4k Pro feature and is licensed under the http4k Commercial License Find out more

Installation (Gradle)

dependencies {
    
    implementation(platform("org.http4k:http4k-bom:6.9.2.0"))

    implementation("org.http4k.pro:http4k-mcp-sdk")
}

About

The Model Context Protocol is an open standard created by Anthropic that defines how apps can feed information to AI language models. It creates a uniform way to link these models with various data sources and tools, which streamlines the integration process. MCP services can be deployed in a Server or Serverless environment.

MCP itself is based on the JSON RPC standard, which is used to communicate between the client and server. Messages are sent from client to server and then asynchronously from server to client. MCP defines a set of standardised capabilities which can be provided by the server or client. One use of these capabilities is to allow pre-trained models to have access to both live data and APIs that can be used by the model to provide answers to user requests. Another use is to provide agentic behaviour by providing standard communications between several MCP entities.

Currently the MCP standard supports the following transports:

  • HTTP Steaming: Clients can interact with the MCP server by either a SSE connection (accepting application/event-stream or via plain HTTP (accepting application/json). Stream resumption and replay is supported on the SSE connection by calling GET with the Last-Event-ID header. All traffic is served by calling the /mcp endpoint.
  • Server Sent Events + HTTP: Clients initiate an SSE connection to the server (on /sse) which is used to send messages to the client asynchronously at any time. Further client -> server requests and notifications are sent via HTTP /messages endpoint, with responses being sent back via the SSE.
  • Standard IO: Clients start a process that communicates with the server via JSON RPC messages via standard input and output streams.

The MCP capabilities include:

  • Tools: are exposed by the server and can be used by the client to perform tasks. The tool consists of a description and a JSON Schema definition for the inputs and outputs.
  • Prompts: given a set of inputs by a client, the server can generate a prompt parameterised to those inputs. This allows servers to generate prompts that are tailored to the client’s data.
  • Resources: are exposed by the server and can be used by the client to access text or binary content an example of this is a browser tool that can access web pages.
  • Roots: the client supplies a list of file roots to the server which can be used to resolve file paths.
  • Completions: The server provides auto-completion of options for a particular Prompt or Resource.
  • Sampling: An MCP server can request an LLN completion for text or binary content a connected Client.

http4k ❤️ Model Context Protocol

http4k provides very good support for the Model Context Protocol, and has been designed to make it easy to build your own MCP-compliant servers in Kotlin, using the familiar http4k methodology of simple and composable functional protocols. Each of the capabilities is modelled as a binding between a capability description and a function that exposes the capability. See Capability Types for more details.

The MCP support in http4k consists of two parts - the http4k-mcp-sdk and the http4k-mcp-desktop application which is used to connect the MCP server to a desktop client such as Claude Desktop.

SDK: http4k-mcp-sdk

The core SDK for working with the Model Context Protocol. You can build your own MCP-compliant applications using this module by plugging in capabilities into the server. The http4k-mcp-sdk module provides a simple way to create either HTTP Streaming, SSE, StdIo or Websocket based servers. For StdIo-based servers, we recommend compiling your server to GraalVM for ease of distribution.

Capabilities

The MCP protocol is based on a set of capabilities that can be provided by the server or client. Each capability can be installed separately into the server, and the client can interact with the server using these capabilities.

Capability: Tools

Tools allow external MCP clients such as LLMs to request the server to perform bespoke functionality such as invoking an API. The Tool capability is modelled as a function typealias ToolHandler = (ToolRequest) -> ToolResponse, and can be bound to a tool definition which describes it’s arguments and outputs using the http4k Lens system:

package content.ecosystem.http4k.reference.mcp

import org.http4k.lens.with
import org.http4k.mcp.ToolHandler
import org.http4k.mcp.ToolRequest
import org.http4k.mcp.ToolResponse
import org.http4k.mcp.model.Content
import org.http4k.mcp.model.Tool
import org.http4k.mcp.model.localDate
import java.time.LocalDate

// tool argument inputs are typesafe lens
val toolArg = Tool.Arg.localDate().required("date", "date in format yyyy-mm-dd")

// the description of the tool exposed to clients
fun toolDefinitionFor(name: String): Tool = Tool(
    "diary_for_${name.replace(" ", "_")}",
    "details $name's diary appointments. Responds with a list of appointments for the given month",
    toolArg,
)

// handles the actual call to tht tool
val diaryToolHandler: ToolHandler = {
    val calendarData = mapOf(
        LocalDate.of(2025, 3, 21) to listOf(
            "08:00 - Breakfast meeting",
            "11:00 - Dentist appointment",
            "16:00 - Project review"
        )
    )

    val date = toolArg(it)
    val appointmentContent = calendarData[date]?.map { Content.Text("$date: $it") } ?: emptyList()

    ToolResponse.Ok(appointmentContent)
}

object DiaryTool {
    @JvmStatic
    fun main() = println(
        // invoke/test the tool offline - just invoke it like a function
        diaryToolHandler(
            ToolRequest().with(Tool.Arg.localDate().required("date") of LocalDate.parse("2025-03-21"))
        )
    )
}

Complex Tools request arguments

The http4k MCP SDK also supports handling of complex arguments in the request. This can be done by using the auto() extension function and passing an example argument instance in order that the complex JSON schema can be rendered. Note that the Kotlin Reflection JAR also needs to be present on the classpath to take advantage of this feature, or you can supply a custom instance of ConfigurableMcpJson (Moshi-based) to work without reflection (we recommend the use of the Kotshi compiler plugin to generate adapters for this use-case).

package content.ecosystem.http4k.reference.mcp

import org.http4k.lens.with
import org.http4k.mcp.ToolResponse
import org.http4k.mcp.model.Tool
import org.http4k.mcp.util.McpJson.auto
import org.http4k.routing.bind

// a complex response object
data class MavenJar(val org: String, val name: String, val version: Int)

// the auto() method is imported from McpJson (requires Kotlin Reflect)
val libDescription = Tool.Arg
    .auto(MavenJar("org.http4k", "http4k-mcp-sdk", 6))
    .required("the maven dependency")

val nextVersion = Tool.Output.auto(MavenJar("org.http4k", "http4k-mcp-sdk", 6)).toLens()

val getNextVersion = Tool(
    "nextVersion",
    "get the next maven version for a library",
    libDescription,
    output = nextVersion
)

object MavenTool {
    @JvmStatic
    fun main() = println(
        getNextVersion bind {
            // we can extract the class automatically using the lens
            val lib: MavenJar = libDescription(it)

            // and then inject the typesafe response object!
            ToolResponse.Ok().with(nextVersion of lib.copy(version = lib.version + 1))
        }
    )
}

Capability: Prompts

Prompts allow the server to generate a prompt based on the client’s inputs. The Prompt capability is modelled as a function (PromptRequest) -> PromptResponse, and can be bound to a prompt definition which describes it’s arguments using the http4k Lens system.

package content.ecosystem.http4k.reference.mcp

import org.http4k.connect.model.Role.Companion.Assistant
import org.http4k.lens.int
import org.http4k.lens.with
import org.http4k.mcp.PromptHandler
import org.http4k.mcp.PromptRequest
import org.http4k.mcp.PromptResponse
import org.http4k.mcp.model.Content
import org.http4k.mcp.model.Message
import org.http4k.mcp.model.Prompt
import org.http4k.mcp.model.PromptName

// argument lenses for the prompt
val name = Prompt.Arg.required("name", "the name of the person to greet")
val age = Prompt.Arg.int().optional("age", "the age of the person to greet")


// the description of the prompt
val prompt: Prompt = Prompt(PromptName.of("Greet"), "Creates a greeting message for a person", name, age)

// handles the actual call to tht prompt
val greetingPromptHandler: PromptHandler = { req: PromptRequest ->
    val content = when (age(req)) {
        null -> Content.Text("Hello, ${name(req)}!")
        else -> Content.Text("Hello, ${name(req)}! How is req being ${age(req)}?")
    }
    PromptResponse(listOf(Message(Assistant, content)))
}


object GreetPersonPrompt {
    @JvmStatic
    fun main() = println(
        // invoke/test the prompt offline - just invoke it like a function
        greetingPromptHandler(
            PromptRequest().with(
                name of "David",
                age of 30
            )
        )
    )
}

Capability: Resources

Resources provide a way to interrogate the contents of data sources such as filesystem, database or website. The Resource capability is modelled as a function (ResourceRequest) -> ResourceResponse. Resources can be static or templated to provide bounds within which the client can interact with the resource.

package content.ecosystem.http4k.reference.mcp

import org.http4k.client.JavaHttpClient
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Uri
import org.http4k.mcp.ResourceHandler
import org.http4k.mcp.ResourceRequest
import org.http4k.mcp.ResourceResponse
import org.http4k.mcp.model.Resource
import org.http4k.mcp.model.ResourceName
import org.jsoup.Jsoup


val websiteResource = Resource.Static(Uri.of("https://http4k.org"), ResourceName.of("HTTP4K"), "description")

// this function provides a static resource that contains all the links from the http4k website
val getLinksResourceHandler: ResourceHandler = {
    val htmlPage = JavaHttpClient()(Request(GET, it.uri))

    val links = getAllLinksFrom(htmlPage)
        .map { Resource.Content.Text(it.text(), Uri.of(it.attr("href"))) }

    ResourceResponse(links)
}

private fun getAllLinksFrom(htmlPage: Response) = Jsoup.parse(htmlPage.bodyString())
    .allElements.toList()
    .filter { it.tagName() == "a" }
    .filter { it.hasAttr("href") }

object LookupAllLinksFromWebResource {
    @JvmStatic
    fun main() = println(
        // invoke/test the prompt offline - just invoke it like a function
        getLinksResourceHandler(ResourceRequest(Uri.of("https://http4k.org")))
    )
}

Capability: Roots

Roots are provided by the client to the server and determine the base paths that the server can use to act within.

Composing MCP Capabilities

http4k MCP lets you combine any number of related capabilities into reusable collections using the CapabilityPack API. This is perfect for organizing related tools, resources, or prompts that logically belong together and shipping them as a module or library.

package content.ecosystem.http4k.reference.mcp

import org.http4k.mcp.server.capability.CapabilityPack
import org.http4k.routing.bind

fun SetOfCapabilities() = CapabilityPack(
    toolDefinitionFor("David") bind diaryToolHandler,
    promptReference bind completionHandler,
    websiteResource bind getLinksResourceHandler,
    prompt bind greetingPromptHandler
)

MCP Servers

Servers are created by combining the configured MCP Protocol with a set of capabilities, an optional security, and a binding to a Server or Serverless backend. The server can be started using any of the http4k server backends which support SSE ( see servers).

package content.ecosystem.http4k.reference.mcp

import org.http4k.core.Uri
import org.http4k.mcp.model.McpEntity
import org.http4k.mcp.protocol.ServerMetaData
import org.http4k.mcp.protocol.ServerProtocolCapability.ToolsChanged
import org.http4k.mcp.protocol.Version
import org.http4k.mcp.server.security.OAuthMcpSecurity
import org.http4k.routing.bind
import org.http4k.routing.mcpHttpStreaming
import org.http4k.server.Helidon
import org.http4k.server.asServer

fun main() {
    // call the correct protocol method here - there are 5 to choose from!
    val mcpServer = mcpHttpStreaming(
        // give the server an identity
        ServerMetaData(McpEntity.of("http4k MCP Server"), Version.of("1.0.0"), ToolsChanged),

        // insert a security implementation
        OAuthMcpSecurity(Uri.of("https://oauth-server")) { it == "my_oauth_token" },

        // bind server capabilities here ...
        toolDefinitionFor("David") bind diaryToolHandler,
        promptReference bind completionHandler,
        websiteResource bind getLinksResourceHandler,
        prompt bind greetingPromptHandler
    )

    // simply start it up!
    mcpServer.asServer(Helidon(3002)).start()
}

Alternatively you can use any non-SSE supporting server backend and forego the SSE support in lieu of request/response via JSON:

package content.ecosystem.http4k.reference.mcp

import org.http4k.mcp.model.McpEntity
import org.http4k.mcp.protocol.ServerMetaData
import org.http4k.mcp.protocol.ServerProtocolCapability.ToolsChanged
import org.http4k.mcp.protocol.Version
import org.http4k.mcp.server.security.NoMcpSecurity
import org.http4k.routing.bind
import org.http4k.routing.mcpHttpNonStreaming
import org.http4k.server.SunHttp
import org.http4k.server.asServer

fun main() {
    // this protocol version does not support SSE connections.
    val mcpServer = mcpHttpNonStreaming(
        ServerMetaData(McpEntity.of("http4k MCP Server"), Version.of("1.0.0"), ToolsChanged),
        NoMcpSecurity,
        toolDefinitionFor("David") bind diaryToolHandler,
    )

    // simply start it up on any server you like!
    mcpServer.asServer(SunHttp(3002)).start()
}

There are a number of different ways customise the MCP protocol server to suit your needs. Features that can be configured are shown below. Note that the main SDK library is designed for simplicity - and you may have to drill down one level to access some of these customisations:

  • Security - Basic, Bearer, API Key or auto-discovered (or custom!) OAuth (specification standard)
  • Session validation (via SessionProvider) - Ensure that the client is authenticated to access the contents of the session
  • Event Store (via SessionEventStore) - Store and resume MCP event streams using the SSE last-event-id header
  • Event Tracking (via SessionEventTracking) - Assign a unique ID to each event to track the progress of the event stream
  • Origin validation (via Filter and SseFilter) - Protect against DNS rebinding attacks by configuring allowed origins

Important: Protecting Against DNS Rebinding Attacks

When deploying an MCP server that uses HTTP Streaming or SSE, you must implement Origin header validation to prevent DNS rebinding attacks. These attacks can allow malicious websites to interact with your MCP server by changing IP addresses after initial DNS resolution, potentially bypassing same-origin policy protections. This can be done by implementing the HTTP (Filter) and SSE specific (SseFilter) filter implementations and attaching them to the Polyhandler that is returned from the mcpXXX() call.

The http4k-mcp-sdk provides protection mechanisms that can be applied to your server:

package content.ecosystem.http4k.reference.mcp

import org.http4k.core.Method.DELETE
import org.http4k.core.Method.GET
import org.http4k.core.Method.POST
import org.http4k.core.Uri
import org.http4k.core.then
import org.http4k.filter.AnyOf
import org.http4k.filter.CorsAndRebindProtection
import org.http4k.filter.CorsPolicy
import org.http4k.filter.OriginPolicy
import org.http4k.filter.ServerFilters
import org.http4k.mcp.model.McpEntity
import org.http4k.mcp.protocol.ServerMetaData
import org.http4k.mcp.protocol.ServerProtocolCapability.ToolsChanged
import org.http4k.mcp.protocol.Version
import org.http4k.mcp.server.security.BearerAuthMcpSecurity
import org.http4k.mcp.server.security.NoMcpSecurity
import org.http4k.mcp.server.security.OAuthMcpSecurity
import org.http4k.routing.bind
import org.http4k.routing.mcpHttpStreaming
import org.http4k.server.Helidon
import org.http4k.server.asServer

fun main() {
    val mcpServer = mcpHttpStreaming(
        ServerMetaData(McpEntity.of("http4k MCP Server"), Version.of("1.0.0"), ToolsChanged),
        BearerAuthMcpSecurity { it == "my_bearer_token" },
        toolDefinitionFor("David") bind diaryToolHandler,
    )

    // Define a CORS policy to protect against cross-origin requests and DNS rebinding attacks
    val corsPolicy = CorsPolicy(
        OriginPolicy.AnyOf("foo.com", "localhost"),
        listOf("allowed-header"), listOf(GET, POST, DELETE)
    )

    ServerFilters.CorsAndRebindProtection(corsPolicy)
        .then(mcpServer)
        .asServer(Helidon(3002)).start()
}

Serverless Example

MCP capabilities can be bound to http4k Serverless functions using the HTTP protocol in non-streaming mode. To activate this simply bind them into the non-streaming HTTP which is a simple HttpHandler.

package content.ecosystem.http4k.reference.mcp

import org.http4k.mcp.model.McpEntity
import org.http4k.mcp.protocol.ServerMetaData
import org.http4k.mcp.protocol.Version
import org.http4k.mcp.server.security.NoMcpSecurity
import org.http4k.routing.bind
import org.http4k.routing.mcpHttpNonStreaming
import org.http4k.serverless.ApiGatewayV2LambdaFunction
import org.http4k.serverless.AppLoader

// This function is an AWS Lambda function.
class McpLambdaFunction : ApiGatewayV2LambdaFunction(AppLoader {
    mcpHttpNonStreaming(
        ServerMetaData(McpEntity.of("http4k mcp over serverless"), Version.of("0.1.0")),
        NoMcpSecurity,
        toolDefinitionFor("David") bind diaryToolHandler
    )
})

MCP Client

http4k provides client classes to connect to your MCP servers via HTTP, SSE, JSONRPC or Websockets. The clients take care of the initial MCP handshake and provide a simple API to send and receive messages to the capabilities, or to register for notifications with an MCP server.

package content.ecosystem.http4k.reference.mcp

import org.http4k.client.JavaHttpClient
import org.http4k.connect.model.ToolName
import org.http4k.core.BodyMode
import org.http4k.core.Uri
import org.http4k.lens.int
import org.http4k.lens.localDate
import org.http4k.lens.with
import org.http4k.mcp.CompletionRequest
import org.http4k.mcp.PromptRequest
import org.http4k.mcp.ResourceRequest
import org.http4k.mcp.ToolRequest
import org.http4k.mcp.client.http.HttpStreamingMcpClient
import org.http4k.mcp.model.McpEntity
import org.http4k.mcp.model.Prompt
import org.http4k.mcp.model.PromptName
import org.http4k.mcp.model.Reference
import org.http4k.mcp.model.Tool
import org.http4k.mcp.model.localDate
import org.http4k.mcp.protocol.Version
import java.time.LocalDate

fun main() {
    val client = HttpStreamingMcpClient(
        McpEntity.of("http4k MCP Client"), Version.of("1.0.0"),
        Uri.of("http://localhost:3001/mcp"),
        JavaHttpClient(responseBodyMode = BodyMode.Stream)
    )

    println(
        ">>> Server handshake\n" +
            client.start()
    )

    println(
        ">>> Tool list\n" +
            client.tools().list()
    )

    println(
        ">>> Tool calling\n" +
            client.tools().call(
                ToolName.of("diary_for_David"),
                ToolRequest().with(
                    Tool.Arg.localDate().required("date") of LocalDate.parse("2025-03-21")
                )
            )
    )

    println(
        ">>> Prompt list\n" +
            client.prompts().list()
    )

    println(
        ">>> Prompt calling\n" +
            client.prompts().get(
                PromptName.of("Greet"),
                PromptRequest().with(
                    Prompt.Arg.required("name") of "David",
                    Prompt.Arg.int().optional("age") of 30
                )
            )
    )

    println(
        ">>> Completions\n" +
            client.completions().complete(
                Reference.Prompt("Greet"),
                CompletionRequest("prefix", "Al")
            )
    )

    println(
        ">>> Resource list\n" +
            client.resources().list()
    )

    println(
        ">>> Resource reading\n" +
            client.resources().read(
                ResourceRequest(Uri.of("https://http4k.org"))
            )
    )

    client.close()
}

http4k-mcp-desktop

A desktop client that bridges StdIo-bound desktop clients such as Claude Desktop with your own MCP servers operating over HTTP/SSE, either locally or remotely. The desktop client is a simple native application that can be downloaded from the http4k GitHub, or built from the http4k source.

To use mcp-desktop client with clients such as Claude Desktop or Cursor:

  1. Download the mcp-desktop binary for your platform from: [https://github.com/http4k/mcp-desktop], or install it with brew:
brew tap http4k/tap
brew install http4k-mcp-desktop
  1. Configure Claude Desktop to use the mcp-desktop binary as an MCP server with the following configuration. You can find the configuration file in claude_desktop_config.json, or by browsing through the developer settings menu. You can add as many MCP servers as you like. Note that Cursor users should use the --transport http-nonstream or --transport jsonrpc option for correct integration:
{
    "mcpServers": {
        "MyMcpServer": {
            "command": "http4k-mcp-desktop",
            // or path to the binary
            "args": [
                "--transport",
                "--http-stream",
                "--url",
                "http://localhost:3001/mcp"
            ]
        }
    }
}

To build mcp-desktop from source:

  1. Clone the http4k MCP Desktop repo
  2. Install a GraalVM supporting JDK
  3. Run ./gradlew :native-compile to build the desktop client binary locally for your platform