@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.
- package/CONTRIBUTING.md +95 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/SKILL.md +214 -0
- package/bin/flowmcp +80 -0
- package/cases/flowmcp.md +79 -0
- package/commands/add.sh +46 -0
- package/commands/connect.sh +61 -0
- package/commands/debug.sh +122 -0
- package/commands/inspect.sh +58 -0
- package/commands/install.sh +67 -0
- package/commands/lang.sh +38 -0
- package/commands/list.sh +35 -0
- package/commands/remove.sh +82 -0
- package/commands/rename.sh +104 -0
- package/commands/rotate.sh +33 -0
- package/commands/run-mcp.sh +27 -0
- package/commands/schema.sh +131 -0
- package/commands/secret-set.sh +37 -0
- package/commands/test.sh +131 -0
- package/friction.md +67 -0
- package/lib/audit.sh +17 -0
- package/lib/bootstrap.sh +18 -0
- package/lib/clients.sh +107 -0
- package/lib/common.sh +66 -0
- package/lib/i18n.sh +248 -0
- package/lib/oauth.sh +22 -0
- package/lib/profiles.sh +58 -0
- package/lib/secrets.sh +89 -0
- package/lib/ui.sh +121 -0
- package/package.json +51 -0
package/cases/flowmcp.md
ADDED
|
@@ -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.
|
package/commands/add.sh
ADDED
|
@@ -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
|
package/commands/lang.sh
ADDED
|
@@ -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
|
package/commands/list.sh
ADDED
|
@@ -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
|