This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+102
View File
@@ -25,6 +25,12 @@ type RunService interface {
Logs(ctx context.Context, c workspace.Caller, profileID uuid.UUID, since int64, limit int) ([]run.LogLineResp, error)
}
// ExecService 是一次性 exec 工具依赖的窄接口,*run.ExecService 直接满足。
type ExecService interface {
RunCommand(ctx context.Context, c workspace.Caller, req run.ExecCommandReq) (run.ExecCommandResp, error)
RunTests(ctx context.Context, c workspace.Caller, req run.ExecTestsReq) (run.ExecTestsResp, error)
}
// registerRunTools 注册 run profile 工具。日志只提供快照式 get_run_logs,
// 流式订阅(WebSocket)不经 MCP 暴露。
//
@@ -40,6 +46,8 @@ func registerRunTools(srv *mcpsdk.Server, deps ServerDeps) {
registerRestartRunProfile(srv, deps)
registerGetRunStatus(srv, deps)
registerGetRunLogs(srv, deps)
registerRunCommand(srv, deps)
registerRunTests(srv, deps)
}
// runProfileInWorkspace 校验 profile 归属于给定 workspace 并返回其 UUID。
@@ -324,3 +332,97 @@ func registerGetRunLogs(srv *mcpsdk.Server, deps ServerDeps) {
return nil, getRunLogsOut{Lines: lines}, nil
})
}
// ===== run_command =====
type runCommandIn struct {
WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"`
WorktreeID string `json:"worktree_id,omitempty" jsonschema:"worktree UUID; empty runs in workspace main_path"`
Command string `json:"command" jsonschema:"executable to run, e.g. go / npm (non-empty)"`
Args []string `json:"args,omitempty" jsonschema:"command arguments"`
Env map[string]string `json:"env,omitempty" jsonschema:"ephemeral env overrides (plaintext, not persisted)"`
TimeoutSec int `json:"timeout_sec,omitempty" jsonschema:"timeout in seconds; clamped to server max"`
}
func registerRunCommand(srv *mcpsdk.Server, deps ServerDeps) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: "run_command", Description: "Run one command to completion in the workspace (main_path or a worktree) and return exit code, stdout, stderr, duration. Sandboxed: only whitelisted env vars are passed; requires project write access."},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in runCommandIn) (*mcpsdk.CallToolResult, run.ExecCommandResp, error) {
if err := CheckToolFromCtx(ctx, "run_command"); err != nil {
return nil, run.ExecCommandResp{}, err
}
c, err := deps.Caller.Resolve(ctx)
if err != nil {
return nil, run.ExecCommandResp{}, err
}
w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
if err != nil {
return nil, run.ExecCommandResp{}, err
}
if in.WorktreeID != "" {
if _, err := worktreeInWorkspace(ctx, deps, wc, w.ID, in.WorktreeID); err != nil {
return nil, run.ExecCommandResp{}, err
}
}
resp, err := deps.Execs.RunCommand(ctx, wc, run.ExecCommandReq{
WorkspaceID: w.ID.String(),
WorktreeID: in.WorktreeID,
Command: in.Command,
Args: in.Args,
Env: in.Env,
TimeoutSec: in.TimeoutSec,
})
if err != nil {
return nil, run.ExecCommandResp{}, err
}
return nil, resp, nil
})
}
// ===== run_tests =====
type runTestsIn struct {
WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"`
WorktreeID string `json:"worktree_id,omitempty" jsonschema:"worktree UUID; empty runs in workspace main_path"`
Command string `json:"command" jsonschema:"test runner, e.g. go (non-empty)"`
Args []string `json:"args,omitempty" jsonschema:"args, e.g. ['test','-json','./...']"`
Format string `json:"format,omitempty" jsonschema:"output format: gotest|junit|vitest|auto (default auto)"`
Env map[string]string `json:"env,omitempty" jsonschema:"ephemeral env overrides (plaintext, not persisted)"`
TimeoutSec int `json:"timeout_sec,omitempty" jsonschema:"timeout in seconds; clamped to server max"`
}
func registerRunTests(srv *mcpsdk.Server, deps ServerDeps) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: "run_tests", Description: "Run a test command and parse its output (go test -json / JUnit XML / vitest JSON) into a structured pass/fail summary. Returns raw output plus a summary; never fails on parse errors."},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in runTestsIn) (*mcpsdk.CallToolResult, run.ExecTestsResp, error) {
if err := CheckToolFromCtx(ctx, "run_tests"); err != nil {
return nil, run.ExecTestsResp{}, err
}
c, err := deps.Caller.Resolve(ctx)
if err != nil {
return nil, run.ExecTestsResp{}, err
}
w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
if err != nil {
return nil, run.ExecTestsResp{}, err
}
if in.WorktreeID != "" {
if _, err := worktreeInWorkspace(ctx, deps, wc, w.ID, in.WorktreeID); err != nil {
return nil, run.ExecTestsResp{}, err
}
}
resp, err := deps.Execs.RunTests(ctx, wc, run.ExecTestsReq{
WorkspaceID: w.ID.String(),
WorktreeID: in.WorktreeID,
Command: in.Command,
Args: in.Args,
Format: in.Format,
Env: in.Env,
TimeoutSec: in.TimeoutSec,
})
if err != nil {
return nil, run.ExecTestsResp{}, err
}
return nil, resp, nil
})
}