feat: add list_apps MCP tool (#19952)

This commit is contained in:
Asher
2025-10-01 13:57:11 -08:00
committed by GitHub
parent ebcfae27a2
commit 94c76b97bd
2 changed files with 193 additions and 0 deletions
+53
View File
@@ -50,6 +50,7 @@ const (
ToolNameWorkspaceEditFile = "coder_workspace_edit_file"
ToolNameWorkspaceEditFiles = "coder_workspace_edit_files"
ToolNameWorkspacePortForward = "coder_workspace_port_forward"
ToolNameWorkspaceListApps = "coder_workspace_list_apps"
ToolNameCreateTask = "coder_create_task"
ToolNameDeleteTask = "coder_delete_task"
ToolNameListTasks = "coder_list_tasks"
@@ -227,6 +228,7 @@ var All = []GenericTool{
WorkspaceEditFile.Generic(),
WorkspaceEditFiles.Generic(),
WorkspacePortForward.Generic(),
WorkspaceListApps.Generic(),
CreateTask.Generic(),
DeleteTask.Generic(),
ListTasks.Generic(),
@@ -1756,6 +1758,57 @@ var WorkspacePortForward = Tool[WorkspacePortForwardArgs, WorkspacePortForwardRe
},
}
type WorkspaceListAppsArgs struct {
Workspace string `json:"workspace"`
}
type WorkspaceListApp struct {
Name string `json:"name"`
URL string `json:"url"`
}
type WorkspaceListAppsResponse struct {
Apps []WorkspaceListApp `json:"apps"`
}
var WorkspaceListApps = Tool[WorkspaceListAppsArgs, WorkspaceListAppsResponse]{
Tool: aisdk.Tool{
Name: ToolNameWorkspaceListApps,
Description: `List the URLs of Coder apps running in a workspace for a single agent.`,
Schema: aisdk.Schema{
Properties: map[string]any{
"workspace": map[string]any{
"type": "string",
"description": workspaceDescription,
},
},
Required: []string{"workspace"},
},
},
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args WorkspaceListAppsArgs) (WorkspaceListAppsResponse, error) {
workspaceName := NormalizeWorkspaceInput(args.Workspace)
_, workspaceAgent, err := findWorkspaceAndAgent(ctx, deps.coderClient, workspaceName)
if err != nil {
return WorkspaceListAppsResponse{}, xerrors.Errorf("failed to find workspace: %w", err)
}
var res WorkspaceListAppsResponse
for _, app := range workspaceAgent.Apps {
name := app.DisplayName
if name == "" {
name = app.Slug
}
res.Apps = append(res.Apps, WorkspaceListApp{
Name: name,
URL: app.URL,
})
}
return res, nil
},
}
type CreateTaskArgs struct {
Input string `json:"input"`
TemplateVersionID string `json:"template_version_id"`
+140
View File
@@ -1147,6 +1147,146 @@ func TestTools(t *testing.T) {
})
}
})
t.Run("WorkspaceListApps", func(t *testing.T) {
t.Parallel()
// nolint:gocritic // This is in a test package and does not end up in the build
_ = dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
Name: "list-app-workspace-one-agent",
OrganizationID: owner.OrganizationID,
OwnerID: member.ID,
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
agents[0].Apps = []*proto.App{
{
Slug: "zero",
Url: "http://zero.dev.coder.com",
},
}
return agents
}).Do()
// nolint:gocritic // This is in a test package and does not end up in the build
_ = dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
Name: "list-app-workspace-multi-agent",
OrganizationID: owner.OrganizationID,
OwnerID: member.ID,
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
agents[0].Apps = []*proto.App{
{
Slug: "one",
Url: "http://one.dev.coder.com",
},
{
Slug: "two",
Url: "http://two.dev.coder.com",
},
{
Slug: "three",
Url: "http://three.dev.coder.com",
},
}
agents = append(agents, &proto.Agent{
Id: uuid.NewString(),
Name: "dev2",
Auth: &proto.Agent_Token{
Token: uuid.NewString(),
},
Env: map[string]string{},
Apps: []*proto.App{
{
Slug: "four",
Url: "http://four.dev.coder.com",
},
},
})
return agents
}).Do()
tests := []struct {
name string
args toolsdk.WorkspaceListAppsArgs
expected []toolsdk.WorkspaceListApp
error string
}{
{
name: "NonExistentWorkspace",
args: toolsdk.WorkspaceListAppsArgs{
Workspace: "list-appp-workspace-does-not-exist",
},
error: "failed to find workspace",
},
{
name: "OneAgentOneApp",
args: toolsdk.WorkspaceListAppsArgs{
Workspace: "list-app-workspace-one-agent",
},
expected: []toolsdk.WorkspaceListApp{
{
Name: "zero",
URL: "http://zero.dev.coder.com",
},
},
},
{
name: "MultiAgent",
args: toolsdk.WorkspaceListAppsArgs{
Workspace: "list-app-workspace-multi-agent",
},
error: "multiple agents found, please specify the agent name",
},
{
name: "MultiAgentOneApp",
args: toolsdk.WorkspaceListAppsArgs{
Workspace: "list-app-workspace-multi-agent.dev2",
},
expected: []toolsdk.WorkspaceListApp{
{
Name: "four",
URL: "http://four.dev.coder.com",
},
},
},
{
name: "MultiAgentMultiApp",
args: toolsdk.WorkspaceListAppsArgs{
Workspace: "list-app-workspace-multi-agent.dev",
},
expected: []toolsdk.WorkspaceListApp{
{
Name: "one",
URL: "http://one.dev.coder.com",
},
{
Name: "three",
URL: "http://three.dev.coder.com",
},
{
Name: "two",
URL: "http://two.dev.coder.com",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tb, err := toolsdk.NewDeps(memberClient)
require.NoError(t, err)
res, err := testTool(t, toolsdk.WorkspaceListApps, tb, tt.args)
if tt.error != "" {
require.Error(t, err)
require.ErrorContains(t, err, tt.error)
} else {
require.NoError(t, err)
require.Equal(t, tt.expected, res.Apps)
}
})
}
})
}
// TestedTools keeps track of which tools have been tested.