Skip to content

Commit ffdb7ee

Browse files
committed
feat: surface workspace base path in CLI, MCP, and import responses
Users struggled to discover that non-default workspace mocks are served under a base path prefix. Now basePath is visible in: - `workspace list` table output (new BASE PATH column) - `workspace create` output (prints base path + example URL) - `workspace show` output (prints base path info) - MCP manage_workspace list/create/switch responses (basePath + hint) - MCP import_mocks response (basePath + hint when workspace is set) - CLI import output (prints workspace name and base path after import) Also: --bind help text now mentions `patch` as valid action across all add/update/http/soap commands. CLAUDE.md updated to match.
1 parent 35abc5f commit ffdb7ee

9 files changed

Lines changed: 72 additions & 12 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ mockd add http --path /api/users/{id} --method DELETE --table users --bind delet
169169
mockd add http --path /api/users/{id}/verify --method POST --table users --bind custom --operation VerifyUser
170170
```
171171

172-
Actions: `list`, `get`, `create`, `update`, `delete`, `custom`. For `custom`, also provide `--operation` (CLI) or `"operation"` (MCP).
172+
Actions: `list`, `get`, `create`, `update`, `patch`, `delete`, `custom`. For `custom`, also provide `--operation` (CLI) or `"operation"` (MCP).
173173

174174
## Template Functions
175175

pkg/cli/add.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ func init() {
142142
addCmd.Flags().StringVar(&addStatefulOperation, "stateful-operation", "", "Wire to a custom stateful operation (e.g., TransferFunds)")
143143

144144
addCmd.Flags().StringVar(&addTable, "table", "", "Bind to a stateful resource table (e.g., users)")
145-
addCmd.Flags().StringVar(&addBindAction, "bind", "", "Stateful action: list, get, create, update, delete, custom")
145+
addCmd.Flags().StringVar(&addBindAction, "bind", "", "Stateful action: list, get, create, update, patch, delete, custom")
146146

147147
addCmd.Flags().StringVar(&addSoapAction, "soap-action", "", "SOAPAction header value")
148148

@@ -402,10 +402,10 @@ Run 'mockd add --help' for more options`)
402402
if bindAction != "" {
403403
validBindActions := map[string]bool{
404404
"list": true, "get": true, "create": true,
405-
"update": true, "delete": true, "custom": true,
405+
"update": true, "patch": true, "delete": true, "custom": true,
406406
}
407407
if !validBindActions[bindAction] {
408-
return nil, fmt.Errorf("invalid --bind value %q: must be one of list, get, create, update, delete, custom", bindAction)
408+
return nil, fmt.Errorf("invalid --bind value %q: must be one of list, get, create, update, patch, delete, custom", bindAction)
409409
}
410410
// --table/--bind is mutually exclusive with --body, --body-file, --sse, --stateful-operation
411411
if body != "" || bodyFile != "" {
@@ -899,10 +899,10 @@ Run 'mockd add --help' for more options`)
899899
if bindAction != "" {
900900
validActions := map[string]bool{
901901
"list": true, "get": true, "create": true,
902-
"update": true, "delete": true, "custom": true,
902+
"update": true, "patch": true, "delete": true, "custom": true,
903903
}
904904
if !validActions[bindAction] {
905-
return nil, fmt.Errorf("invalid --bind %q: must be one of list, get, create, update, delete, custom", bindAction)
905+
return nil, fmt.Errorf("invalid --bind %q: must be one of list, get, create, update, patch, delete, custom", bindAction)
906906
}
907907
}
908908

pkg/cli/bridge.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"path/filepath"
99
"strings"
1010

11+
"github.com/getmockd/mockd/pkg/cliconfig"
1112
"github.com/getmockd/mockd/pkg/config"
1213
"github.com/getmockd/mockd/pkg/portability"
1314
"github.com/spf13/cobra"
@@ -410,6 +411,17 @@ func importData(data []byte, source string, impFormat portability.Format) error
410411
if importReplace {
411412
fmt.Printf("Total mocks: %d\n", result.Total)
412413
}
414+
415+
// Surface workspace base path so users know where mocks are served
416+
ws := resolvedWorkspace()
417+
if ws != "" {
418+
wsClient := NewWorkspaceClient(cliconfig.ResolveAdminURL(adminURL), nil)
419+
wsInfo, err := wsClient.GetWorkspace(ws)
420+
if err == nil && wsInfo != nil && wsInfo.BasePath != "" {
421+
fmt.Printf("Workspace: %s (base path: %s)\n", wsInfo.Name, wsInfo.BasePath)
422+
fmt.Printf("Mocks are served under %s (e.g., %s/v1/resource)\n", wsInfo.BasePath, wsInfo.BasePath)
423+
}
424+
}
413425
})
414426

415427
return nil

pkg/cli/http.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func init() {
8989
httpAddCmd.Flags().StringVar(&addName, "name", "", "Mock display name")
9090
httpAddCmd.Flags().StringVar(&addStatefulOperation, "stateful-operation", "", "Wire to a custom stateful operation (e.g., TransferFunds)")
9191
httpAddCmd.Flags().StringVar(&addTable, "table", "", "Bind to a stateful resource table (e.g., users)")
92-
httpAddCmd.Flags().StringVar(&addBindAction, "bind", "", "Stateful action: list, get, create, update, delete, custom")
92+
httpAddCmd.Flags().StringVar(&addBindAction, "bind", "", "Stateful action: list, get, create, update, patch, delete, custom")
9393

9494
// Missing commands like list/get/delete fall back to root aliases exactly like before
9595
httpCmd.AddCommand(&cobra.Command{

pkg/cli/soap.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func init() {
113113
soapAddCmd.Flags().StringVar(&addOperation, "action", "", "SOAP action")
114114
soapAddCmd.Flags().StringVar(&addResponse, "response", "", "XML response body")
115115
soapAddCmd.Flags().StringVar(&soapAddTable, "table", "", "Bind to a stateful resource table (e.g., users)")
116-
soapAddCmd.Flags().StringVar(&soapAddBindAction, "bind", "", "Stateful action: list, get, create, update, delete, custom")
116+
soapAddCmd.Flags().StringVar(&soapAddBindAction, "bind", "", "Stateful action: list, get, create, update, patch, delete, custom")
117117

118118
// Add list/get/delete generic aliases
119119
soapCmd.AddCommand(&cobra.Command{

pkg/cli/update.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func init() {
4949
updateCmd.Flags().VarP(&updateHeaders, "header", "H", "Response header (key:value), repeatable")
5050
updateCmd.Flags().IntVar(&updateDelay, "delay", 0, "Response delay in milliseconds")
5151
updateCmd.Flags().StringVar(&updateTable, "table", "", "Bind to a stateful resource table")
52-
updateCmd.Flags().StringVar(&updateBind, "bind", "", "Stateful action: list, get, create, update, delete, custom")
52+
updateCmd.Flags().StringVar(&updateBind, "bind", "", "Stateful action: list, get, create, update, patch, delete, custom")
5353
updateCmd.Flags().StringVar(&updateOperation, "operation", "", "Custom operation name (for --bind custom)")
5454
updateCmd.Flags().StringVarP(&updateName, "name", "n", "", "Mock display name")
5555
updateCmd.Flags().StringVar(&updateEnabled, "enabled", "", "Enable or disable the mock (true/false)")

pkg/cli/workspace.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type WorkspaceDTO struct {
2424
Name string `json:"name"`
2525
Type string `json:"type"`
2626
Description string `json:"description,omitempty"`
27+
BasePath string `json:"basePath"`
2728
Path string `json:"path,omitempty"`
2829
URL string `json:"url,omitempty"`
2930
Branch string `json:"branch,omitempty"`
@@ -93,6 +94,9 @@ func runWorkspaceShow() error {
9394
if ws.Description != "" {
9495
result["workspaceDescription"] = ws.Description
9596
}
97+
if ws.BasePath != "" {
98+
result["basePath"] = ws.BasePath
99+
}
96100
}
97101
}
98102

@@ -110,6 +114,10 @@ func runWorkspaceShow() error {
110114
if desc, ok := result["workspaceDescription"].(string); ok {
111115
fmt.Printf(" Description: %s\n", desc)
112116
}
117+
if bp, ok := result["basePath"].(string); ok {
118+
fmt.Printf(" Base path: %s\n", bp)
119+
fmt.Printf(" Mocks served under: %s\n", bp)
120+
}
113121
})
114122
return nil
115123
}
@@ -210,7 +218,7 @@ var workspaceListCmd = &cobra.Command{
210218
}
211219

212220
w := output.Table()
213-
_, _ = fmt.Fprintln(w, "CURRENT\tID\tNAME\tTYPE\tDESCRIPTION")
221+
_, _ = fmt.Fprintln(w, "CURRENT\tID\tNAME\tBASE PATH\tTYPE\tDESCRIPTION")
214222

215223
for _, ws := range workspaces {
216224
current := ""
@@ -231,7 +239,12 @@ var workspaceListCmd = &cobra.Command{
231239
id = id[:17] + "..."
232240
}
233241

234-
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", current, id, ws.Name, ws.Type, description)
242+
basePath := ws.BasePath
243+
if basePath == "" {
244+
basePath = "/"
245+
}
246+
247+
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", current, id, ws.Name, basePath, ws.Type, description)
235248
}
236249

237250
_ = w.Flush()
@@ -292,6 +305,10 @@ var workspaceCreateCmd = &cobra.Command{
292305

293306
printResult(ws, func() {
294307
fmt.Printf("Created workspace %q (ID: %s)\n", ws.Name, ws.ID)
308+
if ws.BasePath != "" {
309+
fmt.Printf(" Base path: %s\n", ws.BasePath)
310+
fmt.Printf(" Mocks in this workspace are served under %s (e.g., %s/v1/resource)\n", ws.BasePath, ws.BasePath)
311+
}
295312
if workspaceCreateUseCurrent {
296313
fmt.Printf("Switched to workspace %q\n", ws.ID)
297314
}

pkg/mcp/tool_context.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ func handleListWorkspaces(_ map[string]interface{}, session *MCPSession, _ *Serv
145145
Name string `json:"name"`
146146
Type string `json:"type,omitempty"`
147147
Description string `json:"description,omitempty"`
148+
BasePath string `json:"basePath"`
148149
Active bool `json:"active"`
149150
}
150151

@@ -155,6 +156,7 @@ func handleListWorkspaces(_ map[string]interface{}, session *MCPSession, _ *Serv
155156
Name: ws.Name,
156157
Type: ws.Type,
157158
Description: ws.Description,
159+
BasePath: ws.BasePath,
158160
Active: ws.ID == currentWS,
159161
})
160162
}
@@ -174,7 +176,8 @@ func handleSwitchWorkspace(args map[string]interface{}, session *MCPSession, _ *
174176
return ToolResultError("id is required"), nil
175177
}
176178

177-
// Validate the workspace exists before switching
179+
// Validate the workspace exists before switching and capture details
180+
var wsName, wsBasePath string
178181
client := session.GetAdminClient()
179182
if client != nil {
180183
workspaces, err := client.ListWorkspaces()
@@ -183,6 +186,8 @@ func handleSwitchWorkspace(args map[string]interface{}, session *MCPSession, _ *
183186
for _, ws := range workspaces {
184187
if ws.ID == id {
185188
found = true
189+
wsName = ws.Name
190+
wsBasePath = ws.BasePath
186191
break
187192
}
188193
}
@@ -200,6 +205,13 @@ func handleSwitchWorkspace(args map[string]interface{}, session *MCPSession, _ *
200205
"switched": true,
201206
"workspace": id,
202207
}
208+
if wsName != "" {
209+
result["name"] = wsName
210+
}
211+
if wsBasePath != "" {
212+
result["basePath"] = wsBasePath
213+
result["hint"] = "Mocks in this workspace are served under " + wsBasePath + " (e.g., " + wsBasePath + "/v1/resource)"
214+
}
203215

204216
return ToolResultJSON(result)
205217
}
@@ -230,6 +242,10 @@ func handleCreateWorkspace(args map[string]interface{}, session *MCPSession, _ *
230242
if ws.Type != "" {
231243
result["type"] = ws.Type
232244
}
245+
if ws.BasePath != "" {
246+
result["basePath"] = ws.BasePath
247+
result["hint"] = "Mocks in this workspace are served under the base path " + ws.BasePath + " (e.g., " + ws.BasePath + "/v1/resource). Use 'manage_workspace switch' to route subsequent MCP operations to this workspace."
248+
}
233249

234250
return ToolResultJSON(result)
235251
}

pkg/mcp/tool_import.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,21 @@ func handleImportMocks(args map[string]interface{}, session *MCPSession, server
110110
result["message"] = importResult.Message
111111
}
112112

113+
// Surface workspace base path so users know where mocks are served.
114+
if workspace != "" {
115+
result["workspace"] = workspace
116+
workspaces, err := client.ListWorkspaces()
117+
if err == nil {
118+
for _, ws := range workspaces {
119+
if ws.ID == workspace && ws.BasePath != "" {
120+
result["basePath"] = ws.BasePath
121+
result["hint"] = "Mocks are served under " + ws.BasePath + " (e.g., " + ws.BasePath + "/v1/resource)"
122+
break
123+
}
124+
}
125+
}
126+
}
127+
113128
return ToolResultJSON(result)
114129
}
115130

0 commit comments

Comments
 (0)