Class ::nsmcp::HttpTransport

::nsmcp::HttpTransport[i] create ... \
           [ -allowedOrigins (default "") ] \
           [ -authenticator:required authenticator:required ] \
           [ -maxBodyBytes:integer (default "65536") ] \
           [ -protocol:required protocol:required ]

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

Class Relations

  • class: ::nx::Class[i]
  • superclass: ::nsmcp::Object[i]
::nx::Class create ::nsmcp::HttpTransport \
     -superclass ::nsmcp::Object

Methods (to be applied on instances)

  • maxBodyBytes (scripted, public)

     <instance of nsmcp::HttpTransport[i]> maxBodyBytes

    Return the configured MCP request-body limit. The server adapter uses this bound before accepting or reading the request body.

    Returns:
    The maximum body size in bytes.

    Testcases:
    No testcase defined.
    return ${:maxBodyBytes}
  • mediaTypes (scripted, public)

     <instance of nsmcp::HttpTransport[i]> mediaTypes header

    Normalize a comma-separated media-type header. Ignore media-type parameters while retaining the lower-case type names used for negotiation.

    Parameters:
    header (required)
    Comma-separated media-type header value.
    Returns:
    The normalized media-type list.

    Testcases:
    No testcase defined.
    set types {}
    foreach part [split [string tolower $header] ,] {
        lappend types [string trim [lindex [split $part \;] 0]]
    }
    return $types
  • questionPoll (scripted, public)

     <instance of nsmcp::HttpTransport[i]> 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.

    Parameters:
    authorization (required)
    Authorization header supplied by the caller.
    server (required)
    Selected NaviServer virtual-server name.
    key (required)
    Administrative question-poll operation selector.
    handle (required)
    Retained job or conversation handle; ownership and expiry checks still apply.
    Returns:
    The internal HTTP response descriptor.

    Testcases:
    No testcase defined.
    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
    }
  • request (scripted, public)

     <instance of nsmcp::HttpTransport[i]> 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.

    Parameters:
    method (required)
    MCP protocol method or HTTP method, depending on the adapter.
    headers (required)
    HTTP header dictionary using lower-case field names.
    body (required)
    Serialized HTTP or JSON-RPC request body.
    server (required)
    Selected NaviServer virtual-server name.
    Returns:
    The internal HTTP response descriptor.

    Testcases:
    No testcase defined.
    # 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
    }
  • response (scripted, public)

     <instance of nsmcp::HttpTransport[i]> response status body \
        [ headers ]

    Construct an internal HTTP response descriptor. Keep the status, serialized body and header dictionary together for the server adapter.

    Parameters:
    status (required)
    HTTP response status.
    body (required)
    Serialized HTTP or JSON-RPC request body.
    headers (optional)
    HTTP header dictionary using lower-case field names.
    Returns:
    The response dictionary with status, body and headers.

    Testcases:
    No testcase defined.
    return [dict create status $status body $body headers $headers]