tsugite-cli 0.3.3__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.
- tsugite/__init__.py +6 -0
- tsugite/agent_composition.py +163 -0
- tsugite/agent_inheritance.py +479 -0
- tsugite/agent_preparation.py +236 -0
- tsugite/agent_runner/__init__.py +45 -0
- tsugite/agent_runner/helpers.py +106 -0
- tsugite/agent_runner/history_integration.py +248 -0
- tsugite/agent_runner/metrics.py +100 -0
- tsugite/agent_runner/runner.py +1879 -0
- tsugite/agent_runner/validation.py +70 -0
- tsugite/agent_utils.py +167 -0
- tsugite/attachments/__init__.py +65 -0
- tsugite/attachments/auto_context.py +199 -0
- tsugite/attachments/base.py +34 -0
- tsugite/attachments/file.py +51 -0
- tsugite/attachments/inline.py +31 -0
- tsugite/attachments/storage.py +178 -0
- tsugite/attachments/url.py +59 -0
- tsugite/attachments/youtube.py +101 -0
- tsugite/benchmark/__init__.py +62 -0
- tsugite/benchmark/config.py +183 -0
- tsugite/benchmark/core.py +292 -0
- tsugite/benchmark/discovery.py +377 -0
- tsugite/benchmark/evaluators.py +671 -0
- tsugite/benchmark/execution.py +657 -0
- tsugite/benchmark/metrics.py +204 -0
- tsugite/benchmark/reports.py +420 -0
- tsugite/benchmark/utils.py +288 -0
- tsugite/builtin_agents/chat-assistant.md +53 -0
- tsugite/builtin_agents/default.md +140 -0
- tsugite/builtin_agents.py +5 -0
- tsugite/cache.py +195 -0
- tsugite/cli/__init__.py +1042 -0
- tsugite/cli/agents.py +148 -0
- tsugite/cli/attachments.py +193 -0
- tsugite/cli/benchmark.py +663 -0
- tsugite/cli/cache.py +113 -0
- tsugite/cli/config.py +272 -0
- tsugite/cli/helpers.py +534 -0
- tsugite/cli/history.py +193 -0
- tsugite/cli/init.py +387 -0
- tsugite/cli/mcp.py +193 -0
- tsugite/cli/tools.py +419 -0
- tsugite/config.py +204 -0
- tsugite/console.py +48 -0
- tsugite/constants.py +21 -0
- tsugite/core/__init__.py +19 -0
- tsugite/core/agent.py +774 -0
- tsugite/core/executor.py +300 -0
- tsugite/core/memory.py +67 -0
- tsugite/core/tools.py +271 -0
- tsugite/docker_cli.py +270 -0
- tsugite/events/__init__.py +55 -0
- tsugite/events/base.py +46 -0
- tsugite/events/bus.py +62 -0
- tsugite/events/events.py +224 -0
- tsugite/exceptions.py +40 -0
- tsugite/history/__init__.py +29 -0
- tsugite/history/index.py +210 -0
- tsugite/history/models.py +106 -0
- tsugite/history/storage.py +157 -0
- tsugite/mcp_client.py +219 -0
- tsugite/mcp_config.py +174 -0
- tsugite/md_agents.py +751 -0
- tsugite/models.py +257 -0
- tsugite/renderer.py +151 -0
- tsugite/shell_tool_config.py +265 -0
- tsugite/templates/assistant.md +14 -0
- tsugite/tools/__init__.py +265 -0
- tsugite/tools/agents.py +312 -0
- tsugite/tools/edit_strategies.py +393 -0
- tsugite/tools/fs.py +329 -0
- tsugite/tools/http.py +239 -0
- tsugite/tools/interactive.py +430 -0
- tsugite/tools/shell.py +129 -0
- tsugite/tools/shell_tools.py +214 -0
- tsugite/tools/tasks.py +339 -0
- tsugite/tsugite.py +7 -0
- tsugite/ui/__init__.py +46 -0
- tsugite/ui/base.py +638 -0
- tsugite/ui/chat.py +265 -0
- tsugite/ui/chat.tcss +92 -0
- tsugite/ui/chat_history.py +286 -0
- tsugite/ui/helpers.py +102 -0
- tsugite/ui/jsonl.py +125 -0
- tsugite/ui/live_template.py +529 -0
- tsugite/ui/plain.py +419 -0
- tsugite/ui/textual_chat.py +642 -0
- tsugite/ui/textual_handler.py +225 -0
- tsugite/ui/widgets/__init__.py +6 -0
- tsugite/ui/widgets/base_scroll_log.py +27 -0
- tsugite/ui/widgets/message_list.py +121 -0
- tsugite/ui/widgets/thought_log.py +80 -0
- tsugite/ui_context.py +90 -0
- tsugite/utils.py +367 -0
- tsugite/xdg.py +104 -0
- tsugite_cli-0.3.3.dist-info/METADATA +325 -0
- tsugite_cli-0.3.3.dist-info/RECORD +101 -0
- tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
- tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
- tsugite_cli-0.3.3.dist-info/licenses/LICENSE +235 -0
tsugite/mcp_client.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""MCP client using official Python SDK.
|
|
2
|
+
|
|
3
|
+
Connects to MCP servers (stdio or HTTP) and loads their tools.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from mcp import ClientSession
|
|
9
|
+
from mcp.client.stdio import StdioServerParameters, stdio_client
|
|
10
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
11
|
+
|
|
12
|
+
from tsugite.core.tools import Tool
|
|
13
|
+
from tsugite.mcp_config import MCPServerConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MCPClient:
|
|
17
|
+
"""Client for connecting to MCP servers.
|
|
18
|
+
|
|
19
|
+
Handles both stdio and HTTP transports. Use as async context manager.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
# Create config
|
|
23
|
+
config = MCPServerConfig(
|
|
24
|
+
name="basic-memory",
|
|
25
|
+
command="uvx",
|
|
26
|
+
args=["basic-memory-mcp"]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Connect and load tools (preferred - automatic cleanup)
|
|
30
|
+
async with MCPClient(config) as client:
|
|
31
|
+
tools = await client.get_tools()
|
|
32
|
+
# Use tools in agent
|
|
33
|
+
agent = TsugiteAgent(model="...", tools=tools)
|
|
34
|
+
|
|
35
|
+
# Or use connect/disconnect manually if needed
|
|
36
|
+
client = MCPClient(config)
|
|
37
|
+
await client.connect()
|
|
38
|
+
tools = await client.get_tools()
|
|
39
|
+
await client.disconnect()
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, server_config: MCPServerConfig):
|
|
43
|
+
"""Initialize MCP client.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
server_config: Configuration for MCP server
|
|
47
|
+
"""
|
|
48
|
+
self.config = server_config
|
|
49
|
+
self.session = None
|
|
50
|
+
self.session_ctx = None
|
|
51
|
+
self.mcp_tools = [] # Raw MCP tool objects
|
|
52
|
+
self.transport = None
|
|
53
|
+
self.transport_ctx = None
|
|
54
|
+
|
|
55
|
+
async def __aenter__(self):
|
|
56
|
+
"""Enter async context manager - connect to server."""
|
|
57
|
+
await self.connect()
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
61
|
+
"""Exit async context manager - disconnect from server."""
|
|
62
|
+
await self.disconnect()
|
|
63
|
+
return False # Don't suppress exceptions
|
|
64
|
+
|
|
65
|
+
async def connect(self):
|
|
66
|
+
"""Connect to MCP server and initialize session.
|
|
67
|
+
|
|
68
|
+
Must be called before get_tools() if not using context manager.
|
|
69
|
+
"""
|
|
70
|
+
if self.config.is_stdio():
|
|
71
|
+
# Connect via stdio (command-line server)
|
|
72
|
+
server_params = StdioServerParameters(
|
|
73
|
+
command=self.config.command,
|
|
74
|
+
args=self.config.args or [],
|
|
75
|
+
env=self.config.env,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Start stdio transport using async with
|
|
79
|
+
self.transport_ctx = stdio_client(server_params)
|
|
80
|
+
# stdio_client returns a proper async context manager, but pylint can't introspect it
|
|
81
|
+
read, write = await self.transport_ctx.__aenter__() # pylint: disable=no-member
|
|
82
|
+
self.transport = (read, write)
|
|
83
|
+
|
|
84
|
+
elif self.config.is_http():
|
|
85
|
+
# Connect via HTTP using async with
|
|
86
|
+
self.transport_ctx = streamablehttp_client(self.config.url)
|
|
87
|
+
# streamablehttp_client returns a proper async context manager, but pylint can't introspect it
|
|
88
|
+
read, write, _ = await self.transport_ctx.__aenter__() # pylint: disable=no-member
|
|
89
|
+
self.transport = (read, write)
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError(f"Unknown transport type: {self.config.type}")
|
|
92
|
+
|
|
93
|
+
# Create session using async with
|
|
94
|
+
self.session_ctx = ClientSession(read, write)
|
|
95
|
+
self.session = await self.session_ctx.__aenter__()
|
|
96
|
+
|
|
97
|
+
# Initialize connection
|
|
98
|
+
await self.session.initialize()
|
|
99
|
+
|
|
100
|
+
# Load available tools
|
|
101
|
+
response = await self.session.list_tools()
|
|
102
|
+
self.mcp_tools = response.tools
|
|
103
|
+
|
|
104
|
+
async def disconnect(self):
|
|
105
|
+
"""Disconnect from MCP server and cleanup resources.
|
|
106
|
+
|
|
107
|
+
Closes the session and transport, preventing resource leaks.
|
|
108
|
+
Should be called when done using the client (unless using context manager).
|
|
109
|
+
"""
|
|
110
|
+
# Exit session context manager
|
|
111
|
+
if self.session_ctx:
|
|
112
|
+
try:
|
|
113
|
+
await self.session_ctx.__aexit__(None, None, None)
|
|
114
|
+
except Exception:
|
|
115
|
+
pass # Best effort cleanup
|
|
116
|
+
self.session_ctx = None
|
|
117
|
+
self.session = None
|
|
118
|
+
|
|
119
|
+
# Exit transport context manager
|
|
120
|
+
if self.transport_ctx:
|
|
121
|
+
try:
|
|
122
|
+
# Proper async context manager, but pylint can't introspect it
|
|
123
|
+
await self.transport_ctx.__aexit__(None, None, None) # pylint: disable=no-member
|
|
124
|
+
except Exception:
|
|
125
|
+
pass # Best effort cleanup
|
|
126
|
+
self.transport_ctx = None
|
|
127
|
+
self.transport = None
|
|
128
|
+
|
|
129
|
+
async def get_tools(self, allowed_tools: Optional[List[str]] = None) -> List[Tool]:
|
|
130
|
+
"""Get tools from MCP server as Tool objects.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
allowed_tools: If provided, only return these tools
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List[Tool]: Tools wrapped in our Tool interface
|
|
137
|
+
"""
|
|
138
|
+
tools = []
|
|
139
|
+
|
|
140
|
+
for mcp_tool in self.mcp_tools:
|
|
141
|
+
# Filter if needed
|
|
142
|
+
if allowed_tools and mcp_tool.name not in allowed_tools:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Convert to our Tool format
|
|
146
|
+
tool = Tool(
|
|
147
|
+
name=mcp_tool.name,
|
|
148
|
+
description=mcp_tool.description or "",
|
|
149
|
+
parameters=mcp_tool.inputSchema or {}, # Already JSON schema!
|
|
150
|
+
function=self._create_tool_caller(mcp_tool.name),
|
|
151
|
+
)
|
|
152
|
+
tools.append(tool)
|
|
153
|
+
|
|
154
|
+
return tools
|
|
155
|
+
|
|
156
|
+
def _create_tool_caller(self, tool_name: str):
|
|
157
|
+
"""Create async function that calls MCP tool.
|
|
158
|
+
|
|
159
|
+
Returns a function that agents can call.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
async def call_mcp_tool(**kwargs):
|
|
163
|
+
"""Call MCP tool and return result."""
|
|
164
|
+
result = await self.session.call_tool(tool_name, arguments=kwargs)
|
|
165
|
+
|
|
166
|
+
# Parse result content
|
|
167
|
+
# MCP returns list of content items
|
|
168
|
+
if result.content:
|
|
169
|
+
# Handle different content types
|
|
170
|
+
content_items = []
|
|
171
|
+
for item in result.content:
|
|
172
|
+
if item.type == "text":
|
|
173
|
+
content_items.append(item.text)
|
|
174
|
+
elif item.type == "image":
|
|
175
|
+
content_items.append(f"[Image: {item.data}]")
|
|
176
|
+
# Add other types as needed
|
|
177
|
+
|
|
178
|
+
# Return combined content
|
|
179
|
+
return "\n".join(content_items) if content_items else None
|
|
180
|
+
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
return call_mcp_tool
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def load_mcp_tools(
|
|
187
|
+
server_config: MCPServerConfig, allowed_tools: Optional[List[str]] = None
|
|
188
|
+
) -> tuple[MCPClient, List[Tool]]:
|
|
189
|
+
"""Connect to MCP server and load tools.
|
|
190
|
+
|
|
191
|
+
Returns both the client and tools. The client must remain alive for tools
|
|
192
|
+
to work, so caller should call client.disconnect() when done with the tools.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
server_config: MCP server configuration
|
|
196
|
+
allowed_tools: Optional list of tool names to load
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
tuple[MCPClient, List[Tool]]: Client and tools from MCP server
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
from tsugite.mcp_config import load_mcp_config
|
|
203
|
+
|
|
204
|
+
# Load config
|
|
205
|
+
config = load_mcp_config()
|
|
206
|
+
server_config = config["basic-memory"]
|
|
207
|
+
|
|
208
|
+
# Load tools and keep client alive
|
|
209
|
+
client, tools = await load_mcp_tools(server_config)
|
|
210
|
+
|
|
211
|
+
# Use tools...
|
|
212
|
+
|
|
213
|
+
# Cleanup when done
|
|
214
|
+
await client.disconnect()
|
|
215
|
+
"""
|
|
216
|
+
client = MCPClient(server_config)
|
|
217
|
+
await client.connect()
|
|
218
|
+
tools = await client.get_tools(allowed_tools)
|
|
219
|
+
return client, tools
|
tsugite/mcp_config.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""MCP server configuration loading and management."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
8
|
+
|
|
9
|
+
from .xdg import get_xdg_config_path, get_xdg_write_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MCPServerConfig(BaseModel):
|
|
13
|
+
"""Configuration for a single MCP server."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(
|
|
16
|
+
extra="forbid",
|
|
17
|
+
str_strip_whitespace=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
command: Optional[str] = None
|
|
22
|
+
args: Optional[List[str]] = None
|
|
23
|
+
env: Optional[Dict[str, str]] = None
|
|
24
|
+
url: Optional[str] = None
|
|
25
|
+
type: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
@model_validator(mode="after")
|
|
28
|
+
def validate_server_config(self) -> "MCPServerConfig":
|
|
29
|
+
"""Validate server configuration and auto-detect type."""
|
|
30
|
+
if self.type is None:
|
|
31
|
+
if self.command:
|
|
32
|
+
self.type = "stdio"
|
|
33
|
+
elif self.url:
|
|
34
|
+
self.type = "http"
|
|
35
|
+
else:
|
|
36
|
+
raise ValueError(f"Server '{self.name}' must have either 'command' or 'url' specified")
|
|
37
|
+
|
|
38
|
+
if self.type == "stdio" and not self.command:
|
|
39
|
+
raise ValueError(f"Stdio server '{self.name}' must have 'command' specified")
|
|
40
|
+
|
|
41
|
+
if self.type == "http" and not self.url:
|
|
42
|
+
raise ValueError(f"HTTP server '{self.name}' must have 'url' specified")
|
|
43
|
+
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def is_stdio(self) -> bool:
|
|
47
|
+
return self.type == "stdio"
|
|
48
|
+
|
|
49
|
+
def is_http(self) -> bool:
|
|
50
|
+
return self.type == "http"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_default_config_path() -> Path:
|
|
54
|
+
return get_xdg_config_path("mcp.json")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_config_path_for_write() -> Path:
|
|
58
|
+
return get_xdg_write_path("mcp.json")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def load_mcp_config(path: Optional[Path] = None) -> Dict[str, MCPServerConfig]:
|
|
62
|
+
"""Load MCP server configurations from JSON file.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
path: Path to mcp.json file. If None, uses default ~/.tsugite/mcp.json
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dictionary mapping server names to MCPServerConfig objects.
|
|
69
|
+
Returns empty dict if file doesn't exist or is invalid.
|
|
70
|
+
"""
|
|
71
|
+
if path is None:
|
|
72
|
+
path = get_default_config_path()
|
|
73
|
+
|
|
74
|
+
if not path.exists():
|
|
75
|
+
return {}
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
79
|
+
data = json.load(f)
|
|
80
|
+
|
|
81
|
+
if "mcpServers" not in data:
|
|
82
|
+
print(f"Warning: No 'mcpServers' key found in {path}")
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
servers = {}
|
|
86
|
+
for name, config in data["mcpServers"].items():
|
|
87
|
+
try:
|
|
88
|
+
servers[name] = MCPServerConfig(
|
|
89
|
+
name=name,
|
|
90
|
+
command=config.get("command"),
|
|
91
|
+
args=config.get("args"),
|
|
92
|
+
env=config.get("env"),
|
|
93
|
+
url=config.get("url"),
|
|
94
|
+
type=config.get("type"),
|
|
95
|
+
)
|
|
96
|
+
except ValueError as e:
|
|
97
|
+
print(f"Warning: Invalid config for server '{name}': {e}")
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
return servers
|
|
101
|
+
|
|
102
|
+
except json.JSONDecodeError as e:
|
|
103
|
+
print(f"Warning: Failed to parse MCP config at {path}: {e}")
|
|
104
|
+
return {}
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print(f"Warning: Failed to load MCP config from {path}: {e}")
|
|
107
|
+
return {}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def save_mcp_config(servers: Dict[str, MCPServerConfig], path: Optional[Path] = None) -> None:
|
|
111
|
+
"""Save MCP server configurations to JSON file.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
servers: Dictionary mapping server names to MCPServerConfig objects
|
|
115
|
+
path: Path to mcp.json file. If None, uses appropriate config location
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
IOError: If file cannot be written
|
|
119
|
+
"""
|
|
120
|
+
if path is None:
|
|
121
|
+
path = get_config_path_for_write()
|
|
122
|
+
|
|
123
|
+
# Ensure directory exists
|
|
124
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
|
|
126
|
+
# Convert MCPServerConfig objects to JSON-serializable dicts
|
|
127
|
+
config_data = {"mcpServers": {}}
|
|
128
|
+
|
|
129
|
+
for name, server in servers.items():
|
|
130
|
+
# Use Pydantic's model_dump to serialize, excluding None values and the name field
|
|
131
|
+
server_dict = server.model_dump(exclude_none=True, exclude={"name"})
|
|
132
|
+
|
|
133
|
+
# For stdio servers, don't include 'type' field (it's implicit)
|
|
134
|
+
if server.is_stdio() and "type" in server_dict:
|
|
135
|
+
del server_dict["type"]
|
|
136
|
+
|
|
137
|
+
config_data["mcpServers"][name] = server_dict
|
|
138
|
+
|
|
139
|
+
# Write with pretty formatting
|
|
140
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
141
|
+
json.dump(config_data, f, indent=2)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def add_server_to_config(server: MCPServerConfig, path: Optional[Path] = None, overwrite: bool = False) -> bool:
|
|
145
|
+
"""Add or update an MCP server in the configuration file.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
server: MCPServerConfig object to add
|
|
149
|
+
path: Path to mcp.json file. If None, uses appropriate config location
|
|
150
|
+
overwrite: If True, overwrite existing server. If False, raise error if exists.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
True if server was added/updated successfully
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: If server already exists and overwrite is False
|
|
157
|
+
"""
|
|
158
|
+
if path is None:
|
|
159
|
+
path = get_config_path_for_write()
|
|
160
|
+
|
|
161
|
+
# Load existing config
|
|
162
|
+
servers = load_mcp_config(path)
|
|
163
|
+
|
|
164
|
+
# Check if server already exists
|
|
165
|
+
if server.name in servers and not overwrite:
|
|
166
|
+
raise ValueError(f"Server '{server.name}' already exists. Use --force to overwrite.")
|
|
167
|
+
|
|
168
|
+
# Add/update server
|
|
169
|
+
servers[server.name] = server
|
|
170
|
+
|
|
171
|
+
# Save config
|
|
172
|
+
save_mcp_config(servers, path)
|
|
173
|
+
|
|
174
|
+
return True
|