Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

76 lines
2.5 KiB
Go

package mcp
import (
"regexp"
"strings"
"github.com/mark3labs/mcp-go/mcp"
)
// ansiEscape matches ANSI / VT escape sequences: CSI (`ESC [ … final`),
// OSC (`ESC ] … terminator`), and the two-byte `ESC <Fe>` escapes.
// Stripping the whole sequence — not just the lone ESC byte — avoids
// leaving a `[31m`-style fragment behind in the text.
var ansiEscape = regexp.MustCompile(
"\x1b\\[[0-?]*[ -/]*[@-~]" + // CSI
"|\x1b\\][^\x07\x1b]*(?:\x07|\x1b\\\\)" + // OSC … BEL / ST
"|\x1b[@-_]") // other two-byte escapes
// scrubControlChars removes content that can corrupt how an MCP client
// renders the tool surface or smuggle terminal-control sequences into a
// client that echoes descriptions to a TTY: ANSI / VT escape sequences
// and C0 / C1 control characters. Tab and newline are kept — they are
// legitimate in a multi-line tool description. Returns s unchanged when
// it carries nothing to scrub (the common case for Gortex's static
// description literals).
func scrubControlChars(s string) string {
if s == "" {
return s
}
cleaned := ansiEscape.ReplaceAllString(s, "")
var b strings.Builder
b.Grow(len(cleaned))
dropped := false
for _, r := range cleaned {
switch {
case r == '\t' || r == '\n':
b.WriteRune(r)
case r < 0x20 || r == 0x7f || (r >= 0x80 && r <= 0x9f):
// C0 control, DEL, or C1 control — drop it.
dropped = true
default:
b.WriteRune(r)
}
}
if !dropped && cleaned == s {
return s
}
return b.String()
}
// scrubToolText sanitizes a tool's human-facing text in place — its
// title, description, and every input-parameter description — before
// the tool is registered. Defense-in-depth: Gortex's tool text is
// static Go source, so in practice this is a no-op, but routing every
// registration through it guarantees no control sequence can ever
// reach a client's tools/list rendering, regardless of where a future
// description string originates.
//
// Pipe characters are deliberately NOT stripped: Gortex tool
// descriptions use `|` heavily and legitimately (enum lists like
// "json | gcx | toon"), and a pipe carries no corruption or injection
// risk on its own.
func scrubToolText(tool *mcp.Tool) {
tool.Title = scrubControlChars(tool.Title)
tool.Description = scrubControlChars(tool.Description)
for _, prop := range tool.InputSchema.Properties {
m, ok := prop.(map[string]any)
if !ok {
continue
}
if d, ok := m["description"].(string); ok {
m["description"] = scrubControlChars(d)
}
}
}