code-puppy 0.0.130__py3-none-any.whl → 0.0.132__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.
- code_puppy/command_line/mcp_commands.py +591 -106
- code_puppy/mcp/blocking_startup.py +404 -0
- code_puppy/mcp/captured_stdio_server.py +282 -0
- code_puppy/mcp/config_wizard.py +151 -117
- code_puppy/mcp/managed_server.py +55 -1
- code_puppy/mcp/server_registry_catalog.py +346 -46
- code_puppy/mcp/system_tools.py +214 -0
- code_puppy/messaging/__init__.py +4 -0
- code_puppy/messaging/message_queue.py +86 -0
- code_puppy/messaging/renderers.py +94 -0
- code_puppy/tui/app.py +24 -1
- code_puppy/tui/components/chat_view.py +33 -18
- code_puppy/tui/components/human_input_modal.py +171 -0
- code_puppy/tui/screens/__init__.py +3 -1
- code_puppy/tui/screens/mcp_install_wizard.py +593 -0
- {code_puppy-0.0.130.dist-info → code_puppy-0.0.132.dist-info}/METADATA +1 -1
- {code_puppy-0.0.130.dist-info → code_puppy-0.0.132.dist-info}/RECORD +21 -16
- {code_puppy-0.0.130.data → code_puppy-0.0.132.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.130.dist-info → code_puppy-0.0.132.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.130.dist-info → code_puppy-0.0.132.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.130.dist-info → code_puppy-0.0.132.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,8 +15,9 @@ from rich.table import Table
|
|
|
15
15
|
from rich.console import Console
|
|
16
16
|
from rich.text import Text
|
|
17
17
|
from rich.panel import Panel
|
|
18
|
-
from rich.columns import Columns
|
|
19
18
|
|
|
19
|
+
from code_puppy.state_management import is_tui_mode
|
|
20
|
+
from code_puppy.messaging import emit_prompt
|
|
20
21
|
from code_puppy.mcp.manager import get_mcp_manager, ServerInfo
|
|
21
22
|
from code_puppy.mcp.managed_server import ServerConfig, ServerState
|
|
22
23
|
from code_puppy.messaging import emit_info, emit_system_message
|
|
@@ -671,10 +672,8 @@ class MCPCommandHandler:
|
|
|
671
672
|
emit_info(f"Failed to add server '{name}'", message_group=group_id)
|
|
672
673
|
|
|
673
674
|
else:
|
|
674
|
-
# No arguments - launch interactive wizard
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
success = run_add_wizard()
|
|
675
|
+
# No arguments - launch interactive wizard with server templates
|
|
676
|
+
success = self._run_interactive_install_wizard(group_id)
|
|
678
677
|
|
|
679
678
|
if success:
|
|
680
679
|
# Reload the agent to pick up new server
|
|
@@ -688,6 +687,398 @@ class MCPCommandHandler:
|
|
|
688
687
|
logger.error(f"Error adding server: {e}")
|
|
689
688
|
emit_info(f"Failed to add server: {e}", message_group=group_id)
|
|
690
689
|
|
|
690
|
+
def _run_interactive_install_wizard(self, group_id: str) -> bool:
|
|
691
|
+
"""Run the interactive MCP server installation wizard using server templates."""
|
|
692
|
+
try:
|
|
693
|
+
from code_puppy.mcp.server_registry_catalog import catalog
|
|
694
|
+
from code_puppy.mcp.system_tools import detector
|
|
695
|
+
from code_puppy.messaging import emit_prompt
|
|
696
|
+
import os
|
|
697
|
+
import json
|
|
698
|
+
|
|
699
|
+
emit_info("🧙 Interactive MCP Server Installation Wizard", message_group=group_id)
|
|
700
|
+
emit_info("", message_group=group_id)
|
|
701
|
+
|
|
702
|
+
# Step 1: Browse and select server
|
|
703
|
+
selected_server = self._interactive_server_selection(group_id)
|
|
704
|
+
if not selected_server:
|
|
705
|
+
return False
|
|
706
|
+
|
|
707
|
+
# Step 2: Get custom server name
|
|
708
|
+
server_name = self._interactive_get_server_name(selected_server, group_id)
|
|
709
|
+
if not server_name:
|
|
710
|
+
return False
|
|
711
|
+
|
|
712
|
+
# Step 3: Handle requirements and configuration
|
|
713
|
+
success = self._interactive_configure_server(selected_server, server_name, group_id)
|
|
714
|
+
return success
|
|
715
|
+
|
|
716
|
+
except ImportError:
|
|
717
|
+
emit_info("Server catalog not available, falling back to basic wizard", message_group=group_id)
|
|
718
|
+
# Fall back to the old wizard
|
|
719
|
+
from code_puppy.mcp.config_wizard import run_add_wizard
|
|
720
|
+
return run_add_wizard(group_id)
|
|
721
|
+
except Exception as e:
|
|
722
|
+
emit_info(f"Installation wizard failed: {e}", message_group=group_id)
|
|
723
|
+
return False
|
|
724
|
+
|
|
725
|
+
def _interactive_server_selection(self, group_id: str):
|
|
726
|
+
"""Interactive server selection from catalog."""
|
|
727
|
+
from code_puppy.mcp.server_registry_catalog import catalog
|
|
728
|
+
from code_puppy.messaging import emit_prompt
|
|
729
|
+
|
|
730
|
+
while True:
|
|
731
|
+
emit_info("📦 Available MCP Servers:", message_group=group_id)
|
|
732
|
+
emit_info("", message_group=group_id)
|
|
733
|
+
|
|
734
|
+
# Show popular servers first
|
|
735
|
+
popular = catalog.get_popular(5)
|
|
736
|
+
if popular:
|
|
737
|
+
emit_info("[bold]Popular Servers:[/bold]", message_group=group_id)
|
|
738
|
+
for i, server in enumerate(popular):
|
|
739
|
+
indicators = []
|
|
740
|
+
if server.verified:
|
|
741
|
+
indicators.append("✓")
|
|
742
|
+
if server.popular:
|
|
743
|
+
indicators.append("⭐")
|
|
744
|
+
|
|
745
|
+
emit_info(f" {i+1}. {server.display_name} {''.join(indicators)}", message_group=group_id)
|
|
746
|
+
emit_info(f" {server.description[:80]}...", message_group=group_id)
|
|
747
|
+
emit_info("", message_group=group_id)
|
|
748
|
+
|
|
749
|
+
# Prompt for selection
|
|
750
|
+
choice = emit_prompt("Enter server number (1-5), 'search <term>' to search, or 'list' to see all categories: ")
|
|
751
|
+
|
|
752
|
+
if not choice.strip():
|
|
753
|
+
if emit_prompt("Cancel installation? [y/N]: ").lower().startswith('y'):
|
|
754
|
+
return None
|
|
755
|
+
continue
|
|
756
|
+
|
|
757
|
+
choice = choice.strip()
|
|
758
|
+
|
|
759
|
+
# Handle numeric selection
|
|
760
|
+
if choice.isdigit():
|
|
761
|
+
try:
|
|
762
|
+
index = int(choice) - 1
|
|
763
|
+
if 0 <= index < len(popular):
|
|
764
|
+
return popular[index]
|
|
765
|
+
else:
|
|
766
|
+
emit_info("Invalid selection. Please try again.", message_group=group_id)
|
|
767
|
+
continue
|
|
768
|
+
except ValueError:
|
|
769
|
+
pass
|
|
770
|
+
|
|
771
|
+
# Handle search
|
|
772
|
+
if choice.lower().startswith('search '):
|
|
773
|
+
search_term = choice[7:].strip()
|
|
774
|
+
results = catalog.search(search_term)
|
|
775
|
+
if results:
|
|
776
|
+
emit_info(f"\n🔍 Search results for '{search_term}':", message_group=group_id)
|
|
777
|
+
for i, server in enumerate(results[:10]):
|
|
778
|
+
indicators = []
|
|
779
|
+
if server.verified:
|
|
780
|
+
indicators.append("✓")
|
|
781
|
+
if server.popular:
|
|
782
|
+
indicators.append("⭐")
|
|
783
|
+
emit_info(f" {i+1}. {server.display_name} {''.join(indicators)}", message_group=group_id)
|
|
784
|
+
emit_info(f" {server.description[:80]}...", message_group=group_id)
|
|
785
|
+
|
|
786
|
+
selection = emit_prompt(f"\nSelect server (1-{min(len(results), 10)}): ")
|
|
787
|
+
if selection.isdigit():
|
|
788
|
+
try:
|
|
789
|
+
index = int(selection) - 1
|
|
790
|
+
if 0 <= index < len(results):
|
|
791
|
+
return results[index]
|
|
792
|
+
except ValueError:
|
|
793
|
+
pass
|
|
794
|
+
else:
|
|
795
|
+
emit_info(f"No servers found for '{search_term}'", message_group=group_id)
|
|
796
|
+
continue
|
|
797
|
+
|
|
798
|
+
# Handle list categories
|
|
799
|
+
if choice.lower() == 'list':
|
|
800
|
+
categories = catalog.list_categories()
|
|
801
|
+
emit_info("\n📂 Categories:", message_group=group_id)
|
|
802
|
+
for i, category in enumerate(categories):
|
|
803
|
+
servers_count = len(catalog.get_by_category(category))
|
|
804
|
+
emit_info(f" {i+1}. {category} ({servers_count} servers)", message_group=group_id)
|
|
805
|
+
|
|
806
|
+
cat_choice = emit_prompt(f"\nSelect category (1-{len(categories)}): ")
|
|
807
|
+
if cat_choice.isdigit():
|
|
808
|
+
try:
|
|
809
|
+
index = int(cat_choice) - 1
|
|
810
|
+
if 0 <= index < len(categories):
|
|
811
|
+
category_servers = catalog.get_by_category(categories[index])
|
|
812
|
+
emit_info(f"\n📦 {categories[index]} Servers:", message_group=group_id)
|
|
813
|
+
for i, server in enumerate(category_servers):
|
|
814
|
+
indicators = []
|
|
815
|
+
if server.verified:
|
|
816
|
+
indicators.append("✓")
|
|
817
|
+
if server.popular:
|
|
818
|
+
indicators.append("⭐")
|
|
819
|
+
emit_info(f" {i+1}. {server.display_name} {''.join(indicators)}", message_group=group_id)
|
|
820
|
+
emit_info(f" {server.description[:80]}...", message_group=group_id)
|
|
821
|
+
|
|
822
|
+
server_choice = emit_prompt(f"\nSelect server (1-{len(category_servers)}): ")
|
|
823
|
+
if server_choice.isdigit():
|
|
824
|
+
try:
|
|
825
|
+
index = int(server_choice) - 1
|
|
826
|
+
if 0 <= index < len(category_servers):
|
|
827
|
+
return category_servers[index]
|
|
828
|
+
except ValueError:
|
|
829
|
+
pass
|
|
830
|
+
except ValueError:
|
|
831
|
+
pass
|
|
832
|
+
continue
|
|
833
|
+
|
|
834
|
+
emit_info("Invalid choice. Please try again.", message_group=group_id)
|
|
835
|
+
|
|
836
|
+
def _interactive_get_server_name(self, selected_server, group_id: str) -> str:
|
|
837
|
+
"""Get custom server name from user."""
|
|
838
|
+
from code_puppy.messaging import emit_prompt
|
|
839
|
+
|
|
840
|
+
emit_info(f"\n🏷️ Server: {selected_server.display_name}", message_group=group_id)
|
|
841
|
+
emit_info(f"Description: {selected_server.description}", message_group=group_id)
|
|
842
|
+
emit_info("", message_group=group_id)
|
|
843
|
+
|
|
844
|
+
while True:
|
|
845
|
+
name = emit_prompt(f"Enter custom name for this server [{selected_server.name}]: ").strip()
|
|
846
|
+
|
|
847
|
+
if not name:
|
|
848
|
+
name = selected_server.name
|
|
849
|
+
|
|
850
|
+
# Validate name
|
|
851
|
+
if not name.replace('-', '').replace('_', '').replace('.', '').isalnum():
|
|
852
|
+
emit_info("Name must contain only letters, numbers, hyphens, underscores, and dots", message_group=group_id)
|
|
853
|
+
continue
|
|
854
|
+
|
|
855
|
+
# Check if name already exists
|
|
856
|
+
existing_server = self._find_server_id_by_name(name)
|
|
857
|
+
if existing_server:
|
|
858
|
+
override = emit_prompt(f"Server '{name}' already exists. Override it? [y/N]: ")
|
|
859
|
+
if not override.lower().startswith('y'):
|
|
860
|
+
continue
|
|
861
|
+
|
|
862
|
+
return name
|
|
863
|
+
|
|
864
|
+
def _interactive_configure_server(self, selected_server, server_name: str, group_id: str) -> bool:
|
|
865
|
+
"""Configure the server with requirements validation."""
|
|
866
|
+
from code_puppy.mcp.system_tools import detector
|
|
867
|
+
from code_puppy.messaging import emit_prompt
|
|
868
|
+
import os
|
|
869
|
+
import json
|
|
870
|
+
|
|
871
|
+
requirements = selected_server.get_requirements()
|
|
872
|
+
|
|
873
|
+
emit_info(f"\n⚙️ Configuring server: {server_name}", message_group=group_id)
|
|
874
|
+
emit_info("", message_group=group_id)
|
|
875
|
+
|
|
876
|
+
# Step 1: Check system requirements
|
|
877
|
+
if not self._interactive_check_system_requirements(requirements, group_id):
|
|
878
|
+
return False
|
|
879
|
+
|
|
880
|
+
# Step 2: Collect environment variables
|
|
881
|
+
env_vars = self._interactive_collect_env_vars(requirements, group_id)
|
|
882
|
+
|
|
883
|
+
# Step 3: Collect command line arguments
|
|
884
|
+
cmd_args = self._interactive_collect_cmd_args(requirements, group_id)
|
|
885
|
+
|
|
886
|
+
# Step 4: Show summary and confirm
|
|
887
|
+
if not self._interactive_confirm_installation(selected_server, server_name, env_vars, cmd_args, group_id):
|
|
888
|
+
return False
|
|
889
|
+
|
|
890
|
+
# Step 5: Install the server
|
|
891
|
+
return self._interactive_install_server(selected_server, server_name, env_vars, cmd_args, group_id)
|
|
892
|
+
|
|
893
|
+
def _interactive_check_system_requirements(self, requirements, group_id: str) -> bool:
|
|
894
|
+
"""Check and validate system requirements."""
|
|
895
|
+
from code_puppy.mcp.system_tools import detector
|
|
896
|
+
|
|
897
|
+
required_tools = requirements.required_tools
|
|
898
|
+
if not required_tools:
|
|
899
|
+
return True
|
|
900
|
+
|
|
901
|
+
emit_info("🔧 Checking system requirements...", message_group=group_id)
|
|
902
|
+
|
|
903
|
+
tool_status = detector.detect_tools(required_tools)
|
|
904
|
+
all_good = True
|
|
905
|
+
|
|
906
|
+
for tool_name, tool_info in tool_status.items():
|
|
907
|
+
if tool_info.available:
|
|
908
|
+
status_text = f"✅ {tool_name}"
|
|
909
|
+
if tool_info.version:
|
|
910
|
+
status_text += f" ({tool_info.version})"
|
|
911
|
+
emit_info(status_text, message_group=group_id)
|
|
912
|
+
else:
|
|
913
|
+
status_text = f"❌ {tool_name} - {tool_info.error or 'Not found'}"
|
|
914
|
+
emit_info(status_text, message_group=group_id)
|
|
915
|
+
|
|
916
|
+
# Show installation suggestions
|
|
917
|
+
suggestions = detector.get_installation_suggestions(tool_name)
|
|
918
|
+
if suggestions:
|
|
919
|
+
emit_info(f" Install: {suggestions[0]}", message_group=group_id)
|
|
920
|
+
all_good = False
|
|
921
|
+
|
|
922
|
+
if not all_good:
|
|
923
|
+
emit_info("", message_group=group_id)
|
|
924
|
+
cont = emit_prompt("Some tools are missing. Continue anyway? [y/N]: ")
|
|
925
|
+
if not cont.lower().startswith('y'):
|
|
926
|
+
emit_info("Installation cancelled", message_group=group_id)
|
|
927
|
+
return False
|
|
928
|
+
|
|
929
|
+
emit_info("", message_group=group_id)
|
|
930
|
+
return True
|
|
931
|
+
|
|
932
|
+
def _interactive_collect_env_vars(self, requirements, group_id: str) -> dict:
|
|
933
|
+
"""Collect environment variables from user."""
|
|
934
|
+
from code_puppy.messaging import emit_prompt
|
|
935
|
+
import os
|
|
936
|
+
|
|
937
|
+
env_vars = {}
|
|
938
|
+
required_env_vars = requirements.environment_vars
|
|
939
|
+
|
|
940
|
+
if not required_env_vars:
|
|
941
|
+
return env_vars
|
|
942
|
+
|
|
943
|
+
emit_info("🔐 Environment Variables:", message_group=group_id)
|
|
944
|
+
|
|
945
|
+
for var in required_env_vars:
|
|
946
|
+
# Check if already set
|
|
947
|
+
current_value = os.environ.get(var, "")
|
|
948
|
+
|
|
949
|
+
if current_value:
|
|
950
|
+
emit_info(f"✅ {var} (already set)", message_group=group_id)
|
|
951
|
+
env_vars[var] = current_value
|
|
952
|
+
else:
|
|
953
|
+
value = emit_prompt(f"📝 Enter value for {var}: ").strip()
|
|
954
|
+
if value:
|
|
955
|
+
env_vars[var] = value
|
|
956
|
+
# Set in current environment too
|
|
957
|
+
os.environ[var] = value
|
|
958
|
+
else:
|
|
959
|
+
emit_info(f"⚠️ {var} left empty", message_group=group_id)
|
|
960
|
+
|
|
961
|
+
emit_info("", message_group=group_id)
|
|
962
|
+
return env_vars
|
|
963
|
+
|
|
964
|
+
def _interactive_collect_cmd_args(self, requirements, group_id: str) -> dict:
|
|
965
|
+
"""Collect command line arguments from user."""
|
|
966
|
+
from code_puppy.messaging import emit_prompt
|
|
967
|
+
|
|
968
|
+
cmd_args = {}
|
|
969
|
+
required_args = requirements.command_line_args
|
|
970
|
+
|
|
971
|
+
if not required_args:
|
|
972
|
+
return cmd_args
|
|
973
|
+
|
|
974
|
+
emit_info("⚡ Command Line Arguments:", message_group=group_id)
|
|
975
|
+
|
|
976
|
+
for arg_config in required_args:
|
|
977
|
+
name = arg_config.get("name", "")
|
|
978
|
+
prompt_text = arg_config.get("prompt", name)
|
|
979
|
+
default = arg_config.get("default", "")
|
|
980
|
+
required = arg_config.get("required", True)
|
|
981
|
+
|
|
982
|
+
indicator = "⚡" if required else "🔧"
|
|
983
|
+
label = f"{indicator} {prompt_text}"
|
|
984
|
+
if not required:
|
|
985
|
+
label += " (optional)"
|
|
986
|
+
if default:
|
|
987
|
+
label += f" [{default}]"
|
|
988
|
+
|
|
989
|
+
value = emit_prompt(f"{label}: ").strip()
|
|
990
|
+
|
|
991
|
+
if not value and default:
|
|
992
|
+
value = default
|
|
993
|
+
|
|
994
|
+
if value:
|
|
995
|
+
cmd_args[name] = value
|
|
996
|
+
elif required:
|
|
997
|
+
emit_info(f"⚠️ Required argument '{name}' left empty", message_group=group_id)
|
|
998
|
+
|
|
999
|
+
emit_info("", message_group=group_id)
|
|
1000
|
+
return cmd_args
|
|
1001
|
+
|
|
1002
|
+
def _interactive_confirm_installation(self, selected_server, server_name: str, env_vars: dict, cmd_args: dict, group_id: str) -> bool:
|
|
1003
|
+
"""Show summary and confirm installation."""
|
|
1004
|
+
from code_puppy.messaging import emit_prompt
|
|
1005
|
+
|
|
1006
|
+
emit_info("📋 Installation Summary:", message_group=group_id)
|
|
1007
|
+
emit_info(f" Server: {selected_server.display_name}", message_group=group_id)
|
|
1008
|
+
emit_info(f" Name: {server_name}", message_group=group_id)
|
|
1009
|
+
emit_info(f" Type: {selected_server.type}", message_group=group_id)
|
|
1010
|
+
|
|
1011
|
+
if env_vars:
|
|
1012
|
+
emit_info(f" Environment variables: {len(env_vars)} set", message_group=group_id)
|
|
1013
|
+
|
|
1014
|
+
if cmd_args:
|
|
1015
|
+
emit_info(f" Command arguments: {len(cmd_args)} configured", message_group=group_id)
|
|
1016
|
+
|
|
1017
|
+
emit_info("", message_group=group_id)
|
|
1018
|
+
|
|
1019
|
+
confirm = emit_prompt("Install this server configuration? [Y/n]: ")
|
|
1020
|
+
return not confirm.lower().startswith('n')
|
|
1021
|
+
|
|
1022
|
+
def _interactive_install_server(self, selected_server, server_name: str, env_vars: dict, cmd_args: dict, group_id: str) -> bool:
|
|
1023
|
+
"""Actually install and register the server."""
|
|
1024
|
+
try:
|
|
1025
|
+
# Get server config with command line argument overrides
|
|
1026
|
+
config_dict = selected_server.to_server_config(server_name, **cmd_args)
|
|
1027
|
+
|
|
1028
|
+
# Update the config with actual environment variable values
|
|
1029
|
+
if 'env' in config_dict:
|
|
1030
|
+
for env_key, env_value in config_dict['env'].items():
|
|
1031
|
+
# If it's a placeholder like $GITHUB_TOKEN, replace with actual value
|
|
1032
|
+
if env_value.startswith('$'):
|
|
1033
|
+
var_name = env_value[1:] # Remove the $
|
|
1034
|
+
if var_name in env_vars:
|
|
1035
|
+
config_dict['env'][env_key] = env_vars[var_name]
|
|
1036
|
+
|
|
1037
|
+
# Create and register the server
|
|
1038
|
+
from code_puppy.mcp import ServerConfig
|
|
1039
|
+
|
|
1040
|
+
server_config = ServerConfig(
|
|
1041
|
+
id=server_name,
|
|
1042
|
+
name=server_name,
|
|
1043
|
+
type=config_dict.pop('type'),
|
|
1044
|
+
enabled=True,
|
|
1045
|
+
config=config_dict
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
server_id = self.manager.register_server(server_config)
|
|
1049
|
+
|
|
1050
|
+
if server_id:
|
|
1051
|
+
# Save to mcp_servers.json for persistence
|
|
1052
|
+
from code_puppy.config import MCP_SERVERS_FILE
|
|
1053
|
+
import json
|
|
1054
|
+
import os
|
|
1055
|
+
|
|
1056
|
+
if os.path.exists(MCP_SERVERS_FILE):
|
|
1057
|
+
with open(MCP_SERVERS_FILE, 'r') as f:
|
|
1058
|
+
data = json.load(f)
|
|
1059
|
+
servers = data.get("mcp_servers", {})
|
|
1060
|
+
else:
|
|
1061
|
+
servers = {}
|
|
1062
|
+
data = {"mcp_servers": servers}
|
|
1063
|
+
|
|
1064
|
+
servers[server_name] = config_dict
|
|
1065
|
+
servers[server_name]['type'] = server_config.type
|
|
1066
|
+
|
|
1067
|
+
os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
|
|
1068
|
+
with open(MCP_SERVERS_FILE, 'w') as f:
|
|
1069
|
+
json.dump(data, f, indent=2)
|
|
1070
|
+
|
|
1071
|
+
emit_info(f"✅ Successfully installed '{server_name}' from {selected_server.display_name}!", message_group=group_id)
|
|
1072
|
+
emit_info(f"Use '/mcp start {server_name}' to start the server", message_group=group_id)
|
|
1073
|
+
return True
|
|
1074
|
+
else:
|
|
1075
|
+
emit_info(f"❌ Failed to register server", message_group=group_id)
|
|
1076
|
+
return False
|
|
1077
|
+
|
|
1078
|
+
except Exception as e:
|
|
1079
|
+
emit_info(f"❌ Installation failed: {str(e)}", message_group=group_id)
|
|
1080
|
+
return False
|
|
1081
|
+
|
|
691
1082
|
def cmd_remove(self, args: List[str]) -> None:
|
|
692
1083
|
"""
|
|
693
1084
|
Remove an MCP server.
|
|
@@ -990,116 +1381,119 @@ class MCPCommandHandler:
|
|
|
990
1381
|
group_id = str(uuid.uuid4())
|
|
991
1382
|
|
|
992
1383
|
try:
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1384
|
+
# If in TUI mode, show message to use Ctrl+T
|
|
1385
|
+
if is_tui_mode():
|
|
1386
|
+
emit_info("In TUI mode, use Ctrl+T to open the MCP Install Wizard", message_group=group_id)
|
|
1387
|
+
return
|
|
996
1388
|
|
|
1389
|
+
# In interactive mode, use the new comprehensive installer
|
|
997
1390
|
if not args:
|
|
998
|
-
|
|
999
|
-
|
|
1391
|
+
# No args - launch interactive wizard
|
|
1392
|
+
success = self._run_interactive_install_wizard(group_id)
|
|
1393
|
+
if success:
|
|
1394
|
+
from code_puppy.agent import reload_mcp_servers
|
|
1395
|
+
reload_mcp_servers()
|
|
1000
1396
|
return
|
|
1001
1397
|
|
|
1398
|
+
# Has args - install directly from catalog
|
|
1002
1399
|
server_id = args[0]
|
|
1003
|
-
|
|
1400
|
+
success = self._install_from_catalog(server_id, group_id)
|
|
1401
|
+
if success:
|
|
1402
|
+
from code_puppy.agent import reload_mcp_servers
|
|
1403
|
+
reload_mcp_servers()
|
|
1404
|
+
return
|
|
1004
1405
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
emit_info(f" • {s.id} - {s.display_name}", message_group=group_id)
|
|
1016
|
-
return
|
|
1406
|
+
except ImportError:
|
|
1407
|
+
emit_info("Server registry not available", message_group=group_id)
|
|
1408
|
+
except Exception as e:
|
|
1409
|
+
logger.error(f"Error installing server: {e}")
|
|
1410
|
+
emit_info(f"Installation failed: {e}", message_group=group_id)
|
|
1411
|
+
|
|
1412
|
+
def _install_from_catalog(self, server_name_or_id: str, group_id: str) -> bool:
|
|
1413
|
+
"""Install a server directly from the catalog by name or ID."""
|
|
1414
|
+
try:
|
|
1415
|
+
from code_puppy.mcp.server_registry_catalog import catalog
|
|
1017
1416
|
|
|
1018
|
-
#
|
|
1019
|
-
|
|
1020
|
-
|
|
1417
|
+
# Try to find server by ID first, then by name/search
|
|
1418
|
+
selected_server = catalog.get_by_id(server_name_or_id)
|
|
1419
|
+
|
|
1420
|
+
if not selected_server:
|
|
1421
|
+
# Try searching by name
|
|
1422
|
+
results = catalog.search(server_name_or_id)
|
|
1423
|
+
if not results:
|
|
1424
|
+
emit_info(f"❌ No server found matching '{server_name_or_id}'", message_group=group_id)
|
|
1425
|
+
emit_info("Try '/mcp add' to browse available servers", message_group=group_id)
|
|
1426
|
+
return False
|
|
1427
|
+
elif len(results) == 1:
|
|
1428
|
+
selected_server = results[0]
|
|
1429
|
+
else:
|
|
1430
|
+
# Multiple matches, show them
|
|
1431
|
+
emit_info(f"🔍 Multiple servers found matching '{server_name_or_id}':", message_group=group_id)
|
|
1432
|
+
for i, server in enumerate(results[:5]):
|
|
1433
|
+
indicators = []
|
|
1434
|
+
if server.verified:
|
|
1435
|
+
indicators.append("✓")
|
|
1436
|
+
if server.popular:
|
|
1437
|
+
indicators.append("⭐")
|
|
1438
|
+
|
|
1439
|
+
indicator_str = ''
|
|
1440
|
+
if indicators:
|
|
1441
|
+
indicator_str = ' ' + ''.join(indicators)
|
|
1442
|
+
|
|
1443
|
+
emit_info(f" {i+1}. {server.display_name}{indicator_str}", message_group=group_id)
|
|
1444
|
+
emit_info(f" ID: {server.id}", message_group=group_id)
|
|
1445
|
+
|
|
1446
|
+
emit_info(f"Please use the exact server ID: '/mcp add <server_id>'", message_group=group_id)
|
|
1447
|
+
return False
|
|
1021
1448
|
|
|
1022
|
-
#
|
|
1023
|
-
|
|
1024
|
-
|
|
1449
|
+
# Show what we're installing
|
|
1450
|
+
emit_info(f"📦 Installing: {selected_server.display_name}", message_group=group_id)
|
|
1451
|
+
description = selected_server.description if selected_server.description else "No description available"
|
|
1452
|
+
emit_info(f"Description: {description}", message_group=group_id)
|
|
1453
|
+
emit_info("", message_group=group_id)
|
|
1025
1454
|
|
|
1026
|
-
#
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1455
|
+
# Get custom name (default to server name)
|
|
1456
|
+
from code_puppy.messaging import emit_prompt
|
|
1457
|
+
server_name = emit_prompt(f"Enter custom name for this server [{selected_server.name}]: ").strip()
|
|
1458
|
+
if not server_name:
|
|
1459
|
+
server_name = selected_server.name
|
|
1460
|
+
|
|
1461
|
+
# Check if name already exists
|
|
1462
|
+
existing_server = self._find_server_id_by_name(server_name)
|
|
1463
|
+
if existing_server:
|
|
1464
|
+
override = emit_prompt(f"Server '{server_name}' already exists. Override it? [y/N]: ")
|
|
1465
|
+
if not override.lower().startswith('y'):
|
|
1466
|
+
emit_info("Installation cancelled", message_group=group_id)
|
|
1467
|
+
return False
|
|
1468
|
+
|
|
1469
|
+
# Configure the server with requirements
|
|
1470
|
+
requirements = selected_server.get_requirements()
|
|
1471
|
+
|
|
1472
|
+
# Check system requirements
|
|
1473
|
+
if not self._interactive_check_system_requirements(requirements, group_id):
|
|
1474
|
+
return False
|
|
1037
1475
|
|
|
1038
|
-
#
|
|
1039
|
-
|
|
1476
|
+
# Collect environment variables
|
|
1477
|
+
env_vars = self._interactive_collect_env_vars(requirements, group_id)
|
|
1040
1478
|
|
|
1041
|
-
#
|
|
1042
|
-
|
|
1043
|
-
id=f"{custom_name}_{hash(custom_name)}",
|
|
1044
|
-
name=custom_name,
|
|
1045
|
-
type=config_dict.pop('type'),
|
|
1046
|
-
enabled=True,
|
|
1047
|
-
config=config_dict
|
|
1048
|
-
)
|
|
1479
|
+
# Collect command line arguments
|
|
1480
|
+
cmd_args = self._interactive_collect_cmd_args(requirements, group_id)
|
|
1049
1481
|
|
|
1050
|
-
#
|
|
1051
|
-
|
|
1482
|
+
# Show summary and confirm
|
|
1483
|
+
if not self._interactive_confirm_installation(selected_server, server_name, env_vars, cmd_args, group_id):
|
|
1484
|
+
return False
|
|
1485
|
+
|
|
1486
|
+
# Install the server
|
|
1487
|
+
return self._interactive_install_server(selected_server, server_name, env_vars, cmd_args, group_id)
|
|
1052
1488
|
|
|
1053
|
-
if server_id:
|
|
1054
|
-
emit_info(f"✅ Installed '{custom_name}' from {template.display_name}", message_group=group_id)
|
|
1055
|
-
|
|
1056
|
-
# Save to mcp_servers.json
|
|
1057
|
-
from code_puppy.config import MCP_SERVERS_FILE
|
|
1058
|
-
import os
|
|
1059
|
-
|
|
1060
|
-
if os.path.exists(MCP_SERVERS_FILE):
|
|
1061
|
-
with open(MCP_SERVERS_FILE, 'r') as f:
|
|
1062
|
-
data = json.load(f)
|
|
1063
|
-
servers = data.get("mcp_servers", {})
|
|
1064
|
-
else:
|
|
1065
|
-
servers = {}
|
|
1066
|
-
data = {"mcp_servers": servers}
|
|
1067
|
-
|
|
1068
|
-
servers[custom_name] = config_dict
|
|
1069
|
-
servers[custom_name]['type'] = server_config.type
|
|
1070
|
-
|
|
1071
|
-
os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
|
|
1072
|
-
with open(MCP_SERVERS_FILE, 'w') as f:
|
|
1073
|
-
json.dump(data, f, indent=2)
|
|
1074
|
-
|
|
1075
|
-
# Show next steps
|
|
1076
|
-
if template.example_usage:
|
|
1077
|
-
emit_info(f"[yellow]Example:[/yellow] {template.example_usage}", message_group=group_id)
|
|
1078
|
-
|
|
1079
|
-
# Check for environment variables
|
|
1080
|
-
env_vars = []
|
|
1081
|
-
if 'env' in config_dict:
|
|
1082
|
-
for key, value in config_dict['env'].items():
|
|
1083
|
-
if value.startswith('$'):
|
|
1084
|
-
env_vars.append(value[1:])
|
|
1085
|
-
|
|
1086
|
-
if env_vars:
|
|
1087
|
-
emit_info(f"[yellow]Required environment variables:[/yellow] {', '.join(env_vars)}", message_group=group_id)
|
|
1088
|
-
emit_info("Set these before starting the server", message_group=group_id)
|
|
1089
|
-
|
|
1090
|
-
emit_info(f"Use '/mcp start {custom_name}' to start the server", message_group=group_id)
|
|
1091
|
-
|
|
1092
|
-
# Reload MCP servers
|
|
1093
|
-
from code_puppy.agent import reload_mcp_servers
|
|
1094
|
-
reload_mcp_servers()
|
|
1095
|
-
else:
|
|
1096
|
-
emit_info(f"Failed to install server", message_group=group_id)
|
|
1097
|
-
|
|
1098
1489
|
except ImportError:
|
|
1099
|
-
emit_info("Server
|
|
1490
|
+
emit_info("Server catalog not available", message_group=group_id)
|
|
1491
|
+
return False
|
|
1100
1492
|
except Exception as e:
|
|
1101
|
-
|
|
1102
|
-
emit_info(f"Installation failed: {e}", message_group=group_id)
|
|
1493
|
+
import traceback
|
|
1494
|
+
emit_info(f"❌ Installation failed: {str(e)}", message_group=group_id)
|
|
1495
|
+
emit_info(f"[dim]Error details: {traceback.format_exc()}[/dim]", message_group=group_id)
|
|
1496
|
+
return False
|
|
1103
1497
|
|
|
1104
1498
|
def _find_server_id_by_name(self, server_name: str) -> Optional[str]:
|
|
1105
1499
|
"""
|
|
@@ -1173,20 +1567,20 @@ class MCPCommandHandler:
|
|
|
1173
1567
|
|
|
1174
1568
|
display, color = state_map.get(state, ("? Unk", "dim"))
|
|
1175
1569
|
return Text(display, style=color)
|
|
1176
|
-
|
|
1570
|
+
|
|
1177
1571
|
def _format_uptime(self, uptime_seconds: Optional[float]) -> str:
|
|
1178
1572
|
"""
|
|
1179
1573
|
Format uptime in a human-readable format.
|
|
1180
|
-
|
|
1574
|
+
|
|
1181
1575
|
Args:
|
|
1182
1576
|
uptime_seconds: Uptime in seconds, or None
|
|
1183
|
-
|
|
1577
|
+
|
|
1184
1578
|
Returns:
|
|
1185
1579
|
Formatted uptime string
|
|
1186
1580
|
"""
|
|
1187
1581
|
if uptime_seconds is None or uptime_seconds <= 0:
|
|
1188
1582
|
return "-"
|
|
1189
|
-
|
|
1583
|
+
|
|
1190
1584
|
# Convert to readable format
|
|
1191
1585
|
if uptime_seconds < 60:
|
|
1192
1586
|
return f"{int(uptime_seconds)}s"
|
|
@@ -1198,7 +1592,7 @@ class MCPCommandHandler:
|
|
|
1198
1592
|
hours = int(uptime_seconds // 3600)
|
|
1199
1593
|
minutes = int((uptime_seconds % 3600) // 60)
|
|
1200
1594
|
return f"{hours}h {minutes}m"
|
|
1201
|
-
|
|
1595
|
+
|
|
1202
1596
|
def _show_detailed_server_status(self, server_id: str, server_name: str, group_id: str = None) -> None:
|
|
1203
1597
|
"""
|
|
1204
1598
|
Show comprehensive status information for a specific server.
|
|
@@ -1295,4 +1689,95 @@ class MCPCommandHandler:
|
|
|
1295
1689
|
|
|
1296
1690
|
except Exception as e:
|
|
1297
1691
|
logger.error(f"Error showing detailed status for server '{server_name}': {e}")
|
|
1298
|
-
emit_info(f"Failed to get detailed status: {e}", message_group=group_id)
|
|
1692
|
+
emit_info(f"Failed to get detailed status: {e}", message_group=group_id)
|
|
1693
|
+
|
|
1694
|
+
def _handle_interactive_requirements(self, template, custom_name: str, group_id: str) -> Dict:
|
|
1695
|
+
"""Handle comprehensive requirements in interactive mode."""
|
|
1696
|
+
from code_puppy.messaging import emit_prompt
|
|
1697
|
+
|
|
1698
|
+
requirements = template.get_requirements()
|
|
1699
|
+
config_overrides = {}
|
|
1700
|
+
|
|
1701
|
+
# 1. Check system requirements
|
|
1702
|
+
if requirements.required_tools:
|
|
1703
|
+
emit_info("[bold cyan]Checking system requirements...[/bold cyan]", message_group=group_id)
|
|
1704
|
+
from code_puppy.mcp.system_tools import detector
|
|
1705
|
+
|
|
1706
|
+
tool_status = detector.detect_tools(requirements.required_tools)
|
|
1707
|
+
missing_tools = []
|
|
1708
|
+
|
|
1709
|
+
for tool_name, tool_info in tool_status.items():
|
|
1710
|
+
if tool_info.available:
|
|
1711
|
+
emit_info(f"✅ {tool_name} ({tool_info.version or 'found'})", message_group=group_id)
|
|
1712
|
+
else:
|
|
1713
|
+
emit_info(f"❌ {tool_name} - {tool_info.error}", message_group=group_id)
|
|
1714
|
+
missing_tools.append(tool_name)
|
|
1715
|
+
|
|
1716
|
+
if missing_tools:
|
|
1717
|
+
emit_info(f"[red]Missing required tools: {', '.join(missing_tools)}[/red]", message_group=group_id)
|
|
1718
|
+
|
|
1719
|
+
# Show installation suggestions
|
|
1720
|
+
for tool in missing_tools:
|
|
1721
|
+
suggestions = detector.get_installation_suggestions(tool)
|
|
1722
|
+
emit_info(f"Install {tool}: {suggestions[0]}", message_group=group_id)
|
|
1723
|
+
|
|
1724
|
+
proceed = emit_prompt("Continue installation anyway? (y/N): ")
|
|
1725
|
+
if proceed.lower() not in ['y', 'yes']:
|
|
1726
|
+
raise Exception("Installation cancelled due to missing requirements")
|
|
1727
|
+
|
|
1728
|
+
# 2. Environment variables
|
|
1729
|
+
env_vars = template.get_environment_vars()
|
|
1730
|
+
if env_vars:
|
|
1731
|
+
emit_info("[bold yellow]Environment Variables:[/bold yellow]", message_group=group_id)
|
|
1732
|
+
|
|
1733
|
+
for var in env_vars:
|
|
1734
|
+
import os
|
|
1735
|
+
if var in os.environ:
|
|
1736
|
+
emit_info(f"✅ {var} (already set)", message_group=group_id)
|
|
1737
|
+
else:
|
|
1738
|
+
try:
|
|
1739
|
+
value = emit_prompt(f"Enter {var}: ")
|
|
1740
|
+
if value.strip():
|
|
1741
|
+
os.environ[var] = value.strip()
|
|
1742
|
+
emit_info(f"[green]Set {var}[/green]", message_group=group_id)
|
|
1743
|
+
else:
|
|
1744
|
+
emit_info(f"[yellow]Skipped {var} (empty value)[/yellow]", message_group=group_id)
|
|
1745
|
+
except Exception as e:
|
|
1746
|
+
emit_info(f"[yellow]Failed to get {var}: {e}[/yellow]", message_group=group_id)
|
|
1747
|
+
|
|
1748
|
+
# 3. Command line arguments
|
|
1749
|
+
cmd_args = requirements.command_line_args
|
|
1750
|
+
if cmd_args:
|
|
1751
|
+
emit_info("[bold green]Command Line Arguments:[/bold green]", message_group=group_id)
|
|
1752
|
+
|
|
1753
|
+
for arg_config in cmd_args:
|
|
1754
|
+
name = arg_config.get("name", "")
|
|
1755
|
+
prompt_text = arg_config.get("prompt", name)
|
|
1756
|
+
default = arg_config.get("default", "")
|
|
1757
|
+
required = arg_config.get("required", True)
|
|
1758
|
+
|
|
1759
|
+
try:
|
|
1760
|
+
if default:
|
|
1761
|
+
value = emit_prompt(f"{prompt_text} (default: {default}): ")
|
|
1762
|
+
value = value.strip() or default
|
|
1763
|
+
else:
|
|
1764
|
+
value = emit_prompt(f"{prompt_text}: ")
|
|
1765
|
+
value = value.strip()
|
|
1766
|
+
|
|
1767
|
+
if value:
|
|
1768
|
+
config_overrides[name] = value
|
|
1769
|
+
emit_info(f"[green]Set {name}={value}[/green]", message_group=group_id)
|
|
1770
|
+
elif required:
|
|
1771
|
+
emit_info(f"[yellow]Required argument {name} not provided[/yellow]", message_group=group_id)
|
|
1772
|
+
|
|
1773
|
+
except Exception as e:
|
|
1774
|
+
emit_info(f"[yellow]Failed to get {name}: {e}[/yellow]", message_group=group_id)
|
|
1775
|
+
|
|
1776
|
+
# 4. Package dependencies (informational)
|
|
1777
|
+
packages = requirements.package_dependencies
|
|
1778
|
+
if packages:
|
|
1779
|
+
emit_info("[bold magenta]Package Dependencies:[/bold magenta]", message_group=group_id)
|
|
1780
|
+
emit_info(f"This server requires: {', '.join(packages)}", message_group=group_id)
|
|
1781
|
+
emit_info("These will be installed automatically when the server starts.", message_group=group_id)
|
|
1782
|
+
|
|
1783
|
+
return config_overrides
|