diff --git a/deploy/verae-mcp-timestamping-go.service b/deploy/verae-mcp-timestamping-go.service new file mode 100644 index 0000000..5bd2455 --- /dev/null +++ b/deploy/verae-mcp-timestamping-go.service @@ -0,0 +1,27 @@ +[Unit] +Description=Verae MCP timestamping server (Go shadow) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=verae-mcp +Group=verae-mcp +WorkingDirectory=/opt/verae/verae-mcp-timestamping-server-go +Environment=HOME=/var/lib/verae-mcp +Environment=VERAE_MCP_TRANSPORT=streamable-http +Environment=VERAE_MCP_HOST=127.0.0.1 +Environment=VERAE_MCP_PORT=8001 +Environment=VERAE_NATS_LISTEN=127.0.0.1:4223 +Environment=VERAE_MCP_REQUIRE_BEARER=0 +ExecStart=/opt/verae/verae-mcp-timestamping-server-go/bin/verae-mcp-timestamping-server +Restart=on-failure +RestartSec=2 +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/var/lib/verae-mcp +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/scripts/parity_smoke.sh b/scripts/parity_smoke.sh new file mode 100755 index 0000000..6966ae3 --- /dev/null +++ b/scripts/parity_smoke.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Compare MCP tools/list on Python :8000 and Go :8001 when both are up. +set -euo pipefail + +PY_URL="${PY_URL:-http://127.0.0.1:8000/mcp}" +GO_URL="${GO_URL:-http://127.0.0.1:8001/mcp}" +WANT_TOOLS="${WANT_TOOLS:-stamp verify hash_report signup login session create_user}" + +need() { command -v "$1" >/dev/null 2>&1 || { echo "parity_smoke: need $1" >&2; exit 2; }; } +need curl +need python3 + +port_up() { + local hostport="$1" + python3 - "$hostport" <<'PY' +import socket, sys +host, _, port = sys.argv[1].rpartition(":") +s = socket.socket() +s.settimeout(0.5) +try: + s.connect((host, int(port))) +except OSError: + sys.exit(1) +finally: + s.close() +PY +} + +# Print sorted tool names (space-separated) for one MCP Streamable HTTP endpoint. +mcp_tools_list() { + local url="$1" + local label="$2" + local hdr body code session + + hdr="$(mktemp)" + body="$(mktemp)" + trap 'rm -f "$hdr" "$body"' RETURN + + code="$(curl -sS -D "$hdr" -o "$body" -w '%{http_code}' \ + -X POST "$url" \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"parity-smoke","version":"0.0.1"}}}')" || true + + if [[ "$code" != "200" && "$code" != "202" ]]; then + echo "parity_smoke: $label initialize HTTP $code at $url" >&2 + cat "$body" >&2 || true + return 1 + fi + + session="$(python3 - "$hdr" <<'PY' +import sys +session = "" +for line in open(sys.argv[1], encoding="utf-8", errors="replace"): + if ":" not in line: + continue + k, v = line.split(":", 1) + if k.strip().lower() == "mcp-session-id": + session = v.strip() +print(session) +PY +)" + + local -a headers=( + -H 'Content-Type: application/json' + -H 'Accept: application/json, text/event-stream' + ) + if [[ -n "$session" ]]; then + headers+=(-H "Mcp-Session-Id: $session") + fi + + curl -sS -o /dev/null \ + -X POST "$url" \ + "${headers[@]}" \ + --data '{"jsonrpc":"2.0","method":"notifications/initialized"}' || true + + code="$(curl -sS -o "$body" -w '%{http_code}' \ + -X POST "$url" \ + "${headers[@]}" \ + --data '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}')" || true + + if [[ "$code" != "200" ]]; then + echo "parity_smoke: $label tools/list HTTP $code at $url" >&2 + cat "$body" >&2 || true + return 1 + fi + + python3 - "$body" <<'PY' +import json, sys +raw = open(sys.argv[1], encoding="utf-8", errors="replace").read().strip() +text = raw +if raw and not raw.lstrip().startswith("{") and "data:" in raw: + parts = [line[5:].strip() for line in raw.splitlines() if line.startswith("data:")] + text = parts[-1] if parts else raw +doc = json.loads(text) +result = doc.get("result") or doc +tools = result.get("tools") or [] +names = sorted({t.get("name") for t in tools if isinstance(t, dict) and t.get("name")}) +print(" ".join(names)) +PY +} + +sorted_words() { + # shellcheck disable=SC2086 + printf '%s\n' $1 | sed '/^$/d' | sort -u | tr '\n' ' ' | sed 's/ *$//' +} + +if ! port_up 127.0.0.1:8000; then + echo "parity_smoke: Python MCP not listening on 127.0.0.1:8000" >&2 + exit 1 +fi +if ! port_up 127.0.0.1:8001; then + echo "parity_smoke: Go MCP not listening on 127.0.0.1:8001" >&2 + exit 1 +fi + +echo "parity_smoke: listing tools on Python $PY_URL" +py_tools="$(mcp_tools_list "$PY_URL" python)" +echo " python: $py_tools" + +echo "parity_smoke: listing tools on Go $GO_URL" +go_tools="$(mcp_tools_list "$GO_URL" go)" +echo " go: $go_tools" + +py_n="$(sorted_words "$py_tools")" +go_n="$(sorted_words "$go_tools")" +want_n="$(sorted_words "$WANT_TOOLS")" + +if [[ "$py_n" != "$go_n" ]]; then + echo "parity_smoke: FAIL tools/list mismatch" >&2 + echo " python: $py_n" >&2 + echo " go: $go_n" >&2 + exit 1 +fi + +for t in $WANT_TOOLS; do + if ! printf '%s\n' $go_tools | grep -qx "$t"; then + echo "parity_smoke: FAIL missing expected tool $t (want $want_n)" >&2 + exit 1 + fi +done + +echo "parity_smoke: OK tools/list match ($go_n)"