Initial commit: Gitea MCP Server
This commit is contained in:
@@ -0,0 +1,598 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/gitea-mcp/pkg/errors"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/params"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
const (
|
||||
MonitorWorkflowDispatchToolName = "monitor_workflow_dispatch"
|
||||
DefaultPollInterval = 10 * time.Second
|
||||
DefaultTimeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
MonitorWorkflowDispatchTool = mcp.NewTool(
|
||||
MonitorWorkflowDispatchToolName,
|
||||
mcp.WithDescription("Dispatch a workflow and monitor its execution until completion. Returns full execution summary including run ID, status, conclusion, duration, and logs."),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithString("workflow_id", mcp.Required(), mcp.Description("workflow ID or filename")),
|
||||
mcp.WithString("ref", mcp.Required(), mcp.Description("git ref (branch/tag) to run workflow on")),
|
||||
mcp.WithObject("inputs", mcp.Description("workflow inputs object")),
|
||||
mcp.WithNumber("timeout_seconds", mcp.Description("polling timeout in seconds (default: 300 = 5 minutes)"), mcp.DefaultNumber(300), mcp.Min(10)),
|
||||
mcp.WithNumber("poll_interval_seconds", mcp.Description("poll interval in seconds (default: 10)"), mcp.DefaultNumber(10), mcp.Min(5)),
|
||||
)
|
||||
)
|
||||
|
||||
type MonitorResult struct {
|
||||
RunID int64 `json:"run_id"`
|
||||
Status string `json:"status"`
|
||||
Conclusion string `json:"conclusion"`
|
||||
WorkflowID string `json:"workflow_id"`
|
||||
WorkflowName string `json:"workflow_name,omitempty"`
|
||||
Branch string `json:"branch"`
|
||||
CommitSHA string `json:"commit_sha"`
|
||||
Duration string `json:"duration"`
|
||||
DurationSec float64 `json:"duration_seconds"`
|
||||
StartedAt string `json:"started_at,omitempty"`
|
||||
CompletedAt string `json:"completed_at,omitempty"`
|
||||
Jobs []JobSummary `json:"jobs"`
|
||||
Logs map[string]JobLogs `json:"logs,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
TimedOut bool `json:"timed_out"`
|
||||
}
|
||||
|
||||
type JobSummary struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Conclusion string `json:"conclusion"`
|
||||
StartedAt string `json:"started_at,omitempty"`
|
||||
CompletedAt string `json:"completed_at,omitempty"`
|
||||
Steps []StepInfo `json:"steps,omitempty"`
|
||||
}
|
||||
|
||||
type StepInfo struct {
|
||||
Name string `json:"name"`
|
||||
Number int `json:"number"`
|
||||
Status string `json:"status"`
|
||||
Conclusion string `json:"conclusion,omitempty"`
|
||||
}
|
||||
|
||||
type JobLogs struct {
|
||||
JobID int64 `json:"job_id"`
|
||||
JobName string `json:"job_name"`
|
||||
Log string `json:"log,omitempty"`
|
||||
Bytes int `json:"bytes"`
|
||||
Truncated bool `json:"truncated"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: MonitorWorkflowDispatchTool,
|
||||
Handler: monitorWorkflowDispatchFn,
|
||||
})
|
||||
}
|
||||
|
||||
func monitorWorkflowDispatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called monitorWorkflowDispatchFn")
|
||||
|
||||
owner, err := params.GetString(req.GetArguments(), "owner")
|
||||
if err != nil || owner == "" {
|
||||
return to.ErrorResult(errors.TranslateError(
|
||||
stderrors.New("owner is required"),
|
||||
map[string]string{"operation": "MonitorWorkflowDispatch"},
|
||||
))
|
||||
}
|
||||
|
||||
repo, err := params.GetString(req.GetArguments(), "repo")
|
||||
if err != nil || repo == "" {
|
||||
return to.ErrorResult(errors.TranslateError(
|
||||
stderrors.New("repo is required"),
|
||||
map[string]string{"operation": "MonitorWorkflowDispatch", "owner": owner},
|
||||
))
|
||||
}
|
||||
|
||||
workflowID, err := params.GetString(req.GetArguments(), "workflow_id")
|
||||
if err != nil || workflowID == "" {
|
||||
return to.ErrorResult(errors.TranslateError(
|
||||
stderrors.New("workflow_id is required"),
|
||||
map[string]string{"operation": "MonitorWorkflowDispatch", "owner": owner, "repo": repo},
|
||||
))
|
||||
}
|
||||
|
||||
ref, err := params.GetString(req.GetArguments(), "ref")
|
||||
if err != nil || ref == "" {
|
||||
return to.ErrorResult(errors.TranslateError(
|
||||
stderrors.New("ref is required"),
|
||||
map[string]string{"operation": "MonitorWorkflowDispatch", "owner": owner, "repo": repo, "workflow_id": workflowID},
|
||||
))
|
||||
}
|
||||
|
||||
timeoutSec := int(params.GetOptionalInt(req.GetArguments(), "timeout_seconds", 300))
|
||||
pollIntervalSec := int(params.GetOptionalInt(req.GetArguments(), "poll_interval_seconds", 10))
|
||||
|
||||
var inputs map[string]any
|
||||
if raw, exists := req.GetArguments()["inputs"]; exists {
|
||||
if m, ok := raw.(map[string]any); ok {
|
||||
inputs = m
|
||||
}
|
||||
}
|
||||
|
||||
if err := checkActionsAPIAvailable(ctx); err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
log.Infof("Dispatching workflow %s for %s/%s on ref %s", workflowID, owner, repo, ref)
|
||||
_, err = dispatchWorkflow(ctx, owner, repo, workflowID, ref, inputs)
|
||||
if err != nil {
|
||||
return to.ErrorResult(errors.TranslateError(err, map[string]string{
|
||||
"operation": "DispatchWorkflow",
|
||||
"owner": owner,
|
||||
"repo": repo,
|
||||
"workflow": workflowID,
|
||||
}))
|
||||
}
|
||||
|
||||
timeout := time.Duration(timeoutSec) * time.Second
|
||||
pollInterval := time.Duration(pollIntervalSec) * time.Second
|
||||
|
||||
log.Infof("Waiting for workflow run to start (timeout: %v)...", timeout)
|
||||
runID, err := waitForRunToStart(ctx, owner, repo, workflowID, ref, pollInterval, timeout)
|
||||
if err != nil {
|
||||
result := MonitorResult{
|
||||
WorkflowID: workflowID,
|
||||
Branch: ref,
|
||||
Error: fmt.Sprintf("Workflow dispatched but run never started: %v", err),
|
||||
}
|
||||
return toErrorResultWithJSON(result)
|
||||
}
|
||||
|
||||
log.Infof("Run %d started, monitoring until completion...", runID)
|
||||
monitorResult, err := monitorRunUntilComplete(ctx, owner, repo, runID, pollInterval, timeout)
|
||||
if err != nil {
|
||||
return to.ErrorResult(errors.TranslateError(err, map[string]string{
|
||||
"operation": "MonitorRun",
|
||||
"owner": owner,
|
||||
"repo": repo,
|
||||
"run_id": fmt.Sprintf("%d", runID),
|
||||
}))
|
||||
}
|
||||
|
||||
log.Infof("Retrieving logs for run %d...", runID)
|
||||
logs, err := retrieveJobLogs(ctx, owner, repo, monitorResult.Jobs)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to retrieve some job logs: %v", err)
|
||||
}
|
||||
monitorResult.Logs = logs
|
||||
|
||||
return toTextResultWithJSON(monitorResult)
|
||||
}
|
||||
|
||||
func checkActionsAPIAvailable(ctx context.Context) error {
|
||||
var versionResp struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
status, err := gitea.DoJSON(ctx, "GET", "version", nil, nil, &versionResp)
|
||||
if err != nil {
|
||||
return errors.TranslateError(
|
||||
fmt.Errorf("failed to check Gitea version: status=%d, err=%v", status, err),
|
||||
map[string]string{"operation": "CheckGiteaVersion"},
|
||||
)
|
||||
}
|
||||
|
||||
major, minor, patch, err := parseVersionForCheck(versionResp.Version)
|
||||
if err != nil {
|
||||
return errors.TranslateError(
|
||||
fmt.Errorf("failed to parse version '%s': %v", versionResp.Version, err),
|
||||
map[string]string{"operation": "ParseVersion"},
|
||||
)
|
||||
}
|
||||
|
||||
if major < 1 || (major == 1 && minor < 23) {
|
||||
return errors.NewEnhancedError(
|
||||
stderrors.New("Actions API not available"),
|
||||
fmt.Sprintf("Actions API requires Gitea 1.23+, found %d.%d.%d", major, minor, patch),
|
||||
errors.CategoryActions,
|
||||
).WithOperation("CheckActionsAPIAvailable")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseVersionForCheck(version string) (int, int, int, error) {
|
||||
version = trimVersionPrefix(version)
|
||||
parts := splitVersion(version)
|
||||
if len(parts) < 2 {
|
||||
return 0, 0, 0, fmt.Errorf("invalid version format: %s", version)
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("invalid major version: %s", parts[0])
|
||||
}
|
||||
|
||||
minor, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("invalid minor version: %s", parts[1])
|
||||
}
|
||||
|
||||
patch := 0
|
||||
if len(parts) >= 3 {
|
||||
patch, err = strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
patch = 0
|
||||
}
|
||||
}
|
||||
|
||||
return major, minor, patch, nil
|
||||
}
|
||||
|
||||
func trimVersionPrefix(v string) string {
|
||||
v = trimOnePrefix(v, "v")
|
||||
v = trimOnePrefix(v, "V")
|
||||
return v
|
||||
}
|
||||
|
||||
func trimOnePrefix(s, prefix string) string {
|
||||
if len(s) > 0 && s[0] == prefix[0] {
|
||||
return s[1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func splitVersion(v string) []string {
|
||||
var parts []string
|
||||
start := 0
|
||||
for i := 0; i < len(v); i++ {
|
||||
if v[i] == '.' {
|
||||
if start < i {
|
||||
parts = append(parts, v[start:i])
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start < len(v) {
|
||||
parts = append(parts, v[start:])
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func dispatchWorkflow(ctx context.Context, owner, repo, workflowID, ref string, inputs map[string]any) (map[string]any, error) {
|
||||
body := map[string]any{
|
||||
"ref": ref,
|
||||
}
|
||||
if inputs != nil {
|
||||
body["inputs"] = inputs
|
||||
}
|
||||
|
||||
err := doJSONWithFallback(ctx, "POST",
|
||||
[]string{
|
||||
fmt.Sprintf("repos/%s/%s/actions/workflows/%s/dispatches", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)),
|
||||
fmt.Sprintf("repos/%s/%s/actions/workflows/%s/dispatch", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)),
|
||||
},
|
||||
nil, body, nil,
|
||||
)
|
||||
if err != nil {
|
||||
var httpErr *gitea.HTTPError
|
||||
if stderrors.As(err, &httpErr) && (httpErr.StatusCode == http.StatusNotFound || httpErr.StatusCode == http.StatusMethodNotAllowed) {
|
||||
return nil, errors.NewEnhancedError(
|
||||
err,
|
||||
fmt.Sprintf("workflow dispatch not supported on this Gitea version (endpoint returned %d)", httpErr.StatusCode),
|
||||
errors.CategoryActions,
|
||||
).WithOperation("DispatchWorkflow")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]any{"message": "workflow dispatched"}, nil
|
||||
}
|
||||
|
||||
func waitForRunToStart(ctx context.Context, owner, repo, workflowID, ref string, pollInterval, timeout time.Duration) (int64, error) {
|
||||
startTime := time.Now()
|
||||
seenRunIDs := make(map[int64]bool)
|
||||
|
||||
for time.Since(startTime) < timeout {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
runs, err := listRecentRuns(ctx, owner, repo, workflowID, ref, 10)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to list runs: %v", err)
|
||||
time.Sleep(pollInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, run := range runs {
|
||||
runID := int64(run["id"].(float64))
|
||||
if seenRunIDs[runID] {
|
||||
continue
|
||||
}
|
||||
seenRunIDs[runID] = true
|
||||
|
||||
status := getString(run, "status")
|
||||
if status == "queued" || status == "in_progress" || status == "waiting" {
|
||||
return runID, nil
|
||||
}
|
||||
createdAt := getString(run, "created_at")
|
||||
if createdAt != "" {
|
||||
runTime, err := time.Parse(time.RFC3339, createdAt)
|
||||
if err == nil && time.Since(runTime) < 2*time.Minute {
|
||||
return runID, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(pollInterval)
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("timeout waiting for run to start after %v", timeout)
|
||||
}
|
||||
|
||||
func listRecentRuns(ctx context.Context, owner, repo, workflowID, ref string, limit int) ([]map[string]any, error) {
|
||||
query := url.Values{}
|
||||
query.Set("limit", strconv.Itoa(limit))
|
||||
|
||||
var result struct {
|
||||
WorkflowRuns []map[string]any `json:"workflow_runs"`
|
||||
}
|
||||
|
||||
err := doJSONWithFallback(ctx, "GET",
|
||||
[]string{
|
||||
fmt.Sprintf("repos/%s/%s/actions/runs", url.PathEscape(owner), url.PathEscape(repo)),
|
||||
},
|
||||
query, nil, &result,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var matchingRuns []map[string]any
|
||||
for _, run := range result.WorkflowRuns {
|
||||
if workflowID != "" {
|
||||
runWorkflowID := getString(run, "workflow_id")
|
||||
runPath := getString(run, "path")
|
||||
if runWorkflowID != workflowID && runPath != workflowID &&
|
||||
runWorkflowID != "" && !containsPath(runPath, workflowID) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ref != "" {
|
||||
headBranch := getString(run, "head_branch")
|
||||
if headBranch != "" && headBranch != ref {
|
||||
continue
|
||||
}
|
||||
}
|
||||
matchingRuns = append(matchingRuns, run)
|
||||
}
|
||||
|
||||
return matchingRuns, nil
|
||||
}
|
||||
|
||||
func containsPath(path, substring string) bool {
|
||||
return len(path) > 0 && len(substring) > 0 &&
|
||||
(path == substring ||
|
||||
(len(path) > len(substring) && (path[len(path)-len(substring):] == substring ||
|
||||
path[:len(substring)] == substring)))
|
||||
}
|
||||
|
||||
func getString(m map[string]any, key string) string {
|
||||
if v, ok := m[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
if v, ok := m[key].(float64); ok {
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func monitorRunUntilComplete(ctx context.Context, owner, repo string, runID int64, pollInterval, timeout time.Duration) (*MonitorResult, error) {
|
||||
startTime := time.Now()
|
||||
var firstSeen time.Time
|
||||
|
||||
for time.Since(startTime) < timeout {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
run, err := getRun(ctx, owner, repo, runID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get run %d: %v", runID, err)
|
||||
}
|
||||
|
||||
status := getString(run, "status")
|
||||
conclusion := getString(run, "conclusion")
|
||||
|
||||
if firstSeen.IsZero() {
|
||||
firstSeen = time.Now()
|
||||
}
|
||||
|
||||
if status == "completed" {
|
||||
return buildMonitorResult(run, runID, owner, repo, firstSeen, time.Now())
|
||||
}
|
||||
|
||||
log.Debugf("Run %d status: %s (conclusion: %s), waiting...", runID, status, conclusion)
|
||||
time.Sleep(pollInterval)
|
||||
}
|
||||
|
||||
run, err := getRun(ctx, owner, repo, runID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("timeout after %v and failed to get final status: %v", timeout, err)
|
||||
}
|
||||
|
||||
result, _ := buildMonitorResult(run, runID, owner, repo, firstSeen, time.Now())
|
||||
result.TimedOut = true
|
||||
result.Error = fmt.Sprintf("Polling timed out after %v", timeout)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getRun(ctx context.Context, owner, repo string, runID int64) (map[string]any, error) {
|
||||
var result map[string]any
|
||||
err := doJSONWithFallback(ctx, "GET",
|
||||
[]string{
|
||||
fmt.Sprintf("repos/%s/%s/actions/runs/%d", url.PathEscape(owner), url.PathEscape(repo), runID),
|
||||
},
|
||||
nil, nil, &result,
|
||||
)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func buildMonitorResult(run map[string]any, runID int64, owner, repo string, started, ended time.Time) (*MonitorResult, error) {
|
||||
result := &MonitorResult{
|
||||
RunID: runID,
|
||||
Status: getString(run, "status"),
|
||||
Conclusion: getString(run, "conclusion"),
|
||||
WorkflowID: getString(run, "workflow_id"),
|
||||
WorkflowName: getString(run, "name"),
|
||||
Branch: getString(run, "head_branch"),
|
||||
CommitSHA: getString(run, "head_sha"),
|
||||
StartedAt: getString(run, "created_at"),
|
||||
CompletedAt: getString(run, "updated_at"),
|
||||
}
|
||||
|
||||
if createdAt := getString(run, "run_started_at"); createdAt != "" {
|
||||
result.StartedAt = createdAt
|
||||
if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
|
||||
started = t
|
||||
}
|
||||
}
|
||||
if updatedAt := getString(run, "updated_at"); updatedAt != "" {
|
||||
if t, err := time.Parse(time.RFC3339, updatedAt); err == nil {
|
||||
ended = t
|
||||
}
|
||||
}
|
||||
|
||||
duration := ended.Sub(started)
|
||||
if duration < 0 {
|
||||
duration = 0
|
||||
}
|
||||
result.Duration = duration.String()
|
||||
result.DurationSec = duration.Seconds()
|
||||
|
||||
jobs, err := listRunJobs(runID, owner, repo, run)
|
||||
if err == nil {
|
||||
result.Jobs = jobs
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func listRunJobs(runID int64, owner, repo string, run map[string]any) ([]JobSummary, error) {
|
||||
if jobsData, ok := run["jobs"].([]any); ok && len(jobsData) > 0 {
|
||||
return parseJobSummaries(jobsData), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func parseJobSummaries(jobsData []any) []JobSummary {
|
||||
var summaries []JobSummary
|
||||
for _, j := range jobsData {
|
||||
job, ok := j.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
summary := JobSummary{
|
||||
ID: int64(job["id"].(float64)),
|
||||
Name: getString(job, "name"),
|
||||
Status: getString(job, "status"),
|
||||
Conclusion: getString(job, "conclusion"),
|
||||
StartedAt: getString(job, "started_at"),
|
||||
CompletedAt: getString(job, "completed_at"),
|
||||
}
|
||||
|
||||
if stepsData, ok := job["steps"].([]any); ok {
|
||||
summary.Steps = parseSteps(stepsData)
|
||||
}
|
||||
|
||||
summaries = append(summaries, summary)
|
||||
}
|
||||
return summaries
|
||||
}
|
||||
|
||||
func parseSteps(stepsData []any) []StepInfo {
|
||||
var steps []StepInfo
|
||||
for _, s := range stepsData {
|
||||
step, ok := s.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
steps = append(steps, StepInfo{
|
||||
Name: getString(step, "name"),
|
||||
Number: int(step["number"].(float64)),
|
||||
Status: getString(step, "status"),
|
||||
Conclusion: getString(step, "conclusion"),
|
||||
})
|
||||
}
|
||||
return steps
|
||||
}
|
||||
|
||||
func retrieveJobLogs(ctx context.Context, owner, repo string, jobs []JobSummary) (map[string]JobLogs, error) {
|
||||
logs := make(map[string]JobLogs)
|
||||
|
||||
for _, job := range jobs {
|
||||
if job.ID == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
logData, _, err := fetchJobLogBytes(ctx, owner, repo, job.ID)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to fetch logs for job %d: %v", job.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
maxLogBytes := 100 * 1024
|
||||
truncated := false
|
||||
if len(logData) > maxLogBytes {
|
||||
logData = logData[len(logData)-maxLogBytes:]
|
||||
truncated = true
|
||||
}
|
||||
|
||||
logs[job.Name] = JobLogs{
|
||||
JobID: job.ID,
|
||||
JobName: job.Name,
|
||||
Log: string(logData),
|
||||
Bytes: len(logData),
|
||||
Truncated: truncated,
|
||||
}
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func toTextResultWithJSON(result *MonitorResult) (*mcp.CallToolResult, error) {
|
||||
jsonBytes, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("failed to marshal result: %v", err))
|
||||
}
|
||||
return to.TextResult(string(jsonBytes))
|
||||
}
|
||||
|
||||
func toErrorResultWithJSON(result MonitorResult) (*mcp.CallToolResult, error) {
|
||||
jsonBytes, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("%s (marshal error: %v)", result.Error, err))
|
||||
}
|
||||
return to.TextResult(fmt.Sprintf("Error: %s\n\nPartial Result:\n%s", result.Error, string(jsonBytes)))
|
||||
}
|
||||
Reference in New Issue
Block a user