Skip to content

Commit 94c76b9

Browse files
authored
feat: add list_apps MCP tool (coder#19952)
1 parent ebcfae2 commit 94c76b9

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

codersdk/toolsdk/toolsdk.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const (
5050
ToolNameWorkspaceEditFile = "coder_workspace_edit_file"
5151
ToolNameWorkspaceEditFiles = "coder_workspace_edit_files"
5252
ToolNameWorkspacePortForward = "coder_workspace_port_forward"
53+
ToolNameWorkspaceListApps = "coder_workspace_list_apps"
5354
ToolNameCreateTask = "coder_create_task"
5455
ToolNameDeleteTask = "coder_delete_task"
5556
ToolNameListTasks = "coder_list_tasks"
@@ -227,6 +228,7 @@ var All = []GenericTool{
227228
WorkspaceEditFile.Generic(),
228229
WorkspaceEditFiles.Generic(),
229230
WorkspacePortForward.Generic(),
231+
WorkspaceListApps.Generic(),
230232
CreateTask.Generic(),
231233
DeleteTask.Generic(),
232234
ListTasks.Generic(),
@@ -1756,6 +1758,57 @@ var WorkspacePortForward = Tool[WorkspacePortForwardArgs, WorkspacePortForwardRe
17561758
},
17571759
}
17581760

1761+
type WorkspaceListAppsArgs struct {
1762+
Workspace string `json:"workspace"`
1763+
}
1764+
1765+
type WorkspaceListApp struct {
1766+
Name string `json:"name"`
1767+
URL string `json:"url"`
1768+
}
1769+
1770+
type WorkspaceListAppsResponse struct {
1771+
Apps []WorkspaceListApp `json:"apps"`
1772+
}
1773+
1774+
var WorkspaceListApps = Tool[WorkspaceListAppsArgs, WorkspaceListAppsResponse]{
1775+
Tool: aisdk.Tool{
1776+
Name: ToolNameWorkspaceListApps,
1777+
Description: `List the URLs of Coder apps running in a workspace for a single agent.`,
1778+
Schema: aisdk.Schema{
1779+
Properties: map[string]any{
1780+
"workspace": map[string]any{
1781+
"type": "string",
1782+
"description": workspaceDescription,
1783+
},
1784+
},
1785+
Required: []string{"workspace"},
1786+
},
1787+
},
1788+
UserClientOptional: true,
1789+
Handler: func(ctx context.Context, deps Deps, args WorkspaceListAppsArgs) (WorkspaceListAppsResponse, error) {
1790+
workspaceName := NormalizeWorkspaceInput(args.Workspace)
1791+
_, workspaceAgent, err := findWorkspaceAndAgent(ctx, deps.coderClient, workspaceName)
1792+
if err != nil {
1793+
return WorkspaceListAppsResponse{}, xerrors.Errorf("failed to find workspace: %w", err)
1794+
}
1795+
1796+
var res WorkspaceListAppsResponse
1797+
for _, app := range workspaceAgent.Apps {
1798+
name := app.DisplayName
1799+
if name == "" {
1800+
name = app.Slug
1801+
}
1802+
res.Apps = append(res.Apps, WorkspaceListApp{
1803+
Name: name,
1804+
URL: app.URL,
1805+
})
1806+
}
1807+
1808+
return res, nil
1809+
},
1810+
}
1811+
17591812
type CreateTaskArgs struct {
17601813
Input string `json:"input"`
17611814
TemplateVersionID string `json:"template_version_id"`

codersdk/toolsdk/toolsdk_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,146 @@ func TestTools(t *testing.T) {
11471147
})
11481148
}
11491149
})
1150+
1151+
t.Run("WorkspaceListApps", func(t *testing.T) {
1152+
t.Parallel()
1153+
1154+
// nolint:gocritic // This is in a test package and does not end up in the build
1155+
_ = dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
1156+
Name: "list-app-workspace-one-agent",
1157+
OrganizationID: owner.OrganizationID,
1158+
OwnerID: member.ID,
1159+
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
1160+
agents[0].Apps = []*proto.App{
1161+
{
1162+
Slug: "zero",
1163+
Url: "http://zero.dev.coder.com",
1164+
},
1165+
}
1166+
return agents
1167+
}).Do()
1168+
1169+
// nolint:gocritic // This is in a test package and does not end up in the build
1170+
_ = dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
1171+
Name: "list-app-workspace-multi-agent",
1172+
OrganizationID: owner.OrganizationID,
1173+
OwnerID: member.ID,
1174+
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
1175+
agents[0].Apps = []*proto.App{
1176+
{
1177+
Slug: "one",
1178+
Url: "http://one.dev.coder.com",
1179+
},
1180+
{
1181+
Slug: "two",
1182+
Url: "http://two.dev.coder.com",
1183+
},
1184+
{
1185+
Slug: "three",
1186+
Url: "http://three.dev.coder.com",
1187+
},
1188+
}
1189+
agents = append(agents, &proto.Agent{
1190+
Id: uuid.NewString(),
1191+
Name: "dev2",
1192+
Auth: &proto.Agent_Token{
1193+
Token: uuid.NewString(),
1194+
},
1195+
Env: map[string]string{},
1196+
Apps: []*proto.App{
1197+
{
1198+
Slug: "four",
1199+
Url: "http://four.dev.coder.com",
1200+
},
1201+
},
1202+
})
1203+
return agents
1204+
}).Do()
1205+
1206+
tests := []struct {
1207+
name string
1208+
args toolsdk.WorkspaceListAppsArgs
1209+
expected []toolsdk.WorkspaceListApp
1210+
error string
1211+
}{
1212+
{
1213+
name: "NonExistentWorkspace",
1214+
args: toolsdk.WorkspaceListAppsArgs{
1215+
Workspace: "list-appp-workspace-does-not-exist",
1216+
},
1217+
error: "failed to find workspace",
1218+
},
1219+
{
1220+
name: "OneAgentOneApp",
1221+
args: toolsdk.WorkspaceListAppsArgs{
1222+
Workspace: "list-app-workspace-one-agent",
1223+
},
1224+
expected: []toolsdk.WorkspaceListApp{
1225+
{
1226+
Name: "zero",
1227+
URL: "http://zero.dev.coder.com",
1228+
},
1229+
},
1230+
},
1231+
{
1232+
name: "MultiAgent",
1233+
args: toolsdk.WorkspaceListAppsArgs{
1234+
Workspace: "list-app-workspace-multi-agent",
1235+
},
1236+
error: "multiple agents found, please specify the agent name",
1237+
},
1238+
{
1239+
name: "MultiAgentOneApp",
1240+
args: toolsdk.WorkspaceListAppsArgs{
1241+
Workspace: "list-app-workspace-multi-agent.dev2",
1242+
},
1243+
expected: []toolsdk.WorkspaceListApp{
1244+
{
1245+
Name: "four",
1246+
URL: "http://four.dev.coder.com",
1247+
},
1248+
},
1249+
},
1250+
{
1251+
name: "MultiAgentMultiApp",
1252+
args: toolsdk.WorkspaceListAppsArgs{
1253+
Workspace: "list-app-workspace-multi-agent.dev",
1254+
},
1255+
expected: []toolsdk.WorkspaceListApp{
1256+
{
1257+
Name: "one",
1258+
URL: "http://one.dev.coder.com",
1259+
},
1260+
{
1261+
Name: "three",
1262+
URL: "http://three.dev.coder.com",
1263+
},
1264+
{
1265+
Name: "two",
1266+
URL: "http://two.dev.coder.com",
1267+
},
1268+
},
1269+
},
1270+
}
1271+
1272+
for _, tt := range tests {
1273+
t.Run(tt.name, func(t *testing.T) {
1274+
t.Parallel()
1275+
1276+
tb, err := toolsdk.NewDeps(memberClient)
1277+
require.NoError(t, err)
1278+
1279+
res, err := testTool(t, toolsdk.WorkspaceListApps, tb, tt.args)
1280+
if tt.error != "" {
1281+
require.Error(t, err)
1282+
require.ErrorContains(t, err, tt.error)
1283+
} else {
1284+
require.NoError(t, err)
1285+
require.Equal(t, tt.expected, res.Apps)
1286+
}
1287+
})
1288+
}
1289+
})
11501290
}
11511291

11521292
// TestedTools keeps track of which tools have been tested.

0 commit comments

Comments
 (0)