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)
14
15// MCPResourceContents holds the contents of an MCP resource returned
16// by the backend.
17type MCPResourceContents struct {
18 URI string `json:"uri"`
19 MIMEType string `json:"mime_type,omitempty"`
20 Text string `json:"text,omitempty"`
21 Blob []byte `json:"blob,omitempty"`
22}
23
24// SetConfigField sets a key/value pair in the config file for the
25// given scope.
26func (b *Backend) SetConfigField(workspaceID string, scope config.Scope, key string, value any) error {
27 ws, err := b.GetWorkspace(workspaceID)
28 if err != nil {
29 return err
30 }
31 return ws.Cfg.SetConfigField(scope, key, value)
32}
33
34// RemoveConfigField removes a key from the config file for the given
35// scope.
36func (b *Backend) RemoveConfigField(workspaceID string, scope config.Scope, key string) error {
37 ws, err := b.GetWorkspace(workspaceID)
38 if err != nil {
39 return err
40 }
41 return ws.Cfg.RemoveConfigField(scope, key)
42}
43
44// UpdatePreferredModel updates the preferred model for the given type
45// and persists it to the config file at the given scope.
46func (b *Backend) UpdatePreferredModel(workspaceID string, scope config.Scope, modelType config.SelectedModelType, model config.SelectedModel) error {
47 ws, err := b.GetWorkspace(workspaceID)
48 if err != nil {
49 return err
50 }
51 return ws.Cfg.UpdatePreferredModel(scope, modelType, model)
52}
53
54// SetCompactMode sets the compact mode setting and persists it.
55func (b *Backend) SetCompactMode(workspaceID string, scope config.Scope, enabled bool) error {
56 ws, err := b.GetWorkspace(workspaceID)
57 if err != nil {
58 return err
59 }
60 return ws.Cfg.SetCompactMode(scope, enabled)
61}
62
63// SetProviderAPIKey sets the API key for a provider and persists it.
64func (b *Backend) SetProviderAPIKey(workspaceID string, scope config.Scope, providerID string, apiKey any) error {
65 ws, err := b.GetWorkspace(workspaceID)
66 if err != nil {
67 return err
68 }
69 return ws.Cfg.SetProviderAPIKey(scope, providerID, apiKey)
70}
71
72// ImportCopilot attempts to import a GitHub Copilot token from disk.
73func (b *Backend) ImportCopilot(workspaceID string) (*oauth.Token, bool, error) {
74 ws, err := b.GetWorkspace(workspaceID)
75 if err != nil {
76 return nil, false, err
77 }
78 token, ok := ws.Cfg.ImportCopilot()
79 return token, ok, nil
80}
81
82// RefreshOAuthToken refreshes the OAuth token for a provider.
83func (b *Backend) RefreshOAuthToken(ctx context.Context, workspaceID string, scope config.Scope, providerID string) error {
84 ws, err := b.GetWorkspace(workspaceID)
85 if err != nil {
86 return err
87 }
88 return ws.Cfg.RefreshOAuthToken(ctx, scope, providerID)
89}
90
91// ProjectNeedsInitialization checks whether the project in this
92// workspace needs initialization.
93func (b *Backend) ProjectNeedsInitialization(workspaceID string) (bool, error) {
94 ws, err := b.GetWorkspace(workspaceID)
95 if err != nil {
96 return false, err
97 }
98 return config.ProjectNeedsInitialization(ws.Cfg)
99}
100
101// MarkProjectInitialized marks the project as initialized.
102func (b *Backend) MarkProjectInitialized(workspaceID string) error {
103 ws, err := b.GetWorkspace(workspaceID)
104 if err != nil {
105 return err
106 }
107 return config.MarkProjectInitialized(ws.Cfg)
108}
109
110// InitializePrompt builds the initialization prompt for the workspace.
111func (b *Backend) InitializePrompt(workspaceID string) (string, error) {
112 ws, err := b.GetWorkspace(workspaceID)
113 if err != nil {
114 return "", err
115 }
116 return agent.InitializePrompt(ws.Cfg)
117}
118
119// EnableDockerMCP validates Docker MCP availability, stages the
120// configuration, starts the MCP client, and persists the config.
121func (b *Backend) EnableDockerMCP(ctx context.Context, workspaceID string) error {
122 ws, err := b.GetWorkspace(workspaceID)
123 if err != nil {
124 return err
125 }
126
127 mcpConfig, err := ws.Cfg.PrepareDockerMCPConfig()
128 if err != nil {
129 return err
130 }
131
132 if err := mcptools.InitializeSingle(ctx, config.DockerMCPName, ws.Cfg); err != nil {
133 disableErr := mcptools.DisableSingle(ws.Cfg, config.DockerMCPName)
134 delete(ws.Cfg.Config().MCP, config.DockerMCPName)
135 return fmt.Errorf("failed to start docker MCP: %w", errors.Join(err, disableErr))
136 }
137
138 if err := ws.Cfg.PersistDockerMCPConfig(mcpConfig); err != nil {
139 disableErr := mcptools.DisableSingle(ws.Cfg, config.DockerMCPName)
140 delete(ws.Cfg.Config().MCP, config.DockerMCPName)
141 return fmt.Errorf("docker MCP started but failed to persist configuration: %w", errors.Join(err, disableErr))
142 }
143
144 return nil
145}
146
147// DisableDockerMCP closes the Docker MCP client, removes the
148// configuration, and persists the change.
149func (b *Backend) DisableDockerMCP(workspaceID string) error {
150 ws, err := b.GetWorkspace(workspaceID)
151 if err != nil {
152 return err
153 }
154
155 if err := mcptools.DisableSingle(ws.Cfg, config.DockerMCPName); err != nil {
156 return fmt.Errorf("failed to disable docker MCP: %w", err)
157 }
158
159 if err := ws.Cfg.DisableDockerMCP(); err != nil {
160 return err
161 }
162
163 return nil
164}
165
166// RefreshMCPTools refreshes the tools for a named MCP server.
167func (b *Backend) RefreshMCPTools(ctx context.Context, workspaceID, name string) error {
168 ws, err := b.GetWorkspace(workspaceID)
169 if err != nil {
170 return err
171 }
172 mcptools.RefreshTools(ctx, ws.Cfg, name)
173 return nil
174}
175
176// ReadMCPResource reads a resource from a named MCP server.
177func (b *Backend) ReadMCPResource(ctx context.Context, workspaceID, name, uri string) ([]MCPResourceContents, error) {
178 ws, err := b.GetWorkspace(workspaceID)
179 if err != nil {
180 return nil, err
181 }
182 contents, err := mcptools.ReadResource(ctx, ws.Cfg, name, uri)
183 if err != nil {
184 return nil, err
185 }
186 result := make([]MCPResourceContents, len(contents))
187 for i, c := range contents {
188 result[i] = MCPResourceContents{
189 URI: c.URI,
190 MIMEType: c.MIMEType,
191 Text: c.Text,
192 Blob: c.Blob,
193 }
194 }
195 return result, nil
196}
197
198// GetMCPPrompt retrieves a prompt from a named MCP server.
199func (b *Backend) GetMCPPrompt(workspaceID, clientID, promptID string, args map[string]string) (string, error) {
200 ws, err := b.GetWorkspace(workspaceID)
201 if err != nil {
202 return "", err
203 }
204 return commands.GetMCPPrompt(ws.Cfg, clientID, promptID, args)
205}
206
207// GetWorkingDir returns the working directory for a workspace.
208func (b *Backend) GetWorkingDir(workspaceID string) (string, error) {
209 ws, err := b.GetWorkspace(workspaceID)
210 if err != nil {
211 return "", err
212 }
213 return ws.Cfg.WorkingDir(), nil
214}