devduck 0.6.0__py3-none-any.whl → 0.7.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.
Potentially problematic release.
This version of devduck might be problematic. Click here for more details.
- devduck/__init__.py +356 -158
- devduck/_version.py +2 -2
- devduck/tools/agentcore_invoke.py +1 -0
- devduck/tools/install_tools.py +103 -2
- {devduck-0.6.0.dist-info → devduck-0.7.0.dist-info}/METADATA +136 -4
- {devduck-0.6.0.dist-info → devduck-0.7.0.dist-info}/RECORD +10 -10
- {devduck-0.6.0.dist-info → devduck-0.7.0.dist-info}/WHEEL +0 -0
- {devduck-0.6.0.dist-info → devduck-0.7.0.dist-info}/entry_points.txt +0 -0
- {devduck-0.6.0.dist-info → devduck-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {devduck-0.6.0.dist-info → devduck-0.7.0.dist-info}/top_level.txt +0 -0
devduck/__init__.py
CHANGED
|
@@ -183,6 +183,194 @@ Last Modified: {modified}"""
|
|
|
183
183
|
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|
|
184
184
|
|
|
185
185
|
|
|
186
|
+
def manage_tools_func(
|
|
187
|
+
action: str,
|
|
188
|
+
package: str = None,
|
|
189
|
+
tool_names: str = None,
|
|
190
|
+
tool_path: str = None,
|
|
191
|
+
) -> Dict[str, Any]:
|
|
192
|
+
"""
|
|
193
|
+
Manage the agent's tool set at runtime using ToolRegistry.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
action: Action to perform - "list", "add", "remove", "reload"
|
|
197
|
+
package: Package name to load tools from (e.g., "strands_tools", "strands_fun_tools")
|
|
198
|
+
tool_names: Comma-separated tool names (e.g., "shell,editor,calculator")
|
|
199
|
+
tool_path: Path to a .py file to load as a tool
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Dict with status and content
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
if not hasattr(devduck, "agent") or not devduck.agent:
|
|
206
|
+
return {"status": "error", "content": [{"text": "Agent not initialized"}]}
|
|
207
|
+
|
|
208
|
+
registry = devduck.agent.tool_registry
|
|
209
|
+
|
|
210
|
+
if action == "list":
|
|
211
|
+
# List tools from registry
|
|
212
|
+
tool_list = list(registry.registry.keys())
|
|
213
|
+
dynamic_tools = list(registry.dynamic_tools.keys())
|
|
214
|
+
|
|
215
|
+
text = f"Currently loaded {len(tool_list)} tools:\n"
|
|
216
|
+
text += "\n".join(f" • {t}" for t in sorted(tool_list))
|
|
217
|
+
if dynamic_tools:
|
|
218
|
+
text += f"\n\nDynamic tools ({len(dynamic_tools)}):\n"
|
|
219
|
+
text += "\n".join(f" • {t}" for t in sorted(dynamic_tools))
|
|
220
|
+
|
|
221
|
+
return {"status": "success", "content": [{"text": text}]}
|
|
222
|
+
|
|
223
|
+
elif action == "add":
|
|
224
|
+
if not package and not tool_path:
|
|
225
|
+
return {
|
|
226
|
+
"status": "error",
|
|
227
|
+
"content": [
|
|
228
|
+
{
|
|
229
|
+
"text": "Either 'package' or 'tool_path' required for add action"
|
|
230
|
+
}
|
|
231
|
+
],
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
added_tools = []
|
|
235
|
+
|
|
236
|
+
# Add from package using process_tools
|
|
237
|
+
if package:
|
|
238
|
+
if not tool_names:
|
|
239
|
+
return {
|
|
240
|
+
"status": "error",
|
|
241
|
+
"content": [
|
|
242
|
+
{"text": "'tool_names' required when adding from package"}
|
|
243
|
+
],
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
tools_to_add = [t.strip() for t in tool_names.split(",")]
|
|
247
|
+
|
|
248
|
+
# Build tool specs: package.tool_name format
|
|
249
|
+
tool_specs = [f"{package}.{tool_name}" for tool_name in tools_to_add]
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
added_tool_names = registry.process_tools(tool_specs)
|
|
253
|
+
added_tools.extend(added_tool_names)
|
|
254
|
+
logger.info(f"Added tools from {package}: {added_tool_names}")
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.error(f"Failed to add tools from {package}: {e}")
|
|
257
|
+
return {
|
|
258
|
+
"status": "error",
|
|
259
|
+
"content": [{"text": f"Failed to add tools: {str(e)}"}],
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# Add from file path using process_tools
|
|
263
|
+
if tool_path:
|
|
264
|
+
try:
|
|
265
|
+
added_tool_names = registry.process_tools([tool_path])
|
|
266
|
+
added_tools.extend(added_tool_names)
|
|
267
|
+
logger.info(f"Added tools from file: {added_tool_names}")
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.error(f"Failed to add tool from {tool_path}: {e}")
|
|
270
|
+
return {
|
|
271
|
+
"status": "error",
|
|
272
|
+
"content": [{"text": f"Failed to add tool: {str(e)}"}],
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if added_tools:
|
|
276
|
+
return {
|
|
277
|
+
"status": "success",
|
|
278
|
+
"content": [
|
|
279
|
+
{
|
|
280
|
+
"text": f"✅ Added {len(added_tools)} tools: {', '.join(added_tools)}\n"
|
|
281
|
+
+ f"Total tools: {len(registry.registry)}"
|
|
282
|
+
}
|
|
283
|
+
],
|
|
284
|
+
}
|
|
285
|
+
else:
|
|
286
|
+
return {"status": "error", "content": [{"text": "No tools were added"}]}
|
|
287
|
+
|
|
288
|
+
elif action == "remove":
|
|
289
|
+
if not tool_names:
|
|
290
|
+
return {
|
|
291
|
+
"status": "error",
|
|
292
|
+
"content": [{"text": "'tool_names' required for remove action"}],
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
tools_to_remove = [t.strip() for t in tool_names.split(",")]
|
|
296
|
+
removed_tools = []
|
|
297
|
+
|
|
298
|
+
# Remove from registry
|
|
299
|
+
for tool_name in tools_to_remove:
|
|
300
|
+
if tool_name in registry.registry:
|
|
301
|
+
del registry.registry[tool_name]
|
|
302
|
+
removed_tools.append(tool_name)
|
|
303
|
+
logger.info(f"Removed tool: {tool_name}")
|
|
304
|
+
|
|
305
|
+
if tool_name in registry.dynamic_tools:
|
|
306
|
+
del registry.dynamic_tools[tool_name]
|
|
307
|
+
logger.info(f"Removed dynamic tool: {tool_name}")
|
|
308
|
+
|
|
309
|
+
if removed_tools:
|
|
310
|
+
return {
|
|
311
|
+
"status": "success",
|
|
312
|
+
"content": [
|
|
313
|
+
{
|
|
314
|
+
"text": f"✅ Removed {len(removed_tools)} tools: {', '.join(removed_tools)}\n"
|
|
315
|
+
+ f"Total tools: {len(registry.registry)}"
|
|
316
|
+
}
|
|
317
|
+
],
|
|
318
|
+
}
|
|
319
|
+
else:
|
|
320
|
+
return {
|
|
321
|
+
"status": "success",
|
|
322
|
+
"content": [{"text": "No tools were removed (not found)"}],
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
elif action == "reload":
|
|
326
|
+
if tool_names:
|
|
327
|
+
# Reload specific tools
|
|
328
|
+
tools_to_reload = [t.strip() for t in tool_names.split(",")]
|
|
329
|
+
reloaded_tools = []
|
|
330
|
+
failed_tools = []
|
|
331
|
+
|
|
332
|
+
for tool_name in tools_to_reload:
|
|
333
|
+
try:
|
|
334
|
+
registry.reload_tool(tool_name)
|
|
335
|
+
reloaded_tools.append(tool_name)
|
|
336
|
+
logger.info(f"Reloaded tool: {tool_name}")
|
|
337
|
+
except Exception as e:
|
|
338
|
+
failed_tools.append((tool_name, str(e)))
|
|
339
|
+
logger.error(f"Failed to reload {tool_name}: {e}")
|
|
340
|
+
|
|
341
|
+
text = ""
|
|
342
|
+
if reloaded_tools:
|
|
343
|
+
text += f"✅ Reloaded {len(reloaded_tools)} tools: {', '.join(reloaded_tools)}\n"
|
|
344
|
+
if failed_tools:
|
|
345
|
+
text += f"❌ Failed to reload {len(failed_tools)} tools:\n"
|
|
346
|
+
for tool_name, error in failed_tools:
|
|
347
|
+
text += f" • {tool_name}: {error}\n"
|
|
348
|
+
|
|
349
|
+
return {"status": "success", "content": [{"text": text}]}
|
|
350
|
+
else:
|
|
351
|
+
# Reload all tools - restart agent
|
|
352
|
+
logger.info("Reloading all tools via restart")
|
|
353
|
+
devduck.restart()
|
|
354
|
+
return {
|
|
355
|
+
"status": "success",
|
|
356
|
+
"content": [{"text": "✅ All tools reloaded - agent restarted"}],
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
else:
|
|
360
|
+
return {
|
|
361
|
+
"status": "error",
|
|
362
|
+
"content": [
|
|
363
|
+
{
|
|
364
|
+
"text": f"Unknown action: {action}. Valid: list, add, remove, reload"
|
|
365
|
+
}
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
logger.error(f"Error in manage_tools: {e}")
|
|
371
|
+
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|
|
372
|
+
|
|
373
|
+
|
|
186
374
|
def get_shell_history_file():
|
|
187
375
|
"""Get the devduck-specific history file path."""
|
|
188
376
|
devduck_history = Path.home() / ".devduck_history"
|
|
@@ -380,6 +568,7 @@ class DevDuck:
|
|
|
380
568
|
self,
|
|
381
569
|
auto_start_servers=True,
|
|
382
570
|
servers=None,
|
|
571
|
+
load_mcp_servers=True,
|
|
383
572
|
):
|
|
384
573
|
"""Initialize the minimalist adaptive agent
|
|
385
574
|
|
|
@@ -392,6 +581,7 @@ class DevDuck:
|
|
|
392
581
|
"mcp": {"port": 8000},
|
|
393
582
|
"ipc": {"socket_path": "/tmp/devduck.sock"}
|
|
394
583
|
}
|
|
584
|
+
load_mcp_servers: Load MCP servers from MCP_SERVERS env var
|
|
395
585
|
"""
|
|
396
586
|
logger.info("Initializing DevDuck agent...")
|
|
397
587
|
try:
|
|
@@ -441,14 +631,13 @@ class DevDuck:
|
|
|
441
631
|
|
|
442
632
|
from strands import Agent, tool
|
|
443
633
|
|
|
444
|
-
#
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
core_tools = self._load_default_tools()
|
|
634
|
+
# Load tools with flexible configuration
|
|
635
|
+
# Default tool config - user can override with DEVDUCK_TOOLS env var
|
|
636
|
+
default_tools = "devduck.tools:system_prompt,store_in_kb,ipc,tcp,websocket,mcp_server,state_manager,tray,ambient,agentcore_config,agentcore_invoke,agentcore_logs,agentcore_agents,install_tools,create_subagent,use_github:strands_tools:shell,editor,file_read,file_write,image_reader,load_tool,retrieve,calculator,use_agent,environment,mcp_client,speak,slack:strands_fun_tools:listen,cursor,clipboard,screen_reader,bluetooth,yolo_vision"
|
|
637
|
+
|
|
638
|
+
tools_config = os.getenv("DEVDUCK_TOOLS", default_tools)
|
|
639
|
+
logger.info(f"Loading tools from config: {tools_config}")
|
|
640
|
+
core_tools = self._load_tools_from_config(tools_config)
|
|
452
641
|
|
|
453
642
|
# Wrap view_logs_tool with @tool decorator
|
|
454
643
|
@tool
|
|
@@ -460,23 +649,51 @@ class DevDuck:
|
|
|
460
649
|
"""View and manage DevDuck logs."""
|
|
461
650
|
return view_logs_tool(action, lines, pattern)
|
|
462
651
|
|
|
652
|
+
# Wrap manage_tools_func with @tool decorator
|
|
653
|
+
@tool
|
|
654
|
+
def manage_tools(
|
|
655
|
+
action: str,
|
|
656
|
+
package: str = None,
|
|
657
|
+
tool_names: str = None,
|
|
658
|
+
tool_path: str = None,
|
|
659
|
+
) -> Dict[str, Any]:
|
|
660
|
+
"""Manage the agent's tool set at runtime - add, remove, list, reload tools on the fly."""
|
|
661
|
+
return manage_tools_func(action, package, tool_names, tool_path)
|
|
662
|
+
|
|
463
663
|
# Add built-in tools to the toolset
|
|
464
|
-
core_tools.extend([view_logs])
|
|
664
|
+
core_tools.extend([view_logs, manage_tools])
|
|
465
665
|
|
|
466
666
|
# Assign tools
|
|
467
667
|
self.tools = core_tools
|
|
468
668
|
|
|
669
|
+
# 🔌 Load MCP servers if enabled
|
|
670
|
+
if load_mcp_servers:
|
|
671
|
+
mcp_clients = self._load_mcp_servers()
|
|
672
|
+
if mcp_clients:
|
|
673
|
+
self.tools.extend(mcp_clients)
|
|
674
|
+
logger.info(f"Loaded {len(mcp_clients)} MCP server(s)")
|
|
675
|
+
|
|
469
676
|
logger.info(f"Initialized {len(self.tools)} tools")
|
|
470
677
|
|
|
471
678
|
# 🎯 Smart model selection
|
|
472
679
|
self.agent_model, self.model = self._select_model()
|
|
473
680
|
|
|
474
681
|
# Create agent with self-healing
|
|
682
|
+
# load_tools_from_directory controlled by DEVDUCK_LOAD_TOOLS_FROM_DIR (default: false)
|
|
683
|
+
load_from_dir = (
|
|
684
|
+
os.getenv("DEVDUCK_LOAD_TOOLS_FROM_DIR", "false").lower() == "true"
|
|
685
|
+
)
|
|
686
|
+
|
|
475
687
|
self.agent = Agent(
|
|
476
688
|
model=self.agent_model,
|
|
477
689
|
tools=self.tools,
|
|
478
690
|
system_prompt=self._build_system_prompt(),
|
|
479
|
-
load_tools_from_directory=
|
|
691
|
+
load_tools_from_directory=load_from_dir,
|
|
692
|
+
trace_attributes={
|
|
693
|
+
"session.id": self.session_id,
|
|
694
|
+
"user.id": self.env_info["hostname"],
|
|
695
|
+
"tags": ["Strands-Agents", "DevDuck"],
|
|
696
|
+
},
|
|
480
697
|
)
|
|
481
698
|
|
|
482
699
|
# 🚀 AUTO-START SERVERS
|
|
@@ -500,31 +717,22 @@ class DevDuck:
|
|
|
500
717
|
|
|
501
718
|
Format: package:tool1,tool2:package2:tool3
|
|
502
719
|
Example: strands_tools:shell,editor:strands_fun_tools:clipboard
|
|
720
|
+
|
|
721
|
+
Note: Only loads what's specified in config - no automatic additions
|
|
503
722
|
"""
|
|
504
723
|
tools = []
|
|
505
|
-
|
|
506
|
-
# Always load DevDuck core tools
|
|
507
|
-
tools.extend(self._load_devduck_tools())
|
|
508
|
-
|
|
509
|
-
# Parse and load configured tools
|
|
510
724
|
current_package = None
|
|
511
725
|
|
|
512
726
|
for segment in config.split(":"):
|
|
513
727
|
segment = segment.strip()
|
|
514
728
|
|
|
515
|
-
# Check if
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
):
|
|
523
|
-
# Single tool from current package
|
|
524
|
-
if current_package:
|
|
525
|
-
tool = self._load_single_tool(current_package, segment)
|
|
526
|
-
if tool:
|
|
527
|
-
tools.append(tool)
|
|
729
|
+
# Check if segment is a package name (contains '.' or '_' and no ',')
|
|
730
|
+
is_package = "," not in segment and ("." in segment or "_" in segment)
|
|
731
|
+
|
|
732
|
+
if is_package:
|
|
733
|
+
# This is a package name - set as current package
|
|
734
|
+
current_package = segment
|
|
735
|
+
logger.debug(f"Switched to package: {current_package}")
|
|
528
736
|
elif "," in segment:
|
|
529
737
|
# Tool list from current package
|
|
530
738
|
if current_package:
|
|
@@ -533,11 +741,15 @@ class DevDuck:
|
|
|
533
741
|
tool = self._load_single_tool(current_package, tool_name)
|
|
534
742
|
if tool:
|
|
535
743
|
tools.append(tool)
|
|
744
|
+
elif current_package:
|
|
745
|
+
# Single tool from current package
|
|
746
|
+
tool = self._load_single_tool(current_package, segment)
|
|
747
|
+
if tool:
|
|
748
|
+
tools.append(tool)
|
|
536
749
|
else:
|
|
537
|
-
|
|
538
|
-
current_package = segment
|
|
750
|
+
logger.warning(f"Skipping segment '{segment}' - no package set")
|
|
539
751
|
|
|
540
|
-
logger.info(f"Loaded tools from
|
|
752
|
+
logger.info(f"Loaded {len(tools)} tools from configuration")
|
|
541
753
|
return tools
|
|
542
754
|
|
|
543
755
|
def _load_single_tool(self, package, tool_name):
|
|
@@ -551,134 +763,103 @@ class DevDuck:
|
|
|
551
763
|
logger.warning(f"Failed to load {tool_name} from {package}: {e}")
|
|
552
764
|
return None
|
|
553
765
|
|
|
554
|
-
def
|
|
555
|
-
"""
|
|
556
|
-
|
|
766
|
+
def _load_mcp_servers(self):
|
|
767
|
+
"""
|
|
768
|
+
Load MCP servers from MCP_SERVERS environment variable using direct loading.
|
|
557
769
|
|
|
558
|
-
|
|
559
|
-
|
|
770
|
+
Uses the experimental managed integration - MCPClient instances are passed
|
|
771
|
+
directly to Agent constructor without explicit context management.
|
|
772
|
+
|
|
773
|
+
Format: JSON with "mcpServers" object
|
|
774
|
+
Example: MCP_SERVERS='{"mcpServers": {"strands": {"command": "uvx", "args": ["strands-agents-mcp-server"]}}}'
|
|
775
|
+
|
|
776
|
+
Returns:
|
|
777
|
+
List of MCPClient instances ready for direct use in Agent
|
|
778
|
+
"""
|
|
779
|
+
import json
|
|
780
|
+
|
|
781
|
+
mcp_servers_json = os.getenv("MCP_SERVERS")
|
|
782
|
+
if not mcp_servers_json:
|
|
783
|
+
logger.debug("No MCP_SERVERS environment variable found")
|
|
784
|
+
return []
|
|
560
785
|
|
|
561
|
-
# Load strands-agents-tools (essential)
|
|
562
786
|
try:
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
editor,
|
|
566
|
-
file_read,
|
|
567
|
-
file_write,
|
|
568
|
-
calculator,
|
|
569
|
-
image_reader,
|
|
570
|
-
use_agent,
|
|
571
|
-
load_tool,
|
|
572
|
-
environment,
|
|
573
|
-
mcp_client,
|
|
574
|
-
retrieve,
|
|
575
|
-
)
|
|
787
|
+
config = json.loads(mcp_servers_json)
|
|
788
|
+
mcp_servers_config = config.get("mcpServers", {})
|
|
576
789
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
editor,
|
|
581
|
-
file_read,
|
|
582
|
-
file_write,
|
|
583
|
-
calculator,
|
|
584
|
-
image_reader,
|
|
585
|
-
use_agent,
|
|
586
|
-
load_tool,
|
|
587
|
-
environment,
|
|
588
|
-
mcp_client,
|
|
589
|
-
retrieve,
|
|
590
|
-
]
|
|
591
|
-
)
|
|
592
|
-
logger.info("✅ strands-agents-tools loaded")
|
|
593
|
-
except ImportError:
|
|
594
|
-
logger.info("strands-agents-tools unavailable")
|
|
790
|
+
if not mcp_servers_config:
|
|
791
|
+
logger.warning("MCP_SERVERS JSON has no 'mcpServers' key")
|
|
792
|
+
return []
|
|
595
793
|
|
|
596
|
-
|
|
597
|
-
if "--mcp" not in sys.argv:
|
|
598
|
-
try:
|
|
599
|
-
from strands_fun_tools import (
|
|
600
|
-
listen,
|
|
601
|
-
cursor,
|
|
602
|
-
clipboard,
|
|
603
|
-
screen_reader,
|
|
604
|
-
yolo_vision,
|
|
605
|
-
)
|
|
794
|
+
mcp_clients = []
|
|
606
795
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
796
|
+
from strands.tools.mcp import MCPClient
|
|
797
|
+
from mcp import stdio_client, StdioServerParameters
|
|
798
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
799
|
+
from mcp.client.sse import sse_client
|
|
611
800
|
|
|
612
|
-
|
|
801
|
+
for server_name, server_config in mcp_servers_config.items():
|
|
802
|
+
try:
|
|
803
|
+
logger.info(f"Loading MCP server: {server_name}")
|
|
804
|
+
|
|
805
|
+
# Determine transport type and create appropriate callable
|
|
806
|
+
if "command" in server_config:
|
|
807
|
+
# stdio transport
|
|
808
|
+
command = server_config["command"]
|
|
809
|
+
args = server_config.get("args", [])
|
|
810
|
+
env = server_config.get("env", None)
|
|
811
|
+
|
|
812
|
+
transport_callable = (
|
|
813
|
+
lambda cmd=command, a=args, e=env: stdio_client(
|
|
814
|
+
StdioServerParameters(command=cmd, args=a, env=e)
|
|
815
|
+
)
|
|
816
|
+
)
|
|
613
817
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
from .tools import (
|
|
619
|
-
tcp,
|
|
620
|
-
websocket,
|
|
621
|
-
ipc,
|
|
622
|
-
mcp_server,
|
|
623
|
-
install_tools,
|
|
624
|
-
use_github,
|
|
625
|
-
create_subagent,
|
|
626
|
-
store_in_kb,
|
|
627
|
-
system_prompt,
|
|
628
|
-
state_manager,
|
|
629
|
-
tray,
|
|
630
|
-
ambient,
|
|
631
|
-
)
|
|
818
|
+
elif "url" in server_config:
|
|
819
|
+
# Determine if SSE or streamable HTTP based on URL path
|
|
820
|
+
url = server_config["url"]
|
|
821
|
+
headers = server_config.get("headers", None)
|
|
632
822
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
)
|
|
649
|
-
logger.info("✅ DevDuck core tools loaded")
|
|
650
|
-
except ImportError as e:
|
|
651
|
-
logger.warning(f"DevDuck tools unavailable: {e}")
|
|
823
|
+
if "/sse" in url:
|
|
824
|
+
# SSE transport
|
|
825
|
+
transport_callable = lambda u=url: sse_client(u)
|
|
826
|
+
else:
|
|
827
|
+
# Streamable HTTP transport (default for HTTP)
|
|
828
|
+
transport_callable = (
|
|
829
|
+
lambda u=url, h=headers: streamablehttp_client(
|
|
830
|
+
url=u, headers=h
|
|
831
|
+
)
|
|
832
|
+
)
|
|
833
|
+
else:
|
|
834
|
+
logger.warning(
|
|
835
|
+
f"MCP server {server_name} has no 'command' or 'url' - skipping"
|
|
836
|
+
)
|
|
837
|
+
continue
|
|
652
838
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
839
|
+
# Create MCPClient with direct loading (experimental managed integration)
|
|
840
|
+
# No need for context managers - Agent handles lifecycle
|
|
841
|
+
prefix = server_config.get("prefix", server_name)
|
|
842
|
+
mcp_client = MCPClient(
|
|
843
|
+
transport_callable=transport_callable, prefix=prefix
|
|
844
|
+
)
|
|
657
845
|
|
|
658
|
-
|
|
846
|
+
mcp_clients.append(mcp_client)
|
|
847
|
+
logger.info(
|
|
848
|
+
f"✓ MCP server '{server_name}' loaded (prefix: {prefix})"
|
|
849
|
+
)
|
|
659
850
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
from .tools.agentcore_agents import agentcore_agents
|
|
664
|
-
|
|
665
|
-
tools.extend(
|
|
666
|
-
[
|
|
667
|
-
agentcore_config,
|
|
668
|
-
agentcore_invoke,
|
|
669
|
-
agentcore_logs,
|
|
670
|
-
agentcore_agents,
|
|
671
|
-
]
|
|
672
|
-
)
|
|
673
|
-
logger.info("✅ AgentCore tools loaded")
|
|
674
|
-
except Exception as e:
|
|
675
|
-
logger.debug(f"AgentCore tools unavailable: {e}")
|
|
676
|
-
else:
|
|
677
|
-
logger.info(
|
|
678
|
-
"⏭️ AgentCore tools disabled (DEVDUCK_DISABLE_AGENTCORE_TOOLS=true)"
|
|
679
|
-
)
|
|
851
|
+
except Exception as e:
|
|
852
|
+
logger.error(f"Failed to load MCP server '{server_name}': {e}")
|
|
853
|
+
continue
|
|
680
854
|
|
|
681
|
-
|
|
855
|
+
return mcp_clients
|
|
856
|
+
|
|
857
|
+
except json.JSONDecodeError as e:
|
|
858
|
+
logger.error(f"Invalid JSON in MCP_SERVERS: {e}")
|
|
859
|
+
return []
|
|
860
|
+
except Exception as e:
|
|
861
|
+
logger.error(f"Error loading MCP servers: {e}")
|
|
862
|
+
return []
|
|
682
863
|
|
|
683
864
|
def _select_model(self):
|
|
684
865
|
"""
|
|
@@ -768,6 +949,7 @@ class DevDuck:
|
|
|
768
949
|
current_time = datetime.now().strftime("%I:%M %p")
|
|
769
950
|
|
|
770
951
|
session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
|
|
952
|
+
self.session_id = session_id
|
|
771
953
|
|
|
772
954
|
# Get own file path for self-modification awareness
|
|
773
955
|
own_file_path = Path(__file__).resolve()
|
|
@@ -834,12 +1016,19 @@ Set DEVDUCK_TOOLS for custom tools:
|
|
|
834
1016
|
- Example: strands_tools:shell,editor:strands_fun_tools:clipboard
|
|
835
1017
|
- Tools are filtered - only specified tools are loaded
|
|
836
1018
|
|
|
837
|
-
## MCP
|
|
1019
|
+
## MCP Integration:
|
|
838
1020
|
- **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
|
|
839
1021
|
- Example: mcp_server(action="start", port=8000)
|
|
840
1022
|
- Connect from Claude Desktop, other agents, or custom clients
|
|
841
1023
|
- Full bidirectional communication
|
|
842
1024
|
|
|
1025
|
+
- **Load MCP Servers** - Set MCP_SERVERS env var to auto-load external MCP servers
|
|
1026
|
+
- Format: JSON with "mcpServers" object
|
|
1027
|
+
- Stdio servers: command, args, env keys
|
|
1028
|
+
- HTTP servers: url, headers keys
|
|
1029
|
+
- Example: MCP_SERVERS='{{"mcpServers": {{"strands": {{"command": "uvx", "args": ["strands-agents-mcp-server"]}}}}}}'
|
|
1030
|
+
- Tools from MCP servers automatically available in agent context
|
|
1031
|
+
|
|
843
1032
|
## Knowledge Base Integration:
|
|
844
1033
|
- **Automatic RAG** - Set DEVDUCK_KNOWLEDGE_BASE_ID to enable automatic retrieval/storage
|
|
845
1034
|
- Before each query: Retrieves relevant context from knowledge base
|
|
@@ -1000,7 +1189,9 @@ When you learn something valuable during conversations:
|
|
|
1000
1189
|
logger.warning(f"No available ports found for TCP server")
|
|
1001
1190
|
continue
|
|
1002
1191
|
|
|
1003
|
-
result = self.agent.tool.tcp(
|
|
1192
|
+
result = self.agent.tool.tcp(
|
|
1193
|
+
action="start_server", port=port, record_direct_tool_call=False
|
|
1194
|
+
)
|
|
1004
1195
|
|
|
1005
1196
|
if result.get("status") == "success":
|
|
1006
1197
|
logger.info(f"✓ TCP server started on port {port}")
|
|
@@ -1022,7 +1213,9 @@ When you learn something valuable during conversations:
|
|
|
1022
1213
|
)
|
|
1023
1214
|
continue
|
|
1024
1215
|
|
|
1025
|
-
result = self.agent.tool.websocket(
|
|
1216
|
+
result = self.agent.tool.websocket(
|
|
1217
|
+
action="start_server", port=port, record_direct_tool_call=False
|
|
1218
|
+
)
|
|
1026
1219
|
|
|
1027
1220
|
if result.get("status") == "success":
|
|
1028
1221
|
logger.info(f"✓ WebSocket server started on port {port}")
|
|
@@ -1048,6 +1241,7 @@ When you learn something valuable during conversations:
|
|
|
1048
1241
|
port=port,
|
|
1049
1242
|
expose_agent=True,
|
|
1050
1243
|
agent=self.agent,
|
|
1244
|
+
record_direct_tool_call=False,
|
|
1051
1245
|
)
|
|
1052
1246
|
|
|
1053
1247
|
if result.get("status") == "success":
|
|
@@ -1075,7 +1269,9 @@ When you learn something valuable during conversations:
|
|
|
1075
1269
|
socket_path = available_socket
|
|
1076
1270
|
|
|
1077
1271
|
result = self.agent.tool.ipc(
|
|
1078
|
-
action="start_server",
|
|
1272
|
+
action="start_server",
|
|
1273
|
+
socket_path=socket_path,
|
|
1274
|
+
record_direct_tool_call=False,
|
|
1079
1275
|
)
|
|
1080
1276
|
|
|
1081
1277
|
if result.get("status") == "success":
|
|
@@ -1094,6 +1290,7 @@ When you learn something valuable during conversations:
|
|
|
1094
1290
|
|
|
1095
1291
|
try:
|
|
1096
1292
|
logger.info(f"Agent call started: {query[:100]}...")
|
|
1293
|
+
|
|
1097
1294
|
# Mark agent as executing to prevent hot-reload interruption
|
|
1098
1295
|
self._agent_executing = True
|
|
1099
1296
|
|
|
@@ -1133,7 +1330,7 @@ When you learn something valuable during conversations:
|
|
|
1133
1330
|
# Check for pending hot-reload
|
|
1134
1331
|
if self._reload_pending:
|
|
1135
1332
|
logger.info("Triggering pending hot-reload after agent completion")
|
|
1136
|
-
print("🦆 Agent finished - triggering pending hot-reload...")
|
|
1333
|
+
print("\n🦆 Agent finished - triggering pending hot-reload...")
|
|
1137
1334
|
self._hot_reload()
|
|
1138
1335
|
|
|
1139
1336
|
return result
|
|
@@ -1148,7 +1345,7 @@ When you learn something valuable during conversations:
|
|
|
1148
1345
|
|
|
1149
1346
|
def restart(self):
|
|
1150
1347
|
"""Restart the agent"""
|
|
1151
|
-
print("🦆 Restarting...")
|
|
1348
|
+
print("\n🦆 Restarting...")
|
|
1152
1349
|
self.__init__()
|
|
1153
1350
|
|
|
1154
1351
|
def _start_file_watcher(self):
|
|
@@ -1193,7 +1390,7 @@ When you learn something valuable during conversations:
|
|
|
1193
1390
|
and current_mtime > self._last_modified
|
|
1194
1391
|
and current_time - last_reload_time > debounce_seconds
|
|
1195
1392
|
):
|
|
1196
|
-
print(f"🦆 Detected changes in {self._watch_file.name}!")
|
|
1393
|
+
print(f"\n🦆 Detected changes in {self._watch_file.name}!")
|
|
1197
1394
|
last_reload_time = current_time
|
|
1198
1395
|
|
|
1199
1396
|
# Check if agent is currently executing
|
|
@@ -1202,7 +1399,7 @@ When you learn something valuable during conversations:
|
|
|
1202
1399
|
"Code change detected but agent is executing - reload pending"
|
|
1203
1400
|
)
|
|
1204
1401
|
print(
|
|
1205
|
-
"🦆 Agent is currently executing - reload will trigger after completion"
|
|
1402
|
+
"\n🦆 Agent is currently executing - reload will trigger after completion"
|
|
1206
1403
|
)
|
|
1207
1404
|
self._reload_pending = True
|
|
1208
1405
|
# Don't update _last_modified yet - keep detecting the change
|
|
@@ -1235,7 +1432,7 @@ When you learn something valuable during conversations:
|
|
|
1235
1432
|
def _hot_reload(self):
|
|
1236
1433
|
"""Hot-reload by restarting the entire Python process with fresh code"""
|
|
1237
1434
|
logger.info("Hot-reload initiated")
|
|
1238
|
-
print("🦆 Hot-reloading via process restart...")
|
|
1435
|
+
print("\n🦆 Hot-reloading via process restart...")
|
|
1239
1436
|
|
|
1240
1437
|
try:
|
|
1241
1438
|
# Set reload flag to prevent recursive reloads during shutdown
|
|
@@ -1252,7 +1449,7 @@ When you learn something valuable during conversations:
|
|
|
1252
1449
|
if hasattr(self, "_watcher_running"):
|
|
1253
1450
|
self._watcher_running = False
|
|
1254
1451
|
|
|
1255
|
-
print("🦆 Restarting process with fresh code...")
|
|
1452
|
+
print("\n🦆 Restarting process with fresh code...")
|
|
1256
1453
|
|
|
1257
1454
|
# Restart the entire Python process
|
|
1258
1455
|
# This ensures all code is freshly loaded
|
|
@@ -1260,8 +1457,8 @@ When you learn something valuable during conversations:
|
|
|
1260
1457
|
|
|
1261
1458
|
except Exception as e:
|
|
1262
1459
|
logger.error(f"Hot-reload failed: {e}")
|
|
1263
|
-
print(f"🦆 Hot-reload failed: {e}")
|
|
1264
|
-
print("🦆 Falling back to manual restart")
|
|
1460
|
+
print(f"\n🦆 Hot-reload failed: {e}")
|
|
1461
|
+
print("\n🦆 Falling back to manual restart")
|
|
1265
1462
|
self._is_reloading = False
|
|
1266
1463
|
|
|
1267
1464
|
def status(self):
|
|
@@ -1552,6 +1749,7 @@ Claude Desktop Config:
|
|
|
1552
1749
|
transport="stdio",
|
|
1553
1750
|
expose_agent=True,
|
|
1554
1751
|
agent=devduck.agent,
|
|
1752
|
+
record_direct_tool_call=False,
|
|
1555
1753
|
)
|
|
1556
1754
|
except Exception as e:
|
|
1557
1755
|
logger.error(f"Failed to start MCP stdio server: {e}")
|
devduck/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.7.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 7, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
devduck/tools/install_tools.py
CHANGED
|
@@ -29,7 +29,7 @@ def install_tools(
|
|
|
29
29
|
and loading their tools into the agent's registry at runtime.
|
|
30
30
|
|
|
31
31
|
Args:
|
|
32
|
-
action: Action to perform - "install", "load", "install_and_load", "list_loaded"
|
|
32
|
+
action: Action to perform - "install", "load", "install_and_load", "list_loaded", "list_available"
|
|
33
33
|
package: Python package to install (e.g., "strands-agents-tools", "strands-fun-tools")
|
|
34
34
|
module: Module to import tools from (e.g., "strands_tools", "strands_fun_tools")
|
|
35
35
|
tool_names: Optional list of specific tools to load. If None, loads all available tools
|
|
@@ -39,6 +39,13 @@ def install_tools(
|
|
|
39
39
|
Result dictionary with status and content
|
|
40
40
|
|
|
41
41
|
Examples:
|
|
42
|
+
# List available tools in a package (without loading)
|
|
43
|
+
install_tools(
|
|
44
|
+
action="list_available",
|
|
45
|
+
package="strands-fun-tools",
|
|
46
|
+
module="strands_fun_tools"
|
|
47
|
+
)
|
|
48
|
+
|
|
42
49
|
# Install and load all tools from strands-agents-tools
|
|
43
50
|
install_tools(
|
|
44
51
|
action="install_and_load",
|
|
@@ -79,13 +86,15 @@ def install_tools(
|
|
|
79
86
|
return _load_tools_from_module(module, tool_names, agent)
|
|
80
87
|
elif action == "list_loaded":
|
|
81
88
|
return _list_loaded_tools(agent)
|
|
89
|
+
elif action == "list_available":
|
|
90
|
+
return _list_available_tools(package, module)
|
|
82
91
|
else:
|
|
83
92
|
return {
|
|
84
93
|
"status": "error",
|
|
85
94
|
"content": [
|
|
86
95
|
{
|
|
87
96
|
"text": f"❌ Unknown action: {action}\n\n"
|
|
88
|
-
f"Valid actions: install, load, install_and_load, list_loaded"
|
|
97
|
+
f"Valid actions: install, load, install_and_load, list_loaded, list_available"
|
|
89
98
|
}
|
|
90
99
|
],
|
|
91
100
|
}
|
|
@@ -306,3 +315,95 @@ def _list_loaded_tools(agent: Any) -> Dict[str, Any]:
|
|
|
306
315
|
"status": "error",
|
|
307
316
|
"content": [{"text": f"❌ Failed to list tools: {str(e)}"}],
|
|
308
317
|
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _list_available_tools(package: Optional[str], module: str) -> Dict[str, Any]:
|
|
321
|
+
"""List available tools in a package without loading them."""
|
|
322
|
+
if not module:
|
|
323
|
+
return {
|
|
324
|
+
"status": "error",
|
|
325
|
+
"content": [
|
|
326
|
+
{"text": "❌ module parameter is required for list_available action"}
|
|
327
|
+
],
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
# Try to import the module
|
|
332
|
+
try:
|
|
333
|
+
imported_module = importlib.import_module(module)
|
|
334
|
+
logger.info(f"Module {module} already installed")
|
|
335
|
+
except ImportError:
|
|
336
|
+
# Module not installed - try to install package first
|
|
337
|
+
if not package:
|
|
338
|
+
return {
|
|
339
|
+
"status": "error",
|
|
340
|
+
"content": [
|
|
341
|
+
{
|
|
342
|
+
"text": f"❌ Module {module} not found and no package specified to install.\n\n"
|
|
343
|
+
f"Please provide the 'package' parameter to install first."
|
|
344
|
+
}
|
|
345
|
+
],
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
logger.info(f"Module {module} not found, installing package {package}")
|
|
349
|
+
install_result = _install_package(package)
|
|
350
|
+
if install_result["status"] == "error":
|
|
351
|
+
return install_result
|
|
352
|
+
|
|
353
|
+
# Try importing again after installation
|
|
354
|
+
try:
|
|
355
|
+
imported_module = importlib.import_module(module)
|
|
356
|
+
except ImportError as e:
|
|
357
|
+
return {
|
|
358
|
+
"status": "error",
|
|
359
|
+
"content": [
|
|
360
|
+
{
|
|
361
|
+
"text": f"❌ Failed to import {module} even after installing {package}: {str(e)}"
|
|
362
|
+
}
|
|
363
|
+
],
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# Discover tools in the module
|
|
367
|
+
available_tools = {}
|
|
368
|
+
for attr_name in dir(imported_module):
|
|
369
|
+
attr = getattr(imported_module, attr_name)
|
|
370
|
+
# Check if it's a tool (has tool_name and tool_spec attributes)
|
|
371
|
+
if hasattr(attr, "tool_name") and hasattr(attr, "tool_spec"):
|
|
372
|
+
tool_spec = attr.tool_spec
|
|
373
|
+
description = tool_spec.get("description", "No description available")
|
|
374
|
+
available_tools[attr.tool_name] = description
|
|
375
|
+
|
|
376
|
+
if not available_tools:
|
|
377
|
+
return {
|
|
378
|
+
"status": "success",
|
|
379
|
+
"content": [{"text": f"⚠️ No tools found in module: {module}"}],
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
# Build result message
|
|
383
|
+
result_lines = [
|
|
384
|
+
f"📦 **Available Tools in {module} ({len(available_tools)})**\n"
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
for tool_name, description in sorted(available_tools.items()):
|
|
388
|
+
# Truncate long descriptions
|
|
389
|
+
if len(description) > 100:
|
|
390
|
+
description = description[:97] + "..."
|
|
391
|
+
|
|
392
|
+
result_lines.append(f"**{tool_name}**")
|
|
393
|
+
result_lines.append(f" {description}\n")
|
|
394
|
+
|
|
395
|
+
result_lines.append(f"\n💡 To load these tools, use:")
|
|
396
|
+
result_lines.append(f" install_tools(action='load', module='{module}')")
|
|
397
|
+
result_lines.append(f" # Or load specific tools:")
|
|
398
|
+
result_lines.append(
|
|
399
|
+
f" install_tools(action='load', module='{module}', tool_names=['tool1', 'tool2'])"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return {"status": "success", "content": [{"text": "\n".join(result_lines)}]}
|
|
403
|
+
|
|
404
|
+
except Exception as e:
|
|
405
|
+
logger.exception(f"Error listing available tools from {module}")
|
|
406
|
+
return {
|
|
407
|
+
"status": "error",
|
|
408
|
+
"content": [{"text": f"❌ Failed to list available tools: {str(e)}"}],
|
|
409
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devduck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: 🦆 Extreme minimalist self-adapting AI agent - one file, self-healing, runtime dependencies
|
|
5
5
|
Author-email: Cagatay Cali <cagataycali@icloud.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -53,6 +53,8 @@ Dynamic: license-file
|
|
|
53
53
|
|
|
54
54
|
One Python file that adapts to your environment, fixes itself, and expands capabilities at runtime.
|
|
55
55
|
|
|
56
|
+
Learn more: https://duck.nyc
|
|
57
|
+
|
|
56
58
|
## 🎬 See It In Action
|
|
57
59
|
|
|
58
60
|
| Feature | What You'll See | Video |
|
|
@@ -176,7 +178,7 @@ devduck("refactor my code to use async/await")
|
|
|
176
178
|
|
|
177
179
|
| Provider | Setup | When to Use |
|
|
178
180
|
|----------|-------|-------------|
|
|
179
|
-
| **Bedrock** (auto-detected) | [Get API key](https://console.aws.amazon.com/bedrock) → `export AWS_BEARER_TOKEN_BEDROCK=...` |
|
|
181
|
+
| **Bedrock** (auto-detected) | [Get API key](https://console.aws.amazon.com/bedrock) → `export AWS_BEARER_TOKEN_BEDROCK=...` | Auto-selected if credentials found |
|
|
180
182
|
| **MLX** (macOS auto-detected) | Auto-detected on Apple Silicon | Local, optimized for M-series Macs |
|
|
181
183
|
| **Ollama** (fallback) | `ollama pull qwen3:1.7b` | Local, free, private (used if Bedrock/MLX unavailable) |
|
|
182
184
|
| **Anthropic** | `export ANTHROPIC_API_KEY=...` | Claude API direct access |
|
|
@@ -304,22 +306,75 @@ No restart. No configuration. Just works.
|
|
|
304
306
|
|----------|---------|---------|
|
|
305
307
|
| `MODEL_PROVIDER` | Auto-detect | `bedrock`, `anthropic`, `github`, `mlx`, `ollama` |
|
|
306
308
|
| `STRANDS_MODEL_ID` | Auto | Model name (e.g., `qwen3:1.7b`, `claude-sonnet-4`) |
|
|
307
|
-
| `DEVDUCK_TOOLS` |
|
|
309
|
+
| `DEVDUCK_TOOLS` | 37 default tools | `package:tool1,tool2:package2:tool3` format |
|
|
310
|
+
| `DEVDUCK_LOAD_TOOLS_FROM_DIR` | `false` | `true`/`false` - Auto-load tools from `./tools/` directory |
|
|
308
311
|
| `DEVDUCK_KNOWLEDGE_BASE_ID` | - | Bedrock KB ID for auto-RAG |
|
|
309
312
|
| `DEVDUCK_TCP_PORT` | `9999` | TCP server port |
|
|
310
313
|
| `DEVDUCK_ENABLE_TCP` | `true` | Enable/disable TCP |
|
|
311
314
|
|
|
312
|
-
|
|
315
|
+
### Tool Configuration Format
|
|
316
|
+
|
|
317
|
+
**Format:** `package:tool1,tool2:package2:tool3`
|
|
318
|
+
|
|
319
|
+
**Directory Auto-Loading:**
|
|
320
|
+
|
|
321
|
+
By default, DevDuck **does not** automatically load tools from the `./tools/` directory. This gives you explicit control over which tools are loaded. To enable automatic loading of tools from `./tools/`, set:
|
|
322
|
+
|
|
313
323
|
```bash
|
|
324
|
+
export DEVDUCK_LOAD_TOOLS_FROM_DIR=true
|
|
325
|
+
devduck
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
When enabled, any `.py` file in `./tools/` with a `@tool` decorator will be loaded automatically. When disabled (default), you control tool loading via `DEVDUCK_TOOLS` or runtime `manage_tools()` calls.
|
|
329
|
+
|
|
330
|
+
**Examples:**
|
|
331
|
+
```bash
|
|
332
|
+
# Minimal (shell + editor only)
|
|
314
333
|
export DEVDUCK_TOOLS="strands_tools:shell,editor"
|
|
334
|
+
|
|
335
|
+
# Dev tools only
|
|
336
|
+
export DEVDUCK_TOOLS="strands_tools:shell,editor,file_read,file_write,calculator"
|
|
337
|
+
|
|
338
|
+
# Full DevDuck + Strands (no fun tools)
|
|
339
|
+
export DEVDUCK_TOOLS="devduck.tools:tcp,websocket,mcp_server,use_github:strands_tools:shell,editor,file_read"
|
|
340
|
+
|
|
341
|
+
# Custom package
|
|
342
|
+
export DEVDUCK_TOOLS="my_tools:custom_tool,another_tool:strands_tools:shell"
|
|
343
|
+
|
|
315
344
|
devduck
|
|
316
345
|
```
|
|
317
346
|
|
|
347
|
+
**Runtime tool management:**
|
|
348
|
+
```python
|
|
349
|
+
# List loaded tools
|
|
350
|
+
manage_tools(action="list")
|
|
351
|
+
|
|
352
|
+
# Add tools at runtime
|
|
353
|
+
manage_tools(action="add", package="strands_fun_tools", tool_names="cursor,clipboard")
|
|
354
|
+
|
|
355
|
+
# Remove tools
|
|
356
|
+
manage_tools(action="remove", tool_names="cursor,clipboard")
|
|
357
|
+
|
|
358
|
+
# Reload specific tools
|
|
359
|
+
manage_tools(action="reload", tool_names="shell,editor")
|
|
360
|
+
|
|
361
|
+
# Reload all (restart agent)
|
|
362
|
+
manage_tools(action="reload")
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Discover tools before loading:**
|
|
366
|
+
```python
|
|
367
|
+
# List available tools in a package
|
|
368
|
+
install_tools(action="list_available", package="strands-fun-tools", module="strands_fun_tools")
|
|
369
|
+
```
|
|
370
|
+
|
|
318
371
|
---
|
|
319
372
|
|
|
320
373
|
|
|
321
374
|
## MCP Integration
|
|
322
375
|
|
|
376
|
+
### Expose DevDuck as MCP Server
|
|
377
|
+
|
|
323
378
|
**Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
324
379
|
```json
|
|
325
380
|
{
|
|
@@ -334,6 +389,83 @@ devduck
|
|
|
334
389
|
|
|
335
390
|
Restart Claude → DevDuck tools appear automatically.
|
|
336
391
|
|
|
392
|
+
### Load External MCP Servers
|
|
393
|
+
|
|
394
|
+
DevDuck can act as an MCP client and load tools from external MCP servers automatically.
|
|
395
|
+
|
|
396
|
+
**Setup:**
|
|
397
|
+
```bash
|
|
398
|
+
export MCP_SERVERS='{
|
|
399
|
+
"mcpServers": {
|
|
400
|
+
"strands": {
|
|
401
|
+
"command": "uvx",
|
|
402
|
+
"args": ["strands-agents-mcp-server"]
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}'
|
|
406
|
+
devduck
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**Supported Transport Types:**
|
|
410
|
+
|
|
411
|
+
| Transport | Configuration | Example |
|
|
412
|
+
|-----------|--------------|---------|
|
|
413
|
+
| **stdio** | `command`, `args`, `env` | Executables via stdin/stdout |
|
|
414
|
+
| **HTTP** | `url`, `headers` | Remote servers via HTTP |
|
|
415
|
+
| **SSE** | `url` (with `/sse` path) | Server-Sent Events streaming |
|
|
416
|
+
|
|
417
|
+
**Examples:**
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
# Stdio server
|
|
421
|
+
export MCP_SERVERS='{
|
|
422
|
+
"mcpServers": {
|
|
423
|
+
"myserver": {
|
|
424
|
+
"command": "python",
|
|
425
|
+
"args": ["server.py"],
|
|
426
|
+
"env": {"API_KEY": "secret"}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}'
|
|
430
|
+
|
|
431
|
+
# HTTP server
|
|
432
|
+
export MCP_SERVERS='{
|
|
433
|
+
"mcpServers": {
|
|
434
|
+
"remote": {
|
|
435
|
+
"url": "https://api.example.com/mcp",
|
|
436
|
+
"headers": {"Authorization": "Bearer token"}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}'
|
|
440
|
+
|
|
441
|
+
# SSE server
|
|
442
|
+
export MCP_SERVERS='{
|
|
443
|
+
"mcpServers": {
|
|
444
|
+
"events": {
|
|
445
|
+
"url": "https://api.example.com/sse"
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}'
|
|
449
|
+
|
|
450
|
+
# Multiple servers
|
|
451
|
+
export MCP_SERVERS='{
|
|
452
|
+
"mcpServers": {
|
|
453
|
+
"strands": {
|
|
454
|
+
"command": "uvx",
|
|
455
|
+
"args": ["strands-agents-mcp-server"]
|
|
456
|
+
},
|
|
457
|
+
"remote": {
|
|
458
|
+
"url": "https://api.example.com/mcp"
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}'
|
|
462
|
+
|
|
463
|
+
devduck
|
|
464
|
+
# Tools from all MCP servers automatically available
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Tool Prefixing:** Each MCP server's tools are prefixed with the server name (e.g., `strands_tool_name`)
|
|
468
|
+
|
|
337
469
|
---
|
|
338
470
|
|
|
339
471
|
## Troubleshooting
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
devduck/__init__.py,sha256=
|
|
1
|
+
devduck/__init__.py,sha256=okf01FoPFrPpiwuGdQcx9Y7kLndn22JuVkA117L6g-4,67753
|
|
2
2
|
devduck/__main__.py,sha256=aeF2RR4k7lzSR2X1QKV9XQPCKhtsH0JYUv2etBBqmL0,145
|
|
3
|
-
devduck/_version.py,sha256=
|
|
3
|
+
devduck/_version.py,sha256=uLbRjFSUZAgfl7V7O8zKV5Db36k7tz87ZIVq3l2SWs0,704
|
|
4
4
|
devduck/agentcore_handler.py,sha256=0DKJTTjoH9P8a70G0f5dOIIwy6bjqaN46voAWaSOpDY,2221
|
|
5
5
|
devduck/test_redduck.py,sha256=ILtKKMuoyVfmhnibmbojpbOsqbcKooZv4j9qtE2LWdw,1750
|
|
6
6
|
devduck/tools/__init__.py,sha256=AmIy8MInaClaZ71fqzy4EQJnBWsLkrv4QW9IIN7UQyw,1367
|
|
@@ -8,11 +8,11 @@ devduck/tools/_ambient_input.py,sha256=3lBgLO81BvkxjgTrQc-EuxNLXmO1oPUt2Ysg1jR4F
|
|
|
8
8
|
devduck/tools/_tray_app.py,sha256=E4rtJcegRsBs_FdQVGdA-0Ax7uxVb6AbuyqjwCArHj0,19405
|
|
9
9
|
devduck/tools/agentcore_agents.py,sha256=fiDNhl7R2tVbp1mEOySJTfGXwap5q3COenYOjiJDE_g,6488
|
|
10
10
|
devduck/tools/agentcore_config.py,sha256=sUD1SrLAqTHjgHctZtVRDz_BvLG_nRB3z6g3EcrcvTM,14780
|
|
11
|
-
devduck/tools/agentcore_invoke.py,sha256=
|
|
11
|
+
devduck/tools/agentcore_invoke.py,sha256=iHOeV8mh1QeJ_dj06MRHbJ1VnvY4WIQxqruxIDw0mz0,17469
|
|
12
12
|
devduck/tools/agentcore_logs.py,sha256=A3YQIoRErJtvzeaMSPNqOLX1BH-vYTbYKs1NXoCnC5E,10222
|
|
13
13
|
devduck/tools/ambient.py,sha256=HB1ZhfeOdOaMU0xe4e44VNUT_-DQ5SY7sl3r4r-4X44,4806
|
|
14
14
|
devduck/tools/create_subagent.py,sha256=UzRz9BmU4PbTveZROEpZ311aH-u-i6x89gttu-CniAE,24687
|
|
15
|
-
devduck/tools/install_tools.py,sha256=
|
|
15
|
+
devduck/tools/install_tools.py,sha256=3uzRg5lEHX-L6gxnFn3mIKjGYDJ3h_AdwGnEwKA9qR0,14284
|
|
16
16
|
devduck/tools/ipc.py,sha256=e3KJeR2HmCKEtVLGNOtf6CeFi3pTDehwd7Fu4JJ19Ms,18607
|
|
17
17
|
devduck/tools/mcp_server.py,sha256=Ybp0PcJKW2TOvghsRL-i8Guqc9WokPwOD2bhVgzoj6Q,21490
|
|
18
18
|
devduck/tools/state_manager.py,sha256=hrleqdVoCboNd8R3wDRUXVKYCZdGoe1j925i948LTHc,10563
|
|
@@ -22,9 +22,9 @@ devduck/tools/tcp.py,sha256=w2m_Jf6vZ4NYu0AwgZd7C7eKs4No2EVHZ2WYIl_Bt0A,22017
|
|
|
22
22
|
devduck/tools/tray.py,sha256=FgVhUtLdsdv5_ERK-RyAIpDE8Zb0IfoqhHQdwMxrHUQ,7547
|
|
23
23
|
devduck/tools/use_github.py,sha256=nr3JSGk48mKUobpgW__2gu6lFyUj93a1XRs3I6vH8W4,13682
|
|
24
24
|
devduck/tools/websocket.py,sha256=A8bqgdDZs8hcf2HctkJzQOzMvb5mXUC7YZ-xqkOyn94,16959
|
|
25
|
-
devduck-0.
|
|
26
|
-
devduck-0.
|
|
27
|
-
devduck-0.
|
|
28
|
-
devduck-0.
|
|
29
|
-
devduck-0.
|
|
30
|
-
devduck-0.
|
|
25
|
+
devduck-0.7.0.dist-info/licenses/LICENSE,sha256=UANcoWwfVeuM9597WUkjEQbzqIUH0bJoE9Tpwgj_LvU,11345
|
|
26
|
+
devduck-0.7.0.dist-info/METADATA,sha256=I7la6VIQ76TqoZkaJ-GbY09Mt5duZ_rMGtW8D1lD-OM,17748
|
|
27
|
+
devduck-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
28
|
+
devduck-0.7.0.dist-info/entry_points.txt,sha256=BAMQaIg_BLZQOTk12bT7hy1dE9oGPLt-_dTbI4cnBnQ,40
|
|
29
|
+
devduck-0.7.0.dist-info/top_level.txt,sha256=ySXWlVronp8xHYfQ_Hdfr463e0EnbWuqyuxs94EU7yk,8
|
|
30
|
+
devduck-0.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|