v0.7.0 — typed MCP errors + bounded tool output
v0.7.0 — typed MCP errors + bounded tool output
Two host-facing capabilities land in this release: hosts can now inspect typed MCP invocation errors and synthesize tool results in response, and tools can advertise output budgets that the executor enforces centrally — with artifact-backed readback for oversized results. Plus a PR-checks workflow.
What's Changed
Typed MCP invocation errors + responder hook
McpError::Invocation now carries a typed McpInvocationError enum with one variant per JSON-RPC error code rmcp recognises (UrlElicitation, InvalidRequest, MethodNotFound, InvalidParams, InternalError, ParseError, ResourceNotFound) plus an Other { code, message, data } forward-compat arm. For URL elicitation the data payload is best-effort parsed into the new UrlElicitationData struct; the raw serde_json::Value is always preserved alongside.
Hosts can hook into the invocation path with McpErrorResponder and decide per-call whether to pass the error through or synthesize a successful CallToolResult for the agent:
use std::sync::Arc;
use agentkit_mcp::{
ErrorResponderOutcome, McpErrorContext, McpErrorResponder, McpHandlerConfig,
McpInvocationError,
};
use async_trait::async_trait;
use rmcp::model::{CallToolResult, Content};
struct UrlElicitationResponder;
#[async_trait]
impl McpErrorResponder for UrlElicitationResponder {
async fn handle(
&self,
error: &McpInvocationError,
_ctx: McpErrorContext<'_>,
) -> ErrorResponderOutcome {
let McpInvocationError::UrlElicitation { data: Some(data), .. } = error else {
return ErrorResponderOutcome::PassThrough;
};
ErrorResponderOutcome::SynthesizeResult(CallToolResult::success(vec![
Content::text(format!("Open this URL to continue: {}", data.url)),
]))
}
}
let handler_config = McpHandlerConfig::new()
.with_error_responder(Arc::new(UrlElicitationResponder));This is the recommended way to surface authorization URLs, server-side rate-limit notices, or any other "the call failed but the model should see something useful" payload.
Bounded tool output with artifact-backed readback
Long-running tools can now blow the model's context budget without poisoning the transcript. Two pieces fit together:
1. Tools advertise a budget through ToolSpec::with_output_limit:
use agentkit_tools_core::{ToolOutputLimit, ToolSpec};
let spec = my_spec.with_output_limit(ToolOutputLimit::store_for_readback(150_000));ToolOutputLimit has three modes:
fail(max_bytes)— return an execution error rather than place an oversized result in the transcript. Use for readback tools themselves, where silent truncation would reintroduce the unbounded loop.inline_clip(max_bytes)— clip with an explicit truncation marker.store_for_readback(max_bytes)— store the full output in an artifact store and replace the model-facing result with a small pointer envelope.
2. The executor enforces it via ConfigurableToolOutputTruncationStrategy, registered once and applied uniformly to native tools, MCP tools, and any custom ToolSource:
use std::sync::Arc;
use agentkit_tools_core::{
BasicToolExecutor, ConfigurableToolOutputTruncationStrategy,
InMemoryToolOutputArtifactStore, ToolOutputLimit, ToolRegistry,
tool_result_readback_registry,
};
let store = Arc::new(InMemoryToolOutputArtifactStore::new());
let registry = ToolRegistry::new()
.with(my_large_output_tool.with_output_limit(
ToolOutputLimit::store_for_readback(150_000),
))
.merge(tool_result_readback_registry(store.clone(), 150_000));
let executor = BasicToolExecutor::from_registry(registry)
.with_output_truncation_strategy(
ConfigurableToolOutputTruncationStrategy::new(store),
);Oversized outputs are stored as ToolOutputArtifact { id, tool_name, call_id, session_id, turn_id, original_bytes, body }. The model reads them back through the tool_result_read tool (registered via tool_result_readback_registry), which returns bounded ToolOutputArtifactSlice { offset, next_offset, original_bytes, eof, content } chunks.
For hosts that need readback to survive across processes, agentkit-tool-fs ships FileToolOutputArtifactStore:
use std::sync::Arc;
use agentkit_tool_fs::FileToolOutputArtifactStore;
use agentkit_tools_core::{
ConfigurableToolOutputTruncationStrategy, tool_result_readback_registry,
};
let store = Arc::new(FileToolOutputArtifactStore::new(".agentkit/tool-results"));
let readback_registry = tool_result_readback_registry(store.clone(), 150_000);
let truncation = ConfigurableToolOutputTruncationStrategy::new(store);Filesystem tools (fs_read, fs_search, etc.) advertise readback-friendly limits by default, so symlink-resolved searches that hit a giant tree no longer wreck the transcript.
PR checks workflow
.github/workflows/pr-checks.yml runs cargo fmt --check, cargo clippy -- -D warnings, and cargo test --workspace against pull requests. No host-facing change, but it locks the bar for incoming contributions.
Migration notes
ItemKindis unchanged since 0.5 — no transcript-shape break here.- Tools that previously implemented ad-hoc truncation can hand the responsibility to
ConfigurableToolOutputTruncationStrategyand remove the per-tool clipping code. - Hosts that match on
McpError::Invocationpayloads need to switch from string parsing to the newMcpInvocationErrorvariants. TheOther { code, message, data }arm keeps unknown server codes accessible. McpHandlerConfiggainswith_error_responder(Arc<dyn McpErrorResponder>); existing handler configs compile unchanged.
Docs
README.md, the mdBook (installation, feature flags, MCP chapter), and per-crate READMEs bump to 0.7.
Commits
- chore: re-order publish script crates (0babd82)
- feat: surface MCP invocation errors and bound tool output (#5) (bca24e5)
- chore: release 0.7.0 (c2bd3d9)
Full Changelog: v0.6.0...v0.7.0