From f296d44f8485bbe3c2497b53a8a7ff9cca8e4da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sat, 19 Sep 2026 12:57:25 +0100 Subject: [PATCH] fix(mcp): preserve structured plugin tool errors (#3815) --- .../internal/mcp/server/plugin_result_test.go | 60 +++++++++++++++++++ apps/backend/internal/mcp/server/server.go | 6 +- 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 apps/backend/internal/mcp/server/plugin_result_test.go diff --git a/apps/backend/internal/mcp/server/plugin_result_test.go b/apps/backend/internal/mcp/server/plugin_result_test.go new file mode 100644 index 0000000000..6bba354f1e --- /dev/null +++ b/apps/backend/internal/mcp/server/plugin_result_test.go @@ -0,0 +1,60 @@ +package mcp + +import ( + "encoding/json" + "testing" + + "github.com/kandev/kandev/internal/mcp/plugintools" + mcplib "github.com/mark3labs/mcp-go/mcp" + "github.com/stretchr/testify/require" +) + +func TestPluginToolPreservesStructuredResultAndErrorStatus(t *testing.T) { + for _, isError := range []bool{false, true} { + name := "success" + if isError { + name = "error" + } + t.Run(name, func(t *testing.T) { + structured := map[string]any{"status": "uncertain", "recipient": "U01234567", "duplicate": true} + backend := &testBackend{response: map[string]any{ + "text": "Check delivery before retrying.", "structured_content": structured, "is_error": isError, + }} + server := New(backend, "session-1", "task-1", 10005, newTestLogger(t), "", false, ModeTask) + definition := plugintools.Definition{ + PluginID: "notifications", LocalName: "notify", ExposedName: plugintools.ExposedName("notifications", "notify"), + Description: "Notify a user", InputSchema: []byte(`{"type":"object"}`), + Surfaces: []string{plugintools.SurfaceKanban}, + } + require.NoError(t, server.SetPluginTools(plugintools.Snapshot{ + Generation: "g", Revision: 1, Tools: []plugintools.Definition{definition}, + })) + + result := callTool(t, server, definition.ExposedName, map[string]any{}) + require.Equal(t, isError, result.IsError) + require.Equal(t, structured, result.StructuredContent) + require.Len(t, result.Content, 1) + require.Equal(t, "Check delivery before retrying.", result.Content[0].(mcplib.TextContent).Text) + }) + } +} + +func TestPluginToolErrorOmitsNilStructuredContent(t *testing.T) { + backend := &testBackend{response: map[string]any{ + "text": "Check delivery before retrying.", "structured_content": nil, "is_error": true, + }} + server := New(backend, "session-1", "task-1", 10005, newTestLogger(t), "", false, ModeTask) + definition := plugintools.Definition{ + PluginID: "notifications", LocalName: "notify", ExposedName: plugintools.ExposedName("notifications", "notify"), + Description: "Notify a user", InputSchema: []byte(`{"type":"object"}`), + Surfaces: []string{plugintools.SurfaceKanban}, + } + require.NoError(t, server.SetPluginTools(plugintools.Snapshot{ + Generation: "g", Revision: 1, Tools: []plugintools.Definition{definition}, + })) + + result := callTool(t, server, definition.ExposedName, map[string]any{}) + serialized, err := json.Marshal(result) + require.NoError(t, err) + require.NotContains(t, string(serialized), `"structuredContent"`) +} diff --git a/apps/backend/internal/mcp/server/server.go b/apps/backend/internal/mcp/server/server.go index 2a6450150d..b562d5831b 100644 --- a/apps/backend/internal/mcp/server/server.go +++ b/apps/backend/internal/mcp/server/server.go @@ -989,7 +989,11 @@ func (s *Server) registerPluginTools() { return nil, err } if result.IsError { - return mcp.NewToolResultError(result.Text), nil + response := mcp.NewToolResultError(result.Text) + if result.StructuredContent != nil { + response.StructuredContent = result.StructuredContent + } + return response, nil } if result.StructuredContent != nil { return mcp.NewToolResultStructured(result.StructuredContent, result.Text), nil