feat(context)!: switch from contexts to function calling
This change modernizes the CopilotChat architecture by implementing a tools-based approach using function calling. Each tool has a schema definition that enables structured parameter collection and validation. Resources are now handled more consistently, with a clearer distinction between content types.
https://platform.openai.com/docs/guides/function-calling?api-mode=responses https://modelcontextprotocol.info/specification/2024-11-05/server/tools/
BREAKING CHANGE: The context API has changed from callback-based input handling to schema-based definitions. BREAKING CHANGE: config.contexts renamed to config.tools BREAKING CHANGE: config.context removed, use config.sticky BREAKING CHANGE: diagnostics moved to separate tool call, selection and buffer calls no longer include them by default BREAKING CHANGE: non-resource based tool calls can no longer be soft stored in sticky, now they are auto expanded to promot BREAKING CHANGE: viewing full context is no longer possible (as now tools can have bigger side effects), gi renamed to gc, now also includes selection BREAKING CHANGE: filenames renamed to glob BREAKING CHANGE: files removed (use glob together with tool calling instead, or buffers/quickfix) BREAKING CHANGE: copilot extension agents removed, tools + mcp servers can replace this feature and maintaining them was pain, they can still be implemented via custom providers anyway BREAKING CHANGE: actions and integrations action removed as they were deprecated for a while
Closes #1045 Closes #1053 Closes #1076 Closes #1090 Closes #1096 Closes #526
TODO:
- [x] Improve response format from contexts
- [x] Maybe rename contexts to tools?
- [x] Add names of tool to embedded context value
- [x] Inline contexts/tools - expand directly to user prompt
- [x] Split diagnostics to separate context
- [x] Mark built in tools as safe, mark everything else unsafe
- [x] How to resolve MCP resources? How to resolve built in resources? Currently hardcoded for file and url, not good
- [x] Rest of commands resources for reuse? git?
- [ ] Properly store tool responses using "tool" role and also send tool_calls in history
- When tool_call response is received, new tool_call prompt, user submitable, editable, sends only tools
- On submit replace with tool_call responses, send proper "tool" role, wait response
- [ ] Parse resource template to schema for input, pass uri to input for resolution
- [x] Rename agent to group, allow sharing single tools as well
https://github.com/ravitemer/mcphub.nvim integration:
local mcp = require('mcphub')
mcp.setup()
mcp.on({ 'servers_updated', 'tool_list_changed', 'resource_list_changed' }, function()
local hub = mcp.get_hub_instance()
if not hub then
return
end
local async = require('plenary.async')
local call_tool = async.wrap(function(server, tool, input, callback)
hub:call_tool(server, tool, input, {
callback = function(res, err)
callback(res, err)
end,
})
end, 4)
local access_resource = async.wrap(function(server, uri, callback)
hub:access_resource(server, uri, {
callback = function(res, err)
callback(res, err)
end,
})
end, 3)
local resources = hub:get_resources()
for _, resource in ipairs(resources) do
local name = resource.name:lower():gsub(' ', '_'):gsub(':', '')
chat.config.functions[name] = {
uri = resource.uri,
description = type(resource.description) == 'string' and resource.description or '',
resolve = function()
local res, err = access_resource(resource.server_name, resource.uri)
if err then
error(err)
end
res = res or {}
local result = res.result or {}
local content = result.contents or {}
local out = {}
for _, message in ipairs(content) do
if message.text then
table.insert(out, {
uri = message.uri,
data = message.text,
mimetype = message.mimeType,
})
end
end
return out
end
}
end
local tools = hub:get_tools()
for _, tool in ipairs(tools) do
chat.config.functions[tool.name] = {
group = tool.server_name,
description = tool.description,
schema = tool.inputSchema,
resolve = function(input)
local res, err = call_tool(tool.server_name, tool.name, input)
if err then
error(err)
end
res = res or {}
local result = res.result or {}
local content = result.content or {}
local out = {}
for _, message in ipairs(content) do
if message.type == 'text' then
table.insert(out, {
data = message.text
})
elseif message.type == 'resource' and message.resource and message.resource.text then
table.insert(out, {
uri = message.resource.uri,
data = message.resource.text,
mimetype = message.resource.mimeType,
})
end
end
return out
end
}
end
end)
Hi, I see you mentioned MCP in the PR so I want to ask: is it planned to support MCP at some point? there's been some effort in the neovim community (mcphub.nvim) and there's experimental MCP support from VectorCode too. With MCP support there'll be a much wider tool/context available to CopilotChat (and we'll no longer need to maintain the context_provider in VectorCode).
Hi, I see you mentioned MCP in the PR so I want to ask: is it planned to support MCP at some point? there's been some effort in the neovim community (mcphub.nvim) and there's experimental MCP support from VectorCode too. With MCP support there'll be a much wider tool/context available to CopilotChat (and we'll no longer need to maintain the
context_providerin VectorCode).
yes the plan is to support mcp servers directly and list tools from there and support exec as well, in this PR ideally (probably new mcp { } config with similar structure as spec)
Getting close to having something that actually works:
a bit more progress, looking nice
Works quite nicely with mcphub as well, still need to deal with converting output but close enough:
require('mcphub').setup({
on_ready = function(hub)
local async = require('plenary.async')
local call_tool = async.wrap(function(server, tool, input, callback)
hub:call_tool(server, tool, input, {
callback = function(response)
callback(response)
end,
})
end, 4)
local tools = hub:get_tools()
for _, tool in ipairs(tools) do
chat.config.tools[tool.name] = {
description = tool.description,
schema = tool.inputSchema,
resolve = function(input, source)
local out = call_tool(tool.server, tool.name, input)
vim.print(out)
return {}
end
}
end
end
})
ok outputs working nice
https://github.com/github/github-mcp-server example:
There are still few issues that I need to think about before merging this even though technically this works:
MCP is very badly designed protocol imo and you have to assume that every single tool has sideeffects because there is no such thing as specifying if tool has side effects or not in the spec. But there are tools that only retrieve resources or text output and are read only (in fact most of them do this but because of few that modify filesystem and do some other dangerous stuff I have to assume every single tool will do this).
This makes tool-first approach kinda dangerous and annoying, but vscode did basically identical thing as me when implementing tools, e.g both LLM can call them and user can call them manually, even though based on MCP spec this should be discouraged, but because the alternative is resources which compared to tools have only very basic spec its something that makes most sense (also looking at some mcp servers, a lot of them basically only give you usable tools and not resources).
At minimum I will probably have to split context and tools, and only support resources for context, and assume every single tool is dangerous to use (especially the external ones provided by MCP servers). I am already assuming this in current implementation, im just missing the split atm, but the fact that some LLM integrations allow you to run this stuff automatically even as an option is a bit worrying.
But the annoying thing about this is auto discovery, because for example openai (and also claude) only supports tools in auto discovery, so I would need both tool that can for example retrieve a file, and something else that has guarantee to be safe operation, which is why both current PR and vscode implementation just uses tools for both
EDIT:
Alternatively i can just mark the built-in tools safe (which are basically current contexts) and mark everything else as unsafe + add #resource tool that can be used to resolve resource references and fill in provided resources. Maybe this would be cleanest.
Ok finally figured out nice way how to structure this, i renamed tools to functions and merged tools and resources together, example of tool + function:
file = {
agent = 'copilot',
uri = 'file://{path}',
description = 'Reads content from a specified file path, even if the file is not currently loaded as a buffer.',
schema = {
type = 'object',
required = { 'path' },
properties = {
path = {
type = 'string',
description = 'Path to file to include in chat context.',
enum = function(source)
return utils.glob(source.cwd(), {
max_count = 0,
})
end,
},
},
},
resolve = function(input)
local data, mimetype = resources.get_file(input.path)
if not data then
error('File not found: ' .. input.path)
end
return {
{
uri = 'file://' .. input.path,
mimetype = mimetype,
data = data,
},
}
end,
},
agent means that it can be shared via @copilot as tool and it having uri it means it can be embedded as resource as well (via #file:<filename> or dynamically resolved via ##file://{path}, e.g when being returned from tool).
So remaining stuff is just some cleanup + documentation, i am probs skipping prompts from MCP for now and will try to finish this this weekend.
I realized i kinda forgot to put tool calls and tool call responses to history properly so I will probably need to adjust that as well
👋🏻 I've set up:
- your branch of CopilotChat.nvim
- the example mcphub config in the description
- mcphub (4.11, 5.0 is out today but that seemed like it might be a problem)
- 2 different MCP Servers (github-mcp, kagi-mcp), 2 different Native Servers (MCPHub and Neovim)
I'm unsure how to tie CopilotChat into these tools. I see in screenshots above you do can you show me my pull requests however Copilot (claude 3.7 sonnet) cannot see any tools. Are there configurations that I need to change to my CopilotChat config (beyond the event listener in the description lua code) to link these two up?
👋🏻 I've set up:
- your branch of CopilotChat.nvim
- the example mcphub config in the description
- mcphub (4.11, 5.0 is out today but that seemed like it might be a problem)
- 2 different MCP Servers (github-mcp, kagi-mcp), 2 different Native Servers (MCPHub and Neovim)
I'm unsure how to tie CopilotChat into these tools. I see in screenshots above you do
can you show me my pull requestshowever Copilot (claude 3.7 sonnet) cannot see any tools. Are there configurations that I need to change to my CopilotChat config (beyond the event listener in the description lua code) to link these two up?
you need to reference the tool or mcp server in chat with @, so for example @github etc, depends on how you named the server in your config
@deathbeam thanks that makes perfect sense! I'm close, I just don't seem to be getting the response? My first line was @github can you show me my prs? and it [properly I assume] removed the @github.
I have Auto Approve: ON in MCPHub so I expected it to execute the get_me line. When it put the #get_me:tooluse... in my chat it just sat, after a few minutes I hit my submit keybinding and then that's how I got the ## Copilot response below it.
## User ───
can you show me my prs?
## Copilot ───
I'll help you retrieve your pull requests. Let me get information about your GitHub account first.
## User ───
#get_me:tooluse_CiBS6aLDQ4iuSXaXqb-HWw
## Copilot ───
I don't have the ability to access your GitHub pull requests directly. To view your open PRs, you would need to use the GitHub CLI (`gh`) or check them in a web browser.
If you have the GitHub CLI installed, you can run:
\```bash
gh pr list --author @me
\```
Or visit GitHub in your browser at https://github.com/pulls to see all your open pull requests across repositories.
im currently in process of changing how tools are used, but for now, you need to also make the tool reference sticky, so add
> @github
etc
Gotcha, that works! I'll try to do some debugging if I can find some time today, just had an exception at https://github.com/CopilotC-Nvim/CopilotChat.nvim/pull/1029/files#diff-7a2411a15623062457419a57f69def5a63e571e69f65b413b402a4a372c056a7R343 I may have something misconfigured though so unless it sparks immediate "oh this is wrong" I'll look into it. I not expecting you to provide me support on your PR, just hoping to help test, validate, and learn.
Error executing vim.schedule lua callback: ...mPackages/start/plenary.nvim/lua/plenary/async/async.lua:18: The coroutine failed with this message: ...Packages/start/CopilotChat.nvim/lua/CopilotChat/init.lua:343:
invalid capture index
stack traceback:
[C]: in function 'error'
...mPackages/start/plenary.nvim/lua/plenary/async/async.lua:18: in function 'callback_or_next'
...mPackages/start/plenary.nvim/lua/plenary/async/async.lua:45: in function 'callback'
/home/tebriel/.config/nvim/init.lua:146: in function 'original_callback'
...mPackages/start/vimplugin-mcphub-nvim/lua/mcphub/hub.lua:432: in function 'callback'
...mPackages/start/vimplugin-mcphub-nvim/lua/mcphub/hub.lua:659: in function 'process_response'
...mPackages/start/vimplugin-mcphub-nvim/lua/mcphub/hub.lua:670: in function ''
vim/_editor.lua: in function <vim/_editor.lua:0>
Yea i noticed that issue as well, I think it has something to do with weird filenames (good example is jdt:// stuff in java decompiled classes etc)
I'll take a crack at the pattern stuff I do enjoy a good regex.
@deathbeam Thought I'd just check in to see if continued development is planned here, or if it is on pause given the vscode move to open source (i.e. better to just copy their behavior?).
@deathbeam Thought I'd just check in to see if continued development is planned here, or if it is on pause given the vscode move to open source (i.e. better to just copy their behavior?).
Yea I want to finish this just did not had much time or motivation lately, I already had pretty good idea how vscode worked and had something that behaved in similar way as vscode before but then I changed it because how it worked wasnt very good (as they basically dont support resources at all there).
@deathbeam Is there any low-hanging fruit we could help out with to get this back into the mainline repo?
Hey I'm seeing that chat is not defined when using the MCP integration code above.
I think this is a fix for the snippet above, but I want to have some eyes on it. Also I'm not 100% sure how this actually automatically allows CopilotChat to make automated changes. Perhaps we can enhance the README to contain this information?
-- Integrates mcphub with CopilotChat, dynamically registering tools and resources as CopilotChat functions.
-- Listens for mcphub events to update available tools/resources in CopilotChat.
local chat = require('CopilotChat')
local mcp = require('mcphub')
-- Initialize mcphub plugin.
mcp.setup()
-- Register event handler for mcphub updates.
mcp.on({ 'servers_updated', 'tool_list_changed', 'resource_list_changed' }, function()
local hub = mcp.get_hub_instance()
if not hub then
return
end
-- Import plenary async utilities.
local async = require('plenary.async')
-- Wraps hub:call_tool in an async function for CopilotChat tool invocation.
-- @param server string: Server name
-- @param tool string: Tool name
-- @param input table: Tool input
-- @param callback function: Callback to receive result and error
local call_tool = async.wrap(function(server, tool, input, callback)
hub:call_tool(server, tool, input, {
callback = function(res, err)
callback(res, err)
end,
})
end, 4)
-- Wraps hub:access_resource in an async function for CopilotChat resource access.
-- @param server string: Server name
-- @param uri string: Resource URI
-- @param callback function: Callback to receive result and error
local access_resource = async.wrap(function(server, uri, callback)
hub:access_resource(server, uri, {
callback = function(res, err)
callback(res, err)
end,
})
end, 3)
-- Register resources as CopilotChat functions.
local resources = hub:get_resources()
for _, resource in ipairs(resources) do
local name = resource.name:lower():gsub(' ', '_'):gsub(':', '')
if not chat.config.functions then
chat.config.functions = {}
end
chat.config.functions[name] = {
uri = resource.uri,
description = type(resource.description) == 'string' and resource.description or '',
-- Resolves the resource and returns its content for CopilotChat.
resolve = function()
local res, err = access_resource(resource.server_name, resource.uri)
if err then
error(err)
end
res = res or {}
local result = res.result or {}
local content = result.contents or {}
local out = {}
for _, message in ipairs(content) do
if message.text then
table.insert(out, {
uri = message.uri,
data = message.text,
mimetype = message.mimeType,
})
end
end
return out
end
}
end
-- Register tools as CopilotChat functions.
local tools = hub:get_tools()
for _, tool in ipairs(tools) do
chat.config.functions[tool.name] = {
group = tool.server_name,
description = tool.description,
schema = tool.inputSchema,
-- Resolves the tool invocation and returns its output for CopilotChat.
-- @param input table: Tool input
resolve = function(input)
local res, err = call_tool(tool.server_name, tool.name, input)
if err then
error(err)
end
res = res or {}
local result = res.result or {}
local content = result.content or {}
local out = {}
for _, message in ipairs(content) do
if message.type == 'text' then
table.insert(out, {
data = message.text
})
elseif message.type == 'resource' and message.resource and message.resource.text then
table.insert(out, {
uri = message.resource.uri,
data = message.resource.text,
mimetype = message.resource.mimeType,
})
end
end
return out
end
}
end
end)
did not rly had time to work on this at all in past few months but I think I got nice interface now working with separate tool call message and proper highlights so its clearly visible what is happening
So only 1 thing remains and that is better resource template handling, but I think I can just skip it for now, so will test this a bit more (any more testing would be helpful too) and then probably merge, but its a lot of breaking changes to configs and everything
Well lets see if this works :)