diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 23a66f86..9fdebc23 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -33,7 +33,7 @@ jobs: github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review' id: pr - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | let prNumber; @@ -54,7 +54,7 @@ jobs: - name: Checkout PR branch if: steps.pr.outcome == 'success' - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ steps.pr.outputs.sha }} repository: ${{ steps.pr.outputs.repo }} @@ -62,7 +62,7 @@ jobs: - name: Checkout repository if: steps.pr.outcome != 'success' - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/cli_tests.yml b/.github/workflows/cli_tests.yml index ede7643e..106d98f1 100644 --- a/.github/workflows/cli_tests.yml +++ b/.github/workflows/cli_tests.yml @@ -15,10 +15,10 @@ jobs: run: working-directory: ./cli steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: package.json cache: npm diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 378905b4..4013a905 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -18,9 +18,9 @@ jobs: sudo apt-get update sudo apt-get install -y libwoff1 - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 id: setup_node with: node-version-file: package.json @@ -29,7 +29,7 @@ jobs: # Cache Playwright browsers - name: Cache Playwright browsers id: cache-playwright - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.cache/ms-playwright # The default Playwright cache path key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} # Cache key based on OS and package-lock.json @@ -51,7 +51,7 @@ jobs: run: npm run test:e2e - name: Upload Playwright Report and Screenshots - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: steps.playwright-tests.conclusion != 'skipped' with: name: playwright-report diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d01f7175..2cc4537b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,12 +12,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check formatting run: npx prettier --check . - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version-file: package.json cache: npm @@ -50,8 +50,8 @@ jobs: id-token: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version-file: package.json cache: npm @@ -77,7 +77,7 @@ jobs: attestations: write id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Log in to the Container registry uses: docker/login-action@v3 @@ -109,7 +109,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@v3 with: subject-name: ghcr.io/${{ github.repository }} subject-digest: ${{ steps.push.outputs.digest }} diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 69bcdd35..33ea09bc 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -203,7 +203,7 @@ const ToolsTab = ({
- {tool.name} + {tool.title || tool.name} {tool.description} @@ -226,7 +226,9 @@ const ToolsTab = ({ /> )}

- {selectedTool ? selectedTool.name : "Select a tool"} + {selectedTool + ? selectedTool.title || selectedTool.name + : "Select a tool"}

diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index 975f79b0..692e05ba 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -159,20 +159,46 @@ export function isPropertyRequired( * Resolves $ref references in JSON schema * @param schema The schema that may contain $ref * @param rootSchema The root schema to resolve references against + * @param visitedRefs Optional set of visited $ref paths to detect circular references * @returns The resolved schema without $ref */ export function resolveRef( schema: JsonSchemaType, rootSchema: JsonSchemaType, + visitedRefs: Set = new Set(), ): JsonSchemaType { + if (!schema) return schema; + if (!("$ref" in schema) || !schema.$ref) { + // Recursively resolve $ref in anyOf (and other nested structures) + if (schema.anyOf && Array.isArray(schema.anyOf)) { + const resolvedAnyOf = schema.anyOf.map((item) => { + if (typeof item === "object" && item !== null) { + return resolveRef(item, rootSchema, visitedRefs); + } + return item; + }); + return { + ...schema, + anyOf: resolvedAnyOf, + }; + } return schema; } const ref = schema.$ref; - // Handle simple #/properties/name references + // Handle all #/ formats (#/properties/, #/$defs/, etc.) if (ref.startsWith("#/")) { + // Check for circular reference + if (visitedRefs.has(ref)) { + console.warn(`Circular reference detected: ${ref}`); + return schema; + } + + // Add current ref to visited set + visitedRefs.add(ref); + const path = ref.substring(2).split("/"); let current: unknown = rootSchema; @@ -186,12 +212,16 @@ export function resolveRef( current = (current as Record)[segment]; } else { // If reference cannot be resolved, return the original schema + visitedRefs.delete(ref); // Clean up on failure console.warn(`Could not resolve $ref: ${ref}`); return schema; } } - return current as JsonSchemaType; + const resolved = current as JsonSchemaType; + + // Recursively resolve nested structures (anyOf, oneOf, items, properties) + return resolveRef(resolved, rootSchema, visitedRefs); } // For other types of references, return the original schema @@ -205,54 +235,28 @@ export function resolveRef( * @returns A normalized schema or the original schema */ export function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType { - // Handle anyOf with exactly string and null (FastMCP pattern) + // Handle anyOf with exactly 2 items (type and null) - unified handling + // Preserves enum and other properties automatically if ( schema.anyOf && schema.anyOf.length === 2 && - schema.anyOf.some((t) => (t as JsonSchemaType).type === "string") && schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") ) { - return { ...schema, type: "string", anyOf: undefined, nullable: true }; - } + const nonNullItem = schema.anyOf.find((t) => { + const item = t as JsonSchemaType; + return item?.type !== "null"; + }) as JsonSchemaType; - // Handle anyOf with exactly boolean and null (FastMCP pattern) - if ( - schema.anyOf && - schema.anyOf.length === 2 && - schema.anyOf.some((t) => (t as JsonSchemaType).type === "boolean") && - schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") - ) { - return { ...schema, type: "boolean", anyOf: undefined, nullable: true }; - } - - // Handle anyOf with exactly number and null (FastMCP pattern) - if ( - schema.anyOf && - schema.anyOf.length === 2 && - schema.anyOf.some((t) => (t as JsonSchemaType).type === "number") && - schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") - ) { - return { ...schema, type: "number", anyOf: undefined, nullable: true }; - } - - // Handle anyOf with exactly integer and null (FastMCP pattern) - if ( - schema.anyOf && - schema.anyOf.length === 2 && - schema.anyOf.some((t) => (t as JsonSchemaType).type === "integer") && - schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") - ) { - return { ...schema, type: "integer", anyOf: undefined, nullable: true }; - } - - // Handle anyOf with exactly array and null (FastMCP pattern) - if ( - schema.anyOf && - schema.anyOf.length === 2 && - schema.anyOf.some((t) => (t as JsonSchemaType).type === "array") && - schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") - ) { - return { ...schema, type: "array", anyOf: undefined, nullable: true }; + // Only process if non-null item has type or enum + if (nonNullItem?.type || nonNullItem?.enum) { + return { + ...schema, + ...nonNullItem, + type: nonNullItem?.type || (nonNullItem?.enum ? "string" : undefined), + nullable: true, + anyOf: undefined, + }; + } } // Handle array type with exactly string and null