Skip to content

Commit 8db4fd8

Browse files
authored
Merge pull request #412 from joreilly/mcp
mcp server
2 parents f25629d + 92a7c88 commit 8db4fd8

File tree

5 files changed

+182
-1
lines changed

5 files changed

+182
-1
lines changed

gradle/libs.versions.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ androidGradlePlugin = "8.9.3"
1010
koin = "4.0.4"
1111
koinCompose = "4.0.4"
1212
koinComposeMultiplatform = "4.0.4"
13-
ktor = "3.1.2"
13+
ktor = "3.1.1"
1414
osmdroidAndroid = "6.1.20"
1515
osmAndroidCompose = "0.0.5"
1616
slf4j = "2.0.16"
@@ -42,6 +42,8 @@ gradleVersionsPlugin = "0.51.0"
4242
shadowPlugin = "7.1.2"
4343
skie = "0.10.0"
4444

45+
mcp = "0.4.0"
46+
4547
minSdk = "24"
4648
compileSdk = "35"
4749
targetSdk = "34"
@@ -121,6 +123,10 @@ ktor-client-darwin = { group = "io.ktor", name = "ktor-client-darwin", version.r
121123
ktor-client-java = { group = "io.ktor", name = "ktor-client-java", version.ref = "ktor" }
122124
ktor-client-js = { group = "io.ktor", name = "ktor-client-js", version.ref = "ktor" }
123125

126+
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
127+
ktor-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
128+
129+
124130
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
125131
loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
126132

@@ -140,6 +146,7 @@ androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test" }
140146
androidx-compose-ui-test-junit = { group = "androidx.compose.ui", name = "ui-test-junit4" }
141147
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
142148

149+
mcp-kotlin = { group = "io.modelcontextprotocol", name = "kotlin-sdk", version.ref = "mcp" }
143150

144151
[bundles]
145152
ktor-common = ["ktor-client-core", "ktor-client-json", "ktor-client-logging", "ktor-client-serialization", "ktor-client-content-negotiation", "ktor-serialization-kotlinx-json"]
@@ -157,3 +164,4 @@ sqlDelight = { id = "app.cash.sqldelight", version.ref = "sqlDelight" }
157164
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
158165
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
159166
skie = { id = "co.touchlab.skie", version.ref = "skie" }
167+
kotlinJvm = { id = "org.jetbrains.kotlin.jvm" }

mcp-server/build.gradle.kts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
plugins {
2+
alias(libs.plugins.kotlinJvm)
3+
alias(libs.plugins.kotlinx.serialization)
4+
alias(libs.plugins.shadowPlugin)
5+
application
6+
}
7+
8+
dependencies {
9+
implementation(libs.mcp.kotlin)
10+
implementation(projects.common)
11+
}
12+
13+
java {
14+
toolchain {
15+
languageVersion = JavaLanguageVersion.of(17)
16+
}
17+
}
18+
19+
application {
20+
mainClass = "MainKt"
21+
}
22+
23+
tasks.shadowJar {
24+
archiveFileName.set("serverAll.jar")
25+
archiveClassifier.set("")
26+
manifest {
27+
attributes["Main-Class"] = "MainKt"
28+
}
29+
}
30+

mcp-server/src/main/kotlin/main.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Entry point.
3+
* It initializes and runs the appropriate server mode based on the input arguments.
4+
*
5+
* Command-line arguments passed to the application:
6+
* - args[0]: Specifies the server mode. Supported values are:
7+
* - "--sse-server": Runs the SSE MCP server.
8+
* - "--stdio": Runs the MCP server using standard input/output.
9+
* Defaults to "--sse-server" if not provided.
10+
* - args[1]: Specifies the port number for the server. Defaults to 3001 if not provided or invalid.
11+
*/
12+
13+
14+
fun main(args: Array<String>) {
15+
val command = args.firstOrNull() ?: "--sse-server"
16+
val port = args.getOrNull(1)?.toIntOrNull() ?: 3001
17+
when (command) {
18+
"--sse-server" -> `run sse mcp server`(port)
19+
"--stdio" -> `run mcp server using stdio`()
20+
else -> {
21+
System.err.println("Unknown command: $command")
22+
}
23+
}
24+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import com.surrus.common.di.initKoin
2+
import com.surrus.common.repository.PeopleInSpaceRepository
3+
import io.ktor.server.application.*
4+
import io.ktor.server.cio.*
5+
import io.ktor.server.engine.*
6+
import io.ktor.server.routing.*
7+
import io.ktor.server.sse.*
8+
import io.ktor.util.collections.*
9+
import io.ktor.utils.io.streams.*
10+
import io.modelcontextprotocol.kotlin.sdk.*
11+
import io.modelcontextprotocol.kotlin.sdk.server.Server
12+
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
13+
import io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport
14+
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
15+
import kotlinx.coroutines.Job
16+
import kotlinx.coroutines.runBlocking
17+
import kotlinx.io.asSink
18+
import kotlinx.io.buffered
19+
20+
21+
private val koin = initKoin(enableNetworkLogs = true).koin
22+
23+
fun configureServer(): Server {
24+
val peopleInSpaceRepository = koin.get<PeopleInSpaceRepository>()
25+
26+
val server = Server(
27+
Implementation(
28+
name = "mcp-kotlin PeopleInSpace server",
29+
version = "1.0.0"
30+
),
31+
ServerOptions(
32+
capabilities = ServerCapabilities(
33+
prompts = ServerCapabilities.Prompts(listChanged = true),
34+
resources = ServerCapabilities.Resources(subscribe = true, listChanged = true),
35+
tools = ServerCapabilities.Tools(listChanged = true)
36+
)
37+
)
38+
)
39+
40+
41+
server.addTool(
42+
name = "get-people-in-space",
43+
description = "The list of people in space endpoint returns the list of people in space right now"
44+
) {
45+
val people = peopleInSpaceRepository.fetchPeople()
46+
CallToolResult(
47+
content =
48+
people.map { TextContent(it.name) }
49+
)
50+
}
51+
52+
return server
53+
}
54+
55+
/**
56+
* Runs an MCP (Model Context Protocol) server using standard I/O for communication.
57+
*
58+
* This function initializes a server instance configured with predefined tools and capabilities.
59+
* It sets up a transport mechanism using standard input and output for communication.
60+
* Once the server starts, it listens for incoming connections, processes requests,
61+
* and executes the appropriate tools. The server shuts down gracefully upon receiving
62+
* a close event.
63+
*/
64+
fun `run mcp server using stdio`() {
65+
val server = configureServer()
66+
val transport = StdioServerTransport(
67+
System.`in`.asInput(),
68+
System.out.asSink().buffered()
69+
)
70+
71+
runBlocking {
72+
server.connect(transport)
73+
val done = Job()
74+
server.onClose {
75+
done.complete()
76+
}
77+
done.join()
78+
}
79+
}
80+
81+
/**
82+
* Launches an SSE (Server-Sent Events) MCP (Model Context Protocol) server on the specified port.
83+
* This server enables clients to connect via SSE for real-time communication and provides endpoints
84+
* for handling specific messages.
85+
*
86+
* @param port The port number on which the SSE server should be started.
87+
*/
88+
fun `run sse mcp server`(port: Int): Unit = runBlocking {
89+
val servers = ConcurrentMap<String, Server>()
90+
91+
val server = configureServer()
92+
embeddedServer(CIO, host = "0.0.0.0", port = port) {
93+
install(SSE)
94+
routing {
95+
sse("/sse") {
96+
val transport = SseServerTransport("/message", this)
97+
98+
servers[transport.sessionId] = server
99+
100+
server.onClose {
101+
servers.remove(transport.sessionId)
102+
}
103+
104+
server.connect(transport)
105+
}
106+
post("/message") {
107+
val sessionId: String = call.request.queryParameters["sessionId"]!!
108+
val transport = servers[sessionId]?.transport as? SseServerTransport
109+
if (transport == null) {
110+
call.respond("Session not found", null)
111+
return@post
112+
}
113+
114+
transport.handlePostMessage(call)
115+
}
116+
}
117+
}.start(wait = true)
118+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ include(":compose-web")
2525
include(":common")
2626
include(":backend")
2727
include(":graphql-server")
28+
include(":mcp-server")

0 commit comments

Comments
 (0)