init MCP server

This commit is contained in:
Travis Shears 2026-02-28 19:47:00 +01:00
parent fff03fa0d5
commit 8ea0eee299
Signed by: travisshears
GPG key ID: CB9BF1910F3F7469
11 changed files with 253 additions and 21 deletions

149
MCP_SERVER_GUIDE.md Normal file
View file

@ -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!

View file

@ -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)

View file

@ -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*))

7
snippets_mcp.bb Executable file
View file

@ -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*))

View file

@ -1,4 +1,4 @@
(ns cli-cms.config
(ns cli.config
(:require
[babashka.fs :as fs]
[clojure.string :as str]))

View file

@ -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")

View file

@ -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]))

View file

@ -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]))

View file

@ -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)]

76
src/cli/mcp.clj Normal file
View file

@ -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)))))

View file

@ -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]))