From 79c5574cb8b81d9dfc7bb5153e2fceccaf814f35 Mon Sep 17 00:00:00 2001 From: adilei Date: Fri, 27 Mar 2026 15:40:31 +0300 Subject: [PATCH] Improve deploy script: colored output, name conflict handling, fixed paths - Numbered steps with colored status indicators - Server logs redirected to /tmp/ for clean output - Auto-retry with random suffix on connector name conflict - Fix paths for sample directory structure (connector/ not mcp-connector/) - Use built JS instead of ts-node - Require environment ID as argument (no hardcoded default) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/deploy.sh | 265 +++++++++++------- 1 file changed, 165 insertions(+), 100 deletions(-) diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/scripts/deploy.sh b/extensibility/mcp/dynamic-mcp-routing-typescript/scripts/deploy.sh index 4fc02905..516f2b61 100755 --- a/extensibility/mcp/dynamic-mcp-routing-typescript/scripts/deploy.sh +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/scripts/deploy.sh @@ -1,13 +1,7 @@ #!/usr/bin/env bash -# -# deploy.sh — Start servers, create a dev tunnel, and deploy/update -# the Power Platform connector in one step. -# -# Usage: -# ./scripts/deploy.sh [ENVIRONMENT_ID] [TENANT_ID] -# -set -euo pipefail +set -uo pipefail +# --- Configuration --- PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" CATALOG_PORT=3000 MCP_PORT=3001 @@ -19,91 +13,123 @@ PROPS_FILE="$PROJECT_DIR/connector/apiProperties.json" SCRIPT_FILE="$PROJECT_DIR/connector/script.csx" PACONN_TOKEN_FILE="$HOME/.paconn/accessTokens.json" -# --- 0. Ensure paconn is logged in (correct tenant) --- -ensure_login() { - local need_login=false +# --- Output helpers --- +BOLD="\033[1m" +DIM="\033[2m" +GREEN="\033[32m" +YELLOW="\033[33m" +RED="\033[31m" +CYAN="\033[36m" +RESET="\033[0m" + +step() { echo -e "\n${BOLD}${CYAN}[$1]${RESET} ${BOLD}$2${RESET}"; } +info() { echo -e " ${DIM}$1${RESET}"; } +ok() { echo -e " ${GREEN}✓${RESET} $1"; } +warn() { echo -e " ${YELLOW}⚠${RESET} $1"; } +fail() { echo -e " ${RED}✗${RESET} $1"; } + +# --- Cleanup --- +cleanup() { + echo "" + step "•" "Shutting down..." + [ -n "${CATALOG_PID:-}" ] && kill "$CATALOG_PID" 2>/dev/null + [ -n "${MCP_PID:-}" ] && kill "$MCP_PID" 2>/dev/null + [ -n "${TUNNEL_PID:-}" ] && kill "$TUNNEL_PID" 2>/dev/null + wait 2>/dev/null + ok "All processes stopped." +} +trap cleanup EXIT + +# ============================================================ +# Step 1: Authentication +# ============================================================ +step "1/5" "Authenticating with Power Platform" - if [ ! -f "$PACONN_TOKEN_FILE" ]; then - echo " No token file found." +need_login=false +if [ ! -f "$PACONN_TOKEN_FILE" ]; then + info "No token file found." + need_login=true +else + expires_on=$(python3 -c "import json; print(json.load(open('$PACONN_TOKEN_FILE')).get('expires_on','0'))" 2>/dev/null || echo "0") + now=$(python3 -c "import time; print(time.time())") + if python3 -c "exit(0 if float('$expires_on') < float('$now') else 1)" 2>/dev/null; then + info "Token expired." need_login=true - else - local expires_on - expires_on=$(python3 -c "import json; print(json.load(open('$PACONN_TOKEN_FILE')).get('expires_on','0'))" 2>/dev/null || echo "0") - local now - now=$(python3 -c "import time; print(time.time())") - if python3 -c "exit(0 if float('$expires_on') < float('$now') else 1)" 2>/dev/null; then - echo " Token expired." - need_login=true - fi + fi - if [ -n "$TENANT_ID" ] && [ "$need_login" = false ]; then - local current_tenant - current_tenant=$(python3 -c "import json; print(json.load(open('$PACONN_TOKEN_FILE')).get('tenant_id',''))" 2>/dev/null || echo "") - if [ "$current_tenant" != "$TENANT_ID" ]; then - echo " Logged into tenant $current_tenant, need $TENANT_ID." - need_login=true - fi + if [ -n "$TENANT_ID" ] && [ "$need_login" = false ]; then + current_tenant=$(python3 -c "import json; print(json.load(open('$PACONN_TOKEN_FILE')).get('tenant_id',''))" 2>/dev/null || echo "") + if [ "$current_tenant" != "$TENANT_ID" ]; then + info "Logged into tenant $current_tenant, need $TENANT_ID." + need_login=true fi fi +fi - if [ "$need_login" = true ]; then - echo " Logging in..." - if [ -n "$TENANT_ID" ]; then - python3 -m paconn login -t "$TENANT_ID" - else - python3 -m paconn login - fi - echo " Login complete." +if [ "$need_login" = true ]; then + warn "Login required — follow the device code prompt below:" + if [ -n "$TENANT_ID" ]; then + python3 -m paconn login -t "$TENANT_ID" else - local user_id - user_id=$(python3 -c "import json; print(json.load(open('$PACONN_TOKEN_FILE')).get('user_id','unknown'))" 2>/dev/null) - echo " Logged in as $user_id" + python3 -m paconn login fi -} + ok "Login complete." +else + user_id=$(python3 -c "import json; print(json.load(open('$PACONN_TOKEN_FILE')).get('user_id','unknown'))" 2>/dev/null) + ok "Logged in as $user_id" +fi -echo "==> Checking paconn login..." -ensure_login +# ============================================================ +# Step 2: Start servers +# ============================================================ +step "2/5" "Starting servers" -# --- Helpers --- -cleanup() { - echo "" - echo "Shutting down..." - [ -n "${CATALOG_PID:-}" ] && kill "$CATALOG_PID" 2>/dev/null - [ -n "${MCP_PID:-}" ] && kill "$MCP_PID" 2>/dev/null - [ -n "${TUNNEL_PID:-}" ] && kill "$TUNNEL_PID" 2>/dev/null - wait 2>/dev/null - echo "Done." -} -trap cleanup EXIT - -# --- 1. Build & start servers --- -echo "==> Building..." +info "Building..." cd "$PROJECT_DIR" -npm run build +npm run build > /dev/null 2>&1 -echo "==> Starting catalog server (port $CATALOG_PORT)..." -PORT=$CATALOG_PORT node build/catalog/index.js & +info "Catalog server on port $CATALOG_PORT..." +PORT=$CATALOG_PORT node build/catalog/index.js > /tmp/catalog-server.log 2>&1 & CATALOG_PID=$! -echo "==> Starting MCP server (port $MCP_PORT)..." -PORT=$MCP_PORT node build/mcp-server/index.js & +info "MCP server on port $MCP_PORT..." +PORT=$MCP_PORT node build/mcp-server/index.js > /tmp/mcp-server.log 2>&1 & MCP_PID=$! -echo " Waiting for servers..." -for i in $(seq 1 15); do - if curl -sf http://localhost:$CATALOG_PORT/instances > /dev/null 2>&1; then - echo " Both servers ready." +info "Waiting for servers to respond..." +SERVERS_READY=false +for i in $(seq 1 20); do + catalog_ok=false + mcp_ok=false + curl -sf http://localhost:$CATALOG_PORT/instances > /dev/null 2>&1 && catalog_ok=true + curl -sf -o /dev/null -w '' http://localhost:$MCP_PORT/instances/contoso/mcp 2>/dev/null && mcp_ok=true + # Also accept connection refused → not ready yet; 404/405 → server is up + curl -s -o /dev/null -w "%{http_code}" http://localhost:$MCP_PORT/ 2>/dev/null | grep -qE "^[2-5]" && mcp_ok=true + if [ "$catalog_ok" = true ] && [ "$mcp_ok" = true ]; then + SERVERS_READY=true break fi sleep 1 done +if [ "$SERVERS_READY" = true ]; then + ok "Catalog server ready on :$CATALOG_PORT" + ok "MCP server ready on :$MCP_PORT" +else + fail "Servers did not start in time." + info "Catalog log: /tmp/catalog-server.log" + info "MCP log: /tmp/mcp-server.log" + exit 1 +fi + +# ============================================================ +# Step 3: Dev tunnel +# ============================================================ +step "3/5" "Creating dev tunnel" -# --- 2. Start devtunnel --- -echo "==> Starting devtunnel for ports $CATALOG_PORT and $MCP_PORT..." +info "Exposing ports $CATALOG_PORT and $MCP_PORT..." devtunnel host -p "$CATALOG_PORT" -p "$MCP_PORT" --allow-anonymous > /tmp/devtunnel-output.log 2>&1 & TUNNEL_PID=$! -echo " Waiting for tunnel..." CATALOG_HOST="" MCP_HOST="" for i in $(seq 1 30); do @@ -112,33 +138,35 @@ for i in $(seq 1 30); do MCP_HOST=$(grep -oE "[a-z0-9]+-${MCP_PORT}\.[a-z]+\.devtunnels\.ms" /tmp/devtunnel-output.log | head -1) break fi + if [ "$i" -eq 30 ]; then + fail "Tunnel did not start in time." + info "Log: /tmp/devtunnel-output.log" + exit 1 + fi sleep 1 done -if [ -z "$CATALOG_HOST" ] || [ -z "$MCP_HOST" ]; then - echo "ERROR: Could not extract tunnel URLs. Log:" - cat /tmp/devtunnel-output.log - exit 1 -fi - -echo " Tunnel ready!" -echo " Catalog: https://$CATALOG_HOST" -echo " MCP: https://$MCP_HOST" +ok "Catalog: https://$CATALOG_HOST" +ok "MCP: https://$MCP_HOST" -# --- 3. Restart catalog with tunnel URL --- +# Restart catalog with public MCP URL +info "Restarting catalog with public MCP URLs..." kill "$CATALOG_PID" 2>/dev/null wait "$CATALOG_PID" 2>/dev/null || true -echo "==> Restarting catalog with MCP_SERVER_BASE=https://$MCP_HOST..." -MCP_SERVER_BASE="https://$MCP_HOST" PORT=$CATALOG_PORT node build/catalog/index.js & +cd "$PROJECT_DIR" +MCP_SERVER_BASE="https://$MCP_HOST" PORT=$CATALOG_PORT node build/catalog/index.js > /tmp/catalog-server.log 2>&1 & CATALOG_PID=$! sleep 3 -echo " Verifying catalog..." -curl -s "https://$CATALOG_HOST/instances" | head -c 200 -echo "" +# Verify +INSTANCE_COUNT=$(curl -s "https://$CATALOG_HOST/instances" 2>/dev/null | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0") +ok "Catalog verified — $INSTANCE_COUNT instances with public MCP URLs" + +# ============================================================ +# Step 4: Update swagger +# ============================================================ +step "4/5" "Updating connector definition" -# --- 4. Update swagger host --- -echo "==> Updating swagger host to $CATALOG_HOST..." python3 -c " import json with open('$SWAGGER_FILE', 'r') as f: @@ -146,37 +174,74 @@ with open('$SWAGGER_FILE', 'r') as f: swagger['host'] = '$CATALOG_HOST' with open('$SWAGGER_FILE', 'w') as f: json.dump(swagger, f, indent=2) -print(' Updated to:', swagger['host']) " +ok "Swagger host → $CATALOG_HOST" + +# ============================================================ +# Step 5: Deploy connector +# ============================================================ +step "5/5" "Deploying connector to environment $ENV_ID" -# --- 5. Deploy or update connector --- if [ -f "$SETTINGS_FILE" ]; then CONNECTOR_ID=$(python3 -c "import json; print(json.load(open('$SETTINGS_FILE'))['connectorId'])") - echo "==> Updating existing connector: $CONNECTOR_ID" + info "Found existing connector: $CONNECTOR_ID" + info "Updating..." python3 -m paconn update \ -e "$ENV_ID" \ -c "$CONNECTOR_ID" \ -d "$SWAGGER_FILE" \ -p "$PROPS_FILE" \ -x "$SCRIPT_FILE" + ok "Connector updated." else - echo "==> Creating new connector..." - python3 -m paconn create \ + info "No settings.json found — creating new connector..." + + CREATE_OUTPUT=$(python3 -m paconn create \ -e "$ENV_ID" \ -d "$SWAGGER_FILE" \ -p "$PROPS_FILE" \ -x "$SCRIPT_FILE" \ - -w - echo " Connector created." + -w 2>&1) || true + + if echo "$CREATE_OUTPUT" | grep -qi "DisplayNameIsInUse\|already exists"; then + SUFFIX=$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c 4) + NEW_TITLE="Dynamic MCP Connector $SUFFIX" + warn "Name already taken. Retrying as '$NEW_TITLE'..." + python3 -c " +import json +with open('$SWAGGER_FILE', 'r') as f: + swagger = json.load(f) +swagger['info']['title'] = '$NEW_TITLE' +with open('$SWAGGER_FILE', 'w') as f: + json.dump(swagger, f, indent=2) +" + python3 -m paconn create \ + -e "$ENV_ID" \ + -d "$SWAGGER_FILE" \ + -p "$PROPS_FILE" \ + -x "$SCRIPT_FILE" \ + -w + ok "Connector '$NEW_TITLE' created." + elif echo "$CREATE_OUTPUT" | grep -qi "created successfully"; then + ok "Connector created." + else + echo "$CREATE_OUTPUT" + fail "Connector creation failed. See output above." + exit 1 + fi fi +# ============================================================ +# Done +# ============================================================ echo "" -echo "=========================================" -echo " Deployment complete!" -echo " Catalog: https://$CATALOG_HOST" -echo " MCP: https://$MCP_HOST" -echo " Environment: $ENV_ID" -echo "=========================================" +echo -e "${BOLD}${GREEN}=========================================${RESET}" +echo -e "${BOLD} Deployment complete!${RESET}" +echo -e "${GREEN}=========================================${RESET}" +echo -e " Catalog: ${CYAN}https://$CATALOG_HOST${RESET}" +echo -e " MCP Server: ${CYAN}https://$MCP_HOST${RESET}" +echo -e " Environment: ${DIM}$ENV_ID${RESET}" +echo -e "${GREEN}=========================================${RESET}" echo "" -echo "Press Ctrl+C to stop servers and tunnel." +echo -e "${DIM}Press Ctrl+C to stop servers and tunnel.${RESET}" wait