292 lines
9.4 KiB
Julia
292 lines
9.4 KiB
Julia
module toolRegistry
|
|
|
|
export registerTool, registerAllTools, clearTools, listTool
|
|
|
|
using Dates
|
|
using JSON, DataStructures
|
|
using ..type
|
|
|
|
|
|
|
|
# ── MCP helper functions ────────────────────────────────────────────
|
|
|
|
"""
|
|
Extract text from MCP tool result content array.
|
|
|
|
Handles JSON-RPC 2.0 result content format:
|
|
{"content": [{"type": "text", "text": "..."}], "isError": false}
|
|
"""
|
|
function _extract_text_content(result::AbstractDict)::String
|
|
content = get(result, "content", Any[])
|
|
if content isa Vector && !isempty(content)
|
|
lines = String[]
|
|
for block in content
|
|
if block isa Dict && get(block, "type", "") == "text"
|
|
push!(lines, string(get(block, "text", "")))
|
|
end
|
|
end
|
|
if !isempty(lines)
|
|
return join(lines, "\n")
|
|
end
|
|
end
|
|
return JSON.json(result)
|
|
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.
|
|
"""
|
|
function _wrap_mcp_tool(mcpserver, tool_def::AbstractDict{String, Any}; eventSink=nothing, sessionId=nothing)::agentTool
|
|
name = tool_def["name"]
|
|
title = get(tool_def, "title", get(tool_def, "label", name))
|
|
desc = get(tool_def, "description", "")
|
|
input_schema = get(tool_def, "inputSchema", Dict{String,Any}())
|
|
|
|
# Normalize inputSchema to OpenAI function format
|
|
if haskey(input_schema, "properties") && input_schema["type"] == "object"
|
|
params = Dict(
|
|
"type" => "object",
|
|
"properties" => input_schema["properties"],
|
|
"required" => get(input_schema, "required", Any[]),
|
|
)
|
|
else
|
|
params = Dict(
|
|
"type" => "object",
|
|
"properties" => Dict{String,Any}(),
|
|
"required" => Any[],
|
|
)
|
|
end
|
|
|
|
return agentTool(
|
|
name=name,
|
|
label=title,
|
|
description=desc,
|
|
inputSchema=params,
|
|
execute=(toolCallId::String, args::AbstractDict{String, Any},
|
|
signal::Union{Nothing,abortSignal},
|
|
eventSink) -> begin
|
|
try
|
|
response = mcpserver("tools/call", name, args, sessionId; eventSink=eventSink)
|
|
|
|
# Parse JSON-RPC 2.0 response envelope
|
|
if haskey(response, "error")
|
|
rpc_error = response["error"]
|
|
err_msg = get(rpc_error, "message", "Unknown MCP error")
|
|
return agentToolResult(
|
|
[textContent("MCP error: $err_msg")],
|
|
Dict{Any,Any}("error" => err_msg),
|
|
nothing, false
|
|
)
|
|
end
|
|
|
|
result_data = get(response, "result", response)
|
|
content_text = _extract_text_content(result_data)
|
|
is_error = get(result_data, "isError", false)
|
|
|
|
return agentToolResult(
|
|
[textContent(content_text)],
|
|
Dict{Any,Any}("isError" => is_error),
|
|
nothing, false
|
|
)
|
|
catch e
|
|
errMsg = sprint(showerror, e)
|
|
return agentToolResult(
|
|
[textContent("MCP call error: $errMsg")],
|
|
Dict{Any,Any}("error" => errMsg),
|
|
nothing, false
|
|
)
|
|
end
|
|
end,
|
|
prepareArguments=nothing,
|
|
validateRequiredArgs=nothing,
|
|
parallelToolExecute=false,
|
|
)
|
|
end
|
|
|
|
"""
|
|
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`. Skips tools already registered.
|
|
|
|
# Returns
|
|
- `Int`: number of new tools registered
|
|
"""
|
|
function register_mcp_tools(tools::OrderedDict{String, agentTool}, mcpserver; eventSink=nothing, sessionId=nothing)::Int
|
|
if mcpserver === nothing
|
|
return 0
|
|
end
|
|
new_count = 0
|
|
try
|
|
eventSink("register_mcp_tools 1")
|
|
response = mcpserver("tools/list", sessionId; eventSink=eventSink)
|
|
eventSink("register_mcp_tools 2")
|
|
# Parse JSON-RPC 2.0 response envelope
|
|
if haskey(response, "error")
|
|
rpc_error = response["error"]
|
|
err_msg = get(rpc_error, "message", "Unknown MCP error")
|
|
println("[toolRegistry] MCP tools/list failed: $err_msg")
|
|
return 0
|
|
end
|
|
if haskey(response, "result")
|
|
response = response["result"]
|
|
end
|
|
|
|
tools_array = response["tools"]
|
|
cursor = get(response, "nextCursor", nothing)
|
|
|
|
for tool_def in tools_array
|
|
name = tool_def["name"]
|
|
if haskey(tools, name)
|
|
continue
|
|
end
|
|
|
|
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink, sessionId=sessionId)
|
|
tools[name] = wrapped
|
|
new_count += 1
|
|
end
|
|
|
|
# Paginate: fetch remaining tools if nextCursor is present
|
|
while cursor !== nothing && cursor !== ""
|
|
response = sessionId !== nothing ? mcpserver("tools/list", sessionId; eventSink=eventSink) : mcpserver("tools/list"; eventSink=eventSink)
|
|
if haskey(response, "result")
|
|
response = response["result"]
|
|
end
|
|
tools_array = get(response, "tools", Any[])
|
|
cursor = get(response, "nextCursor", nothing)
|
|
for tool_def in tools_array
|
|
name = tool_def["name"]
|
|
if haskey(tools, name)
|
|
continue
|
|
end
|
|
wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink, sessionId=sessionId)
|
|
tools[name] = wrapped
|
|
new_count += 1
|
|
end
|
|
end
|
|
|
|
println("[toolRegistry] Discovered $new_count MCP tools. Total: $(length(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)
|
|
println("[toolRegistry] MCP tools/list failed: $errMsg")
|
|
end
|
|
|
|
return new_count
|
|
end
|
|
|
|
"""
|
|
registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool) -> OrderedDict{String, agentTool}
|
|
|
|
Add `tool` to `tools`, overwriting any existing tool with the same name.
|
|
|
|
# Arguments
|
|
- `tools`: Tool dict to modify
|
|
- `tool`: The `agentTool` to register
|
|
|
|
# Returns
|
|
- The same `tools` dict (modified in place)
|
|
"""
|
|
function registerTool(tools::OrderedDict{String, agentTool}, tool::agentTool; eventSink=nothing
|
|
)::OrderedDict{String, agentTool}
|
|
tools[tool.name] = tool
|
|
println("[toolRegistry] Registered tool: $(tool.name)")
|
|
return tools
|
|
end
|
|
|
|
"""
|
|
Remove all tools from the dict.
|
|
|
|
# Arguments
|
|
- `tools`: Tool dict to clear
|
|
|
|
# Returns
|
|
- `nothing`
|
|
"""
|
|
function clearTools(tools::OrderedDict{String, agentTool})::Nothing
|
|
empty!(tools)
|
|
println("[toolRegistry] Registry cleared")
|
|
return nothing
|
|
end
|
|
|
|
# ── listTools tool (auto-discover new MCP tools at runtime) ─────────
|
|
|
|
"""
|
|
listTool(tools::OrderedDict{String, agentTool}, 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 `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(tools::OrderedDict{String, agentTool}, mcpserver; eventSink=nothing, sessionId=nothing)::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) -> begin
|
|
# Discover and register MCP tools (idempotent — skips already registered)
|
|
new_count = register_mcp_tools(tools, mcpserver; eventSink=eventSink, sessionId=sessionId)
|
|
eventSink("tools dump: " * sprint(show, tools))
|
|
|
|
# Always include listTools itself in the count
|
|
total = length(tools)
|
|
|
|
lines = String[
|
|
"- $(t.name): $(t.label) — $(t.description)"
|
|
for (k, t) in 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(tools::OrderedDict{String, agentTool}, mcpserver)
|
|
|
|
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
|
|
- `tools`: The tool dict to populate
|
|
- `mcpserver`: A callable struct that communicates with the MCP server
|
|
"""
|
|
function registerAllTools(tools::OrderedDict{String, agentTool}, mcpserver=nothing; eventSink=nothing, sessionId=nothing)
|
|
list_t = listTool(tools, mcpserver; eventSink=eventSink, sessionId=sessionId)
|
|
registerTool(tools, list_t; eventSink=eventSink)
|
|
end
|
|
|
|
end # module
|