fast-agent-mcp 0.3.8__py3-none-any.whl → 0.3.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

@@ -0,0 +1,600 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from dataclasses import dataclass
5
+ from datetime import datetime, timezone
6
+ from threading import Lock
7
+ from typing import Literal
8
+
9
+ from mcp.types import (
10
+ JSONRPCError,
11
+ JSONRPCMessage,
12
+ JSONRPCNotification,
13
+ JSONRPCRequest,
14
+ JSONRPCResponse,
15
+ RequestId,
16
+ )
17
+ from pydantic import BaseModel, ConfigDict
18
+
19
+ ChannelName = Literal["post-json", "post-sse", "get", "resumption", "stdio"]
20
+ EventType = Literal["message", "connect", "disconnect", "keepalive", "error"]
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class ChannelEvent:
25
+ """Event emitted by the tracking transport indicating channel activity."""
26
+
27
+ channel: ChannelName
28
+ event_type: EventType
29
+ message: JSONRPCMessage | None = None
30
+ raw_event: str | None = None
31
+ detail: str | None = None
32
+ status_code: int | None = None
33
+
34
+
35
+ @dataclass
36
+ class ModeStats:
37
+ messages: int = 0
38
+ request: int = 0
39
+ notification: int = 0
40
+ response: int = 0
41
+ last_summary: str | None = None
42
+ last_at: datetime | None = None
43
+
44
+
45
+ def _summarise_message(message: JSONRPCMessage) -> str:
46
+ root = message.root
47
+ if isinstance(root, JSONRPCRequest):
48
+ method = root.method or ""
49
+ return f"request {method}"
50
+ if isinstance(root, JSONRPCNotification):
51
+ method = root.method or ""
52
+ return f"notify {method}"
53
+ if isinstance(root, JSONRPCResponse):
54
+ return "response"
55
+ if isinstance(root, JSONRPCError):
56
+ code = getattr(root.error, "code", None)
57
+ return f"error {code}" if code is not None else "error"
58
+ return "message"
59
+
60
+
61
+ class ChannelSnapshot(BaseModel):
62
+ """Snapshot of aggregated activity for a single transport channel."""
63
+
64
+ model_config = ConfigDict(arbitrary_types_allowed=True)
65
+
66
+ message_count: int = 0
67
+ mode: str | None = None
68
+ mode_counts: dict[str, int] | None = None
69
+ last_message_summary: str | None = None
70
+ last_message_at: datetime | None = None
71
+ connected: bool | None = None
72
+ state: str | None = None
73
+ last_event: str | None = None
74
+ last_event_at: datetime | None = None
75
+ ping_count: int | None = None
76
+ ping_last_at: datetime | None = None
77
+ last_error: str | None = None
78
+ connect_at: datetime | None = None
79
+ disconnect_at: datetime | None = None
80
+ last_status_code: int | None = None
81
+ request_count: int = 0
82
+ response_count: int = 0
83
+ notification_count: int = 0
84
+ activity_buckets: list[str] | None = None
85
+
86
+
87
+ class TransportSnapshot(BaseModel):
88
+ """Collection of channel snapshots for a transport."""
89
+
90
+ model_config = ConfigDict(arbitrary_types_allowed=True)
91
+
92
+ post: ChannelSnapshot | None = None
93
+ post_json: ChannelSnapshot | None = None
94
+ post_sse: ChannelSnapshot | None = None
95
+ get: ChannelSnapshot | None = None
96
+ resumption: ChannelSnapshot | None = None
97
+ stdio: ChannelSnapshot | None = None
98
+
99
+
100
+ class TransportChannelMetrics:
101
+ """Aggregates low-level channel events into user-visible metrics."""
102
+
103
+ def __init__(self) -> None:
104
+ self._lock = Lock()
105
+
106
+ self._post_modes: set[str] = set()
107
+ self._post_count = 0
108
+ self._post_request_count = 0
109
+ self._post_response_count = 0
110
+ self._post_notification_count = 0
111
+ self._post_last_summary: str | None = None
112
+ self._post_last_at: datetime | None = None
113
+ self._post_mode_stats: dict[str, ModeStats] = {
114
+ "json": ModeStats(),
115
+ "sse": ModeStats(),
116
+ }
117
+
118
+ self._get_connected = False
119
+ self._get_had_connection = False
120
+ self._get_connect_at: datetime | None = None
121
+ self._get_disconnect_at: datetime | None = None
122
+ self._get_last_summary: str | None = None
123
+ self._get_last_at: datetime | None = None
124
+ self._get_last_event: str | None = None
125
+ self._get_last_event_at: datetime | None = None
126
+ self._get_last_error: str | None = None
127
+ self._get_last_status_code: int | None = None
128
+ self._get_message_count = 0
129
+ self._get_request_count = 0
130
+ self._get_response_count = 0
131
+ self._get_notification_count = 0
132
+ self._get_ping_count = 0
133
+ self._get_last_ping_at: datetime | None = None
134
+
135
+ self._resumption_count = 0
136
+ self._resumption_last_summary: str | None = None
137
+ self._resumption_last_at: datetime | None = None
138
+ self._resumption_request_count = 0
139
+ self._resumption_response_count = 0
140
+ self._resumption_notification_count = 0
141
+
142
+ self._stdio_connected = False
143
+ self._stdio_had_connection = False
144
+ self._stdio_connect_at: datetime | None = None
145
+ self._stdio_disconnect_at: datetime | None = None
146
+ self._stdio_count = 0
147
+ self._stdio_last_summary: str | None = None
148
+ self._stdio_last_at: datetime | None = None
149
+ self._stdio_last_event: str | None = None
150
+ self._stdio_last_event_at: datetime | None = None
151
+ self._stdio_last_error: str | None = None
152
+ self._stdio_request_count = 0
153
+ self._stdio_response_count = 0
154
+ self._stdio_notification_count = 0
155
+
156
+ self._response_channel_by_id: dict[RequestId, ChannelName] = {}
157
+
158
+ self._history_bucket_seconds = 30
159
+ self._history_bucket_count = 20
160
+ self._history_priority = {
161
+ "error": 5,
162
+ "disabled": 4,
163
+ "request": 4,
164
+ "response": 3,
165
+ "notification": 2,
166
+ "ping": 2,
167
+ "none": 1,
168
+ }
169
+ self._history: dict[str, deque[tuple[int, str]]] = {
170
+ "post-json": deque(maxlen=self._history_bucket_count),
171
+ "post-sse": deque(maxlen=self._history_bucket_count),
172
+ "get": deque(maxlen=self._history_bucket_count),
173
+ "resumption": deque(maxlen=self._history_bucket_count),
174
+ "stdio": deque(maxlen=self._history_bucket_count),
175
+ }
176
+
177
+ def record_event(self, event: ChannelEvent) -> None:
178
+ now = datetime.now(timezone.utc)
179
+ with self._lock:
180
+ if event.channel in ("post-json", "post-sse"):
181
+ self._handle_post_event(event, now)
182
+ elif event.channel == "get":
183
+ self._handle_get_event(event, now)
184
+ elif event.channel == "resumption":
185
+ self._handle_resumption_event(event, now)
186
+ elif event.channel == "stdio":
187
+ self._handle_stdio_event(event, now)
188
+
189
+ def _handle_post_event(self, event: ChannelEvent, now: datetime) -> None:
190
+ mode = "json" if event.channel == "post-json" else "sse"
191
+ if event.event_type == "message" and event.message is not None:
192
+ self._post_modes.add(mode)
193
+ self._post_count += 1
194
+
195
+ mode_stats = self._post_mode_stats[mode]
196
+ mode_stats.messages += 1
197
+
198
+ classification = self._tally_message_counts("post", event.message, now, sub_mode=mode)
199
+
200
+ summary = "ping" if classification == "ping" else _summarise_message(event.message)
201
+ mode_stats.last_summary = summary
202
+ mode_stats.last_at = now
203
+ self._post_last_summary = summary
204
+ self._post_last_at = now
205
+
206
+ self._record_response_channel(event)
207
+ self._record_history(event.channel, classification, now)
208
+ elif event.event_type == "error":
209
+ self._record_history(event.channel, "error", now)
210
+
211
+ def _handle_get_event(self, event: ChannelEvent, now: datetime) -> None:
212
+ if event.event_type == "connect":
213
+ self._get_connected = True
214
+ self._get_had_connection = True
215
+ self._get_connect_at = now
216
+ self._get_last_event = "connect"
217
+ self._get_last_event_at = now
218
+ self._get_last_error = None
219
+ self._get_last_status_code = None
220
+ elif event.event_type == "disconnect":
221
+ self._get_connected = False
222
+ self._get_disconnect_at = now
223
+ self._get_last_event = "disconnect"
224
+ self._get_last_event_at = now
225
+ elif event.event_type == "keepalive":
226
+ self._register_ping(now)
227
+ self._get_last_event = event.raw_event or "keepalive"
228
+ self._get_last_event_at = now
229
+ self._record_history("get", "ping", now)
230
+ elif event.event_type == "message" and event.message is not None:
231
+ self._get_message_count += 1
232
+ classification = self._tally_message_counts("get", event.message, now)
233
+ summary = "ping" if classification == "ping" else _summarise_message(event.message)
234
+ self._get_last_summary = summary
235
+ self._get_last_at = now
236
+ self._get_last_event = "ping" if classification == "ping" else "message"
237
+ self._get_last_event_at = now
238
+
239
+ self._record_response_channel(event)
240
+ self._record_history("get", classification, now)
241
+ elif event.event_type == "error":
242
+ self._get_last_status_code = event.status_code
243
+ self._get_last_error = event.detail
244
+ self._get_last_event = "error"
245
+ self._get_last_event_at = now
246
+ # Record 405 as "disabled" in timeline, not "error"
247
+ timeline_state = "disabled" if event.status_code == 405 else "error"
248
+ self._record_history("get", timeline_state, now)
249
+
250
+ def _handle_resumption_event(self, event: ChannelEvent, now: datetime) -> None:
251
+ if event.event_type == "message" and event.message is not None:
252
+ self._resumption_count += 1
253
+ classification = self._tally_message_counts("resumption", event.message, now)
254
+ summary = "ping" if classification == "ping" else _summarise_message(event.message)
255
+ self._resumption_last_summary = summary
256
+ self._resumption_last_at = now
257
+
258
+ self._record_response_channel(event)
259
+ self._record_history("resumption", classification, now)
260
+ elif event.event_type == "error":
261
+ self._record_history("resumption", "error", now)
262
+
263
+ def _handle_stdio_event(self, event: ChannelEvent, now: datetime) -> None:
264
+ if event.event_type == "connect":
265
+ self._stdio_connected = True
266
+ self._stdio_had_connection = True
267
+ self._stdio_connect_at = now
268
+ self._stdio_last_event = "connect"
269
+ self._stdio_last_event_at = now
270
+ self._stdio_last_error = None
271
+ elif event.event_type == "disconnect":
272
+ self._stdio_connected = False
273
+ self._stdio_disconnect_at = now
274
+ self._stdio_last_event = "disconnect"
275
+ self._stdio_last_event_at = now
276
+ elif event.event_type == "message":
277
+ self._stdio_count += 1
278
+
279
+ # Handle synthetic events (from ServerStats) vs real message events
280
+ if event.message is not None:
281
+ # Real message event with JSON-RPC content
282
+ classification = self._tally_message_counts("stdio", event.message, now)
283
+ summary = "ping" if classification == "ping" else _summarise_message(event.message)
284
+ self._record_response_channel(event)
285
+ else:
286
+ # Synthetic event from MCP operation activity
287
+ classification = "request" # MCP operations are always requests from client perspective
288
+ self._stdio_request_count += 1
289
+ summary = event.detail or "request"
290
+
291
+ self._stdio_last_summary = summary
292
+ self._stdio_last_at = now
293
+ self._stdio_last_event = "message"
294
+ self._stdio_last_event_at = now
295
+ self._record_history("stdio", classification, now)
296
+ elif event.event_type == "error":
297
+ self._stdio_last_error = event.detail
298
+ self._stdio_last_event = "error"
299
+ self._stdio_last_event_at = now
300
+ self._record_history("stdio", "error", now)
301
+
302
+ def _record_response_channel(self, event: ChannelEvent) -> None:
303
+ if event.message is None:
304
+ return
305
+ root = event.message.root
306
+ request_id: RequestId | None = None
307
+ if isinstance(root, (JSONRPCResponse, JSONRPCError, JSONRPCRequest)):
308
+ request_id = getattr(root, "id", None)
309
+ if request_id is None:
310
+ return
311
+ self._response_channel_by_id[request_id] = event.channel
312
+
313
+ def consume_response_channel(self, request_id: RequestId | None) -> ChannelName | None:
314
+ if request_id is None:
315
+ return None
316
+ with self._lock:
317
+ return self._response_channel_by_id.pop(request_id, None)
318
+
319
+ def _tally_message_counts(
320
+ self,
321
+ channel_key: str,
322
+ message: JSONRPCMessage,
323
+ timestamp: datetime,
324
+ *,
325
+ sub_mode: str | None = None,
326
+ ) -> str:
327
+ classification = self._classify_message(message)
328
+
329
+ if channel_key == "post":
330
+ if classification == "request":
331
+ self._post_request_count += 1
332
+ elif classification == "notification":
333
+ self._post_notification_count += 1
334
+ elif classification == "response":
335
+ self._post_response_count += 1
336
+
337
+ if sub_mode:
338
+ stats = self._post_mode_stats[sub_mode]
339
+ if classification in {"request", "notification", "response"}:
340
+ setattr(stats, classification, getattr(stats, classification) + 1)
341
+ elif channel_key == "get":
342
+ if classification == "ping":
343
+ self._register_ping(timestamp)
344
+ elif classification == "request":
345
+ self._get_request_count += 1
346
+ elif classification == "notification":
347
+ self._get_notification_count += 1
348
+ elif classification == "response":
349
+ self._get_response_count += 1
350
+ elif channel_key == "resumption":
351
+ if classification == "request":
352
+ self._resumption_request_count += 1
353
+ elif classification == "notification":
354
+ self._resumption_notification_count += 1
355
+ elif classification == "response":
356
+ self._resumption_response_count += 1
357
+ elif channel_key == "stdio":
358
+ if classification == "request":
359
+ self._stdio_request_count += 1
360
+ elif classification == "notification":
361
+ self._stdio_notification_count += 1
362
+ elif classification == "response":
363
+ self._stdio_response_count += 1
364
+
365
+ return classification
366
+
367
+ def _register_ping(self, timestamp: datetime) -> None:
368
+ self._get_ping_count += 1
369
+ self._get_last_ping_at = timestamp
370
+
371
+ def _classify_message(self, message: JSONRPCMessage | None) -> str:
372
+ if message is None:
373
+ return "none"
374
+ root = message.root
375
+ method = getattr(root, "method", "")
376
+ method_lower = method.lower() if isinstance(method, str) else ""
377
+
378
+ if isinstance(root, JSONRPCRequest):
379
+ if self._is_ping_method(method_lower):
380
+ return "ping"
381
+ return "request"
382
+ if isinstance(root, JSONRPCNotification):
383
+ if self._is_ping_method(method_lower):
384
+ return "ping"
385
+ return "notification"
386
+ if isinstance(root, (JSONRPCResponse, JSONRPCError)):
387
+ return "response"
388
+ return "none"
389
+
390
+ @staticmethod
391
+ def _is_ping_method(method: str) -> bool:
392
+ if not method:
393
+ return False
394
+ return (
395
+ method == "ping"
396
+ or method.endswith("/ping")
397
+ or method.endswith(".ping")
398
+ )
399
+
400
+ def _record_history(self, channel: str, state: str, timestamp: datetime) -> None:
401
+ if state in {"none", ""}:
402
+ return
403
+ history = self._history.get(channel)
404
+ if history is None:
405
+ return
406
+
407
+ bucket = int(timestamp.timestamp() // self._history_bucket_seconds)
408
+ if history and history[-1][0] == bucket:
409
+ existing = history[-1][1]
410
+ if self._history_priority.get(state, 0) >= self._history_priority.get(existing, 0):
411
+ history[-1] = (bucket, state)
412
+ return
413
+
414
+ while history and bucket - history[0][0] >= self._history_bucket_count:
415
+ history.popleft()
416
+
417
+ history.append((bucket, state))
418
+
419
+ def _build_activity_buckets(self, key: str, now: datetime) -> list[str]:
420
+ history = self._history.get(key)
421
+ if not history:
422
+ return ["none"] * self._history_bucket_count
423
+
424
+ history_map = {bucket: state for bucket, state in history}
425
+ current_bucket = int(now.timestamp() // self._history_bucket_seconds)
426
+ buckets: list[str] = []
427
+ for offset in range(self._history_bucket_count - 1, -1, -1):
428
+ bucket_index = current_bucket - offset
429
+ buckets.append(history_map.get(bucket_index, "none"))
430
+ return buckets
431
+
432
+ def _merge_activity_buckets(self, keys: list[str], now: datetime) -> list[str] | None:
433
+ sequences = [self._build_activity_buckets(key, now) for key in keys if key in self._history]
434
+ if not sequences:
435
+ return None
436
+
437
+ merged: list[str] = []
438
+ for idx in range(self._history_bucket_count):
439
+ best_state = "none"
440
+ best_priority = 0
441
+ for seq in sequences:
442
+ state = seq[idx]
443
+ priority = self._history_priority.get(state, 0)
444
+ if priority > best_priority:
445
+ best_state = state
446
+ best_priority = priority
447
+ merged.append(best_state)
448
+
449
+ if all(state == "none" for state in merged):
450
+ return None
451
+ return merged
452
+
453
+ def _build_post_mode_snapshot(self, mode: str, now: datetime) -> ChannelSnapshot | None:
454
+ stats = self._post_mode_stats[mode]
455
+ if stats.messages == 0:
456
+ return None
457
+ return ChannelSnapshot(
458
+ message_count=stats.messages,
459
+ mode=mode,
460
+ request_count=stats.request,
461
+ response_count=stats.response,
462
+ notification_count=stats.notification,
463
+ last_message_summary=stats.last_summary,
464
+ last_message_at=stats.last_at,
465
+ activity_buckets=self._build_activity_buckets(f"post-{mode}", now),
466
+ )
467
+
468
+ def snapshot(self) -> TransportSnapshot:
469
+ with self._lock:
470
+ if (
471
+ not self._post_count
472
+ and not self._get_message_count
473
+ and not self._get_ping_count
474
+ and not self._resumption_count
475
+ and not self._stdio_count
476
+ and not self._get_connected
477
+ and not self._stdio_connected
478
+ ):
479
+ return TransportSnapshot()
480
+
481
+ now = datetime.now(timezone.utc)
482
+
483
+ post_mode_counts = {
484
+ mode: stats.messages
485
+ for mode, stats in self._post_mode_stats.items()
486
+ if stats.messages
487
+ }
488
+ post_snapshot = None
489
+ if self._post_count:
490
+ if len(self._post_modes) == 0:
491
+ mode = None
492
+ elif len(self._post_modes) == 1:
493
+ mode = next(iter(self._post_modes))
494
+ else:
495
+ mode = "mixed"
496
+ post_snapshot = ChannelSnapshot(
497
+ message_count=self._post_count,
498
+ mode=mode,
499
+ mode_counts=post_mode_counts or None,
500
+ last_message_summary=self._post_last_summary,
501
+ last_message_at=self._post_last_at,
502
+ request_count=self._post_request_count,
503
+ response_count=self._post_response_count,
504
+ notification_count=self._post_notification_count,
505
+ activity_buckets=self._merge_activity_buckets(["post-json", "post-sse"], now),
506
+ )
507
+
508
+ post_json_snapshot = self._build_post_mode_snapshot("json", now)
509
+ post_sse_snapshot = self._build_post_mode_snapshot("sse", now)
510
+
511
+ get_snapshot = None
512
+ if (
513
+ self._get_message_count
514
+ or self._get_ping_count
515
+ or self._get_connected
516
+ or self._get_disconnect_at
517
+ or self._get_last_error
518
+ ):
519
+ if self._get_connected:
520
+ state = "open"
521
+ elif self._get_last_error is not None:
522
+ state = "disabled" if self._get_last_status_code == 405 else "error"
523
+ elif self._get_had_connection:
524
+ state = "off"
525
+ else:
526
+ state = "idle"
527
+
528
+ get_snapshot = ChannelSnapshot(
529
+ connected=self._get_connected,
530
+ state=state,
531
+ connect_at=self._get_connect_at,
532
+ disconnect_at=self._get_disconnect_at,
533
+ message_count=self._get_message_count,
534
+ last_message_summary=self._get_last_summary,
535
+ last_message_at=self._get_last_at,
536
+ ping_count=self._get_ping_count,
537
+ ping_last_at=self._get_last_ping_at,
538
+ last_error=self._get_last_error,
539
+ last_event=self._get_last_event,
540
+ last_event_at=self._get_last_event_at,
541
+ last_status_code=self._get_last_status_code,
542
+ request_count=self._get_request_count,
543
+ response_count=self._get_response_count,
544
+ notification_count=self._get_notification_count,
545
+ activity_buckets=self._build_activity_buckets("get", now),
546
+ )
547
+
548
+ resumption_snapshot = None
549
+ if self._resumption_count:
550
+ resumption_snapshot = ChannelSnapshot(
551
+ message_count=self._resumption_count,
552
+ last_message_summary=self._resumption_last_summary,
553
+ last_message_at=self._resumption_last_at,
554
+ request_count=self._resumption_request_count,
555
+ response_count=self._resumption_response_count,
556
+ notification_count=self._resumption_notification_count,
557
+ activity_buckets=self._build_activity_buckets("resumption", now),
558
+ )
559
+
560
+ stdio_snapshot = None
561
+ if (
562
+ self._stdio_count
563
+ or self._stdio_connected
564
+ or self._stdio_disconnect_at
565
+ or self._stdio_last_error
566
+ ):
567
+ if self._stdio_connected:
568
+ state = "open"
569
+ elif self._stdio_last_error is not None:
570
+ state = "error"
571
+ elif self._stdio_had_connection:
572
+ state = "off"
573
+ else:
574
+ state = "idle"
575
+
576
+ stdio_snapshot = ChannelSnapshot(
577
+ connected=self._stdio_connected,
578
+ state=state,
579
+ connect_at=self._stdio_connect_at,
580
+ disconnect_at=self._stdio_disconnect_at,
581
+ message_count=self._stdio_count,
582
+ last_message_summary=self._stdio_last_summary,
583
+ last_message_at=self._stdio_last_at,
584
+ last_error=self._stdio_last_error,
585
+ last_event=self._stdio_last_event,
586
+ last_event_at=self._stdio_last_event_at,
587
+ request_count=self._stdio_request_count,
588
+ response_count=self._stdio_response_count,
589
+ notification_count=self._stdio_notification_count,
590
+ activity_buckets=self._build_activity_buckets("stdio", now),
591
+ )
592
+
593
+ return TransportSnapshot(
594
+ post=post_snapshot,
595
+ post_json=post_json_snapshot,
596
+ post_sse=post_sse_snapshot,
597
+ get=get_snapshot,
598
+ resumption=resumption_snapshot,
599
+ stdio=stdio_snapshot,
600
+ )
@@ -18,18 +18,22 @@ Common analysis packages such as Pandas, Seaborn and Matplotlib are already inst
18
18
  You can add further packages if needed.
19
19
  Data files are accessible from the /mnt/data/ directory (this is the current working directory).
20
20
  Visualisations should be saved as .png files in the current working directory.
21
+
22
+ {{serverInstructions}}
23
+
21
24
  """,
22
25
  servers=["interpreter"],
23
26
  )
24
- @fast.agent(name="another_test", instruction="", servers=["filesystem"])
27
+ # @fast.agent(name="another_test", instruction="", servers=["filesystem"])
25
28
  async def main() -> None:
26
29
  # Use the app's context manager
27
30
  async with fast.run() as agent:
28
- await agent(
31
+ await agent.interactive()
32
+ await agent.data_analysis(
29
33
  "There is a csv file in the current directory. "
30
34
  "Analyse the file, produce a detailed description of the data, and any patterns it contains.",
31
35
  )
32
- await agent(
36
+ await agent.data_analysis(
33
37
  "Consider the data, and how to usefully group it for presentation to a Human. Find insights, using the Python Interpreter as needed.\n"
34
38
  "Use MatPlotLib to produce insightful visualisations. Save them as '.png' files in the current directory. Be sure to run the code and save the files.\n"
35
39
  "Produce a summary with major insights to the data",
@@ -542,7 +542,27 @@ class ConsoleDisplay:
542
542
  f"{len(content)} Content Blocks" if len(content) > 1 else "1 Content Block"
543
543
  )
544
544
 
545
- # Build right info
545
+ # Build transport channel info for bottom bar
546
+ channel = getattr(result, "transport_channel", None)
547
+ bottom_metadata = None
548
+ if channel:
549
+ # Format channel info for bottom bar
550
+ if channel == "post-json":
551
+ transport_info = "HTTP (JSON-RPC)"
552
+ elif channel == "post-sse":
553
+ transport_info = "HTTP (SSE)"
554
+ elif channel == "get":
555
+ transport_info = "HTTP (SSE)"
556
+ elif channel == "resumption":
557
+ transport_info = "Resumption"
558
+ elif channel == "stdio":
559
+ transport_info = "STDIO"
560
+ else:
561
+ transport_info = channel.upper()
562
+
563
+ bottom_metadata = [transport_info]
564
+
565
+ # Build right info (without channel info)
546
566
  right_info = f"[dim]tool result - {status}[/dim]"
547
567
 
548
568
  # Display using unified method
@@ -551,6 +571,7 @@ class ConsoleDisplay:
551
571
  message_type=MessageType.TOOL_RESULT,
552
572
  name=name,
553
573
  right_info=right_info,
574
+ bottom_metadata=bottom_metadata,
554
575
  is_error=result.isError,
555
576
  truncate_content=True,
556
577
  )