feat: expose MCP tool annotations for tool grouping (#23195)

## Summary
- add shared MCP annotation metadata to toolsdk tools
- emit MCP tool annotations from both coderd and CLI MCP servers
- cover annotation serialization in toolsdk, coderd MCP e2e, and CLI MCP
tests

## Why
- Coder already exposed MCP tools, but it did not populate MCP tool
annotation hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`,
`openWorldHint`).
- Hosts such as Claude Desktop use those hints to classify and group
tools, so without them Coder tools can get lumped together.
- This change adds a shared annotation source in `toolsdk` and has both
MCP servers emit those hints through `mcp.Tool.Annotations`, avoiding
drift between local and remote MCP implementations.

## Testing
- Tested locally on Cladue Desktop and the tools are categorized
correctly.

<table>
<tr>
 <td> Before
 <td> After
<tr>
<td> <img width="613" height="183" alt="image"
src="https://github.com/user-attachments/assets/29d2e3fb-53bc-4ea7-bdb3-f10df4ef996b"
/>
<td> <img width="600" height="457" alt="image"
src="https://github.com/user-attachments/assets/cc384036-c9a7-4db9-9400-43ad51920ff5"
/>
</table>

Note: Done using Coder Agents, reviewed and tested by human locally
This commit is contained in:
Atif Ali
2026-03-18 10:21:45 +00:00
committed by GitHub
parent 66f809388e
commit bd5b62c976
8 changed files with 197 additions and 10 deletions
+6
View File
@@ -1000,6 +1000,12 @@ func mcpFromSDK(sdkTool toolsdk.GenericTool, tb toolsdk.Deps) server.ServerTool
Properties: sdkTool.Schema.Properties,
Required: sdkTool.Schema.Required,
},
Annotations: mcp.ToolAnnotation{
ReadOnlyHint: mcp.ToBoolPtr(sdkTool.MCPAnnotations.ReadOnlyHint),
DestructiveHint: mcp.ToBoolPtr(sdkTool.MCPAnnotations.DestructiveHint),
IdempotentHint: mcp.ToBoolPtr(sdkTool.MCPAnnotations.IdempotentHint),
OpenWorldHint: mcp.ToBoolPtr(sdkTool.MCPAnnotations.OpenWorldHint),
},
},
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
var buf bytes.Buffer
+16 -1
View File
@@ -81,7 +81,13 @@ func TestExpMcpServer(t *testing.T) {
var toolsResponse struct {
Result struct {
Tools []struct {
Name string `json:"name"`
Name string `json:"name"`
Annotations struct {
ReadOnlyHint *bool `json:"readOnlyHint"`
DestructiveHint *bool `json:"destructiveHint"`
IdempotentHint *bool `json:"idempotentHint"`
OpenWorldHint *bool `json:"openWorldHint"`
} `json:"annotations"`
} `json:"tools"`
} `json:"result"`
}
@@ -94,6 +100,15 @@ func TestExpMcpServer(t *testing.T) {
}
slices.Sort(foundTools)
require.Equal(t, []string{"coder_get_authenticated_user"}, foundTools)
annotations := toolsResponse.Result.Tools[0].Annotations
require.NotNil(t, annotations.ReadOnlyHint)
require.NotNil(t, annotations.DestructiveHint)
require.NotNil(t, annotations.IdempotentHint)
require.NotNil(t, annotations.OpenWorldHint)
assert.True(t, *annotations.ReadOnlyHint)
assert.False(t, *annotations.DestructiveHint)
assert.True(t, *annotations.IdempotentHint)
assert.False(t, *annotations.OpenWorldHint)
// Call the tool and ensure it works.
toolPayload := `{"jsonrpc":"2.0","id":3,"method":"tools/call", "params": {"name": "coder_get_authenticated_user", "arguments": {}}}`
+6
View File
@@ -136,6 +136,12 @@ func mcpFromSDK(sdkTool toolsdk.GenericTool, tb toolsdk.Deps) server.ServerTool
Properties: sdkTool.Schema.Properties,
Required: sdkTool.Schema.Required,
},
Annotations: mcp.ToolAnnotation{
ReadOnlyHint: mcp.ToBoolPtr(sdkTool.MCPAnnotations.ReadOnlyHint),
DestructiveHint: mcp.ToBoolPtr(sdkTool.MCPAnnotations.DestructiveHint),
IdempotentHint: mcp.ToBoolPtr(sdkTool.MCPAnnotations.IdempotentHint),
OpenWorldHint: mcp.ToBoolPtr(sdkTool.MCPAnnotations.OpenWorldHint),
},
},
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
var buf bytes.Buffer
+29 -9
View File
@@ -91,21 +91,41 @@ func TestMCPHTTP_E2E_ClientIntegration(t *testing.T) {
// Verify we have some expected Coder tools
var foundTools []string
for _, tool := range tools.Tools {
var userTool *mcp.Tool
var writeFileTool *mcp.Tool
for i := range tools.Tools {
tool := tools.Tools[i]
foundTools = append(foundTools, tool.Name)
switch tool.Name {
case toolsdk.ToolNameGetAuthenticatedUser:
userTool = &tools.Tools[i]
case toolsdk.ToolNameWorkspaceWriteFile:
writeFileTool = &tools.Tools[i]
}
}
// Check for some basic tools that should be available
assert.Contains(t, foundTools, toolsdk.ToolNameGetAuthenticatedUser, "Should have authenticated user tool")
require.NotNil(t, userTool)
require.NotNil(t, writeFileTool)
require.NotNil(t, userTool.Annotations.ReadOnlyHint)
require.NotNil(t, userTool.Annotations.DestructiveHint)
require.NotNil(t, userTool.Annotations.IdempotentHint)
require.NotNil(t, userTool.Annotations.OpenWorldHint)
assert.True(t, *userTool.Annotations.ReadOnlyHint)
assert.False(t, *userTool.Annotations.DestructiveHint)
assert.True(t, *userTool.Annotations.IdempotentHint)
assert.False(t, *userTool.Annotations.OpenWorldHint)
require.NotNil(t, writeFileTool.Annotations.ReadOnlyHint)
require.NotNil(t, writeFileTool.Annotations.DestructiveHint)
require.NotNil(t, writeFileTool.Annotations.IdempotentHint)
require.NotNil(t, writeFileTool.Annotations.OpenWorldHint)
assert.False(t, *writeFileTool.Annotations.ReadOnlyHint)
assert.True(t, *writeFileTool.Annotations.DestructiveHint)
assert.False(t, *writeFileTool.Annotations.IdempotentHint)
assert.False(t, *writeFileTool.Annotations.OpenWorldHint)
// Find and execute the authenticated user tool
var userTool *mcp.Tool
for _, tool := range tools.Tools {
if tool.Name == toolsdk.ToolNameGetAuthenticatedUser {
userTool = &tool
break
}
}
// Execute the authenticated user tool.
require.NotNil(t, userTool, "Expected to find "+toolsdk.ToolNameGetAuthenticatedUser+" tool")
// Execute the tool
+1
View File
@@ -89,6 +89,7 @@ Examples:
Required: []string{"workspace", "command"},
},
},
MCPAnnotations: mcpDestructiveAnnotations,
Handler: func(ctx context.Context, deps Deps, args WorkspaceBashArgs) (res WorkspaceBashResult, err error) {
if args.Workspace == "" {
return WorkspaceBashResult{}, xerrors.New("workspace name cannot be empty")
+2
View File
@@ -299,6 +299,7 @@ List workspaces with multiple filters - running workspaces owned by "alice".
Required: []string{"query"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
Handler: func(ctx context.Context, deps Deps, args SearchArgs) (SearchResult, error) {
query, err := parseSearchQuery(args.Query)
if err != nil {
@@ -419,6 +420,7 @@ var ChatGPTFetch = Tool[FetchArgs, FetchResult]{
Required: []string{"id"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
Handler: func(ctx context.Context, deps Deps, args FetchArgs) (FetchResult, error) {
objectID, err := parseObjectID(args.ID)
if err != nil {
+68
View File
@@ -97,12 +97,47 @@ type Tool[Arg, Ret any] struct {
aisdk.Tool
Handler HandlerFunc[Arg, Ret]
// MCPAnnotations is the shared source of truth for MCP tool
// classification. Both the coderd-hosted MCP server and the CLI MCP
// server translate these hints into mcp.Tool.Annotations so hosts can
// consistently group tools.
MCPAnnotations MCPToolAnnotations
// UserClientOptional indicates whether this tool can function without a valid
// user authentication token. If true, the tool will be available even when
// running in an unauthenticated mode with just an agent token.
UserClientOptional bool
}
// MCPToolAnnotations describes how an MCP host should classify a tool.
type MCPToolAnnotations struct {
ReadOnlyHint bool
DestructiveHint bool
IdempotentHint bool
OpenWorldHint bool
}
var (
mcpReadOnlyAnnotations = MCPToolAnnotations{
ReadOnlyHint: true,
DestructiveHint: false,
IdempotentHint: true,
OpenWorldHint: false,
}
mcpMutationAnnotations = MCPToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: false,
IdempotentHint: false,
OpenWorldHint: false,
}
mcpDestructiveAnnotations = MCPToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: true,
IdempotentHint: false,
OpenWorldHint: false,
}
)
// Generic returns a type-erased version of a TypedTool where the arguments and
// return values are converted to/from json.RawMessage.
// This allows the tool to be referenced without knowing the concrete arguments
@@ -111,6 +146,7 @@ type Tool[Arg, Ret any] struct {
func (t Tool[Arg, Ret]) Generic() GenericTool {
return GenericTool{
Tool: t.Tool,
MCPAnnotations: t.MCPAnnotations,
UserClientOptional: t.UserClientOptional,
Handler: wrap(func(ctx context.Context, deps Deps, args json.RawMessage) (json.RawMessage, error) {
var typedArgs Arg
@@ -134,6 +170,9 @@ type GenericTool struct {
aisdk.Tool
Handler GenericHandlerFunc
// MCPAnnotations are host hints used when this tool is exposed over MCP.
MCPAnnotations MCPToolAnnotations
// UserClientOptional indicates whether this tool can function without a valid
// user authentication token. If true, the tool will be available even when
// running in an unauthenticated mode with just an agent token.
@@ -291,6 +330,7 @@ ONLY report a "complete", "idle", or "failure" state if you have FULLY completed
Required: []string{"summary", "link", "state"},
},
},
MCPAnnotations: mcpMutationAnnotations,
UserClientOptional: true,
Handler: func(_ context.Context, deps Deps, args ReportTaskArgs) (codersdk.Response, error) {
if len(args.Summary) > 160 {
@@ -330,6 +370,7 @@ This returns more data than list_workspaces to reduce token usage.`,
Required: []string{"workspace_id"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
Handler: func(ctx context.Context, deps Deps, args GetWorkspaceArgs) (codersdk.Workspace, error) {
wsID, err := uuid.Parse(args.WorkspaceID)
if err != nil {
@@ -389,6 +430,7 @@ be ready before trying to use or connect to the workspace.
Required: []string{"user", "template_version_id", "name", "rich_parameters"},
},
},
MCPAnnotations: mcpMutationAnnotations,
Handler: func(ctx context.Context, deps Deps, args CreateWorkspaceArgs) (codersdk.Workspace, error) {
tvID, err := uuid.Parse(args.TemplateVersionID)
if err != nil {
@@ -434,6 +476,7 @@ var ListWorkspaces = Tool[ListWorkspacesArgs, []MinimalWorkspace]{
Required: []string{},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
Handler: func(ctx context.Context, deps Deps, args ListWorkspacesArgs) ([]MinimalWorkspace, error) {
owner := args.Owner
if owner == "" {
@@ -471,6 +514,7 @@ var ListTemplates = Tool[NoArgs, []MinimalTemplate]{
Required: []string{},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
Handler: func(ctx context.Context, deps Deps, _ NoArgs) ([]MinimalTemplate, error) {
templates, err := deps.coderClient.Templates(ctx, codersdk.TemplateFilter{})
if err != nil {
@@ -508,6 +552,7 @@ var ListTemplateVersionParameters = Tool[ListTemplateVersionParametersArgs, []co
Required: []string{"template_version_id"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
Handler: func(ctx context.Context, deps Deps, args ListTemplateVersionParametersArgs) ([]codersdk.TemplateVersionParameter, error) {
templateVersionID, err := uuid.Parse(args.TemplateVersionID)
if err != nil {
@@ -530,6 +575,7 @@ var GetAuthenticatedUser = Tool[NoArgs, codersdk.User]{
Required: []string{},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
Handler: func(ctx context.Context, deps Deps, _ NoArgs) (codersdk.User, error) {
return deps.coderClient.User(ctx, "me")
},
@@ -568,6 +614,7 @@ connect to the workspace.
Required: []string{"workspace_id", "transition"},
},
},
MCPAnnotations: mcpDestructiveAnnotations,
Handler: func(ctx context.Context, deps Deps, args CreateWorkspaceBuildArgs) (codersdk.WorkspaceBuild, error) {
workspaceID, err := uuid.Parse(args.WorkspaceID)
if err != nil {
@@ -1061,6 +1108,7 @@ The file_id provided is a reference to a tar file you have uploaded containing t
Required: []string{"file_id"},
},
},
MCPAnnotations: mcpMutationAnnotations,
Handler: func(ctx context.Context, deps Deps, args CreateTemplateVersionArgs) (codersdk.TemplateVersion, error) {
me, err := deps.coderClient.User(ctx, "me")
if err != nil {
@@ -1111,6 +1159,7 @@ var GetWorkspaceAgentLogs = Tool[GetWorkspaceAgentLogsArgs, []string]{
Required: []string{"workspace_agent_id"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
Handler: func(ctx context.Context, deps Deps, args GetWorkspaceAgentLogsArgs) ([]string, error) {
workspaceAgentID, err := uuid.Parse(args.WorkspaceAgentID)
if err != nil {
@@ -1150,6 +1199,7 @@ var GetWorkspaceBuildLogs = Tool[GetWorkspaceBuildLogsArgs, []string]{
Required: []string{"workspace_build_id"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
Handler: func(ctx context.Context, deps Deps, args GetWorkspaceBuildLogsArgs) ([]string, error) {
workspaceBuildID, err := uuid.Parse(args.WorkspaceBuildID)
if err != nil {
@@ -1185,6 +1235,7 @@ var GetTemplateVersionLogs = Tool[GetTemplateVersionLogsArgs, []string]{
Required: []string{"template_version_id"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
Handler: func(ctx context.Context, deps Deps, args GetTemplateVersionLogsArgs) ([]string, error) {
templateVersionID, err := uuid.Parse(args.TemplateVersionID)
if err != nil {
@@ -1225,6 +1276,7 @@ var UpdateTemplateActiveVersion = Tool[UpdateTemplateActiveVersionArgs, string]{
Required: []string{"template_id", "template_version_id"},
},
},
MCPAnnotations: mcpMutationAnnotations,
Handler: func(ctx context.Context, deps Deps, args UpdateTemplateActiveVersionArgs) (string, error) {
templateID, err := uuid.Parse(args.TemplateID)
if err != nil {
@@ -1262,6 +1314,7 @@ var UploadTarFile = Tool[UploadTarFileArgs, codersdk.UploadResponse]{
Required: []string{"files"},
},
},
MCPAnnotations: mcpMutationAnnotations,
Handler: func(ctx context.Context, deps Deps, args UploadTarFileArgs) (codersdk.UploadResponse, error) {
pipeReader, pipeWriter := io.Pipe()
done := make(chan struct{})
@@ -1337,6 +1390,7 @@ var CreateTemplate = Tool[CreateTemplateArgs, codersdk.Template]{
Required: []string{"name", "display_name", "description", "version_id"},
},
},
MCPAnnotations: mcpMutationAnnotations,
Handler: func(ctx context.Context, deps Deps, args CreateTemplateArgs) (codersdk.Template, error) {
me, err := deps.coderClient.User(ctx, "me")
if err != nil {
@@ -1376,6 +1430,7 @@ var DeleteTemplate = Tool[DeleteTemplateArgs, codersdk.Response]{
Required: []string{"template_id"},
},
},
MCPAnnotations: mcpDestructiveAnnotations,
Handler: func(ctx context.Context, deps Deps, args DeleteTemplateArgs) (codersdk.Response, error) {
templateID, err := uuid.Parse(args.TemplateID)
if err != nil {
@@ -1443,6 +1498,7 @@ var WorkspaceLS = Tool[WorkspaceLSArgs, WorkspaceLSResponse]{
Required: []string{"path", "workspace"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args WorkspaceLSArgs) (WorkspaceLSResponse, error) {
conn, err := newAgentConn(ctx, deps.coderClient, args.Workspace)
@@ -1508,6 +1564,7 @@ var WorkspaceReadFile = Tool[WorkspaceReadFileArgs, WorkspaceReadFileResponse]{
Required: []string{"path", "workspace"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args WorkspaceReadFileArgs) (WorkspaceReadFileResponse, error) {
conn, err := newAgentConn(ctx, deps.coderClient, args.Workspace)
@@ -1581,6 +1638,7 @@ content you are trying to write, then re-encode it properly.
Required: []string{"path", "workspace", "content"},
},
},
MCPAnnotations: mcpDestructiveAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args WorkspaceWriteFileArgs) (codersdk.Response, error) {
conn, err := newAgentConn(ctx, deps.coderClient, args.Workspace)
@@ -1643,6 +1701,7 @@ var WorkspaceEditFile = Tool[WorkspaceEditFileArgs, codersdk.Response]{
Required: []string{"path", "workspace", "edits"},
},
},
MCPAnnotations: mcpDestructiveAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args WorkspaceEditFileArgs) (codersdk.Response, error) {
conn, err := newAgentConn(ctx, deps.coderClient, args.Workspace)
@@ -1724,6 +1783,7 @@ var WorkspaceEditFiles = Tool[WorkspaceEditFilesArgs, codersdk.Response]{
Required: []string{"workspace", "files"},
},
},
MCPAnnotations: mcpDestructiveAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args WorkspaceEditFilesArgs) (codersdk.Response, error) {
conn, err := newAgentConn(ctx, deps.coderClient, args.Workspace)
@@ -1770,6 +1830,7 @@ var WorkspacePortForward = Tool[WorkspacePortForwardArgs, WorkspacePortForwardRe
Required: []string{"workspace", "port"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args WorkspacePortForwardArgs) (WorkspacePortForwardResponse, error) {
workspaceName := NormalizeWorkspaceInput(args.Workspace)
@@ -1823,6 +1884,7 @@ var WorkspaceListApps = Tool[WorkspaceListAppsArgs, WorkspaceListAppsResponse]{
Required: []string{"workspace"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args WorkspaceListAppsArgs) (WorkspaceListAppsResponse, error) {
workspaceName := NormalizeWorkspaceInput(args.Workspace)
@@ -1880,6 +1942,7 @@ var CreateTask = Tool[CreateTaskArgs, codersdk.Task]{
Required: []string{"input", "template_version_id"},
},
},
MCPAnnotations: mcpMutationAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args CreateTaskArgs) (codersdk.Task, error) {
if args.Input == "" {
@@ -1934,6 +1997,7 @@ var DeleteTask = Tool[DeleteTaskArgs, codersdk.Response]{
Required: []string{"task_id"},
},
},
MCPAnnotations: mcpDestructiveAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args DeleteTaskArgs) (codersdk.Response, error) {
if args.TaskID == "" {
@@ -1983,6 +2047,7 @@ var ListTasks = Tool[ListTasksArgs, ListTasksResponse]{
Required: []string{},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args ListTasksArgs) (ListTasksResponse, error) {
if args.User == "" {
@@ -2026,6 +2091,7 @@ var GetTaskStatus = Tool[GetTaskStatusArgs, GetTaskStatusResponse]{
Required: []string{"task_id"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args GetTaskStatusArgs) (GetTaskStatusResponse, error) {
if args.TaskID == "" {
@@ -2067,6 +2133,7 @@ var SendTaskInput = Tool[SendTaskInputArgs, codersdk.Response]{
Required: []string{"task_id", "input"},
},
},
MCPAnnotations: mcpMutationAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args SendTaskInputArgs) (codersdk.Response, error) {
if args.TaskID == "" {
@@ -2113,6 +2180,7 @@ var GetTaskLogs = Tool[GetTaskLogsArgs, codersdk.TaskLogsResponse]{
Required: []string{"task_id"},
},
},
MCPAnnotations: mcpReadOnlyAnnotations,
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args GetTaskLogsArgs) (codersdk.TaskLogsResponse, error) {
if args.TaskID == "" {
+69
View File
@@ -61,6 +61,75 @@ func setupWorkspaceForAgent(t *testing.T, opts *coderdtest.Options) (*codersdk.C
return userClient, r.Workspace, r.AgentToken
}
// These tests are dependent on the state of the coder server.
// Running them in parallel is prone to racy behavior.
// nolint:tparallel,paralleltest
func TestGenericToolMCPAnnotations(t *testing.T) {
t.Parallel()
tests := []struct {
name string
toolName string
readOnlyHint bool
destructiveHint bool
idempotentHint bool
openWorldHint bool
}{
{
name: "ReadOnlyTool",
toolName: toolsdk.ToolNameGetAuthenticatedUser,
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
{
name: "DestructiveTool",
toolName: toolsdk.ToolNameWorkspaceWriteFile,
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: false,
},
{
name: "MutatingTool",
toolName: toolsdk.ToolNameCreateWorkspace,
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
},
{
name: "PortForwardIsReadOnly",
toolName: toolsdk.ToolNameWorkspacePortForward,
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var found *toolsdk.GenericTool
for i := range toolsdk.All {
if toolsdk.All[i].Name == tc.toolName {
found = &toolsdk.All[i]
break
}
}
require.NotNil(t, found)
assert.Equal(t, tc.readOnlyHint, found.MCPAnnotations.ReadOnlyHint)
assert.Equal(t, tc.destructiveHint, found.MCPAnnotations.DestructiveHint)
assert.Equal(t, tc.idempotentHint, found.MCPAnnotations.IdempotentHint)
assert.Equal(t, tc.openWorldHint, found.MCPAnnotations.OpenWorldHint)
})
}
}
// These tests are dependent on the state of the coder server.
// Running them in parallel is prone to racy behavior.
// nolint:tparallel,paralleltest