Remove sigil-cli dev-tools modules; sigil-mcp is library-only
The 5 dev-tools modules (tools, nrepl, resources, lint, commands) plus their tests are sigil-cli specific (they expose Sigil's docs, lint, format, test, nREPL eval) and have no consumers outside sigil-cli. Moving them out of this repo makes the library/server boundary clean: bureau/courier/folio/minder/tally and any future MCP server author imports the 3 library modules without dragging along the sigil-cli dev-tools surface.
The dev-tools modules are landing in sigil-cli at (sigil cli mcp ...) in a paired monorepo commit. sigil-cli will pin sigil-mcp ^0.13.1 through from-git as part of that commit.
Top-level (sigil mcp) keeps the library re-exports plus the generic load-tool-module! / load-package-tools! discovery helpers (which let any MCP server dynamically pull in package-provided tools).
Removed dependencies: sigil-args, sigil-ansi, sigil-nrepl (only used by the moved dev-tools modules).
Tests: 62 passed, 2 failed (same 2 baseline failures as pre-split and as monorepo master; pre-existing schema-validation tests).
README.md | 35 ++++---
package.sgl | 3 -
sigil.lock | 15 ---
src/sigil/mcp.sgl | 55 +++--------
src/sigil/mcp/commands.sgl | 89 ------------------
src/sigil/mcp/lint.sgl | 163 --------------------------------
src/sigil/mcp/nrepl.sgl | 137 ---------------------------
src/sigil/mcp/resources.sgl | 369 -------------------------------------------------------------------------
src/sigil/mcp/tools.sgl | 832 --------------------------------------------------------------------------------------------------------------------------------------------------------------------
test/test-nrepl-tools.sgl | 24 -----
test/test-tools.sgl | 65 -------------
11 files changed, 31 insertions(+), 1756 deletions(-)README.mdmodified
## Modules| Module | Purpose ||-------------------------|---------------------------------------------------------------|| `(sigil mcp)` | Top-level `mcp-serve` plus re-exports for the bundled server || `(sigil mcp server)` | Server loop, message routing, handler registration || `(sigil mcp protocol)` | JSON-RPC 2.0 framing and MCP message shapes || `(sigil mcp channel)` | Helpers for channel-based server transports || `(sigil mcp tools)` | Built-in dev-tools (docs, format, test, ...) || `(sigil mcp nrepl)` | Dev-tools backed by an attached nREPL session || `(sigil mcp resources)` | Resource providers (docs, package metadata) || `(sigil mcp lint)` | Code linting surface as an MCP tool || `(sigil mcp commands)` | `sigil mcp` CLI subcommand registration || Module | Purpose ||------------------------|--------------------------------------------------------|| `(sigil mcp)` | Top-level re-exports plus package-tool discovery || `(sigil mcp server)` | Server loop, message routing, handler registration || `(sigil mcp protocol)` | JSON-RPC 2.0 framing and MCP message shapes || `(sigil mcp channel)` | Helpers for channel-based server transports |## Building your own MCP server```scheme(import (sigil mcp server) (sigil mcp protocol))(import (sigil mcp));; Define your tools, register handlers, then run the server loop.```(define server (mcp-server name: "my-server" version: "1.0"))## Running the bundled dev-tools server(mcp-server-register-tool! server name: "echo" description: "Echoes its input back." handler: (lambda (args) ...))(mcp-server-run server)```sigil mcp serve```For a complete example, see sigil-cli's `(sigil cli mcp)` module orprojects like bureau, courier, folio, and minder.## Build and testpackage.sglmodified
dependencies: (list (from-git url: "codeberg:sigil/sigil" package: "sigil-stdlib" version: "^0.13.1") (from-git url: "codeberg:sigil/sigil-json" version: "^0.13.1") (from-git url: "codeberg:sigil/sigil" package: "sigil-args" version: "^0.13.1") (from-git url: "codeberg:sigil/sigil" package: "sigil-ansi" version: "^0.13.1") (from-git url: "codeberg:sigil/sigil" package: "sigil-nrepl" version: "^0.13.1") (from-git url: "codeberg:sigil/sigil-log" version: "^0.13.0")))sigil.lockmodified
ref: "^0.13.1" sha: "811b63925399a663e4b7dd25fb0813d59d33f557" version: "0.13.1") (package name: "sigil-args" url: "codeberg:sigil/sigil" ref: "^0.13.1" sha: "1aea1cecd3487bf07d071da2c1e7eb3cf4bbdb21" package-selector: "sigil-args") (package name: "sigil-ansi" url: "codeberg:sigil/sigil" ref: "^0.13.1" sha: "1aea1cecd3487bf07d071da2c1e7eb3cf4bbdb21" package-selector: "sigil-ansi") (package name: "sigil-nrepl" url: "codeberg:sigil/sigil" ref: "^0.13.1" sha: "1aea1cecd3487bf07d071da2c1e7eb3cf4bbdb21" package-selector: "sigil-nrepl") (package name: "sigil-log" url: "codeberg:sigil/sigil-log" ref: "^0.13.0"src/sigil/mcp.sglmodified
;;; (sigil mcp) - Model Context Protocol server for Sigil.;;; (sigil mcp) - Library for building Model Context Protocol servers in Sigil.;;;;;; Enables AI agents to write idiomatic Sigil code by providing;;; documentation, procedure discovery, and development tools.;;; Re-exports the server, protocol, and channel primitives plus;;; package-tool discovery helpers for assembling MCP servers.;;;;;; ```;;; (import (sigil mcp));;;;;; ;; Start the MCP server (reads from stdin, writes to stdout);;; (mcp-serve);;; (define server (mcp-server name: "my-server" version: "1.0"));;; (mcp-server-register-tool! server ...);;; (mcp-server-run server);;; ```(define-library (sigil mcp) (sigil mcp protocol) (sigil mcp server) (sigil mcp channel) (sigil mcp tools) (sigil mcp nrepl) (sigil mcp resources) (sigil package) (sigil meta)) (export mcp-serve mcp-server (export mcp-server mcp-server? mcp-server-run ;; Re-export for custom setups ;; Server primitives for custom setups mcp-server-initialized mcp-server-register-tool! mcp-server-register-resource! ;; Channel server helpers mcp-channel-server channel-notify! channel-wait-initialized!) channel-wait-initialized! ;; Package-tool discovery for dynamic loading load-tool-module! load-package-tools!) (begin ;; ============================================================ (let ((module-name (package-provides-ref (cdr pair) mcp-tools:))) (when module-name (load-tool-module! server module-name)))) packages)))))) ;; ============================================================ ;; Main Entry Point ;; ============================================================ ;;; Start the MCP server. ;;; ;;; Reads JSON-RPC messages from stdin, writes responses to stdout. ;;; Resource resolution is handled automatically by (sigil resources), ;;; checking project build output, bundled archives, and filesystem. (define (mcp-serve) (let ((server (mcp-server name: "sigil" version: (sigil-version)))) ;; Register core tools (register-tools! server mcp-server-register-tool!) ;; Register nREPL tools (proxy to running app for live development) (register-nrepl-tools! server mcp-server-register-tool!) ;; Discover and load package-provided tools (load-package-tools! server) ;; Register resources (register-resources! server mcp-server-register-resource!) ;; Run the server loop (mcp-server-run server))))) packages))))))))src/sigil/mcp/commands.sgldeleted
;;; (sigil mcp commands) - CLI command for MCP server.;;;;;; Provides the `sigil mcp` command for starting the MCP server.(define-library (sigil mcp commands) (import (sigil mcp) (sigil args) (sigil io) (sigil log) (sigil process) (sigil string) (sigil ansi)) (export mcp-cmd) (begin ;;; Handler for 'sigil mcp serve' - start the MCP server (define (mcp-serve-handler opts args) ;; Configure logging from --log / --log-level before starting. ;; log-configure-from-args! reads directly from (command-line), ;; so the flags just need to be on the CLI — they're declared as ;; options on mcp-cmd so sigil-args doesn't reject them. (log-configure-from-args!) ;; Start the server (blocks until stdin closes) (mcp-serve)) ;;; Handler for 'sigil mcp' without subcommand (define (mcp-handler opts args) (if (null? args) (begin (println "") (println "~a sigil mcp <command>" (a:bold "Usage:")) (println "") (println "~a" (a:bold "Commands:")) (println " serve Start the MCP server (stdio transport)") (println "") (println "~a" (a:bold "Options for serve:")) (println " --log PATH Append logs to PATH (default: stderr)") (println " --log-level LEVEL trace|debug|info|warn|error|fatal") (println "") (println "~a" (a:bold "Example:")) (println " sigil mcp serve") (println " sigil mcp serve --log /tmp/sigil-mcp.log --log-level trace") (println "") (println "The MCP server enables AI agents to write idiomatic Sigil code") (println "by providing documentation, procedure discovery, and dev tools.") (println "") (println "Resource resolution is automatic:") (println " 1. Project build output (if package.sgl found)") (println " 2. Bundled archive (if running from bundle)") (println " 3. Filesystem (library paths)") (println "")) ;; Subcommand provided (let ((subcmd (car args))) (cond ((string=? subcmd "serve") (mcp-serve-handler opts (cdr args))) (else (eprintln "~a Unknown mcp command: ~a" (a:red "Error:") subcmd) (eprintln "Run 'sigil mcp' for available commands.") (exit 1)))))) (define log-opt (option name: 'log long: "log" value: "PATH" description: "Append logs to PATH (default: stderr)")) (define log-level-opt (option name: 'log-level long: "log-level" value: "LEVEL" description: "Log level: trace|debug|info|warn|error|fatal")) ;;; MCP command definition (define mcp-cmd (command name: "mcp" description: "Start MCP server for AI-assisted developmentThe Model Context Protocol (MCP) server enables AI agents likeClaude to write idiomatic Sigil code by providing documentation,procedure discovery, and development tools.Subcommands: serve Start the server (reads JSON-RPC from stdin)" options: (list log-opt log-level-opt) handler: mcp-handler))))src/sigil/mcp/lint.sgldeleted
;;; (sigil mcp lint) - Code linting for Sigil.;;;;;; Provides lint checks for Sigil source code. Currently supports:;;; - unused-imports: Detect imported modules whose exports are not referenced.(define-library (sigil mcp lint) (import (sigil core) (sigil string) (sigil io) (sigil struct) (sigil docs search)) (export lint-code lint-warning lint-warning? lint-warning-check lint-warning-severity lint-warning-message) (begin (define-struct lint-warning (check) (severity) (message)) ;; ============================================================ ;; Main Entry Point ;; ============================================================ ;;; Run lint checks on source code. ;;; Returns a list of lint-warning records. (define (lint-code code checks) (let ((run-all? (or (null? checks) (member "all" checks)))) (let ((warnings '())) (when (or run-all? (member "unused-imports" checks)) (set! warnings (append warnings (check-unused-imports code)))) warnings))) ;; ============================================================ ;; Unused Imports Check ;; ============================================================ ;; Check for imported modules whose exports are never referenced. (define (check-unused-imports code) (guard (e (else '())) (let* ((forms (read-all-forms code)) (imports (extract-imports forms)) (used-symbols (collect-used-symbols forms))) (filter-map (lambda (mod-name) (let ((exports (get-module-export-names mod-name))) (if (or (null? exports) (any-symbol-used? exports used-symbols)) #f (lint-warning check: "unused-imports" severity: "warning" message: (format "Unused import: ~a" mod-name))))) imports)))) ;; Read all top-level forms from source code (define (read-all-forms code) (let ((port (open-input-string code))) (let loop ((forms '())) (let ((form (read port))) (if (eof-object? form) (reverse forms) (loop (cons form forms))))))) ;; Extract module names from import forms (define (extract-imports forms) (apply append (map extract-imports-from-form forms))) ;; Extract import module names from a single form (define (extract-imports-from-form form) (cond ;; Top-level (import ...) ((and (pair? form) (eq? (car form) 'import)) (map import-set->module-name (cdr form))) ;; (define-library ... (import ...)) ((and (pair? form) (eq? (car form) 'define-library)) (let ((import-clause (find-clause 'import (cddr form)))) (if import-clause (map import-set->module-name (cdr import-clause)) '()))) (else '()))) ;; Convert an import set to a module name string. ;; Handles plain module names and prefix/only/except/rename wrappers. (define (import-set->module-name import-set) (cond ((and (pair? import-set) (memq (car import-set) '(only except prefix rename))) (import-set->module-name (cadr import-set))) ((pair? import-set) (format "~a" import-set)) (else (format "~a" import-set)))) ;; Find a named clause in a define-library body (define (find-clause name clauses) (find (lambda (c) (and (pair? c) (eq? (car c) name))) clauses)) ;; Collect all symbols referenced in non-import forms as a list (define (collect-used-symbols forms) (let ((symbols '())) (for-each (lambda (form) (unless (and (pair? form) (memq (car form) '(import define-library))) (set! symbols (walk-collect-symbols form symbols)))) forms) symbols)) ;; Walk a form tree collecting symbol references into a list (define (walk-collect-symbols form acc) (cond ((symbol? form) (if (memq form acc) acc (cons form acc))) ((pair? form) (case (car form) ;; Skip quoted data ((quote) acc) ;; For quasiquote, only collect from unquoted parts ((quasiquote) (walk-quasiquote (cadr form) acc)) (else (fold-left (lambda (a sub) (walk-collect-symbols sub a)) acc form)))) (else acc))) ;; Walk quasiquote, only collecting symbols from unquoted parts (define (walk-quasiquote form acc) (cond ((and (pair? form) (or (eq? (car form) 'unquote) (eq? (car form) 'unquote-splicing))) (walk-collect-symbols (cadr form) acc)) ((pair? form) (walk-quasiquote (cdr form) (walk-quasiquote (car form) acc))) (else acc))) ;; Get export symbol names for a module (define (get-module-export-names module-name-str) (let ((results (get-module-exports module-name-str))) (map (lambda (r) (string->symbol (search-result-name r))) results))) ;; Check if any symbol from a list appears in the used-symbols list (define (any-symbol-used? export-syms used-symbols) (any (lambda (sym) (memq sym used-symbols)) export-syms)) ;; Check if any element in list satisfies predicate (define (any pred lst) (cond ((null? lst) #f) ((pred (car lst)) #t) (else (any pred (cdr lst)))))))src/sigil/mcp/nrepl.sgldeleted
;;; (sigil mcp nrepl) - MCP tools for nREPL interaction;;;;;; Provides MCP tools that proxy to a running Sigil app's nREPL server.;;; Enables AI agents to connect, evaluate code, and inspect module state;;; through the MCP interface.(define-library (sigil mcp nrepl) (import (sigil core) (sigil string) (sigil json) (sigil nrepl client)) (export register-nrepl-tools!) (begin ;; ============================================================ ;; Tool Schemas ;; ============================================================ (define nrepl-connect-schema '((type . "object") (properties . ((host . ((type . "string") (description . "Host to connect to (default: 127.0.0.1)"))) (port . ((type . "integer") (description . "nREPL port to connect to"))))) (required . ("port")))) (define nrepl-eval-schema '((type . "object") (properties . ((code . ((type . "string") (description . "Sigil code to evaluate in the running app"))) (module . ((type . "string") (description . "Module context (e.g. \"(sigil web demo views)\")"))))) (required . ("code")))) (define nrepl-status-schema '((type . "object"))) (define nrepl-disconnect-schema '((type . "object"))) ;; ============================================================ ;; Tool Handlers ;; ============================================================ (define (tool-nrepl-connect args) (let ((host (or (dict-ref args host: #f) "127.0.0.1")) (port (dict-ref args port: #f)) (existing (nrepl-lookup 'mcp))) (when (and existing (nrepl-connected? existing)) (nrepl-disconnect existing) (nrepl-unregister! 'mcp)) (let ((conn (nrepl-connect host port))) (if conn (begin (nrepl-register! 'mcp conn) (format "Connected to nREPL at ~a:~a" host port)) (format "Failed to connect to nREPL at ~a:~a" host port))))) (define (tool-nrepl-eval args) (let ((conn (nrepl-lookup 'mcp))) (if (not (and conn (nrepl-connected? conn))) "Not connected to nREPL. Use sigil/nrepl-connect first." (let ((code (dict-ref args code: "")) (module (dict-ref args module: #f))) (let ((resp (if module (nrepl-eval conn code module) (nrepl-eval conn code)))) (if resp (let ((status (assoc-ref 'status resp)) (value (assoc-ref 'value resp)) (message (assoc-ref 'message resp)) (mod (assoc-ref 'module resp))) (cond ((eq? status 'ok) (if mod (format "~a\n[module: ~a]" (or value "ok") mod) (or value "ok"))) ((eq? status 'error) (format "Error: ~a" (or message value "unknown error"))) (else (format "~a" (or value "ok"))))) "Error: No response from nREPL (connection may be lost)")))))) (define (tool-nrepl-status args) (let ((conn (nrepl-lookup 'mcp))) (if (not conn) "Not connected" (if (not (nrepl-connected? conn)) "Connection lost" (let ((resp (nrepl-eval conn "(values)"))) (if resp (let ((mod (assoc-ref 'module resp))) (format "Connected\nModule: ~a" (or mod "unknown"))) "Connected (no response to ping)")))))) (define (tool-nrepl-disconnect args) (let ((conn (nrepl-lookup 'mcp))) (if (not conn) "Not connected" (begin (nrepl-disconnect conn) (nrepl-unregister! 'mcp) "Disconnected")))) ;; ============================================================ ;; Registration ;; ============================================================ ;;; Register nREPL tools with an MCP server. (define (register-nrepl-tools! server register-tool!) (register-tool! server "sigil/nrepl-connect" "Connect to a running Sigil app's nREPL server for live development" nrepl-connect-schema tool-nrepl-connect) (register-tool! server "sigil/nrepl-eval" "Evaluate Sigil code in the running app (redefine handlers, views, etc.)" nrepl-eval-schema tool-nrepl-eval) (register-tool! server "sigil/nrepl-status" "Check nREPL connection status and current module" nrepl-status-schema tool-nrepl-status) (register-tool! server "sigil/nrepl-disconnect" "Disconnect from the nREPL server" nrepl-disconnect-schema tool-nrepl-disconnect)) ))src/sigil/mcp/resources.sgldeleted
;;; (sigil mcp resources) - MCP resource providers.;;;;;; Provides resource handlers for serving Sigil documentation:;;; sigil://index, sigil://language/*, sigil://style/*, sigil://stdlib/*.;;;;;; Uses (sigil resources) for unified file resolution across project,;;; bundle, and filesystem sources.(define-library (sigil mcp resources) (import (sigil string) (sigil json) (sigil io) (sigil fs) (sigil path) (sigil resources)) (export register-resources! ;; Project resource handlers (for testing) handle-project-overview handle-project-modules) (begin ;; ============================================================ ;; Resource Handlers ;; ============================================================ ;;; Read the main documentation index. (define (handle-index uri) (or (read-resource 'docs "reference/index.md") "# Sigil Documentation\n\nDocumentation index not found.")) ;;; Read a language reference document. (define (handle-language uri) ;; URI: sigil://language/syntax -> docs/reference/language/syntax.md (let* ((name (uri-last-segment uri)) (path (string-append "reference/language/" name ".md"))) (or (read-resource 'docs path) (format "# ~a\n\nDocumentation not found." name)))) ;;; Read a style guide document. (define (handle-style uri) ;; URI: sigil://style/naming -> docs/reference/style/naming.md (let* ((name (uri-last-segment uri)) (path (string-append "reference/style/" name ".md"))) (or (read-resource 'docs path) (format "# ~a\n\nDocumentation not found." name)))) ;;; Read the stdlib module index. (define (handle-stdlib-index uri) (let ((content (read-resource 'lib "index.json"))) (if content (let ((json (json-decode content))) (format-stdlib-index json)) "# Standard Library\n\nModule index not found."))) ;;; Read documentation for a specific stdlib module. (define (handle-stdlib-module uri) ;; URI: sigil://stdlib/sigil/string -> lib/sigil/string.json (let* ((module-path (extract-module-path uri)) (json-path (string-append module-path ".json")) (content (read-resource 'lib json-path))) (if content (let ((json (json-decode content))) (format-module-docs json)) (format "# ~a\n\nModule documentation not found." module-path)))) ;; ============================================================ ;; URI Helpers ;; ============================================================ ;; Extract the last segment from a URI ;; sigil://language/syntax -> syntax (define (uri-last-segment uri) (let ((parts (string-split uri "/"))) (if (null? parts) "" (last parts)))) ;; Extract module path from stdlib URI ;; sigil://stdlib/sigil/string -> sigil/string (define (extract-module-path uri) ;; Remove sigil://stdlib/ prefix (let ((prefix "sigil://stdlib/")) (if (string-starts-with? uri prefix) (substring uri (string-length prefix) (string-length uri)) uri))) (define (last lst) (if (null? (cdr lst)) (car lst) (last (cdr lst)))) ;; ============================================================ ;; Formatting Helpers ;; ============================================================ ;;; Format the stdlib index JSON as markdown. (define (format-stdlib-index json) (let ((modules (or (dict-ref json modules: #f) '()))) (string-append "# Standard Library Modules\n\n" (string-join (map (lambda (m) (let ((name (or (dict-ref m name: #f) "unknown")) (desc (or (dict-ref m description: #f) ""))) (format "- **~a** - ~a" name (first-line desc)))) modules) "\n")))) ;;; Format module documentation JSON as markdown. (define (format-module-docs json) (let ((module-name (or (dict-ref json module: #f) "Unknown")) (description (or (dict-ref json description: #f) "")) (exports (or (dict-ref json exports: #f) '()))) (string-append (format "# ~a\n\n" module-name) (if (string-empty? description) "" (string-append description "\n\n")) "## Exports\n\n" (string-join (map format-export exports) "\n\n")))) ;;; Format a single export as markdown. (define (format-export exp) (let ((name (or (dict-ref exp name: #f) "unknown")) (kind (or (dict-ref exp kind: #f) "procedure")) (desc (or (dict-ref exp description: #f) ""))) (format "### ~a\n\n~a~a" name (if (equal? kind "syntax") "*syntax*\n\n" "") (if (string-empty? desc) "(No documentation)" desc)))) ;;; Get the first line of a string. (define (first-line str) (let ((lines (string-split str "\n"))) (if (null? lines) "" (car lines)))) ;; ============================================================ ;; Project Resources ;; ============================================================ ;;; Handle project overview resource. ;;; Reads package.sgl and generates a project summary. (define (handle-project-overview uri) (let ((pkg-file (find-package-file))) (if pkg-file (format-project-overview (read-package-sgl pkg-file)) "# Project Overview\n\nNo package.sgl found in current directory."))) ;;; Handle project modules resource. ;;; Lists all Sigil modules in the project. (define (handle-project-modules uri) (let ((pkg-file (find-package-file))) (if pkg-file (format-project-modules (path-dirname pkg-file)) "# Project Modules\n\nNo package.sgl found in current directory."))) ;; Find package.sgl in current directory or parents (define (find-package-file) (let ((cwd (current-directory))) (find-package-file-up cwd))) (define (find-package-file-up dir) (let ((pkg (path-join dir "package.sgl"))) (cond ((file-exists? pkg) pkg) ((string=? dir "/") #f) (else (find-package-file-up (path-dirname dir)))))) ;; Read and parse package.sgl (basic s-expression reading) (define (read-package-sgl path) (guard (e (else #f)) (let ((port (open-input-file path))) (let ((forms (read-all-forms port))) (close-port port) forms)))) (define (read-all-forms port) (let loop ((forms '())) (let ((form (read port))) (if (eof-object? form) (reverse forms) (loop (cons form forms)))))) ;; Format project overview from package.sgl forms (define (format-project-overview forms) (if (not forms) "# Project Overview\n\nError reading package.sgl." (let* ((pkg-form (find-form 'package forms)) (ws-form (find-form 'workspace forms)) (name (or (get-prop 'name ws-form) (get-prop 'name pkg-form))) (desc (or (get-prop 'description ws-form) (get-prop 'description pkg-form))) (deps (get-prop 'dependencies pkg-form)) (packages-raw (get-prop 'packages ws-form)) ;; Handle (list "pkg1" "pkg2") form - skip the 'list symbol (packages (if (and packages-raw (pair? packages-raw) (eq? (car packages-raw) 'list)) (cdr packages-raw) packages-raw))) (string-append "# Project Overview\n\n" (if name (format "**Name:** ~a\n\n" name) "") (if desc (format "**Description:** ~a\n\n" desc) "") (if packages (format "## Workspace Packages\n\n~a\n\n" (string-join (map (lambda (p) (format "- ~a" p)) (filter string? packages)) "\n")) "") (if deps (format "## Dependencies\n\n~a\n" (string-join (map (lambda (d) (format "- ~s" d)) deps) "\n")) ""))))) ;; Format project modules listing (define (format-project-modules project-dir) (let ((modules (find-project-modules project-dir))) (string-append "# Project Modules\n\n" (if (null? modules) "No modules found." (string-join (map (lambda (m) (format "- **~a** (`~a`)" (module-name-from-path m project-dir) m)) modules) "\n"))))) ;; Find all .sgl files in src/ directories (define (find-project-modules project-dir) (let ((patterns (list (path-join project-dir "src/**/*.sgl") (path-join project-dir "packages/*/src/**/*.sgl")))) (apply append (map (lambda (pat) (guard (e (else '())) (glob pat))) patterns)))) ;; Convert file path to module name ;; packages/sigil-foo/src/sigil/bar.sgl -> (sigil bar) ;; src/my-app/main.sgl -> (my-app main) (define (module-name-from-path path project-dir) ;; Make path relative to project (let* ((rel (if (string-starts-with? path project-dir) (substring path (string-length project-dir) (string-length path)) path)) (rel (string-trim-left rel "/"))) ;; Extract module path from src/ directory (let ((src-idx (string-find rel "/src/"))) (if src-idx ;; Path like packages/sigil-foo/src/sigil/bar.sgl ;; Take everything after /src/ (let* ((after-src (substring rel (+ src-idx 5) (string-length rel))) (without-ext (string-replace after-src ".sgl" "")) (parts (string-split without-ext "/"))) (format "(~a)" (string-join parts " "))) ;; Path like src/my-app/main.sgl (let* ((without-ext (string-replace rel ".sgl" "")) (parts (string-split without-ext "/")) ;; Skip "src" if present (parts (if (and (pair? parts) (string=? (car parts) "src")) (cdr parts) parts))) (format "(~a)" (string-join parts " "))))))) ;; Helper to find a form by car (define (find-form name forms) (find (lambda (f) (and (pair? f) (eq? (car f) name))) forms)) ;; Helper to get property from form like (package name: "foo" ...) (define (get-prop key form) (if (not form) #f (let loop ((rest (cdr form))) (cond ((null? rest) #f) ((and (keyword? (car rest)) (eq? (keyword->symbol (car rest)) key) (pair? (cdr rest))) (cadr rest)) (else (loop (cdr rest))))))) ;; Check if x is a keyword (define (keyword? x) (and (symbol? x) (let ((s (symbol->string x))) (and (> (string-length s) 0) (char=? (string-ref s (- (string-length s) 1)) #\:))))) ;; Convert keyword to symbol (define (keyword->symbol kw) (let ((s (symbol->string kw))) (string->symbol (substring s 0 (- (string-length s) 1))))) ;; Find first element matching predicate (define (find pred lst) (cond ((null? lst) #f) ((pred (car lst)) (car lst)) (else (find pred (cdr lst))))) ;; Trim leading character from string (define (string-trim-left str char) (if (and (> (string-length str) 0) (char=? (string-ref str 0) (if (char? char) char (string-ref char 0)))) (string-trim-left (substring str 1 (string-length str)) char) str)) ;; ============================================================ ;; Registration ;; ============================================================ ;;; Register all resources with an MCP server. (define (register-resources! server register-resource!) ;; Main index (register-resource! server "sigil://index" "Documentation Index" "Entry point for Sigil documentation" handle-index) ;; Language reference (wildcard) (register-resource! server "sigil://language/*" "Language Reference" "Sigil language syntax and semantics" handle-language) ;; Style guide (wildcard) (register-resource! server "sigil://style/*" "Style Guide" "Code style conventions and best practices" handle-style) ;; Stdlib index (register-resource! server "sigil://stdlib/index" "Standard Library Index" "Overview of all standard library modules" handle-stdlib-index) ;; Stdlib modules (wildcard) (register-resource! server "sigil://stdlib/*" "Standard Library Module" "Documentation for a standard library module" handle-stdlib-module) ;; Project overview (register-resource! server "sigil://project/overview" "Project Overview" "Current project structure, dependencies, and configuration" handle-project-overview) ;; Project modules (register-resource! server "sigil://project/modules" "Project Modules" "List of modules in the current project with file paths" handle-project-modules))))src/sigil/mcp/tools.sgldeleted
;;; (sigil mcp tools) - MCP tool implementations.;;;;;; Provides the core tools for the Sigil MCP server:;;; sigil/eval, sigil/format, sigil/lookup, sigil/search, sigil/exports,;;; sigil/suggest-import, sigil/project-info, sigil/run-file, sigil/lint,;;; sigil/reload.(define-library (sigil mcp tools) (import (sigil json) (sigil string) (sigil io) (sigil fs) (sigil path) (sigil format) (sigil inspect) (sigil error) (sigil diagnostic) (sigil spec) (sigil docs search) (sigil docs lookup) (sigil docs types) (sigil docs packages) (sigil package) (sigil process) (sigil mcp lint)) (export register-tools! ;; Tool schemas for reference eval-tool-schema format-tool-schema lookup-tool-schema search-tool-schema exports-tool-schema check-tool-schema test-tool-schema list-package-docs-tool-schema read-package-doc-tool-schema suggest-import-tool-schema project-info-tool-schema run-file-tool-schema lint-tool-schema reload-tool-schema ;; Tool handlers (for testing/custom use) tool-eval tool-format tool-lookup tool-search tool-exports tool-check tool-test tool-list-package-docs tool-read-package-doc tool-suggest-import tool-project-info tool-run-file tool-lint tool-reload) (begin ;; ============================================================ ;; Tool Schemas ;; ============================================================ (define eval-tool-schema '((type . "object") (properties . ((expression . ((type . "string") (description . "Sigil expression to evaluate"))) (module . ((type . "string") (description . "Optional module context for imports"))))) (required . ("expression")))) (define format-tool-schema '((type . "object") (properties . ((code . ((type . "string") (description . "Sigil code to format"))) (file . ((type . "string") (description . "Path to file to format (alternative to code)"))))))) (define lookup-tool-schema '((type . "object") (properties . ((name . ((type . "string") (description . "Name to look up (procedure, syntax, or module)"))))) (required . ("name")))) (define search-tool-schema '((type . "object") (properties . ((query . ((type . "string") (description . "Search query (name pattern or keywords)"))) (module . ((type . "string") (description . "Limit search to specific module"))) (type . ((type . "string") (enum . ("procedure" "syntax" "variable" "all")) (description . "Type of binding to search for"))))) (required . ("query")))) (define exports-tool-schema '((type . "object") (properties . ((module . ((type . "string") (description . "Module name, e.g. '(sigil json)'"))))) (required . ("module")))) (define check-tool-schema '((type . "object") (properties . ((code . ((type . "string") (description . "Sigil code to check for syntax errors"))) (file . ((type . "string") (description . "Path to file to check (alternative to code)"))))) (required . ()))) (define test-tool-schema '((type . "object") (properties . ((pattern . ((type . "string") (description . "Test name pattern to match (optional)"))) (file . ((type . "string") (description . "Specific test file to run (optional)"))))) (required . ()))) (define list-package-docs-tool-schema '((type . "object") (properties . ((package . ((type . "string") (description . "Filter to specific package (optional)"))))) (required . ()))) (define read-package-doc-tool-schema '((type . "object") (properties . ((package . ((type . "string") (description . "Package name"))) (file . ((type . "string") (description . "Documentation file name (e.g., 'routing.md')"))))) (required . ("package" "file")))) (define suggest-import-tool-schema '((type . "object") (properties . ((symbol . ((type . "string") (description . "Symbol name to find the module for"))))) (required . ("symbol")))) (define project-info-tool-schema '((type . "object") (properties . ((dir . ((type . "string") (description . "Project directory (defaults to current directory)"))))) (required . ()))) (define run-file-tool-schema '((type . "object") (properties . ((file . ((type . "string") (description . "Path to Sigil file to run"))) (args . ((type . "array") (items . ((type . "string"))) (description . "Arguments to pass to the script"))))) (required . ("file")))) (define lint-tool-schema '((type . "object") (properties . ((code . ((type . "string") (description . "Sigil code to lint"))) (file . ((type . "string") (description . "Path to file to lint (alternative to code)"))) (checks . ((type . "array") (items . ((type . "string"))) (description . "Checks to run (default: all). Available: unused-imports"))))) (required . ()))) (define reload-tool-schema '((type . "object") (properties . ((module . ((type . "string") (description . "Module name to reload, e.g. '(sigil json)'"))))) (required . ("module")))) ;; ============================================================ ;; Tool Implementations ;; ============================================================ ;;; Evaluate a Sigil expression and return the result. (define (tool-eval args) (let ((expr (dict-ref args expression: #f)) (module-str (dict-ref args module: #f))) (guard (e (else (json-encode (inspect-error e)))) (let ((result (if module-str (eval-string-in-module (string-append "(import " module-str ") " expr) (read (open-input-string module-str))) (eval-string expr)))) (json-encode (inspect result)))))) ;;; Format Sigil source code and fix paren issues. (define (tool-format args) (let ((code (dict-ref args code: #f)) (file (dict-ref args file: #f))) (guard (e (else (format "Format error: ~a" (exception-message e)))) (let ((result (cond (file (format-file file)) (code (format-string code "<input>")) (else (error "Provide 'code' or 'file'"))))) (let ((output (format-result-output result)) (success (format-result-success result))) ;; Auto-fix: write output back to file when fixes are available (when (and file output success) (call-with-output-file file (lambda (port) (display output port)))) (string-append (format-result->string result) (if output (if file (format "\nFixed file: ~a" file) (string-append "\nFormatted output:\n" output)) ""))))))) ;;; Look up documentation for a name. (define (tool-lookup args) (let ((name (dict-ref args name: #f))) (guard (e (else (format "Lookup error: ~a" (exception-message e)))) (let ((sym (string->symbol name))) ;; Try to find the symbol in the index (let ((results (search-docs name))) (if (null? results) (format "No documentation found for '~a'." name) ;; Get detailed docs for first exact match (let* ((result (find-exact-match name results)) (mod-name (search-result-module result))) (if mod-name (let ((details (load-module-details (read-module-name mod-name)))) (if details (format-export-doc name details) (format-search-result result))) (format-search-result result))))))))) ;; Find exact name match in results (define (find-exact-match name results) (or (find (lambda (r) (string=? (search-result-name r) name)) results) (car results))) ;; Parse module name string to list (define (read-module-name str) (if (string? str) (read (open-input-string str)) str)) ;; Format a search result as output (define (format-search-result result) (format "~a ~a\n ~a: ~a" (search-result-name result) (search-result-module result) (search-result-kind result) (search-result-summary result))) ;; Try to get doc from runtime procedure metadata (define (runtime-doc-fallback module-name-str export-name) (guard (e (else #f)) (let* ((mod-name (read (open-input-string module-name-str))) (mod (find-module mod-name))) (if (not mod) #f (let ((val (module-ref mod (string->symbol export-name)))) (if (not (procedure? val)) #f (let ((meta (procedure-metadata val))) (if (not meta) #f (let ((doc-pair (assq 'doc meta))) (if doc-pair (cdr doc-pair) #f)))))))))) ;; Try to get spec from runtime procedure metadata (define (runtime-spec-fallback module-name-str export-name) (guard (e (else #f)) (let* ((mod-name (read (open-input-string module-name-str))) (mod (find-module mod-name))) (if (not mod) #f (let ((val (module-ref mod (string->symbol export-name)))) (if (not (procedure? val)) #f (spec->string (procedure-spec val)))))))) ;; Look up docs from the source module's JSON (define (source-module-doc name source-mod-str) (guard (e (else #f)) (let* ((mod-name (read (open-input-string source-mod-str))) (details (load-module-details mod-name))) (if (not details) #f (let ((exp (find (lambda (e) (string=? (symbol->string (export-details-name e)) name)) (module-details-exports details)))) (if exp (export-details-description exp) #f)))))) ;; Format detailed export documentation (define (format-export-doc name details) (let ((export (find (lambda (e) (string=? (symbol->string (export-details-name e)) name)) (module-details-exports details)))) (if export (let* ((mod-name-str (format "~a" (module-details-name details))) (desc (or (export-details-description export) (and (export-details-source-module export) (source-module-doc name (export-details-source-module export))) (runtime-doc-fallback mod-name-str name) "No description available.")) (spec-str (runtime-spec-fallback mod-name-str name))) (if spec-str (format "~a ~a\nSpec: ~a\n\n~a" name mod-name-str spec-str desc) (format "~a ~a\n\n~a" name mod-name-str desc))) (format "Export '~a' not found in module." name)))) ;;; Search for procedures by name or description. (define (tool-search args) (let ((query (dict-ref args query: #f)) (module-filter (dict-ref args module: #f)) (type-filter (dict-ref args type: #f))) (guard (e (else (format "Search error: ~a" (exception-message e)))) (let* ((results (search-docs query)) (filtered (filter-results results module-filter type-filter))) (if (null? filtered) (format "No results found for '~a'." query) (format-search-results filtered)))))) ;; Filter results by module and type (define (filter-results results module-filter type-filter) (filter (lambda (r) (and (or (not module-filter) (string=? (search-result-module r) module-filter)) (or (not type-filter) (string=? type-filter "all") (string=? (search-result-kind r) type-filter)))) results)) ;; Format multiple search results (define (format-search-results results) (string-join (map (lambda (r) (let ((spec-str (runtime-spec-fallback (search-result-module r) (search-result-name r)))) (if spec-str (format "~a ~a (~a)\n Spec: ~a\n ~a" (search-result-name r) (search-result-module r) (search-result-kind r) spec-str (search-result-summary r)) (format "~a ~a (~a)\n ~a" (search-result-name r) (search-result-module r) (search-result-kind r) (search-result-summary r))))) results) "\n\n")) ;;; List exports from a module. (define (tool-exports args) (let ((module-str (dict-ref args module: #f))) (guard (e (else (format "Exports error: ~a" (exception-message e)))) (let ((exports (get-module-exports module-str))) (if (null? exports) (format "No exports found for module ~a." module-str) (format-exports-list module-str exports)))))) ;; Format exports list (define (format-exports-list module-str exports) (string-append (format "Exports from ~a:\n\n" module-str) (string-join (map (lambda (r) (let ((spec-str (runtime-spec-fallback module-str (search-result-name r)))) (if spec-str (format " ~a (~a)\n Spec: ~a\n ~a" (search-result-name r) (search-result-kind r) spec-str (search-result-summary r)) (format " ~a (~a) - ~a" (search-result-name r) (search-result-kind r) (search-result-summary r))))) exports) "\n"))) ;;; Check Sigil code for syntax errors. (define (tool-check args) (let ((code (dict-ref args code: #f)) (file (dict-ref args file: #f))) (guard (e (else (format "Syntax error: ~a" (exception-message e)))) (cond ;; Check code string (code (check-code-string code)) ;; Check file (file (if (file-exists? file) (check-code-string (read-file-string file)) (format "File not found: ~a" file))) (else "Error: Either 'code' or 'file' must be provided."))))) ;; Check code string for syntax errors ;; Note: Read/parse errors are hard VM errors, not catchable exceptions. ;; When called via MCP, these will be returned as JSON-RPC error responses. (define (check-code-string code) (guard (e (else (format "Syntax error: ~a" (exception-message e)))) ;; Try to read all expressions - catches runtime errors but not parse errors (let ((port (open-input-string code))) (let loop ((count 0)) (let ((expr (read port))) (if (eof-object? expr) (format "OK: ~a expression~a parsed successfully." count (if (= count 1) "" "s")) (loop (+ count 1)))))))) ;;; Run Sigil tests. ;;; Note: This tool provides basic test running capability. ;;; For full test functionality, use the CLI `sigil test` command. ;; Run `sigil test` as a subprocess. Loading test files directly into ;; the MCP server process proved fragile: a single test with an infinite ;; loop, a blocking I/O call, or an import that tries to initialize heavy ;; state would lock up the server for every client. Delegating to the ;; `sigil test` CLI keeps the test runner in its own process — if it ;; hangs or crashes, the MCP server stays responsive for the next call. (define (tool-test args) (let ((pattern (dict-ref args pattern: #f)) (file (dict-ref args file: #f))) (guard (e (else (format "Test error: ~a" (exception-message e)))) (let* ((sigil-bin (car (command-line))) (cli-args (append (list "test" "--quiet" "--no-color") (if pattern (list "--filter" pattern) '()) (if file (list file) '()))) (proc (apply process-spawn sigil-bin cli-args)) (stdout-str (read-all-string (process-stdout proc))) (stderr-str (read-all-string (process-stderr proc))) (exit-code (process-wait proc))) (string-append (format "Exit code: ~a\n" exit-code) (if (string-empty? stdout-str) "" (format "\n--- stdout ---\n~a" stdout-str)) (if (string-empty? stderr-str) "" (format "\n--- stderr ---\n~a" stderr-str))))))) ;; ============================================================ ;; Package Documentation Tools ;; ============================================================ ;;; List all package documentation with summaries. (define (tool-list-package-docs args) (let ((package-filter (dict-ref args package: #f))) (guard (e (else (format "Error listing package docs: ~a" (exception-message e)))) (if package-filter ;; List docs for specific package (let ((docs (list-package-docs package-filter))) (if (null? docs) (format "No documentation found for package '~a'." package-filter) (format-package-docs-list package-filter docs))) ;; List all packages and their docs (let ((packages (list-packages))) (if (null? packages) "No package documentation found." (format-all-packages-docs packages))))))) ;; Format docs for a single package (define (format-package-docs-list package-name docs) (let ((pkg-info (package-exists? package-name))) (string-append (format "# ~a (~a)\n\n" package-name (if pkg-info (cdr pkg-info) "?")) (string-join (map (lambda (doc) (format "- **~a**: ~a" (car doc) (cdr doc))) docs) "\n")))) ;; Format docs for all packages (define (format-all-packages-docs packages) (string-join (map (lambda (pkg) (let* ((pkg-name (car pkg)) (pkg-version (cdr pkg)) (docs (list-package-docs pkg-name))) (string-append (format "## ~a (~a)\n" pkg-name pkg-version) (if (null? docs) " No documentation files.\n" (string-join (map (lambda (doc) (format "- **~a**: ~a" (car doc) (cdr doc))) docs) "\n"))))) packages) "\n\n")) ;;; Read a specific package documentation file.Showing the first 500 of 834 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.
test/test-nrepl-tools.sgldeleted
(import (sigil test) (sigil string) (sigil json) (sigil mcp nrepl) (sigil mcp server));; ============================================================;; Registration;; ============================================================(test-group "register-nrepl-tools!" (test "registers all nREPL tools" (let ((server (mcp-server name: "test" version: "0.1.0")) (tools '())) (register-nrepl-tools! server (lambda (srv name desc schema handler) (set! tools (cons name tools)))) (assert-true (member "sigil/nrepl-connect" tools)) (assert-true (member "sigil/nrepl-eval" tools)) (assert-true (member "sigil/nrepl-status" tools)) (assert-true (member "sigil/nrepl-disconnect" tools)) (assert-equal 4 (length tools)))))(run-tests)test/test-tools.sgldeleted
(import (sigil test) (sigil json) (sigil string) (sigil mcp tools) (sigil mcp lint));; ============================================================;; sigil/suggest-import;; ============================================================(test-group "suggest-import" (test "known symbol returns module" (let ((result (tool-suggest-import (dict symbol: "string-split")))) ;; If docs index is loaded, we get module info; if not, we get a "No module" message. ;; Both are valid outputs depending on whether docs have been built. (assert-true (or (string-find result "(sigil string)") (string-find result "No module found"))))) (test "unknown symbol returns not found" (let ((result (tool-suggest-import (dict symbol: "zzz-nonexistent-zzz")))) (assert-true (string-find result "No module found")))));; ============================================================;; sigil/project-info;; ============================================================(test-group "project-info" (test "finds sigil workspace" (let ((result (tool-project-info (dict)))) (assert-true (string-find result "Workspace:")) (assert-true (string-find result "sigil")))));; ============================================================;; sigil/run-file;; ============================================================(test-group "run-file" (test "missing file returns error" (let ((result (tool-run-file (dict file: "/tmp/nonexistent-sigil-test.sgl")))) (assert-true (string-find result "File not found")))));; ============================================================;; sigil/lint;; ============================================================(test-group "lint" (test "clean code produces no warnings" (let ((result (tool-lint (dict code: "(define (f x) (+ x 1))")))) (assert-equal "No issues found." result))) (test "lint-code returns list of warnings" (let ((warnings (lint-code "(define (f x) (+ x 1))" '("all")))) (assert-true (list? warnings)) (assert-equal 0 (length warnings)))));; ============================================================;; sigil/reload;; ============================================================(test-group "reload" (test "reload sigil core succeeds" (let ((result (tool-reload (dict module: "(sigil core)")))) (assert-true (string-find result "reloaded successfully")))))(run-tests)