claude-sdk-tutor 0.1.4__py3-none-any.whl → 0.1.6__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.
- app.py +208 -15
- claude/__init__.py +4 -0
- claude/claude_agent.py +8 -2
- claude/history.py +73 -0
- claude/mcp_commands.py +256 -0
- claude/mcp_config.py +140 -0
- claude/widgets.py +29 -0
- {claude_sdk_tutor-0.1.4.dist-info → claude_sdk_tutor-0.1.6.dist-info}/METADATA +60 -1
- claude_sdk_tutor-0.1.6.dist-info/RECORD +12 -0
- claude_sdk_tutor-0.1.4.dist-info/RECORD +0 -8
- {claude_sdk_tutor-0.1.4.dist-info → claude_sdk_tutor-0.1.6.dist-info}/WHEEL +0 -0
- {claude_sdk_tutor-0.1.4.dist-info → claude_sdk_tutor-0.1.6.dist-info}/entry_points.txt +0 -0
- {claude_sdk_tutor-0.1.4.dist-info → claude_sdk_tutor-0.1.6.dist-info}/licenses/LICENSE +0 -0
app.py
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
|
-
from claude_agent_sdk import
|
|
3
|
+
from claude_agent_sdk import (
|
|
4
|
+
AssistantMessage,
|
|
5
|
+
ClaudeAgentOptions,
|
|
6
|
+
ResultMessage,
|
|
7
|
+
SystemMessage,
|
|
8
|
+
query,
|
|
9
|
+
)
|
|
4
10
|
from rich.markdown import Markdown as RichMarkdown
|
|
5
11
|
from rich.panel import Panel
|
|
6
12
|
from textual.app import App, ComposeResult
|
|
@@ -12,6 +18,10 @@ from claude.claude_agent import (
|
|
|
12
18
|
create_claude_client,
|
|
13
19
|
stream_helpful_claude,
|
|
14
20
|
)
|
|
21
|
+
from claude.history import CommandHistory
|
|
22
|
+
from claude.mcp_commands import McpAsyncCommand, McpCommandHandler
|
|
23
|
+
from claude.mcp_config import McpConfigManager
|
|
24
|
+
from claude.widgets import HistoryInput
|
|
15
25
|
|
|
16
26
|
|
|
17
27
|
class MyApp(App):
|
|
@@ -19,8 +29,19 @@ class MyApp(App):
|
|
|
19
29
|
super().__init__()
|
|
20
30
|
self.tutor_mode = True
|
|
21
31
|
self.web_search_enabled = False
|
|
22
|
-
self.
|
|
23
|
-
|
|
32
|
+
self.mcp_config = McpConfigManager()
|
|
33
|
+
self.mcp_handler = McpCommandHandler(self.mcp_config)
|
|
34
|
+
self.mcp_add_state: dict | None = None # For interactive /mcp add wizard
|
|
35
|
+
self.client = self._create_client()
|
|
36
|
+
self.history = CommandHistory()
|
|
37
|
+
self._query_running: bool = False # Track if a query is active
|
|
38
|
+
|
|
39
|
+
def _create_client(self):
|
|
40
|
+
"""Create a new Claude client with current settings."""
|
|
41
|
+
return create_claude_client(
|
|
42
|
+
tutor_mode=self.tutor_mode,
|
|
43
|
+
web_search=self.web_search_enabled,
|
|
44
|
+
mcp_servers=self.mcp_config.get_enabled_servers_for_sdk(),
|
|
24
45
|
)
|
|
25
46
|
|
|
26
47
|
CSS = """
|
|
@@ -59,7 +80,7 @@ class MyApp(App):
|
|
|
59
80
|
yield Static("Welcome to claude SDK tutor!", id="header")
|
|
60
81
|
yield RichLog(markup=True, highlight=True)
|
|
61
82
|
yield LoadingIndicator(id="spinner")
|
|
62
|
-
yield
|
|
83
|
+
yield HistoryInput(history=self.history)
|
|
63
84
|
yield Footer()
|
|
64
85
|
|
|
65
86
|
async def on_mount(self) -> None:
|
|
@@ -85,8 +106,20 @@ class MyApp(App):
|
|
|
85
106
|
log.write(Panel(RichMarkdown(message), title="Slash", border_style="green"))
|
|
86
107
|
|
|
87
108
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
88
|
-
self.query_one(Input).value = ""
|
|
89
109
|
command = event.value.strip()
|
|
110
|
+
self.query_one(HistoryInput).value = ""
|
|
111
|
+
if command:
|
|
112
|
+
self.history.add(command)
|
|
113
|
+
|
|
114
|
+
# Handle interactive MCP add wizard
|
|
115
|
+
if self.mcp_add_state is not None:
|
|
116
|
+
if command.lower() == "/cancel":
|
|
117
|
+
self.mcp_add_state = None
|
|
118
|
+
self.write_slash_message("Cancelled MCP server setup.")
|
|
119
|
+
return
|
|
120
|
+
self._handle_mcp_add_step(command)
|
|
121
|
+
return
|
|
122
|
+
|
|
90
123
|
if command == "/clear":
|
|
91
124
|
self.run_worker(self.clear_conversation())
|
|
92
125
|
return
|
|
@@ -99,24 +132,24 @@ class MyApp(App):
|
|
|
99
132
|
if command == "/help":
|
|
100
133
|
self.show_help()
|
|
101
134
|
return
|
|
135
|
+
if command.lower().startswith("/mcp"):
|
|
136
|
+
self._handle_mcp_command(command)
|
|
137
|
+
return
|
|
102
138
|
self.write_user_message(event.value)
|
|
103
139
|
self.query_one("#spinner", LoadingIndicator).display = True
|
|
140
|
+
self._query_running = True
|
|
104
141
|
self.run_worker(self.get_response(event.value))
|
|
105
142
|
|
|
106
143
|
async def clear_conversation(self) -> None:
|
|
107
144
|
self.query_one(RichLog).clear()
|
|
108
|
-
self.client =
|
|
109
|
-
tutor_mode=self.tutor_mode, web_search=self.web_search_enabled
|
|
110
|
-
)
|
|
145
|
+
self.client = self._create_client()
|
|
111
146
|
await connect_client(self.client)
|
|
112
147
|
self.write_slash_message("Context cleared")
|
|
113
148
|
|
|
114
149
|
async def toggle_tutor_mode(self) -> None:
|
|
115
150
|
self.tutor_mode = not self.tutor_mode
|
|
116
151
|
self.query_one(RichLog).clear()
|
|
117
|
-
self.client =
|
|
118
|
-
tutor_mode=self.tutor_mode, web_search=self.web_search_enabled
|
|
119
|
-
)
|
|
152
|
+
self.client = self._create_client()
|
|
120
153
|
await connect_client(self.client)
|
|
121
154
|
status = "on" if self.tutor_mode else "off"
|
|
122
155
|
self.write_slash_message(f"Tutor mode {status}")
|
|
@@ -124,9 +157,7 @@ class MyApp(App):
|
|
|
124
157
|
async def toggle_web_search(self) -> None:
|
|
125
158
|
self.web_search_enabled = not self.web_search_enabled
|
|
126
159
|
self.query_one(RichLog).clear()
|
|
127
|
-
self.client =
|
|
128
|
-
tutor_mode=self.tutor_mode, web_search=self.web_search_enabled
|
|
129
|
-
)
|
|
160
|
+
self.client = self._create_client()
|
|
130
161
|
await connect_client(self.client)
|
|
131
162
|
status = "on" if self.web_search_enabled else "off"
|
|
132
163
|
self.write_slash_message(f"Web search {status}")
|
|
@@ -137,9 +168,154 @@ class MyApp(App):
|
|
|
137
168
|
- `/help` - Show this help message
|
|
138
169
|
- `/clear` - Clear conversation history and start fresh
|
|
139
170
|
- `/tutor` - Toggle tutor mode on/off (guides learning vs gives code)
|
|
140
|
-
- `/togglewebsearch` - Toggle web search on/off (allows online lookups)
|
|
171
|
+
- `/togglewebsearch` - Toggle web search on/off (allows online lookups)
|
|
172
|
+
- `/mcp` - Manage MCP servers (use `/mcp help` for details)"""
|
|
141
173
|
self.write_slash_message(help_text)
|
|
142
174
|
|
|
175
|
+
def _handle_mcp_command(self, command: str) -> None:
|
|
176
|
+
"""Handle /mcp commands."""
|
|
177
|
+
result = self.mcp_handler.handle_command(command)
|
|
178
|
+
if result is None:
|
|
179
|
+
# Start interactive add wizard
|
|
180
|
+
self.mcp_add_state = {"step": 0, "name": "", "type": "", "data": {}}
|
|
181
|
+
self.write_slash_message(
|
|
182
|
+
"**Add MCP Server**\n\nEnter server name (or `/cancel` to abort):"
|
|
183
|
+
)
|
|
184
|
+
elif isinstance(result, McpAsyncCommand):
|
|
185
|
+
# Async command needs connection testing
|
|
186
|
+
self.query_one("#spinner", LoadingIndicator).display = True
|
|
187
|
+
self.run_worker(self._test_mcp_connections(result))
|
|
188
|
+
else:
|
|
189
|
+
self.write_slash_message(result)
|
|
190
|
+
|
|
191
|
+
async def _test_mcp_connections(self, cmd: McpAsyncCommand) -> None:
|
|
192
|
+
"""Test MCP server connections and display results."""
|
|
193
|
+
try:
|
|
194
|
+
mcp_servers = self.mcp_config.get_enabled_servers_for_sdk()
|
|
195
|
+
if not mcp_servers:
|
|
196
|
+
if cmd.command == "test":
|
|
197
|
+
self.write_slash_message(
|
|
198
|
+
"**MCP Test**\n\nNo enabled servers to test."
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
self.write_slash_message(
|
|
202
|
+
self.mcp_handler.handle_list(cmd.args, connection_status=None)
|
|
203
|
+
)
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
# Build allowed tools for the test
|
|
207
|
+
allowed_tools = [f"mcp__{name}__*" for name in mcp_servers]
|
|
208
|
+
|
|
209
|
+
options = ClaudeAgentOptions(
|
|
210
|
+
mcp_servers=mcp_servers,
|
|
211
|
+
allowed_tools=allowed_tools,
|
|
212
|
+
max_turns=1,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
connection_status: dict[str, str] = {}
|
|
216
|
+
|
|
217
|
+
# Run a minimal query just to get the init message with MCP status
|
|
218
|
+
async for message in query(prompt="test", options=options):
|
|
219
|
+
if isinstance(message, SystemMessage) and message.subtype == "init":
|
|
220
|
+
mcp_info = message.data.get("mcp_servers", [])
|
|
221
|
+
for server in mcp_info:
|
|
222
|
+
name = server.get("name", "unknown")
|
|
223
|
+
status = server.get("status", "unknown")
|
|
224
|
+
connection_status[name] = status
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
# Display results based on command
|
|
228
|
+
if cmd.command == "test":
|
|
229
|
+
self.write_slash_message(
|
|
230
|
+
self.mcp_handler.handle_test(cmd.args, connection_status)
|
|
231
|
+
)
|
|
232
|
+
else: # list
|
|
233
|
+
self.write_slash_message(
|
|
234
|
+
self.mcp_handler.handle_list(cmd.args, connection_status)
|
|
235
|
+
)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
self.write_slash_message(f"**Error** testing MCP connections: {e}")
|
|
238
|
+
finally:
|
|
239
|
+
self.query_one("#spinner", LoadingIndicator).display = False
|
|
240
|
+
|
|
241
|
+
def _handle_mcp_add_step(self, user_input: str) -> None:
|
|
242
|
+
"""Handle a step in the interactive MCP add wizard."""
|
|
243
|
+
state = self.mcp_add_state
|
|
244
|
+
if state is None:
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
step = state["step"]
|
|
248
|
+
|
|
249
|
+
if step == 0:
|
|
250
|
+
# Got server name
|
|
251
|
+
name = user_input.strip()
|
|
252
|
+
if not name:
|
|
253
|
+
self.write_slash_message("**Error**: Name cannot be empty. Try again:")
|
|
254
|
+
return
|
|
255
|
+
if self.mcp_config.get_server(name):
|
|
256
|
+
self.write_slash_message(
|
|
257
|
+
f"**Error**: Server `{name}` already exists. Enter a different name:"
|
|
258
|
+
)
|
|
259
|
+
return
|
|
260
|
+
state["name"] = name
|
|
261
|
+
state["step"] = 1
|
|
262
|
+
self.write_slash_message(
|
|
263
|
+
"Select server type:\n- `stdio` - Local process\n- `sse` - Server-Sent Events\n- `http` - HTTP endpoint"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
elif step == 1:
|
|
267
|
+
# Got server type
|
|
268
|
+
server_type = user_input.strip().lower()
|
|
269
|
+
if server_type not in ("stdio", "sse", "http"):
|
|
270
|
+
self.write_slash_message(
|
|
271
|
+
f"**Error**: Invalid type `{server_type}`. Enter `stdio`, `sse`, or `http`:"
|
|
272
|
+
)
|
|
273
|
+
return
|
|
274
|
+
state["type"] = server_type
|
|
275
|
+
state["step"] = 2
|
|
276
|
+
if server_type == "stdio":
|
|
277
|
+
self.write_slash_message("Enter command to run (e.g., `npx`):")
|
|
278
|
+
else:
|
|
279
|
+
self.write_slash_message("Enter server URL:")
|
|
280
|
+
|
|
281
|
+
elif step == 2:
|
|
282
|
+
# Got command or URL
|
|
283
|
+
value = user_input.strip()
|
|
284
|
+
if not value:
|
|
285
|
+
self.write_slash_message("**Error**: Value cannot be empty. Try again:")
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
if state["type"] == "stdio":
|
|
289
|
+
state["data"]["command"] = value
|
|
290
|
+
state["step"] = 3
|
|
291
|
+
self.write_slash_message(
|
|
292
|
+
"Enter arguments (space-separated, or leave empty):"
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
# SSE or HTTP - URL provided, we're done
|
|
296
|
+
config = {"type": state["type"], "url": value}
|
|
297
|
+
self.mcp_config.add_server(state["name"], config)
|
|
298
|
+
self.write_slash_message(
|
|
299
|
+
f"**Added** server `{state['name']}` ({state['type']})\n\n"
|
|
300
|
+
"Use `/clear` to reconnect with new MCP servers."
|
|
301
|
+
)
|
|
302
|
+
self.mcp_add_state = None
|
|
303
|
+
|
|
304
|
+
elif step == 3:
|
|
305
|
+
# Got args for stdio command
|
|
306
|
+
args = user_input.strip().split() if user_input.strip() else []
|
|
307
|
+
config = {
|
|
308
|
+
"type": "stdio",
|
|
309
|
+
"command": state["data"]["command"],
|
|
310
|
+
"args": args,
|
|
311
|
+
}
|
|
312
|
+
self.mcp_config.add_server(state["name"], config)
|
|
313
|
+
self.write_slash_message(
|
|
314
|
+
f"**Added** server `{state['name']}` (stdio)\n\n"
|
|
315
|
+
"Use `/clear` to reconnect with new MCP servers."
|
|
316
|
+
)
|
|
317
|
+
self.mcp_add_state = None
|
|
318
|
+
|
|
143
319
|
async def get_response(self, text: str) -> None:
|
|
144
320
|
try:
|
|
145
321
|
async for message in stream_helpful_claude(self.client, text):
|
|
@@ -155,6 +331,23 @@ class MyApp(App):
|
|
|
155
331
|
pass # Might want to add logging later
|
|
156
332
|
finally:
|
|
157
333
|
self.query_one("#spinner", LoadingIndicator).display = False
|
|
334
|
+
self._query_running = False
|
|
335
|
+
|
|
336
|
+
def action_cancel_query(self) -> None:
|
|
337
|
+
"""Interrupt the current running query using SDK interrupt."""
|
|
338
|
+
if self._query_running:
|
|
339
|
+
self.run_worker(self._interrupt_query())
|
|
340
|
+
|
|
341
|
+
async def _interrupt_query(self) -> None:
|
|
342
|
+
"""Send interrupt signal to the Claude SDK client."""
|
|
343
|
+
try:
|
|
344
|
+
await self.client.interrupt()
|
|
345
|
+
self.write_slash_message("Query interrupted.")
|
|
346
|
+
except Exception:
|
|
347
|
+
pass # Ignore errors if not connected or no active query
|
|
348
|
+
finally:
|
|
349
|
+
self._query_running = False
|
|
350
|
+
self.query_one("#spinner", LoadingIndicator).display = False
|
|
158
351
|
|
|
159
352
|
|
|
160
353
|
def main():
|
claude/__init__.py
CHANGED
claude/claude_agent.py
CHANGED
|
@@ -13,12 +13,18 @@ Never write complete solutions for them. Instead, help them develop the skills t
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def create_claude_client(
|
|
16
|
-
tutor_mode: bool = True,
|
|
16
|
+
tutor_mode: bool = True,
|
|
17
|
+
web_search: bool = False,
|
|
18
|
+
mcp_servers: dict | None = None,
|
|
17
19
|
) -> ClaudeSDKClient:
|
|
18
20
|
tools = ["Read", "Glob", "Grep"]
|
|
19
21
|
if web_search:
|
|
20
22
|
tools.extend(["WebSearch", "WebFetch"])
|
|
21
|
-
|
|
23
|
+
if mcp_servers:
|
|
24
|
+
# Allow all tools from each configured MCP server
|
|
25
|
+
for server_name in mcp_servers:
|
|
26
|
+
tools.append(f"mcp__{server_name}__*")
|
|
27
|
+
options = ClaudeAgentOptions(allowed_tools=tools, mcp_servers=mcp_servers or {})
|
|
22
28
|
if tutor_mode:
|
|
23
29
|
options.system_prompt = TUTOR_SYSTEM_PROMPT
|
|
24
30
|
return ClaudeSDKClient(options=options)
|
claude/history.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from platformdirs import user_data_dir
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CommandHistory:
|
|
7
|
+
"""Manages command history with persistence to disk."""
|
|
8
|
+
|
|
9
|
+
MAX_ENTRIES = 1000
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.history: list[str] = []
|
|
13
|
+
self.index: int = -1
|
|
14
|
+
self.temp_input: str = ""
|
|
15
|
+
self._history_file = Path(user_data_dir("claude-sdk-tutor")) / "command_history.txt"
|
|
16
|
+
self._load()
|
|
17
|
+
|
|
18
|
+
def _load(self) -> None:
|
|
19
|
+
"""Load history from disk."""
|
|
20
|
+
if self._history_file.exists():
|
|
21
|
+
try:
|
|
22
|
+
lines = self._history_file.read_text().splitlines()
|
|
23
|
+
self.history = lines[-self.MAX_ENTRIES :]
|
|
24
|
+
except OSError:
|
|
25
|
+
self.history = []
|
|
26
|
+
|
|
27
|
+
def _save(self) -> None:
|
|
28
|
+
"""Save history to disk."""
|
|
29
|
+
try:
|
|
30
|
+
self._history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
self._history_file.write_text("\n".join(self.history[-self.MAX_ENTRIES :]))
|
|
32
|
+
except OSError:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def add(self, command: str) -> None:
|
|
36
|
+
"""Add a command to history, skipping consecutive duplicates."""
|
|
37
|
+
command = command.strip()
|
|
38
|
+
if not command:
|
|
39
|
+
return
|
|
40
|
+
if not self.history or self.history[-1] != command:
|
|
41
|
+
self.history.append(command)
|
|
42
|
+
self._save()
|
|
43
|
+
self.reset_navigation()
|
|
44
|
+
|
|
45
|
+
def reset_navigation(self) -> None:
|
|
46
|
+
"""Reset navigation state."""
|
|
47
|
+
self.index = -1
|
|
48
|
+
self.temp_input = ""
|
|
49
|
+
|
|
50
|
+
def navigate_up(self, current_input: str) -> str:
|
|
51
|
+
"""Navigate to previous command in history."""
|
|
52
|
+
if not self.history:
|
|
53
|
+
return current_input
|
|
54
|
+
|
|
55
|
+
if self.index == -1:
|
|
56
|
+
self.temp_input = current_input
|
|
57
|
+
self.index = len(self.history) - 1
|
|
58
|
+
elif self.index > 0:
|
|
59
|
+
self.index -= 1
|
|
60
|
+
|
|
61
|
+
return self.history[self.index]
|
|
62
|
+
|
|
63
|
+
def navigate_down(self, current_input: str) -> str:
|
|
64
|
+
"""Navigate to next command in history, or restore original input."""
|
|
65
|
+
if self.index == -1:
|
|
66
|
+
return current_input
|
|
67
|
+
|
|
68
|
+
if self.index < len(self.history) - 1:
|
|
69
|
+
self.index += 1
|
|
70
|
+
return self.history[self.index]
|
|
71
|
+
else:
|
|
72
|
+
self.index = -1
|
|
73
|
+
return self.temp_input
|
claude/mcp_commands.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""MCP command handling for slash commands."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .mcp_config import McpConfigManager, McpServerEntry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class McpAsyncCommand:
|
|
10
|
+
"""Indicates an async command that needs special handling."""
|
|
11
|
+
|
|
12
|
+
command: str
|
|
13
|
+
args: list[str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class McpCommandHandler:
|
|
17
|
+
"""Handles /mcp slash commands."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config_manager: McpConfigManager):
|
|
20
|
+
self.config = config_manager
|
|
21
|
+
|
|
22
|
+
def parse_command(self, command: str) -> tuple[str, list[str]]:
|
|
23
|
+
"""Parse /mcp command into subcommand and args.
|
|
24
|
+
|
|
25
|
+
Example: "/mcp add myserver" -> ("add", ["myserver"])
|
|
26
|
+
"""
|
|
27
|
+
parts = command.strip().split()
|
|
28
|
+
# Remove /mcp prefix if present
|
|
29
|
+
if parts and parts[0].lower() == "/mcp":
|
|
30
|
+
parts = parts[1:]
|
|
31
|
+
|
|
32
|
+
if not parts:
|
|
33
|
+
return "help", []
|
|
34
|
+
|
|
35
|
+
subcommand = parts[0].lower()
|
|
36
|
+
args = parts[1:]
|
|
37
|
+
return subcommand, args
|
|
38
|
+
|
|
39
|
+
def handle_command(self, command: str) -> str | None | McpAsyncCommand:
|
|
40
|
+
"""Handle a /mcp command.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
str: Markdown response to display
|
|
44
|
+
None: Trigger interactive mode
|
|
45
|
+
McpAsyncCommand: Command needs async handling
|
|
46
|
+
"""
|
|
47
|
+
subcommand, args = self.parse_command(command)
|
|
48
|
+
|
|
49
|
+
# Commands that need async handling (connection testing)
|
|
50
|
+
if subcommand in ("test", "list"):
|
|
51
|
+
return McpAsyncCommand(command=subcommand, args=args)
|
|
52
|
+
|
|
53
|
+
handlers = {
|
|
54
|
+
"add": self.handle_add,
|
|
55
|
+
"remove": self.handle_remove,
|
|
56
|
+
"enable": self.handle_enable,
|
|
57
|
+
"disable": self.handle_disable,
|
|
58
|
+
"status": self.handle_status,
|
|
59
|
+
"help": self.handle_help,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
handler = handlers.get(subcommand, self.handle_help)
|
|
63
|
+
return handler(args)
|
|
64
|
+
|
|
65
|
+
def handle_list(
|
|
66
|
+
self, _args: list[str], connection_status: dict[str, str] | None = None
|
|
67
|
+
) -> str:
|
|
68
|
+
"""List all configured MCP servers with optional connection status."""
|
|
69
|
+
servers = self.config.list_servers()
|
|
70
|
+
|
|
71
|
+
if not servers:
|
|
72
|
+
return "**MCP Servers**\n\nNo servers configured. Use `/mcp add` to add one."
|
|
73
|
+
|
|
74
|
+
lines = ["**MCP Servers**\n"]
|
|
75
|
+
lines.append("| Name | Type | Enabled | Connection | Target |")
|
|
76
|
+
lines.append("|------|------|---------|------------|--------|")
|
|
77
|
+
|
|
78
|
+
for server in servers:
|
|
79
|
+
server_type = server.config.get("type", "stdio")
|
|
80
|
+
enabled = "yes" if server.enabled else "no"
|
|
81
|
+
target = self._get_target_display(server)
|
|
82
|
+
|
|
83
|
+
if connection_status and server.name in connection_status:
|
|
84
|
+
conn = connection_status[server.name]
|
|
85
|
+
elif not server.enabled:
|
|
86
|
+
conn = "—"
|
|
87
|
+
else:
|
|
88
|
+
conn = "unknown"
|
|
89
|
+
|
|
90
|
+
lines.append(
|
|
91
|
+
f"| {server.name} | {server_type} | {enabled} | {conn} | {target} |"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return "\n".join(lines)
|
|
95
|
+
|
|
96
|
+
def handle_test(
|
|
97
|
+
self, args: list[str], connection_status: dict[str, str]
|
|
98
|
+
) -> str:
|
|
99
|
+
"""Format test results for MCP server connections."""
|
|
100
|
+
if not connection_status:
|
|
101
|
+
return "**MCP Test**\n\nNo enabled servers to test."
|
|
102
|
+
|
|
103
|
+
# Filter to specific server if provided
|
|
104
|
+
if args:
|
|
105
|
+
name = args[0]
|
|
106
|
+
if name not in connection_status:
|
|
107
|
+
server = self.config.get_server(name)
|
|
108
|
+
if not server:
|
|
109
|
+
return f"**Error**: Server `{name}` not found."
|
|
110
|
+
if not server.enabled:
|
|
111
|
+
return f"**Error**: Server `{name}` is disabled."
|
|
112
|
+
return f"**Error**: Server `{name}` was not tested."
|
|
113
|
+
connection_status = {name: connection_status[name]}
|
|
114
|
+
|
|
115
|
+
lines = ["**MCP Connection Test**\n"]
|
|
116
|
+
|
|
117
|
+
connected = 0
|
|
118
|
+
failed = 0
|
|
119
|
+
for name, status in connection_status.items():
|
|
120
|
+
if status == "connected":
|
|
121
|
+
lines.append(f"- `{name}`: **connected**")
|
|
122
|
+
connected += 1
|
|
123
|
+
else:
|
|
124
|
+
lines.append(f"- `{name}`: **{status}**")
|
|
125
|
+
failed += 1
|
|
126
|
+
|
|
127
|
+
lines.append(f"\n**Summary**: {connected} connected, {failed} failed")
|
|
128
|
+
return "\n".join(lines)
|
|
129
|
+
|
|
130
|
+
def _get_target_display(self, server: McpServerEntry) -> str:
|
|
131
|
+
"""Get display string for server target."""
|
|
132
|
+
config = server.config
|
|
133
|
+
server_type = config.get("type", "")
|
|
134
|
+
|
|
135
|
+
if server_type == "stdio":
|
|
136
|
+
cmd = config.get("command", "")
|
|
137
|
+
args = config.get("args", [])
|
|
138
|
+
if args:
|
|
139
|
+
return f"{cmd} {' '.join(args[:2])}{'...' if len(args) > 2 else ''}"
|
|
140
|
+
return cmd
|
|
141
|
+
elif server_type in ("sse", "http"):
|
|
142
|
+
return config.get("url", "")
|
|
143
|
+
return "—"
|
|
144
|
+
|
|
145
|
+
def handle_add(self, args: list[str]) -> str | None:
|
|
146
|
+
"""Handle /mcp add command.
|
|
147
|
+
|
|
148
|
+
If args provided: /mcp add <name> <type> <command/url> [args...]
|
|
149
|
+
If no args: return None to trigger interactive mode
|
|
150
|
+
"""
|
|
151
|
+
if not args:
|
|
152
|
+
return None # Trigger interactive mode
|
|
153
|
+
|
|
154
|
+
if len(args) < 3:
|
|
155
|
+
return "**Error**: Usage: `/mcp add <name> <type> <command|url> [args...]`"
|
|
156
|
+
|
|
157
|
+
name = args[0]
|
|
158
|
+
server_type = args[1].lower()
|
|
159
|
+
|
|
160
|
+
if server_type not in ("stdio", "sse", "http"):
|
|
161
|
+
return f"**Error**: Invalid type `{server_type}`. Must be stdio, sse, or http."
|
|
162
|
+
|
|
163
|
+
if self.config.get_server(name):
|
|
164
|
+
return f"**Error**: Server `{name}` already exists."
|
|
165
|
+
|
|
166
|
+
if server_type == "stdio":
|
|
167
|
+
command = args[2]
|
|
168
|
+
cmd_args = args[3:] if len(args) > 3 else []
|
|
169
|
+
config = {"type": "stdio", "command": command, "args": cmd_args}
|
|
170
|
+
else:
|
|
171
|
+
url = args[2]
|
|
172
|
+
config = {"type": server_type, "url": url}
|
|
173
|
+
|
|
174
|
+
self.config.add_server(name, config)
|
|
175
|
+
return f"**Added** server `{name}` ({server_type})"
|
|
176
|
+
|
|
177
|
+
def handle_remove(self, args: list[str]) -> str:
|
|
178
|
+
"""Handle /mcp remove <name> command."""
|
|
179
|
+
if not args:
|
|
180
|
+
return "**Error**: Usage: `/mcp remove <name>`"
|
|
181
|
+
|
|
182
|
+
name = args[0]
|
|
183
|
+
if self.config.remove_server(name):
|
|
184
|
+
return f"**Removed** server `{name}`"
|
|
185
|
+
return f"**Error**: Server `{name}` not found."
|
|
186
|
+
|
|
187
|
+
def handle_enable(self, args: list[str]) -> str:
|
|
188
|
+
"""Handle /mcp enable <name> command."""
|
|
189
|
+
if not args:
|
|
190
|
+
return "**Error**: Usage: `/mcp enable <name>`"
|
|
191
|
+
|
|
192
|
+
name = args[0]
|
|
193
|
+
if self.config.enable_server(name):
|
|
194
|
+
return f"**Enabled** server `{name}`"
|
|
195
|
+
return f"**Error**: Server `{name}` not found."
|
|
196
|
+
|
|
197
|
+
def handle_disable(self, args: list[str]) -> str:
|
|
198
|
+
"""Handle /mcp disable <name> command."""
|
|
199
|
+
if not args:
|
|
200
|
+
return "**Error**: Usage: `/mcp disable <name>`"
|
|
201
|
+
|
|
202
|
+
name = args[0]
|
|
203
|
+
if self.config.disable_server(name):
|
|
204
|
+
return f"**Disabled** server `{name}`"
|
|
205
|
+
return f"**Error**: Server `{name}` not found."
|
|
206
|
+
|
|
207
|
+
def handle_status(self, args: list[str]) -> str:
|
|
208
|
+
"""Handle /mcp status [name] command."""
|
|
209
|
+
if not args:
|
|
210
|
+
# Show summary status
|
|
211
|
+
servers = self.config.list_servers()
|
|
212
|
+
enabled = sum(1 for s in servers if s.enabled)
|
|
213
|
+
return f"**MCP Status**: {enabled}/{len(servers)} servers enabled"
|
|
214
|
+
|
|
215
|
+
name = args[0]
|
|
216
|
+
server = self.config.get_server(name)
|
|
217
|
+
if not server:
|
|
218
|
+
return f"**Error**: Server `{name}` not found."
|
|
219
|
+
|
|
220
|
+
lines = [f"**Server: {name}**\n"]
|
|
221
|
+
lines.append(f"- **Type**: {server.config.get('type', 'unknown')}")
|
|
222
|
+
lines.append(f"- **Status**: {'enabled' if server.enabled else 'disabled'}")
|
|
223
|
+
|
|
224
|
+
server_type = server.config.get("type", "")
|
|
225
|
+
if server_type == "stdio":
|
|
226
|
+
cmd = server.config.get("command", "")
|
|
227
|
+
args_list = server.config.get("args", [])
|
|
228
|
+
full_cmd = f"{cmd} {' '.join(args_list)}".strip()
|
|
229
|
+
lines.append(f"- **Command**: `{full_cmd}`")
|
|
230
|
+
env = server.config.get("env", {})
|
|
231
|
+
if env:
|
|
232
|
+
lines.append(f"- **Env vars**: {', '.join(env.keys())}")
|
|
233
|
+
else:
|
|
234
|
+
url = server.config.get("url", "")
|
|
235
|
+
lines.append(f"- **URL**: {url}")
|
|
236
|
+
|
|
237
|
+
return "\n".join(lines)
|
|
238
|
+
|
|
239
|
+
def handle_help(self, _args: list[str]) -> str:
|
|
240
|
+
"""Show help for /mcp commands."""
|
|
241
|
+
return """**MCP Server Commands**
|
|
242
|
+
|
|
243
|
+
- `/mcp list` - List all configured servers with connection status
|
|
244
|
+
- `/mcp test [name]` - Test MCP server connections
|
|
245
|
+
- `/mcp add` - Add a new server (interactive)
|
|
246
|
+
- `/mcp add <name> <type> <cmd|url> [args]` - Add server directly
|
|
247
|
+
- `/mcp remove <name>` - Remove a server
|
|
248
|
+
- `/mcp enable <name>` - Enable a server
|
|
249
|
+
- `/mcp disable <name>` - Disable a server
|
|
250
|
+
- `/mcp status [name]` - Show server config details
|
|
251
|
+
- `/mcp help` - Show this help
|
|
252
|
+
|
|
253
|
+
**Server Types**
|
|
254
|
+
- `stdio` - Local process (command + args)
|
|
255
|
+
- `sse` - Server-Sent Events endpoint (URL)
|
|
256
|
+
- `http` - HTTP endpoint (URL)"""
|
claude/mcp_config.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""MCP Server configuration management."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class McpServerEntry:
|
|
13
|
+
"""An MCP server configuration entry."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
enabled: bool
|
|
17
|
+
config: dict[str, Any]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class McpConfig:
|
|
22
|
+
"""Full MCP configuration."""
|
|
23
|
+
|
|
24
|
+
version: int = 1
|
|
25
|
+
servers: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class McpConfigManager:
|
|
29
|
+
"""Manages MCP server configurations with persistent storage."""
|
|
30
|
+
|
|
31
|
+
CONFIG_DIR = Path.home() / ".local" / "share" / "claude-sdk-tutor"
|
|
32
|
+
CONFIG_FILE = CONFIG_DIR / "mcp_servers.json"
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self._config: McpConfig = McpConfig()
|
|
36
|
+
self._load()
|
|
37
|
+
|
|
38
|
+
def _load(self) -> None:
|
|
39
|
+
"""Load configuration from disk."""
|
|
40
|
+
if self.CONFIG_FILE.exists():
|
|
41
|
+
try:
|
|
42
|
+
with open(self.CONFIG_FILE) as f:
|
|
43
|
+
data = json.load(f)
|
|
44
|
+
self._config = McpConfig(
|
|
45
|
+
version=data.get("version", 1),
|
|
46
|
+
servers=data.get("servers", {}),
|
|
47
|
+
)
|
|
48
|
+
except (json.JSONDecodeError, OSError):
|
|
49
|
+
self._config = McpConfig()
|
|
50
|
+
else:
|
|
51
|
+
self._config = McpConfig()
|
|
52
|
+
|
|
53
|
+
def _save(self) -> None:
|
|
54
|
+
"""Save configuration to disk."""
|
|
55
|
+
self.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
with open(self.CONFIG_FILE, "w") as f:
|
|
57
|
+
json.dump(
|
|
58
|
+
{"version": self._config.version, "servers": self._config.servers},
|
|
59
|
+
f,
|
|
60
|
+
indent=2,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def list_servers(self) -> list[McpServerEntry]:
|
|
64
|
+
"""List all configured servers."""
|
|
65
|
+
entries = []
|
|
66
|
+
for name, data in self._config.servers.items():
|
|
67
|
+
entries.append(
|
|
68
|
+
McpServerEntry(
|
|
69
|
+
name=name,
|
|
70
|
+
enabled=data.get("enabled", True),
|
|
71
|
+
config=data.get("config", {}),
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
return entries
|
|
75
|
+
|
|
76
|
+
def get_server(self, name: str) -> McpServerEntry | None:
|
|
77
|
+
"""Get a specific server by name."""
|
|
78
|
+
if name not in self._config.servers:
|
|
79
|
+
return None
|
|
80
|
+
data = self._config.servers[name]
|
|
81
|
+
return McpServerEntry(
|
|
82
|
+
name=name,
|
|
83
|
+
enabled=data.get("enabled", True),
|
|
84
|
+
config=data.get("config", {}),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def add_server(self, name: str, config: dict[str, Any]) -> None:
|
|
88
|
+
"""Add a new server configuration."""
|
|
89
|
+
self._config.servers[name] = {"enabled": True, "config": config}
|
|
90
|
+
self._save()
|
|
91
|
+
|
|
92
|
+
def remove_server(self, name: str) -> bool:
|
|
93
|
+
"""Remove a server configuration. Returns True if removed."""
|
|
94
|
+
if name in self._config.servers:
|
|
95
|
+
del self._config.servers[name]
|
|
96
|
+
self._save()
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def enable_server(self, name: str) -> bool:
|
|
101
|
+
"""Enable a server. Returns True if server exists."""
|
|
102
|
+
if name in self._config.servers:
|
|
103
|
+
self._config.servers[name]["enabled"] = True
|
|
104
|
+
self._save()
|
|
105
|
+
return True
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
def disable_server(self, name: str) -> bool:
|
|
109
|
+
"""Disable a server. Returns True if server exists."""
|
|
110
|
+
if name in self._config.servers:
|
|
111
|
+
self._config.servers[name]["enabled"] = False
|
|
112
|
+
self._save()
|
|
113
|
+
return True
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def _expand_env_vars(self, value: Any) -> Any:
|
|
117
|
+
"""Recursively expand environment variables in config values."""
|
|
118
|
+
if isinstance(value, str):
|
|
119
|
+
# Match ${VAR_NAME} pattern
|
|
120
|
+
pattern = r"\$\{([^}]+)\}"
|
|
121
|
+
matches = re.findall(pattern, value)
|
|
122
|
+
result = value
|
|
123
|
+
for var_name in matches:
|
|
124
|
+
env_value = os.environ.get(var_name, "")
|
|
125
|
+
result = result.replace(f"${{{var_name}}}", env_value)
|
|
126
|
+
return result
|
|
127
|
+
elif isinstance(value, dict):
|
|
128
|
+
return {k: self._expand_env_vars(v) for k, v in value.items()}
|
|
129
|
+
elif isinstance(value, list):
|
|
130
|
+
return [self._expand_env_vars(item) for item in value]
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
def get_enabled_servers_for_sdk(self) -> dict[str, dict[str, Any]]:
|
|
134
|
+
"""Get enabled servers in SDK-compatible format with env vars expanded."""
|
|
135
|
+
result = {}
|
|
136
|
+
for name, data in self._config.servers.items():
|
|
137
|
+
if data.get("enabled", True):
|
|
138
|
+
config = data.get("config", {})
|
|
139
|
+
result[name] = self._expand_env_vars(config)
|
|
140
|
+
return result
|
claude/widgets.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from textual.binding import Binding
|
|
2
|
+
from textual.widgets import Input
|
|
3
|
+
|
|
4
|
+
from claude.history import CommandHistory
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HistoryInput(Input):
|
|
8
|
+
"""Input widget with command history navigation."""
|
|
9
|
+
|
|
10
|
+
BINDINGS = [
|
|
11
|
+
Binding("up", "history_previous", "Previous command", show=False),
|
|
12
|
+
Binding("down", "history_next", "Next command", show=False),
|
|
13
|
+
Binding("escape", "app.cancel_query", "Cancel", show=False),
|
|
14
|
+
Binding("ctrl+c", "app.cancel_query", "Cancel", show=False),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
def __init__(self, history: CommandHistory, **kwargs):
|
|
18
|
+
super().__init__(**kwargs)
|
|
19
|
+
self.history = history
|
|
20
|
+
|
|
21
|
+
def action_history_previous(self) -> None:
|
|
22
|
+
"""Navigate to previous command in history."""
|
|
23
|
+
self.value = self.history.navigate_up(self.value)
|
|
24
|
+
self.cursor_position = len(self.value)
|
|
25
|
+
|
|
26
|
+
def action_history_next(self) -> None:
|
|
27
|
+
"""Navigate to next command in history."""
|
|
28
|
+
self.value = self.history.navigate_down(self.value)
|
|
29
|
+
self.cursor_position = len(self.value)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-sdk-tutor
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.13
|
|
7
7
|
Requires-Dist: claude-agent-sdk>=0.1.26
|
|
8
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
8
9
|
Requires-Dist: textual-dev>=1.8.0
|
|
9
10
|
Requires-Dist: textual>=7.5.0
|
|
10
11
|
Requires-Dist: watchfiles>=1.1.1
|
|
@@ -14,6 +15,15 @@ Description-Content-Type: text/markdown
|
|
|
14
15
|
|
|
15
16
|
A terminal-based programming tutor powered by Claude. Built with Textual and the Claude Agent SDK.
|
|
16
17
|
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Tutor Mode** - Claude guides your learning instead of writing code for you
|
|
21
|
+
- **Web Search** - Optional online lookup capability
|
|
22
|
+
- **MCP Servers** - Extend Claude with external tools via Model Context Protocol
|
|
23
|
+
- **Command History** - Navigate previous commands with up/down arrows (persisted across sessions)
|
|
24
|
+
- **Query Interruption** - Cancel long-running queries with Escape or Ctrl+C
|
|
25
|
+
- **File Access** - Claude can read files in your codebase to provide contextual help
|
|
26
|
+
|
|
17
27
|
## Overview
|
|
18
28
|
|
|
19
29
|
Claude Tutor is a TUI (Terminal User Interface) application designed to help you learn programming concepts. Unlike a typical coding assistant, Claude Tutor focuses on teaching rather than writing code for you. It will:
|
|
@@ -57,6 +67,16 @@ Type your programming questions in the input field and press Enter to send. Clau
|
|
|
57
67
|
- **Grey** - Tool usage (when Claude reads files in your codebase)
|
|
58
68
|
- **Green** - Slash command feedback
|
|
59
69
|
|
|
70
|
+
### Keyboard Shortcuts
|
|
71
|
+
|
|
72
|
+
| Key | Action |
|
|
73
|
+
|-----|--------|
|
|
74
|
+
| `Enter` | Send message |
|
|
75
|
+
| `Up` / `Down` | Navigate command history |
|
|
76
|
+
| `Escape` or `Ctrl+C` | Cancel running query |
|
|
77
|
+
|
|
78
|
+
Command history is automatically saved between sessions.
|
|
79
|
+
|
|
60
80
|
## Slash Commands
|
|
61
81
|
|
|
62
82
|
| Command | Description |
|
|
@@ -65,6 +85,45 @@ Type your programming questions in the input field and press Enter to send. Clau
|
|
|
65
85
|
| `/clear` | Clears the conversation history and starts fresh. Your settings are preserved. |
|
|
66
86
|
| `/tutor` | Toggles tutor mode on/off. When on (default), Claude acts as a teacher. When off, Claude responds normally without the tutoring constraints. |
|
|
67
87
|
| `/togglewebsearch` | Toggles web search on/off. When on, Claude can use WebSearch and WebFetch tools to look up information online. Disabled by default. |
|
|
88
|
+
| `/mcp` | Manage MCP servers. Use `/mcp help` for detailed subcommands. |
|
|
89
|
+
|
|
90
|
+
## MCP Server Support
|
|
91
|
+
|
|
92
|
+
Claude Tutor supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers, allowing you to extend Claude's capabilities with external tools.
|
|
93
|
+
|
|
94
|
+
### MCP Commands
|
|
95
|
+
|
|
96
|
+
| Command | Description |
|
|
97
|
+
|---------|-------------|
|
|
98
|
+
| `/mcp list` | List all configured servers with connection status |
|
|
99
|
+
| `/mcp test [name]` | Test MCP server connections |
|
|
100
|
+
| `/mcp add` | Add a new server (interactive wizard) |
|
|
101
|
+
| `/mcp add <name> <type> <cmd\|url> [args]` | Add server directly |
|
|
102
|
+
| `/mcp remove <name>` | Remove a server |
|
|
103
|
+
| `/mcp enable <name>` | Enable a disabled server |
|
|
104
|
+
| `/mcp disable <name>` | Disable a server without removing it |
|
|
105
|
+
| `/mcp status [name]` | Show server configuration details |
|
|
106
|
+
|
|
107
|
+
### Server Types
|
|
108
|
+
|
|
109
|
+
- **stdio** - Local process that communicates via stdin/stdout (e.g., `npx` packages)
|
|
110
|
+
- **sse** - Server-Sent Events endpoint
|
|
111
|
+
- **http** - HTTP endpoint
|
|
112
|
+
|
|
113
|
+
### Example: Adding an MCP Server
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
/mcp add filesystem stdio npx -y @anthropic/mcp-filesystem
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or use the interactive wizard:
|
|
120
|
+
```
|
|
121
|
+
/mcp add
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
After adding or modifying servers, use `/clear` to reconnect with the updated configuration.
|
|
125
|
+
|
|
126
|
+
MCP server configurations are persisted to `~/.local/share/claude-sdk-tutor/mcp_servers.json`.
|
|
68
127
|
|
|
69
128
|
## Tech Stack
|
|
70
129
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
app.py,sha256=i07mRpBc-mrNrmpnhvL3kS-FqcWM7W89Ew6fCKe7I6Y,13444
|
|
2
|
+
claude/__init__.py,sha256=rGXaYQtfDG3XywfxU_vHUR5afA_ArxLxzprR04pSnZM,128
|
|
3
|
+
claude/claude_agent.py,sha256=L-Q1qE1f50UvQ-bckpimHySF5Wek2F1kK9ZfCdwuXdg,1546
|
|
4
|
+
claude/history.py,sha256=-JpVhha552jZkyxuaLbbL2GluQXvFzGmhS6mtB63940,2273
|
|
5
|
+
claude/mcp_commands.py,sha256=O3jKPCkk5l2JuB8aBoZbxE5w1yOX6cHECAVAGX7tKK8,9051
|
|
6
|
+
claude/mcp_config.py,sha256=Ot-S8YweTog80C2MBDjtOdpfwjneaqQmLoTINVdOaxY,4637
|
|
7
|
+
claude/widgets.py,sha256=5f0PwjIBgtUotveqpqAbYYy327PyxUo9y_3wMLTEm7c,1040
|
|
8
|
+
claude_sdk_tutor-0.1.6.dist-info/METADATA,sha256=WBPyB-SaCM9aUwGQa3DKGIXQBXfZUzBxTCplkrPzuLg,4519
|
|
9
|
+
claude_sdk_tutor-0.1.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
10
|
+
claude_sdk_tutor-0.1.6.dist-info/entry_points.txt,sha256=vI78kiiqb59KzHEa8UsnkvCbmCs0IMLXOuO2qiho4U4,46
|
|
11
|
+
claude_sdk_tutor-0.1.6.dist-info/licenses/LICENSE,sha256=KzxybQVVAEGBifrjNj5OGwQ_rsbzCIGPm0xrTL6-VZs,1067
|
|
12
|
+
claude_sdk_tutor-0.1.6.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
app.py,sha256=34d9pmuZ1o-2_sSeEiwoE2BoK2VIIFID2-N9PH6LyOI,5669
|
|
2
|
-
claude/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
claude/claude_agent.py,sha256=-xs3CNr5p2AosKy6UJvf6nTQLdJd8GG7qtS4jd6K_ts,1304
|
|
4
|
-
claude_sdk_tutor-0.1.4.dist-info/METADATA,sha256=qMIq_SSfX3_03Okz3uSIu2RJv2ogd60RO5LQ7EuHA1M,2445
|
|
5
|
-
claude_sdk_tutor-0.1.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
-
claude_sdk_tutor-0.1.4.dist-info/entry_points.txt,sha256=vI78kiiqb59KzHEa8UsnkvCbmCs0IMLXOuO2qiho4U4,46
|
|
7
|
-
claude_sdk_tutor-0.1.4.dist-info/licenses/LICENSE,sha256=KzxybQVVAEGBifrjNj5OGwQ_rsbzCIGPm0xrTL6-VZs,1067
|
|
8
|
-
claude_sdk_tutor-0.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|