Skip to content

Commit 9c93283

Browse files
committed
feat(codecompanion): add image support
1 parent 7020542 commit 9c93283

File tree

8 files changed

+114
-30
lines changed

8 files changed

+114
-30
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ MCP Hub is a MCP client for neovim that seamlessly integrates [MCP (Model Contex
3737
| | Headers || For API keys/tokens |
3838
| **Chat Integration** ||||
3939
| | [Avante.nvim](https://github.com/yetone/avante.nvim) || Tools, resources, resourceTemplates, prompts(as slash_commands) |
40-
| | [CodeCompanion.nvim](https://github.com/olimorris/codecompanion.nvim) || Tools, resources, resourceTemplates, prompts (as slash_commands) |
40+
| | [CodeCompanion.nvim](https://github.com/olimorris/codecompanion.nvim) || Tools, resources, templates, prompts (as slash_commands), 🖼 image responses |
4141
| | [CopilotChat.nvim](https://github.com/CopilotC-Nvim/CopilotChat.nvim) || In-built support [Draft](https://github.com/CopilotC-Nvim/CopilotChat.nvim/pull/1029) |
4242
| **Marketplace** ||||
4343
| | Server Discovery || Browse from verified MCP servers |

doc/extensions/avante.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<video muted src="https://github.com/user-attachments/assets/e33fb5c3-7dbd-40b2-bec5-471a465c7f4d" controls></video>
55
</p>
66

7-
Add MCP capabilities to [Avante.nvim](https://github.com/yetone/avante.nvim) by including the MCP in your setup:
7+
Add MCP capabilities to [Avante.nvim](https://github.com/yetone/avante.nvim) by following these steps:
88

99
## Add Tools To Avante
1010

doc/extensions/codecompanion.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
# CodeCompanion Integration
22

33
<p>
4-
<video controls muted src="https://github.com/user-attachments/assets/cefce4bb-d07f-4423-8873-cf7d56656cd3"></video>
4+
<video muted controls src="https://github.com/user-attachments/assets/70181790-e949-4df6-a690-c5d7a212e7d1"></video>
55
</p>
66

7-
Add MCP capabilities to [CodeCompanion.nvim](https://github.com/olimorris/codecompanion.nvim) by including the MCP in your setup:
7+
Add MCP capabilities to [CodeCompanion.nvim](https://github.com/olimorris/codecompanion.nvim) by adding it as an extension.
88

99
## Features
1010

1111
- Access MCP tools via the `@mcp` tool in the chat buffer.
1212
- Utilize MCP resources as context variables using the `#` prefix (e.g., `#resource_name`).
1313
- Execute MCP prompts directly using `/mcp:prompt_name` slash commands.
14+
- Supports 🖼 images as shown in the demo.
1415
- Receive real-time updates in CodeCompanion when MCP servers change.
1516

1617
## MCP Hub Extension

doc/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Like any MCP client, MCP Hub requires a configuration file to define the MCP ser
8686
| | Headers || For API keys/tokens |
8787
| **Chat Integration** ||||
8888
| | [Avante.nvim](https://github.com/yetone/avante.nvim) || Tools, resources, resourceTemplates, prompts(as slash_commands) |
89-
| | [CodeCompanion.nvim](https://github.com/olimorris/codecompanion.nvim) || Tools, resources, resourceTemplates, prompts (as slash_commands) |
89+
| | [CodeCompanion.nvim](https://github.com/olimorris/codecompanion.nvim) || Tools, resources, templates, prompts (as slash_commands), 🖼 image responses |
9090
| | [CopilotChat.nvim](https://github.com/CopilotC-Nvim/CopilotChat.nvim) || In-built support [Draft](https://github.com/CopilotC-Nvim/CopilotChat.nvim/pull/1029) |
9191
| **Marketplace** ||||
9292
| | Server Discovery || Browse from verified MCP servers |

doc/other/demos.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
# Showcase
22

3-
## HUB UI
43

4+
## CodeCompanion + Image-gen Server (with image support 🖼 )
55
<p>
6-
<video muted controls src="https://github.com/user-attachments/assets/22d14360-5994-455b-8789-4fffd2b598e2"></video>
6+
<video muted controls src="https://github.com/user-attachments/assets/70181790-e949-4df6-a690-c5d7a212e7d1"></video>
7+
</p>
8+
9+
## CodeCompanion + Stagehand Server (with image support 🖼 )
10+
11+
<p>
12+
<video controls muted src="https://github.com/user-attachments/assets/a939be1e-7a77-495b-9727-54f110ec68b5"></video>
713
</p>
814

915
## Avante + Figma
@@ -12,12 +18,14 @@
1218
<video muted controls src="https://github.com/user-attachments/assets/e33fb5c3-7dbd-40b2-bec5-471a465c7f4d"></video>
1319
</p>
1420

15-
## Codecompanion + Todoist
21+
22+
## HUB UI
1623

1724
<p>
18-
<video muted controls src="https://github.com/user-attachments/assets/cefce4bb-d07f-4423-8873-cf7d56656cd3"></video>
25+
<video muted controls src="https://github.com/user-attachments/assets/22d14360-5994-455b-8789-4fffd2b598e2"></video>
1926
</p>
2027

28+
2129
## Marketplace
2230

2331
<p>

lua/mcphub/extensions/codecompanion/init.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ function M.create_tools(opts)
8080
vim.notify("MCP Hub is not initialized", vim.log.levels.WARN)
8181
return ""
8282
end
83+
if not hub:is_ready() then
84+
vim.notify("MCP Hub is not ready yet", vim.log.levels.WARN)
85+
return ""
86+
end
8387
local prompt = ""
8488
if not has_function_calling then
8589
local xml_tool = require("mcphub.extensions.codecompanion.xml_tool")

lua/mcphub/extensions/codecompanion/utils.lua

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,24 @@ end
8888
---@param is_error boolean
8989
---@param has_function_calling boolean
9090
---@param opts MCPHubCodeCompanionConfig
91-
local function add_tool_output(action_name, tool, chat, llm_msg, is_error, has_function_calling, opts)
91+
---@param user_msg string?
92+
---@param images {id:string, base64: string, mimetype : string, cached_file_path: string| nil}[]
93+
local function add_tool_output(action_name, tool, chat, llm_msg, is_error, has_function_calling, opts, user_msg, images)
9294
local config = require("codecompanion.config")
95+
local helpers = require("codecompanion.strategies.chat.helpers")
9396
local show_result_in_chat = opts.show_result_in_chat == true
9497
-- local text = show_result_in_chat and replace_headers(llm_msg) or llm_msg
9598
local text = llm_msg
9699
if has_function_calling then
97100
chat:add_tool_output(
98101
tool,
99102
text,
100-
(show_result_in_chat or is_error) and text
103+
(user_msg or show_result_in_chat or is_error) and (user_msg or text)
101104
or string.format("**`%s` Tool**: Successfully finished", action_name)
102105
)
106+
for _, image in ipairs(images) do
107+
helpers.add_image(chat, image)
108+
end
103109
else
104110
if show_result_in_chat or is_error then
105111
chat:add_buf_message({
@@ -143,16 +149,18 @@ function M.create_output_handlers(action_name, has_function_calling, opts)
143149
action_name,
144150
stderr
145151
)
146-
add_tool_output(action_name, self, agent.chat, err_msg, true, has_function_calling, opts)
152+
add_tool_output(action_name, self, agent.chat, err_msg, true, has_function_calling, opts, nil, {})
147153
end,
148154
success = function(self, agent, cmd, stdout)
155+
local image_cache = require("mcphub.utils.image_cache")
149156
---@type MCPResponseOutput
150157
local result = has_function_calling and stdout[#stdout] or cmd[#cmd]
151158
agent = has_function_calling and agent or self
152-
-- Show text content if present
153-
local tool_call_result_added = false
159+
local to_llm = nil
160+
local to_user = nil
161+
local images = {}
154162
if result.text and result.text ~= "" then
155-
local to_llm = string.format(
163+
to_llm = string.format(
156164
[[**`%s` Tool**: Returned the following:
157165
158166
````
@@ -161,16 +169,52 @@ function M.create_output_handlers(action_name, has_function_calling, opts)
161169
action_name,
162170
result.text
163171
)
164-
add_tool_output(action_name, self, agent.chat, to_llm, false, has_function_calling, opts)
165-
tool_call_result_added = true
166172
end
167-
if not tool_call_result_added then
168-
-- When a tool returns no text content, still send a message to
169-
-- ensure the tool_call_id protocol is satisfied
170-
local to_llm = string.format("**`%s` Tool**: Completed with no output", action_name)
171-
add_tool_output(action_name, self, agent.chat, to_llm, false, has_function_calling, opts)
173+
if result.images and #result.images > 0 then
174+
---When the mcp call returns just images, we need to add the tool output
175+
for _, image in ipairs(result.images) do
176+
local id = string.format("mcp-%s", os.time())
177+
table.insert(images, {
178+
id = id,
179+
base64 = image.data,
180+
mimetype = image.mimeType,
181+
cached_file_path = image_cache.save_image(image.data, image.mimeType),
182+
})
183+
end
184+
--- If there is no text response, add no of images returned
185+
if not to_llm then
186+
to_llm = string.format(
187+
[[**`%s` Tool**: Returned the following:
188+
````
189+
%s
190+
````]],
191+
action_name,
192+
string.format("%d image%s returned", #result.images, #result.images > 1 and "s" or "")
193+
)
194+
end
195+
to_user = to_llm .. (#images > 0 and string.format("\n\n> Preview Images\n") or "")
196+
for _, image in ipairs(images) do
197+
local file = image.cached_file_path
198+
if file then
199+
local file_name = vim.fn.fnamemodify(file, ":t")
200+
to_user = to_user .. string.format("\n![%s](%s)\n", file_name, vim.fn.fnameescape(file))
201+
else
202+
to_user = to_user .. string.format("\n![Image not saved properly](%s)\n", file)
203+
end
204+
end
172205
end
173-
-- TODO: Add image support when codecompanion supports it
206+
local fallback_to_llm = string.format("**`%s` Tool**: Completed with no output", action_name)
207+
add_tool_output(
208+
action_name,
209+
self,
210+
agent.chat,
211+
to_llm or fallback_to_llm,
212+
false,
213+
has_function_calling,
214+
opts,
215+
to_user,
216+
images
217+
)
174218
end,
175219
}
176220
end
@@ -214,7 +258,7 @@ function M.setup_codecompanion_variables(opts)
214258
description = description,
215259
callback = function(self)
216260
-- this is sync and will block the UI (can't use async in variables yet)
217-
local response = hub:access_resource(server_name, uri, {
261+
local result = hub:access_resource(server_name, uri, {
218262
caller = {
219263
type = "codecompanion",
220264
codecompanion = self,
@@ -224,7 +268,22 @@ function M.setup_codecompanion_variables(opts)
224268
},
225269
parse_response = true,
226270
})
227-
return response and response.text
271+
if not result then
272+
return string.format("Accessing resource failed: %s", uri)
273+
end
274+
275+
if result.images and #result.images > 0 then
276+
local helpers = require("codecompanion.strategies.chat.helpers")
277+
for _, image in ipairs(result.images) do
278+
local id = string.format("mcp-%s", os.time())
279+
helpers.add_image(self.Chat, {
280+
id = id,
281+
base64 = image.data,
282+
mimetype = image.mimeType,
283+
})
284+
end
285+
end
286+
return result.text
228287
end,
229288
}
230289
end
@@ -299,11 +358,10 @@ function M.setup_codecompanion_slash_commands(opts)
299358
local text_messages = 0
300359
for i, message in ipairs(messages) do
301360
local output = message.output
302-
--TODO: Currently codecompanion only supports text messages
361+
local mapped_role = message.role == "assistant" and config.constants.LLM_ROLE
362+
or message.role == "system" and config.constants.SYSTEM_ROLE
363+
or config.constants.USER_ROLE
303364
if output.text and output.text ~= "" then
304-
local mapped_role = message.role == "assistant" and config.constants.LLM_ROLE
305-
or message.role == "system" and config.constants.SYSTEM_ROLE
306-
or config.constants.USER_ROLE
307365
text_messages = text_messages + 1
308366
-- if last message is from user, add it to the chat buffer
309367
if i == #messages and mapped_role == config.constants.USER_ROLE then
@@ -318,6 +376,17 @@ function M.setup_codecompanion_slash_commands(opts)
318376
})
319377
end
320378
end
379+
if output.images and #output.images > 0 then
380+
local helpers = require("codecompanion.strategies.chat.helpers")
381+
for _, image in ipairs(output.images) do
382+
local id = string.format("mcp-%s", os.time())
383+
helpers.add_image(self, {
384+
id = id,
385+
base64 = image.data,
386+
mimetype = image.mimeType,
387+
}, { role = mapped_role })
388+
end
389+
end
321390
end
322391
vim.notify(
323392
string.format(

lua/mcphub/utils/image_cache.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ local M = {}
44
-- Cache directory
55
M.cache_dir = vim.fn.stdpath("cache") .. "/mcphub/images"
66

7+
local counter = 0
78
--- Get unique filename based on content hash
89
---@param data string Base64 encoded image data
910
---@param mime_type string MIME type of the image
1011
---@return string filename
1112
local function get_unique_filename(data, mime_type)
1213
-- local hash = vim.fn.sha256(data)
13-
local time = os.time()
14+
local time = os.time() + counter
15+
counter = counter + 1
1416
local ext = mime_type:match("image/(%w+)") or "bin"
1517
return string.format("%s.%s", time, ext)
1618
end

0 commit comments

Comments
 (0)