mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Exposes the experimental Coder Agents chats API through the MCP tool registry, so MCP clients (the hosted `/api/experimental/mcp/http` server and `coder exp mcp server`) can start and drive server-side coding agents. New tools in `codersdk/toolsdk`, all thin wrappers over existing `codersdk.ExperimentalClient` methods: | Tool | Wraps | |---|---| | `coder_create_chat` | `CreateChat` (prompt, optional org, model config, labels) | | `coder_get_chat` | `GetChat` (status, last error, last turn summary, workspace, files) | | `coder_get_chat_messages` | `GetChatMessages` (user-facing parts, chronological, cursor pagination, queued prompts) | | `coder_send_chat_message` | `CreateChatMessage` (queue or interrupt busy behavior) | | `coder_interrupt_chat` | `InterruptChat` | | `coder_archive_chat` | `UpdateChat` with `archived: true` | | `coder_list_chat_model_configs` | `ListChatModelConfigs` (enabled configs with default flag) | Both MCP servers register tools from `toolsdk.All`, so no additional wiring is needed. Responses are trimmed to what an MCP caller needs (IDs as strings, user-facing transcripts) rather than full SDK payloads. No new endpoints and no database changes. Also adds MCP [prompts](https://modelcontextprotocol.io/specification/2026-07-28/server/prompts) for the chat workflows, defined once in `codersdk/toolsdk` and registered by both servers: | Prompt | Purpose | |---|---| | `coder_agents_delegate` | delegate a task to a Coder Agents chat and monitor it to completion | | `coder_agents_check` | check the status and recent activity of an existing chat | Each prompt declares the tools its workflow needs; the stdio server skips prompts whose tools are excluded by `--allowed-tools`. Tests run the tools against a chat-enabled coderdtest instance (fake OpenAI-compatible provider plus in-process AI bridge), covering the full lifecycle, an interrupt against a blocked turn, pagination cursors, permission-dependent model config filtering, and argument validation. Prompt coverage spans SDK rendering, the hosted `prompts/list`/`prompts/get` round trip, and the stdio server including allowlist gating. > Mux created this PR on Mike's behalf.
83 lines
2.6 KiB
Go
83 lines
2.6 KiB
Go
package toolsdk_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/codersdk/toolsdk"
|
|
)
|
|
|
|
func TestChatPrompts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("Metadata", func(t *testing.T) {
|
|
t.Parallel()
|
|
names := map[string]bool{}
|
|
for _, prompt := range toolsdk.AllPrompts {
|
|
require.NotEmpty(t, prompt.Name)
|
|
require.NotEmpty(t, prompt.Description)
|
|
require.NotNil(t, prompt.Render)
|
|
require.NotEmpty(t, prompt.RequiredTools)
|
|
toolNames := make(map[string]bool, len(toolsdk.All))
|
|
for _, tool := range toolsdk.All {
|
|
toolNames[tool.Name] = true
|
|
}
|
|
for _, name := range prompt.RequiredTools {
|
|
require.True(t, toolNames[name], "prompt %q requires unknown tool %q", prompt.Name, name)
|
|
}
|
|
require.False(t, names[prompt.Name], "duplicate prompt name %q", prompt.Name)
|
|
names[prompt.Name] = true
|
|
for _, arg := range prompt.Arguments {
|
|
require.NotEmpty(t, arg.Name)
|
|
require.NotEmpty(t, arg.Description)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("DelegateRequiresTask", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := toolsdk.AgentsDelegate.Render(nil)
|
|
require.ErrorContains(t, err, "missing required prompt argument: task")
|
|
_, err = toolsdk.AgentsDelegate.Render(map[string]string{"task": " "})
|
|
require.ErrorContains(t, err, "missing required prompt argument: task")
|
|
})
|
|
|
|
t.Run("Delegate", func(t *testing.T) {
|
|
t.Parallel()
|
|
text, err := toolsdk.AgentsDelegate.Render(map[string]string{"task": "Fix the flaky test."})
|
|
require.NoError(t, err)
|
|
require.Contains(t, text, "Fix the flaky test.")
|
|
for _, tool := range toolsdk.AgentsDelegate.RequiredTools {
|
|
require.Contains(t, text, tool)
|
|
}
|
|
})
|
|
|
|
t.Run("DelegateWithModelConfig", func(t *testing.T) {
|
|
t.Parallel()
|
|
text, err := toolsdk.AgentsDelegate.Render(map[string]string{
|
|
"task": "Fix the flaky test.",
|
|
"model_config_id": "a2913789-b213-45e3-9d18-561fbb1ec97c",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Contains(t, text, "a2913789-b213-45e3-9d18-561fbb1ec97c")
|
|
require.NotContains(t, text, toolsdk.ToolNameListChatModelConfigs)
|
|
})
|
|
|
|
t.Run("CheckRequiresChatID", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := toolsdk.AgentsCheck.Render(map[string]string{})
|
|
require.ErrorContains(t, err, "missing required prompt argument: chat_id")
|
|
})
|
|
|
|
t.Run("Check", func(t *testing.T) {
|
|
t.Parallel()
|
|
text, err := toolsdk.AgentsCheck.Render(map[string]string{"chat_id": "0bb52d1a-e239-4e7a-ae2a-5abbd7fbf9b5"})
|
|
require.NoError(t, err)
|
|
require.Contains(t, text, "0bb52d1a-e239-4e7a-ae2a-5abbd7fbf9b5")
|
|
for _, tool := range toolsdk.AgentsCheck.RequiredTools {
|
|
require.Contains(t, text, tool)
|
|
}
|
|
})
|
|
}
|