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
@@ -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