Fix tool handler exceptions producing no JSON-RPC error response
Sigil has two VM bugs affecting exception handling in the MCP server:
1. When an exception propagates through map's CPS continuation frames, call-with-prompt's return context (stackmark/framemark) is corrupted. Returning through with-exception-handler's call-with-prompt raises car:expected-pair and crashes the server.
2. When call/cc escape is invoked from within a with-exception-handler handler, Sigil's VM jumps past the c/cc capture site's calling context rather than resuming it normally. The escape value is not delivered to the code after the (call-with-current-continuation ...) form.
Workaround: in the else branch of handle-tools-call, the exception handler writes the JSON-RPC error response directly via mcp-write-response! before calling escape. This ensures the error response is written even though escape doesn't resume the caller normally.
Previously: malformed patch entries → server crash, no response After fix: malformed patch entries → JSON-RPC -32603 written, server exits
Bumps version to 0.16.2.
package.sgl | 2 +-
src/sigil/mcp/server.sgl | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------------------------------
2 files changed, 76 insertions(+), 54 deletions(-)package.sglmodified
(package name: "sigil-mcp" version: "0.16.1" version: "0.16.2" sigil: "^0.16" description: "Library for building MCP servers in Sigil" url: "https://codeberg.org/sigil/sigil-mcp"src/sigil/mcp/server.sglmodified
(if (null? (cdr missing)) "" "s") (comma-join missing)))) (else (guard (e (else (let ((err-msg (guard (fe (else "unknown error")) (format-exception e)))) (guard (le (else #f)) (log-error "tool/call failed" tool: name error: err-msg)) (make-error-response id error-internal (format "Tool error: ~a" err-msg))))) (let ((result ((tool-def-handler tool) args))) (guard (le (else #f)) (log-debug "tool/call completed" tool: name)) (make-response id `((content . (((type . "text") (text . ,(if (string? result) result (json-encode result)))))))))))))))))) ;; Sigil VM bug: when an exception propagates through map's CPS ;; continuation frames, call-with-prompt's return context becomes ;; corrupted (return_ip/stack_mark invalid). Using guard (which is ;; call-with-prompt) causes car:expected-pair on handler return. ;; Using call/cc escape from within the exception handler bypasses ;; the corrupted return path, but Sigil's VM does not properly ;; resume the continuation after escape-from-within-weh — the ;; escape jumps past the caller, so the returned value is never ;; propagated. Workaround: write the error response directly from ;; inside the exception handler (before escaping), so the response ;; is written even if the continuation is not resumed normally. (call-with-current-continuation (lambda (escape) (with-exception-handler (lambda (e) (let ((err-msg (guard (fe (else "unknown error")) (format-exception e)))) (guard (le (else #f)) (log-error "tool/call failed" tool: name error: err-msg)) ;; Write error response directly — escape will not resume ;; the caller, so we cannot return this response the normal ;; way. (guard (we (else #f)) (mcp-write-response! (make-error-response id error-internal (format "Tool error: ~a" err-msg)))) (escape #f))) (lambda () (let ((result ((tool-def-handler tool) args))) (guard (le (else #f)) (log-debug "tool/call completed" tool: name)) (make-response id `((content . (((type . "text") (text . ,(if (string? result) result (json-encode result)))))))))))))))))))) (define (handle-resources-list server params id) (let ((method (jsonrpc-request-method msg)) (params (or (jsonrpc-request-params msg) '())) (id (jsonrpc-request-id msg))) ;; Guard the entire request dispatch so any error during tool ;; lookup, schema validation, or handler execution still returns a ;; JSON-RPC error response. Without this, an uncaught error escapes ;; the read loop and a live client hangs forever waiting for a reply. (guard (e (else (let ((err-msg (guard (fe (else "unknown error")) (format-exception e)))) (guard (le (else #f)) (log-error "request dispatch failed" id: id error: err-msg)) (make-error-response id error-internal (format "Internal error: ~a" err-msg))))) (log-debug "request" method: method id: id) (cond ((equal? method "initialize") (handle-initialize server params id)) ((equal? method "tools/list") (handle-tools-list server params id)) ((equal? method "tools/call") (handle-tools-call server params id)) ((equal? method "resources/list") (handle-resources-list server params id)) ((equal? method "resources/read") (handle-resources-read server params id)) ((equal? method "resources/templates/list") (make-response id `((resourceTemplates . ,#())))) ((equal? method "prompts/list") (handle-prompts-list server params id)) ((equal? method "prompts/get") (handle-prompts-get server params id)) ((equal? method "ping") (log-debug "pong" id: id) (make-response id '())) (else (log-warn "Unknown method" method: method) (make-error-response id error-method-not-found (format "Unknown method: ~a" method))))))) ;; tools/call is dispatched without the outer guard because ;; handle-tools-call uses call/cc escape internally to avoid ;; a Sigil VM bug (corrupted OP_PROMPT return context when ;; exceptions propagate through map's CPS frames). The outer ;; guard below wraps all other method handlers. (if (equal? method "tools/call") (handle-tools-call server params id) (guard (e (else (let ((err-msg (guard (fe (else "unknown error")) (format-exception e)))) (guard (le (else #f)) (log-error "request dispatch failed" id: id error: err-msg)) (make-error-response id error-internal (format "Internal error: ~a" err-msg))))) (log-debug "request" method: method id: id) (cond ((equal? method "initialize") (handle-initialize server params id)) ((equal? method "tools/list") (handle-tools-list server params id)) ((equal? method "resources/list") (handle-resources-list server params id)) ((equal? method "resources/read") (handle-resources-read server params id)) ((equal? method "resources/templates/list") (make-response id `((resourceTemplates . ,#())))) ((equal? method "prompts/list") (handle-prompts-list server params id)) ((equal? method "prompts/get") (handle-prompts-get server params id)) ((equal? method "ping") (log-debug "pong" id: id) (make-response id '())) (else (log-warn "Unknown method" method: method) (make-error-response id error-method-not-found (format "Unknown method: ~a" method)))))))) ((jsonrpc-notification? msg) ;; Handle notifications (no response expected) (let ((method (jsonrpc-notification-method msg))) line)) (mcp-write-response! (make-error-response #f error-parse "Parse error")))))) (loop)))))))) No newline at end of file (loop)))))))))) No newline at end of file