Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 165 additions & 100 deletions extensibility/mcp/dynamic-mcp-routing-typescript/scripts/deploy.sh
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -112,71 +138,110 @@ 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:
swagger = json.load(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