diff --git a/deps.edn b/deps.edn index 40fea2f..76602ff 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,7 @@ {:deps {org.clojure/clojure {:mvn/version "1.12.0"} ;; try older clojure version if you wish ;; org.clojure/clojure {:mvn/version "1.10.3"} + org.clojure/data.json {:mvn/version "2.5.0"} org.clojure/test.check {:mvn/version "1.1.0"} com.gfredericks/test.chuck {:mvn/version "0.2.12"} dev.weavejester/medley {:mvn/version "1.5.0"} diff --git a/src/clojure_experiments/books/clojure_brain_teasers/12_reader_discard.clj b/src/clojure_experiments/books/clojure_brain_teasers/12_reader_discard.clj new file mode 100644 index 0000000..9996723 --- /dev/null +++ b/src/clojure_experiments/books/clojure_brain_teasers/12_reader_discard.clj @@ -0,0 +1,21 @@ +(ns clojure-experiments.books.clojure-brain-teasers.12-reader-discard + "Shows how reader's macro `#_` works - discarding expressions. + See also: + - https://clojure.org/reference/reader + - https://www.expressionsofchange.org/dont-say-homoiconic/") + +(def my-msgs + {:emails [[:from "boss"] [:from "mom"] + #_ #_ [:from "Nigerian Prince"] [:from "LinkedIn"]] + :discord-msgs {"Clojure Camp" 6 + #_ #_ "Heart of Clojure" 3 + "DungeonMasters" 20} + :voicemails ["Your voicemail box is full."]}) +(defn unread [msgs] + (let [{:keys [emails discord-msgs voicemails]} msgs] + (+ (count emails) + (reduce + (vals discord-msgs)) + (count voicemails)))) + +(unread my-msgs) +;; => 29 diff --git a/src/clojure_experiments/mcp/IMPLEMENTATION_NOTES.md b/src/clojure_experiments/mcp/IMPLEMENTATION_NOTES.md new file mode 100644 index 0000000..759e3b0 --- /dev/null +++ b/src/clojure_experiments/mcp/IMPLEMENTATION_NOTES.md @@ -0,0 +1,194 @@ +# MCP Server Implementation Notes + +## Architecture Overview + +This is a simple but complete implementation of the Model Context Protocol (MCP) server in Clojure. + +### Key Components + +1. **Transport Layer** - stdio-based communication + - Reads JSON-RPC messages from stdin + - Writes JSON-RPC responses to stdout + - Uses stderr for server logs + +2. **Protocol Layer** - JSON-RPC 2.0 + - Handles request/response messages + - Implements proper error codes + - Supports notifications (though not currently used) + +3. **MCP Protocol Implementation** + - Initialize handshake + - Tool listing + - Tool execution + +### Protocol Flow + +``` +Client Server + | | + |--- initialize request ------->| + |<-- initialize response -------| + | | + |--- tools/list request ------->| + |<-- tools/list response -------| + | | + |--- tools/call request ------->| + |<-- tools/call response -------| +``` + +## MCP Protocol Version + +This implementation uses MCP protocol version `2024-11-05`. + +## Implemented Methods + +### Core Protocol Methods + +1. **initialize** + - Purpose: Establish connection and exchange capabilities + - Required for all MCP sessions + - Returns server info and capabilities + +2. **tools/list** + - Purpose: List available tools + - Returns array of tool definitions with schemas + +3. **tools/call** + - Purpose: Execute a specific tool + - Takes tool name and arguments + - Returns tool-specific results + +## Tool Implementation Pattern + +Each tool follows this pattern: + +```clojure +;; 1. Define the tool schema +{:name "tool_name" + :description "What the tool does" + :inputSchema {:type "object" + :properties {:param {:type "string" + :description "Parameter description"}} + :required ["param"]}} + +;; 2. Implement the handler function +(defn tool-name-handler + [{:keys [param]}] + {:content [{:type "text" + :text "result"}]}) + +;; 3. Register in tool-handlers map +{"tool_name" tool-name-handler} +``` + +## Error Handling + +The implementation uses standard JSON-RPC 2.0 error codes: + +- `-32601`: Method not found +- `-32603`: Internal error + +Tool-specific errors are returned in the result with `isError: true`. + +## Design Decisions + +### Why stdio transport? + +- Simple and reliable for local processes +- Perfect for desktop applications like Claude Desktop +- No need for network setup or security concerns +- Standard transport for MCP local servers + +### Why not SSE? + +- SSE (Server-Sent Events) is better for web-based clients +- Requires HTTP server setup +- More complex for local use cases +- Can be added later if needed + +### Tool Result Format + +Tools return results in this format: + +```clojure +{:content [{:type "text" + :text "..."}]} +``` + +For errors: + +```clojure +{:isError true + :content [{:type "text" + :text "error message"}]} +``` + +This follows the MCP specification for tool responses. + +## Testing Strategy + +The implementation includes: + +1. **Unit-level testing** via REPL (comment blocks) +2. **Integration testing** via test-client +3. **Manual testing** via stdin/stdout + +## Future Enhancements + +Possible additions: + +1. **More tools** + - `write_file` - Write content to a file + - `search_files` - Search for files by pattern + - `execute_command` - Run shell commands (with safety checks) + +2. **Resources** + - Implement MCP resources for serving content + - Examples: file://, git://, etc. + +3. **Prompts** + - Add prompt templates support + - Examples: code review, documentation, etc. + +4. **Sampling** + - Allow server to request LLM completions + - Advanced use case for autonomous agents + +5. **SSE Transport** + - Add HTTP/SSE transport option + - Useful for web-based clients + +6. **Configuration** + - Add config file support + - Allow configuring allowed directories, etc. + +7. **Security** + - Add path restrictions + - Implement allowlist/denylist for file operations + - Add audit logging + +## References + +- [MCP Specification](https://spec.modelcontextprotocol.io/) +- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) +- [JSON-RPC 2.0 Spec](https://www.jsonrpc.org/specification) +- [Clojure MCP implementations](https://github.com/search?q=clojure+mcp+server&type=repositories) + +## Known Limitations + +1. **No persistent state** - Server doesn't maintain state between requests +2. **No authentication** - Assumes trusted local environment +3. **No rate limiting** - Tools can be called unlimited times +4. **No async operations** - All tools are synchronous +5. **Basic error handling** - Could be more sophisticated + +## Performance Considerations + +- Startup time: ~1-2 seconds (JVM startup) +- Per-request overhead: Minimal (JSON parsing + tool execution) +- Memory usage: Low (~50MB base JVM) + +For production use with frequent calls, consider: +- Using GraalVM native-image for faster startup +- Implementing a persistent server mode +- Adding caching for frequently accessed files diff --git a/src/clojure_experiments/mcp/README.md b/src/clojure_experiments/mcp/README.md new file mode 100644 index 0000000..3e22df6 --- /dev/null +++ b/src/clojure_experiments/mcp/README.md @@ -0,0 +1,238 @@ +# MCP Server in Clojure + +A simple implementation of the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server in Clojure. + +## What is MCP? + +Model Context Protocol (MCP) is an open protocol that enables AI applications to connect to external data sources and tools. It standardizes how applications provide context to LLMs and interact with external tools through a client-server architecture. + +## Features + +This implementation provides: + +- **stdio transport** for local communication (suitable for desktop AI apps) +- **JSON-RPC 2.0** message protocol +- **Tool support** with `read_file` and `list_directory` tools +- **Sandboxed file system access** with configurable allow-lists for security + +## Current Tools + +### read_file + +Reads the contents of a file from the filesystem. + +**Input:** + +- `path` (string, required): The absolute path to the file to read + +**Returns:** + +- File contents as text, or an error if the file doesn't exist or cannot be read + +### list_directory + +Lists the contents of a directory. + +**Input:** + +- `path` (string, required): The absolute path to the directory to list + +**Returns:** + +- List of files and directories (directories are marked with a trailing `/`), or an error if the directory doesn't exist or cannot be read + +## Security: Path Sandboxing + +The server implements path sandboxing to restrict file system access to an allow-list of directories. This prevents the MCP client from accessing sensitive files or directories outside the permitted scope. + +### How it works + +1. **Canonical path resolution**: All paths are resolved to their canonical form to prevent path traversal attacks (e.g., `../../../etc/passwd`) +2. **Allow-list checking**: Each file operation validates that the requested path is within one of the allowed directories +3. **Subdirectory access**: If a directory is in the allow-list, all its subdirectories and files are automatically accessible + +### Configuring Allowed Paths + +#### Option 1: Environment Variable (Recommended for production) + +Set the `MCP_ALLOWED_PATHS` environment variable with a colon-separated (Unix) or semicolon-separated (Windows) list of allowed paths: + +```bash +# Unix/Linux/macOS +export MCP_ALLOWED_PATHS="/Users/username/projects" +clj -M -m clojure-experiments.mcp.server + +# Windows +set MCP_ALLOWED_PATHS="C:\Users\username\projects" +clj -M -m clojure-experiments.mcp.server +``` + +#### Option 2: Programmatic Configuration + +In your code, you can set allowed paths using `set-allowed-paths!`: + +```clojure +(require '[clojure-experiments.mcp.server :as server]) + +;; Set allowed paths +(server/set-allowed-paths! ["/Users/username/projects"]) + +;; Check if a path is allowed +(server/path-allowed? "/Users/username/projects/README.md") +;; => true + +(server/path-allowed? "/etc/passwd") +;; => false +``` + +#### Default Behavior + +If no allowed paths are configured, the server defaults to allowing access only to the current working directory and its subdirectories. + +### Security Best Practices + +1. **Always configure allowed paths** in production environments +2. **Use absolute paths** in the allow-list for clarity and security +3. **Be specific** about allowed directories - don't allow root directories unless necessary +4. **Review the allow-list** regularly to ensure it matches your security requirements +5. **Symbolic links** are resolved to their canonical paths, so they're subject to the same restrictions + +## Usage + +### Running the Server + +```bash +# Start the server using Clojure CLI (defaults to current directory) +clj -M -m clojure-experiments.mcp.server + +# With custom allowed paths +export MCP_ALLOWED_PATHS="/path/to/allowed/dir1:/path/to/allowed/dir2" +clj -M -m clojure-experiments.mcp.server +``` + +The server communicates via stdin/stdout using JSON-RPC 2.0 messages. + +### Testing with Manual JSON-RPC Messages + +You can test the server by sending JSON-RPC messages to stdin: + +**Initialize:** +```json +{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}} +``` + +**List tools:** +```json +{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}} +``` + +**Call read_file:** +```json +{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/path/to/file.txt"}}} +``` + +### Using with AI Applications + +To integrate this MCP server with AI applications like Claude Desktop, you would add it to your MCP configuration file: + +**For Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS): + +```json +{ + "mcpServers": { + "clojure-mcp": { + "command": "clj", + "args": ["-M", "-m", "clojure-experiments.mcp.server"], + "cwd": "/Users/jumar/workspace/clojure/clojure-experiments", + "env": { + "MCP_ALLOWED_PATHS": "/Users/jumar/workspace/clojure/clojure-experiments" + } + } + } +} +``` + +Note: Always configure the `MCP_ALLOWED_PATHS` environment variable when using with AI applications to restrict file system access. + +## Development + +### Testing in the REPL + +The namespace includes a comment block with test examples: + +```clojure +(require '[clojure-experiments.mcp.server :as mcp]) + +;; Test initialize +(mcp/process-request + {:jsonrpc "2.0" + :id 1 + :method "initialize" + :params {:protocolVersion "2024-11-05" + :capabilities {} + :clientInfo {:name "test-client" :version "1.0.0"}}}) + +;; Test list tools +(mcp/process-request + {:jsonrpc "2.0" + :id 2 + :method "tools/list" + :params {}}) + +;; Test read_file +(mcp/process-request + {:jsonrpc "2.0" + :id 3 + :method "tools/call" + :params {:name "read_file" + :arguments {:path "/path/to/some/file.txt"}}}) +``` + +### Adding New Tools + +To add a new tool: + +1. Add the tool definition to the `tools` vector +2. Implement the tool handler function +3. Register the handler in the `tool-handlers` map + +Example: + +```clojure +;; 1. Add tool definition +(def tools + [{:name "read_file" ...} + {:name "my_new_tool" + :description "Description of what the tool does" + :inputSchema {:type "object" + :properties {:param1 {:type "string" + :description "A parameter"}} + :required ["param1"]}}]) + +;; 2. Implement handler +(defn my-new-tool + [{:keys [param1]}] + {:content [{:type "text" + :text (str "Result: " param1)}]}) + +;; 3. Register handler +(def tool-handlers + {"read_file" read-file-tool + "my_new_tool" my-new-tool}) +``` + +## Protocol Reference + +- [MCP Specification](https://spec.modelcontextprotocol.io/) +- [MCP Documentation](https://modelcontextprotocol.io/docs) +- [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification) + +## Next Steps + +Potential improvements: +- Add more file system tools (list directory, write file, etc.) +- Implement resource support for serving content +- Add prompt templates support +- Implement SSE transport for web-based clients +- Add configuration file support +- Add logging and error handling improvements diff --git a/src/clojure_experiments/mcp/claude_desktop_config.example.json b/src/clojure_experiments/mcp/claude_desktop_config.example.json new file mode 100644 index 0000000..b1c2e88 --- /dev/null +++ b/src/clojure_experiments/mcp/claude_desktop_config.example.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "clojure-mcp": { + "command": "clj", + "args": [ + "-M", + "-m", + "clojure-experiments.mcp.server" + ], + "cwd": "/Users/jumar/workspace/clojure/clojure-experiments", + "env": {} + } + } +} diff --git a/src/clojure_experiments/mcp/server.clj b/src/clojure_experiments/mcp/server.clj new file mode 100644 index 0000000..6adfc86 --- /dev/null +++ b/src/clojure_experiments/mcp/server.clj @@ -0,0 +1,362 @@ +(ns clojure-experiments.mcp.server + "A simple Model Context Protocol (MCP) server implementation in Clojure. + + MCP is a protocol that enables AI applications to connect to external data sources + and tools. This implementation uses stdio transport for local communication. + + References: + - https://modelcontextprotocol.io/ + - https://spec.modelcontextprotocol.io/" + (:require [clojure.data.json :as json] + [clojure.java.io :as io] + [clojure.string :as str]) + (:import [java.io BufferedReader InputStreamReader PrintWriter File])) + +;; Configuration and security + +(def ^:dynamic *allowed-paths* + "Allow-list of paths that the MCP server can access. + Can be set via environment variable MCP_ALLOWED_PATHS (colon-separated). + Defaults to the current working directory." + (atom nil)) + +(defn set-allowed-paths! + "Set the allowed paths for file system access. + Paths should be absolute paths to files or directories." + [paths] + (reset! *allowed-paths* + (mapv (fn [path] + (let [file (io/file path)] + (.getCanonicalPath file))) + paths))) + +(defn get-allowed-paths + "Get the currently configured allowed paths." + [] + (or @*allowed-paths* + ;; Default to current working directory if not configured + (let [default-path (.getCanonicalPath (io/file "."))] + (reset! *allowed-paths* [default-path]) + @*allowed-paths*))) + +(defn path-allowed? + "Check if a given path is within the allowed paths. + Returns true if the path or any of its parent directories are in the allow-list. + Uses canonical paths to prevent path traversal attacks." + [path] + (try + (let [file (io/file path) + canonical-path (.getCanonicalPath file) + allowed-paths (get-allowed-paths)] + (some (fn [allowed-path] + ;; Check if the path is the allowed path or within it + (or (= canonical-path allowed-path) + (str/starts-with? canonical-path (str allowed-path File/separator)))) + allowed-paths)) + (catch Exception e + ;; If we can't resolve the canonical path, deny access + false))) + +(defn validate-path + "Validate that a path is allowed. Returns [ok? error-message]." + [path] + (cond + (str/blank? path) + [false "Path cannot be empty"] + + (not (path-allowed? path)) + [false (str "Access denied: Path is outside allowed directories. Allowed paths: " + (str/join ", " (get-allowed-paths)))] + + :else + [true nil])) + +;; JSON-RPC 2.0 message handling + +(defn json-rpc-error + "Create a JSON-RPC 2.0 error response." + [id code message & [data]] + (cond-> {:jsonrpc "2.0" + :id id + :error {:code code + :message message}} + data (assoc-in [:error :data] data))) + +(defn json-rpc-response + "Create a JSON-RPC 2.0 success response." + [id result] + {:jsonrpc "2.0" + :id id + :result result}) + +(defn json-rpc-notification + "Create a JSON-RPC 2.0 notification (no id)." + [method params] + {:jsonrpc "2.0" + :method method + :params params}) + +;; MCP Protocol Implementation + +(def server-info + {:name "clojure-mcp-server" + :version "0.1.0"}) + +(def server-capabilities + {:tools {}}) + +;; Tool definitions + +(def tools + [{:name "read_file" + :description "Read the contents of a file from the filesystem" + :inputSchema {:type "object" + :properties {:path {:type "string" + :description "The absolute path to the file to read"}} + :required ["path"]}} + + {:name "list_directory" + :description "List the contents of a directory" + :inputSchema {:type "object" + :properties {:path {:type "string" + :description "The absolute path to the directory to list"}} + :required ["path"]}}]) + +;; Tool implementations + +(defn read-file-tool + "Implementation of the read_file tool." + [{:keys [path]}] + (let [[valid? error-msg] (validate-path path)] + (if-not valid? + {:isError true + :content [{:type "text" + :text error-msg}]} + (try + (let [file (io/file path)] + (if (.exists file) + (if (.isFile file) + {:content [{:type "text" + :text (slurp file)}]} + {:isError true + :content [{:type "text" + :text (str "Path is not a file: " path)}]}) + {:isError true + :content [{:type "text" + :text (str "File not found: " path)}]})) + (catch Exception e + {:isError true + :content [{:type "text" + :text (str "Error reading file: " (.getMessage e))}]}))))) + +(defn list-directory-tool + "Implementation of the list_directory tool." + [{:keys [path]}] + (let [[valid? error-msg] (validate-path path)] + (if-not valid? + {:isError true + :content [{:type "text" + :text error-msg}]} + (try + (let [file (io/file path)] + (if (.exists file) + (if (.isDirectory file) + (let [files (.listFiles file) + file-list (map (fn [f] + (str (.getName f) + (when (.isDirectory f) "/"))) + (sort-by #(.getName %) files))] + {:content [{:type "text" + :text (str/join "\n" file-list)}]}) + {:isError true + :content [{:type "text" + :text (str "Path is not a directory: " path)}]}) + {:isError true + :content [{:type "text" + :text (str "Directory not found: " path)}]})) + (catch Exception e + {:isError true + :content [{:type "text" + :text (str "Error listing directory: " (.getMessage e))}]}))))) + +(def tool-handlers + {"read_file" read-file-tool + "list_directory" list-directory-tool}) + +;; Request handlers + +(defn handle-initialize + "Handle the initialize request from the client." + [params] + {:protocolVersion "2024-11-05" + :capabilities server-capabilities + :serverInfo server-info}) + +(defn handle-tools-list + "Handle the tools/list request." + [_params] + {:tools tools}) + +(defn handle-tools-call + "Handle the tools/call request." + [{:keys [name arguments]}] + (if-let [handler (get tool-handlers name)] + (handler arguments) + {:isError true + :content [{:type "text" + :text (str "Unknown tool: " name)}]})) + +(def request-handlers + {"initialize" handle-initialize + "tools/list" handle-tools-list + "tools/call" handle-tools-call}) + +;; Message processing + +(defn process-request + "Process a single JSON-RPC request." + [{:keys [id method params] :as request}] + (try + (if-let [handler (get request-handlers method)] + (let [result (handler params)] + (json-rpc-response id result)) + (json-rpc-error id -32601 (str "Method not found: " method))) + (catch Exception e + (json-rpc-error id -32603 "Internal error" {:message (.getMessage e)})))) + +(defn send-message + "Send a JSON-RPC message to stdout." + [writer message] + (let [json-str (json/write-str message)] + (binding [*out* writer] + (println json-str) + (flush)))) + +(defn read-message + "Read a JSON-RPC message from stdin." + [reader] + (when-let [line (.readLine reader)] + (try + (json/read-str line :key-fn keyword) + (catch Exception e + (binding [*err* *err*] + (println "Error parsing JSON:" (.getMessage e))) + nil)))) + +;; Main server loop + +(defn start-server + "Start the MCP server using stdio transport." + [] + (let [reader (BufferedReader. (InputStreamReader. System/in)) + writer (PrintWriter. System/out true)] + (binding [*err* *err*] ; Keep stderr for logging + (println "MCP Server starting...")) + + (loop [] + (when-let [request (read-message reader)] + (let [response (process-request request)] + (send-message writer response)) + (recur))) + + (binding [*err* *err*] + (println "MCP Server stopped.")))) + +(defn -main + "Main entry point for the MCP server. + + Supports environment variables: + - MCP_ALLOWED_PATHS: Colon-separated list of allowed paths (defaults to current directory)" + [& args] + ;; Initialize allowed paths from environment variable if set + (when-let [env-paths (System/getenv "MCP_ALLOWED_PATHS")] + (let [paths (str/split env-paths (re-pattern (System/getProperty "path.separator")))] + (set-allowed-paths! paths) + (binding [*err* *err*] + (println "Allowed paths configured:" (str/join ", " (get-allowed-paths)))))) + + (start-server)) + +(comment + ;; Test the server by simulating requests + + ;; Configure allowed paths for testing + (set-allowed-paths! ["/Users/jumar/workspace/clojure/clojure-experiments"]) + (get-allowed-paths) + ;; => ["/Users/jumar/workspace/clojure/clojure-experiments"] + + ;; Test path validation + (path-allowed? "/Users/jumar/workspace/clojure/clojure-experiments/README.md") + ;; => true + + (path-allowed? "/etc/passwd") + ;; => false + + (path-allowed? "/Users/jumar/workspace/clojure/clojure-experiments/../../../etc/passwd") + ;; => false (canonical path resolution prevents path traversal) + + ;; Initialize request + (process-request + {:jsonrpc "2.0" + :id 1 + :method "initialize" + :params {:protocolVersion "2024-11-05" + :capabilities {} + :clientInfo {:name "test-client" :version "1.0.0"}}}) + ;; => {:jsonrpc "2.0", + ;; :id 1, + ;; :result {:protocolVersion "2024-11-05", + ;; :capabilities {:tools {}}, + ;; :serverInfo {:name "clojure-mcp-server", :version "0.1.0"}}} + + ;; List tools + (process-request + {:jsonrpc "2.0" + :id 2 + :method "tools/list" + :params {}}) + ;; => {:jsonrpc "2.0", + ;; :id 2, + ;; :result {:tools [{:name "read_file", ...}]}} + + ;; Call read_file tool - allowed path + (process-request + {:jsonrpc "2.0" + :id 3 + :method "tools/call" + :params {:name "read_file" + :arguments {:path "/Users/jumar/workspace/clojure/clojure-experiments/README.md"}}}) + + ;; Call read_file tool - disallowed path + (process-request + {:jsonrpc "2.0" + :id 4 + :method "tools/call" + :params {:name "read_file" + :arguments {:path "/etc/passwd"}}}) + ;; => {:jsonrpc "2.0", + ;; :id 4, + ;; :result {:isError true, + ;; :content [{:type "text", + ;; :text "Access denied: Path is outside allowed directories..."}]}} + + ;; Test path traversal attempt + (process-request + {:jsonrpc "2.0" + :id 5 + :method "tools/call" + :params {:name "read_file" + :arguments {:path "/Users/jumar/workspace/clojure/clojure-experiments/../../../etc/passwd"}}}) + ;; => {:jsonrpc "2.0", + ;; :id 5, + ;; :result {:isError true, + ;; :content [{:type "text", + ;; :text "Access denied..."}]}} + + ;; Test with multiple allowed paths + (set-allowed-paths! ["/Users/jumar/workspace/clojure/clojure-experiments" + "/tmp"]) + (path-allowed? "/tmp/test.txt") + ;; => true + + ) diff --git a/src/clojure_experiments/mcp/test_client.clj b/src/clojure_experiments/mcp/test_client.clj new file mode 100644 index 0000000..132a2df --- /dev/null +++ b/src/clojure_experiments/mcp/test_client.clj @@ -0,0 +1,70 @@ +(ns clojure-experiments.mcp.test-client + "Simple test client for the MCP server." + (:require [clojure-experiments.mcp.server :as server])) + +(defn test-initialize [] + (println "\n=== Testing initialize ===") + (let [request {:jsonrpc "2.0" + :id 1 + :method "initialize" + :params {:protocolVersion "2024-11-05" + :capabilities {} + :clientInfo {:name "test-client" :version "1.0.0"}}} + response (server/process-request request)] + (clojure.pprint/pprint response))) + +(defn test-tools-list [] + (println "\n=== Testing tools/list ===") + (let [request {:jsonrpc "2.0" + :id 2 + :method "tools/list" + :params {}} + response (server/process-request request)] + (clojure.pprint/pprint response))) + +(defn test-read-file [path] + (println "\n=== Testing read_file ===" path) + (let [request {:jsonrpc "2.0" + :id 3 + :method "tools/call" + :params {:name "read_file" + :arguments {:path path}}} + response (server/process-request request)] + (clojure.pprint/pprint response))) + +(defn test-unknown-method [] + (println "\n=== Testing unknown method ===") + (let [request {:jsonrpc "2.0" + :id 4 + :method "unknown/method" + :params {}} + response (server/process-request request)] + (clojure.pprint/pprint response))) + +(defn test-list-directory [path] + (println "\n=== Testing list_directory ===" path) + (let [request {:jsonrpc "2.0" + :id 5 + :method "tools/call" + :params {:name "list_directory" + :arguments {:path path}}} + response (server/process-request request)] + (clojure.pprint/pprint response))) + +(defn run-tests [] + (println "Starting MCP Server Tests\n") + (test-initialize) + (test-tools-list) + (test-read-file (str (System/getProperty "user.dir") "/README.md")) + (test-read-file "/does/not/exist.txt") + (test-list-directory (str (System/getProperty "user.dir") "/src")) + (test-list-directory "/does/not/exist") + (test-unknown-method) + (println "\n=== All tests completed ===")) + +(defn -main [& args] + (run-tests)) + +(comment + (run-tests) + ) diff --git a/src/clojure_experiments/mcp/test_server.sh b/src/clojure_experiments/mcp/test_server.sh new file mode 100755 index 0000000..267962c --- /dev/null +++ b/src/clojure_experiments/mcp/test_server.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Simple script to test the MCP server interactively +# Usage: ./test_server.sh + +echo "Starting MCP Server test..." +echo "" + +cd "$(dirname "$0")/../../.." + +# Test 1: Initialize +echo "Test 1: Initialize" +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | clj -M -m clojure-experiments.mcp.server 2>/dev/null & +SERVER_PID=$! +sleep 1 + +# Since the server blocks on reading, let's just use the test client instead +echo "Running test client..." +clj -M -m clojure-experiments.mcp.test-client + +echo "" +echo "Tests completed!"