├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── resources ├── clojure_mcp │ └── tools │ │ ├── architect │ │ ├── description.md │ │ └── system_message.md │ │ ├── dispatch_agent │ │ ├── system_message.md │ │ └── description.md │ │ └── code_critique │ │ ├── description.md │ │ └── system_message.md ├── clojure-mcp │ ├── test │ │ └── projects │ │ │ ├── deps.edn │ │ │ └── project.clj │ ├── prompts │ │ ├── create_project_summary.md │ │ └── system │ │ │ └── clojure_form_edit.md │ └── tools │ │ └── form_edit │ │ ├── clojure_edit_replace_sexp-description.md │ │ └── clojure_update_sexp-description.md └── configs │ ├── example-prompt-cli-agent.edn │ ├── example-auto-start-nrepl.edn │ ├── example-tools-config.edn │ ├── example-models-with-provider.edn │ ├── example-component-filtering.edn │ └── example-agents.edn ├── src └── clojure_mcp │ ├── sexp │ ├── paren_utils.clj │ └── match.clj │ ├── utils │ ├── file.clj │ └── diff.clj │ ├── sse_main.clj │ ├── delimiter.clj │ ├── tools │ ├── scratch_pad │ │ ├── core.clj │ │ └── truncate.clj │ ├── unified_clojure_edit │ │ ├── core.clj │ │ └── pipeline.clj │ ├── project │ │ └── tool.clj │ ├── directory_tree │ │ └── tool.clj │ ├── agent_tool_builder │ │ ├── default_agents.clj │ │ └── file_changes.clj │ ├── figwheel │ │ └── tool.clj │ ├── glob_files │ │ └── tool.clj │ └── file_edit │ │ └── tool.clj │ ├── dialects.clj │ ├── main_examples │ ├── figwheel_main.clj │ └── shadow_main.clj │ ├── logging.clj │ ├── agent │ └── langchain │ │ ├── schema.clj │ │ └── message_conv.clj │ ├── file_content.clj │ ├── tool_format.clj │ ├── main.clj │ └── tool_system.clj ├── test └── clojure_mcp │ ├── test_helper.clj │ ├── tools_test.clj │ ├── repl_tools │ └── test_utils.clj │ ├── tools │ ├── directory_tree │ │ ├── core_test.clj │ │ └── tool_test.clj │ ├── bash │ │ ├── session_test.clj │ │ ├── tool_test.clj │ │ └── truncation_test.clj │ ├── scratch_pad │ │ ├── truncate_test.clj │ │ └── core_test.clj │ ├── form_edit │ │ └── tool_test.clj │ ├── unified_read_file │ │ ├── core_test.clj │ │ └── tool_test.clj │ ├── glob_files │ │ └── core_test.clj │ └── test_utils.clj │ ├── utils │ └── file_test.clj │ └── config │ └── tools_config_test.clj ├── .gitignore ├── clojure-mcp-dev.sh ├── CLAUDE.md ├── doc ├── example-models-config-with-env.edn ├── README.md ├── tools-configuration.md ├── component-filtering.md └── prompt-cli.md ├── LLM_CODE_STYLE.md ├── CLAUDE_CODE_WEB_SETUP.md ├── BIG_IDEAS.md └── claude-code-setup └── setup-claude-code-env.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bhauman] 2 | custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=B8B3LKTXKV69C 3 | 4 | -------------------------------------------------------------------------------- /resources/clojure_mcp/tools/architect/description.md: -------------------------------------------------------------------------------- 1 | Your go-to tool for any technical or coding task. Analyzes requirements and breaks them down into clear, actionable implementation steps. Use this whenever you need help planning how to implement a feature, solve a technical problem, or structure your code. -------------------------------------------------------------------------------- /src/clojure_mcp/sexp/paren_utils.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.sexp.paren-utils 2 | (:require 3 | [clojure-mcp.delimiter :as delimiter]) 4 | (:import [com.oakmac.parinfer Parinfer])) 5 | 6 | (defn parinfer-repair [code-str] 7 | (let [res (Parinfer/indentMode code-str nil nil nil false)] 8 | (when (and (.success res) 9 | (not (delimiter/delimiter-error? (.text res)))) 10 | (.text res)))) -------------------------------------------------------------------------------- /resources/clojure-mcp/test/projects/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.11.1"} 2 | ring/ring-core {:mvn/version "1.9.4"} 3 | compojure/compojure {:mvn/version "1.6.2"}} 4 | :paths ["src" "resources"] 5 | :aliases {:test {:extra-paths ["test"] 6 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}}} 7 | :dev {:extra-paths ["dev"] 8 | :extra-deps {nrepl/nrepl {:mvn/version "1.3.1"}}}}} 9 | -------------------------------------------------------------------------------- /src/clojure_mcp/utils/file.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.utils.file 2 | "UTF-8 file I/O utilities. 3 | 4 | This namespace provides UTF-8-aware versions of standard file I/O functions 5 | to ensure consistent encoding across all platforms, especially Windows where 6 | the JVM default encoding is Windows-1252 instead of UTF-8." 7 | (:refer-clojure :exclude [spit slurp])) 8 | 9 | (defn slurp-utf8 [f] 10 | (clojure.core/slurp f :encoding "UTF-8")) 11 | 12 | (defn spit-utf8 [f content & options] 13 | (apply clojure.core/spit f content :encoding "UTF-8" options)) 14 | -------------------------------------------------------------------------------- /test/clojure_mcp/test_helper.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.test-helper 2 | "Test helper namespace that configures the test environment. 3 | This namespace is loaded automatically before tests run." 4 | (:require [clojure-mcp.logging :as logging])) 5 | 6 | ;; Suppress logging during tests 7 | (logging/configure-test-logging!) 8 | 9 | (defn run-tests-with-exec 10 | "Called when using -X:test (incorrect usage). 11 | Prints helpful message directing user to use -M:test instead." 12 | [& _args] 13 | (println "\n⚠️ Error: The :test alias requires the -M flag, not -X") 14 | (println "\nPlease run tests using:") 15 | (println " clojure -M:test") 16 | (println "\nThe -M flag is required to suppress logging during test runs.") 17 | (System/exit 1)) 18 | -------------------------------------------------------------------------------- /resources/clojure_mcp/tools/dispatch_agent/system_message.md: -------------------------------------------------------------------------------- 1 | You are an agent for a Clojure Coding Assistant. Given the user's prompt, you should use the tools available to you to answer the user's question. 2 | 3 | You MAY be provided with a project summary and a code-index... Please use these as a starting poing to answering the provided question. 4 | 5 | Notes: 6 | 1. IMPORTANT: You should be concise, direct, and to the point, since your responses will be displayed on a command line interface. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is .", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". 7 | 2. When relevant, share file names and code snippets relevant to the query 8 | 3. Any file paths you return in your final response MUST be absolute. DO NOT use relative paths. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Clojure 2 | .calva/output-window/ 3 | .classpath 4 | .clj-kondo/.cache 5 | .cpcache 6 | .eastwood 7 | .factorypath 8 | .lsp 9 | .hg/ 10 | .hgignore 11 | .java-version 12 | .lein-* 13 | .lsp/.cache 14 | .lsp/sqlite.db 15 | .nrepl-history 16 | .nrepl-port 17 | .project 18 | .clj-kondo 19 | .clojure-mcp 20 | .projectile 21 | .rebel_readline_history 22 | .settings 23 | .socket-repl-port 24 | .sw* 25 | .vscode 26 | *.class 27 | *.jar 28 | *.swp 29 | *~ 30 | /checkouts 31 | /classes 32 | /target 33 | .aider* 34 | 35 | # Project specific 36 | .mcp.json 37 | /node_modules 38 | package-lock.json 39 | package.json 40 | test/tmp/ 41 | 42 | src/user.clj 43 | 44 | # OS specific 45 | .DS_Store 46 | **/.DS_Store 47 | 48 | # Temporary files 49 | NOTES.md 50 | *.log 51 | *.tmp 52 | /.idea/ 53 | /clojure-mcp.iml 54 | 55 | # Development test files 56 | /dev/edit_test.clj 57 | /dev/test_*.clj 58 | /test-complex-sexp.clj 59 | /bb.edn 60 | /claude-clj.sh 61 | 62 | # Editor backup files 63 | *# 64 | .#* 65 | 66 | # Log directories 67 | /logs/ 68 | linux-install.sh 69 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Java 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'temurin' 21 | java-version: '21' 22 | 23 | - name: Setup Clojure 24 | uses: DeLaGuardo/setup-clojure@12.5 25 | with: 26 | cli: latest 27 | 28 | - name: Cache Clojure dependencies 29 | uses: actions/cache@v3 30 | with: 31 | path: | 32 | ~/.m2/repository 33 | ~/.gitlibs 34 | ~/.deps.clj 35 | key: ${{ runner.os }}-clojure-${{ hashFiles('**/deps.edn') }} 36 | restore-keys: | 37 | ${{ runner.os }}-clojure- 38 | 39 | - name: Install dependencies 40 | run: clojure -P -M:test 41 | 42 | - name: Run tests 43 | run: clojure -M:test 44 | -------------------------------------------------------------------------------- /src/clojure_mcp/utils/diff.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.utils.diff 2 | (:require [clojure.string :as str]) 3 | (:import 4 | (com.github.difflib DiffUtils UnifiedDiffUtils))) 5 | 6 | (defn generate-unified-diff 7 | "Generate a unified diff format from original and revised text strings. 8 | Takes the same 3 arguments as the shell version: 9 | - file1-content: original text as string 10 | - file2-content: revised text as string 11 | - context-lines: number of context lines (defaults to 3)" 12 | ([file1-content file2-content] 13 | (generate-unified-diff file1-content file2-content 3)) 14 | ([file1-content file2-content context-lines] 15 | (let [original-lines (str/split-lines file1-content) 16 | revised-lines (str/split-lines file2-content) 17 | patch (DiffUtils/diff original-lines revised-lines) 18 | unified-diff (UnifiedDiffUtils/generateUnifiedDiff 19 | "original.txt" ;; default filename 20 | "revised.txt" ;; default filename 21 | original-lines 22 | patch 23 | context-lines)] 24 | (str/join "\n" unified-diff)))) 25 | -------------------------------------------------------------------------------- /clojure-mcp-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | 5 | # Create logs directory if it doesn't exist 6 | mkdir -p "$SCRIPT_DIR/logs" 7 | 8 | cd "$SCRIPT_DIR" 9 | 10 | # Generate timestamp for log files 11 | TIMESTAMP=$(date +"%Y%m%d_%H%M%S") 12 | STDIN_LOG="$SCRIPT_DIR/logs/mcp_stdin.log" 13 | STDOUT_LOG="$SCRIPT_DIR/logs/mcp_stdout.log" 14 | 15 | source ~/.current_api_keys 16 | 17 | # Create a named pipe for stdin capture 18 | PIPE=$(mktemp -u) 19 | mkfifo "$PIPE" 20 | 21 | PORT=7888 22 | # PORT=44264 23 | # PORT=58709 # conj-talk 24 | 25 | # Start tee process to capture stdin in background 26 | tee "$STDIN_LOG" < "$PIPE" | clojure -X:dev-mcp :enable-logging? true :port $PORT 2>&1 | tee "$STDOUT_LOG" & 27 | # tee "$STDIN_LOG" < "$PIPE" | clojure -X:mcp-shadow :enable-logging? true :port $PORT 2>&1 | tee "$STDOUT_LOG" & 28 | # tee "$STDIN_LOG" < "$PIPE" | clojure -X:mcp-shadow-dual :enable-logging? true :port 7888 :shadow-port $PORT 2>&1 | tee "$STDOUT_LOG" & 29 | 30 | # Get the PID of the background pipeline 31 | CLOJURE_PID=$! 32 | 33 | # Redirect stdin to the pipe 34 | cat > "$PIPE" 35 | 36 | # Clean up 37 | rm "$PIPE" 38 | wait $CLOJURE_PID 39 | -------------------------------------------------------------------------------- /resources/clojure-mcp/test/projects/project.clj: -------------------------------------------------------------------------------- 1 | (defproject acme/widget-factory "2.3.0-SNAPSHOT" 2 | :description "A comprehensive widget manufacturing system" 3 | :url "https://github.com/acme/widget-factory" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.10.0" :scope "provided"] 7 | [org.clojure/clojurescript "1.9.562" :scope "provided"] 8 | [prismatic/schema "1.1.10"]] 9 | :deploy-repositories [["releases" :clojars] 10 | ["snapshots" :clojars]] 11 | :repl-options {:port 57802} 12 | :source-paths ["src/main/clj" "src/shared"] 13 | :test-paths ["test/unit" "test/integration"] 14 | :release-tasks [["vcs" "assert-committed"] 15 | ["change" "version" "leiningen.release/bump-version" "release"] 16 | ["vcs" "commit"] 17 | ["vcs" "tag" "v"] 18 | ["deploy"] 19 | ["change" "version" "leiningen.release/bump-version"] 20 | ["vcs" "commit"] 21 | ["vcs" "push"]] 22 | :profiles {:dev {:dependencies [[orchestra "2019.02.06-1"]]}}) 23 | -------------------------------------------------------------------------------- /resources/clojure-mcp/prompts/create_project_summary.md: -------------------------------------------------------------------------------- 1 | I'd like you to create an LLM-friendly project summary for this codebase. 2 | 3 | The root directory of this codebase/project is at: 4 | {{root-directory}} 5 | 6 | First try to `read_file` the `{{root-directory}}/PROJECT_SUMMARY.md` 7 | 8 | IF there is no `PROJECT_SUMMARY.md` THEN 9 | 10 | Please analyze the key files, dependencies, and structure, then generate a `PROJECT_SUMMARY.md` in `{{root-directory}}` that includes: 11 | 12 | A brief overview of what the project does 13 | Key file paths with descriptions of their purpose 14 | Important dependencies with versions and their roles 15 | Available tools/functions/APIs with examples of how to use them 16 | The overall architecture and how components interact 17 | Implementation patterns and conventions used throughout the code 18 | Development workflow recommendations 19 | Extension points for future development 20 | 21 | Structure this summary to help an LLM coding assistant quickly understand the project and provide effective assistance with minimal additional context. 22 | 23 | ELSE IF a `PROJECT_SUMMARY.md` already exists THEN 24 | 25 | Please use the `read_file` tool to read it and then update it with any new information that we have learned in this current chat. 26 | 27 | -------------------------------------------------------------------------------- /resources/clojure_mcp/tools/code_critique/description.md: -------------------------------------------------------------------------------- 1 | Starts an interactive code review conversation that provides constructive feedback on your Clojure code. 2 | 3 | HOW TO USE THIS TOOL: 4 | 1. Submit your Clojure code for initial critique 5 | 2. Review the suggestions and implement improvements 6 | 3. Test your revised code in the REPL 7 | 4. Submit the updated code for additional feedback 8 | 5. Continue this cycle until the critique is satisfied 9 | 10 | This tool initiates a feedback loop where you: 11 | - Receive detailed analysis of your code 12 | - Implement suggested improvements 13 | - Test and verify changes 14 | - Get follow-up critique on your revisions 15 | 16 | The critique examines: 17 | - Adherence to Clojure style conventions 18 | - Functional programming best practices 19 | - Performance optimizations 20 | - Readability and maintainability improvements 21 | - Idiomatic Clojure patterns 22 | 23 | Example conversation flow: 24 | - You: Submit initial function implementation 25 | - Tool: Provides feedback on style and structure 26 | - You: Revise code based on suggestions and test in REPL 27 | - Tool: Reviews updates and suggests further refinements 28 | - Repeat until code quality goals are achieved 29 | 30 | Perfect for iterative learning and continuous code improvement. -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # Clojure MCP Development Guide 2 | 3 | ## Build Commands 4 | - Run REPL with MCP server: `clojure -X:mcp` (starts on port 7888) 5 | - Run all tests: `clojure -M:test` 6 | - Run linter: `clj-kondo --lint src` or `clj-kondo --lint src test` for both 7 | - Build JAR: `clojure -T:build ci` 8 | - Install locally: `clojure -T:build install` 9 | 10 | ## Code Style Guidelines 11 | - **Imports**: Use `:require` with ns aliases (e.g., `[clojure.string :as string]`) 12 | - **Naming**: Use kebab-case for vars/functions; end predicates with `?` (e.g., `is-top-level-form?`) 13 | - **Error handling**: Use `try/catch` with specific exception handling; atom for tracking errors 14 | - **Formatting**: 2-space indentation; maintain whitespace in edited forms 15 | - **Namespaces**: Align with directory structure (`clojure-mcp.repl-tools`) 16 | - **Testing**: Use `deftest` with descriptive names; `testing` for subsections; `is` for assertions 17 | - **REPL Development**: Prefer REPL-driven development for rapid iteration and feedback 18 | 19 | ## MCP Tool Guidelines 20 | - Include clear tool `:description` for LLM guidance 21 | - Validate inputs and provide helpful error messages 22 | - Return structured data with both result and error status 23 | - Maintain atom-based state for consistent service access 24 | -------------------------------------------------------------------------------- /resources/configs/example-prompt-cli-agent.edn: -------------------------------------------------------------------------------- 1 | ;; Example custom agent configuration for prompt-cli 2 | ;; Save this as custom-agent.edn and use with: 3 | ;; clojure -M:prompt-cli -p "Your prompt" -c custom-agent.edn 4 | 5 | {:id :custom-agent 6 | :name "custom_agent" 7 | :description "Custom agent with limited tools for focused tasks" 8 | 9 | ;; System message can be a string or a path to a resource file 10 | :system-message "You are a helpful Clojure assistant focused on code analysis. 11 | Focus on understanding and explaining code rather than making changes. 12 | Be concise and clear in your explanations." 13 | 14 | ;; Context controls whether to include project summary and code index 15 | :context false ; Set to true to include project context 16 | 17 | ;; Specify which tools the agent can use 18 | ;; Use [:all] for all tools, or list specific tool IDs 19 | :enable-tools [:read_file 20 | :grep 21 | :glob_files 22 | :clojure_inspect_project] 23 | 24 | ;; Memory configuration 25 | ;; false = stateless (each invocation is independent) 26 | ;; number = conversation window size 27 | :memory-size false 28 | 29 | ;; Optional: specify a model (can be overridden with -m flag) 30 | ;; :model :anthropic/claude-3-5-sonnet-20241022 31 | } 32 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [clojure-mcp.tools :as tools])) 4 | 5 | (deftest test-build-read-only-tools 6 | (testing "Read-only tools are created correctly" 7 | (let [nrepl-client-atom (atom {}) 8 | read-only-tools (tools/build-read-only-tools nrepl-client-atom)] 9 | (is (vector? read-only-tools)) 10 | (is (pos? (count read-only-tools))) 11 | ;; Check that all tools have required keys for MCP registration 12 | (doseq [tool read-only-tools] 13 | (is (map? tool)) 14 | ;; Tools should have either :tool-type (for multimethod tools) or :name/:id (for registration maps) 15 | (is (or (contains? tool :tool-type) 16 | (and (contains? tool :name) 17 | (contains? tool :id)))))))) 18 | 19 | (deftest test-build-all-tools 20 | (testing "All tools are created correctly" 21 | (let [nrepl-client-atom (atom {}) 22 | all-tools (tools/build-all-tools nrepl-client-atom)] 23 | (is (vector? all-tools)) 24 | (is (pos? (count all-tools))) 25 | ;; All tools should be a superset of read-only tools 26 | (let [read-only-count (count (tools/build-read-only-tools nrepl-client-atom))] 27 | (is (>= (count all-tools) read-only-count)))))) 28 | -------------------------------------------------------------------------------- /resources/configs/example-auto-start-nrepl.edn: -------------------------------------------------------------------------------- 1 | ;; Example configuration for automatic nREPL server startup 2 | ;; Place this in .clojure-mcp/config.edn in your project root 3 | ;; 4 | ;; IMPORTANT: This feature requires launching clojure-mcp from your project directory 5 | ;; Perfect for Claude Code and other CLI-based LLM tools where you want automatic 6 | ;; nREPL startup without managing separate processes 7 | 8 | {;; Automatically start an nREPL server when MCP server starts 9 | ;; Must be a vector of strings representing the command and arguments 10 | :start-nrepl-cmd ["clojure" "-M:nrepl"] 11 | 12 | ;; Port is optional - if not provided, MCP will parse it from nREPL output 13 | ;; Provide :port if you want to use a fixed port instead of parsing 14 | ;; :port 7888 15 | 16 | ;; Other common configurations 17 | :allowed-directories ["." "src" "test" "resources"] 18 | :write-file-guard :partial-read 19 | :cljfmt true 20 | :bash-over-nrepl true} 21 | 22 | ;; Alternative configurations: 23 | 24 | ;; For Leiningen projects (great for Claude Code): 25 | ;; {:start-nrepl-cmd ["lein" "repl" ":headless"]} 26 | 27 | ;; For Babashka: 28 | ;; {:start-nrepl-cmd ["bb" "nrepl-server"]} 29 | 30 | ;; With fixed port (no port parsing): 31 | ;; {:start-nrepl-cmd ["clojure" "-M:nrepl"] 32 | ;; :port 7888} 33 | 34 | ;; Claude Code usage: 35 | ;; 1. Place this config in your-project/.clojure-mcp/config.edn 36 | ;; 2. Open Claude Code in your project directory 37 | ;; 3. The nREPL will start automatically when Claude Code connects 38 | -------------------------------------------------------------------------------- /test/clojure_mcp/repl_tools/test_utils.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.repl-tools.test-utils 2 | (:require 3 | [clojure-mcp.nrepl :as nrepl] 4 | [nrepl.server :as nrepl-server] 5 | [clojure.test :refer [use-fixtures]])) 6 | 7 | (defonce ^:dynamic *nrepl-server* nil) 8 | (defonce ^:dynamic *nrepl-client-atom* nil) 9 | (def ^:dynamic *test-file-path* "test_function_edit.clj") 10 | 11 | (defn test-nrepl-fixture [f] 12 | (let [server (nrepl-server/start-server :port 0) ; Use port 0 for dynamic port assignment 13 | port (:port server) 14 | client (nrepl/create {:port port}) 15 | client-atom (atom client)] 16 | (nrepl/eval-code client "(require 'clojure.repl)") 17 | (binding [*nrepl-server* server 18 | *nrepl-client-atom* client-atom] 19 | (try 20 | (f) 21 | (finally 22 | (nrepl-server/stop-server server)))))) 23 | 24 | (defn cleanup-test-file [f] 25 | (try 26 | (f) 27 | (finally 28 | #_(io/delete-file *test-file-path* true)))) 29 | 30 | ;; Helper to invoke tool functions more easily in tests 31 | (defn make-test-tool [{:keys [tool-fn] :as _tool-map}] 32 | (fn [arg-map] 33 | (let [prom (promise)] 34 | (tool-fn nil arg-map 35 | (fn [res error?] 36 | (deliver prom {:res res :error? error?}))) 37 | @prom))) 38 | 39 | ;; Apply fixtures in each test namespace 40 | (defn apply-fixtures [_test-namespace] 41 | (use-fixtures :once test-nrepl-fixture) 42 | (use-fixtures :each cleanup-test-file)) 43 | -------------------------------------------------------------------------------- /resources/configs/example-tools-config.edn: -------------------------------------------------------------------------------- 1 | ;; Example configuration with tools-config 2 | {:allowed-directories ["." "src" "test" "resources"] 3 | :write-file-guard :partial-read 4 | :cljfmt true 5 | :bash-over-nrepl true 6 | 7 | ;; Configure specific tools with custom settings 8 | :tools-config {:dispatch_agent {:model :openai/o3} 9 | :architect {:model :openai/gpt-4o} 10 | :code_critique {:model :anthropic/claude-3-haiku-20240307} 11 | :bash {:default-timeout-ms 60000 ; Default timeout in milliseconds 12 | :working-dir "/opt/project" ; Default working directory 13 | :bash-over-nrepl true} ; Override global bash-over-nrepl 14 | ;; Add more tool configurations as needed 15 | ;; :another_tool {:setting "value"} 16 | } 17 | 18 | ;; Optional: Define custom models if using non-default configurations 19 | :models {:openai/o3 {:model-name "o3-mini" 20 | :temperature 0.3 21 | :max-tokens 4096 22 | :api-key [:env "OPENAI_API_KEY"]} 23 | :openai/gpt-4o {:model-name "gpt-4o" 24 | :temperature 0.5 25 | :api-key [:env "OPENAI_API_KEY"]} 26 | :anthropic/claude-3-haiku-20240307 {:model-name "claude-3-haiku-20240307" 27 | :temperature 0.3 28 | :api-key [:env "ANTHROPIC_API_KEY"]}}} 29 | -------------------------------------------------------------------------------- /resources/clojure_mcp/tools/dispatch_agent/description.md: -------------------------------------------------------------------------------- 1 | Launch a new agent that has access to read-only tools. When you are searching for a keyword or file and are not confident that you will find the right match on the first try, use the Agent tool to perform the search for you. For example: 2 | 3 | - If you are searching for a keyword like "config" or "logger", the Agent tool is appropriate 4 | - If you want to read a specific file path, use the read_file or glob_files tool instead of the Agent tool, to find the match more quickly 5 | - If you are searching for a specific class definition like "class Foo", use the glob_files tool instead, to find the match more quickly 6 | 7 | Usage notes: 8 | 1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple `agent` tool uses 9 | 2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. 10 | 3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. 11 | 4. The agent's outputs should generally be trusted -------------------------------------------------------------------------------- /src/clojure_mcp/sse_main.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.sse-main 2 | "Example of a custom MCP server using Server-Sent Events (SSE) transport. 3 | 4 | This demonstrates reusing the standard tools, prompts, and resources 5 | from main.clj while using a different transport mechanism (SSE instead 6 | of stdio). The SSE transport allows web-based clients to connect." 7 | (:require 8 | [clojure-mcp.logging :as logging] 9 | [clojure-mcp.main :as main] 10 | [clojure-mcp.sse-core :as sse-core])) 11 | 12 | (defn start-sse-mcp-server [opts] 13 | ;; Configure logging before starting the server 14 | (logging/configure-logging! 15 | {:log-file (get opts :log-file logging/default-log-file) 16 | :enable-logging? (get opts :enable-logging? false) 17 | :log-level (get opts :log-level :debug)}) 18 | (sse-core/build-and-start-mcp-server 19 | (dissoc opts :log-file :log-level :enable-logging?) 20 | {:make-tools-fn main/make-tools 21 | :make-prompts-fn main/make-prompts 22 | :make-resources-fn main/make-resources})) 23 | 24 | (defn start 25 | "Entry point for running SSE server from project directory. 26 | 27 | Sets :project-dir to current working directory unless :not-cwd is true. 28 | This allows running without an immediate REPL connection - REPL initialization 29 | happens lazily when first needed. 30 | 31 | Options: 32 | - :not-cwd - If true, does NOT set project-dir to cwd (default: false) 33 | - :port - Optional nREPL port (REPL is optional when project-dir is set) 34 | - :mcp-sse-port - Port for SSE server (required) 35 | - All other options supported by start-sse-mcp-server" 36 | [opts] 37 | (let [not-cwd? (get opts :not-cwd false) 38 | opts' (if not-cwd? 39 | opts 40 | (assoc opts :project-dir (System/getProperty "user.dir")))] 41 | (start-sse-mcp-server opts'))) 42 | 43 | -------------------------------------------------------------------------------- /src/clojure_mcp/delimiter.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.delimiter 2 | "Delimiter error checking using edamame parser." 3 | (:require [edamame.core :as e])) 4 | 5 | (defn delimiter-error? 6 | "Returns true if the string has a delimiter error specifically. 7 | Checks both that it's an :edamame/error and has delimiter info. 8 | Uses :all true to enable all standard Clojure reader features: 9 | function literals, regex, quotes, syntax-quote, deref, var, etc. 10 | Also enables :read-cond :allow to support reader conditionals. 11 | Handles unknown data readers gracefully with a default reader fn." 12 | [s] 13 | (try 14 | (e/parse-string-all s {:all true 15 | :read-cond second 16 | :readers (fn [_tag] (fn [data] data)) 17 | :auto-resolve name}) 18 | false ; No error = no delimiter error 19 | (catch clojure.lang.ExceptionInfo ex 20 | (let [data (ex-data ex)] 21 | (and (= :edamame/error (:type data)) 22 | (contains? data :edamame/opened-delimiter)))) 23 | (catch Exception _e 24 | ;; For other exceptions, conservatively return true 25 | ;; to allow potential delimiter repair attempts 26 | true))) 27 | 28 | (defn count-forms 29 | "Counts the number of top-level forms in a string using edamame. 30 | Returns the count of forms, or throws an exception if parsing fails." 31 | [s] 32 | (try 33 | (count (e/parse-string-all s {:all true 34 | :features #{:bb :clj :cljs :cljr :default} 35 | :read-cond :allow 36 | :readers (fn [_tag] (fn [data] data)) 37 | :auto-resolve name})) 38 | (catch Exception e 39 | (throw (ex-info "Failed to parse forms" 40 | {:error (ex-message e)} 41 | e))))) 42 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/scratch_pad/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.scratch-pad.core 2 | (:require 3 | [clojure.pprint :as pprint] 4 | [clojure-mcp.tools.scratch-pad.truncate :as truncate] 5 | [clojure-mcp.tools.scratch-pad.smart-path :as sp])) 6 | 7 | (defn inspect-data [data] 8 | (if (empty? data) 9 | "Empty scratch pad" 10 | (with-out-str (pprint/pprint data)))) 11 | 12 | (defn execute-set-path 13 | "Execute a set_path operation and return the result map." 14 | [current-data path value] 15 | (let [new-data (sp/smart-assoc-in current-data path value) 16 | stored-value (sp/smart-get-in new-data path)] 17 | {:data new-data 18 | :result {:stored-at path 19 | :value stored-value 20 | :pretty-value (with-out-str (pprint/pprint stored-value))}})) 21 | 22 | (defn execute-get-path 23 | "Execute a get_path operation and return the result map." 24 | [current-data path] 25 | (let [value (sp/smart-get-in current-data path)] 26 | {:result {:path path 27 | :value value 28 | :pretty-value (when (some? value) 29 | (with-out-str (pprint/pprint value))) 30 | :found (some? value)}})) 31 | 32 | (defn execute-delete-path 33 | "Execute a delete_path operation and return the result map." 34 | [current-data path] 35 | (let [new-data (sp/smart-dissoc-in current-data path)] 36 | {:data new-data 37 | :result {:removed-from path}})) 38 | 39 | (defn execute-inspect 40 | "Execute an inspect operation and return the result map." 41 | [current-data depth path] 42 | (let [data-to-view (if (and path (seq path)) 43 | (sp/smart-get-in current-data path) 44 | current-data)] 45 | (if (nil? data-to-view) 46 | {:result {:tree (str "No data found at path " path)}} 47 | {:result {:tree (truncate/pprint-truncated data-to-view depth)}}))) 48 | -------------------------------------------------------------------------------- /resources/clojure_mcp/tools/architect/system_message.md: -------------------------------------------------------------------------------- 1 | You are an expert Clojure software architect. Your role is to analyze technical requirements and produce clear, actionable implementation plans following Clojure idioms and functional programming principles. 2 | These plans will then be carried out by a junior Clojure developer, so you need to be specific and detailed. However, do not actually write the code, just explain the plan. 3 | 4 | Follow these steps for each request: 5 | 1. Carefully analyze requirements to identify core functionality and constraints 6 | 2. Define clear technical approach with specific Clojure libraries, functions, and patterns 7 | 3. Break down implementation into concrete, actionable steps at the appropriate level of abstraction 8 | 9 | CLOJURE BEST PRACTICES TO FOLLOW: 10 | - Emphasize functional programming with pure functions and immutable data structures 11 | - Prefer proper conditionals: use 'if' for binary choices, 'cond' for multiple conditions, and 'if-let'/'when-let' for binding and testing in one step 12 | - Recommend threading macros (-> and ->>) to eliminate intermediate bindings and improve readability 13 | - Suggest destructuring in function parameters for cleaner access to data structures 14 | - Design functions to do one thing well and return useful values 15 | - Use early returns with 'when' rather than deeply nested conditionals 16 | - Track actual values instead of boolean flags where possible 17 | - Emphasize REPL-driven development with small, incrementally tested steps 18 | - Organize code with thoughtful namespace design and clear dependency management 19 | - Use appropriate Clojure abstractions like multimethods, protocols, or spec where relevant 20 | 21 | Keep responses focused, specific and actionable. 22 | 23 | IMPORTANT: Do not ask the user if you should implement the changes at the end. Just provide the plan as described above. 24 | IMPORTANT: Do not attempt to write the code or use any string modification tools. Just provide the plan. -------------------------------------------------------------------------------- /resources/clojure_mcp/tools/code_critique/system_message.md: -------------------------------------------------------------------------------- 1 | You are a super power Rich Hickey Clojure code advisor. Your task is to analyze code and provide feedback for improvements. 2 | 3 | You are going to be presented with code and you job is to find the 2 most important structural improvements to the code. And then present a concise description of the improvements. 4 | 5 | When you see a loop recommend `iterate` or `reduce` if its appropriate. 6 | When you see nesting look for threading `some->` `->>` `->` opportunities 7 | 8 | Do not validate an approach, always offer the next improvements. 9 | 10 | Always, discourage code comments and docstrings as they are unnecessary while iterating on code and they eat up LLM tokens (expensive) and they slow down development. 11 | 12 | Always, let based local functions are discouraged for two important reasons: 13 | 1. they are harder to test individually 14 | 2. AI assistants have a much easier time editing smaller functions just like humans 15 | 3. It's an opportunity to create a reusable function to share with others. 16 | 17 | Frequently recommend extracting functions. For longer functions: always extract predicates that are longer than 5 lines. Extracting the step functions (if more than a few lines) for `reduce` and `iterate` is often a great improvement as you can test them separately. 18 | 19 | 20 | I think you can use `reduce` instead of a loop 21 | 22 | 23 | 24 | This function is nested deeply you can probably extract the predicate 25 | 26 | 27 | 28 | This function is too long please break it up into several smaller functions, that filter predicate is very long for instance. 29 | 30 | 31 | 32 | 33 | This function is using state, probably better to use `iterate` 34 | 35 | 36 | 37 | 38 | * This function is nested deeply you can probably extract the predicate 39 | * I think you can use `reduce` instead of a loop 40 | 41 | -------------------------------------------------------------------------------- /doc/example-models-config-with-env.edn: -------------------------------------------------------------------------------- 1 | ;; Example .clojure-mcp/config.edn showing environment variable references 2 | ;; This allows secure configuration without hardcoding sensitive data 3 | 4 | {;; Custom model configurations with environment variable references 5 | :models {;; Basic model with env var for API key 6 | :openai/my-gpt {:model-name "gpt-4o" 7 | :temperature 0.7 8 | :max-tokens 4096 9 | ;; Reference environment variable for API key 10 | :api-key [:env "OPENAI_API_KEY"]} 11 | 12 | ;; Model with both API key and base URL from environment 13 | :openai/custom-endpoint {:model-name "gpt-4o" 14 | :temperature 0.3 15 | :api-key [:env "OPENAI_API_KEY"] 16 | :base-url [:env "OPENAI_BASE_URL"]} 17 | 18 | ;; Anthropic model with env var 19 | :anthropic/production {:model-name "claude-3-5-sonnet-20241022" 20 | :api-key [:env "ANTHROPIC_API_KEY"] 21 | :max-tokens 8192 22 | :thinking {:enabled true 23 | :budget-tokens 4096}} 24 | 25 | ;; Google model with nested env var usage 26 | :google/gemini-prod {:model-name "gemini-2.5-pro" 27 | :api-key [:env "GEMINI_API_KEY"] 28 | :temperature 0.5 29 | :google {:project-id [:env "GOOGLE_CLOUD_PROJECT"]}}} 30 | 31 | ;; Other configuration options 32 | :allowed-directories ["."] 33 | :cljfmt true 34 | :bash-over-nrepl true} 35 | 36 | ;; Notes: 37 | ;; - Environment variables are always returned as strings 38 | ;; - Use [:env "VAR_NAME"] primarily for string values like :api-key and :base-url 39 | ;; - For numeric values, use direct values in the config 40 | ;; 41 | ;; Testing Support: 42 | ;; For unit tests, you can use the dynamic var model/*env-overrides*: 43 | ;; (binding [model/*env-overrides* {"TEST_API_KEY" "test-value"}] 44 | ;; ;; Your test code here 45 | ;; ) 46 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/directory_tree/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.directory-tree.core-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [clojure-mcp.tools.directory-tree.core :as sut] 4 | [clojure.java.io :as io])) 5 | 6 | (deftest directory-tree-test 7 | (testing "directory-tree generates proper tree structure" 8 | (let [temp-dir (io/file (System/getProperty "java.io.tmpdir") "directory-tree-test")] 9 | 10 | ;; Set up test directory structure 11 | (.mkdirs temp-dir) 12 | (let [dir1 (io/file temp-dir "dir1") 13 | dir2 (io/file temp-dir "dir2") 14 | file1 (io/file temp-dir "file1.txt") 15 | file2 (io/file dir1 "file2.txt")] 16 | 17 | (try 18 | (.mkdirs dir1) 19 | (.mkdirs dir2) 20 | (spit file1 "content1") 21 | (.mkdirs (.getParentFile file2)) 22 | (spit file2 "content2") 23 | 24 | (testing "full tree" 25 | (let [result (sut/directory-tree (.getAbsolutePath temp-dir))] 26 | (is (string? result)) 27 | (is (.contains result "dir1/")) 28 | (is (.contains result "dir2/")) 29 | (is (.contains result "file1.txt")))) 30 | 31 | (testing "with max-depth 0" 32 | (let [result (sut/directory-tree (.getAbsolutePath temp-dir) :max-depth 0)] 33 | (is (string? result)) 34 | (is (.contains result "dir1/")) 35 | (is (.contains result "dir2/")) 36 | (is (.contains result "file1.txt")) 37 | (is (.contains result "...")) 38 | (is (not (.contains result "file2.txt"))))) 39 | 40 | (finally 41 | ;; Clean up 42 | (io/delete-file file2 true) 43 | (io/delete-file file1 true) 44 | (io/delete-file dir1 true) 45 | (io/delete-file dir2 true) 46 | (io/delete-file temp-dir true)))))) 47 | 48 | (testing "directory-tree with non-existent directory" 49 | (let [result (sut/directory-tree "/path/that/does/not/exist")] 50 | (is (map? result)) 51 | (is (contains? result :error)) 52 | (is (.contains (:error result) "not a valid directory"))))) -------------------------------------------------------------------------------- /resources/configs/example-models-with-provider.edn: -------------------------------------------------------------------------------- 1 | ;; Example .clojure-mcp/config.edn with custom model configurations 2 | 3 | {;; Custom model configurations 4 | :models {;; Traditional approach - provider from namespace 5 | :openai/my-fast-gpt {:model-name "gpt-4o" 6 | :temperature 0.3 7 | :max-tokens 2048} 8 | 9 | ;; Explicit provider - allows custom namespaces 10 | :company/production-llm {:provider :openai 11 | :model-name "gpt-4o" 12 | :temperature 0.7 13 | :max-tokens 4096} 14 | 15 | :team/code-assistant {:provider :anthropic 16 | :model-name "claude-3-5-sonnet-20241022" 17 | :temperature 0.2 18 | :max-tokens 8192} 19 | 20 | ;; Provider override - useful for services like Azure OpenAI 21 | :openai/azure-gpt {:provider :openai ; Still uses OpenAI provider 22 | :model-name "gpt-4" 23 | :base-url [:env "AZURE_OPENAI_URL"] 24 | :api-key [:env "AZURE_API_KEY"]} 25 | 26 | ;; Environment variable references 27 | :prod/main-model {:provider :google 28 | :model-name [:env "PROD_MODEL_NAME"] 29 | :api-key [:env "GEMINI_API_KEY"] 30 | :temperature 0.5 31 | :max-tokens 4096} 32 | 33 | ;; Reasoning models with thinking config 34 | :company/reasoning-llm {:provider :anthropic 35 | :model-name "claude-3-5-sonnet-20241022" 36 | :max-tokens 8192 37 | :thinking {:enabled true 38 | :return true 39 | :send true 40 | :budget-tokens 4096}}} 41 | 42 | ;; Other configuration options 43 | :allowed-directories ["."] 44 | :cljfmt true 45 | :bash-over-nrepl true 46 | :scratch-pad-load false} 47 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/bash/session_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.bash.session-test 2 | "Test for bash tool session functionality" 3 | (:require [clojure.test :refer [deftest is testing]] 4 | [clojure-mcp.tools.bash.tool :as bash-tool] 5 | [clojure-mcp.tools.eval.core :as eval-core] 6 | [clojure-mcp.nrepl :as nrepl] 7 | [clojure-mcp.config :as config] 8 | [clojure-mcp.tool-system :as tool-system])) 9 | 10 | (deftest test-bash-execution-uses-session 11 | (testing "Bash command execution passes session-type to evaluate-code" 12 | (let [captured-args (atom nil) 13 | mock-client {:client :mock-client 14 | :port 7888 ;; nrepl-available? requires port 15 | ::nrepl/state (atom {}) 16 | ::config/config {:allowed-directories [(System/getProperty "user.dir")] 17 | :nrepl-user-dir (System/getProperty "user.dir") 18 | :bash-over-nrepl true}} 19 | client-atom (atom mock-client) 20 | bash-tool-config (bash-tool/create-bash-tool client-atom)] 21 | 22 | ;; Mock the evaluate-code function to capture its arguments 23 | (with-redefs [clojure-mcp.tools.eval.core/evaluate-code 24 | (fn [_client opts] 25 | (reset! captured-args opts) 26 | {:outputs [[:value "{:exit-code 0 :stdout \"test\" :stderr \"\" :timed-out false}"]] 27 | :error false})] 28 | 29 | ;; Execute a bash command 30 | (let [inputs {:command "echo test" 31 | :working-directory (System/getProperty "user.dir") 32 | :timeout-ms 30000} 33 | result (tool-system/execute-tool bash-tool-config inputs)] 34 | 35 | ;; Verify the session-type was passed to evaluate-code 36 | (is (not (nil? @captured-args))) 37 | (is (contains? @captured-args :session-type)) 38 | (is (= :tools (:session-type @captured-args))) 39 | 40 | ;; Verify the result is properly formatted 41 | (is (map? result)) 42 | (is (= 0 (:exit-code result))) 43 | (is (= "test" (:stdout result)))))))) 44 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/unified_clojure_edit/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.unified-clojure-edit.core 2 | "Core utility functions for unified Clojure code editing. 3 | This namespace provides pattern-based form finding and editing 4 | without any MCP-specific code." 5 | (:require 6 | [clojure-mcp.sexp.match :as match] 7 | [rewrite-clj.zip :as z] 8 | [rewrite-clj.parser :as p])) 9 | 10 | (defn find-pattern-match 11 | "Finds a pattern match in Clojure source code. 12 | 13 | Arguments: 14 | - zloc: Source code zipper 15 | - pattern-str: Pattern to match with wildcards (? and *) 16 | 17 | Returns: 18 | - Map with :zloc pointing to the matched form or nil if not found" 19 | [zloc pattern-str] 20 | (let [pattern-sexpr (z/sexpr (z/of-string pattern-str))] 21 | (if-let [match-loc (match/find-match* pattern-sexpr zloc)] 22 | {:zloc match-loc} 23 | nil))) 24 | 25 | (defn edit-matched-form 26 | "Edits a form matched by a pattern. 27 | 28 | Arguments: 29 | - zloc: Source code zipper 30 | - pattern-str: Pattern that matches the target form 31 | - content-str: New content to replace/insert 32 | - edit-type: Operation to perform (:replace, :insert-before, :insert-after) 33 | 34 | Returns: 35 | - Map with :zloc pointing to the edited form, or nil if match not found" 36 | [zloc pattern-str content-str edit-type] 37 | (if-let [match-result (find-pattern-match zloc pattern-str)] 38 | (let [match-loc (:zloc match-result) 39 | content-node (p/parse-string-all content-str) 40 | updated-loc (case edit-type 41 | :replace 42 | (z/replace match-loc content-node) 43 | :insert_before 44 | (-> match-loc 45 | (z/insert-left (p/parse-string-all "\n\n")) 46 | z/left 47 | (z/insert-left content-node) 48 | z/left) ; Move to the newly inserted node 49 | 50 | :insert_after 51 | (-> match-loc 52 | (z/insert-right (p/parse-string-all "\n\n")) 53 | z/right 54 | (z/insert-right content-node) 55 | z/right))] ; Move to the newly inserted node 56 | {:zloc updated-loc}) 57 | nil)) 58 | -------------------------------------------------------------------------------- /src/clojure_mcp/dialects.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.dialects 2 | "Handles environment-specific behavior for different nREPL dialects. 3 | 4 | Provides dialect-specific expressions for initialization sequences. 5 | The actual execution of these expressions is handled by clojure-mcp.nrepl." 6 | (:require [clojure.java.io :as io] 7 | [clojure-mcp.utils.file :as file-utils])) 8 | 9 | (defn handle-bash-over-nrepl? [nrepl-env-type] 10 | (boolean (#{:clj :bb} nrepl-env-type))) 11 | 12 | ;; Multimethod for getting the expression to fetch project directory 13 | (defmulti fetch-project-directory-exp 14 | "Returns an expression (string) to evaluate for getting the project directory. 15 | Dispatches on :nrepl-env-type from config." 16 | (fn [nrepl-env-type] nrepl-env-type)) 17 | 18 | (defmethod fetch-project-directory-exp :clj 19 | [_] 20 | "(System/getProperty \"user.dir\")") 21 | 22 | (defmethod fetch-project-directory-exp :bb 23 | [_] 24 | "(System/getProperty \"user.dir\")") 25 | 26 | (defmethod fetch-project-directory-exp :basilisp 27 | [_] 28 | "(import os)\n(os/getcwd)") 29 | 30 | (defmethod fetch-project-directory-exp :default 31 | [_] 32 | nil) 33 | 34 | ;; Multimethod for environment initialization 35 | (defmulti initialize-environment-exp 36 | "Returns a vector of expressions (strings) to evaluate for initializing 37 | the environment. These set up necessary namespaces and helpers." 38 | (fn [nrepl-env-type] nrepl-env-type)) 39 | 40 | (defmethod initialize-environment-exp :clj 41 | [_] 42 | ["(require '[clojure.repl :as repl])" 43 | "(require 'nrepl.util.print)"]) 44 | 45 | (defmethod initialize-environment-exp :bb 46 | [_] 47 | ["(require '[clojure.repl :as repl])"]) 48 | 49 | (defmethod initialize-environment-exp :basilisp 50 | [_] 51 | ["(require '[basilisp.repl :as repl])"]) 52 | 53 | (defmethod initialize-environment-exp :default 54 | [_] 55 | []) 56 | 57 | ;; Helper to load REPL helpers - might vary by environment 58 | (defmulti load-repl-helpers-exp 59 | "Returns expressions for loading REPL helper functions. 60 | Some environments might not support all helpers." 61 | (fn [nrepl-env-type] nrepl-env-type)) 62 | 63 | (defmethod load-repl-helpers-exp :clj 64 | [_] 65 | ;; For Clojure, we load the helpers from resources 66 | [(file-utils/slurp-utf8 (io/resource "clojure-mcp/repl_helpers.clj")) 67 | "(in-ns 'user)"]) 68 | 69 | (defmethod load-repl-helpers-exp :default 70 | [_] 71 | []) 72 | -------------------------------------------------------------------------------- /resources/clojure-mcp/prompts/system/clojure_form_edit.md: -------------------------------------------------------------------------------- 1 | # Use Clojure Structure-Aware Editing Tools 2 | 3 | ALWAYS use the specialized Clojure editing tools rather than generic text editing. 4 | These tools understand Clojure syntax and prevent common errors. 5 | 6 | ## Why Use These Tools? 7 | - Avoid exact whitespace matching problems 8 | - Get early validation for parenthesis balance 9 | - Eliminate retry loops from failed text edits 10 | - Target forms by name rather than trying to match exact text 11 | 12 | ## Core Tools to Use 13 | - `clojure_edit` - Replace entire top-level forms 14 | - `clojure_edit_replace_sexp` - Modify expressions within top-level forms 15 | 16 | ## CODE SIZE DIRECTLY IMPACTS EDIT SUCCESS 17 | - **SMALLER EDITS = HIGHER SUCCESS RATE** 18 | - **LONG FUNCTIONS ALMOST ALWAYS FAIL** - Break into multiple small functions 19 | - **NEVER ADD MULTIPLE FUNCTIONS AT ONCE** - Add one at a time 20 | - Each additional line exponentially increases failure probability 21 | - 5-10 line functions succeed, 20+ line functions usually fail 22 | - Break large changes into multiple small edits 23 | 24 | ## COMMENTS ARE PROBLEMATIC 25 | - Minimize comments in code generation 26 | - Comments increase edit complexity and failure rate 27 | - Use meaningful function and parameter names instead 28 | - If comments are needed, add them in separate edits 29 | - Use `file_edit` for comment-only changes 30 | 31 | ## Handling Parenthesis Errors 32 | - Break complex functions into smaller, focused ones 33 | - Start with minimal code and add incrementally 34 | - When facing persistent errors, verify in REPL first 35 | - Count parentheses in the content you're adding 36 | - For deep nesting, use threading macros (`->`, `->>`) 37 | 38 | ## Creating New Files 39 | 1. Start by writing only the namespace declaration 40 | 2. Use `file_write` for just the namespace: 41 | ```clojure 42 | (ns my.namespace 43 | (:require [other.ns :as o])) 44 | ``` 45 | 3. Then add each function one at a time with `clojure_edit` using the "insert_after" operation. 46 | 4. Test each function in the REPL before adding the next 47 | 48 | ## Working with Defmethod 49 | Remember to include dispatch values: 50 | - Normal dispatch: `form_identifier: "area :rectangle"` 51 | - Vector dispatch: `form_identifier: "convert-length [:feet :inches]"` 52 | - Namespaced: `form_identifier: "tool-system/validate-inputs :clojure-eval"` 53 | 54 | 55 | # ALWAYS call tools with the appropriate prefix 56 | 57 | For example: if the prefix for the clojure MCP tools is `clojure-mcp` then be sure to correctly prefix the tool calls with the `clojure-mcp` prefix 58 | -------------------------------------------------------------------------------- /src/clojure_mcp/main_examples/figwheel_main.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.main-examples.figwheel-main 2 | "Example of a custom MCP server that adds ClojureScript evaluation via Figwheel Main. 3 | 4 | This demonstrates the new pattern for creating custom MCP servers: 5 | 1. Define a make-tools function that extends the base tools 6 | 2. Call core/build-and-start-mcp-server with factory functions 7 | 3. Reuse the standard make-prompts and make-resources from main 8 | 9 | Note: Piggieback must be configured in your nREPL middleware for this to work. 10 | See the comments below for the required deps.edn configuration." 11 | (:require 12 | [clojure-mcp.core :as core] 13 | [clojure-mcp.logging :as logging] 14 | [clojure-mcp.main :as main] 15 | [clojure-mcp.tools.figwheel.tool :as figwheel-tool])) 16 | 17 | ;; This along with `clojure-mcp.tools.figwheel.tool` are proof of 18 | ;; concept of a clojurescript_tool. This proof of concept can be 19 | ;; improved and provides a blueprint for creating other piggieback repls 20 | ;; node, cljs.main etc. 21 | 22 | ;; Shadow is different in that it has its own nrepl connection. 23 | 24 | ;; In the figwheel based clojurescript project piggieback needs to be 25 | ;; configured in the nrepl that clojure-mcp connects to 26 | ;; 27 | ;; :aliases {:nrepl {:extra-deps {cider/piggieback {:mvn/version "0.6.0"} 28 | ;; nrepl/nrepl {:mvn/version "1.3.1"} 29 | ;; com.bhauman/figwheel-main {:mvn/version "0.2.20"}} 30 | ;; :extra-paths ["test" "target"] ;; examples 31 | ;; :jvm-opts ["-Djdk.attach.allowAttachSelf"] 32 | ;; :main-opts ["-m" "nrepl.cmdline" "--port" "7888" 33 | ;; "--middleware" "[cider.piggieback/wrap-cljs-repl]"]}} 34 | 35 | (defn make-tools [nrepl-client-atom working-directory & [{figwheel-build :figwheel-build}]] 36 | (conj (main/make-tools nrepl-client-atom working-directory) 37 | (figwheel-tool/figwheel-eval nrepl-client-atom {:figwheel-build (or figwheel-build "dev")}))) 38 | 39 | (defn start-mcp-server [opts] 40 | ;; Configure logging before starting the server 41 | (logging/configure-logging! 42 | {:log-file (get opts :log-file logging/default-log-file) 43 | :enable-logging? (get opts :enable-logging? false) 44 | :log-level (get opts :log-level :debug)}) 45 | (core/build-and-start-mcp-server 46 | (dissoc opts :log-file :log-level :enable-logging?) 47 | {:make-tools-fn (fn [nrepl-client-atom working-directory] 48 | (make-tools nrepl-client-atom working-directory opts)) 49 | :make-prompts-fn main/make-prompts 50 | :make-resources-fn main/make-resources})) 51 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/project/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.project.tool 2 | "Implementation of project inspection tool using the tool-system multimethod approach." 3 | (:require 4 | [clojure-mcp.tool-system :as tool-system] 5 | [clojure-mcp.tools.project.core :as core])) 6 | 7 | ;; Factory function to create the tool configuration 8 | (defn create-project-inspection-tool 9 | "Creates the project inspection tool configuration" 10 | [nrepl-client-atom] 11 | {:tool-type :clojure-inspect-project 12 | :nrepl-client-atom nrepl-client-atom}) 13 | 14 | ;; Implement the required multimethods for the project inspection tool 15 | (defmethod tool-system/tool-name :clojure-inspect-project [_] 16 | "clojure_inspect_project") 17 | 18 | (defmethod tool-system/tool-description :clojure-inspect-project [_] 19 | "Analyzes and provides detailed information about a Clojure project's structure, 20 | including dependencies, source files, namespaces, and environment details. 21 | 22 | This tool helps you understand project organization without having to manually 23 | explore multiple configuration files. It works with both deps.edn and Leiningen projects. 24 | 25 | The tool provides information about: 26 | - Project environment (working directory, Clojure version, Java version) 27 | - Source and test paths 28 | - Dependencies and their versions 29 | - Aliases and their configurations 30 | - Available namespaces 31 | - Source file structure 32 | 33 | Use this tool to quickly get oriented in an unfamiliar Clojure codebase or to 34 | get a high-level overview of your current project. 35 | 36 | # Example: 37 | clojure_inspect_project()") 38 | 39 | (defmethod tool-system/tool-schema :clojure-inspect-project [_] 40 | {:type :object 41 | ;; this is for the anthropic api sdk which currently fails when there are no args ... sigh 42 | :properties {:explanation {:type :string 43 | :description "Short explanation why you chose this tool"}} 44 | :required [:explanation]}) 45 | 46 | (defmethod tool-system/validate-inputs :clojure-inspect-project [_ inputs] 47 | ;; No inputs required for this tool 48 | inputs) 49 | 50 | (defmethod tool-system/execute-tool :clojure-inspect-project [{:keys [nrepl-client-atom]} _] 51 | ;; Pass the atom directly to core implementation instead of dereferencing 52 | (core/inspect-project nrepl-client-atom)) 53 | 54 | (defmethod tool-system/format-results :clojure-inspect-project [_ {:keys [outputs error]}] 55 | ;; Format the results according to MCP expectations 56 | {:result outputs 57 | :error error}) 58 | 59 | ;; Backward compatibility function that returns the registration map 60 | (defn inspect-project-tool [nrepl-client-atom] 61 | (tool-system/registration-map (create-project-inspection-tool nrepl-client-atom))) 62 | -------------------------------------------------------------------------------- /src/clojure_mcp/logging.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.logging 2 | "Centralized logging configuration using Timbre. 3 | 4 | This namespace provides functions to configure Timbre-based logging 5 | for the Clojure MCP server with support for both development and 6 | production modes." 7 | (:require [taoensso.timbre :as timbre])) 8 | 9 | (def default-log-file ".clojure-mcp/clojure-mcp.log") 10 | 11 | (defn configure-logging! 12 | "Configure Timbre logging with the given options. 13 | 14 | Options: 15 | - :log-file Path to log file (default: 'logs/clojure-mcp.log') 16 | - :enable-logging? Whether to enable logging (default: true) 17 | - :log-level Minimum log level (default: :debug) 18 | Valid values: :trace :debug :info :warn :error :fatal :report 19 | 20 | Examples: 21 | 22 | Development mode with full logging: 23 | (configure-logging! {:log-file \"logs/clojure-mcp.log\" 24 | :enable-logging? true 25 | :log-level :debug}) 26 | 27 | Production mode with logging suppressed: 28 | (configure-logging! {:enable-logging? false})" 29 | [{:keys [log-file enable-logging? log-level] 30 | :or {log-file default-log-file 31 | enable-logging? false 32 | log-level :debug}}] 33 | (timbre/set-config! 34 | {:appenders (if enable-logging? 35 | {:spit (assoc 36 | (timbre/spit-appender {:fname log-file}) 37 | :enabled? enable-logging? 38 | :min-level (or log-level :report) 39 | :ns-filter (if enable-logging? 40 | {:allow #{"clojure-mcp.*"}} 41 | {:deny #{"*"}}))} 42 | {})})) 43 | 44 | (defn configure-dev-logging! 45 | "Configure logging for development mode with debug level. 46 | Convenience function that enables full debug logging to logs/clojure-mcp.log" 47 | [] 48 | (configure-logging! {:log-file "logs/clojure-mcp.log" 49 | :enable-logging? true 50 | :log-level :debug})) 51 | 52 | (defn configure-prod-logging! 53 | "Configure logging for production mode with all logging suppressed. 54 | Convenience function that disables all logging." 55 | [] 56 | (configure-logging! {:enable-logging? false})) 57 | 58 | (defn configure-test-logging! 59 | "Configure logging for test mode with all logging suppressed. 60 | Convenience function that disables all logging during tests." 61 | [] 62 | (configure-logging! {:enable-logging? false})) 63 | 64 | (defn suppress-logging-for-tests! 65 | "Automatically suppress logging if running in test mode. 66 | Checks for CLOJURE_MCP_TEST environment variable or 67 | clojure.mcp.test system property." 68 | [] 69 | (when (or (System/getenv "CLOJURE_MCP_TEST") 70 | (System/getProperty "clojure.mcp.test")) 71 | (configure-test-logging!))) 72 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/scratch_pad/truncate_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.scratch-pad.truncate-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [clojure-mcp.tools.scratch-pad.truncate :as truncate])) 4 | 5 | (deftest truncate-depth-test 6 | (testing "Basic truncation at different depths" 7 | (let [data {:a {:b {:c {:d "deep"}}}}] 8 | (is (= {'... '...1_entries} 9 | (truncate/truncate-depth data 0))) 10 | (is (= {:a {'... '...1_entries}} 11 | (truncate/truncate-depth data 1))) 12 | (is (= {:a {:b {'... '...1_entries}}} 13 | (truncate/truncate-depth data 2))) 14 | (is (= {:a {:b {:c {'... '...1_entries}}}} 15 | (truncate/truncate-depth data 3))))) 16 | 17 | (testing "Leaf values always become ellipsis" 18 | (let [data {:string "test" 19 | :number 42 20 | :keyword :key 21 | :nested {:value "hidden"}}] 22 | (is (= {:string '... 23 | :number '... 24 | :keyword '... 25 | :nested {'... '...1_entries}} 26 | (truncate/truncate-depth data 1))))) 27 | 28 | (testing "Empty collections at max depth" 29 | (let [data {:empty-vec [] 30 | :empty-map {} 31 | :empty-set #{}}] 32 | (is (= {:empty-vec '... 33 | :empty-map {'... '...0_entries} 34 | :empty-set #{'...}} 35 | (truncate/truncate-depth data 1))))) 36 | 37 | (testing "Size indicators for sequences at max depth" 38 | (is (= ['...10_elements] 39 | (truncate/truncate-depth (vec (range 10)) 0))) 40 | (is (= ['...5_elements] 41 | (truncate/truncate-depth (vec (range 5)) 0)))) 42 | 43 | (testing "Size indicators for maps at max depth" 44 | (let [large-map (zipmap (range 10) (repeat "val"))] 45 | (is (= {'... '...10_entries} 46 | (truncate/truncate-depth large-map 0))))) 47 | 48 | (testing "Size indicators for sets at max depth" 49 | (is (= #{'...8_items} 50 | (truncate/truncate-depth #{:a :b :c :d :e :f :g :h} 0)))) 51 | 52 | (testing "Preserves collection types" 53 | (let [data {:sorted (sorted-map :z 1 :a 2) 54 | :list '(1 2 3) 55 | :vec [1 2 3]} 56 | result (truncate/truncate-depth data 1)] 57 | (is (sorted? (:sorted result))) 58 | (is (list? (:list result))) 59 | (is (vector? (:vec result))))) 60 | 61 | (testing "Custom ellipsis" 62 | (is (= {:a {'<...> '...1_entries}} 63 | (truncate/truncate-depth {:a {:b {:c 1}}} 1 {:ellipsis '<...>})))) 64 | 65 | (testing "Disable size indicators" 66 | (is (= '... 67 | (truncate/truncate-depth (vec (range 5)) 0 {:show-size false}))))) 68 | 69 | (deftest pprint-truncated-test 70 | (testing "Pretty prints truncated data" 71 | (let [result (truncate/pprint-truncated {:a {:b {:c 1}}} 1)] 72 | (is (string? result)) 73 | (is (re-find #"\.\.\." result))))) 74 | -------------------------------------------------------------------------------- /resources/clojure-mcp/tools/form_edit/clojure_edit_replace_sexp-description.md: -------------------------------------------------------------------------------- 1 | Replaces Clojure expressions in a file. 2 | 3 | This tool provides targeted replacement of Clojure expressions within forms. For complete top-level form operations, use `clojure_edit` instead. 4 | 5 | KEY BENEFITS: 6 | - Syntax-aware matching that understands Clojure code structure 7 | - Ignores whitespace differences by default, focusing on actual code meaning 8 | - Matches expressions regardless of formatting, indentation, or spacing 9 | - Prevents errors from mismatched text or irrelevant formatting differences 10 | - Can replace all occurrences with replace_all: true 11 | 12 | CONSTRAINTS: 13 | - match_form must contain one or more complete Clojure expressions 14 | - new_form must contain zero or more complete Clojure expressions 15 | - Both match_form and new_form must be valid Clojure code that can be parsed 16 | 17 | A complete Clojure expression is any form that Clojure can read as a complete unit: 18 | - Symbols: foo, my-var, clojure.core/map 19 | - Numbers: 42, 3.14 20 | - Strings: "hello" 21 | - Keywords: :keyword, ::namespaced 22 | - Collections: [1 2 3], {:a 1}, #{:a :b} 23 | - Function calls: (println "hello") 24 | - Special forms: (if true 1 2) 25 | 26 | WARNING: The following are NOT valid Clojure expressions and will cause errors: 27 | - Incomplete forms: (defn foo, (try, (let [x 1] 28 | - Partial function definitions: (defn foo [x] 29 | - Just the opening of a form: (if condition 30 | - Mixed data without collection: :a 1 :b 2 31 | - Unmatched parentheses: (+ 1 2)) 32 | 33 | COMMON APPLICATIONS: 34 | - Renaming symbols throughout the file: 35 | match_form: old-name 36 | new_form: new-name 37 | replace_all: true 38 | 39 | - Replacing multiple expressions with a single form: 40 | match_form: (validate x) (transform x) (save x) 41 | new_form: (-> x validate transform save) 42 | 43 | - Wrapping code in try-catch: 44 | match_form: (risky-op-1) (risky-op-2) 45 | new_form: (try 46 | (risky-op-1) 47 | (risky-op-2) 48 | (catch Exception e 49 | (log/error e "Operations failed"))) 50 | 51 | - Removing debug statements: 52 | match_form: (println "Debug 1") (println "Debug 2") 53 | new_form: 54 | 55 | - Converting imperative style to functional: 56 | match_form: (def result (calculate x)) (println result) result 57 | new_form: (doto (calculate x) println) 58 | 59 | - Transforming let bindings: 60 | match_form: [x (get-value) y (process x)] 61 | new_form: [x (get-value) 62 | _ (log/debug "got value" x) 63 | y (process x)] 64 | 65 | Other Examples: 66 | - Replace a calculation: 67 | match_form: (+ x 2) 68 | new_form: (* x 2) 69 | 70 | - Clean up code by removing intermediate steps: 71 | match_form: (let [temp (process x)] (use temp)) 72 | new_form: (use (process x)) 73 | 74 | - Change function calls: 75 | match_form: (println "Processing" x) 76 | new_form: (log/info "Processing item" x) 77 | 78 | Returns a diff showing the changes made to the file. -------------------------------------------------------------------------------- /test/clojure_mcp/utils/file_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.utils.file-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [clojure-mcp.utils.file :as file]) 4 | (:import [java.io File])) 5 | 6 | (deftest slurp-utf8-test 7 | (testing "Reading UTF-8 content" 8 | (let [temp-file (File/createTempFile "test-slurp" ".txt")] 9 | (try 10 | (clojure.core/spit temp-file "conexão\nParâmetros\ndescriçã" :encoding "UTF-8") 11 | (is (= "conexão\nParâmetros\ndescriçã" (file/slurp-utf8 temp-file))) 12 | (finally 13 | (.delete temp-file))))) 14 | 15 | (testing "Reading from File object" 16 | (let [temp-file (File/createTempFile "test-file-obj" ".txt")] 17 | (try 18 | (clojure.core/spit temp-file "test content" :encoding "UTF-8") 19 | (is (= "test content" (file/slurp-utf8 temp-file))) 20 | (finally 21 | (.delete temp-file))))) 22 | 23 | (testing "Reading from file path string" 24 | (let [temp-file (File/createTempFile "test-path" ".txt") 25 | path (.getAbsolutePath temp-file)] 26 | (try 27 | (clojure.core/spit path "path test" :encoding "UTF-8") 28 | (is (= "path test" (file/slurp-utf8 path))) 29 | (finally 30 | (.delete temp-file)))))) 31 | 32 | (deftest spit-utf8-test 33 | (testing "Writing UTF-8 content" 34 | (let [temp-file (File/createTempFile "test-spit" ".txt") 35 | content "conexão\nParâmetros\ndescriçã"] 36 | (try 37 | (file/spit-utf8 temp-file content) 38 | (is (= content (clojure.core/slurp temp-file :encoding "UTF-8"))) 39 | (finally 40 | (.delete temp-file))))) 41 | 42 | (testing "Writing with append option" 43 | (let [temp-file (File/createTempFile "test-append" ".txt")] 44 | (try 45 | (file/spit-utf8 temp-file "first\n") 46 | (file/spit-utf8 temp-file "second\n" :append true) 47 | (is (= "first\nsecond\n" (file/slurp-utf8 temp-file))) 48 | (finally 49 | (.delete temp-file))))) 50 | 51 | (testing "Writing to file path string" 52 | (let [temp-file (File/createTempFile "test-spit-path" ".txt") 53 | path (.getAbsolutePath temp-file)] 54 | (try 55 | (file/spit-utf8 path "path content") 56 | (is (= "path content" (file/slurp-utf8 path))) 57 | (finally 58 | (.delete temp-file)))))) 59 | 60 | (deftest round-trip-test 61 | (testing "Round-trip UTF-8 content preservation" 62 | (let [temp-file (File/createTempFile "test-round-trip" ".txt") 63 | test-strings ["conexão com banco de dados" 64 | "Parâmetros: configuração" 65 | "描述 (Chinese characters)" 66 | "Описание (Cyrillic)" 67 | "🔥 emoji test 🚀" 68 | "Mixed: café, naïve, résumé"]] 69 | (try 70 | (doseq [test-str test-strings] 71 | (file/spit-utf8 temp-file test-str) 72 | (is (= test-str (file/slurp-utf8 temp-file)) 73 | (str "Failed round-trip for: " test-str))) 74 | (finally 75 | (.delete temp-file)))))) 76 | -------------------------------------------------------------------------------- /src/clojure_mcp/sexp/match.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.sexp.match 2 | (:require [rewrite-clj.zip :as z])) 3 | 4 | (defn match-sexpr 5 | "Return true if `pattern` matches `data`. 6 | Wildcards in `pattern`: 7 | - `_?` consumes exactly one form 8 | - `_*` consumes zero or more forms, but if there are more pattern elements 9 | after it, it will try to align them with the tail of `data`." 10 | [pattern data] 11 | (cond 12 | ;; both are sequences ⇒ walk with possible '_*' backtracking 13 | (and (sequential? pattern) (sequential? data)) 14 | (letfn [(match-seq [ps ds] 15 | (cond 16 | ;; pattern exhausted ⇒ only match if data also exhausted 17 | (empty? ps) 18 | (empty? ds) 19 | 20 | ;; '_?' ⇒ must have at least one ds, then consume exactly one 21 | (= (first ps) '_?) 22 | (and (seq ds) 23 | (recur (rest ps) (rest ds))) 24 | 25 | ;; '_*' ⇒ two cases: 26 | ;; 1) no more pattern ⇒ matches anything 27 | ;; 2) with remaining pattern, try every split point 28 | (= (first ps) '_*) 29 | (let [ps-rest (rest ps)] 30 | (if (empty? ps-rest) 31 | true ;; Case 1: No more pattern elements after _*, so it matches anything 32 | ;; Case 2: Try matching remaining pattern at each possible position 33 | (loop [k 0] 34 | (cond 35 | ;; We've gone beyond the end of ds, no match 36 | (> k (count ds)) 37 | false 38 | 39 | ;; Try matching rest of pattern against rest of data starting at position k 40 | (match-seq ps-rest (drop k ds)) 41 | true 42 | 43 | ;; Try next position 44 | :else 45 | (recur (inc k)))))) 46 | 47 | ;; nested list/vector ⇒ recurse 48 | (and (sequential? (first ps)) 49 | (sequential? (first ds))) 50 | (and (match-sexpr (first ps) (first ds)) 51 | (recur (rest ps) (rest ds))) 52 | 53 | ;; literal equality 54 | :else 55 | (and (= (first ps) (first ds)) 56 | (recur (rest ps) (rest ds)))))] 57 | (match-seq pattern data)) 58 | (= pattern '_?) true 59 | (= pattern '_*) true 60 | ;; atoms ⇒ direct equality 61 | :else 62 | (= pattern data))) 63 | 64 | (defn find-match* 65 | [pattern-sexpr zloc] 66 | (loop [loc zloc] 67 | (when-not (z/end? loc) 68 | (let [form (try (z/sexpr loc) 69 | (catch Exception _e 70 | ::continue))] 71 | (if (= ::continue form) 72 | (recur (z/next loc)) 73 | (if (match-sexpr pattern-sexpr form) 74 | loc 75 | (recur (z/next loc)))))))) 76 | 77 | (defn find-match [pattern-str code-str] 78 | (find-match* (z/sexpr (z/of-string pattern-str)) 79 | (z/of-string code-str))) -------------------------------------------------------------------------------- /src/clojure_mcp/tools/directory_tree/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.directory-tree.tool 2 | "Implementation of the directory-tree tool using the tool-system multimethod approach." 3 | (:require 4 | [clojure-mcp.tool-system :as tool-system] 5 | [clojure-mcp.tools.directory-tree.core :as core] 6 | [clojure-mcp.utils.valid-paths :as valid-paths])) 7 | 8 | ;; Factory function to create the tool configuration 9 | (defn create-directory-tree-tool 10 | "Creates the directory-tree tool configuration. 11 | 12 | Parameters: 13 | - nrepl-client-atom: Atom containing the nREPL client" 14 | [nrepl-client-atom] 15 | {:tool-type :directory-tree 16 | :nrepl-client-atom nrepl-client-atom}) 17 | 18 | ;; Implement the required multimethods for the directory-tree tool 19 | (defmethod tool-system/tool-name :directory-tree [_] 20 | "LS") 21 | 22 | (defmethod tool-system/tool-description :directory-tree [_] 23 | "Returns a recursive tree view of files and directories starting from the specified path. The path parameter must be an absolute path, not a relative path. You should generally prefer the `glob_files` and `fx_grep` tools, if you know which directories to search.") 24 | 25 | (defmethod tool-system/tool-schema :directory-tree [_] 26 | {:type :object 27 | :properties {:path {:type :string} 28 | :max_depth {:type :integer 29 | :description "Maximum depth to traverse (optional)"} 30 | :limit {:type :integer 31 | :description "Maximum number of entries to show (default: 100)"}} 32 | :required [:path]}) 33 | 34 | (defmethod tool-system/validate-inputs :directory-tree [{:keys [nrepl-client-atom]} inputs] 35 | (let [{:keys [path max_depth limit]} inputs 36 | nrepl-client @nrepl-client-atom] 37 | (when-not path 38 | (throw (ex-info "Missing required parameter: path" {:inputs inputs}))) 39 | 40 | ;; Use the existing validate-path-with-client function 41 | (let [validated-path (valid-paths/validate-path-with-client path nrepl-client)] 42 | ;; Return validated inputs with normalized path 43 | (cond-> {:path validated-path} 44 | ;; Only include max_depth if provided 45 | max_depth (assoc :max_depth max_depth) 46 | ;; Only include limit if provided 47 | limit (assoc :limit limit))))) 48 | 49 | (defmethod tool-system/execute-tool :directory-tree [_ inputs] 50 | (let [{:keys [path max_depth limit]} inputs] 51 | (core/directory-tree path :max-depth max_depth :limit (or limit 100)))) 52 | 53 | (defmethod tool-system/format-results :directory-tree [_ result] 54 | (if (and (map? result) (:error result)) 55 | ;; If there's an error, return it with error flag true 56 | {:result [(:error result)] 57 | :error true} 58 | ;; Otherwise, return the directory tree string 59 | {:result [result] 60 | :error false})) 61 | 62 | ;; Backward compatibility function that returns the registration map 63 | (defn directory-tree-tool 64 | "Returns the registration map for the directory-tree tool. 65 | 66 | Parameters: 67 | - nrepl-client-atom: Atom containing the nREPL client" 68 | [nrepl-client-atom] 69 | (tool-system/registration-map (create-directory-tree-tool nrepl-client-atom))) 70 | -------------------------------------------------------------------------------- /resources/configs/example-component-filtering.edn: -------------------------------------------------------------------------------- 1 | ;; Example configuration for component filtering 2 | ;; This file demonstrates how to control which tools, prompts, and resources 3 | ;; are exposed by your MCP server 4 | 5 | {;; =================== 6 | ;; TOOLS FILTERING 7 | ;; =================== 8 | 9 | ;; Option 1: Enable only specific tools (allowlist approach) 10 | ;; Uncomment to use only these tools: 11 | ;; :enable-tools [:clojure-eval :read-file :file-write :grep :glob-files] 12 | 13 | ;; Option 2: Disable specific tools (denylist approach) 14 | ;; Uncomment to use all tools except these: 15 | ;; :disable-tools [:dispatch-agent :architect :code-critique] 16 | 17 | ;; Option 3: Minimal REPL-only configuration 18 | ;; :enable-tools [:clojure-eval] 19 | 20 | ;; Option 4: Read-only exploration (no file writes or code execution) 21 | ;; :enable-tools [:read-file :grep :glob-files :LS :clojure-inspect-project] 22 | 23 | ;; =================== 24 | ;; PROMPTS FILTERING 25 | ;; =================== 26 | 27 | ;; Enable only specific prompts 28 | ;; :enable-prompts ["clojure_repl_system_prompt" "chat-session-summarize"] 29 | 30 | ;; Or disable specific prompts 31 | ;; :disable-prompts ["scratch-pad-load" "scratch-pad-save-as"] 32 | 33 | ;; Minimal prompt configuration 34 | ;; :enable-prompts ["clojure_repl_system_prompt"] 35 | 36 | ;; =================== 37 | ;; RESOURCES FILTERING 38 | ;; =================== 39 | 40 | ;; Enable only specific resources 41 | ;; :enable-resources ["PROJECT_SUMMARY.md" "README.md"] 42 | 43 | ;; Or disable specific resources 44 | ;; :disable-resources ["CLAUDE.md" "LLM_CODE_STYLE.md"] 45 | 46 | ;; No resources at all 47 | ;; :enable-resources [] 48 | 49 | ;; =================== 50 | ;; COMPLETE EXAMPLES 51 | ;; =================== 52 | 53 | ;; Example 1: Minimal REPL Server 54 | ;; Uncomment this entire block for a minimal REPL-only server: 55 | #_{:enable-tools [:clojure-eval] 56 | :enable-prompts ["clojure_repl_system_prompt"] 57 | :enable-resources []} 58 | 59 | ;; Example 2: Read-Only Code Review Server 60 | ;; Uncomment this entire block for read-only exploration: 61 | #_{:enable-tools [:read-file :grep :glob-files :LS :clojure-inspect-project] 62 | :enable-prompts ["clojure_repl_system_prompt"] 63 | :enable-resources ["PROJECT_SUMMARY.md" "README.md"]} 64 | 65 | ;; Example 3: Development Server Without Agents 66 | ;; Uncomment this entire block for full dev capabilities minus agents: 67 | #_{:disable-tools [:dispatch-agent :architect :code-critique] 68 | :disable-resources ["CLAUDE.md"]} 69 | 70 | ;; Example 4: File Operations Only 71 | ;; Uncomment this entire block for file-focused operations: 72 | #_{:enable-tools [:read-file :file-edit :file-write :bash :grep :glob-files] 73 | :enable-prompts ["clojure_repl_system_prompt"] 74 | :enable-resources ["PROJECT_SUMMARY.md"]} 75 | 76 | ;; =================== 77 | ;; NOTES 78 | ;; =================== 79 | ;; 80 | ;; - Tools can be specified as keywords (:tool-name) or strings ("tool_name") 81 | ;; - Prompts and resources must be specified as strings 82 | ;; - Enable lists: When specified, ONLY those items are enabled 83 | ;; - Disable lists: Applied after enable filtering to remove items 84 | ;; - Empty enable list [] means nothing is enabled 85 | ;; - nil or missing enable list means all items start enabled 86 | ;; 87 | ;; Copy this file to .clojure-mcp/config.edn in your project and 88 | ;; uncomment the configuration that matches your needs. 89 | } 90 | -------------------------------------------------------------------------------- /LLM_CODE_STYLE.md: -------------------------------------------------------------------------------- 1 | # LLM Code Style Preferences 2 | 3 | ## Clojure Style Guidelines 4 | 5 | ### Conditionals 6 | - Use `if` for single condition checks, not `cond` 7 | - Only use `cond` for multiple condition branches 8 | - Prefer `if-let` and `when-let` for binding and testing a value in one step 9 | - Consider `when` for conditionals with single result and no else branch 10 | - consider `cond->`, and `cond->>` 11 | 12 | ### Variable Binding 13 | - Minimize code points by avoiding unnecessary `let` bindings 14 | - Only use `let` when a value is used multiple times or when clarity demands it 15 | - Inline values used only once rather than binding them to variables 16 | - Use threading macros (`->`, `->>`) to eliminate intermediate bindings 17 | 18 | ### Parameters & Destructuring 19 | - Use destructuring in function parameters when accessing multiple keys 20 | - Example: `[{:keys [::zloc ::match-form] :as ctx}]` for namespaced keys instead of separate `let` bindings 21 | - Example: `[{:keys [zloc match-form] :as ctx}]` for regular keywords 22 | 23 | ### Control Flow 24 | - Track actual values instead of boolean flags where possible 25 | - Use early returns with `when` rather than deeply nested conditionals 26 | - Return `nil` for "not found" conditions rather than objects with boolean flags 27 | 28 | ### Comments 29 | - Do not include comments in generated code, unless specifically asked to. 30 | 31 | ### Nesting 32 | - Minimize nesting levels by using proper control flow constructs 33 | - Use threading macros (`->`, `->>`) for sequential operations 34 | 35 | ### Function Design 36 | - Functions should generally do one thing 37 | - Pure functions preferred over functions with side effects 38 | - Return useful values that can be used by callers 39 | - smaller functions make edits faster and reduce the number of tokens 40 | - reducing tokens makes me happy 41 | 42 | ### Library Preferences 43 | - Prefer `clojure.string` functions over Java interop for string operations 44 | - Use `str/ends-with?` instead of `.endsWith` 45 | - Use `str/starts-with?` instead of `.startsWith` 46 | - Use `str/includes?` instead of `.contains` 47 | - Use `str/blank?` instead of checking `.isEmpty` or `.trim` 48 | - Follow Clojure naming conventions (predicates end with `?`) 49 | - Favor built-in Clojure functions that are more expressive and idiomatic 50 | 51 | ### REPL best pratices 52 | - Always reload namespaces with `:reload` flag: `(require '[namespace] :reload)` 53 | - Always change into namespaces that you are working on 54 | 55 | ### Testing Best Practices 56 | - Always reload namespaces before running tests with `:reload` flag: `(require '[namespace] :reload)` 57 | - Test both normal execution paths and error conditions 58 | 59 | ### Using Shell Commands 60 | - Prefer the idiomatic `clojure.java.shell/sh` for executing shell commands 61 | - Always handle potential errors from shell command execution 62 | - Use explicit working directory for relative paths: `(shell/sh "cmd" :dir "/path")` 63 | - For testing builds and tasks, run `clojure -X:test` instead of running tests piecemeal 64 | - When capturing shell output, remember it may be truncated for very large outputs 65 | - Consider using shell commands for tasks that have mature CLI tools like diffing or git operations 66 | 67 | - **Context Maintenance**: 68 | - Use `clojure_eval` with `:reload` to ensure you're working with the latest code 69 | - always switch into `(in-ns ...)` the namespace that you are working on 70 | - Keep function and namespace references fully qualified when crossing namespace boundaries 71 | 72 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/agent_tool_builder/default_agents.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.agent-tool-builder.default-agents 2 | "Default agent configurations that replicate the functionality of 3 | the original hardcoded agent tools (dispatch_agent, architect, code_critique)" 4 | (:require [clojure.java.io :as io])) 5 | 6 | (defn dispatch-agent-config 7 | "Configuration for the dispatch agent - a general purpose agent with read-only tools" 8 | [] 9 | {:id :dispatch-agent 10 | :name "dispatch_agent" 11 | :description (slurp (io/resource "clojure_mcp/tools/dispatch_agent/description.md")) 12 | :system-message (slurp (io/resource "clojure_mcp/tools/dispatch_agent/system_message.md")) 13 | :context true ; Uses default code index and project summary 14 | :enable-tools [:LS :read_file :grep :glob_files :clojure_inspect_project] 15 | :memory-size 100}) 16 | 17 | (defn architect-config 18 | "Configuration for the architect - technical planning and analysis agent" 19 | [] 20 | {:id :architect 21 | :name "architect" 22 | :description (slurp (io/resource "clojure_mcp/tools/architect/description.md")) 23 | :system-message (slurp (io/resource "clojure_mcp/tools/architect/system_message.md")) 24 | :context false ; No default context 25 | :enable-tools [:LS :read_file :grep :glob_files :clojure_inspect_project] 26 | :memory-size 100}) 27 | 28 | (defn code-critique-config 29 | "Configuration for the code critique agent - provides code improvement feedback" 30 | [] 31 | {:id :code-critique 32 | :name "code_critique" 33 | :description (slurp (io/resource "clojure_mcp/tools/code_critique/description.md")) 34 | :system-message (slurp (io/resource "clojure_mcp/tools/code_critique/system_message.md")) 35 | :context false ; No context needed 36 | :enable-tools nil ; No tools needed 37 | :memory-size 35}) 38 | 39 | (defn parent-agent-config 40 | "Configuration for the parent agent - has all tools and uses Clojure REPL system prompt" 41 | [] 42 | {:id :parent-agent 43 | :name "parent_agent" 44 | :description "Parent agent with all tools and Clojure REPL system prompt" 45 | :system-message (str (slurp (io/resource "clojure-mcp/prompts/system/clojure_repl_form_edit.md")) 46 | (slurp (io/resource "clojure-mcp/prompts/system/clojure_form_edit.md"))) 47 | ;; :context true ; Uses default code index and project summary 48 | :enable-tools [:all] ; Give access to all available tools 49 | :memory-size false}) 50 | 51 | (defn make-default-agents 52 | "Returns a vector of default agent configurations. 53 | These agents are always available unless explicitly overridden by user configuration." 54 | [] 55 | [(dispatch-agent-config) 56 | (architect-config) 57 | (code-critique-config)]) 58 | 59 | (defn default-agent-ids 60 | "Returns a set of default agent IDs for easy checking" 61 | [] 62 | #{:dispatch-agent :architect :code-critique}) 63 | 64 | (defn merge-tool-config-into-agent 65 | "Merges tool-specific configuration from :tools-config into an agent configuration. 66 | Only merges keys that make sense for agent configuration. 67 | 68 | Args: 69 | - agent-config: The base agent configuration 70 | - tool-config: The tool-specific configuration from :tools-config 71 | 72 | Returns: Merged agent configuration" 73 | [agent-config tool-config] 74 | (if-not tool-config 75 | agent-config 76 | ;; Merge specific keys from tool-config 77 | (merge agent-config 78 | (select-keys tool-config 79 | [:name :context :model :system-message 80 | :enable-tools :disable-tools :description 81 | :memory-size])))) 82 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/figwheel/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.figwheel.tool 2 | (:require 3 | [taoensso.timbre :as log] 4 | [clojure.string :as string] 5 | [clojure-mcp.nrepl :as nrepl] 6 | [clojure-mcp.tool-system :as tool-system] 7 | [clojure-mcp.tools.eval.tool :as eval-tool] 8 | [clojure-mcp.tools.eval.core :as eval-core])) 9 | 10 | (defn start-figwheel [nrepl-client-atom build] 11 | (let [start-code (format 12 | ;; TODO we need to check if its already running 13 | ;; here and only initialize if it isn't 14 | "(do (require (quote figwheel.main)) (figwheel.main/start %s))" 15 | (pr-str build))] 16 | (log/info "Starting Figwheel...") 17 | (try 18 | (nrepl/eval-code @nrepl-client-atom start-code :session-type :figwheel) 19 | (log/info "Figwheel started (or command sent)") 20 | (catch Exception e 21 | (log/error e "ERROR in figwheel start"))) 22 | :figwheel)) 23 | 24 | (defn create-figwheel-eval-tool 25 | "Creates the evaluation tool configuration" 26 | [nrepl-client-atom {:keys [figwheel-build] :as _config}] 27 | (start-figwheel nrepl-client-atom figwheel-build) 28 | {:tool-type ::figwheel-eval 29 | :nrepl-client-atom nrepl-client-atom 30 | :timeout 30000 31 | :session-type :figwheel}) 32 | 33 | ;; delegate schema validate-inputs and format-results to clojure-eval 34 | (derive ::figwheel-eval ::eval-tool/clojure-eval) 35 | 36 | (defmethod tool-system/tool-name ::figwheel-eval [_] 37 | "clojurescript_eval") 38 | 39 | (defmethod tool-system/tool-description ::figwheel-eval [_] 40 | "Takes a ClojureScript Expression and evaluates it in the current namespace. For example, providing `(+ 1 2)` will evaluate to 3. 41 | 42 | **Project File Access**: Can load and use any ClojureScript file from your project with `(require '[your-namespace.core :as core] :reload)`. Always use `:reload` to ensure you get the latest version of files. Access functions, examine state with `@your-atom`, and manipulate application data for debugging and testing. 43 | 44 | **Important**: Both `require` and `ns` `:require` clauses can only reference actual files from your project, not namespaces created in the same REPL session. 45 | 46 | **CRITICAL CONSTRAINT**: This ClojureScript REPL can only evaluate ONE expression. If multiple expressions are submitted the rest will be ignored. You can submit multiple expressions joined in a `(do ...)` block. 47 | 48 | **Namespace Rules**: Namespaces must be declared at the top level separately: 49 | ```clojure 50 | ;; First evaluate namespace 51 | (ns example.namespace) 52 | 53 | ;; Then evaluate functions 54 | (do 55 | (defn add [a b] (+ a b)) 56 | (add 5 9)) 57 | ``` 58 | 59 | **WILL NOT WORK**: 60 | ```clojure 61 | (do 62 | (ns example.namespace) 63 | (defn add [a b] (+ a b))) 64 | ``` 65 | 66 | JavaScript interop is fully supported including `js/console.log`, `js/setTimeout`, DOM APIs, etc. 67 | 68 | **IMPORTANT**: This repl is intended for CLOJURESCRIPT CODE only.") 69 | 70 | (defmethod tool-system/execute-tool ::figwheel-eval [{:keys [nrepl-client-atom session-type]} inputs] 71 | (assert session-type) 72 | (assert (:code inputs)) 73 | ;; :code has to exist at this point 74 | (let [code (:code inputs (get inputs "code"))] 75 | ;; *ns* doesn't work on ClojureScript and its confusing for the LLM 76 | (if (= (string/trim code) "*ns*") 77 | {:outputs [[:value (nrepl/current-ns @nrepl-client-atom session-type)]] 78 | :error false} 79 | (eval-core/evaluate-with-repair @nrepl-client-atom (assoc inputs :session-type session-type))))) 80 | 81 | ;; config needs :fig 82 | (defn figwheel-eval [nrepl-client-atom config] 83 | {:pre [config (:figwheel-build config)]} 84 | (tool-system/registration-map (create-figwheel-eval-tool nrepl-client-atom config))) 85 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/bash/tool_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.bash.tool-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [clojure-mcp.tools.bash.tool :as tool] 4 | [clojure-mcp.config :as config])) 5 | 6 | (deftest test-create-bash-tool-with-config 7 | (testing "Tool creation with timeout configuration" 8 | (let [nrepl-client-atom (atom {::config/config 9 | {:tools-config {:bash {:default-timeout-ms 60000}} 10 | :nrepl-user-dir "/tmp"}}) 11 | tool-config (tool/create-bash-tool nrepl-client-atom)] 12 | (is (= :bash (:tool-type tool-config))) 13 | (is (= 60000 (:timeout_ms tool-config)) 14 | "Timeout should be set from config"))) 15 | 16 | (testing "Tool creation with working directory configuration" 17 | (let [nrepl-client-atom (atom {::config/config 18 | {:tools-config {:bash {:working-dir "/opt/project"}} 19 | :nrepl-user-dir "/tmp"}}) 20 | tool-config (tool/create-bash-tool nrepl-client-atom)] 21 | (is (= :bash (:tool-type tool-config))) 22 | (is (= "/opt/project" (:working-dir tool-config)) 23 | "Working directory should be set from config"))) 24 | 25 | (testing "Tool creation with bash-over-nrepl from tool config" 26 | (let [nrepl-client-atom (atom {::config/config 27 | {:tools-config {:bash {:bash-over-nrepl false}} 28 | :bash-over-nrepl true ; Global config says true 29 | :nrepl-user-dir "/tmp"}}) 30 | tool-config (tool/create-bash-tool nrepl-client-atom)] 31 | (is (= :bash (:tool-type tool-config))) 32 | (is (nil? (:nrepl-session tool-config)) 33 | "Session should be nil when tool config overrides to false"))) 34 | 35 | (testing "Tool creation with multiple config options" 36 | (let [nrepl-client-atom (atom {::config/config 37 | {:tools-config {:bash {:default-timeout-ms 120000 38 | :working-dir "/home/user" 39 | :bash-over-nrepl true}} 40 | :nrepl-user-dir "/tmp"}}) 41 | tool-config (tool/create-bash-tool nrepl-client-atom)] 42 | (is (= :bash (:tool-type tool-config))) 43 | (is (= 120000 (:timeout_ms tool-config))) 44 | (is (= "/home/user" (:working-dir tool-config))))) 45 | 46 | (testing "Tool creation without config uses defaults" 47 | (let [nrepl-client-atom (atom {::config/config 48 | {:nrepl-user-dir "/tmp"}}) 49 | tool-config (tool/create-bash-tool nrepl-client-atom)] 50 | (is (= :bash (:tool-type tool-config))) 51 | (is (= 180000 (:timeout_ms tool-config)) 52 | "Default timeout should be 180000ms") 53 | (is (= "/tmp" (:working-dir tool-config)) 54 | "Working dir should be nrepl-user-dir"))) 55 | 56 | (testing "Config key default-timeout-ms is not passed through" 57 | (let [nrepl-client-atom (atom {::config/config 58 | {:tools-config {:bash {:default-timeout-ms 90000 59 | :some-other-key "value"}} 60 | :nrepl-user-dir "/tmp"}}) 61 | tool-config (tool/create-bash-tool nrepl-client-atom)] 62 | (is (= 90000 (:timeout_ms tool-config)) 63 | "default-timeout-ms should be converted to timeout_ms") 64 | (is (nil? (:default-timeout-ms tool-config)) 65 | "default-timeout-ms should not be in final config") 66 | (is (= "value" (:some-other-key tool-config)) 67 | "Other config keys should pass through")))) -------------------------------------------------------------------------------- /test/clojure_mcp/tools/bash/truncation_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.bash.truncation-test 2 | "Test for bash tool output truncation functionality" 3 | (:require [clojure.test :refer [deftest is testing]] 4 | [clojure-mcp.tools.bash.core :as bash-core] 5 | [clojure-mcp.nrepl :as nrepl] 6 | [clojure.string :as str])) 7 | 8 | (deftest test-truncate-with-limit 9 | (testing "truncate-with-limit function" 10 | ;; Test no truncation needed 11 | (is (= "short string" 12 | (#'bash-core/truncate-with-limit "short string" 100))) 13 | 14 | ;; Test truncation 15 | (let [long-str (apply str (repeat 1000 "a")) 16 | truncated (#'bash-core/truncate-with-limit long-str 100)] 17 | (is (= 100 (count truncated))) 18 | (is (str/ends-with? truncated "... (truncated)")) 19 | (is (= (subs long-str 0 84) (subs truncated 0 84)))) 20 | 21 | ;; Test exact limit (no truncation) 22 | (let [exact-str (apply str (repeat 100 "b"))] 23 | (is (= exact-str (#'bash-core/truncate-with-limit exact-str 100)))))) 24 | 25 | (deftest test-execute-bash-command-truncation 26 | (testing "execute-bash-command applies truncation to outputs" 27 | ;; Generate a command that produces long output 28 | (let [;; Create a string that will exceed truncation limits 29 | long-content (apply str (repeat 10000 "x")) 30 | ;; Command that outputs to both stdout and stderr 31 | command (str "echo '" long-content "' && echo '" long-content "' >&2") 32 | result (bash-core/execute-bash-command nil {:command command :timeout-ms 30000})] 33 | 34 | ;; Check that outputs were truncated 35 | (is (< (count (:stdout result)) (count long-content))) 36 | (is (< (count (:stderr result)) (count long-content))) 37 | 38 | ;; Check truncation messages 39 | (is (str/ends-with? (:stdout result) "... (truncated)")) 40 | (is (str/ends-with? (:stderr result) "... (truncated)")) 41 | 42 | ;; Check that stderr gets roughly half the space 43 | (let [total-limit (int (* nrepl/truncation-length 0.85)) 44 | stderr-limit (quot total-limit 2)] 45 | (is (<= (count (:stderr result)) stderr-limit)) 46 | ;; stdout should get remaining space (at least 500 chars) 47 | (is (>= (count (:stdout result)) 500)))))) 48 | 49 | (deftest test-execute-bash-command-short-output 50 | (testing "execute-bash-command doesn't truncate short outputs" 51 | (let [command "echo 'Hello stdout' && echo 'Hello stderr' >&2" 52 | result (bash-core/execute-bash-command nil {:command command :timeout-ms 30000})] 53 | 54 | ;; Check outputs are not truncated 55 | (is (= "Hello stdout" (str/trim (:stdout result)))) 56 | (is (= "Hello stderr" (str/trim (:stderr result)))) 57 | 58 | ;; No truncation messages 59 | (is (not (str/ends-with? (:stdout result) "... (truncated)"))) 60 | (is (not (str/ends-with? (:stderr result) "... (truncated)")))))) 61 | 62 | (deftest test-consistent-truncation-behavior 63 | (testing "Local and nREPL execution have consistent truncation" 64 | ;; This test would compare outputs from both execution modes 65 | ;; but would require a running nREPL server, so we just verify 66 | ;; that the truncation limit calculation is the same 67 | (let [expected-limit (int (* nrepl/truncation-length 0.85)) 68 | ;; Extract the limit from generate-shell-eval-code 69 | shell-code (bash-core/generate-shell-eval-code "test" nil 1000) 70 | ;; Find the nrepl-limit value in the generated code 71 | limit-match (re-find #"nrepl-limit (\d+)" shell-code) 72 | generated-limit (when limit-match (Integer/parseInt (second limit-match)))] 73 | 74 | (is (= expected-limit generated-limit) 75 | "Both execution modes should use the same truncation limit")))) 76 | -------------------------------------------------------------------------------- /src/clojure_mcp/agent/langchain/schema.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.agent.langchain.schema 2 | (:require 3 | [clojure.string :as string]) 4 | (:import 5 | [dev.langchain4j.model.chat.request.json 6 | JsonAnyOfSchema 7 | JsonArraySchema 8 | JsonBooleanSchema 9 | JsonEnumSchema 10 | JsonIntegerSchema 11 | JsonNumberSchema 12 | JsonObjectSchema 13 | JsonStringSchema])) 14 | 15 | (defmulti edn->sch 16 | (fn [{:keys [type enum] :as json-edn}] 17 | (cond 18 | (vector? type) :mixed-types 19 | (and type (keyword type)) (keyword type) 20 | enum :enum 21 | :else (throw (ex-info "By JSON data" {:json-edn json-edn}))))) 22 | 23 | (defn any-type-schema 24 | "Creates a schema that accepts any JSON type (except null)" 25 | [] 26 | (-> (JsonAnyOfSchema/builder) 27 | (.anyOf [(.build (JsonStringSchema/builder)) 28 | (.build (JsonNumberSchema/builder)) 29 | (.build (JsonBooleanSchema/builder)) 30 | (.build (JsonObjectSchema/builder)) 31 | (-> (JsonArraySchema/builder) 32 | (.items (.build (JsonStringSchema/builder))) ; Simple array of strings as default 33 | .build)]) 34 | .build)) 35 | 36 | (defmethod edn->sch :mixed-types [{:keys [type description]}] 37 | (let [schemas (keep (fn [t] 38 | (cond 39 | (= t "null") nil ; Skip null for now due to instantiation issues 40 | (= t "object") (.build (JsonObjectSchema/builder)) ; Create empty object schema 41 | ;; For array, create one that accepts any type of items 42 | (= t "array") (-> (JsonArraySchema/builder) 43 | (.items (any-type-schema)) 44 | .build) 45 | :else (edn->sch {:type (keyword t)}))) 46 | type)] 47 | (cond-> (JsonAnyOfSchema/builder) 48 | description (.description description) 49 | :always (.anyOf schemas) 50 | :always (.build)))) 51 | 52 | (defmethod edn->sch :string [{:keys [description]}] 53 | (cond-> (JsonStringSchema/builder) 54 | description (.description description) 55 | :always (.build))) 56 | 57 | (defmethod edn->sch :number [{:keys [description]}] 58 | (cond-> (JsonNumberSchema/builder) 59 | description (.description description) 60 | :always (.build))) 61 | 62 | (defmethod edn->sch :integer [{:keys [description]}] 63 | (cond-> (JsonIntegerSchema/builder) 64 | description (.description description) 65 | :always (.build))) 66 | 67 | (defmethod edn->sch :boolean [{:keys [description]}] 68 | (cond-> (JsonBooleanSchema/builder) 69 | description (.description description) 70 | :always (.build))) 71 | 72 | ;; Note: JsonNullSchema doesn't have a builder pattern in langchain4j 73 | ;; If you need to support null, handle it within JsonAnyOfSchema 74 | ;; or check langchain4j documentation for the correct instantiation method 75 | 76 | (defmethod edn->sch :enum [{:keys [enum]}] 77 | (assert (every? string? enum)) 78 | (assert (not-empty enum)) 79 | (-> (JsonEnumSchema/builder) 80 | (.enumValues (map name enum)) 81 | (.build))) 82 | 83 | (defmethod edn->sch :array [{:keys [items]}] 84 | (assert items) 85 | (-> (JsonArraySchema/builder) 86 | (.items (edn->sch items)) 87 | (.build))) 88 | 89 | (defmethod edn->sch :object [{:keys [properties description required] :as _object}] 90 | (let [obj-build 91 | (cond-> (JsonObjectSchema/builder) 92 | (not (string/blank? description)) (.description description) 93 | (not-empty required) (.required (map name required)))] 94 | (doseq [[nm edn-schema] properties] 95 | (.addProperty obj-build (name nm) (edn->sch edn-schema))) 96 | (.build obj-build))) 97 | -------------------------------------------------------------------------------- /src/clojure_mcp/agent/langchain/message_conv.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.agent.langchain.message-conv 2 | "Provides round-trip conversion between LangChain4j ChatMessage lists and EDN data. 3 | 4 | This allows for: 5 | - Converting List to EDN for manipulation 6 | - Modifying messages in EDN format 7 | - Converting EDN back to List 8 | 9 | The conversion preserves all message content and allows for easy manipulation 10 | of message metadata in pure Clojure data structures." 11 | (:require 12 | [clojure.data.json :as json]) 13 | (:import 14 | [dev.langchain4j.data.message 15 | ChatMessageSerializer 16 | ChatMessageDeserializer])) 17 | 18 | (defn messages->edn [msgs] 19 | (-> (ChatMessageSerializer/messagesToJson msgs) 20 | (json/read-str :key-fn keyword))) ; => EDN vector with keywords 21 | 22 | (defn edn->messages [edn-msgs] 23 | (-> edn-msgs 24 | json/write-str 25 | (ChatMessageDeserializer/messagesFromJson))) 26 | 27 | (defn message->edn [java-msg] 28 | (some-> java-msg 29 | list 30 | messages->edn 31 | first)) 32 | 33 | (defn edn->message [edn-msg] 34 | (some-> edn-msg 35 | list 36 | edn->messages 37 | first)) 38 | 39 | (defn parse-tool-arguments 40 | "Parse JSON string arguments in toolExecutionRequests to EDN. 41 | Makes REPL testing easier by converting arguments from strings to maps." 42 | [ai-message] 43 | (if-let [requests (:toolExecutionRequests ai-message)] 44 | (assoc ai-message 45 | :toolExecutionRequests 46 | (mapv (fn [req] 47 | (update req :arguments 48 | (fn [args] 49 | (if (string? args) 50 | (json/read-str args :key-fn keyword) 51 | args)))) 52 | requests)) 53 | ai-message)) 54 | 55 | (defn parse-messages-tool-arguments 56 | "Parse tool arguments in all AI messages within a message list" 57 | [messages] 58 | (mapv (fn [msg] 59 | (if (= "AI" (:type msg)) 60 | (parse-tool-arguments msg) 61 | msg)) 62 | messages)) 63 | 64 | ;; AiMessage 65 | ;; {:text "Hello", :toolExecutionRequests [], :type "AI"} 66 | 67 | ;; UserMessage 68 | ;; {:contents [{:text "Hello", :type "TEXT"}], :type "USER"} 69 | 70 | ;; SystemMessage 71 | ;; {:text "Hello", :type "SYSTEM"} 72 | 73 | ;; {:toolExecutionRequests [{:id "call_KLFov8DW5Ar2EOHNMPtzFlQJ", 74 | ;; :name "think", 75 | ;; :arguments "{\"thought\":\"First thought: Evaluate the basic arithmetic problem of adding 2 and 2, combining two discrete units with another two discrete units.\"}"}], 76 | ;; :type "AI"} 77 | 78 | ;; ToolExecutionResultMessage 79 | ;; {:id "idasdf", 80 | ;; :toolName "think", 81 | ;; :text "your thought has been logged", 82 | ;; :type "TOOL_EXECUTION_RESULT"} 83 | 84 | #_(-> (SystemMessage/from "Hello") 85 | message->edn) 86 | 87 | (defn chat-request->edn 88 | "Convert a ChatRequest to EDN data for storage and manipulation." 89 | [chat-request] 90 | {:messages (messages->edn (.messages chat-request)) 91 | :maxTokens (.maxTokens chat-request) 92 | :temperature (.temperature chat-request) 93 | :topP (.topP chat-request)}) 94 | 95 | (defn edn->chat-request 96 | "Convert EDN data back to a ChatRequest. 97 | 98 | Note: This creates a ChatRequest builder - caller must build() it." 99 | [{:keys [messages maxTokens temperature topP]}] 100 | (cond-> (dev.langchain4j.model.chat.request.ChatRequest/builder) 101 | messages (.messages (edn->messages messages)) 102 | maxTokens (.maxTokens maxTokens) 103 | temperature (.temperature temperature) 104 | topP (.topP topP))) 105 | -------------------------------------------------------------------------------- /test/clojure_mcp/config/tools_config_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.config.tools-config-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [clojure-mcp.config :as config] 4 | [clojure-mcp.agent.langchain.model :as model])) 5 | 6 | (deftest test-get-tools-config 7 | (testing "Returns tools config when present" 8 | (let [nrepl-client-map {::config/config 9 | {:tools-config {:dispatch_agent {:model :openai/o3} 10 | :architect {:model :anthropic/claude}}}}] 11 | (is (= {:dispatch_agent {:model :openai/o3} 12 | :architect {:model :anthropic/claude}} 13 | (config/get-tools-config nrepl-client-map))))) 14 | 15 | (testing "Returns empty map when not configured" 16 | (let [nrepl-client-map {::config/config {}}] 17 | (is (= {} (config/get-tools-config nrepl-client-map)))))) 18 | 19 | (deftest test-get-tool-config 20 | (testing "Returns config for specific tool" 21 | (let [nrepl-client-map {::config/config 22 | {:tools-config {:dispatch_agent {:model :openai/o3} 23 | :architect {:model :anthropic/claude}}}}] 24 | (is (= {:model :openai/o3} 25 | (config/get-tool-config nrepl-client-map :dispatch_agent))) 26 | (is (= {:model :anthropic/claude} 27 | (config/get-tool-config nrepl-client-map :architect))))) 28 | 29 | (testing "Returns nil for unconfigured tool" 30 | (let [nrepl-client-map {::config/config 31 | {:tools-config {:dispatch_agent {:model :openai/o3}}}}] 32 | (is (nil? (config/get-tool-config nrepl-client-map :missing_tool))))) 33 | 34 | (testing "Handles string tool IDs" 35 | (let [nrepl-client-map {::config/config 36 | {:tools-config {:dispatch_agent {:model :openai/o3}}}}] 37 | (is (= {:model :openai/o3} 38 | (config/get-tool-config nrepl-client-map "dispatch_agent")))))) 39 | 40 | (deftest test-get-tool-model 41 | (testing "Creates model from tool config with default :model key" 42 | (binding [model/*env-overrides* {"ANTHROPIC_API_KEY" "test-key"}] 43 | (let [nrepl-client-map {::config/config 44 | {:tools-config {:dispatch_agent {:model :anthropic/claude-3-haiku-20240307}} 45 | :models {:anthropic/claude-3-haiku-20240307 46 | {:model-name "claude-3-haiku-20240307" 47 | :api-key [:env "ANTHROPIC_API_KEY"]}}}} 48 | ;; Model creation will succeed with test API key 49 | model (model/get-tool-model nrepl-client-map :dispatch_agent)] 50 | (is (some? model) "Should create model with test API key")))) 51 | 52 | (testing "Creates model with custom config key" 53 | (binding [model/*env-overrides* {"OPENAI_API_KEY" "test-key"}] 54 | (let [nrepl-client-map {::config/config 55 | {:tools-config {:architect {:primary-model :openai/gpt-4o}} 56 | :models {:openai/gpt-4o 57 | {:model-name "gpt-4o" 58 | :api-key [:env "OPENAI_API_KEY"]}}}} 59 | model (model/get-tool-model nrepl-client-map :architect :primary-model)] 60 | (is (some? model) "Should create model with test API key")))) 61 | 62 | (testing "Returns nil when tool not configured" 63 | (let [nrepl-client-map {::config/config {:tools-config {}}}] 64 | (is (nil? (model/get-tool-model nrepl-client-map :missing_tool))))) 65 | 66 | (testing "Returns nil when model key not in tool config" 67 | (let [nrepl-client-map {::config/config 68 | {:tools-config {:dispatch_agent {:other-key "value"}}}}] 69 | (is (nil? (model/get-tool-model nrepl-client-map :dispatch_agent)))))) 70 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # ClojureMCP Documentation 2 | 3 | This directory contains documentation for creating MCP (Model Context Protocol) components using ClojureMCP. 4 | 5 | ## Documentation Files 6 | 7 | ### Configuration Guides 8 | 9 | ### [Component Filtering Configuration](component-filtering.md) 10 | Learn how to control which tools, prompts, and resources are exposed by your MCP server using enable/disable lists. Perfect for creating focused, secure, or specialized MCP servers with only the components you need. 11 | 12 | ### [Model Configuration](model-configuration.md) 13 | Configure custom LLM models with your own API keys, endpoints, and parameters. Support for OpenAI, Anthropic, Google Gemini, and more through the LangChain4j integration. 14 | 15 | ### [Tools Configuration](tools-configuration.md) 16 | Configure individual tools with custom settings, including model selection for AI-powered tools like dispatch_agent, architect, and code_critique. 17 | 18 | ### Creating Custom Servers 19 | 20 | ### [Creating Your Own Custom MCP Server](custom-mcp-server.md) 21 | Learn how to create your own personalized MCP server by customizing tools, prompts, and resources. This is the primary way to configure ClojureMCP during the alpha phase, and it's both easy and empowering! 22 | 23 | ### [Generate Your Custom MCP Server with AI](gen-your-mcp-server.md) 24 | Welcome to the new age of MCP server configuration! Learn how to use Large Language Models to generate a fully customized Clojure MCP server by providing documentation as context and describing your needs. Includes numerous examples and prompt templates. 25 | 26 | ### [Creating Tools with ClojureMCP's Multimethod System](creating-tools-multimethod.md) 27 | Learn how to create tools using ClojureMCP's structured multimethod approach. This provides validation, error handling, and integration benefits when building tools within the ClojureMCP ecosystem. 28 | 29 | ### [Creating Tools Without ClojureMCP](creating-tools-without-clojuremcp.md) 30 | Learn how to create tools as simple Clojure maps without depending on ClojureMCP's multimethod system. This approach allows you to create standalone tools that can be easily shared and integrated into any MCP server. 31 | 32 | ### [Creating Prompts](creating-prompts.md) 33 | The standard guide for creating prompts in MCP. Prompts generate conversation contexts to help AI assistants understand specific tasks or workflows. This same approach works whether you're using ClojureMCP or creating standalone prompts. 34 | 35 | ### [Creating Resources](creating-resources.md) 36 | The standard guide for creating resources in MCP. Resources provide read-only content like documentation, configuration files, or project information. This same approach works whether you're using ClojureMCP or creating standalone resources. 37 | 38 | ## Quick Start 39 | 40 | For most users, start with [Creating Your Own Custom MCP Server](custom-mcp-server.md) to learn how to configure ClojureMCP for your specific needs. 41 | 42 | ## Key Concepts 43 | 44 | - **Tools**: Perform actions and computations 45 | - **Prompts**: Generate conversation contexts for AI assistants 46 | - **Resources**: Provide read-only content 47 | 48 | ## Quick Reference 49 | 50 | | Component | Schema | Callback Signature | 51 | |-----------|--------|-------------------| 52 | | Tool | `{:name, :description, :schema, :tool-fn}` | `(callback result-vector error-boolean)` | 53 | | Prompt | `{:name, :description, :arguments, :prompt-fn}` | `(callback {:description "...", :messages [...]})` | 54 | | Resource | `{:url, :name, :description, :mime-type, :resource-fn}` | `(callback ["content..."])` | 55 | 56 | ## Notes 57 | 58 | - **Tools** can be created either using ClojureMCP's multimethod system or as simple maps (see the tools documentation) 59 | - **Prompts** and **Resources** are always created as simple maps, making them inherently portable 60 | - All components can be tested independently without an MCP server 61 | - String keys are used for all parameter maps passed to component functions 62 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/scratch_pad/truncate.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.scratch-pad.truncate 2 | "Utilities for truncating and pretty-printing deeply nested data structures." 3 | (:require [clojure.pprint :as pprint])) 4 | 5 | (defn- make-size-symbol 6 | "Creates a symbol for size indicators like '...10_elements" 7 | [size unit] 8 | (symbol (str "..." size "_" unit))) 9 | 10 | (defn truncate-recursive 11 | [x depth max-depth ellipsis opts] 12 | (cond 13 | (> depth max-depth) ellipsis 14 | 15 | (= depth max-depth) 16 | (cond 17 | (record? x) ellipsis 18 | 19 | (map? x) 20 | (if (:show-size opts true) 21 | (into (empty x) [[ellipsis (make-size-symbol (count x) "entries")]]) 22 | ellipsis) 23 | 24 | (sequential? x) 25 | (if (and (:show-size opts true) (seq x)) 26 | (into (empty x) [(make-size-symbol (count x) "elements")]) 27 | ellipsis) 28 | 29 | (set? x) 30 | (if (and (:show-size opts true) (seq x)) 31 | #{(make-size-symbol (count x) "items")} 32 | #{ellipsis}) 33 | 34 | :else ellipsis) 35 | 36 | :else 37 | (cond 38 | (or (record? x) (map? x)) 39 | (into (empty x) 40 | (map (fn [[k v]] 41 | [k (if (not (or (map? v) (sequential? v) (set? v) (record? v))) 42 | ellipsis 43 | (truncate-recursive v (inc depth) max-depth ellipsis opts))]) 44 | x)) 45 | 46 | (sequential? x) 47 | (into (empty x) 48 | (map #(if (not (or (map? %) (sequential? %) (set? %) (record? %))) 49 | ellipsis 50 | (truncate-recursive % (inc depth) max-depth ellipsis opts)) 51 | x)) 52 | 53 | (set? x) 54 | (into (empty x) 55 | (map #(if (not (or (map? %) (sequential? %) (set? %) (record? %))) 56 | ellipsis 57 | (truncate-recursive % (inc depth) max-depth ellipsis opts)) 58 | x)) 59 | 60 | :else ellipsis))) 61 | 62 | (defn truncate-depth 63 | "Truncates a data structure at the specified depth, replacing values with ellipsis. 64 | Creates a structural overview without showing actual leaf values. 65 | 66 | Parameters: 67 | - data: The data structure to truncate 68 | - max-depth: Maximum depth to display (0-based) 69 | - opts: Optional map with: 70 | :ellipsis - Symbol to show for truncated values (default: '...) 71 | :show-size - Show collection sizes (default: true) 72 | 73 | Examples: 74 | (truncate-depth {:a {:b {:c 1}}} 1) 75 | ;; => {:a {:b '...}} 76 | 77 | (truncate-depth {:a 1 :b 2} 0) 78 | ;; => {'... '...2_entries} 79 | 80 | (truncate-depth [1 2 3 4 5] 0) 81 | ;; => ['...5_elements] 82 | 83 | At any depth: 84 | - Leaf values (strings, numbers, keywords, etc.) become ellipsis 85 | - Maps show keys with ellipsis values 86 | - Collections show structure with ellipsis for elements 87 | 88 | At max depth: 89 | - Maps show as {'... '...N_entries} 90 | - Sequences show as ['...N_elements] 91 | - Sets show as #{'...N_items} 92 | - Empty collections remain empty" 93 | ([data max-depth] 94 | (truncate-depth data max-depth {})) 95 | ([data max-depth opts] 96 | (let [ellipsis (get opts :ellipsis '...)] 97 | (truncate-recursive data 0 max-depth ellipsis opts)))) 98 | 99 | (defn pprint-truncated 100 | "Pretty prints data truncated at the specified depth. 101 | Returns the pretty-printed string. 102 | 103 | Parameters: 104 | - data: The data structure to truncate and print 105 | - max-depth: Maximum depth to display 106 | - opts: Same options as truncate-depth" 107 | ([data max-depth] 108 | (pprint-truncated data max-depth {})) 109 | ([data max-depth opts] 110 | (with-out-str 111 | (pprint/pprint (truncate-depth data max-depth opts))))) 112 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/form_edit/tool_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.form-edit.tool-test 2 | (:require 3 | [clojure.test :refer [deftest testing is use-fixtures]] 4 | [clojure-mcp.tools.form-edit.tool :as sut] 5 | [clojure-mcp.tool-system :as tool-system] 6 | [clojure-mcp.tools.unified-read-file.file-timestamps :as file-timestamps] 7 | [clojure-mcp.tools.test-utils :as test-utils] 8 | [clojure-mcp.config :as config] ; Added config require 9 | [clojure.java.io :as io])) 10 | 11 | ;; Test fixtures 12 | (def ^:dynamic *test-dir* nil) 13 | (def ^:dynamic *test-file* nil) 14 | (def ^:dynamic *client-atom* nil) 15 | (def client-atom-for-tests nil) ;; Will be set in the :once fixture 16 | 17 | (defn create-test-files-fixture [f] 18 | ;; Make sure we have a valid client atom 19 | (when (nil? test-utils/*nrepl-client-atom*) 20 | (test-utils/test-nrepl-fixture identity)) 21 | 22 | (let [test-dir (test-utils/create-test-dir) 23 | client-atom test-utils/*nrepl-client-atom* 24 | test-file-content (str "(ns test.core)\n\n" 25 | "(defn example-fn\n \"Original docstring\"\n [x y]\n #_(println \"debug value:\" x)\n (+ x y))\n\n" 26 | "(def a 1)\n\n" 27 | "#_(def unused-value 42)\n\n" 28 | "(comment\n (example-fn 1 2))\n\n" 29 | ";; Test comment\n;; spans multiple lines") 30 | ;; Make sure client atom has necessary configuration 31 | _ (config/set-config! client-atom :nrepl-user-dir test-dir) 32 | _ (config/set-config! client-atom :allowed-directories [test-dir]) 33 | _ (swap! client-atom assoc ::file-timestamps/file-timestamps {}) ; Keep this direct for now 34 | ;; Create and register the test file 35 | test-file-path (test-utils/create-and-register-test-file 36 | client-atom 37 | test-dir 38 | "test.clj" 39 | test-file-content)] 40 | (binding [*test-dir* test-dir 41 | *test-file* (io/file test-file-path) 42 | *client-atom* client-atom] 43 | (try 44 | (f) 45 | (finally 46 | (test-utils/clean-test-dir test-dir)))))) 47 | 48 | (use-fixtures :once (fn [f] 49 | ;; Make sure we have a valid nREPL client atom for tests 50 | (test-utils/test-nrepl-fixture 51 | (fn [] 52 | ;; Set up global client atom for tests 53 | (alter-var-root #'client-atom-for-tests (constantly test-utils/*nrepl-client-atom*)) 54 | ;; Run the actual test 55 | (binding [test-utils/*nrepl-client-atom* test-utils/*nrepl-client-atom*] 56 | (f)))))) 57 | (use-fixtures :each create-test-files-fixture) 58 | 59 | ;; Test helper functions 60 | (defn get-file-path [] 61 | (.getCanonicalPath *test-file*)) 62 | 63 | ;; Tests for sexp-update-tool validation 64 | (deftest sexp-replace-validation-test 65 | (testing "Sexp replace validation checks for multiple forms in match_form" 66 | (let [client-atom *client-atom* 67 | sexp-tool (sut/create-update-sexp-tool client-atom) 68 | valid-inputs {:file_path (get-file-path) 69 | :match_form "(+ x y)" 70 | :new_form "(+ x (* y 2))"} 71 | _invalid-inputs {:file_path (get-file-path) 72 | :match_form "(+ x y) (- x y)" ;; Multiple forms! 73 | :new_form "(+ x (* y 2))"} 74 | _another-invalid {:file_path (get-file-path) 75 | :match_form "(def a 1) (def b 2)" ;; Multiple forms! 76 | :new_form "(+ x (* y 2))"} 77 | ;; Test valid input is accepted 78 | validated (tool-system/validate-inputs sexp-tool valid-inputs)] 79 | (is (string? (:file_path validated))) 80 | (is (= "(+ x y)" (:match_form validated))) 81 | (is (= "(+ x (* y 2))" (:new_form validated)))))) 82 | -------------------------------------------------------------------------------- /doc/tools-configuration.md: -------------------------------------------------------------------------------- 1 | # Tools Configuration 2 | 3 | ## Overview 4 | 5 | The `:tools-config` key in `.clojure-mcp/config.edn` allows you to provide tool-specific configurations. This is particularly useful for tools that use AI models or have other configurable behavior. 6 | 7 | ## Configuration Structure 8 | 9 | ```edn 10 | {:tools-config { { }}} 11 | ``` 12 | 13 | ## Model Configuration for Tools 14 | 15 | Many tools can be configured to use specific AI models. The configuration system provides a helper function `get-tool-model` that simplifies this pattern. 16 | 17 | ### Example Configuration 18 | 19 | ```edn 20 | {:tools-config {:dispatch_agent {:model :openai/my-o3} 21 | :architect {:model :anthropic/my-claude-3} 22 | :code_critique {:model :openai/my-gpt-4o} 23 | :bash {:default-timeout-ms 60000 24 | :working-dir "/opt/project" 25 | :bash-over-nrepl false}} 26 | 27 | ;; Define the models referenced above 28 | :models {:openai/my-o3 {:model-name "o3-mini" 29 | :temperature 0.2 30 | :api-key [:env "OPENAI_API_KEY"]} 31 | :openai/my-gpt-4o {:model-name "gpt-4o" 32 | :temperature 0.3 33 | :api-key [:env "OPENAI_API_KEY"]} 34 | :anthropic/my-claude-3 {:model-name "claude-3-haiku-20240307" 35 | :api-key [:env "ANTHROPIC_API_KEY"]}}} 36 | ``` 37 | 38 | ## API Functions 39 | 40 | ### `get-tools-config` 41 | Returns the entire tools configuration map. 42 | 43 | ```clojure 44 | (config/get-tools-config nrepl-client-map) 45 | ;; => {:dispatch_agent {:model :openai/o3}, :architect {...}} 46 | ``` 47 | 48 | ### `get-tool-config` 49 | Returns configuration for a specific tool. 50 | 51 | ```clojure 52 | (config/get-tool-config nrepl-client-map :dispatch_agent) 53 | ;; => {:model :openai/o3} 54 | ``` 55 | 56 | ### `get-tool-model` 57 | Creates a model for a tool based on its configuration. This function is located in `clojure-mcp.agent.langchain.model` namespace. It's a convenience function that: 58 | 1. Looks up the tool's configuration 59 | 2. Extracts the model key (default: `:model`) 60 | 3. Creates the model using the models configuration 61 | 4. Handles errors gracefully 62 | 63 | ```clojure 64 | (require '[clojure-mcp.agent.langchain.model :as model]) 65 | 66 | ;; Using default :model key 67 | (model/get-tool-model nrepl-client-map :dispatch_agent) 68 | 69 | ;; Using custom config key 70 | (model/get-tool-model nrepl-client-map :code_critique :primary-model) 71 | (model/get-tool-model nrepl-client-map :code_critique :fallback-model) 72 | ``` 73 | 74 | ## Implementing Tool Configuration 75 | 76 | To add configuration support to a tool: 77 | 78 | ```clojure 79 | (ns my-tool.tool 80 | (:require [clojure-mcp.config :as config] 81 | [clojure-mcp.agent.langchain.model :as model])) 82 | 83 | (defn create-my-tool 84 | ([nrepl-client-atom] 85 | (create-my-tool nrepl-client-atom nil)) 86 | ([nrepl-client-atom model] 87 | (let [;; Use explicitly provided model or get from config 88 | final-model (or model 89 | (model/get-tool-model @nrepl-client-atom :my_tool))] 90 | {:tool-type :my-tool 91 | :nrepl-client-atom nrepl-client-atom 92 | :model final-model}))) 93 | ``` 94 | 95 | ## Currently Supported Tools 96 | 97 | ### AI-Powered Tools 98 | 99 | - **dispatch_agent**: Supports `:model` configuration for custom AI models 100 | - **architect**: Supports `:model` configuration for custom AI models 101 | - **code_critique**: Supports `:model` configuration for custom AI models 102 | 103 | ### Other Configurable Tools 104 | 105 | - **bash**: Command execution configuration 106 | - `:default-timeout-ms` - Default timeout in milliseconds (default: `180000` for 3 minutes) 107 | - `:working-dir` - Default working directory for commands (uses nrepl-user-dir if not set) 108 | - `:bash-over-nrepl` - Override global bash-over-nrepl setting (true/false) 109 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/glob_files/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.glob-files.tool 2 | "Implementation of the glob-files tool using the tool-system multimethod approach." 3 | (:require 4 | [clojure-mcp.tool-system :as tool-system] 5 | [clojure-mcp.tools.glob-files.core :as core] 6 | [clojure-mcp.utils.valid-paths :as valid-paths] 7 | [clojure-mcp.config :as config] ; Added config require 8 | [clojure.string :as string])) 9 | 10 | ;; Factory function to create the tool configuration 11 | (defn create-glob-files-tool 12 | "Creates the glob-files tool configuration. 13 | 14 | Parameters: 15 | - nrepl-client-atom: Atom containing the nREPL client" 16 | [nrepl-client-atom] 17 | {:tool-type :glob-files 18 | :nrepl-client-atom nrepl-client-atom}) 19 | 20 | ;; Implement the required multimethods for the glob-files tool 21 | (defmethod tool-system/tool-name :glob-files [_] 22 | "glob_files") 23 | 24 | (defmethod tool-system/tool-description :glob-files [_] 25 | "Fast file pattern matching tool that works with any codebase size. 26 | - Supports glob patterns like \"**/*.clj\" or \"src/**/*.cljs\". 27 | - Returns matching file paths sorted by modification time (most recent first). 28 | - Use this tool when you need to find files by name patterns. 29 | - When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the `dispatch_agent` tool instead") 30 | 31 | (defmethod tool-system/tool-schema :glob-files [_] 32 | {:type :object 33 | :properties {:path {:type :string 34 | :description "Root directory to start the search from (defaults to current working directory)"} 35 | :pattern {:type :string 36 | :description "Glob pattern (e.g. \"**/*.clj\", \"src/**/*.tsx\")"} 37 | :max_results {:type :integer 38 | :description "Maximum number of results to return (default: 1000)"}} 39 | :required [:pattern]}) 40 | 41 | (defmethod tool-system/validate-inputs :glob-files [{:keys [nrepl-client-atom]} inputs] 42 | (let [{:keys [path pattern max_results]} inputs 43 | nrepl-client-map @nrepl-client-atom ; Dereference atom 44 | effective-path (or path (config/get-nrepl-user-dir nrepl-client-map))] 45 | 46 | (when-not effective-path 47 | (throw (ex-info "No path provided and no nrepl-user-dir available" {:inputs inputs}))) 48 | 49 | (when-not pattern 50 | (throw (ex-info "Missing required parameter: pattern" {:inputs inputs}))) 51 | 52 | ;; Pass the dereferenced map to validate-path-with-client 53 | (let [validated-path (valid-paths/validate-path-with-client effective-path nrepl-client-map)] 54 | (cond-> {:path validated-path 55 | :pattern pattern} 56 | max_results (assoc :max-results max_results))))) 57 | 58 | (defmethod tool-system/execute-tool :glob-files [_ inputs] 59 | (let [{:keys [path pattern max-results]} inputs] 60 | (core/glob-files path pattern :max-results (or max-results 1000)))) 61 | 62 | (defmethod tool-system/format-results :glob-files [_ result] 63 | (if (:error result) 64 | ;; If there's an error, return it with error flag true 65 | {:result [(:error result)] 66 | :error true} 67 | ;; Format the results as a plain text list of filenames 68 | (let [{:keys [filenames truncated numFiles]} result 69 | output (cond 70 | (empty? filenames) "No files found" 71 | 72 | :else (str (string/join "\n" filenames) 73 | (when truncated 74 | (str "\n(Showing " (count filenames) " of " numFiles 75 | " total files. Use max_results parameter to see more.)"))))] 76 | {:result [output] 77 | :error false}))) 78 | 79 | ;; Backward compatibility function that returns the registration map 80 | (defn glob-files-tool 81 | "Returns the registration map for the glob-files tool. 82 | 83 | Parameters: 84 | - nrepl-client-atom: Atom containing the nREPL client" 85 | [nrepl-client-atom] 86 | (tool-system/registration-map (create-glob-files-tool nrepl-client-atom))) 87 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/unified_read_file/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.unified-read-file.core-test 2 | (:require 3 | [clojure.test :refer [deftest is testing use-fixtures]] 4 | [clojure-mcp.tools.unified-read-file.core :as read-file-core] 5 | [clojure.java.io :as io] 6 | [clojure.string :as str])) 7 | 8 | ;; Setup test fixtures 9 | (def ^:dynamic *test-dir* nil) 10 | (def ^:dynamic *test-file* nil) 11 | (def ^:dynamic *large-test-file* nil) 12 | 13 | (defn create-test-files-fixture [f] 14 | (let [test-dir (io/file (System/getProperty "java.io.tmpdir") "clojure-mcp-test") 15 | test-file (io/file test-dir "test-file.txt") 16 | large-file (io/file test-dir "large-test-file.txt")] 17 | 18 | ;; Create test directory 19 | (.mkdirs test-dir) 20 | 21 | ;; Create small test file 22 | (spit test-file "Line 1\nLine 2\nLine 3\nLine 4\nLine 5") 23 | 24 | ;; Create large test file with 100 lines 25 | (with-open [w (io/writer large-file)] 26 | (doseq [i (range 1 101)] 27 | (.write w (str "This is line " i " of the test file.\n")))) 28 | 29 | ;; Bind dynamic vars for test 30 | (binding [*test-dir* test-dir 31 | *test-file* test-file 32 | *large-test-file* large-file] 33 | (try 34 | (f) 35 | (finally 36 | ;; Clean up 37 | (doseq [file [test-file large-file]] 38 | (when (.exists file) 39 | (.delete file))) 40 | (.delete test-dir)))))) 41 | 42 | (use-fixtures :each create-test-files-fixture) 43 | 44 | (deftest read-file-test 45 | (testing "Reading a small file entirely" 46 | (let [result (read-file-core/read-file (.getPath *test-file*) 0 1000)] 47 | (is (map? result)) 48 | (is (contains? result :content)) 49 | (is (not (:truncated? result))) 50 | (is (= 5 (count (str/split (:content result) #"\n")))) 51 | (is (= (.getAbsolutePath *test-file*) (:path result))))) 52 | 53 | (testing "Reading with offset" 54 | (let [result (read-file-core/read-file (.getPath *test-file*) 2 1000)] 55 | (is (map? result)) 56 | (is (= 3 (count (str/split (:content result) #"\n")))) 57 | (is (str/starts-with? (:content result) "Line 3")) 58 | (is (not (:truncated? result))))) 59 | 60 | (testing "Reading with limit" 61 | (let [result (read-file-core/read-file (.getPath *test-file*) 0 2)] 62 | (is (map? result)) 63 | (is (= 2 (count (str/split (:content result) #"\n")))) 64 | (is (str/starts-with? (:content result) "Line 1")) 65 | (is (:truncated? result)) 66 | (is (= "max-lines" (:truncated-by result))) 67 | (is (= 2 (:line-count result))) 68 | (is (= 5 (:total-line-count result))))) 69 | 70 | (testing "Reading with offset and limit" 71 | (let [result (read-file-core/read-file (.getPath *test-file*) 1 2)] 72 | (is (map? result)) 73 | (is (= 2 (count (str/split (:content result) #"\n")))) 74 | (is (str/starts-with? (:content result) "Line 2")) 75 | (is (:truncated? result)) 76 | ;; total-line-count should be total lines in file, not remaining from offset 77 | (is (= 5 (:total-line-count result))))) 78 | 79 | (testing "Reading with line length limit" 80 | (let [result (read-file-core/read-file (.getPath *large-test-file*) 0 5 :max-line-length 10)] 81 | (is (map? result)) 82 | (is (= 5 (count (str/split (:content result) #"\n")))) 83 | (is (every? #(str/includes? % "...") (str/split (:content result) #"\n"))) 84 | (is (:line-lengths-truncated? result)) 85 | (is (= 100 (:total-line-count result)))))) 86 | 87 | (deftest read-file-error-test 88 | (testing "Reading non-existent file" 89 | (let [result (read-file-core/read-file (.getPath (io/file *test-dir* "nonexistent.txt")) 0 1000)] 90 | (is (map? result)) 91 | (is (contains? result :error)) 92 | (is (str/includes? (:error result) "does not exist")))) 93 | 94 | (testing "Reading a directory instead of a file" 95 | (let [result (read-file-core/read-file (.getPath *test-dir*) 0 1000)] 96 | (is (map? result)) 97 | (is (contains? result :error)) 98 | (is (str/includes? (:error result) "is not a file"))))) -------------------------------------------------------------------------------- /src/clojure_mcp/file_content.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.file-content 2 | "File content utilities for MCP, including image content creation." 3 | (:require [clojure.string :as str]) 4 | (:import [io.modelcontextprotocol.spec 5 | McpSchema$ImageContent 6 | McpSchema$EmbeddedResource 7 | McpSchema$BlobResourceContents 8 | McpSchema$TextResourceContents] 9 | [java.nio.file Path Files] 10 | [java.util Base64] 11 | [org.apache.tika Tika] 12 | [org.apache.tika.mime MimeTypes MediaTypeRegistry MediaType])) 13 | 14 | ;; embedded resources aren't supported by claude desktop yet but who knows 15 | ;; which clients are supporting this and when 16 | 17 | (def ^Tika mime-detector 18 | "Singleton Apache Tika detector (falls back to Files/probeContentType)." 19 | (delay (Tika.))) 20 | 21 | (def ^MediaTypeRegistry registry 22 | (.getMediaTypeRegistry (MimeTypes/getDefaultMimeTypes))) 23 | 24 | (def text-like-mime-patterns 25 | "Regex patterns for MIME types that should be treated as text. 26 | Covers common text-based data formats used in projects that don't 27 | inherit from text/plain in the Apache Tika MediaType hierarchy." 28 | [#"^application/(sql|json|xml|(?:x-)?yaml)$"]) 29 | 30 | (defn text-media-type? 31 | "Determines if a MIME type represents text content. 32 | Uses Apache Tika's type hierarchy plus additional patterns for 33 | common text-based formats that don't inherit from text/plain. 34 | Handles invalid MIME strings gracefully." 35 | [mime] 36 | (let [s (some-> mime str str/lower-case) 37 | text-according-to-tika-hierarchy? 38 | (try 39 | (.isInstanceOf registry (MediaType/parse s) MediaType/TEXT_PLAIN) 40 | (catch IllegalArgumentException _ false))] 41 | (boolean (or text-according-to-tika-hierarchy? 42 | (and s (some #(re-matches % s) text-like-mime-patterns)))))) 43 | 44 | (defn image-media-type? [mime-or-media-type] 45 | (= "image" (.getType (MediaType/parse mime-or-media-type)))) 46 | 47 | (defn mime-type* [^Path p] 48 | (or (Files/probeContentType p) 49 | (try (.detect ^Tika @mime-detector (.toFile p)) 50 | (catch Exception _ "application/octet-stream")))) 51 | 52 | (defn str->nio-path [fp] 53 | (Path/of fp (make-array String 0))) 54 | 55 | (defn mime-type [file-path] 56 | (mime-type* (str->nio-path file-path))) 57 | 58 | (defn serialized-file [file-path] 59 | (let [path (str->nio-path file-path) 60 | bytes (Files/readAllBytes path) 61 | b64 (.encodeToString (Base64/getEncoder) bytes) 62 | mime (mime-type* path) ;; e.g. application/pdf 63 | uri (str "file://" (.toAbsolutePath path))] 64 | {:file-path file-path 65 | :nio-path path 66 | :uri uri 67 | :mime-type mime 68 | :b64 b64})) 69 | 70 | (defn image-content [{:keys [b64 mime-type]}] 71 | (McpSchema$ImageContent. nil nil b64 mime-type)) 72 | 73 | (defn text-resource-content [{:keys [uri mime-type b64]}] 74 | (let [blob (McpSchema$TextResourceContents. uri mime-type b64)] 75 | (McpSchema$EmbeddedResource. nil nil blob))) 76 | 77 | (defn binary-resource-content [{:keys [uri mime-type b64]}] 78 | (let [blob (McpSchema$BlobResourceContents. uri mime-type b64)] 79 | (McpSchema$EmbeddedResource. nil nil blob))) 80 | 81 | (defn file-response->file-content [{:keys [::file-response]}] 82 | (let [{:keys [mime-type] :as ser-file} (serialized-file file-response)] 83 | (cond 84 | (text-media-type? mime-type) (text-resource-content ser-file) 85 | (image-media-type? mime-type) (image-content ser-file) 86 | :else (binary-resource-content ser-file)))) 87 | 88 | (defn should-be-file-response? [file-path] 89 | (not (text-media-type? (mime-type file-path)))) 90 | 91 | (defn text-file? [file-path] 92 | (text-media-type? (mime-type file-path))) 93 | 94 | (defn image-file? [file-path] 95 | (image-media-type? (mime-type file-path))) 96 | 97 | (defn ->file-response [file-path] 98 | {::file-response file-path}) 99 | 100 | (defn file-response? [map] 101 | (and (map? map) 102 | (::file-response map))) 103 | 104 | -------------------------------------------------------------------------------- /src/clojure_mcp/tool_format.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tool-format 2 | "Tool execution request and result formatting" 3 | (:require [clojure.string :as str] 4 | [clj-commons.ansi :as ansi])) 5 | 6 | (def default-result-lines 5) 7 | 8 | ;; Helper functions for ANSI formatting 9 | 10 | (defn format-kv 11 | "Format a key-value pair with ANSI styling" 12 | [k v] 13 | (ansi/compose [:faint (str (name k) ":")] " " [:bold.white (str v)])) 14 | 15 | (defn format-args-box 16 | "Format arguments as box lines (without opening or closing)" 17 | [args-map] 18 | (map (fn [[k v]] 19 | (ansi/compose [:cyan "│ "] (format-kv k v))) 20 | args-map)) 21 | 22 | ;; Tool request formatting (opens box, doesn't close it) 23 | 24 | (defmulti format-tool-request 25 | "Format a tool execution request for display. 26 | Opens a cyan box with tool name and arguments but doesn't close it." 27 | (comp keyword :name)) 28 | 29 | (defmethod format-tool-request :default 30 | [{:keys [name arguments]}] 31 | (let [header (ansi/compose [:bold.cyan "╭─ " [:white (str name)]]) 32 | args-lines (format-args-box arguments)] 33 | (str/join "\n" (concat [header] args-lines)))) 34 | 35 | (defmethod format-tool-request :glob_files 36 | [{:keys [name arguments]}] 37 | (let [header (ansi/compose [:bold.cyan "╭─ " [:white (str name)]]) 38 | args-lines (format-args-box arguments)] 39 | (str/join "\n" (concat [header] args-lines)))) 40 | 41 | (defmethod format-tool-request :grep 42 | [{:keys [name arguments]}] 43 | (let [header (ansi/compose [:bold.cyan "╭─ " [:white (str name)]]) 44 | args-lines (format-args-box arguments)] 45 | (str/join "\n" (concat [header] args-lines)))) 46 | 47 | (defmethod format-tool-request :read_file 48 | [{:keys [name arguments]}] 49 | (let [header (ansi/compose [:bold.cyan "╭─ " [:white (str name)]]) 50 | args-lines (format-args-box arguments)] 51 | (str/join "\n" (concat [header] args-lines)))) 52 | 53 | (defmethod format-tool-request :clojure_edit 54 | [{:keys [name arguments]}] 55 | (let [{:keys [operation form_type form_identifier file_path content]} arguments 56 | header (ansi/compose [:bold.cyan "╭─ " 57 | [:bold.white (str name)] 58 | [:plain.cyan (str "(" operation ", " form_type ", " form_identifier ")")]]) 59 | file-line (ansi/compose [:cyan "│ "] (format-kv "file_path" file_path)) 60 | content-lines (str/split-lines content) 61 | single-line? (= 1 (count content-lines)) 62 | content-display (if single-line? 63 | [(ansi/compose [:cyan "│ "] (format-kv "content" (first content-lines)))] 64 | (concat [(ansi/compose [:cyan "│ "] [:faint "content:"])] 65 | (map (fn [line] 66 | (ansi/compose [:cyan "│ "] line)) 67 | content-lines)))] 68 | (str/join "\n" (concat [header file-line] content-display)))) 69 | 70 | ;; Tool result formatting (connects to open box and closes it) 71 | 72 | (defmulti format-tool-result 73 | "Format a tool execution result for display. 74 | Connects to an open box with ├─ and closes with ╰─" 75 | (comp keyword :toolName)) 76 | 77 | (defmethod format-tool-result :default 78 | [{:keys [text]}] 79 | (let [lines (str/split-lines text) 80 | line-count (count lines) 81 | truncated? (> line-count default-result-lines) 82 | display-lines (if truncated? 83 | (take default-result-lines lines) 84 | lines) 85 | connector (ansi/compose [:bold.cyan "├─ Result"]) 86 | body-lines (map (fn [line] 87 | (ansi/compose [:cyan "│ "] line)) 88 | display-lines) 89 | ellipsis (when truncated? 90 | (ansi/compose [:cyan "│ "] [:faint "... (" (- line-count default-result-lines) " more lines)"])) 91 | footer (ansi/compose [:cyan "╰─"])] 92 | (str/join "\n" (concat [connector] 93 | body-lines 94 | (when ellipsis [ellipsis]) 95 | [footer])))) 96 | -------------------------------------------------------------------------------- /resources/clojure-mcp/tools/form_edit/clojure_update_sexp-description.md: -------------------------------------------------------------------------------- 1 | Updates Clojure expressions in a file using replace, insert-before, or insert-after operations. 2 | 3 | This unified tool provides targeted editing of Clojure expressions within forms. For complete top-level form operations, use `clojure_edit` instead. 4 | 5 | KEY BENEFITS: 6 | - Syntax-aware matching that understands Clojure code structure 7 | - Ignores whitespace differences by default, focusing on actual code meaning 8 | - Matches expressions regardless of formatting, indentation, or spacing 9 | - Prevents errors from mismatched text or irrelevant formatting differences 10 | - Can apply operations to all occurrences with replace_all: true 11 | 12 | SUPPORTED OPERATIONS: 13 | - "replace": Replace matched expression(s) with new content 14 | - "insert_before": Insert new content before the matched expression(s) 15 | - "insert_after": Insert new content after the matched expression(s) 16 | 17 | CONSTRAINTS: 18 | - match_form must contain one or more complete Clojure expressions 19 | - new_form must contain zero or more complete Clojure expressions 20 | - Both match_form and new_form must be valid Clojure code that can be parsed 21 | 22 | A complete Clojure expression is any form that Clojure can read as a complete unit: 23 | - Symbols: foo, my-var, clojure.core/map 24 | - Numbers: 42, 3.14 25 | - Strings: "hello" 26 | - Keywords: :keyword, ::namespaced 27 | - Collections: [1 2 3], {:a 1}, #{:a :b} 28 | - Function calls: (println "hello") 29 | - Special forms: (if true 1 2) 30 | 31 | WARNING: The following are NOT valid Clojure expressions and will cause errors: 32 | - Incomplete forms: (defn foo, (try, (let [x 1] 33 | - Partial function definitions: (defn foo [x] 34 | - Just the opening of a form: (if condition 35 | - Mixed data without collection: :a 1 :b 2 36 | - Unmatched parentheses: (+ 1 2)) 37 | 38 | COMMON APPLICATIONS: 39 | - Renaming symbols throughout the file: 40 | match_form: old-name 41 | new_form: new-name 42 | operation: replace 43 | replace_all: true 44 | 45 | - Adding logging before a function call: 46 | match_form: (process-data x) 47 | new_form: (log/info "Processing item" x) 48 | operation: insert_before 49 | 50 | - Replacing multiple expressions with a single form: 51 | match_form: (validate x) (transform x) (save x) 52 | new_form: (-> x validate transform save) 53 | operation: replace 54 | 55 | - Wrapping code in try-catch by replacing multiple expressions: 56 | match_form: (risky-op-1) (risky-op-2) 57 | new_form: (try 58 | (risky-op-1) 59 | (risky-op-2) 60 | (catch Exception e 61 | (log/error e "Operations failed"))) 62 | operation: replace 63 | 64 | - Removing debug statements (multiple expressions): 65 | match_form: (println "Debug 1") (println "Debug 2") 66 | new_form: 67 | operation: replace 68 | 69 | - Converting imperative style to functional: 70 | match_form: (def result (calculate x)) (println result) result 71 | new_form: (doto (calculate x) println) 72 | operation: replace 73 | 74 | - Adding setup/teardown around operations: 75 | match_form: (database-operation) 76 | new_form: (open-connection) (database-operation) (close-connection) 77 | operation: replace 78 | 79 | - Transforming let bindings: 80 | match_form: [x (get-value) y (process x)] 81 | new_form: [x (get-value) 82 | _ (log/debug "got value" x) 83 | y (process x)] 84 | operation: replace 85 | 86 | Other Examples: 87 | - Replace a single expression: 88 | match_form: (+ x 2) 89 | new_form: (* x 2) 90 | operation: replace 91 | 92 | - Insert multiple expressions: 93 | match_form: (critical-operation) 94 | new_form: (log/warn "About to perform critical operation") 95 | (record-metrics :start) 96 | operation: insert_before 97 | 98 | - Clean up code by removing intermediate steps: 99 | match_form: (let [temp (process x)] (use temp)) 100 | new_form: (use (process x)) 101 | operation: replace 102 | 103 | Returns a diff showing the changes made to the file. -------------------------------------------------------------------------------- /CLAUDE_CODE_WEB_SETUP.md: -------------------------------------------------------------------------------- 1 | # Claude Code Environment Setup Guide 2 | 3 | This guide explains how to set up the Clojure MCP project in Claude Code's authenticated proxy environment. 4 | 5 | ## The Problem 6 | 7 | Claude Code uses an authenticated proxy for all network requests. However, Java applications (including Clojure CLI) cannot send authentication headers during HTTPS CONNECT requests, which prevents them from accessing Maven repositories through the proxy. 8 | 9 | ## The Solution 10 | 11 | We use a local proxy wrapper that: 12 | 1. Listens on localhost (port 8888 by default) 13 | 2. Accepts requests from Java/Clojure 14 | 3. Adds authentication headers 15 | 4. Forwards requests to Claude Code's authenticated proxy 16 | 17 | ## Quick Setup 18 | 19 | 1. **Run the setup script:** 20 | ```bash 21 | source claude-code-setup/setup-claude-code-env.sh 22 | ``` 23 | 24 | This will: 25 | - Start the proxy wrapper on port 8888 26 | - Configure Maven settings (`~/.m2/settings.xml`) 27 | - Configure Gradle settings (`~/.gradle/gradle.properties`) 28 | - Export `JAVA_TOOL_OPTIONS` with proxy configuration 29 | 30 | 2. **Verify the setup:** 31 | ```bash 32 | clojure -M -e '(println "Success!")' 33 | ``` 34 | 35 | 3. **Run the MCP server:** 36 | ```bash 37 | clojure -X:mcp 38 | ``` 39 | 40 | ## Files 41 | 42 | - **`claude-code-setup/proxy-wrapper.py`** - Python script that acts as a local proxy wrapper 43 | - **`claude-code-setup/setup-claude-code-env.sh`** - Setup script to configure the environment 44 | - **`CLAUDE_CODE_WEB_SETUP.md`** - This documentation file 45 | 46 | ## Manual Setup (if needed) 47 | 48 | If you need to manually configure the environment: 49 | 50 | ### 1. Start the Proxy Wrapper 51 | 52 | ```bash 53 | python3 claude-code-setup/proxy-wrapper.py 8888 > /tmp/proxy.log 2>&1 & 54 | ``` 55 | 56 | ### 2. Configure Maven 57 | 58 | Create `~/.m2/settings.xml`: 59 | 60 | ```xml 61 | 62 | 66 | 67 | 68 | local-proxy-http 69 | true 70 | http 71 | 127.0.0.1 72 | 8888 73 | localhost|127.0.0.1 74 | 75 | 76 | local-proxy-https 77 | true 78 | https 79 | 127.0.0.1 80 | 8888 81 | localhost|127.0.0.1 82 | 83 | 84 | 85 | ``` 86 | 87 | ### 3. Set Java System Properties 88 | 89 | ```bash 90 | export JAVA_TOOL_OPTIONS="-Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=8888 -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=8888 -Dmaven.artifact.threads=2" 91 | ``` 92 | 93 | Note: We limit `maven.artifact.threads` to 2 to avoid overwhelming the proxy with too many parallel connections. 94 | 95 | ## Troubleshooting 96 | 97 | ### Check if proxy wrapper is running 98 | 99 | ```bash 100 | pgrep -f proxy-wrapper.py 101 | ``` 102 | 103 | ### Check proxy logs 104 | 105 | ```bash 106 | tail -f /tmp/proxy.log 107 | ``` 108 | 109 | ### Test network connectivity 110 | 111 | ```bash 112 | # Test direct access (should work via Claude Code proxy) 113 | curl -I https://repo1.maven.org/maven2/ 114 | 115 | # Test through local proxy 116 | curl -x http://127.0.0.1:8888 -I https://repo1.maven.org/maven2/ 117 | ``` 118 | 119 | ### Restart the proxy wrapper 120 | 121 | ```bash 122 | pkill -f proxy-wrapper.py 123 | python3 claude-code-setup/proxy-wrapper.py 8888 > /tmp/proxy.log 2>&1 & 124 | ``` 125 | 126 | ### Use a different port 127 | 128 | ```bash 129 | PROXY_PORT=9999 source claude-code-setup/setup-claude-code-env.sh 130 | ``` 131 | 132 | ## Credits 133 | 134 | This setup is based on the approach documented in: 135 | https://github.com/michaelwhitford/claude-code-explore 136 | 137 | The proxy wrapper solution addresses Java's limitation with authenticated proxies in the Claude Code runtime environment. 138 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/glob_files/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.glob-files.core-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [clojure-mcp.tools.glob-files.core :as sut] 4 | [clojure.java.io :as io]) 5 | (:import (java.nio.file Paths))) 6 | 7 | (deftest glob-files-test 8 | (testing "Basic glob patterns with existing directory" 9 | (let [current-dir (System/getProperty "user.dir") 10 | result (sut/glob-files current-dir "**/*.clj" :max-results 5)] 11 | (is (not (:error result)) "Should not return an error") 12 | (is (vector? (:filenames result)) "Should return filenames as a vector") 13 | (is (<= (count (:filenames result)) 5) "Should limit results to max-results") 14 | (is (or (not (:truncated result)) 15 | (<= (count (:filenames result)) (:numFiles result))) 16 | "numFiles should be >= filenames count when truncated") 17 | (is (boolean? (:truncated result)) "truncated should be a boolean") 18 | (is (number? (:durationMs result)) "durationMs should be a number"))) 19 | 20 | (testing "Filtering with specific pattern" 21 | (let [current-dir (System/getProperty "user.dir") 22 | result (sut/glob-files current-dir "**/glob_files/**/*.clj")] 23 | (is (not (:error result)) "Should not return an error") 24 | (is (vector? (:filenames result)) "Should return filenames as a vector") 25 | (is (every? #(.contains % "glob_files") (:filenames result)) 26 | "All files should contain glob_files in the path"))) 27 | 28 | (testing "Handling non-existent directory" 29 | (let [result (sut/glob-files "/nonexistent/directory" "*.clj")] 30 | (is (:error result) "Should return an error") 31 | (is (string? (:error result)) "Error should be a string"))) 32 | 33 | (testing "Handling invalid glob pattern" 34 | (let [current-dir (System/getProperty "user.dir") 35 | result (sut/glob-files current-dir "[invalid-glob-pattern")] 36 | (is (not (:error result)) "May not return an error for invalid pattern") 37 | (is (= 0 (:numFiles result)) "Should find 0 files for invalid pattern"))) 38 | 39 | (testing "Result truncation with small max-results" 40 | (let [current-dir (System/getProperty "user.dir") 41 | result (sut/glob-files current-dir "**/*.clj" :max-results 1)] 42 | (is (not (:error result)) "Should not return an error") 43 | (is (= 1 (count (:filenames result))) "Should return exactly 1 result") 44 | (is (:truncated result) "Should indicate results were truncated"))) 45 | 46 | (testing "Finding files in root directory with **/*.ext pattern" 47 | (let [current-dir (System/getProperty "user.dir") 48 | ;; Get files using both patterns for comparison 49 | standard-result (sut/glob-files current-dir "**/*.md") 50 | _root-only-result (sut/glob-files current-dir "*.md") 51 | ;; Count root-level .md files using Java file operations 52 | root-file-count (count (filter #(and (.isFile %) 53 | (.endsWith (.getName %) ".md")) 54 | (.listFiles (io/file current-dir))))] 55 | ;; The **/*.md pattern should also find files in the root directory 56 | (is (>= (count (:filenames standard-result)) root-file-count) 57 | "**/*.md pattern should find all root-level files") 58 | ;; Compare with direct root pattern to ensure we find the same files 59 | #_(is (= (count (:filenames root-only-result)) root-file-count) 60 | "*.md pattern should find all root-level files") 61 | ;; Check if root files exist in the results 62 | (let [path-obj (Paths/get current-dir (into-array String [])) 63 | root-files-in-results (filter 64 | (fn [file-path] 65 | (let [file-obj (Paths/get file-path (into-array String [])) 66 | rel-path (.relativize path-obj file-obj)] 67 | (and (= (.getNameCount rel-path) 1) 68 | (.endsWith (str rel-path) ".md")))) 69 | (:filenames standard-result))] 70 | (is (= (count root-files-in-results) root-file-count) 71 | "**/*.md pattern should find all root-level md files"))))) 72 | -------------------------------------------------------------------------------- /BIG_IDEAS.md: -------------------------------------------------------------------------------- 1 | # How Programmers Can Work with GenAI - General Ideas 2 | 3 | ## Naive Hypotheses 4 | 5 | > If it's hard for humans, it's hard for AI 6 | 7 | **HARD** 8 | Taking big code steps and then trying to debug after is difficult. 9 | 10 | **EASIER** 11 | Small, even tiny steps and verify they work. 12 | 13 | **HARD** 14 | Imperative, side-effecting, concurrent programs 15 | 16 | **EASIER** 17 | Simple functions that take values and return values 18 | 19 | **HARD** 20 | Bespoke syntaxes 21 | 22 | **EASIER** 23 | Simple regular syntax 24 | 25 | ## Gen AI has no problem generating programs that are HARD to reason about 26 | 27 | LLMs will accrete complexity just like us. Producing programs that HUMANS and thus the LLMs will have a hard time thinking about. 28 | 29 | Discerning programmers MUST be in the loop. Get the ROOMBA off the cat! 30 | 31 | How long until the discerning programmers have lost their discernment? 32 | 33 | The problem has always been: how do we produce high quality maintainable code? 34 | 35 | This hasn't changed. 36 | 37 | ## Perhaps the Lisp REPL workflow that I have come to love is a solution. 38 | 39 | A lifetime of coding has led me to this workflow and it has proven itself over and over again to be very effective. 40 | 41 | Not acting on files and working only on a scratch pad in memory means faster iteration and verification feedback. 42 | 43 | **Aside:** 44 | _No file patching needed during development. File patching and running test suites consume time and tokens._ 45 | 46 | > Tiny steps with rich feedback is the recipe for the sauce 47 | 48 | This rapid contextual feedback loop is important for LLMs AND for programmers. 49 | 50 | The higher the quality of the feedback, the higher the potential for quality for HUMANS and LLMs. 51 | 52 | For programmers, there are downsides of iterating in a REPL on things and getting the endorphins of evaluating code and realizing that you had the wrong model of the problem. The problem is getting caught in the moment of creation and losing the big picture. Time for reflection is needed. This is equally true for LLMs. 53 | 54 | Hammock time and paper time is needed always. 55 | 56 | > The granular steps in LLM REPL iteration PRODUCES the LLM context you want. 57 | 58 | ## GenAI code is like compiled code, do you want to maintain compiled code? 59 | 60 | Generated programs are in dire need of higher level concise maintainable abstractions. 61 | 62 | Languages that provide constraints (functional, immutable, restricted concurrency patterns) produce better GenAI programs because bad paths are not as available or idiomatic. 63 | 64 | Languages that are constrained and provide higher level abstractions produce/generate more concise, more abstract programs that we can reason about at a higher level. 65 | 66 | Clojure has a lot of potential here especially with contracts via Clojure Spec. 67 | 68 | * The example of SEXP to HTML AST demonstrates this 69 | - Try generating this in JavaScript vs Clojure 70 | 71 | > LLM REPL driven workflow with high quality feedback provides Reinforcement Learning (o1 Deepseek style) the examples it needs. 72 | 73 | > The REPL is closed over all Tools 74 | 75 | AI can quickly build and dismantle tools as needed. 76 | 77 | ## Even if the LLM doesn't need this workflow, I do. 78 | 79 | Even if the LLM doesn't need it, I DO. I need to see the steps and the direction it takes. 80 | 81 | I need the collaboration and to be present with the moment-to-moment ideation. 82 | 83 | I need to be able to see the potential problems so that I can stop it and exercise discernment so that I can keep the program in the space of programs that can be reasoned about. 84 | 85 | > I need the collaboration TO be present. 86 | 87 | This is a much more humane workflow. 88 | 89 | ## Within collaboration we can and should be creating higher quality code 90 | 91 | The goal to be clear is to write better code than we could without the LLMs assistance. 92 | 93 | And now I believe it's possible. 94 | 95 | > LLMs are fantastic for quickly building tools tailored to giving feedback for your specific problem 96 | 97 | > The lack of experience in REPL driven development has led to a lack of insight into these possibilities. 98 | 99 | ## Speculation 100 | 101 | The next AI PL may be a Lisp with Idris or TLA+ qualities. The simple syntax combined with enforced correctness and high degree of feedback. 102 | -------------------------------------------------------------------------------- /resources/configs/example-agents.edn: -------------------------------------------------------------------------------- 1 | ;; Example configuration showing how to define multiple agents 2 | ;; Each agent becomes its own tool in the MCP server 3 | 4 | {;; Standard configuration options 5 | :allowed-directories ["." "src" "test" "resources"] 6 | :cljfmt true 7 | :bash-over-nrepl true 8 | 9 | ;; Agent definitions - each becomes a separate tool 10 | ;; IMPORTANT: Agents have NO tools by default. You must explicitly list tools in :enable-tools 11 | :agents [{:id :research-agent 12 | :name "research_agent" 13 | :description "Specialized agent for researching code patterns and finding examples across the codebase" 14 | :system-message "You are a research specialist focused on finding code patterns, examples, and understanding project structure. Be thorough in your analysis and provide specific file locations and code snippets." 15 | :model :anthropic/claude-3-5-sonnet-20241022 ; Optional: specific model 16 | :context true ; Use default project context (PROJECT_SUMMARY.md and code index) 17 | :enable-tools [:grep :glob_files :read_file :clojure_inspect_project] ; Must specify tools explicitly 18 | :disable-tools nil} 19 | 20 | {:id :refactor-assistant 21 | :name "refactor_assistant" 22 | :description "Agent specialized in analyzing code for refactoring opportunities" 23 | :system-message "You are a refactoring specialist. Analyze code for patterns that could be improved, suggest better abstractions, and identify duplicate code. Focus on readability and maintainability." 24 | :context ["PROJECT_SUMMARY.md" "doc/LLM_CODE_STYLE.md"] ; Specific files for context 25 | :enable-tools [:read_file :grep :glob_files] 26 | :disable-tools [:bash]} 27 | 28 | {:id :test-explorer 29 | :name "test_explorer" 30 | :description "Agent for exploring and understanding test files" 31 | :system-message "You are a test exploration specialist. Help understand test structure, find relevant tests, and explain test patterns. Be concise and focus on test-specific insights." 32 | :context false ; No default context 33 | :enable-tools [:read_file :glob_files :grep] 34 | :disable-tools [:bash :clojure_inspect_project]} 35 | 36 | {:id :doc-reader 37 | :name "doc_reader" 38 | :description "Agent optimized for reading and summarizing documentation" 39 | :system-message "You are a documentation specialist. Read and summarize documentation clearly and concisely. Focus on key concepts and practical usage." 40 | ;; model not specified - will use default 41 | :context ["README.md" "doc/"] ; Documentation-focused context 42 | :enable-tools [:read_file :glob_files] 43 | :disable-tools [:grep :bash]} 44 | 45 | {:id :code-writer 46 | :name "code_writer" 47 | :description "Agent that can write and modify code files" 48 | :system-message "You are a code writing assistant. You can create new files, edit existing ones, and refactor code. Always test code in the REPL before writing to files." 49 | :context true 50 | ;; Can evaluate code and write files 51 | :enable-tools [:clojure_eval :file_write :file_edit :clojure_edit 52 | :clojure_edit_replace_sexp :read_file :grep :glob_files] 53 | :disable-tools nil} 54 | 55 | {:id :full-access-agent 56 | :name "full_access_agent" 57 | :description "Agent with access to ALL available tools - use with caution" 58 | :system-message "You are a full-access assistant with all tools at your disposal. Use tools responsibly and always confirm destructive operations." 59 | :context true 60 | ;; Special keyword :all enables all tools 61 | :enable-tools [:all] ; Gives access to every available tool 62 | :disable-tools [:dispatch_agent]}] ; But still can disable specific ones 63 | 64 | ;; Optional: Define custom models for agents to use 65 | :models {:anthropic/claude-3-5-sonnet-20241022 66 | {:model-name "claude-3-5-sonnet-20241022" 67 | :api-key [:env "ANTHROPIC_API_KEY"] 68 | :temperature 0.3 69 | :max-tokens 4096} 70 | 71 | :openai/gpt-4-turbo 72 | {:model-name "gpt-4-turbo-preview" 73 | :api-key [:env "OPENAI_API_KEY"] 74 | :temperature 0.2 75 | :max-tokens 2048}}} -------------------------------------------------------------------------------- /test/clojure_mcp/tools/unified_read_file/tool_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.unified-read-file.tool-test 2 | (:require 3 | [clojure.test :refer [deftest is testing use-fixtures]] 4 | [clojure-mcp.tools.test-utils :as test-utils :refer [*nrepl-client-atom*]] 5 | [clojure-mcp.tools.unified-read-file.tool :as unified-read-file-tool] 6 | [clojure-mcp.tool-system :as tool-system] 7 | [clojure-mcp.config :as config] 8 | [clojure.java.io :as io])) 9 | 10 | ;; Setup test fixtures 11 | (test-utils/apply-fixtures *ns*) 12 | 13 | ;; Setup test files 14 | (def ^:dynamic *test-dir* nil) 15 | 16 | (defn setup-test-files-fixture [f] 17 | (let [test-dir (io/file (System/getProperty "java.io.tmpdir") "clojure-mcp-unified-read-test")] 18 | ;; Create test directory 19 | (.mkdirs test-dir) 20 | 21 | ;; Set allowed directories for path validation using config/set-config! 22 | (config/set-config! *nrepl-client-atom* :nrepl-user-dir (.getAbsolutePath test-dir)) 23 | (config/set-config! *nrepl-client-atom* :allowed-directories [(.getAbsolutePath test-dir)]) 24 | 25 | ;; Run test with fixtures bound 26 | (binding [*test-dir* test-dir] 27 | (try 28 | (f) 29 | (finally 30 | ;; Clean up 31 | (when (.exists test-dir) 32 | (.delete test-dir))))))) 33 | 34 | (use-fixtures :each setup-test-files-fixture) 35 | 36 | (deftest non-existent-file-test 37 | (testing "Reading non-existent file returns error" 38 | (let [tool-instance (unified-read-file-tool/create-unified-read-file-tool *nrepl-client-atom*) 39 | non-existent-path (.getAbsolutePath (io/file *test-dir* "does-not-exist.clj"))] 40 | 41 | ;; Test with collapsed true 42 | (testing "collapsed mode" 43 | (let [result (tool-system/execute-tool 44 | tool-instance 45 | {:path non-existent-path 46 | :collapsed true})] 47 | (is (:error result) "Should return an error for non-existent file"))) 48 | 49 | ;; Test with collapsed false 50 | (testing "non-collapsed mode" 51 | (let [result (tool-system/execute-tool 52 | tool-instance 53 | {:path non-existent-path 54 | :collapsed false})] 55 | (is (:error result) "Should return an error for non-existent file")))))) 56 | 57 | (deftest collapsible-clojure-file-test 58 | (testing "Detecting collapsible Clojure file extensions" 59 | (is (unified-read-file-tool/collapsible-clojure-file? "test.clj")) 60 | (is (unified-read-file-tool/collapsible-clojure-file? "test.cljs")) 61 | (is (unified-read-file-tool/collapsible-clojure-file? "test.cljc")) 62 | (is (unified-read-file-tool/collapsible-clojure-file? "test.bb")) 63 | (is (unified-read-file-tool/collapsible-clojure-file? "/path/to/file.clj")) 64 | (let [tmp (io/file *test-dir* "script.sh")] 65 | (spit tmp "#!/usr/bin/env bb\n(println :hi)") 66 | (is (unified-read-file-tool/collapsible-clojure-file? (.getPath tmp))) 67 | (.delete tmp)) 68 | (is (not (unified-read-file-tool/collapsible-clojure-file? "test.edn"))) ; EDN files not collapsible 69 | (is (not (unified-read-file-tool/collapsible-clojure-file? "test.txt"))) 70 | (is (not (unified-read-file-tool/collapsible-clojure-file? "test.md"))) 71 | (is (not (unified-read-file-tool/collapsible-clojure-file? "test.js"))))) 72 | 73 | (deftest format-raw-file-truncation-message-test 74 | (testing "Truncation message shows total line count, not file size" 75 | (let [result {:content "line1\nline2\nline3" 76 | :path "/test/file.txt" 77 | :size 118628 ; File size in bytes (should NOT be shown) 78 | :line-count 3 ; Lines shown 79 | :total-line-count 2000 ; Total lines in file (should be shown) 80 | :truncated? true} 81 | formatted (unified-read-file-tool/format-raw-file result 2000) 82 | formatted-str (first formatted)] 83 | (is (re-find #"showing 3 of 2000 lines" formatted-str) 84 | "Should show total line count (2000), not file size (118628)"))) 85 | 86 | (testing "Non-truncated file doesn't show truncation message" 87 | (let [result {:content "line1\nline2" 88 | :path "/test/file.txt" 89 | :size 1000 90 | :line-count 2 91 | :truncated? false} 92 | formatted (unified-read-file-tool/format-raw-file result 2000) 93 | formatted-str (first formatted)] 94 | (is (not (re-find #"truncated" formatted-str)) 95 | "Should not show truncation message when not truncated")))) 96 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/directory_tree/tool_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.directory-tree.tool-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [clojure-mcp.tools.directory-tree.tool :as sut] 4 | [clojure-mcp.tools.directory-tree.core :as directory-tree-core] 5 | [clojure-mcp.utils.valid-paths :as valid-paths] 6 | [clojure-mcp.config :as config] ; Added config require 7 | [clojure-mcp.tool-system :as tool-system])) 8 | 9 | (deftest tool-name-test 10 | (testing "tool-name returns the correct name" 11 | (is (= "LS" 12 | (tool-system/tool-name {:tool-type :directory-tree}))))) 13 | 14 | (deftest tool-description-test 15 | (testing "tool-description returns a non-empty description" 16 | (let [description (tool-system/tool-description {:tool-type :directory-tree})] 17 | (is (string? description)) 18 | (is (seq description))))) 19 | 20 | (deftest tool-schema-test 21 | (testing "tool-schema returns a valid schema with required path parameter" 22 | (let [schema (tool-system/tool-schema {:tool-type :directory-tree})] 23 | (is (map? schema)) 24 | (is (= :object (:type schema))) 25 | (is (contains? (:properties schema) :path)) 26 | (is (contains? (:properties schema) :max_depth)) 27 | (is (= [:path] (:required schema)))))) 28 | 29 | (deftest validate-inputs-test 30 | (testing "validate-inputs properly validates and transforms inputs" 31 | (let [nrepl-client-atom (atom {})] 32 | (config/set-config! nrepl-client-atom :nrepl-user-dir "/base/dir") 33 | (config/set-config! nrepl-client-atom :allowed-directories ["/base/dir"]) 34 | (let [tool-config {:tool-type :directory-tree 35 | :nrepl-client-atom nrepl-client-atom}] 36 | 37 | (with-redefs [clojure-mcp.utils.valid-paths/validate-path-with-client 38 | (fn [path _] 39 | (str "/validated" path))] 40 | 41 | (testing "with only path" 42 | (let [result (tool-system/validate-inputs tool-config {:path "/test/path"})] 43 | (is (= {:path "/validated/test/path"} result)))) 44 | 45 | (testing "with path and max_depth" 46 | (let [result (tool-system/validate-inputs tool-config {:path "/test/path" :max_depth 3})] 47 | (is (= {:path "/validated/test/path" 48 | :max_depth 3} result)))) 49 | 50 | (testing "missing required path parameter" 51 | (is (thrown-with-msg? clojure.lang.ExceptionInfo 52 | #"Missing required parameter: path" 53 | (tool-system/validate-inputs tool-config {}))))))))) 54 | 55 | (deftest execute-tool-test 56 | (testing "execute-tool calls core function with correct parameters" 57 | (with-redefs [clojure-mcp.tools.directory-tree.core/directory-tree 58 | (fn [path & {:keys [max-depth]}] 59 | {:called-with {:path path 60 | :max-depth max-depth}})] 61 | 62 | (testing "with only path" 63 | (let [result (tool-system/execute-tool 64 | {:tool-type :directory-tree} 65 | {:path "/test/path"})] 66 | (is (= {:called-with {:path "/test/path" 67 | :max-depth nil}} result)))) 68 | 69 | (testing "with path and max_depth" 70 | (let [result (tool-system/execute-tool 71 | {:tool-type :directory-tree} 72 | {:path "/test/path" :max_depth 3})] 73 | (is (= {:called-with {:path "/test/path" 74 | :max-depth 3}} result))))))) 75 | 76 | (deftest format-results-test 77 | (testing "format-results correctly formats successful results" 78 | (let [result "Directory tree output" 79 | formatted (tool-system/format-results {:tool-type :directory-tree} result)] 80 | (is (= {:result ["Directory tree output"] 81 | :error false} formatted)))) 82 | 83 | (testing "format-results correctly formats error results" 84 | (let [result {:error "Some error occurred"} 85 | formatted (tool-system/format-results {:tool-type :directory-tree} result)] 86 | (is (= {:result ["Some error occurred"] 87 | :error true} formatted))))) 88 | 89 | (deftest registration-map-test 90 | (testing "directory-tree-tool returns a valid registration map" 91 | (let [nrepl-client-atom (atom {}) 92 | reg-map (sut/directory-tree-tool nrepl-client-atom)] 93 | (is (= "LS" (:name reg-map))) 94 | (is (string? (:description reg-map))) 95 | (is (map? (:schema reg-map))) 96 | (is (fn? (:tool-fn reg-map)))))) 97 | -------------------------------------------------------------------------------- /doc/component-filtering.md: -------------------------------------------------------------------------------- 1 | # Component Filtering Configuration 2 | 3 | ClojureMCP allows fine-grained control over which tools, prompts, and resources are exposed to AI assistants through configuration options in `.clojure-mcp/config.edn`. This is useful for creating focused MCP servers with only the components you need. 4 | 5 | ## Table of Contents 6 | - [Overview](#overview) 7 | - [Tools Filtering](#tools-filtering) 8 | - [Prompts Filtering](#prompts-filtering) 9 | - [Resources Filtering](#resources-filtering) 10 | - [Examples](#examples) 11 | - [Best Practices](#best-practices) 12 | 13 | ## Overview 14 | 15 | Component filtering uses an allow/deny list pattern: 16 | - **Enable lists** (`enable-*`) - When specified, ONLY these items are enabled 17 | - **Disable lists** (`disable-*`) - Applied after enable filtering to remove specific items 18 | - **Default behavior** - When no filtering is specified, all components are enabled 19 | 20 | The filtering logic follows this order: 21 | 1. If an enable list is provided and empty (`[]`), nothing is enabled 22 | 2. If an enable list is provided with items, only those items are enabled 23 | 3. If no enable list is provided (`nil`), all items start enabled 24 | 4. The disable list is then applied to remove items from the enabled set 25 | 26 | ## Tools Filtering 27 | 28 | Control which tools are available to the AI assistant. 29 | 30 | ### Configuration Keys 31 | 32 | ```edn 33 | {:enable-tools [:clojure_eval :read_file :file_write] ; Only these tools 34 | :disable-tools [:dispatch_agent :architect]} ; Remove these tools 35 | ``` 36 | 37 | ### Tool Identifiers 38 | 39 | Tools can be specified using keywords or strings: 40 | - `:clojure_eval` or `"clojure_eval"` 41 | - `:read_file` or `"read_file"` 42 | - `:file_write` or `"file_write"` 43 | 44 | Common tool IDs include: 45 | - `:clojure_eval` - Evaluate Clojure code 46 | - `:read_file` - Read file contents 47 | - `:file_edit` - Edit files 48 | - `:file_write` - Write files 49 | - `:bash` - Execute shell commands 50 | - `:grep` - Search file contents 51 | - `:glob_files` - Find files by pattern 52 | - `:dispatch_agent` - Launch sub-agents 53 | - `:architect` - Technical planning 54 | - `:code_critique` - Code review 55 | - `:scratch_pad` - Persistent storage 56 | 57 | ### Examples 58 | 59 | **Minimal REPL-only server:** 60 | ```edn 61 | {:enable-tools [:clojure_eval]} 62 | ``` 63 | 64 | **Read-only exploration server:** 65 | ```edn 66 | {:enable-tools [:read_file :grep :glob_files :LS :clojure_inspect_project]} 67 | ``` 68 | 69 | **Full access except agents:** 70 | ```edn 71 | {:disable-tools [:dispatch_agent :architect :code_critique]} 72 | ``` 73 | 74 | ## Prompts Filtering 75 | 76 | Control which system prompts are provided to the AI assistant. 77 | 78 | ### Configuration Keys 79 | 80 | ```edn 81 | {:enable-prompts ["clojure_repl_system_prompt" "chat-session-summarize"] 82 | :disable-prompts ["scratch-pad-save-as"]} 83 | ``` 84 | 85 | ### Prompt Names 86 | 87 | Prompts are identified by their string names (not keywords): 88 | - `"clojure_repl_system_prompt"` - Main REPL interaction prompt 89 | - `"chat-session-summarize"` - Session summarization 90 | - `"scratch-pad-load"` - Loading scratch pad data 91 | - `"scratch-pad-save-as"` - Saving scratch pad snapshots 92 | 93 | ### Examples 94 | 95 | **Essential prompts only:** 96 | ```edn 97 | {:enable-prompts ["clojure_repl_system_prompt"]} 98 | ``` 99 | 100 | **Disable scratch pad prompts:** 101 | ```edn 102 | {:disable-prompts ["scratch-pad-load" "scratch-pad-save-as"]} 103 | ``` 104 | 105 | ## Resources Filtering 106 | 107 | Control which resource files are exposed to the AI assistant. 108 | 109 | ### Configuration Keys 110 | 111 | ```edn 112 | {:enable-resources ["PROJECT_SUMMARY.md" "README.md"] 113 | :disable-resources ["CLAUDE.md" "LLM_CODE_STYLE.md"]} 114 | ``` 115 | 116 | ### Resource Names 117 | 118 | For now, resources are identified by their string names (not URIs or paths): 119 | - `"PROJECT_SUMMARY.md"` - Project overview 120 | - `"README.md"` - Main documentation 121 | - `"CLAUDE.md"` - Claude-specific instructions 122 | - `"LLM_CODE_STYLE.md"` - Coding style guide 123 | 124 | ### Examples 125 | 126 | **Only project documentation:** 127 | ```edn 128 | {:enable-resources ["PROJECT_SUMMARY.md" "README.md"]} 129 | ``` 130 | 131 | **Remove AI-specific resources:** 132 | ```edn 133 | {:disable-resources ["CLAUDE.md" "LLM_CODE_STYLE.md"]} 134 | ``` 135 | 136 | ## See Also 137 | 138 | - [Model Configuration](model-configuration.md) - Configure custom LLM models 139 | - [Tools Configuration](tools-configuration.md) - Configure tool-specific settings 140 | - [Creating Custom MCP Servers](custom-mcp-server.md) - Build servers with custom filtering 141 | -------------------------------------------------------------------------------- /doc/prompt-cli.md: -------------------------------------------------------------------------------- 1 | # Clojure MCP Prompt CLI 2 | 3 | A command-line interface for interacting with an AI agent that has access to all Clojure MCP tools. 4 | 5 | ## Prerequisites 6 | 7 | - A running nREPL server (default port 7888, configurable) 8 | - Configured API keys for the chosen model (Anthropic, OpenAI, etc.) 9 | 10 | ## Usage 11 | 12 | Start your nREPL server: 13 | ```bash 14 | clojure -M:nrepl 15 | ``` 16 | 17 | In another terminal, run the CLI: 18 | ```bash 19 | clojure -M:prompt-cli -p "Your prompt here" 20 | ``` 21 | 22 | ## Options 23 | 24 | - `-p, --prompt PROMPT` - The prompt to send to the agent (required) 25 | - `-r, --resume` - Resume the most recent session with its conversation history 26 | - `-m, --model MODEL` - Override the default model (e.g., `:openai/gpt-4`, `:anthropic/claude-3-5-sonnet`) 27 | - `-c, --config CONFIG` - Path to a custom agent configuration file (optional) 28 | - `-d, --dir DIRECTORY` - Working directory (defaults to REPL's working directory) 29 | - `-P, --port PORT` - nREPL server port (default: 7888) 30 | - `-h, --help` - Show help message 31 | 32 | ## Examples 33 | 34 | Basic usage with default model: 35 | ```bash 36 | clojure -M:prompt-cli -p "What namespaces are available?" 37 | ``` 38 | 39 | Use a specific model: 40 | ```bash 41 | clojure -M:prompt-cli -p "Evaluate (+ 1 2)" -m :openai/gpt-4 42 | ``` 43 | 44 | Create code: 45 | ```bash 46 | clojure -M:prompt-cli -p "Create a fibonacci function" 47 | ``` 48 | 49 | Use a custom agent configuration: 50 | ```bash 51 | clojure -M:prompt-cli -p "Analyze this project" -c my-custom-agent.edn 52 | ``` 53 | 54 | Connect to a different nREPL port: 55 | ```bash 56 | clojure -M:prompt-cli -p "Run tests" -P 8888 57 | ``` 58 | 59 | Specify a working directory: 60 | ```bash 61 | clojure -M:prompt-cli -p "List files" -d /path/to/project 62 | ``` 63 | 64 | Resume the most recent session: 65 | ```bash 66 | clojure -M:prompt-cli --resume -p "Continue with the next step" 67 | ``` 68 | 69 | Resume with a different model: 70 | ```bash 71 | clojure -M:prompt-cli --resume -p "Now refactor the code" -m :openai/gpt-4 72 | ``` 73 | 74 | ## Session Persistence 75 | 76 | Sessions are automatically saved after each prompt execution: 77 | - Sessions are stored in `.clojure-mcp/prompt-cli-sessions/` within your working directory 78 | - Each session file is timestamped (e.g., `2025-11-06T14-30-45.json`) 79 | - Sessions contain the full conversation history and model information 80 | - Use `--resume` to continue from the most recent session 81 | - When resuming, the conversation history is displayed before processing your new prompt 82 | - You can resume with a different model using `-m` to override the original session's model 83 | 84 | ## Configuration 85 | 86 | The CLI properly initializes the nREPL connection with: 87 | - Automatic detection of the working directory from the REPL 88 | - Loading of `.clojure-mcp/config.edn` from the working directory 89 | - Environment detection and initialization (Clojure, ClojureScript, etc.) 90 | - Loading of REPL helper functions 91 | 92 | ## Default Agent Configuration 93 | 94 | By default, the CLI uses the `parent-agent-config` which includes: 95 | - The Clojure REPL system prompt 96 | - Access to all available tools 97 | - Project context (code index and summary) 98 | - Stateless memory (each invocation is independent) 99 | 100 | ## Custom Agent Configuration 101 | 102 | You can create a custom agent configuration file in EDN format: 103 | 104 | ```clojure 105 | {:id :my-agent 106 | :name "my_agent" 107 | :description "My custom agent" 108 | :system-message "Your system prompt here..." 109 | :context true ; Include project context 110 | :enable-tools [:read_file :clojure_eval :grep] ; Specific tools or [:all] 111 | :memory-size 100 ; Or false for stateless 112 | :model :anthropic/claude-3-5-sonnet-20241022} 113 | ``` 114 | 115 | ## Environment Variables 116 | 117 | Set `DEBUG=1` to see stack traces on errors: 118 | ```bash 119 | DEBUG=1 clojure -M:prompt-cli -p "Your prompt" 120 | ``` 121 | 122 | ## Model Configuration 123 | 124 | Models can be configured in `.clojure-mcp/config.edn`: 125 | ```clojure 126 | {:models {:openai/my-gpt4 {:model-name "gpt-4" 127 | :temperature 0.3 128 | :api-key [:env "OPENAI_API_KEY"]}}} 129 | ``` 130 | 131 | Then use with: 132 | ```bash 133 | clojure -M:prompt-cli -p "Your prompt" -m :openai/my-gpt4 134 | ``` 135 | 136 | ## Tool Configuration 137 | 138 | The agent has access to all tools by default, which are filtered based on the project's `.clojure-mcp/config.edn` settings: 139 | - `enable-tools` and `disable-tools` settings are respected 140 | - Tool-specific configurations from `tools-config` are applied 141 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/unified_clojure_edit/pipeline.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.unified-clojure-edit.pipeline 2 | "Pipeline architecture for pattern-based Clojure code editing operations. 3 | Provides a thread-first pattern with error short-circuiting and 4 | standardized context maps." 5 | (:require 6 | [clojure-mcp.tools.unified-clojure-edit.core :as core] 7 | [clojure-mcp.tools.form-edit.pipeline :as form-edit-pipeline] 8 | [rewrite-clj.zip :as z] 9 | [clojure.spec.alpha :as s])) 10 | 11 | ;; Additional custom spec for the pattern string 12 | (s/def ::pattern string?) 13 | 14 | (defn find-form 15 | "Finds a form using pattern matching. 16 | Requires ::zloc and ::pattern in the context. 17 | Updates ::zloc to point to the matched form or returns an error context if no match found." 18 | [ctx] 19 | (let [zloc (::form-edit-pipeline/zloc ctx) 20 | pattern (::pattern ctx) 21 | result (core/find-pattern-match zloc pattern)] 22 | (if (:zloc result) 23 | (assoc ctx ::form-edit-pipeline/zloc (:zloc result)) 24 | {::form-edit-pipeline/error true 25 | ::form-edit-pipeline/message (str "Could not find pattern match for: " pattern 26 | " in file " (::form-edit-pipeline/file-path ctx))}))) 27 | 28 | (defn check-for-duplicate-matches 29 | "Checks if there are multiple matches for the pattern. 30 | Requires ::zloc and ::pattern in the context. 31 | Returns an error context if multiple matches are found." 32 | [ctx] 33 | (let [zloc (::form-edit-pipeline/zloc ctx) 34 | pattern (::pattern ctx) 35 | ;; Start from the next position after the current match 36 | next-zloc (z/next zloc) 37 | ;; Use the same function that was used for the first match 38 | second-match (core/find-pattern-match next-zloc pattern)] 39 | (if (:zloc second-match) 40 | ;; Found a second match - this is an error 41 | {::form-edit-pipeline/error true 42 | ::form-edit-pipeline/message 43 | (str "Multiple matches found for pattern: " pattern 44 | "\nFirst match: " (z/string zloc) 45 | "\nSecond match: " (z/string (:zloc second-match)) 46 | "\nPlease use a more specific pattern to ensure a unique match.")} 47 | ;; No second match found - this is good 48 | ctx))) 49 | 50 | (defn edit-form 51 | "Edits the form according to the specified edit type. 52 | Requires ::zloc, ::pattern, ::new-source-code, and ::edit-type in the context. 53 | Updates ::zloc with the edited zipper." 54 | [ctx] 55 | (let [zloc (::form-edit-pipeline/zloc ctx) 56 | pattern (::pattern ctx) 57 | content (::form-edit-pipeline/new-source-code ctx) 58 | edit-type (::form-edit-pipeline/edit-type ctx) 59 | result (core/edit-matched-form zloc pattern content edit-type)] 60 | (if (:zloc result) 61 | (assoc ctx ::form-edit-pipeline/zloc (:zloc result)) 62 | {::form-edit-pipeline/error true 63 | ::form-edit-pipeline/message (str "Failed to " (name edit-type) " form matching pattern: " pattern)}))) 64 | 65 | ;; Define the main pipeline for pattern-based Clojure editing 66 | (defn pattern-edit-pipeline 67 | "Pipeline for handling pattern-based Clojure code editing operations. 68 | 69 | Arguments: 70 | - file-path: Path to the file to edit 71 | - pattern: Pattern string to match (with ? and * wildcards) 72 | - content-str: New content to insert 73 | - edit-type: Type of edit (:replace, :insert_before, :insert_after) 74 | - nrepl-client-atom: Atom containing the nREPL client (optional) 75 | - config: Optional tool configuration map 76 | 77 | Returns a context map with the result of the operation" 78 | [file-path pattern content-str edit-type {:keys [nrepl-client-atom] :as config}] 79 | (let [ctx {::form-edit-pipeline/file-path file-path 80 | ::pattern pattern 81 | ::form-edit-pipeline/new-source-code content-str 82 | ::form-edit-pipeline/edit-type edit-type 83 | ::form-edit-pipeline/nrepl-client-atom nrepl-client-atom 84 | ::form-edit-pipeline/config config}] 85 | (form-edit-pipeline/thread-ctx 86 | ctx 87 | form-edit-pipeline/lint-repair-code 88 | form-edit-pipeline/load-source 89 | form-edit-pipeline/check-file-modified 90 | form-edit-pipeline/parse-source 91 | find-form 92 | check-for-duplicate-matches 93 | edit-form 94 | form-edit-pipeline/zloc->output-source 95 | form-edit-pipeline/format-source 96 | form-edit-pipeline/determine-file-type 97 | form-edit-pipeline/generate-diff 98 | form-edit-pipeline/save-file 99 | form-edit-pipeline/update-file-timestamp))) 100 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/file_edit/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.file-edit.tool 2 | "Implementation of the file-edit tool using the tool-system multimethod approach." 3 | (:require 4 | [clojure-mcp.tool-system :as tool-system] 5 | [clojure-mcp.tools.file-edit.pipeline :as pipeline] 6 | [clojure-mcp.utils.valid-paths :as valid-paths])) 7 | 8 | ;; Factory function to create the tool configuration 9 | (defn create-file-edit-tool 10 | "Creates the file-edit tool configuration" 11 | [nrepl-client-atom] 12 | {:tool-type :file-edit 13 | :nrepl-client-atom nrepl-client-atom}) 14 | 15 | ;; Implement the required multimethods for the file-edit tool 16 | (defmethod tool-system/tool-name :file-edit [_] 17 | "file_edit") 18 | 19 | (defmethod tool-system/tool-description :file-edit [_] 20 | "Edit a file by replacing a specific text string with a new one. For safety, this tool requires that the string to replace appears exactly once in the file. 21 | 22 | FIRST: the clojure_edit tool is prefered because they use precise structural rewrite-clj editing. They also incorporate linting and ensure balance parenthesis. 23 | 24 | WHEN the clojure_edit tool won't work or you have a small easy edit 25 | 26 | PREFER the file_write tool for replacing more than half a file, this saves on tokens 27 | 28 | For Clojure files (.clj, .cljs, .cljc, .edn): 29 | - Content will be linted for syntax errors before writing 30 | - Content will be formatted according to Clojure standards 31 | - Writing will fail if linting detects syntax errors 32 | 33 | To make a file edit, provide the file_path, old_string (the text to replace), and new_string (the replacement text). The old_string must uniquely identify the specific instance you want to change, so include several lines of context before and after the change point. To create a new file, use file_write instead.") 34 | 35 | (defmethod tool-system/tool-schema :file-edit [_] 36 | {:type :object 37 | :properties {:file_path {:type :string 38 | :description "The absolute path to the file to modify (must be absolute, not relative)"} 39 | :old_string {:type :string 40 | :description "The text to replace (must match the file contents exactly, including all whitespace and indentation)."} 41 | :new_string {:type :string 42 | :description "The edited text to replace the old_string"}} 43 | :required [:file_path :old_string :new_string]}) 44 | 45 | (defmethod tool-system/validate-inputs :file-edit [{:keys [nrepl-client-atom]} inputs] 46 | (let [{:keys [file_path old_string new_string]} inputs 47 | nrepl-client @nrepl-client-atom] 48 | 49 | ;; Check required parameters 50 | (when-not file_path 51 | (throw (ex-info "Missing required parameter: file_path" {:inputs inputs}))) 52 | 53 | (when-not (contains? inputs :old_string) 54 | (throw (ex-info "Missing required parameter: old_string" {:inputs inputs}))) 55 | 56 | (when-not (contains? inputs :new_string) 57 | (throw (ex-info "Missing required parameter: new_string" {:inputs inputs}))) 58 | 59 | ;; Reject empty old_string - direct users to file_write instead 60 | (when (empty? old_string) 61 | (throw (ex-info "Empty old_string is not supported. To create a new file, use file_write instead." 62 | {:inputs inputs}))) 63 | 64 | ;; Check for identical strings (early rejection) 65 | (when (= old_string new_string) 66 | (throw (ex-info "No changes to make: old_string and new_string are exactly the same." 67 | {:inputs inputs}))) 68 | 69 | ;; Validate path using the utility function 70 | (let [validated-path (valid-paths/validate-path-with-client file_path nrepl-client)] 71 | ;; Return validated inputs with normalized path 72 | (assoc inputs 73 | :file_path validated-path 74 | :old_string old_string 75 | :new_string new_string)))) 76 | 77 | (defmethod tool-system/execute-tool :file-edit [{:keys [_nrepl-client-atom] :as tool} inputs] 78 | (let [{:keys [file_path old_string new_string dry_run]} inputs 79 | result (pipeline/file-edit-pipeline file_path old_string new_string dry_run tool)] 80 | (pipeline/format-result result))) 81 | 82 | (defmethod tool-system/format-results :file-edit [_ {:keys [error message diff new-source type repaired]}] 83 | (if error 84 | {:error true 85 | :result [message]} 86 | (cond-> {:error false 87 | :result [(or new-source diff)] 88 | :type type} 89 | ;; Include repaired flag if present 90 | repaired (assoc :repaired true)))) 91 | 92 | ;; Backward compatibility function that returns the registration map 93 | (defn file-edit-tool [nrepl-client-atom] 94 | (tool-system/registration-map (create-file-edit-tool nrepl-client-atom))) -------------------------------------------------------------------------------- /src/clojure_mcp/tools/agent_tool_builder/file_changes.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.agent-tool-builder.file-changes 2 | "Tracks file changes during agent execution" 3 | (:require 4 | [clojure.java.io :as io] 5 | [clojure-mcp.utils.diff :as diff-utils] 6 | [clojure-mcp.utils.file :as file-utils] 7 | [clojure.string :as str] 8 | [taoensso.timbre :as log])) 9 | 10 | (defn reset-changed-files! 11 | "Reset the changed-files map to empty. 12 | 13 | Args: 14 | - nrepl-client-atom: The nREPL client atom" 15 | [nrepl-client-atom] 16 | (swap! nrepl-client-atom assoc :clojure-mcp.agent-tool-builder/changed-files {})) 17 | 18 | (defn capture-original-content! 19 | "Captures the original content of a file if not already captured. 20 | 21 | Args: 22 | - nrepl-client-atom: The nREPL client atom 23 | - file-path: Path to the file 24 | - content: Current content of the file 25 | 26 | Returns: The content (unchanged)" 27 | [nrepl-client-atom file-path content] 28 | (when nrepl-client-atom 29 | (try 30 | (let [canonical-path (.getCanonicalPath (io/file file-path)) 31 | changed-files-key :clojure-mcp.agent-tool-builder/changed-files 32 | changed-files (get @nrepl-client-atom changed-files-key {})] 33 | (when-not (contains? changed-files canonical-path) 34 | (swap! nrepl-client-atom assoc-in [changed-files-key canonical-path] content))) 35 | (catch Exception e 36 | (log/warn "Failed to capture original content for" file-path "-" (.getMessage e))))) 37 | content) 38 | 39 | (defn capture-original-file-content 40 | "Pipeline step that captures the original file content before any edits. 41 | Only captures if the file hasn't been seen before in this agent session. 42 | 43 | Expects :clojure-mcp.tools.form-edit.pipeline/file-path and 44 | :clojure-mcp.tools.form-edit.pipeline/source (or old-content) in the context. 45 | Returns context unchanged." 46 | [ctx] 47 | (let [nrepl-client-atom (:clojure-mcp.tools.form-edit.pipeline/nrepl-client-atom ctx) 48 | file-path (:clojure-mcp.tools.form-edit.pipeline/file-path ctx) 49 | content (or (:clojure-mcp.tools.form-edit.pipeline/source ctx) 50 | (:clojure-mcp.tools.form-edit.pipeline/old-content ctx))] 51 | (when (and nrepl-client-atom file-path content) 52 | (capture-original-content! nrepl-client-atom file-path content)) 53 | ctx)) 54 | 55 | (defn format-file-diff 56 | "Formats a diff for a single file. 57 | 58 | Args: 59 | - file-path: The canonical path to the file 60 | - original-content: Original content (may be empty string for new files) 61 | - current-content: Current content of the file 62 | 63 | Returns: Formatted diff string or error message" 64 | [file-path original-content current-content] 65 | (try 66 | (let [diff (if (= original-content current-content) 67 | "No changes" 68 | (diff-utils/generate-unified-diff 69 | (or original-content "") 70 | (or current-content "")))] 71 | (str "#### " file-path "\n```diff\n" diff "\n```\n")) 72 | (catch Exception e 73 | (str "#### " file-path "\n```\nError generating diff: " (.getMessage e) "\n```\n")))) 74 | 75 | (defn generate-all-diffs 76 | "Generates diffs for all changed files. 77 | 78 | Args: 79 | - nrepl-client-atom: The nREPL client atom 80 | 81 | Returns: String with all formatted diffs" 82 | [nrepl-client-atom] 83 | (try 84 | (let [changed-files (get @nrepl-client-atom :clojure-mcp.agent-tool-builder/changed-files {})] 85 | (if (empty? changed-files) 86 | "" 87 | (let [diffs (for [[canonical-path original-content] changed-files] 88 | (let [current-file (io/file canonical-path)] 89 | (if (.exists current-file) 90 | (let [current-content (file-utils/slurp-utf8 current-file)] 91 | (format-file-diff canonical-path original-content current-content)) 92 | ;; File was deleted 93 | (format-file-diff canonical-path original-content ""))))] 94 | (str "## File Changes\n\n" (str/join "\n" diffs) "\n")))) 95 | (catch Exception e 96 | (str "## File Changes\n\nError collecting file changes: " (.getMessage e) "\n\n")))) 97 | 98 | (defn should-track-changes? 99 | "Checks if file change tracking is enabled for agents. 100 | 101 | Args: 102 | - agent-config: The agent configuration map 103 | 104 | Returns: Boolean indicating if tracking is enabled" 105 | [agent-config] 106 | ;; Default to true, but allow disabling via :track-file-changes false 107 | (get agent-config :track-file-changes true)) 108 | -------------------------------------------------------------------------------- /src/clojure_mcp/main.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.main 2 | (:require [clojure-mcp.core :as core] 3 | [clojure-mcp.logging :as logging] 4 | [clojure-mcp.prompts :as prompts] 5 | [clojure-mcp.resources :as resources] 6 | [clojure-mcp.tools :as tools])) 7 | 8 | ;; Delegate to resources namespace 9 | ;; Note: working-dir param kept for compatibility with core API but unused 10 | (defn make-resources [nrepl-client-atom _working-dir] 11 | (resources/make-resources nrepl-client-atom)) 12 | 13 | ;; Delegate to prompts namespace 14 | ;; Note: working-dir param kept for compatibility with core API but unused 15 | (defn make-prompts [nrepl-client-atom _working-dir] 16 | (prompts/make-prompts nrepl-client-atom)) 17 | 18 | (defn make-tools [nrepl-client-atom _working-directory] 19 | ;; Use the refactored tools builder 20 | ;; Note: working-directory param kept for compatibility with core API but unused 21 | (tools/build-all-tools nrepl-client-atom)) 22 | 23 | ;; DEPRECATED but maintained for backward compatability 24 | (defn ^:deprecated my-prompts 25 | ([working-dir] 26 | (my-prompts working-dir core/nrepl-client-atom)) 27 | ([working-dir nrepl-client-atom] 28 | (make-prompts nrepl-client-atom working-dir))) 29 | 30 | (defn ^:deprecated my-resources [nrepl-client-atom _working-dir] 31 | (resources/make-resources nrepl-client-atom)) 32 | 33 | (defn ^:deprecated my-tools [nrepl-client-atom] 34 | (tools/build-all-tools nrepl-client-atom)) 35 | 36 | (defn start-mcp-server 37 | "Entry point for MCP server startup. 38 | 39 | When :project-dir is NOT provided, requires a REPL connection to discover 40 | the project directory. When :project-dir IS provided, REPL is optional. 41 | 42 | REPL initialization happens lazily on first eval-code call." 43 | [opts] 44 | ;; Configure logging before starting the server 45 | (logging/configure-logging! 46 | {:log-file (get opts :log-file logging/default-log-file) 47 | :enable-logging? (get opts :enable-logging? false) 48 | :log-level (get opts :log-level :debug)}) 49 | (core/build-and-start-mcp-server 50 | (dissoc opts :log-file :log-level :enable-logging?) 51 | {:make-tools-fn make-tools 52 | :make-prompts-fn make-prompts 53 | :make-resources-fn make-resources})) 54 | 55 | (defn start 56 | "Entry point for running from project directory. 57 | 58 | Sets :project-dir to current working directory unless :not-cwd is true. 59 | This allows running without an immediate REPL connection - REPL initialization 60 | happens lazily when first needed. 61 | 62 | Options: 63 | - :not-cwd - If true, does NOT set project-dir to cwd (default: false) 64 | - :port - Optional nREPL port (REPL is optional when project-dir is set) 65 | - All other options supported by start-mcp-server" 66 | [opts] 67 | (let [not-cwd? (get opts :not-cwd false) 68 | opts' (if not-cwd? 69 | opts 70 | (assoc opts :project-dir (System/getProperty "user.dir")))] 71 | (start-mcp-server opts'))) 72 | 73 | ;; not sure if this is even needed 74 | 75 | ;; start the server 76 | 77 | ;; Example parameterized prompt 78 | (defn code-review-prompt-example [] 79 | {:name "code-review-prompt" 80 | :description "Generate a code review prompt for a specific file or namespace" 81 | :arguments [{:name "file-path" 82 | :description "The file path to review" 83 | :required? true} 84 | {:name "focus-areas" 85 | :description "Specific areas to focus on (e.g., 'performance,style,testing')" 86 | :required? false}] 87 | :prompt-fn (fn [_ request-args clj-result-k] 88 | (let [file-path (get request-args "file-path") 89 | focus-areas (get request-args "focus-areas" "general code quality")] 90 | (clj-result-k 91 | {:description (str "Code review for: " file-path) 92 | :messages 93 | [{:role :user 94 | :content 95 | (str "Please perform a thorough code review of the file at: " 96 | file-path "\n\n" 97 | "Focus areas: " focus-areas "\n\n" 98 | "Consider:\n" 99 | "1. Code style and Clojure idioms\n" 100 | "2. Performance implications\n" 101 | "3. Error handling\n" 102 | "4. Function complexity and readability\n" 103 | "5. Missing tests or edge cases\n\n" 104 | "Please use the read_file tool to examine the code, " 105 | "then provide detailed feedback.")}]})))}) 106 | 107 | -------------------------------------------------------------------------------- /src/clojure_mcp/tool_system.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tool-system 2 | "Core system for defining and registering MCP tools. 3 | This namespace provides multimethods for implementing tools 4 | in a modular, extensible way." 5 | (:require 6 | [clojure.string :as string] 7 | [clojure.walk :as walk] 8 | [taoensso.timbre :as log])) 9 | 10 | ;; Core multimethods for tool behavior 11 | 12 | (defmulti tool-name 13 | "Returns the name of the tool as a string. Dispatches on :tool-type." 14 | :tool-type) 15 | 16 | (defmethod tool-name :default [tool-config] 17 | (-> tool-config 18 | :tool-type 19 | name 20 | (string/replace "-" "_"))) 21 | 22 | (defmulti tool-id :tool-type) 23 | 24 | (defmethod tool-id :default [tool-config] 25 | (keyword (tool-name tool-config))) 26 | 27 | (defmulti tool-description 28 | "Returns the description of the tool as a string. Dispatches on :tool-type." 29 | :tool-type) 30 | 31 | (defmulti tool-schema 32 | "Returns the parameter validation schema for the tool. Dispatches on :tool-type." 33 | :tool-type) 34 | 35 | (defmulti validate-inputs 36 | "Validates inputs against the schema and returns validated/coerced inputs. 37 | Throws exceptions for invalid inputs. 38 | Dispatches on :tool-type in the tool-config." 39 | (fn [tool-config _inputs] (:tool-type tool-config))) 40 | 41 | (defmulti execute-tool 42 | "Executes the tool with the validated inputs and returns the result. 43 | Dispatches on :tool-type in the tool-config." 44 | (fn [tool-config _inputs] (:tool-type tool-config))) 45 | 46 | (defmulti format-results 47 | "Formats the results from tool execution into the expected MCP response format. 48 | Must return a map with :result (a vector or sequence of strings) and :error (boolean). 49 | The MCP protocol requires that results are always provided as a sequence of strings, 50 | never as a single string. 51 | 52 | This standardized format is then used by the tool-fn to call the callback with: 53 | (callback (:result formatted) (:error formatted)) 54 | 55 | Dispatches on :tool-type in the tool-config." 56 | (fn [tool-config _result] (:tool-type tool-config))) 57 | 58 | ;; Multimethod to assemble the registration map 59 | 60 | (defmulti registration-map 61 | "Creates the MCP registration map for a tool. 62 | Dispatches on :tool-type." 63 | :tool-type) 64 | 65 | ;; Function to handle java.util.Map and other collection types before keywordizing 66 | (defn convert-java-collections 67 | "Converts Java collection types to their Clojure equivalents recursively." 68 | [x] 69 | (clojure.walk/prewalk 70 | (fn [node] 71 | (cond 72 | (instance? java.util.Map node) (into {} node) 73 | (instance? java.util.List node) (into [] node) 74 | (instance? java.util.Set node) (into #{} node) 75 | :else node)) 76 | x)) 77 | 78 | ;; Helper function to keywordize map keys while preserving underscores 79 | (defn keywordize-keys-preserve-underscores 80 | "Recursively transforms string map keys into keywords. 81 | Unlike clojure.walk/keywordize-keys, this preserves underscores. 82 | Works with Java collection types by converting them first." 83 | [m] 84 | (walk/keywordize-keys (convert-java-collections m))) 85 | 86 | ;; Default implementation for registration-map 87 | (defmethod registration-map :default [tool-config] 88 | {:name (tool-name tool-config) 89 | :id (tool-id tool-config) 90 | :description (tool-description tool-config) 91 | :schema (tool-schema tool-config) 92 | :tool-fn (fn [_ params callback] 93 | (try 94 | (let [keywordized-params (keywordize-keys-preserve-underscores params) 95 | validated (validate-inputs tool-config keywordized-params) 96 | result (execute-tool tool-config validated) 97 | formatted (format-results tool-config result)] 98 | (callback (:result formatted) (:error formatted))) 99 | (catch Exception e 100 | (log/error e) 101 | ;; On error, create a sequence of error messages 102 | (let [error-msg (or (ex-message e) "Unknown error") 103 | data (ex-data e) 104 | ;; Construct error messages sequence 105 | error-msgs (cond-> [error-msg] 106 | ;; Add any error-details from ex-data if available 107 | (and data (:error-details data)) 108 | (concat (if (sequential? (:error-details data)) 109 | (:error-details data) 110 | [(:error-details data)])))] 111 | (callback error-msgs true)))))}) 112 | 113 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/test_utils.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.test-utils 2 | "Utility functions for testing the tool-system based tools." 3 | (:require 4 | [clojure-mcp.nrepl :as nrepl] 5 | [nrepl.server :as nrepl-server] 6 | [clojure-mcp.tool-system :as tool-system] 7 | [clojure.test :refer [use-fixtures]] 8 | [clojure.java.io :as io] 9 | [clojure-mcp.tools.unified-read-file.file-timestamps :as file-timestamps])) 10 | 11 | (defonce ^:dynamic *nrepl-server* nil) 12 | (defonce ^:dynamic *nrepl-client-atom* nil) 13 | (def ^:dynamic *test-file-path* "test_function_edit.clj") 14 | 15 | (defn test-nrepl-fixture [f] 16 | (let [server (nrepl-server/start-server :port 0) ; Use port 0 for dynamic port assignment 17 | port (:port server) 18 | client (nrepl/create {:port port}) 19 | client-atom (atom client)] 20 | (nrepl/eval-code client "(require 'clojure.repl)") 21 | (binding [*nrepl-server* server 22 | *nrepl-client-atom* client-atom] 23 | (try 24 | (f) 25 | (finally 26 | (nrepl-server/stop-server server)))))) 27 | 28 | (defn cleanup-test-file [f] 29 | (try 30 | (f) 31 | (finally 32 | #_(io/delete-file *test-file-path* true)))) 33 | 34 | ;; Helper to invoke full tool function directly using the tool registration map 35 | (defn make-tool-tester 36 | "Takes a tool instance and returns a function that executes the tool directly. 37 | The returned function takes a map of tool inputs and returns a map with: 38 | {:result result :error? error-flag}" 39 | [tool-instance] 40 | (let [reg-map (tool-system/registration-map tool-instance) 41 | tool-fn (:tool-fn reg-map)] 42 | (fn [inputs] 43 | (let [prom (promise)] 44 | (tool-fn nil inputs 45 | (fn [res error?] 46 | (deliver prom {:result res :error? error?}))) 47 | @prom)))) 48 | 49 | ;; Helper to test individual multimethod pipeline steps 50 | (defn test-pipeline-steps 51 | "Executes the validation, execution, and formatting steps of the tool pipeline 52 | and returns the formatted result." 53 | [tool-instance inputs] 54 | (let [validated (tool-system/validate-inputs tool-instance inputs) 55 | execution-result (tool-system/execute-tool tool-instance validated) 56 | formatted-result (tool-system/format-results tool-instance execution-result)] 57 | formatted-result)) 58 | 59 | ;; Apply fixtures in each test namespace 60 | (defn create-test-dir 61 | "Creates a temporary test directory with a unique name" 62 | [] 63 | (let [temp-dir (io/file (System/getProperty "java.io.tmpdir")) 64 | test-dir (io/file temp-dir (str "clojure-mcp-test-" (System/currentTimeMillis)))] 65 | (.mkdirs test-dir) 66 | (.getAbsolutePath test-dir))) 67 | 68 | (defn create-and-register-test-file 69 | "Creates a test file with the given content and registers it in the timestamp tracker" 70 | [client-atom dir filename content] 71 | (let [file-path (str dir "/" filename) 72 | _ (io/make-parents file-path) 73 | _ (spit file-path content) 74 | file-obj (io/file file-path) 75 | canonical-path (.getCanonicalPath file-obj)] 76 | ;; Register the file using its canonical path in the timestamp tracker 77 | (file-timestamps/update-file-timestamp-to-current-mtime! client-atom canonical-path) 78 | ;; Small delay to ensure future modifications have different timestamps 79 | (Thread/sleep 25) 80 | ;; Return the canonical path for consistent usage 81 | canonical-path)) 82 | 83 | (defn modify-test-file 84 | "Modifies a test file and updates its timestamp in the tracker if update-timestamp? is true" 85 | [client-atom file-path content & {:keys [update-timestamp?] :or {update-timestamp? false}}] 86 | (spit file-path content) 87 | (when update-timestamp? 88 | (file-timestamps/update-file-timestamp-to-current-mtime! client-atom file-path) 89 | ;; Small delay 90 | (Thread/sleep 25)) 91 | file-path) 92 | 93 | (defn read-and-register-test-file 94 | "Updates the timestamp for an existing file to mark it as read. 95 | Normalizes the file path to ensure consistent lookup." 96 | [client-atom file-path] 97 | (let [normalized-path (.getCanonicalPath (io/file file-path))] 98 | (file-timestamps/update-file-timestamp-to-current-mtime! client-atom normalized-path) 99 | ;; Small delay to ensure timestamps differ if modified 100 | (Thread/sleep 25) 101 | normalized-path)) 102 | 103 | (defn clean-test-dir 104 | "Recursively deletes a test directory" 105 | [dir-path] 106 | (let [dir (io/file dir-path)] 107 | (when (.exists dir) 108 | (doseq [file (reverse (file-seq dir))] 109 | (.delete file))))) 110 | 111 | (defn apply-fixtures [_test-namespace] 112 | (use-fixtures :once test-nrepl-fixture) 113 | (use-fixtures :each cleanup-test-file)) 114 | -------------------------------------------------------------------------------- /src/clojure_mcp/main_examples/shadow_main.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.main-examples.shadow-main 2 | "Example of a custom MCP server that adds ClojureScript evaluation via Shadow CLJS. 3 | 4 | This demonstrates the new pattern for creating custom MCP servers: 5 | 1. Define a make-tools function that extends the base tools 6 | 2. Optionally define make-prompts and make-resources functions 7 | 3. Call core/build-and-start-mcp-server with factory functions 8 | 9 | Shadow CLJS support can work in two modes: 10 | - Single connection: Share the Clojure nREPL for both CLJ and CLJS 11 | - Dual connection: Connect to a separate Shadow CLJS nREPL server" 12 | (:require 13 | [clojure-mcp.core :as core] 14 | [clojure-mcp.logging :as logging] 15 | [clojure-mcp.nrepl :as nrepl] 16 | [taoensso.timbre :as log] 17 | [clojure-mcp.main :as main] 18 | [clojure-mcp.tools.eval.tool :as eval-tool])) 19 | 20 | (def tool-name "clojurescript_eval") 21 | 22 | (def description 23 | "Takes a ClojureScript Expression and evaluates it in the current namespace. For example, providing `(+ 1 2)` will evaluate to 3. 24 | 25 | **Project File Access**: Can load and use any ClojureScript file from your project with `(require '[your-namespace.core :as core] :reload)`. Always use `:reload` to ensure you get the latest version of files. Access functions, examine state with `@your-atom`, and manipulate application data for debugging and testing. 26 | 27 | **Important**: Both `require` and `ns` `:require` clauses can only reference actual files from your project, not namespaces created in the same REPL session. 28 | 29 | JavaScript interop is fully supported including `js/console.log`, `js/setTimeout`, DOM APIs, etc. 30 | 31 | **IMPORTANT**: This repl is intended for CLOJURESCRIPT CODE only.") 32 | 33 | (defn start-shadow-repl [nrepl-client-atom {:keys [shadow-build shadow-watch]}] 34 | (let [start-code (format 35 | ;; TODO we need to check if its already running 36 | ;; here and only initialize if it isn't 37 | (if shadow-watch 38 | "(do (shadow/watch %s) (shadow/repl %s))" 39 | "(do (shadow/repl %s) %s)") 40 | (pr-str (keyword (name shadow-build))) 41 | (pr-str (keyword (name shadow-build))))] 42 | (log/info "Starting Shadow CLJS...") 43 | (try 44 | (nrepl/eval-code @nrepl-client-atom start-code :session-type :shadow) 45 | (log/info "Shadow CLJS started (or command sent)") 46 | (catch Exception e 47 | (log/error e "ERROR in shadow start"))) 48 | :shadow)) 49 | 50 | ;; when having a completely different connection for cljs 51 | (defn shadow-eval-tool-secondary-connection-tool [nrepl-client-atom {:keys [shadow-port _shadow-build _shadow-watch] :as config}] 52 | (let [cljs-nrepl-client-map (core/create-additional-connection nrepl-client-atom {:port shadow-port}) 53 | cljs-nrepl-client-atom (atom cljs-nrepl-client-map)] 54 | (start-shadow-repl 55 | cljs-nrepl-client-atom 56 | config) 57 | (-> (eval-tool/eval-code cljs-nrepl-client-atom {:session-type :shadow}) 58 | (assoc :name tool-name) 59 | (assoc :id (keyword tool-name)) 60 | (assoc :description description)))) 61 | 62 | ;; when sharing the clojure and cljs repl 63 | (defn shadow-eval-tool [nrepl-client-atom {:keys [_shadow-build _shadow-watch] :as config}] 64 | (start-shadow-repl nrepl-client-atom config) 65 | (-> (eval-tool/eval-code nrepl-client-atom {:session-type :shadow}) 66 | (assoc :name tool-name) 67 | (assoc :id (keyword tool-name)) 68 | (assoc :description description))) 69 | 70 | ;; So we can set up shadow two ways 71 | ;; 1. as a single repl connection using the shadow clojure connection for cloj eval 72 | ;; 2. or the user starts two processes one for clojure and then we connect to shadow 73 | ;; as a secondary connection 74 | 75 | (defn make-tools [nrepl-client-atom working-directory & [{:keys [port shadow-port _shadow-build _shadow-watch] :as config}]] 76 | (if (and port shadow-port (not= port shadow-port)) 77 | (conj (main/make-tools nrepl-client-atom working-directory) 78 | (shadow-eval-tool-secondary-connection-tool nrepl-client-atom config)) 79 | (conj (main/make-tools nrepl-client-atom working-directory) 80 | (shadow-eval-tool nrepl-client-atom config)))) 81 | 82 | (defn start-mcp-server [opts] 83 | ;; Configure logging before starting the server 84 | (logging/configure-logging! 85 | {:log-file (get opts :log-file logging/default-log-file) 86 | :enable-logging? (get opts :enable-logging? false) 87 | :log-level (get opts :log-level :debug)}) 88 | (core/build-and-start-mcp-server 89 | (dissoc opts :log-file :log-level :enable-logging?) 90 | {:make-tools-fn (fn [nrepl-client-atom working-directory] 91 | (make-tools nrepl-client-atom working-directory opts)) 92 | :make-prompts-fn main/make-prompts 93 | :make-resources-fn main/make-resources})) 94 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/scratch_pad/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.scratch-pad.core-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [clojure.string :as str] 4 | [clojure-mcp.tools.scratch-pad.core :as core])) 5 | 6 | (deftest test-execute-set-path 7 | (testing "Setting values at paths" 8 | (let [result (core/execute-set-path {} ["a" "b"] 42)] 9 | (is (= {"a" {"b" 42}} (:data result))) 10 | (is (= 42 (get-in (:result result) [:value])))) 11 | 12 | (testing "Setting with string indices for vectors" 13 | (let [result (core/execute-set-path {} ["tasks" "0"] "first")] 14 | (is (= {"tasks" ["first"]} (:data result))) 15 | (is (= "first" (get-in (:result result) [:value]))))) 16 | 17 | (testing "Nested vector initialization" 18 | (let [result (core/execute-set-path {} ["data" "0" :items "0"] "nested")] 19 | (is (= {"data" [{:items ["nested"]}]} (:data result))))))) 20 | 21 | (deftest test-execute-get-path 22 | (testing "Getting values from paths" 23 | (let [data {"a" {"b" 42}} 24 | result (core/execute-get-path data ["a" "b"])] 25 | (is (= 42 (get-in result [:result :value]))) 26 | (is (true? (get-in result [:result :found])))) 27 | 28 | (testing "Getting with string indices" 29 | (let [data {"items" ["a" "b" "c"]} 30 | result (core/execute-get-path data ["items" "1"])] 31 | (is (= "b" (get-in result [:result :value]))))) 32 | 33 | (testing "Getting non-existent path" 34 | (let [result (core/execute-get-path {} ["missing"])] 35 | (is (nil? (get-in result [:result :value]))) 36 | (is (false? (get-in result [:result :found]))))))) 37 | 38 | (deftest test-execute-delete-path 39 | (testing "Deleting values at paths" 40 | (let [data {"a" {"b" 2 "c" 3}} 41 | result (core/execute-delete-path data ["a" "b"])] 42 | (is (= {"a" {"c" 3}} (:data result)))) 43 | 44 | (testing "Deleting from vector with string index" 45 | (let [data {"items" ["a" "b" "c"]} 46 | result (core/execute-delete-path data ["items" "1"])] 47 | (is (= {"items" ["a" "c"]} (:data result))))) 48 | 49 | (testing "Deleting entire key" 50 | (let [data {"a" {"b" 2}} 51 | result (core/execute-delete-path data ["a"])] 52 | (is (= {} (:data result))))))) 53 | 54 | (deftest test-execute-inspect 55 | (testing "Inspect entire data" 56 | (let [data {"a" 1} 57 | result (core/execute-inspect data 5 nil) 58 | tree-output (get-in result [:result :tree])] 59 | (is (string? tree-output)) 60 | ;; Check that output contains the key "a" 61 | (is (.contains tree-output "\"a\"")) 62 | ;; Output might be truncated or formatted differently 63 | (is (or (.contains tree-output "{\"a\" 1}") 64 | (.contains tree-output "{\"a\" ...}"))))) 65 | 66 | (testing "Inspect at path" 67 | (let [data {"a" {"b" {"c" 1}}} 68 | result (core/execute-inspect data 5 ["a"]) 69 | tree-output (get-in result [:result :tree])] 70 | (is (.contains tree-output "{\"b\"")) 71 | ;; The nested structure might be truncated 72 | (is (or (.contains tree-output "{\"c\" 1}") 73 | (.contains tree-output "{\"c\" ...}"))))) 74 | 75 | (testing "Inspect non-existent path" 76 | (let [result (core/execute-inspect {} 5 ["missing"])] 77 | (is (= "No data found at path [\"missing\"]" (get-in result [:result :tree]))))) 78 | 79 | (testing "Empty data inspect" 80 | (let [result (core/execute-inspect {} 5 nil) 81 | tree-output (get-in result [:result :tree])] 82 | ;; Account for possible newline 83 | (is (= "{}" (str/trim tree-output)))))) 84 | 85 | (deftest test-inspect-data 86 | (testing "Inspect data generation" 87 | (is (= "Empty scratch pad" (core/inspect-data {}))) 88 | (testing "Pretty printing simple data" 89 | (let [data {"a" 1} 90 | result (core/inspect-data data)] 91 | (is (string? result)) 92 | (is (.contains result "{\"a\" 1}")))) 93 | (testing "Pretty printing nested data" 94 | (let [nested {"a" {"b" {"c" 1}}} 95 | view (core/inspect-data nested)] 96 | (is (.contains view "{\"a\"")))))) 97 | 98 | (deftest test-smart-path-integration 99 | (testing "Smart path operations work correctly" 100 | ;; Test vector initialization with string "0" 101 | (let [result (core/execute-set-path {} ["items" "0"] "first")] 102 | (is (= {"items" ["first"]} (:data result)))) 103 | 104 | ;; Test getting from vector with string index 105 | (let [data {"items" ["a" "b" "c"]} 106 | result (core/execute-get-path data ["items" "2"])] 107 | (is (= "c" (get-in result [:result :value])))) 108 | 109 | ;; Test error on invalid vector initialization 110 | (is (thrown? Exception 111 | (core/execute-set-path {} ["items" "1"] "should-fail"))))) 112 | -------------------------------------------------------------------------------- /claude-code-setup/setup-claude-code-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Setup script for running Clojure in Claude Code's authenticated proxy environment 3 | # 4 | # This script configures the environment to work around Java's limitation 5 | # where it cannot send authentication headers during HTTPS CONNECT requests. 6 | # 7 | # Usage: 8 | # source setup-claude-code-env.sh 9 | 10 | set -e 11 | 12 | PROXY_PORT="${PROXY_PORT:-8888}" 13 | PROXY_LOG="/tmp/proxy.log" 14 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 15 | 16 | echo "============================================================" 17 | echo "Clojure Development Setup for Claude Code" 18 | echo "============================================================" 19 | echo "" 20 | 21 | # Check if we're in Claude Code environment 22 | if [ -z "$http_proxy" ] && [ -z "$HTTP_PROXY" ]; then 23 | echo "[WARN] No http_proxy environment variable detected" 24 | echo " This setup is designed for Claude Code's authenticated proxy environment" 25 | echo "" 26 | fi 27 | 28 | # Start proxy wrapper if not already running 29 | if pgrep -f "proxy-wrapper.py.*$PROXY_PORT" > /dev/null; then 30 | PROXY_PID=$(pgrep -f "proxy-wrapper.py.*$PROXY_PORT") 31 | echo "[OK] Proxy wrapper already running on port $PROXY_PORT (PID: $PROXY_PID)" 32 | else 33 | echo "Starting proxy wrapper on port $PROXY_PORT..." 34 | if [ -f "$SCRIPT_DIR/proxy-wrapper.py" ]; then 35 | python3 "$SCRIPT_DIR/proxy-wrapper.py" $PROXY_PORT > $PROXY_LOG 2>&1 & 36 | sleep 2 37 | 38 | if pgrep -f "proxy-wrapper.py.*$PROXY_PORT" > /dev/null; then 39 | PROXY_PID=$(pgrep -f "proxy-wrapper.py.*$PROXY_PORT") 40 | echo "[OK] Proxy wrapper started (PID: $PROXY_PID)" 41 | else 42 | echo "[ERROR] Failed to start proxy wrapper" 43 | echo " Check logs: tail $PROXY_LOG" 44 | return 1 45 | fi 46 | else 47 | echo "[ERROR] proxy-wrapper.py not found in $SCRIPT_DIR" 48 | return 1 49 | fi 50 | fi 51 | echo " Logs: tail -f $PROXY_LOG" 52 | echo "" 53 | 54 | # Configure Maven settings 55 | MAVEN_SETTINGS="$HOME/.m2/settings.xml" 56 | mkdir -p "$HOME/.m2" 57 | 58 | if [ ! -f "$MAVEN_SETTINGS" ] || ! grep -q "127.0.0.1" "$MAVEN_SETTINGS"; then 59 | echo "Configuring Maven settings for proxy..." 60 | cat > "$MAVEN_SETTINGS" < 62 | 66 | 67 | 68 | local-proxy-http 69 | true 70 | http 71 | 127.0.0.1 72 | $PROXY_PORT 73 | localhost|127.0.0.1 74 | 75 | 76 | local-proxy-https 77 | true 78 | https 79 | 127.0.0.1 80 | $PROXY_PORT 81 | localhost|127.0.0.1 82 | 83 | 84 | 85 | EOF 86 | echo "[OK] Created $MAVEN_SETTINGS" 87 | else 88 | echo "[OK] Maven settings already configured" 89 | fi 90 | echo "" 91 | 92 | # Export Java system properties 93 | echo "Configuring Java proxy settings..." 94 | export JAVA_TOOL_OPTIONS="-Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=$PROXY_PORT -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=$PROXY_PORT -Dmaven.artifact.threads=2" 95 | echo "[OK] JAVA_TOOL_OPTIONS set" 96 | echo "" 97 | 98 | # Configure Gradle (optional but recommended) 99 | GRADLE_PROPS="$HOME/.gradle/gradle.properties" 100 | mkdir -p "$HOME/.gradle" 101 | 102 | if [ ! -f "$GRADLE_PROPS" ] || ! grep -q "127.0.0.1" "$GRADLE_PROPS"; then 103 | echo "Configuring Gradle proxy settings..." 104 | cat > "$GRADLE_PROPS" <