上一个 commit (7056657) 漏 add 的 mcp 包文件:
- internal/mcp/server.go (Streamable HTTP Handler)
- internal/mcp/tools/list_clusters.go (发现入口)
- internal/mcp/tools/spark_submit.go (本地 exec 提交)
- internal/mcp/tools/helpers.go (textResult/errResult/encodeJSON)
deps.go / rm.go 已在 Phase 5 Batch 2 一起 commit, 此处不重复。
Co-Authored-By: Claude <noreply@anthropic.com>
81 lines
2.3 KiB
Go
81 lines
2.3 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"spark-mcp-go/internal/logging"
|
|
)
|
|
|
|
// textResult wraps a string into a text CallToolResult.
|
|
func textResult(s string) *mcp.CallToolResult {
|
|
return mcp.NewToolResultText(s)
|
|
}
|
|
|
|
// errResult wraps a string into an error CallToolResult.
|
|
// MCP protocol errors are reserved for exceptional conditions; business
|
|
// errors are reported inside the tool result with IsError set by the library.
|
|
func errResult(s string) *mcp.CallToolResult {
|
|
return mcp.NewToolResultError(s)
|
|
}
|
|
|
|
// encodeJSON marshals v to JSON. On failure it returns a string literal that
|
|
// embeds the error so the caller never receives a nil or empty result.
|
|
func encodeJSON(v any) string {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return fmt.Sprintf("<marshal error: %s>", err)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// startToolCall starts per-tool-call logging. If logging is not initialized
|
|
// (e.g. during tests) it falls back to a no-op logger so handlers never panic.
|
|
func startToolCall(ctx context.Context, logger *slog.Logger, toolName string, params any) logging.ToolCallLogger {
|
|
callLog, err := logging.StartToolCall(ctx, logger, toolName, params)
|
|
if err != nil {
|
|
return &noopToolCallLogger{}
|
|
}
|
|
return callLog
|
|
}
|
|
|
|
type noopToolCallLogger struct{}
|
|
|
|
func (n *noopToolCallLogger) WithResult(any) {}
|
|
func (n *noopToolCallLogger) WithError(error) {}
|
|
func (n *noopToolCallLogger) End() {}
|
|
|
|
// truncateMiddle limits s to roughly max bytes by keeping the first and
|
|
// last halves, separated by a marker. It preserves UTF-8 rune boundaries
|
|
// so the result never starts or ends with a broken multi-byte rune.
|
|
func truncateMiddle(s string, max int) string {
|
|
if len(s) <= max {
|
|
return s
|
|
}
|
|
half := max / 2
|
|
start := s[:half]
|
|
end := s[len(s)-half:]
|
|
for len(start) > 0 && start[len(start)-1] >= 0x80 && start[len(start)-1] < 0xC0 {
|
|
start = start[:len(start)-1]
|
|
}
|
|
for len(end) > 0 && end[0]&0xC0 == 0x80 {
|
|
end = end[1:]
|
|
}
|
|
return start + "\n... [truncated middle] ...\n" + end
|
|
}
|
|
|
|
// truncateTail keeps the last max bytes of s, preserving UTF-8 rune boundaries.
|
|
func truncateTail(s string, max int) string {
|
|
if len(s) <= max {
|
|
return s
|
|
}
|
|
start := len(s) - max
|
|
for start < len(s) && s[start]&0xC0 == 0x80 {
|
|
start++
|
|
}
|
|
return s[start:]
|
|
}
|