Class ::nsmcp::ToolRegistry (public)
::nx::Class ::nsmcp::ToolRegistry
Defined in /usr/local/ns/tcl/nsmcp/01-tools.tcl
- Testcases:
- No testcase defined.
Source code: :variable tools {} :property {stateStore {}} # Nonempty context opts this registry into trusted module providers. :property {providerContext {}} :variable providers {} # Look up provider-owned settings without inserting defaults into either # section. Explicit false/empty values must not fall through to legacy data. :public object method providerConfig {context parameter default} { # # Read a provider setting from the configured sections. # # Try the primary and fallback sections in order, using the supplied default # when neither supplies the parameter. # # @param context Provider configuration dictionary with primary and fallback # section names. # @param parameter NaviServer configuration parameter name. # @param default Value used when the requested configuration entry is # absent. # # @return The configured setting, or the default. # foreach key {section fallbackSection} { if {![dict exists $context $key]} { continue } set section [dict get $context $key] set name $parameter if {$key eq "fallbackSection" && [dict exists $context fallbackParameters $parameter]} { set name [dict get $context fallbackParameters $parameter] } set configuration [ns_configsection $section] if {$configuration ne "" && [ns_set ifind $configuration $name] >= 0} { return [ns_config $section $name] } } return $default } :public method refreshProviders {} { # # Refresh registrations supplied by configured tool providers. # # Reconcile provider descriptors with registry state; this is registration # work, not a tool invocation. # # @return The provider dictionary when a provider context is configured, # otherwise an empty result. # if {${:providerContext} eq ""} { return } set descriptors {} catch { set descriptors [nsv_array get nsmcp:providers] } # Registration is trusted startup Tcl, never an MCP/client operation. dict for {provider descriptor} $descriptors { if {[dict exists ${:providers} $provider]} { continue } foreach field {file factory} { if {![dict exists $descriptor $field]} { error "provider $provider lacks $field" } } if {[dict exists $descriptor requires] && [info commands [dict get $descriptor requires]] eq ""} { continue } set context ${:providerContext} if {[dict exists $descriptor section]} { dict set context fallbackSection [dict get $context section] dict set context section [dict get $descriptor section] } if {[dict exists $descriptor fallbackParameters]} { dict set context fallbackParameters [dict get $descriptor fallbackParameters] } if {[dict exists $descriptor enabledParameter]} { set parameter [dict get $descriptor enabledParameter] set enabled [::nsmcp::ToolRegistry providerConfig $context $parameter false] if {![string is boolean -strict $enabled]} { error "provider $provider requires a boolean $parameter" } if {!$enabled} { continue } } set file [dict get $descriptor file] if {[file pathtype $file] ne "absolute" || ![file isfile $file]} { error "provider $provider must provide an absolute readable Tcl file" } uplevel #0 [list source $file] set factory [dict get $descriptor factory] if {[llength $factory] != 1 || [info commands $factory] eq ""} { error "provider $provider must provide a factory command" } # Validate the entire batch before adding any tool. Factory failures # must not leave a partially registered provider or replace a tool. set candidates [$factory $context] set pending {} foreach tool $candidates { if {![nsf::object::exists $tool] || ![$tool info has type ::nsmcp::Tool]} { error "provider $provider returned a non-Tool object" } set name [$tool name] if {[dict exists ${:tools} $name] || [dict exists $pending $name]} { error "duplicate tool: $name" } dict set pending $name $tool } dict for {name tool} $pending { dict set :tools $name $tool } dict set :providers $provider [dict keys $pending] } return ${:providers} } :public method controlsAvailable {} { # # Report whether runtime tool enable controls are configured. # # The state store determines availability of administrative overrides. # # @return True when a runtime state store is present, otherwise false. # # An explicitly configured backend owns per-server runtime overrides. return [expr {${:stateStore} ne ""}] } :method readEnabled {name} { # Isolate storage access from discovery and invocation. An absent # override means enabled; NSV state is shared by this server's threads. if {${:stateStore} ne "" && [nsv_exists ${:stateStore} $name]} { return [nsv_get ${:stateStore} $name] } return true } :method writeEnabled {name enabled} { nsv_set ${:stateStore} $name $enabled } :public method enabled {name} { # # Report whether a named tool is administratively enabled. # # Refresh provider registrations first; caller permissions are checked # separately during listing and invocation. # # @param name Registered MCP tool name. # # @return The effective enabled flag, or false for an unknown tool. # :refreshProviders # Administrative availability, independent of caller permissions. if {![dict exists ${:tools} $name]} { error "unknown tool: $name" } return [:readEnabled $name] } :public method setEnabled {name enabled:boolean} { # # Change the administrative enable flag for a registered tool. # # This trusted interface affects new calls, not jobs already running or # background collectors. # # @param name Registered MCP tool name. # @param enabled Boolean administrative enable flag. # # @return The enabled flag written to the state store. # # Trusted administrative interface, not an MCP tool. Disabling prevents # new invocations; it does not cancel existing jobs or stop collectors. # Overrides survive interpreter recreation/reload, not process restart. if {![:controlsAvailable]} { error "runtime tool controls are not configured" } :enabled $name set enabled [expr {!!$enabled}] :writeEnabled $name $enabled return $enabled } :public method register {tool} { # # Register a tool object under its configured name. # # Reject duplicate names so dispatch cannot silently replace an existing # registration. # # @param tool Tool object to register. # # @return The updated internal registration dictionary. # set name [$tool name] if {[dict exists ${:tools} $name]} { error "duplicate tool: $name" } dict set :tools $name $tool } :public method describe {} { # # List administrative metadata for registered tools. # # Include permission and enable-state information without executing tools or # applying a caller's permission filter. # # @return A list of tool metadata dictionaries. # :refreshProviders # Administrative metadata for the registered tools. This does not # execute tools or change permission-filtered MCP tools/list results. set result {} foreach name [lsort [dict keys ${:tools}]] { set tool [dict get ${:tools} $name] set description [ns_json parse [ns_json value -- [$tool describe]]] foreach field {adminSummary adminExample} { set value [$tool cget -$field] if {$value ne ""} { dict set description $field $value } } dict set description permission [$tool permission] dict set description enabled [:readEnabled $name] lappend result $description } return $result } :public method fingerprint {name} { # # Hash a registered tool's permission and descriptor. # # Question conversations use this bounded descriptor fingerprint to detect a # changed tool definition. # # @param name Registered MCP tool name whose descriptor is pinned. # # @return The hexadecimal SHA-256 fingerprint; unknown tools raise an error. # :refreshProviders if {![dict exists ${:tools} $name]} { error "Unknown question tool" } set tool [dict get ${:tools} $name] set data [list [$tool permission] [ns_json value -- [$tool describe]]] if {[string bytelength $data]>1048576} { error "Question tool descriptor exceeds limit" } return [ns_crypto::md string -digest sha256 -- $data] } :public method questionSnapshot {context authorizer} { # # Pin the permitted tool inventory for a new question. # # Apply request authorization and question allowlists; outbound upstream # comparison additionally requires its configured provider and switches. # # @param context Authenticated request context carrying the principal, # grants and selected server. # @param authorizer Authorizer used to enforce the request context's # permissions and targets. # # @return A dictionary mapping allowed tool names to descriptor # fingerprints. # set pins {} set listed [:list $context $authorizer] foreach {index type members} [ns_json triples getvalue -output triples -path tools -- $listed] { set metadata [ns_json parse [ns_json value -- [list {} object $members]]] if {![dict exists $metadata annotations readOnlyHint] || ![dict get $metadata annotations readOnlyHint]} { continue } set name [dict get $metadata name] if {$name ni [::nsmcp::questionAllowedTools]} { continue } if {![dict exists $metadata annotations destructiveHint] || [dict get $metadata annotations destructiveHint] || ![dict exists $metadata annotations openWorldHint]} { continue } if {[dict get $metadata annotations openWorldHint]} { # This one outbound tool has a fixed GitHub transport and # administrator-owned repository mappings. All other external # tools remain excluded from question execution. if {$name ne "code_upstream_compare" || ![nsf::is object,type=::nsmcp::CodeUpstreamTool [dict get ${:tools} $name]] || ![ns_config -bool ns/server/[ns_info server]/module/nsmcp enableCodeTools false] || ![ns_config -bool ns/server/[ns_info server]/module/nsmcp enableCodeUpstream false]} { continue } } if {[dict size $pins]>=256 || [string bytelength $name]>128} { error "Question tool inventory exceeds limit" } dict set pins $name [:fingerprint $name] } if {$pins eq {}} { error "No permitted read-only question tools are available" } return $pins } :method verifyQuestionPin {context name} { if {![$context cget -questionRestricted]} { return } set pins [$context cget -questionPins] if {$name ni [::nsmcp::questionAllowedTools] || ![dict exists $pins $name] || [dict get $pins $name] ne [:fingerprint $name]} { return -code error -errorcode {NSMCP RPC -32602} "Question tool definition changed or tool was not pinned; start a new conversation" } } :public method list {context authorizer} { # # List the tools available to an authenticated caller. # # Refresh providers and apply administrative enable flags and the supplied # authorizer. # # @param context Authenticated request context carrying the principal, # grants and selected server. # @param authorizer Authorizer used to enforce the request context's # permissions and targets. # # @return A rooted JSON object containing the tools array. # :refreshProviders set result {} foreach name [lsort [dict keys ${:tools}]] { set tool [dict get ${:tools} $name] if {[:readEnabled $name] && [$authorizer allowed $context [$tool permission]]} { if {[$context cget -questionRestricted]} { if {![dict exists [$context cget -questionPins] $name]} { continue } :verifyQuestionPin $context $name } lappend result [$tool describe] } } set members {} set index 0 foreach description $result { lappend members $index object [lindex $description 2] incr index } return [list {} object [list tools array $members]] } :public method call {context authorizer name arguments} { # # Authorize, validate and invoke a registered tool. # # Check enable state, permissions and question pins before handing arguments # to the tool. # # @param context Authenticated request context carrying the principal, # grants and selected server. # @param authorizer Authorizer used to enforce the request context's # permissions and targets. # @param name Name of the configured or registered item to resolve. # @param arguments Rooted ns_json typed triples for the tool's JSON argument # object. # # @return The tool's rooted JSON result; policy and validation failures # raise RPC errors. # :refreshProviders if {![dict exists ${:tools} $name]} { return -code error -errorcode {NSMCP RPC -32602} "Unknown tool" } set tool [dict get ${:tools} $name] $authorizer require $context [$tool permission] if {![:readEnabled $name]} { return -code error -errorcode {NSMCP RPC -32602} "Tool is disabled on this server" } :verifyQuestionPin $context $name $tool validate $arguments if {[catch { $tool execute $context $arguments } result options]} { if {[lrange [dict get $options -errorcode] 0 1] eq {NSMCP RPC}} { return -options $options $result } ns_log error "nsmcp: tool execution failed: $name" return [list {} object {content array {0 object {type string text text string {Tool execution failed; consult the server log.}}} isError boolean true}] } # Both structured output and text for clients without structured support. return [list {} object [list structuredContent object [lindex $result 2] content array [list 0 object [list type string text text string [ns_json value -- $result]]] isError boolean false]] }XQL Not present: Generic, PostgreSQL, Oracle
![[i]](/resources/acs-subsite/ZoomIn16.gif)