tinyagent-py 0.0.17rc1__tar.gz → 0.0.19__tar.gz

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.
Files changed (43) hide show
  1. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/PKG-INFO +2 -2
  2. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/pyproject.toml +2 -2
  3. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/tiny_code_agent.py +474 -298
  4. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/mcp_client.py +27 -3
  5. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/tiny_agent.py +30 -4
  6. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent_py.egg-info/PKG-INFO +2 -2
  7. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/LICENSE +0 -0
  8. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/README.md +0 -0
  9. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/setup.cfg +0 -0
  10. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/__init__.py +0 -0
  11. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/__init__.py +0 -0
  12. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/example.py +0 -0
  13. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/helper.py +0 -0
  14. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/modal_sandbox.py +0 -0
  15. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/providers/__init__.py +0 -0
  16. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/providers/base.py +0 -0
  17. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/providers/modal_provider.py +0 -0
  18. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/providers/seatbelt_provider.py +0 -0
  19. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/safety.py +0 -0
  20. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/tools/__init__.py +0 -0
  21. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/tools/example_tools.py +0 -0
  22. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/code_agent/utils.py +0 -0
  23. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/hooks/__init__.py +0 -0
  24. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/hooks/gradio_callback.py +0 -0
  25. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/hooks/jupyter_notebook_callback.py +0 -0
  26. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/hooks/logging_manager.py +0 -0
  27. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/hooks/rich_code_ui_callback.py +0 -0
  28. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/hooks/rich_ui_callback.py +0 -0
  29. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/hooks/token_tracker.py +0 -0
  30. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/memory_manager.py +0 -0
  31. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/prompts/code_agent.yaml +0 -0
  32. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/prompts/summarize.yaml +0 -0
  33. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/prompts/truncation.yaml +0 -0
  34. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/storage/__init__.py +0 -0
  35. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/storage/base.py +0 -0
  36. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/storage/json_file_storage.py +0 -0
  37. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/storage/postgres_storage.py +0 -0
  38. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/storage/redis_storage.py +0 -0
  39. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent/storage/sqlite_storage.py +0 -0
  40. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent_py.egg-info/SOURCES.txt +0 -0
  41. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent_py.egg-info/dependency_links.txt +0 -0
  42. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent_py.egg-info/requires.txt +0 -0
  43. {tinyagent_py-0.0.17rc1 → tinyagent_py-0.0.19}/tinyagent_py.egg-info/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinyagent-py
3
- Version: 0.0.17rc1
4
- Summary: TinyAgent with MCP Client, Code Agent (Thinking, Planning, and Executing in Python), and Extendable Hooks, Tiny but powerful
3
+ Version: 0.0.19
4
+ Summary: TinyAgent with MCP Client, CodeAgent (Thinking, Planning, Interactive Python and Shell with high variaety of sandboxing(seatbelt, Modal, E2B, docker, etc) ), and Extendable Hooks, Tiny but powerful
5
5
  Author-email: Mahdi Golchin <golchin@askdev.ai>
6
6
  Project-URL: Homepage, https://github.com/askbudi/tinyagent
7
7
  Project-URL: Bug Tracker, https://github.com/askbudi/tinyagent/issues
@@ -12,8 +12,8 @@ tinyagent = ["prompts/*.yaml"]
12
12
 
13
13
  [project]
14
14
  name = "tinyagent-py"
15
- version = "0.0.17rc1"
16
- description = "TinyAgent with MCP Client, Code Agent (Thinking, Planning, and Executing in Python), and Extendable Hooks, Tiny but powerful"
15
+ version = "0.0.19"
16
+ description = "TinyAgent with MCP Client, CodeAgent (Thinking, Planning, Interactive Python and Shell with high variaety of sandboxing(seatbelt, Modal, E2B, docker, etc) ), and Extendable Hooks, Tiny but powerful"
17
17
  readme = "README.md"
18
18
  authors = [
19
19
  {name="Mahdi Golchin", email="golchin@askdev.ai"}
@@ -31,7 +31,7 @@ DEFAULT_SUMMARY_SYSTEM_PROMPT = (
31
31
 
32
32
  )
33
33
 
34
- class TinyCodeAgent:
34
+ class TinyCodeAgent(TinyAgent):
35
35
  """
36
36
  A TinyAgent specialized for code execution tasks.
37
37
 
@@ -69,6 +69,8 @@ class TinyCodeAgent:
69
69
  ui: Optional[str] = None,
70
70
  truncation_config: Optional[Dict[str, Any]] = None,
71
71
  auto_git_checkpoint: bool = False,
72
+ enable_python_tool: bool = True,
73
+ enable_shell_tool: bool = True,
72
74
  **agent_kwargs
73
75
  ):
74
76
  """
@@ -95,6 +97,8 @@ class TinyCodeAgent:
95
97
  ui: The user interface callback to use ('rich', 'jupyter', or None).
96
98
  truncation_config: Configuration for output truncation (max_tokens, max_lines)
97
99
  auto_git_checkpoint: If True, automatically create git checkpoints after each successful shell command
100
+ enable_python_tool: If True (default), enable the run_python tool for Python code execution
101
+ enable_shell_tool: If True (default), enable the bash tool for shell command execution
98
102
  **agent_kwargs: Additional arguments passed to TinyAgent
99
103
 
100
104
  Provider Config Options:
@@ -136,6 +140,10 @@ class TinyCodeAgent:
136
140
  self.default_workdir = default_workdir or os.getcwd() # Default to current working directory if not specified
137
141
  self.auto_git_checkpoint = auto_git_checkpoint # Enable/disable automatic git checkpoints
138
142
 
143
+ # Store tool enablement flags
144
+ self._python_tool_enabled = enable_python_tool
145
+ self._shell_tool_enabled = enable_shell_tool
146
+
139
147
  # Set up truncation configuration with defaults
140
148
  default_truncation = {
141
149
  "max_tokens": 3000,
@@ -158,8 +166,8 @@ class TinyCodeAgent:
158
166
 
159
167
  self.summary_config = summary_config or {}
160
168
 
161
- # Create the underlying TinyAgent with summary configuration
162
- self.agent = TinyAgent(
169
+ # Initialize the parent TinyAgent with the built system prompt
170
+ super().__init__(
163
171
  model=model,
164
172
  api_key=api_key,
165
173
  system_prompt=self.system_prompt,
@@ -173,7 +181,7 @@ class TinyCodeAgent:
173
181
 
174
182
  # Add LLM tools (not code tools - those go to the provider)
175
183
  if self.tools:
176
- self.agent.add_tools(self.tools)
184
+ self.add_tools(self.tools)
177
185
 
178
186
  # Add the selected UI callback
179
187
  if ui:
@@ -394,221 +402,237 @@ class TinyCodeAgent:
394
402
 
395
403
  def _setup_code_execution_tools(self):
396
404
  """Set up the code execution tools using the code provider."""
397
- @tool(name="run_python", description=dedent("""
398
- This tool receives Python code and executes it in a sandboxed environment.
399
- During each intermediate step, you can use 'print()' to save important information.
400
- These print outputs will appear in the 'Observation:' field for the next step.
405
+ # Clear existing default tools to avoid duplicates
406
+ # Remove existing default tools by name if they exist
407
+ if hasattr(self, 'available_tools'):
408
+ tools_to_remove = []
409
+ for tool_dict in self.available_tools:
410
+ if 'function' in tool_dict and 'name' in tool_dict['function']:
411
+ if tool_dict['function']['name'] in ['run_python', 'bash']:
412
+ tools_to_remove.append(tool_dict)
413
+
414
+ # Remove the tools from available_tools
415
+ for tool_dict in tools_to_remove:
416
+ self.available_tools.remove(tool_dict)
417
+
418
+ if self._python_tool_enabled:
419
+ @tool(name="run_python", description=dedent("""
420
+ This tool receives Python code and executes it in a sandboxed environment.
421
+ During each intermediate step, you can use 'print()' to save important information.
422
+ These print outputs will appear in the 'Observation:' field for the next step.
401
423
 
402
- Args:
403
- code_lines: list[str]: The Python code to execute as a list of strings.
404
- Your code should include all necessary steps for successful execution,
405
- cover edge cases, and include error handling.
406
- Each line should be an independent line of code.
424
+ Args:
425
+ code_lines: list[str]: The Python code to execute as a list of strings.
426
+ Your code should include all necessary steps for successful execution,
427
+ cover edge cases, and include error handling.
428
+ Each line should be an independent line of code.
407
429
 
408
- Returns:
409
- Status of code execution or error message.
410
- """))
411
- async def run_python(code_lines: List[str], timeout: int = 120) -> str:
412
- """Execute Python code using the configured provider."""
413
- try:
414
- # Before execution, ensure provider has the latest user variables
415
- if self.user_variables:
416
- self.code_provider.set_user_variables(self.user_variables)
430
+ Returns:
431
+ Status of code execution or error message.
432
+ """))
433
+ async def run_python(code_lines: List[str], timeout: int = 120) -> str:
434
+ """Execute Python code using the configured provider."""
435
+ try:
436
+ # Before execution, ensure provider has the latest user variables
437
+ if self.user_variables:
438
+ self.code_provider.set_user_variables(self.user_variables)
439
+
440
+ result = await self.code_provider.execute_python(code_lines, timeout)
417
441
 
418
- result = await self.code_provider.execute_python(code_lines, timeout)
419
-
420
- # After execution, update TinyCodeAgent's user_variables from the provider
421
- # This ensures they stay in sync
422
- self.user_variables = self.code_provider.get_user_variables()
423
-
424
- # Apply truncation if enabled
425
- if self.truncation_config["enabled"] and "printed_output" in result:
426
- truncated_output, is_truncated, original_tokens, original_lines = truncate_output(
427
- result["printed_output"],
428
- max_tokens=self.truncation_config["max_tokens"],
429
- max_lines=self.truncation_config["max_lines"]
430
- )
442
+ # After execution, update TinyCodeAgent's user_variables from the provider
443
+ # This ensures they stay in sync
444
+ self.user_variables = self.code_provider.get_user_variables()
431
445
 
432
- if is_truncated:
433
- result["printed_output"] = format_truncation_message(
434
- truncated_output,
435
- is_truncated,
436
- original_tokens,
437
- original_lines,
438
- self.truncation_config["max_lines"],
439
- "python_output"
446
+ # Apply truncation if enabled
447
+ if self.truncation_config["enabled"] and "printed_output" in result:
448
+ truncated_output, is_truncated, original_tokens, original_lines = truncate_output(
449
+ result["printed_output"],
450
+ max_tokens=self.truncation_config["max_tokens"],
451
+ max_lines=self.truncation_config["max_lines"]
440
452
  )
453
+
454
+ if is_truncated:
455
+ result["printed_output"] = format_truncation_message(
456
+ truncated_output,
457
+ is_truncated,
458
+ original_tokens,
459
+ original_lines,
460
+ self.truncation_config["max_lines"],
461
+ "python_output"
462
+ )
463
+
464
+ return json.dumps(result)
465
+ except Exception as e:
466
+ print("!"*100)
467
+ COLOR = {
468
+ "RED": "\033[91m",
469
+ "ENDC": "\033[0m",
470
+ }
471
+ print(f"{COLOR['RED']}{str(e)}{COLOR['ENDC']}")
472
+ print(f"{COLOR['RED']}{traceback.format_exc()}{COLOR['ENDC']}")
473
+ print("!"*100)
474
+
475
+ # Even after an exception, update user_variables from the provider
476
+ # This ensures any variables that were successfully created/modified are preserved
477
+ self.user_variables = self.code_provider.get_user_variables()
478
+
479
+ return json.dumps({"error": f"Error executing code: {str(e)}"})
480
+
481
+ self.add_tool(run_python)
482
+
483
+ if self._shell_tool_enabled:
484
+ @tool(name="bash", description=dedent("""
485
+ This tool executes shell commands securely in a sandboxed environment.
486
+ Only a limited set of safe commands are allowed for security reasons.
487
+ Before executing the command, please follow these steps:
488
+
489
+ 1. Directory Verification:
490
+ - If the command will create new directories or files, first use ls to verify the parent directory exists and is the correct location
491
+ - For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory
492
+
493
+ 2. Command Execution:
494
+ - Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
495
+ - Examples of proper quoting:
496
+ - cd "/Users/name/My Documents" (correct)
497
+ - cd /Users/name/My Documents (incorrect - will fail)
498
+ - python "/path/with spaces/script.py" (correct)
499
+ - python /path/with spaces/script.py (incorrect - will fail)
500
+ - After ensuring proper quoting, execute the command.
501
+ - Capture the output of the command.
502
+
503
+ Usage notes:
504
+ - The command argument is required.
505
+ - You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance.
506
+ - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).
507
+ - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
508
+ - If the output is too large, it will be truncated before being returned to you.
441
509
 
442
- return json.dumps(result)
443
- except Exception as e:
444
- print("!"*100)
445
- COLOR = {
446
- "RED": "\033[91m",
447
- "ENDC": "\033[0m",
448
- }
449
- print(f"{COLOR['RED']}{str(e)}{COLOR['ENDC']}")
450
- print(f"{COLOR['RED']}{traceback.format_exc()}{COLOR['ENDC']}")
451
- print("!"*100)
452
-
453
- # Even after an exception, update user_variables from the provider
454
- # This ensures any variables that were successfully created/modified are preserved
455
- self.user_variables = self.code_provider.get_user_variables()
456
-
457
- return json.dumps({"error": f"Error executing code: {str(e)}"})
458
-
459
- @tool(name="bash", description=dedent("""
460
- This tool executes shell commands securely in a sandboxed environment.
461
- Only a limited set of safe commands are allowed for security reasons.
462
- Before executing the command, please follow these steps:
463
-
464
- 1. Directory Verification:
465
- - If the command will create new directories or files, first use ls to verify the parent directory exists and is the correct location
466
- - For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory
467
-
468
- 2. Command Execution:
469
- - Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
470
- - Examples of proper quoting:
471
- - cd "/Users/name/My Documents" (correct)
472
- - cd /Users/name/My Documents (incorrect - will fail)
473
- - python "/path/with spaces/script.py" (correct)
474
- - python /path/with spaces/script.py (incorrect - will fail)
475
- - After ensuring proper quoting, execute the command.
476
- - Capture the output of the command.
477
-
478
- Usage notes:
479
- - The command argument is required.
480
- - You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance.
481
- - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).
482
- - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
483
- - If the output is too large, it will be truncated before being returned to you.
510
+ - If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` first, which all ${PRODUCT_NAME} users have pre-installed.
511
+ " - When issuing multiple commands, use the ; or && operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).\n" +
512
+ - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
513
+ <good-example>
514
+ pytest /foo/bar/tests
515
+ </good-example>
516
+ <bad-example>
517
+ cd /foo/bar && pytest tests
518
+ </bad-example>
484
519
 
485
- - If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` first, which all ${PRODUCT_NAME} users have pre-installed.
486
- " - When issuing multiple commands, use the ; or && operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).\n" +
487
- - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
488
- <good-example>
489
- pytest /foo/bar/tests
490
- </good-example>
491
- <bad-example>
492
- cd /foo/bar && pytest tests
493
- </bad-example>
494
-
495
- ## IMPORTANT: Bash Tool Usage
496
-
497
- When using the bash tool, you MUST provide all required parameters:
498
-
499
- **Correct Usage:**
500
- ```
501
- bash(
502
- command=["ls", "-la"],
503
- absolute_workdir="/path/to/directory",
504
- description="List files in directory"
505
- )
506
- ```
507
-
508
- **For creating files with content, use these safe patterns:**
509
-
510
- 1. **Simple file creation:**
511
- ```
512
- bash(
513
- command=["touch", "filename.txt"],
514
- absolute_workdir="/working/directory",
515
- description="Create empty file"
516
- )
517
- ```
518
-
519
- 2. **Write content using cat and heredoc:**
520
- ```
521
- bash(
522
- command=["sh", "-c", "cat > filename.txt << 'EOF'\nYour content here\nEOF"],
523
- absolute_workdir="/working/directory",
524
- description="Create file with content"
525
- )
526
- ```
527
-
528
- 3. **Write content using echo:**
529
- ```
530
- bash(
531
- command=["sh", "-c", "echo 'Your content' > filename.txt"],
532
- absolute_workdir="/working/directory",
533
- description="Write content to file"
534
- )
535
- ```
536
-
537
- **Never:**
538
- - Call bash() without all required parameters
539
- - Use complex nested quotes without testing
540
- - Try to create large files in a single command (break into parts)
520
+ ## IMPORTANT: Bash Tool Usage
521
+
522
+ When using the bash tool, you MUST provide all required parameters:
523
+
524
+ **Correct Usage:**
525
+ ```
526
+ bash(
527
+ command=["ls", "-la"],
528
+ absolute_workdir="/path/to/directory",
529
+ description="List files in directory"
530
+ )
531
+ ```
532
+
533
+ **For creating files with content, use these safe patterns:**
534
+
535
+ 1. **Simple file creation:**
536
+ ```
537
+ bash(
538
+ command=["touch", "filename.txt"],
539
+ absolute_workdir="/working/directory",
540
+ description="Create empty file"
541
+ )
542
+ ```
543
+
544
+ 2. **Write content using cat and heredoc:**
545
+ ```
546
+ bash(
547
+ command=["sh", "-c", "cat > filename.txt << 'EOF'\nYour content here\nEOF"],
548
+ absolute_workdir="/working/directory",
549
+ description="Create file with content"
550
+ )
551
+ ```
552
+
553
+ 3. **Write content using echo:**
554
+ ```
555
+ bash(
556
+ command=["sh", "-c", "echo 'Your content' > filename.txt"],
557
+ absolute_workdir="/working/directory",
558
+ description="Write content to file"
559
+ )
560
+ ```
561
+
562
+ **Never:**
563
+ - Call bash() without all required parameters
564
+ - Use complex nested quotes without testing
565
+ - Try to create large files in a single command (break into parts)
541
566
 
542
- Args:
543
- command: list[str]: The shell command to execute as a list of strings. Example: ["ls", "-la"] or ["cat", "file.txt"]
544
-
545
- absolute_workdir: str: could be presented workdir in the system prompt or one of the subdirectories of the workdir. This is the only allowed path, and accessing else will result in an error.
546
- description: str: A clear, concise description of what this command does in 5-10 words.
547
- timeout: int: Maximum execution time in seconds (default: 60).
548
- Returns:
549
- Dictionary with stdout, stderr, and exit_code from the command execution.
550
- If the command is rejected for security reasons, stderr will contain the reason.
551
- The stdout will include information about which working directory was used.
552
- """))
553
- async def run_shell(command: List[str], absolute_workdir: str, description: str, timeout: int = 60) -> str:
554
- """Execute shell commands securely using the configured provider."""
555
- try:
556
- # Use the default working directory if none is specified
557
- effective_workdir = absolute_workdir or self.default_workdir
558
- print(f" {command} to {description}")
559
- # Verify that the working directory exists
560
- if effective_workdir and not os.path.exists(effective_workdir):
561
- return json.dumps({
562
- "stdout": "",
563
- "stderr": f"Working directory does not exist: {effective_workdir}",
564
- "exit_code": 1
565
- })
566
-
567
- if effective_workdir and not os.path.isdir(effective_workdir):
568
- return json.dumps({
569
- "stdout": "",
570
- "stderr": f"Path is not a directory: {effective_workdir}",
571
- "exit_code": 1
572
- })
573
-
574
- result = await self.code_provider.execute_shell(command, timeout, effective_workdir)
575
-
576
- # Apply truncation if enabled
577
- if self.truncation_config["enabled"] and "stdout" in result and result["stdout"]:
578
- truncated_output, is_truncated, original_tokens, original_lines = truncate_output(
579
- result["stdout"],
580
- max_tokens=self.truncation_config["max_tokens"],
581
- max_lines=self.truncation_config["max_lines"]
582
- )
567
+ Args:
568
+ command: list[str]: The shell command to execute as a list of strings. Example: ["ls", "-la"] or ["cat", "file.txt"]
583
569
 
584
- if is_truncated:
585
- result["stdout"] = format_truncation_message(
586
- truncated_output,
587
- is_truncated,
588
- original_tokens,
589
- original_lines,
590
- self.truncation_config["max_lines"],
591
- "bash_output"
570
+ absolute_workdir: str: could be presented workdir in the system prompt or one of the subdirectories of the workdir. This is the only allowed path, and accessing else will result in an error.
571
+ description: str: A clear, concise description of what this command does in 5-10 words.
572
+ timeout: int: Maximum execution time in seconds (default: 60).
573
+ Returns:
574
+ Dictionary with stdout, stderr, and exit_code from the command execution.
575
+ If the command is rejected for security reasons, stderr will contain the reason.
576
+ The stdout will include information about which working directory was used.
577
+ """))
578
+ async def run_shell(command: List[str], absolute_workdir: str, description: str, timeout: int = 60) -> str:
579
+ """Execute shell commands securely using the configured provider."""
580
+ try:
581
+ # Use the default working directory if none is specified
582
+ effective_workdir = absolute_workdir or self.default_workdir
583
+ print(f" {command} to {description}")
584
+ # Verify that the working directory exists
585
+ if effective_workdir and not os.path.exists(effective_workdir):
586
+ return json.dumps({
587
+ "stdout": "",
588
+ "stderr": f"Working directory does not exist: {effective_workdir}",
589
+ "exit_code": 1
590
+ })
591
+
592
+ if effective_workdir and not os.path.isdir(effective_workdir):
593
+ return json.dumps({
594
+ "stdout": "",
595
+ "stderr": f"Path is not a directory: {effective_workdir}",
596
+ "exit_code": 1
597
+ })
598
+
599
+ result = await self.code_provider.execute_shell(command, timeout, effective_workdir)
600
+
601
+ # Apply truncation if enabled
602
+ if self.truncation_config["enabled"] and "stdout" in result and result["stdout"]:
603
+ truncated_output, is_truncated, original_tokens, original_lines = truncate_output(
604
+ result["stdout"],
605
+ max_tokens=self.truncation_config["max_tokens"],
606
+ max_lines=self.truncation_config["max_lines"]
592
607
  )
593
-
594
- # Create a git checkpoint if auto_git_checkpoint is enabled
595
- if self.auto_git_checkpoint and result.get("exit_code", 1) == 0:
596
- checkpoint_result = await self._create_git_checkpoint(command, description, effective_workdir)
597
- self.log_manager.get_logger(__name__).info(f"Git checkpoint {effective_workdir} result: {checkpoint_result}")
598
-
599
- return json.dumps(result)
600
- except Exception as e:
601
- COLOR = {
602
- "RED": "\033[91m",
603
- "ENDC": "\033[0m",
604
- }
605
- print(f"{COLOR['RED']}{str(e)}{COLOR['ENDC']}")
606
- print(f"{COLOR['RED']}{traceback.format_exc()}{COLOR['ENDC']}")
607
-
608
- return json.dumps({"error": f"Error executing shell command: {str(e)}"})
609
-
610
- self.agent.add_tool(run_python)
611
- self.agent.add_tool(run_shell)
608
+
609
+ if is_truncated:
610
+ result["stdout"] = format_truncation_message(
611
+ truncated_output,
612
+ is_truncated,
613
+ original_tokens,
614
+ original_lines,
615
+ self.truncation_config["max_lines"],
616
+ "bash_output"
617
+ )
618
+
619
+ # Create a git checkpoint if auto_git_checkpoint is enabled
620
+ if self.auto_git_checkpoint and result.get("exit_code", 1) == 0:
621
+ checkpoint_result = await self._create_git_checkpoint(command, description, effective_workdir)
622
+ self.log_manager.get_logger(__name__).info(f"Git checkpoint {effective_workdir} result: {checkpoint_result}")
623
+
624
+ return json.dumps(result)
625
+ except Exception as e:
626
+ COLOR = {
627
+ "RED": "\033[91m",
628
+ "ENDC": "\033[0m",
629
+ }
630
+ print(f"{COLOR['RED']}{str(e)}{COLOR['ENDC']}")
631
+ print(f"{COLOR['RED']}{traceback.format_exc()}{COLOR['ENDC']}")
632
+
633
+ return json.dumps({"error": f"Error executing shell command: {str(e)}"})
634
+
635
+ self.add_tool(run_shell)
612
636
 
613
637
  async def _create_git_checkpoint(self, command: List[str], description: str, workdir: str) -> Dict[str, Any]:
614
638
  """
@@ -706,50 +730,17 @@ class TinyCodeAgent:
706
730
  """
707
731
  return self.default_workdir
708
732
 
709
- async def run(self, user_input: str, max_turns: int = 10) -> str:
710
- """
711
- Run the code agent with the given input.
712
-
713
- Args:
714
- user_input: The user's request or question
715
- max_turns: Maximum number of conversation turns
716
-
717
- Returns:
718
- The agent's response
719
- """
720
- return await self.agent.run(user_input, max_turns)
733
+
721
734
 
722
- async def resume(self, max_turns: int = 10) -> str:
723
- """
724
- Resume the conversation without adding a new user message.
725
-
726
- This method continues the conversation from the current state,
727
- allowing the agent to process the existing conversation history
728
- and potentially take additional actions.
729
-
730
- Args:
731
- max_turns: Maximum number of conversation turns
732
-
733
- Returns:
734
- The agent's response
735
- """
736
- return await self.agent.resume(max_turns)
735
+
737
736
 
738
- async def connect_to_server(self, command: str, args: List[str], **kwargs):
739
- """Connect to an MCP server."""
740
- return await self.agent.connect_to_server(command, args, **kwargs)
737
+
741
738
 
742
- def add_callback(self, callback):
743
- """Add a callback to the agent."""
744
- self.agent.add_callback(callback)
739
+
745
740
 
746
- def add_tool(self, tool):
747
- """Add a tool to the agent (LLM tool)."""
748
- self.agent.add_tool(tool)
741
+
749
742
 
750
- def add_tools(self, tools: List[Any]):
751
- """Add multiple tools to the agent (LLM tools)."""
752
- self.agent.add_tools(tools)
743
+
753
744
 
754
745
  def add_code_tool(self, tool):
755
746
  """
@@ -763,8 +754,8 @@ class TinyCodeAgent:
763
754
  self.code_provider.set_code_tools(self.code_tools)
764
755
  # Rebuild system prompt to include new code tools info
765
756
  self.system_prompt = self._build_system_prompt()
766
- # Update the agent's system prompt
767
- self.agent.system_prompt = self.system_prompt
757
+ # Update the system prompt in messages
758
+ self._update_system_prompt()
768
759
 
769
760
  def add_code_tools(self, tools: List[Any]):
770
761
  """
@@ -778,8 +769,8 @@ class TinyCodeAgent:
778
769
  self.code_provider.set_code_tools(self.code_tools)
779
770
  # Rebuild system prompt to include new code tools info
780
771
  self.system_prompt = self._build_system_prompt()
781
- # Update the agent's system prompt
782
- self.agent.system_prompt = self.system_prompt
772
+ # Update the system prompt in messages
773
+ self._update_system_prompt()
783
774
 
784
775
  def remove_code_tool(self, tool_name: str):
785
776
  """
@@ -795,8 +786,8 @@ class TinyCodeAgent:
795
786
  self.code_provider.set_code_tools(self.code_tools)
796
787
  # Rebuild system prompt
797
788
  self.system_prompt = self._build_system_prompt()
798
- # Update the agent's system prompt
799
- self.agent.system_prompt = self.system_prompt
789
+ # Update the system prompt in messages
790
+ self._update_system_prompt()
800
791
 
801
792
  def get_code_tools(self) -> List[Any]:
802
793
  """
@@ -827,8 +818,8 @@ class TinyCodeAgent:
827
818
  self.code_provider.set_user_variables(self.user_variables)
828
819
  # Rebuild system prompt to include new variables info
829
820
  self.system_prompt = self._build_system_prompt()
830
- # Update the agent's system prompt
831
- self.agent.system_prompt = self.system_prompt
821
+ # Update the system prompt in messages
822
+ self._update_system_prompt()
832
823
 
833
824
  def add_user_variable(self, name: str, value: Any):
834
825
  """
@@ -843,7 +834,7 @@ class TinyCodeAgent:
843
834
  # Rebuild system prompt to include new variables info
844
835
  self.system_prompt = self._build_system_prompt()
845
836
  # Update the agent's system prompt
846
- self.agent.system_prompt = self.system_prompt
837
+ self._update_system_prompt()
847
838
 
848
839
  def remove_user_variable(self, name: str):
849
840
  """
@@ -858,7 +849,7 @@ class TinyCodeAgent:
858
849
  # Rebuild system prompt
859
850
  self.system_prompt = self._build_system_prompt()
860
851
  # Update the agent's system prompt
861
- self.agent.system_prompt = self.system_prompt
852
+ self._update_system_prompt()
862
853
 
863
854
  def get_user_variables(self) -> Dict[str, Any]:
864
855
  """
@@ -926,7 +917,7 @@ class TinyCodeAgent:
926
917
  # Rebuild system prompt to include new authorized imports
927
918
  self.system_prompt = self._build_system_prompt()
928
919
  # Update the agent's system prompt
929
- self.agent.system_prompt = self.system_prompt
920
+ self._update_system_prompt()
930
921
 
931
922
  def get_authorized_imports(self) -> List[str]:
932
923
  """
@@ -973,27 +964,22 @@ class TinyCodeAgent:
973
964
  # Rebuild system prompt to reflect updated authorized imports
974
965
  self.system_prompt = self._build_system_prompt()
975
966
  # Update the agent's system prompt
976
- self.agent.system_prompt = self.system_prompt
967
+ self._update_system_prompt()
977
968
 
978
969
  async def close(self):
979
970
  """Clean up resources."""
980
971
  await self.code_provider.cleanup()
981
- await self.agent.close()
972
+ await super().close()
982
973
 
983
- def clear_conversation(self):
984
- """Clear the conversation history."""
985
- self.agent.clear_conversation()
986
-
987
- @property
988
- def messages(self):
989
- """Get the conversation messages."""
990
- return self.agent.messages
974
+
991
975
 
992
- @property
993
- def session_id(self):
994
- """Get the session ID."""
995
- return self.agent.session_id
976
+
996
977
 
978
+ def _update_system_prompt(self):
979
+ """Update the system prompt in the messages array."""
980
+ if self.messages and len(self.messages) > 0:
981
+ self.messages[0]["content"] = self.system_prompt
982
+
997
983
  def set_check_string_obfuscation(self, enabled: bool):
998
984
  """
999
985
  Enable or disable string obfuscation detection.
@@ -1008,32 +994,9 @@ class TinyCodeAgent:
1008
994
  if hasattr(self.code_provider, 'check_string_obfuscation'):
1009
995
  self.code_provider.check_string_obfuscation = enabled
1010
996
 
1011
- async def summarize(self) -> str:
1012
- """
1013
- Generate a summary of the current conversation history.
1014
-
1015
- Args:
1016
- Returns:
1017
- A string containing the conversation summary
1018
- """
1019
- # Use the underlying TinyAgent's summarize_conversation method
1020
- return await self.agent.summarize()
1021
-
1022
- async def compact(self) -> bool:
1023
- """
1024
- Compact the conversation history by replacing it with a summary.
1025
-
1026
- This method delegates to the underlying TinyAgent's compact method,
1027
- which:
1028
- 1. Generates a summary of the current conversation
1029
- 2. If successful, replaces the conversation with just [system, user] messages
1030
- where the user message contains the summary
1031
- 3. Returns True if compaction was successful, False otherwise
997
+
1032
998
 
1033
- Returns:
1034
- Boolean indicating whether the compaction was successful
1035
- """
1036
- return await self.agent.compact()
999
+
1037
1000
 
1038
1001
  def add_ui_callback(self, ui_type: str, optimized: bool = True):
1039
1002
  """
@@ -1122,6 +1085,48 @@ class TinyCodeAgent:
1122
1085
  """
1123
1086
  return self.auto_git_checkpoint
1124
1087
 
1088
+ def enable_python_tool(self, enabled: bool = True):
1089
+ """
1090
+ Enable or disable the Python code execution tool.
1091
+
1092
+ Args:
1093
+ enabled: If True, enable the run_python tool. If False, disable it.
1094
+ """
1095
+ if enabled != self._python_tool_enabled:
1096
+ self._python_tool_enabled = enabled
1097
+ # Re-setup tools to reflect the change
1098
+ self._setup_code_execution_tools()
1099
+
1100
+ def enable_shell_tool(self, enabled: bool = True):
1101
+ """
1102
+ Enable or disable the shell command execution tool.
1103
+
1104
+ Args:
1105
+ enabled: If True, enable the bash tool. If False, disable it.
1106
+ """
1107
+ if enabled != self._shell_tool_enabled:
1108
+ self._shell_tool_enabled = enabled
1109
+ # Re-setup tools to reflect the change
1110
+ self._setup_code_execution_tools()
1111
+
1112
+ def get_python_tool_status(self) -> bool:
1113
+ """
1114
+ Get the current status of the Python tool.
1115
+
1116
+ Returns:
1117
+ True if the run_python tool is enabled, False otherwise.
1118
+ """
1119
+ return self._python_tool_enabled
1120
+
1121
+ def get_shell_tool_status(self) -> bool:
1122
+ """
1123
+ Get the current status of the shell tool.
1124
+
1125
+ Returns:
1126
+ True if the bash tool is enabled, False otherwise.
1127
+ """
1128
+ return self._shell_tool_enabled
1129
+
1125
1130
  def set_environment_variables(self, env_vars: Dict[str, str]):
1126
1131
  """
1127
1132
  Set environment variables for the code execution provider.
@@ -1534,6 +1539,35 @@ async def run_example():
1534
1539
  await agent_seatbelt.connect_to_server("npx", ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"])
1535
1540
  await agent_seatbelt.connect_to_server("npx", ["-y", "@modelcontextprotocol/server-sequential-thinking"])
1536
1541
 
1542
+ # Example: connecting with environment variables
1543
+ env_vars = {
1544
+ "MCP_DEBUG": "true",
1545
+ "RATE_LIMIT": "100",
1546
+ "CUSTOM_CONFIG": "seatbelt_mode"
1547
+ }
1548
+
1549
+ # Create a simple Modal agent to demonstrate environment variable usage
1550
+ agent_modal = TinyCodeAgent(
1551
+ model="gpt-4.1-mini",
1552
+ tools=[search_web],
1553
+ code_tools=[data_processor],
1554
+ provider="modal",
1555
+ local_execution=False,
1556
+ api_key=api_key
1557
+ )
1558
+
1559
+ try:
1560
+ await agent_modal.connect_to_server(
1561
+ "npx",
1562
+ ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"],
1563
+ env=env_vars
1564
+ )
1565
+ logger.info("Successfully connected Modal agent with environment variables")
1566
+ except Exception as e:
1567
+ logger.warning(f"Environment variable example failed: {e}")
1568
+ finally:
1569
+ await agent_modal.close()
1570
+
1537
1571
  # Test the seatbelt agent
1538
1572
  response_seatbelt = await agent_seatbelt.run("""
1539
1573
  I have some sample data. Please use the data_processor tool in Python to analyze my sample_data
@@ -1645,6 +1679,148 @@ async def run_example():
1645
1679
  print("\n" + "="*80)
1646
1680
  print("⚠️ Seatbelt provider is not supported on this system. Skipping seatbelt tests.")
1647
1681
 
1682
+ # Test optional tool functionality
1683
+ print("\n" + "="*80)
1684
+ print("🔧 Testing optional tool functionality")
1685
+
1686
+ # Create an agent with only Python tool enabled (no shell tool)
1687
+ print("Creating agent with only Python tool enabled...")
1688
+ agent_python_only = TinyCodeAgent(
1689
+ model="gpt-4.1-mini",
1690
+ tools=[search_web],
1691
+ code_tools=[data_processor],
1692
+ user_variables={"test_data": [1, 2, 3, 4, 5]},
1693
+ enable_python_tool=True,
1694
+ enable_shell_tool=False, # Disable shell tool
1695
+ local_execution=True
1696
+ )
1697
+
1698
+ # Connect to MCP servers
1699
+ await agent_python_only.connect_to_server("npx", ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"])
1700
+ await agent_python_only.connect_to_server("npx", ["-y", "@modelcontextprotocol/server-sequential-thinking"])
1701
+
1702
+ # Check tool status
1703
+ print(f"Python tool enabled: {agent_python_only.get_python_tool_status()}")
1704
+ print(f"Shell tool enabled: {agent_python_only.get_shell_tool_status()}")
1705
+
1706
+ # Test Python execution (should work)
1707
+ python_response = await agent_python_only.run("""
1708
+ Use the data_processor tool to analyze the test_data and show me the results.
1709
+ """)
1710
+ print("Python Tool Test (should work):")
1711
+ print(python_response)
1712
+
1713
+ # Test shell execution (should not work - tool disabled)
1714
+ shell_response = await agent_python_only.run("""
1715
+ Run 'ls -la' to list files in the current directory.
1716
+ """)
1717
+ print("Shell Tool Test (should not work - tool disabled):")
1718
+ print(shell_response)
1719
+
1720
+ # Now enable the shell tool dynamically
1721
+ print("\nEnabling shell tool dynamically...")
1722
+ agent_python_only.enable_shell_tool(True)
1723
+ print(f"Shell tool enabled: {agent_python_only.get_shell_tool_status()}")
1724
+
1725
+ # Test shell execution again (should work now)
1726
+ shell_response2 = await agent_python_only.run("""
1727
+ Run 'ls -la' to list files in the current directory.
1728
+ """)
1729
+ print("Shell Tool Test (should work now - tool enabled):")
1730
+ print(shell_response2)
1731
+
1732
+ # Create an agent with only shell tool enabled (no Python tool)
1733
+ print("\nCreating agent with only shell tool enabled...")
1734
+ agent_shell_only = TinyCodeAgent(
1735
+ model="gpt-4.1-mini",
1736
+ tools=[search_web],
1737
+ code_tools=[data_processor],
1738
+ user_variables={"test_data": [1, 2, 3, 4, 5]},
1739
+ enable_python_tool=False, # Disable Python tool
1740
+ enable_shell_tool=True,
1741
+ local_execution=True
1742
+ )
1743
+
1744
+ # Connect to MCP servers
1745
+ await agent_shell_only.connect_to_server("npx", ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"])
1746
+ await agent_shell_only.connect_to_server("npx", ["-y", "@modelcontextprotocol/server-sequential-thinking"])
1747
+
1748
+ # Check tool status
1749
+ print(f"Python tool enabled: {agent_shell_only.get_python_tool_status()}")
1750
+ print(f"Shell tool enabled: {agent_shell_only.get_shell_tool_status()}")
1751
+
1752
+ # Test shell execution (should work)
1753
+ shell_response3 = await agent_shell_only.run("""
1754
+ Run 'pwd' to show the current working directory.
1755
+ """)
1756
+ print("Shell Tool Test (should work):")
1757
+ print(shell_response3)
1758
+
1759
+ # Test Python execution (should not work - tool disabled)
1760
+ python_response2 = await agent_shell_only.run("""
1761
+ Use the data_processor tool to analyze the test_data and show me the results.
1762
+ """)
1763
+ print("Python Tool Test (should not work - tool disabled):")
1764
+ print(python_response2)
1765
+
1766
+ # Now enable the Python tool dynamically
1767
+ print("\nEnabling Python tool dynamically...")
1768
+ agent_shell_only.enable_python_tool(True)
1769
+ print(f"Python tool enabled: {agent_shell_only.get_python_tool_status()}")
1770
+
1771
+ # Test Python execution again (should work now)
1772
+ python_response3 = await agent_shell_only.run("""
1773
+ Use the data_processor tool to analyze the test_data and show me the results.
1774
+ """)
1775
+ print("Python Tool Test (should work now - tool enabled):")
1776
+ print(python_response3)
1777
+
1778
+ # Create an agent with both tools disabled
1779
+ print("\nCreating agent with both tools disabled...")
1780
+ agent_no_tools = TinyCodeAgent(
1781
+ model="gpt-4.1-mini",
1782
+ tools=[search_web],
1783
+ code_tools=[data_processor],
1784
+ user_variables={"test_data": [1, 2, 3, 4, 5]},
1785
+ enable_python_tool=False, # Disable Python tool
1786
+ enable_shell_tool=False, # Disable shell tool
1787
+ local_execution=True
1788
+ )
1789
+
1790
+ # Connect to MCP servers
1791
+ await agent_no_tools.connect_to_server("npx", ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"])
1792
+ await agent_no_tools.connect_to_server("npx", ["-y", "@modelcontextprotocol/server-sequential-thinking"])
1793
+
1794
+ # Check tool status
1795
+ print(f"Python tool enabled: {agent_no_tools.get_python_tool_status()}")
1796
+ print(f"Shell tool enabled: {agent_no_tools.get_shell_tool_status()}")
1797
+
1798
+ # Test both tools (should not work - both disabled)
1799
+ no_tools_response = await agent_no_tools.run("""
1800
+ Try to use both Python and shell tools to analyze the test_data and list files.
1801
+ """)
1802
+ print("Both Tools Test (should not work - both disabled):")
1803
+ print(no_tools_response)
1804
+
1805
+ # Enable both tools dynamically
1806
+ print("\nEnabling both tools dynamically...")
1807
+ agent_no_tools.enable_python_tool(True)
1808
+ agent_no_tools.enable_shell_tool(True)
1809
+ print(f"Python tool enabled: {agent_no_tools.get_python_tool_status()}")
1810
+ print(f"Shell tool enabled: {agent_no_tools.get_shell_tool_status()}")
1811
+
1812
+ # Test both tools again (should work now)
1813
+ both_tools_response = await agent_no_tools.run("""
1814
+ Use both Python and shell tools: first analyze the test_data with data_processor, then list files with ls.
1815
+ """)
1816
+ print("Both Tools Test (should work now - both enabled):")
1817
+ print(both_tools_response)
1818
+
1819
+ # Clean up
1820
+ await agent_python_only.close()
1821
+ await agent_shell_only.close()
1822
+ await agent_no_tools.close()
1823
+
1648
1824
  await agent_remote.close()
1649
1825
  await agent_local.close()
1650
1826
 
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import json
3
3
  import logging
4
+ import traceback
4
5
  from typing import Dict, List, Optional, Any, Tuple, Callable
5
6
 
6
7
  # Keep your MCPClient implementation unchanged
@@ -57,16 +58,17 @@ class MCPClient:
57
58
  logger.debug(f"Callback is a regular function")
58
59
  callback(event_name, self, **kwargs)
59
60
  except Exception as e:
60
- logger.error(f"Error in callback for {event_name}: {str(e)}")
61
+ logger.error(f"Error in callback for {event_name}: {str(e)} {traceback.format_exc()}")
61
62
 
62
- async def connect(self, command: str, args: list[str]):
63
+ async def connect(self, command: str, args: list[str], env: dict[str, str] = None):
63
64
  """
64
65
  Launches the MCP server subprocess and initializes the client session.
65
66
  :param command: e.g. "python" or "node"
66
67
  :param args: list of args to pass, e.g. ["my_server.py"] or ["build/index.js"]
68
+ :param env: dictionary of environment variables to pass to the subprocess
67
69
  """
68
70
  # Prepare stdio transport parameters
69
- params = StdioServerParameters(command=command, args=args)
71
+ params = StdioServerParameters(command=command, args=args, env=env)
70
72
  # Open the stdio client transport
71
73
  self.stdio, self.sock_write = await self.exit_stack.enter_async_context(
72
74
  stdio_client(params)
@@ -156,6 +158,28 @@ async def run_example():
156
158
  result = await client.call_tool("echo", {"message": "Hello, MCP!"})
157
159
  mcp_logger.info(f"Echo result: {result}")
158
160
 
161
+ # Example with environment variables
162
+ mcp_logger.info("Testing with environment variables...")
163
+ client_with_env = MCPClient(logger=mcp_logger)
164
+
165
+ # Example: connecting with environment variables
166
+ env_vars = {
167
+ "DEBUG": "true",
168
+ "LOG_LEVEL": "info",
169
+ "CUSTOM_VAR": "example_value"
170
+ }
171
+
172
+ try:
173
+ await client_with_env.connect(
174
+ "python",
175
+ ["-m", "mcp.examples.echo_server"],
176
+ env=env_vars
177
+ )
178
+ mcp_logger.info("Successfully connected with environment variables")
179
+ await client_with_env.close()
180
+ except Exception as e:
181
+ mcp_logger.warning(f"Environment variable example failed (expected): {e}")
182
+
159
183
  finally:
160
184
  # Clean up
161
185
  await client.close()
@@ -705,11 +705,12 @@ class TinyAgent:
705
705
  self.logger.debug(f"Callback is a regular function")
706
706
  callback(event_name, self, **kwargs)
707
707
  except Exception as e:
708
- self.logger.error(f"Error in callback for {event_name}: {str(e)}")
708
+ self.logger.error(f"Error in callback for {event_name}: {str(e)} {traceback.format_exc()}")
709
709
 
710
710
  async def connect_to_server(self, command: str, args: List[str],
711
711
  include_tools: Optional[List[str]] = None,
712
- exclude_tools: Optional[List[str]] = None) -> None:
712
+ exclude_tools: Optional[List[str]] = None,
713
+ env: Optional[Dict[str, str]] = None) -> None:
713
714
  """
714
715
  Connect to an MCP server and fetch available tools.
715
716
 
@@ -718,6 +719,7 @@ class TinyAgent:
718
719
  args: List of arguments for the server
719
720
  include_tools: Optional list of tool name patterns to include (if provided, only matching tools will be added)
720
721
  exclude_tools: Optional list of tool name patterns to exclude (matching tools will be skipped)
722
+ env: Optional dictionary of environment variables to pass to the subprocess
721
723
  """
722
724
  # 1) Create and connect a brand-new client
723
725
  client = MCPClient()
@@ -726,7 +728,7 @@ class TinyAgent:
726
728
  for callback in self.callbacks:
727
729
  client.add_callback(callback)
728
730
 
729
- await client.connect(command, args)
731
+ await client.connect(command, args, env)
730
732
  self.mcp_clients.append(client)
731
733
 
732
734
  # 2) List tools on *this* server
@@ -1717,7 +1719,21 @@ async def run_example():
1717
1719
 
1718
1720
  # Connect to MCP servers for additional tools
1719
1721
  try:
1722
+ # Example: connecting without environment variables (existing behavior)
1720
1723
  await agent1.connect_to_server("npx", ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"])
1724
+
1725
+ # Example: connecting with environment variables
1726
+ env_vars = {
1727
+ "DEBUG": "true",
1728
+ "LOG_LEVEL": "info",
1729
+ "API_TIMEOUT": "30"
1730
+ }
1731
+ await agent1.connect_to_server(
1732
+ "npx",
1733
+ ["-y", "@modelcontextprotocol/server-sequential-thinking"],
1734
+ env=env_vars
1735
+ )
1736
+ agent_logger.info("Successfully connected to MCP servers with environment variables")
1721
1737
  except Exception as e:
1722
1738
  agent_logger.error(f"Failed to connect to MCP servers: {e}")
1723
1739
 
@@ -1747,7 +1763,17 @@ async def run_example():
1747
1763
 
1748
1764
  # Connect to the same MCP server
1749
1765
  try:
1750
- await agent2.connect_to_server("npx", ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"])
1766
+ # Example with environment variables for o4-mini model
1767
+ env_vars = {
1768
+ "NODE_ENV": "production",
1769
+ "CACHE_ENABLED": "false"
1770
+ }
1771
+ await agent2.connect_to_server(
1772
+ "npx",
1773
+ ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"],
1774
+ env=env_vars
1775
+ )
1776
+ agent_logger.info("Successfully connected o4-mini agent with environment variables")
1751
1777
  except Exception as e:
1752
1778
  agent_logger.error(f"Failed to connect to MCP servers: {e}")
1753
1779
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinyagent-py
3
- Version: 0.0.17rc1
4
- Summary: TinyAgent with MCP Client, Code Agent (Thinking, Planning, and Executing in Python), and Extendable Hooks, Tiny but powerful
3
+ Version: 0.0.19
4
+ Summary: TinyAgent with MCP Client, CodeAgent (Thinking, Planning, Interactive Python and Shell with high variaety of sandboxing(seatbelt, Modal, E2B, docker, etc) ), and Extendable Hooks, Tiny but powerful
5
5
  Author-email: Mahdi Golchin <golchin@askdev.ai>
6
6
  Project-URL: Homepage, https://github.com/askbudi/tinyagent
7
7
  Project-URL: Bug Tracker, https://github.com/askbudi/tinyagent/issues
File without changes