Class ::nsmcp::HttpTransport (public)
::nx::Class ::nsmcp::HttpTransport
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
![[i]](/resources/acs-subsite/ZoomIn16.gif)