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
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
tags: mcp-optimizer:latest

- name: Install ToolHive
uses: StacklokLabs/toolhive-actions/install@v0
uses: StacklokLabs/toolhive-actions/install@6a095f99aa2fd6cd92cf0bb94bdf509b99820c06 # v0.0.3

- name: Run ToolHive server
run: |
Expand Down
113 changes: 113 additions & 0 deletions .github/workflows/update-thv-models.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: Update ToolHive Models

on:
schedule:
# Run every day at midnight UTC
- cron: '0 0 * * *'
workflow_dispatch: # Allow manual trigger

permissions:
contents: write
pull-requests: write
issues: write

concurrency:
group: update-thv-models
cancel-in-progress: true

jobs:
update-models:
name: Update ToolHive API Models
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

- name: Install ToolHive
uses: StacklokLabs/toolhive-actions/install@6a095f99aa2fd6cd92cf0bb94bdf509b99820c06 # v0.0.3

- name: Install uv
uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1
with:
enable-cache: true
python-version: '3.13'

- name: Install Task
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2.0.0
with:
version: 3.44.1

- name: Install Dependencies
run: task install

- name: Generate ToolHive Models
run: task generate-thv-models

- name: Check for Changes
id: check-changes
run: |
if git diff --quiet && git diff --cached --quiet; then
echo "No changes detected"
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "Changes detected"
echo "has_changes=true" >> $GITHUB_OUTPUT
fi

- name: Create Pull Request
if: steps.check-changes.outputs.has_changes == 'true'
id: create-pr
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: |
Update ToolHive API models

Automated update of ToolHive API models from OpenAPI specification.
branch: update-thv-models-${{ github.run_id }}
delete-branch: true
title: 'chore: Update ToolHive API models'
body: |
## Summary
This PR updates the ToolHive API models generated from the latest OpenAPI specification.

## Changes
- Updated Pydantic models in `src/mcp_optimizer/toolhive/api_models/`

## Notes
- This PR was automatically generated by the [update-thv-models workflow](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
- The models are generated using `datamodel-codegen` from ToolHive's OpenAPI endpoint

🤖 Generated with [GitHub Actions](https://github.com/features/actions)
labels: |
automated
dependencies

- name: Notify on Failure
if: failure()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const runUrl = `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`;

// Create an issue to notify about the failure
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: '⚠️ ToolHive Model Update Workflow Failed',
body: `## Workflow Failure Alert

The automated ToolHive model update workflow has failed.

**Details:**
- Workflow: \`${{ github.workflow }}\`
- Run ID: \`${{ github.run_id }}\`
- Triggered by: \`${{ github.event_name }}\`
- Branch: \`${{ github.ref_name }}\`

**Action Required:**
Please investigate the failure and fix any issues with the model generation process.

[View Failed Workflow Run](${runUrl})`,
labels: ['automated', 'bug', 'ci-failure']
});
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ tasks:
generate-thv-models:
desc: Generate Pydantic models from Toolhive's OpenAPI specification
cmds:
- ./generate_toolhive_models.sh
- ./scripts/generate_toolhive_models.sh
deps:
- install

Expand Down
14 changes: 0 additions & 14 deletions generate_toolhive_models.sh

This file was deleted.

139 changes: 139 additions & 0 deletions scripts/generate_toolhive_models.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env bash

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
fi
echo "Attempt $i/$MAX_ATTEMPTS: Waiting for OpenAPI endpoint..."
sleep 1
done
fi

# Save current models to temp directory for comparison
if [ -d "$OUTPUT_DIR" ]; then
echo "Backing up current models..."
cp -r "$OUTPUT_DIR" "$TEMP_DIR/backup"
fi

# Remove old models (with safety check)
if [ -n "$OUTPUT_DIR" ] && [ "$OUTPUT_DIR" != "/" ]; then
rm -rf "$OUTPUT_DIR"
else
echo "ERROR: Invalid OUTPUT_DIR value"
exit 1
fi

# Generate new models
echo "Generating models from OpenAPI specification..."
uv run datamodel-codegen \
--url http://127.0.0.1:8080/api/openapi.json \
--output "$OUTPUT_DIR" \
--input-file-type openapi \
--use-standard-collections \
--use-subclass-enum \
--snake-case-field \
--collapse-root-models \
--target-python-version 3.13 \
--output-model-type pydantic_v2.BaseModel

# Check if there are meaningful changes (excluding timestamp)
if [ -d "$TEMP_DIR/backup" ]; then
echo "Checking for meaningful changes..."

# Create copies with timestamp lines removed for comparison
mkdir -p "$TEMP_DIR/new" "$TEMP_DIR/old"

# Process new files
if [ -d "$OUTPUT_DIR" ] && [ -n "$(ls -A "$OUTPUT_DIR"/*.py 2>/dev/null)" ]; then
for file in "$OUTPUT_DIR"/*.py; do
if [ -f "$file" ]; then
filename=$(basename "$file")
grep --text -v "^# timestamp:" "$file" > "$TEMP_DIR/new/$filename" || true
fi
done
fi

# Process old files
for file in "$TEMP_DIR/backup"/*.py; do
if [ -f "$file" ]; then
filename=$(basename "$file")
grep --text -v "^# timestamp:" "$file" > "$TEMP_DIR/old/$filename" || true
fi
done

# Compare directories and collect changed files
CHANGED_FILES=()
for file in "$TEMP_DIR/new"/*.py; do
filename=$(basename "$file")
old_file="$TEMP_DIR/old/$filename"

if [ ! -f "$old_file" ]; then
CHANGED_FILES+=("$filename (new file)")
elif ! diff "$file" "$old_file" > /dev/null 2>&1; then
CHANGED_FILES+=("$filename")
fi
done

# Check for deleted files
for file in "$TEMP_DIR/old"/*.py; do
filename=$(basename "$file")
new_file="$TEMP_DIR/new/$filename"

if [ ! -f "$new_file" ]; then
CHANGED_FILES+=("$filename (deleted)")
fi
done

if [ ${#CHANGED_FILES[@]} -eq 0 ]; then
echo "No meaningful changes detected (only timestamp updated)"
echo "Restoring original files..."
rm -rf "$OUTPUT_DIR"
mv "$TEMP_DIR/backup" "$OUTPUT_DIR"
exit 0
else
echo "Meaningful changes detected in ${#CHANGED_FILES[@]} file(s):"
printf ' - %s\n' "${CHANGED_FILES[@]}"
fi
fi

echo "Model generation complete!"