Files
nezha_domains/cmd/dashboard/controller/mcp_error_structured_test.go
T
naiba bb38a9d4d2 fix(mcp): accept agents reporting version without v prefix and stop masking tool errors
compareSemver fell back to lexical string ordering when the numeric
segments were equal, so an agent reporting "2.1.0" compared as older than
the gate constant "v2.1.0" (0x32 < 0x76). This wrongly flagged every agent
as unsupported_agent, breaking server.exec and fs.* on all online servers.
Drop the fallback: semverParts already strips the v prefix, so equal
numeric segments mean equal versions.

The error was further masked because tool error responses shipped a
structuredContent {error_code,error} that violates the tool outputSchema
(which requires exit_code/stdout/...). Strict MCP clients validated it and
rejected the whole response with -32602, hiding the real isError text.
Omit structuredContent on error; the cause stays in content[].text.
2026-05-31 16:18:57 +00:00

68 lines
2.0 KiB
Go

package controller
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/nezhahq/nezha/model"
"github.com/nezhahq/nezha/service/singleton"
)
// A tool error must not ship structuredContent: strict clients validate it
// against the tool's outputSchema (which requires exec/fs result fields) and
// reject the whole response with -32602, masking the real isError text.
func TestServerExec_ErrorResult_OmitsStructuredContent(t *testing.T) {
cleanup, uid := setupMCPTest(t)
defer cleanup()
srv, _ := singleton.ServerShared.Get(7)
srv.SetTaskStream(&execErrorStream{errMsg: "agent disabled command execution"})
tok, _ := mkToken(t, uid, []string{model.ScopeServerExec}, nil)
c, w := mcpCallCtx(t, tok, uid, jsonRPCRequest{
JSONRPC: "2.0", ID: json.RawMessage("1"), Method: "tools/call",
Params: jsonObj(t, toolCallParams{
Name: "server.exec",
Arguments: jsonRaw(map[string]any{
"server_id": 7,
"cmd": "whoami",
"timeout_seconds": 2,
}),
}),
})
mcpEndpoint(c)
_, tcr := decodeRPC(w)
require.NotNil(t, tcr)
require.True(t, tcr.IsError)
require.Contains(t, tcr.Content[0].Text, "agent disabled command execution",
"error text must carry the real cause")
require.Nil(t, tcr.StructuredContent,
"error responses must omit structuredContent so strict clients don't validate it against outputSchema and mask the real error")
}
func TestScopeDenied_OmitsStructuredContent(t *testing.T) {
cleanup, uid := setupMCPTest(t)
defer cleanup()
tok, _ := mkToken(t, uid, []string{model.ScopeServerRead}, nil)
c, w := mcpCallCtx(t, tok, uid, jsonRPCRequest{
JSONRPC: "2.0", ID: json.RawMessage("1"), Method: "tools/call",
Params: jsonObj(t, toolCallParams{
Name: "server.exec",
Arguments: jsonRaw(map[string]any{"server_id": 7, "cmd": "echo"}),
}),
})
mcpEndpoint(c)
_, tcr := decodeRPC(w)
require.NotNil(t, tcr)
require.True(t, tcr.IsError)
require.Nil(t, tcr.StructuredContent,
"scope-denied error must also omit structuredContent")
}