Class ::nsmcp::ToolRegistry (public)

 ::nx::Class ::nsmcp::ToolRegistry[i]

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
[ hide source ] | [ make this the default ]
Show another procedure: