gemcode 0.3.30__tar.gz → 0.3.31__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 (119) hide show
  1. {gemcode-0.3.30/src/gemcode.egg-info → gemcode-0.3.31}/PKG-INFO +1 -1
  2. {gemcode-0.3.30 → gemcode-0.3.31}/pyproject.toml +1 -1
  3. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/agent.py +96 -6
  4. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/cli.py +4 -14
  5. gemcode-0.3.31/src/gemcode/mcp_loader.py +147 -0
  6. gemcode-0.3.31/src/gemcode/openapi_loader.py +127 -0
  7. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/repl_commands.py +8 -0
  8. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/repl_slash.py +111 -0
  9. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/session_runtime.py +21 -0
  10. gemcode-0.3.31/src/gemcode/tools/notes.py +106 -0
  11. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tui/input_handler.py +3 -0
  12. {gemcode-0.3.30 → gemcode-0.3.31/src/gemcode.egg-info}/PKG-INFO +1 -1
  13. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode.egg-info/SOURCES.txt +2 -0
  14. gemcode-0.3.30/src/gemcode/mcp_loader.py +0 -57
  15. {gemcode-0.3.30 → gemcode-0.3.31}/LICENSE +0 -0
  16. {gemcode-0.3.30 → gemcode-0.3.31}/MANIFEST.in +0 -0
  17. {gemcode-0.3.30 → gemcode-0.3.31}/README.md +0 -0
  18. {gemcode-0.3.30 → gemcode-0.3.31}/setup.cfg +0 -0
  19. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/__init__.py +0 -0
  20. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/__main__.py +0 -0
  21. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/audit.py +0 -0
  22. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/autocompact.py +0 -0
  23. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/callbacks.py +0 -0
  24. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/capability_routing.py +0 -0
  25. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/compaction.py +0 -0
  26. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/computer_use/__init__.py +0 -0
  27. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/computer_use/browser_computer.py +0 -0
  28. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/config.py +0 -0
  29. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/context_budget.py +0 -0
  30. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/context_warning.py +0 -0
  31. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/credentials.py +0 -0
  32. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/hitl_session.py +0 -0
  33. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/hooks.py +0 -0
  34. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/interactions.py +0 -0
  35. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/invoke.py +0 -0
  36. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/kairos_daemon.py +0 -0
  37. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/limits.py +0 -0
  38. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/live_audio_engine.py +0 -0
  39. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/logging_config.py +0 -0
  40. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/memory/__init__.py +0 -0
  41. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/memory/embedding_memory_service.py +0 -0
  42. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/memory/file_memory_service.py +0 -0
  43. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/modality_tools.py +0 -0
  44. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/model_errors.py +0 -0
  45. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/model_routing.py +0 -0
  46. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/paths.py +0 -0
  47. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/permissions.py +0 -0
  48. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/plugins/__init__.py +0 -0
  49. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  50. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  51. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/pricing.py +0 -0
  52. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/prompt_suggestions.py +0 -0
  53. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/__init__.py +0 -0
  54. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/config.py +0 -0
  55. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/deps.py +0 -0
  56. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/engine.py +0 -0
  57. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/stop_hooks.py +0 -0
  58. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/token_budget.py +0 -0
  59. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/query/transitions.py +0 -0
  60. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/refine.py +0 -0
  61. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/slash_commands.py +0 -0
  62. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/thinking.py +0 -0
  63. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tool_prompt_manifest.py +0 -0
  64. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tool_registry.py +0 -0
  65. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/__init__.py +0 -0
  66. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/bash.py +0 -0
  67. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/browser.py +0 -0
  68. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/edit.py +0 -0
  69. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/filesystem.py +0 -0
  70. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/search.py +0 -0
  71. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/shell.py +0 -0
  72. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/shell_gate.py +0 -0
  73. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/subtask.py +0 -0
  74. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/think.py +0 -0
  75. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/todo.py +0 -0
  76. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools/web.py +0 -0
  77. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tools_inspector.py +0 -0
  78. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/trust.py +0 -0
  79. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tui/scrollback.py +0 -0
  80. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tui/spinner.py +0 -0
  81. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tui/welcome_banner.py +0 -0
  82. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/tui/welcome_rich.py +0 -0
  83. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/version.py +0 -0
  84. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/vertex.py +0 -0
  85. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/web/__init__.py +0 -0
  86. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/web/claude_sse_adapter.py +0 -0
  87. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/web/terminal_repl.py +0 -0
  88. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode/workspace_hints.py +0 -0
  89. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode.egg-info/dependency_links.txt +0 -0
  90. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode.egg-info/entry_points.txt +0 -0
  91. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode.egg-info/requires.txt +0 -0
  92. {gemcode-0.3.30 → gemcode-0.3.31}/src/gemcode.egg-info/top_level.txt +0 -0
  93. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_agent_instruction.py +0 -0
  94. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_autocompact.py +0 -0
  95. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_capability_routing.py +0 -0
  96. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_claude_web_adapter_sse.py +0 -0
  97. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_cli_init.py +0 -0
  98. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_computer_use_permissions.py +0 -0
  99. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_context_budget.py +0 -0
  100. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_context_warning.py +0 -0
  101. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_credentials.py +0 -0
  102. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_interactive_permission_ask.py +0 -0
  103. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_kairos_scheduler.py +0 -0
  104. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_modality_tools.py +0 -0
  105. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_model_error_retry.py +0 -0
  106. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_model_errors.py +0 -0
  107. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_model_routing.py +0 -0
  108. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_paths.py +0 -0
  109. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_permissions.py +0 -0
  110. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_prompt_suggestions.py +0 -0
  111. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_repl_commands.py +0 -0
  112. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_repl_slash.py +0 -0
  113. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_slash_commands.py +0 -0
  114. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_thinking_config.py +0 -0
  115. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_token_budget.py +0 -0
  116. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_tool_context_circulation.py +0 -0
  117. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_tools.py +0 -0
  118. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_tools_inspector.py +0 -0
  119. {gemcode-0.3.30 → gemcode-0.3.31}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.30
3
+ Version: 0.3.31
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.30"
7
+ version = "0.3.31"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -49,11 +49,77 @@ def _chain_before_model_callbacks(*callbacks):
49
49
 
50
50
 
51
51
  def _load_gemini_md(project_root: Path) -> str:
52
- for name in ("GEMINI.md", "gemini.md"):
53
- p = project_root / name
54
- if p.is_file():
55
- return p.read_text(encoding="utf-8", errors="replace")[:50_000]
56
- return ""
52
+ """
53
+ Load GEMINI.md / .gemcode/NOTES.md from a Claude Code–style hierarchy.
54
+
55
+ Priority (later entries override earlier ones, all are concatenated):
56
+ 1. ~/.gemcode/GEMINI.md — user-global instructions (all projects)
57
+ 2. Walk UP from project_root: each directory's GEMINI.md / .gemcode/GEMINI.md
58
+ (org-level files at higher dirs, project-level at project_root)
59
+ 3. project_root/GEMINI.md — the primary project instructions
60
+ 4. project_root/.gemcode/GEMINI.md — alternative location
61
+ 5. project_root/.gemcode/notes.md — agent auto-generated notes (read-only context)
62
+
63
+ Max total: 80,000 chars. Each file is capped at 30,000 chars.
64
+ HTML comments (<!-- ... -->) are stripped before injection (saves tokens).
65
+ """
66
+ import re
67
+
68
+ _NAMES = ("GEMINI.md", "gemini.md", ".gemcode/GEMINI.md")
69
+ _FILE_CAP = 30_000
70
+ _TOTAL_CAP = 80_000
71
+ _COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
72
+
73
+ def _read(p: Path) -> str:
74
+ if not p.is_file():
75
+ return ""
76
+ try:
77
+ raw = p.read_text(encoding="utf-8", errors="replace")[:_FILE_CAP]
78
+ # Strip HTML comments (like Claude Code does — saves tokens)
79
+ return _COMMENT_RE.sub("", raw).strip()
80
+ except OSError:
81
+ return ""
82
+
83
+ seen: set[Path] = set()
84
+ sections: list[str] = []
85
+
86
+ def _add(p: Path, label: str | None = None) -> None:
87
+ resolved = p.resolve()
88
+ if resolved in seen:
89
+ return
90
+ seen.add(resolved)
91
+ text = _read(p)
92
+ if text:
93
+ sections.append(f"<!-- {label or str(p)} -->\n{text}" if label else text)
94
+
95
+ # 1. User-global: ~/.gemcode/GEMINI.md
96
+ user_global = Path.home() / ".gemcode" / "GEMINI.md"
97
+ _add(user_global, "user-global (~/.gemcode/GEMINI.md)")
98
+
99
+ # 2. Walk UP from project_root to filesystem root — loads org / monorepo-level instructions
100
+ walk = project_root.resolve()
101
+ ancestors = []
102
+ while walk != walk.parent:
103
+ walk = walk.parent
104
+ if walk == Path.home() or walk == Path("/"):
105
+ break
106
+ ancestors.append(walk)
107
+ # Walk outer→inner (org first, closer dirs later — later = higher priority)
108
+ for ancestor in reversed(ancestors):
109
+ for name in _NAMES:
110
+ _add(ancestor / name)
111
+
112
+ # 3+4. Project-root level instructions (primary location)
113
+ for name in ("GEMINI.md", "gemini.md", ".gemcode/GEMINI.md", ".gemcode/gemini.md"):
114
+ _add(project_root / name)
115
+
116
+ # 5. Agent-generated notes (informational context, not instructions)
117
+ notes = project_root / ".gemcode" / "notes.md"
118
+ if notes.is_file():
119
+ _add(notes, "agent notes (.gemcode/notes.md)")
120
+
121
+ combined = "\n\n---\n\n".join(s for s in sections if s.strip())
122
+ return combined[:_TOTAL_CAP]
57
123
 
58
124
 
59
125
  def _build_runtime_facts(cfg: GemCodeConfig) -> str:
@@ -527,7 +593,23 @@ For tasks where quality matters:
527
593
  - Prefer small, testable, accurate changes over broad rewrites.
528
594
 
529
595
  ## Workspace scope
530
- All file tools use paths **relative to the project root** (where GemCode was started). The root may be the home folder — subfolders like `Desktop`, `Desktop/code`, `Documents` are inside the sandbox. Call `list_directory("Desktop")` or `glob_files("**/*name*.ts")` instead of assuming access is blocked. Only treat access as denied when a tool returns an explicit `error`."""
596
+ All file tools use paths **relative to the project root** (where GemCode was started). The root may be the home folder — subfolders like `Desktop`, `Desktop/code`, `Documents` are inside the sandbox. Call `list_directory("Desktop")` or `glob_files("**/*name*.ts")` instead of assuming access is blocked. Only treat access as denied when a tool returns an explicit `error`.
597
+
598
+ ## Agent notes (.gemcode/notes.md)
599
+ You have two tools to persist project insights across sessions, like Claude Code's auto-memory:
600
+
601
+ - **`append_project_note(note)`** — write a note to `.gemcode/notes.md`. Use this proactively when you discover something worth remembering:
602
+ - Build/test/lint commands you discover ("Build: `npm run build` — requires Node 20")
603
+ - Key file locations ("Auth middleware: `src/middleware/auth.ts`")
604
+ - Known issues or patterns ("DB migrations: always run `prisma db push` after schema changes")
605
+ - User workflow preferences ("User prefers running tests before committing")
606
+ - Architecture decisions or tricky patterns
607
+
608
+ Call this **immediately** when you discover something useful — not just at the end of tasks.
609
+ Notes are loaded at session start so future sessions inherit this knowledge.
610
+
611
+ - **`read_project_notes()`** — read current notes before starting a new project task.
612
+ If notes exist and you haven't read them yet, read them first to avoid re-discovering known information."""
531
613
 
532
614
  # Inject capability-specific strategy sections only when those caps are on.
533
615
  if getattr(cfg, "enable_computer_use", False):
@@ -590,6 +672,14 @@ def build_root_agent(
590
672
  except Exception:
591
673
  pass
592
674
 
675
+ # Agent auto-notes: write project insights to .gemcode/notes.md (Claude Code MEMORY.md equivalent)
676
+ try:
677
+ from gemcode.tools.notes import build_notes_tools
678
+ notes_tools = build_notes_tools(cfg.project_root)
679
+ tools = [*tools, *notes_tools]
680
+ except Exception:
681
+ pass
682
+
593
683
  if extra_tools:
594
684
  tools = [*tools, *extra_tools]
595
685
 
@@ -151,13 +151,8 @@ async def _run_prompt(
151
151
  _maybe_prompt_google_api_key()
152
152
  require_google_api_key()
153
153
  _initialize_gemcode_project(cfg)
154
- extra: list = []
155
- if use_mcp:
156
- from gemcode.mcp_loader import load_mcp_toolsets
157
-
158
- extra = load_mcp_toolsets(cfg)
159
-
160
- runner = create_runner(cfg, extra_tools=extra or None)
154
+ # MCP and OpenAPI toolsets are now loaded inside create_runner() directly.
155
+ runner = create_runner(cfg, extra_tools=None)
161
156
  try:
162
157
  collected = await run_turn(
163
158
  runner,
@@ -184,13 +179,8 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
184
179
  require_google_api_key()
185
180
  _initialize_gemcode_project(cfg)
186
181
 
187
- extra: list = []
188
- if use_mcp:
189
- from gemcode.mcp_loader import load_mcp_toolsets
190
-
191
- extra = load_mcp_toolsets(cfg)
192
-
193
- runner = create_runner(cfg, extra_tools=extra or None)
182
+ # MCP and OpenAPI toolsets are now loaded inside create_runner() directly.
183
+ runner = create_runner(cfg, extra_tools=None)
194
184
  try:
195
185
  # For CLI UX, show concise tool summaries (helps users see what ran).
196
186
  if os.environ.get("GEMCODE_EMIT_TOOL_USE_SUMMARIES") is None:
@@ -0,0 +1,147 @@
1
+ """
2
+ Optional MCP toolsets from `.gemcode/mcp.json`.
3
+
4
+ Now supports all three ADK connection types — like Claude Code's MCP integration:
5
+
6
+ Schema (example):
7
+ {
8
+ "servers": [
9
+ {
10
+ "name": "filesystem",
11
+ "stdio": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] }
12
+ },
13
+ {
14
+ "name": "github",
15
+ "http": {
16
+ "url": "https://api.githubcopilot.com/mcp/",
17
+ "headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" }
18
+ }
19
+ },
20
+ {
21
+ "name": "notion",
22
+ "sse": { "url": "https://mcp.notion.com/sse" }
23
+ }
24
+ ]
25
+ }
26
+
27
+ Connection types:
28
+ stdio — local subprocess (npx, python, etc.) — legacy, always worked
29
+ http — Streamable HTTP (remote servers, Cloud Run, Smithery.ai)
30
+ sse — Server-Sent Events (older remote servers; use http when possible)
31
+
32
+ Header values support ${ENV_VAR} substitution from environment.
33
+
34
+ Requires: pip install gemcode[mcp]
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import json
40
+ import os
41
+ import re
42
+ from pathlib import Path
43
+ from typing import Any
44
+
45
+ from gemcode.config import GemCodeConfig
46
+
47
+ _ENV_VAR_RE = re.compile(r"\$\{([A-Z_][A-Z0-9_]*)\}")
48
+
49
+
50
+ def _expand_env(value: str) -> str:
51
+ """Expand ${VAR} placeholders from environment variables."""
52
+ return _ENV_VAR_RE.sub(lambda m: os.environ.get(m.group(1), m.group(0)), value)
53
+
54
+
55
+ def _expand_headers(headers: dict) -> dict:
56
+ return {k: _expand_env(str(v)) for k, v in headers.items()}
57
+
58
+
59
+ def load_mcp_toolsets(cfg: GemCodeConfig) -> list:
60
+ path = cfg.project_root / ".gemcode" / "mcp.json"
61
+ if not path.is_file():
62
+ return []
63
+ try:
64
+ data = json.loads(path.read_text(encoding="utf-8"))
65
+ except json.JSONDecodeError as e:
66
+ raise ValueError(f"Invalid mcp.json: {e}") from e
67
+
68
+ try:
69
+ from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
70
+ from mcp import StdioServerParameters
71
+ except ImportError as e:
72
+ raise ImportError("Install MCP extras: pip install gemcode[mcp]") from e
73
+
74
+ servers = data.get("servers") or []
75
+ toolsets: list[Any] = []
76
+
77
+ for s in servers:
78
+ prefix = s.get("name") or "mcp"
79
+ tool_filter: list[str] | None = s.get("tools") or None # optional allowlist
80
+
81
+ # ── stdio (local subprocess) ─────────────────────────────────────────
82
+ if "stdio" in s:
83
+ stdio = s["stdio"]
84
+ cmd = stdio.get("command")
85
+ args = stdio.get("args") or []
86
+ env_extra = {k: _expand_env(str(v)) for k, v in (stdio.get("env") or {}).items()}
87
+ if not cmd:
88
+ continue
89
+ params = StdioServerParameters(
90
+ command=cmd,
91
+ args=args,
92
+ env={**os.environ, **env_extra} if env_extra else None,
93
+ )
94
+ kw: dict[str, Any] = dict(connection_params=params, tool_name_prefix=prefix)
95
+ if tool_filter:
96
+ kw["tool_filter"] = tool_filter
97
+ toolsets.append(McpToolset(**kw))
98
+ continue
99
+
100
+ # ── http (Streamable HTTP — modern, Cloud Run-friendly) ──────────────
101
+ if "http" in s:
102
+ http_cfg = s["http"]
103
+ url = _expand_env(http_cfg.get("url", ""))
104
+ if not url:
105
+ continue
106
+ headers = _expand_headers(http_cfg.get("headers") or {})
107
+ try:
108
+ from google.adk.tools.mcp_tool.mcp_session_manager import (
109
+ StreamableHTTPConnectionParams,
110
+ )
111
+ params_http = StreamableHTTPConnectionParams(url=url, headers=headers or None)
112
+ kw = dict(connection_params=params_http, tool_name_prefix=prefix)
113
+ if tool_filter:
114
+ kw["tool_filter"] = tool_filter
115
+ toolsets.append(McpToolset(**kw))
116
+ except ImportError:
117
+ # Fallback: try SseConnectionParams for older ADK builds
118
+ try:
119
+ from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams
120
+ params_sse_fb = SseConnectionParams(url=url, headers=headers or None)
121
+ kw = dict(connection_params=params_sse_fb, tool_name_prefix=prefix)
122
+ if tool_filter:
123
+ kw["tool_filter"] = tool_filter
124
+ toolsets.append(McpToolset(**kw))
125
+ except ImportError:
126
+ pass # ADK version doesn't support remote MCP — skip silently
127
+ continue
128
+
129
+ # ── sse (Server-Sent Events — older remote servers) ──────────────────
130
+ if "sse" in s:
131
+ sse_cfg = s["sse"]
132
+ url = _expand_env(sse_cfg.get("url", ""))
133
+ if not url:
134
+ continue
135
+ headers = _expand_headers(sse_cfg.get("headers") or {})
136
+ try:
137
+ from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams
138
+ params_sse = SseConnectionParams(url=url, headers=headers or None)
139
+ kw = dict(connection_params=params_sse, tool_name_prefix=prefix)
140
+ if tool_filter:
141
+ kw["tool_filter"] = tool_filter
142
+ toolsets.append(McpToolset(**kw))
143
+ except ImportError:
144
+ pass # ADK version too old — skip
145
+ continue
146
+
147
+ return toolsets
@@ -0,0 +1,127 @@
1
+ """
2
+ Auto-loads OpenAPI specs from `.gemcode/openapi/` as ADK OpenAPIToolset instances.
3
+
4
+ Inspired by the ADK community pattern: drop an OpenAPI spec in a directory and
5
+ the agent automatically gets REST API tools for it — no manual tool wiring needed.
6
+
7
+ Supported formats: .yaml, .yml, .json
8
+
9
+ Example:
10
+ .gemcode/openapi/
11
+ ├── github.yaml → tools: mcp__github__list_repos, mcp__github__create_pr, …
12
+ ├── sentry.json → tools: mcp__sentry__list_issues, …
13
+ └── internal_api.yaml → tools from your company's internal REST API
14
+
15
+ The filename (without extension) is used as the tool name prefix.
16
+ So `github.yaml` → tools prefixed with `github_`.
17
+
18
+ Auth:
19
+ Each spec file can have a matching `.auth` sidecar file with JSON:
20
+ {
21
+ "type": "api_key", // "api_key" | "bearer" | "basic" | "oauth2"
22
+ "header": "X-API-Key",
23
+ "value": "${GITHUB_TOKEN}" // ${ENV_VAR} is expanded from environment
24
+ }
25
+ For oauth2: {"type": "oauth2", "token_url": "...", "client_id": "...", "client_secret": "${VAR}"}
26
+
27
+ Requires: pip install google-adk (OpenAPIToolset is included in the main package)
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ import logging
34
+ import os
35
+ import re
36
+ from pathlib import Path
37
+ from typing import Any
38
+
39
+ log = logging.getLogger(__name__)
40
+
41
+ _ENV_VAR_RE = re.compile(r"\$\{([A-Z_][A-Z0-9_]*)\}")
42
+ _OPENAPI_DIR = ".gemcode/openapi"
43
+
44
+
45
+ def _expand(value: str) -> str:
46
+ return _ENV_VAR_RE.sub(lambda m: os.environ.get(m.group(1), m.group(0)), value)
47
+
48
+
49
+ def _load_auth(spec_path: Path):
50
+ """Try to load auth config from <spec_stem>.auth next to the spec file."""
51
+ auth_path = spec_path.with_suffix(".auth")
52
+ if not auth_path.is_file():
53
+ auth_path = spec_path.parent / (spec_path.stem + ".auth.json")
54
+ if not auth_path.is_file():
55
+ return None, None
56
+ try:
57
+ auth_data = json.loads(auth_path.read_text())
58
+ except Exception:
59
+ return None, None
60
+
61
+ auth_type = auth_data.get("type", "").lower()
62
+ try:
63
+ from google.adk.auth import AuthCredential, AuthCredentialTypes
64
+ if auth_type == "api_key":
65
+ return None, AuthCredential(
66
+ auth_type=AuthCredentialTypes.API_KEY,
67
+ api_key=_expand(auth_data.get("value", "")),
68
+ )
69
+ if auth_type in ("bearer", "token"):
70
+ return None, AuthCredential(
71
+ auth_type=AuthCredentialTypes.HTTP,
72
+ http={"scheme": "bearer", "token": _expand(auth_data.get("value", ""))},
73
+ )
74
+ except ImportError:
75
+ pass
76
+ return None, None
77
+
78
+
79
+ def load_openapi_toolsets(project_root: Path) -> list[Any]:
80
+ """
81
+ Scan .gemcode/openapi/ for spec files and return OpenAPIToolset instances.
82
+
83
+ Returns an empty list if the directory doesn't exist or no valid specs are found.
84
+ Errors loading individual specs are logged but don't crash GemCode.
85
+ """
86
+ openapi_dir = project_root / _OPENAPI_DIR
87
+ if not openapi_dir.is_dir():
88
+ return []
89
+
90
+ try:
91
+ from google.adk.tools.openapi_tool.openapi_spec_parser.openapi_toolset import (
92
+ OpenAPIToolset,
93
+ )
94
+ except ImportError:
95
+ log.debug("[openapi] OpenAPIToolset not available in this ADK version — skipping")
96
+ return []
97
+
98
+ toolsets: list[Any] = []
99
+
100
+ for spec_file in sorted(openapi_dir.iterdir()):
101
+ if spec_file.suffix.lower() not in (".yaml", ".yml", ".json"):
102
+ continue
103
+ if spec_file.stem.endswith(".auth"):
104
+ continue
105
+
106
+ prefix = spec_file.stem # e.g. "github" from "github.yaml"
107
+ fmt = "json" if spec_file.suffix.lower() == ".json" else "yaml"
108
+
109
+ try:
110
+ spec_str = spec_file.read_text(encoding="utf-8", errors="replace")
111
+ except OSError as e:
112
+ log.warning("[openapi] Could not read %s: %s", spec_file.name, e)
113
+ continue
114
+
115
+ _, auth_credential = _load_auth(spec_file)
116
+
117
+ try:
118
+ kwargs: dict[str, Any] = dict(spec_str=spec_str, spec_str_type=fmt)
119
+ if auth_credential is not None:
120
+ kwargs["auth_credential"] = auth_credential
121
+ toolset = OpenAPIToolset(**kwargs)
122
+ toolsets.append(toolset)
123
+ log.info("[openapi] Loaded spec '%s' from %s", prefix, spec_file.name)
124
+ except Exception as exc:
125
+ log.warning("[openapi] Failed to load %s: %s", spec_file.name, exc)
126
+
127
+ return toolsets
@@ -189,6 +189,14 @@ def slash_help_lines() -> list[str]:
189
189
  " (CLI) gemcode -C DIR Use a project folder as root (recommended vs. ~ )",
190
190
  " (CLI) gemcode login Save or change API key (~/.gemcode/credentials.json)",
191
191
  "",
192
+ " Project setup:",
193
+ " /init Analyze project structure and generate GEMINI.md",
194
+ " /init force Regenerate GEMINI.md even if it already exists",
195
+ " /cost Show token usage and estimated cost for this session",
196
+ " /notes Show agent auto-notes (.gemcode/notes.md)",
197
+ " /notes clear Delete all notes",
198
+ " /notes edit Open notes in $EDITOR",
199
+ "",
192
200
  " Session:",
193
201
  " /help Show this help",
194
202
  " /status Full status: model, capabilities, thinking, limits",
@@ -154,6 +154,117 @@ async def process_repl_slash(
154
154
  out()
155
155
  return ReplSlashResult(skip_model_turn=True)
156
156
 
157
+ # ── /init ─────────────────────────────────────────────────────────────────
158
+ # ── /notes ────────────────────────────────────────────────────────────────
159
+ if name == "notes":
160
+ sub = (sc.args or "").strip().lower()
161
+ notes_path = cfg.project_root / ".gemcode" / "notes.md"
162
+ if sub == "clear":
163
+ if notes_path.exists():
164
+ notes_path.unlink()
165
+ out("Notes cleared.")
166
+ else:
167
+ out("No notes file found.")
168
+ out()
169
+ return ReplSlashResult(skip_model_turn=True)
170
+ if sub == "edit":
171
+ import subprocess
172
+ editor = os.environ.get("EDITOR", "nano")
173
+ notes_path.parent.mkdir(parents=True, exist_ok=True)
174
+ if not notes_path.exists():
175
+ notes_path.write_text("# GemCode Agent Notes\n*Auto-generated project notes.*\n\n", encoding="utf-8")
176
+ subprocess.run([editor, str(notes_path)])
177
+ return ReplSlashResult(skip_model_turn=True)
178
+ # Default: show notes
179
+ if notes_path.exists():
180
+ content = notes_path.read_text(encoding="utf-8", errors="replace")
181
+ out(f"Agent notes ({notes_path}):")
182
+ out("─" * 60)
183
+ out(content)
184
+ out("─" * 60)
185
+ out(" /notes clear Delete all notes")
186
+ out(" /notes edit Open in $EDITOR")
187
+ else:
188
+ out(f"No agent notes yet ({notes_path}).")
189
+ out("The agent will auto-create notes as it discovers project insights.")
190
+ out()
191
+ return ReplSlashResult(skip_model_turn=True)
192
+
193
+ # ── /cost ─────────────────────────────────────────────────────────────────
194
+ if name == "cost":
195
+ from gemcode.pricing import format_cost, format_tokens
196
+ stats = getattr(cfg, "_last_turn_stats", None)
197
+ out("Session cost summary")
198
+ out("─" * 40)
199
+ if stats:
200
+ out(f" Last turn input tokens : {format_tokens(stats.get('in', 0) or 0)}")
201
+ out(f" Last turn output tokens: {format_tokens(stats.get('out', 0) or 0)}")
202
+ think = stats.get("think", 0) or 0
203
+ if think:
204
+ out(f" Last turn thinking : {format_tokens(think)}")
205
+ cache = stats.get("cache", 0) or 0
206
+ if cache:
207
+ out(f" Last turn cache read : {format_tokens(cache)}")
208
+ lc = stats.get("turn_cost")
209
+ out(f" Last turn cost : {format_cost(lc) if lc is not None else '(unknown model pricing)'}")
210
+ out()
211
+ out(f" Session total tokens : {format_tokens(stats.get('session_total', 0) or 0)}")
212
+ sc = stats.get("session_cost")
213
+ if sc and sc > 0:
214
+ out(f" Session total cost : {format_cost(sc)}")
215
+ else:
216
+ out(" Session total cost : (accumulating — will show after first turn)")
217
+ else:
218
+ out(" No turn completed yet. Send a message to see token/cost stats.")
219
+ out()
220
+ out(f" Model: {getattr(cfg, 'model', 'unknown')}")
221
+ out()
222
+ out(" Note: costs are estimates based on published Gemini pricing.")
223
+ out(" Thinking tokens billed at same rate as output tokens.")
224
+ return ReplSlashResult(skip_model_turn=True)
225
+
226
+ if name == "init":
227
+ gemini_md = cfg.project_root / "GEMINI.md"
228
+ if gemini_md.exists() and (sc.args or "").strip().lower() not in ("force", "overwrite", "-f"):
229
+ out(f"GEMINI.md already exists at {gemini_md}.")
230
+ out("Use /init force to regenerate it, or edit it manually.")
231
+ out()
232
+ return ReplSlashResult(skip_model_turn=True)
233
+ # Dispatch to the model to analyze the project and write GEMINI.md.
234
+ init_prompt = (
235
+ "Analyze this codebase and generate a GEMINI.md file for me.\n\n"
236
+ "To do this:\n"
237
+ "1. Run `list_directory('.')` to understand the project structure\n"
238
+ "2. Read `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, `README.md` "
239
+ "or equivalent to understand the project type and dependencies\n"
240
+ "3. Look at the source directory structure (src/, lib/, app/, etc.)\n"
241
+ "4. Check for test directories and test runner config\n"
242
+ "5. Look for linting/formatting config files (.eslintrc, .prettierrc, ruff.toml, etc.)\n\n"
243
+ "Then write a GEMINI.md file at the project root containing:\n"
244
+ "# Project Name\n"
245
+ "One-sentence description.\n\n"
246
+ "## Build & Test\n"
247
+ "- How to install dependencies\n"
248
+ "- How to build\n"
249
+ "- How to run tests\n"
250
+ "- How to lint/format\n\n"
251
+ "## Architecture\n"
252
+ "- Key directories and what they contain\n"
253
+ "- Important entry points\n"
254
+ "- Key abstractions or patterns used\n\n"
255
+ "## Coding standards\n"
256
+ "- Language/framework conventions\n"
257
+ "- Any style requirements found in config files\n\n"
258
+ "## Workflow\n"
259
+ "- Any git branching rules from README or CONTRIBUTING\n"
260
+ "- PR/commit conventions\n\n"
261
+ "Keep it under 200 lines. Write the file to GEMINI.md now."
262
+ )
263
+ out("Analyzing project to generate GEMINI.md…")
264
+ out("(GemCode will read the project structure and write a starting GEMINI.md)")
265
+ out()
266
+ return ReplSlashResult(model_prompt=init_prompt)
267
+
157
268
  if name == "hooks":
158
269
  out("\n".join(format_hooks_lines(cfg)))
159
270
  out()
@@ -56,6 +56,27 @@ def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None) -> Runner
56
56
  else:
57
57
  merged_extra_tools = modality_tools or None
58
58
 
59
+ # ── MCP toolsets from .gemcode/mcp.json ─────────────────────────────────
60
+ # Supports stdio, http (Streamable HTTP), and sse connection types.
61
+ try:
62
+ from gemcode.mcp_loader import load_mcp_toolsets
63
+ mcp_tools = load_mcp_toolsets(cfg)
64
+ if mcp_tools:
65
+ merged_extra_tools = list(merged_extra_tools or []) + mcp_tools
66
+ except Exception:
67
+ pass # MCP not installed or mcp.json invalid — continue without
68
+
69
+ # ── OpenAPI toolsets from .gemcode/openapi/ ──────────────────────────────
70
+ # Drop any *.yaml / *.json OpenAPI spec in .gemcode/openapi/ to auto-generate
71
+ # REST API tools for that service (GitHub, Sentry, internal APIs, etc.)
72
+ try:
73
+ from gemcode.openapi_loader import load_openapi_toolsets
74
+ oa_tools = load_openapi_toolsets(cfg.project_root)
75
+ if oa_tools:
76
+ merged_extra_tools = list(merged_extra_tools or []) + oa_tools
77
+ except Exception:
78
+ pass # OpenAPIToolset not in this ADK version — continue without
79
+
59
80
  # Computer-use: ADK ComputerUseToolset backed by our Playwright BrowserComputer.
60
81
  # Also inject standalone browser inspection tools (screenshot, get_text, etc.)
61
82
  # so the agent can read page state without performing side-effecting actions.
@@ -0,0 +1,106 @@
1
+ """
2
+ Agent auto-notes tool — inspired by Claude Code's MEMORY.md auto-writing.
3
+
4
+ Claude Code lets Claude write its own notes to ~/.claude/projects/<proj>/MEMORY.md.
5
+ GemCode provides an equivalent: the agent can write structured notes to
6
+ .gemcode/notes.md which are loaded back at the start of each session
7
+ (via the GEMINI.md hierarchy in agent.py).
8
+
9
+ This is different from the persistent memory system (memories.jsonl):
10
+ - memories.jsonl: semantic search-backed cross-session knowledge (requires /memory on)
11
+ - notes.md: a free-form markdown notes file the agent curates manually
12
+ — always loaded, zero-config, like a sticky notepad for project insights
13
+
14
+ The tool exposes two operations:
15
+ - append_project_note(note): add a bullet point or section to notes.md
16
+ - read_project_notes(): read current notes.md content
17
+
18
+ Usage by agent:
19
+ When the agent discovers something worth remembering (build command, a tricky
20
+ pattern, architecture insight, gotcha), it should call append_project_note to
21
+ persist it for future sessions. The agent instruction tells it when to do this.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import os
27
+ from datetime import datetime
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+
32
+ def _notes_path(project_root_str: str | None = None) -> Path:
33
+ root = Path(project_root_str) if project_root_str else Path.cwd()
34
+ return root / ".gemcode" / "notes.md"
35
+
36
+
37
+ def build_notes_tools(project_root: Path) -> list:
38
+ """Return [append_project_note, read_project_notes] bound to this project."""
39
+
40
+ notes_file = project_root / ".gemcode" / "notes.md"
41
+
42
+ def append_project_note(note: str) -> dict[str, Any]:
43
+ """
44
+ Append a note to the project notes file (.gemcode/notes.md).
45
+
46
+ Use this to record important insights, patterns, commands, or gotchas that
47
+ will be useful in future sessions — like a persistent sticky notepad.
48
+
49
+ Best for:
50
+ - Build/test commands you discovered
51
+ - Architecture patterns or important abstractions
52
+ - Known issues or tricky edge cases
53
+ - User preferences or workflow conventions
54
+ - Important file locations or entry points
55
+
56
+ Not for:
57
+ - Every single thing the user says (be selective — only durable insights)
58
+ - Sensitive data or credentials
59
+
60
+ Args:
61
+ note: The note to append. Use markdown. Start with a heading or bullet.
62
+ Example: "- **Build**: `npm run build` (requires Node 20+)"
63
+ Example: "## Architecture\\nAuth is handled by src/auth/middleware.ts"
64
+ """
65
+ if not note or not note.strip():
66
+ return {"error": "note cannot be empty"}
67
+ try:
68
+ notes_file.parent.mkdir(parents=True, exist_ok=True)
69
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
70
+ # If file doesn't exist, create with header
71
+ if not notes_file.exists():
72
+ header = f"# GemCode Agent Notes\n*Auto-generated project notes. Edit freely.*\n\n"
73
+ notes_file.write_text(header, encoding="utf-8")
74
+ current = notes_file.read_text(encoding="utf-8", errors="replace")
75
+ # Avoid duplicating identical notes
76
+ stripped = note.strip()
77
+ if stripped in current:
78
+ return {"status": "already_exists", "note": stripped}
79
+ entry = f"\n<!-- {timestamp} -->\n{stripped}\n"
80
+ notes_file.write_text(current + entry, encoding="utf-8")
81
+ return {"status": "appended", "notes_path": str(notes_file)}
82
+ except OSError as e:
83
+ return {"error": f"Could not write notes: {e}"}
84
+
85
+ def read_project_notes() -> dict[str, Any]:
86
+ """
87
+ Read the current contents of .gemcode/notes.md.
88
+
89
+ Use this to check what has been previously noted about this project before
90
+ starting a task — avoids re-discovering things already documented.
91
+
92
+ Returns the full notes content as a string, or an empty string if no notes exist.
93
+ """
94
+ if not notes_file.exists():
95
+ return {"content": "", "exists": False, "path": str(notes_file)}
96
+ try:
97
+ content = notes_file.read_text(encoding="utf-8", errors="replace")
98
+ return {"content": content, "exists": True, "path": str(notes_file), "chars": len(content)}
99
+ except OSError as e:
100
+ return {"error": f"Could not read notes: {e}"}
101
+
102
+ # Bind the project root to the function names so they show correctly in tool manifests
103
+ append_project_note.__name__ = "append_project_note"
104
+ read_project_notes.__name__ = "read_project_notes"
105
+
106
+ return [append_project_note, read_project_notes]
@@ -41,6 +41,9 @@ except ImportError:
41
41
  # ---------------------------------------------------------------------------
42
42
  SLASH_COMMANDS: list[tuple[str, str]] = [
43
43
  ("help", "List all available commands"),
44
+ ("init", "Analyze project and generate GEMINI.md project instructions"),
45
+ ("cost", "Show session token usage and estimated USD cost breakdown"),
46
+ ("notes", "View agent auto-generated project notes (.gemcode/notes.md)"),
44
47
  ("clear", "Start a fresh session (clears history) · alias: /session new"),
45
48
  ("model", "View or switch model · /model use <id> · /model list"),
46
49
  ("mode", "Set model mode · /mode fast|balanced|quality|auto"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.30
3
+ Version: 0.3.31
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -27,6 +27,7 @@ src/gemcode/mcp_loader.py
27
27
  src/gemcode/modality_tools.py
28
28
  src/gemcode/model_errors.py
29
29
  src/gemcode/model_routing.py
30
+ src/gemcode/openapi_loader.py
30
31
  src/gemcode/paths.py
31
32
  src/gemcode/permissions.py
32
33
  src/gemcode/pricing.py
@@ -70,6 +71,7 @@ src/gemcode/tools/bash.py
70
71
  src/gemcode/tools/browser.py
71
72
  src/gemcode/tools/edit.py
72
73
  src/gemcode/tools/filesystem.py
74
+ src/gemcode/tools/notes.py
73
75
  src/gemcode/tools/search.py
74
76
  src/gemcode/tools/shell.py
75
77
  src/gemcode/tools/shell_gate.py
@@ -1,57 +0,0 @@
1
- """
2
- Optional MCP toolsets from `.gemcode/mcp.json`.
3
-
4
- Schema (example):
5
- {
6
- "servers": [
7
- {
8
- "name": "docs",
9
- "stdio": { "command": "npx", "args": ["-y", "@some/mcp-server"] }
10
- }
11
- ]
12
- }
13
-
14
- Requires: pip install gemcode[mcp]
15
- """
16
-
17
- from __future__ import annotations
18
-
19
- import json
20
- from pathlib import Path
21
- from typing import Any
22
-
23
- from gemcode.config import GemCodeConfig
24
-
25
-
26
- def load_mcp_toolsets(cfg: GemCodeConfig) -> list:
27
- path = cfg.project_root / ".gemcode" / "mcp.json"
28
- if not path.is_file():
29
- return []
30
- try:
31
- data = json.loads(path.read_text(encoding="utf-8"))
32
- except json.JSONDecodeError as e:
33
- raise ValueError(f"Invalid mcp.json: {e}") from e
34
-
35
- try:
36
- from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
37
- from mcp import StdioServerParameters
38
- except ImportError as e:
39
- raise ImportError("Install MCP extras: pip install gemcode[mcp]") from e
40
-
41
- servers = data.get("servers") or []
42
- toolsets: list[Any] = []
43
- for s in servers:
44
- stdio = s.get("stdio") or {}
45
- cmd = stdio.get("command")
46
- args = stdio.get("args") or []
47
- if not cmd:
48
- continue
49
- params = StdioServerParameters(command=cmd, args=args)
50
- prefix = s.get("name") or "mcp"
51
- toolsets.append(
52
- McpToolset(
53
- connection_params=params,
54
- tool_name_prefix=prefix,
55
- )
56
- )
57
- return toolsets
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes