Repository navigation
Installation
pip install toolregistry==0.19.0Added
- Public validation error types (#340):
ValidationError/ErrorDetailandSchemaValidationError/SchemaErrorDetailare now importable fromtoolregistry.Tool.validate_parameters()raises the first pair and raw JSON Schema validation raises the second; previously both lived only behindtoolregistry._vendor, so a downstream server could not map a validation failure to its own error format without importing a private path. The names are re-exports of the classes actually raised, pinned by identity intests/test_validation_errors.py. - Relevance floor on
discover_toolsfuzzy results (#330): BM25 hits scoring belowmin_score_ratioof the best hit are dropped, so a query with one clear match is no longer padded with unrelated tools that merely share a token. The default is0.3;ToolDiscoveryTool(..., min_score_ratio=...)andenable_tool_discovery(min_score_ratio=...)tune it, and0disables the floor. The floor is relative because BM25 scores are not comparable across queries; the best hit is always kept and exact-name lookups bypass it. - Per-source filters in declarative config (#297):
MCPSourceandOpenAPISourceacceptinclude/exclude, so a config entry can narrow an MCP server or OpenAPI spec instead of registering everything. Each value may be a bare string or a list offnmatchglobs; the two sides are mutually exclusive. An omitted filter means no filtering, while an empty list is an explicit empty filter that matches nothing. Unknown keys on a source entry or underauthnow raiseConfigErrornaming the entry and the offending keys instead of being silently ignored. call_deferredtolerates client-normalized tool names (#277):_target_toolis resolved by collapsing/and-to_and folding case, so a name an MCP client rewrote (weather_get_current) reaches the registered tool (weather-get-current). An exact registered spelling always wins over a normalized match, so two names that collide after normalization still dispatch to their own tool. On a miss the error lists close registered names (Did you mean: datetime-now?) so the model can self-correct in one turn.discover_toolsinput validation and CJK search (#278):querynow carriesminLength: 1plus a non-whitespacepattern, andtop_kcarriesminimum: 1; both also raiseValueErrorat runtime. A zero-dependency tokenizer adds character bigrams for CJK runs before BM25 indexing, so Chinese queries match Chinese tool descriptions instead of silently returning nothing. Thediscover_toolsdescription now states which tool text queries are matched against.- Execution hint metadata (#280):
ToolMetadatagainstitle,read_only_hint,destructive_hint,idempotent_hintandopen_world_hint. All default toNone(unset / unknown), which stays distinguishable from an explicitFalse.ToolMetadata.execution_hintsreturns the resolveddict[str, bool], derivingread_only_hintfromToolTag.READ_ONLYanddestructive_hintfromToolTag.DESTRUCTIVE, and fillingdestructive_hint=Falsefor a read-only tool so the pair cannot contradict the protocol defaults. Keys map 1:1 onto the MCPreadOnlyHint/destructiveHint/idempotentHint/openWorldHintannotations.discover_toolsdeclares itself read-only;call_deferredmakes no claim because its effect depends on the target tool. Declaring both a read-only and a destructive hint raises aUserWarning, and the config loader warns about unrecognised keys undertool_metadata.<tool>. - Registration-time
include/excludefilters (#281):register()andregister_async()acceptincludeorexcludefor themcp,openapi, andlangchainsources, so a source can be narrowed as it is registered instead of registering everything and disabling what is unwanted. Patterns arefnmatchglobs matched against both the raw source name (the MCP tool name, the OpenAPIoperationId, the LangChain tool name) and the post-namespace registered name.includeandexcludeare mutually exclusive, and passing either tonative/classraisesValueError. Filtered-out tools never reach the registry, so they stay invisible todiscover_toolsand the admin panel, and the filter is re-applied on refresh so excluded tools neither reappear nor get reported as removed. - Tool overrides (#282):
override(name, tool, create=False)replaces a registered tool and records the displaced definition,restore_override(name)puts it back, andlist_overrides()returns the displaced definitions. Both directions emit the newChangeEventType.OVERRIDEevent;override(..., create=True)on an unregistered name also emitsREGISTER, so listeners keyed on registration still learn the tool appeared. A refresh leaves overridden names untouched, re-records the displaced definition from the remote sorestore_override()reverts to what the source serves now, and reports the suppressed names in a newRefreshResult.overriddentuple instead of counting them asunchanged. A plainregister()onto an overridden name drops the override record, symmetric with unregistering, and namespace-rewriting operations (merge,spinoff,reduce_namespace) keep override records keyed to the renamed tools. - Replacement on plain registration is now visible (#290): registering onto an existing name still replaces the stored tool, but now emits a
UserWarningnaming the discarded definition together with a newChangeEventType.REPLACEevent whosemetadata["replaced_tool"]carries it. Passallow_replace=Truetoregister()/register_async()or to theregister_*_toolsintegration methods to acknowledge the replacement and suppress the warning; the event still fires, so observation is not gated on the flag. Re-registering a field-equalTool— including the same object under the same namespace — is not a replacement, and when the discarded tool held an override the warning says the recorded override was also dropped.
Changed
- LangChain integration is structural (#313) (breaking):
source="langchain"now accepts any object withname,description, a parameter schema (input_schema,args_schema,parameters, orargs), and a sync/async runner.langchain-coreandlangchain-communityare no longer installed bytoolregistry[langchain](the extra is now empty) and are not imported at runtime. The old unconditionaldel input_schema["description"]raisedKeyErrorfor any args model without a docstring, onlangchain-core0.3.x and 1.x alike; registration is now version-independent, pydantic-v1args_schemamodels are read throughschema()sorequiredis preserved, and the sunsetlangchain-communitydependency is gone. RealBaseToolcompatibility is checked by a new scheduledlangchain-driftworkflow againstlangchain-core0.3.x and 1.x on Python 3.10 and 3.13. update_namespace(force=False)preserves the existing namespace (#310) (behavior change): an already-qualified tool is returned unchanged. Previously thenamespacefield was rewritten to the new value whilenamekept the old prefix, leaving the two fields inconsistent. Passforce=Trueto replace it.Tool.qualified_namereturnsname(#310) (behavior change): for hand-built tools whosenamespace/method_namefields disagree withname, the property now returns the authoritativenameinstead of reconstructing{namespace}-{method_name}. Tools created throughregister()or thefrom_*factories are unaffected.- Discovery index rebuild is deferred for register/unregister (#289):
REGISTER/UNREGISTERnow mark the BM25 index stale instead of rebuilding it per event, matching the existingENABLE/DISABLE/METADATA_UPDATEpath. Bulk registration pays one rebuild at the nextdiscover(); deferred-tool descriptions are still refreshed eagerly. - Config loader rejects unknown keys (#297) (behavior change): a misspelled key on a tool source entry or under
authnow raisesConfigErrorwith the entry index and valid key set. Configs that relied on a silently ignored key will fail at load time instead of partially applying. name_sepapplies to every registration source (#302): native functions and class methods now honorToolRegistry(name_sep="."), matching MCP, OpenAPI, and LangChain.Tool.from_function(..., sep=...)and the new read-onlyToolRegistry.name_sepexpose the same rule. Default-behavior is unchanged.- Tool results with no JSON projection now fail loudly (breaking) (#279): a result that cannot be encoded to JSON raises a
TypeErrornaming the tool and the type instead of being silently converted withstr(). The message suggests the fix (to_dict(),model_dump(),tolist(), or returning astr).invoke()surfaces this as anErrorResult, so the public "never raises" contract is unchanged. - Process-backend results are encoded in the parent (#279): the process worker no longer stringifies its result, so the encoder — and the raise above — now apply on the default execution mode rather than only on the thread backend. A tool result crossing a process boundary must therefore be picklable.
Fixed
- A homogeneous corpus no longer turns a working query into nothing (#342): when every token of a multi-token query was above the document-frequency floor, the token filter left nothing to search and
discover()returned[]— so adding a word tofile(4 hits) producedfile on disk(0 hits). The filter now falls back to the query's capability tokens (tokens that appear in at least one tool name) and keeps the rarest, which reproduces the single-word result on a corpus where every tool says "work with a file on disk". A query made only of boilerplate description words has no capability token and still returns nothing, which is what #332 wanted. VALIDATION_ERRORScatches both validation failures (#342):ValidationError(signature-derived tools) andSchemaValidationError(raw JSON Schema) are siblings from different vendored modules, soexcept ValidationErrornever caught the second. The new tuple is exported fromtoolregistryso a caller can map either failure to one outcome withexcept VALIDATION_ERRORS:without enumerating the classes.- Discovery results carry the declared output schema (#337):
discover_toolsreturned only the input parameters, so a model discovering a deferred tool learned what it takes but not what it returns — even though the tool emitsstructuredContentconforming to a declaredoutputSchema. A result for a tool withToolMetadata(extra={"output_schema": ...})now includes that schema beside the input one, insidefunctionfor OpenAI-style definitions and at the top level for the flatopenai-responses, Anthropic and Gemini shapes. Both exact-name and fuzzy results carry it, and tools without a declared schema are unchanged. - A repeated
enable_tool_discovery()reports ignored options (#336): the second call returns the existing discoverer unchanged, somin_score_ratio,max_df_ratioandfield_weightspassed to it used to be dropped without a word — which made retuning look broken. Differing values now raise aUserWarningnaming them and pointing atdisable_tool_discovery(); omitted options and values that match the live instance stay silent, so idempotent calls do not become noisy. The docstring states that the first call wins. - A term shared by a few tools stays findable (#334): document frequency is only treated as a signal once a term appears in at least three indexed tools, so in a four-tool registry a word used by two tools is searched instead of being dropped at the 50%
max_df_ratioboundary. A single-token query is never filtered: one word is a capability keyword rather than prose padded with stopwords, sodraworsearchis not discarded for being common, while multi-word stopword queries such asthe a ofstill return nothing. - A directly constructed
ToolDiscoveryTooltracks registry changes (#334): the discoverer now subscribes toREGISTER/UNREGISTER/OVERRIDE/ENABLE/DISABLE/METADATA_UPDATEitself, so a tool registered after construction is searchable at the nextdiscover()without a manualrebuild_index(), and the document-frequency map no longer describes a corpus that has moved on.close()unsubscribes (idempotently);disable_tool_discovery()calls it. - Query tokens present in most tools no longer produce filler hits (#332):
discover_toolsdrops query tokens that appear in at leastmax_df_ratioof the indexed tools (default0.5; configurable onToolDiscoveryToolandenable_tool_discovery,0disables) before scoring. On a corpus with no weather tool,weather forecast for a citynow returns nothing instead of near-tied matches driven byforanda, andthe a ofreturns nothing. The filter is corpus-adaptive rather than a stopword list, requires the term to occur in at least three indexed tools, and composes with themin_score_ratiofloor.min_score_ratioandmax_df_rationow rejectboolvalues. - MCP stdio scripts run under the ToolRegistry interpreter (#323): a
.pysource now starts withsys.executableinstead of thepythonfound onPATH. When a virtualenv or conda environment was not first onPATH, the server started in a different interpreter and died at import, which the client could only report asConnection closed. Callers that need a specific interpreter can still pass the{"command": ..., "args": [...]}form. - Native tools survive a cloudpickle round-trip on Python 3.11+ (#319):
Toolno longer puts its dynamically generated parameter TypedDict into the pickle state. cloudpickle could serialize that type but not load it again (TypeError: cannot inherit from both a TypedDict type and a non-TypedDict base class), so a native tool was unusable across a process boundary. The validation model is regenerated from the wrapped function on load; if regeneration fails, the tool loads without parameter validation and emits aUserWarninginstead of failing the unpickle. - Executor lifecycle follow-ups (#321): a process pool created while the parent was single-threaded now migrates from
forktoforkserver/spawnwhen the process later grows threads (or warns once if work is still in flight), so new workers are never forked from a threaded parent.ToolRegistry.close(wait=True)/close_async(wait=True)drain in-flight and queued execution; the default remains non-blocking. - Python 3.14 deprecation in the executor backends (#314): the thread and process backends use
inspect.iscoroutinefunctioninstead of the deprecatedasyncio.iscoroutinefunction, sopython -W errorusers can execute tools on Python 3.14 and later. - Execution resources are released by
close()(#311):ToolRegistry.close()andclose_async()now shut down the thread and process execution backends in addition to the remote integrations; the backends recreate their pools lazily, so the registry remains usable and repeated calls are safe. Pools are created lazily on first submit, so a registry that never executes anything holds no worker threads or IPC pipes. The process backend forks when the parent is single-threaded and starts workers throughforkserver(orspawn) once other threads are running, so scripts, REPLs, and piped input keep working while threaded hosts no longer fork. A threaded file-based entry point needs the standardif __name__ == "__main__":guard; a threaded parent with a non-importable__main__warns and falls back tofork.close()abandons in-flight work by default; passwait=Trueto drain it (see #321). Leaked registries are also closed at pytest module teardown, the shared async runtime is stopped between modules, and CI jobs carry explicit timeouts. The dedicated LangChain job is kept so the suite always has a langchain-free run. spinoff()andreduce_namespace()honor the registry separator (#310): namespace operations now useTool.namespace/Tool.method_nameinstead of parsing keys with".".spinoff()works under the default-, matchesname_sep, avoids matching a longer namespace (mathvsmath_ops), and the returned registry inherits the source separator and execution mode.reduce_namespace()flattens only tools in the single remaining namespace, so unqualified tools are no longer truncated (helperno longer becomesr), and collisions raise instead of overwriting. Names are rebuilt from the fields rather than blind-sliced,merge()converts incoming names to the receiving registry's separator and carries incoming override records, and changingname_sepat runtime re-keys registered tools. Disabled state moves with the tool throughspinoff,merge,reduce_namespace, and separator changes, so a disabled destructive tool cannot come back live, and every operation that renames tools marks the discovery index stale sodiscover()/call_deferred()use the new keys.Tool.qualified_namereturns the registeredname.- Replacement warnings no longer skip hooks or events (#301): the replacement
UserWarningis emitted after post-register hooks and theREGISTER/REPLACEevents. With warnings configured as errors, the exception now follows a consistent registry state that every listener has observed. - Filter warnings report the caller's file and line (#300):
ToolNameFilter.warn_if_all_filtered_out()and the replacement warning use a caller-attributed helper.AsyncRuntime.run_sync()carries the synchronous caller's location into the coroutine, so the sync bridge no longer reports integration internals; executor-thread paths fall back to the immediate caller. - Remote registration and refresh agree on namespaced names (#295): both paths derive the registry key with the registry separator. With
name_sep=".", anincludethat matches at registration no longer misses there and match at refresh, and refresh no longer produces a double prefix such asgh.gh-list_issues. - Remote filters run before tool construction (#296): MCP, OpenAPI, and LangChain apply
include/excludeto the raw and qualified names before building the wrapper. An explicitly excluded malformed operation or failing schema extraction can no longer abort the whole source. discover_toolsno longer advertises disabled tools (#276): the BM25 index is built from enabled tools only, matchingget_deferred_summaries(), and the exact-name path rejects disabled tools.ENABLE/DISABLE/METADATA_UPDATEmark the index stale rather than rebuilding per event, so a bulk disable or profile application pays one rebuild at the nextdiscover(). This also fixes a stale per-documentdeferredflag that madediscover()omitschemaand reportdeferred: falsefor a tool thatget_deferred_summaries()listed as deferred.- Non-serializable tool results are JSON-encoded (#279): dataclasses, Pydantic models, objects exposing
to_dict()/tolist(), enums, paths, decimals, UUIDs, datetimes, bytes and sets reach the wire as valid JSON instead of Pythonrepr()text. Top-level and nested projections now agree, and set encoding is deterministic. - Process-backend pickling failures explain the backend (#288): a result that cannot be pickled out of the worker (a generator, a lambda, a live connection) now surfaces as
UnpicklableResultError, naming the process backend and asking for a picklable value — the thread backend skips pickling but still requires an encodable result — instead of a bareTypeError/AttributeErrorfrompickle. Native (class-based) tools use the process backend by default and get the same guidance; MCP and OpenAPI tools are unaffected because theirnatural_backend="inline"forces the thread backend. ToolMetadata.all_tagstolerates plain-string tags:update_tool_metadata(tags=["x"])no longer aborts index rebuilding withAttributeError.- Registration filters match normalized names and surface empty results (#291): a bare string passed to
include/excludeis now a single pattern instead of an iterable of characters, so a one-item JSON/YAMLinclude: nameworks. Patterns match four spellings — the raw source name, the post-namespace registered name, and the snake_case form of each — soinclude=["bash_execute"]selects a tool advertised asbash-execute. MCP and OpenAPI registration emit aUserWarningwhen an active filter leaves the source empty, distinguishing "matched none" (include) from "excluded all" (exclude); LangChain registers one tool per call and stays silent. Matching remains case-sensitive, and a normalized spelling selects every tool whose normalized name collides.
Full Changelog: v0.18.1...v0.19.0