tools: persist spark_submit calls to SubmissionRepo

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tao.chen
2026-07-14 13:45:07 +08:00
co-authored by Claude
parent a2232cecc4
commit 573397dbb4
4 changed files with 162 additions and 31 deletions
+44 -18
View File
@@ -6,10 +6,12 @@ import (
"log/slog"
"sort"
"strconv"
"time"
"github.com/mark3labs/mcp-go/mcp"
"spark-mcp-go/internal/executor"
"spark-mcp-go/internal/storage"
)
const SparkSubmitName = "spark_submit"
@@ -98,7 +100,8 @@ func (d *Deps) SparkSubmitHandler(ctx context.Context, req mcp.CallToolRequest)
// letting spark-submit produce a confusing FileNotFoundException later.
// Placed after all required-string parses so error messages surface in
// field order (cluster_id, master, ..., script_path, queue, ...).
if _, err := d.UploadStore.Validate(scriptPath); err != nil {
fileID, err := d.UploadStore.Validate(scriptPath)
if err != nil {
if d.Logger != nil {
d.Logger.Warn("spark_submit.unminted_path_rejected", slog.String("path", scriptPath), slog.String("err", err.Error()))
}
@@ -173,35 +176,58 @@ func (d *Deps) SparkSubmitHandler(ctx context.Context, req mcp.CallToolRequest)
})
if err != nil {
callLog.WithError(err)
// Even on error, result carries ExitCode/Stderr for the LLM.
if result != nil {
callLog.WithResult(map[string]any{
"binary": binary,
"argv": cmd,
"app_id": result.AppID,
"exit_code": result.ExitCode,
"duration_ms": result.DurationMS,
})
callLog.WithResult(sparkSubmitResultLog(binary, cmd, result))
return textResult(encodeJSON(map[string]any{
"app_id": result.AppID,
"exit_code": result.ExitCode,
"stdout_tail": result.StdoutTail,
"stderr_tail": result.StderrTail,
"duration_ms": result.DurationMS,
"error": err.Error(),
"app_id": result.AppID,
"tracking_url": result.TrackingURL,
"exit_code": result.ExitCode,
"stdout_tail": result.StdoutTail,
"stderr_tail": result.StderrTail,
"duration_ms": result.DurationMS,
"error": err.Error(),
})), nil
}
return errResult("spark_submit: " + err.Error()), nil
}
callLog.WithResult(map[string]any{
if result.AppID != "" && d.SubmissionRepo != nil {
sub := storage.Submission{
FileID: fileID,
AppID: result.AppID,
TrackingURL: result.TrackingURL,
ClusterID: clusterID,
ExitCode: result.ExitCode,
DurationMS: result.DurationMS,
SubmittedAt: time.Now(),
}
if createErr := d.SubmissionRepo.Create(ctx, sub); createErr != nil {
if d.Logger != nil {
d.Logger.Warn("spark_submit.persist_failed", slog.String("file_id", fileID), slog.String("app_id", result.AppID), slog.String("err", createErr.Error()))
} else {
slog.Default().Warn("spark_submit.persist_failed", slog.String("file_id", fileID), slog.String("app_id", result.AppID), slog.String("err", createErr.Error()))
}
}
}
callLog.WithResult(sparkSubmitResultLog(binary, cmd, result))
return textResult(encodeJSON(result)), nil
}
// sparkSubmitResultLog builds the standard result map used for both logging
// and the admin audit trail.
func sparkSubmitResultLog(binary string, cmd []string, result *executor.Result) map[string]any {
m := map[string]any{
"binary": binary,
"argv": cmd,
"app_id": result.AppID,
"exit_code": result.ExitCode,
"duration_ms": result.DurationMS,
})
return textResult(encodeJSON(result)), nil
}
if result.TrackingURL != "" {
m["tracking_url"] = result.TrackingURL
}
return m
}
// SparkSubmitCommandOpts holds the structured arguments used to build the