├── .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)))))))
--------------------------------------------------------------------------------