Class ::nsmcp::HttpTransport (public)

 ::nx::Class ::nsmcp::HttpTransport[i]

Defined in /usr/local/ns/tcl/nsmcp/transport.tcl

Testcases:
No testcase defined.
Source code:
    :property authenticator:required
    :property protocol:required
    :property {allowedOrigins {}}
    :property {maxBodyBytes:integer 65536}

    :public method response {status body {headers {}}} {
        #
        # Construct an internal HTTP response descriptor.
        #
        # Keep the status, serialized body and header dictionary together for the
        # server adapter.
        #
        # @param status HTTP response status.
        # @param body Serialized HTTP or JSON-RPC request body.
        # @param headers HTTP header dictionary using lower-case field names.
        #
        # @return The response dictionary with status, body and headers.
        #

        return [dict create status $status body $body headers $headers]
    }

    :public method mediaTypes {header} {
        #
        # Normalize a comma-separated media-type header.
        #
        # Ignore media-type parameters while retaining the lower-case type names
        # used for negotiation.
        #
        # @param header Comma-separated media-type header value.
        #
        # @return The normalized media-type list.
        #

        set types {}
        foreach part [split [string tolower $header] ,] {
            lappend types [string trim [lindex [split $part \;] 0]]
        }
        return $types
    }

    :method questionPolicy {context key} {
        if {![regexp {^[0-9a-f]{64}$} $key] || ![nsv_get nsmcp:question-observers $key observer]
            || ![dict exists $observer toolPins] || [dict get $observer principal] ne [$context principal]
            || [dict get $observer server] ne [$context server] || [dict get $observer deadline]<=[clock milliseconds]} {
            error "Question policy expired or unavailable"
        }
        ::nsmcp::questionRestrict $context [dict get $observer toolPins] true
        return $observer
    }

    :public method questionPoll {authorization server key handle} {
        #
        # Authenticate and dispatch an administrative question poll operation.
        #
        # Apply the question credential policy and always clean up the temporary
        # request context.
        #
        # @param authorization Authorization header supplied by the caller.
        # @param server Selected NaviServer virtual-server name.
        # @param key Administrative question-poll operation selector.
        # @param handle Retained job or conversation handle; ownership and expiry
        #        checks still apply.
        #
        # @return The internal HTTP response descriptor.
        #

        set context [${:authenticator} authenticate $authorization $server]
        try {
            :questionPolicy $context $key
            set response [${:protocol} handle $context [ns_json value -- [list {} object [list jsonrpc string 2.0 id number 1  method string tools/call params object [list name string log_results arguments object [list jobHandle string $handle]]]]]]
            set root [ns_json parse -output triples -- [dict get $response body]]
            return [ns_json value -- [list {} object [ns_json triples getvalue -output triples -path result -- $root]]]
        } finally {
            $context destroy
        }
    }

    :public method request {method headers body server} {
        #
        # Handle one MCP HTTP request using normalized headers.
        #
        # Apply origin, method, media-type, authentication and protocol checks
        # before dispatch; clean up request-owned objects on exit.
        #
        # @param method MCP protocol method or HTTP method, depending on the
        #        adapter.
        # @param headers HTTP header dictionary using lower-case field names.
        # @param body Serialized HTTP or JSON-RPC request body.
        # @param server Selected NaviServer virtual-server name.
        #
        # @return The internal HTTP response descriptor.
        #

        # Header dictionary uses lower-case keys; created by Server.http.
        try {
            if {[dict exists $headers origin] &&
                [dict get $headers origin] ni ${:allowedOrigins}} {
                return [:response 403 "Origin not permitted"]
            }
            set authorization {}
            if {[dict exists $headers authorization]} {
                set authorization [dict get $headers authorization]
            }
            if {[catch {
                ${:authenticator} authenticate $authorization $server
            } context options]} {
                set ec [dict get $options -errorcode]
                if {[lrange $ec 0 1] eq {NSMCP HTTP}} {
                    return [:response [lindex $ec 2] "Authentication required"  [dict create WWW-Authenticate {Bearer realm="nsmcp"}]]
                }
                ns_log error "nsmcp: authentication adapter failed"
                return [:response 500 "Internal error"]
            }
            if {$server ni [$context targets]} {
                return [:response 403 "Target not permitted"]
            }
            if {$method ne "POST"} {
                return [:response 405 "Method not allowed" [dict create Allow POST]]
            }
            if {[dict exists $headers mcp-protocol-version] &&
                ![${:protocol} supports [dict get $headers mcp-protocol-version]]} {
                return [:response 400 "Unsupported protocol version"]
            }
            if {![dict exists $headers content-type] ||
                [:mediaTypes [dict get $headers content-type]] ne {application/json}} {
                return [:response 415 "Expected application/json"]
            }
            if {![dict exists $headers accept]} {
                return [:response 406 "Missing Accept header"]
            }
            set accepted [:mediaTypes [dict get $headers accept]]
            if {"application/json" ni $accepted || "text/event-stream" ni $accepted} {
                return [:response 406 "Accept must include application/json and text/event-stream"]
            }
            if {[string length [encoding convertto utf-8 $body]] > ${:maxBodyBytes}} {
                return [:response 413 "Request body too large"]
            }
            if {[dict exists $headers x-nsmcp-question]} {
                set key [dict get $headers x-nsmcp-question]
                if {[catch {
                    :questionPolicy $context $key
                } observer]} {
                    return [:response 403 "Question policy expired, unavailable or credential scope not permitted"]
                }
                if {[catch {
                    ns_json parse -top container -- $body
                } request]} {
                    return [:response 400 "Invalid question request"]
                }
                if {[dict exists $request method] && [dict get $request method] eq "tools/call"} {
                    set limit [ns_config -int ns/server/$server/module/nsmcp questionMaxToolCalls 32]
                    if {$limit<1 || [nsv_incr nsmcp:question-call-count $key]>$limit} {
                        return [:response 403 "Question tool-call budget exceeded"]
                    }
                }
            }
            set response [${:protocol} handle $context $body]
            if {[dict exists $headers x-nsmcp-question] && [nsf::object::exists ::nsmcp::QuestionJobPoller]} {
                set key [dict get $headers x-nsmcp-question]
                if {[regexp {^[0-9a-f]{64}$} $key] && [nsv_exists nsmcp:question-observers $key] && [nsv_get nsmcp:question-observers $key observer]
                    && [dict get $observer principal] eq [$context principal]
                    && [dict get $observer server] eq $server && [dict get $observer deadline]>[clock milliseconds]} {
                    set request [ns_json parse $body]
                    if {[dict exists $request method] && [dict get $request method] eq "tools/call"
                        && [dict get $response status]==200 && [dict get $response body] ne ""} {
                        set root [ns_json parse -output triples -- [dict get $response body]]
                        if {![catch {
                            ns_json triples getvalue -output triples -path result -- $root
                        } result]} {
                            set json [::nsmcp::QuestionJobPoller wait [dict get $request params name]  [ns_json value -- [list {} object $result]] [dict get $observer deadline]  [dict get $observer emit] [list [self] questionPoll $authorization $server $key]]
                            set fields {}
                            foreach {k t v} [lindex $root 2] {
                                if {$k eq "result"} {
                                    set v [lindex [ns_json parse -output triples -- $json] 2]
                                }
                                lappend fields $k $t $v
                            }
                            dict set response body [ns_json value -- [list {} object $fields]]
                        }
                    }
                }
            }
            dict set response headers [dict create Content-Type {application/json; charset=utf-8}]
            ns_log debug "nsmcp: principal=[ns_json value -type string -- [$context principal]] target=[ns_json value -type string -- $server] status=[dict get $response status]"
            return $response
        } finally {
            # Immediate cleanup plus the deallocate hook as a fallback for aborts.
            ::nsmcp::cleanup
        }
    }

    :public method maxBodyBytes {} {
        #
        # Return the configured MCP request-body limit.
        #
        # The server adapter uses this bound before accepting or reading the request
        # body.
        #
        # @return The maximum body size in bytes.
        #

        return ${:maxBodyBytes}
    }
XQL Not present:
Generic, PostgreSQL, Oracle
[ hide source ] | [ make this the default ]
Show another procedure: