nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
nc1709/logger.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Structured logging for NC1709 CLI
|
|
3
|
+
Provides consistent, configurable logging across all modules
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NC1709Logger:
|
|
13
|
+
"""Custom logger with colored console output and file logging"""
|
|
14
|
+
|
|
15
|
+
# ANSI color codes
|
|
16
|
+
COLORS = {
|
|
17
|
+
"DEBUG": "\033[36m", # Cyan
|
|
18
|
+
"INFO": "\033[32m", # Green
|
|
19
|
+
"WARNING": "\033[33m", # Yellow
|
|
20
|
+
"ERROR": "\033[31m", # Red
|
|
21
|
+
"CRITICAL": "\033[35m", # Magenta
|
|
22
|
+
"RESET": "\033[0m"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Emoji prefixes for different log levels
|
|
26
|
+
EMOJIS = {
|
|
27
|
+
"DEBUG": "🔍",
|
|
28
|
+
"INFO": "✅",
|
|
29
|
+
"WARNING": "⚠️",
|
|
30
|
+
"ERROR": "❌",
|
|
31
|
+
"CRITICAL": "🚨"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
name: str = "nc1709",
|
|
37
|
+
level: int = logging.INFO,
|
|
38
|
+
log_file: Optional[str] = None,
|
|
39
|
+
use_colors: bool = True,
|
|
40
|
+
use_emojis: bool = True
|
|
41
|
+
):
|
|
42
|
+
"""Initialize the logger
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
name: Logger name
|
|
46
|
+
level: Logging level
|
|
47
|
+
log_file: Optional path to log file
|
|
48
|
+
use_colors: Whether to use colored output
|
|
49
|
+
use_emojis: Whether to use emoji prefixes
|
|
50
|
+
"""
|
|
51
|
+
self.logger = logging.getLogger(name)
|
|
52
|
+
self.logger.setLevel(level)
|
|
53
|
+
self.logger.handlers = [] # Clear any existing handlers
|
|
54
|
+
|
|
55
|
+
self.use_colors = use_colors
|
|
56
|
+
self.use_emojis = use_emojis
|
|
57
|
+
|
|
58
|
+
# Console handler
|
|
59
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
60
|
+
console_handler.setLevel(level)
|
|
61
|
+
console_handler.setFormatter(self._create_formatter(colored=use_colors))
|
|
62
|
+
self.logger.addHandler(console_handler)
|
|
63
|
+
|
|
64
|
+
# File handler (if specified)
|
|
65
|
+
if log_file:
|
|
66
|
+
log_path = Path(log_file).expanduser()
|
|
67
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
file_handler = logging.FileHandler(log_path)
|
|
69
|
+
file_handler.setLevel(logging.DEBUG) # Log everything to file
|
|
70
|
+
file_handler.setFormatter(self._create_formatter(colored=False))
|
|
71
|
+
self.logger.addHandler(file_handler)
|
|
72
|
+
|
|
73
|
+
def _create_formatter(self, colored: bool = False) -> logging.Formatter:
|
|
74
|
+
"""Create a log formatter
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
colored: Whether to include color codes
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Logging formatter
|
|
81
|
+
"""
|
|
82
|
+
if colored:
|
|
83
|
+
return ColoredFormatter(self.COLORS, self.EMOJIS, self.use_emojis)
|
|
84
|
+
else:
|
|
85
|
+
return logging.Formatter(
|
|
86
|
+
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
87
|
+
datefmt="%Y-%m-%d %H:%M:%S"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def debug(self, msg: str, *args, **kwargs):
|
|
91
|
+
"""Log debug message"""
|
|
92
|
+
self.logger.debug(msg, *args, **kwargs)
|
|
93
|
+
|
|
94
|
+
def info(self, msg: str, *args, **kwargs):
|
|
95
|
+
"""Log info message"""
|
|
96
|
+
self.logger.info(msg, *args, **kwargs)
|
|
97
|
+
|
|
98
|
+
def warning(self, msg: str, *args, **kwargs):
|
|
99
|
+
"""Log warning message"""
|
|
100
|
+
self.logger.warning(msg, *args, **kwargs)
|
|
101
|
+
|
|
102
|
+
def error(self, msg: str, *args, **kwargs):
|
|
103
|
+
"""Log error message"""
|
|
104
|
+
self.logger.error(msg, *args, **kwargs)
|
|
105
|
+
|
|
106
|
+
def critical(self, msg: str, *args, **kwargs):
|
|
107
|
+
"""Log critical message"""
|
|
108
|
+
self.logger.critical(msg, *args, **kwargs)
|
|
109
|
+
|
|
110
|
+
def set_level(self, level: int):
|
|
111
|
+
"""Set logging level"""
|
|
112
|
+
self.logger.setLevel(level)
|
|
113
|
+
for handler in self.logger.handlers:
|
|
114
|
+
if isinstance(handler, logging.StreamHandler):
|
|
115
|
+
handler.setLevel(level)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ColoredFormatter(logging.Formatter):
|
|
119
|
+
"""Custom formatter with colors and emojis"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, colors: dict, emojis: dict, use_emojis: bool = True):
|
|
122
|
+
super().__init__()
|
|
123
|
+
self.colors = colors
|
|
124
|
+
self.emojis = emojis
|
|
125
|
+
self.use_emojis = use_emojis
|
|
126
|
+
|
|
127
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
128
|
+
level = record.levelname
|
|
129
|
+
color = self.colors.get(level, "")
|
|
130
|
+
reset = self.colors.get("RESET", "")
|
|
131
|
+
emoji = self.emojis.get(level, "") if self.use_emojis else ""
|
|
132
|
+
|
|
133
|
+
# Format: emoji [LEVEL] message
|
|
134
|
+
if emoji:
|
|
135
|
+
formatted = f"{emoji} {color}[{level}]{reset} {record.getMessage()}"
|
|
136
|
+
else:
|
|
137
|
+
formatted = f"{color}[{level}]{reset} {record.getMessage()}"
|
|
138
|
+
|
|
139
|
+
return formatted
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Global logger instance
|
|
143
|
+
_logger: Optional[NC1709Logger] = None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_logger(
|
|
147
|
+
name: str = "nc1709",
|
|
148
|
+
level: Optional[int] = None,
|
|
149
|
+
log_file: Optional[str] = None
|
|
150
|
+
) -> NC1709Logger:
|
|
151
|
+
"""Get or create the global logger instance
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: Logger name
|
|
155
|
+
level: Logging level (default: INFO)
|
|
156
|
+
log_file: Optional log file path
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
NC1709Logger instance
|
|
160
|
+
"""
|
|
161
|
+
global _logger
|
|
162
|
+
|
|
163
|
+
if _logger is None:
|
|
164
|
+
from .config import get_config
|
|
165
|
+
config = get_config()
|
|
166
|
+
|
|
167
|
+
# Get settings from config
|
|
168
|
+
verbose = config.get("ui.verbose", False)
|
|
169
|
+
use_colors = config.get("ui.color", True)
|
|
170
|
+
log_dir = Path.home() / ".nc1709" / "logs"
|
|
171
|
+
|
|
172
|
+
if level is None:
|
|
173
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
174
|
+
|
|
175
|
+
if log_file is None:
|
|
176
|
+
log_file = str(log_dir / f"nc1709_{datetime.now().strftime('%Y%m%d')}.log")
|
|
177
|
+
|
|
178
|
+
_logger = NC1709Logger(
|
|
179
|
+
name=name,
|
|
180
|
+
level=level,
|
|
181
|
+
log_file=log_file,
|
|
182
|
+
use_colors=use_colors,
|
|
183
|
+
use_emojis=True
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return _logger
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def reset_logger():
|
|
190
|
+
"""Reset the global logger instance"""
|
|
191
|
+
global _logger
|
|
192
|
+
_logger = None
|
nc1709/mcp/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NC1709 MCP (Model Context Protocol) Support
|
|
3
|
+
Enables integration with external tools via the MCP standard
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .server import MCPServer
|
|
7
|
+
from .client import MCPClient
|
|
8
|
+
from .protocol import MCPMessage, MCPTool, MCPResource
|
|
9
|
+
from .manager import MCPManager
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"MCPServer",
|
|
13
|
+
"MCPClient",
|
|
14
|
+
"MCPMessage",
|
|
15
|
+
"MCPTool",
|
|
16
|
+
"MCPResource",
|
|
17
|
+
"MCPManager"
|
|
18
|
+
]
|
nc1709/mcp/client.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Client Implementation
|
|
3
|
+
Connects to external MCP servers to access their tools
|
|
4
|
+
"""
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .protocol import MCPMessage, MCPTool, MCPResource, MCPErrorCode
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class MCPServerConnection:
|
|
17
|
+
"""Represents a connection to an MCP server"""
|
|
18
|
+
name: str
|
|
19
|
+
command: str
|
|
20
|
+
args: List[str] = field(default_factory=list)
|
|
21
|
+
env: Dict[str, str] = field(default_factory=dict)
|
|
22
|
+
process: Optional[subprocess.Popen] = None
|
|
23
|
+
tools: List[MCPTool] = field(default_factory=list)
|
|
24
|
+
resources: List[MCPResource] = field(default_factory=list)
|
|
25
|
+
connected: bool = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MCPClient:
|
|
29
|
+
"""
|
|
30
|
+
MCP Client for NC1709.
|
|
31
|
+
|
|
32
|
+
Connects to external MCP servers and makes their tools
|
|
33
|
+
available within NC1709.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
"""Initialize the MCP client"""
|
|
38
|
+
self._servers: Dict[str, MCPServerConnection] = {}
|
|
39
|
+
self._request_id = 0
|
|
40
|
+
self._pending_requests: Dict[int, asyncio.Future] = {}
|
|
41
|
+
|
|
42
|
+
def _next_id(self) -> int:
|
|
43
|
+
"""Get next request ID"""
|
|
44
|
+
self._request_id += 1
|
|
45
|
+
return self._request_id
|
|
46
|
+
|
|
47
|
+
async def connect(
|
|
48
|
+
self,
|
|
49
|
+
name: str,
|
|
50
|
+
command: str,
|
|
51
|
+
args: Optional[List[str]] = None,
|
|
52
|
+
env: Optional[Dict[str, str]] = None
|
|
53
|
+
) -> bool:
|
|
54
|
+
"""Connect to an MCP server
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
name: Server name
|
|
58
|
+
command: Command to start the server
|
|
59
|
+
args: Command arguments
|
|
60
|
+
env: Environment variables
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if connected successfully
|
|
64
|
+
"""
|
|
65
|
+
if name in self._servers and self._servers[name].connected:
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
args = args or []
|
|
69
|
+
env = env or {}
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Start the server process
|
|
73
|
+
full_env = {**dict(subprocess.os.environ), **env}
|
|
74
|
+
process = subprocess.Popen(
|
|
75
|
+
[command] + args,
|
|
76
|
+
stdin=subprocess.PIPE,
|
|
77
|
+
stdout=subprocess.PIPE,
|
|
78
|
+
stderr=subprocess.PIPE,
|
|
79
|
+
env=full_env
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
connection = MCPServerConnection(
|
|
83
|
+
name=name,
|
|
84
|
+
command=command,
|
|
85
|
+
args=args,
|
|
86
|
+
env=env,
|
|
87
|
+
process=process
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
self._servers[name] = connection
|
|
91
|
+
|
|
92
|
+
# Initialize the connection
|
|
93
|
+
init_response = await self._send_request(
|
|
94
|
+
name,
|
|
95
|
+
"initialize",
|
|
96
|
+
{
|
|
97
|
+
"protocolVersion": "2024-11-05",
|
|
98
|
+
"clientInfo": {
|
|
99
|
+
"name": "nc1709",
|
|
100
|
+
"version": "1.0.0"
|
|
101
|
+
},
|
|
102
|
+
"capabilities": {}
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if init_response and "error" not in init_response:
|
|
107
|
+
connection.connected = True
|
|
108
|
+
|
|
109
|
+
# Fetch available tools
|
|
110
|
+
tools_response = await self._send_request(name, "tools/list", {})
|
|
111
|
+
if tools_response and "tools" in tools_response:
|
|
112
|
+
connection.tools = [
|
|
113
|
+
MCPTool(
|
|
114
|
+
name=t["name"],
|
|
115
|
+
description=t.get("description", ""),
|
|
116
|
+
parameters=[] # Parse inputSchema if needed
|
|
117
|
+
)
|
|
118
|
+
for t in tools_response["tools"]
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
# Fetch available resources
|
|
122
|
+
resources_response = await self._send_request(name, "resources/list", {})
|
|
123
|
+
if resources_response and "resources" in resources_response:
|
|
124
|
+
connection.resources = [
|
|
125
|
+
MCPResource(
|
|
126
|
+
uri=r["uri"],
|
|
127
|
+
name=r["name"],
|
|
128
|
+
description=r.get("description", "")
|
|
129
|
+
)
|
|
130
|
+
for r in resources_response["resources"]
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"Failed to connect to MCP server {name}: {e}")
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
async def disconnect(self, name: str) -> bool:
|
|
142
|
+
"""Disconnect from an MCP server
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
name: Server name
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
True if disconnected
|
|
149
|
+
"""
|
|
150
|
+
if name not in self._servers:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
connection = self._servers[name]
|
|
154
|
+
|
|
155
|
+
if connection.process:
|
|
156
|
+
connection.process.terminate()
|
|
157
|
+
try:
|
|
158
|
+
connection.process.wait(timeout=5)
|
|
159
|
+
except subprocess.TimeoutExpired:
|
|
160
|
+
connection.process.kill()
|
|
161
|
+
|
|
162
|
+
connection.connected = False
|
|
163
|
+
del self._servers[name]
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
async def disconnect_all(self) -> None:
|
|
167
|
+
"""Disconnect from all servers"""
|
|
168
|
+
for name in list(self._servers.keys()):
|
|
169
|
+
await self.disconnect(name)
|
|
170
|
+
|
|
171
|
+
async def call_tool(
|
|
172
|
+
self,
|
|
173
|
+
server_name: str,
|
|
174
|
+
tool_name: str,
|
|
175
|
+
arguments: Optional[Dict[str, Any]] = None
|
|
176
|
+
) -> Dict[str, Any]:
|
|
177
|
+
"""Call a tool on an MCP server
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
server_name: Server name
|
|
181
|
+
tool_name: Tool name
|
|
182
|
+
arguments: Tool arguments
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Tool result
|
|
186
|
+
"""
|
|
187
|
+
if server_name not in self._servers:
|
|
188
|
+
return {"error": f"Server not connected: {server_name}"}
|
|
189
|
+
|
|
190
|
+
connection = self._servers[server_name]
|
|
191
|
+
if not connection.connected:
|
|
192
|
+
return {"error": f"Server not connected: {server_name}"}
|
|
193
|
+
|
|
194
|
+
response = await self._send_request(
|
|
195
|
+
server_name,
|
|
196
|
+
"tools/call",
|
|
197
|
+
{
|
|
198
|
+
"name": tool_name,
|
|
199
|
+
"arguments": arguments or {}
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return response or {"error": "No response from server"}
|
|
204
|
+
|
|
205
|
+
async def read_resource(
|
|
206
|
+
self,
|
|
207
|
+
server_name: str,
|
|
208
|
+
uri: str
|
|
209
|
+
) -> Dict[str, Any]:
|
|
210
|
+
"""Read a resource from an MCP server
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
server_name: Server name
|
|
214
|
+
uri: Resource URI
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Resource contents
|
|
218
|
+
"""
|
|
219
|
+
if server_name not in self._servers:
|
|
220
|
+
return {"error": f"Server not connected: {server_name}"}
|
|
221
|
+
|
|
222
|
+
response = await self._send_request(
|
|
223
|
+
server_name,
|
|
224
|
+
"resources/read",
|
|
225
|
+
{"uri": uri}
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return response or {"error": "No response from server"}
|
|
229
|
+
|
|
230
|
+
def list_servers(self) -> List[Dict[str, Any]]:
|
|
231
|
+
"""List connected servers
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
List of server info dicts
|
|
235
|
+
"""
|
|
236
|
+
return [
|
|
237
|
+
{
|
|
238
|
+
"name": name,
|
|
239
|
+
"connected": conn.connected,
|
|
240
|
+
"tools": len(conn.tools),
|
|
241
|
+
"resources": len(conn.resources)
|
|
242
|
+
}
|
|
243
|
+
for name, conn in self._servers.items()
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
def get_tools(self, server_name: Optional[str] = None) -> List[MCPTool]:
|
|
247
|
+
"""Get available tools
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
server_name: Filter by server (None for all)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of tools
|
|
254
|
+
"""
|
|
255
|
+
if server_name:
|
|
256
|
+
if server_name in self._servers:
|
|
257
|
+
return self._servers[server_name].tools
|
|
258
|
+
return []
|
|
259
|
+
|
|
260
|
+
all_tools = []
|
|
261
|
+
for conn in self._servers.values():
|
|
262
|
+
all_tools.extend(conn.tools)
|
|
263
|
+
return all_tools
|
|
264
|
+
|
|
265
|
+
def get_resources(self, server_name: Optional[str] = None) -> List[MCPResource]:
|
|
266
|
+
"""Get available resources
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
server_name: Filter by server (None for all)
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of resources
|
|
273
|
+
"""
|
|
274
|
+
if server_name:
|
|
275
|
+
if server_name in self._servers:
|
|
276
|
+
return self._servers[server_name].resources
|
|
277
|
+
return []
|
|
278
|
+
|
|
279
|
+
all_resources = []
|
|
280
|
+
for conn in self._servers.values():
|
|
281
|
+
all_resources.extend(conn.resources)
|
|
282
|
+
return all_resources
|
|
283
|
+
|
|
284
|
+
async def _send_request(
|
|
285
|
+
self,
|
|
286
|
+
server_name: str,
|
|
287
|
+
method: str,
|
|
288
|
+
params: Dict[str, Any]
|
|
289
|
+
) -> Optional[Dict[str, Any]]:
|
|
290
|
+
"""Send a request to an MCP server
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
server_name: Server name
|
|
294
|
+
method: Method name
|
|
295
|
+
params: Request parameters
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Response data or None
|
|
299
|
+
"""
|
|
300
|
+
if server_name not in self._servers:
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
connection = self._servers[server_name]
|
|
304
|
+
if not connection.process:
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
request_id = self._next_id()
|
|
308
|
+
request = MCPMessage.request(request_id, method, params)
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
# Send request
|
|
312
|
+
request_json = request.to_json() + "\n"
|
|
313
|
+
connection.process.stdin.write(request_json.encode())
|
|
314
|
+
connection.process.stdin.flush()
|
|
315
|
+
|
|
316
|
+
# Read response (with timeout)
|
|
317
|
+
# Note: This is a simplified sync implementation
|
|
318
|
+
# A production version would use proper async I/O
|
|
319
|
+
response_line = connection.process.stdout.readline()
|
|
320
|
+
if response_line:
|
|
321
|
+
response = MCPMessage.from_json(response_line.decode())
|
|
322
|
+
if response.error:
|
|
323
|
+
return {"error": response.error}
|
|
324
|
+
return response.result
|
|
325
|
+
|
|
326
|
+
except Exception as e:
|
|
327
|
+
print(f"Error communicating with MCP server {server_name}: {e}")
|
|
328
|
+
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
async def auto_discover(self, config_path: Optional[str] = None) -> int:
|
|
332
|
+
"""Auto-discover MCP servers from configuration
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
config_path: Path to MCP config file
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Number of servers discovered
|
|
339
|
+
"""
|
|
340
|
+
if config_path is None:
|
|
341
|
+
# Look for standard config locations
|
|
342
|
+
config_paths = [
|
|
343
|
+
Path.home() / ".nc1709" / "mcp.json",
|
|
344
|
+
Path.cwd() / ".nc1709" / "mcp.json",
|
|
345
|
+
Path.cwd() / "mcp.json"
|
|
346
|
+
]
|
|
347
|
+
else:
|
|
348
|
+
config_paths = [Path(config_path)]
|
|
349
|
+
|
|
350
|
+
count = 0
|
|
351
|
+
|
|
352
|
+
for path in config_paths:
|
|
353
|
+
if path.exists():
|
|
354
|
+
try:
|
|
355
|
+
config = json.loads(path.read_text())
|
|
356
|
+
servers = config.get("mcpServers", {})
|
|
357
|
+
|
|
358
|
+
for name, server_config in servers.items():
|
|
359
|
+
command = server_config.get("command")
|
|
360
|
+
args = server_config.get("args", [])
|
|
361
|
+
env = server_config.get("env", {})
|
|
362
|
+
|
|
363
|
+
if command:
|
|
364
|
+
if await self.connect(name, command, args, env):
|
|
365
|
+
count += 1
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
print(f"Error loading MCP config from {path}: {e}")
|
|
369
|
+
|
|
370
|
+
return count
|