From 8ea0eee299b4f81ab3b23179f6fb22200496a72b Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Sat, 28 Feb 2026 19:47:00 +0100 Subject: [PATCH] init MCP server --- MCP_SERVER_GUIDE.md | 149 ++++++++++++++++++++++++++++++++ README.md | 4 +- snippets_cms.bb | 4 +- snippets_mcp.bb | 7 ++ src/{cli_cms => cli}/config.clj | 2 +- src/{cli_cms => cli}/create.clj | 8 +- src/{cli_cms => cli}/delete.clj | 4 +- src/{cli_cms => cli}/edit.clj | 6 +- src/{cli_cms => cli}/main.clj | 10 +-- src/cli/mcp.clj | 76 ++++++++++++++++ src/{cli_cms => cli}/view.clj | 4 +- 11 files changed, 253 insertions(+), 21 deletions(-) create mode 100644 MCP_SERVER_GUIDE.md create mode 100755 snippets_mcp.bb rename src/{cli_cms => cli}/config.clj (93%) rename src/{cli_cms => cli}/create.clj (77%) rename src/{cli_cms => cli}/delete.clj (91%) rename src/{cli_cms => cli}/edit.clj (96%) rename src/{cli_cms => cli}/main.clj (74%) create mode 100644 src/cli/mcp.clj rename src/{cli_cms => cli}/view.clj (94%) diff --git a/MCP_SERVER_GUIDE.md b/MCP_SERVER_GUIDE.md new file mode 100644 index 0000000..fc6d197 --- /dev/null +++ b/MCP_SERVER_GUIDE.md @@ -0,0 +1,149 @@ +# MCP Server for Code Snippets Database + +## Overview + +Building an MCP (Model Context Protocol) server to create a CRUD interface for managing code snippets. The goal is to enable Claude to save commands/code directly to your snippets database through natural conversation. + +## What is an MCP Server? + +An MCP server is an executable that communicates with clients (like Claude IDE) by exchanging JSON messages. At its core: + +1. **Reads JSON** from stdin (messages from the client) +2. **Writes JSON** to stdout (responses back) +3. Implements the MCP protocol using JSON-RPC 2.0 + +Official docs: https://modelcontextprotocol.io/docs/concepts/architecture + +## Transport Mechanisms + +MCP servers support three transport types (choose one): + +### 1. **Stdio** (Most Common) +- Server runs as a subprocess +- Client spawns the executable and pipes JSON over stdin/stdout +- Best for: local integrations, Claude IDE plugins + +### 2. **HTTP** +- Server runs as an HTTP service (listens on a port) +- Client sends JSON over HTTP requests +- Best for: remote servers, shared services, multiple clients + +### 3. **SSE** (Server-Sent Events) +- Used alongside HTTP for server→client streaming +- Less common + +For this project, **stdio is the natural choice** — simple implementation and works seamlessly with Claude IDE. + +## Protocol Flow + +### 1. Initialize Handshake + +**Client → Server:** +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "claude-code", + "version": "1.0.0" + } + } +} +``` + +**Server → Client Response:** +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": { + "listChanged": true + }, + "resources": { + "listChanged": true + }, + "prompts": { + "listChanged": true + } + }, + "serverInfo": { + "name": "snippets-server", + "version": "1.0.0" + } + } +} +``` + +Key points: +- **JSON-RPC 2.0 format**: includes `jsonrpc`, `id`, and `method` fields +- **Server declares capabilities**: tells client what it offers (tools, resources, prompts) +- **Protocol version agreement**: both sides confirm compatibility +- **Server info**: client learns about your server + +### 2. Subsequent Calls + +After initialization, the client can call methods like: +- `tools/list` - get list of available tools +- `tools/call` - execute a specific tool +- `resources/list` - get available resources +- `resources/read` - read a resource + +## Implementation Plan for Snippets Server + +Your MCP server should expose CRUD tools: + +1. **`create_snippet`** - Save a new snippet (language, description, code) +2. **`list_snippets`** - List all snippets or filter by language/tag +3. **`get_snippet`** - Retrieve a specific snippet by ID +4. **`update_snippet`** - Modify an existing snippet +5. **`delete_snippet`** - Remove a snippet +6. **`search_snippets`** - Search snippets by content or description + +Each tool definition includes: +- Name and description +- Input schema (JSON Schema format) +- Output schema + +## Getting Started + +1. Choose a language (Node.js/Bun, Python, Go, Rust recommended) +2. Create a basic stdio-based server that: + - Reads from stdin + - Parses JSON-RPC messages + - Implements initialize handshake + - Implements tools/list and tools/call +3. Connect to your snippets database/storage +4. Test by running the server and sending JSON messages + +## Useful Resources + +- MCP Protocol Spec: https://modelcontextprotocol.io/ +- Official example servers: https://github.com/modelcontextprotocol/servers +- JSON-RPC 2.0: https://www.jsonrpc.org/specification + +## Use Case Flow + +``` +User (Claude IDE): +"Save this bash command to my snippets" + ↓ +Claude analyzes and calls: + tools/call with tool="create_snippet" + ↓ +MCP Server receives JSON-RPC request + ↓ +Server creates snippet in database + ↓ +Returns success response to Claude + ↓ +User sees confirmation and snippet is saved +``` + +This creates a seamless workflow where your snippet database grows through natural conversation! diff --git a/README.md b/README.md index 55e69cb..f437c21 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,14 @@ Interactive elements are powered by [Gum](https://github.com/charmbracelet/gum). To run commands: ```shell -$ bb -m cli-cms.main create +$ bb -m cli.main create ``` Or to play around and call individual functions using the REPL: ```shell $ rlwrap bb repl -user=> (require '[cli-cms.create :as command]) +user=> (require '[cli.create :as command]) user=> (def mock-snippet {:title "mock snippet from repl" :slug "mock" :tags '("mock") :markdown "# This is a mock"}) #'user/mock-snippet user=> (command/create-snippet mock-snippet) diff --git a/snippets_cms.bb b/snippets_cms.bb index 50c10fa..77c7c3a 100755 --- a/snippets_cms.bb +++ b/snippets_cms.bb @@ -1,7 +1,7 @@ #!/usr/bin/env bb (ns snippets-cms - (:require [cli-cms.main])) + (:require [cli.main])) (when (= *file* (System/getProperty "babashka.file")) - (apply cli-cms.main/-main *command-line-args*)) + (apply cli.main/-main *command-line-args*)) diff --git a/snippets_mcp.bb b/snippets_mcp.bb new file mode 100755 index 0000000..e5c0685 --- /dev/null +++ b/snippets_mcp.bb @@ -0,0 +1,7 @@ +#!/usr/bin/env bb + +(ns snippets-mcp + (:require [cli.mcp])) + +(when (= *file* (System/getProperty "babashka.file")) + (apply cli.mcp/-main *command-line-args*)) diff --git a/src/cli_cms/config.clj b/src/cli/config.clj similarity index 93% rename from src/cli_cms/config.clj rename to src/cli/config.clj index becc534..39602a7 100644 --- a/src/cli_cms/config.clj +++ b/src/cli/config.clj @@ -1,4 +1,4 @@ -(ns cli-cms.config +(ns cli.config (:require [babashka.fs :as fs] [clojure.string :as str])) diff --git a/src/cli_cms/create.clj b/src/cli/create.clj similarity index 77% rename from src/cli_cms/create.clj rename to src/cli/create.clj index 0148971..102818a 100644 --- a/src/cli_cms/create.clj +++ b/src/cli/create.clj @@ -1,16 +1,16 @@ -(ns cli-cms.create +(ns cli.create (:require [com.travisshears.gum-utils :as utils] [clojure.string :as str] - [cli-cms.config :refer [config]] + [cli.config :refer [config]] [babashka.http-client :as http] [cheshire.core :as json] [babashka.process :refer [shell]])) (defn create-snippet [{:keys [title slug markdown tags]}] (http/post (str (:backend-host config) "/api/snippet") - {:headers {:content-type "application/json"} - :body (json/encode {:title title :slug slug :markdown markdown :tags tags})})) + {:headers {:content-type "application/json"} + :body (json/encode {:title title :slug slug :markdown markdown :tags tags})})) (defn run [] (let [title (utils/prompt-for "title") diff --git a/src/cli_cms/delete.clj b/src/cli/delete.clj similarity index 91% rename from src/cli_cms/delete.clj rename to src/cli/delete.clj index 211ae14..3b0359d 100644 --- a/src/cli_cms/delete.clj +++ b/src/cli/delete.clj @@ -1,7 +1,7 @@ -(ns cli-cms.delete +(ns cli.delete (:require [com.travisshears.gum-utils :as utils] - [cli-cms.config :refer [config]] + [cli.config :refer [config]] [clojure.pprint :refer [pprint]] [babashka.http-client :as http] [cheshire.core :as json])) diff --git a/src/cli_cms/edit.clj b/src/cli/edit.clj similarity index 96% rename from src/cli_cms/edit.clj rename to src/cli/edit.clj index 4b2e847..6c16f20 100644 --- a/src/cli_cms/edit.clj +++ b/src/cli/edit.clj @@ -1,9 +1,9 @@ -(ns cli-cms.edit +(ns cli.edit (:require [babashka.http-client :as http] [clojure.pprint :refer [pprint]] - [cli-cms.config :refer [config]] - [cli-cms.view :as view] + [cli.config :refer [config]] + [cli.view :as view] [cheshire.core :as json] [com.travisshears.gum-utils :as utils])) diff --git a/src/cli_cms/main.clj b/src/cli/main.clj similarity index 74% rename from src/cli_cms/main.clj rename to src/cli/main.clj index 1f9acea..28f2b08 100644 --- a/src/cli_cms/main.clj +++ b/src/cli/main.clj @@ -1,10 +1,10 @@ -(ns cli-cms.main +(ns cli.main (:require - [cli-cms.create :as create] - [cli-cms.edit :as edit] - [cli-cms.delete :as delete] + [cli.create :as create] + [cli.edit :as edit] + [cli.delete :as delete] [com.travisshears.gum-utils :as utils] - [cli-cms.view :as view])) + [cli.view :as view])) (defn color-test [] (doseq [color (range 0 255)] diff --git a/src/cli/mcp.clj b/src/cli/mcp.clj new file mode 100644 index 0000000..9186bcf --- /dev/null +++ b/src/cli/mcp.clj @@ -0,0 +1,76 @@ +(ns cli.mcp + (:require + [cli.view :as view] + [cheshire.core :as json])) + +;; Tool definitions +(def available-tools + [{:name "list_snippets" + :description "List all available code snippets" + :inputSchema {:type "object" + :properties {} + :required []}}]) + +;; Tool implementations +(defn list-snippets-impl [] + (try + (let [snippets (view/fetch-snippets)] + {:success true + :snippets (map #(select-keys % [:id :title :slug :tags]) snippets)}) + (catch Exception e + {:success false + :error (.getMessage e)}))) + +;; Handle tools/list request +(defn handle-tools-list [id] + {:jsonrpc "2.0" + :id id + :result {:tools available-tools}}) + +;; Handle tools/call request +(defn handle-tools-call [id tool-name tool-input] + {:jsonrpc "2.0" + :id id + :result (case tool-name + "list_snippets" (list-snippets-impl) + {:error (str "Unknown tool: " tool-name)})}) + +;; Handle initialize request +(defn handle-initialize [id] + {:jsonrpc "2.0" + :id id + :result {:protocolVersion "2024-11-05" + :capabilities {:tools {:listChanged true} + :resources {:listChanged true} + :prompts {:listChanged true}} + :serverInfo {:name "snippets-server" + :version "1.0.0"}}}) + +;; Main request dispatcher +(defn handle-request [request] + (let [method (:method request) + id (:id request) + params (:params request)] + (case method + "initialize" (handle-initialize id) + "tools/list" (handle-tools-list id) + "tools/call" (handle-tools-call id (:name params) (:arguments params)) + {:jsonrpc "2.0" + :id id + :error {:code -32601 + :message (str "Method not found: " method)}}))) + +(defn -main [& args] + ;; Read JSON-RPC requests from stdin and process them + (loop [] + (let [line (try (read-line) (catch Exception _))] + (when line + (try + (let [request (json/parse-string line true) + response (handle-request request)] + (println (json/encode response))) + (catch Exception e + (println (json/encode {:jsonrpc "2.0" + :error {:code -32700 + :message "Parse error"}})))) + (recur))))) diff --git a/src/cli_cms/view.clj b/src/cli/view.clj similarity index 94% rename from src/cli_cms/view.clj rename to src/cli/view.clj index 6e30e03..9e25274 100644 --- a/src/cli_cms/view.clj +++ b/src/cli/view.clj @@ -1,8 +1,8 @@ -(ns cli-cms.view +(ns cli.view (:require [babashka.http-client :as http] [clojure.pprint :refer [pprint]] - [cli-cms.config :refer [config]] + [cli.config :refer [config]] [cheshire.core :as json] [clojure.string :as str] [com.travisshears.gum-utils :as utils]))