-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmcp-smoke.main.kts
More file actions
executable file
·176 lines (157 loc) · 5.58 KB
/
Copy pathmcp-smoke.main.kts
File metadata and controls
executable file
·176 lines (157 loc) · 5.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/usr/bin/env kotlin
/**
* Minimal MCP SSE smoke client for argo-workflows-mcp.
*
* Connects to the MCP SSE endpoint, performs the JSON-RPC handshake
* (initialize -> notifications/initialized -> tools/list), then calls
* add_connection and list_workflows. Use it when no native MCP client is
* available. See LOCAL_ARGO_MCP_TESTING.md for the surrounding smoke plan.
*
* Usage:
* - ./mcp-smoke.main.kts # defaults to http://localhost:8080/
* - ./mcp-smoke.main.kts http://host:port/
*
* Requires the `kotlin` compiler on PATH (e.g. `sdk install kotlin`).
*/
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.put
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread
val base = (args.firstOrNull() ?: "http://localhost:8080/").let {
if (it.endsWith("/")) it else "$it/"
}
val json = Json { prettyPrint = true; ignoreUnknownKeys = true }
val client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build()
val messages = LinkedBlockingQueue<String>()
val endpointQueue = LinkedBlockingQueue<String>()
// Background SSE reader: splits the stream into events and routes the
// `endpoint` event (the POST URL with sessionId) separately from responses.
thread(isDaemon = true, name = "sse-reader") {
val request = HttpRequest.newBuilder(URI.create(base))
.header("Accept", "text/event-stream")
.GET()
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
var event: String? = null
val dataLines = mutableListOf<String>()
response.body().bufferedReader().forEachLine { raw ->
val line = raw.trimEnd('\n', '\r')
when {
line.isEmpty() -> {
if (dataLines.isNotEmpty()) {
val data = dataLines.joinToString("\n")
if (event == "endpoint") endpointQueue.put(data) else messages.put(data)
}
event = null
dataLines.clear()
}
line.startsWith("event:") -> event = line.substringAfter("event:").trim()
line.startsWith("data:") -> dataLines.add(line.substringAfter("data:").trim())
}
}
}
val endpoint = endpointQueue.poll(5, TimeUnit.SECONDS)
?: error("Did not receive MCP endpoint event within 5s")
val postUrl = URI.create(base).resolve(endpoint)
println("MCP endpoint: $endpoint")
var nextId = 1
fun post(payload: JsonObject) {
val request = HttpRequest.newBuilder(postUrl)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload.toString()))
.build()
client.send(request, HttpResponse.BodyHandlers.discarding())
}
fun waitForId(id: Int, timeoutSeconds: Long = 10): JsonObject {
val deadline = System.currentTimeMillis() + timeoutSeconds * 1000
while (true) {
val remaining = deadline - System.currentTimeMillis()
if (remaining <= 0) error("Timed out waiting for response id=$id")
val raw = messages.poll(remaining, TimeUnit.MILLISECONDS)
?: error("Timed out waiting for response id=$id")
val obj = json.parseToJsonElement(raw).jsonObject
if ((obj["id"] as? JsonPrimitive)?.intOrNull == id) return obj
}
}
fun request(method: String, params: JsonObject = buildJsonObject {}): JsonObject {
val id = nextId++
post(
buildJsonObject {
put("jsonrpc", "2.0")
put("id", id)
put("method", method)
put("params", params)
},
)
return waitForId(id)
}
fun callTool(name: String, arguments: JsonObject = buildJsonObject {}): JsonObject =
request(
"tools/call",
buildJsonObject {
put("name", name)
put("arguments", arguments)
},
)
fun pretty(obj: JsonObject) = println(json.encodeToString(JsonObject.serializer(), obj))
pretty(
request(
"initialize",
buildJsonObject {
put("protocolVersion", "2024-11-05")
put("capabilities", buildJsonObject {})
put(
"clientInfo",
buildJsonObject {
put("name", "local-argo-smoke")
put("version", "0.1")
},
)
},
),
)
post(
buildJsonObject {
put("jsonrpc", "2.0")
put("method", "notifications/initialized")
},
)
val tools = request("tools/list")
val toolNames = tools["result"]?.jsonObject?.get("tools")?.jsonArray
?.mapNotNull { (it.jsonObject["name"] as? JsonPrimitive)?.content }
println("TOOLS: $toolNames")
pretty(
callTool(
"add_connection",
buildJsonObject {
put("name", "docker-desktop-local")
put("base_url", "https://localhost:2746")
put("default_namespace", "argo")
put("auth_type", "none")
put("insecure_skip_tls_verify", true)
put("activate", true)
},
),
)
pretty(
callTool(
"list_workflows",
buildJsonObject {
put("namespace", "argo")
put("limit", 10)
},
),
)