dragon-cli 0.2.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.
- dragon_cli/__init__.py +5 -0
- dragon_cli/__main__.py +4 -0
- dragon_cli/_cuda.py +42 -0
- dragon_cli/app.py +123 -0
- dragon_cli/banner.py +96 -0
- dragon_cli/chat/__init__.py +4 -0
- dragon_cli/chat/commands.py +175 -0
- dragon_cli/chat/session.py +97 -0
- dragon_cli/chat/streaming.py +18 -0
- dragon_cli/cli.py +147 -0
- dragon_cli/config.py +28 -0
- dragon_cli/hardware.py +361 -0
- dragon_cli/mcp/__init__.py +10 -0
- dragon_cli/mcp/manager.py +148 -0
- dragon_cli/mcp/tool_call.py +17 -0
- dragon_cli/menu.py +1140 -0
- dragon_cli/model_hub.py +252 -0
- dragon_cli/models/__init__.py +11 -0
- dragon_cli/models/catalog.py +205 -0
- dragon_cli/models/loader.py +187 -0
- dragon_cli/recommendation.py +295 -0
- dragon_cli/runtime.py +252 -0
- dragon_cli/server.py +144 -0
- dragon_cli/setup_llm.py +197 -0
- dragon_cli/storage/__init__.py +3 -0
- dragon_cli/storage/conversations.py +134 -0
- dragon_cli/storage/database.py +32 -0
- dragon_cli/web.py +112 -0
- dragon_cli-0.2.0.dist-info/METADATA +147 -0
- dragon_cli-0.2.0.dist-info/RECORD +34 -0
- dragon_cli-0.2.0.dist-info/WHEEL +5 -0
- dragon_cli-0.2.0.dist-info/entry_points.txt +3 -0
- dragon_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- dragon_cli-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) stdio server connections and tool-calling."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from ..config import C_DIM, C_GREEN, C_RED, C_RESET, C_YELLOW, EXAMPLE_MCP_CONFIG, MCP_CONFIG_FILE
|
|
6
|
+
|
|
7
|
+
# Ready-to-use MCP server presets. ``args`` may contain ``{dir}``` placeholders
|
|
8
|
+
# filled in at add-time. Each is a commonly used, publicly available connector.
|
|
9
|
+
MCP_PRESETS = {
|
|
10
|
+
"filesystem": {
|
|
11
|
+
"description": "Read/write files in a folder you choose",
|
|
12
|
+
"command": "npx",
|
|
13
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
|
|
14
|
+
},
|
|
15
|
+
"fetch": {
|
|
16
|
+
"description": "Fetch and summarize web pages",
|
|
17
|
+
"command": "uvx",
|
|
18
|
+
"args": ["--with", "mcp<1.0", "mcp-server-fetch"],
|
|
19
|
+
},
|
|
20
|
+
"git": {
|
|
21
|
+
"description": "Inspect and operate on local git repos",
|
|
22
|
+
"command": "uvx",
|
|
23
|
+
"args": ["mcp-server-git", "--repository", "."],
|
|
24
|
+
},
|
|
25
|
+
"memory": {
|
|
26
|
+
"description": "Persistent knowledge graph memory",
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "@modelcontextprotocol/server-memory"],
|
|
29
|
+
},
|
|
30
|
+
"sqlite": {
|
|
31
|
+
"description": "Query a local SQLite database file",
|
|
32
|
+
"command": "uvx",
|
|
33
|
+
"args": ["mcp-server-sqlite", "--db-path", "memory.db"],
|
|
34
|
+
},
|
|
35
|
+
"time": {
|
|
36
|
+
"description": "Current time and timezone conversion",
|
|
37
|
+
"command": "uvx",
|
|
38
|
+
"args": ["mcp-server-time"],
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_mcp_config() -> dict:
|
|
44
|
+
"""Read mcp_servers.json, creating the example file if absent."""
|
|
45
|
+
if not MCP_CONFIG_FILE.exists():
|
|
46
|
+
MCP_CONFIG_FILE.write_text(json.dumps(EXAMPLE_MCP_CONFIG, indent=2), encoding="utf-8")
|
|
47
|
+
try:
|
|
48
|
+
return json.loads(MCP_CONFIG_FILE.read_text(encoding="utf-8"))
|
|
49
|
+
except (json.JSONDecodeError, OSError):
|
|
50
|
+
return {"mcpServers": {}}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def save_mcp_config(servers: dict) -> None:
|
|
54
|
+
"""Write the mcpServers dict back to mcp_servers.json."""
|
|
55
|
+
MCP_CONFIG_FILE.write_text(
|
|
56
|
+
json.dumps({"mcpServers": servers}, indent=2), encoding="utf-8"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MCPManager:
|
|
61
|
+
"""Connects to MCP (stdio) servers defined in mcp_servers.json."""
|
|
62
|
+
|
|
63
|
+
def __init__(self):
|
|
64
|
+
self.sessions = {} # server_name -> ClientSession
|
|
65
|
+
self.tools = {} # "server.tool" -> {description, schema, server, tool}
|
|
66
|
+
self._stack = None
|
|
67
|
+
|
|
68
|
+
async def start(self):
|
|
69
|
+
if not MCP_CONFIG_FILE.exists():
|
|
70
|
+
MCP_CONFIG_FILE.write_text(json.dumps(EXAMPLE_MCP_CONFIG, indent=2), encoding="utf-8")
|
|
71
|
+
print(f"{C_YELLOW}[mcp]{C_RESET} Created an example {MCP_CONFIG_FILE}. "
|
|
72
|
+
f"Edit it and restart to enable connectors.")
|
|
73
|
+
return
|
|
74
|
+
try:
|
|
75
|
+
from contextlib import AsyncExitStack
|
|
76
|
+
from mcp import ClientSession, StdioServerParameters
|
|
77
|
+
from mcp.client.stdio import stdio_client
|
|
78
|
+
except ImportError:
|
|
79
|
+
print(f"{C_RED}[mcp]{C_RESET} Package 'mcp' is not installed. Run: pip install mcp")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
config = json.loads(MCP_CONFIG_FILE.read_text(encoding="utf-8"))
|
|
83
|
+
servers = config.get("mcpServers", {})
|
|
84
|
+
if not servers:
|
|
85
|
+
print(f"{C_DIM}[mcp] No servers configured in {MCP_CONFIG_FILE}.{C_RESET}")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
self._stack = AsyncExitStack()
|
|
89
|
+
await self._stack.__aenter__()
|
|
90
|
+
for name, cfg in servers.items():
|
|
91
|
+
try:
|
|
92
|
+
params = StdioServerParameters(
|
|
93
|
+
command=cfg["command"], args=cfg.get("args", []), env=cfg.get("env"),
|
|
94
|
+
)
|
|
95
|
+
read, write = await self._stack.enter_async_context(stdio_client(params))
|
|
96
|
+
session = await self._stack.enter_async_context(ClientSession(read, write))
|
|
97
|
+
await session.initialize()
|
|
98
|
+
result = await session.list_tools()
|
|
99
|
+
self.sessions[name] = session
|
|
100
|
+
for t in result.tools:
|
|
101
|
+
# mcp SDK >=1.0 renamed inputSchema -> input_schema.
|
|
102
|
+
schema = getattr(t, "input_schema", None)
|
|
103
|
+
if schema is None:
|
|
104
|
+
schema = getattr(t, "inputSchema", {}) or {}
|
|
105
|
+
self.tools[f"{name}.{t.name}"] = {
|
|
106
|
+
"server": name, "tool": t.name,
|
|
107
|
+
"description": t.description or "",
|
|
108
|
+
"schema": schema,
|
|
109
|
+
}
|
|
110
|
+
print(f"{C_GREEN}[mcp]{C_RESET} '{name}' connected ({len(result.tools)} tools)")
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"{C_RED}[mcp]{C_RESET} Failed to connect '{name}': {e}")
|
|
113
|
+
|
|
114
|
+
async def call(self, tool_name: str, arguments: dict) -> str:
|
|
115
|
+
info = self.tools.get(tool_name)
|
|
116
|
+
if not info:
|
|
117
|
+
return f"Error: tool '{tool_name}' does not exist."
|
|
118
|
+
session = self.sessions[info["server"]]
|
|
119
|
+
try:
|
|
120
|
+
result = await session.call_tool(info["tool"], arguments or {})
|
|
121
|
+
parts = []
|
|
122
|
+
for c in result.content:
|
|
123
|
+
parts.append(getattr(c, "text", str(c)))
|
|
124
|
+
return "\n".join(parts)[:8000] # keep the context from overflowing
|
|
125
|
+
except Exception as e:
|
|
126
|
+
return f"Error running the tool: {e}"
|
|
127
|
+
|
|
128
|
+
async def stop(self):
|
|
129
|
+
if self._stack:
|
|
130
|
+
try:
|
|
131
|
+
await self._stack.aclose()
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
def tools_prompt(self) -> str:
|
|
136
|
+
if not self.tools:
|
|
137
|
+
return ""
|
|
138
|
+
lines = ["\nYou have access to the following tools (MCP):"]
|
|
139
|
+
for name, info in self.tools.items():
|
|
140
|
+
schema = json.dumps(info["schema"].get("properties", {}), ensure_ascii=False)
|
|
141
|
+
lines.append(f"- {name}: {info['description']} | parameters: {schema}")
|
|
142
|
+
lines.append(
|
|
143
|
+
"\nTo use a tool, respond ONLY with a block in this format:\n"
|
|
144
|
+
'```tool_call\n{"tool": "server.tool_name", "arguments": {...}}\n```\n'
|
|
145
|
+
"After receiving the result, respond to the user normally. "
|
|
146
|
+
"If you don't need a tool, respond directly."
|
|
147
|
+
)
|
|
148
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Parsing of ```tool_call``` blocks emitted by the model."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
TOOL_CALL_RE = re.compile(r"```tool_call\s*(\{.*?\})\s*```", re.DOTALL)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_tool_call(text: str):
|
|
10
|
+
match = TOOL_CALL_RE.search(text)
|
|
11
|
+
if not match:
|
|
12
|
+
return None
|
|
13
|
+
try:
|
|
14
|
+
data = json.loads(match.group(1))
|
|
15
|
+
return data.get("tool"), data.get("arguments", {})
|
|
16
|
+
except json.JSONDecodeError:
|
|
17
|
+
return None
|