forbin-mcp 0.1.0__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.
- forbin/__init__.py +36 -0
- forbin/__main__.py +9 -0
- forbin/cli.py +288 -0
- forbin/client.py +232 -0
- forbin/config.py +28 -0
- forbin/display.py +260 -0
- forbin/tools.py +193 -0
- forbin/utils.py +111 -0
- forbin_mcp-0.1.0.dist-info/METADATA +392 -0
- forbin_mcp-0.1.0.dist-info/RECORD +13 -0
- forbin_mcp-0.1.0.dist-info/WHEEL +4 -0
- forbin_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- forbin_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
forbin/display.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import List, Any, Optional
|
|
3
|
+
from rich.console import Console, Group
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
from rich.syntax import Syntax
|
|
8
|
+
from rich.control import Control
|
|
9
|
+
from rich.segment import ControlType
|
|
10
|
+
|
|
11
|
+
# Global console instance with constrained width for better readability
|
|
12
|
+
console = Console(width=100)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def display_logo():
|
|
16
|
+
"""Display the Forbin ASCII logo."""
|
|
17
|
+
logo = """
|
|
18
|
+
[bold cyan]
|
|
19
|
+
███████╗ ██████╗ ██████╗ ██████╗ ██╗███╗ ██╗
|
|
20
|
+
██╔════╝██╔═══██╗██╔══██╗██╔══██╗██║████╗ ██║
|
|
21
|
+
█████╗ ██║ ██║██████╔╝██████╔╝██║██╔██╗ ██║
|
|
22
|
+
██╔══╝ ██║ ██║██╔══██╗██╔══██╗██║██║╚██╗██║
|
|
23
|
+
██║ ╚██████╔╝██║ ██║██████╔╝██║██║ ╚████║
|
|
24
|
+
╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚═╝╚═╝ ╚═══╝[/bold cyan]
|
|
25
|
+
[dim] MCP Remote Tool Tester v1.0.0[/dim]
|
|
26
|
+
[italic dim] "This is the voice of world control..."[/italic dim]
|
|
27
|
+
"""
|
|
28
|
+
console.print(logo)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def display_config_panel(server_url: Optional[str], health_url: Optional[str] = None):
|
|
32
|
+
"""Display configuration information in a panel."""
|
|
33
|
+
|
|
34
|
+
config_table = Table.grid(padding=(0, 2))
|
|
35
|
+
config_table.add_column(style="bold cyan", justify="right")
|
|
36
|
+
config_table.add_column(style="white")
|
|
37
|
+
|
|
38
|
+
server_url_display = server_url or "[dim]Not configured[/dim]"
|
|
39
|
+
config_table.add_row("Server URL:", server_url_display)
|
|
40
|
+
if health_url:
|
|
41
|
+
config_table.add_row("Health URL:", health_url)
|
|
42
|
+
else:
|
|
43
|
+
config_table.add_row("Health URL:", "[dim]Not configured[/dim]")
|
|
44
|
+
|
|
45
|
+
console.print()
|
|
46
|
+
console.print(
|
|
47
|
+
Panel(
|
|
48
|
+
config_table,
|
|
49
|
+
title="[bold]Configuration[/bold]",
|
|
50
|
+
title_align="left",
|
|
51
|
+
border_style="cyan",
|
|
52
|
+
padding=(1, 2),
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
console.print()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def display_step(
|
|
59
|
+
step_num: int, total_steps: int, title: str, status: str = "in_progress", update: bool = False
|
|
60
|
+
):
|
|
61
|
+
"""Display a step indicator with status.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
step_num: Current step number
|
|
65
|
+
total_steps: Total number of steps
|
|
66
|
+
title: Step title
|
|
67
|
+
status: One of 'in_progress', 'success', 'skip'
|
|
68
|
+
update: If True, updates the previous line instead of creating a new one
|
|
69
|
+
"""
|
|
70
|
+
icons = {"in_progress": ">", "success": "+", "skip": "-"}
|
|
71
|
+
|
|
72
|
+
colors = {"in_progress": "yellow", "success": "green", "skip": "dim"}
|
|
73
|
+
|
|
74
|
+
icon = icons.get(status, "*")
|
|
75
|
+
color = colors.get(status, "white")
|
|
76
|
+
|
|
77
|
+
step_text = f"[{color}]{icon} Step {step_num}/{total_steps}:[/{color}] [bold {color}]{title}[/bold {color}]"
|
|
78
|
+
|
|
79
|
+
if update:
|
|
80
|
+
# Move cursor up one line and clear it, then print the updated status
|
|
81
|
+
console.control(Control((ControlType.CURSOR_UP, 1), (ControlType.ERASE_IN_LINE, 2)))
|
|
82
|
+
console.print(step_text)
|
|
83
|
+
else:
|
|
84
|
+
console.print(step_text)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def display_tools(tools: List[Any]):
|
|
88
|
+
"""Display a compact list of available tools."""
|
|
89
|
+
if not tools:
|
|
90
|
+
console.print(
|
|
91
|
+
Panel(
|
|
92
|
+
"No tools available on this server.", title="Available Tools", border_style="yellow"
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
console.print()
|
|
98
|
+
console.print("[bold underline]Available Tools[/bold underline]")
|
|
99
|
+
console.print()
|
|
100
|
+
|
|
101
|
+
for i, tool in enumerate(tools, 1):
|
|
102
|
+
description = tool.description.strip() if tool.description else "No description"
|
|
103
|
+
# Truncate long descriptions for compact display
|
|
104
|
+
if len(description) > 60:
|
|
105
|
+
description = description[:57] + "..."
|
|
106
|
+
console.print(
|
|
107
|
+
f" [bold cyan]{i:2}[/bold cyan]. [white]{tool.name}[/white] [dim]- {description}[/dim]"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
console.print()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _highlight_json_in_text(text: str):
|
|
114
|
+
"""Highlight JSON content in text with syntax colors.
|
|
115
|
+
|
|
116
|
+
Detects JSON objects/arrays and applies basic syntax highlighting.
|
|
117
|
+
Returns a Text object with styled content.
|
|
118
|
+
"""
|
|
119
|
+
import re
|
|
120
|
+
|
|
121
|
+
# Simple check if text looks like it contains JSON
|
|
122
|
+
if not any(char in text for char in ["{", "[", '":']):
|
|
123
|
+
return text
|
|
124
|
+
|
|
125
|
+
# Try to detect and highlight JSON-like patterns
|
|
126
|
+
result = Text()
|
|
127
|
+
|
|
128
|
+
# Pattern to match JSON-like content (simple approach)
|
|
129
|
+
# This will highlight common JSON patterns with colors
|
|
130
|
+
current_pos = 0
|
|
131
|
+
|
|
132
|
+
# Find JSON strings (simple pattern for "key": "value")
|
|
133
|
+
string_pattern = r'"([^"\\]*(\\.[^"\\]*)*)"'
|
|
134
|
+
|
|
135
|
+
for match in re.finditer(string_pattern, text):
|
|
136
|
+
# Add text before match
|
|
137
|
+
if match.start() > current_pos:
|
|
138
|
+
result.append(text[current_pos : match.start()])
|
|
139
|
+
|
|
140
|
+
# Add the matched string with color
|
|
141
|
+
matched_text = match.group(0)
|
|
142
|
+
|
|
143
|
+
# Check if this looks like a key (followed by :)
|
|
144
|
+
next_char_pos = match.end()
|
|
145
|
+
if next_char_pos < len(text) and text[next_char_pos : next_char_pos + 1].strip().startswith(
|
|
146
|
+
":"
|
|
147
|
+
):
|
|
148
|
+
result.append(matched_text, style="bold cyan") # JSON key
|
|
149
|
+
else:
|
|
150
|
+
result.append(matched_text, style="green") # JSON value
|
|
151
|
+
|
|
152
|
+
current_pos = match.end()
|
|
153
|
+
|
|
154
|
+
# Add remaining text
|
|
155
|
+
if current_pos < len(text):
|
|
156
|
+
remaining = text[current_pos:]
|
|
157
|
+
# Highlight other JSON syntax
|
|
158
|
+
remaining = remaining.replace("{", "{\u200b") # Add zero-width space for splitting
|
|
159
|
+
remaining = remaining.replace("}", "}\u200b")
|
|
160
|
+
remaining = remaining.replace("[", "[\u200b")
|
|
161
|
+
remaining = remaining.replace("]", "]\u200b")
|
|
162
|
+
remaining = remaining.replace(":", ":\u200b")
|
|
163
|
+
|
|
164
|
+
for part in remaining.split("\u200b"):
|
|
165
|
+
if part in ["{", "}", "[", "]"]:
|
|
166
|
+
result.append(part, style="bold yellow")
|
|
167
|
+
elif part == ":":
|
|
168
|
+
result.append(part, style="dim")
|
|
169
|
+
else:
|
|
170
|
+
result.append(part)
|
|
171
|
+
|
|
172
|
+
return result if len(result) > 0 else text
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def display_tool_header(tool: Any):
|
|
176
|
+
"""Display a simple header for the tool view."""
|
|
177
|
+
console.print()
|
|
178
|
+
console.rule(f"[bold cyan]{tool.name}[/bold cyan]")
|
|
179
|
+
console.print()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def display_tool_menu():
|
|
183
|
+
"""Display the tool view menu options."""
|
|
184
|
+
console.print("[bold underline]Options:[/bold underline]")
|
|
185
|
+
console.print(" [bold cyan]d[/bold cyan] - View details")
|
|
186
|
+
console.print(" [bold cyan]r[/bold cyan] - Run tool")
|
|
187
|
+
console.print(" [bold cyan]b[/bold cyan] - Back to tool list")
|
|
188
|
+
console.print(" [bold cyan]q[/bold cyan] - Quit")
|
|
189
|
+
console.print()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _parse_description_with_code_blocks(description: str) -> List[Any]:
|
|
193
|
+
"""Parse description and extract code blocks for syntax highlighting."""
|
|
194
|
+
import re
|
|
195
|
+
|
|
196
|
+
content: List[Any] = []
|
|
197
|
+
|
|
198
|
+
# Pattern to match ```json ... ``` or ``` ... ``` code blocks
|
|
199
|
+
code_block_pattern = r"```(\w*)\n(.*?)```"
|
|
200
|
+
|
|
201
|
+
last_end = 0
|
|
202
|
+
for match in re.finditer(code_block_pattern, description, re.DOTALL):
|
|
203
|
+
# Add text before the code block
|
|
204
|
+
before_text = description[last_end : match.start()].strip()
|
|
205
|
+
if before_text:
|
|
206
|
+
content.append(Text(before_text))
|
|
207
|
+
content.append(Text(""))
|
|
208
|
+
|
|
209
|
+
# Get language and code
|
|
210
|
+
lang = match.group(1) or "json"
|
|
211
|
+
code = match.group(2).strip()
|
|
212
|
+
|
|
213
|
+
# Add syntax-highlighted code block
|
|
214
|
+
content.append(Syntax(code, lang, theme="monokai", line_numbers=False))
|
|
215
|
+
content.append(Text(""))
|
|
216
|
+
|
|
217
|
+
last_end = match.end()
|
|
218
|
+
|
|
219
|
+
# Add any remaining text after the last code block
|
|
220
|
+
remaining = description[last_end:].strip()
|
|
221
|
+
if remaining:
|
|
222
|
+
content.append(Text(remaining))
|
|
223
|
+
|
|
224
|
+
return content
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def display_tool_schema(tool: Any):
|
|
228
|
+
"""Display detailed schema for a specific tool with syntax-highlighted JSON."""
|
|
229
|
+
|
|
230
|
+
content: List[Any] = []
|
|
231
|
+
|
|
232
|
+
# Description with parsed code blocks
|
|
233
|
+
if tool.description:
|
|
234
|
+
parsed_content = _parse_description_with_code_blocks(tool.description)
|
|
235
|
+
content.extend(parsed_content)
|
|
236
|
+
if parsed_content:
|
|
237
|
+
content.append(Text(""))
|
|
238
|
+
|
|
239
|
+
# Input Schema as syntax-highlighted JSON
|
|
240
|
+
if tool.inputSchema:
|
|
241
|
+
content.append(Text("Input Schema:", style="bold underline"))
|
|
242
|
+
content.append(Text(""))
|
|
243
|
+
json_str = json.dumps(tool.inputSchema, indent=2)
|
|
244
|
+
content.append(Syntax(json_str, "json", theme="monokai", line_numbers=False))
|
|
245
|
+
else:
|
|
246
|
+
content.append(Text("No input parameters required.", style="dim"))
|
|
247
|
+
|
|
248
|
+
# Combine all content
|
|
249
|
+
panel_content = Group(*content)
|
|
250
|
+
|
|
251
|
+
console.print()
|
|
252
|
+
console.print(
|
|
253
|
+
Panel(
|
|
254
|
+
panel_content,
|
|
255
|
+
title=f"[bold]{tool.name}[/bold] - Details",
|
|
256
|
+
border_style="blue",
|
|
257
|
+
expand=False,
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
console.print()
|
forbin/tools.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any, Dict, List, TYPE_CHECKING
|
|
4
|
+
from rich.prompt import Prompt
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.syntax import Syntax
|
|
7
|
+
|
|
8
|
+
from .display import console
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .client import MCPSession
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def list_tools(mcp_session: "MCPSession") -> List[Any]:
|
|
15
|
+
"""
|
|
16
|
+
List all available tools from the MCP server.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
mcp_session: Connected MCPSession
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of tool objects
|
|
23
|
+
"""
|
|
24
|
+
with console.status(" [dim]Retrieving tool manifest...[/dim]", spinner="dots"):
|
|
25
|
+
tools = await asyncio.wait_for(mcp_session.list_tools(), timeout=15.0)
|
|
26
|
+
|
|
27
|
+
return tools
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_parameter_value(value_str: str, param_type: str) -> Any:
|
|
31
|
+
"""Parse a string input into the appropriate type."""
|
|
32
|
+
if not value_str.strip():
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
if param_type == "boolean":
|
|
36
|
+
return value_str.lower() in ("true", "t", "yes", "y", "1")
|
|
37
|
+
elif param_type == "integer":
|
|
38
|
+
try:
|
|
39
|
+
return int(value_str)
|
|
40
|
+
except ValueError:
|
|
41
|
+
# Re-raise to be caught by caller
|
|
42
|
+
raise
|
|
43
|
+
elif param_type == "number":
|
|
44
|
+
try:
|
|
45
|
+
return float(value_str)
|
|
46
|
+
except ValueError:
|
|
47
|
+
raise
|
|
48
|
+
elif param_type in ("object", "array"):
|
|
49
|
+
try:
|
|
50
|
+
return json.loads(value_str)
|
|
51
|
+
except json.JSONDecodeError:
|
|
52
|
+
raise
|
|
53
|
+
else: # string
|
|
54
|
+
return value_str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_tool_parameters(tool: Any) -> Dict[str, Any]:
|
|
58
|
+
"""Interactively collect parameters for a tool."""
|
|
59
|
+
params: dict[str, Any] = {}
|
|
60
|
+
|
|
61
|
+
if not tool.inputSchema or not isinstance(tool.inputSchema, dict):
|
|
62
|
+
return params
|
|
63
|
+
|
|
64
|
+
properties = tool.inputSchema.get("properties", {})
|
|
65
|
+
required = tool.inputSchema.get("required", [])
|
|
66
|
+
|
|
67
|
+
if not properties:
|
|
68
|
+
return params
|
|
69
|
+
|
|
70
|
+
console.print()
|
|
71
|
+
console.rule("[bold cyan]ENTER PARAMETERS[/bold cyan]")
|
|
72
|
+
console.print("Enter parameter values (press [bold]Enter[/bold] to skip optional parameters)\n")
|
|
73
|
+
|
|
74
|
+
for param_name, param_info in properties.items():
|
|
75
|
+
param_type = param_info.get("type", "string")
|
|
76
|
+
param_desc = param_info.get("description", "")
|
|
77
|
+
is_required = param_name in required
|
|
78
|
+
|
|
79
|
+
# Show parameter info
|
|
80
|
+
req_str = "[red](required)[/red]" if is_required else "[green](optional)[/green]"
|
|
81
|
+
console.print(f"[bold cyan]{param_name}[/bold cyan] ({param_type}) {req_str}")
|
|
82
|
+
if param_desc:
|
|
83
|
+
console.print(f" [dim]{param_desc}[/dim]")
|
|
84
|
+
|
|
85
|
+
# Show enum values if present
|
|
86
|
+
if "enum" in param_info:
|
|
87
|
+
console.print(f" Allowed values: {', '.join(str(v) for v in param_info['enum'])}")
|
|
88
|
+
|
|
89
|
+
# Get value
|
|
90
|
+
while True:
|
|
91
|
+
try:
|
|
92
|
+
# We use generic Prompt and handle manual validation to support complex types and skipping
|
|
93
|
+
value_str = Prompt.ask(" ->", default="", show_default=False)
|
|
94
|
+
|
|
95
|
+
if not value_str:
|
|
96
|
+
if is_required:
|
|
97
|
+
console.print(
|
|
98
|
+
" [red]This parameter is required. Please enter a value.[/red]"
|
|
99
|
+
)
|
|
100
|
+
continue
|
|
101
|
+
else:
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
# Parse the value
|
|
105
|
+
value = parse_parameter_value(value_str, param_type)
|
|
106
|
+
params[param_name] = value
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
except (ValueError, json.JSONDecodeError) as e:
|
|
110
|
+
console.print(f" [red]Invalid value for type {param_type}:[/red] {e}")
|
|
111
|
+
console.print(" Please try again.")
|
|
112
|
+
|
|
113
|
+
console.print()
|
|
114
|
+
|
|
115
|
+
return params
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def call_tool(mcp_session: "MCPSession", tool: Any, params: Dict[str, Any]):
|
|
119
|
+
"""Call a tool with the given parameters."""
|
|
120
|
+
console.print()
|
|
121
|
+
console.rule("[bold magenta]CALLING TOOL[/bold magenta]")
|
|
122
|
+
console.print(f"Tool: [bold]{tool.name}[/bold]")
|
|
123
|
+
console.print()
|
|
124
|
+
|
|
125
|
+
# Show parameters nicely
|
|
126
|
+
if params:
|
|
127
|
+
json_str = json.dumps(params, indent=2)
|
|
128
|
+
console.print(
|
|
129
|
+
Panel(
|
|
130
|
+
Syntax(json_str, "json", theme="monokai", line_numbers=False),
|
|
131
|
+
title="[bold]Parameters[/bold]",
|
|
132
|
+
title_align="left",
|
|
133
|
+
border_style="cyan",
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
console.print("[dim]No parameters[/dim]")
|
|
138
|
+
|
|
139
|
+
console.print("\n[bold]Executing...[/bold]")
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
with console.status("Waiting for response...", spinner="dots"):
|
|
143
|
+
result = await mcp_session.call_tool(tool.name, params)
|
|
144
|
+
|
|
145
|
+
console.print("\n[bold green]Tool execution completed![/bold green]\n")
|
|
146
|
+
console.rule("[bold green]RESULT[/bold green]")
|
|
147
|
+
console.print()
|
|
148
|
+
|
|
149
|
+
# Extract and display result
|
|
150
|
+
if result.content:
|
|
151
|
+
for item in result.content:
|
|
152
|
+
text = getattr(item, "text", None)
|
|
153
|
+
if text:
|
|
154
|
+
# Try to detect if it looks like JSON for syntax highlighting
|
|
155
|
+
text_stripped = text.strip()
|
|
156
|
+
if text_stripped.startswith(("{", "[")) and text_stripped.endswith(("}", "]")):
|
|
157
|
+
try:
|
|
158
|
+
# Validate and pretty-print JSON
|
|
159
|
+
parsed = json.loads(text_stripped)
|
|
160
|
+
formatted = json.dumps(parsed, indent=2)
|
|
161
|
+
console.print(
|
|
162
|
+
Panel(
|
|
163
|
+
Syntax(formatted, "json", theme="monokai", line_numbers=False),
|
|
164
|
+
border_style="green",
|
|
165
|
+
title="[bold]Response[/bold]",
|
|
166
|
+
title_align="left",
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
continue
|
|
170
|
+
except json.JSONDecodeError:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
# For non-JSON text responses
|
|
174
|
+
console.print(
|
|
175
|
+
Panel(
|
|
176
|
+
text.strip(),
|
|
177
|
+
border_style="green",
|
|
178
|
+
title="[bold]Response[/bold]",
|
|
179
|
+
title_align="left",
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
console.print(str(item))
|
|
184
|
+
else:
|
|
185
|
+
console.print("[dim]No content returned[/dim]")
|
|
186
|
+
|
|
187
|
+
console.print()
|
|
188
|
+
console.rule()
|
|
189
|
+
console.print()
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
console.print(f"[bold red]Tool execution failed:[/bold red] {type(e).__name__}")
|
|
193
|
+
console.print(f" Error: {e}\n")
|
forbin/utils.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import asyncio
|
|
3
|
+
import select
|
|
4
|
+
from . import config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Suppress stderr warnings from MCP library (like "Session termination failed: 400")
|
|
8
|
+
class FilteredStderr:
|
|
9
|
+
def __init__(self, original_stderr):
|
|
10
|
+
self.original_stderr = original_stderr
|
|
11
|
+
self.suppress_patterns = [
|
|
12
|
+
"Error in post_writer",
|
|
13
|
+
"Session termination failed",
|
|
14
|
+
"httpx.HTTPStatusError",
|
|
15
|
+
"streamable_http.py",
|
|
16
|
+
"Traceback (most recent call last)",
|
|
17
|
+
"File ", # Suppress file paths in tracebacks
|
|
18
|
+
"raise ",
|
|
19
|
+
"await ",
|
|
20
|
+
"BrokenResourceError",
|
|
21
|
+
"ClosedResourceError",
|
|
22
|
+
"raise_for_status",
|
|
23
|
+
"handle_request_async",
|
|
24
|
+
"_handle_post_request",
|
|
25
|
+
]
|
|
26
|
+
self.buffer = ""
|
|
27
|
+
self.suppressing = False
|
|
28
|
+
self.suppress_depth = 0
|
|
29
|
+
|
|
30
|
+
def write(self, text):
|
|
31
|
+
# If verbose mode is ON, don't suppress anything
|
|
32
|
+
if config.VERBOSE:
|
|
33
|
+
self.original_stderr.write(text)
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
# Check if this line starts a suppressible error block
|
|
37
|
+
if any(pattern in text for pattern in self.suppress_patterns):
|
|
38
|
+
self.suppressing = True
|
|
39
|
+
self.suppress_depth = 10 # Suppress next 10 lines
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# If we're suppressing, decrement counter
|
|
43
|
+
if self.suppressing:
|
|
44
|
+
if text.strip() == "":
|
|
45
|
+
# Blank line ends suppression
|
|
46
|
+
self.suppressing = False
|
|
47
|
+
self.suppress_depth = 0
|
|
48
|
+
else:
|
|
49
|
+
# Any line during suppression decrements counter
|
|
50
|
+
self.suppress_depth -= 1
|
|
51
|
+
if self.suppress_depth <= 0:
|
|
52
|
+
self.suppressing = False
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# If not suppressing, write to original stderr
|
|
58
|
+
if not self.suppressing:
|
|
59
|
+
self.original_stderr.write(text)
|
|
60
|
+
|
|
61
|
+
def flush(self):
|
|
62
|
+
self.original_stderr.flush()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def setup_logging():
|
|
66
|
+
"""Replace stderr with filtered version."""
|
|
67
|
+
sys.stderr = FilteredStderr(sys.stderr)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def listen_for_toggle():
|
|
71
|
+
"""
|
|
72
|
+
Background task to listen for 'v' key to toggle verbose logging.
|
|
73
|
+
Uses non-blocking stdin read.
|
|
74
|
+
"""
|
|
75
|
+
# Only try to import termios/tty on Unix-like systems
|
|
76
|
+
try:
|
|
77
|
+
import termios
|
|
78
|
+
import tty
|
|
79
|
+
except ImportError:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
fd = sys.stdin.fileno()
|
|
83
|
+
# Check if we are in a terminal
|
|
84
|
+
if not sys.stdin.isatty():
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
old_settings = termios.tcgetattr(fd)
|
|
88
|
+
try:
|
|
89
|
+
tty.setcbreak(fd)
|
|
90
|
+
while True:
|
|
91
|
+
# Non-blocking check for input
|
|
92
|
+
if select.select([sys.stdin], [], [], 0.1)[0]:
|
|
93
|
+
char = sys.stdin.read(1).lower()
|
|
94
|
+
if char == "v":
|
|
95
|
+
config.VERBOSE = not config.VERBOSE
|
|
96
|
+
from .display import console
|
|
97
|
+
|
|
98
|
+
status = (
|
|
99
|
+
"[bold green]ON[/bold green]"
|
|
100
|
+
if config.VERBOSE
|
|
101
|
+
else "[bold red]OFF[/bold red]"
|
|
102
|
+
)
|
|
103
|
+
# Clear current line and print toggle status
|
|
104
|
+
console.print(f"\n[bold cyan]Verbose logging toggled {status}[/bold cyan]")
|
|
105
|
+
|
|
106
|
+
await asyncio.sleep(0.1)
|
|
107
|
+
except Exception:
|
|
108
|
+
# Silently fail if something goes wrong with the terminal settings
|
|
109
|
+
pass
|
|
110
|
+
finally:
|
|
111
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|