├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── BIG_IDEAS.md ├── CLAUDE.md ├── LICENSE.md ├── LLM_CODE_STYLE.md ├── PROJECT_SUMMARY.md ├── README.md ├── clj-sandbox-example.sb ├── deps.edn ├── dev └── logback.xml ├── doc ├── README.md ├── creating-prompts.md ├── creating-resources.md ├── creating-tools-multimethod.md ├── creating-tools-without-clojuremcp.md ├── custom-mcp-server.md └── gen-your-mcp-server.md ├── resources └── clojure-mcp │ ├── prompts │ ├── CLOJURE.md │ ├── clojure-repl-guide.md │ ├── create_project_summary.md │ ├── dev_prompt.md │ ├── repl_driven.md │ ├── spec_modifier.md │ ├── system │ │ ├── clojure_edit.md │ │ ├── clojure_edit_tool_inst.md │ │ ├── clojure_flex.md │ │ ├── clojure_form_edit.md │ │ ├── clojure_pattern_edit.md │ │ ├── clojure_repl.md │ │ ├── clojure_repl_form_edit.md │ │ ├── clojure_repl_pattern_edit.md │ │ └── incremental_file_creation.md │ └── test_modifier.md │ ├── repl_helpers.clj │ └── test │ └── projects │ ├── deps.edn │ └── project.clj ├── src └── clojure_mcp │ ├── agent │ ├── langchain.clj │ └── langchain │ │ └── schema.clj │ ├── config.clj │ ├── core.clj │ ├── file_content.clj │ ├── linting.clj │ ├── main.clj │ ├── main_examples │ ├── figwheel_main.clj │ └── shadow_main.clj │ ├── nrepl.clj │ ├── other_tools │ ├── README.md │ ├── create_directory │ │ ├── core.clj │ │ └── tool.clj │ ├── list_directory │ │ ├── core.clj │ │ └── tool.clj │ ├── move_file │ │ ├── core.clj │ │ └── tool.clj │ ├── namespace │ │ ├── core.clj │ │ └── tool.clj │ └── symbol │ │ ├── core.clj │ │ └── tool.clj │ ├── prompts.clj │ ├── resources.clj │ ├── sexp │ ├── match.clj │ └── paren_utils.clj │ ├── sse_core.clj │ ├── sse_main.clj │ ├── tool_system.clj │ ├── tools │ ├── architect │ │ ├── core.clj │ │ └── tool.clj │ ├── bash │ │ ├── core.clj │ │ └── tool.clj │ ├── code_critique │ │ ├── core.clj │ │ └── tool.clj │ ├── directory_tree │ │ ├── core.clj │ │ └── tool.clj │ ├── dispatch_agent │ │ ├── core.clj │ │ └── tool.clj │ ├── eval │ │ ├── core.clj │ │ └── tool.clj │ ├── figwheel │ │ └── tool.clj │ ├── file_edit │ │ ├── core.clj │ │ ├── pipeline.clj │ │ └── tool.clj │ ├── file_write │ │ ├── core.clj │ │ └── tool.clj │ ├── form_edit │ │ ├── combined_edit_tool.clj │ │ ├── core.clj │ │ ├── pipeline.clj │ │ └── tool.clj │ ├── glob_files │ │ ├── core.clj │ │ └── tool.clj │ ├── grep │ │ ├── core.clj │ │ └── tool.clj │ ├── project │ │ ├── core.clj │ │ └── tool.clj │ ├── read_file │ │ ├── core.clj │ │ ├── file_timestamps.clj │ │ └── tool.clj │ ├── scratch_pad │ │ ├── core.clj │ │ ├── tool.clj │ │ └── truncate.clj │ ├── think │ │ └── tool.clj │ ├── unified_clojure_edit │ │ ├── core.clj │ │ ├── pipeline.clj │ │ └── tool.clj │ └── unified_read_file │ │ ├── pattern_core.clj │ │ └── tool.clj │ └── utils │ ├── diff.clj │ ├── emacs_integration.clj │ ├── mcp_log_client.clj │ └── valid_paths.clj └── test └── clojure_mcp ├── agent └── langchain │ └── schema_test.clj ├── other_tools ├── create_directory │ ├── core_test.clj │ └── tool_test.clj ├── list_directory │ ├── core_test.clj │ └── tool_test.clj ├── move_file │ ├── core_test.clj │ └── tool_test.clj ├── namespace │ ├── core_test.clj │ └── tool_test.clj └── symbol │ ├── core_test.clj │ └── tool_test.clj ├── repl_tools └── test_utils.clj ├── sexp └── match_test.clj ├── tools ├── directory_tree │ ├── core_test.clj │ └── tool_test.clj ├── eval │ ├── core_test.clj │ └── tool_test.clj ├── file_edit │ ├── core_test.clj │ └── tool_test.clj ├── file_write │ ├── core_test.clj │ └── tool_test.clj ├── form_edit │ ├── core_test.clj │ ├── pipeline_test.clj │ ├── sexp_replace_test.clj │ └── tool_test.clj ├── glob_files │ ├── core_test.clj │ └── tool_test.clj ├── grep │ ├── core_test.clj │ └── tool_test.clj ├── project │ ├── core_test.clj │ └── tool_test.clj ├── read_file │ ├── core_test.clj │ └── tool_test.clj ├── scratch_pad │ ├── core_test.clj │ ├── tool_test.clj │ └── truncate_test.clj ├── test_utils.clj ├── think │ └── tool_test.clj └── unified_read_file │ └── pattern_core_test.clj └── utils └── valid_paths_test.clj /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bhauman] 2 | custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=B8B3LKTXKV69C 3 | 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Clojure 2 | .calva/output-window/ 3 | .classpath 4 | .clj-kondo/.cache 5 | .cpcache 6 | .eastwood 7 | .factorypath 8 | .hg/ 9 | .hgignore 10 | .java-version 11 | .lein-* 12 | .lsp/.cache 13 | .lsp/sqlite.db 14 | .nrepl-history 15 | .nrepl-port 16 | .project 17 | .rebel_readline_history 18 | .settings 19 | .socket-repl-port 20 | .sw* 21 | .vscode 22 | *.class 23 | *.jar 24 | *.swp 25 | *~ 26 | /checkouts 27 | /classes 28 | /target 29 | .aider* 30 | 31 | # Project specific 32 | .mcp.json 33 | /node_modules 34 | package-lock.json 35 | package.json 36 | test/tmp/ 37 | 38 | src/user.clj 39 | 40 | # OS specific 41 | .DS_Store 42 | **/.DS_Store 43 | 44 | # Temporary files 45 | NOTES.md 46 | *.log 47 | *.tmp 48 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -X:test` 6 | - Run single test: `clojure -X:test :dirs '["test"]' :include '"repl_tools_test"'` 7 | - Run linter: `clojure -M:lint` (checks src directory) 8 | - Build JAR: `clojure -T:build ci` 9 | - Install locally: `clojure -T:build install` 10 | 11 | ## Code Style Guidelines 12 | - **Imports**: Use `:require` with ns aliases (e.g., `[clojure.string :as string]`) 13 | - **Naming**: Use kebab-case for vars/functions; end predicates with `?` (e.g., `is-top-level-form?`) 14 | - **Error handling**: Use `try/catch` with specific exception handling; atom for tracking errors 15 | - **Formatting**: 2-space indentation; maintain whitespace in edited forms 16 | - **Namespaces**: Align with directory structure (`clojure-mcp.repl-tools`) 17 | - **Testing**: Use `deftest` with descriptive names; `testing` for subsections; `is` for assertions 18 | - **REPL Development**: Prefer REPL-driven development for rapid iteration and feedback 19 | - Don't use the clojure -X:lint tool in the workflow 20 | 21 | ## MCP Tool Guidelines 22 | - Include clear tool `:description` for LLM guidance 23 | - Validate inputs and provide helpful error messages 24 | - Return structured data with both result and error status 25 | - Maintain atom-based state for consistent service access 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dev/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logs/clojure-mcp.log 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | logs/clojure-mcp-%d{yyyy-MM-dd}.log 12 | 13 | 1000 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /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 | ### [Creating Your Own Custom MCP Server](custom-mcp-server.md) 8 | 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! 9 | 10 | ### [Generate Your Custom MCP Server with AI](gen-your-mcp-server.md) 11 | 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. 12 | 13 | ### [Creating Tools with ClojureMCP's Multimethod System](creating-tools-multimethod.md) 14 | 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. 15 | 16 | ### [Creating Tools Without ClojureMCP](creating-tools-without-clojuremcp.md) 17 | 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. 18 | 19 | ### [Creating Prompts](creating-prompts.md) 20 | 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. 21 | 22 | ### [Creating Resources](creating-resources.md) 23 | 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. 24 | 25 | ## Quick Start 26 | 27 | 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. 28 | 29 | ## Key Concepts 30 | 31 | - **Tools**: Perform actions and computations 32 | - **Prompts**: Generate conversation contexts for AI assistants 33 | - **Resources**: Provide read-only content 34 | 35 | ## Quick Reference 36 | 37 | | Component | Schema | Callback Signature | 38 | |-----------|--------|-------------------| 39 | | Tool | `{:name, :description, :schema, :tool-fn}` | `(callback result-vector error-boolean)` | 40 | | Prompt | `{:name, :description, :arguments, :prompt-fn}` | `(callback {:description "...", :messages [...]})` | 41 | | Resource | `{:url, :name, :description, :mime-type, :resource-fn}` | `(callback ["content..."])` | 42 | 43 | ## Notes 44 | 45 | - **Tools** can be created either using ClojureMCP's multimethod system or as simple maps (see the tools documentation) 46 | - **Prompts** and **Resources** are always created as simple maps, making them inherently portable 47 | - All components can be tested independently without an MCP server 48 | - String keys are used for all parameter maps passed to component functions 49 | -------------------------------------------------------------------------------- /resources/clojure-mcp/prompts/CLOJURE.md: -------------------------------------------------------------------------------- 1 | # Clojure Style Guide 2 | 3 | A concise summary of key Clojure style conventions for LLM context. 4 | 5 | ## Source Code Layout 6 | 7 | - Use spaces for indentation (2 spaces) 8 | - Limit lines to 80 characters where feasible 9 | - Use Unix-style line endings 10 | - One namespace per file 11 | - Terminate files with newline 12 | - No trailing whitespace 13 | - Empty line between top-level forms 14 | - No blank lines within definition forms 15 | 16 | ## Naming Conventions 17 | 18 | - Use `lisp-case` for functions and variables: `(def some-var)`, `(defn some-fun)` 19 | - Use `CapitalCase` for protocols, records, structs, types: `(defprotocol MyProtocol)` 20 | - End predicate function names with `?`: `(defn palindrome?)` 21 | - End unsafe transaction functions with `!`: `(defn reset!)` 22 | - Use `->` for conversion functions: `(defn f->c)` 23 | - Use `*earmuffs*` for dynamic vars: `(def ^:dynamic *db*)` 24 | - Use `_` for unused bindings: `(fn [_ b] b)` 25 | 26 | ## Namespace Conventions 27 | 28 | - No single-segment namespaces 29 | - Prefer `:require` over `:use` 30 | - Common namespace aliases: 31 | - `[clojure.string :as str]` 32 | - `[clojure.java.io :as io]` 33 | - `[clojure.edn :as edn]` 34 | - `[clojure.walk :as walk]` 35 | - `[clojure.zip :as zip]` 36 | - `[clojure.data.json :as json]` 37 | 38 | ## Function Style 39 | 40 | ```clojure 41 | ;; Good function style examples 42 | (defn foo 43 | "Docstring goes here." 44 | [x] 45 | (bar x)) 46 | 47 | ;; Multiple arity - align args 48 | (defn foo 49 | "I have two arities." 50 | ([x] 51 | (foo x 1)) 52 | ([x y] 53 | (+ x y))) 54 | 55 | ;; Threading macros for readability 56 | (-> person 57 | :address 58 | :city 59 | str/upper-case) 60 | 61 | (->> items 62 | (filter active?) 63 | (map :name) 64 | (into [])) 65 | ``` 66 | 67 | ## Collections 68 | 69 | - Prefer vectors `[]` over lists `()` for sequences 70 | - Use keywords for map keys: `{:name "John" :age 42}` 71 | - Use sets as predicates: `(filter #{:a :b} coll)` 72 | - Prefer `vec` over `into []` 73 | - Avoid Java collections/arrays 74 | 75 | ## Common Idioms 76 | 77 | ```clojure 78 | ;; Use when instead of (if x (do ...)) 79 | (when test 80 | (do-this) 81 | (do-that)) 82 | 83 | ;; Use if-let for conditional binding 84 | (if-let [val (may-return-nil)] 85 | (do-something val) 86 | (handle-nil-case)) 87 | 88 | ;; Use cond with :else 89 | (cond 90 | (neg? n) "negative" 91 | (pos? n) "positive" 92 | :else "zero") 93 | 94 | ;; Use case for constants 95 | (case day 96 | :mon "Monday" 97 | :tue "Tuesday" 98 | "unknown") 99 | ``` 100 | 101 | ## Documentation 102 | 103 | - Start docstrings with complete sentence 104 | - Use Markdown in docstrings 105 | - Document all function arguments with backticks 106 | - Reference vars with backticks: `clojure.core/str` 107 | - Link to other vars with `[[var-name]]` 108 | 109 | ## Testing 110 | 111 | - Put tests in `test/` directory 112 | - Name test namespaces `*.test` 113 | - Name tests with `-test` suffix 114 | - Use `deftest` macro 115 | 116 | ## Common Metadata 117 | 118 | ```clojure 119 | ;; Version added 120 | (def ^{:added "1.0"} foo 42) 121 | 122 | ;; Deprecation 123 | (def ^{:deprecated "2.0"} old-foo 42) 124 | 125 | ;; No documentation 126 | (def ^:no-doc internal-thing 42) 127 | 128 | ;; Private 129 | (def ^:private secret 42) 130 | ``` 131 | -------------------------------------------------------------------------------- /resources/clojure-mcp/prompts/clojure-repl-guide.md: -------------------------------------------------------------------------------- 1 | # Iterative Development with the Clojure REPL: Emphasizing Values, Limiting Side Effects 2 | 3 | The REPL (Read-Eval-Print-Loop) is central to the Clojure development experience. It provides immediate feedback that accelerates learning and development through tight iterations. Here's how to leverage the REPL effectively while emphasizing functional programming principles. 4 | 5 | ## Core Principles 6 | 7 | 1. **Values over State** - Design functions that transform immutable data rather than mutating shared state. 8 | 2. **Pure Functions** - Favor functions that produce the same output given the same input, without side effects. 9 | 3. **Data Transformation** - View programs as pipelines that transform data through a series of operations. 10 | 11 | ## REPL-Driven Development Workflow 12 | 13 | ### 1. Explore and Experiment 14 | 15 | Start by exploring small pieces of functionality in isolation: 16 | 17 | ```clojure 18 | ;; Try out simple expressions 19 | user=> (+ 1 2 3) 20 | 6 21 | 22 | ;; Explore data structures 23 | user=> (def sample-data [{:name "Alice" :score 42} 24 | {:name "Bob" :score 27} 25 | {:name "Charlie" :score 35}]) 26 | #'user/sample-data 27 | 28 | ;; Test simple transformations 29 | user=> (map :score sample-data) 30 | (42 27 35) 31 | ``` 32 | 33 | ### 2. Build Functions Incrementally 34 | 35 | Develop functions step by step, testing each piece: 36 | 37 | ```clojure 38 | ;; Define a simple function 39 | user=> (defn average [numbers] 40 | (/ (apply + numbers) (count numbers))) 41 | #'user/average 42 | 43 | ;; Test it immediately 44 | user=> (average [1 2 3 4 5]) 45 | 3 46 | 47 | ;; Compose with other functions 48 | user=> (average (map :score sample-data)) 49 | 34.666666666666664 50 | ``` 51 | 52 | ### 3. Compose and Refine 53 | 54 | Combine functions into more complex operations: 55 | 56 | ```clojure 57 | ;; Define a function to get high scorers 58 | user=> (defn high-scorers [threshold data] 59 | (filter #(> (:score %) threshold) data)) 60 | #'user/high-scorers 61 | 62 | ;; Test it 63 | user=> (high-scorers 30 sample-data) 64 | ({:name "Alice", :score 42} {:name "Charlie", :score 35}) 65 | 66 | ;; Refine by composing functions 67 | user=> (defn average-high-score [threshold data] 68 | (average (map :score (high-scorers threshold data)))) 69 | #'user/average-high-score 70 | 71 | user=> (average-high-score 30 sample-data) 72 | 38.5 73 | ``` 74 | 75 | ### 4. Extract Pure Logic from Side Effects 76 | 77 | Separate pure data transformation from I/O operations: 78 | 79 | ```clojure 80 | ;; Pure function: processes data 81 | (defn analyze-user-scores [data] 82 | {:average (average (map :score data)) 83 | :high-performers (count (high-scorers 30 data)) 84 | :highest-score (:score (apply max-key :score data))}) 85 | 86 | ;; Side effect: displays results (kept separate) 87 | (defn report-analysis [analysis] 88 | (println "Average score:" (:average analysis)) 89 | (println "High performers:" (:high-performers analysis)) 90 | (println "Highest score:" (:highest-score analysis))) 91 | 92 | ;; Usage in REPL - compose but don't mix 93 | user=> (def analysis-result (analyze-user-scores sample-data)) 94 | #'user/analysis-result 95 | 96 | user=> analysis-result 97 | {:average 34.666666666666664, :high-performers 2, :highest-score 42} 98 | 99 | user=> (report-analysis analysis-result) 100 | Average score: 34.666666666666664 101 | High performers: 2 102 | Highest score: 42 103 | nil 104 | ``` 105 | 106 | ## Benefits of This Approach 107 | 108 | - **Easier Testing**: Pure functions with no side effects are trivial to test. 109 | - **Reasoning Simplicity**: Programs are easier to understand when state changes are limited. 110 | - **Composition**: Small, focused functions can be combined like building blocks. 111 | - **REPL Friendliness**: Value-oriented code is naturally REPL-friendly. 112 | 113 | ## Tips for REPL Success 114 | 115 | 1. Keep functions small and focused on a single transformation. 116 | 2. Use `comment` forms to save useful REPL experiments in your code. 117 | 3. Leverage `def` for intermediate results during exploration. 118 | 4. Design for composition by having functions take and return similar data structures. 119 | 5. Push side effects to the edges of your system. 120 | 121 | Remember, the REPL is not just a tool for running code—it's a dynamic environment for iterative exploration, learning, and crafting elegant solutions through immediate feedback. 122 | -------------------------------------------------------------------------------- /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/prompts/dev_prompt.md: -------------------------------------------------------------------------------- 1 | I'd like to develop Clojure code in a REPL driven style I have given you access to a Clojure REPL throught the clojure mcp tool. 2 | 3 | The code will be functional code where functions take args and return results. This will be preferred over side effects. But we can use side effects as a last resort to service the larger goal. 4 | 5 | I'm am going to supply a problem statement and I'd like you to work through the problem with me iteratively step by step. 6 | 7 | You can create an artifact for the developed code and update it as appropriate. 8 | 9 | The expression doesn't have to be a complete function it can a simple sub expression. 10 | 11 | Where each step you evaluate an expression to verify that it does what you thing it will do. 12 | 13 | Println use id HIGHLY discouraged. Prefer evaluating subexpressions to test them vs using println. 14 | 15 | I'd like you to display what's being evaluated as a code block before invoking the evaluation tool. 16 | 17 | If something isn't working feel free to use the other clojure tools available. 18 | 19 | If a function isn't found you can search for it using `symbol-search` and you can also `symbol-completions` to help find what you are looking for. 20 | 21 | If you are having a hard time with something you can also lookup documentation on a function using `symbol-documentation` 22 | 23 | You can also lookup source code with the `source-code` tool to see how a certain function is implemented. 24 | 25 | The main thing is to work step by step to incrementally develop a solution to a problem. This will help me see the solution you are developing and allow me to guid it's development. 26 | 27 | -------------------------------------------------------------------------------- /resources/clojure-mcp/prompts/repl_driven.md: -------------------------------------------------------------------------------- 1 | # REPL Driven Development 2 | 3 | I'd like to develop Clojure code in a REPL Driven Development style. 4 | 5 | I have given you access to a Clojure REPL using the `clojure_eval` mcp tool. 6 | 7 | Use the `clojure_eval` to develop code in the REPL enviroment. 8 | 9 | # Writing out code 10 | 11 | You have some very effective tools for editing Clojure code. 12 | 13 | * `clojure_file_outline` - important for getting the ordered overview of the state of the file 14 | Clojure file outline is the quickest way to learn more about a file and its functions. 15 | 16 | 17 | * `clojure_edit_replace_form` 18 | * `clojure_edit_insert_before_form` 19 | * `clojure_edit_insert_after_form` 20 | * `clojure_edit_comment_block` 21 | * `clojure_edit_replace_docstring` 22 | 23 | These tools lint code Clojure and and format it cleanly into the target files. 24 | 25 | USE `edit_file` and `write_file` AS A LAST RESORT when the above functions are not working. 26 | 27 | Using the `clojure_edit` tools saves development time and tokens and it really makes me happy!!! Because I can finish my work sooner. Thank you! 28 | 29 | # Before starting 30 | 31 | Use `clojure_inspect_project` tool to get valuable information about the project 32 | 33 | # Development guidance 34 | 35 | * develop and validate a solution as a function or set of functions in the REPL with `clojure_eval` 36 | - you can require reload a namespace under focus if necessary 37 | - you can create definitions and validate them incrementally 38 | * you can write the solutions to the to correct files using the specialised `clojure_edit` tools 39 | * you can require reload the namespace, and test the various functions in the REPL to verify that they are working correctly 40 | * ONLY after a solution is validated THEN you can commit the changes to git 41 | -------------------------------------------------------------------------------- /resources/clojure-mcp/prompts/spec_modifier.md: -------------------------------------------------------------------------------- 1 | Let's use the connected repl to model a problem with Clojure Spec 2 | 3 | Let's start with spec and iteratively evaluating it in the REPL. 4 | 5 | Let's develop Clojure specs and especially `s/fdef` along with their 6 | stubbed out empty function definitions. 7 | 8 | The `s/fdef` definitions should use `any?` as little as possible 9 | 10 | Also when disigning specs we should attempt to be precise and cover all cases, if a spec contract is ill defined then this is a probably a problem with the spec contract design. Which will probably turn into code complexity down the line. 11 | 12 | When defining or redefining functions we should ALWAYS call 13 | `instrument` after defining the function. 14 | 15 | -------------------------------------------------------------------------------- /resources/clojure-mcp/prompts/system/clojure_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_definition` - Replace entire top-level forms 14 | - `clojure_edit_insert_before/after_definition` - Add code around forms 15 | - `clojure_edit_replace_sexp` - Modify expressions within functions 16 | - `clojure_edit_replace_docstring` - Update only docstrings 17 | 18 | ## CODE SIZE DIRECTLY IMPACTS EDIT SUCCESS 19 | - **SMALLER EDITS = HIGHER SUCCESS RATE** 20 | - **LONG FUNCTIONS ALMOST ALWAYS FAIL** - Break into multiple small functions 21 | - **NEVER ADD MULTIPLE FUNCTIONS AT ONCE** - Add one at a time 22 | - Each additional line exponentially increases failure probability 23 | - 5-10 line functions succeed, 20+ line functions usually fail 24 | - Break large changes into multiple small edits 25 | 26 | ## COMMENTS ARE PROBLEMATIC 27 | - Minimize comments in code generation 28 | - Comments increase edit complexity and failure rate 29 | - Use meaningful function and parameter names instead 30 | - If comments are needed, add them in separate edits 31 | - Use `clojure_edit_replace_comment_block` for comment-only changes 32 | 33 | ## Handling Parenthesis Errors 34 | - Break complex functions into smaller, focused ones 35 | - Start with minimal code and add incrementally 36 | - When facing persistent errors, verify in REPL first 37 | - Count parentheses in the content you're adding 38 | - For deep nesting, use threading macros (`->`, `->>`) 39 | 40 | ## Creating New Files 41 | 1. Start by writing only the namespace declaration 42 | 2. Use `file_write` for just the namespace: 43 | ```clojure 44 | (ns my.namespace 45 | (:require [other.ns :as o])) 46 | ``` 47 | 3. Then add each function one at a time with `clojure_edit_insert_after_definition` 48 | 4. Test each function in the REPL before adding the next 49 | 50 | ## Working with Defmethod 51 | Remember to include dispatch values: 52 | - Normal dispatch: `form_identifier: "area :rectangle"` 53 | - Vector dispatch: `form_identifier: "convert-length [:feet :inches]"` 54 | - Namespaced: `form_identifier: "tool-system/validate-inputs :clojure-eval"` 55 | -------------------------------------------------------------------------------- /resources/clojure-mcp/prompts/system/clojure_edit_tool_inst.md: -------------------------------------------------------------------------------- 1 | 2 | # IMPORTANT GUIDANCE FOR EDITING CLOJURE FILES 3 | 4 | ## Pay specific attention to balancing parenthesis 5 | 6 | When editing Clojure code, ALWAYS use the specialized Clojure-aware tools instead of generic text editing functions, even if text editing seems simpler at first glance. 7 | 8 | ## Why Specialized Tools Are Superior to Text Editing 9 | 10 | As an AI assistant, your probabilistic nature creates specific challenges when editing Clojure code: 11 | 12 | 1. **Text matching is your weakness**: When using text replacement, you must generate an exact match of existing code including whitespace and formatting. This frequently fails and creates frustrating retry loops. 13 | 14 | 2. **Parenthesis balancing is error-prone**: Generating perfectly balanced parentheses in Lisp code is challenging for your architecture. Even one mismatched parenthesis causes complete failure. Better to detect these errors early and fix them. 15 | 16 | 3. **Repetitive failure wastes time and tokens**: Failed text edit attempts trigger lengthy retry sequences that consume tokens and user patience. 17 | 18 | The specialized Clojure editing tools directly address these limitations: 19 | 20 | - **Target by name, not text**: `clojure_edit_replace_definition` requires only the form type and name, not exact text matching 21 | - **Structure-aware matching**: `clojure_edit_replace_sexp` matches structure, ignoring troublesome whitespace differences 22 | - **Early syntax validation**: Catches your parenthesis errors before writing to files 23 | - **Specific error messages**: When errors occur, you receive precise feedback rather than generic "no match found" errors 24 | 25 | ## CRITICAL WARNING: Parenthesis Balancing 26 | 27 | Despite using these specialized tools, you MUST still be extremely careful with parenthesis balancing in the code you generate. The tools will validate syntax and REJECT any code with mismatched parentheses, braces, or brackets. 28 | 29 | To avoid this common failure mode: 30 | - Count opening and closing parentheses carefully 31 | - Pay special attention to nested expressions 32 | - Consider building smaller functions and building incrementally to make balancing easier. 33 | 34 | **Larger function definitions pose significantly higher risk of parenthesis errors.** 35 | When working with complex or lengthy functions: 36 | - Break your work into smaller, focused functions rather than rewriting an entire large function 37 | - Extract pieces of complex logic using `clojure_edit_replace_sexp` to modify them separately 38 | - For major refactoring, consider creating helper functions to handle discrete pieces of logic 39 | - Verify each smaller edit works before moving to the next, building confidence incrementally 40 | 41 | This incremental approach dramatically reduces parenthesis errors and makes troubleshooting simpler when errors do occur. 42 | 43 | **Deep expression nesting also poses higher risks of parenthesis errors.** 44 | - Consider using the reading macros like `->` and `->>` to reduce expression nesting 45 | - Consider using iteration patterns like `reduce`, `iterate` etc. and factoring out the step function to a separate high level function 46 | 47 | **Botom line**: 48 | Long functions and deep complex expressoins make it harder to create, edit and reason about code. Much better to make top level definitions smaller and more focused. 49 | 50 | ## When to Use Each Tool 51 | 52 | For top-level forms (functions, defs, etc.): 53 | - Use `clojure_edit_replace_definition` instead of attempting to match and replace entire functions 54 | - Form identification by name eliminates your text-matching limitations 55 | 56 | For targeted changes within functions: 57 | - Use `clojure_edit_replace_sexp` instead of trying to match specific lines or expressions with exact whitespace 58 | - Syntax-aware matching means you don't need to reproduce formatting perfectly 59 | 60 | For working with defmethod forms: 61 | - Always include the dispatch value in the form name (e.g., "shape/area :square") 62 | - This converts a challenging text-matching problem into a simple naming operation 63 | 64 | ## Remember 65 | 66 | When you resort to text editing for Clojure code: 67 | - You're fighting against your probabilistic architecture 68 | - You're choosing a path with higher failure rates 69 | - You're creating more work for yourself and the user 70 | - You're consuming more tokens for the same result 71 | 72 | ALWAYS select the appropriate Clojure-aware tool over generic text editing - this is not just a preference, but a fundamental requirement for effective Clojure assistance. 73 | -------------------------------------------------------------------------------- /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 functions 15 | - `clojure_edit_replace_docstring` - Update only docstrings 16 | 17 | ## CODE SIZE DIRECTLY IMPACTS EDIT SUCCESS 18 | - **SMALLER EDITS = HIGHER SUCCESS RATE** 19 | - **LONG FUNCTIONS ALMOST ALWAYS FAIL** - Break into multiple small functions 20 | - **NEVER ADD MULTIPLE FUNCTIONS AT ONCE** - Add one at a time 21 | - Each additional line exponentially increases failure probability 22 | - 5-10 line functions succeed, 20+ line functions usually fail 23 | - Break large changes into multiple small edits 24 | 25 | ## COMMENTS ARE PROBLEMATIC 26 | - Minimize comments in code generation 27 | - Comments increase edit complexity and failure rate 28 | - Use meaningful function and parameter names instead 29 | - If comments are needed, add them in separate edits 30 | - Use `clojure_edit_replace_comment_block` for comment-only changes 31 | 32 | ## Handling Parenthesis Errors 33 | - Break complex functions into smaller, focused ones 34 | - Start with minimal code and add incrementally 35 | - When facing persistent errors, verify in REPL first 36 | - Count parentheses in the content you're adding 37 | - For deep nesting, use threading macros (`->`, `->>`) 38 | 39 | ## Creating New Files 40 | 1. Start by writing only the namespace declaration 41 | 2. Use `file_write` for just the namespace: 42 | ```clojure 43 | (ns my.namespace 44 | (:require [other.ns :as o])) 45 | ``` 46 | 3. Then add each function one at a time with `clojure_edit` using the "insert_after" operation. 47 | 4. Test each function in the REPL before adding the next 48 | 49 | ## Working with Defmethod 50 | Remember to include dispatch values: 51 | - Normal dispatch: `form_identifier: "area :rectangle"` 52 | - Vector dispatch: `form_identifier: "convert-length [:feet :inches]"` 53 | - Namespaced: `form_identifier: "tool-system/validate-inputs :clojure-eval"` 54 | -------------------------------------------------------------------------------- /resources/clojure-mcp/prompts/system/clojure_pattern_edit.md: -------------------------------------------------------------------------------- 1 | # IMPORTANT GUIDANCE FOR PATTERN-BASED CLOJURE EDITING 2 | 3 | ## Pay specific attention to balancing parenthesis 4 | 5 | When editing Clojure code, ALWAYS use the specialized pattern-based editing tool `clojure_edit` instead of generic text editing functions, even if text editing seems simpler at first glance. 6 | 7 | ## Why Pattern-Based Editing Is Superior to Text Editing 8 | 9 | As an AI assistant, your probabilistic nature creates specific challenges when editing Clojure code: 10 | 11 | 1. **Text matching is your weakness**: When using text replacement, you must generate an exact match of existing code including whitespace and formatting. This frequently fails and creates frustrating retry loops. 12 | 13 | 2. **Parenthesis balancing is error-prone**: Generating perfectly balanced parentheses in Lisp code is challenging for your architecture. Even one mismatched parenthesis causes complete failure. Better to detect these errors early and fix them. 14 | 15 | 3. **Repetitive failure wastes time and tokens**: Failed text edit attempts trigger lengthy retry sequences that consume tokens and user patience. 16 | 17 | The pattern-based Clojure editing tool directly addresses these limitations: 18 | 19 | - **Target by pattern, not exact text**: `clojure_edit` uses wildcards (`_?` and `_*`) to match code by structure rather than requiring exact text 20 | - **Structure-aware matching**: Patterns match the logical structure, ignoring troublesome whitespace differences 21 | - **Early syntax validation**: Catches your parenthesis errors before writing to files 22 | - **Specific error messages**: When errors occur, you receive precise feedback rather than generic "no match found" errors 23 | 24 | ## CRITICAL WARNING: Parenthesis Balancing 25 | 26 | Despite using the pattern-based editing tool, you MUST still be extremely careful with parenthesis balancing in the code you generate. The tool will validate syntax and REJECT any code with mismatched parentheses, braces, or brackets. 27 | 28 | To avoid this common failure mode: 29 | - Count opening and closing parentheses carefully 30 | - Pay special attention to nested expressions 31 | - Consider building smaller functions and building incrementally to make balancing easier. 32 | 33 | **Larger function definitions pose significantly higher risk of parenthesis errors.** 34 | When working with complex or lengthy functions: 35 | - Break your work into smaller, focused functions rather than rewriting an entire large function 36 | - Extract pieces of complex logic to modify them separately 37 | - For major refactoring, consider creating helper functions to handle discrete pieces of logic 38 | - Verify each smaller edit works before moving to the next, building confidence incrementally 39 | 40 | This incremental approach dramatically reduces parenthesis errors and makes troubleshooting simpler when errors do occur. 41 | 42 | **Deep expression nesting also poses higher risks of parenthesis errors.** 43 | - Consider using the threading macros like `->` and `->>` to reduce expression nesting 44 | - Consider using iteration patterns like `reduce`, `iterate` etc. and factoring out the step function to a separate high-level function 45 | 46 | **Bottom line**: 47 | Long functions and deep complex expressions make it harder to create, edit and reason about code. Much better to make top-level definitions smaller and more focused. 48 | 49 | **IMPORTANT**: When using the `clojure_edit` tool use the smallest UNIQUE patterh to to the matching. 50 | 51 | The following are examples of the MOST common patterns that you will use. 52 | 53 | ## Pattern Matching Examples 54 | 55 | For matching top-level forms: 56 | - Match namespace: `(ns my-cool-thing.core _*)` 57 | - Match specific function: `(defn hello-world _*)` 58 | - Match specific function: `(defn add-bignumb _? [_? _?] *)` 59 | - Match specific test: `(deftest test-hello-world-ret-value _*)` 60 | - Match method with dispatch: `(defmethod shape/area :square _*)` 61 | - Match with vector dispatch: `(defmethod convert-units [:meters :feet] _*)` 62 | 63 | For targeted changes: 64 | - Match specific testing block: `(testing "Formatting value output" _*)` 65 | 66 | ## Sexp match 67 | The patterns `_?` and `_*` are not required you can simply match a sexp. 68 | 69 | - Match namespace require `[clojure.string :as str]` 70 | - Match namespace require block `(:requires [clojure.string :as str])` 71 | - Match specific test: `(is (= "42" (format-value [:value "42"])))` 72 | 73 | ## Remember 74 | 75 | When you resort to text editing for Clojure code: 76 | - You're fighting against your probabilistic architecture 77 | - You're choosing a path with higher failure rates 78 | - You're creating more work for yourself and the user 79 | - You're consuming more tokens for the same result 80 | 81 | ALWAYS use `clojure_edit` over generic text editing - this is not just a preference, but a fundamental requirement for effective Clojure assistance. 82 | -------------------------------------------------------------------------------- /resources/clojure-mcp/prompts/system/incremental_file_creation.md: -------------------------------------------------------------------------------- 1 | # Incremental File Creation for Clojure 2 | 3 | When creating new Clojure files, follow this incremental approach to maximize success: 4 | 5 | ## The Incremental File Creation Method 6 | 7 | 1. **Start with namespace only** 8 | - Create file with just the namespace declaration: 9 | ```clojure 10 | (ns my.namespace 11 | (:require [other.ns :as o])) 12 | ``` 13 | 14 | 2. **Add one function at a time** 15 | - Use `clojure_edit_insert_after_definition` targeting the namespace 16 | - Keep functions small (5-10 lines) 17 | - Verify in REPL before adding the next function 18 | 19 | 3. **Test as you build** 20 | - After adding each function: 21 | - Require the namespace with `:reload` 22 | - Test the function in the REPL 23 | - Fix any issues before continuing 24 | 25 | 4. **Grow complexity gradually** 26 | - Start with core/helper functions 27 | - Build more complex functions that use the helpers 28 | - Maintain testability at each step 29 | 30 | ## Example Workflow 31 | 32 | ``` 33 | # Step 1: Create minimal file with namespace 34 | file_write: 35 | file_path: "/path/to/my_utils.clj" 36 | content: "(ns my.utils 37 | (:require [clojure.string :as str]))" 38 | 39 | # Step 2: Add first helper function 40 | clojure_edit_insert_after_definition: 41 | file_path: "/path/to/my_utils.clj" 42 | form_type: "ns" 43 | form_identifier: "my.utils" 44 | content: "(defn format-name [name] 45 | (str/capitalize name))" 46 | 47 | # Step 3: Test in REPL 48 | clojure_eval: 49 | code: "(require '[my.utils :as utils] :reload) 50 | (utils/format-name \"alice\")" 51 | 52 | # Step 4: Add next function that uses the first 53 | clojure_edit_insert_after_definition: 54 | file_path: "/path/to/my_utils.clj" 55 | form_type: "defn" 56 | form_identifier: "format-name" 57 | content: "(defn generate-greeting [name] 58 | (str \"Hello, \" (format-name name) \"!\"))" 59 | ``` 60 | 61 | Remember: Small incremental steps with immediate testing produces the most reliable code. -------------------------------------------------------------------------------- /resources/clojure-mcp/prompts/test_modifier.md: -------------------------------------------------------------------------------- 1 | Let's do test driven development. 2 | Let's use `clojure.test` for the tests. 3 | Let's define some tests with the `clojure_eval` tool along with the code to be tested. 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | JsonArraySchema 7 | JsonBooleanSchema 8 | JsonEnumSchema 9 | JsonIntegerSchema 10 | JsonNumberSchema 11 | JsonObjectSchema 12 | JsonStringSchema 13 | JsonSchemaElement])) 14 | 15 | (defmulti edn->sch 16 | (fn [{:keys [type enum] :as json-edn}] 17 | (or 18 | (and type (keyword type)) 19 | (and enum :enum) 20 | (throw (ex-info "By JSON data" {:json-edn json-edn}))))) 21 | 22 | (defmethod edn->sch :string [{:keys [description]}] 23 | (cond-> (JsonStringSchema/builder) 24 | description (.description description) 25 | :always (.build))) 26 | 27 | (defmethod edn->sch :number [{:keys [description]}] 28 | (cond-> (JsonNumberSchema/builder) 29 | description (.description description) 30 | :always (.build))) 31 | 32 | (defmethod edn->sch :integer [{:keys [description]}] 33 | (cond-> (JsonIntegerSchema/builder) 34 | description (.description description) 35 | :always (.build))) 36 | 37 | (defmethod edn->sch :boolean [{:keys [description]}] 38 | (cond-> (JsonBooleanSchema/builder) 39 | description (.description description) 40 | :always (.build))) 41 | 42 | (defmethod edn->sch :enum [{:keys [enum]}] 43 | (assert (every? string? enum)) 44 | (assert (not-empty enum)) 45 | (-> (JsonEnumSchema/builder) 46 | (.enumValues (map name enum)) 47 | (.build))) 48 | 49 | (defmethod edn->sch :array [{:keys [items]}] 50 | (assert items) 51 | (-> (JsonArraySchema/builder) 52 | (.items (edn->sch items)) 53 | (.build))) 54 | 55 | (defmethod edn->sch :object [{:keys [properties description required]}] 56 | (assert properties) 57 | (let [obj-build 58 | (cond-> (JsonObjectSchema/builder) 59 | (not (string/blank? description)) (.description description) 60 | (not-empty required) (.required (map name required)))] 61 | (doseq [[nm edn-schema] properties] 62 | (.addProperty obj-build (name nm) (edn->sch edn-schema))) 63 | (.build obj-build))) 64 | 65 | 66 | (comment 67 | (edn->sch {:type :string 68 | :description "Hello"}) 69 | (edn->sch {:type :integer 70 | :description "Hello"}) 71 | (edn->sch {:type :number 72 | :description "Hello"}) 73 | (edn->sch {:type :boolean 74 | :description "Hello"}) 75 | 76 | (edn->sch {:enum ["a"]}) 77 | 78 | (edn->sch {:type :array 79 | :items {:type :integer 80 | :description "Hello"}}) 81 | 82 | (edn->sch {:type :object 83 | :description "HOWDy" 84 | :properties {:name {:type :string 85 | :description "The name"} 86 | :edits {:type :array 87 | :items {:type :object 88 | :properties {:old {:type :string 89 | :description "The name"} 90 | :new {:type :string 91 | :description "The name"} 92 | } 93 | :required [:old :new]}}} 94 | }) 95 | ) 96 | -------------------------------------------------------------------------------- /src/clojure_mcp/config.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.config 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure-mcp.nrepl :as nrepl] 5 | [clojure.edn :as edn] 6 | [clojure.tools.logging :as log])) 7 | 8 | (defn- relative-to [dir path] 9 | (try 10 | (let [f (io/file path)] 11 | (if (.isAbsolute f) 12 | (.getCanonicalPath f) 13 | (.getCanonicalPath (io/file dir path)))) 14 | (catch Exception e 15 | (log/warn "Bad file paths " (pr-str [dir path])) 16 | nil))) 17 | 18 | (defn process-remote-config [{:keys [allowed-directories emacs-notify] :as config} user-dir] 19 | (let [ud (io/file user-dir)] 20 | (assert (and (.isAbsolute ud) 21 | (.isDirectory ud))) 22 | (cond-> config 23 | user-dir (assoc :nrepl-user-dir (.getCanonicalPath ud)) 24 | true 25 | (assoc :allowed-directories 26 | (->> (cons user-dir allowed-directories) 27 | (keep #(relative-to user-dir %)) 28 | distinct 29 | vec)) 30 | (some? (:emacs-notify config)) 31 | (assoc :emacs-notify (boolean (:emacs-notify config)))))) 32 | 33 | (defn load-remote-config [nrepl-client user-dir] 34 | (let [remote-cfg-str 35 | (nrepl/tool-eval-code 36 | nrepl-client 37 | (pr-str 38 | '(do 39 | (require '[clojure.java.io :as io]) 40 | (if-let [f (clojure.java.io/file "." ".clojure-mcp" "config.edn")] 41 | (when (.exists f) (clojure.edn/read-string (slurp f))))))) 42 | remote-config (try (edn/read-string remote-cfg-str) 43 | (catch Exception _ {})) 44 | processed-config (process-remote-config remote-config user-dir)] 45 | (log/info "Loaded remote-config:" remote-config) 46 | (log/info "Processed config:" processed-config) 47 | processed-config)) 48 | 49 | (defn get-config [nrepl-client-map k] 50 | (get-in nrepl-client-map [::config k])) 51 | 52 | (defn get-allowed-directories [nrepl-client-map] 53 | (get-config nrepl-client-map :allowed-directories)) 54 | 55 | (defn get-emacs-notify [nrepl-client-map] 56 | (get-config nrepl-client-map :emacs-notify)) 57 | 58 | (defn get-nrepl-user-dir [nrepl-client-map] 59 | (get-config nrepl-client-map :nrepl-user-dir)) 60 | 61 | (defn set-config! [nrepl-client-atom k v] 62 | (swap! nrepl-client-atom assoc-in [::config k] v)) 63 | 64 | -------------------------------------------------------------------------------- /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 | (defn text-media-type? [mime] 25 | (.isInstanceOf registry (MediaType/parse mime) MediaType/TEXT_PLAIN)) 26 | 27 | (defn image-media-type? [mime-or-media-type] 28 | (= "image" (.getType (MediaType/parse mime-or-media-type)))) 29 | 30 | (defn mime-type* [^Path p] 31 | (or (Files/probeContentType p) 32 | (try (.detect ^Tika @mime-detector (.toFile p)) 33 | (catch Exception _ "application/octet-stream")))) 34 | 35 | (defn str->nio-path [fp] 36 | (Path/of fp (make-array String 0))) 37 | 38 | (defn mime-type [file-path] 39 | (mime-type* (str->nio-path file-path))) 40 | 41 | (defn serialized-file [file-path] 42 | (let [path (str->nio-path file-path) 43 | bytes (Files/readAllBytes path) 44 | b64 (.encodeToString (Base64/getEncoder) bytes) 45 | mime (mime-type* path) ;; e.g. application/pdf 46 | uri (str "file://" (.toAbsolutePath path))] 47 | {:file-path file-path 48 | :nio-path path 49 | :uri uri 50 | :mime-type mime 51 | :b64 b64})) 52 | 53 | (defn image-content [{:keys [b64 mime-type]}] 54 | (McpSchema$ImageContent. nil nil b64 mime-type)) 55 | 56 | (defn text-resource-content [{:keys [uri mime-type b64]}] 57 | (let [blob (McpSchema$TextResourceContents. uri mime-type b64)] 58 | (McpSchema$EmbeddedResource. nil nil blob))) 59 | 60 | (defn binary-resource-content [{:keys [uri mime-type b64]}] 61 | (let [blob (McpSchema$BlobResourceContents. uri mime-type b64)] 62 | (McpSchema$EmbeddedResource. nil nil blob))) 63 | 64 | (defn file-response->file-content [{:keys [::file-response]}] 65 | (let [{:keys [nio-path mime-type] :as ser-file} (serialized-file file-response)] 66 | (cond 67 | (text-media-type? mime-type) (text-resource-content ser-file) 68 | (image-media-type? mime-type) (image-content ser-file) 69 | :else (binary-resource-content ser-file)))) 70 | 71 | (defn should-be-file-response? [file-path] 72 | (not (text-media-type? (mime-type file-path)))) 73 | 74 | (defn text-file? [file-path] 75 | (text-media-type? (mime-type file-path))) 76 | 77 | (defn image-file? [file-path] 78 | (image-media-type? (mime-type file-path))) 79 | 80 | (defn ->file-response [file-path] 81 | {::file-response file-path}) 82 | 83 | (defn file-response? [map] 84 | (and (map? map) 85 | (::file-response map))) 86 | 87 | (comment 88 | (-> (->file-response "./dev/sponsors.pdf") 89 | file-response->file-content) 90 | 91 | (should-be-file-response? "./dev/logback.xml") 92 | (file-content "./dev/sponsors.pdf") 93 | (text-media-type? (mime-type (str->nio-path "hello.md")))) 94 | -------------------------------------------------------------------------------- /src/clojure_mcp/linting.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.linting 2 | (:require 3 | [clj-kondo.core :as kondo] 4 | [clj-kondo.impl.parser :as parser] 5 | [clojure.string :as string])) 6 | 7 | (defn lint 8 | "Lints Clojure code string using clj-kondo. 9 | Returns nil if no issues found, or map with :report and :error? keys. 10 | 11 | Options: 12 | - lang: Language type (:clj, :cljs, or :cljc)" 13 | ([form-str] (lint form-str {})) 14 | ([form-str {:keys [lang]}] 15 | (let [config {:ignore [:invalid-arity 16 | :unresolved-symbol 17 | :clj-kondo-config 18 | :deprecated-var 19 | :deprecated-namespace 20 | :deps.edn 21 | :bb.edn-undefined-task 22 | :bb.edn-cyclic-task-dependency 23 | :bb.edn-unexpected-key 24 | :bb.edn-task-missing-docstring 25 | :docstring-blank 26 | :docstring-no-summary 27 | :docstring-leading-trailing-whitespace 28 | :file 29 | #_:reduce-without-init 30 | :line-length 31 | :missing-docstring 32 | :namespace-name-mismatch 33 | :non-arg-vec-return-type-hint 34 | :private-call 35 | :redefined-var 36 | :redundant-ignore 37 | :schema-misplaced-return 38 | :java-static-field-call 39 | :unused-alias 40 | ;; :unused-binding ;; <-- Removed this line to enable the warning 41 | :unused-import 42 | :unresolved-namespace 43 | :unresolved-symbol 44 | :unresolved-var 45 | :unused-namespace 46 | :unused-private-var 47 | :unused-referred-var 48 | :use]} 49 | lint-opts (cond-> {:lint ["-"] :config config} 50 | lang (assoc :lang lang)) 51 | res (with-in-str form-str 52 | (kondo/run! lint-opts))] 53 | (when (not-empty (:findings res)) 54 | {:report (with-out-str 55 | (kondo/print! res)) 56 | :error? (some-> res :summary :error (> 0))})))) 57 | 58 | (defn lint-delims [str] 59 | (try 60 | (parser/parse-string str) 61 | ;; linting passes 62 | false 63 | (catch clojure.lang.ExceptionInfo e 64 | (if-let [findings (:findings (ex-data e))] 65 | {:report 66 | (some->> findings 67 | not-empty 68 | (map (fn [{:keys [row col message]}] 69 | (format ":%d:%d: Error: %s" row col message))) 70 | (string/join "\n")) 71 | :error? true} 72 | (throw e))))) 73 | 74 | (defn format-lint-warnings 75 | "Formats lint warnings into a more readable string. 76 | Takes the result from the lint function and returns a formatted string." 77 | [lint-result] 78 | (if (nil? lint-result) 79 | "No linting issues found." 80 | (let [report (:report lint-result) 81 | is-error (:error? lint-result) 82 | severity (if is-error "errors" "warnings")] 83 | (str "Code has linting " severity ":\n\n" report)))) 84 | 85 | (defn count-forms [code-str] 86 | (count (:children (parser/parse-string code-str)))) 87 | 88 | -------------------------------------------------------------------------------- /src/clojure_mcp/main_examples/figwheel_main.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.main-examples.figwheel-main 2 | (:require 3 | [clojure-mcp.core :as core] 4 | [clojure-mcp.config :as config] 5 | [clojure-mcp.main :as main] 6 | [clojure-mcp.tools.figwheel.tool :as figwheel-tool])) 7 | 8 | ;; This along with `clojure-mcp.tools.figwheel.tool` are proof of 9 | ;; concept of a clojurescript_tool. This proof of concept can be 10 | ;; improved and provides a blueprint for creating other piggieback repls 11 | ;; node, cljs.main etc. 12 | 13 | ;; Shadow is different in that it has its own nrepl connection. 14 | 15 | ;; In the figwheel based clojurescript project piggieback needs to be 16 | ;; configured in the nrepl that clojure-mcp connects to 17 | ;; 18 | ;; :aliases {:nrepl {:extra-deps {cider/piggieback {:mvn/version "0.6.0"} 19 | ;; nrepl/nrepl {:mvn/version "1.3.1"} 20 | ;; com.bhauman/figwheel-main {:mvn/version "0.2.20"}} 21 | ;; :extra-paths ["test" "target"] ;; examples 22 | ;; :jvm-opts ["-Djdk.attach.allowAttachSelf"] 23 | ;; :main-opts ["-m" "nrepl.cmdline" "--port" "7888" 24 | ;; "--middleware" "[cider.piggieback/wrap-cljs-repl]"]}} 25 | 26 | (defn my-tools [nrepl-client-atom figwheel-build] 27 | (conj (main/my-tools nrepl-client-atom) 28 | (figwheel-tool/figwheel-eval nrepl-client-atom {:figwheel-build figwheel-build}))) 29 | 30 | ;; not sure if this is even needed 31 | (def nrepl-client-atom (atom nil)) 32 | 33 | ;; start the server 34 | (defn start-mcp-server [nrepl-args] 35 | ;; the nrepl-args are a map with :port :host :figwheel-build 36 | (let [nrepl-client-map (core/create-and-start-nrepl-connection nrepl-args) 37 | working-dir (config/get-nrepl-user-dir nrepl-client-map) 38 | resources (main/my-resources nrepl-client-map working-dir) 39 | _ (reset! nrepl-client-atom nrepl-client-map) 40 | tools (my-tools nrepl-client-atom (:figwheel-build nrepl-args "dev")) 41 | prompts (main/my-prompts working-dir) 42 | mcp (core/mcp-server)] 43 | (doseq [tool tools] 44 | (core/add-tool mcp tool)) 45 | (doseq [resource resources] 46 | (core/add-resource mcp resource)) 47 | (doseq [prompt prompts] 48 | (core/add-prompt mcp prompt)) 49 | (swap! nrepl-client-atom assoc :mcp-server mcp) 50 | nil)) 51 | -------------------------------------------------------------------------------- /src/clojure_mcp/main_examples/shadow_main.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.main-examples.shadow-main 2 | (:require 3 | [clojure-mcp.core :as core] 4 | [clojure-mcp.config :as config] 5 | [clojure-mcp.nrepl :as nrepl] 6 | [clojure.tools.logging :as log] 7 | [clojure-mcp.main :as main] 8 | [clojure-mcp.tools.eval.tool :as eval-tool])) 9 | 10 | (def tool-name "clojurescript_eval") 11 | 12 | (def description 13 | "Takes a ClojureScript Expression and evaluates it in the current namespace. For example, providing `(+ 1 2)` will evaluate to 3. 14 | 15 | **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. 16 | 17 | **Important**: Both `require` and `ns` `:require` clauses can only reference actual files from your project, not namespaces created in the same REPL session. 18 | 19 | JavaScript interop is fully supported including `js/console.log`, `js/setTimeout`, DOM APIs, etc. 20 | 21 | **IMPORTANT**: This repl is intended for CLOJURESCRIPT CODE only.") 22 | 23 | (defn start-shadow-repl [nrepl-client-atom cljs-session {:keys [shadow-build shadow-watch]}] 24 | (let [start-code (format 25 | ;; TODO we need to check if its already running 26 | ;; here and only initialize if it isn't 27 | (if shadow-watch 28 | "(do (shadow/watch %s) (shadow/repl %s))" 29 | "(do (shadow/repl %s) %s)") 30 | (pr-str (keyword (name shadow-build))) 31 | (pr-str (keyword (name shadow-build))))] 32 | (nrepl/eval-code-msg 33 | @nrepl-client-atom start-code {:session cljs-session} 34 | (->> identity 35 | (nrepl/out-err #(log/info %) #(log/info %)) 36 | (nrepl/value #(log/info %)) 37 | (nrepl/done (fn [_] (log/info "done"))) 38 | (nrepl/error (fn [args] 39 | (log/info (pr-str args)) 40 | (log/info "ERROR in shadow start"))))) 41 | cljs-session)) 42 | 43 | ;; when having a completely different connection for cljs 44 | (defn shadow-eval-tool-secondary-connection-tool [nrepl-client-atom {:keys [shadow-port shadow-build shadow-watch] :as config}] 45 | (let [cljs-nrepl-client-map (core/create-additional-connection nrepl-client-atom {:port shadow-port}) 46 | cljs-nrepl-client-atom (atom cljs-nrepl-client-map)] 47 | (start-shadow-repl 48 | cljs-nrepl-client-atom 49 | (nrepl/eval-session cljs-nrepl-client-map) 50 | config) 51 | (-> (eval-tool/eval-code cljs-nrepl-client-atom) 52 | (assoc :name tool-name) 53 | (assoc :description description)))) 54 | 55 | ;; when sharing the clojure and cljs repl 56 | (defn shadow-eval-tool [nrepl-client-atom {:keys [shadow-build shadow-watch] :as config}] 57 | (let [cljs-session (nrepl/new-session @nrepl-client-atom) 58 | _ (start-shadow-repl nrepl-client-atom cljs-session config)] 59 | (-> (eval-tool/eval-code nrepl-client-atom {:nrepl-session cljs-session}) 60 | (assoc :name tool-name) 61 | (assoc :description description)))) 62 | 63 | ;; So we can set up shadow two ways 64 | ;; 1. as a single repl connection using the shadow clojure connection for cloj eval 65 | ;; 2. or the user starts two processes one for clojure and then we connect to shadow 66 | ;; as a secondary connection 67 | 68 | (defn my-tools [nrepl-client-atom {:keys [port shadow-port shadow-build shadow-watch] :as config}] 69 | (if (and port shadow-port (not= port shadow-port)) 70 | (conj (main/my-tools nrepl-client-atom) 71 | (shadow-eval-tool-secondary-connection-tool nrepl-client-atom config)) 72 | (conj (main/my-tools nrepl-client-atom) 73 | (shadow-eval-tool nrepl-client-atom config)))) 74 | 75 | ;; not sure if this is even needed 76 | (def nrepl-client-atom (atom nil)) 77 | 78 | ;; start the server 79 | (defn start-mcp-server [nrepl-args] 80 | ;; the nrepl-args are a map with :port :host :figwheel-build 81 | (let [nrepl-client-map (core/create-and-start-nrepl-connection nrepl-args) 82 | working-dir (config/get-nrepl-user-dir nrepl-client-map) 83 | resources (main/my-resources nrepl-client-map working-dir) 84 | _ (reset! nrepl-client-atom nrepl-client-map) 85 | tools (my-tools nrepl-client-atom nrepl-args) 86 | prompts (main/my-prompts working-dir) 87 | mcp (core/mcp-server)] 88 | (doseq [tool tools] 89 | (core/add-tool mcp tool)) 90 | (doseq [resource resources] 91 | (core/add-resource mcp resource)) 92 | (doseq [prompt prompts] 93 | (core/add-prompt mcp prompt)) 94 | (swap! nrepl-client-atom assoc :mcp-server mcp) 95 | nil)) 96 | -------------------------------------------------------------------------------- /src/clojure_mcp/other_tools/README.md: -------------------------------------------------------------------------------- 1 | # Other Tools - Deprecated but Preserved 2 | 3 | ## Overview 4 | 5 | This directory contains tools that have been moved out of active use in the main Clojure MCP server. While these tools are **not registered in `main.clj`** and therefore not available by default, they remain fully functional with passing tests. 6 | 7 | ## Status: Deprecated but Valuable 8 | 9 | These tools were moved here because they **found little use** in typical Clojure development workflows. However, they retain value as: 10 | 11 | - **Reference implementations** for building new tools 12 | - **Examples** of the tool-system architecture 13 | - **Specialized tools** for custom MCP servers or specific use cases 14 | - **Learning resources** for understanding the multimethod-based tool pattern 15 | 16 | ## Available Tools 17 | 18 | ### File System Operations 19 | - **`create_directory/`** - Create directories and nested directory structures 20 | - **`list_directory/`** - List files and directories with formatted output 21 | - **`move_file/`** - Move and rename files/directories 22 | 23 | ### Clojure Introspection Tools 24 | - **`namespace/`** - Namespace exploration and analysis 25 | - `current_namespace` - Get the current REPL namespace 26 | - `clojure_list_namespaces` - List all loaded namespaces 27 | - `clojure_list_vars_in_namespace` - Inspect vars in a specific namespace 28 | 29 | - **`symbol/`** - Symbol information and documentation 30 | - `symbol_completions` - Get symbol completions for a prefix 31 | - `symbol_metadata` - Retrieve complete symbol metadata 32 | - `symbol_documentation` - Get symbol documentation and arglists 33 | - `source_code` - Retrieve source code for symbols 34 | - `symbol_search` - Search for symbols across all namespaces 35 | 36 | ## Usage Options 37 | 38 | ### 1. Re-activate for Main Server 39 | To make any of these tools available in the main MCP server: 40 | 41 | ```clojure 42 | ;; In main.clj, add to imports: 43 | [clojure-mcp.other-tools.create-directory.tool :as create-dir-tool] 44 | 45 | ;; In my-tools function, add: 46 | (create-dir-tool/create-directory-tool nrepl-client-atom) 47 | ``` 48 | 49 | ### 2. Custom MCP Server 50 | Create a specialized MCP server using the core API: 51 | 52 | ```clojure 53 | (ns my-custom-server 54 | (:require [clojure-mcp.core :as core] 55 | [clojure-mcp.other-tools.namespace.tool :as ns-tool] 56 | [clojure-mcp.other-tools.symbol.tool :as symbol-tool])) 57 | 58 | (defn create-introspection-server [] 59 | (let [mcp (core/mcp-server)] 60 | (core/add-tool mcp (ns-tool/current-namespace-tool nrepl-client-atom)) 61 | (core/add-tool mcp (symbol-tool/symbol-search-tool nrepl-client-atom)) 62 | mcp)) 63 | ``` 64 | 65 | ### 3. Direct Usage in Code 66 | All tools can be used directly via their registration functions: 67 | 68 | ```clojure 69 | (require '[clojure-mcp.other-tools.create-directory.tool :as create-dir]) 70 | (def tool-fn (:tool-fn (create-dir/create-directory-tool client-atom))) 71 | ``` 72 | 73 | ## Architecture Reference 74 | 75 | Each tool follows the standard pattern: 76 | - **`core.clj`** - Pure functionality without MCP dependencies 77 | - **`tool.clj`** - MCP integration using the tool-system multimethods 78 | - **Corresponding tests** in `/test/clojure_mcp/other_tools/` 79 | 80 | This makes them excellent examples for understanding how to build new tools using the multimethod-based architecture. 81 | 82 | ## Maintenance Status 83 | 84 | - ✅ **Fully tested** - All tools have comprehensive test suites 85 | - ✅ **Functionally complete** - Tools work as designed 86 | - ⚠️ **Not actively maintained** - No new features planned 87 | - ⚠️ **Deprecated from main server** - Not available by default 88 | 89 | ## Contributing 90 | 91 | While these tools are deprecated from the main server, improvements are welcome: 92 | - Bug fixes and maintenance updates 93 | - Better documentation and examples 94 | - Performance improvements 95 | - Enhanced error handling 96 | 97 | However, consider whether new features might be better implemented as part of the active tool set instead. 98 | -------------------------------------------------------------------------------- /src/clojure_mcp/other_tools/create_directory/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.create-directory.core 2 | "Core implementation for the create-directory tool. 3 | This namespace contains the pure functionality without any MCP-specific code." 4 | (:require 5 | [clojure.java.io :as io])) 6 | 7 | (defn create-directory 8 | "Creates a directory (and parent directories if needed) or succeeds silently if it exists. 9 | 10 | Parameters: 11 | - path: The validated and normalized path to the directory to create 12 | 13 | Returns a map with: 14 | - :success - Boolean indicating if the operation was successful 15 | - :path - The directory path 16 | - :exists - Boolean indicating if the directory already existed 17 | - :created - Boolean indicating if the directory was newly created 18 | - :error - Error message if the operation failed (only when success is false)" 19 | [path] 20 | (let [dir-file (io/file path)] 21 | (cond 22 | ;; Case 1: Directory already exists 23 | (.exists dir-file) 24 | (if (.isDirectory dir-file) 25 | {:success true 26 | :path path 27 | :exists true 28 | :created false} 29 | {:success false 30 | :path path 31 | :error (str "Path exists but is a file, not a directory: " path)}) 32 | 33 | ;; Case 2: Try to create directory 34 | :else 35 | (try 36 | (if (.mkdirs dir-file) 37 | {:success true 38 | :path path 39 | :exists false 40 | :created true} 41 | {:success false 42 | :path path 43 | :error (str "Failed to create directory: " path)}) 44 | (catch Exception e 45 | {:success false 46 | :path path 47 | :error (str "Error creating directory: " (.getMessage e))}))))) 48 | 49 | (comment 50 | ;; === Examples of using the create-directory core functionality directly === 51 | 52 | ;; Test within temp directory 53 | (def temp-dir (System/getProperty "java.io.tmpdir")) 54 | (def test-path (str temp-dir "/test-dir/nested/path")) 55 | 56 | ;; Create directory 57 | (create-directory test-path) 58 | 59 | ;; Verify directory exists 60 | (.exists (io/file test-path)) ;; Should be true 61 | (.isDirectory (io/file test-path)) ;; Should be true 62 | 63 | ;; Create same directory again (should not error) 64 | (create-directory test-path) 65 | 66 | ;; Create a file that conflicts with directory path 67 | (def conflict-path (str temp-dir "/test-file-not-dir")) 68 | (spit conflict-path "test content") 69 | (create-directory conflict-path) ;; Should fail 70 | 71 | ;; Clean up 72 | (io/delete-file conflict-path) 73 | (.delete (io/file test-path)) 74 | (.delete (io/file (str temp-dir "/test-dir/nested"))) 75 | (.delete (io/file (str temp-dir "/test-dir")))) -------------------------------------------------------------------------------- /src/clojure_mcp/other_tools/create_directory/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.create-directory.tool 2 | "Implementation of the create-directory tool using the tool-system multimethod approach." 3 | (:require 4 | [clojure-mcp.tool-system :as tool-system] 5 | [clojure-mcp.other-tools.create-directory.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-tool 10 | "Creates the create-directory tool configuration" 11 | [nrepl-client-atom] 12 | {:tool-type :create-directory 13 | :nrepl-client-atom nrepl-client-atom}) 14 | 15 | ;; Implement the required multimethods for the create-directory tool 16 | (defmethod tool-system/tool-name :create-directory [_] 17 | "create_directory") 18 | 19 | (defmethod tool-system/tool-description :create-directory [_] 20 | "Create a new directory or ensure a directory exists. Can create multiple nested directories in one operation. If the directory already exists, this operation will succeed silently. Perfect for setting up directory structures for projects or ensuring required paths exist. Only works within allowed directories.") 21 | 22 | (defmethod tool-system/tool-schema :create-directory [_] 23 | {:type :object 24 | :properties {:path {:type :string 25 | :description "The path to the directory to create or ensure exists."}} 26 | :required [:path]}) 27 | 28 | (defmethod tool-system/validate-inputs :create-directory [{:keys [nrepl-client-atom]} inputs] 29 | (let [{:keys [path]} inputs 30 | nrepl-client @nrepl-client-atom] 31 | ;; Validate required parameters 32 | (when-not path 33 | (throw (ex-info "Missing required parameter: path" {:inputs inputs}))) 34 | 35 | ;; Validate path using the utility function 36 | (let [validated-path (valid-paths/validate-path-with-client path nrepl-client)] 37 | ;; Return validated inputs with normalized path 38 | (assoc inputs :path validated-path)))) 39 | 40 | (defmethod tool-system/execute-tool :create-directory [_ inputs] 41 | (let [{:keys [path]} inputs] 42 | ;; Delegate to core implementation 43 | (core/create-directory path))) 44 | 45 | (defmethod tool-system/format-results :create-directory [_ {:keys [success error path exists created] :as result}] 46 | (if success 47 | ;; Success case 48 | {:result [(cond 49 | exists (str "Directory already exists: " path) 50 | created (str "Created directory: " path) 51 | :else (str "Directory operation completed: " path))] 52 | :error false} 53 | ;; Error case 54 | {:result [(or error "Unknown error creating directory")] 55 | :error true})) 56 | 57 | ;; Backward compatibility function that returns the registration map 58 | (defn create-directory-tool-registration [nrepl-client-atom] 59 | (tool-system/registration-map (create-directory-tool nrepl-client-atom))) 60 | 61 | (comment 62 | ;; === Examples of using the create-directory tool === 63 | 64 | ;; Setup for REPL-based testing 65 | (def client-atom (atom (clojure-mcp.nrepl/create {:port 7888}))) 66 | (clojure-mcp.nrepl/start-polling @client-atom) 67 | 68 | ;; Create a tool instance 69 | (def dir-tool (create-directory-tool client-atom)) 70 | 71 | ;; Test the individual multimethod steps 72 | (def inputs {:path "/tmp/test-dir/nested"}) 73 | (def validated (tool-system/validate-inputs dir-tool inputs)) 74 | (def result (tool-system/execute-tool dir-tool validated)) 75 | (def formatted (tool-system/format-results dir-tool result)) 76 | 77 | ;; Generate the full registration map 78 | (def reg-map (tool-system/registration-map dir-tool)) 79 | 80 | ;; Test running the tool-fn directly 81 | (def tool-fn (:tool-fn reg-map)) 82 | (tool-fn nil {"path" "/tmp/test-dir/nested"} 83 | (fn [result error] (println "Result:" result "Error:" error))) 84 | 85 | ;; Clean up 86 | (clojure-mcp.nrepl/stop-polling @client-atom)) -------------------------------------------------------------------------------- /src/clojure_mcp/other_tools/list_directory/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.list-directory.core 2 | "Core implementation for the list-directory tool. 3 | This namespace contains the pure functionality without any MCP-specific code." 4 | (:require 5 | [clojure.java.io :as io])) 6 | 7 | (defn list-directory 8 | "Lists files and directories at the specified path. 9 | 10 | Parameters: 11 | - path: The validated and normalized path to the directory 12 | 13 | Returns: 14 | - A map with :files and :directories vectors and :full-path string. 15 | - A map with :error string if the path does not exist or is not a directory." 16 | [path] 17 | (let [dir (io/file path)] 18 | (if (.exists dir) 19 | (if (.isDirectory dir) 20 | (let [contents (.listFiles dir) 21 | files (filter #(.isFile %) contents) 22 | dirs (filter #(.isDirectory %) contents)] 23 | {:files (mapv #(.getName %) files) 24 | :directories (mapv #(.getName %) dirs) 25 | :full-path (.getAbsolutePath dir)}) 26 | {:error (str path " is not a directory")}) 27 | {:error (str path " does not exist")}))) -------------------------------------------------------------------------------- /src/clojure_mcp/other_tools/list_directory/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.list-directory.tool 2 | "Implementation of the list-directory tool using the tool-system multimethod approach." 3 | (:require 4 | [clojure-mcp.tool-system :as tool-system] 5 | [clojure-mcp.other-tools.list-directory.core :as core] 6 | [clojure-mcp.utils.valid-paths :as valid-paths] 7 | [clojure.string :as str])) 8 | 9 | ;; Factory function to create the tool configuration 10 | (defn create-list-directory-tool 11 | "Creates the list-directory tool configuration. 12 | 13 | Parameters: 14 | - nrepl-client-atom: Atom containing the nREPL client" 15 | [nrepl-client-atom] 16 | {:tool-type :list-directory 17 | :nrepl-client-atom nrepl-client-atom}) 18 | 19 | ;; Helper function to format directory listing output 20 | (defn format-directory-listing 21 | "Format directory listing into a readable string representation. 22 | 23 | Parameters: 24 | - result: Result map from list-directory function 25 | 26 | Returns a formatted string with directory content listing" 27 | [result] 28 | (if (:error result) 29 | (:error result) 30 | (let [{:keys [files directories full-path]} result] 31 | (with-out-str 32 | (println "Directory:" full-path) 33 | (println "===============================") 34 | (when (seq directories) 35 | (println "\nDirectories:") 36 | (doseq [dir (sort directories)] 37 | (println "[DIR]" dir))) 38 | (when (seq files) 39 | (println "\nFiles:") 40 | (doseq [file (sort files)] 41 | (println "[FILE]" file))) 42 | (when (and (empty? directories) (empty? files)) 43 | (println "Directory is empty")))))) 44 | 45 | ;; Implement the required multimethods for the list-directory tool 46 | (defmethod tool-system/tool-name :list-directory [_] 47 | "fs_list_directory") 48 | 49 | (defmethod tool-system/tool-description :list-directory [_] 50 | "Lists all files and directories at the specified path. 51 | Returns a formatted directory listing with files and subdirectories clearly labeled.") 52 | 53 | (defmethod tool-system/tool-schema :list-directory [_] 54 | {:type :object 55 | :properties {:path {:type :string}} 56 | :required [:path]}) 57 | 58 | (defmethod tool-system/validate-inputs :list-directory [{:keys [nrepl-client-atom]} inputs] 59 | (let [{:keys [path]} inputs 60 | nrepl-client @nrepl-client-atom] 61 | (when-not path 62 | (throw (ex-info "Missing required parameter: path" {:inputs inputs}))) 63 | 64 | ;; Use the existing validate-path-with-client function 65 | (let [validated-path (valid-paths/validate-path-with-client path nrepl-client)] 66 | ;; Return validated inputs with normalized path 67 | {:path validated-path}))) 68 | 69 | (defmethod tool-system/execute-tool :list-directory [_ inputs] 70 | (let [{:keys [path]} inputs] 71 | ;; Call our implementation in core namespace 72 | (core/list-directory path))) 73 | 74 | (defmethod tool-system/format-results :list-directory [_ result] 75 | (if (and (map? result) (:error result)) 76 | ;; If there's an error, return it with error flag true 77 | {:result [(:error result)] 78 | :error true} 79 | ;; Otherwise, format the directory listing and return it 80 | {:result [(format-directory-listing result)] 81 | :error false})) 82 | 83 | ;; Backward compatibility function that returns the registration map 84 | (defn list-directory-tool 85 | "Returns the registration map for the list-directory tool. 86 | 87 | Parameters: 88 | - nrepl-client-atom: Atom containing the nREPL client" 89 | [nrepl-client-atom] 90 | (tool-system/registration-map (create-list-directory-tool nrepl-client-atom))) -------------------------------------------------------------------------------- /src/clojure_mcp/other_tools/move_file/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.move-file.core 2 | "Core implementation for the move-file tool. 3 | This namespace contains the pure functionality without any MCP-specific code." 4 | (:require 5 | [clojure.java.io :as io])) 6 | 7 | (defn move-file 8 | "Moves or renames a file or directory. 9 | 10 | Parameters: 11 | - source: The validated and normalized source path 12 | - destination: The validated and normalized destination path 13 | 14 | Returns a map with: 15 | - :success - Boolean indicating if the operation was successful 16 | - :source - The source path 17 | - :destination - The destination path 18 | - :error - Error message if the operation failed 19 | 20 | The operation will fail if: 21 | - The source doesn't exist 22 | - The destination already exists 23 | - The move operation fails for any reason" 24 | [source destination] 25 | (let [source-file (io/file source) 26 | dest-file (io/file destination)] 27 | 28 | (cond 29 | ;; Case 1: Source doesn't exist 30 | (not (.exists source-file)) 31 | {:success false 32 | :source source 33 | :destination destination 34 | :error (str "Source file or directory does not exist: " source)} 35 | 36 | ;; Case 2: Destination already exists 37 | (.exists dest-file) 38 | {:success false 39 | :source source 40 | :destination destination 41 | :error (str "Destination already exists: " destination)} 42 | 43 | ;; Case 3: Try to perform the move 44 | :else 45 | (try 46 | ;; Determine the type before moving the file 47 | (let [file-type (cond 48 | (.isFile source-file) "file" 49 | (.isDirectory source-file) "directory" 50 | :else "unknown")] 51 | (if (.renameTo source-file dest-file) 52 | ;; Success case 53 | {:success true 54 | :source source 55 | :destination destination 56 | :type file-type} 57 | ;; Rename failed but didn't throw exception 58 | {:success false 59 | :source source 60 | :destination destination 61 | :error "Move operation failed. This could be due to permissions or trying to move across different filesystems."})) 62 | (catch Exception e 63 | {:success false 64 | :source source 65 | :destination destination 66 | :error (str "Error during move operation: " (.getMessage e))}))))) 67 | 68 | (comment 69 | ;; === Examples of using the move-file core functionality directly === 70 | 71 | ;; Test within temp directory 72 | (def temp-dir (System/getProperty "java.io.tmpdir")) 73 | (def test-source (str temp-dir "/test-source.txt")) 74 | (def test-dest (str temp-dir "/test-dest.txt")) 75 | 76 | ;; Create a test file 77 | (spit test-source "Test content") 78 | 79 | ;; Test move operation 80 | (move-file test-source test-dest) 81 | 82 | ;; Verify destination exists and source doesn't 83 | (.exists (io/file test-dest)) ;; Should be true 84 | (.exists (io/file test-source)) ;; Should be false 85 | 86 | ;; Clean up 87 | (io/delete-file test-dest)) -------------------------------------------------------------------------------- /src/clojure_mcp/other_tools/move_file/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.move-file.tool 2 | "Implementation of the move-file tool using the tool-system multimethod approach." 3 | (:require 4 | [clojure-mcp.tool-system :as tool-system] 5 | [clojure-mcp.other-tools.move-file.core :as core] 6 | [clojure-mcp.utils.valid-paths :as valid-paths])) 7 | 8 | ;; Factory function to create the tool configuration 9 | (defn create-move-file-tool 10 | "Creates the move-file tool configuration" 11 | [nrepl-client-atom] 12 | {:tool-type :move-file 13 | :nrepl-client-atom nrepl-client-atom}) 14 | 15 | ;; Implement the required multimethods for the move-file tool 16 | (defmethod tool-system/tool-name :move-file [_] 17 | "move_file") 18 | 19 | (defmethod tool-system/tool-description :move-file [_] 20 | "Move or rename files and directories. Can move files between directories and rename them in a single operation. If the destination exists, the operation will fail. Works across different directories and can be used for simple renaming within the same directory. Both source and destination must be within allowed directories.") 21 | 22 | (defmethod tool-system/tool-schema :move-file [_] 23 | {:type :object 24 | :properties {:source {:type :string 25 | :description "The source file or directory path to move or rename."} 26 | :destination {:type :string 27 | :description "The destination file or directory path."}} 28 | :required [:source :destination]}) 29 | 30 | (defmethod tool-system/validate-inputs :move-file [{:keys [nrepl-client-atom]} inputs] 31 | (let [{:keys [source destination]} inputs 32 | nrepl-client @nrepl-client-atom] 33 | ;; Validate required parameters 34 | (when-not source 35 | (throw (ex-info "Missing required parameter: source" {:inputs inputs}))) 36 | 37 | (when-not destination 38 | (throw (ex-info "Missing required parameter: destination" {:inputs inputs}))) 39 | 40 | ;; Validate both paths using the utility function 41 | (let [validated-source (valid-paths/validate-path-with-client source nrepl-client) 42 | validated-destination (valid-paths/validate-path-with-client destination nrepl-client)] 43 | ;; Return validated inputs with normalized paths 44 | (assoc inputs 45 | :source validated-source 46 | :destination validated-destination)))) 47 | 48 | (defmethod tool-system/execute-tool :move-file [_ inputs] 49 | (let [{:keys [source destination]} inputs] 50 | ;; Delegate to core implementation 51 | (core/move-file source destination))) 52 | 53 | (defmethod tool-system/format-results :move-file [_ {:keys [success error source destination type] :as result}] 54 | (if success 55 | ;; Success case 56 | {:result [(str "Successfully moved " type " from " source " to " destination)] 57 | :error false} 58 | ;; Error case 59 | {:result [(or error "Unknown error during move operation")] 60 | :error true})) 61 | 62 | ;; Backward compatibility function that returns the registration map 63 | (defn move-file-tool [nrepl-client-atom] 64 | (tool-system/registration-map (create-move-file-tool nrepl-client-atom))) 65 | 66 | (comment 67 | ;; === Examples of using the move-file tool === 68 | 69 | ;; Setup for REPL-based testing 70 | (def client-atom (atom (clojure-mcp.nrepl/create {:port 7888}))) 71 | (clojure-mcp.nrepl/start-polling @client-atom) 72 | 73 | ;; Create a tool instance 74 | (def move-tool (create-move-file-tool client-atom)) 75 | 76 | ;; Test the individual multimethod steps 77 | (def inputs {:source "/tmp/test-source.txt" :destination "/tmp/test-dest.txt"}) 78 | (def validated (tool-system/validate-inputs move-tool inputs)) 79 | (def result (tool-system/execute-tool move-tool validated)) 80 | (def formatted (tool-system/format-results move-tool result)) 81 | 82 | ;; Generate the full registration map 83 | (def reg-map (tool-system/registration-map move-tool)) 84 | 85 | ;; Test running the tool-fn directly 86 | (def tool-fn (:tool-fn reg-map)) 87 | (tool-fn nil {"source" "/tmp/test-source.txt" "destination" "/tmp/test-dest.txt"} 88 | (fn [result error] (println "Result:" result "Error:" error))) 89 | 90 | ;; Clean up 91 | (clojure-mcp.nrepl/stop-polling @client-atom)) -------------------------------------------------------------------------------- /src/clojure_mcp/other_tools/namespace/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.namespace.core 2 | "Core implementation for namespace-related tools. 3 | This namespace contains the pure functionality without any MCP-specific code." 4 | (:require 5 | [clojure.string :as string] 6 | [clojure-mcp.nrepl :as nrepl])) 7 | 8 | (defn get-current-namespace 9 | "Returns the current namespace from the nREPL client's state. 10 | 11 | Parameters: 12 | - nrepl-client: The nREPL client to use 13 | 14 | Returns a map with: 15 | - :namespace - The current namespace as a string, or nil if not available 16 | - :error - Set to true if no current namespace was found" 17 | [nrepl-client] 18 | (let [current-ns (some-> nrepl-client 19 | (nrepl/current-ns 20 | (nrepl/eval-session nrepl-client)))] 21 | (if current-ns 22 | {:namespace current-ns :error false} 23 | {:error true :message "No current namespace found"}))) 24 | 25 | (defn get-all-namespaces 26 | "Returns a list of all currently loaded namespaces. 27 | 28 | Parameters: 29 | - nrepl-client: The nREPL client to use for evaluation 30 | - eval-fn: A function that takes a client and code string, and returns a result string 31 | 32 | Returns a map with: 33 | - :namespaces - A vector of namespace strings sorted alphabetically 34 | - :error - Set to true if there was an error during evaluation" 35 | [nrepl-client eval-fn] 36 | (let [code "(map str (sort (map ns-name (all-ns))))" 37 | result-str (eval-fn nrepl-client code)] 38 | (if result-str 39 | (try 40 | (let [namespaces (read-string result-str)] 41 | {:namespaces (vec namespaces) :error false}) 42 | (catch Exception e 43 | {:error true :message (str "Error parsing namespaces: " (.getMessage e))})) 44 | {:error true :message "Error retrieving namespaces"}))) 45 | 46 | (defn get-vars-in-namespace 47 | "Returns metadata for all public vars in a given namespace. 48 | 49 | Parameters: 50 | - nrepl-client: The nREPL client to use for evaluation 51 | - eval-fn: A function that takes a client and code string, and returns a result string 52 | - namespace: The namespace to list vars from (as a string) 53 | 54 | Returns a map with: 55 | - :vars - A vector of maps containing metadata for each var 56 | - :error - Set to true if there was an error during evaluation" 57 | [nrepl-client eval-fn namespace] 58 | (let [ns-str (string/trim namespace) 59 | code (pr-str `(when-let [ns-obj# (find-ns (symbol ~ns-str))] 60 | (->> (ns-publics ns-obj#) 61 | vals ;; Get the var objects 62 | (map meta) ;; Get metadata for each var 63 | (map #(select-keys % [:arglists :doc :name :ns])) ;; Select desired keys 64 | (map #(update % :ns str)) 65 | (sort-by :name) ;; Sort by name for consistent order 66 | vec))) ;; Convert to vector 67 | result-str (eval-fn nrepl-client code)] 68 | (cond 69 | ;; Case 1: nREPL evaluation failed entirely 70 | (nil? result-str) 71 | {:error true :message "Error evaluating code to list vars."} 72 | 73 | ;; Case 2: Try to parse the result 74 | :else 75 | (try 76 | (let [result-val (read-string result-str)] 77 | (if result-val 78 | {:vars result-val :error false} 79 | {:error true :message (str "Namespace '" ns-str "' not found.")})) 80 | (catch Exception e 81 | {:error true :message (str "Error parsing result: " (.getMessage e))}))))) 82 | -------------------------------------------------------------------------------- /src/clojure_mcp/other_tools/symbol/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.symbol.core 2 | "Core implementation for symbol-related tools. 3 | This namespace contains the pure functionality without any MCP-specific code." 4 | (:require 5 | [clojure.string :as string] 6 | [clojure-mcp.nrepl :as nrepl])) 7 | 8 | (defn get-symbol-completions 9 | "Returns completions for a given prefix in the current namespace. 10 | 11 | Parameters: 12 | - nrepl-client: The nREPL client to use 13 | - prefix: The symbol prefix to complete (or empty string for all symbols) 14 | 15 | Returns a map with: 16 | - :completions - A vector of completion candidates 17 | - :error - Set to true if there was an error during retrieval" 18 | [nrepl-client prefix] 19 | (try 20 | ;; Use the completions function directly from clojure-mcp.nrepl 21 | (let [completions-raw (clojure-mcp.nrepl/completions nrepl-client prefix) 22 | candidates (mapv :candidate completions-raw)] 23 | {:completions candidates :error false}) 24 | (catch Exception e 25 | {:error true :message (str "Error retrieving completions: " (.getMessage e))}))) 26 | 27 | (defn get-symbol-metadata 28 | "Returns complete metadata for a given symbol. 29 | 30 | Parameters: 31 | - nrepl-client: The nREPL client to use 32 | - symbol-name: The name of the symbol to look up (as a string) 33 | 34 | Returns a map with: 35 | - :metadata - The complete metadata for the symbol 36 | - :error - Set to true if there was an error or the symbol was not found" 37 | [nrepl-client symbol-name] 38 | (try 39 | (if-let [metadata (nrepl/lookup nrepl-client symbol-name)] 40 | {:metadata metadata :error false} 41 | {:error true :message (str "Symbol '" symbol-name "' not found")}) 42 | (catch Exception e 43 | {:error true :message (str "Error retrieving metadata: " (.getMessage e))}))) 44 | 45 | (defn get-symbol-documentation 46 | "Returns the documentation and arglists for a given symbol. 47 | 48 | Parameters: 49 | - nrepl-client: The nREPL client to use 50 | - symbol-name: The name of the symbol to look up (as a string) 51 | 52 | Returns a map with: 53 | - :arglists - The argument lists for the symbol (if it's a function) 54 | - :doc - The docstring for the symbol 55 | - :error - Set to true if there was an error or the symbol was not found" 56 | [nrepl-client symbol-name] 57 | (try 58 | (if-let [metadata (nrepl/lookup nrepl-client symbol-name)] 59 | {:arglists (:arglists metadata) 60 | :doc (:doc metadata) 61 | :error false} 62 | {:error true :message (str "Symbol '" symbol-name "' not found")}) 63 | (catch Exception e 64 | {:error true :message (str "Error retrieving documentation: " (.getMessage e))}))) 65 | 66 | (defn get-source-code 67 | "Returns the source code for a given symbol. 68 | 69 | Parameters: 70 | - nrepl-client: The nREPL client to use for evaluation 71 | - symbol-name: The name of the symbol to get source for (as a string) 72 | 73 | Returns a map with: 74 | - :source - The source code as a string 75 | - :error - Set to true if there was an error during evaluation" 76 | [nrepl-client symbol-name] 77 | (let [code (pr-str `(clojure.repl/source-fn (symbol ~symbol-name))) 78 | result-str (nrepl/tool-eval-code nrepl-client code)] 79 | (if result-str 80 | (try 81 | (let [source (read-string result-str)] 82 | (if (nil? source) 83 | {:error true :message (str "Source not found for '" symbol-name "'")} 84 | {:source source :error false})) 85 | (catch Exception e 86 | {:error true :message (str "Error parsing source: " (.getMessage e))})) 87 | {:error true :message (str "Error retrieving source for '" symbol-name "'")}))) 88 | 89 | (defn search-symbols 90 | "Searches for symbols containing the given string across all namespaces. 91 | 92 | Parameters: 93 | - nrepl-client: The nREPL client to use for evaluation 94 | - search-str: The string to search for in symbol names 95 | 96 | Returns a map with: 97 | - :matches - A vector of matching symbol names 98 | - :error - Set to true if there was an error during evaluation" 99 | [nrepl-client search-str] 100 | (let [code (pr-str `(clojure.repl/apropos ~search-str)) 101 | result-str (nrepl/tool-eval-code nrepl-client code)] 102 | (if result-str 103 | (try 104 | (let [matches (some->> (read-string result-str) 105 | (map str) 106 | (remove #(string/starts-with? % "cider.nrepl")) 107 | vec)] 108 | (if (empty? matches) 109 | {:matches ["No matches found"] :error false} 110 | {:matches matches :error false})) 111 | (catch Exception e 112 | {:error true :message (str "Error parsing search results: " (.getMessage e))})) 113 | {:error true :message (str "Error searching for '" search-str "'")}))) -------------------------------------------------------------------------------- /src/clojure_mcp/resources.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.resources 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :as str] 4 | [clojure.data.json :as json] 5 | [clojure.edn :as edn] 6 | [clojure-mcp.nrepl :as mcp-nrepl] 7 | [clojure-mcp.config :as config] ; Added config require 8 | [clojure-mcp.tools.project.core :as project]) 9 | (:import [io.modelcontextprotocol.spec McpSchema$Resource McpSchema$ReadResourceResult])) 10 | 11 | (defn read-file [full-path] 12 | (let [file (io/file full-path)] 13 | (if (.exists file) 14 | (try 15 | (slurp file) 16 | (catch Exception e 17 | (throw (ex-info (str "reading file- " full-path 18 | "\nException- " (.getMessage e)) 19 | {:path full-path} 20 | e)))) 21 | (throw (ex-info (str "File not found- " full-path 22 | "\nAbsolute path- " (.getAbsolutePath file)) 23 | {:path full-path}))))) 24 | 25 | (defn create-file-resource 26 | "Creates a resource specification for serving a file. 27 | Takes a full file path resolved with the correct working directory." 28 | [url name description mime-type full-path] 29 | {:url url 30 | :name name 31 | :description description 32 | :mime-type mime-type 33 | :resource-fn 34 | (fn [_ _ clj-result-k] 35 | (try 36 | (let [result (read-file full-path)] 37 | (clj-result-k [result])) 38 | (catch Exception e 39 | (clj-result-k [(str "Error in resource function: " 40 | (ex-message e) 41 | "\nFor file: " full-path)]))))}) 42 | 43 | (defn create-string-resource 44 | "Creates a resource specification for serving a string. 45 | Accepts nrepl-client-atom for consistency with create-file-resource, but doesn't use it." 46 | [url name description mime-type contents & [nrepl-client-atom]] 47 | {:url url 48 | :name name 49 | :description description 50 | :mime-type mime-type 51 | :resource-fn (fn [_ _ clj-result-k] 52 | (clj-result-k contents))}) 53 | -------------------------------------------------------------------------------- /src/clojure_mcp/sexp/match.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.sexp.match 2 | (:require [rewrite-clj.zip :as z] 3 | [rewrite-clj.node :as n] 4 | [rewrite-clj.parser :as p])) 5 | 6 | (defn match-sexpr 7 | "Return true if `pattern` matches `data`. 8 | Wildcards in `pattern`: 9 | - `_?` consumes exactly one form 10 | - `_*` consumes zero or more forms, but if there are more pattern elements 11 | after it, it will try to align them with the tail of `data`." 12 | [pattern data] 13 | (cond 14 | ;; both are sequences ⇒ walk with possible '_*' backtracking 15 | (and (sequential? pattern) (sequential? data)) 16 | (letfn [(match-seq [ps ds] 17 | (cond 18 | ;; pattern exhausted ⇒ only match if data also exhausted 19 | (empty? ps) 20 | (empty? ds) 21 | 22 | ;; '_?' ⇒ must have at least one ds, then consume exactly one 23 | (= (first ps) '_?) 24 | (and (seq ds) 25 | (recur (rest ps) (rest ds))) 26 | 27 | ;; '_*' ⇒ two cases: 28 | ;; 1) no more pattern ⇒ matches anything 29 | ;; 2) with remaining pattern, try every split point 30 | (= (first ps) '_*) 31 | (let [ps-rest (rest ps)] 32 | (if (empty? ps-rest) 33 | true ;; Case 1: No more pattern elements after _*, so it matches anything 34 | ;; Case 2: Try matching remaining pattern at each possible position 35 | (loop [k 0] 36 | (cond 37 | ;; We've gone beyond the end of ds, no match 38 | (> k (count ds)) 39 | false 40 | 41 | ;; Try matching rest of pattern against rest of data starting at position k 42 | (match-seq ps-rest (drop k ds)) 43 | true 44 | 45 | ;; Try next position 46 | :else 47 | (recur (inc k)))))) 48 | 49 | ;; nested list/vector ⇒ recurse 50 | (and (sequential? (first ps)) 51 | (sequential? (first ds))) 52 | (and (match-sexpr (first ps) (first ds)) 53 | (recur (rest ps) (rest ds))) 54 | 55 | ;; literal equality 56 | :else 57 | (and (= (first ps) (first ds)) 58 | (recur (rest ps) (rest ds)))))] 59 | (match-seq pattern data)) 60 | (= pattern '_?) true 61 | (= pattern '_*) true 62 | ;; atoms ⇒ direct equality 63 | :else 64 | (= pattern data))) 65 | 66 | (defn find-match* 67 | [pattern-sexpr zloc] 68 | (loop [loc zloc] 69 | (when-not (z/end? loc) 70 | (let [form (try (z/sexpr loc) 71 | (catch Exception e 72 | ::continue))] 73 | (if (= ::continue form) 74 | (recur (z/next loc)) 75 | (if (match-sexpr pattern-sexpr form) 76 | loc 77 | (recur (z/next loc)))))))) 78 | 79 | (defn find-match [pattern-str code-str] 80 | (find-match* (z/sexpr (z/of-string pattern-str)) 81 | (z/of-string code-str))) 82 | 83 | (comment 84 | 85 | ;; Define a multimethod for calculating area 86 | (defmulti area :type) 87 | 88 | (defmethod area :circle 89 | [shape] 90 | (let [{:keys [radius]} shape] 91 | ;; Using exact Math/PI constant instead of hardcoded value 92 | (* Math/PI radius radius)))) 93 | 94 | #_(z/string (find-match "(defmethod area :circle [shape] 95 | (let [{:keys [radius]} shape] 96 | ;; Using exact Math/PI constant instead of hardcoded value 97 | (* _* radius radius ) 98 | ))" (slurp "src/clojure_mcp/sexp/match.clj"))) 99 | 100 | ;; Example tests 101 | #_(match-sexpr '(+ _* radius radius) '(+ 3 radius radius)) 102 | #_(match-sexpr '(defn calc [x y] _* (if (> x 10) x y)) 103 | '(defn calc [x y] (println "calculating") (if (> x 10) x y))) 104 | #_(match-sexpr '(* Math/PI radius radius) '(* Math/PI radius radius)) ;; Regular * operators match normally 105 | -------------------------------------------------------------------------------- /src/clojure_mcp/sse_core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.sse-core 2 | (:require 3 | [clojure-mcp.main :as main] 4 | [clojure-mcp.core :as core] 5 | [clojure-mcp.config :as config] 6 | [clojure.tools.logging :as log]) 7 | (:import 8 | [io.modelcontextprotocol.server.transport 9 | HttpServletSseServerTransportProvider] 10 | [org.eclipse.jetty.server Server] 11 | [org.eclipse.jetty.servlet ServletContextHandler ServletHolder] 12 | [jakarta.servlet.http HttpServlet HttpServletRequest HttpServletResponse] 13 | [io.modelcontextprotocol.server McpServer McpServerFeatures 14 | McpServerFeatures$AsyncToolSpecification 15 | McpServerFeatures$AsyncResourceSpecification] 16 | [io.modelcontextprotocol.spec 17 | McpSchema$ServerCapabilities] 18 | [com.fasterxml.jackson.databind ObjectMapper])) 19 | 20 | ;; helpers for setting up an sse mcp server 21 | 22 | (defn mcp-sse-server [] 23 | (log/info "Starting SSE MCP server") 24 | (try 25 | (let [transport-provider (HttpServletSseServerTransportProvider. (ObjectMapper.) "/mcp/message") 26 | server (-> (McpServer/async transport-provider) 27 | (.serverInfo "clojure-server" "0.1.0") 28 | (.capabilities (-> (McpSchema$ServerCapabilities/builder) 29 | (.tools true) 30 | (.prompts true) 31 | (.resources true true) 32 | #_(.logging) 33 | (.build))) 34 | (.build))] 35 | (log/info "SSE MCP server initialized successfully") 36 | {:provider-servlet transport-provider 37 | :mcp-server server}) 38 | (catch Exception e 39 | (log/error e "Failed to initialize SSE MCP server") 40 | (throw e)))) 41 | 42 | (defn host-mcp-servlet 43 | "Main function to start the embedded Jetty server." 44 | [servlet port] 45 | (let [server (Server. port) 46 | context (ServletContextHandler. ServletContextHandler/SESSIONS)] 47 | (.setContextPath context "/") 48 | (.addServlet context (ServletHolder. servlet) "/") 49 | (.setHandler server context) 50 | (.start server) 51 | (println (str "Clojure tooling SSE MCP server started on port " port ".")) 52 | (.join server))) 53 | -------------------------------------------------------------------------------- /src/clojure_mcp/sse_main.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.sse-main 2 | (:require 3 | [clojure-mcp.main :as main] 4 | [clojure-mcp.sse-core :as sse-core] 5 | [clojure-mcp.core :as core] 6 | [clojure-mcp.config :as config] 7 | [clojure.tools.logging :as log])) 8 | 9 | (def nrepl-client-atom (atom nil)) 10 | 11 | (defn start-sse-mcp-server [args] 12 | ;; the nrepl-args are a map with :port :host 13 | ;; we also need an :mcp-sse-port so we'll default to 8078?? 14 | (let [mcp-port (:mcp-sse-port args 8078) 15 | nrepl-client-map (core/create-and-start-nrepl-connection args) 16 | working-dir (config/get-nrepl-user-dir nrepl-client-map) 17 | resources (main/my-resources nrepl-client-map working-dir) 18 | _ (reset! nrepl-client-atom nrepl-client-map) 19 | tools (main/my-tools nrepl-client-atom) 20 | prompts (main/my-prompts working-dir) 21 | {:keys [mcp-server provider-servlet] } (sse-core/mcp-sse-server)] 22 | (doseq [tool tools] 23 | (core/add-tool mcp-server tool)) 24 | (doseq [resource resources] 25 | (core/add-resource mcp-server resource)) 26 | (doseq [prompt prompts] 27 | (core/add-prompt mcp-server prompt)) 28 | ;; hold onto this so you can shut it down if necessary 29 | (swap! nrepl-client-atom assoc :mcp-server mcp-server) 30 | (sse-core/host-mcp-servlet provider-servlet mcp-port) 31 | nil)) 32 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/architect/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.architect.tool 2 | (:require [clojure-mcp.tool-system :as tool-system] 3 | [clojure-mcp.tools.architect.core :as core])) 4 | 5 | (defn create-architect-tool 6 | "Creates the architect tool configuration. 7 | 8 | Args: 9 | - nrepl-client-atom: Required nREPL client atom 10 | - model: Optional pre-built langchain model to use instead of auto-detection" 11 | ([nrepl-client-atom] 12 | (create-architect-tool nrepl-client-atom nil)) 13 | ([nrepl-client-atom model] 14 | {:tool-type :architect 15 | :nrepl-client-atom nrepl-client-atom 16 | :model model})) 17 | 18 | (defn architect-tool 19 | "Returns a tool registration for the architect tool compatible with the MCP system. 20 | 21 | Usage: 22 | 23 | Basic usage with auto-detected reasoning model: 24 | (architect-tool nrepl-client-atom) 25 | 26 | With custom model configuration: 27 | (architect-tool nrepl-client-atom {:model my-custom-model}) 28 | 29 | Where: 30 | - nrepl-client-atom: Required nREPL client atom 31 | - config: Optional config map with keys: 32 | - :model - Pre-built langchain model to use instead of auto-detection 33 | 34 | Examples: 35 | ;; Default reasoning model (with medium reasoning effort) 36 | (def my-architect (architect-tool nrepl-client-atom)) 37 | 38 | ;; Custom Anthropic model 39 | (def fast-model (-> (chain/create-anthropic-model \"claude-3-haiku-20240307\") (.build))) 40 | (def fast-architect (architect-tool nrepl-client-atom {:model fast-model})) 41 | 42 | ;; Custom OpenAI reasoning model with high effort 43 | (def reasoning-model (-> (chain/create-openai-model \"o1-preview\") 44 | (chain/default-request-parameters #(chain/reasoning-effort % :high)) 45 | (.build))) 46 | (def reasoning-architect (architect-tool nrepl-client-atom {:model reasoning-model}))" 47 | ([nrepl-client-atom] 48 | (architect-tool nrepl-client-atom nil)) 49 | ([nrepl-client-atom {:keys [model]}] 50 | (tool-system/registration-map (create-architect-tool nrepl-client-atom model)))) 51 | 52 | (defmethod tool-system/tool-name :architect [_] 53 | "architect") 54 | 55 | (defmethod tool-system/tool-description :architect [_] 56 | "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.") 57 | 58 | (defmethod tool-system/tool-schema :architect [_] 59 | {:type :object 60 | :properties {:prompt {:type :string 61 | :description "The technical request or coding task to analyze"} 62 | :context {:type :string 63 | :description "Optional context from previous conversation or system state"}} 64 | :required [:prompt]}) 65 | 66 | (defmethod tool-system/validate-inputs :architect [_ inputs] 67 | (core/validate-architect-inputs inputs)) 68 | 69 | (defmethod tool-system/execute-tool :architect [{:keys [nrepl-client-atom model] :as tool} inputs] 70 | (core/architect tool inputs)) 71 | 72 | (defmethod tool-system/format-results :architect [_ {:keys [result error] :as results}] 73 | {:result [result] 74 | :error error}) 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/dispatch_agent/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.dispatch-agent.tool 2 | (:require [clojure-mcp.tool-system :as tool-system] 3 | [clojure-mcp.tools.dispatch-agent.core :as core])) 4 | 5 | (defn create-dispatch-agent-tool 6 | "Creates the dispatch agent tool configuration. 7 | 8 | Args: 9 | - nrepl-client-atom: Required nREPL client atom 10 | - model: Optional pre-built langchain model to use instead of auto-detection" 11 | ([nrepl-client-atom] 12 | (create-dispatch-agent-tool nrepl-client-atom nil)) 13 | ([nrepl-client-atom model] 14 | {:tool-type :dispatch-agent 15 | :nrepl-client-atom nrepl-client-atom 16 | :model model})) 17 | 18 | (defn dispatch-agent-tool 19 | "Returns a tool registration for the dispatch-agent tool compatible with the MCP system. 20 | 21 | Usage: 22 | 23 | Basic usage with auto-detected model: 24 | (dispatch-agent-tool nrepl-client-atom) 25 | 26 | With custom model configuration: 27 | (dispatch-agent-tool nrepl-client-atom {:model my-custom-model}) 28 | 29 | Where: 30 | - nrepl-client-atom: Required nREPL client atom 31 | - config: Optional config map with keys: 32 | - :model - Pre-built langchain model to use instead of auto-detection 33 | 34 | Examples: 35 | ;; Default model 36 | (def my-agent (dispatch-agent-tool nrepl-client-atom)) 37 | 38 | ;; Custom Anthropic model 39 | (def fast-model (-> (chain/create-anthropic-model \"claude-3-haiku-20240307\") (.build))) 40 | (def fast-agent (dispatch-agent-tool nrepl-client-atom {:model fast-model})) 41 | 42 | ;; Custom OpenAI model 43 | (def reasoning-model (-> (chain/create-openai-model \"o1-preview\") (.build))) 44 | (def reasoning-agent (dispatch-agent-tool nrepl-client-atom {:model reasoning-model}))" 45 | ([nrepl-client-atom] 46 | (dispatch-agent-tool nrepl-client-atom nil)) 47 | ([nrepl-client-atom {:keys [model]}] 48 | (tool-system/registration-map (create-dispatch-agent-tool nrepl-client-atom model)))) 49 | 50 | (defmethod tool-system/tool-name :dispatch-agent [_] 51 | "dispatch_agent") 52 | 53 | (defmethod tool-system/tool-description :dispatch-agent [_] 54 | "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: 55 | 56 | - If you are searching for a keyword like \"config\" or \"logger\", the Agent tool is appropriate 57 | - 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 58 | - If you are searching for a specific class definition like \"class Foo\", use the glob_files tool instead, to find the match more quickly 59 | 60 | Usage notes: 61 | 1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple `agent` tool uses 62 | 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. 63 | 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. 64 | 4. The agent's outputs should generally be trusted") 65 | 66 | (defmethod tool-system/tool-schema :dispatch-agent [_] 67 | {:type :object 68 | :properties {:prompt {:type :string 69 | :description "The prompt to send to the agent"}} 70 | :required [:prompt]}) 71 | 72 | (defmethod tool-system/validate-inputs :dispatch-agent [_ inputs] 73 | (core/validate-dispatch-agent-inputs inputs)) 74 | 75 | (defmethod tool-system/execute-tool :dispatch-agent [tool {:keys [prompt]}] 76 | (core/dispatch-agent tool prompt)) 77 | 78 | (defmethod tool-system/format-results :dispatch-agent [_ {:keys [result error] :as results}] 79 | {:result [result] 80 | :error error}) 81 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/figwheel/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.figwheel.tool 2 | (:require 3 | [clojure.tools.logging :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 | 11 | (defn start-figwheel [nrepl-client-atom build] 12 | (let [figwheel-session (nrepl/new-session @nrepl-client-atom) 13 | start-code (format 14 | ;; TODO we need to check if its already running 15 | ;; here and only initialize if it isn't 16 | "(do (require (quote figwheel.main)) (figwheel.main/start %s))" 17 | (pr-str build))] 18 | (nrepl/eval-code-msg 19 | @nrepl-client-atom start-code {:session figwheel-session} 20 | (->> identity 21 | (nrepl/out-err #(log/info %) #(log/info %)) 22 | (nrepl/value #(log/info %)) 23 | (nrepl/done (fn [_] (log/info "done"))) 24 | (nrepl/error (fn [args] 25 | (log/info (pr-str args)) 26 | (log/info "ERROR in figwheel start"))))) 27 | figwheel-session)) 28 | 29 | (defn create-figwheel-eval-tool 30 | "Creates the evaluation tool configuration" 31 | [nrepl-client-atom {:keys [figwheel-build] :as config}] 32 | (let [figwheel-session (start-figwheel nrepl-client-atom figwheel-build)] 33 | {:tool-type ::figwheel-eval 34 | :nrepl-client-atom nrepl-client-atom 35 | :timeout 30000 36 | :session figwheel-session})) 37 | 38 | ;; delegate schema validate-inputs and format-results to clojure-eval 39 | (derive ::figwheel-eval ::eval-tool/clojure-eval) 40 | 41 | (defmethod tool-system/tool-name ::figwheel-eval [_] 42 | "clojurescript_eval") 43 | 44 | (defmethod tool-system/tool-description ::figwheel-eval [_] 45 | "Takes a ClojureScript Expression and evaluates it in the current namespace. For example, providing `(+ 1 2)` will evaluate to 3. 46 | 47 | **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. 48 | 49 | **Important**: Both `require` and `ns` `:require` clauses can only reference actual files from your project, not namespaces created in the same REPL session. 50 | 51 | **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. 52 | 53 | **Namespace Rules**: Namespaces must be declared at the top level separately: 54 | ```clojure 55 | ;; First evaluate namespace 56 | (ns example.namespace) 57 | 58 | ;; Then evaluate functions 59 | (do 60 | (defn add [a b] (+ a b)) 61 | (add 5 9)) 62 | ``` 63 | 64 | **WILL NOT WORK**: 65 | ```clojure 66 | (do 67 | (ns example.namespace) 68 | (defn add [a b] (+ a b))) 69 | ``` 70 | 71 | JavaScript interop is fully supported including `js/console.log`, `js/setTimeout`, DOM APIs, etc. 72 | 73 | **IMPORTANT**: This repl is intended for CLOJURESCRIPT CODE only.") 74 | 75 | (defmethod tool-system/execute-tool ::figwheel-eval [{:keys [nrepl-client-atom session]} inputs] 76 | (assert session) 77 | (assert (:code inputs)) 78 | ;; :code has to exist at this point 79 | (let [code (:code inputs (get inputs "code"))] 80 | ;; *ns* doesn't work on ClojureScript and its confusing for the LLM 81 | (if (= (string/trim code) "*ns*") 82 | {:outputs [[:value (nrepl/current-ns @nrepl-client-atom session)]] 83 | :error false} 84 | (eval-core/evaluate-with-repair @nrepl-client-atom (assoc inputs :session session))))) 85 | 86 | ;; config needs :fig 87 | (defn figwheel-eval [nrepl-client-atom config] 88 | {:pre [config (:figwheel-build config)]} 89 | (tool-system/registration-map (create-figwheel-eval-tool nrepl-client-atom config))) 90 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/file_write/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.file-write.core 2 | "Core implementation for the file-write tool. 3 | This namespace contains the pure functionality without any MCP-specific code." 4 | (:require 5 | [clojure.java.io :as io] 6 | [clojure-mcp.tools.form-edit.pipeline :as pipeline] 7 | [clojure-mcp.utils.diff :as diff-utils] 8 | [clojure-mcp.linting :as linting] 9 | [rewrite-clj.zip :as z])) 10 | 11 | (defn is-clojure-file? 12 | "Check if a file is a Clojure-related file based on its extension. 13 | 14 | Parameters: 15 | - file-path: Path to the file to check 16 | 17 | Returns true if the file has a Clojure-related extension (.clj, .cljs, .cljc, .edn), 18 | false otherwise." 19 | [file-path] 20 | (let [lower-path (clojure.string/lower-case file-path)] 21 | (some #(clojure.string/ends-with? lower-path %) [".clj" ".cljs" ".cljc" ".edn"]))) 22 | 23 | (defn write-clojure-file 24 | "Write content to a Clojure file, with linting, formatting, and diffing. 25 | 26 | Parameters: 27 | - file-path: Validated path to the file to write 28 | - content: Content to write to the file 29 | 30 | Returns: 31 | - A map with :error, :type, :file-path, and :diff keys" 32 | [nrepl-client-atom file-path content] 33 | (let [file (io/file file-path) 34 | file-exists? (.exists file) 35 | old-content (if file-exists? (slurp file) "") 36 | 37 | ;; Create a context map for the pipeline 38 | initial-ctx {::pipeline/nrepl-client-atom nrepl-client-atom 39 | ::pipeline/file-path file-path 40 | ::pipeline/source old-content 41 | ::pipeline/new-source-code content 42 | ::pipeline/old-content old-content 43 | ::pipeline/file-exists? file-exists?} 44 | 45 | ;; Use thread-ctx to run the pipeline 46 | result (pipeline/thread-ctx 47 | initial-ctx 48 | pipeline/lint-repair-code 49 | (fn [ctx] 50 | (assoc ctx ::pipeline/output-source (::pipeline/new-source-code ctx))) 51 | pipeline/format-source ;; Format the content 52 | pipeline/generate-diff ;; Generate diff between old and new content 53 | pipeline/determine-file-type ;; Determine if creating or updating 54 | pipeline/save-file)] ;; Save the file and get offsets 55 | 56 | ;; Format the result for tool consumption 57 | (if (::pipeline/error result) 58 | {:error true 59 | :message (::pipeline/message result)} 60 | {:error false 61 | :type (::pipeline/type result) 62 | :file-path (::pipeline/file-path result) 63 | :diff (::pipeline/diff result)}))) 64 | 65 | (defn write-text-file 66 | "Write content to a non-Clojure text file, with diffing but no linting or formatting. 67 | 68 | Parameters: 69 | - file-path: Validated path to the file to write 70 | - content: Content to write to the file 71 | 72 | Returns: 73 | - A map with :error, :type, :file-path, and :diff keys" 74 | [file-path content] 75 | (try 76 | (let [file (io/file file-path) 77 | file-exists? (.exists file) 78 | old-content (if file-exists? (slurp file) "") 79 | ;; Only generate diff if the file already exists 80 | diff (if file-exists? 81 | (if (= old-content content) 82 | "" 83 | (diff-utils/generate-diff-via-shell old-content content 3)) 84 | "")] 85 | 86 | ;; Write the content directly 87 | (spit file content) 88 | 89 | {:error false 90 | :type (if file-exists? "update" "create") 91 | :file-path file-path 92 | :diff diff}) 93 | (catch Exception e 94 | {:error true 95 | :message (str "Error writing file: " (.getMessage e))}))) 96 | 97 | (defn write-file 98 | "Write content to a file, detecting the file type and using appropriate processing. 99 | For Clojure files (.clj, .cljs, .cljc, .edn), applies linting and formatting. 100 | For other file types, writes directly with no processing. 101 | 102 | Parameters: 103 | - file-path: Validated path to the file to write 104 | - content: Content to write to the file 105 | 106 | Returns: 107 | - A map with :error, :type, :file-path, and :diff keys" 108 | [nrepl-client-atom file-path content] 109 | (if (is-clojure-file? file-path) 110 | (write-clojure-file nrepl-client-atom file-path content) 111 | (write-text-file file-path content))) 112 | -------------------------------------------------------------------------------- /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.data.json :as json] 9 | [clojure.string :as string])) 10 | 11 | ;; Factory function to create the tool configuration 12 | (defn create-glob-files-tool 13 | "Creates the glob-files tool configuration. 14 | 15 | Parameters: 16 | - nrepl-client-atom: Atom containing the nREPL client" 17 | [nrepl-client-atom] 18 | {:tool-type :glob-files 19 | :nrepl-client-atom nrepl-client-atom}) 20 | 21 | ;; Implement the required multimethods for the glob-files tool 22 | (defmethod tool-system/tool-name :glob-files [_] 23 | "glob_files") 24 | 25 | (defmethod tool-system/tool-description :glob-files [_] 26 | "Fast file pattern matching tool that works with any codebase size. 27 | - Supports glob patterns like \"**/*.clj\" or \"src/**/*.cljs\". 28 | - Returns matching file paths sorted by modification time (most recent first). 29 | - Use this tool when you need to find files by name patterns. 30 | - When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the `dispatch_agent` tool instead") 31 | 32 | (defmethod tool-system/tool-schema :glob-files [_] 33 | {:type :object 34 | :properties {:path {:type :string 35 | :description "Root directory to start the search from (defaults to current working directory)"} 36 | :pattern {:type :string 37 | :description "Glob pattern (e.g. \"**/*.clj\", \"src/**/*.tsx\")"} 38 | :max_results {:type :integer 39 | :description "Maximum number of results to return (default: 1000)"}} 40 | :required [:pattern]}) 41 | 42 | (defmethod tool-system/validate-inputs :glob-files [{:keys [nrepl-client-atom]} inputs] 43 | (let [{:keys [path pattern max_results]} inputs 44 | nrepl-client-map @nrepl-client-atom ; Dereference atom 45 | effective-path (or path (config/get-nrepl-user-dir nrepl-client-map))] 46 | 47 | (when-not effective-path 48 | (throw (ex-info "No path provided and no nrepl-user-dir available" {:inputs inputs}))) 49 | 50 | (when-not pattern 51 | (throw (ex-info "Missing required parameter: pattern" {:inputs inputs}))) 52 | 53 | ;; Pass the dereferenced map to validate-path-with-client 54 | (let [validated-path (valid-paths/validate-path-with-client effective-path nrepl-client-map)] 55 | (cond-> {:path validated-path 56 | :pattern pattern} 57 | max_results (assoc :max-results max_results))))) 58 | 59 | (defmethod tool-system/execute-tool :glob-files [_ inputs] 60 | (let [{:keys [path pattern max-results]} inputs] 61 | (core/glob-files path pattern :max-results (or max-results 1000)))) 62 | 63 | (defmethod tool-system/format-results :glob-files [_ result] 64 | (if (:error result) 65 | ;; If there's an error, return it with error flag true 66 | {:result [(:error result)] 67 | :error true} 68 | ;; Format the results as a plain text list of filenames 69 | (let [{:keys [filenames truncated]} result 70 | output (cond 71 | (empty? filenames) "No files found" 72 | 73 | :else (str (string/join "\n" filenames) 74 | (when truncated 75 | "\n(Results are truncated. Consider using a more specific path or pattern.)")))] 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 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/grep/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.grep.tool 2 | "Implementation of the grep tool using the tool-system multimethod approach." 3 | (:require 4 | [clojure-mcp.tool-system :as tool-system] 5 | [clojure-mcp.tools.grep.core :as core] 6 | [clojure-mcp.utils.valid-paths :as valid-paths] 7 | [clojure-mcp.config :as config] ; Added config require 8 | [clojure.data.json :as json])) 9 | 10 | ;; Factory function to create the tool configuration 11 | (defn create-grep-tool 12 | "Creates the grep tool configuration. 13 | 14 | Parameters: 15 | - nrepl-client-atom: Atom containing the nREPL client" 16 | [nrepl-client-atom] 17 | {:tool-type :grep 18 | :nrepl-client-atom nrepl-client-atom}) 19 | 20 | ;; Implement the required multimethods for the grep tool 21 | (defmethod tool-system/tool-name :grep [_] 22 | "fs_grep") 23 | 24 | (defmethod tool-system/tool-description :grep [_] 25 | "Fast content search tool that works with any codebase size. 26 | - Searches file contents using regular expressions. 27 | - Supports full regex syntax (eg. \"log.*Error\", \"function\\s+\\w+\", etc.). 28 | - Filter files by pattern with the include parameter (eg. \"*.js\", \"*.{ts,tsx}\"). 29 | - Returns matching file paths sorted by modification time. 30 | - Use this tool when you need to find files containing specific patterns. 31 | - When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the `dispatch_agent` tool instead") 32 | 33 | (defmethod tool-system/tool-schema :grep [_] 34 | {:type :object 35 | :properties {:path {:type :string 36 | :description "The directory to search in. Defaults to the current working directory."} 37 | :pattern {:type :string 38 | :description "The regular expression pattern to search for in file contents"} 39 | :include {:type :string 40 | :description "File pattern to include in the search (e.g. \"*.clj\", \"*.{clj,cljs}\")"} 41 | :max_results {:type :integer 42 | :description "Maximum number of results to return (default: 1000)"}} 43 | :required [:pattern]}) 44 | 45 | (defmethod tool-system/validate-inputs :grep [{:keys [nrepl-client-atom]} inputs] 46 | (let [{:keys [path pattern include max_results]} inputs 47 | nrepl-client-map @nrepl-client-atom ; Dereference atom 48 | effective-path (or path (config/get-nrepl-user-dir nrepl-client-map))] 49 | (when-not effective-path 50 | (throw (ex-info "Missing required parameter: path" {:inputs inputs}))) 51 | 52 | (when-not pattern 53 | (throw (ex-info "Missing required parameter: pattern" {:inputs inputs}))) 54 | 55 | ;; Pass the dereferenced map to validate-path-with-client 56 | (let [validated-path (valid-paths/validate-path-with-client effective-path nrepl-client-map)] 57 | (cond-> {:path validated-path 58 | :pattern pattern} 59 | include (assoc :include include) 60 | max_results (assoc :max-results max_results))))) 61 | 62 | (defmethod tool-system/execute-tool :grep [_ inputs] 63 | (let [{:keys [path pattern include max-results]} inputs] 64 | (core/grep-files path pattern 65 | :include include 66 | :max-results (or max-results 1000)))) 67 | 68 | (defmethod tool-system/format-results :grep [_ result] 69 | (if (:error result) 70 | ;; If there's an error, return it with error flag true 71 | {:result [(:error result)] 72 | :error true} 73 | ;; Otherwise, format the results in a human-readable way 74 | (let [{:keys [filenames numFiles truncated]} result 75 | output (cond 76 | (nil? filenames) 77 | "No files found" 78 | 79 | (zero? numFiles) 80 | "No files found" 81 | 82 | :else 83 | (let [base-message (str "Found " numFiles " file" (when-not (= numFiles 1) "s") "\n" 84 | (clojure.string/join "\n" filenames))] 85 | (if truncated 86 | (str base-message "\n(Results are truncated. Consider using a more specific path or pattern.)") 87 | base-message)))] 88 | {:result [output] 89 | :error false}))) 90 | 91 | ;; Backward compatibility function that returns the registration map 92 | (defn grep-tool 93 | "Returns the registration map for the grep tool. 94 | 95 | Parameters: 96 | - nrepl-client-atom: Atom containing the nREPL client" 97 | [nrepl-client-atom] 98 | (tool-system/registration-map (create-grep-tool nrepl-client-atom))) 99 | -------------------------------------------------------------------------------- /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 | ;; Delegate to core implementation 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 | 63 | (comment 64 | ;; === Examples of using the project inspection tool === 65 | 66 | ;; Setup for REPL-based testing 67 | (def client-atom (atom (clojure-mcp.nrepl/create {:port 7888}))) 68 | (clojure-mcp.nrepl/start-polling @client-atom) 69 | 70 | ;; Create a tool instance 71 | (def inspect-tool (create-project-inspection-tool client-atom)) 72 | 73 | ;; Test the individual multimethod steps 74 | (def result (tool-system/execute-tool inspect-tool {})) 75 | (def formatted (tool-system/format-results inspect-tool result)) 76 | 77 | ;; Generate the full registration map 78 | (def reg-map (tool-system/registration-map inspect-tool)) 79 | 80 | ;; Test running the tool-fn directly 81 | (def tool-fn (:tool-fn reg-map)) 82 | (tool-fn nil {} (fn [result error] (println "Result:" result "Error:" error))) 83 | 84 | ;; Make a simpler test function that works like tool-fn 85 | (defn test-tool [] 86 | (let [prom (promise)] 87 | (tool-fn nil {} 88 | (fn [result error] 89 | (deliver prom (if error {:error error} {:result result})))) 90 | @prom)) 91 | 92 | ;; Test inspection 93 | (test-tool) 94 | 95 | ;; Clean up 96 | (clojure-mcp.nrepl/stop-polling @client-atom) 97 | ) 98 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/read_file/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.read-file.core 2 | "Core implementation for the read-file tool. 3 | This namespace contains the pure functionality without any MCP-specific code." 4 | (:require 5 | [clojure.java.io :as io] 6 | [clojure.string :as str])) 7 | 8 | (defn read-file 9 | "Reads a file from the filesystem, with optional line offset and limit. 10 | 11 | Parameters: 12 | - path: The validated and normalized path to the file 13 | - offset: Line number to start reading from (0-indexed, default 0) 14 | - limit: Maximum number of lines to read (default 2000) 15 | - max-line-length: Maximum length per line before truncation (default 1000) 16 | 17 | Returns a map with: 18 | - :content - The file contents as a string 19 | - :path - The absolute path to the file 20 | - :truncated? - Whether the file was truncated due to line limit 21 | - :truncated-by - The reason for truncation (e.g., 'max-lines') 22 | - :size - The file size in bytes 23 | - :line-count - The number of lines returned 24 | - :offset - The line offset used 25 | - :max-line-length - The max line length used 26 | - :line-lengths-truncated? - Whether any lines were truncated in length 27 | 28 | If the file doesn't exist or cannot be read, returns a map with :error key" 29 | [path offset limit & {:keys [max-line-length] :or {max-line-length 1000}}] 30 | (let [file (io/file path)] 31 | (if (.exists file) 32 | (if (.isFile file) 33 | (try 34 | (if (and (nil? limit) (zero? offset) (nil? max-line-length)) 35 | ;; Simple case - just read the whole file 36 | {:content (slurp file) 37 | :path (.getAbsolutePath file) 38 | :truncated? false} 39 | ;; Complex case with limits 40 | (let [size (.length file) 41 | lines (with-open [rdr (io/reader file)] 42 | (doall 43 | (cond->> (drop offset (line-seq rdr)) 44 | limit (take (inc limit)) 45 | true (map 46 | (fn [line] 47 | (if (and max-line-length (> (count line) max-line-length)) 48 | (str (subs line 0 max-line-length) "...") 49 | line)))))) 50 | truncated-by-lines? (and limit (> (count lines) limit)) 51 | content-lines (if truncated-by-lines? (take limit lines) lines) 52 | content (str/join "\n" content-lines)] 53 | {:content content 54 | :path (.getAbsolutePath file) 55 | :truncated? truncated-by-lines? 56 | :truncated-by (when truncated-by-lines? "max-lines") 57 | :size size 58 | :line-count (count content-lines) 59 | :offset offset 60 | :max-line-length max-line-length 61 | :line-lengths-truncated? (and max-line-length 62 | (some #(.contains % "...") lines))})) 63 | (catch Exception e 64 | {:error (str "Error reading file: " (.getMessage e))})) 65 | {:error (str path " is not a file")}) 66 | {:error (str path " does not exist")}))) -------------------------------------------------------------------------------- /src/clojure_mcp/tools/scratch_pad/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.scratch-pad.core 2 | (:require 3 | [clojure.edn :as edn] 4 | [clojure.pprint :as pprint] 5 | [clojure-mcp.tools.scratch-pad.truncate :as truncate])) 6 | 7 | (defn dissoc-in-data [data path] 8 | (if (empty? path) 9 | data 10 | (if (= 1 (count path)) 11 | (dissoc data (first path)) 12 | (update-in data (butlast path) dissoc (last path))))) 13 | 14 | (defn inspect-data [data] 15 | (if (empty? data) 16 | "Empty scratch pad" 17 | (with-out-str (pprint/pprint data)))) 18 | 19 | (defn execute-set-path 20 | "Execute a set_path operation and return the result map." 21 | [current-data path value] 22 | (let [new-data (assoc-in current-data path value) 23 | stored-value (get-in new-data path)] 24 | {:data new-data 25 | :result {:stored-at path 26 | :value stored-value 27 | :pretty-value (with-out-str (pprint/pprint stored-value))}})) 28 | 29 | (defn execute-get-path 30 | "Execute a get_path operation and return the result map." 31 | [current-data path] 32 | (let [value (get-in current-data path)] 33 | {:result {:path path 34 | :value value 35 | :pretty-value (when (some? value) 36 | (with-out-str (pprint/pprint value))) 37 | :found (some? value)}})) 38 | 39 | (defn execute-delete-path 40 | "Execute a delete_path operation and return the result map." 41 | [current-data path] 42 | (let [new-data (dissoc-in-data current-data path)] 43 | {:data new-data 44 | :result {:removed-from path}})) 45 | 46 | (defn execute-inspect 47 | "Execute an inspect operation and return the result map." 48 | [current-data depth path] 49 | (let [data-to-view (if (and path (not (empty? path))) 50 | (get-in current-data path) 51 | current-data)] 52 | (if (nil? data-to-view) 53 | {:result {:tree (str "No data found at path " path)}} 54 | {:result {:tree (truncate/pprint-truncated data-to-view depth)}}))) 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/clojure_mcp/tools/think/tool.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.think.tool 2 | "Implementation of the think tool using the tool-system multimethod approach." 3 | (:require 4 | [clojure-mcp.tool-system :as tool-system])) 5 | 6 | (defmethod tool-system/tool-name :think [_] 7 | "think") 8 | 9 | (defmethod tool-system/tool-description :think [_] 10 | "Use the tool to think about something. It will not obtain new information or make any changes to the repository, but just log the thought. Use it when complex reasoning or brainstorming is needed. 11 | Common use cases: 12 | * when having difficulty finding/reading the files you need 13 | * when having difficulty writing out code 14 | * when having using the tools or the clojure tools 15 | * When exploring a repository and discovering the source of a bug, call this tool to brainstorm several unique ways of fixing the bug, and assess which change(s) are likely to be simplest and most effective 16 | * After receiving test results, use this tool to brainstorm ways to fix failing tests 17 | * When planning a complex refactoring, use this tool to outline different approaches and their tradeoffs 18 | * When designing a new feature, use this tool to think through architecture decisions and implementation details 19 | * When debugging a complex issue, use this tool to organize your thoughts and hypotheses 20 | The tool simply logs your thought process for better transparency and does not execute any code or make changes.") 21 | 22 | (defmethod tool-system/tool-schema :think [_] 23 | {:type "object" 24 | :properties {"thought" {:type "string" 25 | :description "The thought to log"}} 26 | :required ["thought"]}) 27 | 28 | (defmethod tool-system/validate-inputs :think [_ {:keys [thought] :as inputs}] 29 | (when-not thought 30 | (throw (ex-info "Missing required parameter: thought" {:inputs inputs}))) 31 | ;; Return the validated inputs 32 | inputs) 33 | 34 | (defmethod tool-system/execute-tool :think [_ {:keys [thought]}] 35 | {:result "Your thought has been logged." 36 | :error false}) 37 | 38 | (defmethod tool-system/format-results :think [_ result] 39 | (if (:error result) 40 | {:result [(:result result)] 41 | :error true} 42 | {:result [(:result result)] 43 | :error false})) 44 | 45 | (defn create-think-tool [nrepl-client-atom] 46 | {:tool-type :think 47 | :nrepl-client-atom nrepl-client-atom}) 48 | 49 | (defn think-tool 50 | "Returns the registration map for the think tool. 51 | 52 | Parameters: 53 | - nrepl-client-atom: Atom containing the nREPL client" 54 | [nrepl-client-atom] 55 | (tool-system/registration-map (create-think-tool nrepl-client-atom))) 56 | -------------------------------------------------------------------------------- /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 | [clojure-mcp.tools.form-edit.core :as form-edit] 8 | [rewrite-clj.zip :as z] 9 | [rewrite-clj.parser :as p] 10 | [rewrite-clj.node :as n] 11 | [cljfmt.core :as fmt] 12 | [clojure.string :as str] 13 | [clojure.java.io :as io])) 14 | 15 | ;; Re-export common utilities from form-edit.core 16 | (def format-source-string form-edit/format-source-string) 17 | (def load-file-content form-edit/load-file-content) 18 | (def save-file-content form-edit/save-file-content) 19 | (def zloc-offsets form-edit/zloc-offsets) 20 | 21 | (defn find-pattern-match 22 | "Finds a pattern match in Clojure source code. 23 | 24 | Arguments: 25 | - zloc: Source code zipper 26 | - pattern-str: Pattern to match with wildcards (? and *) 27 | 28 | Returns: 29 | - Map with :zloc pointing to the matched form or nil if not found" 30 | [zloc pattern-str] 31 | (let [pattern-sexpr (z/sexpr (z/of-string pattern-str)) 32 | source-str (z/root-string zloc)] 33 | (if-let [match-loc (match/find-match* pattern-sexpr zloc)] 34 | {:zloc match-loc} 35 | nil))) 36 | 37 | (defn edit-matched-form 38 | "Edits a form matched by a pattern. 39 | 40 | Arguments: 41 | - zloc: Source code zipper 42 | - pattern-str: Pattern that matches the target form 43 | - content-str: New content to replace/insert 44 | - edit-type: Operation to perform (:replace, :insert-before, :insert-after) 45 | 46 | Returns: 47 | - Map with :zloc pointing to the edited form, or nil if match not found" 48 | [zloc pattern-str content-str edit-type] 49 | (if-let [match-result (find-pattern-match zloc pattern-str)] 50 | (let [match-loc (:zloc match-result) 51 | content-node (p/parse-string-all content-str) 52 | updated-loc (case edit-type 53 | :replace 54 | (z/replace match-loc content-node) 55 | :insert_before 56 | (-> match-loc 57 | (z/insert-left (p/parse-string-all "\n\n")) 58 | z/left 59 | (z/insert-left content-node) 60 | z/left) ; Move to the newly inserted node 61 | 62 | :insert_after 63 | (-> match-loc 64 | (z/insert-right (p/parse-string-all "\n\n")) 65 | z/right 66 | (z/insert-right content-node) 67 | z/right))] ; Move to the newly inserted node 68 | {:zloc updated-loc}) 69 | nil)) 70 | -------------------------------------------------------------------------------- /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 | [clojure-mcp.utils.emacs-integration :as emacs] 9 | [clojure-mcp.tools.read-file.file-timestamps :as file-timestamps] 10 | [rewrite-clj.zip :as z] 11 | [rewrite-clj.parser :as p] 12 | [clojure-mcp.linting :as linting] 13 | [clojure-mcp.sexp.paren-utils :as paren-utils] 14 | [clojure.spec.alpha :as s] 15 | [clojure.string :as str] 16 | [clojure.java.io :as io])) 17 | 18 | ;; Additional custom spec for the pattern string 19 | (s/def ::pattern string?) 20 | 21 | (defn find-form 22 | "Finds a form using pattern matching. 23 | Requires ::zloc and ::pattern in the context. 24 | Updates ::zloc to point to the matched form or returns an error context if no match found." 25 | [ctx] 26 | (let [zloc (::form-edit-pipeline/zloc ctx) 27 | pattern (::pattern ctx) 28 | result (core/find-pattern-match zloc pattern)] 29 | (if (:zloc result) 30 | (assoc ctx ::form-edit-pipeline/zloc (:zloc result)) 31 | {::form-edit-pipeline/error true 32 | ::form-edit-pipeline/message (str "Could not find pattern match for: " pattern 33 | " in file " (::form-edit-pipeline/file-path ctx))}))) 34 | 35 | (defn check-for-duplicate-matches 36 | "Checks if there are multiple matches for the pattern. 37 | Requires ::zloc and ::pattern in the context. 38 | Returns an error context if multiple matches are found." 39 | [ctx] 40 | (let [zloc (::form-edit-pipeline/zloc ctx) 41 | pattern (::pattern ctx) 42 | ;; Start from the next position after the current match 43 | next-zloc (z/next zloc) 44 | ;; Use the same function that was used for the first match 45 | second-match (core/find-pattern-match next-zloc pattern)] 46 | (if (:zloc second-match) 47 | ;; Found a second match - this is an error 48 | {::form-edit-pipeline/error true 49 | ::form-edit-pipeline/message 50 | (str "Multiple matches found for pattern: " pattern 51 | "\nFirst match: " (z/string zloc) 52 | "\nSecond match: " (z/string (:zloc second-match)) 53 | "\nPlease use a more specific pattern to ensure a unique match.")} 54 | ;; No second match found - this is good 55 | ctx))) 56 | 57 | (defn edit-form 58 | "Edits the form according to the specified edit type. 59 | Requires ::zloc, ::pattern, ::new-source-code, and ::edit-type in the context. 60 | Updates ::zloc with the edited zipper." 61 | [ctx] 62 | (let [zloc (::form-edit-pipeline/zloc ctx) 63 | pattern (::pattern ctx) 64 | content (::form-edit-pipeline/new-source-code ctx) 65 | edit-type (::form-edit-pipeline/edit-type ctx) 66 | result (core/edit-matched-form zloc pattern content edit-type)] 67 | (if (:zloc result) 68 | (assoc ctx ::form-edit-pipeline/zloc (:zloc result)) 69 | {::form-edit-pipeline/error true 70 | ::form-edit-pipeline/message (str "Failed to " (name edit-type) " form matching pattern: " pattern)}))) 71 | 72 | ;; Define the main pipeline for pattern-based Clojure editing 73 | (defn pattern-edit-pipeline 74 | "Pipeline for handling pattern-based Clojure code editing operations. 75 | 76 | Arguments: 77 | - file-path: Path to the file to edit 78 | - pattern: Pattern string to match (with ? and * wildcards) 79 | - content-str: New content to insert 80 | - edit-type: Type of edit (:replace, :insert_before, :insert_after) 81 | - nrepl-client-atom: Atom containing the nREPL client (optional) 82 | - config: Optional tool configuration map 83 | 84 | Returns a context map with the result of the operation" 85 | [file-path pattern content-str edit-type {:keys [nrepl-client-atom] :as config}] 86 | (let [ctx {::form-edit-pipeline/file-path file-path 87 | ::pattern pattern 88 | ::form-edit-pipeline/new-source-code content-str 89 | ::form-edit-pipeline/edit-type edit-type 90 | ::form-edit-pipeline/nrepl-client-atom nrepl-client-atom 91 | ::form-edit-pipeline/config config}] 92 | (form-edit-pipeline/thread-ctx 93 | ctx 94 | form-edit-pipeline/lint-repair-code 95 | form-edit-pipeline/load-source 96 | form-edit-pipeline/check-file-modified 97 | form-edit-pipeline/parse-source 98 | find-form 99 | check-for-duplicate-matches 100 | edit-form 101 | form-edit-pipeline/capture-edit-offsets 102 | form-edit-pipeline/zloc->output-source 103 | form-edit-pipeline/format-source 104 | form-edit-pipeline/determine-file-type 105 | form-edit-pipeline/generate-diff 106 | form-edit-pipeline/save-file 107 | form-edit-pipeline/update-file-timestamp 108 | form-edit-pipeline/highlight-form))) 109 | -------------------------------------------------------------------------------- /src/clojure_mcp/utils/diff.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.utils.diff 2 | (:require [clojure.string :as str] 3 | [clojure.java.shell :as shell])) 4 | 5 | (defn diff-has-tmp-file-headers? 6 | [lines] 7 | (and (>= (count lines) 2) 8 | (str/starts-with? (first lines) "---") 9 | (str/includes? (first lines) "clj-diff-") 10 | (str/starts-with? (nth lines 1) "+++") 11 | (str/includes? (nth lines 1) "clj-diff-"))) 12 | 13 | (defn truncate-diff-output 14 | [diff-output] 15 | (if (str/blank? diff-output) 16 | diff-output 17 | (let [lines (str/split-lines diff-output)] 18 | (if (diff-has-tmp-file-headers? lines) 19 | (str/join "\n" (drop 2 lines)) 20 | diff-output)))) 21 | 22 | (defn generate-diff-via-shell 23 | "Generates a unified diff between two strings using the external diff command. 24 | Truncates the first two lines which contain temporary file paths. 25 | Requires two strings (file1-content, file2-content) and context-lines count." 26 | [file1-content file2-content context-lines] 27 | (let [temp-prefix "clj-diff-" 28 | temp-suffix ".tmp" 29 | file1 (java.io.File/createTempFile temp-prefix temp-suffix) 30 | file2 (java.io.File/createTempFile temp-prefix temp-suffix)] 31 | (try 32 | (spit file1 file1-content) 33 | (spit file2 file2-content) 34 | 35 | (let [file1-path (.getAbsolutePath file1) 36 | file2-path (.getAbsolutePath file2) 37 | ;; Use -U for unified diff format with specified context 38 | diff-result (shell/sh "diff" "-U" (str context-lines) file1-path file2-path) 39 | diff-output (if (= 0 (:exit diff-result)) 40 | ;; Files are identical or diff succeeded 41 | (:out diff-result) 42 | ;; Diff command indicated differences (exit code 1) or an error (other non-zero) 43 | ;; Exit code 1 is normal for differences, so we still return the output. 44 | ;; Handle other errors specifically if needed. 45 | (if (= 1 (:exit diff-result)) 46 | (:out diff-result) 47 | (throw (ex-info "Diff command failed" {:result diff-result}))))] 48 | ;; Truncate the first two lines (temporary file paths) 49 | (truncate-diff-output diff-output)) 50 | (finally 51 | ;; Ensure temporary files are deleted 52 | (.delete file1) 53 | (.delete file2))))) 54 | -------------------------------------------------------------------------------- /test/clojure_mcp/other_tools/create_directory/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.create-directory.core-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-mcp.other-tools.create-directory.core :as core] 4 | [clojure.java.io :as io] 5 | [clojure.string :as str])) 6 | 7 | ;; Dynamic variables for test directories and files 8 | (def ^:dynamic *test-dir* nil) 9 | (def ^:dynamic *nested-dir* nil) 10 | (def ^:dynamic *conflict-file* nil) 11 | 12 | ;; Test fixture to set up a test environment 13 | (defn create-test-environment-fixture [f] 14 | (let [tmp-dir (io/file (System/getProperty "java.io.tmpdir") 15 | (str "create-dir-test-" (System/currentTimeMillis))) 16 | nested-dir (io/file tmp-dir "nested/path") 17 | conflict-file (io/file tmp-dir "file-not-dir")] 18 | 19 | ;; Create root test directory 20 | (.mkdirs tmp-dir) 21 | 22 | ;; Create a file that will conflict with directory path testing 23 | (spit conflict-file "test content") 24 | 25 | ;; Bind dynamic vars for the test 26 | (binding [*test-dir* tmp-dir 27 | *nested-dir* nested-dir 28 | *conflict-file* conflict-file] 29 | 30 | ;; Run the test 31 | (try 32 | (f) 33 | (finally 34 | ;; Cleanup 35 | (when (.exists conflict-file) 36 | (.delete conflict-file)) 37 | (when (.exists nested-dir) 38 | (.delete nested-dir)) 39 | (when (.exists (io/file tmp-dir "nested")) 40 | (.delete (io/file tmp-dir "nested"))) 41 | (when (.exists tmp-dir) 42 | (.delete tmp-dir))))))) 43 | 44 | ;; Use the fixture for all tests 45 | (use-fixtures :each create-test-environment-fixture) 46 | 47 | (deftest create-new-directory-test 48 | (testing "Creating a new directory" 49 | (let [dir-path (.getAbsolutePath *nested-dir*) 50 | result (core/create-directory dir-path)] 51 | (is (:success result) "Directory creation should succeed") 52 | (is (:created result) "Should indicate directory was created") 53 | (is (not (:exists result)) "Should indicate directory did not previously exist") 54 | (is (.exists *nested-dir*) "Directory should now exist on disk") 55 | (is (.isDirectory *nested-dir*) "Should be a directory, not a file")))) 56 | 57 | (deftest existing-directory-test 58 | (testing "Operation succeeds silently when directory already exists" 59 | ;; First, create the directory 60 | (.mkdirs *nested-dir*) 61 | (is (.exists *nested-dir*) "Setup check: directory should exist") 62 | 63 | ;; Then test calling create-directory on the existing directory 64 | (let [dir-path (.getAbsolutePath *nested-dir*) 65 | result (core/create-directory dir-path)] 66 | (is (:success result) "Operation should succeed") 67 | (is (not (:created result)) "Should indicate directory was not newly created") 68 | (is (:exists result) "Should indicate directory already existed") 69 | (is (.exists *nested-dir*) "Directory should still exist on disk")))) 70 | 71 | (deftest file-conflict-test 72 | (testing "Operation fails when path exists as a file" 73 | (let [file-path (.getAbsolutePath *conflict-file*) 74 | result (core/create-directory file-path)] 75 | (is (not (:success result)) "Operation should fail") 76 | (is (:error result) "Should have an error message") 77 | (is (str/includes? (:error result) "is a file") 78 | "Error should mention path exists as a file")))) 79 | -------------------------------------------------------------------------------- /test/clojure_mcp/other_tools/create_directory/tool_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.create-directory.tool-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-mcp.other-tools.create-directory.tool :as tool] 4 | [clojure-mcp.tool-system :as tool-system] 5 | [clojure-mcp.config :as config] ; Added config require 6 | [clojure.java.io :as io] 7 | [clojure.string :as str])) 8 | 9 | ;; Create a mock nREPL client atom for testing 10 | (def mock-client-atom (atom {})) 11 | (config/set-config! mock-client-atom :nrepl-user-dir (System/getProperty "user.dir")) 12 | (config/set-config! mock-client-atom :allowed-directories [(System/getProperty "user.dir")]) 13 | 14 | ;; Test the tool-name multimethod 15 | (deftest tool-name-test 16 | (testing "Tool name is correct" 17 | (let [tool-config (tool/create-directory-tool mock-client-atom)] 18 | (is (= "create_directory" (tool-system/tool-name tool-config)))))) 19 | 20 | ;; Test the tool-description multimethod 21 | (deftest tool-description-test 22 | (testing "Tool description is not empty" 23 | (let [tool-config (tool/create-directory-tool mock-client-atom) 24 | description (tool-system/tool-description tool-config)] 25 | (is (string? description)) 26 | (is (not (empty? description)))))) 27 | 28 | ;; Test the tool-schema multimethod 29 | (deftest tool-schema-test 30 | (testing "Schema includes required properties" 31 | (let [tool-config (tool/create-directory-tool mock-client-atom) 32 | schema (tool-system/tool-schema tool-config)] 33 | (is (= :object (:type schema))) 34 | (is (contains? (:properties schema) :path)) 35 | (is (= [:path] (:required schema)))))) 36 | 37 | ;; Test the validate-inputs multimethod 38 | (deftest validate-inputs-test 39 | (testing "Validation rejects missing path parameter" 40 | (let [tool-config (tool/create-directory-tool mock-client-atom) 41 | inputs {}] 42 | (is (thrown? Exception (tool-system/validate-inputs tool-config inputs)))))) 43 | 44 | ;; Test the format-results multimethod 45 | (deftest format-results-test 46 | (testing "Format successful results for new directory" 47 | (let [tool-config (tool/create-directory-tool mock-client-atom) 48 | result {:success true 49 | :path "/test-dir" 50 | :exists false 51 | :created true} 52 | formatted (tool-system/format-results tool-config result)] 53 | (is (not (:error formatted))) 54 | (is (vector? (:result formatted))) 55 | (is (= 1 (count (:result formatted)))) 56 | (is (str/includes? (first (:result formatted)) "Created directory")))) 57 | 58 | (testing "Format successful results for existing directory" 59 | (let [tool-config (tool/create-directory-tool mock-client-atom) 60 | result {:success true 61 | :path "/test-dir" 62 | :exists true 63 | :created false} 64 | formatted (tool-system/format-results tool-config result)] 65 | (is (not (:error formatted))) 66 | (is (vector? (:result formatted))) 67 | (is (= 1 (count (:result formatted)))) 68 | (is (str/includes? (first (:result formatted)) "already exists")))) 69 | 70 | (testing "Format error results" 71 | (let [tool-config (tool/create-directory-tool mock-client-atom) 72 | result {:success false 73 | :path "/test-dir" 74 | :error "Test error message"} 75 | formatted (tool-system/format-results tool-config result)] 76 | (is (:error formatted)) 77 | (is (vector? (:result formatted))) 78 | (is (= 1 (count (:result formatted)))) 79 | (is (= "Test error message" (first (:result formatted))))))) 80 | 81 | ;; Test registration map creation 82 | (deftest registration-map-test 83 | (testing "Creates a valid registration map" 84 | (let [tool-config (tool/create-directory-tool mock-client-atom) 85 | reg-map (tool-system/registration-map tool-config)] 86 | (is (= "create_directory" (:name reg-map))) 87 | (is (string? (:description reg-map))) 88 | (is (map? (:schema reg-map))) 89 | (is (fn? (:tool-fn reg-map)))))) 90 | 91 | ;; Compatibility function test 92 | (deftest compatibility-function-test 93 | (testing "create-directory-tool-registration function returns a valid registration map" 94 | (let [reg-map (tool/create-directory-tool-registration mock-client-atom)] 95 | (is (= "create_directory" (:name reg-map))) 96 | (is (string? (:description reg-map))) 97 | (is (map? (:schema reg-map))) 98 | (is (fn? (:tool-fn reg-map)))))) -------------------------------------------------------------------------------- /test/clojure_mcp/other_tools/list_directory/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.list-directory.core-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-mcp.other-tools.list-directory.core :as sut] 4 | [clojure.java.io :as io])) 5 | 6 | (deftest list-directory-test 7 | (testing "list-directory lists files and directories correctly" 8 | (let [temp-dir (io/file (System/getProperty "java.io.tmpdir") "list-directory-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 temp-dir "file2.clj")] 16 | 17 | (try 18 | (.mkdirs dir1) 19 | (.mkdirs dir2) 20 | (spit file1 "content1") 21 | (spit file2 "content2") 22 | 23 | (testing "valid directory" 24 | (let [result (sut/list-directory (.getAbsolutePath temp-dir))] 25 | (is (map? result)) 26 | (is (not (contains? result :error))) 27 | (is (contains? result :files)) 28 | (is (contains? result :directories)) 29 | (is (contains? result :full-path)) 30 | (is (= (.getAbsolutePath temp-dir) (:full-path result))) 31 | (is (= 2 (count (:files result)))) 32 | (is (= 2 (count (:directories result)))) 33 | (is (some #(= "file1.txt" %) (:files result))) 34 | (is (some #(= "file2.clj" %) (:files result))) 35 | (is (some #(= "dir1" %) (:directories result))) 36 | (is (some #(= "dir2" %) (:directories result))))) 37 | 38 | (finally 39 | ;; Clean up 40 | (io/delete-file file1 true) 41 | (io/delete-file file2 true) 42 | (io/delete-file dir1 true) 43 | (io/delete-file dir2 true) 44 | (io/delete-file temp-dir true)))))) 45 | 46 | (testing "list-directory with non-existent path" 47 | (let [result (sut/list-directory "/path/that/does/not/exist")] 48 | (is (map? result)) 49 | (is (contains? result :error)) 50 | (is (.contains (:error result) "does not exist")))) 51 | 52 | (testing "list-directory with file path instead of directory" 53 | (let [temp-file (io/file (System/getProperty "java.io.tmpdir") "test-file.txt")] 54 | (try 55 | (spit temp-file "test content") 56 | (let [result (sut/list-directory (.getAbsolutePath temp-file))] 57 | (is (map? result)) 58 | (is (contains? result :error)) 59 | (is (.contains (:error result) "is not a directory"))) 60 | (finally 61 | (io/delete-file temp-file true)))))) 62 | -------------------------------------------------------------------------------- /test/clojure_mcp/other_tools/move_file/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.move-file.core-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-mcp.other-tools.move-file.core :as core] 4 | [clojure.java.io :as io] 5 | [clojure.string :as str])) 6 | 7 | ;; Dynamic variables for test directories and files 8 | (def ^:dynamic *test-dir* nil) 9 | (def ^:dynamic *test-source* nil) 10 | (def ^:dynamic *test-dest* nil) 11 | 12 | ;; Test fixture to create and cleanup test files/directories 13 | (defn create-test-files-fixture [f] 14 | (let [tmp-dir (io/file (System/getProperty "java.io.tmpdir") "move-file-test") 15 | source-file (io/file tmp-dir "source.txt") 16 | dest-file (io/file tmp-dir "dest.txt")] 17 | 18 | ;; Create test directory 19 | (.mkdirs tmp-dir) 20 | 21 | ;; Create test source file 22 | (spit source-file "Test content for move operation") 23 | 24 | ;; Bind dynamic vars for the test 25 | (binding [*test-dir* tmp-dir 26 | *test-source* (.getAbsolutePath source-file) 27 | *test-dest* (.getAbsolutePath dest-file)] 28 | 29 | ;; Run the test 30 | (try 31 | (f) 32 | (finally 33 | ;; Cleanup 34 | (when (.exists dest-file) 35 | (.delete dest-file)) 36 | (when (.exists source-file) 37 | (.delete source-file)) 38 | (when (.exists tmp-dir) 39 | (.delete tmp-dir))))))) 40 | 41 | ;; Use the fixture for all tests 42 | (use-fixtures :each create-test-files-fixture) 43 | 44 | (deftest move-file-success-test 45 | (testing "Successfully moving a file" 46 | (let [result (core/move-file *test-source* *test-dest*)] 47 | (is (:success result) "Move operation should succeed") 48 | (is (= "file" (:type result)) "Should identify as file type") 49 | (is (not (.exists (io/file *test-source*))) "Source file should no longer exist") 50 | (is (.exists (io/file *test-dest*)) "Destination file should exist") 51 | (is (= "Test content for move operation" 52 | (slurp *test-dest*)) "Content should be preserved")))) 53 | 54 | (deftest move-file-failure-tests 55 | (testing "Failure when source doesn't exist" 56 | (let [non-existent-source (str *test-dir* "/non-existent.txt") 57 | result (core/move-file non-existent-source *test-dest*)] 58 | (is (not (:success result)) "Move should fail") 59 | (is (:error result) "Error message should be provided") 60 | (is (str/includes? (:error result) "does not exist") 61 | "Error should mention non-existent source"))) 62 | 63 | (testing "Failure when destination already exists" 64 | ;; Create the destination file 65 | (spit *test-dest* "Existing content") 66 | (let [result (core/move-file *test-source* *test-dest*)] 67 | (is (not (:success result)) "Move should fail") 68 | (is (:error result) "Error message should be provided") 69 | (is (str/includes? (:error result) "already exists") 70 | "Error should mention existing destination") 71 | (is (.exists (io/file *test-source*)) "Source file should still exist") 72 | (is (= "Test content for move operation" 73 | (slurp *test-source*)) "Source content should be preserved") 74 | (is (= "Existing content" 75 | (slurp *test-dest*)) "Destination content should be unchanged")))) -------------------------------------------------------------------------------- /test/clojure_mcp/other_tools/move_file/tool_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.move-file.tool-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-mcp.other-tools.move-file.tool :as tool] 4 | [clojure-mcp.tool-system :as tool-system] 5 | [clojure-mcp.config :as config] ; Added config require 6 | [clojure.java.io :as io] 7 | [clojure.string :as str])) 8 | 9 | ;; Create a mock nREPL client atom for testing 10 | (def mock-client-atom (atom {})) 11 | (config/set-config! mock-client-atom :nrepl-user-dir (System/getProperty "user.dir")) 12 | (config/set-config! mock-client-atom :allowed-directories [(System/getProperty "user.dir")]) 13 | 14 | ;; Test the tool-name multimethod 15 | (deftest tool-name-test 16 | (testing "Tool name is correct" 17 | (let [tool-config (tool/create-move-file-tool mock-client-atom)] 18 | (is (= "move_file" (tool-system/tool-name tool-config)))))) 19 | 20 | ;; Test the tool-description multimethod 21 | (deftest tool-description-test 22 | (testing "Tool description is not empty" 23 | (let [tool-config (tool/create-move-file-tool mock-client-atom) 24 | description (tool-system/tool-description tool-config)] 25 | (is (string? description)) 26 | (is (not (empty? description)))))) 27 | 28 | ;; Test the tool-schema multimethod 29 | (deftest tool-schema-test 30 | (testing "Schema includes required properties" 31 | (let [tool-config (tool/create-move-file-tool mock-client-atom) 32 | schema (tool-system/tool-schema tool-config)] 33 | (is (= :object (:type schema))) 34 | (is (contains? (:properties schema) :source)) 35 | (is (contains? (:properties schema) :destination)) 36 | (is (= [:source :destination] (:required schema)))))) 37 | 38 | ;; Test the validate-inputs multimethod 39 | (deftest validate-inputs-test 40 | (testing "Validation rejects missing source parameter" 41 | (let [tool-config (tool/create-move-file-tool mock-client-atom) 42 | inputs {:destination "/tmp/dest.txt"}] 43 | (is (thrown? Exception (tool-system/validate-inputs tool-config inputs))))) 44 | 45 | (testing "Validation rejects missing destination parameter" 46 | (let [tool-config (tool/create-move-file-tool mock-client-atom) 47 | inputs {:source "/tmp/source.txt"}] 48 | (is (thrown? Exception (tool-system/validate-inputs tool-config inputs)))))) 49 | 50 | ;; Test the format-results multimethod 51 | (deftest format-results-test 52 | (testing "Format successful results" 53 | (let [tool-config (tool/create-move-file-tool mock-client-atom) 54 | result {:success true 55 | :source "/source.txt" 56 | :destination "/dest.txt" 57 | :type "file"} 58 | formatted (tool-system/format-results tool-config result)] 59 | (is (not (:error formatted))) 60 | (is (vector? (:result formatted))) 61 | (is (= 1 (count (:result formatted)))) 62 | (is (str/includes? (first (:result formatted)) "Successfully moved")))) 63 | 64 | (testing "Format error results" 65 | (let [tool-config (tool/create-move-file-tool mock-client-atom) 66 | result {:success false 67 | :source "/source.txt" 68 | :destination "/dest.txt" 69 | :error "Test error message"} 70 | formatted (tool-system/format-results tool-config result)] 71 | (is (:error formatted)) 72 | (is (vector? (:result formatted))) 73 | (is (= 1 (count (:result formatted)))) 74 | (is (= "Test error message" (first (:result formatted))))))) 75 | 76 | ;; Test registration map creation 77 | (deftest registration-map-test 78 | (testing "Creates a valid registration map" 79 | (let [tool-config (tool/create-move-file-tool mock-client-atom) 80 | reg-map (tool-system/registration-map tool-config)] 81 | (is (= "move_file" (:name reg-map))) 82 | (is (string? (:description reg-map))) 83 | (is (map? (:schema reg-map))) 84 | (is (fn? (:tool-fn reg-map)))))) 85 | 86 | ;; Compatibility function test 87 | (deftest compatibility-function-test 88 | (testing "move-file-tool function returns a valid registration map" 89 | (let [reg-map (tool/move-file-tool mock-client-atom)] 90 | (is (= "move_file" (:name reg-map))) 91 | (is (string? (:description reg-map))) 92 | (is (map? (:schema reg-map))) 93 | (is (fn? (:tool-fn reg-map)))))) -------------------------------------------------------------------------------- /test/clojure_mcp/other_tools/namespace/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.namespace.core-test 2 | (:require 3 | [clojure.test :refer [deftest is testing use-fixtures]] 4 | [clojure-mcp.other-tools.namespace.core :as sut] 5 | [clojure-mcp.nrepl :as nrepl] 6 | [nrepl.server :as nrepl-server] 7 | [clojure.string :as str])) 8 | 9 | ;; Use the common test utils for nREPL server setup 10 | (defonce ^:dynamic *nrepl-server* nil) 11 | (defonce ^:dynamic *nrepl-client-atom* nil) 12 | 13 | (defn test-nrepl-fixture [f] 14 | (let [server (nrepl.server/start-server :port 0) 15 | port (:port server) 16 | client (clojure-mcp.nrepl/create {:port port}) 17 | client-atom (atom client)] 18 | (clojure-mcp.nrepl/start-polling client) 19 | ;; Load some namespaces for testing 20 | (clojure-mcp.nrepl/eval-code client "(require 'clojure.repl)" identity) 21 | (clojure-mcp.nrepl/eval-code client "(require 'clojure.string)" identity) 22 | (binding [*nrepl-server* server 23 | *nrepl-client-atom* client-atom] 24 | (try 25 | (f) 26 | (finally 27 | (clojure-mcp.nrepl/stop-polling client) 28 | (nrepl.server/stop-server server)))))) 29 | 30 | (use-fixtures :once test-nrepl-fixture) 31 | 32 | ;; Helper function to evaluate code for tests 33 | (defn eval-helper [client code] 34 | (nrepl/tool-eval-code client code)) 35 | 36 | (deftest get-current-namespace-test 37 | (testing "get-current-namespace returns the current namespace when available" 38 | ;; First set a known current namespace 39 | (nrepl/eval-code @*nrepl-client-atom* "(in-ns 'user)" identity) 40 | (let [result (sut/get-current-namespace @*nrepl-client-atom*)] 41 | (is (map? result)) 42 | (is (= "user" (:namespace result))) 43 | (is (false? (:error result))))) 44 | 45 | (testing "get-current-namespace with custom namespace" 46 | ;; Create and switch to a test namespace 47 | (nrepl/eval-code @*nrepl-client-atom* "(create-ns 'test-ns)" identity) 48 | (nrepl/eval-code @*nrepl-client-atom* "(in-ns 'test-ns)" identity) 49 | (let [result (sut/get-current-namespace @*nrepl-client-atom*)] 50 | (is (map? result)) 51 | (is (= "test-ns" (:namespace result))) 52 | (is (false? (:error result)))) 53 | ;; Switch back to user namespace for subsequent tests 54 | (nrepl/eval-code @*nrepl-client-atom* "(in-ns 'user)" identity))) 55 | 56 | (deftest get-all-namespaces-test 57 | (testing "get-all-namespaces returns namespaces when available" 58 | (let [result (sut/get-all-namespaces @*nrepl-client-atom* eval-helper)] 59 | (is (map? result)) 60 | (is (vector? (:namespaces result))) 61 | (is (false? (:error result))) 62 | ;; Check for some common namespaces that should be there 63 | (is (some #(= "clojure.core" %) (:namespaces result))) 64 | (is (some #(= "clojure.string" %) (:namespaces result))) 65 | (is (some #(= "user" %) (:namespaces result)))))) 66 | 67 | (deftest get-vars-in-namespace-test 68 | (testing "get-vars-in-namespace returns vars for clojure.string" 69 | (let [result (sut/get-vars-in-namespace @*nrepl-client-atom* eval-helper "clojure.string")] 70 | (is (map? result)) 71 | (is (vector? (:vars result))) 72 | (is (false? (:error result))) 73 | ;; Check some common functions in clojure.string 74 | (is (some #(= 'join (:name %)) (:vars result))) 75 | (is (some #(= 'split (:name %)) (:vars result))))) 76 | 77 | (testing "get-vars-in-namespace returns error for nonexistent namespace" 78 | (let [result (sut/get-vars-in-namespace @*nrepl-client-atom* eval-helper "nonexistent.namespace")] 79 | (is (map? result)) 80 | (is (true? (:error result))) 81 | (is (string? (:message result))) 82 | (is (str/includes? (:message result) "not found"))))) -------------------------------------------------------------------------------- /test/clojure_mcp/other_tools/symbol/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.other-tools.symbol.core-test 2 | (:require 3 | [clojure.test :refer [deftest is testing use-fixtures]] 4 | [clojure-mcp.other-tools.symbol.core :as sut] 5 | [clojure-mcp.nrepl :as nrepl] 6 | [nrepl.server :as nrepl-server] 7 | [clojure.string :as str])) 8 | 9 | ;; Use the common test utils for nREPL server setup 10 | (defonce ^:dynamic *nrepl-server* nil) 11 | (defonce ^:dynamic *nrepl-client-atom* nil) 12 | 13 | (defn test-nrepl-fixture [f] 14 | (let [server (nrepl.server/start-server :port 0) 15 | port (:port server) 16 | client (clojure-mcp.nrepl/create {:port port}) 17 | client-atom (atom client)] 18 | (clojure-mcp.nrepl/start-polling client) 19 | ;; Load some namespaces for testing 20 | (clojure-mcp.nrepl/eval-code client "(require 'clojure.repl)" identity) 21 | (clojure-mcp.nrepl/eval-code client "(require 'clojure.string)" identity) 22 | (binding [*nrepl-server* server 23 | *nrepl-client-atom* client-atom] 24 | (try 25 | (f) 26 | (finally 27 | (clojure-mcp.nrepl/stop-polling client) 28 | (nrepl.server/stop-server server)))))) 29 | 30 | (use-fixtures :once test-nrepl-fixture) 31 | 32 | ;; We'll use the actual nREPL implementation for real results 33 | 34 | (deftest get-symbol-completions-test 35 | (testing "get-symbol-completions returns valid response structure" 36 | (let [client @*nrepl-client-atom* 37 | result (sut/get-symbol-completions client "ma")] 38 | (is (map? result)) 39 | (is (vector? (:completions result))) 40 | ;; Don't require specific completions since the test environment 41 | ;; might not have full completion support configured 42 | (is (boolean? (:error result))) 43 | ;; If there's no error, we should have a completions vector (even if empty) 44 | (when-not (:error result) 45 | (is (vector? (:completions result))))))) 46 | 47 | (deftest get-symbol-metadata-test 48 | (testing "get-symbol-metadata returns metadata for a valid symbol" 49 | (let [client @*nrepl-client-atom* 50 | result (sut/get-symbol-metadata client "map")] 51 | (is (map? result)) 52 | (is (map? (:metadata result))) 53 | (is (= (str (:name (:metadata result))) "map") "Name should be map") 54 | (is (false? (:error result))))) 55 | 56 | (testing "get-symbol-metadata handles missing symbols" 57 | (let [client @*nrepl-client-atom* 58 | result (sut/get-symbol-metadata client "this-symbol-does-not-exist-anywhere")] 59 | (is (map? result)) 60 | (is (true? (:error result))) 61 | (is (string? (:message result))) 62 | (is (str/includes? (:message result) "not found"))))) 63 | 64 | (deftest get-symbol-documentation-test 65 | (testing "get-symbol-documentation returns doc and arglists for a valid symbol" 66 | (let [client @*nrepl-client-atom* 67 | result (sut/get-symbol-documentation client "map")] 68 | (is (map? result)) 69 | (is (sequential? (:arglists result))) 70 | (is (not (empty? (:arglists result)))) 71 | (is (string? (:doc result))) 72 | (is (str/includes? (:doc result) "Returns a lazy")) 73 | (is (false? (:error result))))) 74 | 75 | (testing "get-symbol-documentation handles missing symbols" 76 | (let [client @*nrepl-client-atom* 77 | result (sut/get-symbol-documentation client "this-symbol-does-not-exist-anywhere")] 78 | (is (map? result)) 79 | (is (true? (:error result))) 80 | (is (string? (:message result))) 81 | (is (str/includes? (:message result) "not found"))))) 82 | 83 | (deftest get-source-code-test 84 | (testing "get-source-code returns source for a valid symbol" 85 | (let [client @*nrepl-client-atom* 86 | ;; Use a well-known function to test source retrieval 87 | result (sut/get-source-code client "map")] 88 | (is (map? result)) 89 | (is (string? (:source result))) 90 | (is (str/includes? (:source result) "(defn map")) 91 | (is (false? (:error result))))) 92 | 93 | (testing "get-source-code handles missing source" 94 | (let [client @*nrepl-client-atom* 95 | result (sut/get-source-code client "this-symbol-does-not-exist-anywhere")] 96 | (is (map? result)) 97 | (is (true? (:error result))) 98 | (is (string? (:message result))) 99 | (is (str/includes? (:message result) "Source not found"))))) 100 | 101 | (deftest search-symbols-test 102 | (testing "search-symbols returns matches for a valid search" 103 | (let [client @*nrepl-client-atom* 104 | result (sut/search-symbols client "map")] 105 | (is (map? result)) 106 | (is (vector? (:matches result))) 107 | (is (some #(= % "clojure.core/map") (:matches result))) 108 | (is (some #(= % "clojure.core/mapv") (:matches result))) 109 | (is (false? (:error result))))) 110 | 111 | (testing "search-symbols handles empty results" 112 | (let [client @*nrepl-client-atom* 113 | result (sut/search-symbols client "xyz123nonexistent")] 114 | (is (map? result)) 115 | (is (vector? (:matches result))) 116 | (is (= ["No matches found"] (:matches result))) 117 | (is (false? (:error result)))))) 118 | -------------------------------------------------------------------------------- /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/start-polling client) 17 | (nrepl/eval-code client "(require 'clojure.repl)" identity) 18 | (binding [*nrepl-server* server 19 | *nrepl-client-atom* client-atom] 20 | (try 21 | (f) 22 | (finally 23 | (nrepl/stop-polling client) 24 | (nrepl-server/stop-server server)))))) 25 | 26 | (defn cleanup-test-file [f] 27 | (try 28 | (f) 29 | (finally 30 | #_(io/delete-file *test-file-path* true)))) 31 | 32 | ;; Helper to invoke tool functions more easily in tests 33 | (defn make-test-tool [{:keys [tool-fn] :as _tool-map}] 34 | (fn [arg-map] 35 | (let [prom (promise)] 36 | (tool-fn nil arg-map 37 | (fn [res error?] 38 | (deliver prom {:res res :error? error?}))) 39 | @prom))) 40 | 41 | ;; Apply fixtures in each test namespace 42 | (defn apply-fixtures [test-namespace] 43 | (use-fixtures :once test-nrepl-fixture) 44 | (use-fixtures :each cleanup-test-file)) -------------------------------------------------------------------------------- /test/clojure_mcp/tools/directory_tree/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.directory-tree.core-test 2 | (:require [clojure.test :refer :all] 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"))))) -------------------------------------------------------------------------------- /test/clojure_mcp/tools/directory_tree/tool_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.directory-tree.tool-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-mcp.tools.directory-tree.tool :as sut] 4 | [clojure-mcp.config :as config] ; Added config require 5 | [clojure-mcp.tool-system :as tool-system])) 6 | 7 | (deftest tool-name-test 8 | (testing "tool-name returns the correct name" 9 | (is (= "LS" 10 | (tool-system/tool-name {:tool-type :directory-tree}))))) 11 | 12 | (deftest tool-description-test 13 | (testing "tool-description returns a non-empty description" 14 | (let [description (tool-system/tool-description {:tool-type :directory-tree})] 15 | (is (string? description)) 16 | (is (not (empty? description)))))) 17 | 18 | (deftest tool-schema-test 19 | (testing "tool-schema returns a valid schema with required path parameter" 20 | (let [schema (tool-system/tool-schema {:tool-type :directory-tree})] 21 | (is (map? schema)) 22 | (is (= :object (:type schema))) 23 | (is (contains? (:properties schema) :path)) 24 | (is (contains? (:properties schema) :max_depth)) 25 | (is (= [:path] (:required schema)))))) 26 | 27 | (deftest validate-inputs-test 28 | (testing "validate-inputs properly validates and transforms inputs" 29 | (let [nrepl-client-atom (atom {})] 30 | (config/set-config! nrepl-client-atom :nrepl-user-dir "/base/dir") 31 | (config/set-config! nrepl-client-atom :allowed-directories ["/base/dir"]) 32 | (let [tool-config {:tool-type :directory-tree 33 | :nrepl-client-atom nrepl-client-atom}] 34 | 35 | (with-redefs [clojure-mcp.utils.valid-paths/validate-path-with-client 36 | (fn [path _] 37 | (str "/validated" path))] 38 | 39 | (testing "with only path" 40 | (let [result (tool-system/validate-inputs tool-config {:path "/test/path"})] 41 | (is (= {:path "/validated/test/path"} result)))) 42 | 43 | (testing "with path and max_depth" 44 | (let [result (tool-system/validate-inputs tool-config {:path "/test/path" :max_depth 3})] 45 | (is (= {:path "/validated/test/path" 46 | :max_depth 3} result)))) 47 | 48 | (testing "missing required path parameter" 49 | (is (thrown-with-msg? clojure.lang.ExceptionInfo 50 | #"Missing required parameter: path" 51 | (tool-system/validate-inputs tool-config {}))))))))) 52 | 53 | (deftest execute-tool-test 54 | (testing "execute-tool calls core function with correct parameters" 55 | (with-redefs [clojure-mcp.tools.directory-tree.core/directory-tree 56 | (fn [path & {:keys [max-depth]}] 57 | {:called-with {:path path 58 | :max-depth max-depth}})] 59 | 60 | (testing "with only path" 61 | (let [result (tool-system/execute-tool 62 | {:tool-type :directory-tree} 63 | {:path "/test/path"})] 64 | (is (= {:called-with {:path "/test/path" 65 | :max-depth nil}} result)))) 66 | 67 | (testing "with path and max_depth" 68 | (let [result (tool-system/execute-tool 69 | {:tool-type :directory-tree} 70 | {:path "/test/path" :max_depth 3})] 71 | (is (= {:called-with {:path "/test/path" 72 | :max-depth 3}} result))))))) 73 | 74 | (deftest format-results-test 75 | (testing "format-results correctly formats successful results" 76 | (let [result "Directory tree output" 77 | formatted (tool-system/format-results {:tool-type :directory-tree} result)] 78 | (is (= {:result ["Directory tree output"] 79 | :error false} formatted)))) 80 | 81 | (testing "format-results correctly formats error results" 82 | (let [result {:error "Some error occurred"} 83 | formatted (tool-system/format-results {:tool-type :directory-tree} result)] 84 | (is (= {:result ["Some error occurred"] 85 | :error true} formatted))))) 86 | 87 | (deftest registration-map-test 88 | (testing "directory-tree-tool returns a valid registration map" 89 | (let [nrepl-client-atom (atom {}) 90 | reg-map (sut/directory-tree-tool nrepl-client-atom)] 91 | (is (= "LS" (:name reg-map))) 92 | (is (string? (:description reg-map))) 93 | (is (map? (:schema reg-map))) 94 | (is (fn? (:tool-fn reg-map)))))) 95 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/glob_files/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.glob-files.core-test 2 | (:require [clojure.test :refer :all] 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 (= (count (:filenames result)) (:numFiles result)) "numFiles should match filenames count") 15 | (is (boolean? (:truncated result)) "truncated should be a boolean") 16 | (is (number? (:durationMs result)) "durationMs should be a number"))) 17 | 18 | (testing "Filtering with specific pattern" 19 | (let [current-dir (System/getProperty "user.dir") 20 | result (sut/glob-files current-dir "**/glob_files/**/*.clj")] 21 | (is (not (:error result)) "Should not return an error") 22 | (is (vector? (:filenames result)) "Should return filenames as a vector") 23 | (is (every? #(.contains % "glob_files") (:filenames result)) 24 | "All files should contain glob_files in the path"))) 25 | 26 | (testing "Handling non-existent directory" 27 | (let [result (sut/glob-files "/nonexistent/directory" "*.clj")] 28 | (is (:error result) "Should return an error") 29 | (is (string? (:error result)) "Error should be a string"))) 30 | 31 | (testing "Handling invalid glob pattern" 32 | (let [current-dir (System/getProperty "user.dir") 33 | result (sut/glob-files current-dir "[invalid-glob-pattern")] 34 | (is (not (:error result)) "May not return an error for invalid pattern") 35 | (is (= 0 (:numFiles result)) "Should find 0 files for invalid pattern"))) 36 | 37 | (testing "Result truncation with small max-results" 38 | (let [current-dir (System/getProperty "user.dir") 39 | result (sut/glob-files current-dir "**/*.clj" :max-results 1)] 40 | (is (not (:error result)) "Should not return an error") 41 | (is (= 1 (count (:filenames result))) "Should return exactly 1 result") 42 | (is (:truncated result) "Should indicate results were truncated"))) 43 | 44 | (testing "Finding files in root directory with **/*.ext pattern" 45 | (let [current-dir (System/getProperty "user.dir") 46 | ;; Get files using both patterns for comparison 47 | standard-result (sut/glob-files current-dir "**/*.md") 48 | root-only-result (sut/glob-files current-dir "*.md") 49 | ;; Count root-level .md files using Java file operations 50 | root-file-count (count (filter #(and (.isFile %) 51 | (.endsWith (.getName %) ".md")) 52 | (.listFiles (io/file current-dir))))] 53 | ;; The **/*.md pattern should also find files in the root directory 54 | (is (>= (count (:filenames standard-result)) root-file-count) 55 | "**/*.md pattern should find all root-level files") 56 | ;; Compare with direct root pattern to ensure we find the same files 57 | (is (= (count (:filenames root-only-result)) root-file-count) 58 | "*.md pattern should find all root-level files") 59 | ;; Check if root files exist in the results 60 | (let [path-obj (Paths/get current-dir (into-array String [])) 61 | root-files-in-results (filter 62 | (fn [file-path] 63 | (let [file-obj (Paths/get file-path (into-array String [])) 64 | rel-path (.relativize path-obj file-obj)] 65 | (and (= (.getNameCount rel-path) 1) 66 | (.endsWith (str rel-path) ".md")))) 67 | (:filenames standard-result))] 68 | (is (= (count root-files-in-results) root-file-count) 69 | "**/*.md pattern should find all root-level md files"))))) 70 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/project/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.project.core-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [clojure.java.io :as io] 4 | [clojure.edn :as edn] 5 | [clojure-mcp.tools.project.core :refer [inspect-project-code]])) 6 | 7 | (deftest inspect-project-code-test 8 | (testing "inspect-project-code generates valid REPL expression" 9 | (let [code (inspect-project-code)] 10 | (is (seq? code)) 11 | (is (= 'let (first code)))))) 12 | 13 | (deftest lein-project-parsing-test 14 | (testing "parses real Leiningen project.clj file" 15 | (let [project-file (io/resource "clojure-mcp/test/projects/project.clj") 16 | project-content (-> project-file slurp read-string) 17 | lein-config (->> project-content 18 | (drop 3) 19 | (partition 2) 20 | (map (fn [[k v]] [k v])) 21 | (into {})) 22 | project-name (second project-content) 23 | version (nth project-content 2)] 24 | 25 | (is (= 'acme/widget-factory project-name)) 26 | (is (= "2.3.0-SNAPSHOT" version)) 27 | (is (= "A comprehensive widget manufacturing system" (:description lein-config))) 28 | (is (= "https://github.com/acme/widget-factory" (:url lein-config))) 29 | (is (map? (:license lein-config))) 30 | (is (= "Eclipse Public License" (get-in lein-config [:license :name]))) 31 | (is (vector? (:dependencies lein-config))) 32 | (is (= 3 (count (:dependencies lein-config)))) 33 | (is (= ["src/main/clj" "src/shared"] (:source-paths lein-config))) 34 | (is (= ["test/unit" "test/integration"] (:test-paths lein-config))) 35 | (is (= {:port 57802} (:repl-options lein-config))) 36 | (is (map? (:profiles lein-config)))))) 37 | 38 | (deftest lein-project-defaults-test 39 | (testing "handles default source and test paths for Leiningen projects" 40 | (let [minimal-project '(defproject my-app "1.0.0" 41 | :dependencies [['org.clojure/clojure "1.11.1"]]) 42 | 43 | lein-config (->> minimal-project 44 | (drop 3) 45 | (partition 2) 46 | (map (fn [[k v]] [k v])) 47 | (into {})) 48 | 49 | ;; Test default path logic 50 | source-paths (or (:source-paths lein-config) ["src"]) 51 | test-paths (or (:test-paths lein-config) ["test"])] 52 | 53 | (is (= ["src"] source-paths)) 54 | (is (= ["test"] test-paths)))) 55 | 56 | (testing "respects explicit source and test paths" 57 | (let [custom-paths-project '(defproject my-app "1.0.0" 58 | :source-paths ["src/main/clj" "src/shared"] 59 | :test-paths ["test/unit" "test/integration"] 60 | :dependencies [['org.clojure/clojure "1.11.1"]]) 61 | 62 | lein-config (->> custom-paths-project 63 | (drop 3) 64 | (partition 2) 65 | (map (fn [[k v]] [k v])) 66 | (into {})) 67 | 68 | source-paths (or (:source-paths lein-config) ["src"]) 69 | test-paths (or (:test-paths lein-config) ["test"])] 70 | 71 | (is (= ["src/main/clj" "src/shared"] source-paths)) 72 | (is (= ["test/unit" "test/integration"] test-paths))))) 73 | 74 | (deftest deps-project-parsing-test 75 | (testing "parses real deps.edn file" 76 | (let [deps-file (io/resource "clojure-mcp/test/projects/deps.edn") 77 | deps-content (-> deps-file slurp edn/read-string)] 78 | 79 | (is (map? deps-content)) 80 | (is (map? (:deps deps-content))) 81 | (is (= 3 (count (:deps deps-content)))) 82 | (is (= ["src" "resources"] (:paths deps-content))) 83 | (is (map? (:aliases deps-content))) 84 | (is (contains? (:aliases deps-content) :test)) 85 | (is (contains? (:aliases deps-content) :dev)) 86 | (is (= ["test"] (get-in deps-content [:aliases :test :extra-paths]))) 87 | (is (= ["dev"] (get-in deps-content [:aliases :dev :extra-paths])))))) 88 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/read_file/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.read-file.core-test 2 | (:require 3 | [clojure.test :refer [deftest is testing use-fixtures]] 4 | [clojure-mcp.tools.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 | 68 | (testing "Reading with offset and limit" 69 | (let [result (read-file-core/read-file (.getPath *test-file*) 1 2)] 70 | (is (map? result)) 71 | (is (= 2 (count (str/split (:content result) #"\n")))) 72 | (is (str/starts-with? (:content result) "Line 2")) 73 | (is (:truncated? result)))) 74 | 75 | (testing "Reading with line length limit" 76 | (let [result (read-file-core/read-file (.getPath *large-test-file*) 0 5 :max-line-length 10)] 77 | (is (map? result)) 78 | (is (= 5 (count (str/split (:content result) #"\n")))) 79 | (is (every? #(str/includes? % "...") (str/split (:content result) #"\n"))) 80 | (is (:line-lengths-truncated? result))))) 81 | 82 | (deftest read-file-error-test 83 | (testing "Reading non-existent file" 84 | (let [result (read-file-core/read-file (.getPath (io/file *test-dir* "nonexistent.txt")) 0 1000)] 85 | (is (map? result)) 86 | (is (contains? result :error)) 87 | (is (str/includes? (:error result) "does not exist")))) 88 | 89 | (testing "Reading a directory instead of a file" 90 | (let [result (read-file-core/read-file (.getPath *test-dir*) 0 1000)] 91 | (is (map? result)) 92 | (is (contains? result :error)) 93 | (is (str/includes? (:error result) "is not a file"))))) -------------------------------------------------------------------------------- /test/clojure_mcp/tools/scratch_pad/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.scratch-pad.core-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-mcp.tools.scratch-pad.core :as core])) 4 | 5 | (deftest test-dissoc-in-data 6 | (testing "Removing values at paths" 7 | (let [data {"a" {"b" 2 "c" 3}}] 8 | (is (= {"a" {"c" 3}} (core/dissoc-in-data data ["a" "b"]))) 9 | (is (= {} (core/dissoc-in-data data ["a"]))) 10 | (is (= data (core/dissoc-in-data data []))) 11 | (is (= {"a" {}} (core/dissoc-in-data {"a" {"b" 2}} ["a" "b"])))))) 12 | 13 | (deftest test-inspect-data 14 | (testing "Inspect data generation" 15 | (is (= "Empty scratch pad" (core/inspect-data {}))) 16 | (testing "Pretty printing simple data" 17 | (let [data {"a" 1} 18 | result (core/inspect-data data)] 19 | (is (string? result)) 20 | (is (.contains result "{\"a\" 1}")))) 21 | (testing "Pretty printing nested data" 22 | (let [nested {"a" {"b" {"c" 1}}} 23 | view (core/inspect-data nested)] 24 | (is (.contains view "{\"a\"")))))) ; Should truncate at depth 2 25 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/scratch_pad/truncate_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.scratch-pad.truncate-test 2 | (:require [clojure.test :refer :all] 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 | (let [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 | -------------------------------------------------------------------------------- /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.config :as config] 5 | [clojure-mcp.nrepl :as nrepl] 6 | [nrepl.server :as nrepl-server] 7 | [clojure-mcp.tool-system :as tool-system] 8 | [clojure.test :refer [use-fixtures]] 9 | [clojure.java.io :as io] 10 | [clojure-mcp.tools.read-file.file-timestamps :as file-timestamps])) 11 | 12 | (defonce ^:dynamic *nrepl-server* nil) 13 | (defonce ^:dynamic *nrepl-client-atom* nil) 14 | (def ^:dynamic *test-file-path* "test_function_edit.clj") 15 | 16 | (defn test-nrepl-fixture [f] 17 | (let [server (nrepl-server/start-server :port 0) ; Use port 0 for dynamic port assignment 18 | port (:port server) 19 | client (nrepl/create {:port port}) 20 | client-atom (atom client)] 21 | (nrepl/start-polling client) 22 | (nrepl/eval-code client "(require 'clojure.repl)" identity) 23 | (binding [*nrepl-server* server 24 | *nrepl-client-atom* client-atom] 25 | (try 26 | (f) 27 | (finally 28 | (nrepl/stop-polling client) 29 | (nrepl-server/stop-server server)))))) 30 | 31 | (defn cleanup-test-file [f] 32 | (try 33 | (f) 34 | (finally 35 | #_(io/delete-file *test-file-path* true)))) 36 | 37 | ;; Helper to invoke full tool function directly using the tool registration map 38 | (defn make-tool-tester [tool-instance] 39 | "Takes a tool instance and returns a function that executes the tool directly. 40 | The returned function takes a map of tool inputs and returns a map with: 41 | {:result result :error? error-flag}" 42 | (let [reg-map (tool-system/registration-map tool-instance) 43 | tool-fn (:tool-fn reg-map)] 44 | (fn [inputs] 45 | (let [prom (promise)] 46 | (tool-fn nil inputs 47 | (fn [res error?] 48 | (deliver prom {:result res :error? error?}))) 49 | @prom)))) 50 | 51 | ;; Helper to test individual multimethod pipeline steps 52 | (defn test-pipeline-steps [tool-instance inputs] 53 | "Executes the validation, execution, and formatting steps of the tool pipeline 54 | and returns the formatted result." 55 | (let [validated (tool-system/validate-inputs tool-instance inputs) 56 | execution-result (tool-system/execute-tool tool-instance validated) 57 | formatted-result (tool-system/format-results tool-instance execution-result)] 58 | formatted-result)) 59 | 60 | ;; Apply fixtures in each test namespace 61 | (defn create-test-dir 62 | "Creates a temporary test directory with a unique name" 63 | [] 64 | (let [temp-dir (io/file (System/getProperty "java.io.tmpdir")) 65 | test-dir (io/file temp-dir (str "clojure-mcp-test-" (System/currentTimeMillis)))] 66 | (.mkdirs test-dir) 67 | (.getAbsolutePath test-dir))) 68 | 69 | (defn create-and-register-test-file 70 | "Creates a test file with the given content and registers it in the timestamp tracker" 71 | [client-atom dir filename content] 72 | (let [file-path (str dir "/" filename) 73 | _ (io/make-parents file-path) 74 | _ (spit file-path content) 75 | file-obj (io/file file-path) 76 | canonical-path (.getCanonicalPath file-obj)] 77 | ;; Register the file using its canonical path in the timestamp tracker 78 | (file-timestamps/update-file-timestamp-to-current-mtime! client-atom canonical-path) 79 | ;; Small delay to ensure future modifications have different timestamps 80 | (Thread/sleep 25) 81 | ;; Return the canonical path for consistent usage 82 | canonical-path)) 83 | 84 | (defn modify-test-file 85 | "Modifies a test file and updates its timestamp in the tracker if update-timestamp? is true" 86 | [client-atom file-path content & {:keys [update-timestamp?] :or {update-timestamp? false}}] 87 | (spit file-path content) 88 | (when update-timestamp? 89 | (file-timestamps/update-file-timestamp-to-current-mtime! client-atom file-path) 90 | ;; Small delay 91 | (Thread/sleep 25)) 92 | file-path) 93 | 94 | (defn read-and-register-test-file 95 | "Updates the timestamp for an existing file to mark it as read. 96 | Normalizes the file path to ensure consistent lookup." 97 | [client-atom file-path] 98 | (let [normalized-path (.getCanonicalPath (io/file file-path))] 99 | (file-timestamps/update-file-timestamp-to-current-mtime! client-atom normalized-path) 100 | ;; Small delay to ensure timestamps differ if modified 101 | (Thread/sleep 25) 102 | normalized-path)) 103 | 104 | (defn clean-test-dir 105 | "Recursively deletes a test directory" 106 | [dir-path] 107 | (let [dir (io/file dir-path)] 108 | (when (.exists dir) 109 | (doseq [file (reverse (file-seq dir))] 110 | (.delete file))))) 111 | 112 | (defn apply-fixtures [test-namespace] 113 | (use-fixtures :once test-nrepl-fixture) 114 | (use-fixtures :each cleanup-test-file)) 115 | -------------------------------------------------------------------------------- /test/clojure_mcp/tools/think/tool_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mcp.tools.think.tool-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-mcp.tool-system :as tool-system] 4 | [clojure-mcp.tools.think.tool :as sut])) 5 | 6 | (deftest think-tool-multimethods 7 | (let [tool-config {:tool-type :think}] 8 | (testing "tool-name returns the correct name" 9 | (is (= "think" (tool-system/tool-name tool-config)))) 10 | 11 | (testing "tool-description returns a non-empty string" 12 | (let [description (tool-system/tool-description tool-config)] 13 | (is (string? description)) 14 | (is (not (empty? description))))) 15 | 16 | (testing "tool-schema returns a valid schema" 17 | (let [schema (tool-system/tool-schema tool-config)] 18 | (is (map? schema)) 19 | (is (= "object" (:type schema))) 20 | (is (map? (:properties schema))) 21 | (is (contains? (:properties schema) "thought")))) 22 | 23 | (testing "validate-inputs validates correctly" 24 | (is (= {:thought "Some thought"} 25 | (tool-system/validate-inputs tool-config {:thought "Some thought"}))) 26 | (is (thrown? clojure.lang.ExceptionInfo 27 | (tool-system/validate-inputs tool-config {})))) 28 | 29 | (testing "execute-tool returns expected result" 30 | (let [result (tool-system/execute-tool tool-config {:thought "Test execution"})] 31 | (is (map? result)) 32 | (is (= false (:error result))) 33 | (is (= "Your thought has been logged." (:result result))))) 34 | 35 | (testing "format-results formats correctly" 36 | (let [success-result (tool-system/format-results tool-config {:result "Success" :error false}) 37 | error-result (tool-system/format-results tool-config {:result "Error" :error true})] 38 | (is (= ["Success"] (:result success-result))) 39 | (is (= false (:error success-result))) 40 | (is (= ["Error"] (:result error-result))) 41 | (is (= true (:error error-result))))))) --------------------------------------------------------------------------------