@forhuman/flowmcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ # case: flowmcp
2
+
3
+ **Target:** `flowmcp` itself (own repo, not a wrapped third party
4
+ in the sense the rest of this file means it — see "contract origin" below).
5
+ **Terrain:** mixed — a defined local contract (profiles, secrets, client
6
+ config entries) plus a discovered remote one (Webflow's hosted MCP server
7
+ OAuth surface).
8
+ **Distribution:** npm (`@forhuman/flowmcp`), bash + Node/`npx`
9
+ runtime, `bin` aliases `flowmcp` and `fmcp`. Chosen because the
10
+ audience is JS-ecosystem developers/agencies already running Node for
11
+ `npx mcp-remote` / `npx webflow-mcp-server` anyway — a compiled binary would
12
+ have added packaging cost with no reachability gain for this audience.
13
+
14
+ ## Contract origin
15
+
16
+ Mixed, not stated out loud at the time (should have been — this case entry
17
+ is that admission). `lib/profiles.sh`/`lib/secrets.sh`/`lib/clients.sh` are
18
+ defined: we own the shape of a profile and a client config entry. The OAuth
19
+ surface against `mcp.webflow.com` was discovered: verified live (DCR
20
+ registration + full PKCE exchange) rather than assumed from Webflow's classic
21
+ Data API docs, which describe a different, client-secret-only flow.
22
+
23
+ ## What broke
24
+
25
+ A test-org name collision overwrote a real client's entry in the user's real
26
+ Claude Desktop config during manual testing of `rename` — recovered from the
27
+ tool's own automatic `.bak`, no data lost. Full account in `friction.md`,
28
+ folded in below.
29
+
30
+ ## Blocks adopted / rejected
31
+
32
+ - **Adopted:** OS keychain via `security`/`secret-tool`, with a `chmod 600`
33
+ file fallback only when no keychain backend exists.
34
+ - **Adopted, then replaced:** a hand-rolled PKCE + refresh + local-callback
35
+ OAuth flow, built and verified working, then thrown away for `mcp-remote`
36
+ (github.com/punkpeye/mcp-remote) once it was clear that package already
37
+ solves exactly this and is what the MCP ecosystem uses. What we kept
38
+ owning: per-org isolation via `MCP_REMOTE_CONFIG_DIR`.
39
+ - **Rejected:** a manual OAuth-App-setup step (client_id/secret the user
40
+ creates by hand before `connect` works) — assumed necessary, disproven by
41
+ reading `mcp.webflow.com`'s own `.well-known/oauth-authorization-server`.
42
+
43
+ ## Agent-first checklist, applied to this build
44
+
45
+ - `--json` + automatic JSON on non-TTY stdout: done, across every command
46
+ (read and write).
47
+ - No prompt blocks a non-interactive run: `secret-set`/`rotate` refuse
48
+ outright without a real TTY rather than hanging.
49
+ - `schema` command with a version field: added (`commands/schema.sh`,
50
+ `schema_version: 1`).
51
+ - Exit codes: 0 success, 1 for both user error and failed check — not yet
52
+ split further; revisit if a caller needs to distinguish "org not found"
53
+ from "network unreachable" programmatically rather than reading `.error`.
54
+ - Data on stdout, diagnostics on stderr: yes — `wfw_say_err/warn/hint` all
55
+ write to stderr, JSON payloads always to stdout.
56
+ - Secrets never echo, never accept as an argument: enforced at the CLI
57
+ surface (`secret-set`/`rotate` read via `read -s`, no org's token is ever
58
+ interpolated into a printed string).
59
+ - Trust ladder: sized to the actual damage. `install`/`remove`/`rename`
60
+ mutate real, shared, global client config files (Claude Code/Desktop/
61
+ Cursor) — that is the one place in this CLI where a wrong call has a real
62
+ blast radius (see "what broke" above), so those three now have `--dry-run`
63
+ and `remove` requires `--yes` with no interactive fallback. `add`/`connect`/
64
+ `list`/`inspect`/`test`/`debug` earn no gate: worst case is a wasted API
65
+ call or a duplicate profile you can `remove`.
66
+ - Audit log: append-only, `~/.flowmcp/audit/<YYYY-MM>.jsonl`,
67
+ written after the action completes (not two-phase pending/final) since
68
+ none of the mutating commands here are async or long-running enough for a
69
+ killed-mid-flight state to matter the way it would for a payment API.
70
+ - `nextSteps`: added to `inspect`/`test`/`debug`/`install`/`remove`/`rename`
71
+ JSON output as `next_steps: [string]`.
72
+
73
+ ## What I'd do differently
74
+
75
+ Open `friction.md` on day one instead of reconstructing it after the fact.
76
+ The rename/real-org collision is exactly the kind of finding the skill says
77
+ gets rediscovered when nobody writes it down at the time — this build
78
+ rediscovered a version of it we'd already have known to avoid if a prior
79
+ case existed.
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: flowmcp add <org> [--label "Display Name"]
3
+ # Registers profile metadata only, no token handling here. The suggested
4
+ # next step is `flowmcp connect <org>` (browser OAuth, no token to copy);
5
+ # `flowmcp secret-set <org>` is the fallback for headless environments with
6
+ # no browser available.
7
+
8
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/bootstrap.sh"
9
+
10
+ org="${1:?Usage: flowmcp add <org> [--label \"Display Name\"]}"
11
+ shift || true
12
+ label="$org"
13
+ while [[ $# -gt 0 ]]; do
14
+ case "$1" in
15
+ --label) label="$2"; shift 2 ;;
16
+ *) echo "error: unknown option '$1'" >&2; exit 1 ;;
17
+ esac
18
+ done
19
+
20
+ wfw_valid_org "$org" || {
21
+ wfw_say_err "$(wfw_t msg_invalid_org "$org")"
22
+ exit 1
23
+ }
24
+
25
+ if wfw_profile_exists "$org"; then
26
+ wfw_say_err "$(wfw_t msg_org_exists "$org")"
27
+ exit 1
28
+ fi
29
+
30
+ wfw_profile_write_new "$org" "$label"
31
+ wfw_audit_log "add" "$org" "ok"
32
+
33
+ backend="$(wfw_secret_backend)"
34
+ wfw_say_ok "$(wfw_t msg_add_ok "$org" "$label" "$backend")"
35
+ echo
36
+ wfw_say_next "$(wfw_t msg_add_recommend "$org")"
37
+ echo
38
+ echo " flowmcp connect $org"
39
+ echo
40
+ echo "$(wfw_t msg_add_no_browser)"
41
+ echo
42
+ echo " flowmcp secret-set $org"
43
+ echo
44
+ echo "${WFW_C_DIM}$(wfw_t msg_add_explain1)"
45
+ echo "$(wfw_t msg_add_explain2 "$backend")"
46
+ echo "$(wfw_t msg_add_explain3)${WFW_C_RESET}"
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: flowmcp connect <org> [--label "Name"]
3
+ #
4
+ # Human-friendly alternative to add + secret-set: opens a browser, the
5
+ # client logs into their own Webflow account and approves access, and the
6
+ # session is captured automatically — no copy/paste of a token by anyone,
7
+ # ever, and no setup step required beforehand.
8
+ #
9
+ # Under the hood this runs `npx mcp-remote` against Webflow's own hosted
10
+ # MCP server (mcp.webflow.com), which handles the full OAuth dance itself
11
+ # (dynamic client registration, PKCE, browser open, token refresh) — we
12
+ # just give each org an isolated storage directory so sessions never mix.
13
+ # Must be run interactively (it opens a real browser and waits for you).
14
+
15
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/bootstrap.sh"
16
+ wfw_require_npx
17
+
18
+ org="${1:?Usage: flowmcp connect <org> [--label \"Name\"]}"
19
+ shift || true
20
+ label="$org"
21
+ while [[ $# -gt 0 ]]; do
22
+ case "$1" in
23
+ --label) label="$2"; shift 2 ;;
24
+ *) echo "error: unknown option '$1'" >&2; exit 1 ;;
25
+ esac
26
+ done
27
+
28
+ wfw_valid_org "$org" || {
29
+ wfw_say_err "$(wfw_t msg_invalid_org "$org")"
30
+ exit 1
31
+ }
32
+
33
+ if wfw_profile_exists "$org"; then
34
+ echo "${WFW_C_DIM}$(wfw_t msg_reconnecting "$org")${WFW_C_RESET}"
35
+ else
36
+ wfw_profile_write_new "$org" "$label" "mcp-remote"
37
+ fi
38
+
39
+ remote_dir="$(wfw_mcp_remote_dir "$org")"
40
+ mkdir -p "$remote_dir"
41
+
42
+ wfw_t msg_connect_opening "$org"
43
+ wfw_t msg_connect_ctrlc1
44
+ wfw_t msg_connect_ctrlc2
45
+ echo
46
+
47
+ trap '' INT # let mcp-remote (child) react to Ctrl+C; don't let it kill this script too
48
+ MCP_REMOTE_CONFIG_DIR="$remote_dir" npx -y mcp-remote "$WFW_MCP_URL" --resource "$WFW_MCP_URL" || true
49
+ trap - INT
50
+
51
+ if wfw_mcp_remote_connected "$org"; then
52
+ wfw_profile_set_auth_method "$org" "mcp-remote"
53
+ wfw_audit_log "connect" "$org" "ok"
54
+ wfw_say_ok "$(wfw_t msg_connect_success "$org")"
55
+ wfw_say_next "$(wfw_t msg_connect_next "$org" "$org")"
56
+ else
57
+ wfw_audit_log "connect" "$org" "fail" "no completed session found"
58
+ wfw_say_err "$(wfw_t msg_connect_fail "$org")"
59
+ wfw_say_hint "$(wfw_t msg_connect_fail_hint "$org")"
60
+ exit 1
61
+ fi
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: flowmcp debug <org> [--json]
3
+ # Runs a battery of non-secret-leaking diagnostics and prints a summary.
4
+ # JSON automatically when stdout isn't a real TTY (e.g. an agent's tool call).
5
+
6
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/bootstrap.sh"
7
+
8
+ org="${1:?Usage: flowmcp debug <org> [--json]}"
9
+ shift || true
10
+ json_flag=""
11
+ [[ "${1:-}" == "--json" ]] && json_flag="1"
12
+
13
+ WFW_CHECKS=()
14
+
15
+ # wfw_check <name> <ok|fail|warn|note> <detail>
16
+ wfw_check() {
17
+ local name="$1" level="$2" detail="$3"
18
+ WFW_CHECKS+=("$(jq -nc --arg n "$name" --arg l "$level" --arg d "$detail" '{name:$n, level:$l, detail:$d}')")
19
+ if ! wfw_json_mode "$json_flag"; then
20
+ case "$level" in
21
+ ok) echo "ok: $detail" ;;
22
+ fail) echo "$(wfw_t dbg_level_fail): $detail" ;;
23
+ warn) echo "$(wfw_t dbg_level_warn): $detail" ;;
24
+ *) echo "$(wfw_t dbg_level_note): $detail" ;;
25
+ esac
26
+ fi
27
+ }
28
+
29
+ wfw_section() {
30
+ wfw_json_mode "$json_flag" || echo "-- $1 --"
31
+ }
32
+
33
+ wfw_json_mode "$json_flag" || wfw_t dbg_header "$org"
34
+
35
+ wfw_section "$(wfw_t dbg_sec_profile)"
36
+ if wfw_profile_exists "$org"; then
37
+ wfw_check "profile" "ok" "profile exists at $(wfw_profile_path "$org")"
38
+ else
39
+ wfw_check "profile" "fail" "no profile registered. Run: flowmcp add $org"
40
+ if wfw_json_mode "$json_flag"; then
41
+ jq -nc --arg org "$org" --argjson checks "$(printf '%s\n' "${WFW_CHECKS[@]}" | jq -sc '.')" \
42
+ '{org: $org, checks: $checks, next_steps: ["flowmcp add \($org)"]}'
43
+ fi
44
+ exit 1
45
+ fi
46
+
47
+ auth_method="$(jq -r '.auth_method // "pat"' <<<"$(wfw_profile_read "$org")")"
48
+ wfw_section "$(wfw_t dbg_sec_auth "$auth_method")"
49
+
50
+ if [[ "$auth_method" == "mcp-remote" ]]; then
51
+ if wfw_mcp_remote_connected "$org"; then
52
+ wfw_check "session" "ok" "a saved mcp-remote session exists for '$org'"
53
+ else
54
+ wfw_check "session" "fail" "no saved session. Run: flowmcp connect $org"
55
+ fi
56
+ wfw_section "$(wfw_t dbg_sec_mcpremote_avail)"
57
+ if command -v npx >/dev/null 2>&1; then
58
+ if ver="$(npm view mcp-remote version 2>/dev/null)"; then
59
+ wfw_check "mcp-remote" "ok" "reachable on npm (latest: $ver)"
60
+ else
61
+ wfw_check "mcp-remote" "warn" "could not reach npm registry to check mcp-remote (offline?)"
62
+ fi
63
+ else
64
+ wfw_check "mcp-remote" "fail" "npx not found — install Node.js"
65
+ fi
66
+ else
67
+ backend="$(wfw_secret_backend)"
68
+ wfw_section "$(wfw_t dbg_sec_secret_backend "$backend")"
69
+ if wfw_secret_exists "$org"; then
70
+ wfw_check "secret" "ok" "a token is stored for '$org'"
71
+ else
72
+ wfw_check "secret" "fail" "no token stored. Run: flowmcp secret-set $org"
73
+ fi
74
+ wfw_section "$(wfw_t dbg_sec_wfmcp_avail)"
75
+ if command -v npx >/dev/null 2>&1; then
76
+ if ver="$(npm view webflow-mcp-server version 2>/dev/null)"; then
77
+ wfw_check "webflow-mcp-server" "ok" "reachable on npm (latest: $ver)"
78
+ else
79
+ wfw_check "webflow-mcp-server" "warn" "could not reach npm registry to check webflow-mcp-server (offline?)"
80
+ fi
81
+ else
82
+ wfw_check "webflow-mcp-server" "fail" "npx not found — install Node.js"
83
+ fi
84
+ fi
85
+
86
+ wfw_section "$(wfw_t dbg_sec_cred_check)"
87
+ test_json="$("$WFW_COMMANDS_DIR/test.sh" "$org" --json 2>/dev/null || true)"
88
+ test_status="$(jq -r '.status // "fail"' <<<"$test_json" 2>/dev/null || echo fail)"
89
+ wfw_check "credentials" "$([[ "$test_status" == "ok" ]] && echo ok || echo fail)" "$(jq -r '.error // "valid"' <<<"$test_json" 2>/dev/null || echo "check failed")"
90
+ if ! wfw_json_mode "$json_flag"; then
91
+ "$WFW_COMMANDS_DIR/test.sh" "$org" || true
92
+ fi
93
+
94
+ wfw_section "$(wfw_t dbg_sec_client_configs)"
95
+ for spec in "claude-code:user" "claude-code:project" "claude-desktop:user" "cursor:user" "cursor:project"; do
96
+ client="${spec%%:*}"; scope="${spec#*:}"
97
+ path="$(wfw_client_config_path "$client" "$scope" 2>/dev/null || true)"
98
+ [[ -z "$path" ]] && continue
99
+ name="config:$spec"
100
+ if [[ -f "$path" ]]; then
101
+ if jq empty "$path" >/dev/null 2>&1; then
102
+ has_entry="$(jq -r --arg n "webflow-$org" '.mcpServers[$n] // empty' "$path")"
103
+ if [[ -n "$has_entry" ]]; then
104
+ wfw_check "$name" "ok" "$path has 'webflow-$org' entry"
105
+ else
106
+ wfw_check "$name" "note" "$path exists, valid JSON, no 'webflow-$org' entry"
107
+ fi
108
+ else
109
+ wfw_check "$name" "fail" "$path exists but is not valid JSON"
110
+ fi
111
+ else
112
+ wfw_check "$name" "note" "$path does not exist"
113
+ fi
114
+ done
115
+
116
+ if wfw_json_mode "$json_flag"; then
117
+ jq -nc --arg org "$org" --argjson checks "$(printf '%s\n' "${WFW_CHECKS[@]}" | jq -sc '.')" \
118
+ '{org: $org, checks: $checks,
119
+ next_steps: [$checks[] | select(.level == "fail") | .detail | select(startswith("Run: ") or test("flowmcp")) ] }'
120
+ else
121
+ wfw_t dbg_done
122
+ fi
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: flowmcp inspect <org> [--live] [--json]
3
+ # Prints full profile metadata (no token). Add --live to also re-run test.
4
+ # JSON automatically when stdout isn't a real TTY (e.g. an agent's tool call).
5
+
6
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/bootstrap.sh"
7
+
8
+ org="${1:?Usage: flowmcp inspect <org> [--live] [--json]}"
9
+ shift || true
10
+ live="" json_flag=""
11
+ while [[ $# -gt 0 ]]; do
12
+ case "$1" in
13
+ --live) live="1"; shift ;;
14
+ --json) json_flag="1"; shift ;;
15
+ *) echo "error: unknown option '$1'" >&2; exit 1 ;;
16
+ esac
17
+ done
18
+
19
+ wfw_profile_exists "$org" || {
20
+ echo "error: no org '$org' registered" >&2
21
+ exit 1
22
+ }
23
+
24
+ if [[ -n "$live" ]]; then
25
+ "$WFW_COMMANDS_DIR/test.sh" "$org" >/dev/null 2>&1 || true
26
+ fi
27
+
28
+ auth_method="$(jq -r '.auth_method // "pat"' <<<"$(wfw_profile_read "$org")")"
29
+ if [[ "$auth_method" == "mcp-remote" ]]; then
30
+ wfw_mcp_remote_connected "$org" && connected="true" || connected="false"
31
+ else
32
+ wfw_secret_exists "$org" && connected="true" || connected="false"
33
+ fi
34
+
35
+ if wfw_json_mode "$json_flag"; then
36
+ if [[ "$connected" == "false" ]]; then
37
+ if [[ "$auth_method" == "mcp-remote" ]]; then
38
+ next="[\"flowmcp connect $org\"]"
39
+ else
40
+ next="[\"flowmcp secret-set $org\"]"
41
+ fi
42
+ else
43
+ next="[\"flowmcp test $org\"]"
44
+ fi
45
+ jq --argjson connected "$connected" --argjson next "$next" \
46
+ '. + {connected: $connected, next_steps: $next}' "$(wfw_profile_path "$org")"
47
+ exit 0
48
+ fi
49
+
50
+ jq . "$(wfw_profile_path "$org")"
51
+ echo
52
+ if [[ "$connected" == "false" ]]; then
53
+ if [[ "$auth_method" == "mcp-remote" ]]; then
54
+ wfw_t msg_inspect_no_session "$org" "$org"
55
+ else
56
+ wfw_t msg_inspect_no_token "$org" "$org"
57
+ fi
58
+ fi
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: flowmcp install <org> <client> [--scope user|project] [--force] [--dry-run] [--json]
3
+ # client: claude-code | claude-desktop | cursor
4
+ #
5
+ # Writes an mcpServers entry that points at run-mcp.sh, so the token itself
6
+ # never has to be written into the client's config file.
7
+
8
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/bootstrap.sh"
9
+
10
+ org="${1:?Usage: flowmcp install <org> <client> [--scope user|project] [--force] [--dry-run]}"
11
+ client="${2:?Usage: flowmcp install <org> <client> [--scope user|project] [--force] [--dry-run]}"
12
+ shift 2
13
+ scope="user"
14
+ force=""
15
+ dry_run=""
16
+ json_flag=""
17
+ while [[ $# -gt 0 ]]; do
18
+ case "$1" in
19
+ --scope) scope="$2"; shift 2 ;;
20
+ --force) force="--force"; shift ;;
21
+ --dry-run) dry_run="1"; shift ;;
22
+ --json) json_flag="1"; shift ;;
23
+ *) echo "error: unknown option '$1'" >&2; exit 1 ;;
24
+ esac
25
+ done
26
+
27
+ wfw_profile_exists "$org" || {
28
+ wfw_say_err "$(wfw_t msg_install_no_org "$org" "$org" "$org")"
29
+ exit 1
30
+ }
31
+
32
+ config_path="$(wfw_client_config_path "$client" "$scope")" || exit 1
33
+ auth_method="$(jq -r '.auth_method // "pat"' <<<"$(wfw_profile_read "$org")")"
34
+ server_name="webflow-$org"
35
+
36
+ server_json="$(wfw_build_server_json "$org" "$auth_method")"
37
+ if [[ "$auth_method" == "mcp-remote" ]]; then
38
+ note="$(wfw_t msg_install_note_mcpremote)"
39
+ else
40
+ note="$(wfw_t msg_install_note_pat)"
41
+ fi
42
+
43
+ if [[ -n "$dry_run" ]]; then
44
+ if wfw_json_mode "$json_flag"; then
45
+ jq -nc --arg org "$org" --arg client "$client" --arg scope "$scope" --arg path "$config_path" \
46
+ --arg name "$server_name" --argjson entry "$server_json" \
47
+ '{ok: true, dry_run: true, org: $org, client: $client, scope: $scope, path: $path,
48
+ server_name: $name, entry: $entry, next_steps: ["flowmcp install \($org) \($client) --scope \($scope)"]}'
49
+ else
50
+ echo "${WFW_C_DIM}dry-run — would merge into $config_path:${WFW_C_RESET}"
51
+ jq -n --arg name "$server_name" --argjson entry "$server_json" '{mcpServers: {($name): $entry}}'
52
+ fi
53
+ exit 0
54
+ fi
55
+
56
+ wfw_client_merge_server "$config_path" "$server_name" "$server_json" "$force"
57
+ wfw_audit_log "install" "$org" "ok" "client=$client scope=$scope path=$config_path auth=$auth_method"
58
+
59
+ if wfw_json_mode "$json_flag"; then
60
+ jq -nc --arg org "$org" --arg client "$client" --arg scope "$scope" --arg path "$config_path" \
61
+ --arg name "$server_name" \
62
+ '{ok: true, dry_run: false, org: $org, client: $client, scope: $scope, path: $path, server_name: $name,
63
+ next_steps: ["restart \($client) to pick up the new server", "flowmcp test \($org)"]}'
64
+ else
65
+ wfw_say_ok "$(wfw_t msg_install_ok "$server_name" "$config_path")"
66
+ echo "${WFW_C_DIM}($note — $(wfw_t msg_install_restart "$client"))${WFW_C_RESET}"
67
+ fi
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: flowmcp lang [en|es] [--json]
3
+ # With no argument, prints the current language. With en/es, sets it —
4
+ # this is how a human changes the choice made at first run (or an agent
5
+ # sets it on their behalf; unlike secret-set/rotate, no token is involved
6
+ # here, so there's no reason to gate this behind a TTY).
7
+
8
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/bootstrap.sh"
9
+
10
+ json_flag=""
11
+ new_lang=""
12
+ for arg in "$@"; do
13
+ case "$arg" in
14
+ --json) json_flag="1" ;;
15
+ en|es) new_lang="$arg" ;;
16
+ *)
17
+ echo "error: unknown language '$arg' (supported: en, es)" >&2
18
+ exit 1
19
+ ;;
20
+ esac
21
+ done
22
+
23
+ if [[ -n "$new_lang" ]]; then
24
+ echo "$new_lang" > "$WFW_LANG_FILE"
25
+ if wfw_json_mode "$json_flag"; then
26
+ jq -nc --arg lang "$new_lang" '{ok: true, lang: $lang}'
27
+ else
28
+ wfw_say_ok "$(wfw_t msg_lang_set "$new_lang")"
29
+ fi
30
+ exit 0
31
+ fi
32
+
33
+ current="$(wfw_lang)"
34
+ if wfw_json_mode "$json_flag"; then
35
+ jq -nc --arg lang "$current" '{lang: $lang}'
36
+ else
37
+ echo "$current"
38
+ fi
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: flowmcp list [--json]
3
+ # Lists registered orgs and their last test status. Never prints tokens.
4
+ # JSON automatically when stdout isn't a real TTY (e.g. an agent's tool call).
5
+
6
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/bootstrap.sh"
7
+
8
+ json_flag=""
9
+ [[ "${1:-}" == "--json" ]] && json_flag="1"
10
+
11
+ orgs=($(wfw_profile_list))
12
+
13
+ if wfw_json_mode "$json_flag"; then
14
+ if [[ ${#orgs[@]} -eq 0 ]]; then
15
+ echo "[]"
16
+ exit 0
17
+ fi
18
+ jq -sc '.' <(for org in "${orgs[@]}"; do wfw_profile_read "$org"; done)
19
+ exit 0
20
+ fi
21
+
22
+ if [[ ${#orgs[@]} -eq 0 ]]; then
23
+ wfw_t msg_no_orgs
24
+ exit 0
25
+ fi
26
+
27
+ printf "${WFW_C_DIM}%-20s %-24s %-16s %-10s %s${WFW_C_RESET}\n" "ORG" "LABEL" "BACKEND" "LAST TEST" "TESTED AT"
28
+ for org in "${orgs[@]}"; do
29
+ p="$(wfw_profile_read "$org")"
30
+ label="$(jq -r '.label' <<<"$p")"
31
+ backend="$(jq -r '.secret_backend' <<<"$p")"
32
+ status="$(jq -r '.last_test.status // "never"' <<<"$p")"
33
+ ts="$(jq -r '.last_test.timestamp // "-"' <<<"$p")"
34
+ printf "%-20s %-24s %-16s %s %s\n" "$org" "$label" "$backend" "$(wfw_status_color_padded "$status" 10)" "$ts"
35
+ done
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: flowmcp remove <org> --yes [--from client:scope ...] [--dry-run] [--json]
3
+ # Destructive: deletes the profile and the stored secret. Requires --yes.
4
+ # Optionally also strips the entry from named client configs, e.g.:
5
+ # flowmcp remove acme --yes --from claude-code:user --from cursor:project
6
+
7
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/bootstrap.sh"
8
+
9
+ org="${1:?Usage: flowmcp remove <org> --yes [--from client:scope]... [--dry-run]}"
10
+ shift || true
11
+ confirmed=""
12
+ dry_run=""
13
+ json_flag=""
14
+ froms=()
15
+ while [[ $# -gt 0 ]]; do
16
+ case "$1" in
17
+ --yes) confirmed="1"; shift ;;
18
+ --from) froms+=("$2"); shift 2 ;;
19
+ --dry-run) dry_run="1"; shift ;;
20
+ --json) json_flag="1"; shift ;;
21
+ *) echo "error: unknown option '$1'" >&2; exit 1 ;;
22
+ esac
23
+ done
24
+
25
+ wfw_profile_exists "$org" || {
26
+ wfw_say_err "$(wfw_t msg_org_not_found "$org")"
27
+ exit 1
28
+ }
29
+
30
+ if [[ -n "$dry_run" ]]; then
31
+ targets=()
32
+ for spec in "${froms[@]:-}"; do
33
+ [[ -z "$spec" ]] && continue
34
+ client="${spec%%:*}"; scope="${spec#*:}"
35
+ config_path="$(wfw_client_config_path "$client" "$scope" 2>/dev/null || true)"
36
+ [[ -n "$config_path" && -f "$config_path" ]] && targets+=("$config_path")
37
+ done
38
+ if wfw_json_mode "$json_flag"; then
39
+ jq -nc --arg org "$org" --argjson targets "$(printf '%s\n' "${targets[@]:-}" | jq -R . | jq -sc 'map(select(length>0))')" \
40
+ '{ok: true, dry_run: true, org: $org, would_delete: ["profile", "stored credentials"], would_strip_from: $targets}'
41
+ else
42
+ echo "${WFW_C_DIM}dry-run — would delete profile + stored credentials for '$org'${WFW_C_RESET}"
43
+ for t in "${targets[@]:-}"; do echo "${WFW_C_DIM}would strip 'webflow-$org' from $t${WFW_C_RESET}"; done
44
+ fi
45
+ exit 0
46
+ fi
47
+
48
+ if [[ -z "$confirmed" ]]; then
49
+ if wfw_json_mode "$json_flag"; then
50
+ jq -nc --arg org "$org" '{ok: false, error: "confirmation required", next_steps: ["flowmcp remove \($org) --yes"]}'
51
+ else
52
+ wfw_say_err "$(wfw_t msg_remove_confirm_needed "$org")"
53
+ wfw_say_hint "$(wfw_t msg_remove_confirm_hint)"
54
+ echo "${WFW_C_DIM}exit 1 · refused by a gate · nothing was deleted${WFW_C_RESET}" >&2
55
+ fi
56
+ exit 1
57
+ fi
58
+
59
+ removed_from=()
60
+ for spec in "${froms[@]:-}"; do
61
+ [[ -z "$spec" ]] && continue
62
+ client="${spec%%:*}"
63
+ scope="${spec#*:}"
64
+ config_path="$(wfw_client_config_path "$client" "$scope" 2>/dev/null || true)"
65
+ if [[ -n "$config_path" && -f "$config_path" ]]; then
66
+ wfw_client_remove_server "$config_path" "webflow-$org"
67
+ removed_from+=("$config_path")
68
+ wfw_json_mode "$json_flag" || wfw_say_ok "$(wfw_t msg_remove_stripped "$org" "$config_path")"
69
+ fi
70
+ done
71
+
72
+ wfw_secret_delete "$org"
73
+ rm -rf "$(wfw_mcp_remote_dir "$org")"
74
+ wfw_profile_delete "$org"
75
+ wfw_audit_log "remove" "$org" "ok"
76
+
77
+ if wfw_json_mode "$json_flag"; then
78
+ jq -nc --arg org "$org" --argjson removed "$(printf '%s\n' "${removed_from[@]:-}" | jq -R . | jq -sc 'map(select(length>0))')" \
79
+ '{ok: true, dry_run: false, org: $org, removed_from: $removed}'
80
+ else
81
+ wfw_say_ok "$(wfw_t msg_remove_ok "$org")"
82
+ fi