This repository was archived by the owner on Jun 27, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathbash_bg_tool.go
More file actions
131 lines (123 loc) · 4.15 KB
/
Copy pathbash_bg_tool.go
File metadata and controls
131 lines (123 loc) · 4.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// Package core — MCP surface for Bash background tasks. The
// underlying registry is in bash_bg.go; this file is the wiring
// layer mapping {BashOutput, BashKill} onto Get/Kill helpers and
// rendering the snapshot under the standard core-tool envelope.
package core
import (
"context"
"fmt"
"strings"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// bashTaskResult wraps a BashTaskSnapshot under BaseResult so the
// snapshot ships with the same operation/duration_ms framing every
// other core tool emits.
type bashTaskResult struct {
BaseResult
BashTaskSnapshot
}
func (r bashTaskResult) Render() string {
if r.IsError() {
return r.ErrorLine(r.Command)
}
var b strings.Builder
fmt.Fprintf(&b, "$ %s &\n", r.Command)
if r.Stdout != "" {
b.WriteString(strings.TrimRight(r.Stdout, "\n"))
b.WriteByte('\n')
}
if r.Stderr != "" {
b.WriteString("\n--- stderr ---\n")
b.WriteString(strings.TrimRight(r.Stderr, "\n"))
b.WriteByte('\n')
}
if r.Stdout == "" && r.Stderr == "" {
b.WriteString("(no output yet)\n")
}
extras := []string{
fmt.Sprintf("task: %s", r.ID),
fmt.Sprintf("status: %s", r.Status),
}
if string(r.Status) != "active" {
extras = append(extras, fmt.Sprintf("exit %d", r.ExitCode))
}
if r.TimedOut {
extras = append(extras, "TIMED OUT")
}
b.WriteByte('\n')
b.WriteString(r.FooterLine(extras...))
return b.String()
}
// RegisterBashOutput exposes GetBashTask over MCP as BashOutput.
func RegisterBashOutput(s *server.MCPServer) {
tool := mcp.NewTool(
"BashOutput",
mcp.WithDescription(
"Poll a background Bash task — returns live stdout / stderr, "+
"status (active / done / failed / cancelled), and exit_code "+
"once terminal. Use after `Bash background=true` to check "+
"progress on a long-running command (test run, build, "+
"scraper) without waiting for it to finish. Read-only and "+
"non-blocking — call repeatedly until status is terminal. "+
"NOT for cancelling — use BashKill; NOT for synchronous "+
"foreground commands — call Bash directly.",
),
mcp.WithString("task_id",
mcp.Required(),
mcp.Description("The task_id returned by `Bash background=true`."),
),
)
s.AddTool(tool, func(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
id, err := req.RequireString("task_id")
if err != nil {
return mcp.NewToolResultError("missing required argument: task_id"), nil
}
snap, ok := GetBashTask(id)
if !ok {
return mcp.NewToolResultError(fmt.Sprintf("no background bash task: %s", id)), nil
}
return resultOf(bashTaskResult{
BaseResult: BaseResult{Operation: "BashOutput"},
BashTaskSnapshot: snap,
}), nil
})
}
// RegisterBashKill exposes KillBashTask over MCP as BashKill. The
// snapshot is returned post-cancel so the caller sees the terminal
// status (or `cancelled` if the kill won the race against a quick
// exit).
func RegisterBashKill(s *server.MCPServer) {
tool := mcp.NewTool(
"BashKill",
mcp.WithDescription(
"Cancel a runaway background Bash task started with `Bash "+
"background=true`. Sends SIGKILL to the whole process group "+
"so children get reaped too. Use when a polled task (via "+
"BashOutput) has been active too long, the operator says "+
"\"stop that\", or you need to abort before starting a "+
"replacement command. No-op when the task is already terminal. "+
"Returns the post-kill snapshot. NOT for inspecting status — "+
"use BashOutput; NOT for foreground commands — they cancel "+
"on context timeout automatically.",
),
mcp.WithString("task_id",
mcp.Required(),
mcp.Description("The task_id returned by `Bash background=true`."),
),
)
s.AddTool(tool, func(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
id, err := req.RequireString("task_id")
if err != nil {
return mcp.NewToolResultError("missing required argument: task_id"), nil
}
snap, ok := KillBashTask(id)
if !ok {
return mcp.NewToolResultError(fmt.Sprintf("no background bash task: %s", id)), nil
}
return resultOf(bashTaskResult{
BaseResult: BaseResult{Operation: "BashKill"},
BashTaskSnapshot: snap,
}), nil
})
}