mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add list_apps MCP tool (#19952)
This commit is contained in:
@@ -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"`
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user