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:
@@ -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
@@ -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": {}}}`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user