@hunyed15/codecgc 0.1.0
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.
- package/.claude/hooks/route-edit.ps1 +86 -0
- package/INSTALLATION.md +550 -0
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/bin/cgc-build.js +4 -0
- package/bin/cgc-doctor.js +4 -0
- package/bin/cgc-entry.js +4 -0
- package/bin/cgc-external-audit.js +4 -0
- package/bin/cgc-fix.js +4 -0
- package/bin/cgc-history.js +4 -0
- package/bin/cgc-install.js +4 -0
- package/bin/cgc-lifecycle.js +4 -0
- package/bin/cgc-package-audit.js +4 -0
- package/bin/cgc-plan.js +4 -0
- package/bin/cgc-release-readiness.js +4 -0
- package/bin/cgc-review.js +4 -0
- package/bin/cgc-route.js +4 -0
- package/bin/cgc-status.js +4 -0
- package/bin/cgc-test.js +4 -0
- package/bin/cgc.js +4 -0
- package/bin/codecgc.js +1284 -0
- package/codecgc/cgc/SKILL.md +46 -0
- package/codecgc/cgc-arch/SKILL.md +61 -0
- package/codecgc/cgc-build/SKILL.md +53 -0
- package/codecgc/cgc-decide/SKILL.md +55 -0
- package/codecgc/cgc-fix/SKILL.md +47 -0
- package/codecgc/cgc-learn/SKILL.md +46 -0
- package/codecgc/cgc-onboard/SKILL.md +52 -0
- package/codecgc/cgc-plan/SKILL.md +48 -0
- package/codecgc/cgc-refactor/SKILL.md +46 -0
- package/codecgc/cgc-req/SKILL.md +61 -0
- package/codecgc/cgc-review/SKILL.md +57 -0
- package/codecgc/cgc-roadmap/SKILL.md +55 -0
- package/codecgc/cgc-test/SKILL.md +21 -0
- package/codecgc/reference/api-cgc-review-libdoc.md +13 -0
- package/codecgc/reference/artifact-class-policy.md +81 -0
- package/codecgc/reference/build-flow.md +95 -0
- package/codecgc/reference/checklist-contract.md +103 -0
- package/codecgc/reference/execution-audit.md +121 -0
- package/codecgc/reference/execution-model.md +118 -0
- package/codecgc/reference/execution-routing.md +130 -0
- package/codecgc/reference/executor-contract.md +87 -0
- package/codecgc/reference/external-capability-registry.json +104 -0
- package/codecgc/reference/fix-flow.md +94 -0
- package/codecgc/reference/fixture-governance.md +60 -0
- package/codecgc/reference/flow-execution.md +65 -0
- package/codecgc/reference/lifecycle-map.md +172 -0
- package/codecgc/reference/lifecycle-playbook.md +104 -0
- package/codecgc/reference/long-lived-artifacts.md +98 -0
- package/codecgc/reference/operation-guide.md +242 -0
- package/codecgc/reference/release-maintenance-playbook.md +150 -0
- package/codecgc/reference/review-writeback.md +141 -0
- package/codecgc/reference/role-model.md +128 -0
- package/codecgc/reference/runtime-boundary.md +72 -0
- package/codecgc/reference/shared-conventions.md +93 -0
- package/codecgc/reference/workflow-scaffold.md +57 -0
- package/codexmcp/LICENSE +21 -0
- package/codexmcp/README.md +294 -0
- package/codexmcp/pyproject.toml +37 -0
- package/codexmcp/src/codexmcp/__init__.py +4 -0
- package/codexmcp/src/codexmcp/cli.py +12 -0
- package/codexmcp/src/codexmcp/server.py +529 -0
- package/geminimcp/README.md +258 -0
- package/geminimcp/pyproject.toml +15 -0
- package/geminimcp/src/geminimcp/__init__.py +4 -0
- package/geminimcp/src/geminimcp/cli.py +12 -0
- package/geminimcp/src/geminimcp/server.py +465 -0
- package/model-routing.yaml +30 -0
- package/package.json +90 -0
- package/requirements.txt +1 -0
- package/scripts/README-codecgc-cli.md +89 -0
- package/scripts/audit_codecgc_external_capabilities.py +276 -0
- package/scripts/audit_codecgc_historical_audits.py +242 -0
- package/scripts/audit_codecgc_lifecycle.py +241 -0
- package/scripts/audit_codecgc_package_runtime.py +445 -0
- package/scripts/audit_codecgc_release_readiness.py +202 -0
- package/scripts/audit_codecgc_review_policy.py +82 -0
- package/scripts/audit_codecgc_workflow_history.py +317 -0
- package/scripts/build_codecgc_task.py +487 -0
- package/scripts/codecgc_artifact_roots.py +40 -0
- package/scripts/codecgc_cli.py +843 -0
- package/scripts/codecgc_command_surface.py +28 -0
- package/scripts/codecgc_console_io.py +45 -0
- package/scripts/codecgc_executor_registry.py +54 -0
- package/scripts/codecgc_file_evidence.py +349 -0
- package/scripts/codecgc_flow_control.py +233 -0
- package/scripts/codecgc_governance_dedupe.py +161 -0
- package/scripts/codecgc_plan_decision.py +103 -0
- package/scripts/codecgc_review_control.py +588 -0
- package/scripts/codecgc_roadmap_templates.py +149 -0
- package/scripts/codecgc_routing_paths.py +16 -0
- package/scripts/codecgc_routing_template.py +135 -0
- package/scripts/codecgc_runtime_paths.py +22 -0
- package/scripts/codecgc_session_recovery.py +44 -0
- package/scripts/codecgc_step_control.py +154 -0
- package/scripts/codecgc_workflow_runtime.py +63 -0
- package/scripts/codecgc_workflow_templates.py +437 -0
- package/scripts/entry_codecgc_workflow.py +3419 -0
- package/scripts/exercise_mcp_tools.py +109 -0
- package/scripts/expand_codecgc_roadmap.py +664 -0
- package/scripts/init_codecgc_roadmap.py +134 -0
- package/scripts/init_codecgc_workflow.py +207 -0
- package/scripts/install_codecgc.py +938 -0
- package/scripts/migrate_demo_workflows_to_fixtures.py +128 -0
- package/scripts/normalize_codecgc_audits.py +114 -0
- package/scripts/normalize_codecgc_governance_docs.py +79 -0
- package/scripts/normalize_codecgc_workflow_docs.py +269 -0
- package/scripts/plan_codecgc_workflow.py +970 -0
- package/scripts/refresh_codecgc_review_policy.py +223 -0
- package/scripts/review_codecgc_workflow.py +88 -0
- package/scripts/route_codecgc_workflow.py +671 -0
- package/scripts/run_codecgc_build.py +104 -0
- package/scripts/run_codecgc_fix.py +104 -0
- package/scripts/run_codecgc_flow_step.py +165 -0
- package/scripts/run_codecgc_task.py +410 -0
- package/scripts/run_codecgc_test.py +105 -0
- package/scripts/sync_codecgc_mcp_config.py +41 -0
- package/scripts/write_codecgc_architecture.py +78 -0
- package/scripts/write_codecgc_decision.py +83 -0
- package/scripts/write_codecgc_explore.py +118 -0
- package/scripts/write_codecgc_guide.py +141 -0
- package/scripts/write_codecgc_learning.py +87 -0
- package/scripts/write_codecgc_libdoc.py +140 -0
- package/scripts/write_codecgc_refactor.py +78 -0
- package/scripts/write_codecgc_requirement.py +78 -0
- package/scripts/write_codecgc_review.py +291 -0
- package/scripts/write_codecgc_roadmap.py +122 -0
- package/scripts/write_codecgc_trick.py +123 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"""FastMCP server implementation for the Codex MCP project."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import queue
|
|
8
|
+
import subprocess
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Annotated, Any, Dict, Generator, List, Literal, Optional
|
|
14
|
+
|
|
15
|
+
from mcp.server.fastmcp import FastMCP
|
|
16
|
+
from pydantic import BeforeValidator, Field
|
|
17
|
+
import shutil
|
|
18
|
+
|
|
19
|
+
mcp = FastMCP("Codex MCP Server-from guda.studio")
|
|
20
|
+
|
|
21
|
+
# Mirror of model-routing.yaml frontend_paths — keep these hints in sync with
|
|
22
|
+
# route-edit.ps1 and geminimcp/server.py BACKEND_PATH_HINTS.
|
|
23
|
+
FRONTEND_PATH_HINTS = (
|
|
24
|
+
"apps/web/",
|
|
25
|
+
"src/components/",
|
|
26
|
+
"src/pages/",
|
|
27
|
+
"src/app/",
|
|
28
|
+
"src/styles/",
|
|
29
|
+
"src/ui/",
|
|
30
|
+
"components/",
|
|
31
|
+
"pages/",
|
|
32
|
+
"app/",
|
|
33
|
+
"styles/",
|
|
34
|
+
"ui/",
|
|
35
|
+
"web/",
|
|
36
|
+
"frontend/",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
FRONTEND_FILE_SUFFIXES = (
|
|
40
|
+
".tsx",
|
|
41
|
+
".jsx",
|
|
42
|
+
".css",
|
|
43
|
+
".scss",
|
|
44
|
+
".sass",
|
|
45
|
+
".less",
|
|
46
|
+
".styl",
|
|
47
|
+
".html",
|
|
48
|
+
".vue",
|
|
49
|
+
".svelte",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _empty_str_to_none(value: str | None) -> str | None:
|
|
54
|
+
"""Convert empty strings to None for optional UUID parameters."""
|
|
55
|
+
if isinstance(value, str) and not value.strip():
|
|
56
|
+
return None
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _normalize_path_text(path_value: Path | str) -> str:
|
|
61
|
+
"""Normalize a path-like value to a forward-slash string."""
|
|
62
|
+
return str(path_value).replace("\\", "/").strip()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_probably_frontend_path(path_value: Path | str) -> bool:
|
|
66
|
+
"""Best-effort check to keep backend-only Codex tasks away from frontend files."""
|
|
67
|
+
normalized = _normalize_path_text(path_value).lower().lstrip("./")
|
|
68
|
+
if any(hint in normalized for hint in FRONTEND_PATH_HINTS):
|
|
69
|
+
return True
|
|
70
|
+
suffix = Path(normalized).suffix.lower()
|
|
71
|
+
return suffix in FRONTEND_FILE_SUFFIXES
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _build_backend_task_prompt(
|
|
75
|
+
task_summary: str,
|
|
76
|
+
target_paths: List[Path],
|
|
77
|
+
constraints: List[str],
|
|
78
|
+
acceptance_criteria: List[str],
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Build a constrained prompt for backend implementation tasks."""
|
|
81
|
+
lines = [
|
|
82
|
+
"You are implementing a backend-only coding task.",
|
|
83
|
+
"Stay strictly within the provided target paths.",
|
|
84
|
+
"Do not edit frontend files, styling files, or UI components.",
|
|
85
|
+
"Return a concise implementation summary, followed by risks if any remain.",
|
|
86
|
+
"",
|
|
87
|
+
"Task summary:",
|
|
88
|
+
task_summary.strip(),
|
|
89
|
+
"",
|
|
90
|
+
"Target paths:",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
lines.extend(f"- {_normalize_path_text(path)}" for path in target_paths)
|
|
94
|
+
|
|
95
|
+
if constraints:
|
|
96
|
+
lines.append("")
|
|
97
|
+
lines.append("Constraints:")
|
|
98
|
+
lines.extend(f"- {item.strip()}" for item in constraints if item.strip())
|
|
99
|
+
|
|
100
|
+
if acceptance_criteria:
|
|
101
|
+
lines.append("")
|
|
102
|
+
lines.append("Acceptance criteria:")
|
|
103
|
+
lines.extend(f"- {item.strip()}" for item in acceptance_criteria if item.strip())
|
|
104
|
+
|
|
105
|
+
return "\n".join(lines).strip()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _validate_backend_target_paths(target_paths: List[Path]) -> tuple[bool, List[str], str]:
|
|
109
|
+
"""Validate backend task target paths and return policy check notes."""
|
|
110
|
+
if not target_paths:
|
|
111
|
+
return False, [], "The `target_paths` field must contain at least one file or directory."
|
|
112
|
+
|
|
113
|
+
normalized_paths = [_normalize_path_text(path) for path in target_paths]
|
|
114
|
+
frontend_hits = [
|
|
115
|
+
path_text
|
|
116
|
+
for path_text in normalized_paths
|
|
117
|
+
if _is_probably_frontend_path(path_text)
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
policy_checks = [
|
|
121
|
+
"target_paths_present",
|
|
122
|
+
"backend_scope_requested",
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
if frontend_hits:
|
|
126
|
+
return (
|
|
127
|
+
False,
|
|
128
|
+
policy_checks,
|
|
129
|
+
"The backend executor refused frontend-like paths: "
|
|
130
|
+
+ ", ".join(frontend_hits),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
policy_checks.append("frontend_boundary_check_passed")
|
|
134
|
+
return True, policy_checks, ""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def run_shell_command(cmd: list[str]) -> Generator[str, None, None]:
|
|
138
|
+
"""Execute a command and stream its output line-by-line.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
cmd: Command and arguments as a list (e.g., ["codex", "exec", "prompt"])
|
|
142
|
+
|
|
143
|
+
Yields:
|
|
144
|
+
Output lines from the command
|
|
145
|
+
"""
|
|
146
|
+
# On Windows, codex is exposed via a *.cmd shim. Use cmd.exe with /s so
|
|
147
|
+
# user prompts containing quotes/newlines aren't reinterpreted as shell syntax.
|
|
148
|
+
popen_cmd = cmd.copy()
|
|
149
|
+
codex_path = shutil.which('codex') or cmd[0]
|
|
150
|
+
popen_cmd[0] = codex_path
|
|
151
|
+
|
|
152
|
+
process = subprocess.Popen(
|
|
153
|
+
popen_cmd,
|
|
154
|
+
shell=False,
|
|
155
|
+
stdin=subprocess.DEVNULL,
|
|
156
|
+
stdout=subprocess.PIPE,
|
|
157
|
+
stderr=subprocess.STDOUT,
|
|
158
|
+
universal_newlines=True,
|
|
159
|
+
encoding='utf-8',
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
output_queue: queue.Queue[str | None] = queue.Queue()
|
|
163
|
+
GRACEFUL_SHUTDOWN_DELAY = 0.3
|
|
164
|
+
|
|
165
|
+
def is_turn_completed(line: str) -> bool:
|
|
166
|
+
"""Check if the line indicates turn completion via JSON parsing."""
|
|
167
|
+
try:
|
|
168
|
+
data = json.loads(line)
|
|
169
|
+
return data.get("type") == "turn.completed"
|
|
170
|
+
except (json.JSONDecodeError, AttributeError, TypeError):
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
def read_output() -> None:
|
|
174
|
+
"""Read process output in a separate thread."""
|
|
175
|
+
if process.stdout:
|
|
176
|
+
for line in iter(process.stdout.readline, ""):
|
|
177
|
+
stripped = line.strip()
|
|
178
|
+
output_queue.put(stripped)
|
|
179
|
+
if is_turn_completed(stripped):
|
|
180
|
+
time.sleep(GRACEFUL_SHUTDOWN_DELAY)
|
|
181
|
+
process.terminate()
|
|
182
|
+
break
|
|
183
|
+
process.stdout.close()
|
|
184
|
+
output_queue.put(None)
|
|
185
|
+
|
|
186
|
+
thread = threading.Thread(target=read_output)
|
|
187
|
+
thread.start()
|
|
188
|
+
|
|
189
|
+
# Yield lines while process is running
|
|
190
|
+
while True:
|
|
191
|
+
try:
|
|
192
|
+
line = output_queue.get(timeout=0.5)
|
|
193
|
+
if line is None:
|
|
194
|
+
break
|
|
195
|
+
yield line
|
|
196
|
+
except queue.Empty:
|
|
197
|
+
if process.poll() is not None and not thread.is_alive():
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
process.wait(timeout=5)
|
|
202
|
+
except subprocess.TimeoutExpired:
|
|
203
|
+
process.kill()
|
|
204
|
+
process.wait()
|
|
205
|
+
thread.join(timeout=5)
|
|
206
|
+
|
|
207
|
+
while not output_queue.empty():
|
|
208
|
+
try:
|
|
209
|
+
line = output_queue.get_nowait()
|
|
210
|
+
if line is not None:
|
|
211
|
+
yield line
|
|
212
|
+
except queue.Empty:
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _execute_codex_session(
|
|
217
|
+
*,
|
|
218
|
+
prompt: str,
|
|
219
|
+
cd: Path,
|
|
220
|
+
sandbox: Literal["read-only", "workspace-write", "danger-full-access"],
|
|
221
|
+
session_id: str,
|
|
222
|
+
skip_git_repo_check: bool,
|
|
223
|
+
return_all_messages: bool,
|
|
224
|
+
image: List[Path],
|
|
225
|
+
model: str,
|
|
226
|
+
yolo: bool,
|
|
227
|
+
profile: str,
|
|
228
|
+
) -> Dict[str, Any]:
|
|
229
|
+
"""Execute Codex CLI and return the parsed MCP response payload."""
|
|
230
|
+
if not cd.exists():
|
|
231
|
+
return {
|
|
232
|
+
"success": False,
|
|
233
|
+
"error": f"The workspace root directory `{cd.absolute().as_posix()}` does not exist.",
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
cmd = ["codex", "exec", "--sandbox", sandbox, "--cd", str(cd), "--json"]
|
|
237
|
+
|
|
238
|
+
if len(image):
|
|
239
|
+
cmd.extend(["--image", ",".join(image)])
|
|
240
|
+
|
|
241
|
+
if model:
|
|
242
|
+
cmd.extend(["--model", model])
|
|
243
|
+
|
|
244
|
+
if profile:
|
|
245
|
+
cmd.extend(["--profile", profile])
|
|
246
|
+
|
|
247
|
+
if yolo:
|
|
248
|
+
cmd.append("--yolo")
|
|
249
|
+
|
|
250
|
+
if skip_git_repo_check:
|
|
251
|
+
cmd.append("--skip-git-repo-check")
|
|
252
|
+
|
|
253
|
+
if session_id:
|
|
254
|
+
cmd.extend(["resume", str(session_id)])
|
|
255
|
+
|
|
256
|
+
if os.name == "nt":
|
|
257
|
+
prompt = windows_escape(prompt)
|
|
258
|
+
cmd += ["--", prompt]
|
|
259
|
+
|
|
260
|
+
all_messages: list[Dict[str, Any]] = []
|
|
261
|
+
agent_messages = ""
|
|
262
|
+
success = True
|
|
263
|
+
err_message = ""
|
|
264
|
+
thread_id: Optional[str] = None
|
|
265
|
+
|
|
266
|
+
for line in run_shell_command(cmd):
|
|
267
|
+
try:
|
|
268
|
+
line_dict = json.loads(line.strip())
|
|
269
|
+
all_messages.append(line_dict)
|
|
270
|
+
item = line_dict.get("item", {})
|
|
271
|
+
item_type = item.get("type", "")
|
|
272
|
+
if item_type == "agent_message":
|
|
273
|
+
agent_messages = agent_messages + item.get("text", "")
|
|
274
|
+
if line_dict.get("thread_id") is not None:
|
|
275
|
+
thread_id = line_dict.get("thread_id")
|
|
276
|
+
if "fail" in line_dict.get("type", ""):
|
|
277
|
+
success = False if len(agent_messages) == 0 else success
|
|
278
|
+
err_message += "\n\n[codex error] " + line_dict.get("error", {}).get("message", "")
|
|
279
|
+
if "error" in line_dict.get("type", ""):
|
|
280
|
+
error_msg = line_dict.get("message", "")
|
|
281
|
+
import re
|
|
282
|
+
|
|
283
|
+
is_reconnecting = bool(re.match(r"^Reconnecting\.\.\.\s+\d+/\d+", error_msg))
|
|
284
|
+
if not is_reconnecting:
|
|
285
|
+
success = False if len(agent_messages) == 0 else success
|
|
286
|
+
err_message += "\n\n[codex error] " + error_msg
|
|
287
|
+
|
|
288
|
+
except json.JSONDecodeError:
|
|
289
|
+
err_message += "\n\n[json decode error] " + line
|
|
290
|
+
continue
|
|
291
|
+
except Exception as error:
|
|
292
|
+
err_message += "\n\n[unexpected error] " + f"Unexpected error: {error}. Line: {line!r}"
|
|
293
|
+
success = False
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
if thread_id is None:
|
|
297
|
+
success = False
|
|
298
|
+
err_message = "Failed to get `SESSION_ID` from the codex session. \n\n" + err_message
|
|
299
|
+
|
|
300
|
+
if len(agent_messages) == 0:
|
|
301
|
+
success = False
|
|
302
|
+
err_message = (
|
|
303
|
+
"Failed to get `agent_messages` from the codex session. "
|
|
304
|
+
"\n\n You can try to set `return_all_messages` to `True` to get the full reasoning information. "
|
|
305
|
+
+ err_message
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if success:
|
|
309
|
+
result: Dict[str, Any] = {
|
|
310
|
+
"success": True,
|
|
311
|
+
"SESSION_ID": thread_id,
|
|
312
|
+
"agent_messages": agent_messages,
|
|
313
|
+
}
|
|
314
|
+
else:
|
|
315
|
+
result = {"success": False, "error": err_message}
|
|
316
|
+
|
|
317
|
+
if return_all_messages:
|
|
318
|
+
result["all_messages"] = all_messages
|
|
319
|
+
|
|
320
|
+
return result
|
|
321
|
+
|
|
322
|
+
def windows_escape(prompt):
|
|
323
|
+
"""
|
|
324
|
+
Windows 风格的字符串转义函数。
|
|
325
|
+
把常见特殊字符转义成 \\ 形式,适合命令行、JSON 或路径使用。
|
|
326
|
+
比如:\n 变成 \\n," 变成 \\"。
|
|
327
|
+
"""
|
|
328
|
+
# 先处理反斜杠,避免它干扰其他替换
|
|
329
|
+
result = prompt.replace('\\', '\\\\')
|
|
330
|
+
# 双引号,转义成 \",防止字符串边界乱套
|
|
331
|
+
result = result.replace('"', '\\"')
|
|
332
|
+
# 换行符,Windows 常用 \r\n,但我们分开转义
|
|
333
|
+
result = result.replace('\n', '\\n')
|
|
334
|
+
result = result.replace('\r', '\\r')
|
|
335
|
+
# 制表符,空格的“超级版”
|
|
336
|
+
result = result.replace('\t', '\\t')
|
|
337
|
+
# 其他常见:退格符(像按了后退键)、换页符(打印机跳页用)
|
|
338
|
+
result = result.replace('\b', '\\b')
|
|
339
|
+
result = result.replace('\f', '\\f')
|
|
340
|
+
# 如果有单引号,也转义下(不过 Windows 命令行不那么严格,但保险起见)
|
|
341
|
+
result = result.replace("'", "\\'")
|
|
342
|
+
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
@mcp.tool(
|
|
346
|
+
name="codex",
|
|
347
|
+
description="""
|
|
348
|
+
Executes a non-interactive Codex session via CLI to perform AI-assisted coding tasks in a secure workspace.
|
|
349
|
+
This tool wraps the `codex exec` command, enabling model-driven code generation, debugging, or automation based on natural language prompts.
|
|
350
|
+
It supports resuming ongoing sessions for continuity and enforces sandbox policies to prevent unsafe operations. Ideal for integrating Codex into MCP servers for agentic workflows, such as code reviews or repo modifications.
|
|
351
|
+
|
|
352
|
+
**Key Features:**
|
|
353
|
+
- **Prompt-Driven Execution:** Send task instructions to Codex for step-by-step code handling.
|
|
354
|
+
- **Workspace Isolation:** Operate within a specified directory, with optional Git repo skipping.
|
|
355
|
+
- **Security Controls:** Three sandbox levels balance functionality and safety.
|
|
356
|
+
- **Session Persistence:** Resume prior conversations via `SESSION_ID` for iterative tasks.
|
|
357
|
+
|
|
358
|
+
**Edge Cases & Best Practices:**
|
|
359
|
+
- Ensure `cd` exists and is accessible; tool fails silently on invalid paths.
|
|
360
|
+
- For most repos, prefer "read-only" to avoid accidental changes.
|
|
361
|
+
- If needed, set `return_all_messages` to `True` to parse "all_messages" for detailed tracing (e.g., reasoning, tool calls, etc.).
|
|
362
|
+
""",
|
|
363
|
+
meta={"version": "0.0.0", "author": "guda.studio"},
|
|
364
|
+
)
|
|
365
|
+
async def codex(
|
|
366
|
+
PROMPT: Annotated[str, "Instruction for the task to send to codex."],
|
|
367
|
+
cd: Annotated[Path, "Set the workspace root for codex before executing the task."],
|
|
368
|
+
sandbox: Annotated[
|
|
369
|
+
Literal["read-only", "workspace-write", "danger-full-access"],
|
|
370
|
+
Field(
|
|
371
|
+
description="Sandbox policy for model-generated commands. Defaults to `read-only`."
|
|
372
|
+
),
|
|
373
|
+
] = "read-only",
|
|
374
|
+
SESSION_ID: Annotated[
|
|
375
|
+
str,
|
|
376
|
+
"Resume the specified session of the codex. Defaults to `None`, start a new session.",
|
|
377
|
+
] = "",
|
|
378
|
+
skip_git_repo_check: Annotated[
|
|
379
|
+
bool,
|
|
380
|
+
"Allow codex running outside a Git repository (useful for one-off directories).",
|
|
381
|
+
] = True,
|
|
382
|
+
return_all_messages: Annotated[
|
|
383
|
+
bool,
|
|
384
|
+
"Return all messages (e.g. reasoning, tool calls, etc.) from the codex session. Set to `False` by default, only the agent's final reply message is returned.",
|
|
385
|
+
] = False,
|
|
386
|
+
image: Annotated[
|
|
387
|
+
List[Path],
|
|
388
|
+
Field(
|
|
389
|
+
description="Attach one or more image files to the initial prompt. Separate multiple paths with commas or repeat the flag.",
|
|
390
|
+
),
|
|
391
|
+
] = [],
|
|
392
|
+
model: Annotated[
|
|
393
|
+
str,
|
|
394
|
+
Field(
|
|
395
|
+
description="The model to use for the codex session. This parameter is strictly prohibited unless explicitly specified by the user.",
|
|
396
|
+
),
|
|
397
|
+
] = "",
|
|
398
|
+
yolo: Annotated[
|
|
399
|
+
bool,
|
|
400
|
+
Field(
|
|
401
|
+
description="Run every command without approvals or sandboxing. Only use when `sandbox` couldn't be applied.",
|
|
402
|
+
),
|
|
403
|
+
] = False,
|
|
404
|
+
profile: Annotated[
|
|
405
|
+
str,
|
|
406
|
+
"Configuration profile name to load from `~/.codex/config.toml`. This parameter is strictly prohibited unless explicitly specified by the user.",
|
|
407
|
+
] = "",
|
|
408
|
+
) -> Dict[str, Any]:
|
|
409
|
+
"""Execute a Codex CLI session and return the results."""
|
|
410
|
+
return _execute_codex_session(
|
|
411
|
+
prompt=PROMPT,
|
|
412
|
+
cd=cd,
|
|
413
|
+
sandbox=sandbox,
|
|
414
|
+
session_id=SESSION_ID,
|
|
415
|
+
skip_git_repo_check=skip_git_repo_check,
|
|
416
|
+
return_all_messages=return_all_messages,
|
|
417
|
+
image=image,
|
|
418
|
+
model=model,
|
|
419
|
+
yolo=yolo,
|
|
420
|
+
profile=profile,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@mcp.tool(
|
|
425
|
+
name="implement_backend_task",
|
|
426
|
+
description="""
|
|
427
|
+
Executes a backend-only implementation task via Codex with extra policy checks.
|
|
428
|
+
Use this tool when Claude has already completed planning/design and needs Codex to
|
|
429
|
+
perform the actual backend code changes inside a constrained path set.
|
|
430
|
+
""",
|
|
431
|
+
meta={"version": "0.1.0", "author": "CodeCGC"},
|
|
432
|
+
)
|
|
433
|
+
async def implement_backend_task(
|
|
434
|
+
task_id: Annotated[str, "Stable task identifier for audit and review."],
|
|
435
|
+
task_summary: Annotated[str, "Backend implementation summary prepared by the orchestrator."],
|
|
436
|
+
target_paths: Annotated[
|
|
437
|
+
List[Path],
|
|
438
|
+
Field(
|
|
439
|
+
description="Backend paths Codex is allowed to touch for this task.",
|
|
440
|
+
),
|
|
441
|
+
],
|
|
442
|
+
constraints: Annotated[
|
|
443
|
+
List[str],
|
|
444
|
+
Field(
|
|
445
|
+
description="Non-negotiable implementation constraints for this backend task.",
|
|
446
|
+
),
|
|
447
|
+
] = [],
|
|
448
|
+
acceptance_criteria: Annotated[
|
|
449
|
+
List[str],
|
|
450
|
+
Field(
|
|
451
|
+
description="Acceptance criteria the implementation should satisfy.",
|
|
452
|
+
),
|
|
453
|
+
] = [],
|
|
454
|
+
cd: Annotated[Path, "Workspace root for the backend task." ] = Path("."),
|
|
455
|
+
SESSION_ID: Annotated[
|
|
456
|
+
str,
|
|
457
|
+
"Resume the specified Codex session. Empty string starts a new session.",
|
|
458
|
+
] = "",
|
|
459
|
+
sandbox: Annotated[
|
|
460
|
+
Literal["read-only", "workspace-write", "danger-full-access"],
|
|
461
|
+
Field(
|
|
462
|
+
description="Sandbox policy for the backend task. Defaults to `workspace-write`."
|
|
463
|
+
),
|
|
464
|
+
] = "workspace-write",
|
|
465
|
+
return_all_messages: Annotated[
|
|
466
|
+
bool,
|
|
467
|
+
"Return full Codex event logs for debugging. Defaults to `False`.",
|
|
468
|
+
] = False,
|
|
469
|
+
model: Annotated[
|
|
470
|
+
str,
|
|
471
|
+
Field(
|
|
472
|
+
description="Optional model override. Only use when explicitly requested by the user.",
|
|
473
|
+
),
|
|
474
|
+
] = "",
|
|
475
|
+
profile: Annotated[
|
|
476
|
+
str,
|
|
477
|
+
"Optional Codex profile from `~/.codex/config.toml`.",
|
|
478
|
+
] = "",
|
|
479
|
+
) -> Dict[str, Any]:
|
|
480
|
+
"""Execute a backend-only Codex task with CodeCGC policy checks."""
|
|
481
|
+
valid, policy_checks, validation_error = _validate_backend_target_paths(target_paths)
|
|
482
|
+
if not valid:
|
|
483
|
+
return {
|
|
484
|
+
"success": False,
|
|
485
|
+
"task_id": task_id,
|
|
486
|
+
"policy_checks": policy_checks,
|
|
487
|
+
"error": validation_error,
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
prompt = _build_backend_task_prompt(
|
|
491
|
+
task_summary=task_summary,
|
|
492
|
+
target_paths=target_paths,
|
|
493
|
+
constraints=constraints,
|
|
494
|
+
acceptance_criteria=acceptance_criteria,
|
|
495
|
+
)
|
|
496
|
+
result = _execute_codex_session(
|
|
497
|
+
prompt=prompt,
|
|
498
|
+
cd=cd,
|
|
499
|
+
sandbox=sandbox,
|
|
500
|
+
session_id=SESSION_ID,
|
|
501
|
+
skip_git_repo_check=True,
|
|
502
|
+
return_all_messages=return_all_messages,
|
|
503
|
+
image=[],
|
|
504
|
+
model=model,
|
|
505
|
+
yolo=False,
|
|
506
|
+
profile=profile,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
if not result.get("success"):
|
|
510
|
+
result["task_id"] = task_id
|
|
511
|
+
result["policy_checks"] = policy_checks
|
|
512
|
+
return result
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
"success": True,
|
|
516
|
+
"task_id": task_id,
|
|
517
|
+
"SESSION_ID": result["SESSION_ID"],
|
|
518
|
+
"summary": result["agent_messages"],
|
|
519
|
+
"agent_messages": result["agent_messages"],
|
|
520
|
+
"changed_files": [_normalize_path_text(path) for path in target_paths],
|
|
521
|
+
"policy_checks": policy_checks + ["backend_executor_completed"],
|
|
522
|
+
"risks": [],
|
|
523
|
+
**({"all_messages": result["all_messages"]} if return_all_messages and "all_messages" in result else {}),
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def run() -> None:
|
|
528
|
+
"""Start the MCP server over stdio transport."""
|
|
529
|
+
mcp.run(transport="stdio")
|