Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions .github/workflows/update-thv-models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand Down
55 changes: 22 additions & 33 deletions scripts/generate_toolhive_models.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/mcp_optimizer/toolhive/api_models/__init__.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/mcp_optimizer/toolhive/api_models/audit.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
7 changes: 6 additions & 1 deletion src/mcp_optimizer/toolhive/api_models/auth.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/mcp_optimizer/toolhive/api_models/authz.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/mcp_optimizer/toolhive/api_models/client.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/mcp_optimizer/toolhive/api_models/core.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/mcp_optimizer/toolhive/api_models/groups.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/mcp_optimizer/toolhive/api_models/ignore.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
13 changes: 12 additions & 1 deletion src/mcp_optimizer/toolhive/api_models/permissions.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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'
Expand All @@ -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


Expand Down
2 changes: 1 addition & 1 deletion src/mcp_optimizer/toolhive/api_models/registry.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
27 changes: 15 additions & 12 deletions src/mcp_optimizer/toolhive/api_models/runner.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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'
Expand All @@ -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):
Expand Down Expand Up @@ -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]"',
Expand Down
2 changes: 1 addition & 1 deletion src/mcp_optimizer/toolhive/api_models/secrets.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
7 changes: 6 additions & 1 deletion src/mcp_optimizer/toolhive/api_models/telemetry.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/mcp_optimizer/toolhive/api_models/types.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Loading