mirror of
https://github.com/mcp-use/mcp-use.git
synced 2026-09-14 18:06:01 +08:00
1109 lines
38 KiB
YAML
1109 lines
38 KiB
YAML
name: CI
|
||
permissions:
|
||
contents: read
|
||
|
||
# ============================================================================
|
||
# CI Workflow Overview
|
||
# ============================================================================
|
||
#
|
||
# This workflow provides comprehensive testing for both Python and TypeScript
|
||
# libraries with intelligent path-based filtering to only run relevant tests.
|
||
#
|
||
# WORKFLOW STRUCTURE:
|
||
#
|
||
# 1. detect-changes (Always runs first)
|
||
# - Uses path filters to determine which parts of the codebase changed
|
||
# - Outputs: python, typescript, knip, create-mcp-use-app
|
||
# - Subsequent jobs use these outputs to conditionally run
|
||
#
|
||
# 2. Python Tests (runs if libraries/python/** changed)
|
||
# ├─ python-lint: Ruff linting and formatting checks
|
||
# ├─ python-unit-tests: Matrix [Python 3.11, 3.12]
|
||
# ├─ python-transport-tests: Matrix [stdio, sse, streamable_http]
|
||
# │ └─ python-transport-tests-report: Posts PR comment with test results table
|
||
# ├─ python-primitive-tests: Matrix [7 MCP primitives]
|
||
# │ └─ python-primitive-tests-report: Posts PR comment with test results table
|
||
# ├─ python-integration-tests: Other integration tests
|
||
# └─ python-agent-tests: Matrix [4 agent test suites] (requires API keys)
|
||
#
|
||
# 3. TypeScript Tests (runs if libraries/typescript/** or this workflow changed)
|
||
# ├─ typescript-lint: ESLint checks
|
||
# ├─ typescript-build: Build all packages
|
||
# ├─ typescript-knip: Unused code/deps check (fails CI on findings)
|
||
# ├─ typescript/mcp-use: Unit tests + agent integration tests (requires API keys)
|
||
# ├─ typescript/inspector: Unit tests for inspector package
|
||
# ├─ typescript/cli: Unit tests for CLI package
|
||
# ├─ typescript/tunnel: Protocol and local relay integration tests
|
||
# ├─ typescript-release-channel-test: release-channel helper suite (main + canary PRs)
|
||
# └─ typescript-changeset-check: PR validation (PRs only)
|
||
#
|
||
# 4. create-mcp-use-app Tests (runs if that package or this workflow changed)
|
||
# ├─ create-mcp-use-app-build: Build, unit test, and pack the CLI
|
||
# └─ create-app-tests: Matrix [3 OS × 3 PM × 3 templates × 2 flags]
|
||
# - Tests project creation across Ubuntu/macOS/Windows
|
||
# - Tests npm, yarn, pnpm package managers
|
||
# - Tests starter, mcp-ui, mcp-apps templates
|
||
# - Collapsed into one expandable job for clean UI
|
||
#
|
||
|
||
# ============================================================================
|
||
|
||
on:
|
||
push:
|
||
branches: [main, canary]
|
||
pull_request:
|
||
branches: [main, canary]
|
||
workflow_call:
|
||
inputs:
|
||
ref:
|
||
description: "Commit SHA or ref to test"
|
||
required: true
|
||
type: string
|
||
python:
|
||
description: "Run Python jobs"
|
||
required: false
|
||
type: boolean
|
||
default: true
|
||
typescript:
|
||
description: "Run TypeScript jobs"
|
||
required: false
|
||
type: boolean
|
||
default: true
|
||
create_mcp_use_app:
|
||
description: "Run create-mcp-use-app jobs"
|
||
required: false
|
||
type: boolean
|
||
default: false
|
||
run_changeset_check:
|
||
description: "Run changeset check (usually PR-only)"
|
||
required: false
|
||
type: boolean
|
||
default: false
|
||
|
||
jobs:
|
||
detect-changes:
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
python: ${{ steps.out.outputs.python }}
|
||
typescript: ${{ steps.out.outputs.typescript }}
|
||
knip: ${{ steps.out.outputs.knip }}
|
||
create-mcp-use-app: ${{ steps.out.outputs.create-mcp-use-app }}
|
||
release-channel: ${{ steps.out.outputs.release-channel }}
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
fetch-depth: 0
|
||
- uses: dorny/paths-filter@v3
|
||
id: filter
|
||
# Reusable workflows keep the caller's event name; ref is required for calls.
|
||
if: ${{ !inputs.ref }}
|
||
with:
|
||
filters: |
|
||
python:
|
||
- 'libraries/python/**'
|
||
typescript:
|
||
- 'libraries/typescript/**'
|
||
- '.github/workflows/ci.yml'
|
||
knip:
|
||
- 'libraries/typescript/**'
|
||
- '.github/workflows/ci.yml'
|
||
create-mcp-use-app:
|
||
- 'libraries/typescript/packages/create-mcp-use-app/**'
|
||
- '.github/workflows/ci.yml'
|
||
release-channel:
|
||
- 'libraries/typescript/scripts/release-channel.mjs'
|
||
- 'libraries/typescript/scripts/release-channel.test.mjs'
|
||
- 'libraries/typescript/package.json'
|
||
- '.github/workflows/ci.yml'
|
||
- '.github/workflows/typescript-release.yml'
|
||
- name: Compute outputs
|
||
id: out
|
||
uses: actions/github-script@v9
|
||
env:
|
||
CI_INPUT_REF: ${{ inputs.ref }}
|
||
CI_INPUT_PYTHON: ${{ inputs.python }}
|
||
CI_INPUT_TYPESCRIPT: ${{ inputs.typescript }}
|
||
CI_INPUT_CREATE: ${{ inputs.create_mcp_use_app }}
|
||
with:
|
||
script: |
|
||
const fromWfCall = Boolean(process.env.CI_INPUT_REF);
|
||
const py = fromWfCall ? (process.env.CI_INPUT_PYTHON === 'true') : ("${{ steps.filter.outputs.python }}" === 'true');
|
||
const ts = fromWfCall ? (process.env.CI_INPUT_TYPESCRIPT === 'true') : ("${{ steps.filter.outputs.typescript }}" === 'true');
|
||
const knip = fromWfCall ? (process.env.CI_INPUT_TYPESCRIPT === 'true') : ("${{ steps.filter.outputs.knip }}" === 'true');
|
||
const create = fromWfCall ? (process.env.CI_INPUT_CREATE === 'true') : ("${{ steps.filter.outputs.create-mcp-use-app }}" === 'true');
|
||
core.setOutput('python', String(py));
|
||
core.setOutput('typescript', String(ts));
|
||
core.setOutput('knip', String(knip));
|
||
core.setOutput('create-mcp-use-app', String(create));
|
||
// The reusable-workflow caller only reports python/typescript/create-app,
|
||
// so it cannot tell us whether a release workflow changed. The helper test
|
||
// takes a few seconds, so run it on every workflow_call rather than guess.
|
||
const releaseChannel = fromWfCall ? true : ("${{ steps.filter.outputs.release-channel }}" === 'true');
|
||
core.setOutput('release-channel', String(releaseChannel));
|
||
|
||
# Python Jobs
|
||
python-lint:
|
||
needs: detect-changes
|
||
if: needs.detect-changes.outputs.python == 'true'
|
||
runs-on: ubuntu-latest
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/python
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- name: Set up Python 3.11
|
||
uses: actions/setup-python@v7
|
||
with:
|
||
python-version: "3.11"
|
||
- name: Install dependencies
|
||
run: |
|
||
python -m pip install --upgrade pip
|
||
# Pinned so a new ruff release cannot change what this job checks.
|
||
# 0.16.0 began formatting fenced code blocks inside Markdown, which
|
||
# turned every libraries/python PR red on README examples that were
|
||
# never covered before. Bump deliberately, alongside any reformat.
|
||
pip install ruff==0.15.22
|
||
- name: Lint with ruff
|
||
run: |
|
||
ruff check .
|
||
- name: Format check with ruff
|
||
run: |
|
||
ruff format --check .
|
||
|
||
# (skipped) python-ty-check job is disabled, pre commits will make use compliant, then we can add this back.
|
||
# Pietro: this is me, I will check when is a good time to re enable. keep the comment.
|
||
# python-ty-check:
|
||
# needs: python-lint
|
||
# runs-on: ubuntu-latest
|
||
# defaults:
|
||
# run:
|
||
# working-directory: libraries/python
|
||
# steps:
|
||
# - uses: actions/checkout@v7
|
||
# with:
|
||
# ref: ${{ inputs.ref || github.sha }}
|
||
# - name: Set up Python 3.11
|
||
# uses: actions/setup-python@v7
|
||
# with:
|
||
# python-version: "3.11"
|
||
# - name: Install dependencies
|
||
# run: |
|
||
# python -m pip install --upgrade pip
|
||
# pip install ty
|
||
# - name: Type check with ty
|
||
# run: |
|
||
# ty check .
|
||
|
||
python-unit-tests:
|
||
needs: python-lint
|
||
name: "python-unit"
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
matrix:
|
||
python-version: ["3.11", "3.12"]
|
||
deps: ["latest", "minimum"]
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/python
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- name: Set up Python ${{ matrix.python-version }}
|
||
uses: actions/setup-python@v7
|
||
with:
|
||
python-version: ${{ matrix.python-version }}
|
||
- name: Install uv
|
||
run: pip install uv
|
||
- name: Install dependencies (latest)
|
||
if: matrix.deps == 'latest'
|
||
run: uv pip install --system -e ".[dev,anthropic,openai,search,e2b]"
|
||
- name: Install dependencies (minimum)
|
||
if: matrix.deps == 'minimum'
|
||
run: uv pip install --system -e ".[dev,anthropic,openai,search,e2b]" --resolution lowest-direct
|
||
- name: Test with pytest
|
||
env:
|
||
MCP_USE_ANONYMIZED_TELEMETRY: false
|
||
run: pytest -vv tests/unit
|
||
|
||
python-transport-tests:
|
||
needs: python-lint
|
||
name: "python-transport/${{ matrix.transport }}"
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
transport: [stdio, sse, streamable_http]
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/python
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- name: Set up Python 3.11
|
||
uses: actions/setup-python@v7
|
||
with:
|
||
python-version: "3.11"
|
||
- name: Install uv
|
||
run: |
|
||
pip install uv
|
||
- name: Install dependencies
|
||
run: |
|
||
uv pip install --system .[dev,anthropic,openai,search,e2b]
|
||
- name: Run integration tests for ${{ matrix.transport }} transport
|
||
id: test
|
||
env:
|
||
MCP_USE_ANONYMIZED_TELEMETRY: false
|
||
run: |
|
||
pytest -vv tests/integration/client/transports/test_${{ matrix.transport }}.py \
|
||
--junit-xml=test-results-${{ matrix.transport }}.xml || true
|
||
# Check if tests passed
|
||
if grep -q 'failures="0"' test-results-${{ matrix.transport }}.xml && \
|
||
grep -q 'errors="0"' test-results-${{ matrix.transport }}.xml; then
|
||
echo "status=passed" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "status=failed" >> $GITHUB_OUTPUT
|
||
fi
|
||
- name: Upload test results
|
||
uses: actions/upload-artifact@v7
|
||
if: always()
|
||
with:
|
||
name: transport-test-results-${{ matrix.transport }}
|
||
path: libraries/python/test-results-${{ matrix.transport }}.xml
|
||
retention-days: 7
|
||
- name: Fail if tests failed
|
||
if: steps.test.outputs.status == 'failed'
|
||
run: exit 1
|
||
|
||
python-transport-tests-report:
|
||
needs: python-transport-tests
|
||
if: always() && github.event_name == 'pull_request' && needs.python-transport-tests.result != 'skipped'
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
pull-requests: write
|
||
steps:
|
||
- name: Download all test results
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||
with:
|
||
pattern: transport-test-results-*
|
||
path: test-results
|
||
merge-multiple: false
|
||
- name: Generate PR comment
|
||
uses: actions/github-script@v9
|
||
with:
|
||
script: |
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const transports = ['stdio', 'sse', 'streamable_http'];
|
||
let tableRows = [];
|
||
let allPassed = true;
|
||
|
||
for (const transport of transports) {
|
||
const xmlPath = path.join('test-results', `transport-test-results-${transport}`, `test-results-${transport}.xml`);
|
||
let status = '⏭️ Skipped';
|
||
let tests = '-';
|
||
let passed = '-';
|
||
let failed = '-';
|
||
let errors = '-';
|
||
let time = '-';
|
||
|
||
if (fs.existsSync(xmlPath)) {
|
||
const content = fs.readFileSync(xmlPath, 'utf8');
|
||
|
||
// Parse basic stats from JUnit XML
|
||
const testsMatch = content.match(/tests="(\d+)"/);
|
||
const failuresMatch = content.match(/failures="(\d+)"/);
|
||
const errorsMatch = content.match(/errors="(\d+)"/);
|
||
const timeMatch = content.match(/time="([\d.]+)"/);
|
||
|
||
if (testsMatch) {
|
||
const totalTests = parseInt(testsMatch[1]);
|
||
const failureCount = failuresMatch ? parseInt(failuresMatch[1]) : 0;
|
||
const errorCount = errorsMatch ? parseInt(errorsMatch[1]) : 0;
|
||
const passedCount = totalTests - failureCount - errorCount;
|
||
|
||
tests = totalTests.toString();
|
||
passed = passedCount.toString();
|
||
failed = failureCount.toString();
|
||
errors = errorCount.toString();
|
||
time = timeMatch ? `${parseFloat(timeMatch[1]).toFixed(2)}s` : '-';
|
||
|
||
if (failureCount === 0 && errorCount === 0) {
|
||
status = '✅ Passed';
|
||
} else {
|
||
status = '❌ Failed';
|
||
allPassed = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
tableRows.push(`| ${transport} | ${status} | ${tests} | ${passed} | ${failed} | ${errors} | ${time} |`);
|
||
}
|
||
|
||
const overallStatus = allPassed ? '✅ All transport tests passed!' : '❌ Some transport tests failed';
|
||
|
||
const comment = `## Python Transport Tests Results
|
||
|
||
${overallStatus}
|
||
|
||
| Transport | Status | Tests | Passed | Failed | Errors | Time |
|
||
|-----------|--------|-------|--------|--------|--------|------|
|
||
${tableRows.join('\n')}
|
||
`;
|
||
|
||
// Find existing comment
|
||
const { data: comments } = await github.rest.issues.listComments({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
issue_number: context.issue.number,
|
||
});
|
||
|
||
const botComment = comments.find(c =>
|
||
c.user.type === 'Bot' &&
|
||
c.body.includes('## Python Transport Tests Results')
|
||
);
|
||
|
||
if (botComment) {
|
||
await github.rest.issues.updateComment({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
comment_id: botComment.id,
|
||
body: comment,
|
||
});
|
||
} else {
|
||
await github.rest.issues.createComment({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
issue_number: context.issue.number,
|
||
body: comment,
|
||
});
|
||
}
|
||
|
||
python-primitive-tests:
|
||
needs: python-lint
|
||
name: "python-primitive/${{ matrix.primitive }}"
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
primitive:
|
||
[
|
||
sampling,
|
||
tools,
|
||
resources,
|
||
prompts,
|
||
elicitation,
|
||
notifications,
|
||
auth,
|
||
roots,
|
||
]
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/python
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- name: Set up Python 3.11
|
||
uses: actions/setup-python@v7
|
||
with:
|
||
python-version: "3.11"
|
||
- name: Install uv
|
||
run: |
|
||
pip install uv
|
||
- name: Install dependencies
|
||
run: |
|
||
uv pip install --system .[dev,anthropic,openai,search,e2b]
|
||
- name: Run integration tests for ${{ matrix.primitive }} primitive
|
||
id: test
|
||
env:
|
||
MCP_USE_ANONYMIZED_TELEMETRY: false
|
||
run: |
|
||
pytest -vv tests/integration/client/primitives/test_${{ matrix.primitive }}.py \
|
||
--junit-xml=test-results-${{ matrix.primitive }}.xml || true
|
||
# Check if tests passed
|
||
if grep -q 'failures="0"' test-results-${{ matrix.primitive }}.xml && \
|
||
grep -q 'errors="0"' test-results-${{ matrix.primitive }}.xml; then
|
||
echo "status=passed" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "status=failed" >> $GITHUB_OUTPUT
|
||
fi
|
||
- name: Upload test results
|
||
uses: actions/upload-artifact@v7
|
||
if: always()
|
||
with:
|
||
name: primitive-test-results-${{ matrix.primitive }}
|
||
path: libraries/python/test-results-${{ matrix.primitive }}.xml
|
||
retention-days: 7
|
||
- name: Fail if tests failed
|
||
if: steps.test.outputs.status == 'failed'
|
||
run: exit 1
|
||
|
||
python-primitive-tests-report:
|
||
needs: python-primitive-tests
|
||
if: always() && github.event_name == 'pull_request' && needs.python-primitive-tests.result != 'skipped'
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
pull-requests: write
|
||
steps:
|
||
- name: Download all test results
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||
with:
|
||
pattern: primitive-test-results-*
|
||
path: test-results
|
||
merge-multiple: false
|
||
- name: Generate PR comment
|
||
uses: actions/github-script@v9
|
||
with:
|
||
script: |
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const primitives = ['sampling', 'tools', 'resources', 'prompts', 'elicitation', 'notifications', 'auth', 'roots'];
|
||
let tableRows = [];
|
||
let allPassed = true;
|
||
|
||
for (const primitive of primitives) {
|
||
const xmlPath = path.join('test-results', `primitive-test-results-${primitive}`, `test-results-${primitive}.xml`);
|
||
let status = '⏭️ Skipped';
|
||
let tests = '-';
|
||
let passed = '-';
|
||
let failed = '-';
|
||
let errors = '-';
|
||
let time = '-';
|
||
|
||
if (fs.existsSync(xmlPath)) {
|
||
const content = fs.readFileSync(xmlPath, 'utf8');
|
||
|
||
// Parse basic stats from JUnit XML
|
||
const testsMatch = content.match(/tests="(\d+)"/);
|
||
const failuresMatch = content.match(/failures="(\d+)"/);
|
||
const errorsMatch = content.match(/errors="(\d+)"/);
|
||
const timeMatch = content.match(/time="([\d.]+)"/);
|
||
|
||
if (testsMatch) {
|
||
const totalTests = parseInt(testsMatch[1]);
|
||
const failureCount = failuresMatch ? parseInt(failuresMatch[1]) : 0;
|
||
const errorCount = errorsMatch ? parseInt(errorsMatch[1]) : 0;
|
||
const passedCount = totalTests - failureCount - errorCount;
|
||
|
||
tests = totalTests.toString();
|
||
passed = passedCount.toString();
|
||
failed = failureCount.toString();
|
||
errors = errorCount.toString();
|
||
time = timeMatch ? `${parseFloat(timeMatch[1]).toFixed(2)}s` : '-';
|
||
|
||
if (failureCount === 0 && errorCount === 0) {
|
||
status = '✅ Passed';
|
||
} else {
|
||
status = '❌ Failed';
|
||
allPassed = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
tableRows.push(`| ${primitive} | ${status} | ${tests} | ${passed} | ${failed} | ${errors} | ${time} |`);
|
||
}
|
||
|
||
const overallStatus = allPassed ? '✅ All primitive tests passed!' : '❌ Some primitive tests failed';
|
||
|
||
const comment = `## Python Primitive Tests Results
|
||
|
||
${overallStatus}
|
||
|
||
| Primitive | Status | Tests | Passed | Failed | Errors | Time |
|
||
|-----------|--------|-------|--------|--------|--------|------|
|
||
${tableRows.join('\n')}
|
||
`;
|
||
|
||
// Find existing comment
|
||
const { data: comments } = await github.rest.issues.listComments({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
issue_number: context.issue.number,
|
||
});
|
||
|
||
const botComment = comments.find(c =>
|
||
c.user.type === 'Bot' &&
|
||
c.body.includes('## Python Primitive Tests Results')
|
||
);
|
||
|
||
if (botComment) {
|
||
await github.rest.issues.updateComment({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
comment_id: botComment.id,
|
||
body: comment,
|
||
});
|
||
} else {
|
||
await github.rest.issues.createComment({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
issue_number: context.issue.number,
|
||
body: comment,
|
||
});
|
||
}
|
||
|
||
python-integration-tests:
|
||
needs: python-lint
|
||
name: "python-integration"
|
||
runs-on: ubuntu-latest
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/python
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- name: Set up Python 3.11
|
||
uses: actions/setup-python@v7
|
||
with:
|
||
python-version: "3.11"
|
||
- name: Install uv
|
||
run: |
|
||
pip install uv
|
||
- name: Install dependencies
|
||
run: |
|
||
uv pip install --system .[dev,anthropic,openai,search,e2b]
|
||
- name: Run other integration tests
|
||
env:
|
||
MCP_USE_ANONYMIZED_TELEMETRY: false
|
||
run: pytest -vv tests/integration/client/others/
|
||
|
||
python-agent-tests:
|
||
needs: python-lint
|
||
name: "python-agent/${{ matrix.test }}"
|
||
runs-on: ubuntu-latest
|
||
env:
|
||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
test: [agent_run, agent_stream, agent_structured_output, server_manager]
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/python
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- name: Set up Python 3.11
|
||
uses: actions/setup-python@v7
|
||
with:
|
||
python-version: "3.11"
|
||
- name: Install uv
|
||
run: |
|
||
pip install uv
|
||
- name: Install dependencies
|
||
run: |
|
||
uv pip install --system .[dev,anthropic,openai,search,e2b]
|
||
- name: Run agent test for ${{ matrix.test }}
|
||
if: env.OPENAI_API_KEY != ''
|
||
env:
|
||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||
MCP_USE_ANONYMIZED_TELEMETRY: false
|
||
run: pytest -vv tests/integration/agent/test_${{ matrix.test }}.py
|
||
|
||
# TypeScript Jobs
|
||
typescript-lint:
|
||
needs: detect-changes
|
||
if: needs.detect-changes.outputs.typescript == 'true'
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
matrix:
|
||
node-version: [22.23.1]
|
||
name: typescript-lint (node-${{ matrix.node-version }})
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/typescript
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
|
||
- name: Setup pnpm
|
||
uses: pnpm/action-setup@v4
|
||
with:
|
||
version: 11.13.1
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v7
|
||
with:
|
||
node-version: ${{ matrix.node-version }}
|
||
cache: "pnpm"
|
||
cache-dependency-path: libraries/typescript/pnpm-lock.yaml
|
||
|
||
- name: Install Dependencies
|
||
run: pnpm install --no-frozen-lockfile
|
||
|
||
- name: Build Client Types
|
||
run: pnpm build:client
|
||
|
||
- name: Build Tunnel Types
|
||
run: pnpm build:tunnel
|
||
|
||
- name: Check Formatting
|
||
run: pnpm format:check
|
||
|
||
- name: Run Linter
|
||
run: pnpm lint
|
||
|
||
typescript-build:
|
||
needs: typescript-lint
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
matrix:
|
||
node-version: [22.23.1]
|
||
name: typescript-build (node-${{ matrix.node-version }})
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/typescript
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
|
||
- name: Setup pnpm
|
||
uses: pnpm/action-setup@v4
|
||
with:
|
||
version: 11.13.1
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v7
|
||
with:
|
||
node-version: ${{ matrix.node-version }}
|
||
cache: "pnpm"
|
||
cache-dependency-path: libraries/typescript/pnpm-lock.yaml
|
||
|
||
- name: Install Dependencies
|
||
run: pnpm install --no-frozen-lockfile
|
||
|
||
- name: Build Packages
|
||
run: pnpm build
|
||
|
||
typescript-test-mcp-use:
|
||
needs: typescript-lint
|
||
name: "typescript/mcp-use (node-${{ matrix.node-version }})"
|
||
runs-on: ubuntu-latest
|
||
env:
|
||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||
strategy:
|
||
matrix:
|
||
node-version: [22.23.1]
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/typescript
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- uses: pnpm/action-setup@v4
|
||
with:
|
||
version: 11.13.1
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: ${{ matrix.node-version }}
|
||
cache: "pnpm"
|
||
cache-dependency-path: libraries/typescript/pnpm-lock.yaml
|
||
- name: Install Dependencies
|
||
run: pnpm install --no-frozen-lockfile
|
||
- name: Build Packages
|
||
run: pnpm build
|
||
- name: Run client Unit Tests
|
||
env:
|
||
MCP_USE_ANONYMIZED_TELEMETRY: false
|
||
run: pnpm --filter @mcp-use/client test:run
|
||
- name: Run mcp-use Unit Tests
|
||
env:
|
||
MCP_USE_ANONYMIZED_TELEMETRY: false
|
||
run: pnpm --filter mcp-use test:run
|
||
- name: Run @mcp-use/agent Unit Tests
|
||
env:
|
||
MCP_USE_ANONYMIZED_TELEMETRY: false
|
||
run: pnpm --filter @mcp-use/agent test:unit
|
||
- name: Run @mcp-use/agent native OpenAI e2e
|
||
if: env.OPENAI_API_KEY != ''
|
||
env:
|
||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||
MCP_USE_ANONYMIZED_TELEMETRY: false
|
||
run: pnpm --filter @mcp-use/agent test:integration:native-openai
|
||
|
||
typescript-knip:
|
||
needs: detect-changes
|
||
if: needs.detect-changes.outputs.knip == 'true'
|
||
name: typescript-knip
|
||
runs-on: ubuntu-latest
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/typescript
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- uses: pnpm/action-setup@v4
|
||
with:
|
||
version: 11.13.1
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: 22.23.1
|
||
cache: "pnpm"
|
||
cache-dependency-path: libraries/typescript/pnpm-lock.yaml
|
||
- name: Install Dependencies
|
||
run: pnpm install --frozen-lockfile
|
||
- name: Run Knip
|
||
run: pnpm exec knip --reporter github-actions
|
||
|
||
typescript-test-inspector:
|
||
needs: typescript-lint
|
||
name: "typescript/inspector (node-${{ matrix.node-version }})"
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
matrix:
|
||
node-version: [22.23.1]
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/typescript
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- uses: pnpm/action-setup@v4
|
||
with:
|
||
version: 11.13.1
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: ${{ matrix.node-version }}
|
||
cache: "pnpm"
|
||
cache-dependency-path: libraries/typescript/pnpm-lock.yaml
|
||
- name: Install Dependencies
|
||
run: pnpm install --no-frozen-lockfile
|
||
- name: Build Packages
|
||
run: pnpm build
|
||
- name: Run Inspector Tests
|
||
env:
|
||
MCP_USE_ANONYMIZED_TELEMETRY: false
|
||
run: pnpm --filter @mcp-use/inspector test:unit
|
||
|
||
typescript-test-cli:
|
||
needs: typescript-lint
|
||
name: "typescript/cli (${{ matrix.os }}, node-${{ matrix.node-version }})"
|
||
runs-on: ${{ matrix.os }}
|
||
strategy:
|
||
# Keep the other OS running when one fails: comparing the two is the
|
||
# whole reason this job has an os matrix.
|
||
fail-fast: false
|
||
matrix:
|
||
os: [ubuntu-latest, windows-latest]
|
||
node-version: [22.23.1]
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/typescript
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- uses: pnpm/action-setup@v4
|
||
with:
|
||
version: 11.13.1
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: ${{ matrix.node-version }}
|
||
cache: "pnpm"
|
||
cache-dependency-path: libraries/typescript/pnpm-lock.yaml
|
||
- name: Install Dependencies
|
||
run: pnpm install --no-frozen-lockfile
|
||
- name: Build Packages
|
||
run: pnpm build
|
||
- name: Verify CLI build artifact
|
||
shell: bash
|
||
run: test -f packages/cli/dist/bin.js || { echo "::warning::dist/bin.js missing after pnpm build, rebuilding CLI explicitly"; pnpm --filter @mcp-use/cli build; }
|
||
- name: Run CLI Tests
|
||
env:
|
||
MCP_USE_ANONYMIZED_TELEMETRY: false
|
||
run: pnpm --filter @mcp-use/cli --if-present test:run
|
||
|
||
typescript-test-tunnel:
|
||
needs: typescript-lint
|
||
name: "typescript/tunnel (node-${{ matrix.node-version }})"
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
matrix:
|
||
node-version: [22.23.1]
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/typescript
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- uses: pnpm/action-setup@v4
|
||
with:
|
||
version: 11.13.1
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: ${{ matrix.node-version }}
|
||
cache: pnpm
|
||
cache-dependency-path: libraries/typescript/pnpm-lock.yaml
|
||
- name: Install Dependencies
|
||
run: pnpm install --frozen-lockfile
|
||
- name: Build Tunnel
|
||
run: pnpm build:tunnel
|
||
- name: Run Tunnel Tests
|
||
run: pnpm --filter @mcp-use/tunnel test:run
|
||
|
||
typescript-release-channel-test:
|
||
needs: detect-changes
|
||
if: needs.detect-changes.outputs.release-channel == 'true'
|
||
runs-on: ubuntu-latest
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/typescript
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
|
||
- name: Setup pnpm
|
||
uses: pnpm/action-setup@v4
|
||
with:
|
||
version: 11.13.1
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v7
|
||
with:
|
||
node-version: 22.23.1
|
||
cache: "pnpm"
|
||
cache-dependency-path: libraries/typescript/pnpm-lock.yaml
|
||
|
||
- name: Install Dependencies
|
||
run: pnpm install --no-frozen-lockfile
|
||
|
||
- name: Test release-channel helper
|
||
run: pnpm test:release-channel
|
||
|
||
typescript-changeset-check:
|
||
needs: detect-changes
|
||
if: ${{ (inputs.ref && inputs.run_changeset_check) || (!inputs.ref && github.event_name == 'pull_request' && github.base_ref != 'canary' && needs.detect-changes.outputs.typescript == 'true') }}
|
||
runs-on: ubuntu-latest
|
||
defaults:
|
||
run:
|
||
working-directory: libraries/typescript
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
fetch-depth: 0
|
||
|
||
- name: Setup pnpm
|
||
uses: pnpm/action-setup@v4
|
||
with:
|
||
version: 11.13.1
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v7
|
||
with:
|
||
node-version: 22.23.1
|
||
cache: "pnpm"
|
||
cache-dependency-path: libraries/typescript/pnpm-lock.yaml
|
||
|
||
- name: Install Dependencies
|
||
run: pnpm install --no-frozen-lockfile
|
||
|
||
- name: Prevent canary-to-main version regression
|
||
if: github.event_name == 'pull_request' && github.base_ref == 'main' && github.head_ref == 'canary'
|
||
run: pnpm release-channel preflight --channel stable
|
||
|
||
- name: Check for Changesets
|
||
env:
|
||
BASE_REF: ${{ github.base_ref || 'main' }}
|
||
HEAD_REF: ${{ github.head_ref }}
|
||
run: |
|
||
if [ "$BASE_REF" = "main" ] && [ "$HEAD_REF" = "canary" ]; then
|
||
pending_changesets="$(node scripts/release-channel.mjs pending)"
|
||
if [ -n "$pending_changesets" ]; then
|
||
echo "✅ Pending changeset found"
|
||
printf '%s\n' "$pending_changesets"
|
||
pnpm changeset status --since="origin/${BASE_REF}"
|
||
else
|
||
echo "✅ No pending changesets; prerelease changesets are already applied"
|
||
fi
|
||
elif [ -n "$(ls -A .changeset/*.md 2>/dev/null | grep -v README.md)" ]; then
|
||
echo "✅ Changeset found"
|
||
pnpm changeset status --since="origin/${BASE_REF}"
|
||
else
|
||
echo "⚠️ No changeset found. If this PR includes changes that should be published, please add a changeset:"
|
||
echo " pnpm changeset"
|
||
echo ""
|
||
echo "If this PR doesn't require a changeset (docs, tests, internal changes), you can ignore this message."
|
||
fi
|
||
|
||
# create-mcp-use-app Tests (only when CLI package changes)
|
||
create-mcp-use-app-build:
|
||
needs: [detect-changes, typescript-lint]
|
||
if: needs.detect-changes.outputs.create-mcp-use-app == 'true'
|
||
name: "typescript/create-mcp-use-app/build"
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- uses: pnpm/action-setup@v4
|
||
with:
|
||
version: 11.13.1
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: 22.23.1
|
||
cache: "pnpm"
|
||
cache-dependency-path: libraries/typescript/pnpm-lock.yaml
|
||
- name: Install Dependencies
|
||
working-directory: libraries/typescript
|
||
run: pnpm install --no-frozen-lockfile
|
||
- name: Build All Packages
|
||
working-directory: libraries/typescript
|
||
run: pnpm build
|
||
- name: Run create-mcp-use-app Unit Tests
|
||
working-directory: libraries/typescript
|
||
run: pnpm --filter create-mcp-use-app test
|
||
- name: Pack create-mcp-use-app
|
||
working-directory: libraries/typescript/packages/create-mcp-use-app
|
||
run: npm pack
|
||
- name: Pack @mcp-use/cli
|
||
working-directory: libraries/typescript/packages/cli
|
||
run: npm pack
|
||
- name: Pack @mcp-use/inspector
|
||
working-directory: libraries/typescript/packages/inspector
|
||
run: npm pack
|
||
- name: Pack mcp-use
|
||
working-directory: libraries/typescript/packages/server
|
||
run: npm pack
|
||
- name: Upload All Package Artifacts
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: workspace-packages
|
||
path: |
|
||
libraries/typescript/packages/create-mcp-use-app/create-mcp-use-app-*.tgz
|
||
libraries/typescript/packages/server/mcp-use-*.tgz
|
||
libraries/typescript/packages/cli/mcp-use-cli-*.tgz
|
||
libraries/typescript/packages/inspector/mcp-use-inspector-*.tgz
|
||
retention-days: 1
|
||
|
||
create-mcp-use-app-tests:
|
||
needs: create-mcp-use-app-build
|
||
name: "typescript/create-app-tests"
|
||
runs-on: ${{ matrix.os }}
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||
pm: [npm, yarn, pnpm]
|
||
template: [starter, mcp-apps, blank]
|
||
flag: ["", "--dev"]
|
||
exclude:
|
||
# Only test --dev on ubuntu with npm
|
||
- os: macos-latest
|
||
flag: "--dev"
|
||
- os: windows-latest
|
||
flag: "--dev"
|
||
- pm: yarn
|
||
flag: "--dev"
|
||
- pm: pnpm
|
||
flag: "--dev"
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: ${{ inputs.ref || github.sha }}
|
||
- uses: pnpm/action-setup@v4
|
||
if: matrix.pm == 'pnpm'
|
||
with:
|
||
version: 11.13.1
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: 22.23.1
|
||
- name: Setup Yarn
|
||
if: matrix.pm == 'yarn'
|
||
run: corepack enable
|
||
- name: Download Package Artifact
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||
with:
|
||
name: workspace-packages
|
||
path: ./packages
|
||
- name: Test CLI Creation
|
||
shell: bash
|
||
run: |
|
||
# Get package path
|
||
if [ "${{ runner.os }}" == "Windows" ]; then
|
||
PACKAGE_PATH=$(powershell -Command "(Get-ChildItem -Recurse -Filter 'create-mcp-use-app-*.tgz' | Select-Object -First 1).FullName")
|
||
else
|
||
PACKAGE_PATH=$(realpath $(find ./packages -name 'create-mcp-use-app-*.tgz' -type f | head -n1))
|
||
fi
|
||
|
||
# Run create command based on PM
|
||
case "${{ matrix.pm }}" in
|
||
npm)
|
||
npx --yes --package="$PACKAGE_PATH" create-mcp-use-app test-app --template ${{ matrix.template }} ${{ matrix.flag }}
|
||
;;
|
||
yarn)
|
||
yarn dlx -p "create-mcp-use-app@file:$PACKAGE_PATH" create-mcp-use-app test-app --template ${{ matrix.template }} ${{ matrix.flag }}
|
||
;;
|
||
pnpm)
|
||
pnpm --package="$PACKAGE_PATH" dlx create-mcp-use-app test-app --template ${{ matrix.template }} ${{ matrix.flag }}
|
||
;;
|
||
esac
|
||
|
||
# Verify creation
|
||
[ -d "test-app" ] || exit 1
|
||
[ -f "test-app/package.json" ] || exit 1
|
||
[ -f "test-app/index.ts" ] || exit 1
|
||
echo "✅ Project created successfully"
|
||
|
||
- name: Test dev command (non --dev flag only)
|
||
if: matrix.flag == ''
|
||
shell: bash
|
||
working-directory: test-app
|
||
run: |
|
||
# Start dev server in background
|
||
case "${{ matrix.pm }}" in
|
||
npm)
|
||
npm run dev &
|
||
;;
|
||
yarn)
|
||
yarn dev &
|
||
;;
|
||
pnpm)
|
||
pnpm dev &
|
||
;;
|
||
esac
|
||
|
||
DEV_PID=$!
|
||
|
||
# Wait for server to start (10 seconds should be enough)
|
||
sleep 10
|
||
|
||
# Kill the dev server
|
||
if [ "${{ runner.os }}" == "Windows" ]; then
|
||
# On Windows, kill by port is more reliable than DEV_PID
|
||
# because bash's $! may not map to the actual Windows process ID
|
||
DEV_PORT=3000
|
||
for pid in $(netstat -ano 2>/dev/null | grep ":$DEV_PORT " | grep "LISTENING" | awk '{print $5}' | sort -u); do
|
||
taskkill //PID $pid //F 2>/dev/null || true
|
||
done
|
||
# Also try the process tree kill as fallback
|
||
taskkill //PID $DEV_PID //T //F 2>/dev/null || true
|
||
else
|
||
kill $DEV_PID 2>/dev/null || true
|
||
fi
|
||
|
||
# Wait for process to exit
|
||
wait $DEV_PID 2>/dev/null || true
|
||
|
||
echo "✅ Dev server started and stopped successfully"
|