Skip to content

Add spec validation with user feedback for config.edn #92

@bhauman

Description

@bhauman

I've worked with Claude to generate a document outlining the implementation of a way to implement config validation.

The goal is to validate the spec on load and provide feedback to the user if something is wrong.

I'm referring to Clojure Spec in the document but I'm mainly interested in providing good feedback to the user so if Malli is better suited then let's use that.

Stretch goal is catching key misspellings.

Please make minimal changes to the main.clj and core.clj as we can use clojure-mcp.config/process-config and throw an error that we can catch in core.clj or main.clj to print out the result.

Anyway here is the doc outlining a spec implementation:

Overview

We need to add comprehensive validation for the configuration system in clojure-mcp.config using Clojure Spec with Expound for friendly error messages. The validation should be integrated directly into the process-config function to catch errors early in the configuration loading pipeline, failing immediately on invalid configurations to ensure only valid configs are processed.

Background

The Clojure MCP project uses a configuration system that loads settings from .clojure-mcp/config.edn files. Currently, validation is ad-hoc and scattered throughout the codebase. By adding spec validation with Expound that runs by default, we can:

  • Provide clear, human-readable error messages for invalid configurations
  • Catch configuration errors at load time in process-config
  • Ensure type safety for all configuration values
  • Document expected configuration structure through specs
  • Give users helpful guidance on fixing configuration issues

Key Integration Point

The validation should be integrated directly into the clojure-mcp.config/process-config function (lines 18-41). This ensures:

  • Validation happens automatically on every configuration load
  • Invalid configurations fail immediately with helpful error messages
  • Only valid configurations proceed through the system

Implementation Plan

1. Create New Namespace: clojure-mcp.config.spec

Location: src/clojure_mcp/config/spec.clj

Tasks:

  • Create the new file and namespace
  • Require clojure.spec.alpha
  • Consider requiring clojure-mcp.agent.langchain.model-spec to reuse model-related specs
  • Follow the pattern established in src/clojure_mcp/agent/langchain/model_spec.clj

2. Define Basic Type Specs

Define reusable specs for common types:

  • ::path - String representing a valid file path
  • ::env-ref - Environment variable reference format: [:env "VAR_NAME"]
  • Basic types like ::boolean, ::string, ::keyword if needed

3. Define Specs for Each Configuration Key

Create specs for all configuration keys (reference src/clojure_mcp/config.clj for all get-* functions):

Key Spec Definition Notes
::allowed-directories (s/coll-of string?) Collection of directory paths
::emacs-notify boolean?
::write-file-guard #{:full-read :partial-read false} Limited set of values
::cljfmt boolean?
::bash-over-nrepl boolean?
::nrepl-env-type #{:clj :bb :basilisp :scittle} Limited set of environments
::scratch-pad-load boolean?
::scratch-pad-file string?
::models (s/map-of keyword? ::model-config) Map of model configurations
::tools-config (s/map-of keyword? map?) Tool-specific configs
::agents (s/coll-of ::agent-config) Collection of agent configs
::mcp-client (s/nilable string?) Optional string
::dispatch-agent-context (s/or :boolean boolean? :paths (s/coll-of string?)) Boolean or file paths
::enable-tools (s/nilable (s/coll-of (s/or :keyword keyword? :string string?)))
::disable-tools (s/nilable (s/coll-of (s/or :keyword keyword? :string string?)))
::enable-prompts (s/nilable (s/coll-of string?))
::disable-prompts (s/nilable (s/coll-of string?))
::enable-resources (s/nilable (s/coll-of string?))
::disable-resources (s/nilable (s/coll-of string?))
::resources (s/map-of string? ::resource-entry) See section 4 for detailed spec
::prompts (s/map-of string? ::prompt-entry) See section 4 for detailed spec

4. Define Complex Nested Specs

Model Configuration Spec

  • Reuse/reference specs from clojure-mcp.agent.langchain.model-spec
  • Should validate fields like :model-name, :temperature, :api-key, :thinking, etc.
  • Support environment variable references

Agent Configuration Spec

Define spec for agent configurations including:

  • :id - keyword identifier
  • :name - string display name
  • :description - string description
  • :model - model reference
  • :system-prompt - string or prompt reference
  • Other agent-specific fields

Resources Configuration Spec

Resources are not just simple maps. Each resource entry requires:

(s/def ::description string?)
(s/def ::file-path string?)
(s/def ::url (s/nilable string?))
(s/def ::mime-type (s/nilable string?))
(s/def ::resource-entry
  (s/keys :req-un [::description ::file-path]
          :opt-un [::url ::mime-type]))
(s/def ::resources (s/map-of string? ::resource-entry))

See doc/configuring-resources.md for full documentation.

Prompts Configuration Spec

Prompts are not just simple maps. Each prompt entry requires:

(s/def ::description string?)  ; Reused from resources
(s/def ::file string?)
(s/def ::content (s/or :string string? 
                       :file-ref (s/keys :req-un [::file])))
(s/def ::name string?)
(s/def ::required? boolean?)
(s/def ::prompt-arg
  (s/keys :req-un [::name ::description]
          :opt-un [::required?]))
(s/def ::args (s/coll-of ::prompt-arg))
(s/def ::prompt-entry
  (s/keys :req-un [::description]
          :opt-un [::content ::args]))
(s/def ::prompts (s/map-of string? ::prompt-entry))

See doc/configuring-prompts.md for full documentation.

5. Define Main Configuration Spec

(s/def ::config
  (s/keys :opt-un [::allowed-directories
                   ::emacs-notify
                   ::write-file-guard
                   ::cljfmt
                   ::bash-over-nrepl
                   ::nrepl-env-type
                   ::scratch-pad-load
                   ::scratch-pad-file
                   ::models
                   ::tools-config
                   ::agents
                   ::mcp-client
                   ::dispatch-agent-context
                   ::enable-tools
                   ::disable-tools
                   ::enable-prompts
                   ::disable-prompts
                   ::enable-resources
                   ::disable-resources
                   ::resources
                   ::prompts]))

6. Create Validation Functions with Expound

Add expound as a dependency for friendly error messages:

expound/expound {:mvn/version "0.9.0"}

Following the pattern from model_spec.clj, create validation functions with expound support:

(require '[expound.alpha :as expound])

(defn validate-config
  "Validates a configuration map against the spec.
   Returns the config if valid, throws ex-info with friendly expound output if not."
  [config]
  (if (s/valid? ::config config)
    config
    (let [explain-data (s/explain-data ::config config)
          friendly-error (with-out-str 
                          (expound/printer explain-data))]
      (throw (ex-info (str "Invalid configuration:\n" friendly-error)
                      {:explain-data explain-data
                       :config config})))))

(defn validate-key
  "Validates a specific configuration key with friendly errors."
  [key value]
  ...)

(defn explain-config
  "Returns human-readable explanation of validation errors using expound."
  [config]
(defn conform-config
  "Conforms and coerces configuration values."
  [config]
  ...)

When validation fails with expound, users will see helpful, readable error messages instead of cryptic spec failures. For example:

Invalid configuration:
-- Spec failed --------------------

  {:write-file-guard :invalid-value,
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   :resources {"my-doc" {:file-path "doc.md"}},
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   ...}

should be one of: :full-read, :partial-read, false

-- Spec failed --------------------

  {:resources {"my-doc" {:file-path "doc.md"}},
               ^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^
   ...}

Missing required key: :description

Resources must include both :description and :file-path
See doc/configuring-resources.md for details

-- Detected 2 errors -------------------

7. Integration with process-config Function

Primary Integration Point: src/clojure_mcp/config.clj - process-config function (lines 18-41)

The validation should be integrated directly into the process-config function and run by default:

(defn process-config 
  [{:keys [allowed-directories emacs-notify write-file-guard cljfmt 
           bash-over-nrepl nrepl-env-type] :as config} 
   user-dir]
  (let [ud (io/file user-dir)]
    (assert (and (.isAbsolute ud) (.isDirectory ud)))
    
    ;; Always validate the raw config - fail fast on invalid configs
    (clojure-mcp.config.spec/validate-config config)) ; This will throw with friendly expound errors if invalid
    
    ;; Existing validation for write-file-guard
    (when (some? write-file-guard)
      (when-not (contains? #{:full-read :partial-read false} write-file-guard)
        ...))
    
    ;; Rest of existing process-config logic
    (cond-> config
      ...)))

Implementation Notes:

  • Validation runs by default on every config load
  • Invalid configurations immediately throw exceptions with friendly expound messages
  • Fail-fast approach ensures only valid configurations are processed

8. Testing

Location: test/clojure_mcp/config/spec_test.clj

Test cases to implement:

  • Valid configurations from resources/configs/*.edn
  • Invalid configurations with expected error messages
  • Edge cases like environment variable reference resolution
  • Validation function behavior
  • Test specific invalid cases:
    • Invalid :write-file-guard values
    • Invalid :nrepl-env-type values
    • Malformed :resources entries (missing required fields)
    • Malformed :prompts entries (invalid arg structures)
    • Invalid environment variable references
  • Verify that process-config throws on validation errors

9. Documentation

Resources and References

Key Files to Reference

  • Main config namespace: src/clojure_mcp/config.clj
  • Model spec example: src/clojure_mcp/agent/langchain/model_spec.clj
  • Example configurations: resources/configs/*.edn
  • Configuration documentation:
    • README.md (Configuration section starting at line 981)
    • PROJECT_SUMMARY.md (Configuration System section)
    • doc/model-configuration.md
    • doc/component-filtering.md
    • doc/configuring-resources.md (Resource entry structure and requirements)
    • doc/configuring-prompts.md (Prompt entry structure and requirements)

Example Configuration Files

  • resources/configs/example-component-filtering.edn
  • resources/configs/example-models-with-provider.edn
  • resources/configs/example-agents.edn
  • resources/configs/example-tools-config.edn

Notes for Implementation

  1. Environment Variable References: The configuration supports references like [:env "OPENAI_API_KEY"] which are resolved at runtime. The spec should validate the structure but not the actual environment variable existence.

  2. Fail-Fast Design: Invalid configurations should immediately throw exceptions with clear expound error messages. This ensures users fix configuration problems before the system starts.

  3. Model Configuration: The model configuration system already has comprehensive specs in model_spec.clj. Consider reusing these rather than duplicating.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions