regcode 0.9.0__tar.gz → 0.10.0__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 (50) hide show
  1. {regcode-0.9.0 → regcode-0.10.0}/GOALS.md +39 -0
  2. {regcode-0.9.0 → regcode-0.10.0}/PKG-INFO +2 -1
  3. regcode-0.10.0/config.yaml.template +56 -0
  4. {regcode-0.9.0 → regcode-0.10.0}/pyproject.toml +2 -1
  5. {regcode-0.9.0 → regcode-0.10.0}/regcode/__init__.py +1 -1
  6. {regcode-0.9.0 → regcode-0.10.0}/regcode/cli.py +84 -1
  7. {regcode-0.9.0 → regcode-0.10.0}/regcode/config.py +36 -0
  8. {regcode-0.9.0 → regcode-0.10.0}/regcode/main.py +112 -1
  9. regcode-0.10.0/regcode/mcp_client.py +595 -0
  10. {regcode-0.9.0 → regcode-0.10.0}/regcode/tui.py +13 -0
  11. {regcode-0.9.0 → regcode-0.10.0}/tests/test_cli.py +1 -0
  12. regcode-0.10.0/tests/test_mcp.py +376 -0
  13. regcode-0.10.0/tests/test_mcp_status.py +188 -0
  14. {regcode-0.9.0 → regcode-0.10.0}/uv.lock +109 -10
  15. regcode-0.9.0/config.yaml.template +0 -16
  16. {regcode-0.9.0 → regcode-0.10.0}/.gitignore +0 -0
  17. {regcode-0.9.0 → regcode-0.10.0}/.gitlab-ci.yml +0 -0
  18. {regcode-0.9.0 → regcode-0.10.0}/.python-version +0 -0
  19. {regcode-0.9.0 → regcode-0.10.0}/.qwen/settings.json +0 -0
  20. {regcode-0.9.0 → regcode-0.10.0}/AGENTS.md +0 -0
  21. {regcode-0.9.0 → regcode-0.10.0}/README.md +0 -0
  22. {regcode-0.9.0 → regcode-0.10.0}/main.py +0 -0
  23. {regcode-0.9.0 → regcode-0.10.0}/regcode/clipboard.py +0 -0
  24. {regcode-0.9.0 → regcode-0.10.0}/regcode/conversation_manager.py +0 -0
  25. {regcode-0.9.0 → regcode-0.10.0}/regcode/dependencies.py +0 -0
  26. {regcode-0.9.0 → regcode-0.10.0}/regcode/monty_sandbox.py +0 -0
  27. {regcode-0.9.0 → regcode-0.10.0}/regcode/paste.py +0 -0
  28. {regcode-0.9.0 → regcode-0.10.0}/regcode/permissions.py +0 -0
  29. {regcode-0.9.0 → regcode-0.10.0}/regcode/sandbox.py +0 -0
  30. {regcode-0.9.0 → regcode-0.10.0}/regcode/tools/__init__.py +0 -0
  31. {regcode-0.9.0 → regcode-0.10.0}/regcode/tools/base.py +0 -0
  32. {regcode-0.9.0 → regcode-0.10.0}/regcode/tools/builtins.py +0 -0
  33. {regcode-0.9.0 → regcode-0.10.0}/regcode/tools/registry.py +0 -0
  34. {regcode-0.9.0 → regcode-0.10.0}/regcode/tools/review_notes.py +0 -0
  35. {regcode-0.9.0 → regcode-0.10.0}/regcode/tools/web_crawl.py +0 -0
  36. {regcode-0.9.0 → regcode-0.10.0}/regcode/tools/web_search.py +0 -0
  37. {regcode-0.9.0 → regcode-0.10.0}/regcode/tracing.py +0 -0
  38. {regcode-0.9.0 → regcode-0.10.0}/tests/__init__.py +0 -0
  39. {regcode-0.9.0 → regcode-0.10.0}/tests/conftest.py +0 -0
  40. {regcode-0.9.0 → regcode-0.10.0}/tests/test_api.py +0 -0
  41. {regcode-0.9.0 → regcode-0.10.0}/tests/test_clipboard.py +0 -0
  42. {regcode-0.9.0 → regcode-0.10.0}/tests/test_config.py +0 -0
  43. {regcode-0.9.0 → regcode-0.10.0}/tests/test_context_manager.py +0 -0
  44. {regcode-0.9.0 → regcode-0.10.0}/tests/test_dependencies.py +0 -0
  45. {regcode-0.9.0 → regcode-0.10.0}/tests/test_main.py +0 -0
  46. {regcode-0.9.0 → regcode-0.10.0}/tests/test_monty_sandbox.py +0 -0
  47. {regcode-0.9.0 → regcode-0.10.0}/tests/test_osv_cve.py +0 -0
  48. {regcode-0.9.0 → regcode-0.10.0}/tests/test_provider_extra.py +0 -0
  49. {regcode-0.9.0 → regcode-0.10.0}/tests/test_review_notes.py +0 -0
  50. {regcode-0.9.0 → regcode-0.10.0}/tests/test_web_search.py +0 -0
@@ -505,6 +505,45 @@ All phases complete. 50/50 tests passing. Lint clean.
505
505
 
506
506
  - ✅ WebSearchTool: connects to SearXNG, defaults to https://search.regmi.dev, no extra deps needed
507
507
 
508
+ ## MCP Support (2026-07-05)
509
+
510
+ - ✅ `mcp` package added to `pyproject.toml` dependencies
511
+ - ✅ `McpServerConfig` Pydantic model in `regcode/config.py` — supports both stdio (command+args+env) and HTTP (url+headers) transports, with per-tool timeout
512
+ - ✅ `regcode/mcp_client.py` — MCP client with two transport strategies:
513
+ • **stdio** — uses MCP library's `stdio_client` for command-based servers (npx, uvx)
514
+ • **HTTP blocking JSON-RPC** — uses `httpx.Client` directly for servers that return 200 OK with JSON body (not 202 + SSE). Supports session-ID propagation via `mcp-session-id` header, credential injection via custom headers.
515
+ • Discovers tools via `tools/list` JSON-RPC call, registers them into Agent's `ToolRegistry` with `mcp_{server}_{tool}` naming
516
+ • Credential redaction in error messages
517
+ - ✅ `Agent.__init__` calls `_discover_mcp_tools()` at startup — reads `mcp_servers` from config, connects to each server, registers tools. Gracefully skips if `mcp` package not installed or connection fails.
518
+ - ✅ `Agent.reload_mcp()` method — hot-reloads MCP connections (closes old, discovers new) without restart
519
+ - ✅ `Agent._mcp_status()` — returns formatted text showing all MCP servers, their status, and discovered tools
520
+ - ✅ `Agent.cleanup()` includes MCP session close
521
+ - ✅ `McpClient.get_server_info()` — returns introspection data for all configured servers (name, transport, connected status, error, tool list with params)
522
+ - ✅ `/mcp` slash command in TUI — lists all configured MCP servers, shows connection status, enumerates discovered tools with descriptions and parameters
523
+ - ✅ CLI `configure` command generates `mcp_servers` section in config.yaml (interactive prompt for stdio/HTTP servers)
524
+ - ✅ `config.yaml.template` includes commented MCP server examples for stdio (npx, uvx) and HTTP transport
525
+ - ✅ 31 MCP config tests: `TestMcpServerConfig`, `TestConfigMcpServers`, `TestSanitizeName`, `TestRedactCredentials`, `TestMcpClientAgentIntegration`, `TestMcpClientConnectAll`
526
+ - ✅ 7 MCP status tests: `test_get_server_info_*`, `test_mcp_status_format*`
527
+ - ✅ All 231 tests pass, lint clean
528
+
529
+ Example config.yaml MCP section:
530
+ ```yaml
531
+ mcp_servers:
532
+ github:
533
+ command: npx
534
+ args: ["-y", "@modelcontextprotocol/server-github"]
535
+ env:
536
+ GITHUB_TOKEN: ghp_xxxxx
537
+ timeout: 60
538
+ filesystem:
539
+ command: uvx
540
+ args: ["mcp-server-filesystem", "/tmp"]
541
+ remote_api:
542
+ url: https://mcp.example.com/v1/mcp
543
+ headers:
544
+ Authorization: Bearer sk-xxxxx
545
+ ```
546
+
508
547
  ## Completed (2026-06-30 — TUI fixes)
509
548
 
510
549
  1. ✅ AGENTS.md injection — Agent now reads AGENTS.md/agents.md from CWD or allowed directory and injects it into system prompt (both `_build_system_prompt` and `_build_system_prompt_no_tools`). Appended after base system_prompt, before tool descriptions.
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regcode
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: Minimalistic coding agent with Python API and CLI
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: boto3>=1.43.40
7
7
  Requires-Dist: click>=8.0
8
8
  Requires-Dist: crawl4ai>=0.4.0
9
9
  Requires-Dist: litellm>=1.83.17
10
+ Requires-Dist: mcp>=1.0.0
10
11
  Requires-Dist: mlflow>=3.14.0
11
12
  Requires-Dist: pathspec>=1.1.1
12
13
  Requires-Dist: pydantic-monty>=0.0.18
@@ -0,0 +1,56 @@
1
+ agent:
2
+ # Context window management
3
+ enable_compaction: true
4
+ compaction_threshold: 0.8
5
+ context_window: 128000
6
+
7
+ provider:
8
+ api_key: "${OPENAI_API_KEY}"
9
+ model: "openai/gpt-4o"
10
+ max_tokens: 4096
11
+ temperature: 0.7
12
+
13
+ # ── MCP Servers (optional) ──────────────────────────────────────
14
+ #
15
+ # Add MCP servers to extend the agent with external tools.
16
+ # The agent connects on startup, discovers available tools,
17
+ # and registers them as `mcp_{server}_{tool_name}`.
18
+ #
19
+ # Two transport types are supported:
20
+ #
21
+ # 1. Stdio (command-based, e.g. npx, uvx):
22
+ # command — executable to run
23
+ # args — arguments passed to the command
24
+ # env — environment variables (only these are passed,
25
+ # NOT the full shell environment)
26
+ #
27
+ # 2. HTTP (StreamableHTTP URL):
28
+ # url — server endpoint
29
+ # headers — HTTP headers sent with each request
30
+ #
31
+ # Example:
32
+
33
+ mcp_servers:
34
+ # GitHub server — uses npx to run the official MCP server
35
+ #github:
36
+ # command: "npx"
37
+ # args: ["-y", "@modelcontextprotocol/server-github"]
38
+ # env:
39
+ # GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}"
40
+ # timeout: 60
41
+
42
+ # Filesystem server — lets the agent read/write arbitrary files
43
+ # filesystem:
44
+ # command: "uvx"
45
+ # args: ["mcp-server-filesystem", "/tmp"]
46
+
47
+ # Remote HTTP server
48
+ # remote_api:
49
+ # url: "https://mcp.example.com/v1/mcp"
50
+ # headers:
51
+ # Authorization: "Bearer ${MCP_API_KEY}"
52
+
53
+ tools:
54
+ code_review: true
55
+ security_scan: false
56
+ sandbox: false
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "regcode"
3
- version = "0.9.0"
3
+ version = "0.10.0"
4
4
  description = "Minimalistic coding agent with Python API and CLI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -18,6 +18,7 @@ dependencies = [
18
18
  "pathspec>=1.1.1",
19
19
  "pyperclip>=1.9.0",
20
20
  "boto3>=1.43.40",
21
+ "mcp>=1.0.0",
21
22
  ]
22
23
 
23
24
  [project.scripts]
@@ -2,4 +2,4 @@ from regcode.config import Config
2
2
  from regcode.main import Agent, AgentStatus, ToolCall
3
3
 
4
4
  __all__ = ["Agent", "Config", "AgentStatus", "ToolCall"]
5
- __version__ = "0.9.0"
5
+ __version__ = "0.10.0"
@@ -139,8 +139,84 @@ def configure(output_path):
139
139
  else:
140
140
  click.echo(" Using default provider settings.")
141
141
 
142
+ # --- MCP servers config ---
143
+ yes = _confirm("Configure MCP servers? (optional)")
144
+ if yes:
145
+ mcp_section: dict[str, dict] = {}
146
+ while True:
147
+ server_name = click.prompt(
148
+ "Server name (e.g. github, filesystem)",
149
+ default="",
150
+ )
151
+ if not server_name:
152
+ break
153
+
154
+ transport = click.prompt(
155
+ "Transport type",
156
+ type=click.Choice(["stdio", "http"]),
157
+ default="stdio",
158
+ )
159
+
160
+ if transport == "stdio":
161
+ command = click.prompt("Command (e.g. npx, uvx)")
162
+ args_str = click.prompt(
163
+ "Arguments (comma-separated, e.g. -y,package-name)",
164
+ default="",
165
+ )
166
+ args = [
167
+ a.strip()
168
+ for a in args_str.split(",")
169
+ if a.strip()
170
+ ] if args_str else []
171
+
172
+ env_str = click.prompt(
173
+ "Environment variables (KEY=VALUE lines, empty to skip)",
174
+ default="",
175
+ )
176
+ env = {}
177
+ for line in env_str.splitlines():
178
+ line = line.strip()
179
+ if line and "=" in line:
180
+ k, v = line.split("=", 1)
181
+ env[k.strip()] = v.strip()
182
+
183
+ mcp_section[server_name] = {
184
+ "command": command,
185
+ "args": args,
186
+ "env": env if env else None,
187
+ }
188
+ else:
189
+ url = click.prompt("Server URL (e.g. https://example.com/mcp)")
190
+ headers_str = click.prompt(
191
+ "Headers (KEY=VALUE lines, empty to skip)",
192
+ default="",
193
+ )
194
+ headers = {}
195
+ for line in headers_str.splitlines():
196
+ line = line.strip()
197
+ if line and "=" in line:
198
+ k, v = line.split("=", 1)
199
+ headers[k.strip()] = v.strip()
200
+
201
+ mcp_section[server_name] = {
202
+ "url": url,
203
+ "headers": headers if headers else None,
204
+ }
205
+
206
+ if mcp_section:
207
+ from regcode.config import McpServerConfig
208
+
209
+ config.mcp_servers = {
210
+ name: McpServerConfig.model_validate(
211
+ {k: v for k, v in cfg.items() if v is not None}
212
+ )
213
+ for name, cfg in mcp_section.items()
214
+ }
215
+ else:
216
+ click.echo(" Using default MCP settings (no servers).")
217
+
142
218
  # --- Write config.yaml ---
143
- data = {
219
+ data: dict = {
144
220
  "agent": {
145
221
  "context_window": config.agent.context_window,
146
222
  "compaction_threshold": config.agent.compaction_threshold,
@@ -157,6 +233,13 @@ def configure(output_path):
157
233
  "tool_budget": config.tool_budget,
158
234
  }
159
235
 
236
+ # Add MCP servers section if configured
237
+ if hasattr(config, "mcp_servers") and config.mcp_servers:
238
+ data["mcp_servers"] = {
239
+ name: cfg.model_dump(exclude_none=True)
240
+ for name, cfg in config.mcp_servers.items()
241
+ }
242
+
160
243
  with open(output_path, "w") as f:
161
244
  yaml.dump(data, f, default_flow_style=False)
162
245
  click.echo(f"\nconfig.yaml written to {output_path}")
@@ -38,11 +38,46 @@ default_system_prompt = dedent(
38
38
  Start directly with your review content. Do not address user or
39
39
  use fillers like "Now I have enough information to provide a review." or
40
40
  "Here is my review:"
41
+
42
+ ## Code implementation
43
+ If you are asked to implement code, do what a responsible developer would do:
44
+ - researh what library versions are used
45
+ - if library versions are newer than your knowledge cutoff, research first.
46
+ - write code
47
+ - check if tests exist. Always write tests for your code if they don't exist.
48
+ - Make sure to comply to users coding pattern and style.
41
49
  """
42
50
  )
43
51
 
44
52
  logger = logging.getLogger(__name__)
45
53
 
54
+ class McpServerConfig(BaseModel):
55
+ """Configuration for a single MCP server.
56
+
57
+ Supports two transport types:
58
+ - Stdio: command-based (npx, uvx, or any executable)
59
+ - HTTP: URL-based (StreamableHTTP)
60
+
61
+ Exactly one of ``command`` or ``url`` must be provided.
62
+ """
63
+
64
+ model_config = ConfigDict(extra="allow")
65
+
66
+ # Stdio transport
67
+ command: str | None = None
68
+ args: list[str] = []
69
+ env: dict[str, str] = {}
70
+ connect_timeout: int = 60
71
+
72
+ # HTTP transport
73
+ url: str | None = None
74
+ headers: dict[str, str] = {}
75
+ connect_timeout_http: int = 60
76
+
77
+ # Per-tool-call timeout
78
+ timeout: int = 120
79
+
80
+
46
81
  class ToolSetConfig(BaseModel):
47
82
  """Configuration for a set of tools."""
48
83
 
@@ -126,6 +161,7 @@ class Config(BaseModel):
126
161
  permissions: list[AgentPermission] = [AgentPermission.READ, AgentPermission.EXECUTE]
127
162
  toolset_config: ToolSetConfig = ToolSetConfig()
128
163
  debug_mode: bool = False
164
+ mcp_servers: dict[str, McpServerConfig] = {}
129
165
 
130
166
  @classmethod
131
167
  def load(cls, path: str | None = None) -> "Config":
@@ -121,6 +121,16 @@ class Agent:
121
121
  for tool in create_default_tools():
122
122
  self.registry.register(tool)
123
123
 
124
+ # Discover and register MCP tools
125
+ self._mcp_client = None
126
+ mcp_results = self._discover_mcp_tools()
127
+ if mcp_results:
128
+ logger.info(
129
+ "MCP discovery complete: %d servers, %d tools",
130
+ len(mcp_results),
131
+ sum(len(t) for t in mcp_results.values()),
132
+ )
133
+
124
134
  # Apply permission filtering after tools are registered
125
135
  self._filter_tools_by_permissions()
126
136
 
@@ -135,6 +145,105 @@ class Agent:
135
145
  if not has_all:
136
146
  self.registry.disable_tool(tool_name)
137
147
 
148
+ def _discover_mcp_tools(self) -> dict[str, list[str]]:
149
+ """Discover and register MCP tools from configured servers.
150
+
151
+ Reads ``mcp_servers`` from config, connects to each server,
152
+ discovers available tools, and registers them into the agent's
153
+ tool registry with the prefix ``mcp_{server}_{tool}``.
154
+
155
+ Returns a dict mapping server name → list of registered tool names.
156
+ """
157
+ if not self.config.mcp_servers:
158
+ return {}
159
+
160
+ from regcode.mcp_client import McpClient
161
+
162
+ try:
163
+ self._mcp_client = McpClient(
164
+ mcp_servers_config={
165
+ name: cfg.model_dump(exclude_none=True)
166
+ for name, cfg in self.config.mcp_servers.items()
167
+ },
168
+ registry=self.registry,
169
+ )
170
+ results = self._mcp_client.connect_all()
171
+ return results
172
+ except Exception:
173
+ logger.error(
174
+ "MCP discovery failed: %s",
175
+ sys.exc_info()[1],
176
+ exc_info=True,
177
+ )
178
+ return {}
179
+
180
+ def reload_mcp(self) -> dict[str, list[str]]:
181
+ """Reload MCP servers and re-register their tools.
182
+
183
+ Closes any existing connections, discovers new tools, and
184
+ registers them into the registry. Useful after changing
185
+ ``mcp_servers`` in config without restarting the agent.
186
+
187
+ Returns a dict mapping server name → list of registered tool names.
188
+ """
189
+ # Close existing connections
190
+ if self._mcp_client is not None:
191
+ self._mcp_client.close()
192
+ self._mcp_client = None
193
+
194
+ return self._discover_mcp_tools()
195
+
196
+ def _mcp_status(self) -> str:
197
+ """Return formatted text showing all MCP servers and their tools.
198
+
199
+ Used by the /mcp slash command for debugging and visibility.
200
+ """
201
+ if self._mcp_client is None:
202
+ return "No MCP servers configured."
203
+
204
+ servers = self._mcp_client.get_server_info()
205
+
206
+ if not servers:
207
+ return "No MCP servers configured."
208
+
209
+ lines = ["═══ MCP Servers ═══"]
210
+
211
+ for server in servers:
212
+ if server["connected"]:
213
+ status = "[green]connected[/]"
214
+ else:
215
+ status = f"[red]{server['error']}[/]"
216
+ transport = server["transport"]
217
+ name = server["name"]
218
+ lines.append(
219
+ f"\n [bold]{name}[/] "
220
+ f"— transport: {transport}, status: {status}"
221
+ )
222
+
223
+ if server["tools"]:
224
+ for tool in server["tools"]:
225
+ lines.append(f" • {tool['name']}")
226
+ desc = tool.get("description", "")
227
+ if desc:
228
+ # Truncate long descriptions
229
+ if len(desc) > 80:
230
+ desc = desc[:77] + "..."
231
+ lines.append(f" {desc}")
232
+ if tool.get("params"):
233
+ for p in tool["params"]:
234
+ pdef = f"{p['name']} ({p['type']})"
235
+ if p.get("description"):
236
+ if len(p["description"]) > 60:
237
+ pdef += f" — {p['description'][:57]}..."
238
+ else:
239
+ pdef += f" — {p['description']}"
240
+ lines.append(f" └─ {pdef}")
241
+ else:
242
+ lines.append(" (no tools discovered)")
243
+
244
+ lines.append("")
245
+ return "\n".join(lines)
246
+
138
247
  def _format_tool_descriptions(self) -> str:
139
248
  """Format tool definitions for inclusion in system prompts.
140
249
 
@@ -1304,8 +1413,10 @@ class Agent:
1304
1413
  )
1305
1414
 
1306
1415
  def cleanup(self) -> None:
1307
- """Clean up sandbox resources."""
1416
+ """Clean up sandbox and MCP resources."""
1308
1417
  self.sandbox.cleanup()
1418
+ if self._mcp_client is not None:
1419
+ self._mcp_client.close()
1309
1420
 
1310
1421
  def __enter__(self) -> "Agent":
1311
1422
  """Enter context manager."""