Phase 5 Batch 1 (补): mcp/server + 2 Tool (Batch 1 commit 时漏 stage)

上一个 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>
This commit is contained in:
tao.chen
2026-07-10 17:24:10 +08:00
co-authored by Claude
parent e6411b0dc4
commit be4ca460c9
3 changed files with 219 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
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:]
}