fast-agent-mcp 0.3.13__py3-none-any.whl → 0.3.15__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.
- fast_agent/agents/llm_agent.py +59 -37
- fast_agent/agents/llm_decorator.py +13 -2
- fast_agent/agents/mcp_agent.py +21 -5
- fast_agent/agents/tool_agent.py +41 -29
- fast_agent/agents/workflow/router_agent.py +2 -1
- fast_agent/cli/commands/check_config.py +48 -1
- fast_agent/config.py +65 -2
- fast_agent/constants.py +3 -0
- fast_agent/context.py +42 -9
- fast_agent/core/fastagent.py +14 -1
- fast_agent/core/logging/listeners.py +1 -1
- fast_agent/core/validation.py +31 -33
- fast_agent/event_progress.py +2 -3
- fast_agent/human_input/form_fields.py +4 -1
- fast_agent/interfaces.py +12 -2
- fast_agent/llm/fastagent_llm.py +31 -0
- fast_agent/llm/model_database.py +2 -2
- fast_agent/llm/model_factory.py +8 -1
- fast_agent/llm/provider_key_manager.py +1 -0
- fast_agent/llm/provider_types.py +1 -0
- fast_agent/llm/request_params.py +3 -1
- fast_agent/mcp/mcp_aggregator.py +313 -40
- fast_agent/mcp/mcp_connection_manager.py +39 -9
- fast_agent/mcp/prompt_message_extended.py +2 -2
- fast_agent/mcp/skybridge.py +45 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/transport_tracking.py +37 -3
- fast_agent/mcp/types.py +24 -0
- fast_agent/resources/examples/workflows/router.py +1 -0
- fast_agent/resources/setup/fastagent.config.yaml +7 -1
- fast_agent/ui/console_display.py +946 -84
- fast_agent/ui/elicitation_form.py +23 -1
- fast_agent/ui/enhanced_prompt.py +153 -58
- fast_agent/ui/interactive_prompt.py +57 -34
- fast_agent/ui/markdown_truncator.py +942 -0
- fast_agent/ui/mcp_display.py +110 -29
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/rich_progress.py +4 -1
- fast_agent/ui/streaming_buffer.py +449 -0
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/METADATA +4 -3
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/RECORD +44 -38
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""SSE transport wrapper that emits channel events for UI display."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable
|
|
8
|
+
from urllib.parse import parse_qs, urljoin, urlparse
|
|
9
|
+
|
|
10
|
+
import anyio
|
|
11
|
+
import httpx
|
|
12
|
+
import mcp.types as types
|
|
13
|
+
from httpx_sse import aconnect_sse
|
|
14
|
+
from httpx_sse._exceptions import SSEError
|
|
15
|
+
from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client
|
|
16
|
+
from mcp.shared.message import SessionMessage
|
|
17
|
+
|
|
18
|
+
from fast_agent.mcp.transport_tracking import ChannelEvent, ChannelName
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from anyio.abc import TaskStatus
|
|
22
|
+
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
ChannelHook = Callable[[ChannelEvent], None]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _extract_session_id(endpoint_url: str) -> str | None:
|
|
30
|
+
parsed = urlparse(endpoint_url)
|
|
31
|
+
query_params = parse_qs(parsed.query)
|
|
32
|
+
for key in ("sessionId", "session_id", "session"):
|
|
33
|
+
values = query_params.get(key)
|
|
34
|
+
if values:
|
|
35
|
+
return values[0]
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _emit_channel_event(
|
|
40
|
+
channel_hook: ChannelHook | None,
|
|
41
|
+
channel: ChannelName,
|
|
42
|
+
event_type: str,
|
|
43
|
+
*,
|
|
44
|
+
message: types.JSONRPCMessage | None = None,
|
|
45
|
+
raw_event: str | None = None,
|
|
46
|
+
detail: str | None = None,
|
|
47
|
+
status_code: int | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
if channel_hook is None:
|
|
50
|
+
return
|
|
51
|
+
try:
|
|
52
|
+
channel_hook(
|
|
53
|
+
ChannelEvent(
|
|
54
|
+
channel=channel,
|
|
55
|
+
event_type=event_type, # type: ignore[arg-type]
|
|
56
|
+
message=message,
|
|
57
|
+
raw_event=raw_event,
|
|
58
|
+
detail=detail,
|
|
59
|
+
status_code=status_code,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
except Exception:
|
|
63
|
+
logger.debug("Channel hook raised an exception", exc_info=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _format_http_error(exc: httpx.HTTPStatusError) -> tuple[int | None, str]:
|
|
67
|
+
status_code: int | None = None
|
|
68
|
+
detail = str(exc)
|
|
69
|
+
if exc.response is not None:
|
|
70
|
+
status_code = exc.response.status_code
|
|
71
|
+
reason = exc.response.reason_phrase or ""
|
|
72
|
+
if not reason:
|
|
73
|
+
try:
|
|
74
|
+
reason = (exc.response.text or "").strip()
|
|
75
|
+
except Exception:
|
|
76
|
+
reason = ""
|
|
77
|
+
detail = f"HTTP {status_code}: {reason or 'response'}"
|
|
78
|
+
return status_code, detail
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@asynccontextmanager
|
|
82
|
+
async def tracking_sse_client(
|
|
83
|
+
url: str,
|
|
84
|
+
headers: dict[str, Any] | None = None,
|
|
85
|
+
timeout: float = 5,
|
|
86
|
+
sse_read_timeout: float = 60 * 5,
|
|
87
|
+
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
|
|
88
|
+
auth: httpx.Auth | None = None,
|
|
89
|
+
channel_hook: ChannelHook | None = None,
|
|
90
|
+
) -> AsyncGenerator[
|
|
91
|
+
tuple[
|
|
92
|
+
MemoryObjectReceiveStream[SessionMessage | Exception],
|
|
93
|
+
MemoryObjectSendStream[SessionMessage],
|
|
94
|
+
Callable[[], str | None],
|
|
95
|
+
],
|
|
96
|
+
None,
|
|
97
|
+
]:
|
|
98
|
+
"""
|
|
99
|
+
Client transport for SSE with channel activity tracking.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
read_stream_writer, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception](
|
|
103
|
+
0
|
|
104
|
+
)
|
|
105
|
+
write_stream, write_stream_reader = anyio.create_memory_object_stream[SessionMessage](0)
|
|
106
|
+
|
|
107
|
+
session_id: str | None = None
|
|
108
|
+
|
|
109
|
+
def get_session_id() -> str | None:
|
|
110
|
+
return session_id
|
|
111
|
+
|
|
112
|
+
async with anyio.create_task_group() as tg:
|
|
113
|
+
try:
|
|
114
|
+
logger.debug("Connecting to SSE endpoint: %s", url)
|
|
115
|
+
async with httpx_client_factory(
|
|
116
|
+
headers=headers,
|
|
117
|
+
auth=auth,
|
|
118
|
+
timeout=httpx.Timeout(timeout, read=sse_read_timeout),
|
|
119
|
+
) as client:
|
|
120
|
+
connected = False
|
|
121
|
+
post_connected = False
|
|
122
|
+
|
|
123
|
+
async def sse_reader(
|
|
124
|
+
task_status: TaskStatus[str] = anyio.TASK_STATUS_IGNORED,
|
|
125
|
+
):
|
|
126
|
+
try:
|
|
127
|
+
async for sse in event_source.aiter_sse():
|
|
128
|
+
if sse.event == "endpoint":
|
|
129
|
+
endpoint_url = urljoin(url, sse.data)
|
|
130
|
+
logger.debug("Received SSE endpoint URL: %s", endpoint_url)
|
|
131
|
+
|
|
132
|
+
url_parsed = urlparse(url)
|
|
133
|
+
endpoint_parsed = urlparse(endpoint_url)
|
|
134
|
+
if (
|
|
135
|
+
url_parsed.scheme != endpoint_parsed.scheme
|
|
136
|
+
or url_parsed.netloc != endpoint_parsed.netloc
|
|
137
|
+
):
|
|
138
|
+
error_msg = (
|
|
139
|
+
"Endpoint origin does not match connection origin: "
|
|
140
|
+
f"{endpoint_url}"
|
|
141
|
+
)
|
|
142
|
+
logger.error(error_msg)
|
|
143
|
+
_emit_channel_event(
|
|
144
|
+
channel_hook,
|
|
145
|
+
"get",
|
|
146
|
+
"error",
|
|
147
|
+
detail=error_msg,
|
|
148
|
+
)
|
|
149
|
+
raise ValueError(error_msg)
|
|
150
|
+
|
|
151
|
+
nonlocal session_id
|
|
152
|
+
session_id = _extract_session_id(endpoint_url)
|
|
153
|
+
task_status.started(endpoint_url)
|
|
154
|
+
elif sse.event == "message":
|
|
155
|
+
try:
|
|
156
|
+
message = types.JSONRPCMessage.model_validate_json(sse.data)
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
logger.exception("Error parsing server message")
|
|
159
|
+
_emit_channel_event(
|
|
160
|
+
channel_hook,
|
|
161
|
+
"get",
|
|
162
|
+
"error",
|
|
163
|
+
detail="Error parsing server message",
|
|
164
|
+
)
|
|
165
|
+
await read_stream_writer.send(exc)
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
_emit_channel_event(channel_hook, "get", "message", message=message)
|
|
169
|
+
await read_stream_writer.send(SessionMessage(message))
|
|
170
|
+
else:
|
|
171
|
+
_emit_channel_event(
|
|
172
|
+
channel_hook,
|
|
173
|
+
"get",
|
|
174
|
+
"keepalive",
|
|
175
|
+
raw_event=sse.event or "keepalive",
|
|
176
|
+
)
|
|
177
|
+
except SSEError as sse_exc:
|
|
178
|
+
logger.exception("Encountered SSE exception")
|
|
179
|
+
_emit_channel_event(
|
|
180
|
+
channel_hook,
|
|
181
|
+
"get",
|
|
182
|
+
"error",
|
|
183
|
+
detail=str(sse_exc),
|
|
184
|
+
)
|
|
185
|
+
raise
|
|
186
|
+
except Exception as exc:
|
|
187
|
+
logger.exception("Error in sse_reader")
|
|
188
|
+
_emit_channel_event(
|
|
189
|
+
channel_hook,
|
|
190
|
+
"get",
|
|
191
|
+
"error",
|
|
192
|
+
detail=str(exc),
|
|
193
|
+
)
|
|
194
|
+
await read_stream_writer.send(exc)
|
|
195
|
+
finally:
|
|
196
|
+
await read_stream_writer.aclose()
|
|
197
|
+
|
|
198
|
+
async def post_writer(endpoint_url: str):
|
|
199
|
+
try:
|
|
200
|
+
async with write_stream_reader:
|
|
201
|
+
async for session_message in write_stream_reader:
|
|
202
|
+
try:
|
|
203
|
+
payload = session_message.message.model_dump(
|
|
204
|
+
by_alias=True,
|
|
205
|
+
mode="json",
|
|
206
|
+
exclude_none=True,
|
|
207
|
+
)
|
|
208
|
+
except Exception:
|
|
209
|
+
logger.exception("Invalid session message payload")
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
_emit_channel_event(
|
|
213
|
+
channel_hook,
|
|
214
|
+
"post-sse",
|
|
215
|
+
"message",
|
|
216
|
+
message=session_message.message,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
response = await client.post(endpoint_url, json=payload)
|
|
221
|
+
response.raise_for_status()
|
|
222
|
+
except httpx.HTTPStatusError as exc:
|
|
223
|
+
status_code, detail = _format_http_error(exc)
|
|
224
|
+
_emit_channel_event(
|
|
225
|
+
channel_hook,
|
|
226
|
+
"post-sse",
|
|
227
|
+
"error",
|
|
228
|
+
detail=detail,
|
|
229
|
+
status_code=status_code,
|
|
230
|
+
)
|
|
231
|
+
raise
|
|
232
|
+
except httpx.HTTPStatusError:
|
|
233
|
+
logger.exception("HTTP error in post_writer")
|
|
234
|
+
except Exception:
|
|
235
|
+
logger.exception("Error in post_writer")
|
|
236
|
+
_emit_channel_event(
|
|
237
|
+
channel_hook,
|
|
238
|
+
"post-sse",
|
|
239
|
+
"error",
|
|
240
|
+
detail="Error sending client message",
|
|
241
|
+
)
|
|
242
|
+
finally:
|
|
243
|
+
await write_stream.aclose()
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
async with aconnect_sse(
|
|
247
|
+
client,
|
|
248
|
+
"GET",
|
|
249
|
+
url,
|
|
250
|
+
) as event_source:
|
|
251
|
+
try:
|
|
252
|
+
event_source.response.raise_for_status()
|
|
253
|
+
except httpx.HTTPStatusError as exc:
|
|
254
|
+
status_code, detail = _format_http_error(exc)
|
|
255
|
+
_emit_channel_event(
|
|
256
|
+
channel_hook,
|
|
257
|
+
"get",
|
|
258
|
+
"error",
|
|
259
|
+
detail=detail,
|
|
260
|
+
status_code=status_code,
|
|
261
|
+
)
|
|
262
|
+
raise
|
|
263
|
+
|
|
264
|
+
_emit_channel_event(channel_hook, "get", "connect")
|
|
265
|
+
connected = True
|
|
266
|
+
|
|
267
|
+
endpoint_url = await tg.start(sse_reader)
|
|
268
|
+
_emit_channel_event(channel_hook, "post-sse", "connect")
|
|
269
|
+
post_connected = True
|
|
270
|
+
tg.start_soon(post_writer, endpoint_url)
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
yield read_stream, write_stream, get_session_id
|
|
274
|
+
finally:
|
|
275
|
+
tg.cancel_scope.cancel()
|
|
276
|
+
except Exception:
|
|
277
|
+
raise
|
|
278
|
+
finally:
|
|
279
|
+
if connected:
|
|
280
|
+
_emit_channel_event(channel_hook, "get", "disconnect")
|
|
281
|
+
if post_connected:
|
|
282
|
+
_emit_channel_event(channel_hook, "post-sse", "disconnect")
|
|
283
|
+
finally:
|
|
284
|
+
await read_stream_writer.aclose()
|
|
285
|
+
await read_stream.aclose()
|
|
286
|
+
await write_stream_reader.aclose()
|
|
287
|
+
await write_stream.aclose()
|
|
@@ -82,6 +82,8 @@ class ChannelSnapshot(BaseModel):
|
|
|
82
82
|
response_count: int = 0
|
|
83
83
|
notification_count: int = 0
|
|
84
84
|
activity_buckets: list[str] | None = None
|
|
85
|
+
activity_bucket_seconds: int | None = None
|
|
86
|
+
activity_bucket_count: int | None = None
|
|
85
87
|
|
|
86
88
|
|
|
87
89
|
class TransportSnapshot(BaseModel):
|
|
@@ -95,12 +97,18 @@ class TransportSnapshot(BaseModel):
|
|
|
95
97
|
get: ChannelSnapshot | None = None
|
|
96
98
|
resumption: ChannelSnapshot | None = None
|
|
97
99
|
stdio: ChannelSnapshot | None = None
|
|
100
|
+
activity_bucket_seconds: int | None = None
|
|
101
|
+
activity_bucket_count: int | None = None
|
|
98
102
|
|
|
99
103
|
|
|
100
104
|
class TransportChannelMetrics:
|
|
101
105
|
"""Aggregates low-level channel events into user-visible metrics."""
|
|
102
106
|
|
|
103
|
-
def __init__(
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
bucket_seconds: int | None = None,
|
|
110
|
+
bucket_count: int | None = None,
|
|
111
|
+
) -> None:
|
|
104
112
|
self._lock = Lock()
|
|
105
113
|
|
|
106
114
|
self._post_modes: set[str] = set()
|
|
@@ -155,8 +163,22 @@ class TransportChannelMetrics:
|
|
|
155
163
|
|
|
156
164
|
self._response_channel_by_id: dict[RequestId, ChannelName] = {}
|
|
157
165
|
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
try:
|
|
167
|
+
seconds = 30 if bucket_seconds is None else int(bucket_seconds)
|
|
168
|
+
except (TypeError, ValueError):
|
|
169
|
+
seconds = 30
|
|
170
|
+
if seconds <= 0:
|
|
171
|
+
seconds = 30
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
count = 20 if bucket_count is None else int(bucket_count)
|
|
175
|
+
except (TypeError, ValueError):
|
|
176
|
+
count = 20
|
|
177
|
+
if count <= 0:
|
|
178
|
+
count = 20
|
|
179
|
+
|
|
180
|
+
self._history_bucket_seconds = seconds
|
|
181
|
+
self._history_bucket_count = count
|
|
160
182
|
self._history_priority = {
|
|
161
183
|
"error": 5,
|
|
162
184
|
"disabled": 4,
|
|
@@ -463,6 +485,8 @@ class TransportChannelMetrics:
|
|
|
463
485
|
last_message_summary=stats.last_summary,
|
|
464
486
|
last_message_at=stats.last_at,
|
|
465
487
|
activity_buckets=self._build_activity_buckets(f"post-{mode}", now),
|
|
488
|
+
activity_bucket_seconds=self._history_bucket_seconds,
|
|
489
|
+
activity_bucket_count=self._history_bucket_count,
|
|
466
490
|
)
|
|
467
491
|
|
|
468
492
|
def snapshot(self) -> TransportSnapshot:
|
|
@@ -503,6 +527,8 @@ class TransportChannelMetrics:
|
|
|
503
527
|
response_count=self._post_response_count,
|
|
504
528
|
notification_count=self._post_notification_count,
|
|
505
529
|
activity_buckets=self._merge_activity_buckets(["post-json", "post-sse"], now),
|
|
530
|
+
activity_bucket_seconds=self._history_bucket_seconds,
|
|
531
|
+
activity_bucket_count=self._history_bucket_count,
|
|
506
532
|
)
|
|
507
533
|
|
|
508
534
|
post_json_snapshot = self._build_post_mode_snapshot("json", now)
|
|
@@ -543,6 +569,8 @@ class TransportChannelMetrics:
|
|
|
543
569
|
response_count=self._get_response_count,
|
|
544
570
|
notification_count=self._get_notification_count,
|
|
545
571
|
activity_buckets=self._build_activity_buckets("get", now),
|
|
572
|
+
activity_bucket_seconds=self._history_bucket_seconds,
|
|
573
|
+
activity_bucket_count=self._history_bucket_count,
|
|
546
574
|
)
|
|
547
575
|
|
|
548
576
|
resumption_snapshot = None
|
|
@@ -555,6 +583,8 @@ class TransportChannelMetrics:
|
|
|
555
583
|
response_count=self._resumption_response_count,
|
|
556
584
|
notification_count=self._resumption_notification_count,
|
|
557
585
|
activity_buckets=self._build_activity_buckets("resumption", now),
|
|
586
|
+
activity_bucket_seconds=self._history_bucket_seconds,
|
|
587
|
+
activity_bucket_count=self._history_bucket_count,
|
|
558
588
|
)
|
|
559
589
|
|
|
560
590
|
stdio_snapshot = None
|
|
@@ -588,6 +618,8 @@ class TransportChannelMetrics:
|
|
|
588
618
|
response_count=self._stdio_response_count,
|
|
589
619
|
notification_count=self._stdio_notification_count,
|
|
590
620
|
activity_buckets=self._build_activity_buckets("stdio", now),
|
|
621
|
+
activity_bucket_seconds=self._history_bucket_seconds,
|
|
622
|
+
activity_bucket_count=self._history_bucket_count,
|
|
591
623
|
)
|
|
592
624
|
|
|
593
625
|
return TransportSnapshot(
|
|
@@ -597,4 +629,6 @@ class TransportChannelMetrics:
|
|
|
597
629
|
get=get_snapshot,
|
|
598
630
|
resumption=resumption_snapshot,
|
|
599
631
|
stdio=stdio_snapshot,
|
|
632
|
+
activity_bucket_seconds=self._history_bucket_seconds,
|
|
633
|
+
activity_bucket_count=self._history_bucket_count,
|
|
600
634
|
)
|
fast_agent/mcp/types.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from fast_agent.interfaces import AgentProtocol
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from fast_agent.context import Context
|
|
9
|
+
from fast_agent.mcp.mcp_aggregator import MCPAggregator
|
|
10
|
+
from fast_agent.ui.console_display import ConsoleDisplay
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class McpAgentProtocol(AgentProtocol, Protocol):
|
|
15
|
+
"""Agent protocol with MCP-specific surface area."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def aggregator(self) -> MCPAggregator: ...
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def display(self) -> "ConsoleDisplay": ...
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def context(self) -> "Context | None": ...
|
|
@@ -15,6 +15,11 @@ default_model: gpt-5-mini.low
|
|
|
15
15
|
# mcp_ui_output_dir: ".fast-agent/ui" # Where to write MCP-UI HTML files (relative to CWD if not absolute)
|
|
16
16
|
# mcp_ui_mode: enabled
|
|
17
17
|
|
|
18
|
+
# MCP timeline display (adjust activity window/intervals in MCP UI + fast-agent check)
|
|
19
|
+
mcp_timeline:
|
|
20
|
+
steps: 20 # number of timeline buckets to render
|
|
21
|
+
step_seconds: 15 # seconds per bucket (accepts values like "45s", "2m")
|
|
22
|
+
|
|
18
23
|
# Logging and Console Configuration:
|
|
19
24
|
logger:
|
|
20
25
|
# level: "debug" | "info" | "warning" | "error"
|
|
@@ -23,13 +28,14 @@ logger:
|
|
|
23
28
|
|
|
24
29
|
# Switch the progress display on or off
|
|
25
30
|
progress_display: true
|
|
26
|
-
|
|
27
31
|
# Show chat User/Assistant messages on the console
|
|
28
32
|
show_chat: true
|
|
29
33
|
# Show tool calls on the console
|
|
30
34
|
show_tools: true
|
|
31
35
|
# Truncate long tool responses on the console
|
|
32
36
|
truncate_tools: true
|
|
37
|
+
# Streaming renderer for assistant responses: "markdown", "plain", or "none"
|
|
38
|
+
streaming: markdown
|
|
33
39
|
|
|
34
40
|
# MCP Servers
|
|
35
41
|
mcp:
|