Skip to content

Commit 0e31e81

Browse files
committed
initial mcp server
1 parent 719b683 commit 0e31e81

File tree

8 files changed

+242
-7
lines changed

8 files changed

+242
-7
lines changed

SharedCode/src/commonMain/kotlin/com/surrus/galwaybus/common/di/Koin.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,15 @@ import kotlin.time.ExperimentalTime
1919

2020
expect fun platformModule(): Module
2121

22-
@ExperimentalTime
2322
fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclaration = {}) =
2423
startKoin {
2524
appDeclaration()
2625
modules(commonModule(), platformModule())
2726
}
2827

2928
// called by iOS etc
30-
@ExperimentalTime
3129
fun initKoin() = initKoin() {}
3230

33-
@ExperimentalTime
3431
fun commonModule() = module {
3532
single { createJson() }
3633
single { createHttpClient(get()) }

SharedCode/src/jvmMain/kotlin/com/surrus/galwaybus/common/di/KoinJVM.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ package com.surrus.galwaybus.common.di
33
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
44
import com.russhwolf.settings.ExperimentalSettingsApi
55
import com.russhwolf.settings.ExperimentalSettingsImplementation
6-
import com.russhwolf.settings.JvmPreferencesSettings
76
import com.russhwolf.settings.ObservableSettings
7+
import com.russhwolf.settings.PreferencesSettings
88
import com.surrus.galwaybus.db.MyDatabase
99
import org.koin.dsl.module
1010
import java.util.prefs.Preferences
1111

1212
@OptIn(ExperimentalSettingsApi::class, ExperimentalSettingsImplementation::class)
1313
actual fun platformModule() = module {
14-
single<ObservableSettings> { JvmPreferencesSettings(Preferences.userRoot()) }
14+
single<ObservableSettings> { PreferencesSettings(Preferences.userRoot()) }
1515
single { createDb() }
1616
}
1717

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ plugins {
22
alias(libs.plugins.android.application) apply false
33
alias(libs.plugins.ksp) apply false
44
alias(libs.plugins.kotlinMultiplatform) apply false
5-
alias(libs.plugins.kotlin.serialization) apply false
5+
alias(libs.plugins.kotlinx.serialization) apply false
66
alias(libs.plugins.kmpNativeCoroutines) apply false
77
alias(libs.plugins.sqlDelight) apply false
88
}

gradle/libs.versions.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ lifecycleLivedataKtx = "2.9.1"
4949
navigationFragmentKtx = "2.9.0"
5050
navigationUiKtx = "2.9.0"
5151

52+
mcp = "0.5.0"
53+
shadowPlugin = "8.1.1"
54+
5255

5356
[libraries]
5457
kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
@@ -145,6 +148,7 @@ androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navi
145148
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
146149
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
147150

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

149153
[bundles]
150154
multiplatformSettings = ["multiplatform-settings", "multiplatform-settings-coroutines"]
@@ -155,8 +159,10 @@ ktor-common = ["ktor-client-core", "ktor-client-json", "ktor-client-logging", "k
155159
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
156160
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
157161
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
158-
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
162+
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
159163
kmpNativeCoroutines = { id = "com.rickclephas.kmp.nativecoroutines", version.ref = "kmpNativeCoroutines" }
160164
sqlDelight = { id = "app.cash.sqldelight", version.ref = "sqlDelight" }
161165
googleServices = { id = "com.google.gms.google-services", version.ref = "googleServices" }
162166
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
167+
kotlinJvm = { id = "org.jetbrains.kotlin.jvm" }
168+
shadowPlugin = { id = "com.github.johnrengelman.shadow", version.ref = "shadowPlugin" }

mcp-server/build.gradle.kts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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(libs.koin.core)
11+
implementation(projects.sharedCode)
12+
}
13+
14+
java {
15+
toolchain {
16+
languageVersion = JavaLanguageVersion.of(17)
17+
}
18+
}
19+
20+
application {
21+
mainClass = "MainKt"
22+
}
23+
24+
tasks.shadowJar {
25+
archiveFileName.set("serverAll.jar")
26+
archiveClassifier.set("")
27+
manifest {
28+
attributes["Main-Class"] = "MainKt"
29+
}
30+
}
31+

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: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import com.surrus.galwaybus.common.GalwayBusRepository
2+
import com.surrus.galwaybus.common.di.initKoin
3+
import com.surrus.galwaybus.common.model.Result
4+
import io.ktor.server.cio.*
5+
import io.ktor.server.engine.*
6+
import io.ktor.utils.io.streams.*
7+
import io.modelcontextprotocol.kotlin.sdk.*
8+
import io.modelcontextprotocol.kotlin.sdk.server.*
9+
import kotlinx.coroutines.Job
10+
import kotlinx.coroutines.runBlocking
11+
import kotlinx.io.asSink
12+
import kotlinx.io.buffered
13+
import kotlinx.serialization.json.JsonObject
14+
import kotlinx.serialization.json.JsonPrimitive
15+
import kotlinx.serialization.json.jsonPrimitive
16+
17+
18+
private val koin = initKoin(enableNetworkLogs = true).koin
19+
20+
fun configureServer(): Server {
21+
val galwayBusRepository = koin.get<GalwayBusRepository>()
22+
23+
val server = Server(
24+
Implementation(
25+
name = "GalwayBus MCP Server",
26+
version = "1.0.0"
27+
),
28+
ServerOptions(
29+
capabilities = ServerCapabilities(
30+
tools = ServerCapabilities.Tools(listChanged = true)
31+
)
32+
)
33+
)
34+
35+
36+
server.addTool(
37+
name = "get-bus-routes",
38+
description = "List of bus routes"
39+
) {
40+
val busRoutes = galwayBusRepository.fetchBusRoutes()
41+
CallToolResult(
42+
content =
43+
busRoutes.map { TextContent("${it.timetableId}, ${it.longName}") }
44+
)
45+
}
46+
47+
48+
49+
server.addTool(
50+
name = "get-nearest-stops",
51+
description = "List nearest bus stops",
52+
) { request ->
53+
val latitude = 53.2743394
54+
val longitude = -9.0514163
55+
val routeStopsResult = galwayBusRepository.fetchNearestStops(latitude, longitude)
56+
if (routeStopsResult is Result.Success) {
57+
val routeStops = routeStopsResult.data
58+
CallToolResult(
59+
content = routeStops.map { TextContent(it.toString()) }
60+
)
61+
} else {
62+
CallToolResult(
63+
content = listOf(TextContent("Error getting route stops."))
64+
)
65+
}
66+
}
67+
68+
69+
server.addTool(
70+
name = "get-bus-departures",
71+
description = "List",
72+
inputSchema = Tool.Input(
73+
properties = JsonObject(
74+
mapOf("stopRef" to JsonPrimitive("string"))
75+
),
76+
required = listOf("stopRef")
77+
)
78+
79+
) { request ->
80+
val stopRef = request.arguments["stopRef"]
81+
if (stopRef == null) {
82+
return@addTool CallToolResult(
83+
content = listOf(TextContent("The 'stopRef' parameter is required."))
84+
)
85+
}
86+
87+
val busDeparturesResult = galwayBusRepository.fetchBusStopDepartures(stopRef.jsonPrimitive.content)
88+
if (busDeparturesResult is Result.Success) {
89+
val routeStops = busDeparturesResult.data
90+
CallToolResult(
91+
content = routeStops.map { TextContent(it.toString()) }
92+
)
93+
} else {
94+
CallToolResult(
95+
content = listOf(TextContent("Error getting bus departures."))
96+
)
97+
}
98+
}
99+
100+
101+
server.addTool(
102+
name = "get-route-stops",
103+
description = "List or stops for particular bus route",
104+
inputSchema = Tool.Input(
105+
properties = JsonObject(
106+
mapOf("routeId" to JsonPrimitive("string"))
107+
),
108+
required = listOf("routeId")
109+
)
110+
111+
) { request ->
112+
val routeId = request.arguments["routeId"]
113+
if (routeId == null) {
114+
return@addTool CallToolResult(
115+
content = listOf(TextContent("The 'routeId' parameter is required."))
116+
)
117+
}
118+
119+
val routeStopsResult = galwayBusRepository.fetchRouteStops(routeId.jsonPrimitive.content)
120+
if (routeStopsResult is Result.Success) {
121+
val routeStops = routeStopsResult.data
122+
CallToolResult(
123+
content = routeStops.map { TextContent(it.toString()) }
124+
)
125+
} else {
126+
CallToolResult(
127+
content = listOf(TextContent("Error getting route stops."))
128+
)
129+
}
130+
}
131+
132+
return server
133+
}
134+
135+
/**
136+
* Runs an MCP (Model Context Protocol) server using standard I/O for communication.
137+
*
138+
* This function initializes a server instance configured with predefined tools and capabilities.
139+
* It sets up a transport mechanism using standard input and output for communication.
140+
* Once the server starts, it listens for incoming connections, processes requests,
141+
* and executes the appropriate tools. The server shuts down gracefully upon receiving
142+
* a close event.
143+
*/
144+
fun `run mcp server using stdio`() {
145+
val server = configureServer()
146+
val transport = StdioServerTransport(
147+
System.`in`.asInput(),
148+
System.out.asSink().buffered()
149+
)
150+
151+
runBlocking {
152+
server.connect(transport)
153+
val done = Job()
154+
server.onClose {
155+
done.complete()
156+
}
157+
done.join()
158+
}
159+
}
160+
161+
/**
162+
* Launches an SSE (Server-Sent Events) MCP (Model Context Protocol) server on the specified port.
163+
* This server enables clients to connect via SSE for real-time communication and provides endpoints
164+
* for handling specific messages.
165+
*
166+
* @param port The port number on which the SSE server should be started.
167+
*/
168+
fun `run sse mcp server`(port: Int): Unit = runBlocking {
169+
val server = configureServer()
170+
embeddedServer(CIO, host = "0.0.0.0", port = port) {
171+
mcp {
172+
server
173+
}
174+
}.start(wait = true)
175+
}

settings.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@ dependencyResolutionManagement {
1919
}
2020
}
2121

22+
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
2223
include(":android-app")
2324
include(":SharedCode")
25+
include(":mcp-server")

0 commit comments

Comments
 (0)