11#! /usr/bin/env bash
2- #
3- # deploy.sh — Start servers, create a dev tunnel, and deploy/update
4- # the Power Platform connector in one step.
5- #
6- # Usage:
7- # ./scripts/deploy.sh [ENVIRONMENT_ID] [TENANT_ID]
8- #
9- set -euo pipefail
2+ set -uo pipefail
103
4+ # --- Configuration ---
115PROJECT_DIR=" $( cd " $( dirname " $0 " ) /.." && pwd) "
126CATALOG_PORT=3000
137MCP_PORT=3001
@@ -19,91 +13,123 @@ PROPS_FILE="$PROJECT_DIR/connector/apiProperties.json"
1913SCRIPT_FILE=" $PROJECT_DIR /connector/script.csx"
2014PACONN_TOKEN_FILE=" $HOME /.paconn/accessTokens.json"
2115
22- # --- 0. Ensure paconn is logged in (correct tenant) ---
23- ensure_login () {
24- local need_login=false
16+ # --- Output helpers ---
17+ BOLD=" \033[1m"
18+ DIM=" \033[2m"
19+ GREEN=" \033[32m"
20+ YELLOW=" \033[33m"
21+ RED=" \033[31m"
22+ CYAN=" \033[36m"
23+ RESET=" \033[0m"
24+
25+ step () { echo -e " \n${BOLD}${CYAN} [$1 ]${RESET} ${BOLD} $2 ${RESET} " ; }
26+ info () { echo -e " ${DIM} $1 ${RESET} " ; }
27+ ok () { echo -e " ${GREEN} ✓${RESET} $1 " ; }
28+ warn () { echo -e " ${YELLOW} ⚠${RESET} $1 " ; }
29+ fail () { echo -e " ${RED} ✗${RESET} $1 " ; }
30+
31+ # --- Cleanup ---
32+ cleanup () {
33+ echo " "
34+ step " •" " Shutting down..."
35+ [ -n " ${CATALOG_PID:- } " ] && kill " $CATALOG_PID " 2> /dev/null
36+ [ -n " ${MCP_PID:- } " ] && kill " $MCP_PID " 2> /dev/null
37+ [ -n " ${TUNNEL_PID:- } " ] && kill " $TUNNEL_PID " 2> /dev/null
38+ wait 2> /dev/null
39+ ok " All processes stopped."
40+ }
41+ trap cleanup EXIT
42+
43+ # ============================================================
44+ # Step 1: Authentication
45+ # ============================================================
46+ step " 1/5" " Authenticating with Power Platform"
2547
26- if [ ! -f " $PACONN_TOKEN_FILE " ]; then
27- echo " No token file found."
48+ need_login=false
49+ if [ ! -f " $PACONN_TOKEN_FILE " ]; then
50+ info " No token file found."
51+ need_login=true
52+ else
53+ expires_on=$( python3 -c " import json; print(json.load(open('$PACONN_TOKEN_FILE ')).get('expires_on','0'))" 2> /dev/null || echo " 0" )
54+ now=$( python3 -c " import time; print(time.time())" )
55+ if python3 -c " exit(0 if float('$expires_on ') < float('$now ') else 1)" 2> /dev/null; then
56+ info " Token expired."
2857 need_login=true
29- else
30- local expires_on
31- expires_on=$( python3 -c " import json; print(json.load(open('$PACONN_TOKEN_FILE ')).get('expires_on','0'))" 2> /dev/null || echo " 0" )
32- local now
33- now=$( python3 -c " import time; print(time.time())" )
34- if python3 -c " exit(0 if float('$expires_on ') < float('$now ') else 1)" 2> /dev/null; then
35- echo " Token expired."
36- need_login=true
37- fi
58+ fi
3859
39- if [ -n " $TENANT_ID " ] && [ " $need_login " = false ]; then
40- local current_tenant
41- current_tenant=$( python3 -c " import json; print(json.load(open('$PACONN_TOKEN_FILE ')).get('tenant_id',''))" 2> /dev/null || echo " " )
42- if [ " $current_tenant " != " $TENANT_ID " ]; then
43- echo " Logged into tenant $current_tenant , need $TENANT_ID ."
44- need_login=true
45- fi
60+ if [ -n " $TENANT_ID " ] && [ " $need_login " = false ]; then
61+ current_tenant=$( python3 -c " import json; print(json.load(open('$PACONN_TOKEN_FILE ')).get('tenant_id',''))" 2> /dev/null || echo " " )
62+ if [ " $current_tenant " != " $TENANT_ID " ]; then
63+ info " Logged into tenant $current_tenant , need $TENANT_ID ."
64+ need_login=true
4665 fi
4766 fi
67+ fi
4868
49- if [ " $need_login " = true ]; then
50- echo " Logging in..."
51- if [ -n " $TENANT_ID " ]; then
52- python3 -m paconn login -t " $TENANT_ID "
53- else
54- python3 -m paconn login
55- fi
56- echo " Login complete."
69+ if [ " $need_login " = true ]; then
70+ warn " Login required — follow the device code prompt below:"
71+ if [ -n " $TENANT_ID " ]; then
72+ python3 -m paconn login -t " $TENANT_ID "
5773 else
58- local user_id
59- user_id=$( python3 -c " import json; print(json.load(open('$PACONN_TOKEN_FILE ')).get('user_id','unknown'))" 2> /dev/null)
60- echo " Logged in as $user_id "
74+ python3 -m paconn login
6175 fi
62- }
76+ ok " Login complete."
77+ else
78+ user_id=$( python3 -c " import json; print(json.load(open('$PACONN_TOKEN_FILE ')).get('user_id','unknown'))" 2> /dev/null)
79+ ok " Logged in as $user_id "
80+ fi
6381
64- echo " ==> Checking paconn login..."
65- ensure_login
82+ # ============================================================
83+ # Step 2: Start servers
84+ # ============================================================
85+ step " 2/5" " Starting servers"
6686
67- # --- Helpers ---
68- cleanup () {
69- echo " "
70- echo " Shutting down..."
71- [ -n " ${CATALOG_PID:- } " ] && kill " $CATALOG_PID " 2> /dev/null
72- [ -n " ${MCP_PID:- } " ] && kill " $MCP_PID " 2> /dev/null
73- [ -n " ${TUNNEL_PID:- } " ] && kill " $TUNNEL_PID " 2> /dev/null
74- wait 2> /dev/null
75- echo " Done."
76- }
77- trap cleanup EXIT
78-
79- # --- 1. Build & start servers ---
80- echo " ==> Building..."
87+ info " Building..."
8188cd " $PROJECT_DIR "
82- npm run build
89+ npm run build > /dev/null 2>&1
8390
84- echo " ==> Starting catalog server ( port $CATALOG_PORT ) ..."
85- PORT=$CATALOG_PORT node build/catalog/index.js &
91+ info " Catalog server on port $CATALOG_PORT ..."
92+ PORT=$CATALOG_PORT node build/catalog/index.js > /tmp/catalog-server.log 2>&1 &
8693CATALOG_PID=$!
8794
88- echo " ==> Starting MCP server ( port $MCP_PORT ) ..."
89- PORT=$MCP_PORT node build/mcp-server/index.js &
95+ info " MCP server on port $MCP_PORT ..."
96+ PORT=$MCP_PORT node build/mcp-server/index.js > /tmp/mcp-server.log 2>&1 &
9097MCP_PID=$!
9198
92- echo " Waiting for servers..."
93- for i in $( seq 1 15) ; do
94- if curl -sf http://localhost:$CATALOG_PORT /instances > /dev/null 2>&1 ; then
95- echo " Both servers ready."
99+ info " Waiting for servers to respond..."
100+ SERVERS_READY=false
101+ for i in $( seq 1 20) ; do
102+ catalog_ok=false
103+ mcp_ok=false
104+ curl -sf http://localhost:$CATALOG_PORT /instances > /dev/null 2>&1 && catalog_ok=true
105+ curl -sf -o /dev/null -w ' ' http://localhost:$MCP_PORT /instances/contoso/mcp 2> /dev/null && mcp_ok=true
106+ # Also accept connection refused → not ready yet; 404/405 → server is up
107+ curl -s -o /dev/null -w " %{http_code}" http://localhost:$MCP_PORT / 2> /dev/null | grep -qE " ^[2-5]" && mcp_ok=true
108+ if [ " $catalog_ok " = true ] && [ " $mcp_ok " = true ]; then
109+ SERVERS_READY=true
96110 break
97111 fi
98112 sleep 1
99113done
114+ if [ " $SERVERS_READY " = true ]; then
115+ ok " Catalog server ready on :$CATALOG_PORT "
116+ ok " MCP server ready on :$MCP_PORT "
117+ else
118+ fail " Servers did not start in time."
119+ info " Catalog log: /tmp/catalog-server.log"
120+ info " MCP log: /tmp/mcp-server.log"
121+ exit 1
122+ fi
123+
124+ # ============================================================
125+ # Step 3: Dev tunnel
126+ # ============================================================
127+ step " 3/5" " Creating dev tunnel"
100128
101- # --- 2. Start devtunnel ---
102- echo " ==> Starting devtunnel for ports $CATALOG_PORT and $MCP_PORT ..."
129+ info " Exposing ports $CATALOG_PORT and $MCP_PORT ..."
103130devtunnel host -p " $CATALOG_PORT " -p " $MCP_PORT " --allow-anonymous > /tmp/devtunnel-output.log 2>&1 &
104131TUNNEL_PID=$!
105132
106- echo " Waiting for tunnel..."
107133CATALOG_HOST=" "
108134MCP_HOST=" "
109135for i in $( seq 1 30) ; do
@@ -112,71 +138,110 @@ for i in $(seq 1 30); do
112138 MCP_HOST=$( grep -oE " [a-z0-9]+-${MCP_PORT} \.[a-z]+\.devtunnels\.ms" /tmp/devtunnel-output.log | head -1)
113139 break
114140 fi
141+ if [ " $i " -eq 30 ]; then
142+ fail " Tunnel did not start in time."
143+ info " Log: /tmp/devtunnel-output.log"
144+ exit 1
145+ fi
115146 sleep 1
116147done
117148
118- if [ -z " $CATALOG_HOST " ] || [ -z " $MCP_HOST " ]; then
119- echo " ERROR: Could not extract tunnel URLs. Log:"
120- cat /tmp/devtunnel-output.log
121- exit 1
122- fi
123-
124- echo " Tunnel ready!"
125- echo " Catalog: https://$CATALOG_HOST "
126- echo " MCP: https://$MCP_HOST "
149+ ok " Catalog: https://$CATALOG_HOST "
150+ ok " MCP: https://$MCP_HOST "
127151
128- # --- 3. Restart catalog with tunnel URL ---
152+ # Restart catalog with public MCP URL
153+ info " Restarting catalog with public MCP URLs..."
129154kill " $CATALOG_PID " 2> /dev/null
130155wait " $CATALOG_PID " 2> /dev/null || true
131- echo " ==> Restarting catalog with MCP_SERVER_BASE=https:// $MCP_HOST ... "
132- MCP_SERVER_BASE=" https://$MCP_HOST " PORT=$CATALOG_PORT node build/catalog/index.js &
156+ cd " $PROJECT_DIR "
157+ MCP_SERVER_BASE=" https://$MCP_HOST " PORT=$CATALOG_PORT node build/catalog/index.js > /tmp/catalog-server.log 2>&1 &
133158CATALOG_PID=$!
134159sleep 3
135160
136- echo " Verifying catalog..."
137- curl -s " https://$CATALOG_HOST /instances" | head -c 200
138- echo " "
161+ # Verify
162+ 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" )
163+ ok " Catalog verified — $INSTANCE_COUNT instances with public MCP URLs"
164+
165+ # ============================================================
166+ # Step 4: Update swagger
167+ # ============================================================
168+ step " 4/5" " Updating connector definition"
139169
140- # --- 4. Update swagger host ---
141- echo " ==> Updating swagger host to $CATALOG_HOST ..."
142170python3 -c "
143171import json
144172with open('$SWAGGER_FILE ', 'r') as f:
145173 swagger = json.load(f)
146174swagger['host'] = '$CATALOG_HOST '
147175with open('$SWAGGER_FILE ', 'w') as f:
148176 json.dump(swagger, f, indent=2)
149- print(' Updated to:', swagger['host'])
150177"
178+ ok " Swagger host → $CATALOG_HOST "
179+
180+ # ============================================================
181+ # Step 5: Deploy connector
182+ # ============================================================
183+ step " 5/5" " Deploying connector to environment $ENV_ID "
151184
152- # --- 5. Deploy or update connector ---
153185if [ -f " $SETTINGS_FILE " ]; then
154186 CONNECTOR_ID=$( python3 -c " import json; print(json.load(open('$SETTINGS_FILE '))['connectorId'])" )
155- echo " ==> Updating existing connector: $CONNECTOR_ID "
187+ info " Found existing connector: $CONNECTOR_ID "
188+ info " Updating..."
156189 python3 -m paconn update \
157190 -e " $ENV_ID " \
158191 -c " $CONNECTOR_ID " \
159192 -d " $SWAGGER_FILE " \
160193 -p " $PROPS_FILE " \
161194 -x " $SCRIPT_FILE "
195+ ok " Connector updated."
162196else
163- echo " ==> Creating new connector..."
164- python3 -m paconn create \
197+ info " No settings.json found — creating new connector..."
198+
199+ CREATE_OUTPUT=$( python3 -m paconn create \
165200 -e " $ENV_ID " \
166201 -d " $SWAGGER_FILE " \
167202 -p " $PROPS_FILE " \
168203 -x " $SCRIPT_FILE " \
169- -w
170- echo " Connector created."
204+ -w 2>&1 ) || true
205+
206+ if echo " $CREATE_OUTPUT " | grep -qi " DisplayNameIsInUse\|already exists" ; then
207+ SUFFIX=$( LC_ALL=C tr -dc ' a-z0-9' < /dev/urandom | head -c 4)
208+ NEW_TITLE=" Dynamic MCP Connector $SUFFIX "
209+ warn " Name already taken. Retrying as '$NEW_TITLE '..."
210+ python3 -c "
211+ import json
212+ with open('$SWAGGER_FILE ', 'r') as f:
213+ swagger = json.load(f)
214+ swagger['info']['title'] = '$NEW_TITLE '
215+ with open('$SWAGGER_FILE ', 'w') as f:
216+ json.dump(swagger, f, indent=2)
217+ "
218+ python3 -m paconn create \
219+ -e " $ENV_ID " \
220+ -d " $SWAGGER_FILE " \
221+ -p " $PROPS_FILE " \
222+ -x " $SCRIPT_FILE " \
223+ -w
224+ ok " Connector '$NEW_TITLE ' created."
225+ elif echo " $CREATE_OUTPUT " | grep -qi " created successfully" ; then
226+ ok " Connector created."
227+ else
228+ echo " $CREATE_OUTPUT "
229+ fail " Connector creation failed. See output above."
230+ exit 1
231+ fi
171232fi
172233
234+ # ============================================================
235+ # Done
236+ # ============================================================
173237echo " "
174- echo " ========================================="
175- echo " Deployment complete!"
176- echo " Catalog: https://$CATALOG_HOST "
177- echo " MCP: https://$MCP_HOST "
178- echo " Environment: $ENV_ID "
179- echo " ========================================="
238+ echo -e " ${BOLD}${GREEN} =========================================${RESET} "
239+ echo -e " ${BOLD} Deployment complete!${RESET} "
240+ echo -e " ${GREEN} =========================================${RESET} "
241+ echo -e " Catalog: ${CYAN} https://$CATALOG_HOST ${RESET} "
242+ echo -e " MCP Server: ${CYAN} https://$MCP_HOST ${RESET} "
243+ echo -e " Environment: ${DIM} $ENV_ID ${RESET} "
244+ echo -e " ${GREEN} =========================================${RESET} "
180245echo " "
181- echo " Press Ctrl+C to stop servers and tunnel."
246+ echo -e " ${DIM} Press Ctrl+C to stop servers and tunnel.${RESET} "
182247wait
0 commit comments