#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" if [[ -f .env ]]; then set -a # shellcheck source=/dev/null source .env set +a fi PORT=8501 BASE_URL="http://127.0.0.1:$PORT" TEST_GRAPH="${EXAMPLE_GRAPH:-http://example.org/catalog#test}" DERIVED_PROPERTY="http://example.org/relations#derivedFrom" SOURCE_BATCH_PROPERTY="http://example.org/relations#sourceBatch" ORIGIN_BATCH_PROPERTY="http://example.org/relations#originBatch" CATEGORY_TYPE="http://example.org/catalog#Category" ROOT_ITEM="http://example.org/catalog#Item_Prototype" PATH_SUBJECT="http://example.org/catalog#Item_Prime" SAMPLE_FIXTURE="catalog_fixture.ttl" : "${MCP_ALLOW_EXAMPLE_LOAD:=true}" if ! command -v jq >/dev/null 2>&1; then echo "ERROR: jq is required for test output parsing." exit 1 fi pass_count=0 fail_count=0 section() { echo echo "============================================================" echo "$1" echo "============================================================" } pass() { pass_count=$((pass_count + 1)) echo "✅ PASS: $1" } fail() { fail_count=$((fail_count + 1)) echo "❌ FAIL: $1" echo " detail: $2" } call_mcp() { local payload="$1" curl -sS -X POST "$BASE_URL/rpc" \ -H "Content-Type: application/json" \ -d "$payload" } assert_tool_ok() { local label="$1" local payload="$2" local response response="$(call_mcp "$payload")" || { fail "$label" "HTTP request failed" return 1 } if ! echo "$response" | jq -e . >/dev/null 2>&1; then fail "$label" "Non-JSON response: $response" return 1 fi local status status="$(echo "$response" | jq -r '.status // empty')" if [[ "$status" != "ok" ]]; then local detail detail="$(echo "$response" | jq -r '.detail // .error // "unknown error"')" fail "$label" "$detail" return 1 fi pass "$label" TOOL_LAST_RESPONSE="$response" return 0 } section "Load sample dataset" if [[ "${MCP_ALLOW_EXAMPLE_LOAD:-false}" == "true" ]]; then payload=$(jq -n --arg file "$SAMPLE_FIXTURE" --arg graph "$TEST_GRAPH" '{"tool":"load_examples","input":{"files":[$file],"graph":$graph}}') TOOL_LAST_RESPONSE="" if assert_tool_ok "load_examples" "$payload"; then echo "Loaded fixtures:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.loaded[] | " - " + (.file + " → " + .graph)' || true fi else echo "MCP_ALLOW_EXAMPLE_LOAD not true; skipping sample dataset load." fi section "Health check" root_json="$(curl -sS "$BASE_URL/")" echo "$root_json" | jq '{status, virtuoso, tools, guardrails}' if [[ "$(echo "$root_json" | jq -r '.status // empty')" == "MCP server running" ]]; then pass "root endpoint" else fail "root endpoint" "unexpected status" fi section "Read-only tools" TOOL_LAST_RESPONSE="" if assert_tool_ok "list_graphs" '{"tool":"list_graphs","input":{}}'; then echo "Graphs returned:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].g.value' | sed 's/^/ - /' || true fi section "Update path (fabricated triples)" TOOL_LAST_RESPONSE="" payload=$(jq -n --arg subject "http://example.org/plain#TestItem1" --arg predicate "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" --arg object "http://example.org/plain#Specimen" --arg graph "$TEST_GRAPH" '{"tool":"insert_triple","input":{"subject":$subject,"predicate":$predicate,"object":$object,"object_type":"uri","graph":$graph}}') if assert_tool_ok "insert_triple" "$payload"; then echo "Inserted query preview:" echo "$TOOL_LAST_RESPONSE" | jq -r '.query' | sed 's/^/ /' fi EXAMPLE_TTL=$(cat <<'EOF' @prefix ex: . @prefix rdfs: . ex:PlainItem a ex:Specimen ; rdfs:label "Plain test item" . EOF ) payload=$(jq -n --arg ttl "$EXAMPLE_TTL" --arg graph "$TEST_GRAPH" '{"tool":"batch_insert","input":{"ttl":$ttl,"graph":$graph}}') TOOL_LAST_RESPONSE="" if assert_tool_ok "batch_insert (fabricated TTL)" "$payload"; then echo "Batch insert query:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.query' | sed 's/^/ /' fi section "Helper retrieval checks" TOOL_LAST_RESPONSE="" payload=$(jq -n --arg type_uri "$CATEGORY_TYPE" --argjson limit 5 '{"tool":"get_entities_by_type","input":{"type_uri":$type_uri,"limit":$limit}}') if assert_tool_ok "get_entities_by_type(Category)" "$payload"; then echo "Entity IRIs:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].s.value' | sed 's/^/ - /' || true fi TOOL_LAST_RESPONSE="" payload=$(jq -n --arg term "Variant" --argjson limit 5 '{"tool":"search_label","input":{"term":$term,"limit":$limit}}') if assert_tool_ok "search_label(term=Variant)" "$payload"; then echo "Label hits:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - " + (.label.value // "") + " (" + .s.value + ")"' || true fi TOOL_LAST_RESPONSE="" payload=$(jq -n --arg subject_uri "$PATH_SUBJECT" --argjson limit 10 '{"tool":"get_predicates_for_subject","input":{"subject_uri":$subject_uri,"limit":$limit}}') if assert_tool_ok "get_predicates_for_subject(Prime Item)" "$payload"; then echo "Predicates found:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].p.value' | sed 's/^/ - /' || true fi TOOL_LAST_RESPONSE="" payload=$(jq -n --arg subject_uri "$PATH_SUBJECT" '{"tool":"get_labels_for_subject","input":{"subject_uri":$subject_uri}}') if assert_tool_ok "get_labels_for_subject(Prime Item)" "$payload"; then echo "Subject labels:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].label.value' | sed 's/^/ - /' || true fi section "Relationship inspection" TOOL_LAST_RESPONSE="" payload=$(jq -n --arg subject_uri "$ROOT_ITEM" --arg property_uri "$DERIVED_PROPERTY" --arg direction "incoming" --argjson limit 20 '{"tool":"traverse_property","input":{"subject_uri":$subject_uri,"property_uri":$property_uri,"direction":$direction,"limit":$limit}}') if assert_tool_ok "traverse_property(derivedFrom incoming)" "$payload"; then echo "Items derived from the prototype:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - " + (.neighbor.value) + " (label: " + (.label.value // "") + ")"' fi section "Ontology discovery" TOOL_LAST_RESPONSE="" if assert_tool_ok "list_classes(term=cycle)" '{"tool":"list_classes","input":{"term":"cycle","limit":10}}'; then echo "Class hits:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - " + .class.value + " (" + (.label.value // "") + ")"' || true fi TOOL_LAST_RESPONSE="" if assert_tool_ok "list_properties(term=derived)" '{"tool":"list_properties","input":{"term":"derived","limit":10}}'; then echo "Property hits:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - " + .property.value + " (" + (.label.value // "") + ")"' || true fi TOOL_LAST_RESPONSE="" payload=$(jq -n --arg property_uri "$DERIVED_PROPERTY" --argjson limit 5 '{"tool":"describe_property","input":{"property_uri":$property_uri,"usage_limit":$limit}}') if assert_tool_ok "describe_property(derivedFrom)" "$payload"; then echo "derivedFrom metadata rows:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.metadata.results.bindings | length | tostring | " - rows: " + .' fi section "Subject/path helpers" TOOL_LAST_RESPONSE="" payload=$(jq -n --arg subject_uri "$PATH_SUBJECT" --argjson limit 10 '{"tool":"describe_subject","input":{"subject_uri":$subject_uri,"limit":$limit}}') if assert_tool_ok "describe_subject(Prime Item)" "$payload"; then echo "Subject triples:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - " + .predicate.value + " -> " + (.objectLabel.value // .object.value)' || true fi TOOL_LAST_RESPONSE="" payload=$(jq -n --arg subject_uri "$PATH_SUBJECT" --arg property1 "$SOURCE_BATCH_PROPERTY" --arg property2 "$ORIGIN_BATCH_PROPERTY" --argjson limit 5 '{"tool":"path_traverse","input":{"subject_uri":$subject_uri,"property_path":[$property1,$property2],"limit":$limit}}') if assert_tool_ok "path_traverse(batch lineage)" "$payload"; then echo "Path traverse results:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.result.results.bindings[] | " - " + (.n1Label.value // .n1.value) + " -> " + (.n2Label.value // .n2.value)' || true fi TOOL_LAST_RESPONSE="" payload=$(jq -n --arg property_uri "$DERIVED_PROPERTY" --argjson examples_limit 3 '{"tool":"property_usage_statistics","input":{"property_uri":$property_uri,"examples_limit":$examples_limit}}') if assert_tool_ok "property_usage_statistics" "$payload"; then echo "derivedFrom usage count:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.count.results.bindings[0].usageCount.value // "0" | " - " + .' echo "Sample bindings:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.examples.results.bindings[] | " - " + (.subjectLabel.value // .subject.value) + " -> " + (.objectLabel.value // .object.value)' || true fi TOOL_LAST_RESPONSE="" payload=$(jq -n --arg ttl " \"batch helper\" ." --arg graph "$TEST_GRAPH" '{"tool":"batch_insert","input":{"ttl":$ttl,"graph":$graph}}') if assert_tool_ok "batch_insert(test)" "$payload"; then echo "Batch insert query:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.query' | sed 's/^/ /' fi section "Summary" echo "Passed: $pass_count" echo "Failed: $fail_count" if [[ "$fail_count" -gt 0 ]]; then exit 1 fi echo "All checks passed."