Files
coder/codersdk/toolsdk/prompts_test.go
T
Michael Suchacz 8d4d0b35dd feat: add Coder Agents chat tools to the MCP toolsdk (#28025)
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.
2026-08-13 18:32:47 +02:00

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)
}
})
}