From 2878659de0256df81b6d5f3a2fec1bcdb8cfef0d Mon Sep 17 00:00:00 2001 From: Alejandro Ponce Date: Thu, 23 Oct 2025 16:05:54 +0300 Subject: [PATCH 1/2] Fixes on update-thv-models The script is failing currently. I will invesitgate by runnin in this branch/PR --- .github/workflows/update-thv-models.yml | 65 ++++++++++++++++++++++++- scripts/generate_toolhive_models.sh | 55 +++++++++------------ 2 files changed, 85 insertions(+), 35 deletions(-) diff --git a/.github/workflows/update-thv-models.yml b/.github/workflows/update-thv-models.yml index 4ad5642..1a450ec 100644 --- a/.github/workflows/update-thv-models.yml +++ b/.github/workflows/update-thv-models.yml @@ -5,6 +5,9 @@ on: # Run every day at midnight UTC - cron: '0 0 * * *' workflow_dispatch: # Allow manual trigger + push: + branches: + - fix-gha-generate-thv-models # TODO: Remove after debugging permissions: contents: write @@ -37,12 +40,70 @@ jobs: with: version: 3.44.1 - - name: Install Dependencies - run: task install + - name: Start ToolHive Server + run: | + echo "Starting thv serve --openapi on port 8080..." + thv serve --openapi --port 8080 & + THV_PID=$! + echo "THV_PID=$THV_PID" >> $GITHUB_ENV + echo "thv serve started with PID: $THV_PID" + + echo "Waiting for thv serve to be ready..." + MAX_ATTEMPTS=30 + for i in $(seq 1 $MAX_ATTEMPTS); do + # Check if the process is still running + if ! kill -0 "$THV_PID" 2>/dev/null; then + wait "$THV_PID" || true + exit_code=$? + echo "ERROR: thv serve process (PID: $THV_PID) exited unexpectedly with code $exit_code" + exit $exit_code + fi + + # Check if endpoint returns valid JSON with expected content + if response=$(curl -s --max-time 5 http://127.0.0.1:8080/api/openapi.json 2>&1); then + # Validate JSON + if echo "$response" | python3 -m json.tool > /dev/null 2>&1; then + # Check for openapi field + if echo "$response" | grep -q "openapi"; then + echo "thv serve is ready!" + break + else + echo "Attempt $i/$MAX_ATTEMPTS: Response doesn't contain 'openapi' field" + fi + else + echo "Attempt $i/$MAX_ATTEMPTS: Invalid JSON response" + fi + else + echo "Attempt $i/$MAX_ATTEMPTS: curl failed" + fi + + if [ $i -eq $MAX_ATTEMPTS ]; then + echo "ERROR: thv serve did not become ready after $MAX_ATTEMPTS attempts" + echo "Last response: $response" + # Kill the process if it's still running + kill "$THV_PID" 2>/dev/null || true + exit 1 + fi + sleep 1 + done - name: Generate ToolHive Models + env: + MANAGE_THV: "false" run: task generate-thv-models + - name: Stop ToolHive Server + if: always() + run: | + if [ -n "$THV_PID" ] && kill -0 "$THV_PID" 2>/dev/null; then + echo "Stopping thv serve (PID: $THV_PID)..." + kill "$THV_PID" 2>/dev/null || true + wait "$THV_PID" 2>/dev/null || true + echo "thv serve stopped" + else + echo "thv serve process not running or already stopped" + fi + - name: Check for Changes id: check-changes run: | diff --git a/scripts/generate_toolhive_models.sh b/scripts/generate_toolhive_models.sh index 2a01f1c..4f18567 100755 --- a/scripts/generate_toolhive_models.sh +++ b/scripts/generate_toolhive_models.sh @@ -4,50 +4,39 @@ set -euo pipefail OUTPUT_DIR="src/mcp_optimizer/toolhive/api_models" TEMP_DIR="$(mktemp -d)" -THV_PID="" -MANAGE_THV="${MANAGE_THV:-true}" # Cleanup function cleanup() { local exit_code=$? - if [ -n "$THV_PID" ] && [ "$MANAGE_THV" = "true" ]; then - # Check if process exists before attempting to kill - if kill -0 "$THV_PID" 2>/dev/null; then - echo "Stopping thv serve (PID: $THV_PID)..." - kill "$THV_PID" 2>/dev/null || true - wait "$THV_PID" 2>/dev/null || true - fi - fi rm -rf "$TEMP_DIR" exit $exit_code } trap cleanup EXIT INT TERM -# Start thv serve if we're managing it -if [ "$MANAGE_THV" = "true" ]; then - echo "Starting thv serve --openapi on port 8080..." - thv serve --openapi --port 8080 & - THV_PID=$! - - echo "Waiting for thv serve to be ready..." - MAX_ATTEMPTS=30 - for i in $(seq 1 $MAX_ATTEMPTS); do - # Check if endpoint returns valid JSON with expected content - response=$(curl -s --max-time 5 http://127.0.0.1:8080/api/openapi.json 2>&1) - if [ $? -eq 0 ] && echo "$response" | python3 -m json.tool > /dev/null 2>&1 && echo "$response" | grep -q "openapi"; then - echo "thv serve is ready!" - break - fi - if [ $i -eq $MAX_ATTEMPTS ]; then - echo "ERROR: thv serve did not become ready" - echo "Last response: $response" - exit 1 +# Verify thv serve is running and accessible +echo "Checking if thv serve is accessible..." +MAX_ATTEMPTS=5 +for i in $(seq 1 $MAX_ATTEMPTS); do + if response=$(curl -s --max-time 5 http://127.0.0.1:8080/api/openapi.json 2>&1); then + # Validate JSON + if echo "$response" | uv run python -m json.tool > /dev/null 2>&1; then + # Check for openapi field + if echo "$response" | grep -q "openapi"; then + echo "thv serve is accessible!" + break + fi fi - echo "Attempt $i/$MAX_ATTEMPTS: Waiting for OpenAPI endpoint..." - sleep 1 - done -fi + fi + + if [ $i -eq $MAX_ATTEMPTS ]; then + echo "ERROR: thv serve is not accessible at http://127.0.0.1:8080/api/openapi.json" + echo "Please ensure thv serve is running with --openapi flag on port 8080" + exit 1 + fi + echo "Attempt $i/$MAX_ATTEMPTS: Waiting for thv serve..." + sleep 1 +done # Save current models to temp directory for comparison if [ -d "$OUTPUT_DIR" ]; then From 6dce841036bf2c9131c4a86c3b188b4a15f5db22 Mon Sep 17 00:00:00 2001 From: aponcedeleonch <7890853+aponcedeleonch@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:14:21 +0000 Subject: [PATCH 2/2] Update ToolHive API models Automated update of ToolHive API models from OpenAPI specification. --- .../toolhive/api_models/__init__.py | 2 +- .../toolhive/api_models/audit.py | 2 +- src/mcp_optimizer/toolhive/api_models/auth.py | 7 ++++- .../toolhive/api_models/authz.py | 2 +- .../toolhive/api_models/client.py | 2 +- src/mcp_optimizer/toolhive/api_models/core.py | 4 +-- .../toolhive/api_models/groups.py | 2 +- .../toolhive/api_models/ignore.py | 2 +- .../toolhive/api_models/permissions.py | 13 ++++++++- .../toolhive/api_models/registry.py | 2 +- .../toolhive/api_models/runner.py | 27 ++++++++++--------- .../toolhive/api_models/secrets.py | 2 +- .../toolhive/api_models/telemetry.py | 7 ++++- .../toolhive/api_models/types.py | 2 +- src/mcp_optimizer/toolhive/api_models/v1.py | 8 +++++- 15 files changed, 57 insertions(+), 27 deletions(-) diff --git a/src/mcp_optimizer/toolhive/api_models/__init__.py b/src/mcp_optimizer/toolhive/api_models/__init__.py index 087d470..5ffa176 100644 --- a/src/mcp_optimizer/toolhive/api_models/__init__.py +++ b/src/mcp_optimizer/toolhive/api_models/__init__.py @@ -1,3 +1,3 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 diff --git a/src/mcp_optimizer/toolhive/api_models/audit.py b/src/mcp_optimizer/toolhive/api_models/audit.py index 6baeb2b..35f85cd 100644 --- a/src/mcp_optimizer/toolhive/api_models/audit.py +++ b/src/mcp_optimizer/toolhive/api_models/audit.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations diff --git a/src/mcp_optimizer/toolhive/api_models/auth.py b/src/mcp_optimizer/toolhive/api_models/auth.py index 7c125dd..9927871 100644 --- a/src/mcp_optimizer/toolhive/api_models/auth.py +++ b/src/mcp_optimizer/toolhive/api_models/auth.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations @@ -36,6 +36,11 @@ class TokenValidatorConfig(BaseModel): alias='clientSecret', description='ClientSecret is the optional OIDC client secret for introspection', ) + insecure_allow_http: Optional[bool] = Field( + None, + alias='insecureAllowHTTP', + description='InsecureAllowHTTP allows HTTP (non-HTTPS) OIDC issuers for development/testing\nWARNING: This is insecure and should NEVER be used in production', + ) introspection_url: Optional[str] = Field( None, alias='introspectionURL', diff --git a/src/mcp_optimizer/toolhive/api_models/authz.py b/src/mcp_optimizer/toolhive/api_models/authz.py index 4158a3c..f9ba8cc 100644 --- a/src/mcp_optimizer/toolhive/api_models/authz.py +++ b/src/mcp_optimizer/toolhive/api_models/authz.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations diff --git a/src/mcp_optimizer/toolhive/api_models/client.py b/src/mcp_optimizer/toolhive/api_models/client.py index f9be820..264118f 100644 --- a/src/mcp_optimizer/toolhive/api_models/client.py +++ b/src/mcp_optimizer/toolhive/api_models/client.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations diff --git a/src/mcp_optimizer/toolhive/api_models/core.py b/src/mcp_optimizer/toolhive/api_models/core.py index 9b52b49..40d45c4 100644 --- a/src/mcp_optimizer/toolhive/api_models/core.py +++ b/src/mcp_optimizer/toolhive/api_models/core.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations @@ -35,7 +35,7 @@ class Workload(BaseModel): ) proxy_mode: Optional[str] = Field( None, - description='ProxyMode is the proxy mode for stdio transport (sse or streamable-http).', + description='ProxyMode is the proxy mode that clients should use to connect.\nFor stdio transports, this will be the proxy mode (sse or streamable-http).\nFor direct transports (sse/streamable-http), this will be the same as TransportType.', ) remote: Optional[bool] = Field( None, diff --git a/src/mcp_optimizer/toolhive/api_models/groups.py b/src/mcp_optimizer/toolhive/api_models/groups.py index ad075df..7f105a5 100644 --- a/src/mcp_optimizer/toolhive/api_models/groups.py +++ b/src/mcp_optimizer/toolhive/api_models/groups.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations diff --git a/src/mcp_optimizer/toolhive/api_models/ignore.py b/src/mcp_optimizer/toolhive/api_models/ignore.py index 9ac7d18..d161b6a 100644 --- a/src/mcp_optimizer/toolhive/api_models/ignore.py +++ b/src/mcp_optimizer/toolhive/api_models/ignore.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations diff --git a/src/mcp_optimizer/toolhive/api_models/permissions.py b/src/mcp_optimizer/toolhive/api_models/permissions.py index 0680128..39658e2 100644 --- a/src/mcp_optimizer/toolhive/api_models/permissions.py +++ b/src/mcp_optimizer/toolhive/api_models/permissions.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations @@ -9,6 +9,12 @@ from pydantic import BaseModel, Field +class InboundNetworkPermissions(BaseModel): + allow_host: Optional[list[str]] = Field( + None, description='AllowHost is a list of allowed hosts for inbound connections' + ) + + class OutboundNetworkPermissions(BaseModel): allow_host: Optional[list[str]] = Field( None, description='AllowHost is a list of allowed hosts' @@ -22,6 +28,11 @@ class OutboundNetworkPermissions(BaseModel): class NetworkPermissions(BaseModel): + inbound: Optional[InboundNetworkPermissions] = None + mode: Optional[str] = Field( + None, + description='Mode specifies the network mode for the container (e.g., "host", "bridge", "none")\nWhen empty, the default container runtime network mode is used', + ) outbound: Optional[OutboundNetworkPermissions] = None diff --git a/src/mcp_optimizer/toolhive/api_models/registry.py b/src/mcp_optimizer/toolhive/api_models/registry.py index 403c916..9f457a7 100644 --- a/src/mcp_optimizer/toolhive/api_models/registry.py +++ b/src/mcp_optimizer/toolhive/api_models/registry.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations diff --git a/src/mcp_optimizer/toolhive/api_models/runner.py b/src/mcp_optimizer/toolhive/api_models/runner.py index 2a4b88a..fd1784a 100644 --- a/src/mcp_optimizer/toolhive/api_models/runner.py +++ b/src/mcp_optimizer/toolhive/api_models/runner.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations @@ -12,13 +12,13 @@ class RemoteAuthConfig(BaseModel): - authorize_url: Optional[str] = Field(None, alias='authorizeURL') - callback_port: Optional[int] = Field(None, alias='callbackPort') - client_id: Optional[str] = Field(None, alias='clientID') - client_secret: Optional[str] = Field(None, alias='clientSecret') - client_secret_file: Optional[str] = Field(None, alias='clientSecretFile') + authorize_url: Optional[str] = None + callback_port: Optional[int] = None + client_id: Optional[str] = None + client_secret: Optional[str] = None + client_secret_file: Optional[str] = None env_vars: Optional[list[registry.EnvVar]] = Field( - None, alias='envVars', description='Environment variables for the client' + None, description='Environment variables for the client' ) headers: Optional[list[registry.Header]] = Field( None, description='Headers for HTTP requests' @@ -27,14 +27,13 @@ class RemoteAuthConfig(BaseModel): None, description='OAuth endpoint configuration (from registry)' ) oauth_params: Optional[dict[str, str]] = Field( - None, - alias='oauthParams', - description='OAuth parameters for server-specific customization', + None, description='OAuth parameters for server-specific customization' ) scopes: Optional[list[str]] = None - skip_browser: Optional[bool] = Field(None, alias='skipBrowser') + skip_browser: Optional[bool] = None timeout: Optional[str] = Field(None, examples=['5m']) - token_url: Optional[str] = Field(None, alias='tokenURL') + token_url: Optional[str] = None + use_pkce: Optional[bool] = None class ToolOverride(BaseModel): @@ -153,6 +152,10 @@ class RunConfig(BaseModel): None, description='Transport is the transport mode (stdio, sse, or streamable-http)', ) + trust_proxy_headers: Optional[bool] = Field( + None, + description='TrustProxyHeaders indicates whether to trust X-Forwarded-* headers from reverse proxies', + ) volumes: Optional[list[str]] = Field( None, description='Volumes are the directory mounts to pass to the container\nFormat: "host-path:container-path[:ro]"', diff --git a/src/mcp_optimizer/toolhive/api_models/secrets.py b/src/mcp_optimizer/toolhive/api_models/secrets.py index 9a7870b..7be1f0b 100644 --- a/src/mcp_optimizer/toolhive/api_models/secrets.py +++ b/src/mcp_optimizer/toolhive/api_models/secrets.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations diff --git a/src/mcp_optimizer/toolhive/api_models/telemetry.py b/src/mcp_optimizer/toolhive/api_models/telemetry.py index 05526aa..d99c586 100644 --- a/src/mcp_optimizer/toolhive/api_models/telemetry.py +++ b/src/mcp_optimizer/toolhive/api_models/telemetry.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations @@ -10,6 +10,11 @@ class Config(BaseModel): + custom_attributes: Optional[dict[str, str]] = Field( + None, + alias='customAttributes', + description="CustomAttributes contains custom resource attributes to be added to all telemetry signals.\nThese are parsed from CLI flags (--otel-custom-attributes) or environment variables\n(OTEL_RESOURCE_ATTRIBUTES) as key=value pairs.\nWe use map[string]string for proper JSON serialization instead of []attribute.KeyValue\nwhich doesn't marshal/unmarshal correctly.", + ) enable_prometheus_metrics_path: Optional[bool] = Field( None, alias='enablePrometheusMetricsPath', diff --git a/src/mcp_optimizer/toolhive/api_models/types.py b/src/mcp_optimizer/toolhive/api_models/types.py index c250838..2a1b35f 100644 --- a/src/mcp_optimizer/toolhive/api_models/types.py +++ b/src/mcp_optimizer/toolhive/api_models/types.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations diff --git a/src/mcp_optimizer/toolhive/api_models/v1.py b/src/mcp_optimizer/toolhive/api_models/v1.py index c9f2261..b94072b 100644 --- a/src/mcp_optimizer/toolhive/api_models/v1.py +++ b/src/mcp_optimizer/toolhive/api_models/v1.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://127.0.0.1:8080/api/openapi.json -# timestamp: 2025-09-30T12:49:55+00:00 +# timestamp: 2025-10-24T12:14:20+00:00 from __future__ import annotations @@ -262,6 +262,9 @@ class CreateRequest(BaseModel): None, description='Tools override' ) transport: Optional[str] = Field(None, description='Transport configuration') + trust_proxy_headers: Optional[bool] = Field( + None, description='Whether to trust X-Forwarded-* headers from reverse proxies' + ) url: Optional[str] = Field(None, description='Remote server specific fields') volumes: Optional[list[str]] = Field(None, description='Volume mounts') @@ -316,6 +319,9 @@ class UpdateRequest(BaseModel): None, description='Tools override' ) transport: Optional[str] = Field(None, description='Transport configuration') + trust_proxy_headers: Optional[bool] = Field( + None, description='Whether to trust X-Forwarded-* headers from reverse proxies' + ) url: Optional[str] = Field(None, description='Remote server specific fields') volumes: Optional[list[str]] = Field(None, description='Volume mounts')