Skip to content

Commit 6de7a34

Browse files
committed
Support MCP resources as a new context
1 parent 9debee7 commit 6de7a34

File tree

9 files changed

+120
-35
lines changed

9 files changed

+120
-35
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Support MCP resources as a new context.
6+
57
## 0.14.4
68

79
- Fix usage miscalculation.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ _Demo using [eca-vscode](https://github.com/editor-code-assistant/eca-vscode)_
2828
- :gear: **Single configuration**: Configure eca making it work the same in any editor via global or local configs.
2929
- :loop: **Chat** interface: ask questions, review code, work together to code.
3030
- :coffee: **Agentic**: let LLM work as an agent with its native tools and MCPs you can configure.
31-
- :syringe: **Context**: support: giving more details about your code to the LLM.
31+
- :syringe: **Context**: support: giving more details about your code to the LLM, including MCP resources and prompts.
3232
- :rocket: **Multi models**: OpenAI, Anthropic, Ollama local models, and custom user config models.
3333

3434
## Rationale

docs/features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Provides access to run shell commands, useful to run build tools, tests, and oth
3535

3636
### Contexts
3737

38-
User can include contexts to the chat, which can help LLM generate output with better quality.
38+
User can include contexts to the chat, including MCP resources, which can help LLM generate output with better quality.
3939
Here are the current supported contexts types:
4040

4141
- `file`: a file in the workspace, server will pass its content to LLM (Supports optional line range).

docs/models.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
## Built-in providers and capabilities
44

5-
| model | MCP / tools | MCP prompts | thinking/reasioning | prompt caching | web_search |
6-
|-----------|-------------|-------------|---------------------|----------------|------------|
7-
| OpenAI ||| X | X ||
8-
| Anthropic ||| X |||
9-
| Ollama ||| X | X | X |
5+
| model | tools (MCP) | reasoning / thinking | prompt caching | web_search |
6+
|-----------|-------------|----------------------|----------------|------------|
7+
| OpenAI || | ||
8+
| Anthropic |||||
9+
| Ollama ||| X | X |
1010

1111
### OpenAI
1212

docs/protocol.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ type ChatModel =
278278
*/
279279
type OllamaRunningModel = string
280280

281-
type ChatContext = FileContext | DirectoryContext | WebContext | RepoMapContext;
281+
type ChatContext = FileContext | DirectoryContext | WebContext | RepoMapContext | McpResourceContext;
282282

283283
/**
284284
* Context related to a file in the workspace
@@ -328,7 +328,39 @@ interface WebContext {
328328
*/
329329
interface RepoMapContext {
330330
type: 'repoMap';
331-
}
331+
}
332+
333+
/***
334+
* A MCP resource available from a MCP server.
335+
*/
336+
interface McpResourceContext {
337+
type: 'mcpResource';
338+
339+
/**
340+
* The URI of the resource like file://foo/bar.clj
341+
*/
342+
uri: string;
343+
344+
/**
345+
* The name of the resource.
346+
*/
347+
name: string;
348+
349+
/**
350+
* The description of the resource.
351+
*/
352+
description: string;
353+
354+
/**
355+
* The mimeType of the resource like `text/markdown`.
356+
*/
357+
mimeType: string;
358+
359+
/**
360+
* The server name of this MCP resource.
361+
*/
362+
server: string;
363+
}
332364
```
333365

334366
_Response:_

src/eca/features/chat.clj

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818

1919
(def ^:private logger-tag "[CHAT]")
2020

21-
(defn ^:private raw-contexts->refined [contexts]
22-
(mapcat (fn [{:keys [type path lines-range]}]
23-
(case type
21+
(defn ^:private raw-contexts->refined [contexts db]
22+
(mapcat (fn [{:keys [type path lines-range uri]}]
23+
(case (name type)
2424
"file" [{:type :file
2525
:path path
2626
:partial (boolean lines-range)
@@ -32,7 +32,13 @@
3232
{:type :file
3333
:path filename
3434
:content (llm-api/refine-file-context filename nil)}))))
35-
"repoMap" [{:type :repoMap}]))
35+
"repoMap" [{:type :repoMap}]
36+
"mcpResource" (mapv
37+
(fn [{:keys [text]}]
38+
{:type :mcpResource
39+
:uri uri
40+
:content text})
41+
(:contents (f.mcp/get-resource! uri db)))))
3642
contexts))
3743

3844
(defn default-model [db config]
@@ -138,7 +144,7 @@
138144
(let [db @db*
139145
manual-approval? (get-in config [:toolCall :manualApproval] false)
140146
rules (f.rules/all config (:workspace-folders db))
141-
refined-contexts (raw-contexts->refined contexts)
147+
refined-contexts (raw-contexts->refined contexts db)
142148
repo-map* (delay (f.index/repo-map db {:as-string? true}))
143149
instructions (f.prompt/build-instructions refined-contexts rules repo-map* (or behavior (:chat-default-behavior db)))
144150
past-messages (get-in db [:chats chat-id :messages] [])
@@ -362,16 +368,18 @@
362368
(take 200) ;; for performance, user can always make query specific for better results.
363369
(map (fn [file-or-dir]
364370
{:type (if (fs/directory? file-or-dir)
365-
"directory"
366-
"file")
371+
:directory
372+
:file)
367373
:path (str (fs/canonicalize file-or-dir))})))
368374
(:workspace-folders @db*))
369-
root-dirs (mapv (fn [{:keys [uri]}] {:type "directory"
375+
root-dirs (mapv (fn [{:keys [uri]}] {:type :directory
370376
:path (shared/uri->filename uri)})
371377
(:workspace-folders @db*))
372-
all-contexts (concat [{:type "repoMap"}]
378+
mcp-resources (mapv #(assoc % :type :mcpResource) (f.mcp/all-resources @db*))
379+
all-contexts (concat [{:type :repoMap}]
373380
root-dirs
374-
all-subfiles-and-dirs)]
381+
all-subfiles-and-dirs
382+
mcp-resources)]
375383
{:chat-id chat-id
376384
:contexts (set/difference (set all-contexts)
377385
(set contexts))}))

src/eca/features/prompt.clj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@
3131
""
3232
"<contexts>"
3333
(reduce
34-
(fn [context-str {:keys [type path content partial]}]
34+
(fn [context-str {:keys [type path content partial uri]}]
3535
(str context-str (case type
3636
:file (if partial
3737
(format "<file partial=true path=\"%s\">...\n%s\n...</file>\n" path content)
3838
(format "<file path=\"%s\">%s</file>\n" path content))
39-
:repoMap (format "<repoMap description=\"Workspaces structure in a tree view, spaces represent file hierarchy\" >%s</repoMap>" @repo-map*)
39+
:repoMap (format "<repoMap description=\"Workspaces structure in a tree view, spaces represent file hierarchy\" >%s</repoMap>\n" @repo-map*)
40+
:mcpResource (format "<resource uri=\"%s\">%s</resource>\n" uri content)
4041
"")))
4142
""
4243
refined-contexts)

src/eca/features/tools/mcp.clj

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@
1818
McpSchema$Prompt
1919
McpSchema$PromptArgument
2020
McpSchema$PromptMessage
21+
McpSchema$ReadResourceRequest
22+
McpSchema$Resource
23+
McpSchema$ResourceContents
2124
McpSchema$Root
2225
McpSchema$TextContent
26+
McpSchema$TextResourceContents
2327
McpSchema$Tool
2428
McpTransport]
2529
[java.time Duration]
@@ -74,6 +78,8 @@
7478
:command (:command server-config)
7579
:args (:args server-config)
7680
:tools (get-in db [:mcp-clients mcp-name :tools])
81+
:prompts (get-in db [:mcp-clients mcp-name :prompts])
82+
:resources (get-in db [:mcp-clients mcp-name :resources])
7783
:status status})
7884

7985
(defn ^:private ->content [^McpSchema$Content content-client]
@@ -82,6 +88,16 @@
8288
:text (.text ^McpSchema$TextContent content-client)}
8389
nil))
8490

91+
(defn ^:private ->resource-content [^McpSchema$ResourceContents resource-content-client]
92+
(cond
93+
(instance? McpSchema$TextResourceContents resource-content-client)
94+
{:type :text
95+
:uri (.uri resource-content-client)
96+
:text (.text ^McpSchema$TextResourceContents resource-content-client)}
97+
98+
:else
99+
nil))
100+
85101
(defn ^:private list-server-tools [^ObjectMapper obj-mapper ^McpSyncClient client]
86102
(mapv (fn [^McpSchema$Tool tool-client]
87103
{:name (.name tool-client)
@@ -102,6 +118,25 @@
102118
(.arguments prompt-client))})
103119
(.prompts (.listPrompts client))))
104120

121+
(defn ^:private list-server-resources [^McpSyncClient client]
122+
(try
123+
(mapv (fn [^McpSchema$Resource resource-client]
124+
{:uri (.uri resource-client)
125+
:name (.name resource-client)
126+
:description (.description resource-client)
127+
:mime-type (.mimeType resource-client)})
128+
(.resources (.listResources client)))
129+
(catch Exception e
130+
(logger/debug logger-tag "Could not list resources:" (.getMessage e))
131+
[])))
132+
133+
(defn ^:private mcp-client-from-db [pred db]
134+
(->> (vals (:mcp-clients db))
135+
(keep (fn [{:keys [client resources]}]
136+
(when (some pred resources)
137+
client)))
138+
first))
139+
105140
(defn ^:private initialize-server! [name db* config on-server-updated]
106141
(let [db @db*
107142
workspaces (:workspace-folders @db*)
@@ -117,6 +152,7 @@
117152
(.initialize client)
118153
(swap! db* assoc-in [:mcp-clients name :tools] (list-server-tools obj-mapper client))
119154
(swap! db* assoc-in [:mcp-clients name :prompts] (list-server-prompts client))
155+
(swap! db* assoc-in [:mcp-clients name :resources] (list-server-resources client))
120156
(on-server-updated (->server name server-config :running @db*)))
121157
(catch Exception e
122158
(logger/warn logger-tag (format "Could not initialize MCP server %s. Error: %s" name (.getMessage e)))
@@ -156,11 +192,7 @@
156192
(:mcp-clients db)))
157193

158194
(defn call-tool! [^String name ^Map arguments db]
159-
(let [mcp-client (->> (vals (:mcp-clients db))
160-
(keep (fn [{:keys [client tools]}]
161-
(when (some #(= name (:name %)) tools)
162-
client)))
163-
first)
195+
(let [mcp-client (mcp-client-from-db #(= name (:name %)) db)
164196
result (try
165197
(let [result (.callTool ^McpSyncClient mcp-client
166198
(McpSchema$CallToolRequest. name arguments))]
@@ -179,12 +211,14 @@
179211
(mapv #(assoc % :server (name server-name)) prompts)))
180212
(:mcp-clients db)))
181213

214+
(defn all-resources [db]
215+
(into []
216+
(mapcat (fn [[server-name {:keys [resources]}]]
217+
(mapv #(assoc % :server (name server-name)) resources)))
218+
(:mcp-clients db)))
219+
182220
(defn get-prompt! [^String name ^Map arguments db]
183-
(let [mcp-client (->> (vals (:mcp-clients db))
184-
(keep (fn [{:keys [client prompts]}]
185-
(when (some #(= name (:name %)) prompts)
186-
client)))
187-
first)
221+
(let [mcp-client (mcp-client-from-db #(= name (:name %)) db)
188222
prompt (.getPrompt ^McpSyncClient mcp-client (McpSchema$GetPromptRequest. name arguments))
189223
result {:description (.description prompt)
190224
:messages (mapv (fn [^McpSchema$PromptMessage message]
@@ -194,8 +228,14 @@
194228
(logger/debug logger-tag "Prompt result:" result)
195229
result))
196230

231+
(defn get-resource! [^String uri db]
232+
(let [mcp-client (mcp-client-from-db #(= uri (:uri %)) db)
233+
resource (.readResource ^McpSyncClient mcp-client (McpSchema$ReadResourceRequest. uri))
234+
result {:contents (mapv ->resource-content (.contents resource))}]
235+
(logger/debug logger-tag "Resource result:" result)
236+
result))
237+
197238
(defn shutdown! [db*]
198-
(doseq [[_name {:keys [_client]}] (:mcp-clients @db*)]
199-
;; TODO NoClassDefFound being thrown for some reason
200-
#_(.closeGracefully ^McpSyncClient client))
239+
(doseq [[_name {:keys [client]}] (:mcp-clients @db*)]
240+
(.closeGracefully ^McpSyncClient client))
201241
(swap! db* assoc :mcp-clients {}))

test/eca/features/prompt_test.clj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
(testing "Should create instructions with rules, contexts, and behavior"
99
(let [refined-contexts [{:type :file :path "foo.clj" :content "(ns foo)"}
1010
{:type :file :path "bar.clj" :content "(def a 1)" :partial true}
11-
{:type :repoMap :path nil :content nil}]
11+
{:type :repoMap}
12+
{:type :mcpResource :uri "custom://my-resource" :content "some-cool-content"}]
1213
rules [{:name "rule1" :content "First rule"}
1314
{:name "rule2" :content "Second rule"}]
1415
fake-repo-map (delay "TREE")
@@ -22,5 +23,6 @@
2223
(is (string/includes? result "<file path=\"foo.clj\">(ns foo)</file>"))
2324
(is (string/includes? result "<file partial=true path=\"bar.clj\">...\n(def a 1)\n...</file>"))
2425
(is (string/includes? result "<repoMap description=\"Workspaces structure in a tree view, spaces represent file hierarchy\" >TREE</repoMap>"))
26+
(is (string/includes? result "<resource uri=\"custom://my-resource\">some-cool-content</resource>"))
2527
(is (string/includes? result "</contexts>"))
2628
(is (string? result)))))

0 commit comments

Comments
 (0)