mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-13 19:20:06 +00:00
975c30a773
The MCP adapter previously wrapped any error returned by MCPToolManager.ExecuteTool
into a Go error returned from the fantasy.AgentTool.Run interface. The fantasy
agent loop treats those as critical errors and aborts the entire turn —
discarding all prior reasoning, tool calls, and results.
In practice that meant a single misbehaved MCP server returning a JSON-RPC
"-32602 Invalid params" (e.g. a Zod schema mismatch on the server's input
validation) would kill an in-progress turn after the model had already done
dozens of seconds of useful work, with no way for the model to see the
validation message and self-correct.
This mismatched the contract that native Kit tools follow: native tools
return errors via kit.ErrorResult(...), which become soft tool-result errors
that the model reads and can act on (retry with corrected args, try a
different tool, give up gracefully).
Make the MCP path behave the same way:
- JSON-RPC protocol errors, transport failures, and server-side schema
rejections are now returned as fantasy.NewTextErrorResponse(...) with
err == nil, so the agent loop continues and the model sees the failure
in-band as a tool result it can reason about.
- Context cancellation (ctx.Err() != nil) remains a critical error so
callers can abort turns deterministically. This is the only case where
bubbling up is correct — the caller intentionally tore the turn down
and the agent must not keep spinning.
- Server-side soft errors (CallToolResult{ isError: true }) and the
happy path are unchanged.
The agent loop's MaxSteps cap already bounds the worst case for a
permanently broken MCP server, so there is no risk of unbounded retries.
Side effect: extracted a tiny mcpExecutor interface for the one method the
adapter uses (ExecuteTool), purely so the adapter is unit-testable in
isolation without standing up a full MCPToolManager + connection pool.
Behavior change note for downstream consumers: code that relied on
host.PromptResult / Stream returning a Go error containing
"mcp tool execution failed" will no longer see those errors — the
failure information is now in the assistant's final response (or in the
OnAfterToolResult / OnToolResult hooks, where IsError will be true).
Context cancellation continues to surface as an error from those calls
as before.
Co-authored-by: space_cowboy <space_cowboy@mark3labs.com>
89 lines
3.0 KiB
Go
89 lines
3.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"charm.land/fantasy"
|
|
|
|
"github.com/mark3labs/kit/internal/tools"
|
|
)
|
|
|
|
// mcpExecutor is the subset of *tools.MCPToolManager that the adapter
|
|
// actually uses. Extracted as an interface so the adapter is unit-testable
|
|
// without constructing a full manager + connection pool.
|
|
type mcpExecutor interface {
|
|
ExecuteTool(ctx context.Context, prefixedName, inputJSON string) (*tools.MCPToolResult, error)
|
|
}
|
|
|
|
// mcpAgentTool adapts an tools.MCPTool to the fantasy.AgentTool interface.
|
|
// This keeps the fantasy dependency confined to the agent layer — the tools
|
|
// package is a pure MCP client library with no LLM framework dependency.
|
|
type mcpAgentTool struct {
|
|
tool tools.MCPTool
|
|
exec mcpExecutor
|
|
providerOptions fantasy.ProviderOptions
|
|
}
|
|
|
|
// Info returns the fantasy tool info including name, description, and parameter schema.
|
|
func (t *mcpAgentTool) Info() fantasy.ToolInfo {
|
|
return fantasy.ToolInfo{
|
|
Name: t.tool.Name,
|
|
Description: t.tool.Description,
|
|
Parameters: t.tool.Parameters,
|
|
Required: t.tool.Required,
|
|
}
|
|
}
|
|
|
|
// Run executes the MCP tool by delegating to the MCPToolManager.
|
|
//
|
|
// MCP-side failures (JSON-RPC protocol errors, transport failures, schema
|
|
// validation rejections from the server) are surfaced to the model as soft
|
|
// tool errors rather than escalated to a critical agent error. This matches
|
|
// the contract that native Kit tools follow via kit.ErrorResult(...) and
|
|
// lets the model self-correct (e.g. retry with a fixed argument shape) or
|
|
// give up gracefully rather than aborting the turn mid-run.
|
|
//
|
|
// Context cancellation is the one exception: if the caller cancelled the
|
|
// context the turn was aborted intentionally, so we propagate the ctx error
|
|
// to let the agent loop unwind cleanly.
|
|
func (t *mcpAgentTool) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
|
|
result, err := t.exec.ExecuteTool(ctx, t.tool.Name, call.Input)
|
|
if err != nil {
|
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
|
return fantasy.ToolResponse{}, ctxErr
|
|
}
|
|
return fantasy.NewTextErrorResponse(
|
|
fmt.Sprintf("MCP tool %q failed: %s", t.tool.Name, err.Error()),
|
|
), nil
|
|
}
|
|
|
|
if result.IsError {
|
|
return fantasy.NewTextErrorResponse(result.Content), nil
|
|
}
|
|
return fantasy.NewTextResponse(result.Content), nil
|
|
}
|
|
|
|
// ProviderOptions returns provider-specific options for this tool.
|
|
func (t *mcpAgentTool) ProviderOptions() fantasy.ProviderOptions {
|
|
return t.providerOptions
|
|
}
|
|
|
|
// SetProviderOptions sets provider-specific options for this tool.
|
|
func (t *mcpAgentTool) SetProviderOptions(opts fantasy.ProviderOptions) {
|
|
t.providerOptions = opts
|
|
}
|
|
|
|
// mcpToolsToAgentTools converts a slice of MCPTool to fantasy.AgentTool
|
|
// implementations that route execution through the MCPToolManager.
|
|
func mcpToolsToAgentTools(mcpTools []tools.MCPTool, manager *tools.MCPToolManager) []fantasy.AgentTool {
|
|
agentTools := make([]fantasy.AgentTool, len(mcpTools))
|
|
for i, t := range mcpTools {
|
|
agentTools[i] = &mcpAgentTool{
|
|
tool: t,
|
|
exec: manager,
|
|
}
|
|
}
|
|
return agentTools
|
|
}
|