update mcp definition example

This commit is contained in:
2026-08-20 09:58:55 +07:00
parent 5829c82d05
commit 7ffb720f86
11 changed files with 346 additions and 918 deletions
+176 -34
View File
@@ -7,7 +7,7 @@ using JSON, DataStructures
using ..type
"""
Per-agent isolated tool storage.
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.
@@ -39,61 +39,203 @@ function toolStore(; name::String="default")::toolStore
toolStore(OrderedDict{String, agentTool}(), name)
end
# ── MCP helper functions ────────────────────────────────────────────
"""
listTool(store::toolStore) -> agentTool
Extract text from MCP tool result content array.
Return an `agentTool` definition for listing registered tools.
Handles MCP's content format: [{"type":"text","text":"..."}]
"""
function _extract_text_content(result::Dict)::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
Each call produces a **new** tool object that captures (closes over)
`store`. `register_all_tools` auto-registers one so the LLM can discover tools
at runtime.
"""
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::Dict{String,Any})::agentTool
name = tool_def["toolName"]
title = get(tool_def, "title", 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::Dict{String,Any},
signal::Union{Nothing,abortSignal},
onPartialResult::Function,
llmCall=nothing) -> begin
try
response = mcpserver("tools/call", name, args)
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")`, parses the response,
and registers each discovered tool. Skips tools already registered.
Returns `(new_count, tool_list_text)`.
"""
function _register_mcp_tools(mcpserver, store::toolStore)::Tuple{Int, String}
if mcpserver === nothing
return (0, "")
end
try
response = mcpserver("tools/list")
tools_array = response["tools"]
new_count = 0
for tool_def in tools_array
name = tool_def["toolName"]
if haskey(store.tools, name)
continue
end
wrapped = _wrap_mcp_tool(mcpserver, tool_def)
store.tools[name] = wrapped
new_count += 1
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)
catch e
errMsg = sprint(showerror, e)
return (0, "MCP tools/list failed: $errMsg")
end
end
"""
listTool(store::toolStore, mcpserver) -> agentTool
MCP-aware listTools tool.
First call: queries the MCP server via `mcpserver("tools/list")`, registers
all discovered tools into the shared `store.tools` (in-place mutation),
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 whose tools will be listed when the tool runs
- `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
julia> store = toolStore(name="agent1");
# User provides an MCP server client (callable struct)
mcp = MyMCPClient("nats://localhost:4222")
store = toolStore(name="agent1")
registerTool(store, listTool(store, mcp))
julia> register_all_tools(store) # auto-registers listTools
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
[toolRegistry:agent1] Registered tool: listTools
julia> tools = getTools(store)
OrderedDict{String, agentTool} with 4 entries:
"getWeather" => agentTool(...)
"getTime" => agentTool(...)
"writeTool" => agentTool(...)
"listTools" => agentTool(...)
# When agent calls listTools(), tools are discovered from MCP server
# and registered into store.tools in real time.
```
"""
function listTool(store::toolStore)::agentTool
function listTool(store::toolStore, mcpserver)::agentTool
return agentTool(
name = "listTools",
label = "List Tools",
description = "List all available tools with their names, labels, and descriptions. Use this before creating a new tool to check for name collisions.",
inputSchema = Dict{String,Any}(
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, args, signal, onPartialResult) -> begin
tools = getTools(store)
if isempty(tools)
result_text = "No tools registered."
execute=(toolCallId::String, args::Dict{String,Any},
signal::Union{Nothing,abortSignal},
onPartialResult::Function, 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
lines = String["- $(t.name): $(t.label) — $(t.description)" for (k, t) in tools]
result_text = "Available tools:\n" * join(lines, "\n")
# 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" => length(tools)),
Dict{Any,Any}("count" => total),
nothing, false
)
end,
prepareArguments = nothing,
validateRequiredArgs = nothing,
parallelToolExecute = false
prepareArguments=nothing,
validateRequiredArgs=nothing,
parallelToolExecute=false,
)
end
@@ -115,7 +257,7 @@ Add `tool` to `store`, overwriting any existing tool with the same name.
```julia
julia> store = toolStore(name="agent1");
julia> registerTool(store, listTool(store))
julia> registerTool(store, listTool(store, nothing))
[toolRegistry:agent1] Registered tool: listTools
OrderedDict{String, agentTool} with 1 entry:
"listTools" => agentTool(...)