src/server.bend
ezmcp@0.1.0.0 · 465 lines · 59 definitions
1
2
3
4
5
6import Base
7import emerging-ezjson@1.2.0.0/main.bend as Json
8import emerging-ezjson@1.2.0.0/src/value.bend as Value
9import ezmcp@0.1.0.0/src/rpc.bend as Rpc
10
11
12type Tool is Data:
13 Tool{name: String, description: String, input: Value.Json}
14
15
16
17type Server is Data:
18 Server{name: String, version: String, instructions: String,
19 tools: List<&2, Tool>}
20
21
22
23type Act is Data:
24 Quiet{}
25 Send{reply: Value.Json}
26 Fault{reply: Value.Json}
27 Run{id: Value.Json, name: String, args: Value.Json}
28 Batch{acts: List<&2, Act>}
29
30
31
32type Turn is Data:
33 Turn{act: Act, rev: String}
34
35
36type Step is Data:
37 Now{out: Rpc.Out}
38 Call{name: String, args: Value.Json}
39
40
41type Method is Data:
42 Initialize{}
43 Ping{}
44 ListTools{}
45 CallTool{}
46 Other{}
47
48
49
50
51def latest() -> String:
52 "2025-11-25"
53
54
55
56def oldest() -> String:
57 "2025-03-26"
58
59
60def revisions() -> List<&2, String>:
61 [oldest(), "2025-06-18", latest()]
62
63
64def supported(+val: String) -> Bool:
65 Bool.or(String.eq(val, oldest()),
66 Bool.or(String.eq(val, "2025-06-18"), String.eq(val, latest())))
67
68
69
70def batches(rev: String) -> Bool:
71 String.eq(rev, oldest())
72
73
74
75def negotiate.of(val: String, ok: Bool) -> String:
76 match ok:
77 case True{}:
78 val
79 case False{}:
80 latest()
81
82
83def negotiate(asked: Maybe<&2, String>) -> String:
84 match asked:
85 case None{}:
86 latest()
87 case Some{+v}:
88 negotiate.of(v, supported(v))
89
90
91
92def session.ask(rev: String, params: Value.Json, init: Bool) -> String:
93 match init:
94 case True{}:
95 negotiate(Json.as_str(Json.get(params, "protocolVersion")))
96 case False{}:
97 rev
98
99
100
101def session(rev: String, msg: Rpc.Msg) -> String:
102 match msg:
103 case Rpc.Ask{id, +m, params}:
104 session.ask(rev, params, String.eq(m, "initialize"))
105 case Rpc.Tell{m, params}:
106 rev
107 case Rpc.Answer{id}:
108 rev
109 case Rpc.Junk{id, why}:
110 rev
111
112
113
114
115
116def capabilities(tools: List<&2, Tool>) -> Value.Json:
117 match tools:
118 case []:
119 Json.obj([])
120 case t <> rest:
121 Json.obj([("tools", Json.obj([("listChanged", Json.bool(False{}))]))])
122
123def init.info(name: String, version: String) -> Value.Json:
124 Json.obj([("name", Json.str(name)), ("version", Json.str(version))])
125
126
127def init.extra(
128 instructions: String,
129 empty: Bool
130) -> List<&1, String & Value.Json>:
131 match empty:
132 case True{}:
133 []
134 case False{}:
135 [("instructions", Json.str(instructions))]
136
137
138def init.with(
139 version: String,
140 caps: Value.Json,
141 info: Value.Json,
142 +instructions: String
143) -> Value.Json:
144 Json.obj(List.append(&1, String & Value.Json,
145 [("protocolVersion", Json.str(version)), ("capabilities", caps),
146 ("serverInfo", info)],
147 init.extra(instructions, String.is_empty(instructions))))
148
149
150def init(srv: Server, params: Value.Json) -> Value.Json:
151 match srv:
152 case Server{name, version, instructions, tools}:
153 init.with(negotiate(Json.as_str(Json.get(params, "protocolVersion"))),
154 capabilities(tools), init.info(name, version), instructions)
155
156
157
158
159def tool.json(tail: Tool) -> Value.Json:
160 match tail:
161 case Tool{name, description, input}:
162 Json.obj([("name", Json.str(name)), ("description", Json.str(description)),
163 ("inputSchema", input)])
164
165def tool.all(tools: List<&2, Tool>) -> List<&2, Value.Json>:
166 match tools:
167 case []:
168 []
169 case t <> rest:
170 tool.json(t) <> tool.all(rest)
171
172
173def list(srv: Server) -> Value.Json:
174 match srv:
175 case Server{name, version, instructions, tools}:
176 Json.obj([("tools", Json.arr(tool.all(tools)))])
177
178
179def known.or(hit: Bool, rest: Unit -> Bool) -> Bool:
180 match hit:
181 case True{}:
182 True{}
183 case False{}:
184 rest(Unit{})
185
186
187def known(tools: List<&2, Tool>, +name: String) -> Bool:
188 match tools:
189 case []:
190 False{}
191 case Tool{tname, _desc, _input} <> rest:
192 known.or(String.eq(tname, name), _u => known(rest, name))
193
194
195def invalid(message: String) -> Step:
196 Now{Rpc.Err{Rpc.InvalidParams{}, message}}
197
198
199
200def call.args(+name: String, has: Bool, +args: Value.Json) -> Step:
201 match has:
202 case False{}:
203 Call{name, Json.obj([])}
204 case True{}:
205 Bool.pick(Step, Json.is_obj(args), Call{name, args},
206 invalid("tools/call: arguments must be an object"))
207
208def call.known(+name: String, +params: Value.Json, hit: Bool) -> Step:
209 match hit:
210 case False{}:
211 invalid("Unknown tool: " ++ name)
212 case True{}:
213 call.args(name, Json.has(params, "arguments"),
214 Json.get(params, "arguments"))
215
216def call.named(
217 tools: List<&2, Tool>,
218 +params: Value.Json,
219 name: Maybe<&2, String>
220) -> Step:
221 match name:
222 case None{}:
223 invalid("tools/call: params.name must be a string")
224 case Some{+n}:
225 call.known(n, params, known(tools, n))
226
227
228
229def call(srv: Server, +params: Value.Json) -> Step:
230 match srv:
231 case Server{name, version, instructions, tools}:
232 call.named(tools, params, Json.as_str(Json.get(params, "name")))
233
234
235
236def method.of(+meth: String) -> Method:
237 Bool.pick(Method, String.eq(meth, "initialize"), Initialize{},
238 Bool.pick(Method, String.eq(meth, "ping"), Ping{},
239 Bool.pick(Method, String.eq(meth, "tools/list"), ListTools{},
240 Bool.pick(Method, String.eq(meth, "tools/call"), CallTool{}, Other{}))))
241
242def dispatch.of(
243 srv: Server,
244 meth: String,
245 params: Value.Json,
246 which: Method
247) -> Step:
248 match which:
249 case Initialize{}:
250 Now{Rpc.Ok{init(srv, params)}}
251 case Ping{}:
252 Now{Rpc.Ok{Json.obj([])}}
253 case ListTools{}:
254 Now{Rpc.Ok{list(srv)}}
255 case CallTool{}:
256 call(srv, params)
257 case Other{}:
258 Now{Rpc.Err{Rpc.MethodNotFound{}, "Method not found: " ++ meth}}
259
260
261def dispatch(srv: Server, +meth: String, params: Value.Json) -> Step:
262 dispatch.of(srv, meth, params, method.of(meth))
263
264
265def answer(id: Value.Json, text: Step) -> Act:
266 match text:
267 case Now{out}:
268 Send{Rpc.reply(id, out)}
269 case Call{name, args}:
270 Run{id, name, args}
271
272
273
274def handle(srv: Server, msg: Rpc.Msg) -> Act:
275 match msg:
276 case Rpc.Ask{id, m, params}:
277 answer(id, dispatch(srv, m, params))
278 case Rpc.Tell{m, params}:
279 Quiet{}
280 case Rpc.Answer{id}:
281 Quiet{}
282 case Rpc.Junk{id, why}:
283 Fault{Rpc.reply(id, Rpc.Err{Rpc.InvalidRequest{}, why})}
284
285
286def unparsed() -> Act:
287 Fault{Rpc.reply(Json.null(),
288 Rpc.Err{Rpc.ParseError{}, "Parse error"})}
289
290
291
292def batch.init(id: Value.Json, msg: Rpc.Msg, init: Bool, srv: Server) -> Act:
293 match init:
294 case True{}:
295 Send{Rpc.reply(id, Rpc.Err{Rpc.InvalidRequest{},
296 "Invalid Request: initialize must not be part of a batch"})}
297 case False{}:
298 handle(srv, msg)
299
300
301
302
303def batch.one(srv: Server, +msg: Rpc.Msg) -> Act:
304 match msg:
305 case Rpc.Ask{+id, +m, params}:
306 batch.init(id, msg, String.eq(m, "initialize"), srv)
307 case other:
308 handle(srv, other)
309
310
311def batch.all(+srv: Server, items: List<&2, Value.Json>) -> List<&2, Act>:
312 match items:
313 case []:
314 []
315 case i <> rest:
316 batch.one(srv, Rpc.classify(i)) <> batch.all(srv, rest)
317
318
319
320def batch(srv: Server, items: List<&2, Value.Json>) -> Act:
321 match items:
322 case []:
323 Fault{Rpc.reply(Json.null(), Rpc.Err{Rpc.InvalidRequest{},
324 "Invalid Request: empty batch"})}
325 case i <> rest:
326 Batch{batch.all(srv, i <> rest)}
327
328
329def batch.some(replies: List<&2, Maybe<&2, Value.Json>>) -> List<&2, Value.Json>:
330 match replies:
331 case []:
332 []
333 case None{} <> rest:
334 batch.some(rest)
335 case Some{r} <> rest:
336 r <> batch.some(rest)
337
338def batch.gather(replies: List<&2, Value.Json>) -> Maybe<&2, Value.Json>:
339 match replies:
340 case []:
341 None{}
342 case r <> rest:
343 Some{Json.arr(r <> rest)}
344
345
346
347def batch.reply(replies: List<&2, Maybe<&2, Value.Json>>) -> Maybe<&2, Value.Json>:
348 batch.gather(batch.some(replies))
349
350
351
352
353def turn.msg(srv: Server, rev: String, +msg: Rpc.Msg) -> Turn:
354 Turn{handle(srv, msg), session(rev, msg)}
355
356
357
358def turn.value(srv: Server, rev: String, +val: Value.Json, many: Bool) -> Turn:
359 match many:
360 case True{}:
361 Turn{batch(srv, Json.items(val)), rev}
362 case False{}:
363 turn.msg(srv, rev, Rpc.classify(val))
364
365
366def turn.parsed(srv: Server, +rev: String, meth: Maybe<&2, Value.Json>) -> Turn:
367 match meth:
368 case None{}:
369 Turn{unparsed(), rev}
370 case Some{+v}:
371 turn.value(srv, rev, v, Bool.and(Json.is_arr(v), batches(rev)))
372
373
374
375def turn(srv: Server, rev: String, text: String) -> Turn:
376 turn.parsed(srv, rev, Json.parse(text))
377
378
379def turn.act(tn: Turn) -> Act:
380 match tn:
381 case Turn{act, rev}:
382 act
383
384
385def step(srv: Server, rev: String, text: String) -> Act:
386 turn.act(turn(srv, rev, text))
387
388
389
390def without.pair(hit: Bool, key: String, val: Value.Json, rest: Value.Json) -> Value.Json:
391 match hit:
392 case True{}:
393 rest
394 case False{}:
395 Value.JPair{key, val, rest}
396
397def without.bind(
398 hit: Bool,
399 src: String,
400 num: U32,
401 val: Value.Json,
402 rest: Value.Json
403) -> Value.Json:
404 match hit:
405 case True{}:
406 rest
407 case False{}:
408 Value.JBind{src, num, val, rest}
409
410
411def without.go(cells: Value.Json, +key: String) -> Value.Json:
412 match cells:
413 case Value.JPair{+k, v, r}:
414 without.pair(Value.find.eq(k, key), k, v, without.go(r, key))
415 case Value.JBind{+src, +num, v, r}:
416 without.bind(Value.span.eq(src, num, key), src, num, v,
417 without.go(r, key))
418 case other:
419 other
420
421
422def without(val: Value.Json, key: String) -> Value.Json:
423 match val:
424 case Value.JObj{pairs}:
425 Value.JObj{without.go(pairs, key)}
426 case other:
427 other
428
429def shape.of(result: Value.Json, old: Bool) -> Value.Json:
430 match old:
431 case True{}:
432 without(result, "structuredContent")
433 case False{}:
434 result
435
436
437
438
439def shape(rev: String, result: Value.Json) -> Value.Json:
440 shape.of(result, String.eq(rev, oldest()))
441
442
443
444
445def ran(rev: String, id: Value.Json, out: Rpc.Out) -> Value.Json:
446 match out:
447 case Rpc.Ok{result}:
448 Rpc.reply(id, Rpc.Ok{shape(rev, result)})
449 case Rpc.Err{c, message}:
450 Rpc.reply(id, Rpc.Err{c, message})
451
452
453
454
455def text(text2: String) -> Value.Json:
456 Json.obj([("type", Json.str("text")), ("text", Json.str(text2))])
457
458
459def result(blocks: List<&2, Value.Json>, bad: Bool) -> Value.Json:
460 Json.obj([("content", Json.arr(blocks)), ("isError", Json.bool(bad))])
461
462
463def structured(+val: Value.Json, bad: Bool) -> Value.Json:
464 Json.obj([("content", Json.arr([text(Json.print(val))])),
465 ("structuredContent", val), ("isError", Json.bool(bad))])