Skip to content

Commit 92ffaef

Browse files
Skobeltsynclaude
andcommitted
test(#1754): loopback live-MCP — agent exposes sqrt(π/e), MCP client receives digits
Replaces "needs MCP_REDMINE_URL" with a self-contained in-JVM loopback fixture. The framework's own McpServer + McpClient form both ends of the wire. Shape: - algebra agent (Agent<String, String>) with one implementedBy skill that computes sqrt(π/e) to 50 decimal places. - π and e stored as IntArray(61) of digits (the digits-as-arrays spec); arithmetic via BigInteger: scaled = pi_int * 10^(2*scale) / e_int root = floor(sqrt(scaled)) - McpServer.from(algebra) on port=0 (auto-assigned loopback). - McpClient.connect(server.url) → calls "compute" tool over the wire and gets back the decimal string. Three assertions on the MCP-transported result: 1. Leading digits "1.0750476" match the hardcoded canonical prefix. 2. First 7 decimal digits match Math.sqrt(Math.PI / Math.E) — a double-precision sanity floor that proves nothing weird happened to the typing or wire encoding. 3. Self-consistency: (result)² equals π/e to ~20 decimal places via BigDecimal round-trip — catches any deeper precision corruption on the wire. After this lands, `./gradlew mcpIntegrationTest` has real coverage without any external MCP setup. Tagged `live-mcp` so it joins the existing aggregate task. Note: live-llm-style locale fix — String.format defaults to host locale (comma decimals on some systems); forced Locale.ROOT for the sanity-floor comparison. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3b54fa8 commit 92ffaef

1 file changed

Lines changed: 146 additions & 0 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package agents_engine.mcp
2+
3+
import agents_engine.core.agent
4+
import org.junit.jupiter.api.AfterEach
5+
import org.junit.jupiter.api.Tag
6+
import org.junit.jupiter.api.Test
7+
import java.math.BigInteger
8+
import kotlin.test.assertTrue
9+
10+
/**
11+
* #1754 — self-contained live-MCP test. Replaces the previous "needs
12+
* MCP_REDMINE_URL" requirement with a loopback fixture using the
13+
* framework's own MCP server + client end-to-end.
14+
*
15+
* Shape:
16+
* 1. Build a tool agent `algebra` whose one skill computes
17+
* `sqrt(π / e)` to a configurable number of digits.
18+
* 2. Expose it via `McpServer.from(algebra)` on an auto-assigned
19+
* loopback port.
20+
* 3. Connect an `McpClient` to the server's URL.
21+
* 4. Invoke the exposed tool over the MCP wire and assert the
22+
* returned digits match the canonical sqrt(π/e) to 30 places.
23+
*
24+
* Per the design call: π and e are stored as digit arrays (the
25+
* "custom algebra" digits-as-arrays shape); the actual arithmetic
26+
* runs on BigInteger.
27+
*/
28+
class LoopbackMcpAlgebraTest {
29+
30+
private var mcpServer: McpServer? = null
31+
private var mcpClient: McpClient? = null
32+
33+
@AfterEach
34+
fun teardown() {
35+
mcpClient?.close()
36+
mcpServer?.stop()
37+
}
38+
39+
@Tag("live-mcp")
40+
@Test
41+
fun `loopback — algebra agent exposes sqrt(pi over e) over MCP and an MCP client receives the digits`() {
42+
val algebra = agent<String, String>("algebra") {
43+
skills {
44+
skill<String, String>("compute", "Computes sqrt(pi/e) to many decimal places") {
45+
implementedBy { _ -> computeSqrtPiOverE(scale = 50) }
46+
}
47+
}
48+
}
49+
50+
val server = McpServer.from(algebra) {
51+
port = 0
52+
expose("compute")
53+
}.start().also { mcpServer = it }
54+
55+
val mcp = McpClient.connect(server.url).also { mcpClient = it }
56+
57+
// Call the tool over the MCP wire — String-input skill schema is
58+
// `{input: string}`, so we pass an arbitrary input string (the
59+
// skill ignores it).
60+
val result = mcp.call("compute", mapOf("input" to "go"))?.toString()
61+
?: error("MCP call returned null")
62+
63+
// sqrt(π/e) starts 1.0750476... — verified against Math.sqrt
64+
// as a double-precision sanity check, plus a self-consistency
65+
// square-back below for the high-precision tail.
66+
assertTrue(
67+
result.startsWith("1.0750476"),
68+
"expected MCP round-trip to return sqrt(π/e) starting with \"1.0750476\"; got: \"$result\"",
69+
)
70+
71+
// Math.sqrt(Math.PI / Math.E) as a low-precision floor sanity check.
72+
// Locale.ROOT forces a period decimal separator regardless of host locale.
73+
val approx = Math.sqrt(Math.PI / Math.E)
74+
val approxRounded = String.format(java.util.Locale.ROOT, "%.7f", approx)
75+
assertTrue(
76+
result.startsWith(approxRounded),
77+
"MCP-returned digits must agree with Math.sqrt(Math.PI/Math.E) at 7-decimal precision; " +
78+
"expected prefix \"$approxRounded\"; got: \"$result\"",
79+
)
80+
81+
// Self-consistency: take 30 leading decimal digits of result,
82+
// square them in BigDecimal, compare to π/e to ~25 digits. Catches
83+
// any deeper precision corruption from the MCP wire.
84+
val resultBD = java.math.BigDecimal(result.take(33)) // "1." + 30 digits
85+
val squared = resultBD.multiply(resultBD).setScale(25, java.math.RoundingMode.HALF_UP)
86+
val piBD = java.math.BigDecimal(PI_DIGITS.joinToString("").let { it.substring(0, 1) + "." + it.substring(1) })
87+
val eBD = java.math.BigDecimal(E_DIGITS.joinToString("").let { it.substring(0, 1) + "." + it.substring(1) })
88+
val expectedRatio = piBD.divide(eBD, 25, java.math.RoundingMode.HALF_UP)
89+
val diff = squared.subtract(expectedRatio).abs()
90+
assertTrue(
91+
diff < java.math.BigDecimal("1e-20"),
92+
"result² must equal π/e to ~20 decimal places; squared=$squared expected=$expectedRatio diff=$diff",
93+
)
94+
}
95+
96+
/**
97+
* sqrt(π/e) to `scale` digits past the decimal point. π and e are
98+
* stored as digit arrays per the design; the actual arithmetic runs
99+
* on BigInteger.
100+
*
101+
* Strategy:
102+
* - Treat the digit arrays as integers: pi = 3141592..., e = 2718281...
103+
* - Each carries an implicit decimal shift equal to (digits - 1).
104+
* - To get sqrt(π/e) × 10^scale, compute:
105+
* numerator = pi × 10^(eShift + 2*scale - piShift)
106+
* scaled = numerator / e (integer division)
107+
* root = floor(sqrt(scaled))
108+
* `root` is then sqrt(π/e) × 10^scale (as an integer); we
109+
* format it with the decimal point after the first digit.
110+
*/
111+
private fun computeSqrtPiOverE(scale: Int): String {
112+
val pi = BigInteger(PI_DIGITS.joinToString(""))
113+
val e = BigInteger(E_DIGITS.joinToString(""))
114+
val piShift = PI_DIGITS.size - 1
115+
val eShift = E_DIGITS.size - 1
116+
117+
val exponent = eShift + 2 * scale - piShift
118+
require(exponent >= 0) { "scale too large for the digit array sizes" }
119+
val tenPow = BigInteger.TEN.pow(exponent)
120+
val scaled = pi.multiply(tenPow).divide(e)
121+
val root = scaled.sqrt() // floor(sqrt) — exact for square inputs, off-by-one for the last digit otherwise
122+
123+
val s = root.toString()
124+
return s.substring(0, 1) + "." + s.substring(1)
125+
}
126+
127+
companion object {
128+
// π to 60 digits past the decimal (61 digit-array entries total).
129+
// 3.14159265358979323846264338327950288419716939937510582097494
130+
private val PI_DIGITS = intArrayOf(
131+
3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3, 2, 3, 8, 4,
132+
6, 2, 6, 4, 3, 3, 8, 3, 2, 7, 9, 5, 0, 2, 8, 8, 4, 1, 9, 7,
133+
1, 6, 9, 3, 9, 9, 3, 7, 5, 1, 0, 5, 8, 2, 0, 9, 7, 4, 9, 4,
134+
4,
135+
)
136+
137+
// e to 60 digits past the decimal (61 digit-array entries total).
138+
// 2.71828182845904523536028747135266249775724709369995957496696
139+
private val E_DIGITS = intArrayOf(
140+
2, 7, 1, 8, 2, 8, 1, 8, 2, 8, 4, 5, 9, 0, 4, 5, 2, 3, 5, 3,
141+
6, 0, 2, 8, 7, 4, 7, 1, 3, 5, 2, 6, 6, 2, 4, 9, 7, 7, 5, 7,
142+
2, 4, 7, 0, 9, 3, 6, 9, 9, 9, 5, 9, 5, 7, 4, 9, 6, 6, 9, 6,
143+
7,
144+
)
145+
}
146+
}

0 commit comments

Comments
 (0)