mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 18:44:05 +02:00
Co-authored-by: Claude Opus 4.8 <[email protected]> Co-authored-by: James Brunton <[email protected]> Co-authored-by: Reece Browne <[email protected]> Co-authored-by: ConnorYoh <[email protected]> Co-authored-by: Reece <[email protected]> Co-authored-by: EthanHealy01 <[email protected]> Co-authored-by: Ludy <[email protected]>
422 lines
18 KiB
Bash
422 lines
18 KiB
Bash
#!/bin/bash
|
|
# Validate the Keycloak MCP OAuth2 resource-server path end-to-end against a real Keycloak.
|
|
# Runs every check (no set -e) and exits non-zero if anything failed.
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
KEYCLOAK_HOST="${KEYCLOAK_HOST:-kubernetes.docker.internal}"
|
|
REALM="stirling-mcp"
|
|
CLIENT_ID="mcp-test-client"
|
|
CLIENT_SECRET="mcp-test-secret"
|
|
MCP_URL="http://localhost:8080/mcp"
|
|
PRM_URL="http://localhost:8080/.well-known/oauth-protected-resource"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
pass() { echo -e "${GREEN}✓${NC} $1"; PASS=$((PASS + 1)); }
|
|
fail() { echo -e "${RED}✗ $1${NC}"; FAIL=$((FAIL + 1)); }
|
|
|
|
echo -e "${BLUE}╔════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${BLUE}║ Validating MCP Test Environment ║${NC}"
|
|
echo -e "${BLUE}╚════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
|
|
# Pick a Keycloak base URL that resolves from this host (both share KC_HOSTNAME issuer).
|
|
KC_BASE="http://${KEYCLOAK_HOST}:9080"
|
|
if ! curl -sf --max-time 3 "$KC_BASE/realms/$REALM" >/dev/null 2>&1; then
|
|
KC_BASE="http://localhost:9080"
|
|
fi
|
|
TOKEN_URL="$KC_BASE/realms/$REALM/protocol/openid-connect/token"
|
|
echo -e "${BLUE}Using Keycloak base:${NC} $KC_BASE"
|
|
echo ""
|
|
|
|
# liveness
|
|
echo -e "${YELLOW}[0] Liveness${NC}"
|
|
if curl -sf "$KC_BASE/realms/$REALM" > /dev/null 2>&1; then
|
|
pass "Keycloak realm '$REALM' reachable"
|
|
else
|
|
fail "Keycloak realm '$REALM' not reachable - is the stack up?"
|
|
fi
|
|
if curl -sf "$KC_BASE/realms/$REALM/.well-known/openid-configuration" > /dev/null 2>&1; then
|
|
pass "Keycloak OIDC discovery reachable"
|
|
else
|
|
fail "Keycloak OIDC discovery not reachable"
|
|
fi
|
|
if curl -sf http://localhost:8080/api/v1/info/status 2>/dev/null | grep -q "UP"; then
|
|
pass "Stirling PDF is UP"
|
|
else
|
|
fail "Stirling PDF is not UP"
|
|
fi
|
|
echo ""
|
|
|
|
# protected-resource metadata (RFC 9728)
|
|
echo -e "${YELLOW}[1] Protected-resource metadata (RFC 9728)${NC}"
|
|
PRM=$(curl -s -o /tmp/mcp_prm.json -w "%{http_code}" "$PRM_URL")
|
|
PRM_BODY=$(cat /tmp/mcp_prm.json 2>/dev/null)
|
|
if [ "$PRM" = "200" ]; then
|
|
pass "GET $PRM_URL -> 200 (publicly discoverable)"
|
|
else
|
|
fail "GET $PRM_URL -> $PRM (expected 200)"
|
|
fi
|
|
if echo "$PRM_BODY" | grep -q '"resource"'; then
|
|
pass "metadata advertises a 'resource' id"
|
|
else
|
|
fail "metadata missing 'resource' id"
|
|
fi
|
|
if echo "$PRM_BODY" | grep -q "realms/$REALM"; then
|
|
pass "metadata points clients at the Keycloak authorization server"
|
|
else
|
|
fail "metadata missing the Keycloak authorization server (realms/$REALM)"
|
|
fi
|
|
if echo "$PRM_BODY" | grep -q "mcp.tools"; then
|
|
pass "metadata advertises mcp.tools scopes"
|
|
else
|
|
fail "metadata missing mcp.tools scopes"
|
|
fi
|
|
|
|
# RFC 9728 path-inserted form: clients derive {origin}/.well-known/oauth-protected-resource/mcp
|
|
# for a resource at /mcp. This must serve the SAME customized metadata; a default document here
|
|
# (no authorization_servers) makes clients fall back to treating Stirling as its own AS.
|
|
PRM_SUB=$(curl -s -o /tmp/mcp_prm_sub.json -w "%{http_code}" "${PRM_URL}/mcp")
|
|
PRM_SUB_BODY=$(cat /tmp/mcp_prm_sub.json 2>/dev/null)
|
|
if [ "$PRM_SUB" = "200" ]; then
|
|
pass "GET ${PRM_URL}/mcp -> 200 (RFC 9728 path-inserted form)"
|
|
else
|
|
fail "GET ${PRM_URL}/mcp -> $PRM_SUB (expected 200)"
|
|
fi
|
|
if echo "$PRM_SUB_BODY" | grep -q "realms/$REALM"; then
|
|
pass "path-inserted metadata advertises the Keycloak authorization server"
|
|
else
|
|
fail "path-inserted metadata missing authorization_servers (filter-chain fall-through regression)"
|
|
fi
|
|
WWW_AUTH=$(curl -s -o /dev/null -D - -X POST "$MCP_URL" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"jsonrpc":"2.0","id":1,"method":"ping"}' | grep -i '^WWW-Authenticate:')
|
|
if echo "$WWW_AUTH" | grep -q 'oauth-protected-resource/mcp'; then
|
|
pass "401 WWW-Authenticate advertises the path-inserted metadata URL"
|
|
else
|
|
fail "401 WWW-Authenticate lacks the path-inserted metadata URL: $WWW_AUTH"
|
|
fi
|
|
echo ""
|
|
|
|
# unauthenticated access is rejected
|
|
echo -e "${YELLOW}[2] Unauthenticated requests are rejected${NC}"
|
|
RPC_LIST='{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
NO_TOKEN=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$MCP_URL" \
|
|
-H "Content-Type: application/json" -d "$RPC_LIST")
|
|
if [ "$NO_TOKEN" = "401" ]; then
|
|
pass "POST /mcp with no token -> 401"
|
|
else
|
|
fail "POST /mcp with no token -> $NO_TOKEN (expected 401)"
|
|
fi
|
|
BAD_TOKEN=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$MCP_URL" \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer not-a-real-jwt" -d "$RPC_LIST")
|
|
if [ "$BAD_TOKEN" = "401" ]; then
|
|
pass "POST /mcp with garbage token -> 401"
|
|
else
|
|
fail "POST /mcp with garbage token -> $BAD_TOKEN (expected 401)"
|
|
fi
|
|
echo ""
|
|
|
|
# fetch a real user token from Keycloak (password grant)
|
|
echo -e "${YELLOW}[3] Fetch a real access token from Keycloak (mcpuser)${NC}"
|
|
get_token() {
|
|
# $1 username, $2 password -> access_token (empty on failure)
|
|
curl -s -X POST "$TOKEN_URL" \
|
|
--data-urlencode "grant_type=password" \
|
|
--data-urlencode "client_id=$CLIENT_ID" \
|
|
--data-urlencode "client_secret=$CLIENT_SECRET" \
|
|
--data-urlencode "username=$1" \
|
|
--data-urlencode "password=$2" \
|
|
--data-urlencode "scope=openid email mcp.tools.read mcp.tools.write" \
|
|
| sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p'
|
|
}
|
|
USER_TOKEN=$(get_token "[email protected]" "mcppassword")
|
|
if [ -n "$USER_TOKEN" ]; then
|
|
pass "Obtained access token for [email protected]"
|
|
else
|
|
fail "Could not obtain access token for [email protected] (check Keycloak/client)"
|
|
fi
|
|
echo ""
|
|
|
|
# authenticated tools/list (account-binding succeeds)
|
|
echo -e "${YELLOW}[4] Authenticated MCP access (provisioned user)${NC}"
|
|
if [ -n "$USER_TOKEN" ]; then
|
|
LIST_CODE=$(curl -s -o /tmp/mcp_list.json -w "%{http_code}" -X POST "$MCP_URL" \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $USER_TOKEN" -d "$RPC_LIST")
|
|
LIST_BODY=$(cat /tmp/mcp_list.json 2>/dev/null)
|
|
if [ "$LIST_CODE" = "200" ]; then
|
|
pass "POST /mcp tools/list with valid token -> 200"
|
|
else
|
|
fail "POST /mcp tools/list with valid token -> $LIST_CODE (expected 200)"
|
|
fi
|
|
for t in stirling_describe_operation stirling_convert stirling_pages stirling_misc stirling_security; do
|
|
if echo "$LIST_BODY" | grep -q "\"$t\""; then
|
|
pass "tools/list advertises $t"
|
|
else
|
|
fail "tools/list missing $t"
|
|
fi
|
|
done
|
|
else
|
|
fail "Skipping authenticated checks - no user token"
|
|
fi
|
|
echo ""
|
|
|
|
# tools/call stirling_describe_operation
|
|
echo -e "${YELLOW}[5] Deep schema via stirling_describe_operation${NC}"
|
|
if [ -n "$USER_TOKEN" ]; then
|
|
OP=$(echo "$LIST_BODY" | grep -oE '"enum":\[[^]]+\]' | head -1 | grep -oE '"[^"]+"' | sed -n '2p' | tr -d '"')
|
|
if [ -n "$OP" ]; then
|
|
echo -e " ${BLUE}using operation:${NC} $OP"
|
|
DESC_RPC="{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"stirling_describe_operation\",\"arguments\":{\"operation\":\"$OP\"}}}"
|
|
DESC_CODE=$(curl -s -o /tmp/mcp_desc.json -w "%{http_code}" -X POST "$MCP_URL" \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $USER_TOKEN" -d "$DESC_RPC")
|
|
DESC_BODY=$(cat /tmp/mcp_desc.json 2>/dev/null)
|
|
if [ "$DESC_CODE" = "200" ] && echo "$DESC_BODY" | grep -q "parametersSchema"; then
|
|
pass "describe_operation('$OP') returned a parameters schema"
|
|
else
|
|
fail "describe_operation('$OP') -> $DESC_CODE (no parametersSchema in body)"
|
|
fi
|
|
else
|
|
fail "Could not extract an operation id from tools/list (no enabled ops?)"
|
|
fi
|
|
else
|
|
fail "Skipping describe check - no user token"
|
|
fi
|
|
echo ""
|
|
|
|
# account-binding: valid Keycloak user with no Stirling account -> 403
|
|
echo -e "${YELLOW}[6] Account-binding rejects users without a Stirling account${NC}"
|
|
GHOST_TOKEN=$(get_token "[email protected]" "ghostpassword")
|
|
if [ -n "$GHOST_TOKEN" ]; then
|
|
pass "Obtained a valid Keycloak token for [email protected]"
|
|
GHOST_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$MCP_URL" \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $GHOST_TOKEN" -d "$RPC_LIST")
|
|
if [ "$GHOST_CODE" = "403" ]; then
|
|
pass "Valid token, no Stirling account -> 403 (account-binding enforced)"
|
|
else
|
|
fail "ghost user -> $GHOST_CODE (expected 403)"
|
|
fi
|
|
else
|
|
fail "Could not obtain a token for [email protected] (cannot test account-binding)"
|
|
fi
|
|
echo ""
|
|
|
|
# hardening: bad tokens & endpoint isolation
|
|
echo -e "${YELLOW}[7] Hardening - bad tokens & endpoint isolation${NC}"
|
|
|
|
b64url() { printf '%s' "$1" | base64 | tr '+/' '-_' | tr -d '='; }
|
|
|
|
# unsigned alg:none token must be rejected
|
|
NONE_JWT="$(b64url '{"alg":"none","typ":"JWT"}').$(b64url '{"sub":"mcpuser","iss":"http://kubernetes.docker.internal:9080/realms/stirling-mcp","aud":"http://localhost:8080/mcp","email":"[email protected]","scope":"mcp.tools.read mcp.tools.write","exp":9999999999}')."
|
|
NONE_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$MCP_URL" \
|
|
-H "Content-Type: application/json" -H "Authorization: Bearer $NONE_JWT" -d "$RPC_LIST")
|
|
if [ "$NONE_CODE" = "401" ] || [ "$NONE_CODE" = "400" ]; then
|
|
pass "Unsigned alg:none token -> $NONE_CODE (rejected; a real signature is required)"
|
|
else
|
|
fail "alg:none token -> $NONE_CODE (expected 400/401)"
|
|
fi
|
|
|
|
# valid token minted for a different audience must be rejected (RFC 8707)
|
|
WRONG_AUD=$(curl -s -X POST "$TOKEN_URL" \
|
|
--data-urlencode "grant_type=password" \
|
|
--data-urlencode "client_id=other-client" \
|
|
--data-urlencode "[email protected]" \
|
|
--data-urlencode "password=mcppassword" \
|
|
--data-urlencode "scope=openid email" \
|
|
| sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
|
|
if [ -n "$WRONG_AUD" ]; then
|
|
WA_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$MCP_URL" \
|
|
-H "Content-Type: application/json" -H "Authorization: Bearer $WRONG_AUD" -d "$RPC_LIST")
|
|
if [ "$WA_CODE" = "401" ]; then
|
|
pass "Valid token, wrong audience -> 401 (RFC 8707 audience binding)"
|
|
else
|
|
fail "wrong-audience token -> $WA_CODE (expected 401)"
|
|
fi
|
|
else
|
|
fail "Could not mint a wrong-audience token from other-client"
|
|
fi
|
|
|
|
if [ -n "$USER_TOKEN" ]; then
|
|
# the /mcp guard covers every method: GET without a token -> 401
|
|
GET_NOAUTH=$(curl -s -o /dev/null -w "%{http_code}" -X GET "$MCP_URL")
|
|
if [ "$GET_NOAUTH" = "401" ]; then
|
|
pass "GET /mcp with no token -> 401 (chain guards all methods, not just POST)"
|
|
else
|
|
fail "GET /mcp no token -> $GET_NOAUTH (expected 401)"
|
|
fi
|
|
|
|
# unknown JSON-RPC method is refused with -32601
|
|
UNK=$(curl -s -X POST "$MCP_URL" -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $USER_TOKEN" \
|
|
-d '{"jsonrpc":"2.0","id":9,"method":"admin/deleteEverything"}')
|
|
if echo "$UNK" | grep -q "32601"; then
|
|
pass "Unknown JSON-RPC method -> error -32601 (not dispatched)"
|
|
else
|
|
fail "Unknown method not rejected as expected: $UNK"
|
|
fi
|
|
|
|
# isolation: the MCP token must not unlock admin endpoints
|
|
ADM_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X GET \
|
|
"http://localhost:8080/api/v1/admin/settings" \
|
|
-H "Authorization: Bearer $USER_TOKEN")
|
|
if [ "$ADM_CODE" = "401" ] || [ "$ADM_CODE" = "403" ] || [ "$ADM_CODE" = "302" ]; then
|
|
pass "MCP token on /api/v1/admin/settings -> $ADM_CODE (no cross-endpoint access)"
|
|
else
|
|
fail "MCP token reached an admin endpoint -> $ADM_CODE (expected 401/403/302)"
|
|
fi
|
|
else
|
|
fail "Skipping method/isolation checks - no user token"
|
|
fi
|
|
echo ""
|
|
|
|
# regression checks
|
|
echo -e "${YELLOW}[8] Regression checks${NC}"
|
|
if [ -n "$USER_TOKEN" ]; then
|
|
# a PDF category tool must return an honest error (isError), not a fake success
|
|
CAT=$(curl -s -X POST "$MCP_URL" -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $USER_TOKEN" \
|
|
-d '{"jsonrpc":"2.0","id":81,"method":"tools/call","params":{"name":"stirling_security","arguments":{"operation":"add-password"}}}')
|
|
if echo "$CAT" | grep -q '"isError":true'; then
|
|
pass "category tool returns isError, not a fake success"
|
|
else
|
|
fail "category tool did not return isError: $CAT"
|
|
fi
|
|
|
|
# malformed JSON -> JSON-RPC parse error (-32700) envelope
|
|
MAL=$(curl -s -X POST "$MCP_URL" -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $USER_TOKEN" -d '{ not valid json ')
|
|
if echo "$MAL" | grep -q "32700"; then
|
|
pass "malformed JSON -> -32700 envelope"
|
|
else
|
|
fail "malformed JSON not enveloped as -32700: $MAL"
|
|
fi
|
|
|
|
# valid JSON but wrong shape -> invalid request (-32600)
|
|
WS=$(curl -s -X POST "$MCP_URL" -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $USER_TOKEN" -d '{"hello":"world"}')
|
|
if echo "$WS" | grep -q "32600"; then
|
|
pass "wrong-shape JSON -> -32600"
|
|
else
|
|
fail "wrong-shape JSON wrong error code: $WS"
|
|
fi
|
|
|
|
# initialize echoes the client protocolVersion (negotiation)
|
|
INIT=$(curl -s -X POST "$MCP_URL" -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $USER_TOKEN" \
|
|
-d '{"jsonrpc":"2.0","id":82,"method":"initialize","params":{"protocolVersion":"2025-03-26"}}')
|
|
if echo "$INIT" | grep -q '"protocolVersion":"2025-03-26"'; then
|
|
pass "initialize negotiates the client's protocolVersion"
|
|
else
|
|
fail "initialize did not echo client protocolVersion: $INIT"
|
|
fi
|
|
else
|
|
fail "Skipping regression checks - no user token"
|
|
fi
|
|
|
|
# chunked body (no Content-Length) over the cap -> 413, pre-auth
|
|
CHUNK=$( { printf '{"jsonrpc":"2.0","id":1,"method":"x","params":"'; head -c 300000 /dev/zero | tr '\0' A; printf '"}'; } \
|
|
| curl -s -o /dev/null -w "%{http_code}" -X POST "$MCP_URL" \
|
|
-H "Content-Type: application/json" -H "Transfer-Encoding: chunked" --data-binary @- )
|
|
if [ "$CHUNK" = "413" ]; then
|
|
pass "chunked oversized body -> 413"
|
|
else
|
|
fail "chunked oversized body -> $CHUNK (expected 413)"
|
|
fi
|
|
|
|
# behind a proxy, the 401 resource_metadata URL must honour X-Forwarded-*
|
|
WWW=$(curl -s -D - -o /dev/null -X POST "$MCP_URL" -H "Content-Type: application/json" \
|
|
-H "X-Forwarded-Proto: https" -H "X-Forwarded-Host: mcp.example.com" -d "$RPC_LIST" \
|
|
| tr -d '\r' | grep -i "^www-authenticate:")
|
|
if echo "$WWW" | grep -q "https://mcp.example.com/.well-known/oauth-protected-resource"; then
|
|
pass "401 resource_metadata honours X-Forwarded-*"
|
|
else
|
|
fail "resource_metadata ignored forwarded headers: $WWW"
|
|
fi
|
|
echo ""
|
|
|
|
# CORS for browser-based MCP clients
|
|
echo -e "${YELLOW}[9] CORS (browser MCP clients)${NC}"
|
|
ORIGIN="http://127.0.0.1:6274"
|
|
PF=$(curl -s -D - -o /dev/null -X OPTIONS "$MCP_URL" \
|
|
-H "Origin: $ORIGIN" -H "Access-Control-Request-Method: POST" \
|
|
-H "Access-Control-Request-Headers: authorization,content-type" | tr -d '\r')
|
|
PF_CODE=$(printf '%s' "$PF" | sed -n 's#^HTTP/[0-9.]* \([0-9][0-9][0-9]\).*#\1#p' | head -1)
|
|
if printf '%s' "$PF_CODE" | grep -qE '^2'; then
|
|
pass "OPTIONS /mcp preflight -> $PF_CODE (not 401 - browsers can call /mcp)"
|
|
else
|
|
fail "OPTIONS /mcp preflight -> $PF_CODE (expected 2xx)"
|
|
fi
|
|
if printf '%s' "$PF" | grep -qi "access-control-allow-origin"; then
|
|
pass "preflight advertises Access-Control-Allow-Origin"
|
|
else
|
|
fail "preflight missing Access-Control-Allow-Origin"
|
|
fi
|
|
if curl -s -D - -o /dev/null -H "Origin: $ORIGIN" "$PRM_URL" | tr -d '\r' | grep -qi "access-control-allow-origin"; then
|
|
pass "protected-resource metadata sends Access-Control-Allow-Origin (browser-readable)"
|
|
else
|
|
fail "metadata missing Access-Control-Allow-Origin (browser can't read discovery)"
|
|
fi
|
|
echo ""
|
|
|
|
# real MCP client (official @modelcontextprotocol/sdk)
|
|
echo -e "${YELLOW}[10] Real MCP SDK client (not just curl)${NC}"
|
|
CHECK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/mcp-client-check"
|
|
if ! command -v node >/dev/null 2>&1; then
|
|
echo -e " ${YELLOW}(node not installed - skipping real-client check)${NC}"
|
|
elif [ -z "$USER_TOKEN" ]; then
|
|
fail "skipping real-client check - no user token"
|
|
else
|
|
if [ ! -d "$CHECK_DIR/node_modules" ]; then
|
|
echo " installing MCP SDK (first run)..."
|
|
(cd "$CHECK_DIR" && npm install --silent --no-fund --no-audit >/dev/null 2>&1)
|
|
fi
|
|
if MODE=oauth MCP_URL="$MCP_URL" MCP_BEARER="$USER_TOKEN" node "$CHECK_DIR/check.mjs"; then
|
|
pass "official MCP SDK client connected + validated over streamable HTTP (oauth)"
|
|
else
|
|
fail "official MCP SDK client could not validate the server (oauth)"
|
|
fi
|
|
fi
|
|
echo ""
|
|
|
|
# real operation execution (rotate a real PDF, inline)
|
|
echo -e "${YELLOW}[11] Real operation execution (rotate-pdf, inline file)${NC}"
|
|
SAMPLE_PDF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../app/common/src/test/resources/example.pdf"
|
|
if [ -z "$USER_TOKEN" ]; then
|
|
fail "skipping execution check - no user token"
|
|
elif [ ! -f "$SAMPLE_PDF" ]; then
|
|
fail "sample PDF not found at $SAMPLE_PDF"
|
|
else
|
|
B64=$(base64 -w0 "$SAMPLE_PDF" 2>/dev/null || base64 "$SAMPLE_PDF" | tr -d '\n')
|
|
EXEC_RPC=$(printf '{"jsonrpc":"2.0","id":91,"method":"tools/call","params":{"name":"stirling_pages","arguments":{"operation":"rotate-pdf","fileName":"example.pdf","parameters":{"angle":90},"file":"%s"}}}' "$B64")
|
|
EXEC=$(curl -s -X POST "$MCP_URL" -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $USER_TOKEN" -d "$EXEC_RPC")
|
|
if echo "$EXEC" | grep -q '"isError":true'; then
|
|
fail "rotate-pdf returned isError: $(printf '%s' "$EXEC" | head -c 300)"
|
|
elif echo "$EXEC" | grep -q 'fileId='; then
|
|
pass "rotate-pdf processed a real PDF over MCP and returned a result fileId"
|
|
else
|
|
fail "rotate-pdf returned no result fileId: $(printf '%s' "$EXEC" | head -c 300)"
|
|
fi
|
|
fi
|
|
echo ""
|
|
|
|
# summary
|
|
rm -f /tmp/mcp_prm.json /tmp/mcp_list.json /tmp/mcp_desc.json 2>/dev/null
|
|
echo -e "${BLUE}────────────────────────────────────────────────────${NC}"
|
|
if [ "$FAIL" -eq 0 ]; then
|
|
echo -e "${GREEN}All MCP environment checks passed! ($PASS passed)${NC}"
|
|
exit 0
|
|
else
|
|
echo -e "${RED}MCP validation failed: $FAIL failed, $PASS passed.${NC}"
|
|
exit 1
|
|
fi
|