Skip to content

Commit 239e7da

Browse files
committed
feat: implement resource support
- Add resource schema validation and registry - Implement resource list, read, subscribe operations - Add resource change notifications - Add resource management functions - Match implementation style of tools and prompts
1 parent dd868dc commit 239e7da

File tree

2 files changed

+180
-9
lines changed

2 files changed

+180
-9
lines changed

components/mcp-server/src/mcp_clj/mcp_server/core.clj

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
[json-rpc-server
2121
session-id->session
2222
tool-registry
23-
prompt-registry])
23+
prompt-registry
24+
resource-registry])
2425

2526
(defn- request-session-id [request]
2627
(get ((:query-params request)) "session_id"))
@@ -49,6 +50,24 @@
4950
"notifications/prompts/list_changed"
5051
nil))
5152

53+
(defn- notify-resources-changed!
54+
"Notify all sessions that the resource list has changed"
55+
[server]
56+
(log/info :server/notify-resources-changed {:server server})
57+
(json-rpc/notify-all!
58+
@(:json-rpc-server server)
59+
"notifications/resources/list_changed"
60+
nil))
61+
62+
(defn- notify-resource-updated!
63+
"Notify all sessions that a resource has been updated"
64+
[server uri]
65+
(log/info :server/notify-resource-updated {:server server :uri uri})
66+
(json-rpc/notify-all!
67+
@(:json-rpc-server server)
68+
"notifications/resources/updated"
69+
{:uri uri}))
70+
5271
(defn- text-map [msg]
5372
{:type "text" :text msg})
5473

@@ -115,9 +134,27 @@
115134

116135
(defn- handle-list-resources
117136
"Handle resources/list request from client"
118-
[_server params]
137+
[server params]
119138
(log/info :server/resources-list)
120-
(resources/list-resources params))
139+
(resources/list-resources (:resource-registry server) params))
140+
141+
(defn- handle-read-resource
142+
"Handle resources/read request from client"
143+
[server params]
144+
(log/info :server/resources-read)
145+
(resources/read-resource (:resource-registry server) params))
146+
147+
(defn- handle-subscribe-resource
148+
"Handle resources/subscribe request from client"
149+
[server params]
150+
(log/info :server/resources-subscribe)
151+
(resources/subscribe-resource (:resource-registry server) params))
152+
153+
(defn- handle-unsubscribe-resource
154+
"Handle resources/unsubscribe request from client"
155+
[server params]
156+
(log/info :server/resources-unsubscribe)
157+
(resources/unsubscribe-resource (:resource-registry server) params))
121158

122159
(defn- handle-list-prompts
123160
"Handle prompts/list request from client"
@@ -154,6 +191,9 @@
154191
"tools/list" handle-list-tools
155192
"tools/call" handle-call-tool
156193
"resources/list" handle-list-resources
194+
"resources/read" handle-read-resource
195+
"resources/subscribe" handle-subscribe-resource
196+
"resources/unsubscribe" handle-unsubscribe-resource
157197
"prompts/list" handle-list-prompts
158198
"prompts/get" handle-get-prompt}
159199
(fn [handler]
@@ -212,11 +252,30 @@
212252
@(:json-rpc-server server)
213253
(:session-id session))))
214254

255+
(defn add-resource!
256+
"Add or update a resource in a running server"
257+
[server resource]
258+
(log/info :server/add-resource!)
259+
(when-not (resources/valid-resource? resource)
260+
(throw (ex-info "Invalid resource definition" {:resource resource})))
261+
(swap! (:resource-registry server) assoc (:name resource) resource)
262+
(notify-resources-changed! server)
263+
server)
264+
265+
(defn remove-resource!
266+
"Remove a resource from a running server"
267+
[server resource-name]
268+
(log/info :server/remove-resource!)
269+
(swap! (:resource-registry server) dissoc resource-name)
270+
(notify-resources-changed! server)
271+
server)
272+
215273
(defn create-server
216274
"Create MCP server instance"
217-
[{:keys [port tools prompts]
275+
[{:keys [port tools prompts resources]
218276
:or {tools tools/default-tools
219-
prompts prompts/default-prompts}}]
277+
prompts prompts/default-prompts
278+
resources resources/default-resources}}]
220279
(doseq [tool (vals tools)]
221280
(when-not (tools/valid-tool? tool)
222281
(throw (ex-info "Invalid tool in constructor" {:tool tool}))))
@@ -226,12 +285,14 @@
226285
(let [session-id->session (atom {})
227286
tool-registry (atom tools)
228287
prompt-registry (atom prompts)
288+
resource-registry (atom resources)
229289
rpc-server-prom (promise)
230290
server (->MCPServer
231291
rpc-server-prom
232292
session-id->session
233293
tool-registry
234-
prompt-registry)
294+
prompt-registry
295+
resource-registry)
235296
json-rpc-server (json-rpc/create-server
236297
{:port port
237298
:on-sse-connect (partial on-sse-connect server)

components/mcp-server/src/mcp_clj/mcp_server/resources.clj

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,119 @@
22
"MCP resource endpoints"
33
(:require [mcp-clj.log :as log]))
44

5+
(defrecord ResourceDefinition [name uri mime-type description annotations implementation])
6+
7+
(defn- valid-string? [x]
8+
(and (string? x)
9+
(pos? (count x))))
10+
11+
(defn- valid-uri? [x]
12+
(try
13+
(java.net.URI. x)
14+
true
15+
(catch Exception _
16+
false)))
17+
18+
(defn- valid-audience-value? [x]
19+
(#{"user" "assistant"} x))
20+
21+
(defn- valid-audience? [x]
22+
(and (vector? x)
23+
(every? valid-audience-value? x)))
24+
25+
(defn- valid-priority? [x]
26+
(and (number? x)
27+
(<= 0 x 1)))
28+
29+
(defn- valid-annotations? [annotations]
30+
(and (map? annotations)
31+
(or (nil? (:priority annotations))
32+
(valid-priority? (:priority annotations)))
33+
(or (nil? (:audience annotations))
34+
(valid-audience? (:audience annotations)))))
35+
36+
(defn valid-resource?
37+
"Validate a resource definition.
38+
Returns true if valid, throws ex-info with explanation if not."
39+
[{:keys [name uri mime-type annotations] :as resource}]
40+
(when-not (valid-string? name)
41+
(throw (ex-info "name must be a non-empty string"
42+
{:type :validation-error
43+
:field :name
44+
:value name
45+
:resource resource})))
46+
(when-not (valid-uri? uri)
47+
(throw (ex-info "uri must be a valid URI string"
48+
{:type :validation-error
49+
:field :uri
50+
:value uri
51+
:resource resource})))
52+
(when (and mime-type (not (valid-string? mime-type)))
53+
(throw (ex-info "mime-type must be a non-empty string"
54+
{:type :validation-error
55+
:field :mime-type
56+
:value mime-type
57+
:resource resource})))
58+
(when (and annotations (not (valid-annotations? annotations)))
59+
(throw (ex-info "invalid annotations"
60+
{:type :validation-error
61+
:field :annotations
62+
:value annotations
63+
:resource resource})))
64+
true)
65+
66+
(defn resource-definition
67+
"Convert a ResourceDefinition to the wire format"
68+
[{:keys [name uri mime-type description annotations]}]
69+
(cond-> {:name name
70+
:uri uri}
71+
mime-type (assoc :mimeType mime-type)
72+
description (assoc :description description)
73+
annotations (assoc :annotations annotations)))
74+
75+
(def default-resources {})
76+
577
(defn list-resources
678
"List available resources.
7-
Returns empty list for current implementation."
8-
[params]
79+
Returns a map with :resources containing resource definitions."
80+
[registry params]
981
(log/info :resources/list {:params params})
10-
{:resources []})
82+
{:resources (mapv resource-definition (vals @registry))})
83+
84+
(defn- read-resource-impl
85+
"Default implementation for reading a resource"
86+
[{:keys [implementation] :as resource} uri]
87+
(if implementation
88+
(implementation uri)
89+
{:contents [{:uri uri
90+
:text "Resource not implemented"}]
91+
:isError true}))
92+
93+
(defn read-resource
94+
"Read a resource by URI.
95+
Returns contents of the resource."
96+
[registry {:keys [uri] :as params}]
97+
(log/info :resources/read {:params params})
98+
(if-let [resource (some #(when (= uri (:uri %)) %) (vals @registry))]
99+
(read-resource-impl resource uri)
100+
{:contents [{:uri uri
101+
:text "Resource not found"}]
102+
:isError true}))
103+
104+
(defn subscribe-resource
105+
"Subscribe to updates for a resource.
106+
Returns empty result if successful, error if resource not found."
107+
[registry {:keys [uri] :as params}]
108+
(log/info :resources/subscribe {:params params})
109+
(if (some #(when (= uri (:uri %)) %) (vals @registry))
110+
{}
111+
{:content [{:type "text"
112+
:text (str "Resource not found: " uri)}]
113+
:isError true}))
114+
115+
(defn unsubscribe-resource
116+
"Unsubscribe from updates for a resource.
117+
Returns empty result if successful."
118+
[_registry {:keys [uri] :as params}]
119+
(log/info :resources/unsubscribe {:params params})
120+
{})

0 commit comments

Comments
 (0)