This commit is contained in:
2026-08-21 13:13:38 +07:00
parent c59f6bfa61
commit da21790263
8 changed files with 276 additions and 323 deletions
+106 -139
View File
@@ -1,6 +1,6 @@
module toolRegistry
export toolStore, registerTool, getTools, clearTools, listTool
export toolStore, registerTool, registerAllTools, getTools, clearTools, listTool
using Dates
using JSON, DataStructures
@@ -9,8 +9,7 @@ using ..type
"""
Per-agent isolated tool storage.
Each agent gets its own `toolStore` so tool registration is independent —
`registerTool(store, tool)` only affects that agent's tool set.
Each agent gets its own `toolStore` so tool registration is independent.
# Fields
- `tools::OrderedDict{String, agentTool}` — keyed by name for O(1) lookup + ordered iteration
@@ -28,12 +27,6 @@ Create a new empty tool store.
# Keyword Arguments
- `name::String`: Display name for logging (default: `"default"`)
# Example
```julia
julia> store = toolStore(name="agent1")
toolStore(OrderedDict{String, agentTool}(), "agent1")
```
"""
function toolStore(; name::String="default")::toolStore
toolStore(OrderedDict{String, agentTool}(), name)
@@ -46,8 +39,6 @@ Extract text from MCP tool result content array.
Handles JSON-RPC 2.0 result content format:
{"content": [{"type": "text", "text": "..."}], "isError": false}
Returns the joined text content, or JSON-serialized fallback if no text blocks found.
"""
function _extract_text_content(result::Dict)::String
content = get(result, "content", Any[])
@@ -69,10 +60,9 @@ end
Wrap an MCP tool definition as an `agentTool`.
The returned tool's `execute` function calls the MCP server's "tools/call"
method with the validated arguments. Responses are in JSON-RPC 2.0 format
with result/error envelopes handled by the execute function.
method with the validated arguments.
"""
function _wrap_mcp_tool(mcpserver, tool_def::Dict{String,Any})::agentTool
function _wrap_mcp_tool(mcpserver, tool_def::AbstractDict{String, Any}; eventSink=nothing)::agentTool
name = tool_def["name"]
title = get(tool_def, "title", get(tool_def, "label", name))
desc = get(tool_def, "description", "")
@@ -98,9 +88,9 @@ function _wrap_mcp_tool(mcpserver, tool_def::Dict{String,Any})::agentTool
label=title,
description=desc,
inputSchema=params,
execute=(toolCallId::String, args::Dict{String,Any},
execute=(toolCallId::String, args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal},
agentEventSink,
eventSink,
llmCall=nothing) -> begin
try
response = mcpserver("tools/call", name, args)
@@ -145,14 +135,18 @@ Discover and register MCP tools into `store.tools`.
Queries the MCP server via `mcpserver("tools/list")` (JSON-RPC 2.0 format),
parses the response envelope, and registers each discovered tool.
Handles pagination via `nextCursor` — keeps fetching until cursor is empty.
Skips tools already registered. Returns `(new_count, tool_list_text)`.
Handles pagination via `nextCursor`. Skips tools already registered.
# Returns
- `Int`: number of new tools registered
"""
function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
function register_mcp_tools(mcpserver, store::toolStore; eventSink=nothing)::Int
if mcpserver === nothing
return (0, "")
return 0
end
new_count = 0
try
response = mcpserver("tools/list")
@@ -160,7 +154,8 @@ function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
if haskey(response, "error")
rpc_error = response["error"]
err_msg = get(rpc_error, "message", "Unknown MCP error")
return (0, "MCP tools/list failed: $err_msg")
println("[toolRegistry:$(store.name)] MCP tools/list failed: $err_msg")
return 0
end
if haskey(response, "result")
response = response["result"]
@@ -169,13 +164,13 @@ function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
tools_array = response["tools"]
cursor = get(response, "nextCursor", nothing)
new_count = 0
for tool_def in tools_array
name = tool_def["name"]
if haskey(store.tools, name)
continue
end
wrapped = _wrap_mcp_tool(mcpserver, tool_def)
@show "tool_def $(typeof(tool_def))"
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
store.tools[name] = wrapped
new_count += 1
end
@@ -193,101 +188,29 @@ function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
if haskey(store.tools, name)
continue
end
wrapped = _wrap_mcp_tool(mcpserver, tool_def)
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink)
store.tools[name] = wrapped
new_count += 1
end
end
# Build readable tool list
lines = String[
"- $(t.name): $(t.label) — $(t.description)"
for (k, t) in store.tools
]
tool_list_text = "Discovered $(new_count) MCP tools. Total registered: $(length(store.tools)).\nAvailable tools:\n" * join(lines, "\n")
return (new_count, tool_list_text)
println("[toolRegistry:$(store.name)] Discovered $new_count MCP tools. Total: $(length(store.tools))")
catch e
bt = catch_backtrace()
err_msg = sprint() do io
showerror(io, e, bt)
println(io)
end
eventSink(err_msg)
errMsg = sprint(showerror, e)
return (0, "MCP tools/list failed: $errMsg")
println("[toolRegistry:$(store.name)] MCP tools/list failed: $errMsg")
end
return new_count
end
"""
listTool(store::toolStore, mcpserver) -> agentTool
MCP-aware listTools tool (JSON-RPC 2.0 protocol).
First call: queries the MCP server via `mcpserver("tools/list")`, registers
all discovered tools into the shared `store.tools` (in-place mutation, with
pagination via nextCursor), then returns the full tool list.
Subsequent calls: returns the current list (tools remain registered).
This is the only pre-registered tool. All other tools come from the
MCP server and are loaded at runtime when the LLM calls listTools().
# Arguments
- `store`: The tool store to populate with MCP tools
- `mcpserver`: A callable struct that communicates with the MCP server.
Called as `mcpserver("tools/list")` or `mcpserver("tools/call", args)`.
Returns parsed JSON dicts.
# Example
```julia
# User provides an MCP server client (callable struct)
mcp = MyMCPClient("nats://localhost:4222")
store = toolStore(name="agent1")
registerTool(store, listTool(store, mcp))
# When agent calls listTools(), tools are discovered from MCP server
# and registered into store.tools in real time.
```
"""
function listTool(store::toolStore, mcpserver)::agentTool
return agentTool(
name="listTools",
label="List Tools",
description="List all available tools. First call discovers and registers all tools from the MCP server. After discovery, new tools become immediately available for use.",
inputSchema=Dict{String,Any}(
"type" => "object",
"properties" => Dict{String,Any}(),
"required" => Any[]
),
execute=(toolCallId::String, args::Dict{String,Any},
signal::Union{Nothing,abortSignal},
agentEventSink, llmCall=nothing) -> begin
# Discover and register MCP tools (idempotent — skips already registered)
new_count, tool_list = _register_mcp_tools(mcpserver, store)
# Always include listTools itself in the count
total = length(store.tools)
if new_count > 0
result_text = tool_list
else
# Already discovered — just return current list
lines = String[
"- $(t.name): $(t.label) — $(t.description)"
for (k, t) in store.tools
]
result_text = "Available tools ($total):\n" * join(lines, "\n")
end
return agentToolResult(
[textContent(result_text)],
Dict{Any,Any}("count" => total),
nothing, false
)
end,
prepareArguments=nothing,
validateRequiredArgs=nothing,
parallelToolExecute=false,
)
end
# Note: register_all_tools is defined in YiemAgent.jl where tool functions are in scope
"""
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
@@ -299,18 +222,9 @@ Add `tool` to `store`, overwriting any existing tool with the same name.
# Returns
- The same `store.tools` dict (modified in place)
# Example
```julia
julia> store = toolStore(name="agent1");
julia> registerTool(store, listTool(store, nothing))
[toolRegistry:agent1] Registered tool: listTools
OrderedDict{String, agentTool} with 1 entry:
"listTools" => agentTool(...)
```
"""
function registerTool(store::toolStore, tool::agentTool)::OrderedDict{String, agentTool}
function registerTool(store::toolStore, tool::agentTool; eventSink=nothing
)::OrderedDict{String, agentTool}
store.tools[tool.name] = tool
println("[$(store.name)] Registered tool: $(tool.name)")
return store.tools
@@ -319,22 +233,13 @@ end
"""
Return the tools registered in `store`.
The returned dict is the **same object** stored inside `store` — mutations
to it (e.g. via `registerTool`) are visible through subsequent calls.
The returned dict is the **same object** stored inside `store`.
# Arguments
- `store`: Tool store to query
# Returns
- `OrderedDict{String, agentTool}`: Tools keyed by name, in registration order
# Example
```julia
julia> tools = getTools(store)
OrderedDict{String, agentTool} with 2 entries:
"getWeather" => agentTool(...)
"getTime" => agentTool(...)
```
"""
function getTools(store::toolStore)::OrderedDict{String, agentTool}
return store.tools
@@ -348,16 +253,6 @@ Remove all tools from `store`.
# Returns
- `nothing`
# Example
```julia
julia> clearTools(store)
[toolRegistry:agent1] Registry cleared
nothing
julia> getTools(store)
OrderedDict{String, agentTool} with 0 entries
```
"""
function clearTools(store::toolStore)::Nothing
empty!(store.tools)
@@ -365,4 +260,76 @@ function clearTools(store::toolStore)::Nothing
return nothing
end
# ── listTools tool (auto-discover new MCP tools at runtime) ─────────
"""
listTool(store::toolStore, mcpserver) -> agentTool
MCP-aware listTools tool (JSON-RPC 2.0 protocol).
First call: queries the MCP server via `mcpserver("tools/list")`, registers
all discovered tools into the shared `store.tools` (in-place mutation, with
pagination via nextCursor), then returns the full tool list.
Subsequent calls: returns the current list (tools remain registered).
"""
function listTool(store::toolStore, mcpserver)::agentTool
return agentTool(
name="listTools",
label="List Tools",
description="List all available tools. First call discovers and registers all tools from the MCP server. After discovery, new tools become immediately available for use.",
inputSchema=Dict{String,Any}(
"type" => "object",
"properties" => Dict{String,Any}(),
"required" => Any[]
),
execute=(toolCallId::String, args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal},
eventSink, llmCall=nothing) -> begin
# Discover and register MCP tools (idempotent — skips already registered)
new_count = register_mcp_tools(mcpserver, store)
# Always include listTools itself in the count
total = length(store.tools)
lines = String[
"- $(t.name): $(t.label) — $(t.description)"
for (k, t) in store.tools
]
result_text = "Available tools ($total):\n" * join(lines, "\n")
return agentToolResult(
[textContent(result_text)],
Dict{Any,Any}("count" => total),
nothing, false
)
end,
prepareArguments=nothing,
validateRequiredArgs=nothing,
parallelToolExecute=false,
)
end
# ── High-level API ──────────────────────────────────────────────────
"""
registerAllTools(store::toolStore, mcpserver) -> toolStore
Register all tools for an agent:
1. Auto-discover existing tools from the MCP server
2. Register the `listTools` tool so the agent can discover new tools at runtime
# Arguments
- `store`: The tool store to populate
- `mcpserver`: A callable struct that communicates with the MCP server
# Returns
- The populated `toolStore`
"""
function registerAllTools(store::toolStore, mcpserver=nothing; eventSink=nothing)::toolStore
register_mcp_tools(mcpserver, store; eventSink=eventSink)
registerTool(store, listTool(store, mcpserver); eventSink=eventSink)
return store
end
end # module