1package backend
2
3import (
4 "context"
5 "errors"
6 "fmt"
7
8 "github.com/charmbracelet/crush/internal/agent"
9 mcptools "github.com/charmbracelet/crush/internal/agent/tools/mcp"
10 "github.com/charmbracelet/crush/internal/commands"
11 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/oauth"
13 "github.com/charmbracelet/crush/internal/proto"
14 "github.com/charmbracelet/crush/internal/skills"
15)
16
17// MCPResourceContents holds the contents of an MCP resource returned
18// by the backend.
19type MCPResourceContents struct {
20 URI string `json:"uri"`
21 MIMEType string `json:"mime_type,omitempty"`
22 Text string `json:"text,omitempty"`
23 Blob []byte `json:"blob,omitempty"`
24}
25
26// SetConfigField sets a key/value pair in the config file for the
27// given scope.
28func (b *Backend) SetConfigField(workspaceID string, scope config.Scope, key string, value any) error {
29 ws, err := b.GetWorkspace(workspaceID)
30 if err != nil {
31 return err
32 }
33 return ws.Cfg.SetConfigField(scope, key, value)
34}
35
36// RemoveConfigField removes a key from the config file for the given
37// scope.
38func (b *Backend) RemoveConfigField(workspaceID string, scope config.Scope, key string) error {
39 ws, err := b.GetWorkspace(workspaceID)
40 if err != nil {
41 return err
42 }
43 return ws.Cfg.RemoveConfigField(scope, key)
44}
45
46// UpdatePreferredModel updates the preferred model for the given type
47// and persists it to the config file at the given scope.
48func (b *Backend) UpdatePreferredModel(workspaceID string, scope config.Scope, modelType config.SelectedModelType, model config.SelectedModel) error {
49 ws, err := b.GetWorkspace(workspaceID)
50 if err != nil {
51 return err
52 }
53 return ws.Cfg.UpdatePreferredModel(scope, modelType, model)
54}
55
56// SetCompactMode sets the compact mode setting and persists it.
57func (b *Backend) SetCompactMode(workspaceID string, scope config.Scope, enabled bool) error {
58 ws, err := b.GetWorkspace(workspaceID)
59 if err != nil {
60 return err
61 }
62 return ws.Cfg.SetCompactMode(scope, enabled)
63}
64
65// SetProviderAPIKey sets the API key for a provider and persists it.
66func (b *Backend) SetProviderAPIKey(workspaceID string, scope config.Scope, providerID string, apiKey any) error {
67 ws, err := b.GetWorkspace(workspaceID)
68 if err != nil {
69 return err
70 }
71 return ws.Cfg.SetProviderAPIKey(scope, providerID, apiKey)
72}
73
74// ImportCopilot attempts to import a GitHub Copilot token from disk.
75func (b *Backend) ImportCopilot(workspaceID string) (*oauth.Token, bool, error) {
76 ws, err := b.GetWorkspace(workspaceID)
77 if err != nil {
78 return nil, false, err
79 }
80 token, ok := ws.Cfg.ImportCopilot()
81 return token, ok, nil
82}
83
84// RefreshOAuthToken refreshes the OAuth token for a provider.
85func (b *Backend) RefreshOAuthToken(ctx context.Context, workspaceID string, scope config.Scope, providerID string) error {
86 ws, err := b.GetWorkspace(workspaceID)
87 if err != nil {
88 return err
89 }
90 return ws.Cfg.RefreshOAuthToken(ctx, scope, providerID)
91}
92
93// ProjectNeedsInitialization checks whether the project in this
94// workspace needs initialization.
95func (b *Backend) ProjectNeedsInitialization(workspaceID string) (bool, error) {
96 ws, err := b.GetWorkspace(workspaceID)
97 if err != nil {
98 return false, err
99 }
100 return config.ProjectNeedsInitialization(ws.Cfg)
101}
102
103// MarkProjectInitialized marks the project as initialized.
104func (b *Backend) MarkProjectInitialized(workspaceID string) error {
105 ws, err := b.GetWorkspace(workspaceID)
106 if err != nil {
107 return err
108 }
109 return config.MarkProjectInitialized(ws.Cfg)
110}
111
112// InitializePrompt builds the initialization prompt for the workspace.
113func (b *Backend) InitializePrompt(workspaceID string) (string, error) {
114 ws, err := b.GetWorkspace(workspaceID)
115 if err != nil {
116 return "", err
117 }
118 return agent.InitializePrompt(ws.Cfg)
119}
120
121// ReadSkill reads a skill's content by ID.
122func (b *Backend) ReadSkill(ctx context.Context, workspaceID, skillID string) ([]byte, proto.SkillReadResult, error) {
123 ws, err := b.GetWorkspace(workspaceID)
124 if err != nil {
125 return nil, proto.SkillReadResult{}, err
126 }
127
128 mgr := ws.Skills
129 content, result, err := skills.ReadContent(
130 mgr.ActiveSkills(), mgr.ResolvedPaths(), mgr.WorkingDir(), skillID,
131 )
132 if err != nil {
133 return nil, proto.SkillReadResult{}, err
134 }
135 return content, proto.SkillReadResult{
136 Name: result.Name,
137 Description: result.Description,
138 Source: string(result.Source),
139 Builtin: result.Builtin,
140 }, nil
141}
142
143// ListSkills returns the effective visible skills for a workspace.
144func (b *Backend) ListSkills(workspaceID string) ([]proto.SkillInfo, error) {
145 ws, err := b.GetWorkspace(workspaceID)
146 if err != nil {
147 return nil, err
148 }
149 mgr := ws.Skills
150 entries := skills.Catalog(mgr.ActiveSkills(), mgr.ResolvedPaths(), mgr.WorkingDir())
151 result := make([]proto.SkillInfo, len(entries))
152 for i, entry := range entries {
153 result[i] = proto.SkillInfo{
154 ID: entry.ID,
155 Name: entry.Name,
156 Description: entry.Description,
157 Label: entry.Label,
158 Source: string(entry.Source),
159 }
160 }
161 return result, nil
162}
163
164// EnableDockerMCP validates Docker MCP availability, stages the
165// configuration, starts the MCP client, and persists the config.
166func (b *Backend) EnableDockerMCP(ctx context.Context, workspaceID string) error {
167 ws, err := b.GetWorkspace(workspaceID)
168 if err != nil {
169 return err
170 }
171
172 mcpConfig, err := ws.Cfg.PrepareDockerMCPConfig()
173 if err != nil {
174 return err
175 }
176
177 if err := mcptools.InitializeSingle(ctx, config.DockerMCPName, ws.Cfg); err != nil {
178 disableErr := mcptools.DisableSingle(ws.Cfg, config.DockerMCPName)
179 delete(ws.Cfg.Config().MCP, config.DockerMCPName)
180 return fmt.Errorf("failed to start docker MCP: %w", errors.Join(err, disableErr))
181 }
182
183 if err := ws.Cfg.PersistDockerMCPConfig(mcpConfig); err != nil {
184 disableErr := mcptools.DisableSingle(ws.Cfg, config.DockerMCPName)
185 delete(ws.Cfg.Config().MCP, config.DockerMCPName)
186 return fmt.Errorf("docker MCP started but failed to persist configuration: %w", errors.Join(err, disableErr))
187 }
188
189 return nil
190}
191
192// DisableDockerMCP closes the Docker MCP client, removes the
193// configuration, and persists the change.
194func (b *Backend) DisableDockerMCP(workspaceID string) error {
195 ws, err := b.GetWorkspace(workspaceID)
196 if err != nil {
197 return err
198 }
199
200 if err := mcptools.DisableSingle(ws.Cfg, config.DockerMCPName); err != nil {
201 return fmt.Errorf("failed to disable docker MCP: %w", err)
202 }
203
204 if err := ws.Cfg.DisableDockerMCP(); err != nil {
205 return err
206 }
207
208 return nil
209}
210
211// RefreshMCPTools refreshes the tools for a named MCP server.
212func (b *Backend) RefreshMCPTools(ctx context.Context, workspaceID, name string) error {
213 ws, err := b.GetWorkspace(workspaceID)
214 if err != nil {
215 return err
216 }
217 mcptools.RefreshTools(ctx, ws.Cfg, name)
218 return nil
219}
220
221// ReadMCPResource reads a resource from a named MCP server.
222func (b *Backend) ReadMCPResource(ctx context.Context, workspaceID, name, uri string) ([]MCPResourceContents, error) {
223 ws, err := b.GetWorkspace(workspaceID)
224 if err != nil {
225 return nil, err
226 }
227 contents, err := mcptools.ReadResource(ctx, ws.Cfg, name, uri)
228 if err != nil {
229 return nil, err
230 }
231 result := make([]MCPResourceContents, len(contents))
232 for i, c := range contents {
233 result[i] = MCPResourceContents{
234 URI: c.URI,
235 MIMEType: c.MIMEType,
236 Text: c.Text,
237 Blob: c.Blob,
238 }
239 }
240 return result, nil
241}
242
243// GetMCPPrompt retrieves a prompt from a named MCP server.
244func (b *Backend) GetMCPPrompt(workspaceID, clientID, promptID string, args map[string]string) (string, error) {
245 ws, err := b.GetWorkspace(workspaceID)
246 if err != nil {
247 return "", err
248 }
249 return commands.GetMCPPrompt(ws.Cfg, clientID, promptID, args)
250}
251
252// GetWorkingDir returns the working directory for a workspace.
253func (b *Backend) GetWorkingDir(workspaceID string) (string, error) {
254 ws, err := b.GetWorkspace(workspaceID)
255 if err != nil {
256 return "", err
257 }
258 return ws.Cfg.WorkingDir(), nil
259}