Best practices
August 2, 2026 ยท View on GitHub
Keep tools small and explicit
Use one action per function, narrow annotations, short docstrings, and stable names.
Prefer JSON-native return values over custom objects. Use version as application
metadata when a contract changes; Samsarix Core does not route versions for you.
Treat calls as untrusted input
Type validation does not replace authorization. Re-check tenant, user, path, resource, and quota permissions inside the tool. Prefer allowlists and resolved resource IDs over raw filesystem paths or shell fragments.
Use ToolRuntime(policy=...) for centralized defense in depth when every valid call
must pass an application-owned allow/deny decision. Keep the policy small, cancellation
friendly, and free of side effects. It sees a detached but sensitive argument snapshot;
do not log that snapshot, and return an explicit ToolPolicyDecision rather than
raising for an expected denial. A request-local ContextVar is one way for a trusted
host to expose already-authenticated scopes without adding credentials to tool schemas.
The policy is not authentication, a tenant quota store, or durable human approval.
Never register a callable merely because an untrusted client supplied its import path. The registry is for trusted application code, not dynamic code loading.
Protect sensitive data
Leave expose_exceptions=False in shared environments. Do not place credentials in
tool descriptions, tags, defaults, arguments sent to a model, outputs returned to
an untrusted caller, or external logs.
Samsarix Core itself does not log call content. Its opt-in MCP operational events contain only status, duration, invocation ID, and an application-approved tool identifier. If the host adds other logging or tracing, redact inputs, outputs, exception messages, paths, tenant identifiers, and authorization material by default.
For per-invocation traces or service-level indicators, opt in with
ToolRuntime(lifecycle_handler=...). The immutable events omit call content, but tool
names disclose application structure, invocation IDs are correlatable, and an unknown
requested name can be attacker-controlled. Keep the synchronous callback non-blocking,
allowlist metric labels, and hand network export to a bounded/batched host processor.
See the lifecycle observability guide for an OpenTelemetry adapter
that deliberately omits sensitive argument and result attributes.
Bound every external dependency
The runtime timeout bounds how long the caller waits. A sync function's thread cannot be force-stopped. Set connect/read/query/process timeouts in the tool itself, make cancellation-friendly async calls, and make side effects idempotent when the caller might retry after a timeout.
Register a tool with max_concurrency=N when its downstream API, database pool, model
deployment, or other resource has a lower safe concurrency than the runtime as a whole.
Choose the limit from measured capacity and the dependency's quota. Samsarix acquires
this tool-specific slot before a global execution slot, preserving unrelated tool
availability while the constrained tool queues.
The per-tool limit is an in-process execution bulkhead, not a request-rate limit, tenant quota, circuit breaker, or process sandbox. Keep total admission finite, set downstream I/O deadlines, and add caller-aware controls at the hosting boundary. See the vendor-neutral bulkhead pattern guidance for the reliability trade-offs and complementary controls.
Choose the runtime-wide max_concurrency from aggregate downstream capacity, not CPU count alone. Set
max_pending_invocations to the total policy/execution work one process can safely
hold; monitor busy and peak_pending_invocations to find sustained saturation.
Tune max_batch_size,
max_argument_bytes, max_output_bytes, max_value_depth, and max_value_nodes
below upstream transport limits, with enough headroom for legitimate contracts.
Keep ToolRegistry.max_tools close to the catalog size you actually expose.
For stdio MCP servers, also tune max_in_flight_requests to bound calls waiting
behind the runtime's execution limit.
For long-running async tools, report meaningful phase or item-count progress only
when work actually advances. Keep values strictly increasing and tune
max_progress_updates plus max_progress_message_bytes below client and
transport limits. Progress messages cross the protocol boundary and may be logged
or displayed, so keep document content, paths, credentials, and tenant data out of
them.
Enable experimental MCP tasks only for work that benefits from deferred result
retrieval. Prefer task_support="optional" while client support is uneven. Keep
max_retained_tasks below downstream capacity, use the shortest practical task
TTL, and remember that the final result remains in memory until expiry. Local
stdio does not expose tasks.list; a network adapter must bind get/result/cancel
to authenticated requestor identity and add per-requestor quotas and rate limits.
These limits are process-local; they are not tenant quotas or request-rate limits. A network host still needs authentication, per-principal admission, rate limits, and aggregate memory/connection limits.
MCP recommends a client-side human confirmation surface for tool calls. Preserve that UI even when the server also uses a programmatic policy gate; annotations are hints and server policy cannot prove that a person reviewed a call. See the official MCP tool interaction guidance.
Handle results deliberately
Branch on ToolStatus; do not infer success from a truthy output. A busy result is
retryable because no tool or policy code ran, but use capped exponential backoff with
jitter rather than retrying immediately. Other failures remain non-retryable because
a timed-out sync function may still finish and cause its side effect. Apply any
broader retry policy only when the tool's semantics make that safe.
Let caller cancellation propagate. Use async with ToolRuntime(...) so resources
close on success and failure. Context-manager close does not wait for a timed-out
sync thread. During controlled shutdown, stop upstream admission and call
await runtime.aclose(wait_for_sync=True, timeout=<deadline>); treat a False
result as a failed quiescence check rather than assuming the side effect stopped.
Test contracts, not mocks
Test schema output, missing/extra/wrongly typed arguments, successful sync and async calls, output validation, failure redaction, timeout behavior, cancellation, and concurrency limits. Install the built wheel in an isolated environment before a release; importing from the source checkout alone does not validate packaging.