Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions .github/workflows/ci.yml → .github/workflows/ci-chat-server.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
name: CI
name: chat-server

on:
push:
branches: [ main ]
paths:
- .github/workflows/ci-chat-server.yml
- chat-server/**
pull_request:
branches: [ main ]
paths:
- .github/workflows/ci-chat-server.yml
- chat-server/**

jobs:
build:
Expand All @@ -28,12 +34,10 @@ jobs:
with:
java-version: 21
distribution: temurin
cache: gradle
cache-dependency-path: chat-server/build.gradle.kts

- name: Build mcp-server
working-directory: mcp-server
run: ./gradlew build --no-daemon

- name: Build chat-server
- name: Build
working-directory: chat-server
run: ./gradlew build --no-daemon

Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/ci-mcp-server.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: mcp-server

on:
push:
branches: [ main ]
paths:
- .github/workflows/ci-mcp-server.yml
- mcp-server/**
pull_request:
branches: [ main ]
paths:
- .github/workflows/ci-mcp-server.yml
- mcp-server/**

jobs:
build:
name: build
runs-on: ubuntu-latest
steps:

- name: Checkout
uses: actions/checkout@v4

- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: 21
distribution: temurin
cache: gradle
cache-dependency-path: mcp-server/build.gradle.kts

- name: Build
working-directory: mcp-server
run: ./gradlew build --no-daemon
4 changes: 4 additions & 0 deletions mcp-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.junit.jupiter:junit-jupiter-params")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")

testImplementation("org.mockito:mockito-core:5.17.0")
testImplementation("org.mockito:mockito-junit-jupiter:5.17.0")
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
}

dependencyManagement {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.rogervinas

import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.whenever
import com.rogervinas.tools.BookingService
import io.modelcontextprotocol.client.McpClient
import io.modelcontextprotocol.client.McpSyncClient
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest
import io.modelcontextprotocol.spec.McpSchema.TextContent
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
import org.junit.jupiter.api.TestMethodOrder
import org.mockito.Mockito.doReturn
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.context.bean.override.mockito.MockitoBean
import java.time.LocalDate
import java.util.function.Consumer


@SpringBootTest(webEnvironment = RANDOM_PORT)
@TestInstance(PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class McpServerApplicationTest {

@LocalServerPort
val port: Int = 0

val client: McpSyncClient by lazy {
McpClient.sync(HttpClientSseClientTransport("http://localhost:$port")).build().apply {
initialize()
ping()
}
}

@AfterAll
fun closeClient() {
client.closeGracefully()
}

@MockitoBean
lateinit var bookingService: BookingService

@Test
@Order(0)
fun `should list tools`() {
assertThat(client.listTools().tools).singleElement().satisfies(Consumer {
assertThat(it.name).isEqualTo("book")
assertThat(it.description).isEqualTo("make a reservation for accommodation for a given city and date")
})
}

@Test
fun `should book`() {
val bookResult = "Booking is done!"
val cityCaptor = argumentCaptor<String>()
val checkinDateCaptor = argumentCaptor<LocalDate>()
val checkoutDateCaptor = argumentCaptor<LocalDate>()
doReturn(bookResult)
.whenever(bookingService)
.book(cityCaptor.capture(), checkinDateCaptor.capture(), checkoutDateCaptor.capture())

val city = "Barcelona"
val checkinDate = "2025-04-15"
val checkoutDate = "2025-04-18"
val result = client.callTool(
CallToolRequest(
"book",
mapOf(
"city" to city,
"checkinDate" to checkinDate,
"checkoutDate" to checkoutDate
)
)
)

assertThat(result.isError).isFalse()
assertThat(result.content).singleElement().isInstanceOfSatisfying(TextContent::class.java) {
// TODO why is text double quoted?
assertThat(it.text).isEqualTo("\"$bookResult\"")
}
assertThat(cityCaptor.allValues).singleElement().isEqualTo(city)
assertThat(checkinDateCaptor.allValues).singleElement().isEqualTo(LocalDate.parse(checkinDate))
assertThat(checkoutDateCaptor.allValues).singleElement().isEqualTo(LocalDate.parse(checkoutDate))
}
}