Register the self-hosted Sentry MCP server and keep OAuth sign-ins across updates
link-mcp gains a stdio transport: the servers row names a launcher under user/agents/mcp/, run from the checkout so every machine runs the same script, and the token reaches it as MCP_TOKEN. sentry-gbrown is the first, pointing Sentry's MCP server at sentry.gbrown.org with a read-only token from the ignored env file. A server already registered exactly as its row would register it is now left alone. Re-adding it every run threw away the sign-in of the OAuth servers, bill-tracker and atlassian, on every update.
This commit is contained in:
1 parent
58a64d6cee
commit
eb0056ac94
3 files changed
+79
-15
No files matched your search
+58
-14
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
# MCP servers, registered with the agent runtimes on this machine.
|
# MCP servers, registered with the agent runtimes on this machine.
|
||||||
#
|
#
|
||||||
# An MCP server is a URL plus a bearer token, and the token is the whole reason
|
# An MCP server is a URL plus a bearer token (or, for a stdio server, a launcher
|
||||||
|
# script plus a token), and the token is the whole reason
|
||||||
# this is a stage rather than a manifest line. Panama is a public repository, so
|
# this is a stage rather than a manifest line. Panama is a public repository, so
|
||||||
# the tokens cannot live in it, and neither runtime keeps its server list in a
|
# the tokens cannot live in it, and neither runtime keeps its server list in a
|
||||||
# file that could be symlinked anyway: Codex writes them into config.toml beside
|
# file that could be symlinked anyway: Codex writes them into config.toml beside
|
||||||
@@ -62,17 +63,14 @@ fi
|
|||||||
registered=0
|
registered=0
|
||||||
skipped=0
|
skipped=0
|
||||||
|
|
||||||
while read -r name transport url token_var _rest; do
|
while read -r name transport target token_var _rest; do
|
||||||
case "${name:-}" in ''|'#'*) continue ;; esac
|
case "${name:-}" in ''|'#'*) continue ;; esac
|
||||||
if [[ -z "${transport:-}" || -z "${url:-}" ]]; then
|
if [[ -z "${transport:-}" || -z "${target:-}" ]]; then
|
||||||
warn "Ignoring malformed row for '$name'."
|
warn "Ignoring malformed row for '$name'."
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The token normally travels as an Authorization header. A server that only
|
token=""
|
||||||
# reads it from the query string (Penpot's userToken) marks the spot with
|
|
||||||
# {TOKEN} in its URL instead, and gets no header.
|
|
||||||
header=()
|
|
||||||
if [[ -n "${token_var:-}" ]]; then
|
if [[ -n "${token_var:-}" ]]; then
|
||||||
token="${!token_var:-}"
|
token="${!token_var:-}"
|
||||||
if [[ -z "$token" ]]; then
|
if [[ -z "$token" ]]; then
|
||||||
@@ -80,20 +78,66 @@ while read -r name transport url token_var _rest; do
|
|||||||
skipped=$((skipped + 1))
|
skipped=$((skipped + 1))
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# An http server's token normally travels as an Authorization header. One
|
||||||
|
# that only reads it from the query string (Penpot's userToken) marks the
|
||||||
|
# spot with {TOKEN} in its URL instead, and gets no header. A stdio server's
|
||||||
|
# target is a launcher under user/agents/mcp/, run from this checkout so
|
||||||
|
# every machine runs the same script, and its token arrives as MCP_TOKEN.
|
||||||
|
url="$target"
|
||||||
|
args=()
|
||||||
|
if [[ "$transport" == stdio ]]; then
|
||||||
|
launcher="$MCP_DIR/$target"
|
||||||
|
if [[ ! -x "$launcher" ]]; then
|
||||||
|
warn "Skipping $name: $launcher is not an executable launcher."
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
[[ -n "$token" ]] && args=(-e "MCP_TOKEN=$token")
|
||||||
|
elif [[ -n "$token" ]]; then
|
||||||
if [[ "$url" == *'{TOKEN}'* ]]; then
|
if [[ "$url" == *'{TOKEN}'* ]]; then
|
||||||
url="${url//\{TOKEN\}/$token}"
|
url="${url//\{TOKEN\}/$token}"
|
||||||
else
|
else
|
||||||
header=(-H "Authorization: $token")
|
args=(-H "Authorization: $token")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Re-registering is how this stays idempotent across upgrades, and how a
|
# A server already registered exactly as this row would register it is left
|
||||||
# rotated token reaches the runtime. Removing first avoids the CLI refusing
|
# alone. Removing and re-adding it would throw away the OAuth session of a
|
||||||
# a name it already knows. Neither call may print the token, so both are
|
# server that signs in on its own (bill-tracker, atlassian), so every
|
||||||
# quiet unless they fail.
|
# upgrade would sign them out again.
|
||||||
|
if [[ "$transport" == stdio ]]; then
|
||||||
|
want="$(jq -cn --arg c "$launcher" --arg t "$token" \
|
||||||
|
'{command: $c, env: (if $t == "" then null else {MCP_TOKEN: $t} end)}')"
|
||||||
|
have="$(jq -c --arg n "$name" \
|
||||||
|
'.mcpServers[$n] // {} | {command, env: (if (.env // {}) == {} then null else .env end)}' \
|
||||||
|
"$HOME/.claude.json" 2>/dev/null || true)"
|
||||||
|
else
|
||||||
|
auth=""
|
||||||
|
[[ "${args[0]:-}" == -H ]] && auth="${args[1]#Authorization: }"
|
||||||
|
want="$(jq -cn --arg u "$url" --arg a "$auth" \
|
||||||
|
'{url: $u, auth: (if $a == "" then null else $a end)}')"
|
||||||
|
have="$(jq -c --arg n "$name" \
|
||||||
|
'.mcpServers[$n] // {} | {url, auth: (.headers.Authorization // null)}' \
|
||||||
|
"$HOME/.claude.json" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
if [[ -n "$have" && "$have" == "$want" ]]; then
|
||||||
|
log "Already registered $name."
|
||||||
|
registered=$((registered + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Re-registering is how a new server or a rotated token reaches the
|
||||||
|
# runtime. Removing first avoids the CLI refusing a name it already knows.
|
||||||
|
# Neither call may print the token, so both are quiet unless they fail.
|
||||||
claude mcp remove "$name" -s user >/dev/null 2>&1 || true
|
claude mcp remove "$name" -s user >/dev/null 2>&1 || true
|
||||||
if claude mcp add --transport "$transport" "$name" "$url" "${header[@]}" \
|
if [[ "$transport" == stdio ]]; then
|
||||||
-s user >/dev/null 2>&1; then
|
add=(claude mcp add "$name" -s user "${args[@]}" -- "$launcher")
|
||||||
|
else
|
||||||
|
add=(claude mcp add --transport "$transport" "$name" "$url" "${args[@]}" -s user)
|
||||||
|
fi
|
||||||
|
if "${add[@]}" >/dev/null 2>&1; then
|
||||||
log "Registered $name."
|
log "Registered $name."
|
||||||
registered=$((registered + 1))
|
registered=$((registered + 1))
|
||||||
else
|
else
|
||||||
|
|||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sentry's MCP server, pointed at the self-hosted sentry.gbrown.org.
|
||||||
|
#
|
||||||
|
# link-mcp registers this file as a stdio server and hands it the token as
|
||||||
|
# MCP_TOKEN, read from SENTRY_GBROWN_TOKEN in user/agents/mcp/env. The token is
|
||||||
|
# a read-only personal token (Sentry > User settings > Personal Tokens, scopes
|
||||||
|
# alerts/event/org/project/team:read). The hosted sentry.io server cannot reach
|
||||||
|
# a self-hosted instance, which is why this one runs locally.
|
||||||
|
set -euo pipefail
|
||||||
|
: "${MCP_TOKEN:?MCP_TOKEN is not set; register this server through link-mcp}"
|
||||||
|
export SENTRY_ACCESS_TOKEN="$MCP_TOKEN"
|
||||||
|
exec npx -y @sentry/mcp-server@latest --host=sentry.gbrown.org --organization-slug=sentry
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
# MCP servers registered with the local agent runtimes.
|
# MCP servers registered with the local agent runtimes.
|
||||||
#
|
#
|
||||||
# <name> <transport> <url> <token variable>
|
# <name> <transport> <url or launcher> <token variable>
|
||||||
|
#
|
||||||
|
# transport is http (third column is a URL) or stdio (third column is a launcher
|
||||||
|
# under user/agents/mcp/, which receives the token as MCP_TOKEN).
|
||||||
#
|
#
|
||||||
# The token variable names an entry in user/agents/mcp/env, which is ignored by
|
# The token variable names an entry in user/agents/mcp/env, which is ignored by
|
||||||
# git. This file carries no secret, so it is tracked like the rest of user/ and
|
# git. This file carries no secret, so it is tracked like the rest of user/ and
|
||||||
@@ -29,3 +32,8 @@ atlassian http https://mcp.atlassian.com/v1/mcp
|
|||||||
# key from Penpot's Account > Integrations, and Penpot only reads it from the
|
# key from Penpot's Account > Integrations, and Penpot only reads it from the
|
||||||
# query string, hence {TOKEN}. The same key connects the in-file MCP plugin.
|
# query string, hence {TOKEN}. The same key connects the in-file MCP plugin.
|
||||||
penpot http https://penpot.gbrown.org/mcp/stream?userToken={TOKEN} PENPOT_MCP_TOKEN
|
penpot http https://penpot.gbrown.org/mcp/stream?userToken={TOKEN} PENPOT_MCP_TOKEN
|
||||||
|
|
||||||
|
# Sentry for the self-hosted sentry.gbrown.org. A stdio server: the third column
|
||||||
|
# is a launcher under user/agents/mcp/, and the token reaches it as MCP_TOKEN.
|
||||||
|
# The hosted mcp.sentry.dev only speaks to sentry.io.
|
||||||
|
sentry-gbrown stdio bin/sentry-gbrown SENTRY_GBROWN_TOKEN
|
||||||
Reference in new issue
Block a user